paper/diagnose.py takes inflow, capacity, backlog and target time per
link and returns utilization, throughput time and backlog growth per
link, the binding link, the type of measure in Theory of Constraints
order (exploit, subordinate, elevate) and, for the binding link, the
capacity needed under the coordinated rule from sim.py, sized on the
forecast flow rather than the queue.
The example file holds the stylized month-24 values from the coordinated
run and reproduces section 9.5 (utilization 1.28, +19 % capacity);
checked against simulate_endo("koord"), which orders +19 % in month 24.
sim.py and paper/data/ are unchanged. Time paths and the stability check
for the police rule are not in this version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
6.3 KiB
Python
125 lines
6.3 KiB
Python
"""Flaskehalsdiagnose for straffesakskjeden fra egne tall (første versjon).
|
|
|
|
Bruk (fra paper/):
|
|
../.venv/bin/python diagnose.py eksempel_domstol.json
|
|
|
|
Inndata er en JSON-fil med ett objekt per ledd i kjeden, i rekkefølge. Alle
|
|
strømmer i saker per måned, beholdninger i saker, tider i måneder:
|
|
|
|
{"ledd": [
|
|
{"navn": "Domstol",
|
|
"innstromning": 134, # saker inn per måned nå
|
|
"kapasitet": 105, # saker ledd kan avgjøre per måned
|
|
"restanse": 669, # saker i kø nå
|
|
"maal_tid": 3, # mål for saksbehandlingstid (mnd)
|
|
"prognose_innstromning": 134, # valgfritt: strømmen når balansen er gjenopprettet
|
|
"maks_andel_utenom": 0.2, # valgfritt: andel som kan ledes til forenklede reaksjoner
|
|
"restanse_for_12_mnd": 400} # valgfritt: gir observert vekst i restansen
|
|
]}
|
|
|
|
Verktøyet følger trinn (c)-(e) i metoden (artikkelen, avsnitt 9.4-9.5):
|
|
(c) finn skranken som binder: høyest utnyttelse, deretter raskest voksende restanse;
|
|
(d) velg tiltakstype etter Theory of Constraints: utnytt, underordne, utvid;
|
|
(e) dimensjoner kapasiteten etter prognosen for strømmen, ikke etter køen, med
|
|
den koordinerte regelen fra sim.py (simulate_endo, policy="koord"):
|
|
Z_ny = (xi_min * F_prognose + Q_overskudd / T_avvikling) / ups_star.
|
|
Forløpet over tid (simulering med egne tall) er ikke med i denne versjonen.
|
|
Restansen over målet regnes som restanse - maal_tid * kapasitet (Littles lov).
|
|
sim.py trekker i tillegg fra minste behandlingstid m_D; eksempelfila er satt
|
|
slik at overskuddet blir det samme som i sim.py i måned 24.
|
|
Bruker ingenting fra sim.py og endrer ikke paper/data/.
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
UPS_STAR = 0.95 # målutnyttelse ved dimensjonering (ENDO["ups_star"] i sim.py)
|
|
UPS_HI = 1.0 # bestill når utnyttelsen er over denne (ENDO["ups_hi"])
|
|
T_CLEAR = 24.0 # restansen avvikles over denne tiden, mnd (ENDO["T_clear"])
|
|
L1, L2 = 6.0, 24.0 # bestilt kapasitet virker lineært fra L1 til L2 mnd etter vedtak
|
|
|
|
|
|
def tall(x, d=2):
|
|
return f"{x:,.{d}f}".replace(",", " ").replace(".", ",")
|
|
|
|
|
|
def analyser(ledd):
|
|
F = float(ledd["innstromning"])
|
|
Z = float(ledd["kapasitet"])
|
|
Q = float(ledd["restanse"])
|
|
tid_maal = float(ledd["maal_tid"])
|
|
ups = F / Z
|
|
tid = Q / Z # Littles lov: beholdning / avgangsrate
|
|
if "restanse_for_12_mnd" in ledd:
|
|
vekst = (Q - float(ledd["restanse_for_12_mnd"])) / 12.0
|
|
vekst_kilde = "observert siste 12 mnd"
|
|
else:
|
|
vekst = F - min(F, Z) # det som ikke avgjøres, legger seg i kø
|
|
vekst_kilde = "innstrømning minus kapasitet"
|
|
return dict(navn=ledd["navn"], F=F, Z=Z, Q=Q, tid_maal=tid_maal, ups=ups, tid=tid,
|
|
vekst=vekst, vekst_kilde=vekst_kilde,
|
|
Fp=float(ledd.get("prognose_innstromning", F)),
|
|
andel=float(ledd.get("maks_andel_utenom", 0.0)))
|
|
|
|
|
|
def tiltak(a):
|
|
"""Trinn (d) og (e) for det bindende leddet."""
|
|
ut = []
|
|
xi_min = 1.0 - a["andel"]
|
|
Q_ref = a["tid_maal"] * a["Z"] # kø som svarer til målet for saksbehandlingstid
|
|
Q_exc = max(a["Q"] - Q_ref, 0.0)
|
|
ut.append("1. Utnytt leddet: fjern tapt tid i skranken (ikke regnet her).")
|
|
if a["andel"] > 0:
|
|
ups_u = xi_min * a["Fp"] / a["Z"]
|
|
ut.append(f"2. Led saker utenom: med {tall(a['andel'] * 100, 0)} % til forenklede reaksjoner "
|
|
f"blir utnyttelsen {tall(ups_u)} (fra {tall(a['Fp'] / a['Z'])} med prognosen).")
|
|
if ups_u <= UPS_HI and Q_exc == 0:
|
|
ut.append(" Det rekker: kapasiteten trenger ikke utvides.")
|
|
return ut
|
|
else:
|
|
ut.append("2. Led saker utenom: ikke oppgitt at noen andel kan ledes utenom dette leddet.")
|
|
behov = (xi_min * a["Fp"] + Q_exc / T_CLEAR) / UPS_STAR
|
|
if behov > UPS_HI * a["Z"]:
|
|
okning = behov / a["Z"] - 1.0
|
|
ut.append(f"3. Utvid: dimensjoner etter prognosen ({tall(a['Fp'], 1)} saker/mnd), ikke etter køen. "
|
|
f"Restanse over målet: {tall(Q_exc, 0)} saker, avviklet over {T_CLEAR:.0f} mnd.")
|
|
ut.append(f" Kapasitet: ({tall(xi_min)} x {tall(a['Fp'], 1)} + {tall(Q_exc, 0)}/{T_CLEAR:.0f}) / "
|
|
f"{tall(UPS_STAR)} = {tall(behov, 1)} saker/mnd, det vil si {tall(okning * 100, 0)} % mer.")
|
|
ut.append(f" Bestilt kapasitet virker gradvis fra {L1:.0f} til {L2:.0f} mnd etter vedtak. "
|
|
"Når restansen er avviklet, går andelen utenom tilbake, og behovet vurderes på nytt.")
|
|
else:
|
|
ut.append("3. Utvid: ikke nødvendig med disse tallene.")
|
|
return ut
|
|
|
|
|
|
def main(fil):
|
|
with open(fil, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
rader = [analyser(l) for l in data["ledd"]]
|
|
print("Trinn (c): utnyttelse og restanse i hvert ledd\n")
|
|
print(f"{'Ledd':<22}{'Utnyttelse':>11}{'Saksbeh.tid':>13}{'Mål':>6}{'Restansevekst/mnd':>19}")
|
|
for a in rader:
|
|
print(f"{a['navn']:<22}{tall(a['ups']):>11}{tall(a['tid'], 1):>13}{tall(a['tid_maal'], 1):>6}"
|
|
f"{tall(a['vekst'], 0):>19}")
|
|
# skranken: høyest utnyttelse, deretter raskest voksende restanse relativt til kapasiteten
|
|
b = max(rader, key=lambda a: (round(a["ups"], 2), a["vekst"] / a["Z"]))
|
|
print()
|
|
if b["ups"] <= UPS_HI and b["vekst"] <= 0:
|
|
print(f"Ingen ledd binder: høyeste utnyttelse er {tall(b['ups'])} ({b['navn']}), og ingen restanse vokser.")
|
|
return
|
|
print(f"Skranken som binder: {b['navn']} (utnyttelse {tall(b['ups'])}, saksbehandlingstid "
|
|
f"{tall(b['tid'], 1)} mnd mot mål {tall(b['tid_maal'], 1)}; vekst: {b['vekst_kilde']}).")
|
|
nedstroms = [a["navn"] for a in rader[rader.index(b) + 1:]]
|
|
if nedstroms:
|
|
print(f"Merk: tallene for {', '.join(nedstroms)} viser bare det som slipper gjennom skranken. "
|
|
"Når den løses, øker innstrømningen dit.")
|
|
print("\nTrinn (d)-(e): tiltak etter Theory of Constraints, i denne rekkefølgen\n")
|
|
for linje in tiltak(b):
|
|
print(linje)
|
|
print("\nÅ bremse leddene foran skranken til dens takt er ikke et tiltak: i modellen gir det det dårligste utfallet.")
|
|
print("Kontroller så de rettsstatlige skrankene (trinn f): det er sakstyper, ikke prosentandeler, som flyttes.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
sys.exit("Bruk: python diagnose.py <inndata.json>")
|
|
main(sys.argv[1])
|