268 lines
9.5 KiB
Dart
268 lines
9.5 KiB
Dart
import '../data/disease_data.dart';
|
||
import '../models/symptom.dart';
|
||
import '../models/disease.dart';
|
||
|
||
class FuzzyMamdani {
|
||
Map<String, double> calculateMembership(double value) {
|
||
if (value < 0 || value > 1) {
|
||
throw ArgumentError(
|
||
'Nilai bobot gejala harus berada dalam rentang 0 – 1');
|
||
}
|
||
|
||
Map<String, double> memberships = {
|
||
'rendah': 0.0,
|
||
'sedang': 0.0,
|
||
'tinggi': 0.0,
|
||
};
|
||
|
||
if (value <= 0.4) {
|
||
memberships['rendah'] = 1.0;
|
||
} else if (value <= 0.5) {
|
||
memberships['rendah'] = (0.5 - value) / (0.5 - 0.4);
|
||
}
|
||
|
||
if (value >= 0.4 && value <= 0.5) {
|
||
memberships['sedang'] = (value - 0.4) / (0.5 - 0.4);
|
||
} else if (value > 0.5 && value <= 0.7) {
|
||
memberships['sedang'] = 1.0;
|
||
} else if (value > 0.7 && value <= 0.8) {
|
||
memberships['sedang'] = (0.8 - value) / (0.8 - 0.7);
|
||
}
|
||
|
||
if (value >= 0.7 && value <= 0.8) {
|
||
memberships['tinggi'] = (value - 0.7) / (0.8 - 0.7);
|
||
} else if (value > 0.8) {
|
||
memberships['tinggi'] = 1.0;
|
||
}
|
||
|
||
return memberships;
|
||
}
|
||
|
||
Map<String, Map<String, double>> fuzzify(Map<String, double> userInputs) {
|
||
if (userInputs.isEmpty) {
|
||
throw ArgumentError('Input gejala tidak boleh kosong');
|
||
}
|
||
for (var symptomId in userInputs.keys) {
|
||
if (!symptoms.any((s) => s.id == symptomId)) {
|
||
throw ArgumentError('ID gejala tidak valid: $symptomId');
|
||
}
|
||
}
|
||
|
||
Map<String, Map<String, double>> memberships = {};
|
||
userInputs.forEach((symptomId, value) {
|
||
memberships[symptomId] = calculateMembership(value);
|
||
});
|
||
return memberships;
|
||
}
|
||
|
||
Map<String, Map<String, dynamic>> forwardChaining(
|
||
Map<String, Map<String, double>> memberships) {
|
||
Map<String, Map<String, dynamic>> diseaseResults = {};
|
||
Map<String, Symptom> symptomMap = {
|
||
for (var symptom in symptoms) symptom.id: symptom
|
||
};
|
||
|
||
for (var rule in rules) {
|
||
int totalSymptoms = rule.symptomIds.length;
|
||
int matchedSymptoms = 0;
|
||
List<String> matchedSymptomIds = [];
|
||
Map<String, double> totalMembership = {
|
||
'rendah': 0.0,
|
||
'sedang': 0.0,
|
||
'tinggi': 0.0,
|
||
};
|
||
double totalWeight = 0.0;
|
||
|
||
for (var symptomId in rule.symptomIds) {
|
||
if (memberships.containsKey(symptomId)) {
|
||
matchedSymptoms++;
|
||
matchedSymptomIds.add(symptomId);
|
||
totalMembership['rendah'] =
|
||
totalMembership['rendah']! + memberships[symptomId]!['rendah']!;
|
||
totalMembership['sedang'] =
|
||
totalMembership['sedang']! + memberships[symptomId]!['sedang']!;
|
||
totalMembership['tinggi'] =
|
||
totalMembership['tinggi']! + memberships[symptomId]!['tinggi']!;
|
||
totalWeight += symptomMap[symptomId]!.weight;
|
||
}
|
||
}
|
||
|
||
if (matchedSymptoms > 0) {
|
||
double matchPercentage = matchedSymptoms / totalSymptoms;
|
||
double weightFactor = totalWeight / matchedSymptoms;
|
||
Map<String, double> ruleScores = {
|
||
'rendah': (totalMembership['rendah']! / matchedSymptoms) *
|
||
matchPercentage *
|
||
weightFactor,
|
||
'sedang': (totalMembership['sedang']! / matchedSymptoms) *
|
||
matchPercentage *
|
||
weightFactor,
|
||
'tinggi': (totalMembership['tinggi']! / matchedSymptoms) *
|
||
matchPercentage *
|
||
weightFactor,
|
||
};
|
||
diseaseResults[rule.diseaseId] = {
|
||
'scores': ruleScores,
|
||
'matchPercentage': matchPercentage,
|
||
'matchedSymptoms': matchedSymptomIds,
|
||
'totalWeight': totalWeight,
|
||
};
|
||
}
|
||
}
|
||
|
||
Map<String, String> recommendations = {};
|
||
Map<String, double> tempConfidenceScores = defuzzify(diseaseResults);
|
||
|
||
diseaseResults.forEach((diseaseId, data) {
|
||
double confidence = tempConfidenceScores[diseaseId] ?? 0.0;
|
||
double totalWeight = data['totalWeight'] as double;
|
||
List<String> matchedSymptoms = data['matchedSymptoms'] as List<String>;
|
||
|
||
if (diseaseId == 'P1') {
|
||
if (confidence > 40 && totalWeight > 1.5) {
|
||
recommendations[diseaseId] =
|
||
'Segera lakukan penyemprotan intensif dengan fungisida difenokonazol setiap 5 hari.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida difenokonazol dengan interval penyemprotan 7 hari.';
|
||
}
|
||
} else if (diseaseId == 'P2') {
|
||
if (confidence > 30 && matchedSymptoms.contains('GP15')) {
|
||
recommendations[diseaseId] =
|
||
'Lakukan pengendalian dengan insektisida tambahan karena ada indikasi serangan hama.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan mulsa plastik untuk mencegah penyebaran.';
|
||
}
|
||
} else if (diseaseId == 'P3') {
|
||
if (confidence > 50 && totalWeight > 2.0) {
|
||
recommendations[diseaseId] =
|
||
'Segera lakukan drainase lahan dan gunakan fungisida berbahan aktif metalaksil.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida berbahan aktif metalaksil dengan interval 7 hari.';
|
||
}
|
||
} else if (diseaseId == 'P4') {
|
||
if (confidence > 40 && matchedSymptoms.contains('GP9')) {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida berbahan aktif azoksistrobin dan lakukan sanitasi lahan.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida berbahan aktif azoksistrobin dengan interval 7 hari.';
|
||
}
|
||
} else if (diseaseId == 'P5') {
|
||
if (confidence > 30 && matchedSymptoms.contains('GP16')) {
|
||
recommendations[diseaseId] =
|
||
'Lakukan pemangkasan daun yang terinfeksi dan gunakan fungisida berbahan aktif mankozeb.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida berbahan aktif mankozeb dengan interval 7 hari.';
|
||
}
|
||
} else if (diseaseId == 'P6') {
|
||
if (confidence > 50 && matchedSymptoms.contains('GP18')) {
|
||
recommendations[diseaseId] =
|
||
'Lakukan penggantian media tanam dan gunakan fungisida berbahan aktif karbendazim.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan fungisida berbahan aktif karbendazim dengan interval 7 hari.';
|
||
}
|
||
} else if (diseaseId == 'P7') {
|
||
if (confidence > 50 && matchedSymptoms.contains('GP20')) {
|
||
recommendations[diseaseId] =
|
||
'Lakukan pembakaran tanaman yang terinfeksi dan gunakan bakterisida berbahan aktif streptomisin.';
|
||
} else {
|
||
recommendations[diseaseId] =
|
||
'Gunakan bakterisida berbahan aktif streptomisin dengan interval 7 hari.';
|
||
}
|
||
}
|
||
});
|
||
|
||
List<String> highConfidenceDiseases = tempConfidenceScores.entries
|
||
.where((entry) => entry.value > 30)
|
||
.map((entry) => entry.key)
|
||
.toList();
|
||
|
||
if (highConfidenceDiseases.length > 1) {
|
||
List<String> diseaseNames = highConfidenceDiseases.map((diseaseId) {
|
||
final disease = diseases.firstWhere(
|
||
(d) => d.id == diseaseId,
|
||
orElse: () => Disease(
|
||
id: diseaseId,
|
||
name: 'Penyakit Tidak Diketahui',
|
||
symptomIds: [],
|
||
solutions: []),
|
||
);
|
||
return disease.name;
|
||
}).toList();
|
||
|
||
String generalRecommendation =
|
||
'Konsultasikan dengan ahli pertanian karena ada indikasi infeksi ganda (${diseaseNames.join(", ")}).';
|
||
highConfidenceDiseases.forEach((diseaseId) {
|
||
recommendations[diseaseId] =
|
||
(recommendations[diseaseId] ?? '') + '\n$generalRecommendation';
|
||
});
|
||
}
|
||
|
||
diseaseResults.forEach((diseaseId, data) {
|
||
data['recommendation'] = recommendations[diseaseId] ?? '';
|
||
});
|
||
|
||
return diseaseResults;
|
||
}
|
||
|
||
Map<String, double> defuzzify(
|
||
Map<String, Map<String, dynamic>> diseaseResults) {
|
||
Map<String, double> results = {};
|
||
|
||
const double lowValue = 20;
|
||
const double mediumValue = 60;
|
||
const double highValue = 90;
|
||
|
||
diseaseResults.forEach((diseaseId, data) {
|
||
Map<String, double> scores = data['scores'] as Map<String, double>;
|
||
double matchPercentage = data['matchPercentage'] as double;
|
||
|
||
double numerator = 0.0;
|
||
double denominator = 0.0;
|
||
|
||
numerator += lowValue * scores['rendah']!;
|
||
numerator += mediumValue * scores['sedang']!;
|
||
numerator += highValue * scores['tinggi']!;
|
||
|
||
denominator += scores['rendah']! + scores['sedang']! + scores['tinggi']!;
|
||
|
||
if (denominator == 0) {
|
||
results[diseaseId] = 0.0;
|
||
} else {
|
||
double centroid = numerator / denominator;
|
||
double adjustmentFactor = 2.4 + (1.05 * (1 - matchPercentage));
|
||
double confidence = (centroid * matchPercentage) * adjustmentFactor;
|
||
results[diseaseId] = confidence.clamp(0, 100);
|
||
results[diseaseId] =
|
||
double.parse(results[diseaseId]!.toStringAsFixed(1));
|
||
}
|
||
});
|
||
|
||
return results;
|
||
}
|
||
|
||
Map<String, Map<String, dynamic>> diagnose(Map<String, double> userInputs) {
|
||
var memberships = fuzzify(userInputs);
|
||
var diseaseResults = forwardChaining(memberships);
|
||
var confidenceScores = defuzzify(diseaseResults);
|
||
|
||
Map<String, Map<String, dynamic>> finalResults = {};
|
||
diseaseResults.forEach((diseaseId, data) {
|
||
finalResults[diseaseId] = {
|
||
'confidence': confidenceScores[diseaseId] ?? 0.0,
|
||
'matchPercentage': data['matchPercentage'],
|
||
'matchedSymptoms': data['matchedSymptoms'],
|
||
'recommendation': data['recommendation'],
|
||
'totalWeight': data['totalWeight'],
|
||
};
|
||
});
|
||
|
||
return finalResults;
|
||
}
|
||
}
|