dynamisk-rettsstat/tests/test_scenario.py
Kjell Tore Guttormsen 776325c680
fix: findings from the independent review
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>
2026-11-11 15:27:14 +01:00

572 lines
30 KiB
Python

# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""scenario.py: gjenskaping av notatets datafiler, validering, sveip, utdata og determinisme."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import pytest
import scenario
from conftest import EKSEMPLER, FASIT, kjor
if TYPE_CHECKING:
from pathlib import Path
NOTATET = EKSEMPLER / "scenarioer" / "notatet"
NYE = EKSEMPLER / "scenarioer"
EKSAKT_RASKE = ("p3", "p6", "p11", "p11d", "kjede_s1", "kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5",
"endo_e5", "endo_des", "endo_desp", "endo_ko")
# Kolonnene i notatets fil: (scenariofil, kjøring, kolonne i kjøringens fil).
KOLONNEVIS: dict[str, dict[str, tuple[str, str, str]]] = {
"sat_B": {"t": ("sat_B", "ksat=inf", "t"), "K": ("sat_B", "ksat=inf", "K"),
"Ks3": ("sat_B", "ksat=3", "K"), "Ks15": ("sat_B", "ksat=1.5", "K")},
"sat_C": {"t": ("sat_C", "ksat=inf", "t"), "K": ("sat_C", "ksat=inf", "K"),
"Ks3": ("sat_C", "ksat=3", "K"), "Ks15": ("sat_C", "ksat=1.5", "K")},
"vippe": {"t": ("vippe", "regulator=fast_forsinkelse=3", "t"),
"fast": ("vippe", "regulator=fast_forsinkelse=3", "K"),
"p3": ("vippe", "regulator=P_forsinkelse=3", "K"), "p6": ("vippe", "regulator=P_forsinkelse=6", "K"),
"p3cap": ("vippe_tak", "forsinkelse=3", "K"), "p6cap": ("vippe_tak", "forsinkelse=6", "K")},
}
for _s in ("prio_s2", "prio_s3", "prio_s5"):
KOLONNEVIS[_s] = {
**{c: (_s, "prio=true", c) for c in ("t", "K", "KA", "KB", "tauA", "tauB")},
"tauF": (_s, "prio=false", "tauA"), "KF": (_s, "prio=false", "K"), "KAF": (_s, "prio=false", "KA"),
}
def kjor_fil(fil: Path, ut: Path, capsys: pytest.CaptureFixture[str]) -> tuple[int, str, str]:
"""Kjør scenario.main i denne prosessen."""
kode = scenario.main([str(fil), "--ut", str(ut)])
fanget = capsys.readouterr()
return kode, fanget.out, fanget.err
def skriv(tmp_path: Path, data: object, navn: str = "s.json") -> Path:
"""Skriv en scenariofil."""
fil = tmp_path / navn
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return fil
def kolonner(fil: Path) -> dict[str, list[str]]:
"""Kolonnene i en fil i notatets format, som tekst."""
linjer = fil.read_text(encoding="utf-8").splitlines()
hode = linjer[0].split()
rader = [linje.split(" ") for linje in linjer[1:]]
return {h: [r[i] for r in rader] for i, h in enumerate(hode)}
# ---------------- notatets datafiler ----------------
@pytest.mark.parametrize("navn", EKSAKT_RASKE)
def test_gjenskaper_notatets_fil_eksakt(navn: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, _, err = kjor_fil(NOTATET / f"{navn}.json", tmp_path, capsys)
assert kode == 0, err
assert (tmp_path / f"{navn}.dat").read_bytes() == (FASIT / "data" / f"{navn}.dat").read_bytes()
@pytest.mark.treg
def test_gjenskaper_mpc11_eksakt(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, _, err = kjor_fil(NOTATET / "mpc11.json", tmp_path, capsys)
assert kode == 0, err
assert (tmp_path / "mpc11.dat").read_bytes() == (FASIT / "data" / "mpc11.dat").read_bytes()
@pytest.mark.parametrize("navn", sorted(KOLONNEVIS))
def test_gjenskaper_notatets_fil_kolonnevis(navn: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kjort: dict[str, Path] = {}
for scenariofil, _, _ in KOLONNEVIS[navn].values():
if scenariofil not in kjort:
kode, _, err = kjor_fil(NOTATET / f"{scenariofil}.json", tmp_path / scenariofil, capsys)
assert kode == 0, err
kjort[scenariofil] = tmp_path / scenariofil
fasit = kolonner(FASIT / "data" / f"{navn}.dat")
assert set(fasit) == set(KOLONNEVIS[navn])
for kolonne, (scenariofil, kjoring, kilde) in KOLONNEVIS[navn].items():
egen = kolonner(kjort[scenariofil] / f"{scenariofil}__{kjoring}.dat")
assert egen[kilde] == fasit[kolonne], f"{navn}: kolonne {kolonne}"
def test_alle_notatets_simuleringer_har_en_scenariofil() -> None:
simulerte = {f.stem for f in (FASIT / "data").glob("*.dat")} - {"stab", "thumb"}
dekket = set(EKSAKT_RASKE) | {"mpc11"} | set(KOLONNEVIS)
assert simulerte == dekket
# ---------------- utdata ----------------
def test_sammendrag_csv_og_resultatjson(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"navn": "test", "modell": "simulate_chain",
"tiltak": {"kapasitet": {"faktor": 1.34}}})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
mappe = tmp_path / "ut"
assert sorted(p.name for p in mappe.iterdir()) == ["resultat.json", "sammendrag.md", "test.csv"]
assert ut.startswith((mappe / "sammendrag.md").read_text(encoding="utf-8"))
assert f"Resultatene ligger i {mappe}" in ut
assert "ikke kalibrert på norske data" in ut
assert "ikke prognoser" in ut
assert "tiltak.kapasitet.faktor" in ut
assert "Slik leses tallene: 1,00 er nivået" in ut
csv = (mappe / "test.csv").read_text(encoding="utf-8").splitlines()
assert csv[0] == "t,K,U,L,T,Q,tauD,JD,RD,xi,Z,Z_rel"
assert len(csv) == 1 + 121 # én rad per måned, 0-120
assert csv[13].split(",")[0] == "12"
data = json.loads((mappe / "resultat.json").read_text(encoding="utf-8"))
assert set(data["versjoner"]) == {"python", "numpy", "scipy"}
assert set(data["kjerne_sha256"]) == {"sim.py", "diagnose.py", "forlop.py"}
brukt = data["kjoringer"][0]["brukt"]
assert brukt["parametere"]["P"]["ksat"] == "inf"
assert brukt["tiltak"]["kapasitet"] == {"faktor": 1.34, "fra": 24.0, "over": 24.0}
assert data["kjoringer"][0]["nokkeltall"]["Z_slutt"] == pytest.approx(1.34)
def test_standard_mappe_er_resultater_under_arbeidsmappa(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
fil = skriv(tmp_path, {"navn": "Første forsøk", "modell": "simulate"})
monkeypatch.chdir(tmp_path)
assert scenario.main([str(fil)]) == 0
capsys.readouterr()
assert (tmp_path / "resultater" / "Forste-forsok" / "Forste-forsok.csv").is_file()
def test_samme_inndata_gir_samme_bytes(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = NYE / "utvidelse_bolge.json"
for mappe in ("a", "b"):
assert kjor_fil(fil, tmp_path / mappe, capsys)[0] == 0
a = sorted((tmp_path / "a").iterdir())
assert [p.name for p in a] == sorted(p.name for p in (tmp_path / "b").iterdir())
for p in a:
assert p.read_bytes() == (tmp_path / "b" / p.name).read_bytes(), p.name
def test_ny_kjoring_i_samme_mappe_rydder_bare_egne_filer(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
sveip = skriv(tmp_path, {"navn": "x", "modell": "simulate",
"sveip": {"parameter": "styring.forsinkelse", "verdier": [3, 6]}})
enkel = skriv(tmp_path, {"navn": "x", "modell": "simulate"}, "enkel.json")
ut = tmp_path / "ut"
assert kjor_fil(sveip, ut, capsys)[0] == 0
(ut / "notat.txt").write_text("min egen fil", encoding="utf-8")
assert kjor_fil(enkel, ut, capsys)[0] == 0
assert sorted(p.name for p in ut.iterdir()) == ["notat.txt", "resultat.json", "sammendrag.md", "x.csv"]
def test_nekter_mappe_med_andre_filer(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate"})
ut = tmp_path / "ut"
ut.mkdir()
(ut / "viktig.txt").write_text("x", encoding="utf-8")
kode, _, err = kjor_fil(fil, ut, capsys)
assert kode == 2
assert "ikke er et tidligere resultat" in err
assert sorted(p.name for p in ut.iterdir()) == ["viktig.txt"]
def test_valgte_kolonner_og_hvert(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"navn": "k", "modell": "simulate_endo", "regel": "koord", "slutt": 36,
"utdata": {"kolonner": ["t", "Z_rel", "xi"], "hvert": 240}})
assert kjor_fil(fil, tmp_path / "ut", capsys)[0] == 0
linjer = (tmp_path / "ut" / "k.csv").read_text(encoding="utf-8").splitlines()
assert linjer[0] == "t,Z_rel,xi"
assert [linje.split(",")[0] for linje in linjer[1:]] == ["0", "12", "24", "36"]
assert linjer[1] == "0,1,1"
# ---------------- sveip ----------------
def test_sveip_over_avviklingstid(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"navn": "ko", "modell": "simulate_endo", "regel": "koord", "slutt": 180,
"sveip": {"parameter": "parametere.ENDO.T_clear", "verdier": [12, None, "inf"]}})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
brukt = [k["brukt"]["parametere"]["ENDO"]["T_clear"] for k in data["kjoringer"]]
assert brukt == [12.0, 24.0, "inf"] # null = notatets verdi
assert [k["fil"] for k in data["kjoringer"]] == [
"ko__T_clear=12.csv", "ko__T_clear=standard.csv", "ko__T_clear=inf.csv"]
standard = data["kjoringer"][1]
assert standard["vedtak"] == ["mnd 24: bestill +19 % (F_pred=0.134 Q_exc=0.27)",
"mnd 36: bestill +12 % (F_pred=0.134 Q_exc=0.56)"]
assert round(standard["nokkeltall"]["kapasitetsaar"], 1) == 3.5 # notatets tabell i avsnitt 8.5
assert round(standard["nokkeltall"]["frafall"], 1) == 8.3
assert "T_clear=standard" in ut
def test_rutenett_gir_alle_kombinasjoner_og_matrise(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"navn": "g", "modell": "simulate", "slutt": 150,
"sveip": [{"parameter": "styring.reaktivitet", "verdier": [1, 3]},
{"parameter": "styring.forsinkelse", "verdier": [3, 11]}]})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
par = [(k["brukt"]["styring"]["reaktivitet"], k["brukt"]["styring"]["forsinkelse"]) for k in data["kjoringer"]]
assert par == [(1.0, 3.0), (1.0, 11.0), (3.0, 3.0), (3.0, 11.0)]
spenn = {p: k["nokkeltall"]["spenn_K_sist"] for p, k in zip(par, data["kjoringer"])}
assert spenn[3.0, 11.0] > 0.3 > spenn[3.0, 3.0] # regime C svinger, regime A ikke
assert "rader: styring.reaktivitet, kolonner: styring.forsinkelse" in ut
def test_sveip_gjennom_kjor_py(tmp_path: Path) -> None:
prosess = kjor("scenario", str(NOTATET / "sat_B.json"), "--ut", str(tmp_path))
assert prosess.returncode == 0, prosess.stderr
assert "ksat=1,5" in prosess.stdout
assert len(list(tmp_path.glob("*.dat"))) == 3
# ---------------- validering ----------------
@pytest.mark.parametrize(("data", "tekster"), [
({"modell": "simulate", "styrng": {}}, ["styrng: ukjent felt", 'Mente du "styring"?', "Gyldige felt her:"]),
({"modell": "simulat"}, ["modell: må være ett av", 'Mente du "simulate"?']),
({"modell": "simulate", "parametere": {"P": {"gama": 0.1}}},
["parametere.P.gama: ukjent parameter i P", 'Mente du "gamma"?', "Gyldige: gamma, eta"]),
({"modell": "simulate", "parametere": {"CH0": {"Z": 1}}},
["parametere.CH0: brukes ikke av modellen simulate", "bruker: P"]),
({"modell": "simulate_chain", "parametere": {"CH0": {"Zfun": 1}}}, ["settes med tiltak.kapasitet"]),
({"modell": "simulate", "styring": {"forsinkelse": "tre"}},
["styring.forsinkelse: må være et tall minst 0", '"tre"']),
({"modell": "simulate", "parametere": {"P": {"Lam0": 1.2}}}, ["parametere.P.Lam0", "mindre enn 1"]),
({"modell": "simulate", "parametere": {"P": {"gamma": "inf"}}}, ["uendelig er ikke tillatt her"]),
({"modell": "simulate_chain2", "parametere": {"CH2": {"c": 2.5}}}, ["parametere.CH2.c: må være et heltall"]),
({"modell": "simulate_endo", "regel": "koord", "parametere": {"ENDO": {"qloop": 1}}},
["parametere.ENDO.qloop: må være true eller false"]),
({"modell": "simulate_endo"}, ["regel: mangler", "sched, desentral, desentral_prog, koord"]),
({"modell": "simulate", "regel": "koord"}, ["regel: gjelder bare simulate_endo"]),
({"modell": "simulate_chain2", "styring": {"regulator": "MPC"}}, ["styring.regulator", '"P"']),
({"modell": "simulate", "sjokk": {"til": 36}}, ["sjokk.til: gjelder bare simulate_soc", "paadrag_tidsserie"]),
({"modell": "simulate", "tiltak": {"kapasitet": {"faktor": 1.2}}},
["tiltak.kapasitet: brukes ikke av modellen simulate", "simulate_chain, simulate_chain2, simulate_endo"]),
({"modell": "simulate_endo", "regel": "koord", "tiltak": {"kapasitet": {"faktor": 1.2}}},
['bare regel "sched"']),
({"modell": "simulate_chain", "parametere": {"CH0": {"Z": "inf"}}, "tiltak": {"kapasitet": {"faktor": 1.2}}},
["tiltak.kapasitet: har ingen mening"]),
({"modell": "simulate", "sjokk": {"faktor": 1.2}, "parametere": {"P": {"shock": 1.2}}},
["sjokk.faktor: er oppgitt to ganger"]),
({"modell": "simulate", "sveip": {"parameter": "styring.reaktivtet", "verdier": [1, 2]}},
["sveip[0].parameter", 'ukjent felt "reaktivtet" under styring', 'Mente du "reaktivitet"?']),
({"modell": "simulate", "sveip": {"parameter": "parametere.P", "verdier": [1]}},
["er en gruppe, ikke én verdi", "parametere.P.gamma"]),
({"modell": "simulate", "sveip": {"parameter": "navn", "verdier": ["a"]}},
["begynner ikke med et felt som kan sveipes"]),
({"modell": "simulate", "sveip": {"parameter": "styring.forsinkelse", "verdier": [3, -1]}},
["styring.forsinkelse: må være et tall minst 0", "(i sveipet: styring.forsinkelse = -1)"]),
({"modell": "simulate", "sveip": {"parameter": "styring.forsinkelse", "verdier": [3, 3.0]}},
["har samme verdi to ganger"]),
({"modell": "simulate", "utdata": {"kolonner": ["t", "Q"]}}, ['utdata.kolonner: "Q" finnes ikke', "t, K, U, L, T"]),
({"modell": "simulate", "utdata": {"overskrift": "t K"}}, ['gjelder bare "format": "notat"']),
({"modell": "utvidelse:finnes_ikke"}, ['fant ikke utvidelsen "finnes_ikke"', "utvidelse:paadrag_tidsserie"]),
])
def test_feil_i_scenariofila_gir_kode_2_og_navngir_feltet(
data: object, tekster: list[str], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, data)
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert ut == ""
assert err.startswith(f"Feil i scenariofila {fil}: ")
for t in tekster:
assert t in err, err
assert not (tmp_path / "ut").exists()
@pytest.mark.parametrize(("innhold", "tekst"), [
('{"modell": "simulate",\n "slutt": 120,,}', "ikke gyldig JSON: feil på linje 2, kolonne 15"),
('{"modell": "simulate", "parametere": {"P": {"ksat": Infinity}}}', 'Skriv uendelig som "inf"'),
('{"modell": "simulate", "slutt": 120, "slutt": 60}', "slutt: feltet står to ganger"),
("[1, 2]", "må være ett JSON-objekt"),
])
def test_feil_json(innhold: str, tekst: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = tmp_path / "s.json"
fil.write_text(innhold, encoding="utf-8")
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert tekst in err, err
def test_fil_som_mangler_og_feil_bruk(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
assert kjor_fil(tmp_path / "finnes_ikke.json", tmp_path / "ut", capsys)[2].startswith("Fant ikke fila")
assert scenario.main([]) == 2
assert "Mangler scenariofil" in capsys.readouterr().err
assert scenario.main(["a.json", "--tull"]) == 2
assert "Ukjent valg --tull" in capsys.readouterr().err
assert scenario.main(["--hjelp"]) == 0
assert capsys.readouterr().out.startswith("Bruk:")
def test_alle_eksempelscenarioer_er_gyldige() -> None:
filer = sorted(NYE.glob("*.json")) + sorted(NOTATET.glob("*.json"))
assert len(filer) >= 26
for fil in filer:
sc = scenario.les_scenario(fil)
assert sc.kjoringer, fil.name
if fil.parent == NYE:
assert sc.beskrivelse, f"{fil.name} mangler beskrivelse"
# ---------------- startlikevekten (gjennomgang E, funn 1) ----------------
def test_endret_grunnparameter_stopper_med_to_veier_ut(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_chain", "parametere": {"P": {"gamma": 0.15}}, "sjokk": {"faktor": 1.0}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
for t in ("startlikevekt: startpunktet er ikke en likevekt", "pådraget Omega0 (0,12)",
"ville drevet bort fra 1,00 også uten sjokk", "passer ikke lenger til strømmen",
'"startlikevekt": "kalibrer"', "(P.Omega0, CH0.Z, CH0.m)", '"startlikevekt": "ubalanse"'):
assert t in err, err
assert not (tmp_path / "ut").exists()
@pytest.mark.parametrize(("p", "modell"), [({"Lam0": 0.6}, "simulate"), ({"delta": 0.03}, "simulate"),
({"T0": 1.2}, "simulate"), ({"eta": 2.0}, "simulate"),
({"Omega0": 0.13}, "simulate"), ({"Kstar": 1.1}, "simulate"),
({"Tprop": 0.9}, "simulate"), ({"Tmax": 0.9}, "simulate"),
({"gamma": 0.1}, "simulate_soc"), ({"Lam0": 0.7}, "simulate_chain2")])
def test_hver_grunnparameter_i_likevekten_sjekkes(p: dict[str, float], modell: str, tmp_path: Path,
capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": modell, "parametere": {"P": p}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert "startpunktet er ikke en likevekt" in err, err
@pytest.mark.parametrize(("modell", "par"), [("simulate", {"P": {"ksat": 1.5}}),
("simulate_soc", {"SOC": {"eps": 0.5}}),
("simulate_chain", {"CH0": {"Z": 0.12}}),
("simulate_chain", {"CH0": {"varsigma": 0.02}})])
def test_parametere_kjernen_selv_rekalibrerer_stopper_ikke(
modell: str, par: dict[str, object], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": modell, "parametere": par})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0, err
def test_kalibrer_gir_ro_for_sjokket(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"navn": "k", "modell": "simulate", "startlikevekt": "kalibrer",
"parametere": {"P": {"Lam0": 0.6, "gamma": 0.2}}, "sjokk": {"faktor": 1.0}})
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0, err
assert "Referansetilstanden er kalibrert på nytt" in ut
assert "P.Omega0 0,14 (var 0,12)" in ut
assert "tilliten er 0,60 der" in ut
rader = (tmp_path / "ut" / "k.csv").read_text(encoding="utf-8").splitlines()[1:]
assert all(abs(float(r.split(",")[1]) - 1.0) < 1e-9 for r in rader) # K står i ro
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
kalibrert = data["kjoringer"][0]["startlikevekt"]["kalibrert"]
assert kalibrert["P.Omega0"]["for"] == 0.12
assert data["kjoringer"][0]["brukt"]["parametere"]["P"]["Omega0"] == kalibrert["P.Omega0"]["etter"]
def test_kalibrer_kjeden_regner_om_z_og_m(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "kalibrer",
"parametere": {"P": {"gamma": 0.15}}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0, err
brukt = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))["kjoringer"][0]["brukt"]
j0 = 0.15 * 0.8 * 1.0
assert brukt["parametere"]["CH0"]["Z"] == j0 / 0.95 # sim.py:151
assert brukt["parametere"]["CH0"]["m"] == 3.0 - j0 / (j0 / 0.95 * (1 / 3.0 + 0.01)) # sim.py:155
assert brukt["parametere"]["P"]["Omega0"] == pytest.approx(j0 + 0.02, abs=1e-15)
@pytest.mark.parametrize("p", [{"Kstar": 1.1}, {"Omega0": 0.13}, {"Tprop": 0.9}])
def test_kalibrer_retter_ikke_det_som_er_satt_eller_ligger_fast(
p: dict[str, float], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate", "startlikevekt": "kalibrer", "parametere": {"P": p}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert '"kalibrer" kan ikke gjøre starten til en likevekt' in err
def test_ubalanse_kjorer_og_viser_driften(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate", "startlikevekt": "ubalanse", "parametere": {"P": {"Lam0": 0.6}},
"sjokk": {"faktor": 1.0}})
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0, err
assert "1,00 er nivået i referanselikevekten" not in ut
assert 'startpunktet er ikke en likevekt ("startlikevekt": "ubalanse"): pådraget Omega0' in ut
assert "Fram til måned 12 har K gått fra 1,000 til 1,0" in ut
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
assert data["kjoringer"][0]["startlikevekt"]["ubalanse"]
def test_ubalanse_med_sjokk_viser_driften_til_sjokkmaaneden(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "ubalanse",
"parametere": {"P": {"gamma": 0.15}}, "sjokk": {"fra": 18}})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
assert "Fram til måned 18 har K gått fra 1,000 til" in ut
assert "saksbehandlingstiden (mnd) fra 15,7 til" in ut
@pytest.mark.parametrize("valg", ["krev", "kalibrer", "ubalanse"])
def test_simulate_endo_med_annen_strom_ved_start_stopper_alltid(
valg: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_endo", "regel": "koord", "startlikevekt": valg,
"parametere": {"P": {"gamma": 0.11}}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert "sim.py:588" in err
assert "endring i kjernen" in err
def test_eta_i_kjeden_kan_ikke_kalibreres(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "kalibrer", "parametere": {"P": {"eta": 2.0}}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert "uten eta" in err
# ---------------- funn 2, 7, 8, 12, 13, 14 og kapasitet i simulate_chain2 ----------------
def test_kjoring_som_eksploderer_stopper_for_noe_skrives(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "utvidelse:paadrag_tidsserie",
"utvidelse": {"punkter": [[12, 1e308], [13, 1e308]]}})
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert ut == ""
assert "ikke er endelige tall" in err
assert "Ingenting er skrevet" in err
assert not (tmp_path / "ut").exists()
def test_absurd_k_gir_merknad(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate", "sjokk": {"faktor": 1e6}})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
assert "## Merknader (kjøringen er ikke stoppet)" in ut
assert "utenfor området kjernen selv regner på" in ut
def test_notatets_vippekjoring_gir_ingen_merknad(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
assert kjor_fil(NOTATET / "vippe.json", tmp_path / "ut", capsys)[1].count("Merknader") == 0
def test_mappe_som_ikke_kan_skrives(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate"})
laast = tmp_path / "laast"
laast.mkdir()
laast.chmod(0o500)
try:
kode, _, err = kjor_fil(fil, laast / "ut", capsys)
finally:
laast.chmod(0o700)
assert kode == 2
assert err.startswith(f"Kan ikke skrive i {laast / 'ut'}: ")
assert err.rstrip().endswith("Velg en annen mappe med --ut.")
assert "Traceback" not in err
def test_koen_har_enhet(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, ut, _ = kjor_fil(NOTATET / "kjede_s2.json", tmp_path / "ut", capsys)
assert kode == 0
assert "Kø i domstolen ved slutt (mnd innstrømning før sjokket)" in ut
assert "Køen er målt i måneder av innstrømningen til domstolen før sjokket" in ut
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
assert data["kjoringer"][0]["nokkeltall"]["Q_slutt_mnd"] == pytest.approx(56.4, abs=0.05) # Qend 5,64 / 0,1
@pytest.mark.parametrize(("periode", "tekst"), [(0.07, "minst 1"), (1.5, "helt antall måneder")])
def test_budsjettsyklus_er_hele_maaneder(periode: float, tekst: str, tmp_path: Path,
capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_endo", "regel": "koord", "parametere": {"ENDO": {"period": periode}}})
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 2
assert "parametere.ENDO.period" in err
assert tekst in err
@pytest.mark.parametrize(("data", "tekst"), [
({"modell": "simulate", "slutt": 0.04, "styring": {"forsinkelse": 0}}, "slutt: må være et tall minst 1"),
({"modell": "simulate", "slutt": 2}, "slutt: horisonten (2 mnd) er kortere enn forsinkelsen"),
({"modell": "simulate", "sveip": {"parameter": "slutt", "verdier": [2]}}, "slutt: horisonten (2 mnd)"),
])
def test_for_kort_horisont_navngir_slutt(data: dict[str, object], tekst: str, tmp_path: Path,
capsys: pytest.CaptureFixture[str]) -> None:
kode, _, err = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
assert kode == 2
assert tekst in err, err
@pytest.mark.parametrize(("data", "tekst"), [
({"modell": "simulate_chain", "slutt": 6}, "Sjokket kommer i måned 12, ikke før slutt (6)"),
({"modell": "simulate_soc", "slutt": 30}, "Sjokket varer til måned 36, etter slutt (30)"),
])
def test_sjokk_utenfor_horisonten_gir_merknad(data: dict[str, object], tekst: str, tmp_path: Path,
capsys: pytest.CaptureFixture[str]) -> None:
kode, ut, _ = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
assert kode == 0
assert tekst in ut
@pytest.mark.parametrize("innhold", [{"kjoringer": [{"fil": "viktig.docx"}]},
{"scenario": "x", "scenariofil_sha256": "", "kjerne_sha256": {},
"kjoringer": [{"fil": "viktig.docx"}, {"fil": "../utenfor.csv"}]}])
def test_sletter_aldri_andres_filer(innhold: dict[str, object], tmp_path: Path,
capsys: pytest.CaptureFixture[str]) -> None:
ut = tmp_path / "ut"
ut.mkdir()
(ut / "resultat.json").write_text(json.dumps(innhold), encoding="utf-8")
(ut / "viktig.docx").write_text("x", encoding="utf-8")
(tmp_path / "utenfor.csv").write_text("x", encoding="utf-8")
kjor_fil(skriv(tmp_path, {"navn": "n", "modell": "simulate"}), ut, capsys)
assert (ut / "viktig.docx").is_file()
assert (tmp_path / "utenfor.csv").is_file()
def test_kapasitet_i_simulate_chain2_vises(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
fil = skriv(tmp_path, {"modell": "simulate_chain2", "tiltak": {"kapasitet": {"faktor": 1.34},
"andel_utenom": {"andel": 0.2}}})
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
assert kode == 0
tall = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))["kjoringer"][0]["nokkeltall"]
assert tall["Z_slutt"] == pytest.approx(1.34)
assert tall["utenom_slutt_pst"] == pytest.approx(20.0)
assert "Kapasitet, ved slutt (1,00 = ved start)" in ut
def test_bruk_nevner_riktig_python() -> None:
assert scenario.BRUK.startswith(f"Bruk: {scenario.PYTHON} modell/kjor.py scenario")
# ---------------- modeller med kø driver sakte før sjokket ----------------
def test_modell_med_ko_viser_driften_for_sjokket(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, ut, _ = kjor_fil(NOTATET / "kjede_s2.json", tmp_path / "ut", capsys)
assert kode == 0
assert "1,00 er nivået ved start for kriminaliteten K og politiets innsats T (notatets referansetilstand)" in ut
assert "nesten, men ikke helt, en likevekt" in ut
assert "referanselikevekten" not in ut
assert "i ro" not in ut
assert "Fram til måned 12 har K gått fra 1,000 til 1,007, innsatsen T fra 1,00 til 1,02" in ut
def test_modell_med_ko_uten_sjokk_sier_at_den_kryper(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, ut, _ = kjor_fil(NYE / "uten_sjokk_kjede.json", tmp_path / "ut", capsys)
assert kode == 0
assert "Fram til måned" not in ut
assert 'kryper modellen med kø sakte oppover av seg selv; en kjøring uten sjokk ("sjokk": {"faktor": 1})' in ut
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
tall = data["kjoringer"][0]["nokkeltall"]
assert round(tall["K_slutt"], 3) == 1.018
assert round(tall["tauD_slutt"], 1) == 4.6
@pytest.mark.parametrize("modell", ["simulate_chain2", "simulate_endo", "utvidelse:paadrag_tidsserie"])
def test_alle_modeller_med_ko_far_driftlinjen(modell: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
data: dict[str, object] = {"modell": modell}
if modell == "simulate_endo":
data["regel"] = "koord"
kode, ut, _ = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
assert kode == 0
assert "nesten, men ikke helt, en likevekt" in ut
assert "Fram til måned 12 har K gått fra 1,000 til" in ut
@pytest.mark.parametrize("fil", ["p3", "vippe"])
def test_modell_uten_ko_har_ingen_driftlinje(fil: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
kode, ut, _ = kjor_fil(NOTATET / f"{fil}.json", tmp_path / "ut", capsys)
assert kode == 0
assert "Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket" in ut
assert "Fram til måned" not in ut
assert "nesten, men ikke helt" not in ut