feat(forlop): start the time course in the backlog given in the file

forlop.py now puts the backlog from the input file straight into the
model at the time the numbers apply, instead of letting the model build
it up itself (which reached at most 15 months and left larger backlogs
"ikke regnet"). It carries its own copy of the simulate_endo loop
(simuler, policies koord and sched) and checks on every run that the
copy matches sim.py bit for bit. sim.py and paper/data are untouched
(22/22 data files identical after a fresh run).

The example file gives the same numbers as before (+19 %, +12 %, 1,31;
8,3 / 0,74 / 1,10 / +31 % / 3,5; 68,7 / 0,65 / 1,26). A copy with
backlog 3000 now gives a course starting at 3 000 (+119 %). The course
is not computed when crime falls below 0,80 or police effort below 0,50
in the model while the backlog is cleared.

Independent review before commit: 8 findings, 7 fixed (the eighth is an
older column-width issue for seven-digit numbers).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-10 18:01:55 +01:00
commit 2badc25e51
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 172 additions and 45 deletions

View file

@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file.
## 2026-11-10 — forløpet starter i dagens restanse
- `paper/forlop.py` legger nå restansen i fila rett inn i modellen, så forløpet starter i restansen du oppgir. Før måtte modellen bygge opp restansen selv, og var den større enn modellen rakk på 15 måneder, ble forløpet ikke regnet. En kopi av eksempelfila med restanse 3 000 ga før «ikke regnet» og gir nå et forløp som starter i 3 000 (119 % mer kapasitet i første vedtak, som i `diagnose.py`). En restanse under målet legges også inn som den er, ned til modellens minste restanse.
- Eksempelfila gir samme tall som før: 19 % nå, 12 % året etter og kapasitet 1,31 til slutt. For de norske tallene starter fengselsforløpet nå i køen på 596 dommer i stedet for i 686 (61 over målet på 625). Første vedtak er fortsatt 15 % og neste 7 %; høyeste kapasitet blir 22 % over utgangsnivået (før 23 %).
- `paper/sim.py` er ikke endret, og figurdataene er de samme (22 av 22 filer byte for byte like etter ny kjøring). For å kunne legge inn restansen har `forlop.py` sin egen utgave av regnestegene for den koordinerte regelen og for kjøringen uten tiltak. Hver kjøring kontrollerer at den gir nøyaktig samme svar som `sim.py` når restansen ikke legges inn.
- Nye forbehold i utskriften: når restansen er større enn modellens forhistorie kan forklare, når kriminaliteten i modellen faller under nivået før økningen mens restansen avvikles, og hvor sammenligningen uten tiltak starter. Faller kriminaliteten i modellen under 0,80 eller politiets innsats under 0,50 av nivået før økningen mens restansen avvikles, regnes forløpet ikke.
## 2026-11-10 — forløp over tid med egne tall
- Nytt skript `paper/forlop.py`: leser samme fil som `diagnose.py`, sjekker om styringen tåler sin egen forsinkelse, og viser måned for måned hva som skjer når anbefalingen følges, sammenlignet med å ikke gjøre noe i kjeden. Det viser også hva tiltaket gjør med resten av kjeden: politiets innsats, andelen ledet utenom, og om skranken flytter seg til leddene etter. Eksempelfila gjenskaper avsnitt 9.5 (19 % nå, 12 % året etter, kapasitet 1,31 til slutt). Bare leddet som binder, har kø i modellen. Skriptet regner ikke forløpet når styringen er ustabil eller for nær grensen, eller når restansen i fila er større enn modellen rekker å bygge opp før første vedtak.

View file

@ -18,7 +18,7 @@ Concept note (Norwegian, LaTeX) and its simulation code. The principle: enforcem
- Setup: `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`
- Run simulation (about 2 min, from `paper/`): `../.venv/bin/python sim.py`
- Bottleneck diagnosis from own numbers (stdlib only, from `paper/`): `python3 diagnose.py eksempel_domstol.json`; must reproduce section 9.5 (utilization 1,28, +19 % capacity). `python3 diagnose.py norge_2025.json` must match the table in README (1,09 / 1,04 / 0,88 / 1,09).
- Time course and stability check from own numbers (needs the venv, from `paper/`): `../.venv/bin/python forlop.py eksempel_domstol.json`; must reproduce section 9.5 and table 10 (critical delay 7,2; +19 % now, +12 % after 12 months, capacity 137,9 = 1,31; with measures 8,3 / 0,74 / 1,10 / +31 % / 3,5; without 68,7 / 0,65 / 1,26). `forlop.py` imports `sim.py` and must never require a change there.
- Time course and stability check from own numbers (needs the venv, from `paper/`): `../.venv/bin/python forlop.py eksempel_domstol.json`; must reproduce section 9.5 and table 10 (critical delay 7,2; +19 % now, +12 % after 12 months, capacity 137,9 = 1,31; with measures 8,3 / 0,74 / 1,10 / +31 % / 3,5; without 68,7 / 0,65 / 1,26). A copy of the example with `restanse` 3000 for Domstol must give a course that starts at 3 000 (first decision +119 %, as `diagnose.py`). `forlop.py` imports `sim.py` and must never require a change there. It has its own copy of the `simulate_endo` loop (`simuler`, policies `koord` and `sched` only) so that the backlog in the file can be put in directly; every run checks that the copy matches `simulate_endo` bit for bit, so a change to `simulate_endo` must be mirrored in `simuler`.
- Reproduction check after a run: `git status paper/data` shows no changes (verified 2026-11-10: 22/22 files identical with Python 3.14.0, numpy 2.5.4, scipy 1.18.1)
- LaTeX build (from `paper/`, TeX Live 2026 in `/Library/TeX/texbin`): `pdflatex main.tex` and `pdflatex notat.tex` three times each, `xelatex karusell.tex` twice; copy to `pdf/` (verified 2026-11-10: main 58 pages, notat 2, karusell 16, no undefined references). Build in a scratch copy of `paper/` to keep aux files out of the repo, and give each run a time limit: one pdflatex run has been seen to hang.

View file

@ -57,13 +57,15 @@ Diagnosen regner ikke med kapasitet som er bevilget, men ikke virker ennå: oppg
Svaret har tre deler:
- **Stabilitetssjekk.** I fila oppgir du forsinkelsen fra kriminalitetsbildet endrer seg til innsatsen er endret, og hvor kraftig innsatsen reagerer (`"styring"`). Skriptet sier om forsinkelsen er kortere enn den kritiske, og hvor kraftig reaksjonen kan være. Med eksempelfila er den kritiske forsinkelsen 7,2 måneder mot 3, altså stabilt. Er styringen ustabil eller for nær grensen, regner skriptet ikke forløpet, og sier hvorfor.
- **Forløp.** Leddet som binder, legges inn i modellen med dine tall for kapasitet, prognose, restanse og mål. Du ser budsjettvedtakene, kapasiteten, andelen ledet utenom, restansen og saksbehandlingstiden, og en sammenligning med å ikke gjøre noe i kjeden. Eksempelfila gjenskaper avsnitt 9.5: 19 prosent mer kapasitet nå, 12 prosent til året etter, og en kapasitet på 1,31 ganger utgangsnivået til slutt.
- **Forløp.** Leddet som binder, legges inn i modellen med dine tall for kapasitet, prognose, restanse og mål. Forløpet starter i restansen du oppgir (er den lavere enn modellens minste restanse, starter det der, og skriptet sier fra). Du ser budsjettvedtakene, kapasiteten, andelen ledet utenom, restansen og saksbehandlingstiden, og en sammenligning med å ikke gjøre noe i kjeden. Eksempelfila gjenskaper avsnitt 9.5: 19 prosent mer kapasitet nå, 12 prosent til året etter, og en kapasitet på 1,31 ganger utgangsnivået til slutt.
- **Resten av kjeden.** Politiets innsats, andelen påtalemyndigheten leder utenom, og hva flere avgjorte saker betyr for leddene som står etter i fila. I eksempelet flytter skranken seg til kriminalomsorgen (utnyttelse 1,13 til slutt).
Forbehold:
- Bare leddet som binder, har kø i modellen, og det regnes som leddet som tar imot sakene politiets innsats gir (domstolen i notatet). For leddene etter regner skriptet bare hva flere avgjorte saker betyr hvis de får sakene sine gjennom skranken. Hvor sakene som ledes utenom havner, er ikke regnet. Leddene foran vises som politiets innsats og andelen ledet utenom.
- Kapasitet, restanse og saksbehandlingstid følger tallene dine. Kriminalitet, tillit og politiets innsats følger modellens stiliserte parametere, som ikke er kalibrert på norske data. Rekkefølgen og retningen er poenget, ikke tallene.
- Modellen starter med et ledd i balanse, en varig økning og første budsjettvedtak høyst 15 måneder senere. Er restansen din så stor at modellen ikke rekker å bygge den opp på den tiden, regner skriptet ikke forløpet. Da gjelder diagnosen alene. Mindre avvik skriver skriptet ut.
- Restansen legges inn direkte, så forløpet starter i restansen du oppgir, også når den har vokst i årevis. Kriminalitet, tillit og politiets innsats starter likevel fra modellens egen forhistorie: et ledd i balanse, en varig økning og første budsjettvedtak høyst 15 måneder senere. For en restanse som har vokst lenger enn det, kommer virkningen av lang saksbehandlingstid derfor først etter start i modellen. Skriptet sier fra når det gjelder.
- Modellen legger halvparten av virkningen av en sak til tidspunktet saken avgjøres. Når en stor restanse avvikles, faller kriminaliteten i modellen derfor under nivået før økningen, og skriptet skriver ut et forbehold. Faller kriminaliteten i modellen under 0,80 eller politiets innsats under 0,50 av nivået før økningen, regner skriptet ikke forløpet. Da gjelder diagnosen alene. Med tallene i eksempelfila skjer det ved en restanse mellom 3 750 og 4 000 saker.
- Sammenligningen uten tiltak får samme tillegg eller fradrag i restansen som forløpet med tiltak. Den starter høyere når modellen har ledet saker utenom før første vedtak, og skriptet skriver ut hvor den starter.
- Stabilitetssjekken gjelder likevekten før økningen og bruker modellens stiliserte tall for hvor fort innsatsen virker.
### Første forsøk med norske tall
@ -89,7 +91,7 @@ Forbehold:
- Hvert ledd har sine egne saksenheter, så bare utnyttelsen kan sammenlignes på tvers av ledd. Politiets rapport og SSB teller heller ikke det samme. Politiets driftsstatistikk har 348 534 anmeldte lovbrudd og 349 450 påtaleavgjorte saker. SSBs offisielle statistikk har 308 438 etterforskede lovbrudd og holder utenfor om lag 35 300 anmeldelser som ble henlagt utenfor straffesak. Regnes kapasitetshenleggelsene som SSBs andel (9,8 % av de etterforskede lovbruddene) i stedet for som et antall, blir utnyttelsen for politi og påtale 1,11 i stedet for 1,09.
- Kapasitetshenleggelsene er en registrert begrunnelse. Politidirektoratet og riksadvokaten melder om forholdsvis store forskjeller mellom politidistriktene i bruken av henleggelseskoder. Riksadvokatens nye rundskriv om henleggelser (nr. 2/2025) gjelder fra 1. januar 2026, så tall fra 2026 kan ikke uten videre sammenlignes med tidligere år.
- Skriptet anslår at fengslene trenger om lag 15 % mer kapasitet (358 mot 313 iverksettelser i måneden). Om lag 9 % skal til for å holde følge med strømmen. Resten er margin, fordi verktøyet dimensjonerer til 95 % utnyttelse. Det er et grovt anslag.
- Forløpet over tid (`../.venv/bin/python forlop.py norge_2025.json`) gir for fengsel 15 % mer kapasitet nå og 7 % til året etter, fordi køen vokser mens den første kapasiteten bygges opp. Det er modellens stiliserte dynamikk lagt på norske størrelser, ikke en prognose. Køen til fengsel er dessuten under målet på to måneder, mens modellen starter på målet.
- Forløpet over tid (`../.venv/bin/python forlop.py norge_2025.json`) gir for fengsel 15 % mer kapasitet nå og 7 % til året etter, fordi køen vokser mens den første kapasiteten bygges opp. Det er modellens stiliserte dynamikk lagt på norske størrelser, ikke en prognose. Forløpet starter i køen på 596 dommer, som er under målet på to måneder (625).
## Hva notatet gjør

View file

@ -18,10 +18,10 @@ 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.
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.
@ -29,7 +29,10 @@ 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/.
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
@ -41,14 +44,15 @@ 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)
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)
@ -108,8 +112,9 @@ def stabilitet(tau, kp):
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)."""
"""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"]
@ -131,27 +136,117 @@ def modell(b):
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)
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."""
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
return F1, Q_exc, r["Q"][i]
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."""
"""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
@ -250,8 +345,8 @@ def main(fil, valgt=None):
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"])
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 "
@ -260,22 +355,29 @@ def main(fil, valgt=None):
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
# 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)
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)
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 "
@ -293,23 +395,35 @@ def main(fil, valgt=None):
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å "
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å).")
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."))
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:
@ -321,8 +435,8 @@ def main(fil, valgt=None):
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.")
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.")
@ -346,6 +460,10 @@ def main(fil, valgt=None):
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.")
@ -377,7 +495,7 @@ def main(fil, valgt=None):
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)]
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))}), "