import '../data/disease_data.dart'; import '../models/symptom.dart'; import '../models/disease.dart'; class FuzzyMamdani { Map calculateMembership(double value) { if (value < 0 || value > 1) { throw ArgumentError( 'Nilai bobot gejala harus berada dalam rentang 0 – 1'); } Map 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> fuzzify(Map 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> memberships = {}; userInputs.forEach((symptomId, value) { memberships[symptomId] = calculateMembership(value); }); return memberships; } Map> forwardChaining( Map> memberships) { Map> diseaseResults = {}; Map symptomMap = { for (var symptom in symptoms) symptom.id: symptom }; for (var rule in rules) { int totalSymptoms = rule.symptomIds.length; int matchedSymptoms = 0; List matchedSymptomIds = []; Map 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 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 recommendations = {}; Map tempConfidenceScores = defuzzify(diseaseResults); diseaseResults.forEach((diseaseId, data) { double confidence = tempConfidenceScores[diseaseId] ?? 0.0; double totalWeight = data['totalWeight'] as double; List matchedSymptoms = data['matchedSymptoms'] as List; 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 highConfidenceDiseases = tempConfidenceScores.entries .where((entry) => entry.value > 30) .map((entry) => entry.key) .toList(); if (highConfidenceDiseases.length > 1) { List 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 defuzzify( Map> diseaseResults) { Map results = {}; const double lowValue = 20; const double mediumValue = 60; const double highValue = 90; diseaseResults.forEach((diseaseId, data) { Map scores = data['scores'] as Map; 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> diagnose(Map userInputs) { var memberships = fuzzify(userInputs); var diseaseResults = forwardChaining(memberships); var confidenceScores = defuzzify(diseaseResults); Map> 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; } }