scenario.py runs all five simulators of the core from a JSON scenario file, with sweeps, and reproduces 20 of the note's data files (14 byte for byte, 6 column-wise). Extensions live outside the core and must reproduce it exactly in a limit case; paadrag_tidsserie is the worked example. arbeidskopi.py gives the user a copy of the model they can change. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
208 lines
8.7 KiB
Python
208 lines
8.7 KiB
Python
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
|
"""Arbeidskopi: kopier modellen til en mappe brukeren eier og kan endre.
|
|
|
|
Bruk:
|
|
python3 modell/kjor.py arbeidskopi <mappe>
|
|
python3 modell/arbeidskopi.py <mappe>
|
|
|
|
Kopierer modell/, eksempler/, fasit/, tests/ og docs/ (det som finnes), og
|
|
pyproject.toml og LICENSE når de finnes, og skriver LES-MEG.md i mappa. Nekter
|
|
hvis mappa finnes og ikke er tom. Kopierer ikke .venv, __pycache__, mellomlagre
|
|
eller resultater/. Bare standardbiblioteket.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shlex
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MODELLMAPPE = Path(__file__).resolve().parent
|
|
ROT = MODELLMAPPE.parent
|
|
MAPPER = ("modell", "eksempler", "fasit", "tests", "docs")
|
|
FILER = ("pyproject.toml", "LICENSE")
|
|
HOPP_OVER = (".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "resultater",
|
|
"rettsstat-modell", ".DS_Store", "*.pyc", "*.pyo")
|
|
NODVENDIG = ("kjor.py", "scenario.py", "kontroll.py", "arbeidskopi.py", "requirements.txt",
|
|
"kjerne/sim.py", "kjerne/diagnose.py", "kjerne/forlop.py", "kjerne/KILDE.json",
|
|
"utvidelser/__init__.py")
|
|
DOKUMENTER = (
|
|
("scenarioformat.md", "scenariofila: alle felt, sveip og utdata"),
|
|
("utvidelser.md", "hvordan du legger til en mekanisme som utvidelse, med mal"),
|
|
("modellkart.md", "fra ligning i notatet til funksjon og parameter i koden"),
|
|
("inndata.md", "inndatafila til diagnose og forlop"),
|
|
("forbehold.md", "hva modellen kan og ikke kan brukes til"),
|
|
)
|
|
BRUK = "Bruk: python3 modell/kjor.py arbeidskopi <mappe>"
|
|
|
|
|
|
class KopiFeil(Exception):
|
|
"""Feil i bruken eller i kildemappa; gir exit-kode 2."""
|
|
|
|
|
|
def sjekk_kilde() -> None:
|
|
"""Stopp hvis modell/ ikke er komplett."""
|
|
mangler = [n for n in NODVENDIG if not (MODELLMAPPE / n).is_file()]
|
|
if mangler:
|
|
melding = (f"Modellen i {MODELLMAPPE} er ikke komplett, så arbeidskopien ville ikke virket. Mangler: "
|
|
+ ", ".join(f"modell/{n}" for n in mangler) + ".")
|
|
raise KopiFeil(melding)
|
|
|
|
|
|
def sjekk_mal(mal: Path) -> None:
|
|
"""Mappa må være ny eller tom, og ikke ligge inne i det som kopieres."""
|
|
if mal.exists() and not mal.is_dir():
|
|
melding = f"{mal} finnes og er en fil, ikke en mappe."
|
|
raise KopiFeil(melding)
|
|
if mal.is_dir() and any(mal.iterdir()):
|
|
melding = (f"Mappa {mal} finnes og er ikke tom. Velg en ny mappe; arbeidskopi skriver aldri over noe "
|
|
"som finnes.")
|
|
raise KopiFeil(melding)
|
|
for navn in MAPPER:
|
|
kilde = (ROT / navn).resolve()
|
|
if mal == kilde or kilde in mal.parents:
|
|
melding = f"{mal} ligger inne i {kilde}, som skal kopieres. Velg en mappe utenfor."
|
|
raise KopiFeil(melding)
|
|
|
|
|
|
def kildeversjon() -> str:
|
|
"""Commit i notatets repo som kjernen er kopiert fra (fra KILDE.json)."""
|
|
try:
|
|
data = json.loads((MODELLMAPPE / "kjerne" / "KILDE.json").read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return "ukjent"
|
|
if not isinstance(data, dict):
|
|
return "ukjent"
|
|
commit = str(data.get("commit", "ukjent"))
|
|
repo = str(data.get("repo", data.get("url", "")))
|
|
return f"{repo} @ {commit[:7]}" if repo else commit[:7]
|
|
|
|
|
|
def les_meg(mal: Path, docs: list[str]) -> str:
|
|
"""Innholdet i LES-MEG.md."""
|
|
kjor = f"python3 {shlex.quote(str(mal / 'modell' / 'kjor.py'))}"
|
|
eks = mal / "eksempler"
|
|
|
|
def q(sti: Path) -> str:
|
|
return shlex.quote(str(sti))
|
|
|
|
linjer = [
|
|
"# Arbeidskopi av modellen bak «Den dynamiske rettsstaten»",
|
|
"",
|
|
(f"Denne mappa er din egen kopi av modellen i pluginen dynamisk-rettsstat. Kjernen i `modell/kjerne/` er "
|
|
f"en uendret kopi av notatets kode ({kildeversjon()}); sjekksummene står i `modell/kjerne/KILDE.json`."),
|
|
"",
|
|
"## Hva du kan endre",
|
|
"",
|
|
"Alt i mappa kan endres, også kjernen. Tre veier, fra minst til mest inngripende:",
|
|
"",
|
|
"1. Andre forutsetninger: skriv en scenariofil (se `docs/scenarioformat.md`). Ingen kode endres.",
|
|
("2. En ny mekanisme ved siden av kjernen: legg den i `modell/utvidelser/` (se `docs/utvidelser.md`). "
|
|
"Regelen er den samme som i notatet: utvidelsen må ha et grensetilfelle som gjenskaper kjernen eksakt."),
|
|
("3. Endre kjernen selv (`modell/kjerne/sim.py`). Kontrollen under sier da hvilke av notatets resultater "
|
|
"som flytter seg, og hvor mye."),
|
|
"",
|
|
"## Slik kjører du kopien",
|
|
"",
|
|
"Kommandoene bruker koden i denne mappa:",
|
|
"",
|
|
"```",
|
|
f"{kjor} scenario {q(eks / 'scenarioer' / 'avviklingstid_koordinert.json')}",
|
|
f"{kjor} diagnose {q(eks / 'eksempel_domstol.json')}",
|
|
f"{kjor} forlop {q(eks / 'eksempel_domstol.json')}",
|
|
"```",
|
|
"",
|
|
"Fra pluginen kan du peke på kopien med `--modell`:",
|
|
"`python3 <plugin>/modell/kjor.py --modell " + q(mal) + " scenario <fil.json>`. Ferdighetene i "
|
|
"pluginen bruker kopien selv når mappa du jobber i, har en `rettsstat-modell/`.",
|
|
"",
|
|
("Scenario, forløp og kontroll trenger numpy og scipy. Mangler de, sier kommandoen fra; "
|
|
f"`{kjor} miljo --opprett` installerer dem i en egen mappe."),
|
|
"",
|
|
"## Slik kontrollerer du en endring",
|
|
"",
|
|
"```",
|
|
f"{kjor} kontroll # alt, om lag 1-2 minutter",
|
|
f"{kjor} kontroll --rask # uten den fulle kjøringen av sim.py",
|
|
"```",
|
|
"",
|
|
"Kontrollen sammenligner med notatets resultater i `fasit/` og ender med ett av tre nivåer:",
|
|
"",
|
|
"- «bit for bit som notatet»: endringen flytter ingen av notatets resultater.",
|
|
("- «innenfor toleranse»: tallene avviker med høyst én enhet i siste viste siffer, som med andre versjoner "
|
|
"av numpy og scipy."),
|
|
("- «endret: ...»: noe har flyttet seg. I en arbeidskopi er det en opplysning, ikke en feil. Detaljene "
|
|
"sier hvilke datafiler og utskrifter som er endret, og største avvik per kolonne."),
|
|
"",
|
|
"Kontrollen kjører også `grensetilfelle()` i hver utvidelse; hvert avvik skal være eksakt 0.",
|
|
"Testene kjøres med `python3 -m pytest` i mappa (krever pytest; de trege med `-m treg`).",
|
|
"",
|
|
"## Dokumentasjon",
|
|
"",
|
|
]
|
|
if docs:
|
|
linjer.extend(f"- `docs/{navn}`: {hva}" for navn, hva in DOKUMENTER if navn in docs)
|
|
linjer.extend(f"- `docs/{navn}`" for navn in docs if navn not in {n for n, _ in DOKUMENTER})
|
|
else:
|
|
linjer.append("- (docs/ fantes ikke da kopien ble laget)")
|
|
linjer += [
|
|
"",
|
|
"## Sende en forbedring tilbake",
|
|
"",
|
|
("Se `BIDRA.md` i pluginen. Kort: lag en patch av endringene (for eksempel med `git diff` hvis du har lagt "
|
|
"mappa i git) og send den på e-post med en forklaring og utskriften fra kontrollen."),
|
|
"",
|
|
"Tekst og kode er under lisensene i pluginen (MIT for kode og data, se `LICENSE`).",
|
|
"",
|
|
]
|
|
return "\n".join(linjer)
|
|
|
|
|
|
def kopier(mal: Path) -> int:
|
|
"""Lag arbeidskopien. Returnerer antall filer."""
|
|
sjekk_kilde()
|
|
sjekk_mal(mal)
|
|
mal.mkdir(parents=True, exist_ok=True)
|
|
ignorer = shutil.ignore_patterns(*HOPP_OVER)
|
|
for navn in MAPPER:
|
|
kilde = ROT / navn
|
|
if kilde.is_dir():
|
|
shutil.copytree(kilde, mal / navn, ignore=ignorer)
|
|
for navn in FILER:
|
|
if (ROT / navn).is_file():
|
|
shutil.copy2(ROT / navn, mal / navn)
|
|
docs = sorted(p.name for p in (mal / "docs").glob("*.md")) if (mal / "docs").is_dir() else []
|
|
(mal / "LES-MEG.md").write_text(les_meg(mal, docs), encoding="utf-8")
|
|
return sum(1 for p in mal.rglob("*") if p.is_file())
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Kommandoen arbeidskopi. Exit-kode 0 ok, 2 feil i bruken."""
|
|
if argv and argv[0] in {"-h", "--hjelp", "--help"}:
|
|
sys.stdout.write(BRUK + "\n")
|
|
return 0
|
|
if len(argv) != 1 or not argv[0].strip():
|
|
sys.stderr.write(BRUK + "\n")
|
|
return 2
|
|
mal = Path(argv[0]).expanduser().resolve()
|
|
try:
|
|
antall = kopier(mal)
|
|
except KopiFeil as e:
|
|
sys.stderr.write(f"{e}\n")
|
|
return 2
|
|
except OSError as e:
|
|
sys.stderr.write(f"Kunne ikke lage arbeidskopien i {mal}: {e}\n")
|
|
return 2
|
|
kjor = shlex.quote(str(mal / "modell" / "kjor.py"))
|
|
sys.stdout.write(f"Arbeidskopien er laget i {mal} ({antall} filer).\n"
|
|
f"Les {mal / 'LES-MEG.md'} først. Kjør den med:\n"
|
|
f" python3 {kjor} scenario <fil.json>\n"
|
|
f"og kontroller endringer med:\n"
|
|
f" python3 {kjor} kontroll\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|