1:- module(ap_model,
    2          [ score/2,
    3            likelihood_score/3,
    4            spread_score/3,
    5            escalation_score/3,
    6            pressure_score/3,
    7            crowd_bucket/2,
    8            risk_band/2,
    9            predicted_state/3,
   10            top_factors/3
   11          ]).

Model heuristik explainable Antisipasi Pejabat.

Koefisien v0.1.0.0 adalah bobot heuristik terdokumentasi. Bobot belum merupakan hasil estimasi kausal dan tidak boleh dipresentasikan sebagai probabilitas empiris terkalibrasi tanpa evaluasi data historis yang sesuai.

   20:- use_module(library(lists)).   21:- use_module(ap_validation).   22:- use_module(ap_legal).   23
   24positive_weight(grievance,              0.14, 'akumulasi keluhan').
   25positive_weight(trigger,                0.13, 'pemicu langsung').
   26positive_weight(salience,               0.10, 'kepentingan isu').
   27positive_weight(online,                 0.08, 'momentum daring').
   28positive_weight(coalition,              0.08, 'koalisi lintas kelompok').
   29positive_weight(organization,           0.07, 'kapasitas mobilisasi').
   30positive_weight(economic_stress,        0.07, 'tekanan ekonomi').
   31positive_weight(institutional_distrust, 0.08, 'ketidakpercayaan institusional').
   32positive_weight(incident_shock,         0.08, 'insiden pemicu tambahan').
   33positive_weight(recent_precedent,       0.04, 'preseden aksi terbaru').
   34positive_weight(media_attention,        0.05, 'perhatian media').
   35positive_weight(public_support,         0.05, 'dukungan publik').
   36positive_weight(regional_relevance,     0.03, 'relevansi lintas wilayah').
   37
   38protective_weight(govt_communication,   0.06, 'komunikasi pemerintah').
   39protective_weight(govt_responsiveness,  0.08, 'responsivitas/dialog').
   40
   41spread_weight(online,             0.20, 'momentum daring').
   42spread_weight(coalition,          0.15, 'koalisi').
   43spread_weight(salience,           0.14, 'kepentingan isu').
   44spread_weight(regional_relevance, 0.18, 'relevansi lintas wilayah').
   45spread_weight(public_support,     0.12, 'dukungan publik').
   46spread_weight(media_attention,    0.11, 'perhatian media').
   47spread_weight(recent_precedent,   0.10, 'preseden aksi').
   48
   49score(Input, Report) :-
   50    ap_validation:normalized_scenario(Input, S),
   51    likelihood_score(S, Likelihood, LikContrib),
   52    spread_score(S, Spread, SpreadContrib),
   53    escalation_score(S, Escalation, EscContrib),
   54    pressure_score(S, Pressure, _),
   55    confidence_score(S, Confidence),
   56    risk_band(Likelihood, LikBand),
   57    risk_band(Spread, SpreadBand),
   58    risk_band(Escalation, EscBand),
   59    predicted_state(Likelihood, Escalation, State),
   60    predicted_scale(S, Likelihood, Spread, Scale),
   61    turnout_index(S, Likelihood, Spread, TurnoutIndex),
   62    crowd_bucket(TurnoutIndex, CrowdBucket),
   63    continuation_score(S, Likelihood, Spread, Continue),
   64    policy_outlook(S, Likelihood, Pressure, Outlook),
   65    ap_legal:legal_note(S, Legal),
   66    top_factors(LikContrib, 5, TopPositive),
   67    protective_factors(S, Protective),
   68    Report = report{
   69        scenario:S,
   70        likelihood:Likelihood,
   71        likelihood_band:LikBand,
   72        spread:Spread,
   73        spread_band:SpreadBand,
   74        escalation:Escalation,
   75        escalation_band:EscBand,
   76        political_pressure:Pressure,
   77        confidence:Confidence,
   78        predicted_state:State,
   79        predicted_scale:Scale,
   80        crowd_bucket:CrowdBucket,
   81        continuation:Continue,
   82        policy_outlook:Outlook,
   83        legal_note:Legal,
   84        top_factors:TopPositive,
   85        protective_factors:Protective,
   86        contributions:_{likelihood:LikContrib, spread:SpreadContrib, escalation:EscContrib},
   87        model_status:'HEURISTIC_UNCALIBRATED_V0_1'
   88    }.
   89
   90likelihood_score(S, Score, Contributions) :-
   91    findall(C-Label-Key,
   92            ( positive_weight(Key, W, Label),
   93              get_dict(Key, S, V),
   94              C is V*W
   95            ), Pos),
   96    sum_contributions(Pos, P),
   97    findall(C-Label-Key,
   98            ( protective_weight(Key, W, Label),
   99              get_dict(Key, S, V),
  100              C is -(V*W)
  101            ), Neg),
  102    sum_contributions(Neg, N),
  103    scope_likelihood_bonus(S.scope, SB),
  104    Raw is 8 + P + N + SB,
  105    ap_validation:clamp(0, 100, Raw, Capped),
  106    Score is round(Capped),
  107    append(Pos, Neg, Contributions).
  108
  109spread_score(S, Score, Contributions) :-
  110    findall(C-Label-Key,
  111            ( spread_weight(Key, W, Label),
  112              get_dict(Key, S, V),
  113              C is V*W
  114            ), Cs),
  115    sum_contributions(Cs, Base),
  116    scope_spread_bonus(S.scope, Bonus),
  117    Raw is Base + Bonus,
  118    ap_validation:clamp(0, 100, Raw, Capped),
  119    Score is round(Capped),
  120    Contributions = Cs.
  121
  122escalation_score(S, Score, Contributions) :-
  123    DurationPressure is min(100, S.duration_days*12),
  124    LowControl is 100-S.organizer_control,
  125    LowResponse is 100-S.govt_responsiveness,
  126    Pairs = [
  127        grievance-0.14-'akumulasi keluhan',
  128        incident_shock-0.20-'insiden pemicu tambahan',
  129        intervention_pressure-0.18-'tekanan/intervensi',
  130        institutional_distrust-0.11-'ketidakpercayaan institusional',
  131        trigger-0.10-'pemicu langsung'
  132    ],
  133    findall(C-Label-Key,
  134            ( member(Key-W-Label, Pairs),
  135              get_dict(Key, S, V), C is V*W
  136            ), C0),
  137    C1 is DurationPressure*0.08,
  138    C2 is LowControl*0.10,
  139    C3 is LowResponse*0.09,
  140    Extra = [C1-'durasi tekanan'-duration_days,
  141             C2-'kontrol penyelenggara rendah'-organizer_control,
  142             C3-'respons kebijakan rendah'-govt_responsiveness],
  143    append(C0, Extra, Contributions),
  144    sum_contributions(Contributions, Raw0),
  145    Raw is Raw0*0.94,
  146    ap_validation:clamp(0, 100, Raw, Capped),
  147    Score is round(Capped).
  148
  149pressure_score(S, Score, Contributions) :-
  150    Pairs = [
  151        public_support-0.24-'dukungan publik',
  152        salience-0.18-'kepentingan isu',
  153        coalition-0.15-'koalisi',
  154        media_attention-0.13-'perhatian media',
  155        spread_proxy-0.15-'potensi penyebaran',
  156        grievance-0.15-'akumulasi keluhan'
  157    ],
  158    spread_score(S, Spread, _),
  159    findall(C-Label-Key,
  160            ( member(Key-W-Label, Pairs),
  161              pressure_value(Key, S, Spread, V), C is V*W
  162            ), Contributions),
  163    sum_contributions(Contributions, Raw),
  164    ap_validation:clamp(0, 100, Raw, Capped),
  165    Score is round(Capped).
  166
  167pressure_value(spread_proxy, _S, Spread, Spread) :- !.
  168pressure_value(Key, S, _Spread, V) :- get_dict(Key, S, V).
  169
  170confidence_score(S, Score) :-
  171    Q = S.evidence_quality,
  172    Base is 25 + Q*0.40,
  173    ( S.incident_shock > 0 -> Bonus = 3 ; Bonus = 0 ),
  174    Raw is Base + Bonus,
  175    ap_validation:clamp(20, 68, Raw, Capped),
  176    Score is round(Capped).
  177
  178continuation_score(S, Likelihood, Spread, Score) :-
  179    Raw is Likelihood*0.42 + Spread*0.31 + S.grievance*0.17 +
  180           (100-S.govt_responsiveness)*0.10,
  181    ap_validation:clamp(0, 100, Raw, Capped),
  182    Score is round(Capped).
  183
  184policy_outlook(S, _Likelihood, Pressure, Outlook) :-
  185    R = S.govt_responsiveness,
  186    ( Pressure >= 75, R >= 60 -> Outlook = 'peluang_konsesi_atau_review_tinggi'
  187    ; Pressure >= 60, R >= 40 -> Outlook = 'peluang_dialog_atau_penyesuaian_moderat'
  188    ; Pressure >= 60, R < 40  -> Outlook = 'tekanan_tinggi_respons_rendah'
  189    ; Pressure < 40           -> Outlook = 'tekanan_kebijakan_relatively_rendah'
  190    ; Outlook = 'hasil_belum_jelas'
  191    ).
  192
  193predicted_scale(S, Likelihood, Spread, Scale) :-
  194    Base is Likelihood*0.45 + Spread*0.55,
  195    scope_scale_adjust(S.scope, A), X is Base + A,
  196    ( X < 35 -> Scale = 'terbatas/lokal'
  197    ; X < 52 -> Scale = 'kota/area'
  198    ; X < 68 -> Scale = 'lintas-kota'
  199    ; X < 82 -> Scale = 'multi-provinsi'
  200    ; Scale = 'nasional-potensial'
  201    ).
  202
  203turnout_index(S, Likelihood, Spread, Index) :-
  204    PopFactor is min(100, 25 + log(S.population_millions+1)*22),
  205    Raw is Likelihood*0.36 + Spread*0.22 + S.organization*0.17 +
  206           S.public_support*0.15 + PopFactor*0.10,
  207    ap_validation:clamp(0, 100, Raw, Capped),
  208    Index is round(Capped).
  209
  210% Mapping indeks internal ke bucket ukuran ACLED. Ini bukan estimasi jumlah presisi.
  211crowd_bucket(I, 'very small (<20)') :- I < 25, !.
  212crowd_bucket(I, 'small (20-99)') :- I < 42, !.
  213crowd_bucket(I, 'medium (100-999)') :- I < 62, !.
  214crowd_bucket(I, 'large (1,000-9,999)') :- I < 80, !.
  215crowd_bucket(_, 'massive (10,000+)').
  216
  217risk_band(S, sangat_rendah) :- S < 20, !.
  218risk_band(S, rendah) :- S < 40, !.
  219risk_band(S, sedang) :- S < 60, !.
  220risk_band(S, tinggi) :- S < 80, !.
  221risk_band(_, sangat_tinggi).
  222
  223predicted_state(L, _E, 'tidak_ada_aksi_besar_terdeteksi') :- L < 35, !.
  224predicted_state(_L, E, 'peaceful_protest_paling_mungkin') :- E < 35, !.
  225predicted_state(_L, E, 'risiko_protest_with_intervention') :- E < 58, !.
  226predicted_state(_L, E, 'risiko_excessive_force_or_violent_demonstration_meningkat') :- E >= 58.
  227
  228scope_likelihood_bonus(lokal, 0) :- !.
  229scope_likelihood_bonus(kota, 1) :- !.
  230scope_likelihood_bonus(provinsi, 2) :- !.
  231scope_likelihood_bonus(multi_provinsi, 4) :- !.
  232scope_likelihood_bonus(nasional, 5) :- !.
  233scope_likelihood_bonus(_, 0).
  234
  235scope_spread_bonus(lokal, -8) :- !.
  236scope_spread_bonus(kota, -3) :- !.
  237scope_spread_bonus(provinsi, 1) :- !.
  238scope_spread_bonus(multi_provinsi, 5) :- !.
  239scope_spread_bonus(nasional, 8) :- !.
  240scope_spread_bonus(_, 0).
  241
  242scope_scale_adjust(lokal, -12) :- !.
  243scope_scale_adjust(kota, -5) :- !.
  244scope_scale_adjust(provinsi, 0) :- !.
  245scope_scale_adjust(multi_provinsi, 8) :- !.
  246scope_scale_adjust(nasional, 12) :- !.
  247scope_scale_adjust(_, 0).
  248
  249sum_contributions(Cs, Sum) :-
  250    findall(V, member(V-_-_, Cs), Vs), sum_list(Vs, Sum).
  251
  252top_factors(Contributions, N, Top) :-
  253    include(positive_contribution, Contributions, Positive),
  254    predsort(compare_contrib_desc, Positive, Sorted),
  255    take(N, Sorted, Top0),
  256    maplist(contrib_dict, Top0, Top).
  257
  258positive_contribution(V-_-_) :- V > 0.
  259compare_contrib_desc(Order, A-_-_, B-_-_) :- compare(Order, B, A).
  260
  261take(0, _Xs, []) :- !.
  262take(_, [], []) :- !.
  263take(N, [X|Xs], [X|Ys]) :- N1 is N-1, take(N1, Xs, Ys).
  264
  265contrib_dict(V-Label-Key, _{field:Key, label:Label, contribution:Rounded}) :-
  266    Rounded is round(V*10)/10.
  267
  268protective_factors(S, Factors) :-
  269    findall(_{field:Key,label:Label,value:V,effect:Effect},
  270            ( protective_weight(Key, W, Label),
  271              get_dict(Key, S, V), Effect is round(V*W*10)/10
  272            ), Factors)