# 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/.py`, kjøres med en scenariofil (`"modell": "utvidelse:"`) 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 ("" 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/.py` og bytt ut mekanismen. ```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 ``` `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/.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:"` og parametrene under `"utvidelse"`, og kjør `python3 modell/kjor.py scenario `. Sveip virker som ellers, også over `utvidelse.`. 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.