dynamisk-rettsstat/modell/kjerne/forlop.py
Kjell Tore Guttormsen 464ad38b49
feat: model core, launcher, reproduction check and the first two skills
The core in modell/kjerne/ is an unmodified copy of sim.py, diagnose.py and
forlop.py from den-dynamiske-rettsstaten @2badc25, pinned by sha256.
kjor.py is the single entry point; kontroll.py reproduces the note's 22 data
files and printed results. Skills: diagnose and forlop (Norwegian).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 13:53:32 +01:00

527 lines
30 KiB
Python

"""Forløp over tid og stabilitetssjekk for leddet som binder, fra egne tall.
Bruk (fra paper/, med samme Python-miljø som sim.py):
../.venv/bin/python forlop.py eksempel_domstol.json
../.venv/bin/python forlop.py norge_2025.json "Tingrett, meddomsrett" # velg ledd selv
Leser samme fil som diagnose.py. To ting kan legges til:
{"styring": {"forsinkelse": 3, # måneder fra kriminalitetsbildet endrer seg til innsatsen
# er endret (statistikk + beslutning + iverksetting)
"reaktivitet": 3}, # prosent mer innsats per prosent avvik fra målet
"ledd": [{..., "innstromning_for_okning": 100}]} # saker inn per måned før strømmen økte
Uten "styring" brukes regime A i notatet (3 og 3). Uten "innstromning_for_okning"
antas leddet 95 % utnyttet før økningen, som i notatet.
Hva skriptet gjør:
1. Stabilitetssjekk: ligger forsinkelsen under den kritiske forsinkelsen for denne
reaktiviteten (notatet, proposisjon 1 og avsnitt 9.3; critical_delay i sim.py)?
2. Forløp: leddet som binder, legges inn som modellens ledd med kø (domstolen i
avsnitt 9.5), og den koordinerte regelen kjøres måned for måned. Størrelsen på
økningen settes slik at regelens første budsjettvedtak ser prognosen i fila.
Restansen legges inn direkte: forløpet starter i restansen i fila, på
tidspunktet tallene gjelder. «Nå» er første budsjettvedtak, tre måneder etter.
3. Resten av kjeden: politiets innsats, andelen ledet utenom, og hva flere avgjorte
saker betyr for leddene som står etter i fila.
Kapasitet, restanse og saksbehandlingstid følger tallene i fila. Kriminalitet,
tillit og politiets innsats følger modellens stiliserte parametere (tabell 3 i
notatet), som ikke er kalibrert på norske data.
Bruker sim.py uendret og skriver ingenting til paper/data/. For å kunne starte i
restansen i fila har skriptet sin egen utgave av regnestegene i simulate_endo
(simuler() under). Hver kjøring kontrollerer at den gir nøyaktig samme svar som
simulate_endo når restansen ikke legges inn.
"""
import json
import math
import re
import sys
import numpy as np
from scipy.optimize import brentq
import diagnose
from diagnose import tall
from sim import (CH0, DT, ENDO, P, UPS0, VARSIGMA, Omega, critical_delay, deriv_chain_xz,
init_chain, predict_F, simulate_endo)
J0 = P["gamma"] * P["Lam0"] * P["T0"] # modellens strøm før økningen (0,10 per måned)
NAA = 24.0 # modellmåneden for første budsjettvedtak etter økningen
OBS = NAA - ENDO["lagD"] # vedtaket bygger på tall som er lagD måneder gamle
SLUTT = 180.0 # som tabell 7 og 10 i notatet
LAM_MYK = P["Lam0"] - 0.05 # myk grense for tillit (0,75), som i mpc_step
K_GULV, T_GULV = 0.80, 0.50 # under dette (1 = før økningen) regnes forløpet ikke
UPS_FOR_MIN = ENDO["ups_lo"] + 0.01 # under dette bygger modellens regel ned kapasitet før økningen
UPS_FOR_MAKS = 0.98 # over dette bestiller den kapasitet før økningen
RADER = (-3, 0, 6, 12, 18, 24, 36, 48, 60, 72, 96, 120, 156)
def idx(mnd):
return int(round(mnd / DT))
def les_styring(data):
s = data.get("styring", {})
if not isinstance(s, dict):
sys.exit('Feil i inndata: "styring" må være et objekt med "forsinkelse" og "reaktivitet".')
ut = {}
for felt, standard, maks in (("forsinkelse", 3.0, 60.0), ("reaktivitet", 3.0, 50.0)):
verdi = s.get(felt, standard)
try:
x = float("nan") if isinstance(verdi, bool) else float(verdi)
except (TypeError, ValueError):
x = float("nan")
if not 0 < x <= maks:
sys.exit(f'Feil i inndata, styring: "{felt}" må være et tall større enn 0 og høyst {maks:.0f}, '
f"ikke {json.dumps(verdi, ensure_ascii=False)}.")
ut[felt] = x
ut["oppgitt"] = "styring" in data
return ut
def stabilitet(tau, kp):
"""Lineær stabilitet i referanselikevekten: x' = -a x - b x(t - tau)."""
c = P["gamma"] * P["Lam0"] # fjerningsrate per enhet innsats (0,10 per måned)
a = c * P["T0"] + P["delta"]
b = c * kp
tc = critical_delay(a, b)
print("Stabilitetssjekk: tåler styringen sin egen forsinkelse?\n")
print(f"Forsinkelse {tall(tau, 1)} mnd, reaktivitet {tall(kp, 1)} "
f"(10 % over målet gir {tall(kp * 10, 0)} % mer innsats).")
if math.isinf(tc):
print("Stabil uansett forsinkelse: reaksjonen er svakere enn systemets egen demping "
f"(reaktivitet høyst {tall(a / c, 1)}).")
return True
stabil = tau < tc
print(f"Kritisk forsinkelse: {tall(tc, 1)} mnd. " +
(f"Forsinkelsen er kortere, så styringen er stabil (margin {tall(tc - tau, 1)} mnd)." if stabil else
"Forsinkelsen er lengre, så styringen skaper svingninger selv: satsinger og kutt avløser "
f"hverandre. Ved stabilitetsgrensen er perioden om lag "
f"{tall(2 * math.pi / math.sqrt(b * b - a * a), 0)} mnd."))
if critical_delay(a, c * 50.0) < tau:
kp_maks = brentq(lambda k: critical_delay(a, c * k) - tau, a / c + 1e-6, 50.0)
print(f"Med denne forsinkelsen er største stabile reaktivitet {tall(kp_maks, 1)}; "
f"tommelfingerregelen gir under {tall(math.pi / (2 * c * tau), 1)}.")
if not stabil:
print(f"To veier ut: demp reaksjonen, eller kort forsinkelsen ned til under {tall(tc, 1)} mnd.")
print("(Regnet med modellens stiliserte tall: innsatsen fjerner 10 % av kriminaliteten per måned, "
"og 2 % faller fra av seg selv.)")
return stabil
def modell(b):
"""Skaler leddet til modellens enheter. Returnerer skala, kjede, regelparametere, målene
regelens første vedtak skal se (prognose og restanse over målet), og køen som svarer til
restansen i fila, alt i modellens enheter."""
Z_u = b["Z"]
antatt = b["F_for"] is None
F_for = UPS0 * Z_u if antatt else b["F_for"]
hva = (f"innstrømningen før økningen ({tall(F_for, 1)} saker/mnd"
+ (f", antatt {tall(UPS0 * 100, 0)} % av kapasiteten)" if antatt else ")"))
if not UPS_FOR_MIN * Z_u <= F_for <= UPS_FOR_MAKS * Z_u:
sys.exit(f"Forløpet forutsetter et ledd som var i balanse før økningen: {hva} må ligge mellom "
f"{tall(UPS_FOR_MIN * 100, 0)} og {tall(UPS_FOR_MAKS * 100, 0)} % av kapasiteten "
f"({tall(Z_u, 1)}). Utenfor det ville modellens regel ha endret kapasiteten allerede før "
"økningen.")
if b["Fp"] <= F_for:
sys.exit(f"Forløpet har ingenting å vise for {b['navn']}: prognosen ({tall(b['Fp'], 1)} saker/mnd) "
f"er ikke høyere enn {hva}.")
skala = F_for / J0 # saker per måned per modellenhet
Z = Z_u / skala
tau_ref = b["Q_ref"] / Z_u # målet som måneder av kapasiteten modellen bruker
if tau_ref <= 0:
sys.exit("Forløpet trenger et mål for saksbehandlingstid eller restanse som er større enn 0.")
m = tau_ref - J0 / (Z * (1.0 / tau_ref + VARSIGMA)) # som MD i sim.py: gir tau_ref før økningen
ch = dict(CH0, Z=Z, m=m)
e = dict(xi_min=1.0 - b["andel"])
# Modellen viser restansen som (m + Q/Z) * Z. Køen Q som gir restansen i fila, er derfor
# restansen minus m * Z; modellen kan ikke ha mindre restanse enn m * Z.
return dict(skala=skala, ch=ch, e=e, F_for=F_for, antatt=antatt, tau_ref=tau_ref,
F_maal=b["Fp"] / skala, Q_maal=max(b["Q"] - b["Q_ref"], 0.0) / skala,
Q_inn=max(b["Q"] / skala - m * Z, 0.0), Q_min=m * Z)
def simuler(policy, tau, p, ch, e, kp, tend, inn=None):
"""Samme regnesteg som simulate_endo i sim.py for policy "koord" og "sched" (uten
kvalitetssløyfe og uten tidsplaner for kapasitet og andel utenom), men køen kan settes
direkte: inn = (steg, dQ) legger dQ til køen i det tidssteget (køen blir aldri negativ). Uten
inn skal svaret være nøyaktig det samme som fra simulate_endo; kontroller_integrator() prøver
det i hver kjøring. Endres simulate_endo, må denne endres likt."""
e = dict(ENDO, **(e or {}))
n = int(tend / DT) + 1
t = np.arange(n) * DT
K = np.ones(n); U = np.full(n, p["U0"]); L = np.full(n, p["Lam0"]); T = np.full(n, p["T0"])
Q = np.zeros(n); tauD = np.zeros(n); JD = np.zeros(n); RD = np.zeros(n); Fa = np.zeros(n)
Z = np.full(n, ch["Z"]); xi = np.ones(n)
Q0, tau_ref = init_chain(dict(ch, Zfun=None, xifun=None), p)
Q[0] = Q0
lag = int(round(tau / DT)); lagD = int(round(e["lagD"] / DT))
xi_tgt = 1.0
binding = False
orders = []
Z_down = None
log = []
for k in range(n - 1):
if inn is not None and k == inn[0]:
Q[k] = max(Q[k] + inn[1], 0.0) # opp eller ned til restansen i fila
tk = t[k]
Kobs = K[k - lag] if k - lag >= 0 else 1.0
T[k] = np.clip(p["T0"] + kp * (Kobs - p["Kstar"]), 0.0, p["Tmax"])
i = k - lagD
if i >= 0:
Q_hat, F_hat = Q[i], Fa[i]
else:
Q_hat, F_hat = Q0, 0.1
at_month = abs(tk - round(tk)) < 1e-9
if policy == "koord":
Q_ref = (tau_ref - ch["m"]) * Z[k]
Q_exc = max(Q_hat - Q_ref, 0.0)
if at_month and k > 0:
binding = binding or F_hat > e["ups_hi"] * Z[k]
xi_tgt = float(np.clip((e["ups_star"] * Z[k] - Q_exc / e["T_clear"]) / max(F_hat, 1e-9),
e["xi_min"], 1.0)) if binding else 1.0
if at_month and k > 0 and abs(tk % e["period"]) < 1e-9:
pending = sum(dZ * (1 - np.clip((tk - a) / (b - a), 0, 1)) for a, b, dZ in orders)
Z_comm = Z[k] + pending
i_ = max(i, 0)
F1 = predict_F(K[i_], U[i_], L[i_], Q[i_], Omega(t[i_], p), p, ch, tau_ref, e["H_pred"], kp, lag)
need, surplus_ref = e["xi_min"] * F1 + Q_exc / e["T_clear"], F1
sig = f"F_pred={F1:.3f} Q_exc={Q_exc:.2f}"
if need > e["ups_hi"] * Z_comm:
dZ = e["kZ"] * (need / e["ups_star"] - Z_comm)
orders.append((tk + e["L1"], tk + e["L2"], dZ)); Z_down = None
log.append(f"mnd {tk:.0f}: bestill +{dZ / ch['Z'] * 100:.0f} % ({sig})")
elif surplus_ref < e["ups_lo"] * Z[k] and pending < 1e-9:
Z_down = surplus_ref / e["ups_star"]
log.append(f"mnd {tk:.0f}: bygg ned mot {Z_down / ch['Z']:.2f} ({sig})")
rate = sum(dZ / (b - a) for a, b, dZ in orders if a <= tk < b)
if Z_down is not None and rate == 0.0 and Z[k] > Z_down:
rate -= (Z[k] - Z_down) / e["tau_down"]
Z[k + 1] = Z[k] + DT * rate
xi[k + 1] = xi[k] + DT * (xi_tgt - xi[k]) / e["tau_xi"]
elif policy != "sched":
raise ValueError(policy)
dK, dU, dL, dQ, tauD[k], JD[k], RD[k], Fa[k] = deriv_chain_xz(
K[k], U[k], L[k], T[k], Q[k], Z[k], xi[k], 0.0, tk, p, ch, tau_ref, mu3=0.0)
K[k + 1] = K[k] + DT * dK
U[k + 1] = U[k] + DT * dU
L[k + 1] = np.clip(L[k] + DT * dL, 1e-4, 1 - 1e-4)
Q[k + 1] = max(Q[k] + DT * dQ, 0.0)
for arr in (T, tauD, JD, RD, Fa):
arr[-1] = arr[-2]
return dict(t=t, K=K, U=U, L=L, T=T, Q=Q, tauD=tauD, JD=JD, RD=RD, Z=Z, xi=xi, tau_ref=tau_ref, log=log)
def kontroller_integrator(tau, p, ch, e, kp):
"""Stopp hvis simuler() uten innlagt restanse ikke gir nøyaktig samme svar som simulate_endo."""
for policy in ("koord", "sched"):
egen = simuler(policy, tau, p, ch, e, kp, SLUTT)
fasit = simulate_endo(policy, tau=tau, p=p, ch=ch, e=e, kp=kp, tend=SLUTT)
like = (egen["log"] == fasit["log"] and egen["tau_ref"] == fasit["tau_ref"]
and all(np.array_equal(egen[n], fasit[n])
for n in ("t", "K", "U", "L", "T", "Q", "tauD", "JD", "RD", "Z", "xi")))
if not like:
sys.exit("Intern kontroll feilet: regnestegene i forlop.py (simuler) gir ikke lenger samme svar "
f'som simulate_endo i sim.py (policy "{policy}"). Er sim.py endret, må simuler() '
"oppdateres likt. Forløpet er ikke regnet.")
def sett_ved_vedtak(okning, steg, mo, tau, kp, H=ENDO["H_pred"]):
"""Det den koordinerte regelen ser ved første vedtak (sim.py, simulate_endo, budsjettsløyfen):
prognosen F1 og restansen over målet, begge fra tilstanden lagD måneder før. Returnerer også
køen modellen selv har bygd opp på det tidspunktet."""
p = dict(P, shock=okning, t_shock=(steg - 0.5) * DT)
r = simulate_endo("koord", tau=tau, p=p, ch=mo["ch"], e=mo["e"], kp=kp, tend=OBS)
i = idx(OBS)
F1 = predict_F(r["K"][i], r["U"][i], r["L"][i], r["Q"][i], Omega(r["t"][i], p), p, mo["ch"],
r["tau_ref"], H, kp, idx(tau))
Q_exc = max(r["Q"][i] - (r["tau_ref"] - mo["ch"]["m"]) * mo["ch"]["Z"], 0.0)
return F1, Q_exc, r["Q"][i]
def kalibrer(mo, tau, kp):
"""Finn økningen i pådraget, slik at første vedtak ser F_maal, og tidssteget den begynner, slik at
restansen modellen selv bygger opp, kommer nærmest Q_maal. Økningen må begynne etter at budsjettet
i måned 12 har sett sine tall, og før tallene til vedtaket i måned 24 er tatt ut. Returnerer
(økning, steg, kant), der kant er "tidlig" eller "sent" når restansen ikke kan treffes innenfor
det vinduet. Avviket som står igjen, dekkes ved at restansen i fila legges inn direkte (main)."""
def okning_for(steg):
f = lambda x: sett_ved_vedtak(x, steg, mo, tau, kp)[0] - mo["F_maal"]
lav, hoy = 1.0001, 3.0
if f(lav) > 0:
sys.exit("Forløpet har ingenting å vise: prognosen ligger for nær innstrømningen før økningen.")
if f(hoy) < 0:
sys.exit("Forløpet dekker ikke disse tallene: prognosen ligger for høyt over innstrømningen før "
f"økningen ({tall(mo['F_maal'] / J0, 1)} ganger; modellen rekker om lag 3).")
return brentq(f, lav, hoy, xtol=1e-6)
def avvik(steg):
x = okning_for(steg)
return x, sett_ved_vedtak(x, steg, mo, tau, kp)[1] - mo["Q_maal"]
tidlig, sent = idx(NAA - 12.0 - ENDO["lagD"]) + 1, idx(OBS) - 1
x_t, g_t = avvik(tidlig)
if g_t <= 0: # selv den lengste forhistorien gir for liten restanse
return x_t, tidlig, "tidlig"
x_s, g_s = avvik(sent)
if g_s >= 0: # selv den korteste gir for stor
return x_s, sent, "sent"
while sent - tidlig > 1:
midt = (tidlig + sent) // 2
x_m, g_m = avvik(midt)
if g_m > 0:
tidlig, x_t, g_t = midt, x_m, g_m
else:
sent, x_s, g_s = midt, x_m, g_m
return (x_t, tidlig, None) if abs(g_t) <= abs(g_s) else (x_s, sent, None)
def naar(mnd):
if mnd == 0:
return "nå"
return f"om {mnd:.0f} mnd" if mnd > 0 else f"for {-mnd:.0f} mnd siden"
def vedtak(r, mo):
"""Budsjettvedtakene i forløpet, fra loggen til simulate_endo. Samme nedbygging år etter år
vises én gang."""
ut = []
for linje in r["log"]:
t = re.match(r"mnd (\d+): bestill \+(\d+) %", linje)
if t:
ut.append((float(t.group(1)) - NAA, f"bestill {t.group(2)} % mer kapasitet", int(t.group(2))))
continue
t = re.match(r"mnd (\d+): bygg ned mot ([\d.]+)", linje)
if not t:
sys.exit(f"Kjenner ikke igjen vedtaket «{linje}» fra sim.py; forlop.py må oppdateres.")
tekst = ("bygg ned ved naturlig avgang mot om lag "
f"{tall(float(t.group(2)) * mo['ch']['Z'] * mo['skala'], 0)} saker/mnd")
if not (ut and ut[-1][1] == tekst):
ut.append((float(t.group(1)) - NAA, tekst, None))
return ut
def nokkeltall(r, Z0, fra):
t = r["t"]
return dict(tid=r["tauD"].max(), tillit=r["L"].min(), krim=r["K"][t >= fra].mean(),
kap=r["Z"].max() / Z0 - 1.0,
kapaar=np.trapezoid(np.maximum(r["Z"] - Z0, 0) / Z0, t) / 12)
def ikke_regnet(grunn):
print(f"Forløp: ikke regnet. {grunn}")
def main(fil, valgt=None):
data = diagnose.les(fil)
st = les_styring(data)
tau, kp = st["forsinkelse"], st["reaktivitet"]
rader = [diagnose.analyser(ledd) for ledd in data["ledd"]]
if valgt is None:
b = diagnose.skranke(rader)
else:
treff = [a for a in rader if a["navn"] == valgt]
if not treff:
sys.exit(f'Fant ikke leddet "{valgt}". Leddene i fila: ' + ", ".join(a["navn"] for a in rader) + ".")
b = treff[0]
stabil = stabilitet(tau, kp)
if not st["oppgitt"]:
print('(Fila har ikke "styring"; forsinkelse 3 mnd og reaktivitet 3 er regime A i notatet.)')
print()
if valgt is None and not diagnose.binder(b):
print(f"Forløp: ingen ledd binder (høyeste utnyttelse er {tall(b['ups'])}, {b['navn']}), "
"så det er ikke noe tiltak å følge over tid.")
return
if not stabil:
ikke_regnet("Når styringen selv skaper svingninger, finnes det ingen ny balanse å dimensjonere etter, "
"og prognosen regelen bruker, blir tilfeldig. Rett styringen først (trinn (e) i metoden "
"krever det), og kjør på nytt.")
return
mo = modell(b)
okning, steg, kant = kalibrer(mo, tau, kp)
s, ch = mo["skala"], mo["ch"]
Z0 = ch["Z"]
F1, _, Q_egen = sett_ved_vedtak(okning, steg, mo, tau, kp)
F2 = sett_ved_vedtak(okning, steg, mo, tau, kp, H=2 * ENDO["H_pred"])[0]
if abs(F2 - F1) > 0.01 * F1:
ikke_regnet("Stabilitetssjekken er bestått, men prognosen for strømmen har ikke falt til ro innen "
f"horisonten på {tall(ENDO['H_pred'], 0)} mnd ({tall(F1 * s, 1)} saker/mnd, mot "
f"{tall(F2 * s, 1)} med dobbel horisont): styringen er for nær grensen eller for treg. Da "
"finnes det ikke ett tall å dimensjonere etter. Prøv med kortere forsinkelse.")
return
Q_u = max(b["Q"] - b["Q_ref"], 0.0)
behov = ((1.0 - b["andel"]) * b["Fp"] + Q_u / diagnose.T_CLEAR) / diagnose.UPS_STAR
def bestilling(x): # som regelen: ingen bestilling når behovet før marginen er dekket
return x / b["Z"] - 1.0 if x * ENDO["ups_star"] > ENDO["ups_hi"] * b["Z"] else 0.0
# Restansen i fila legges inn i tidssteget tallene til første vedtak tas ut (OBS): køen flyttes
# fra det modellens egen forhistorie ga, til restansen i fila. Resten av tilstanden (kriminalitet,
# tillit, politiinnsats) kommer fra forhistorien. Kjøringen uten tiltak får samme flytting, så
# den starter høyere når regelen har ledet saker utenom i forhistorien (som i tabell 10).
p = dict(P, shock=okning, t_shock=(steg - 0.5) * DT)
kontroller_integrator(tau, p, ch, mo["e"], kp)
inn = (idx(OBS), mo["Q_inn"] - Q_egen)
r = simuler("koord", tau, p, ch, mo["e"], kp, SLUTT, inn=inn)
r0 = simuler("sched", tau, p, ch, mo["e"], kp, SLUTT, inn=inn)
K_min, T_min = float(r["K"][idx(OBS):].min()), float(r["T"][idx(OBS):].min())
if K_min < K_GULV or T_min < T_GULV:
ikke_regnet(f"Mens restansen i fila ({tall(b['Q'], 0)} saker, {tall(b['Q'] / b['Z'], 0)} måneders "
f"kapasitet) avvikles, faller kriminaliteten i modellen til {tall(max(K_min, 0.0))} og "
f"politiets innsats til {tall(T_min)} (1,00 = nivået før økningen). Skriptet regner ikke "
f"forløpet når kriminaliteten kommer under {tall(K_GULV)} eller innsatsen under "
f"{tall(T_GULV)}: modellen legger halvparten av virkningen av en sak til tidspunktet saken "
"avgjøres, og det holder ikke når restansen er mange ganger større enn strømmen. Bruk "
"diagnosen.")
return
alle = vedtak(r, mo)
if any(mnd < 0 for mnd, _, _ in alle):
sys.exit("Forløpet kan ikke legges med disse tallene: modellens regel gjør vedtak før økningen er "
"observert. Kontroller kapasiteten og innstrømningen før økningen.")
print(f"Forløp over tid for {'leddet som binder' if valgt is None else 'leddet du valgte'}: {b['navn']}\n")
print("Slik er tallene lagt inn i modellen:")
print("- Leddet regnes som modellens ledd med kø: det tar imot sakene politiets innsats gir "
"(domstolen i notatet).")
print(f"- Fra fila: kapasitet {tall(b['Z'], 1)} saker/mnd"
+ (" (etter skjult avgang)" if b["skjult"] > 0 else "")
+ f", prognose {tall(b['Fp'], 1)} saker/mnd"
+ ("" if b["har_prognose"] else " (innstrømningen nå; ingen prognose oppgitt)")
+ f", restanse {tall(b['Q'], 0)} ({tall(Q_u, 0)} over målet på {tall(b['Q_ref'], 0)}), "
f"høyst {tall(b['andel'] * 100, 0)} % ledet utenom.")
print(f"- Før økningen: {tall(mo['F_for'], 1)} saker/mnd inn, utnyttelse {tall(mo['F_for'] / b['Z'])}"
+ (' (antatt; oppgi "innstromning_for_okning" for å endre).' if mo["antatt"] else " (oppgitt)."))
print(f"- Modellen gjenskaper prognosen med en varig økning i det strukturelle pådraget på "
f"{tall((okning - 1) * 100, 0)} % som begynte for {tall(NAA - steg * DT, 0)} mnd siden"
+ ({"sent": " (det seneste modellen tillater)", "tidlig": " (det tidligste modellen tillater)"}[kant]
if kant else "") + ".")
print(f"- «Nå» er første budsjettvedtak etter økningen. Tallene det bygger på, er "
f"{tall(ENDO['lagD'], 0)} mnd gamle, som tallene i fila.")
start_u = (mo["Q_inn"] + mo["Q_min"]) * s # restansen forløpet starter i, i saker
egen_u = (Q_egen + mo["Q_min"]) * s # restansen modellens egen forhistorie ga
print(f"- Restansen er lagt inn direkte: forløpet starter i {tall(start_u, 0)} saker på tidspunktet "
f"tallene gjelder (for {tall(ENDO['lagD'], 0)} mnd siden)."
+ (f" Modellens egen forhistorie ville gitt {tall(egen_u, 0)}."
if abs(egen_u - start_u) > max(0.02 * b["Q_ref"], 1.0) else ""))
if start_u - b["Q"] > 0.5:
print(f"- Avvik: restansen i fila ({tall(b['Q'], 0)}) er lavere enn modellen kan ha. Med denne "
f"kapasiteten regner modellen minst {tall(mo['Q_min'] / Z0, 1)} mnd behandlingstid, altså "
f"{tall(start_u, 0)} saker.")
if start_u - egen_u > max(0.25 * b["Q_ref"], 1.0):
print(f"- Forbehold: en så stor restanse har vokst lenger enn modellens forhistorie på "
f"{tall(NAA - steg * DT, 0)} mnd. Kriminalitet, tillit og politiinnsats starter derfor som om "
"restansen var ny, og virkningen av lang saksbehandlingstid kommer i modellen først etter "
"start. I virkeligheten har den alt virket en stund.")
if b["andel"] > 0 and r["xi"][idx(NAA)] < 0.995:
print(f"- I modellens forhistorie har den koordinerte regelen alt begynt å lede saker utenom "
f"({tall((1 - r['xi'][idx(NAA)]) * 100, 0)} % nå).")
if K_min < 0.995 or T_min < 0.995:
print(f"- Forbehold: mens restansen avvikles, faller kriminaliteten i modellen til {tall(K_min)} og "
f"politiets innsats til {tall(T_min)} (1,00 = nivået før økningen). Modellen legger halvparten "
"av virkningen av en sak til tidspunktet saken avgjøres. Om det holder for gamle saker, er "
"usikkert.")
print("\nVedtak i forløpet (budsjettet vedtar én gang i året; bestilt kapasitet virker gradvis "
f"fra {tall(ENDO['L1'], 0)} til {tall(ENDO['L2'], 0)} mnd etter vedtak):")
for mnd, tekst, _ in alle:
print(f"- {naar(mnd)}: {tekst}")
if not alle:
print("- ingen: regelen bestiller ikke kapasitet med disse tallene.")
forste = next((pst for mnd, _, pst in alle if mnd == 0 and pst is not None), None)
diag = round(bestilling(behov) * 100)
if forste is not None and forste == diag:
print(f" Første vedtak er det samme som diagnose.py regner ut ({diag} %).")
elif forste is not None:
print(f" diagnose.py regner {diag} % for første vedtak; forskjellen er avrunding i modellens "
"prognose.")
if len([1 for _, _, pst in alle if pst is not None]) > 1:
print(" Senere bestillinger kommer fordi restansen har vokst, eller prognosen er endret, mens den "
"første kapasiteten bygges opp.")
print(f"\n{'Mnd fra nå':>10}{'Kapasitet':>11}{'Utenom':>8}{'Restanse':>10}{'Saksbeh.tid':>13}"
f"{'Politiinnsats':>15}{'Kriminalitet':>14}{'Tillit':>8}")
for mnd in RADER:
k = idx(mnd + NAA)
print(f"{mnd:>10}{tall(r['Z'][k] * s, 1):>11}{tall((1 - r['xi'][k]) * 100, 0) + ' %':>8}"
f"{tall(r['tauD'][k] * r['Z'][k] * s, 0):>10}{tall(r['tauD'][k], 1):>13}"
f"{tall(r['T'][k]):>15}{tall(r['K'][k]):>14}{tall(r['L'][k]):>8}")
print("(Kapasitet i saker/mnd, restanse i saker, saksbehandlingstid i mnd = restanse delt på kapasitet. "
"Politiinnsats og kriminalitet: 1 = nivået før økningen. Tillit: 0,80 før økningen.)")
fra = steg * DT
med, uten = nokkeltall(r, Z0, fra), nokkeltall(r0, Z0, fra)
print(f"\nMed og uten tiltak i kjeden, fra økningen begynte til {tall(SLUTT - NAA, 0)} mnd fra nå:\n")
print(f"{'':<34}{'Med tiltakene':>14}{'Uten':>10}")
print(f"{'Lengste saksbehandlingstid (mnd)':<34}{tall(med['tid'], 1):>14}{tall(uten['tid'], 1):>10}")
print(f"{'Laveste tillit':<34}{tall(med['tillit']):>14}{tall(uten['tillit']):>10}")
print(f"{'Kriminalitet, gjennomsnitt':<34}{tall(med['krim']):>14}{tall(uten['krim']):>10}")
print(f"{'Høyeste kapasitet':<34}{'+' + tall(med['kap'] * 100, 0) + ' %':>14}{'+0 %':>10}")
print(f"{'Kapasitetsår over utgangsnivået':<34}{tall(med['kapaar'], 1):>14}{tall(0, 1):>10}")
uten_u = (r0["Q"][idx(OBS)] + mo["Q_min"]) * s
if abs(uten_u - start_u) >= 1.0:
print(f"(Uten tiltak starter restansen i {tall(uten_u, 0)} saker, ikke {tall(start_u, 0)}: i den "
"kjøringen er ingen saker ledet utenom før nå.)")
if med["tillit"] < LAM_MYK:
print(f"Tilliten kommer under modellens myke grense ({tall(LAM_MYK)}). Trinn (f) peker da på "
"prosessuell kvalitet i tillegg.")
sent = r["t"] > SLUTT - 60.0
spenn = {n: (float(r[n][sent].min()), float(r[n][sent].max())) for n in ("K", "tauD", "Z")}
i_ro = (spenn["K"][1] - spenn["K"][0] <= 0.01
and spenn["tauD"][1] - spenn["tauD"][0] <= 0.05 * mo["tau_ref"]
and spenn["Z"][1] - spenn["Z"][0] <= 0.01 * Z0)
if i_ro:
print(f"Forløpet faller til ro: kriminaliteten ender på {tall(r['K'][-1])} og saksbehandlingstiden på "
f"{tall(r['tauD'][-1], 1)} mnd.")
ups_slutt = r["JD"][-1] / r["Z"][-1]
if ups_slutt < ENDO["ups_star"] - 0.03:
print(f"Kapasiteten ender høyere enn strømmen krever (utnyttelse {tall(ups_slutt)} mot målet "
f"{tall(ENDO['ups_star'])}): det som ble bestilt for å avvikle restansen, blir stående, fordi "
f"regelen først bygger ned under {tall(ENDO['ups_lo'])}.")
else:
print("Forløpet har ikke falt til ro ved slutten. De siste fem årene ligger kriminaliteten mellom "
f"{tall(spenn['K'][0])} og {tall(spenn['K'][1])}, saksbehandlingstiden mellom "
f"{tall(spenn['tauD'][0], 1)} og {tall(spenn['tauD'][1], 1)} mnd og kapasiteten mellom "
f"{tall(spenn['Z'][0] * s, 1)} og {tall(spenn['Z'][1] * s, 1)} saker/mnd. Sluttallene over er da "
"ikke en ny balanse.")
print("\nResten av kjeden:")
print(f"- Politiet: innsatsen går fra 1,00 til høyst {tall(r['T'].max())} og ender på {tall(r['T'][-1])}. "
"Å bremse politiet til leddets takt er ikke et tiltak (diagnose.py).")
if b["andel"] > 0:
utenom = 1.0 - r["xi"]
print(f"- Saker ledet utenom {b['navn']}: høyst {tall(utenom.max() * 100, 0)} % "
f"({naar(round(r['t'][int(np.argmax(utenom))] - NAA))}) og {tall(utenom[-1] * 100, 0)} % til "
"slutt. Hvor de sakene havner, er ikke regnet.")
R_data = r["RD"][idx(OBS) - 1] # steget før restansen legges inn: avgangen følger køen i modellen
topp, slutt = r["RD"].max() / R_data, r["RD"][-1] / R_data
print(f"- Ut av {b['navn']}: i modellen avgjør leddet på det meste {tall((topp - 1) * 100, 0)} % flere saker "
f"enn på tidspunktet tallene i fila gjelder ({naar(round(r['t'][int(np.argmax(r['RD']))] - NAA))}), "
f"og {tall((slutt - 1) * 100, 0)} % flere til slutt.")
plass = next(i for i, a in enumerate(rader) if a is b)
for a in rader[plass + 1:]:
if max(topp, slutt) <= 1.005:
print(f"- {a['navn']}: uendret innstrømning herfra.")
continue
ny_topp, ny_slutt = a["F"] * topp, a["F"] * slutt
dom = ("Leddet binder allerede, og presset øker." if a["ups"] > diagnose.UPS_HI else
"Skranken flytter seg dit." if ny_slutt > diagnose.UPS_HI * a["Z"] else
"Leddet er over full utnyttelse mens restansen avvikles, ikke varig."
if ny_topp > diagnose.UPS_HI * a["Z"] else "Leddet tar det unna.")
print(f"- {a['navn']}: får leddet sakene sine gjennom {b['navn']}, går innstrømningen fra "
f"{tall(a['F'], 1)} til om lag {tall(ny_topp, 1)} per mnd på det meste og {tall(ny_slutt, 1)} til "
f"slutt, mot kapasitet {tall(a['Z'], 1)}: utnyttelse {tall(ny_topp / a['Z'])} og "
f"{tall(ny_slutt / a['Z'])}. {dom}")
if not rader[plass + 1:]:
print(f"- Fila har ingen ledd etter {b['navn']}.")
print("\nLes forløpet slik: kapasitet, restanse og saksbehandlingstid følger tallene i fila. Politiinnsats, "
"kriminalitet og tillit følger modellens stiliserte parametere, som ikke er kalibrert på norske data. "
"Rekkefølgen og retningen er poenget, ikke tallene. Bare leddet som binder, har kø i modellen.")
if __name__ == "__main__":
if len(sys.argv) not in (2, 3):
sys.exit('Bruk: python forlop.py <inndata.json> ["navn på ledd"]')
main(*sys.argv[1:])