# ====================================================================== # --- graduation_logic.py --- # ====================================================================== import networkx as nx from typing import Dict, Any, List def _predict_naive(current_semester: int, total_sks_passed: int, last_gpa: float) -> Dict[str, Any]: """ Logika perhitungan matematis dasar (Naive) berdasarkan SKS dan IPK. Menggunakan teks deskripsi custom sesuai permintaan user. """ TARGET_SKS = 144 TARGET_SEMESTER = 8 MAX_SKS_REGULAR = 24 # Batas absolut reguler (biasanya IP >= 3.00) LIMIT_SKS_LOW_GPA = 20 # Batas jika IP < 3.00 SAFE_THRESHOLD = 18 # Batas aman/santai # 1. Hitung Sisa sks_needed = TARGET_SKS - total_sks_passed semesters_left = (TARGET_SEMESTER - current_semester) + 1 # Stats dasar untuk dikembalikan stats = { "sks_needed": sks_needed, "semesters_left": semesters_left, "required_pace": 0, "student_capacity": int(MAX_SKS_REGULAR if last_gpa >= 3.00 else LIMIT_SKS_LOW_GPA) } # --- LOGIC: Sudah Semester Akhir / Lewat --- if semesters_left <= 0: if sks_needed <= 0: return { "status": "Lulus", "color": "green", "description": "Selamat! Anda telah menyelesaikan kebutuhan SKS minimal.", "stats": stats } else: stats["required_pace"] = sks_needed return { "status": "Terlambat", "color": "red", "description": f"Saat ini semester {current_semester} dan SKS belum terpenuhi. Target lulus 8 semester sudah terlewat.", "stats": stats } # 2. Hitung Kecepatan yang Dibutuhkan (Required Pace) required_sks_per_sem = sks_needed / semesters_left stats["required_pace"] = round(required_sks_per_sem, 2) # 3. Evaluasi Status (Teks dari User) student_capacity = stats["student_capacity"] if required_sks_per_sem > MAX_SKS_REGULAR: # KASUS: Mustahil reguler return { "status": "🔴 Terlambat", "color": "red", "description": f"Target lulus semester 8 tidak memungkinkan. Anda butuh rata-rata {required_sks_per_sem:.1f} SKS yang perlu dipenuhi setiap semester selanjutnya, melebihi batas reguler yaitu 24 SKS.", "stats": stats } elif required_sks_per_sem <= SAFE_THRESHOLD: # KASUS: Aman return { "status": "🟢 Aman", "color": "green", "description": f"Posisi aman. Beban ringan, sisa (~{required_sks_per_sem:.1f} SKS yang perlu dipenuhi tiap semester. Pertahankan performa tiap semester!", "stats": stats } elif required_sks_per_sem <= student_capacity: # KASUS: Padat tapi Masih Mungkin status_text = "🟡 Jadwal Relatif Padat" return { "status": status_text, "color": "yellow", "description": f"Diperkirakan anda butuh ~{required_sks_per_sem:.1f} SKS yang perlu dipenuhi untuk semester-semester selanjutnya. Kapasitas SKS Anda ({student_capacity}) Sudah cukup mendukung untuk mengejar target ini.", "stats": stats } else: # KASUS: Terhambat IPK return { "status": "🟠 Rawan Terlambat", "color": "orange", "description": f"Hati-hati! Anda butuh {required_sks_per_sem:.1f} SKS tiap semester agar lulus tepat waktu, tapi IPK saat ini membatasi jatah cuma {student_capacity} SKS.", "stats": stats } def predict_graduation_status( current_semester: int, total_sks_passed: int, last_gpa: float, graph_G: nx.DiGraph = None, # Optional: Graph object (pass by reference) passed_courses: List[str] = None # Optional: List kode MK lulus ) -> Dict[str, Any]: """ Fungsi utama: Menjalankan logika Naive, lalu melakukan Override jika ditemukan masalah struktural pada graf (rantai prasyarat). """ # 1. Jalankan Prediksi Naive (Matematis) result = _predict_naive(current_semester, total_sks_passed, last_gpa) # Jika data graf tidak lengkap atau status sudah Critical/Lulus, kembalikan hasil naive if graph_G is None or passed_courses is None: return result if result["color"] == "red" or result["status"] == "Lulus": return result # 2. LOGIKA OVERRIDE: Cek Rantai Prasyarat (Critical Path) try: # A. Identifikasi MK yang BELUM lulus all_courses = set(graph_G.nodes()) passed_set = set(passed_courses) unpassed_courses = list(all_courses - passed_set) if not unpassed_courses: return result # B. Buat Subgraph (Hanya berisi matkul sisa & relasinya) subgraph_remaining = graph_G.subgraph(unpassed_courses) # C. Hitung Longest Path (Rantai Terpanjang) di subgraph # dag_longest_path mengembalikan list node, misal ['A', 'B', 'C'] -> Panjang 3 if nx.is_directed_acyclic_graph(subgraph_remaining): longest_chain_path = nx.dag_longest_path(subgraph_remaining) min_semesters_needed_structural = len(longest_chain_path) semesters_left = result["stats"]["semesters_left"] # D. Bandingkan dengan Sisa Waktu if min_semesters_needed_structural > semesters_left: result["status"] = "🔴 Terlambat (Struktural)" result["color"] = "red" result["description"] = ( f"SKS tersisa cukup, namun terdeteksi rantai prasyarat panjang yang terdiri" f"({min_semesters_needed_structural} Mata Kuliah Beruntun) dan tidak bisa diambil sekaligus dalam sisa waktu." ) # Tambahkan info debug ke stats result["stats"]["structural_issue"] = True result["stats"]["longest_chain_len"] = min_semesters_needed_structural result["stats"]["longest_chain_path"] = longest_chain_path except Exception as e: print(f"Graph Analysis Warning: {e}") # Jika error graf, fallback ke hasil naive return result return result