File size: 6,532 Bytes
410b443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# ======================================================================
# --- 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