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>
687 lines
27 KiB
Python
687 lines
27 KiB
Python
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
|
"""Én inngang til modellen bak notatet «Den dynamiske rettsstaten».
|
|
|
|
Bruk:
|
|
python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
|
|
|
|
Bare standardbiblioteket, Python 3.9 eller nyere. Kommandoene står i HJELP under.
|
|
|
|
Exit-koder: 0 ok, 1 forskjeller funnet av kontroll, 2 feil i inndata eller bruk,
|
|
3 miljøet mangler (numpy eller scipy).
|
|
|
|
Valg av Python for kommandoene som trenger numpy og scipy: (1) denne tolken hvis den har
|
|
numpy 2.0 eller nyere (numpy.trapezoid) og scipy; (2) ellers Python i den egne mappa
|
|
<data>/venv, som kjører samme kommando på nytt; (3) ellers exit-kode 3 med en melding om
|
|
hva som mangler. Ingenting installeres uten «miljo --opprett».
|
|
|
|
Grensesnitt for modulene i modell-mappa (scenario.py, kontroll.py, arbeidskopi.py):
|
|
hver har ``main(argv: list[str]) -> int``, der argv er argumentene etter kommandonavnet.
|
|
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
|
|
import os
|
|
import platform
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, NamedTuple, TypedDict
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
from types import ModuleType
|
|
|
|
HER = Path(__file__).resolve().parent
|
|
VENV_MERKE = "DYNAMISK_RETTSSTAT_VENV" # satt når kjor.py er startet på nytt med Python fra den egne mappa
|
|
KJERNEMODULER = ("sim", "diagnose", "forlop")
|
|
MODELLMODULER = ("scenario", "kontroll", "arbeidskopi")
|
|
|
|
OK, FORSKJELLER, FEIL_BRUK, MILJO_MANGLER = 0, 1, 2, 3
|
|
|
|
HJELP = """\
|
|
Bruk: python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
|
|
|
|
Kommandoer:
|
|
diagnose <fil> finn leddet som binder, fra egne tall (trenger bare Python)
|
|
forlop <fil> ["ledd"] stabilitetssjekk og forløp måned for måned (numpy og scipy)
|
|
scenario <fil> [--ut <mappe>] kjør en scenariofil (numpy og scipy)
|
|
notatet [--ut <mappe>] kjør notatets simuleringer uendret, om lag 1-2 min (numpy og scipy)
|
|
kontroll [--rask] kontroller at modellen regner som notatet
|
|
miljo [--opprett] vis Python, numpy og scipy; --opprett installerer dem i en egen mappe
|
|
arbeidskopi <mappe> lag en kopi av modellen som du kan endre
|
|
|
|
Valg før kommandoen:
|
|
--data <mappe> mappa der den egne Python-mappa (venv) med numpy og scipy ligger
|
|
(standard ~/.dynamisk-rettsstat)
|
|
--modell <mappe> bruk modellen i en arbeidskopi i stedet for pluginens
|
|
|
|
Exit-koder: 0 ok, 1 kontroll fant forskjeller, 2 feil i inndata eller bruk,
|
|
3 numpy eller scipy mangler.
|
|
"""
|
|
|
|
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")
|
|
|
|
|
|
class Bruksfeil(Exception):
|
|
"""Feil i kommandolinjen. Meldingen er til brukeren og gir exit-kode 2."""
|
|
|
|
|
|
class Valg(NamedTuple):
|
|
"""Kommandolinjen, tolket."""
|
|
|
|
argv: list[str] # hele kommandolinjen, slik den kom inn
|
|
data: Path # mappa der den egne venv ligger
|
|
data_oppgitt: bool # om --data ble oppgitt med en brukbar verdi
|
|
modell: Path # modell-mappa som brukes (pluginens eller en arbeidskopi)
|
|
kommando: str
|
|
args: list[str] # argumentene etter kommandoen
|
|
|
|
|
|
class Beskrivelse(TypedDict):
|
|
"""Hva en Python-tolk har av numpy og scipy."""
|
|
|
|
python: str
|
|
sti: str
|
|
numpy: str | None
|
|
trapezoid: bool
|
|
scipy: str | None
|
|
|
|
|
|
class Tolk(NamedTuple):
|
|
"""Hvilken Python som skal regne: «denne», «venv» eller «mangler»."""
|
|
|
|
hvor: str
|
|
python: Path | None
|
|
mangler: list[str]
|
|
|
|
|
|
# ---------------- kommandolinjen ----------------
|
|
|
|
def standard_datamappe() -> Path:
|
|
"""Mappa som brukes når --data ikke er oppgitt."""
|
|
return Path.home() / ".dynamisk-rettsstat"
|
|
|
|
|
|
def datamappe(verdi: str | None) -> tuple[Path, bool]:
|
|
"""Tom verdi, eller en verdi med «${» (ikke satt inn av Claude), betyr «ikke oppgitt»."""
|
|
if verdi is None or not verdi.strip() or "${" in verdi:
|
|
return standard_datamappe(), False
|
|
return Path(verdi).expanduser().resolve(), True
|
|
|
|
|
|
def modellmappe(verdi: str | None) -> Path:
|
|
"""Modell-mappa: pluginens egen, eller en arbeidskopi (mappa selv eller mappa over modell/)."""
|
|
if verdi is None:
|
|
return HER
|
|
if not verdi.strip():
|
|
melding = "--modell må ha en mappe etter seg."
|
|
raise Bruksfeil(melding)
|
|
sti = Path(verdi).expanduser().resolve()
|
|
for kandidat in (sti, sti / "modell"):
|
|
if (kandidat / "kjerne").is_dir():
|
|
return kandidat
|
|
melding = f"Fant ingen modell i {sti}: mappa må ha undermappa kjerne/ eller modell/kjerne/."
|
|
raise Bruksfeil(melding)
|
|
|
|
|
|
def tolk_argumenter(argv: list[str]) -> Valg:
|
|
"""Tolk de globale valgene og finn kommandoen."""
|
|
verdier: dict[str, str | None] = {"--data": None, "--modell": None}
|
|
i = 0
|
|
while i < len(argv) and argv[i].startswith("-"):
|
|
arg = argv[i]
|
|
if arg in {"-h", "--hjelp", "--help"}:
|
|
return Valg(argv=argv, data=standard_datamappe(), data_oppgitt=False, modell=HER, kommando="hjelp",
|
|
args=[])
|
|
navn, likhetstegn, verdi = arg.partition("=")
|
|
if navn not in verdier:
|
|
melding = f"Ukjent valg {arg}. Gyldige valg før kommandoen: --data <mappe>, --modell <mappe>."
|
|
raise Bruksfeil(melding)
|
|
if likhetstegn:
|
|
i += 1
|
|
elif i + 1 < len(argv):
|
|
verdi = argv[i + 1]
|
|
i += 2
|
|
else:
|
|
melding = f"{navn} må ha en mappe etter seg."
|
|
raise Bruksfeil(melding)
|
|
verdier[navn] = verdi
|
|
if i >= len(argv):
|
|
melding = "Mangler kommando."
|
|
raise Bruksfeil(melding)
|
|
kommando, args = argv[i], argv[i + 1:]
|
|
if kommando not in KOMMANDOER and kommando != "_beskriv":
|
|
melding = f"Ukjent kommando «{kommando}». Gyldige kommandoer: {', '.join(KOMMANDOER)}."
|
|
raise Bruksfeil(melding)
|
|
data, oppgitt = datamappe(verdier["--data"])
|
|
return Valg(argv, data, oppgitt, modellmappe(verdier["--modell"]), kommando, args)
|
|
|
|
|
|
# ---------------- Python-miljøet ----------------
|
|
|
|
def beskriv_denne() -> Beskrivelse:
|
|
"""Python, numpy og scipy i tolken som kjører nå."""
|
|
ut = Beskrivelse(python=platform.python_version(), sti=sys.executable, numpy=None, trapezoid=False, scipy=None)
|
|
try:
|
|
numpy = importlib.import_module("numpy")
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
ut["numpy"] = str(getattr(numpy, "__version__", "ukjent versjon"))
|
|
ut["trapezoid"] = hasattr(numpy, "trapezoid")
|
|
try:
|
|
importlib.import_module("scipy.optimize")
|
|
scipy = importlib.import_module("scipy")
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
ut["scipy"] = str(getattr(scipy, "__version__", "ukjent versjon"))
|
|
return ut
|
|
|
|
|
|
def mangler(beskrivelse: Beskrivelse) -> list[str]:
|
|
"""Det som mangler for å regne med numpy og scipy, i ord."""
|
|
ut = []
|
|
if beskrivelse["numpy"] is None:
|
|
ut.append("numpy")
|
|
elif not beskrivelse["trapezoid"]:
|
|
ut.append(f"numpy 2.0 eller nyere (har {beskrivelse['numpy']})")
|
|
if beskrivelse["scipy"] is None:
|
|
ut.append("scipy")
|
|
return ut
|
|
|
|
|
|
def beskriv_tolk(python: Path) -> Beskrivelse | None:
|
|
"""Beskriv en annen Python-tolk ved å la den kjøre denne fila. None hvis det ikke går."""
|
|
try:
|
|
resultat = subprocess.run(
|
|
[str(python), str(Path(__file__).resolve()), "_beskriv"],
|
|
capture_output=True, text=True, encoding="utf-8", check=False,
|
|
)
|
|
except OSError:
|
|
return None
|
|
if resultat.returncode != 0:
|
|
return None
|
|
try:
|
|
data = json.loads(resultat.stdout.strip().splitlines()[-1])
|
|
except (ValueError, IndexError):
|
|
return None
|
|
return Beskrivelse(
|
|
python=str(data["python"]), sti=str(data["sti"]), numpy=data["numpy"],
|
|
trapezoid=bool(data["trapezoid"]), scipy=data["scipy"],
|
|
)
|
|
|
|
|
|
def venv_mappe(data: Path) -> Path:
|
|
"""Den egne Python-mappa med numpy og scipy."""
|
|
return data / "venv"
|
|
|
|
|
|
def venv_python(data: Path) -> Path:
|
|
"""Python-tolken i den egne mappa."""
|
|
if os.name == "nt":
|
|
return venv_mappe(data) / "Scripts" / "python.exe"
|
|
return venv_mappe(data) / "bin" / "python"
|
|
|
|
|
|
def velg_tolk(data: Path) -> Tolk:
|
|
"""Velg Python for kommandoene som trenger numpy og scipy."""
|
|
mangel = mangler(beskriv_denne())
|
|
if not mangel:
|
|
return Tolk("denne", Path(sys.executable), [])
|
|
kandidat = venv_python(data)
|
|
if kandidat.exists() and os.environ.get(VENV_MERKE) != "1":
|
|
return Tolk("venv", kandidat, mangel)
|
|
return Tolk("mangler", None, mangel)
|
|
|
|
|
|
def kjor_paa_nytt(python: Path, argv: list[str]) -> int:
|
|
"""Kjør samme kommando med en annen Python og gi videre exit-koden."""
|
|
sys.stdout.flush()
|
|
miljo = dict(os.environ)
|
|
miljo[VENV_MERKE] = "1"
|
|
return subprocess.run([str(python), str(Path(__file__).resolve()), *argv], env=miljo, check=False).returncode
|
|
|
|
|
|
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)]
|
|
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:
|
|
"""Norsk melding for exit-kode 3."""
|
|
linjer = [
|
|
f"«{valg.kommando}» trenger numpy (2.0 eller nyere) og scipy. Mangler: {', '.join(tolk.mangler)}.",
|
|
f"Python som ble brukt: {sys.executable} ({platform.python_version()}).",
|
|
]
|
|
if os.environ.get(VENV_MERKE) == "1":
|
|
linjer.append(f"Den egne mappa {venv_mappe(valg.data)} mangler dem også. Kommandoen under installerer "
|
|
"dem der på nytt fra PyPI (krever internett):")
|
|
else:
|
|
linjer.append(f"Fant ingen egen mappe med numpy og scipy i {venv_mappe(valg.data)}. Kommandoen under "
|
|
"lager den og installerer numpy og scipy fra PyPI der (krever internett):")
|
|
linjer.append(f" {kommando_tekst(valg, 'miljo', '--opprett')}")
|
|
linjer.append("Ingenting installeres uten at du ber om det. diagnose virker uten numpy og scipy.")
|
|
return "\n".join(linjer)
|
|
|
|
|
|
# ---------------- å kjøre kjernen og modulene ----------------
|
|
|
|
def forbered_stier(modell: Path) -> None:
|
|
"""Legg modell-mappa og kjernen først i sys.path, og glem moduler lastet fra en annen mappe."""
|
|
sys.dont_write_bytecode = True # ingen __pycache__ i plugin-mappa
|
|
for navn in (*KJERNEMODULER, *MODELLMODULER):
|
|
sys.modules.pop(navn, None)
|
|
fjern = {str(modell), str(modell / "kjerne")}
|
|
if modell != HER:
|
|
fjern.add(str(HER))
|
|
sys.path[:] = [sti for sti in sys.path if str(Path(sti or ".").resolve()) not in fjern]
|
|
sys.path[:0] = [str(modell / "kjerne"), str(modell)]
|
|
|
|
|
|
def exit_kode(kode: object) -> int:
|
|
"""Exit-koden for et sys.exit(kode) fra kjernen eller en modul."""
|
|
if kode is None:
|
|
return OK
|
|
if isinstance(kode, int):
|
|
return kode
|
|
sys.stdout.flush()
|
|
print(kode, file=sys.stderr)
|
|
return FEIL_BRUK
|
|
|
|
|
|
def fang(funksjon: Callable[[], object], hva: str) -> int:
|
|
"""Kjør funksjonen og gjør sys.exit og uventede feil om til exit-koder."""
|
|
try:
|
|
resultat = funksjon()
|
|
except SystemExit as stopp:
|
|
return exit_kode(stopp.code)
|
|
except Exception: # noqa: BLE001 - alt som ikke er SystemExit, er en feil i koden, ikke i inndata
|
|
sys.stdout.flush()
|
|
traceback.print_exc()
|
|
print(f"Uventet feil i {hva} (se over).", file=sys.stderr)
|
|
return FEIL_BRUK
|
|
if isinstance(resultat, int) and not isinstance(resultat, bool):
|
|
return resultat
|
|
return OK
|
|
|
|
|
|
def last_kjerne(modell: Path, navn: str) -> ModuleType:
|
|
"""Last en kjernemodul fra modell-mappa/kjerne."""
|
|
forbered_stier(modell)
|
|
return importlib.import_module(navn)
|
|
|
|
|
|
def last_modul(modell: Path, navn: str) -> ModuleType | None:
|
|
"""Last modell-mappa/<navn>.py. None hvis fila ikke finnes."""
|
|
fil = modell / f"{navn}.py"
|
|
if not fil.is_file():
|
|
return None
|
|
forbered_stier(modell)
|
|
spec = importlib.util.spec_from_file_location(navn, fil)
|
|
if spec is None or spec.loader is None:
|
|
return None
|
|
modul = importlib.util.module_from_spec(spec)
|
|
sys.modules[navn] = modul
|
|
spec.loader.exec_module(modul)
|
|
return modul
|
|
|
|
|
|
def kjor_modul(valg: Valg, navn: str) -> int:
|
|
"""Kall main(argv) i modell-mappa/<navn>.py med argumentene etter kommandoen."""
|
|
fil = valg.modell / f"{navn}.py"
|
|
if not fil.is_file():
|
|
print(f"Kommandoen «{valg.kommando}» er ikke på plass i denne modellen ennå: fant ikke {fil}.",
|
|
file=sys.stderr)
|
|
return FEIL_BRUK
|
|
|
|
def start() -> object:
|
|
main = getattr(last_modul(valg.modell, navn), "main", None)
|
|
if not callable(main):
|
|
sys.exit(f"{fil} mangler funksjonen main(argv).")
|
|
return main(list(valg.args))
|
|
|
|
return fang(start, f"{navn}.py")
|
|
|
|
|
|
# ---------------- 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")
|
|
|
|
|
|
def kommando_forlop(valg: Valg) -> int:
|
|
"""Kjernens forlop.main."""
|
|
if len(valg.args) not in {1, 2}:
|
|
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"
|
|
args = list(valg.args)
|
|
if args:
|
|
if len(args) != 2 or args[0] != "--ut": # noqa: PLR2004 - «--ut <mappe>»
|
|
melding = "Bruk: kjor.py notatet [--ut <mappe>]"
|
|
raise Bruksfeil(melding)
|
|
ut = Path(args[1]).expanduser()
|
|
ut = ut.resolve()
|
|
if ut == valg.modell or valg.modell in ut.parents:
|
|
melding = f"Ut-mappa {ut} ligger inne i modell-mappa {valg.modell}. Velg en annen med --ut <mappe>."
|
|
raise Bruksfeil(melding)
|
|
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)
|
|
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")
|
|
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:
|
|
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:
|
|
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}.")
|
|
print("«kontroll» sammenligner pluginens egen kjøring med notatet.")
|
|
return OK
|
|
|
|
|
|
def laaste_versjoner(modell: Path) -> dict[str, str]:
|
|
"""Notatets låste versjoner fra modell/requirements.txt."""
|
|
ut = {}
|
|
fil = modell / "requirements.txt"
|
|
if fil.is_file():
|
|
for linje in fil.read_text(encoding="utf-8").splitlines():
|
|
navn, skille, versjon = linje.split("#")[0].strip().partition("==")
|
|
if skille:
|
|
ut[navn.strip().lower()] = versjon.strip()
|
|
return ut
|
|
|
|
|
|
def vis_tolk(beskrivelse: Beskrivelse, laast: dict[str, str]) -> list[str]:
|
|
"""Linjer som beskriver Python, numpy og scipy i en tolk."""
|
|
linjer = [f" Python {beskrivelse['python']} ({beskrivelse['sti']})"]
|
|
for navn in ("numpy", "scipy"):
|
|
versjon = beskrivelse["numpy"] if navn == "numpy" else beskrivelse["scipy"]
|
|
notatet = f" (notatet: {laast[navn]})" if navn in laast else ""
|
|
linjer.append(f" {navn}: {versjon or 'mangler'}{notatet}")
|
|
return linjer
|
|
|
|
|
|
def samme_som_notatet(beskrivelse: Beskrivelse, laast: dict[str, str]) -> bool:
|
|
"""Sjekk om tolken har notatets låste versjoner av numpy og scipy."""
|
|
return beskrivelse["numpy"] == laast.get("numpy") and beskrivelse["scipy"] == laast.get("scipy")
|
|
|
|
|
|
def rapport_miljo(valg: Valg) -> None:
|
|
"""Skriv ut hva denne Python og den egne mappa har."""
|
|
laast = laaste_versjoner(valg.modell)
|
|
denne = beskriv_denne()
|
|
print("Python som kjører nå:")
|
|
print("\n".join(vis_tolk(denne, laast)))
|
|
vpy = venv_python(valg.data)
|
|
print(f"\nEgen mappe for numpy og scipy: {venv_mappe(valg.data)}")
|
|
venv: Beskrivelse | None = None
|
|
if not vpy.exists():
|
|
print(" finnes ikke. «miljo --opprett» lager den og installerer numpy og scipy fra PyPI "
|
|
"(krever internett).")
|
|
else:
|
|
venv = beskriv_tolk(vpy)
|
|
if venv is None:
|
|
print(f" finnes, men {vpy} lot seg ikke starte. «miljo --opprett» prøver å lage den på nytt.")
|
|
else:
|
|
print("\n".join(vis_tolk(venv, laast)))
|
|
print()
|
|
regner = None
|
|
if not mangler(denne):
|
|
regner = denne
|
|
print("forlop, scenario, notatet og kontroll regner med Python som kjører nå.")
|
|
elif venv is not None and not mangler(venv):
|
|
regner = venv
|
|
print("forlop, scenario, notatet og kontroll regner med Python i den egne mappa.")
|
|
else:
|
|
hva = ", ".join(mangler(venv if venv is not None else denne))
|
|
print(f"forlop, scenario, notatet og kontroll kan ikke regne nå: {hva} mangler.")
|
|
if regner is not None:
|
|
if samme_som_notatet(regner, laast):
|
|
print("Versjonene av numpy og scipy er de samme som notatets, så kontroll kan gi bit for bit.")
|
|
else:
|
|
print("Versjonene av numpy og scipy er ikke de samme som notatets: kontroll sammenligner da "
|
|
"med toleranse, ikke bit for bit.")
|
|
|
|
|
|
def kjor_prosess(kommando: list[str]) -> int:
|
|
"""Kjør et program (pip, venv) med utskriften rett til skjermen. Exit-koden tilbake."""
|
|
sys.stdout.flush()
|
|
try:
|
|
return subprocess.run(kommando, check=False).returncode
|
|
except OSError as feil:
|
|
print(f"Kunne ikke starte {kommando[0]}: {feil}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def opprett_miljo(valg: Valg) -> int:
|
|
"""Lag den egne mappa og installer numpy og scipy: låste versjoner først, ellers nyeste."""
|
|
mappe = venv_mappe(valg.data)
|
|
vpy = venv_python(valg.data)
|
|
if not vpy.exists():
|
|
print(f"Lager egen Python-mappa {mappe} med {sys.executable} ({platform.python_version()}).")
|
|
valg.data.mkdir(parents=True, exist_ok=True)
|
|
if kjor_prosess([sys.executable, "-m", "venv", str(mappe)]) != 0 or not vpy.exists():
|
|
print(f"Kunne ikke lage {mappe}. På noen Linux-systemer må pakken python3-venv installeres først.",
|
|
file=sys.stderr)
|
|
return MILJO_MANGLER
|
|
pip = [str(vpy), "-m", "pip", "install", "--disable-pip-version-check"]
|
|
krav = valg.modell / "requirements.txt"
|
|
laast = laaste_versjoner(valg.modell)
|
|
print(f"\nInstallerer notatets versjoner ({', '.join(f'{n} {v}' for n, v in laast.items())}) fra PyPI.")
|
|
toleranse = False
|
|
if kjor_prosess([*pip, "-r", str(krav)]) != 0:
|
|
print("\nNotatets låste versjoner lot seg ikke installere for denne Python-versjonen. "
|
|
"Prøver numpy 2.0 eller nyere og scipy.")
|
|
toleranse = True
|
|
if kjor_prosess([*pip, "numpy>=2.0", "scipy"]) != 0:
|
|
print(f"\nInstallasjonen feilet. Se meldingene over. Mappa {mappe} kan slettes.", file=sys.stderr)
|
|
return MILJO_MANGLER
|
|
print()
|
|
rapport_miljo(valg)
|
|
if toleranse:
|
|
print("\nNumpy og scipy er ikke notatets låste versjoner. Resultatene kontrolleres da med toleranse, "
|
|
"ikke bit for bit.")
|
|
return OK
|
|
|
|
|
|
def kommando_miljo(valg: Valg) -> int:
|
|
"""Vis miljøet, eller lag den egne mappa med --opprett."""
|
|
if valg.args == ["--opprett"]:
|
|
return opprett_miljo(valg)
|
|
if valg.args:
|
|
melding = "Bruk: kjor.py miljo [--opprett]"
|
|
raise Bruksfeil(melding)
|
|
rapport_miljo(valg)
|
|
return OK
|
|
|
|
|
|
def kommando_beskriv(_valg: Valg) -> int:
|
|
"""Intern kommando for beskriv_tolk(): skriv beskrivelsen av denne tolken som JSON."""
|
|
print(json.dumps(beskriv_denne()))
|
|
return OK
|
|
|
|
|
|
def kommando_hjelp(_valg: Valg) -> int:
|
|
"""Skriv bruken."""
|
|
print(HJELP, end="")
|
|
return OK
|
|
|
|
|
|
def kommando_regner(valg: Valg) -> int:
|
|
"""Kommandoene som trenger numpy og scipy: velg Python først (se toppen av fila)."""
|
|
if valg.kommando == "scenario" and not (valg.modell / "scenario.py").is_file():
|
|
return kjor_modul(valg, "scenario") # sier at kommandoen ikke er på plass ennå
|
|
if valg.kommando == "forlop" and valg.args and not Path(valg.args[0]).exists():
|
|
print(f"Fant ikke fila {valg.args[0]}.", file=sys.stderr)
|
|
return FEIL_BRUK
|
|
tolk = velg_tolk(valg.data)
|
|
if tolk.hvor == "venv" and tolk.python is not None:
|
|
return kjor_paa_nytt(tolk.python, valg.argv)
|
|
if tolk.hvor == "mangler" and valg.kommando != "kontroll": # kontroll gjør det den kan uten
|
|
print(melding_mangler(valg, tolk), file=sys.stderr)
|
|
return MILJO_MANGLER
|
|
handlinger: dict[str, Callable[[Valg], int]] = {
|
|
"forlop": kommando_forlop, "notatet": kommando_notatet,
|
|
"scenario": lambda v: kjor_modul(v, "scenario"), "kontroll": lambda v: kjor_modul(v, "kontroll"),
|
|
}
|
|
return handlinger[valg.kommando](valg)
|
|
|
|
|
|
def utfor(valg: Valg) -> int:
|
|
"""Utfør kommandoen."""
|
|
handlinger: dict[str, Callable[[Valg], int]] = {
|
|
"hjelp": kommando_hjelp, "_beskriv": kommando_beskriv,
|
|
"diagnose": kommando_diagnose, "miljo": kommando_miljo,
|
|
"arbeidskopi": lambda v: kjor_modul(v, "arbeidskopi"),
|
|
}
|
|
return handlinger.get(valg.kommando, kommando_regner)(valg)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Inngangen. Returnerer exit-koden."""
|
|
try:
|
|
if not argv:
|
|
print(HJELP, end="", file=sys.stderr)
|
|
return FEIL_BRUK
|
|
return utfor(tolk_argumenter(argv))
|
|
except Bruksfeil as feil:
|
|
sys.stdout.flush()
|
|
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
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|