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>
This commit is contained in:
parent
2669759f5a
commit
776325c680
14 changed files with 1524 additions and 199 deletions
|
|
@ -14,8 +14,10 @@ eller resultater/. Bare standardbiblioteket.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -35,7 +37,24 @@ DOKUMENTER = (
|
|||
("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>"
|
||||
|
||||
|
||||
def paa_windows(*, windows: bool | None = None) -> bool:
|
||||
"""Sann på Windows (eller når testene ber om det)."""
|
||||
return os.name == "nt" if windows is None else windows
|
||||
|
||||
|
||||
def python_navn(*, windows: bool | None = None) -> str:
|
||||
"""Tolken heter python på Windows og python3 ellers."""
|
||||
return "python" if paa_windows(windows=windows) else "python3"
|
||||
|
||||
|
||||
def sitert(sti: Path | str, *, windows: bool | None = None) -> str:
|
||||
"""En sti slik den må skrives på kommandolinja: cmd/PowerShell på Windows, POSIX-skall ellers."""
|
||||
return subprocess.list2cmdline([str(sti)]) if paa_windows(windows=windows) else shlex.quote(str(sti))
|
||||
|
||||
|
||||
BRUK = f"Bruk: {python_navn()} modell/kjor.py arbeidskopi <mappe>"
|
||||
|
||||
|
||||
class KopiFeil(Exception):
|
||||
|
|
@ -60,11 +79,11 @@ def sjekk_mal(mal: Path) -> None:
|
|||
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)
|
||||
rot = ROT.resolve()
|
||||
if mal == rot or rot in mal.parents:
|
||||
melding = (f"{mal} ligger inne i {rot}, modellen som skal kopieres. Velg en mappe utenfor, for eksempel "
|
||||
"i arbeidsmappa di.")
|
||||
raise KopiFeil(melding)
|
||||
|
||||
|
||||
def kildeversjon() -> str:
|
||||
|
|
@ -80,13 +99,14 @@ def kildeversjon() -> str:
|
|||
return f"{repo} @ {commit[:7]}" if repo else commit[:7]
|
||||
|
||||
|
||||
def les_meg(mal: Path, docs: list[str]) -> str:
|
||||
def les_meg(mal: Path, docs: list[str], *, windows: bool | None = None) -> str:
|
||||
"""Innholdet i LES-MEG.md."""
|
||||
kjor = f"python3 {shlex.quote(str(mal / 'modell' / 'kjor.py'))}"
|
||||
py = python_navn(windows=windows)
|
||||
kjor = f"{py} {sitert(mal / 'modell' / 'kjor.py', windows=windows)}"
|
||||
eks = mal / "eksempler"
|
||||
|
||||
def q(sti: Path) -> str:
|
||||
return shlex.quote(str(sti))
|
||||
return sitert(sti, windows=windows)
|
||||
|
||||
linjer = [
|
||||
"# Arbeidskopi av modellen bak «Den dynamiske rettsstaten»",
|
||||
|
|
@ -115,7 +135,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
"```",
|
||||
"",
|
||||
"Fra pluginen kan du peke på kopien med `--modell`:",
|
||||
"`python3 <plugin>/modell/kjor.py --modell " + q(mal) + " scenario <fil.json>`. Ferdighetene i "
|
||||
f"`{py} <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; "
|
||||
|
|
@ -137,7 +157,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
"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`).",
|
||||
f"Testene kjøres med `{py} -m pytest` i mappa (krever pytest; de trege med `-m treg`).",
|
||||
"",
|
||||
"## Dokumentasjon",
|
||||
"",
|
||||
|
|
@ -154,7 +174,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
("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`).",
|
||||
"Alt i pluginen er under MIT-lisensen (se `LICENSE`).",
|
||||
"",
|
||||
]
|
||||
return "\n".join(linjer)
|
||||
|
|
@ -195,12 +215,12 @@ def main(argv: list[str]) -> int:
|
|||
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"))
|
||||
kjor = f"{python_navn()} {sitert(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" {kjor} scenario <fil.json>\n"
|
||||
f"og kontroller endringer med:\n"
|
||||
f" python3 {kjor} kontroll\n")
|
||||
f" {kjor} kontroll\n")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
120
modell/kjor.py
120
modell/kjor.py
|
|
@ -19,10 +19,18 @@ hver har ``main(argv: list[str]) -> int``, der argv er argumentene etter kommand
|
|||
Før main kalles, ligger modell-mappa og modell-mappa/kjerne først i sys.path, slik at
|
||||
``import sim`` gir kjernen i samme modell-mappe. ``sys.exit("melding")`` gir exit-kode 2
|
||||
og meldingen uendret; ``sys.exit(n)`` gir exit-kode n.
|
||||
|
||||
Før diagnose og forlop kalles, avvises ukjente feltnavn i hvert ledd og i "styring" (exit-kode
|
||||
2, med «Mente du ...?»), fordi kjernen ser bort fra felt den ikke kjenner, og en skrivefeil
|
||||
ellers gir en stille standardverdi. Gyldige felt i et ledd: "navn", kjernens diagnose.TALLFELT
|
||||
(lest fra kjernen i modell-mappa når kommandoen kjøres) og merknadsfeltene i ANNOTASJONSFELT.
|
||||
Gyldige felt i "styring": forlop.STYRINGSFELT hvis kjernen har den, ellers STYRINGSFELT her,
|
||||
pluss merknadsfeltene. Andre felt på øverste nivå (som "merknad" og "kilder") er tillatt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
|
|
@ -68,6 +76,8 @@ Exit-koder: 0 ok, 1 kontroll fant forskjeller, 2 feil i inndata eller bruk,
|
|||
"""
|
||||
|
||||
TREGER_REGNEMILJO = ("forlop", "scenario", "notatet")
|
||||
ANNOTASJONSFELT = ("merknad", "kilde", "kilder") # fri tekst i et ledd eller i styring; leses ikke av regningen
|
||||
STYRINGSFELT = ("forsinkelse", "reaktivitet") # det forlop.les_styring leser
|
||||
KOMMANDOER = ("diagnose", "forlop", "scenario", "notatet", "kontroll", "miljo", "arbeidskopi")
|
||||
|
||||
|
||||
|
|
@ -252,12 +262,23 @@ def kjor_paa_nytt(python: Path, argv: list[str]) -> int:
|
|||
return subprocess.run([str(python), str(Path(__file__).resolve()), *argv], env=miljo, check=False).returncode
|
||||
|
||||
|
||||
def kommando_tekst(valg: Valg, *args: str) -> str:
|
||||
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data."""
|
||||
deler = ["python3", str(Path(__file__).resolve())]
|
||||
def python_navn(system: str = os.name) -> str:
|
||||
"""Navnet brukeren skriver for Python: «python» på Windows, ellers «python3»."""
|
||||
return "python" if system == "nt" else "python3"
|
||||
|
||||
|
||||
def kommando_tekst(valg: Valg, *args: str, system: str = os.name) -> str:
|
||||
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data.
|
||||
|
||||
På Windows (system "nt") med «python» og Windows-sitering, ellers med «python3» og POSIX-sitering.
|
||||
"""
|
||||
deler = [python_navn(system), str(Path(__file__).resolve())]
|
||||
if valg.data_oppgitt:
|
||||
deler += ["--data", str(valg.data)]
|
||||
return " ".join(shlex.quote(d) for d in [*deler, *args])
|
||||
deler += list(args)
|
||||
if system == "nt":
|
||||
return subprocess.list2cmdline(deler)
|
||||
return " ".join(shlex.quote(d) for d in deler)
|
||||
|
||||
|
||||
def melding_mangler(valg: Valg, tolk: Tolk) -> str:
|
||||
|
|
@ -358,12 +379,63 @@ def kjor_modul(valg: Valg, navn: str) -> int:
|
|||
|
||||
# ---------------- kommandoene ----------------
|
||||
|
||||
def feltmelding(hvor: str, felt: str, gyldige: tuple[str, ...]) -> str:
|
||||
"""Melding om et ukjent feltnavn, med forslag når det ligner et gyldig."""
|
||||
treff = difflib.get_close_matches(felt, gyldige, n=1, cutoff=0.6)
|
||||
forslag = f' Mente du "{treff[0]}"?' if treff else ""
|
||||
return (f'Feil i inndata, {hvor}: feltet "{felt}" finnes ikke.{forslag} '
|
||||
f"Gyldige felt: {', '.join(gyldige)}.")
|
||||
|
||||
|
||||
def ukjent_felt(fil: str, diagnose: object, forlop: object = None) -> str | None:
|
||||
"""Melding om det første ukjente feltet i et ledd eller i styring, ellers None.
|
||||
|
||||
Fila leses bare for å finne feltnavn. Kan den ikke leses, eller har den feil form, sier
|
||||
kjernen fra om det selv, med sin egen melding.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(Path(fil).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
tallfelt = tuple(str(f) for f in getattr(diagnose, "TALLFELT", ()))
|
||||
gyldige_ledd = ("navn", *tallfelt, *ANNOTASJONSFELT)
|
||||
alle_ledd = data.get("ledd")
|
||||
for nr, ledd in enumerate(alle_ledd if isinstance(alle_ledd, list) else [], 1):
|
||||
if not isinstance(ledd, dict):
|
||||
continue
|
||||
for felt in ledd:
|
||||
if felt not in gyldige_ledd:
|
||||
return feltmelding(f"ledd {nr} ({ledd.get('navn', 'uten navn')})", felt, gyldige_ledd)
|
||||
styring = data.get("styring")
|
||||
if isinstance(styring, dict):
|
||||
gyldige_styring = (*(str(f) for f in getattr(forlop, "STYRINGSFELT", STYRINGSFELT)), *ANNOTASJONSFELT)
|
||||
for felt in styring:
|
||||
if felt not in gyldige_styring:
|
||||
return feltmelding("styring", felt, gyldige_styring)
|
||||
return None
|
||||
|
||||
|
||||
def avvis_ukjente_felt(fil: str, diagnose: object, forlop: object = None) -> int | None:
|
||||
"""Skriv meldingen og gi exit-kode 2 når fila har et ukjent felt; None når den er i orden."""
|
||||
melding = ukjent_felt(fil, diagnose, forlop)
|
||||
if melding is None:
|
||||
return None
|
||||
sys.stdout.flush()
|
||||
print(melding, file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
def kommando_diagnose(valg: Valg) -> int:
|
||||
"""Kjernens diagnose.main."""
|
||||
if len(valg.args) != 1:
|
||||
melding = "Bruk: kjor.py diagnose <inndata.json>"
|
||||
raise Bruksfeil(melding)
|
||||
diagnose = last_kjerne(valg.modell, "diagnose")
|
||||
feil = avvis_ukjente_felt(valg.args[0], diagnose)
|
||||
if feil is not None:
|
||||
return feil
|
||||
return fang(lambda: diagnose.main(valg.args[0]), "diagnose.py")
|
||||
|
||||
|
||||
|
|
@ -373,9 +445,19 @@ def kommando_forlop(valg: Valg) -> int:
|
|||
melding = 'Bruk: kjor.py forlop <inndata.json> ["navn på ledd"]'
|
||||
raise Bruksfeil(melding)
|
||||
forlop = last_kjerne(valg.modell, "forlop")
|
||||
feil = avvis_ukjente_felt(valg.args[0], getattr(forlop, "diagnose", None), forlop)
|
||||
if feil is not None:
|
||||
return feil
|
||||
return fang(lambda: forlop.main(*valg.args), "forlop.py")
|
||||
|
||||
|
||||
def kan_ikke_skrive(mappe: Path, feil: OSError) -> int:
|
||||
"""Norsk melding og exit-kode 2 når ut-mappa ikke kan lages eller skrives i."""
|
||||
sys.stdout.flush()
|
||||
print(f"Kan ikke skrive i {mappe}: {feil.strerror or feil}. Velg en annen mappe med --ut.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
def kommando_notatet(valg: Valg) -> int:
|
||||
"""Kjør kjernens sim.py uendret, med ut-mappa som arbeidsmappe (sim.py skriver data/ der)."""
|
||||
ut = Path.cwd() / "resultater" / "notatet"
|
||||
|
|
@ -392,21 +474,33 @@ def kommando_notatet(valg: Valg) -> int:
|
|||
if ut.exists() and not ut.is_dir():
|
||||
melding = f"{ut} finnes og er ikke en mappe. Velg en annen med --ut <mappe>."
|
||||
raise Bruksfeil(melding)
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
utskrift = ut / "sim_utskrift.txt"
|
||||
try:
|
||||
(ut / "data").mkdir(parents=True, exist_ok=True)
|
||||
prove = ut / "data" / ".skriveprove"
|
||||
prove.write_text("", encoding="utf-8")
|
||||
prove.unlink()
|
||||
logg = utskrift.open("w", encoding="utf-8")
|
||||
except OSError as feil:
|
||||
return kan_ikke_skrive(ut, feil)
|
||||
print(f"Kjører notatets simuleringer (kjerne/sim.py, uendret) i {ut}. Det tar om lag 1-2 minutter.\n",
|
||||
flush=True)
|
||||
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1", PYTHONUNBUFFERED="1")
|
||||
utskrift = ut / "sim_utskrift.txt"
|
||||
with utskrift.open("w", encoding="utf-8") as logg, subprocess.Popen(
|
||||
with logg, subprocess.Popen(
|
||||
[sys.executable, str(valg.modell / "kjerne" / "sim.py")],
|
||||
cwd=str(ut), stdout=subprocess.PIPE, text=True, encoding="utf-8", env=miljo,
|
||||
) as prosess:
|
||||
if prosess.stdout is not None:
|
||||
for linje in prosess.stdout:
|
||||
try:
|
||||
for linje in prosess.stdout or ():
|
||||
sys.stdout.write(linje)
|
||||
logg.write(linje)
|
||||
except OSError as feil:
|
||||
prosess.kill()
|
||||
return kan_ikke_skrive(ut, feil)
|
||||
if prosess.returncode != 0:
|
||||
print(f"sim.py stoppet med exit-kode {prosess.returncode}.", file=sys.stderr)
|
||||
sys.stdout.flush()
|
||||
print(f"Kjøringen av sim.py stoppet underveis (exit-kode {prosess.returncode}); se meldingene over. "
|
||||
f"Datafilene i {ut / 'data'} er ikke fullstendige.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
antall = len(list((ut / "data").glob("*.dat")))
|
||||
print(f"\nFerdig: {antall} datafiler i {ut / 'data'}, utskriften i {utskrift}.")
|
||||
|
|
@ -581,7 +675,11 @@ def main(argv: list[str]) -> int:
|
|||
return utfor(tolk_argumenter(argv))
|
||||
except Bruksfeil as feil:
|
||||
sys.stdout.flush()
|
||||
print(f"{feil}\nSkriv «python3 kjor.py --hjelp» for bruk.", file=sys.stderr)
|
||||
print(f"{feil}\nSkriv «{python_navn()} kjor.py --hjelp» for bruk.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
except OSError as feil: # exit-kode 1 er forbeholdt kontroll som finner forskjeller
|
||||
sys.stdout.flush()
|
||||
print(f"Kan ikke lese eller skrive {feil.filename or ''}: {feil.strerror or feil}.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,30 +5,48 @@ Bruk:
|
|||
python3 kjor.py kontroll [--rask]
|
||||
python3 kontroll.py [--rask] (med en Python som har numpy og scipy)
|
||||
|
||||
Fire kontroller, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen: sha256 for filene i kjerne/ mot kjerne/KILDE.json.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst).
|
||||
Kontrollene, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen og fasit: sha256 for filene i kjerne/ og fasit/ mot notatets verdier, som står som
|
||||
konstanter i denne fila (NOTATETS_KJERNE og NOTATETS_FASIT). Ankeret er notatet, ikke
|
||||
filene i samme mappe: kjerne/KILDE.json er bare en opplysning.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst, eksakt: diagnose.py bruker bare
|
||||
standardbiblioteket, så ingen toleranse).
|
||||
3. forlop.py på begge eksempelfilene mot fasit/ (tekst; trenger numpy og scipy).
|
||||
4. Full kjøring av sim.py i en midlertidig mappe: datafilene mot fasit/data/ byte for byte,
|
||||
med største absolutte avvik per kolonne når en fil er ulik, og utskriften mot
|
||||
fasit/sim_utskrift.txt. Om lag 1-2 min; --rask hopper over den.
|
||||
I tillegg kjøres grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
5. grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
6. Scenariokjøreren: scenariofilene i eksempler/scenarioer/notatet/ som gjenskaper notatets
|
||||
datafiler byte for byte, kjørt gjennom scenario.py i denne modell-mappa, mot fasit/data/.
|
||||
De 13 raske er med i --rask; mpc11 (MPC, treg) bare i full kjøring.
|
||||
Er en fasit-fil endret i forhold til notatet, sier en sammenligning mot den ingenting om
|
||||
notatet; den regnes da som endret.
|
||||
|
||||
Nivået til slutt:
|
||||
- «bit for bit som notatet»: alt er likt byte for byte.
|
||||
- «bit for bit som notatet»: alt er kjørt og likt byte for byte (med --rask: alt unntatt den
|
||||
fulle kjøringen, og det står i linja).
|
||||
- «innenfor toleranse»: teksten er lik, og hvert tall avviker med høyst én enhet i siste
|
||||
viste siffer (det som kan skje med andre versjoner av numpy og scipy, eller en annen maskin).
|
||||
- «endret»: alt annet. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som
|
||||
arbeidskopi.py lager) er det en opplysning, ikke en feil: detaljene sier hvilke av
|
||||
notatets resultater som har flyttet seg, og hvor mye.
|
||||
- Mangler numpy eller scipy, sies bare at det som ble kjørt, var uten avvik, og hva som ikke
|
||||
er kontrollert.
|
||||
- «endret»: alt annet. Detaljene sier hvilke av notatets resultater som har flyttet seg, og
|
||||
hvor mye. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som arbeidskopi.py lager) er
|
||||
det en opplysning, ikke en feil; ellers avhenger det av om modellen er endret med vilje.
|
||||
En utvidelse som ikke gjenskaper kjernen, er alltid en feil i utvidelsen.
|
||||
|
||||
Exit-kode: 0 for bit for bit og innenfor toleranse, 1 for endret, 3 når numpy eller scipy
|
||||
mangler for noe av kontrollen (det som kunne kjøres, er kjørt), 2 for feil bruk.
|
||||
|
||||
Når kjernen synkroniseres fra notatets repo (ny commit), må disse oppdateres sammen:
|
||||
kjerne/*.py, kjerne/KILDE.json, fasit/ (lages på nytt med notatets eget miljø), og
|
||||
konstantene NOTATETS_COMMIT, NOTATETS_KJERNE og NOTATETS_FASIT her. Sha256 for fasit/data/
|
||||
skal være lik notatets paper/data/ ved samme commit.
|
||||
|
||||
Grensesnitt: main(argv: list[str]) -> int, som kjor.py kaller.
|
||||
Utvidelser: utvidelser/<navn>.py (ikke filer som begynner med _) med
|
||||
grensetilfelle() -> list av (beskrivelse, største absolutte avvik mot kjernen). Hver
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path.
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path. Scenariokjøreren
|
||||
kjøres på samme måte: scenario.main([fil, "--ut", mappe]) skal skrive <navn>.dat i mappa.
|
||||
Bare standardbiblioteket her; kjernen kjøres som egne prosesser med samme Python.
|
||||
"""
|
||||
|
||||
|
|
@ -52,13 +70,57 @@ KJERNE = MODELL / "kjerne"
|
|||
ROT = MODELL.parent
|
||||
FASIT = ROT / "fasit"
|
||||
EKSEMPLER = ROT / "eksempler"
|
||||
SCENARIOER = EKSEMPLER / "scenarioer" / "notatet"
|
||||
EKSEMPELFILER = ("eksempel_domstol", "norge_2025")
|
||||
ARBEIDSKOPI_MERKE = "LES-MEG.md"
|
||||
|
||||
# Notatet: https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten @ 2badc25 (se synkroniseringen over).
|
||||
NOTATETS_REPO = "https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten"
|
||||
NOTATETS_COMMIT = "2badc25e5134ee26309bd8c3bd008ac64afe55a4"
|
||||
NOTATETS_KJERNE = {
|
||||
"sim.py": "96857a92492f5768bc5d1d98bec30f4292b603c1273913e69901b6a1b220a01e",
|
||||
"diagnose.py": "e5a20c68615836dd854c1414346c7932037eefaf1adb29a18458dbd590a3367a",
|
||||
"forlop.py": "5b369e30f98783b712f76e5dc90bcea4f711603944690c4f19da01efd29c6e21",
|
||||
}
|
||||
NOTATETS_FASIT = {
|
||||
"data/endo_des.dat": "cc200766c80dd486611b4f0a0e20e229c6f1842763b205e86760bf17458f594f",
|
||||
"data/endo_desp.dat": "66794618783bd098a16ddbb427c20d46795f6805e5a14f14122f86e3f8825847",
|
||||
"data/endo_e5.dat": "de137561fe952336f94008378f1bed4b72bbec91ca9613a054c47860086c5671",
|
||||
"data/endo_ko.dat": "ebbf9a096d04a5a52977303b221ee6d8144fc771354c86b7d0ba85262e543378",
|
||||
"data/kjede_s1.dat": "5f7597aea8a39909cfa3784f841a4e4f11cff8822721fa42a5ee90e3ffcd9ce2",
|
||||
"data/kjede_s2.dat": "9dedcc6821a286eca9f05d1a908e377f11c1e542baf7a30c3658b1128f7aacf9",
|
||||
"data/kjede_s3.dat": "a88c964f3a98c2eb962fa2066b3c1d1f4d092b0abfadc2b93c8cd6d747b9a68d",
|
||||
"data/kjede_s4.dat": "6b7dccdcade9eead1e483d046e745a1291b2238c2027f05ee38ee248787cd407",
|
||||
"data/kjede_s5.dat": "cdf936372eed8f3e1117508172f63e8c72f89294cb06caf994262652d044fe59",
|
||||
"data/mpc11.dat": "2609b5bb73efb12e31adf3d4b40c6798e93d40e6bd68afb2583a565d60cb3be0",
|
||||
"data/p11.dat": "526a839a393c2020fff0280a39c263e0ea73d7131ff86d3d9b734be919e884da",
|
||||
"data/p11d.dat": "dfa920ebeafef640735ce27eb98e50f6977e20b643a9c140a1672df86d8e8bf3",
|
||||
"data/p3.dat": "448def8fa662a3b68152d184a33888f7d5a0cfcdfe619064a51a47ff07d6037c",
|
||||
"data/p6.dat": "fcb78c473b4927aaf1a8360a6e2652d0363eaa6b2ac2a80ef551eb577ca4390d",
|
||||
"data/prio_s2.dat": "38389ac7a3d4cc2b5ad4cf54248f0a98d89bb21eb5c8f4f1eba799c6b4b5c7fc",
|
||||
"data/prio_s3.dat": "b675f87eefbcd4097732a258b6e81d06b5b5d96d4bcfbc8599270cc2079baf7e",
|
||||
"data/prio_s5.dat": "dd8744bd75ee27eceb9f312c4740058d13973cdcabf499fef895d2fe32c41813",
|
||||
"data/sat_B.dat": "6a24ce8f69964a055deaaa9260375b8e4cb1c6c09f1a113fd20f6f96a4251847",
|
||||
"data/sat_C.dat": "851d218a3a7772da65e3b19301cdf2006582358df873bab9fcaf7c3da0236fa7",
|
||||
"data/stab.dat": "9a09e14290634beafa1560a31097f76d26c592e7be46b7590e1480a01b60974e",
|
||||
"data/thumb.dat": "49c07f7ae6c23c9f728be27c422e76e8afa895b9bdd46c43ea2f5d5ebe7c767d",
|
||||
"data/vippe.dat": "7185a3edff28dede529a230504d63d85e4821096730acd293855cd74bd005ed1",
|
||||
"diagnose_eksempel_domstol.txt": "226212b0177b54a5402c2fd83d2df334184c7533b295bb3a72a277a4b99f82f0",
|
||||
"diagnose_norge_2025.txt": "3764c9236ec5afde9a47ebbeacec3e0d42cde98123a7d5a6e11c3fbec56a696e",
|
||||
"forlop_eksempel_domstol.txt": "b56cf2a7408d9611088eebaba787ab5d5a1dcdd3ee6116ce73dc226e1cea88c4",
|
||||
"forlop_norge_2025.txt": "ec75dae235031743816ad4b2d63830fcee6050e9e1477acb68269e7e9ded456a",
|
||||
"sim_utskrift.txt": "6225f9427845d915914a0bf78acf4359f6935d437f48cb94e1da48220a99eafd",
|
||||
}
|
||||
# Scenariofilene som gjenskaper notatets datafil med samme navn byte for byte.
|
||||
SCENARIO_RASKE = ("p3", "p6", "p11", "p11d", "kjede_s1", "kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5",
|
||||
"endo_e5", "endo_des", "endo_desp", "endo_ko")
|
||||
SCENARIO_TREGE = ("mpc11",)
|
||||
|
||||
LIK, TOLERANSE, ENDRET, IKKE_KJORT = "lik", "toleranse", "endret", "ikke kjørt"
|
||||
RANG = {LIK: 0, TOLERANSE: 1, IKKE_KJORT: 2, ENDRET: 3}
|
||||
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning
|
||||
SLINGRING = 1e-9 # relativ slingring når avviket sammenlignes med én enhet i siste siffer
|
||||
UTVIDELSER = "Utvidelser" # merkelappen for kontroll 5
|
||||
|
||||
TALL = re.compile(r"[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?")
|
||||
|
||||
|
|
@ -75,6 +137,32 @@ print()
|
|||
print(json.dumps([[str(b), float(a)] for b, a in modul.grensetilfelle()]))
|
||||
"""
|
||||
|
||||
SCENARIO_KJORER = """\
|
||||
import contextlib, importlib.util, io, json, os, sys, traceback
|
||||
modell, ut, filer = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
sys.path[:0] = [os.path.join(modell, "kjerne"), modell]
|
||||
spec = importlib.util.spec_from_file_location("scenario", os.path.join(modell, "scenario.py"))
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
sys.modules["scenario"] = modul
|
||||
spec.loader.exec_module(modul)
|
||||
svar = []
|
||||
for fil in filer:
|
||||
navn = os.path.splitext(os.path.basename(fil))[0]
|
||||
feil = io.StringIO()
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(feil):
|
||||
try:
|
||||
kode = modul.main([fil, "--ut", os.path.join(ut, navn)])
|
||||
except SystemExit as stopp:
|
||||
kode = stopp.code if isinstance(stopp.code, int) else 2
|
||||
feil.write(str(stopp.code or ""))
|
||||
except Exception:
|
||||
kode = 2
|
||||
feil.write(traceback.format_exc())
|
||||
svar.append([navn, kode, feil.getvalue()[-3000:]])
|
||||
print()
|
||||
print(json.dumps(svar))
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dom:
|
||||
|
|
@ -84,6 +172,7 @@ class Dom:
|
|||
linje: str
|
||||
detaljer: list[str] = field(default_factory=list)
|
||||
avvik: float = 0.0
|
||||
ikke_kjort: str = "" # hva som ikke ble kontrollert, når status er IKKE_KJORT
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -226,44 +315,77 @@ def sha256(fil: Path) -> str:
|
|||
return hashlib.sha256(fil.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
|
||||
def lest_fasit(rel: str, endret_fasit: set[str]) -> str | None:
|
||||
"""Innholdet i fasit/<rel>, eller None når fila mangler eller er endret i forhold til notatet."""
|
||||
fil = FASIT / rel
|
||||
if rel in endret_fasit or not fil.is_file():
|
||||
return None
|
||||
return fil.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def ugyldig_fasit(rel: str) -> str:
|
||||
"""Detaljen når en fasit-fil ikke kan brukes som notatets verdi."""
|
||||
if not (FASIT / rel).is_file():
|
||||
return f"fant ikke fasit/{rel}"
|
||||
return f"fasit/{rel} er endret i forhold til notatet, så sammenligningen sier ingenting om notatet"
|
||||
|
||||
|
||||
# ---------------- kontrollene ----------------
|
||||
|
||||
def kontroll_kjerne() -> Dom:
|
||||
"""1. Kjernen mot sha256 i KILDE.json."""
|
||||
kildefil = KJERNE / "KILDE.json"
|
||||
if not kildefil.is_file():
|
||||
return Dom(ENDRET, f"Kjernen: fant ikke {kildefil}, så kjernen kan ikke sammenlignes med notatet.")
|
||||
kilde = json.loads(kildefil.read_text(encoding="utf-8"))
|
||||
"""1. Kjernen mot notatets sha256 (konstantene over, ikke KILDE.json)."""
|
||||
endret = []
|
||||
for navn, info in kilde["filer"].items():
|
||||
for navn, verdi in NOTATETS_KJERNE.items():
|
||||
fil = KJERNE / navn
|
||||
if not fil.is_file():
|
||||
endret.append(f"{navn} mangler")
|
||||
elif sha256(fil) != info["sha256"]:
|
||||
elif sha256(fil) != verdi:
|
||||
endret.append(f"{navn} er endret")
|
||||
versjon = f"{kilde['repo']} @ {kilde['commit'][:7]}"
|
||||
versjon = f"{NOTATETS_REPO} @ {NOTATETS_COMMIT[:7]}"
|
||||
if not endret:
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(kilde['filer'])} filer, {versjon}).")
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(NOTATETS_KJERNE)} filer, {versjon}).")
|
||||
return Dom(ENDRET, f"Kjernen: endret fra notatet ({versjon}): {', '.join(endret)}.")
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit."""
|
||||
def endrede_fasitfiler() -> set[str]:
|
||||
"""Filene i fasit/ som mangler eller ikke har notatets sha256."""
|
||||
return {rel for rel, verdi in NOTATETS_FASIT.items()
|
||||
if not (FASIT / rel).is_file() or sha256(FASIT / rel) != verdi}
|
||||
|
||||
|
||||
def kontroll_fasit(endret_fasit: set[str]) -> Dom:
|
||||
"""1. Fasit mot notatets sha256."""
|
||||
if not endret_fasit:
|
||||
return Dom(LIK, f"Fasit: uendret fra notatet ({len(NOTATETS_FASIT)} filer).")
|
||||
detaljer = [f"fasit/{rel}: " + ("mangler" if not (FASIT / rel).is_file() else "har ikke notatets sha256")
|
||||
for rel in sorted(endret_fasit)]
|
||||
return Dom(ENDRET, f"Fasit: fasit er endret i forhold til notatet ({len(endret_fasit)} av "
|
||||
f"{len(NOTATETS_FASIT)} filer).", detaljer, math.inf)
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path, endret_fasit: set[str], *, toleranse: bool) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit.
|
||||
|
||||
toleranse=False: bare byte-lik tekst godtas (diagnose.py avhenger ikke av numpy og scipy).
|
||||
"""
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for eksempel in EKSEMPELFILER:
|
||||
fasitfil = FASIT / f"{skript}_{eksempel}.txt"
|
||||
rel = f"{skript}_{eksempel}.txt"
|
||||
fasit = lest_fasit(rel, endret_fasit)
|
||||
inndata = EKSEMPLER / f"{eksempel}.json"
|
||||
mangler = [str(f) for f in (fasitfil, inndata) if not f.is_file()]
|
||||
if mangler:
|
||||
if fasit is None or not inndata.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{eksempel}: fant ikke {', '.join(mangler)}")
|
||||
detaljer.append(f"{eksempel}: " + (ugyldig_fasit(rel) if fasit is None else f"fant ikke {inndata}"))
|
||||
continue
|
||||
prosess = kjor_python([str(KJERNE / f"{skript}.py"), str(inndata)], mappe)
|
||||
if prosess.returncode != 0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{eksempel}:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
continue
|
||||
resultat = sammenlign_tekst(fasitfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
resultat = sammenlign_tekst(fasit, prosess.stdout)
|
||||
if resultat.status == TOLERANSE and not toleranse:
|
||||
resultat.status = ENDRET
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status != LIK:
|
||||
|
|
@ -278,23 +400,27 @@ def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kontroll_full(mappe: Path) -> Dom:
|
||||
def kontroll_full(mappe: Path, endret_fasit: set[str]) -> Dom:
|
||||
"""4. Full kjøring av sim.py i en midlertidig mappe mot fasit/data og fasit/sim_utskrift.txt."""
|
||||
print(" (kjører sim.py, om lag 1-2 minutter ...)", flush=True)
|
||||
prosess = kjor_python([str(KJERNE / "sim.py")], mappe)
|
||||
if prosess.returncode != 0:
|
||||
return Dom(ENDRET, "Full kjøring: sim.py stoppet med en feil.", feilmelding(prosess), math.inf)
|
||||
fasit_data = FASIT / "data"
|
||||
navn_fasit = sorted(f.name for f in fasit_data.glob("*.dat"))
|
||||
navn_fasit = sorted(rel[len("data/"):] for rel in NOTATETS_FASIT if rel.startswith("data/"))
|
||||
navn_naa = sorted(f.name for f in (mappe / "data").glob("*.dat"))
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for navn in sorted(set(navn_fasit) | set(navn_naa)):
|
||||
if navn not in navn_naa or navn not in navn_fasit:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa else "finnes ikke i fasit"))
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa
|
||||
else "er ikke en av notatets datafiler"))
|
||||
continue
|
||||
resultat = sammenlign_dat((fasit_data / navn).read_text(encoding="utf-8"),
|
||||
(mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
fasit = lest_fasit(f"data/{navn}", endret_fasit)
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: {ugyldig_fasit(f'data/{navn}')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, (mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
|
|
@ -302,11 +428,11 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{navn} ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
utskriftfil = FASIT / "sim_utskrift.txt"
|
||||
if utskriftfil.is_file():
|
||||
utskrift = sammenlign_tekst(utskriftfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
fasit_utskrift = lest_fasit("sim_utskrift.txt", endret_fasit)
|
||||
if fasit_utskrift is None:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [ugyldig_fasit("sim_utskrift.txt")])
|
||||
else:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [f"fant ikke {utskriftfil}"])
|
||||
utskrift = sammenlign_tekst(fasit_utskrift, prosess.stdout)
|
||||
statuser.append(utskrift.status)
|
||||
storst = max(storst, utskrift.avvik)
|
||||
if utskrift.status != LIK:
|
||||
|
|
@ -318,40 +444,112 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def grensetilfeller(fil: Path, mappe: Path) -> tuple[list[str], list[str], float]:
|
||||
"""Kjør grensetilfelle() i én utvidelse. Returnerer statuser, detaljer og største avvik."""
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() kunne ikke kjøres",
|
||||
*(" " + linje for linje in feilmelding(prosess))], math.inf
|
||||
if not tilfeller:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett."], 0.0
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik == 0.0:
|
||||
statuser.append(LIK)
|
||||
continue
|
||||
statuser.append(ENDRET)
|
||||
hvem = beskrivelse if str(beskrivelse).startswith(fil.stem) else f"{fil.stem}: {beskrivelse}"
|
||||
detaljer.append(f"{hvem}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
return statuser, detaljer, storst
|
||||
|
||||
|
||||
def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
||||
"""Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
"""5. Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
utvidelser = sorted(f for f in (MODELL / "utvidelser").glob("*.py") if not f.name.startswith("_"))
|
||||
if not utvidelser:
|
||||
return Dom(LIK, "Utvidelser: ingen å kontrollere.")
|
||||
return Dom(LIK, f"{UTVIDELSER}: ingen å kontrollere.")
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, f"Utvidelser: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.")
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
return Dom(IKKE_KJORT, f"{UTVIDELSER}: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.",
|
||||
ikke_kjort="utvidelsenes grensetilfeller")
|
||||
statuser, detaljer, storst, feilet = [], [], 0.0, []
|
||||
for fil in utvidelser:
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{fil.stem}: grensetilfelle() kunne ikke kjøres", *(" " + linje for linje in
|
||||
feilmelding(prosess))]
|
||||
continue
|
||||
if not tilfeller:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett.")
|
||||
continue
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik != 0.0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: {beskrivelse}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
else:
|
||||
statuser.append(LIK)
|
||||
status = verst(statuser)
|
||||
egne, egne_detaljer, avvik = grensetilfeller(fil, mappe)
|
||||
statuser += egne
|
||||
detaljer += egne_detaljer
|
||||
storst = max(storst, avvik)
|
||||
if ENDRET in egne:
|
||||
feilet.append(fil.stem)
|
||||
navn = ", ".join(f.stem for f in utvidelser)
|
||||
linje = (f"Utvidelser: grensetilfellene gjenskaper kjernen eksakt ({navn})." if status == LIK
|
||||
else f"Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt ({navn}; se under).")
|
||||
if not feilet:
|
||||
return Dom(LIK, f"{UTVIDELSER}: grensetilfellene gjenskaper kjernen eksakt ({navn}).", [], storst)
|
||||
linje = (f"{UTVIDELSER}: {', '.join(feilet)} gjenskaper ikke kjernen i grensetilfellet. Det er en feil i "
|
||||
"utvidelsen, og resultatene den gir, kan ikke brukes (se under).")
|
||||
return Dom(ENDRET, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kjor_scenarioer(navn: tuple[str, ...], mappe: Path) -> tuple[dict[str, tuple[int, str]], list[str]]:
|
||||
"""Kjør scenariofilene gjennom scenario.py i en egen prosess.
|
||||
|
||||
Returnerer {navn: (exit-kode, stderr)} og feillinjer når prosessen selv stoppet.
|
||||
"""
|
||||
ut = mappe / "scenarioer"
|
||||
filer = [str(SCENARIOER / f"{n}.json") for n in navn]
|
||||
prosess = kjor_python(["-c", SCENARIO_KJORER, str(MODELL), str(ut), *filer], mappe)
|
||||
try:
|
||||
svar = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
svar = None
|
||||
if svar is None:
|
||||
return {}, ["scenario.py kunne ikke lastes:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
return {str(n): (int(kode), str(feil)) for n, kode, feil in svar}, []
|
||||
|
||||
|
||||
def kontroll_scenario(mappe: Path, endret_fasit: set[str], *, rask: bool, regnemiljo: bool) -> Dom:
|
||||
"""6. Scenariokjøreren gjenskaper notatets datafiler fra scenariofilene i eksempler/scenarioer/notatet/."""
|
||||
navn = SCENARIO_RASKE if rask else SCENARIO_RASKE + SCENARIO_TREGE
|
||||
if not (MODELL / "scenario.py").is_file():
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {MODELL / 'scenario.py'}, så den er ikke kontrollert.",
|
||||
[], math.inf)
|
||||
mangler = [n for n in navn if not (SCENARIOER / f"{n}.json").is_file()]
|
||||
if mangler:
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {len(mangler)} av {len(navn)} scenariofiler i "
|
||||
f"{SCENARIOER}, så den er ikke kontrollert.", [f"mangler: {', '.join(mangler)}"], math.inf)
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, "Scenariokjøreren: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="scenariokjøreren")
|
||||
resultater, feil = kjor_scenarioer(navn, mappe)
|
||||
if feil:
|
||||
return Dom(ENDRET, "Scenariokjøreren: scenario.py stoppet før noen fil var kjørt.", feil, math.inf)
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for n in navn:
|
||||
kode, melding = resultater.get(n, (2, "ble ikke kjørt"))
|
||||
fasit = lest_fasit(f"data/{n}.dat", endret_fasit)
|
||||
laget = mappe / "scenarioer" / n / f"{n}.dat"
|
||||
if kode != 0 or not laget.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{n}.json: stoppet med exit-kode {kode}" if kode != 0 else f"{n}.json: laget ikke {n}.dat",
|
||||
*(" " + linje for linje in melding.strip().splitlines()[-MAKS_LINJER // 2:])]
|
||||
continue
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{n}.dat: {ugyldig_fasit(f'data/{n}.dat')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, laget.read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
like += 1
|
||||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{n}.dat ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
status = verst(statuser)
|
||||
linje = (f"Scenariokjøreren: {like} av {len(navn)} av notatets datafiler gjenskapt byte for byte fra "
|
||||
"eksempler/scenarioer/notatet/" + (" (mpc11 bare i full kjøring)." if rask else "."))
|
||||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
|
|
@ -360,25 +558,35 @@ def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
|||
def niva(dommer: list[Dom], *, rask: bool, arbeidskopi: bool) -> list[str]:
|
||||
"""Linjene om nivået som er oppnådd."""
|
||||
kjort = [d for d in dommer if d.status != IKKE_KJORT]
|
||||
hoppet_over = [d.ikke_kjort for d in dommer if d.status == IKKE_KJORT]
|
||||
status = verst([d.status for d in kjort])
|
||||
deler = (["uten full kjøring, --rask"] if rask else []) + (
|
||||
["bare det som er kjørt"] if any(d.status == IKKE_KJORT for d in dommer) else [])
|
||||
hva = f" ({'; '.join(deler)})" if deler else ""
|
||||
if status == LIK:
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
hva = " (uten full kjøring, --rask)" if rask else ""
|
||||
if status == ENDRET:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if UTVIDELSER in endret:
|
||||
linjer.append("En utvidelse som ikke gjenskaper kjernen i grensetilfellet, har en feil. Resultatene den "
|
||||
"gir, kan ikke brukes før den er rettet.")
|
||||
if [e for e in endret if e != UTVIDELSER]:
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Er dette pluginen slik den er installert, er det en feil: pluginens modell skal regne "
|
||||
"som notatet. Er modellen endret med vilje (for eksempel i en klone av repoet), er det "
|
||||
"en opplysning: detaljene over viser hvilke av notatets resultater som har flyttet "
|
||||
"seg, og hvor mye.")
|
||||
elif hoppet_over:
|
||||
kjorte = [d.linje.split(":")[0] for d in kjort]
|
||||
linjer = [(f"Nivå{hva}: ingen avvik i det som ble kjørt ({', '.join(kjorte)}). Ikke kontrollert: "
|
||||
f"{', '.join(hoppet_over)}.")]
|
||||
elif status == TOLERANSE:
|
||||
storst = max(d.avvik for d in kjort)
|
||||
linjer = [(f"Nivå{hva}: innenfor toleranse (største avvik {vis_tall(storst)}): hvert tall avviker med "
|
||||
"høyst én enhet i siste viste siffer.")]
|
||||
else:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Pluginens modell skal regne som notatet, så dette er en feil.")
|
||||
if any(d.status == IKKE_KJORT for d in dommer):
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
if hoppet_over:
|
||||
linjer.append(f"Ikke kjørt fordi numpy eller scipy mangler i {sys.executable}. "
|
||||
"«kjor.py miljo --opprett» installerer dem i en egen mappe.")
|
||||
return linjer
|
||||
|
|
@ -402,21 +610,25 @@ def main(argv: list[str]) -> int:
|
|||
print(f" {linje}")
|
||||
sys.stdout.flush()
|
||||
|
||||
endret_fasit = endrede_fasitfiler()
|
||||
with tempfile.TemporaryDirectory(prefix="dynamisk-rettsstat-") as tmp:
|
||||
mappe = Path(tmp)
|
||||
vis("1.", kontroll_kjerne())
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe))
|
||||
vis("1.", kontroll_fasit(endret_fasit))
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe, endret_fasit, toleranse=False))
|
||||
if regnemiljo:
|
||||
vis("3.", kontroll_utskrift("forlop", mappe))
|
||||
vis("3.", kontroll_utskrift("forlop", mappe, endret_fasit, toleranse=True))
|
||||
else:
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler.", ikke_kjort="forlop"))
|
||||
if rask:
|
||||
print("4. Full kjøring: hoppet over (--rask).")
|
||||
elif regnemiljo:
|
||||
vis("4.", kontroll_full(mappe))
|
||||
vis("4.", kontroll_full(mappe, endret_fasit))
|
||||
else:
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="full kjøring av sim.py"))
|
||||
vis("5.", kontroll_utvidelser(mappe, regnemiljo=regnemiljo))
|
||||
vis("6.", kontroll_scenario(mappe, endret_fasit, rask=rask, regnemiljo=regnemiljo))
|
||||
print()
|
||||
print("\n".join(niva(dommer, rask=rask, arbeidskopi=arbeidskopi)))
|
||||
statuser = [d.status for d in dommer]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import importlib.util
|
|||
import itertools
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -87,9 +88,10 @@ SATT_AV_TILTAK = {
|
|||
("CH2", "xiBfun"): "tiltak.andel_utenom",
|
||||
}
|
||||
|
||||
TOPPFELT = ("navn", "beskrivelse", "merknad", "modell", "styring", "parametere", "regel", "sjokk",
|
||||
"tiltak", "slutt", "sveip", "utdata", "utvidelse")
|
||||
SVEIPBARE = ("styring", "parametere", "regel", "sjokk", "tiltak", "slutt", "utvidelse")
|
||||
TOPPFELT = ("navn", "beskrivelse", "merknad", "modell", "startlikevekt", "styring", "parametere", "regel",
|
||||
"sjokk", "tiltak", "slutt", "sveip", "utdata", "utvidelse")
|
||||
STARTVALG = ("krev", "kalibrer", "ubalanse")
|
||||
SVEIPBARE = ("styring", "parametere", "regel", "sjokk", "tiltak", "slutt", "utvidelse", "startlikevekt")
|
||||
STYRINGSFELT = ("regulator", "forsinkelse", "reaktivitet", "mpc_kjenner_kjeden")
|
||||
SJOKKFELT = ("faktor", "fra", "til")
|
||||
TILTAKSFELT = ("andel_utenom", "kapasitet", "trommel_buffer_tau")
|
||||
|
|
@ -105,6 +107,7 @@ STANDARD_TILTAK_FRA = 24.0 # T_TILTAK i sim.py
|
|||
STANDARD_TILTAK_OVER = 24.0 # T_UTV i sim.py
|
||||
STANDARD_SOC_SJOKK = (1.6, 12.0, 36.0) # simulate_soc: shock, t_on, t_off
|
||||
MAKS_SLUTT = 2400.0
|
||||
MIN_SLUTT = 1.0 # kjernen trenger minst to tidssteg; én måned er minste meningsfulle horisont
|
||||
MAKS_KJORINGER = 400
|
||||
RUTENETT = 2 # høyst to parametere i et sveip
|
||||
SEN_FASE = 60.0 # spennet i K måles etter denne måneden (som amp(K,t>60) i sim.py)
|
||||
|
|
@ -112,6 +115,11 @@ SNITT_FRA = 12.0 # summary_endo: gjennomsnitt fra måned 12
|
|||
MAKS_KOLONNER_I_TABELL = 6
|
||||
CSV_HVERT = 20 # standard: én rad per måned (DT = 0,05)
|
||||
NOTAT_HVERT = 10 # write_csv i sim.py
|
||||
LIKEVEKT_TOL = 1e-12 # største tillatte rest i dK/dt ved start (pådraget er om lag 0,12 per måned)
|
||||
FLYTPARAMETERE = ("gamma", "Lam0", "T0") # gir strømmen J0 = gamma Lam0 T0 som domstolen er kalibrert for
|
||||
ENDO_FAST_STROM = 0.1 # simulate_endo: observert strøm de første lagD månedene (sim.py:588), fast i kjernen
|
||||
UTEN_SJOKK_MND = 12.0 # måneden driften vises for når det ikke er noe sjokk (notatets sjokkmåned)
|
||||
K_OMRADE = (0.01, 12.0) # merknad utenfor: området soc_equilibria i sim.py leter i (notatets K: 0,83-9,82)
|
||||
|
||||
|
||||
class ScenarioFeil(Exception):
|
||||
|
|
@ -156,12 +164,13 @@ GRENSER: dict[tuple[str, str], Grense] = {
|
|||
("ENDO", "k_xi"): IKKE_NEG, ("ENDO", "tau_xi"): POSITIV, ("ENDO", "ups_star"): POSITIV,
|
||||
("ENDO", "ups_hi"): POSITIV, ("ENDO", "ups_lo"): IKKE_NEG, ("ENDO", "kZ"): IKKE_NEG,
|
||||
("ENDO", "L1"): IKKE_NEG, ("ENDO", "L2"): POSITIV, ("ENDO", "tau_down"): POSITIV,
|
||||
("ENDO", "T_clear"): POSITIV_INF, ("ENDO", "H_pred"): POSITIV, ("ENDO", "period"): POSITIV,
|
||||
("ENDO", "T_clear"): POSITIV_INF, ("ENDO", "H_pred"): POSITIV, ("ENDO", "period"): Grense(1.0),
|
||||
("ENDO", "mu3"): IKKE_NEG, ("ENDO", "Lam_star"): APEN_ANDEL, ("ENDO", "k_q"): IKKE_NEG,
|
||||
("ENDO", "q_max"): IKKE_NEG, ("ENDO", "lag_survey"): IKKE_NEG,
|
||||
("SOC", "eps"): IKKE_NEG, ("SOC", "Kh"): POSITIV,
|
||||
}
|
||||
HELTALLSGRENSER = {("CH2", "c"): (1, 50), ("SOC", "pexp"): (1, 20)}
|
||||
HELE_MAANEDER = {("ENDO", "period")} # budsjettsyklusen: vedtak når t % period == 0 i hele måneder (sim.py:614)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- kjernen
|
||||
|
|
@ -455,6 +464,9 @@ class Kjoring:
|
|||
tiltak: dict[str, object]
|
||||
dicts: dict[str, dict[str, object]]
|
||||
utvidelse: dict[str, object] = field(default_factory=dict)
|
||||
startlikevekt: str = "krev"
|
||||
ubalanse: list[str] = field(default_factory=list) # hvorfor starten ikke er en likevekt
|
||||
kalibrert: list[tuple[str, float, float]] = field(default_factory=list) # (sti, notatets, ny)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -545,7 +557,10 @@ def parameterverdi(navn: str, nokkel: str, standard: object, verdi: object, sti:
|
|||
if isinstance(standard, int):
|
||||
nedre, ovre = HELTALLSGRENSER.get((navn, nokkel), (-10**9, 10**9))
|
||||
return heltall(verdi, sti, nedre, ovre)
|
||||
return tallverdi(verdi, sti, GRENSER.get((navn, nokkel), Grense()))
|
||||
x = tallverdi(verdi, sti, GRENSER.get((navn, nokkel), Grense()))
|
||||
if (navn, nokkel) in HELE_MAANEDER and not x.is_integer():
|
||||
feil(sti, f"må være et helt antall måneder (kjernen gjør vedtak bare i hele måneder), ikke {vis(verdi)}.")
|
||||
return x
|
||||
|
||||
|
||||
def les_styring(raa: object, modell: str, slutt: float) -> tuple[str, float, float, bool | None]:
|
||||
|
|
@ -553,8 +568,10 @@ def les_styring(raa: object, modell: str, slutt: float) -> tuple[str, float, flo
|
|||
s = objekt(raa if raa is not None else {}, "styring", STYRINGSFELT)
|
||||
regulatorer = REGULATORER.get(modell, ("P", "MPC", "fast"))
|
||||
regulator = tekst(s.get("regulator", "P"), "styring.regulator", regulatorer)
|
||||
forsinkelse = tallverdi(s.get("forsinkelse", STANDARD_FORSINKELSE), "styring.forsinkelse",
|
||||
Grense(0.0, ovre=slutt))
|
||||
forsinkelse = tallverdi(s.get("forsinkelse", STANDARD_FORSINKELSE), "styring.forsinkelse", IKKE_NEG)
|
||||
if forsinkelse > slutt:
|
||||
feil("slutt", f"horisonten ({tall_kort(slutt)} mnd) er kortere enn forsinkelsen i styringen "
|
||||
f"(styring.forsinkelse = {tall_kort(forsinkelse)} mnd). Øk slutt eller kort ned forsinkelsen.")
|
||||
reaktivitet = tallverdi(s.get("reaktivitet", STANDARD_REAKTIVITET), "styring.reaktivitet",
|
||||
Grense(0.0, ovre=100.0))
|
||||
mpc: bool | None = None
|
||||
|
|
@ -725,6 +742,166 @@ def sjekk_kombinasjoner(k: Kjoring, sim: ModuleType, utv: ModuleType | None) ->
|
|||
sjekk_endo(k)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- startlikevekten
|
||||
|
||||
|
||||
def tall_g(x: float) -> str:
|
||||
"""Et tall med fire gjeldende sifre, norsk desimaltegn."""
|
||||
return f"{x:.4g}".replace(".", ",")
|
||||
|
||||
|
||||
def oppgitte_nokler(parametere: object) -> dict[str, set[str]]:
|
||||
"""Nøklene fila setter selv under parametere, per gruppe."""
|
||||
ut: dict[str, set[str]] = {}
|
||||
if isinstance(parametere, dict):
|
||||
for navn, d in parametere.items():
|
||||
if isinstance(d, dict):
|
||||
ut[str(navn)] = {str(n) for n in d}
|
||||
return ut
|
||||
|
||||
|
||||
def paadragsrest(sim: ModuleType, k: Kjoring) -> float:
|
||||
"""dK/dt ved start (K = 1, Lambda = Lam0, T = T0, før sjokket), regnet med kjernens funksjoner."""
|
||||
p = k.dicts["P"]
|
||||
om = tallfelt(p, "Omega0")
|
||||
if k.modell == "simulate_soc": # kjernen rekalibrerer Omega0 for rekrutteringsleddet (sim.py:455)
|
||||
s = k.dicts["SOC"]
|
||||
om = om - tallfelt(s, "eps") * float(sim.hill(1.0, s)) + tallfelt(s, "eps") * float(sim.hill(1.0, s))
|
||||
fjernet = float(sim.removal(1.0, tallfelt(p, "Lam0"), tallfelt(p, "T0"), p))
|
||||
return om - fjernet - tallfelt(p, "delta") * 1.0
|
||||
|
||||
|
||||
def strom_j0(p: dict[str, object]) -> float:
|
||||
"""Strømmen til domstolen ved start slik kjernen kalibrerer den (J0 i sim.py:154, uten eta)."""
|
||||
return tallfelt(p, "gamma") * tallfelt(p, "Lam0") * tallfelt(p, "T0") * 1.0
|
||||
|
||||
|
||||
class Startfunn(NamedTuple):
|
||||
"""Det som gjør at starten ikke er en likevekt."""
|
||||
|
||||
kan_kalibreres: list[str]
|
||||
kan_ikke: list[str]
|
||||
rettinger: list[tuple[str, str, float, float]] # (gruppe, nøkkel, gammel, ny)
|
||||
|
||||
|
||||
def funn_grunnmodell(sim: ModuleType, k: Kjoring, oppgitt: dict[str, set[str]], f: Startfunn) -> None:
|
||||
"""Pådrag, mål, tak og proporsjonalitet i grunnmodellen."""
|
||||
p = k.dicts["P"]
|
||||
rest = paadragsrest(sim, k)
|
||||
if abs(rest) > LIKEVEKT_TOL:
|
||||
ny = tallfelt(p, "Omega0") - rest
|
||||
tekst_ = (f"pådraget Omega0 ({tall_g(tallfelt(p, 'Omega0'))}) er ikke lik det politiets innsats fjerner "
|
||||
f"pluss det som faller fra av seg selv ved K = 1 ({tall_g(ny)}), så kriminaliteten ville "
|
||||
"drevet bort fra 1,00 også uten sjokk")
|
||||
if "Omega0" in oppgitt.get("P", set()):
|
||||
f.kan_ikke.append(tekst_ + " (Omega0 er satt i fila)")
|
||||
else:
|
||||
f.kan_kalibreres.append(tekst_)
|
||||
f.rettinger.append(("P", "Omega0", tallfelt(p, "Omega0"), ny))
|
||||
t0, kstar = tallfelt(p, "T0"), tallfelt(p, "Kstar")
|
||||
regel_virker = k.regulator == "MPC" or (k.regulator == "P" and k.reaktivitet > 0)
|
||||
if regel_virker and kstar != 1.0:
|
||||
f.kan_ikke.append(f"målet Kstar er {tall_g(kstar)}, ikke 1, så styringen flytter innsatsen bort fra T0 "
|
||||
"allerede ved start")
|
||||
if k.regulator != "fast" and t0 > tallfelt(p, "Tmax"):
|
||||
f.kan_ikke.append(f"T0 ({tall_g(t0)}) er over taket Tmax ({tall_g(tallfelt(p, 'Tmax'))})")
|
||||
if t0 > tallfelt(p, "Tprop") * 1.0:
|
||||
f.kan_ikke.append(f"T0 ({tall_g(t0)}) er over Tprop ({tall_g(tallfelt(p, 'Tprop'))}) ganger K, så "
|
||||
"innsatsen regnes som uforholdsmessig og tilliten faller fra start")
|
||||
|
||||
|
||||
def funn_domstol(sim: ModuleType, k: Kjoring, navn: str, oppgitt: dict[str, set[str]], f: Startfunn) -> None:
|
||||
"""Domstolens kapasitet og behandlingstid mot strømmen ved start (CH0 eller CH2)."""
|
||||
p, ch = k.dicts["P"], k.dicts[navn]
|
||||
if tallfelt(p, "eta") != 1.0:
|
||||
f.kan_ikke.append(f"eta er {tall_g(tallfelt(p, 'eta'))}, men kjernen regner strømmen til domstolen ved "
|
||||
"start uten eta (init_chain og simulate_chain2 i sim.py), så køen starter ikke i balanse")
|
||||
j0 = strom_j0(p)
|
||||
if j0 <= 0:
|
||||
f.kan_ikke.append("strømmen til domstolen er 0 ved start (gamma, Lam0 eller T0 er 0), så domstolen kan "
|
||||
"ikke kalibreres")
|
||||
return
|
||||
satt = oppgitt.get(navn, set())
|
||||
flyt_endret = any(n in oppgitt.get("P", set()) and p[n] != sim.P[n] for n in FLYTPARAMETERE)
|
||||
z = tallfelt(ch, "Z")
|
||||
if flyt_endret and not {"Z", "m"} <= satt:
|
||||
z_ny = z if "Z" in satt else j0 / float(sim.UPS0)
|
||||
f.kan_kalibreres.append(
|
||||
f"domstolens kapasitet og minste behandlingstid ({navn}.Z, {navn}.m) er notatets, regnet for en "
|
||||
f"strøm på {tall_g(strom_j0(sim.P))} saker per måned, men strømmen er nå {tall_g(j0)}, så "
|
||||
"kapasitet og saksbehandlingstid passer ikke lenger til strømmen")
|
||||
if "Z" not in satt:
|
||||
f.rettinger.append((navn, "Z", z, z_ny))
|
||||
if "m" not in satt:
|
||||
f.rettinger.append((navn, "m", tallfelt(ch, "m") if ch["m"] != "kalibrer" else math.nan, math.nan))
|
||||
z = z_ny
|
||||
andel = (tallfelt(ch, "xi") if navn == "CH0"
|
||||
else tallfelt(ch, "piA") * tallfelt(ch, "xiA") + (1 - tallfelt(ch, "piA")) * tallfelt(ch, "xiB"))
|
||||
if andel * j0 >= z:
|
||||
f.kan_ikke.append(f"domstolen får {tall_g(andel * j0)} saker per måned ved start, minst like mye som "
|
||||
f"kapasiteten {navn}.Z = {tall_g(z)}, så køen kan ikke stå i ro")
|
||||
|
||||
|
||||
def kalibrert_m(sim: ModuleType, k: Kjoring, navn: str) -> float:
|
||||
"""Minste behandlingstid m som gir saksbehandlingstid TAU_REF ved start.
|
||||
|
||||
Som MD i sim.py:155, eller calibrate_m for flere spor.
|
||||
"""
|
||||
ch, p = k.dicts[navn], k.dicts["P"]
|
||||
j0, z, tau = strom_j0(p), tallfelt(ch, "Z"), float(sim.TAU_REF)
|
||||
if navn == "CH2" and ch["c"] != 1:
|
||||
return float(sim.calibrate_m(dict(ch, m=None), p))
|
||||
return tau - j0 / (z * (1.0 / tau + tallfelt(ch, "varsigma")))
|
||||
|
||||
|
||||
def startmelding(f: Startfunn, kan_kalibrere: bool) -> str: # noqa: FBT001 - intern hjelper
|
||||
"""Melding når starten ikke er en likevekt og fila ikke har valgt hva som skal skje."""
|
||||
grunner = "; ".join([*f.kan_kalibreres, *f.kan_ikke])
|
||||
ut = (f"startpunktet er ikke en likevekt: {grunner}. Tallene ville da vist en drift som ikke kommer av "
|
||||
"sjokket eller tiltakene. To veier: ")
|
||||
if kan_kalibrere:
|
||||
ut += ('"startlikevekt": "kalibrer" regner referansetilstanden om (' + ", ".join(
|
||||
f"{g}.{n}" for g, n, _, _ in f.rettinger) + ") slik at startpunktet blir notatets referansetilstand; ")
|
||||
else:
|
||||
ut += '"startlikevekt": "kalibrer" kan ikke rette dette (verdiene er satt i fila eller ligger fast); '
|
||||
return ut + ('"startlikevekt": "ubalanse" kjører fra ubalansen med vilje og viser i sammendraget hvor '
|
||||
"langt modellen driver før sjokket. Se docs/scenarioformat.md.")
|
||||
|
||||
|
||||
def vurder_start(sim: ModuleType, k: Kjoring, parametere: object) -> None:
|
||||
"""Stopp, kalibrer eller merk kjøringen når startpunktet ikke er en likevekt ("startlikevekt")."""
|
||||
oppgitt = oppgitte_nokler(parametere)
|
||||
f = Startfunn([], [], [])
|
||||
funn_grunnmodell(sim, k, oppgitt, f)
|
||||
kjede = next((n for n in ("CH0", "CH2") if n in k.dicts), None)
|
||||
if kjede is not None and not math.isinf(tallfelt(k.dicts[kjede], "Z")):
|
||||
funn_domstol(sim, k, kjede, oppgitt, f)
|
||||
if k.modell == "simulate_endo":
|
||||
f0 = float(sim.removal(1.0, tallfelt(k.dicts["P"], "Lam0"), tallfelt(k.dicts["P"], "T0"), k.dicts["P"]))
|
||||
if abs(f0 - ENDO_FAST_STROM) > LIKEVEKT_TOL:
|
||||
feil("parametere.P", f"simulate_endo i kjernen bruker {tall_g(ENDO_FAST_STROM)} som observert strøm de "
|
||||
f"første månedene (sim.py:588), men med disse parametrene er strømmen ved start {tall_g(f0)}. Det "
|
||||
"endrer hva den koordinerte regelen gjør, og kan ikke rettes fra scenariofila. Å endre gamma, "
|
||||
"Lam0, T0, eta eller ksat i simulate_endo er derfor en endring i kjernen (arbeidskopi og "
|
||||
"ferdigheten forbedre).")
|
||||
if not f.kan_kalibreres and not f.kan_ikke:
|
||||
return
|
||||
if k.startlikevekt == "krev":
|
||||
feil("startlikevekt", startmelding(f, kan_kalibrere=not f.kan_ikke))
|
||||
if k.startlikevekt == "ubalanse":
|
||||
k.ubalanse = [*f.kan_kalibreres, *f.kan_ikke]
|
||||
return
|
||||
if f.kan_ikke:
|
||||
feil("startlikevekt", '"kalibrer" kan ikke gjøre starten til en likevekt: ' + "; ".join(f.kan_ikke)
|
||||
+ '. Endre verdiene, eller bruk "startlikevekt": "ubalanse".')
|
||||
for gruppe, nokkel, gammel, ny in f.rettinger:
|
||||
verdi = kalibrert_m(sim, k, gruppe) if (gruppe, nokkel) in {("CH0", "m"), ("CH2", "m")} else ny
|
||||
k.dicts[gruppe][nokkel] = verdi
|
||||
k.kalibrert.append((f"{gruppe}.{nokkel}", gammel, verdi))
|
||||
if abs(paadragsrest(sim, k)) > LIKEVEKT_TOL:
|
||||
feil("startlikevekt", "kalibreringen ga ikke en likevekt; dette er en feil i scenariokjøreren.")
|
||||
|
||||
|
||||
def les_utvidelse(raa: object, utv: ModuleType | None) -> dict[str, object]:
|
||||
"""Valider utvidelsens parametere mot PARAMETERE (samme type som standardverdien)."""
|
||||
if utv is None:
|
||||
|
|
@ -766,7 +943,7 @@ def los_opp(sim: ModuleType, raa: dict[str, object], modell: str, utv: ModuleTyp
|
|||
variant: Variant) -> Kjoring:
|
||||
"""Valider én variant av scenariofila og lag en kjøring."""
|
||||
objekt(raa, "", TOPPFELT)
|
||||
slutt = tallverdi(raa.get("slutt", float(sim.TEND)), "slutt", Grense(0.0, nedre_med=False, ovre=MAKS_SLUTT))
|
||||
slutt = tallverdi(raa.get("slutt", float(sim.TEND)), "slutt", Grense(MIN_SLUTT, ovre=MAKS_SLUTT))
|
||||
regel = les_regel(raa, modell)
|
||||
regulator, forsinkelse, reaktivitet, mpc = les_styring(raa.get("styring"), modell, slutt)
|
||||
dicts = les_parametere(sim, raa.get("parametere"), parameterdicts(sim, modell, utv), modell)
|
||||
|
|
@ -781,6 +958,8 @@ def los_opp(sim: ModuleType, raa: dict[str, object], modell: str, utv: ModuleTyp
|
|||
mpc_kjenner_kjeden=mpc, regel=regel, slutt=slutt, sjokk=sjokk,
|
||||
tiltak=les_tiltak(raa.get("tiltak"), modell, regel), dicts=dicts,
|
||||
utvidelse=les_utvidelse(raa.get("utvidelse"), utv))
|
||||
k.startlikevekt = tekst(raa.get("startlikevekt", "krev"), "startlikevekt", STARTVALG)
|
||||
vurder_start(sim, k, raa.get("parametere"))
|
||||
sjekk_kombinasjoner(k, sim, utv)
|
||||
return k
|
||||
|
||||
|
|
@ -1008,7 +1187,9 @@ def kjor(sim: ModuleType, k: Kjoring, utv: ModuleType | None) -> Resultat:
|
|||
mpc_knows_chain=bool(k.mpc_kjenner_kjeden)))
|
||||
avledet_kjede(sim, ch, raa)
|
||||
elif k.modell == "simulate_chain2":
|
||||
raa = dict(sim.simulate_chain2(tau, bygg_tiltak(k, "CH2", k.dicts["CH2"]), p=p, kp=kp))
|
||||
ch2 = bygg_tiltak(k, "CH2", k.dicts["CH2"])
|
||||
raa = dict(sim.simulate_chain2(tau, ch2, p=p, kp=kp))
|
||||
avledet_kjede2(ch2, raa)
|
||||
elif k.modell == "simulate_soc":
|
||||
raa = fra_tuppel(sim.simulate_soc(k.regulator, tau, p, s=k.dicts["SOC"], kp=kp,
|
||||
shock=k.sjokk["faktor"], t_on=k.sjokk["fra"],
|
||||
|
|
@ -1031,6 +1212,20 @@ def avledet_kjede(sim: ModuleType, ch: dict[str, object], raa: dict[str, object]
|
|||
raa["Z_rel"] = z / tallfelt(ch, "Z")
|
||||
|
||||
|
||||
def avledet_kjede2(ch2: dict[str, object], raa: dict[str, object]) -> None:
|
||||
"""Kapasitet og andelen av alle saker som går til domstolen over tid i simulate_chain2 (som court2)."""
|
||||
t = raa["t"]
|
||||
if not isinstance(t, np.ndarray):
|
||||
return
|
||||
zfun, xibfun = ch2.get("Zfun"), ch2.get("xiBfun")
|
||||
z = np.array([float(zfun(float(x))) if callable(zfun) else tallfelt(ch2, "Z") for x in t])
|
||||
xib = np.array([float(xibfun(float(x))) if callable(xibfun) else tallfelt(ch2, "xiB") for x in t])
|
||||
pia = tallfelt(ch2, "piA")
|
||||
raa["Z"] = z
|
||||
raa["Z_rel"] = z / tallfelt(ch2, "Z")
|
||||
raa["xi"] = pia * tallfelt(ch2, "xiA") + (1.0 - pia) * xib
|
||||
|
||||
|
||||
def del_opp(raa: dict[str, object], k: Kjoring) -> Resultat:
|
||||
"""Skill tidsserier, skalarer og logg."""
|
||||
t = raa.get("t")
|
||||
|
|
@ -1052,13 +1247,19 @@ def del_opp(raa: dict[str, object], k: Kjoring) -> Resultat:
|
|||
|
||||
def kjor_trygt(sim: ModuleType, k: Kjoring, utv: ModuleType | None) -> Resultat:
|
||||
"""Som kjor, men regnefeil i kjernen blir en norsk melding (exit-kode 2)."""
|
||||
hvem = f" ({kolonneoverskrift(k, 1)})" if k.sveipverdier else ""
|
||||
try:
|
||||
return kjor(sim, k, utv)
|
||||
r = kjor(sim, k, utv)
|
||||
except (ArithmeticError, ValueError) as e:
|
||||
hvem = f" ({kolonneoverskrift(k, 1)})" if k.sveipverdier else ""
|
||||
melding = (f"Kjøringen{hvem} stoppet i kjernen: {e}. Parameterkombinasjonen gir trolig ingen "
|
||||
"likevekt ved start eller en løsning som vokser over alle grenser.")
|
||||
"likevekt ved start eller en løsning som vokser over alle grenser. Ingenting er skrevet.")
|
||||
raise ScenarioFeil(melding) from None
|
||||
ikke_tall = [navn for navn, v in r.serier.items() if not np.all(np.isfinite(v))]
|
||||
if ikke_tall:
|
||||
melding = (f"Kjøringen{hvem} ga verdier som ikke er endelige tall (uendelig eller NaN) i "
|
||||
f"{', '.join(ikke_tall)}: løsningen har vokst over alle grenser. Ingenting er skrevet.")
|
||||
raise ScenarioFeil(melding)
|
||||
return r
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- nøkkeltall
|
||||
|
|
@ -1115,16 +1316,17 @@ def tall_innsats_tillit(n: Nokkeltall, s: dict[str, Arr]) -> None:
|
|||
n.legg("L_slutt", "Tillit, ved slutt", s["L"][-1], 3)
|
||||
|
||||
|
||||
def tall_kjede(n: Nokkeltall, s: dict[str, Arr], endo: bool) -> None: # noqa: FBT001 - intern hjelper
|
||||
"""Saksbehandlingstid, kø og strøm i kjeden (summary, summary2)."""
|
||||
def tall_kjede(n: Nokkeltall, s: dict[str, Arr], endo: bool, j0: float) -> None: # noqa: FBT001 - intern hjelper
|
||||
"""Saksbehandlingstid, kø og strøm i kjeden (summary, summary2). Køen i måneder av strømmen J0."""
|
||||
for navn, tekst_ in (("tauD", "Saksbehandlingstid i domstolen"), ("tauA", "Saksbehandlingstid klasse A"),
|
||||
("tauB", "Saksbehandlingstid klasse B")):
|
||||
if navn in s:
|
||||
n.legg(f"{navn}_maks", f"{tekst_}, lengste (mnd)", s[navn].max(), 1)
|
||||
n.legg(f"{navn}_slutt", f"{tekst_}, ved slutt (mnd)", s[navn][-1], 1)
|
||||
for navn in ("Q", "QA", "QB"):
|
||||
for navn, hva in (("Q", "Kø i domstolen"), ("QA", "Kø klasse A"), ("QB", "Kø klasse B")):
|
||||
if navn in s:
|
||||
n.legg(f"{navn}_slutt", f"Kø {navn}, ved slutt", s[navn][-1], 2)
|
||||
n.legg(f"{navn}_slutt_mnd", f"{hva} ved slutt (mnd innstrømning før sjokket)",
|
||||
float(s[navn][-1]) / j0 if j0 > 0 else None, 1)
|
||||
if "JD" in s and "RD" in s and not endo:
|
||||
rd = float(s["RD"][-1])
|
||||
n.legg("inn_ut_slutt", "Saker inn / avgjort, ved slutt", float(s["JD"][-1]) / rd if rd > 0 else None, 2)
|
||||
|
|
@ -1146,7 +1348,7 @@ def tall_endo(n: Nokkeltall, s: dict[str, Arr], ch: dict[str, object], p: dict[s
|
|||
"""Frafall og kapasitetsår (summary_endo; frafallet i måneder av innstrømningen, som i notatets tabell)."""
|
||||
t = s["t"]
|
||||
z0 = tallfelt(ch, "Z")
|
||||
j0 = tallfelt(p, "gamma") * tallfelt(p, "Lam0") * tallfelt(p, "T0") # J0 i sim.py
|
||||
j0 = strom_j0(p)
|
||||
n.legg("frafall", "Frafall fra køen, i måneder av innstrømningen før sjokket",
|
||||
float(np.trapezoid(tallfelt(ch, "varsigma") * s["Q"], t)) / j0 if j0 > 0 else None, 1)
|
||||
n.legg("kapasitetsaar", "Kapasitetsår over utgangsnivået",
|
||||
|
|
@ -1161,7 +1363,7 @@ def nokkeltall(r: Resultat, k: Kjoring) -> list[Tall]:
|
|||
if "K" in s:
|
||||
tall_krim(n, s, endo)
|
||||
tall_innsats_tillit(n, s)
|
||||
tall_kjede(n, s, endo)
|
||||
tall_kjede(n, s, endo, strom_j0(k.dicts["P"]))
|
||||
tall_tiltak(n, s)
|
||||
if endo and "Z" in s and "Q" in s:
|
||||
tall_endo(n, s, k.dicts["CH0"], k.dicts["P"])
|
||||
|
|
@ -1308,6 +1510,16 @@ def hode(u: Utfall) -> list[str]:
|
|||
linjer += ["", ("Parametrene er notatets stiliserte verdier (tabell 3 i notatet), ikke kalibrert på norske "
|
||||
f"data, unntatt det scenariofila setter selv: {valgt}. Tallene viser modellens dynamikk, "
|
||||
"ikke prognoser."), ""]
|
||||
kalibrert = [(k, k.kalibrert) for k in sc.kjoringer if k.kalibrert]
|
||||
if kalibrert:
|
||||
linjer += [('Referansetilstanden er kalibrert på nytt ("startlikevekt": "kalibrer"): pådraget og domstolen '
|
||||
"er regnet om som i notatets kalibrering, for de nye verdiene:"), ""]
|
||||
for k, endr in kalibrert:
|
||||
hvem = f"{kolonneoverskrift(k, len(sc.sveip))}: " if sc.sveip else ""
|
||||
linjer.append("- " + hvem + "; ".join(
|
||||
f"{sti} {tall_g(ny)}" + ("" if math.isnan(gammel) else f" (var {tall_g(gammel)})")
|
||||
for sti, gammel, ny in endr))
|
||||
linjer.append("")
|
||||
return linjer
|
||||
|
||||
|
||||
|
|
@ -1325,14 +1537,130 @@ def vedtakslinjer(u: Utfall) -> list[str]:
|
|||
return linjer
|
||||
|
||||
|
||||
def sjokkstart(k: Kjoring, r: Resultat) -> float | None:
|
||||
"""Måneden sjokket (eller pådraget fra utvidelsen) begynner; None uten sjokk."""
|
||||
s = r.serier
|
||||
if k.modell == "simulate_soc":
|
||||
return k.sjokk["fra"] if k.sjokk["faktor"] != 1.0 else None
|
||||
if "Omega_faktor" in s:
|
||||
endret = np.flatnonzero(s["Omega_faktor"] != 1.0)
|
||||
return float(s["t"][endret[0]]) if endret.size else None
|
||||
p = k.dicts["P"]
|
||||
return tallfelt(p, "t_shock") if tallfelt(p, "shock") != 1.0 else None
|
||||
|
||||
|
||||
def drift_maaned(k: Kjoring, r: Resultat) -> float:
|
||||
"""Måneden driften før sjokket vises for: sjokkets start, eller måned 12 uten sjokk (høyst slutt)."""
|
||||
m = sjokkstart(k, r)
|
||||
return min(UTEN_SJOKK_MND if m is None else m, k.slutt)
|
||||
|
||||
|
||||
def har_ko(r: Resultat) -> bool:
|
||||
"""Sann for modeller med kø i domstolen (simulate_chain, simulate_chain2, simulate_endo, utvidelse med kjede)."""
|
||||
return "Q" in r.serier or "QA" in r.serier
|
||||
|
||||
|
||||
def driftlinje(k: Kjoring, r: Resultat, dt: float, antall_sveip: int, *, med_grunner: bool) -> str:
|
||||
"""Hvor langt en kjøring har drevet fra start til sjokket (ubalanse, eller kø i domstolen)."""
|
||||
s = r.serier
|
||||
m = drift_maaned(k, r)
|
||||
i = min(round(m / dt), len(s["t"]) - 1)
|
||||
deler = [f"{hva} fra {tall(float(s[navn][0]), d)} til {tall(float(s[navn][i]), d)}"
|
||||
for navn, hva, d in (("K", "K", 3), ("T", "innsatsen T", 2), ("L", "tilliten", 3),
|
||||
("tauD", "saksbehandlingstiden (mnd)", 1), ("tauA", "tiden i klasse A (mnd)", 1),
|
||||
("tauB", "tiden i klasse B (mnd)", 1))
|
||||
if navn in s]
|
||||
hvem = f"{kolonneoverskrift(k, antall_sveip)}: " if antall_sveip else ""
|
||||
grunner = f"{'; '.join(k.ubalanse)}. " if med_grunner else ""
|
||||
deler[0] = deler[0].replace(" fra ", " gått fra ", 1)
|
||||
return f"- {hvem}{grunner}Fram til måned {tall_kort(m)} har " + ", ".join(deler) + "."
|
||||
|
||||
|
||||
KO_DRIFT = ("I modeller med kø i domstolen er dette nesten, men ikke helt, en likevekt: det som faller fra køen, "
|
||||
"blir aldri fjernet fra kriminaliteten, så K og saksbehandlingstiden kryper sakte oppover også uten sjokk.")
|
||||
|
||||
|
||||
def ko_driftlinjer(u: Utfall, kjoringer: list[tuple[Kjoring, Resultat]]) -> list[str]:
|
||||
"""Driften før sjokket i kjøringer med kø som starter i notatets referansetilstand."""
|
||||
med_sjokk = [(k, r) for k, r in kjoringer if (sjokkstart(k, r) or 0.0) > 0.0]
|
||||
uten = len(med_sjokk) < len(kjoringer)
|
||||
linjer: list[str] = []
|
||||
if med_sjokk:
|
||||
linjer += ["", "Slik har modellen drevet før sjokket:", ""]
|
||||
linjer += [driftlinje(k, r, u.dt, len(u.sc.sveip), med_grunner=False) for k, r in med_sjokk]
|
||||
if uten:
|
||||
linjer += ["", ("Uten sjokk (eller med sjokk fra måned 0) kryper modellen med kø sakte oppover av seg selv; en "
|
||||
'kjøring uten sjokk ("sjokk": {"faktor": 1}) viser hvor mye.')]
|
||||
return linjer
|
||||
|
||||
|
||||
def forklaring(u: Utfall) -> list[str]:
|
||||
"""Hvordan tallene leses; byttes ut når starten ikke er en likevekt."""
|
||||
k0 = u.sc.kjoringer[0]
|
||||
lam0 = {tallfelt(k.dicts["P"], "Lam0") for k in u.sc.kjoringer}
|
||||
tillit = f"tilliten er {tall(lam0.pop(), 2)} der" if len(lam0) == 1 else "tilliten er Lam0 der"
|
||||
ko = any(har_ko(r) for r in u.resultater)
|
||||
resten = ("Kapasitet: 1,00 er kapasiteten ved start. Tider i måneder."
|
||||
+ (f" Køen er målt i måneder av innstrømningen til domstolen før sjokket (J0 = "
|
||||
f"{tall_g(strom_j0(k0.dicts['P']))} i modellens enheter); i datafilene står køen i modellens "
|
||||
"enheter." if ko else ""))
|
||||
alle = list(zip(u.sc.kjoringer, u.resultater))
|
||||
skjeve = [(k, r) for k, r in alle if k.ubalanse]
|
||||
rette_med_ko = [(k, r) for k, r in alle if not k.ubalanse and har_ko(r)]
|
||||
if not skjeve and not ko:
|
||||
return [(f"Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket for kriminaliteten K og "
|
||||
f"politiets innsats T; {tillit}. {resten}")]
|
||||
if not skjeve:
|
||||
return [(f"Slik leses tallene: 1,00 er nivået ved start for kriminaliteten K og politiets innsats T "
|
||||
f"(notatets referansetilstand); {tillit}. {KO_DRIFT} {resten}"), *ko_driftlinjer(u, rette_med_ko)]
|
||||
like = len({tuple(k.ubalanse) for k, _ in skjeve}) == 1
|
||||
linjer = [('Slik leses tallene: startpunktet er ikke en likevekt ("startlikevekt": "ubalanse")'
|
||||
+ (": " + "; ".join(skjeve[0][0].ubalanse) if like else "") + ". Tallene er målt fra "
|
||||
"startverdiene (K og T er 1,00 ved start), men modellen ville drevet bort fra dem også uten sjokk, "
|
||||
f"så 1,00 er ikke et nivå den holder seg på. {resten} Slik har modellen drevet før sjokket:"), ""]
|
||||
linjer += [driftlinje(k, r, u.dt, len(u.sc.sveip), med_grunner=not like) for k, r in skjeve]
|
||||
if len(skjeve) < len(u.sc.kjoringer):
|
||||
linjer += ["", ("Kjøringene som ikke er nevnt, starter i notatets referansetilstand." + (f" {KO_DRIFT}" if
|
||||
rette_med_ko else ""))]
|
||||
linjer += ko_driftlinjer(u, rette_med_ko) if rette_med_ko else []
|
||||
return linjer
|
||||
|
||||
|
||||
def merknader(u: Utfall) -> list[str]:
|
||||
"""Merknader (ikke stopp): K langt utenfor modellens område, og sjokk utenfor horisonten."""
|
||||
ut: list[str] = []
|
||||
for k, r in zip(u.sc.kjoringer, u.resultater):
|
||||
hvem = f" ({kolonneoverskrift(k, len(u.sc.sveip))})" if u.sc.sveip else ""
|
||||
if "K" in r.serier:
|
||||
kmax, kmin = float(r.serier["K"].max()), float(r.serier["K"].min())
|
||||
if kmax > K_OMRADE[1] or kmin < K_OMRADE[0]:
|
||||
ut.append(f"Kriminaliteten{hvem} går fra {tall(kmin, 3)} til {tall(kmax, 3)}, utenfor området "
|
||||
f"kjernen selv regner på (K mellom {tall(K_OMRADE[0], 2)} og {tall(K_OMRADE[1], 0)}, der "
|
||||
"soc_equilibria i sim.py leter etter likevekter; notatets egne kjøringer ligger mellom "
|
||||
"0,83 og 9,82). Tallene er regnet slik modellen sier, men ligningene er ikke laget for "
|
||||
"så store utslag.")
|
||||
if k.modell == "simulate_soc":
|
||||
if k.sjokk["fra"] >= k.slutt:
|
||||
ut.append(f"Sjokket{hvem} begynner i måned {tall_kort(k.sjokk['fra'])}, ikke før slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ingen virkning av det.")
|
||||
elif k.sjokk["til"] > k.slutt:
|
||||
ut.append(f"Sjokket{hvem} varer til måned {tall_kort(k.sjokk['til'])}, etter slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ikke at det går over.")
|
||||
elif k.sjokk and k.sjokk["faktor"] != 1.0 and k.sjokk["fra"] >= k.slutt:
|
||||
ut.append(f"Sjokket{hvem} kommer i måned {tall_kort(k.sjokk['fra'])}, ikke før slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ingen virkning av det.")
|
||||
return list(dict.fromkeys(ut))
|
||||
|
||||
|
||||
def sammendrag(u: Utfall) -> str:
|
||||
"""Teksten i sammendrag.md (og på skjermen)."""
|
||||
linjer = [*hode(u), "## Nøkkeltall", "", *nokkeltabeller(u.sc, u.alle)]
|
||||
if len(u.sc.sveip) == RUTENETT:
|
||||
linjer += matrise(u.sc, u.alle)
|
||||
linjer.append("Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket for kriminaliteten K "
|
||||
"og politiets innsats T; tilliten er 0,80 der. Kapasitet: 1,00 er kapasiteten ved start. "
|
||||
"Tider i måneder.")
|
||||
linjer += forklaring(u)
|
||||
merk = merknader(u)
|
||||
if merk:
|
||||
linjer += ["", "## Merknader (kjøringen er ikke stoppet)", "", *(f"- {m}" for m in merk)]
|
||||
linjer += vedtakslinjer(u)
|
||||
linjer += ["", "## Filer", ""]
|
||||
linjer.extend(f"- {navn}: {', '.join(kolonner)}" for navn, kolonner in u.filer)
|
||||
|
|
@ -1378,6 +1706,11 @@ def resultatjson(sim: ModuleType, u: Utfall) -> str:
|
|||
post["skalarer"] = r.skalarer
|
||||
if r.logg:
|
||||
post["vedtak"] = r.logg
|
||||
post["startlikevekt"] = {
|
||||
"valg": k.startlikevekt,
|
||||
"kalibrert": {sti: {"for": gammel, "etter": ny} for sti, gammel, ny in k.kalibrert},
|
||||
"ubalanse": k.ubalanse,
|
||||
}
|
||||
kjoringer.append(post)
|
||||
forklaring: dict[str, str] = {}
|
||||
for tallene in u.alle:
|
||||
|
|
@ -1403,33 +1736,64 @@ def resultatjson(sim: ModuleType, u: Utfall) -> str:
|
|||
return json.dumps(json_verdi(data), ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
|
||||
EGNE_NOKLER = ("scenario", "scenariofil_sha256", "kjerne_sha256", "kjoringer") # i resultat.json
|
||||
EGEN_DATAFIL = re.compile(r"[^/\\]+\.(csv|dat)")
|
||||
|
||||
|
||||
def egne_filer(gammel: Path) -> list[str] | None:
|
||||
"""Filene et tidligere resultat laget, eller None hvis resultat.json ikke er kjørerens."""
|
||||
try:
|
||||
data = json.loads(gammel.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if not isinstance(data, dict) or not all(n in data for n in EGNE_NOKLER):
|
||||
return None
|
||||
kjoringer = data["kjoringer"]
|
||||
if not isinstance(kjoringer, list):
|
||||
return None
|
||||
filer = [str(x.get("fil", "")) for x in kjoringer if isinstance(x, dict)]
|
||||
return [f for f in filer if EGEN_DATAFIL.fullmatch(f) and f not in {".csv", ".dat"}]
|
||||
|
||||
|
||||
def klargjor_mappe(ut: Path) -> None:
|
||||
"""Lag mappa; fjern bare filer et tidligere resultat i samme mappe har laget."""
|
||||
"""Lag mappa; fjern bare datafilene, sammendraget og resultat.json et tidligere resultat laget."""
|
||||
if ut.exists() and not ut.is_dir():
|
||||
melding = f"--ut: {ut} finnes og er ikke en mappe."
|
||||
raise ScenarioFeil(melding)
|
||||
if ut.exists() and any(ut.iterdir()):
|
||||
gammel = ut / "resultat.json"
|
||||
if not gammel.is_file():
|
||||
melding = (f"--ut: mappa {ut} finnes og har filer som ikke er et tidligere resultat. Velg en "
|
||||
"annen mappe med --ut.")
|
||||
filer = egne_filer(ut / "resultat.json")
|
||||
if filer is None:
|
||||
melding = (f"--ut: mappa {ut} finnes og har filer som ikke er et tidligere resultat fra "
|
||||
"scenariokjøreren. Velg en annen mappe med --ut.")
|
||||
raise ScenarioFeil(melding)
|
||||
try:
|
||||
data = json.loads(gammel.read_text(encoding="utf-8"))
|
||||
filer = [str(x["fil"]) for x in data.get("kjoringer", [])]
|
||||
except (OSError, ValueError, TypeError, KeyError, AttributeError):
|
||||
filer = []
|
||||
for navn in [*filer, "sammendrag.md", "resultat.json"]:
|
||||
sti = ut / navn
|
||||
if sti.parent == ut and sti.is_file():
|
||||
if sti.parent == ut and sti.is_file() and not sti.is_symlink():
|
||||
sti.unlink()
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def skriv_alt(sim: ModuleType, sc: Scenario, fil: Path, mappe: Path,
|
||||
resultater: list[Resultat]) -> str:
|
||||
"""Skriv datafilene, sammendraget og resultat.json. Returnerer sammendraget."""
|
||||
alle = [nokkeltall(r, k) for r, k in zip(resultater, sc.kjoringer)]
|
||||
klargjor_mappe(mappe)
|
||||
filer: list[tuple[str, list[str]]] = []
|
||||
for k, r in zip(sc.kjoringer, resultater):
|
||||
navn = filnavn(sc, k)
|
||||
filer.append((navn, skriv_csv(sim, sc, r, mappe / navn)))
|
||||
u = Utfall(sc=sc, fil=fil, alle=alle, resultater=resultater, filer=filer, dt=float(sim.DT))
|
||||
tekst_ = sammendrag(u)
|
||||
(mappe / "sammendrag.md").write_text(tekst_, encoding="utf-8")
|
||||
(mappe / "resultat.json").write_text(resultatjson(sim, u), encoding="utf-8")
|
||||
return tekst_
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- inngang
|
||||
|
||||
|
||||
BRUK = ("Bruk: python3 modell/kjor.py scenario <fil.json> [--ut <mappe>]\n"
|
||||
PYTHON = "python" if os.name == "nt" else "python3" # tolken heter python på Windows
|
||||
BRUK = (f"Bruk: {PYTHON} modell/kjor.py scenario <fil.json> [--ut <mappe>]\n"
|
||||
"Formatet er beskrevet i docs/scenarioformat.md.")
|
||||
|
||||
|
||||
|
|
@ -1468,18 +1832,14 @@ def kjor_scenario(fil: Path, ut: Path | None) -> tuple[str, Path]:
|
|||
sim = last_kjerne()
|
||||
mappe = ut if ut is not None else Path.cwd() / "resultater" / slug(sc.navn)
|
||||
resultater = [kjor_trygt(sim, k, sc.utvidelse) for k in sc.kjoringer]
|
||||
alle = [nokkeltall(r, k) for r, k in zip(resultater, sc.kjoringer)]
|
||||
for r in resultater:
|
||||
kolonner_for(sc, r)
|
||||
klargjor_mappe(mappe)
|
||||
filer: list[tuple[str, list[str]]] = []
|
||||
for k, r in zip(sc.kjoringer, resultater):
|
||||
navn = filnavn(sc, k)
|
||||
filer.append((navn, skriv_csv(sim, sc, r, mappe / navn)))
|
||||
u = Utfall(sc=sc, fil=fil, alle=alle, resultater=resultater, filer=filer, dt=float(sim.DT))
|
||||
tekst_ = sammendrag(u)
|
||||
(mappe / "sammendrag.md").write_text(tekst_, encoding="utf-8")
|
||||
(mappe / "resultat.json").write_text(resultatjson(sim, u), encoding="utf-8")
|
||||
try:
|
||||
tekst_ = skriv_alt(sim, sc, fil, mappe, resultater)
|
||||
except OSError as e:
|
||||
grunn = e.strerror or str(e)
|
||||
melding = f"Kan ikke skrive i {mappe}: {grunn}. Velg en annen mappe med --ut."
|
||||
raise ScenarioFeil(melding) from None
|
||||
return tekst_, mappe
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ PARAMETERE: dict[str, object] = {
|
|||
"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
|
||||
# Scenariofiler i eksempler/scenarioer/notatet/ som utvidelsen med standardverdiene skal gjenskape byte for byte
|
||||
# når "modell" byttes til "utvidelse:paadrag_tidsserie" (tests/test_utvidelser.py kjører dem gjennom kjøreren).
|
||||
GJENSKAPER_NOTATET = ("kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5")
|
||||
FORMER = ("trapp", "lineaer")
|
||||
MAKS_PUNKTER = 1000
|
||||
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue