"""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 ") main(sys.argv[1])