den-dynamiske-rettsstaten/paper/forlop.py
Kjell Tore Guttormsen 01d335e941
feat(forlop): time course and stability check from own numbers
New script paper/forlop.py reads the same input file as diagnose.py.
It checks whether the steering tolerates its own delay, places the
binding link in the model as the link with a queue, calibrates the size
and start of the increase so that the coordinated rule's first budget
decision sees the numbers in the file, and runs simulate_endo month by
month with and without measures. It also reports what the measure does
to the rest of the chain: police effort, the share diverted, and whether
the bottleneck moves to the links after it.

The example file reproduces section 9.5 and table 10: +19 % now, +12 %
after 12 months, capacity 1,31; 8,3 / 0,74 / 1,10 / 3,5 with measures and
68,7 / 0,65 / 1,26 without. The stability block reproduces section 9.3
with delay 11 and reactivity 3. The script declines to print a time
course when the steering is unstable, when the forecast has not settled
within the horizon, or when the model cannot build the backlog in the
file before the first decision.

sim.py is imported unchanged; paper/data is untouched. diagnose.py gives
identical output for both input files; the bottleneck selection is now a
function so forlop.py can reuse it, and true/false are rejected where a
number is expected. Reviewed by an independent agent; its 14 findings
are addressed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:20:31 +01:00

409 lines
23 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
(simulate_endo, policy="koord"). Størrelsen på økningen og tidspunktet den
begynte, settes slik at regelens første budsjettvedtak ser tallene i fila:
prognosen for strømmen og restansen over målet. «Nå» er dette vedtaket.
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/.
"""
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, 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
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 og målene
regelens første vedtak skal se (prognose og restanse over målet, 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"])
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)
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."""
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
def kalibrer(mo, tau, kp):
"""Finn økningen i pådraget og tidssteget den begynner, slik at første vedtak ser F_maal og 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."""
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_sett = sett_ved_vedtak(okning, steg, mo, tau, kp)
F2, _ = sett_ved_vedtak(okning, steg, mo, tau, kp, H=2 * ENDO["H_pred"])
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
behov_modell = (mo["e"]["xi_min"] * F1 + Q_sett / ENDO["T_clear"]) / ENDO["ups_star"] * s
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
if abs(bestilling(behov_modell) - bestilling(behov)) > 0.02:
ikke_regnet(f"Modellen bygger opp restansen selv, og den treffer ikke tallene i fila ved første vedtak: "
f"{tall(Q_sett * s, 0)} saker over målet mot {tall(Q_u, 0)} i fila. Første vedtak ville blitt "
f"{tall(bestilling(behov_modell) * 100, 0)} % mer kapasitet, mens diagnose.py regner "
f"{tall(bestilling(behov) * 100, 0)} %. Et forløp fra et annet utgangspunkt enn ditt "
"ville villede. Bruk diagnosen; denne versjonen kan ikke starte i en restanse som har "
"vokst lenge.")
return
p = dict(P, shock=okning, t_shock=(steg - 0.5) * DT)
r = simulate_endo("koord", tau=tau, p=p, ch=ch, e=mo["e"], kp=kp, tend=SLUTT)
r0 = simulate_endo("sched", tau=tau, p=p, ch=ch, e=mo["e"], kp=kp, tend=SLUTT)
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 tallene 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.")
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å).")
avvik = abs(Q_sett * s - Q_u) > max(0.02 * b["Q_ref"], 1.0)
if avvik:
print(f"- Avvik: modellen starter med {tall(Q_sett * s, 0)} saker over målet, fila har {tall(Q_u, 0)}. "
+ ("Restansen i fila er under målet. Modellen kan ikke starte under målet, og den har noe "
"restanse over målet allerede før økningen." if b["Q"] < b["Q_ref"] else
"Modellen rekker ikke å bygge opp like stor restanse før første vedtak." if Q_sett * s < Q_u else
"Modellen får ikke restansen lavere før første vedtak."))
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; modellens restanse ved start ligger litt unna "
"fila.")
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}")
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)]
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:])