Scenario runner: stops when overridden base parameters leave the start out of balance (new field startlikevekt: krev, kalibrer, ubalanse); stops on non-finite results; says that models with a court queue drift slowly even without a shock and prints the drift before the shock; queue in months of inflow; never deletes files it did not write; stricter limits. Check: anchored on the note's own checksums for core and reference files, runs the scenario files too, exact comparison for the diagnosis, honest wording when parts were skipped or an extension is broken. Launcher: unknown field names in input are rejected with a suggestion; unwritable folders give a message, not a traceback. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
14 KiB
Utvidelser: å legge til en mekanisme
En utvidelse er en mekanisme modellen ikke har, lagt ved siden av kjernen i stedet for inn i den. Den ligger i
modell/utvidelser/<navn>.py, kjøres med en scenariofil ("modell": "utvidelse:<navn>") og kontrolleres av
kontroll. Kjernen (modell/kjerne/) forblir notatets kode, så det er alltid klart hva som er notatets modell og hva
som er lagt til.
Regelen
Samme regel som i notatets kode (sim.py, Del 3: «Alle utvidelser har grensetilfeller som gjenskaper Del 1 og 2
eksakt»): en utvidelse skal ha et grensetilfelle som gjenskaper kjernen eksakt, og en test som viser det. Grensetilfellet er en verdi av utvidelsens parametere der mekanismen forsvinner,
for eksempel en faktor lik 1, et ledd lik 0 eller en kapasitet lik uendelig. Med den verdien skal utvidelsen gi
nøyaktig de samme tallene som simulatoren i kjernen den bygger på: største absolutte avvik over alle tidsserier skal
være 0.0, ikke bare lite.
Hvorfor eksakt: da vet både du og leseren at alt utvidelsen viser utover kjernen, kommer fra mekanismen, ikke fra en annen tallbehandling, en annen tidsstegning eller en feil i kopien av løkka. Avvik på 10⁻¹⁵ betyr at regnestegene er gjort i en annen rekkefølge, og da er det ikke lenger kjernens modell.
Grensesnittet
| Navn | Type | Krav |
|---|---|---|
NAVN |
tekst | lik filnavnet uten .py |
BESKRIVELSE |
tekst | én setning; står i sammendraget etter modellnavnet |
PARAMETERE |
dict | utvidelsens parametere med standardverdier. Standardverdiene bør være grensetilfellet |
simuler(scenario) -> dict |
funksjon | kjører modellen; se under |
grensetilfelle() -> list[tuple[str, float]] |
funksjon | minst ett tilfelle: (beskrivelse, største absolutte avvik mot kjernen). Hvert avvik skal være 0.0 |
BRUKER |
tuple, valgfri | hvilke av kjernens dicts ("P", "CH0", "CH2", "ENDO", "SOC") som kan overstyres under parametere. Uten den: alle fem |
sjekk(scenario) -> None |
funksjon, valgfri | kalles før kjøringen; en ValueError("felt: hva som er galt") gir exit-kode 2 med meldingen din |
GJENSKAPER_NOTATET |
tuple, valgfri | navn på scenariofiler i eksempler/scenarioer/notatet/ (uten .json) som utvidelsen med standardverdiene skal gjenskape byte for byte når "modell" byttes til "utvidelse:<navn>". Testene kjører dem gjennom kjøreren og sammenligner med fasit/data |
Hva simuler får
Scenariokjøreren validerer scenariofila og gir simuler én kjøring om gangen (ett sveip gir flere kall), som en dict:
{
"navn": "T_clear=24", # kjøringens navn ("<navn>" uten sveip)
"styring": {"regulator": "P", "forsinkelse": 3.0, "reaktivitet": 3.0},
"slutt": 120.0, # horisont i måneder
"tiltak": {...}, # tiltakene slik de står i fila (oppløst, med standardverdier)
"parametere": { # kjernens dicts, fullt utfylt med overstyringene
"P": {...}, # som sim.P, med det fila endrer
"CH0": {...}, # som sim.CH0, med Zfun, xifun og dbr satt fra tiltak
... # bare dictene i BRUKER
},
"utvidelse": {...}, # PARAMETERE med verdiene fra fila
}
Dictene kan sendes rett til kjernens funksjoner (sim.deriv_chain(..., p, ch, ...)). Typene i "utvidelse" er
sjekket mot standardverdiene: tall (også "inf"), heltall, true/false, tekst eller liste. Innholdet i en liste
sjekker du selv i sjekk. Feltet sjokk i scenariofila er ikke tillatt for utvidelser: pådraget er utvidelsens sak.
Før simuler kalles, sjekker kjøreren at startpunktet er en likevekt for dictene i BRUKER, på samme måte som for
kjernens modeller (startlikevekt i scenarioformat.md): P for grunnmodellen, og CH0 eller
CH2 for domstolen når kapasiteten er endelig. Med "startlikevekt": "kalibrer" får simuler de kalibrerte
verdiene i parametere.
Hva simuler returnerer
En dict med tidsserien "t" (numpy-array) og andre tidsserier med samme lengde, med kjernens navn der det gir mening
(K, U, L, T, Q, tauD, JD, RD, Z, Z_rel, xi, …). Kjøreren behandler dem som en kjøring av
kjernen: nøkkeltallene velges etter hvilke tidsserier som finnes, alle kan skrives til CSV, og utdata.kolonner kan
velge blant dem. Tall (ikke arrays) blir «skalarer» i resultat.json; en liste med tekst under "log" blir
vedtakslinjer i sammendraget.
Hvordan utvidelser kontrolleres
python3 modell/kjor.py kontroll --raskkjørergrensetilfelle()i hver utvidelse (hver fil imodell/utvidelser/som ikke begynner med_) og sier «Utvidelser: grensetilfellene gjenskaper kjernen eksakt (...)», eller hvilket tilfelle som ikke gjør det.tests/test_utvidelser.pygår gjennom alle utvidelsene: grensesnittet finnes, og hvert avvik er0.0. En ny utvidelse er med uten at testen endres.- Avviket
grensetilfelle()rapporterer, er utvidelsens egen beregning; en utvidelse som bare returnerer0.0, ville gått gjennom. Derfor bør utvidelsen oppgiGJENSKAPER_NOTATET: da kjører testen de scenariofilene gjennom scenariokjøreren med utvidelsen og sammenligner datafilene byte for byte medfasit/data, uavhengig avgrensetilfelle(). Mangler den, sier testen fra (hoppet over, med grunn). resultat.jsonhar sha256 for utvidelsens fil, så det er sporbart hvilken versjon en kjøring brukte.
Gjennomført eksempel: paadrag_tidsserie
I kjernen er det strukturelle pådraget Ω₀ før måned 12 og Ω₀ · 1,3 etterpå (Omega i sim.py): ett varig sjokk.
Utvidelsen lar pådraget følge Ω₀ · f(t), der f er en tidsserie du oppgir. Da kan en forbigående bølge, et sjokk i
flere trinn eller en gradvis endring kjøres.
| Parameter | Standard | Hva |
|---|---|---|
form |
"trapp" |
"trapp": f holder verdien fra hvert punkt til neste; "lineaer": rett linje mellom punktene |
punkter |
[[12.0, 1.3]] |
[måned, faktor], stigende måneder. Før første punkt er f = 1, etter siste er f lik siste verdi |
- Styring: P-regelen (
styring.regulatormå være"P"). - Med kjeden (
parametere.CH0.Zendelig, standard): regnestegene isimulate_chain, med kjernensderiv_chainog pådraget gitt somOm0. Tiltakene virker som isimulate_chain(andel_utenom,kapasitet,trommel_buffer_tau). - Uten kjeden (
"CH0": {"Z": "inf"}): regnestegene isimulate. Utvidelsen har en egen løkke for dette i stedet for å lasimulate_chainmed uendelig kapasitet gjøre jobben, fordi den bare er eksakt liksimulatefor regime A (for regime B–D er avviket opptil 3,8·10⁻¹⁵, se modellkart.md, del (e)). P.shockogP.t_shockbrukes ikke (pådraget følgerpunkter); endres de, er det en feil.- Tidsserien
Omega_faktor(f(t)) skrives med i CSV-fila.
Grensetilfellene, som grensetilfelle() returnerer:
| Tilfelle | Kjernen | Avvik |
|---|---|---|
trapp med ett punkt [12, 1.3], notatets CH0 |
simulate_chain("P", 3.0, CH0) |
0.0 (t, K, U, L, T, Q, tauD, JD, RD og tau_ref) |
samme trapp, CH0.Z = inf |
simulate("P", 3.0) |
0.0 (t, K, U, L, T) |
Utvidelsen oppgir GJENSKAPER_NOTATET = ("kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5"): kjørt gjennom
scenariokjøreren med standardverdiene (og tiltakene fra scenario 3–5) gir den notatets datafiler byte for byte. Testene
viser i tillegg at den med CH0.Z = inf gir p3.dat.
Eksempel: eksempler/scenarioer/utvidelse_bolge.json kjører en bølge (40 % høyere pådrag fra måned 18 til 36, med
lineær stigning og fall) med og uten flaskehals i domstolen.
Mal
Malen under er en hel, kjørbar utvidelse: politiets effektivitet (gamma i fjerningsleddet) endres med en faktor
fra en gitt måned. Med faktor 1 forsvinner mekanismen, og utvidelsen gjenskaper simulate("P", ...) eksakt. Kopier
den til modell/utvidelser/<ditt_navn>.py og bytt ut mekanismen.
"""Utvidelse: politiets effektivitet endres med en faktor fra en gitt måned.
Bygger på simulate i sim.py (grunnmodellen uten kjede). Grensetilfelle: faktor = 1
gjenskaper simulate("P", tau) eksakt.
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
from typing import Any
import numpy as np
NAVN = "mal_effektivitet" # samme som filnavnet uten .py
BESKRIVELSE = "politiets effektivitet ganges med en faktor fra en gitt måned (mal)"
PARAMETERE: dict[str, object] = { # standardverdiene er grensetilfellet
"faktor": 1.0, # effektiviteten ganges med dette ...
"fra": 24.0, # ... fra denne måneden
}
BRUKER = ("P",) # kjernens dicts som kan overstyres under "parametere"
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
def _sim() -> Any:
"""Kjernen fra samme modellmappe som utvidelsen."""
if str(KJERNE) not in sys.path:
sys.path.insert(0, str(KJERNE))
return importlib.import_module("sim")
def sjekk(scenario: dict[str, Any]) -> None:
"""Valgfri: stopp med en norsk melding før kjøringen."""
if scenario["styring"]["regulator"] != "P":
raise ValueError('styring.regulator: denne utvidelsen bruker bare "P".')
if scenario["utvidelse"]["faktor"] < 0:
raise ValueError("utvidelse.faktor: må være minst 0.")
def simuler(scenario: dict[str, Any]) -> dict[str, Any]:
"""Løkka fra simulate i sim.py, med mekanismen lagt inn i fjerningsleddet."""
sim = _sim()
p = scenario["parametere"]["P"]
tau = scenario["styring"]["forsinkelse"]
kp = scenario["styring"]["reaktivitet"]
u = scenario["utvidelse"]
dt = sim.DT
n = int(scenario["slutt"] / 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"])
lag = int(round(tau / dt))
for k in range(n - 1):
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"])
# Mekanismen: med faktor 1.0 er F * 1.0 nøyaktig F, så grensetilfellet blir eksakt.
f = u["faktor"] if t[k] >= u["fra"] else 1.0
F = sim.removal(K[k], L[k], T[k], p) * f
# Resten er deriv() i sim.py, med F i stedet for removal(...). Samme rekkefølge på regnestegene.
dK = sim.Omega(t[k], p) - F - p["delta"] * K[k]
Ueq = np.clip(p["U0"] - p["psiU"] * (K[k] - 1.0), 0.0, 1.0)
dU = p["kU"] * (Ueq - U[k])
over = max(0.0, T[k] - p["Tprop"] * K[k])
dL = p["kL"] * (p["Lam0"] + p["mu1"] * (U[k] - p["U0"]) - L[k]) - p["mu2"] * L[k] * over ** 2
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)
T[-1] = T[-2]
return {"t": t, "K": K, "U": U, "L": L, "T": T}
def grensetilfelle() -> list[tuple[str, float]]:
"""Faktor 1 gjenskaper simulate i kjernen; avvikene skal være eksakt 0.0."""
sim = _sim()
ut = []
for tau in (3.0, 11.0):
scenario = {"styring": {"regulator": "P", "forsinkelse": tau, "reaktivitet": 3.0},
"slutt": float(sim.TEND), "parametere": {"P": dict(sim.P)},
"utvidelse": {"faktor": 1.0, "fra": 24.0}}
egen = simuler(scenario)
kjerne = dict(zip(("t", "K", "U", "L", "T"), sim.simulate("P", tau)))
avvik = max(float(np.abs(egen[n] - kjerne[n]).max()) for n in kjerne)
ut.append((f'mal_effektivitet: faktor 1 = simulate("P", {tau:g})', avvik))
return ut
tests/test_utvidelser.py legger malen inn i en kopi av modellen og kontrollerer at grensetilfellene er 0.0 og at
den kan kjøres med en scenariofil, så malen virker slik den står her.
Trinn for trinn
- Lag en arbeidskopi du kan endre:
python3 modell/kjor.py arbeidskopi rettsstat-modell. Resten gjøres i kopien. - Finn stedet i modellen mekanismen hører til: hvilken ligning, hvilken funksjon i
sim.py, hvilke parametere (modellkart.md). Velg simulatoren du bygger på: den enkleste som har det mekanismen trenger. - Kopier malen til
modell/utvidelser/<navn>.py. SettNAVNlik filnavnet, skrivBESKRIVELSE, og legg utvidelsens parametere iPARAMETEREmed grensetilfellet som standardverdier. - Kopier løkka fra simulatoren du bygger på inn i
simuler, uendret, og legg mekanismen inn slik at den forsvinner i grensetilfellet. Det som gjør avviket eksakt 0: gang med1.0, legg til0.0, eller velg en gren (if) som gjør nøyaktig det kjernen gjør. Bruk kjernens funksjoner (removal,deriv_chain,court,chain_at, …) der du kan, i stedet for å skrive dem av. Endre ikke rekkefølgen på regnestegene:a - b - coga - (b + c)kan gi forskjellig siste siffer. - Skriv
grensetilfelle(): kjørsimulermed grenseverdiene og kjernen med de samme forutsetningene, og returner største absolutte avvik over alle tidsseriene. Ta gjerne med flere tilfeller (andre forsinkelser, med og uten kjede). - Kontroller:
python3 modell/kjor.py kontroll --raskskal si at grensetilfellene gjenskaper kjernen eksakt. Kjør testene (python3 -m pytest tests/test_utvidelser.py); utvidelsen er med automatisk. OppgiGJENSKAPER_NOTATEThvis standardverdiene gjenskaper en av notatets scenariofiler, så sjekkes grensetilfellet også uavhengig av din egengrensetilfelle(). Skriv gjerne en egen test for det mekanismen skal gjøre utenfor grensetilfellet. - Bruk den: skriv en scenariofil med
"modell": "utvidelse:<navn>"og parametrene under"utvidelse", og kjørpython3 modell/kjor.py scenario <fil.json>. Sveip virker som ellers, også overutvidelse.<parameter>. - Send den tilbake hvis du vil: se
BIDRA.mdi pluginen.
Vil du heller endre kjernen selv, kan du det i arbeidskopien. Da sier kontroll hvilke av notatets resultater som
flytter seg, og hvor mye. En utvidelse er likevel å foretrekke når mekanismen kan stå ved siden av: notatets
resultater ligger da fast, og grensetilfellet viser at de gjør det.