feat(scenario): scenario runner, extensions and working copy
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>
This commit is contained in:
parent
464ad38b49
commit
03151cfb3b
36 changed files with 3580 additions and 5 deletions
208
modell/arbeidskopi.py
Normal file
208
modell/arbeidskopi.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Arbeidskopi: kopier modellen til en mappe brukeren eier og kan endre.
|
||||
|
||||
Bruk:
|
||||
python3 modell/kjor.py arbeidskopi <mappe>
|
||||
python3 modell/arbeidskopi.py <mappe>
|
||||
|
||||
Kopierer modell/, eksempler/, fasit/, tests/ og docs/ (det som finnes), og
|
||||
pyproject.toml og LICENSE når de finnes, og skriver LES-MEG.md i mappa. Nekter
|
||||
hvis mappa finnes og ikke er tom. Kopierer ikke .venv, __pycache__, mellomlagre
|
||||
eller resultater/. Bare standardbiblioteket.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MODELLMAPPE = Path(__file__).resolve().parent
|
||||
ROT = MODELLMAPPE.parent
|
||||
MAPPER = ("modell", "eksempler", "fasit", "tests", "docs")
|
||||
FILER = ("pyproject.toml", "LICENSE")
|
||||
HOPP_OVER = (".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "resultater",
|
||||
"rettsstat-modell", ".DS_Store", "*.pyc", "*.pyo")
|
||||
NODVENDIG = ("kjor.py", "scenario.py", "kontroll.py", "arbeidskopi.py", "requirements.txt",
|
||||
"kjerne/sim.py", "kjerne/diagnose.py", "kjerne/forlop.py", "kjerne/KILDE.json",
|
||||
"utvidelser/__init__.py")
|
||||
DOKUMENTER = (
|
||||
("scenarioformat.md", "scenariofila: alle felt, sveip og utdata"),
|
||||
("utvidelser.md", "hvordan du legger til en mekanisme som utvidelse, med mal"),
|
||||
("modellkart.md", "fra ligning i notatet til funksjon og parameter i koden"),
|
||||
("inndata.md", "inndatafila til diagnose og forlop"),
|
||||
("forbehold.md", "hva modellen kan og ikke kan brukes til"),
|
||||
)
|
||||
BRUK = "Bruk: python3 modell/kjor.py arbeidskopi <mappe>"
|
||||
|
||||
|
||||
class KopiFeil(Exception):
|
||||
"""Feil i bruken eller i kildemappa; gir exit-kode 2."""
|
||||
|
||||
|
||||
def sjekk_kilde() -> None:
|
||||
"""Stopp hvis modell/ ikke er komplett."""
|
||||
mangler = [n for n in NODVENDIG if not (MODELLMAPPE / n).is_file()]
|
||||
if mangler:
|
||||
melding = (f"Modellen i {MODELLMAPPE} er ikke komplett, så arbeidskopien ville ikke virket. Mangler: "
|
||||
+ ", ".join(f"modell/{n}" for n in mangler) + ".")
|
||||
raise KopiFeil(melding)
|
||||
|
||||
|
||||
def sjekk_mal(mal: Path) -> None:
|
||||
"""Mappa må være ny eller tom, og ikke ligge inne i det som kopieres."""
|
||||
if mal.exists() and not mal.is_dir():
|
||||
melding = f"{mal} finnes og er en fil, ikke en mappe."
|
||||
raise KopiFeil(melding)
|
||||
if mal.is_dir() and any(mal.iterdir()):
|
||||
melding = (f"Mappa {mal} finnes og er ikke tom. Velg en ny mappe; arbeidskopi skriver aldri over noe "
|
||||
"som finnes.")
|
||||
raise KopiFeil(melding)
|
||||
for navn in MAPPER:
|
||||
kilde = (ROT / navn).resolve()
|
||||
if mal == kilde or kilde in mal.parents:
|
||||
melding = f"{mal} ligger inne i {kilde}, som skal kopieres. Velg en mappe utenfor."
|
||||
raise KopiFeil(melding)
|
||||
|
||||
|
||||
def kildeversjon() -> str:
|
||||
"""Commit i notatets repo som kjernen er kopiert fra (fra KILDE.json)."""
|
||||
try:
|
||||
data = json.loads((MODELLMAPPE / "kjerne" / "KILDE.json").read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return "ukjent"
|
||||
if not isinstance(data, dict):
|
||||
return "ukjent"
|
||||
commit = str(data.get("commit", "ukjent"))
|
||||
repo = str(data.get("repo", data.get("url", "")))
|
||||
return f"{repo} @ {commit[:7]}" if repo else commit[:7]
|
||||
|
||||
|
||||
def les_meg(mal: Path, docs: list[str]) -> str:
|
||||
"""Innholdet i LES-MEG.md."""
|
||||
kjor = f"python3 {shlex.quote(str(mal / 'modell' / 'kjor.py'))}"
|
||||
eks = mal / "eksempler"
|
||||
|
||||
def q(sti: Path) -> str:
|
||||
return shlex.quote(str(sti))
|
||||
|
||||
linjer = [
|
||||
"# Arbeidskopi av modellen bak «Den dynamiske rettsstaten»",
|
||||
"",
|
||||
(f"Denne mappa er din egen kopi av modellen i pluginen dynamisk-rettsstat. Kjernen i `modell/kjerne/` er "
|
||||
f"en uendret kopi av notatets kode ({kildeversjon()}); sjekksummene står i `modell/kjerne/KILDE.json`."),
|
||||
"",
|
||||
"## Hva du kan endre",
|
||||
"",
|
||||
"Alt i mappa kan endres, også kjernen. Tre veier, fra minst til mest inngripende:",
|
||||
"",
|
||||
"1. Andre forutsetninger: skriv en scenariofil (se `docs/scenarioformat.md`). Ingen kode endres.",
|
||||
("2. En ny mekanisme ved siden av kjernen: legg den i `modell/utvidelser/` (se `docs/utvidelser.md`). "
|
||||
"Regelen er den samme som i notatet: utvidelsen må ha et grensetilfelle som gjenskaper kjernen eksakt."),
|
||||
("3. Endre kjernen selv (`modell/kjerne/sim.py`). Kontrollen under sier da hvilke av notatets resultater "
|
||||
"som flytter seg, og hvor mye."),
|
||||
"",
|
||||
"## Slik kjører du kopien",
|
||||
"",
|
||||
"Kommandoene bruker koden i denne mappa:",
|
||||
"",
|
||||
"```",
|
||||
f"{kjor} scenario {q(eks / 'scenarioer' / 'avviklingstid_koordinert.json')}",
|
||||
f"{kjor} diagnose {q(eks / 'eksempel_domstol.json')}",
|
||||
f"{kjor} forlop {q(eks / 'eksempel_domstol.json')}",
|
||||
"```",
|
||||
"",
|
||||
"Fra pluginen kan du peke på kopien med `--modell`:",
|
||||
"`python3 <plugin>/modell/kjor.py --modell " + q(mal) + " scenario <fil.json>`. Ferdighetene i "
|
||||
"pluginen bruker kopien selv når mappa du jobber i, har en `rettsstat-modell/`.",
|
||||
"",
|
||||
("Scenario, forløp og kontroll trenger numpy og scipy. Mangler de, sier kommandoen fra; "
|
||||
f"`{kjor} miljo --opprett` installerer dem i en egen mappe."),
|
||||
"",
|
||||
"## Slik kontrollerer du en endring",
|
||||
"",
|
||||
"```",
|
||||
f"{kjor} kontroll # alt, om lag 1-2 minutter",
|
||||
f"{kjor} kontroll --rask # uten den fulle kjøringen av sim.py",
|
||||
"```",
|
||||
"",
|
||||
"Kontrollen sammenligner med notatets resultater i `fasit/` og ender med ett av tre nivåer:",
|
||||
"",
|
||||
"- «bit for bit som notatet»: endringen flytter ingen av notatets resultater.",
|
||||
("- «innenfor toleranse»: tallene avviker med høyst én enhet i siste viste siffer, som med andre versjoner "
|
||||
"av numpy og scipy."),
|
||||
("- «endret: ...»: noe har flyttet seg. I en arbeidskopi er det en opplysning, ikke en feil. Detaljene "
|
||||
"sier hvilke datafiler og utskrifter som er endret, og største avvik per kolonne."),
|
||||
"",
|
||||
"Kontrollen kjører også `grensetilfelle()` i hver utvidelse; hvert avvik skal være eksakt 0.",
|
||||
"Testene kjøres med `python3 -m pytest` i mappa (krever pytest; de trege med `-m treg`).",
|
||||
"",
|
||||
"## Dokumentasjon",
|
||||
"",
|
||||
]
|
||||
if docs:
|
||||
linjer.extend(f"- `docs/{navn}`: {hva}" for navn, hva in DOKUMENTER if navn in docs)
|
||||
linjer.extend(f"- `docs/{navn}`" for navn in docs if navn not in {n for n, _ in DOKUMENTER})
|
||||
else:
|
||||
linjer.append("- (docs/ fantes ikke da kopien ble laget)")
|
||||
linjer += [
|
||||
"",
|
||||
"## Sende en forbedring tilbake",
|
||||
"",
|
||||
("Se `BIDRA.md` i pluginen. Kort: lag en patch av endringene (for eksempel med `git diff` hvis du har lagt "
|
||||
"mappa i git) og send den på e-post med en forklaring og utskriften fra kontrollen."),
|
||||
"",
|
||||
"Tekst og kode er under lisensene i pluginen (MIT for kode og data, se `LICENSE`).",
|
||||
"",
|
||||
]
|
||||
return "\n".join(linjer)
|
||||
|
||||
|
||||
def kopier(mal: Path) -> int:
|
||||
"""Lag arbeidskopien. Returnerer antall filer."""
|
||||
sjekk_kilde()
|
||||
sjekk_mal(mal)
|
||||
mal.mkdir(parents=True, exist_ok=True)
|
||||
ignorer = shutil.ignore_patterns(*HOPP_OVER)
|
||||
for navn in MAPPER:
|
||||
kilde = ROT / navn
|
||||
if kilde.is_dir():
|
||||
shutil.copytree(kilde, mal / navn, ignore=ignorer)
|
||||
for navn in FILER:
|
||||
if (ROT / navn).is_file():
|
||||
shutil.copy2(ROT / navn, mal / navn)
|
||||
docs = sorted(p.name for p in (mal / "docs").glob("*.md")) if (mal / "docs").is_dir() else []
|
||||
(mal / "LES-MEG.md").write_text(les_meg(mal, docs), encoding="utf-8")
|
||||
return sum(1 for p in mal.rglob("*") if p.is_file())
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
"""Kommandoen arbeidskopi. Exit-kode 0 ok, 2 feil i bruken."""
|
||||
if argv and argv[0] in {"-h", "--hjelp", "--help"}:
|
||||
sys.stdout.write(BRUK + "\n")
|
||||
return 0
|
||||
if len(argv) != 1 or not argv[0].strip():
|
||||
sys.stderr.write(BRUK + "\n")
|
||||
return 2
|
||||
mal = Path(argv[0]).expanduser().resolve()
|
||||
try:
|
||||
antall = kopier(mal)
|
||||
except KopiFeil as e:
|
||||
sys.stderr.write(f"{e}\n")
|
||||
return 2
|
||||
except OSError as e:
|
||||
sys.stderr.write(f"Kunne ikke lage arbeidskopien i {mal}: {e}\n")
|
||||
return 2
|
||||
kjor = shlex.quote(str(mal / "modell" / "kjor.py"))
|
||||
sys.stdout.write(f"Arbeidskopien er laget i {mal} ({antall} filer).\n"
|
||||
f"Les {mal / 'LES-MEG.md'} først. Kjør den med:\n"
|
||||
f" python3 {kjor} scenario <fil.json>\n"
|
||||
f"og kontroller endringer med:\n"
|
||||
f" python3 {kjor} kontroll\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
1508
modell/scenario.py
Normal file
1508
modell/scenario.py
Normal file
File diff suppressed because it is too large
Load diff
8
modell/utvidelser/__init__.py
Normal file
8
modell/utvidelser/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Utvidelser: mekanismer utenfor kjernen.
|
||||
|
||||
Hver modul her har NAVN, BESKRIVELSE, PARAMETERE, simuler(scenario) og
|
||||
grensetilfelle(). Regelen er den samme som i notatet: en utvidelse skal ha et
|
||||
grensetilfelle som gjenskaper kjernen eksakt, og en test som viser det. Se
|
||||
docs/utvidelser.md.
|
||||
"""
|
||||
258
modell/utvidelser/paadrag_tidsserie.py
Normal file
258
modell/utvidelser/paadrag_tidsserie.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Utvidelse: det strukturelle pådraget følger en tidsserie.
|
||||
|
||||
I kjernen (sim.py, Omega) er pådraget Omega0 før måned t_shock og Omega0 * shock
|
||||
etterpå: ett varig sjokk. Her følger pådraget Omega0 * f(t), der f er en
|
||||
tidsserie brukeren oppgir, som trappetrinn eller stykkevis lineær. Da kan en
|
||||
forbigående bølge, et sjokk i flere trinn eller en gradvis endring kjøres.
|
||||
|
||||
Styringen er P-regelen fra kjernen. Med kjeden (parametere.CH0.Z endelig) er
|
||||
regnestegene de samme som i simulate_chain, med tiltakene i kjeden (andel
|
||||
utenom, kapasitet, trommel-buffer-tau). Uten kjeden (CH0.Z = "inf") er de de
|
||||
samme som i simulate.
|
||||
|
||||
Grensetilfelle: et trappetrinn lik notatets sjokk (faktor 1,3 fra måned 12)
|
||||
gjenskaper simulate_chain("P", 3.0, CH0) og, med CH0.Z = inf, simulate("P", 3.0)
|
||||
bit for bit. Regnestegene under er kopier av løkkene i sim.py; endres de der,
|
||||
må de endres likt her, og grensetilfellet viser om det er glemt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
Arr = npt.NDArray[np.float64]
|
||||
|
||||
NAVN = "paadrag_tidsserie"
|
||||
BESKRIVELSE = ("det strukturelle pådraget Omega følger en tidsserie (trappetrinn eller stykkevis "
|
||||
"lineær) i stedet for ett varig sjokk; P-regel, med eller uten straffesakskjeden")
|
||||
PARAMETERE: dict[str, object] = {
|
||||
"form": "trapp", # "trapp" eller "lineaer"
|
||||
"punkter": [[12.0, 1.3]], # [måned, faktor på Omega0]; før første punkt er faktoren 1
|
||||
}
|
||||
BRUKER = ("P", "CH0") # kjernens dicts som kan overstyres under parametere
|
||||
FORMER = ("trapp", "lineaer")
|
||||
MAKS_PUNKTER = 1000
|
||||
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
|
||||
|
||||
|
||||
def _sim() -> ModuleType:
|
||||
"""Kjernen (sim.py) fra modellmappa denne utvidelsen ligger i."""
|
||||
sim = sys.modules.get("sim")
|
||||
if sim is not None and Path(str(getattr(sim, "__file__", ""))).resolve().parent == KJERNE:
|
||||
return sim
|
||||
sys.modules.pop("sim", None)
|
||||
if str(KJERNE) in sys.path:
|
||||
sys.path.remove(str(KJERNE))
|
||||
sys.path.insert(0, str(KJERNE))
|
||||
return importlib.import_module("sim")
|
||||
|
||||
|
||||
def _punkter(raa: object) -> list[tuple[float, float]]:
|
||||
"""Valider punktene: [[måned, faktor], ...] med stigende måneder."""
|
||||
hvor = "utvidelse.punkter"
|
||||
if not isinstance(raa, list) or not 1 <= len(raa) <= MAKS_PUNKTER:
|
||||
melding = f"{hvor}: må være en liste med 1 til {MAKS_PUNKTER} punkter [måned, faktor]."
|
||||
raise ValueError(melding)
|
||||
ut: list[tuple[float, float]] = []
|
||||
for nr, p in enumerate(raa):
|
||||
ok = (isinstance(p, list) and len(p) == 2 # noqa: PLR2004 - et punkt er et par
|
||||
and all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in p))
|
||||
if not ok:
|
||||
melding = f"{hvor}[{nr}]: må være et par [måned, faktor] med tall, ikke {p!r}."
|
||||
raise ValueError(melding)
|
||||
tid, faktor = float(p[0]), float(p[1])
|
||||
if not (math.isfinite(tid) and math.isfinite(faktor)) or tid < 0 or faktor < 0:
|
||||
melding = f"{hvor}[{nr}]: måned og faktor må være tall fra og med 0."
|
||||
raise ValueError(melding)
|
||||
if ut and tid <= ut[-1][0]:
|
||||
melding = f"{hvor}[{nr}]: månedene må stige (måned {tid:g} kommer etter {ut[-1][0]:g})."
|
||||
raise ValueError(melding)
|
||||
ut.append((tid, faktor))
|
||||
return ut
|
||||
|
||||
|
||||
def faktor(tid: float, form: str, punkter: list[tuple[float, float]]) -> float:
|
||||
"""f(t): 1 før første punkt; trapp holder verdien til neste punkt, lineaer interpolerer."""
|
||||
if tid < punkter[0][0]:
|
||||
return 1.0
|
||||
if form == "trapp":
|
||||
verdi = 1.0
|
||||
for t0, f0 in punkter:
|
||||
if tid >= t0:
|
||||
verdi = f0
|
||||
else:
|
||||
break
|
||||
return verdi
|
||||
for (t0, f0), (t1, f1) in zip(punkter, punkter[1:]):
|
||||
if t0 <= tid < t1:
|
||||
return f0 + (f1 - f0) * (tid - t0) / (t1 - t0)
|
||||
return punkter[-1][1]
|
||||
|
||||
|
||||
def sjekk(scenario: dict[str, object]) -> None:
|
||||
"""Kalles av scenariokjøreren før kjøringen; ValueError gir en norsk feilmelding."""
|
||||
sim = _sim()
|
||||
styring = cast("dict[str, Any]", scenario["styring"])
|
||||
if styring["regulator"] != "P":
|
||||
melding = 'styring.regulator: utvidelsen paadrag_tidsserie bruker bare "P".'
|
||||
raise ValueError(melding)
|
||||
par = cast("dict[str, dict[str, Any]]", scenario["parametere"])
|
||||
for nokkel in ("shock", "t_shock"):
|
||||
if par["P"][nokkel] != sim.P[nokkel]:
|
||||
melding = (f"parametere.P.{nokkel}: brukes ikke av paadrag_tidsserie. Pådraget følger "
|
||||
"utvidelse.punkter.")
|
||||
raise ValueError(melding)
|
||||
if math.isinf(par["CH0"]["Z"]) and scenario.get("tiltak"):
|
||||
melding = "tiltak: virker bare i kjeden, og parametere.CH0.Z er inf (ingen kjede)."
|
||||
raise ValueError(melding)
|
||||
u = cast("dict[str, Any]", scenario["utvidelse"])
|
||||
if u["form"] not in FORMER:
|
||||
melding = f'utvidelse.form: må være "trapp" eller "lineaer", ikke {u["form"]!r}.'
|
||||
raise ValueError(melding)
|
||||
_punkter(u["punkter"])
|
||||
|
||||
|
||||
def _deriv(tilstand: tuple[float, float, float, float], om: float,
|
||||
p: dict[str, Any]) -> tuple[float, float, float]:
|
||||
"""Som deriv i sim.py, med pådraget om i stedet for Omega(t, p). Tilstand: K, U, L, T."""
|
||||
sim = _sim()
|
||||
krim, trygg, tillit, innsats = tilstand
|
||||
d_krim = om - sim.removal(krim, tillit, innsats, p) - p["delta"] * krim
|
||||
u_eq = np.clip(p["U0"] - p["psiU"] * (krim - 1.0), 0.0, 1.0)
|
||||
d_trygg = p["kU"] * (u_eq - trygg)
|
||||
over = max(0.0, innsats - p["Tprop"] * krim)
|
||||
d_tillit = (p["kL"] * (p["Lam0"] + p["mu1"] * (trygg - p["U0"]) - tillit)
|
||||
- p["mu2"] * tillit * over ** 2)
|
||||
return d_krim, d_trygg, d_tillit
|
||||
|
||||
|
||||
def _uten_kjede(t: Arr, om: Arr, styring: tuple[float, float], p: dict[str, Any]) -> dict[str, object]:
|
||||
"""Løkka i simulate (P-regelen), med pådraget fra tidsserien. Styring: (tau, kp)."""
|
||||
tau, kp = styring
|
||||
sim = _sim()
|
||||
dt = sim.DT
|
||||
n = len(t)
|
||||
krim = np.ones(n)
|
||||
trygg = np.full(n, p["U0"])
|
||||
tillit = np.full(n, p["Lam0"])
|
||||
innsats = np.full(n, p["T0"])
|
||||
lag = round(tau / dt)
|
||||
for k in range(n - 1):
|
||||
k_obs = krim[k - lag] if k - lag >= 0 else 1.0
|
||||
innsats[k] = np.clip(p["T0"] + kp * (k_obs - p["Kstar"]), 0.0, p["Tmax"])
|
||||
d_krim, d_trygg, d_tillit = _deriv((krim[k], trygg[k], tillit[k], innsats[k]), om[k], p)
|
||||
krim[k + 1] = krim[k] + dt * d_krim
|
||||
trygg[k + 1] = trygg[k] + dt * d_trygg
|
||||
tillit[k + 1] = np.clip(tillit[k] + dt * d_tillit, 1e-4, 1 - 1e-4)
|
||||
innsats[-1] = innsats[-2]
|
||||
return {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats}
|
||||
|
||||
|
||||
def _med_kjede(t: Arr, om: Arr, styring: tuple[float, float], p: dict[str, Any],
|
||||
ch: dict[str, Any]) -> dict[str, object]:
|
||||
"""Løkka i simulate_chain (P-regelen og trommel-buffer-tau), med pådraget fra tidsserien."""
|
||||
tau, kp = styring
|
||||
sim = _sim()
|
||||
dt = sim.DT
|
||||
n = len(t)
|
||||
krim = np.ones(n)
|
||||
trygg = np.full(n, p["U0"])
|
||||
tillit = np.full(n, p["Lam0"])
|
||||
innsats = np.full(n, p["T0"])
|
||||
ko, tid_d, inn_d, ut_d = np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)
|
||||
q0, tau_ref = sim.init_chain(ch, p)
|
||||
ko[0] = q0
|
||||
lag = round(tau / dt)
|
||||
for k in range(n - 1):
|
||||
k_obs = krim[k - lag] if k - lag >= 0 else 1.0
|
||||
innsats[k] = np.clip(p["T0"] + kp * (k_obs - p["Kstar"]), 0.0, p["Tmax"])
|
||||
if ch["dbr"] is not None:
|
||||
z, xi = sim.chain_at(ch, t[k])
|
||||
t_tak = ch["dbr"] * z / (xi * p["gamma"] * tillit[k] ** p["eta"] * k_obs)
|
||||
innsats[k] = min(innsats[k], max(t_tak, 0.0))
|
||||
d_krim, d_trygg, d_tillit, d_ko, tid_d[k], inn_d[k], ut_d[k] = sim.deriv_chain(
|
||||
krim[k], trygg[k], tillit[k], innsats[k], ko[k], t[k], p, ch, tau_ref, Om0=om[k])
|
||||
krim[k + 1] = krim[k] + dt * d_krim
|
||||
trygg[k + 1] = trygg[k] + dt * d_trygg
|
||||
tillit[k + 1] = np.clip(tillit[k] + dt * d_tillit, 1e-4, 1 - 1e-4)
|
||||
ko[k + 1] = max(ko[k] + dt * d_ko, 0.0)
|
||||
innsats[-1] = innsats[-2]
|
||||
tid_d[-1] = tid_d[-2]
|
||||
inn_d[-1] = inn_d[-2]
|
||||
ut_d[-1] = ut_d[-2]
|
||||
par = [sim.chain_at(ch, float(x)) for x in t]
|
||||
z_t = np.array([float(a) for a, _ in par])
|
||||
return {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats, "Q": ko, "tauD": tid_d,
|
||||
"JD": inn_d, "RD": ut_d, "Z": z_t, "Z_rel": z_t / ch["Z"],
|
||||
"xi": np.array([float(b) for _, b in par]), "tau_ref": tau_ref}
|
||||
|
||||
|
||||
def simuler(scenario: dict[str, object]) -> dict[str, object]:
|
||||
"""Kjør modellen med pådraget fra tidsserien. Nøkler som i kjernen (t, K, U, L, T, ...)."""
|
||||
sim = _sim()
|
||||
styring = cast("dict[str, Any]", scenario["styring"])
|
||||
par = cast("dict[str, dict[str, Any]]", scenario["parametere"])
|
||||
u = cast("dict[str, Any]", scenario["utvidelse"])
|
||||
p, ch = par["P"], par["CH0"]
|
||||
punkter = _punkter(u["punkter"])
|
||||
slutt = float(cast("float", scenario["slutt"]))
|
||||
n = int(slutt / sim.DT) + 1
|
||||
t = np.arange(n) * sim.DT
|
||||
f = np.array([faktor(x, u["form"], punkter) for x in t])
|
||||
om = np.array([p["Omega0"] * x for x in f])
|
||||
tau_kp = (float(styring["forsinkelse"]), float(styring["reaktivitet"]))
|
||||
ut = _uten_kjede(t, om, tau_kp, p) if math.isinf(ch["Z"]) else _med_kjede(t, om, tau_kp, p, ch)
|
||||
ut["Omega_faktor"] = f
|
||||
return ut
|
||||
|
||||
|
||||
def _scenario(p: dict[str, Any], ch: dict[str, Any], slutt: float) -> dict[str, object]:
|
||||
return {
|
||||
"styring": {"regulator": "P", "forsinkelse": 3.0, "reaktivitet": 3.0},
|
||||
"slutt": slutt,
|
||||
"parametere": {"P": p, "CH0": ch},
|
||||
"tiltak": {},
|
||||
"utvidelse": {"form": "trapp", "punkter": [[float(p["t_shock"]), float(p["shock"])]]},
|
||||
}
|
||||
|
||||
|
||||
def _avvik(egen: dict[str, object], fasit: dict[str, Any], navn: tuple[str, ...]) -> float:
|
||||
"""Største absolutte avvik over tidsseriene (inf hvis lengdene er ulike)."""
|
||||
storst = 0.0
|
||||
for n in navn:
|
||||
a, b = np.asarray(egen[n]), np.asarray(fasit[n])
|
||||
if a.shape != b.shape:
|
||||
return math.inf
|
||||
storst = max(storst, float(np.abs(a - b).max()))
|
||||
return storst
|
||||
|
||||
|
||||
def grensetilfelle() -> list[tuple[str, float]]:
|
||||
"""Trappetrinn lik notatets sjokk gjenskaper kjernen; avvikene skal være eksakt 0."""
|
||||
sim = _sim()
|
||||
slutt = float(sim.TEND)
|
||||
egen = simuler(_scenario(dict(sim.P), dict(sim.CH0), slutt))
|
||||
fasit = sim.simulate_chain("P", 3.0, sim.CH0)
|
||||
d_kjede = max(_avvik(egen, fasit, ("t", "K", "U", "L", "T", "Q", "tauD", "JD", "RD")),
|
||||
abs(float(cast("float", egen["tau_ref"])) - float(fasit["tau_ref"])))
|
||||
egen0 = simuler(_scenario(dict(sim.P), dict(sim.CH0, Z=math.inf), slutt))
|
||||
t, krim, trygg, tillit, innsats = sim.simulate("P", 3.0)
|
||||
d_uten = _avvik(egen0, {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats},
|
||||
("t", "K", "U", "L", "T"))
|
||||
return [
|
||||
('paadrag_tidsserie: trapp lik notatets sjokk, med kjeden = simulate_chain("P", 3, CH0)',
|
||||
d_kjede),
|
||||
('paadrag_tidsserie: trapp lik notatets sjokk, CH0.Z = inf = simulate("P", 3)', d_uten),
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue