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>
527 lines
30 KiB
Python
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:])
|