scenario.py runs all five simulators of the core from a JSON scenario file, with sweeps, and reproduces 20 of the note's data files (14 byte for byte, 6 column-wise). Extensions live outside the core and must reproduce it exactly in a limit case; paadrag_tidsserie is the worked example. arbeidskopi.py gives the user a copy of the model they can change. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
229 lines
13 KiB
Markdown
229 lines
13 KiB
Markdown
# 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 |
|
||
|
||
### Hva `simuler` får
|
||
|
||
Scenariokjøreren validerer scenariofila og gir `simuler` én kjøring om gangen (ett sveip gir flere kall), som en dict:
|
||
|
||
```python
|
||
{
|
||
"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.
|
||
|
||
### 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 --rask` kjører `grensetilfelle()` i hver utvidelse (hver fil i
|
||
`modell/utvidelser/` som ikke begynner med `_`) og sier «Utvidelser: grensetilfellene gjenskaper kjernen eksakt
|
||
(...)», eller hvilket tilfelle som ikke gjør det.
|
||
- `tests/test_utvidelser.py` går gjennom alle utvidelsene: grensesnittet finnes, og hvert avvik er `0.0`. En ny
|
||
utvidelse er med uten at testen endres.
|
||
- `resultat.json` har 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.regulator` må være `"P"`).
|
||
- Med kjeden (`parametere.CH0.Z` endelig, standard): regnestegene i `simulate_chain`, med kjernens `deriv_chain` og
|
||
pådraget gitt som `Om0`. Tiltakene virker som i `simulate_chain` (`andel_utenom`, `kapasitet`,
|
||
`trommel_buffer_tau`).
|
||
- Uten kjeden (`"CH0": {"Z": "inf"}`): regnestegene i `simulate`. Utvidelsen har en egen løkke for dette i stedet
|
||
for å la `simulate_chain` med uendelig kapasitet gjøre jobben, fordi den bare er eksakt lik `simulate` for regime A
|
||
(for regime B–D er avviket opptil 3,8·10⁻¹⁵, se [modellkart.md](modellkart.md), del (e)).
|
||
- `P.shock` og `P.t_shock` brukes ikke (pådraget følger `punkter`); 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) |
|
||
|
||
Testene viser i tillegg at utvidelsen kjørt gjennom scenariokjøreren med standardverdiene gir notatets
|
||
`kjede_s2.dat` og `p3.dat` byte for byte, og med tiltakene fra scenario 5 notatets `kjede_s5.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.
|
||
|
||
<!-- mal:start -->
|
||
```python
|
||
"""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
|
||
```
|
||
<!-- mal:slutt -->
|
||
|
||
`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
|
||
|
||
1. **Lag en arbeidskopi** du kan endre: `python3 modell/kjor.py arbeidskopi rettsstat-modell`. Resten gjøres i
|
||
kopien.
|
||
2. **Finn stedet i modellen** mekanismen hører til: hvilken ligning, hvilken funksjon i `sim.py`, hvilke parametere
|
||
([modellkart.md](modellkart.md)). Velg simulatoren du bygger på: den enkleste som har det mekanismen trenger.
|
||
3. **Kopier malen** til `modell/utvidelser/<navn>.py`. Sett `NAVN` lik filnavnet, skriv `BESKRIVELSE`, og legg
|
||
utvidelsens parametere i `PARAMETERE` med grensetilfellet som standardverdier.
|
||
4. **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 med `1.0`, legg til `0.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 - c` og
|
||
`a - (b + c)` kan gi forskjellig siste siffer.
|
||
5. **Skriv `grensetilfelle()`**: kjør `simuler` med 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).
|
||
6. **Kontroller**: `python3 modell/kjor.py kontroll --rask` skal si at grensetilfellene gjenskaper kjernen eksakt.
|
||
Kjør testene (`python3 -m pytest tests/test_utvidelser.py`); utvidelsen er med automatisk. Skriv gjerne en egen
|
||
test for det mekanismen skal gjøre utenfor grensetilfellet.
|
||
7. **Bruk den**: skriv en scenariofil med `"modell": "utvidelse:<navn>"` og parametrene under `"utvidelse"`, og kjør
|
||
`python3 modell/kjor.py scenario <fil.json>`. Sveip virker som ellers, også over `utvidelse.<parameter>`.
|
||
8. **Send den tilbake** hvis du vil: se `BIDRA.md` i 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.
|