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>
572 lines
30 KiB
Python
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
|