feat(scenario): scenario runner, extensions and working copy
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>
This commit is contained in:
parent
464ad38b49
commit
03151cfb3b
36 changed files with 3580 additions and 5 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -41,11 +41,11 @@ justice chain (mostly non-programmers) and researchers. The note and its code li
|
|||
passes through as 2 with the message unchanged), 3 numpy/scipy missing.
|
||||
|
||||
## Commands
|
||||
- Development environment: `python3 -m venv .venv && .venv/bin/pip install -r modell/requirements.txt pytest ruff mypy`
|
||||
- Tests: `.venv/bin/python -m pytest tests/`
|
||||
- Lint and types (not the core): `.venv/bin/ruff check modell tests` and `.venv/bin/mypy modell`
|
||||
- Reproduction check against the note: `python3 modell/kjor.py kontroll` (full run, about 2 min) or
|
||||
`python3 modell/kjor.py kontroll --rask`
|
||||
- Development environment (versions pinned in `pyproject.toml`, group `dev`): `python3 -m venv .venv && .venv/bin/pip install --group dev`
|
||||
- Tests: `.venv/bin/pytest` (fast, about 1.5 min); `.venv/bin/pytest -m treg` runs the note's full simulation (about 2.5 min)
|
||||
- Lint and types (the core is excluded in `pyproject.toml`): `.venv/bin/ruff check .` and `.venv/bin/mypy .`
|
||||
- Reproduction check against the note: `.venv/bin/python modell/kjor.py kontroll` (full run, about 75 s) or
|
||||
`.venv/bin/python modell/kjor.py kontroll --rask`
|
||||
- Try the skills' commands: `python3 modell/kjor.py diagnose eksempler/eksempel_domstol.json`,
|
||||
`python3 modell/kjor.py forlop eksempler/eksempel_domstol.json`
|
||||
- Validate the plugin: `claude plugin validate .`
|
||||
|
|
|
|||
327
docs/scenarioformat.md
Normal file
327
docs/scenarioformat.md
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
# Scenarioformatet
|
||||
|
||||
En scenariofil er en JSON-fil som sier hvilken av modellens simuleringer som skal kjøres, med hvilke forutsetninger.
|
||||
Scenariokjøreren (`modell/scenario.py`) kaller kjernen i `modell/kjerne/sim.py` uendret med verdiene i fila, og
|
||||
skriver et sammendrag, tidsseriene og alle parametere slik de ble brukt.
|
||||
|
||||
```
|
||||
python3 modell/kjor.py scenario <fil.json> [--ut <mappe>]
|
||||
```
|
||||
|
||||
Alt som ikke står i fila, er notatets verdier (tabell 3 i notatet). De er stiliserte og ikke kalibrert på norske
|
||||
data, og sammendraget sier det. Tallene viser modellens dynamikk, ikke prognoser. Feltnavnene er norske; navnene på
|
||||
modellens parametere er kjernens egne, slik at de kan slås opp i `sim.py` og i [modellkart.md](modellkart.md).
|
||||
|
||||
## Et første eksempel
|
||||
|
||||
```json
|
||||
{
|
||||
"navn": "kapasitet_tidlig",
|
||||
"beskrivelse": "Hva om domstolens kapasitet økes allerede fra måned 12, ikke 24?",
|
||||
"modell": "simulate_chain",
|
||||
"tiltak": {"kapasitet": {"faktor": 1.34, "fra": 12, "over": 24}},
|
||||
"sveip": {"parameter": "styring.forsinkelse", "verdier": [3, 6, 11]}
|
||||
}
|
||||
```
|
||||
|
||||
Dette kjører den koblede modellen med domstolskø (avsnitt 8.2) tre ganger, med forsinkelse 3, 6 og 11 måneder i
|
||||
politiets styring, og med kapasiteten i domstolen økt med 34 % lineært fra måned 12 til 36. Resten er notatets
|
||||
regime A og scenario 2.
|
||||
|
||||
## Feltene
|
||||
|
||||
| Felt | Type | Standard | Gjelder | Hva |
|
||||
|---|---|---|---|---|
|
||||
| `navn` | tekst | filnavnet uten `.json` | alle | navn på kjøringen; gir navnet på resultatmappa og filene |
|
||||
| `beskrivelse` | tekst | tom | alle | hvilket spørsmål scenarioet svarer på; står øverst i sammendraget |
|
||||
| `merknad` | tekst | ingen | alle | fritekst for deg selv; brukes ikke |
|
||||
| `modell` | tekst | **må oppgis** | | hvilken simulator, se under |
|
||||
| `styring` | objekt | regime A | alle | regulator, forsinkelse og reaktivitet for politiets innsats |
|
||||
| `parametere` | objekt | notatets verdier | alle | overstyring av kjernens parametere, gruppert som i kjernen |
|
||||
| `regel` | tekst | **må oppgis** for `simulate_endo` | `simulate_endo` | beslutningsregel for andel utenom og kapasitet |
|
||||
| `sjokk` | objekt | notatets sjokk | alle unntatt utvidelser | størrelse og tidspunkt for makrosjokket |
|
||||
| `tiltak` | objekt | ingen tiltak | `simulate_chain`, `simulate_chain2`, `simulate_endo` med regel `sched`, utvidelser med kjede | andel utenom, kapasitet, trommel–buffer–tau |
|
||||
| `slutt` | tall | 120 | alle | horisont i måneder (høyst 2 400) |
|
||||
| `sveip` | objekt eller liste | ingen | alle | én parameter over en liste verdier, eller to (rutenett) |
|
||||
| `utdata` | objekt | CSV med alle tidsserier, én rad per måned | alle | format, kolonner og hvor tett radene står |
|
||||
| `utvidelse` | objekt | utvidelsens `PARAMETERE` | `utvidelse:<navn>` | utvidelsens egne parametere |
|
||||
|
||||
Ukjente felt er en feil, med en melding som nevner feltet og lister de gyldige (og foreslår det nærmeste ved en
|
||||
skrivefeil). Det samme gjelder ukjente parameternavn. En skrivefeil gir aldri en stille standardverdi.
|
||||
|
||||
Tall skrives med punktum som desimaltegn, som JSON krever. Uendelig skrives som teksten `"inf"` (i anførselstegn),
|
||||
og bare der det er tillatt (står i tabellene under). `Infinity` og `NaN` er ikke gyldig JSON og avvises. Står et felt
|
||||
to ganger i samme objekt, er det en feil.
|
||||
|
||||
### `modell`
|
||||
|
||||
| Verdi | Hva | Kjernens funksjon | Avsnitt |
|
||||
|---|---|---|---|
|
||||
| `"simulate"` | grunnmodellen uten straffesakskjede: kriminalitet K, trygghet U, tillit Λ, innsats T | `simulate(controller, tau, p, kp)` | 8.1 (regime A–E) |
|
||||
| `"simulate_chain"` | domstolen som ledd med kø, koblet til grunnmodellen | `simulate_chain(controller, tau, ch, p, kp, mpc_knows_chain)` | 7.3 og 8.2 |
|
||||
| `"simulate_chain2"` | domstol med to sakstyper (A alvorlige, B øvrige), forrang eller felles kø, c spor | `simulate_chain2(tau, ch2, p, kp)` | 8.3 |
|
||||
| `"simulate_soc"` | rekruttering og vippepunkt, med et forbigående sjokk | `simulate_soc(controller, tau, p, s, kp, shock, t_on, t_off)` | 8.4 |
|
||||
| `"simulate_endo"` | endogene tiltak: andel utenom og kapasitet bestemt av beslutningsregler | `simulate_endo(policy, tau, p, ch, e, kp, tend, xi_sched, Z_sched)` | 8.5 |
|
||||
| `"utvidelse:<navn>"` | en mekanisme utenfor kjernen, i `modell/utvidelser/<navn>.py` | `simuler(scenario)` i utvidelsen | se [utvidelser.md](utvidelser.md) |
|
||||
|
||||
### `styring`
|
||||
|
||||
| Felt | Type | Standard | Gyldig | Hva |
|
||||
|---|---|---|---|---|
|
||||
| `regulator` | tekst | `"P"` | se under | hvordan politiets innsats T settes |
|
||||
| `forsinkelse` | tall (mnd) | 3 | 0 til `slutt` | τ: fra kriminalitetsbildet endrer seg til innsatsen er endret |
|
||||
| `reaktivitet` | tall | 3 | 0 til 100 | k_p: prosent mer innsats per prosent avvik fra målet (P-regelen) |
|
||||
| `mpc_kjenner_kjeden` | true/false | true | bare `simulate_chain` med `"MPC"` | om MPC-ens interne modell har med køen |
|
||||
|
||||
Regulatorer: `"P"` (proporsjonal regel) for alle modeller; `"MPC"` (modellprediktiv styring, avsnitt 8.1, regime D)
|
||||
for `simulate` og `simulate_chain`; `"fast"` (innsatsen står fast på T₀) for `simulate_soc`. Standardverdiene er
|
||||
regime A i notatet. MPC er tregt: én kjøring tar om lag 7 sekunder.
|
||||
|
||||
### `parametere`
|
||||
|
||||
Overstyring av kjernens parametere, gruppert etter dicten de står i i `sim.py`. Bare dictene modellen bruker, er
|
||||
tillatt:
|
||||
|
||||
| Gruppe | Brukes av | Nøkler |
|
||||
|---|---|---|
|
||||
| `P` | alle | `gamma`, `eta`, `delta`, `Omega0`, `shock`, `t_shock`, `T0`, `Tmax`, `Kstar`, `kU`, `U0`, `psiU`, `kL`, `mu1`, `mu2`, `Tprop`, `Lam0`, `ksat` |
|
||||
| `CH0` | `simulate_chain`, `simulate_endo`, `utvidelse:paadrag_tidsserie` | `Z`, `m`, `theta`, `chi`, `mu4`, `tauh`, `varsigma`, `xi` |
|
||||
| `CH2` | `simulate_chain2` | `Z`, `m`, `c`, `prio`, `piA`, `thetaA`, `thetaB`, `chi`, `mu4`, `tauh`, `varsigma`, `xiA`, `xiB` |
|
||||
| `ENDO` | `simulate_endo` | `lagD`, `xi_min`, `tau_thr`, `k_xi`, `tau_xi`, `ups_star`, `ups_hi`, `ups_lo`, `kZ`, `L1`, `L2`, `tau_down`, `T_clear`, `H_pred`, `period`, `qloop`, `mu3`, `Lam_star`, `k_q`, `q_max`, `lag_survey` |
|
||||
| `SOC` | `simulate_soc` | `eps`, `Kh`, `pexp` |
|
||||
|
||||
Hva hver parameter betyr, hvilken ligning den står i og notatets verdi: [modellkart.md](modellkart.md), del (b).
|
||||
Listen over gyldige nøkler leses fra kjernen hver gang, så en parameter du legger til i en arbeidskopi, kan
|
||||
overstyres uten å endre kjøreren.
|
||||
|
||||
Typen følger kjernens standardverdi: tall, heltall (`CH2.c`, `SOC.pexp`) eller true/false (`CH2.prio`,
|
||||
`ENDO.qloop`). Kjøreren sjekker også at verdien gir mening:
|
||||
|
||||
- uendelig (`"inf"`) er tillatt for `P.ksat` (ingen politimetning), `CH0.Z` (ingen flaskehals, = regime A) og
|
||||
`ENDO.T_clear` (restansen avvikles ikke);
|
||||
- andeler ligger mellom 0 og 1 (`U0`, `theta`, `xi`, `xi_min`, …), og `Lam0`, `piA` og `Lam_star` strengt mellom;
|
||||
- tider, forsterkninger og rater er ikke negative, og tidskonstanter og kapasiteter er større enn 0;
|
||||
- `ENDO.L1` < `ENDO.L2` og `ENDO.ups_lo` < `ENDO.ups_hi`.
|
||||
|
||||
`CH2.m` kan settes til `"kalibrer"`: da regnes m ut med kjernens `calibrate_m`, slik at saksbehandlingstiden i
|
||||
utgangslikevekten blir 3 måneder (det notatet gjør for c = 10 spor). Med flere spor (`c` > 1) og uten kalibrering
|
||||
er utgangslikevekten en annen enn notatets.
|
||||
|
||||
Funksjonene i kjedene (`Zfun`, `xifun`, `xiBfun`) og `dbr` settes ikke her, men med `tiltak`.
|
||||
|
||||
Viktig: `CH0.Z` og `CH0.m` (og `CH2.Z`, `CH2.m`) er regnet ut fra `P` da `sim.py` ble lest inn (`ZD0`, `MD`). Endrer
|
||||
du `P.gamma`, `P.Lam0` eller `P.T0`, regnes de ikke om; domstolen er da ikke lenger 95 % utnyttet før sjokket. Oppgi
|
||||
`CH0.Z` og `CH0.m` selv om du vil ha det.
|
||||
|
||||
### `regel` (bare `simulate_endo`)
|
||||
|
||||
| Verdi | Hva (avsnitt 8.5) |
|
||||
|---|---|
|
||||
| `"sched"` | faste tidsplaner; med `tiltak` er det scenario 5 som vedtak (`endo_e5`), uten er det scenario 2 |
|
||||
| `"desentral"` | påtalemyndigheten justerer andelen utenom etter observert saksbehandlingstid; budsjettet bestiller etter observert innstrømning |
|
||||
| `"desentral_prog"` | som `desentral`, men budsjettet dimensjonerer etter prognosen |
|
||||
| `"koord"` | koordinert regel: andel utenom og kapasitet ut fra en felles kapasitetsbalanse (metoden i avsnitt 7.7) |
|
||||
|
||||
Kvalitetssløyfen slås på med `"parametere": {"ENDO": {"qloop": true}}`. Avviklingstiden for restansen i den
|
||||
koordinerte regelen er `ENDO.T_clear`.
|
||||
|
||||
### `sjokk`
|
||||
|
||||
| Felt | Type | Standard | Hva |
|
||||
|---|---|---|---|
|
||||
| `faktor` | tall > 0 | 1,30 (`simulate_soc`: 1,6) | det strukturelle pådraget Ω ganges med dette |
|
||||
| `fra` | tall ≥ 0 (mnd) | 12 | når sjokket kommer |
|
||||
| `til` | tall (mnd) | 36 | bare `simulate_soc`: når sjokket er over |
|
||||
|
||||
For alle modeller unntatt `simulate_soc` er sjokket varig, og `faktor` og `fra` er det samme som `P.shock` og
|
||||
`P.t_shock`; oppgis begge steder, er det en feil. I `simulate_soc` er sjokket forbigående og settes bare her. Vil du
|
||||
ha et forbigående sjokk, en bølge eller flere trinn i de andre modellene, bruk utvidelsen `paadrag_tidsserie`
|
||||
([utvidelser.md](utvidelser.md)); utvidelser tar ikke `sjokk`.
|
||||
|
||||
### `tiltak`
|
||||
|
||||
| Felt | Type | Standard | Hva | Notatets verdi |
|
||||
|---|---|---|---|---|
|
||||
| `andel_utenom.andel` | tall i [0, 1) | må oppgis | andelen av sakene som ledes til forenklede reaksjoner | 0,2 |
|
||||
| `andel_utenom.fra` | tall ≥ 0 (mnd) | 24 | fra hvilken måned | 24 |
|
||||
| `kapasitet.faktor` | tall > 0 | må oppgis | domstolens kapasitet ganges med dette til slutt | 1,34 |
|
||||
| `kapasitet.fra` | tall ≥ 0 (mnd) | 24 | økningen begynner | 24 |
|
||||
| `kapasitet.over` | tall > 0 (mnd) | 24 | økningen fordeles lineært over så mange måneder | 24 |
|
||||
| `trommel_buffer_tau` | tall > 0 | ingen | politiets innsats begrenses slik at strømmen til domstolen er høyst dette ganger kapasiteten | 0,95 |
|
||||
|
||||
Hvor tiltakene virker:
|
||||
|
||||
- `simulate_chain`: andelen blir `xifun` (ξ_D = 1 − andel fra måned `fra`), kapasiteten `Zfun`
|
||||
(Z_D = Z · (1 + (faktor − 1) · min(max((t − fra)/over, 0), 1)), samme regneuttrykk som `Z_utv` i `sim.py`), og
|
||||
trommel–buffer–tau `dbr`. Scenario 3, 4 og 5 i notatet.
|
||||
- `simulate_chain2`: andelen er av alle saker og tas fra klasse B, som i notatet: ξ_B = 1 − andel/(1 − `piA`).
|
||||
Andelen kan derfor være høyst 1 − `piA`.
|
||||
- `simulate_endo`: bare med regel `"sched"`; tiltakene blir tidsplanene `xi_sched` og `Z_sched`. De andre reglene
|
||||
bestemmer andel og kapasitet selv. `trommel_buffer_tau` brukes ikke her.
|
||||
- Kapasitet og trommel–buffer–tau gir ingen mening når `CH0.Z` er `"inf"`, og avvises.
|
||||
|
||||
### `slutt`
|
||||
|
||||
Horisont i måneder, større enn 0 og høyst 2 400. Standard er 120 (`TEND` i `sim.py`); notatets kjøringer med
|
||||
`simulate_endo` bruker 180. For simulatorene som leser den globale `TEND`, setter kjøreren `sim.TEND` midlertidig og
|
||||
setter den tilbake etterpå; `simulate_endo` får den som argumentet `tend`. Tidssteget er alltid `DT` = 0,05 måned.
|
||||
|
||||
### `sveip`
|
||||
|
||||
Ett sveip kjører samme scenario én gang per verdi:
|
||||
|
||||
```json
|
||||
"sveip": {"parameter": "parametere.ENDO.T_clear", "verdier": [12, 24, 48, "inf"]}
|
||||
```
|
||||
|
||||
To sveip i en liste gir et rutenett (alle kombinasjoner, den første parameteren ytterst):
|
||||
|
||||
```json
|
||||
"sveip": [
|
||||
{"parameter": "styring.reaktivitet", "verdier": [1, 2, 3]},
|
||||
{"parameter": "styring.forsinkelse", "verdier": [3, 6, 11]}
|
||||
]
|
||||
```
|
||||
|
||||
`parameter` er en sti med punktum inn i scenariofila. Stien kan begynne med `styring`, `parametere`, `regel`,
|
||||
`sjokk`, `tiltak`, `slutt` eller `utvidelse`, og må ende i én verdi, for eksempel:
|
||||
|
||||
| Sti | Hva |
|
||||
|---|---|
|
||||
| `styring.reaktivitet`, `styring.forsinkelse`, `styring.regulator` | styringen |
|
||||
| `parametere.P.ksat`, `parametere.CH0.Z`, `parametere.ENDO.T_clear`, `parametere.ENDO.qloop`, … | en av kjernens parametere |
|
||||
| `regel` | beslutningsregelen i `simulate_endo` |
|
||||
| `sjokk.faktor`, `sjokk.fra`, `sjokk.til` | sjokket |
|
||||
| `tiltak.kapasitet.faktor`, `tiltak.andel_utenom.andel`, `tiltak.trommel_buffer_tau`, … | tiltakene |
|
||||
| `slutt` | horisonten |
|
||||
| `utvidelse.punkter`, … | en av utvidelsens parametere |
|
||||
|
||||
Regler:
|
||||
|
||||
- Verdien i sveipet erstatter verdien i fila for den stien. Hver kombinasjon valideres som om den sto i fila, og en
|
||||
feil sier hvilken kombinasjon den gjelder.
|
||||
- `null` som verdi betyr kjernens verdi (feltet tas bort), for eksempel `"verdier": [null, "inf"]` for notatets
|
||||
kapasitet og ingen flaskehals. Den vises som «standard».
|
||||
- Høyst to parametere og høyst 400 kjøringer. Samme verdi to ganger (også 3 og 3.0) er en feil.
|
||||
- Kjøringene heter `<siste del av stien>=<verdi>`, i et rutenett skilt med `_`, for eksempel `T_clear=24` og
|
||||
`reaktivitet=3_forsinkelse=11`. Filene heter `<navn>__<kjøring>.csv`.
|
||||
|
||||
### `utdata`
|
||||
|
||||
| Felt | Type | Standard | Hva |
|
||||
|---|---|---|---|
|
||||
| `format` | `"csv"` eller `"notat"` | `"csv"` | filformatet for tidsseriene |
|
||||
| `kolonner` | liste med navn | alle tidsseriene modellen gir | hvilke tidsserier, i hvilken rekkefølge |
|
||||
| `hvert` | heltall ≥ 1 | 20 (`csv`), 10 (`notat`) | hvert så mange tidssteg skrives (20 = én rad per måned) |
|
||||
| `overskrift` | tekst | kolonnenavnene | bare `"notat"`: overskriftslinjen, ett ord per kolonne |
|
||||
|
||||
`"csv"`: komma mellom feltene, punktum som desimaltegn, én overskriftslinje med kolonnenavnene og tallene med ti
|
||||
gjeldende sifre. Leses direkte av R, Python, regneark og pgfplots (`col sep=comma`). Fila heter `<navn>.csv`.
|
||||
|
||||
`"notat"`: nøyaktig notatets filformat, skrevet med kjernens egen `write_csv`: mellomrom mellom feltene, `%.5f`,
|
||||
hver tiende rad, og overskriften du gir. Fila heter `<navn>.dat`. Med dette formatet gjenskaper scenariofilene i
|
||||
`eksempler/scenarioer/notatet/` notatets datafiler, og notatets figurer kan lages på nytt fra dem (pgfplots i
|
||||
LaTeX-kilden leser filene direkte).
|
||||
|
||||
## Tidsseriene per modell
|
||||
|
||||
| Modell | Tidsserier (kolonnenavn) |
|
||||
|---|---|
|
||||
| `simulate`, `simulate_soc` | `t K U L T` |
|
||||
| `simulate_chain` | `t K U L T Q tauD JD RD xi`, og `Z Z_rel` når kapasiteten er endelig |
|
||||
| `simulate_chain2` | `t K KA KB U L T QA QB tauA tauB RA RB` |
|
||||
| `simulate_endo` | `t K U L T Q tauD JD RD Z xi q Z_rel` |
|
||||
| `utvidelse:paadrag_tidsserie` | som `simulate_chain` (med kjede) eller `simulate` (uten), og `Omega_faktor` |
|
||||
|
||||
`t` er tid i måneder, `K` kriminalitet (1 = før sjokket), `U` trygghet, `L` tillit Λ (0,80 før sjokket), `T`
|
||||
politiets innsats (1 = før sjokket), `Q` køen i domstolen, `tauD` saksbehandlingstiden (mnd), `JD` saker inn og `RD`
|
||||
saker avgjort per måned, `xi` andelen som går til domstolen (1 − andel utenom), `Z` kapasiteten og `Z_rel` kapasiteten
|
||||
delt på kapasiteten ved start, `q` prosessuell kvalitet. I `simulate_chain2` er `KA`/`KB`, `QA`/`QB`, `tauA`/`tauB` og
|
||||
`RA`/`RB` det samme per sakstype. Kø, strøm og kapasitet er i modellens normaliserte enheter. `Z`, `xi` og `Z_rel` i
|
||||
`simulate_chain` regnes ut av kjøreren fra kjernens `chain_at`; `Z_rel` i `simulate_endo` er det notatets `endo_*.dat`
|
||||
kaller `Z`.
|
||||
|
||||
## Hva kjøreren skriver
|
||||
|
||||
I mappa `resultater/<navn>/` under mappa du står i, eller i mappa du gir med `--ut`:
|
||||
|
||||
- `sammendrag.md`: hva som ble kjørt, hvilke felt fila satte selv, nøkkeltallene i en tabell (én kolonne per
|
||||
kjøring, høyst seks per tabell), for et rutenett også en tabell med ett nøkkeltall over begge parametrene (spennet
|
||||
i K de siste 60 månedene når horisonten er over 120 mnd, ellers spennet etter måned 60), og for
|
||||
`simulate_endo` budsjettvedtakene fra regelen. Det samme skrives til skjermen, sammen med stien til mappa.
|
||||
- én CSV (eller `.dat`) per kjøring;
|
||||
- `resultat.json`: scenarioets navn og fil med sha256, Python-, numpy- og scipy-versjon, sha256 for de tre filene i
|
||||
kjernen (og for utvidelsen), og for hver kjøring alle parametere slik simulatoren fikk dem (fullt utfylt, også det
|
||||
fila ikke satte), nøkkeltallene med full presisjon og budsjettvedtakene. Uendelig står som `"inf"`.
|
||||
|
||||
Samme scenariofil gir samme bytes i alle tre filene på samme maskin og med samme versjoner: det står ingen
|
||||
tidspunkter i dem. Finnes mappa fra før med et tidligere resultat, tas bare filene det resultatet laget bort. Finnes
|
||||
den med andre filer, stopper kjøreren uten å skrive noe.
|
||||
|
||||
## Nøkkeltallene
|
||||
|
||||
Fra det kjernens `summary`, `summary2` og `summary_endo` skriver ut, med norske navn og norsk tallformat. Et
|
||||
nøkkeltall vises bare når modellen har tidsserien det bygger på.
|
||||
|
||||
| Nøkkel i `resultat.json` | Hva |
|
||||
|---|---|
|
||||
| `K_snitt` | kriminalitet, gjennomsnitt fra måned 12 (bare `simulate_endo`, som `Kmean`) |
|
||||
| `K_maks`, `K_slutt` | kriminalitet, høyeste og ved slutt |
|
||||
| `spenn_K` | høyeste minus laveste K etter måned 60 (`amp(K,t>60)` i `sim.py`) |
|
||||
| `spenn_K_sist` | høyeste minus laveste K de siste 60 månedene, når horisonten er over 120 mnd: vedvarende svingning |
|
||||
| `KA_slutt`, `KB_slutt` | belastning per sakstype ved slutt (`simulate_chain2`) |
|
||||
| `T_maks`, `T_slutt` | politiets innsats |
|
||||
| `L_min`, `L_slutt` | tillit |
|
||||
| `tauD_maks`, `tauD_slutt` (og `tauA_*`, `tauB_*`) | saksbehandlingstid, mnd |
|
||||
| `Q_slutt` (og `QA_slutt`, `QB_slutt`) | kø ved slutt |
|
||||
| `inn_ut_slutt` | saker inn delt på saker avgjort ved slutt (`ups_end` i `summary`) |
|
||||
| `Z_maks`, `Z_slutt` | kapasitet, 1,00 = ved start (når kapasiteten endres) |
|
||||
| `utenom_maks_pst`, `utenom_slutt_pst` | andel ledet utenom, prosent (når den endres) |
|
||||
| `q_maks` | prosessuell kvalitet (når kvalitetssløyfen er på) |
|
||||
| `frafall`, `kapasitetsaar` | frafall fra køen summert over tid, i måneder av innstrømningen før sjokket (som i notatets tabell i avsnitt 8.5; `summary_endo` skriver det i modellens enheter, en tidel av dette), og kapasitetsår over utgangsnivået (`simulate_endo`) |
|
||||
|
||||
Under tabellen står: 1,00 er nivået i referanselikevekten før sjokket for K og T, tilliten er 0,80 der, kapasitet
|
||||
1,00 er kapasiteten ved start, tider er i måneder.
|
||||
|
||||
## Feil og exit-koder
|
||||
|
||||
Exit-kode 0 når alt gikk, 2 ved feil i scenariofila eller bruken. Meldingen nevner feltet:
|
||||
|
||||
```
|
||||
Feil i scenariofila mitt.json: parametere.P.gama: ukjent parameter i P. Mente du "gamma"? Gyldige: gamma, eta, ...
|
||||
Feil i scenariofila mitt.json: styring.forsinkelse: må være et tall minst 0 og høyst 120, ikke "tre".
|
||||
Feil i scenariofila mitt.json: sveip[0].parameter: "styring.reaktivtet": ukjent felt "reaktivtet" under styring. ...
|
||||
Fila mitt.json er ikke gyldig JSON: feil på linje 2, kolonne 15.
|
||||
```
|
||||
|
||||
Stopper en kjøring i kjernen (en kombinasjon uten likevekt ved start eller med en løsning som vokser over alle
|
||||
grenser), sier meldingen hvilken kjøring det var, og ingenting skrives.
|
||||
|
||||
## Eksempelfilene
|
||||
|
||||
### Notatets datafiler: `eksempler/scenarioer/notatet/`
|
||||
|
||||
Hver fil gjenskaper en av notatets datafiler med `"format": "notat"`. Testene i `tests/test_scenario.py` sammenligner.
|
||||
|
||||
| Notatets fil | Scenariofil | Gjenskapt |
|
||||
|---|---|---|
|
||||
| `p3`, `p6`, `p11`, `p11d`, `mpc11` | samme navn | byte for byte |
|
||||
| `kjede_s1` … `kjede_s5` | samme navn | byte for byte |
|
||||
| `endo_e5`, `endo_des`, `endo_desp`, `endo_ko` | samme navn | byte for byte |
|
||||
| `sat_B`, `sat_C` | samme navn (sveip over `P.ksat`: `inf`, 3, 1,5) | kolonne for kolonne |
|
||||
| `prio_s2`, `prio_s3`, `prio_s5` | samme navn (sveip over `CH2.prio`: med og uten forrang) | kolonne for kolonne |
|
||||
| `vippe` | `vippe.json` (fast, p3, p6) og `vippe_tak.json` (p3cap, p6cap) | kolonne for kolonne |
|
||||
|
||||
«Kolonne for kolonne»: notatets fil setter sammen kolonner fra flere kjøringer; hver kolonne er lik tegn for tegn
|
||||
den tilsvarende kolonnen i kjøringens fil. `stab.dat` og `thumb.dat` er analytiske kart (`critical_delay`), ikke
|
||||
simuleringer, og har ingen scenariofil.
|
||||
|
||||
### Spørsmål notatet ikke har figur for: `eksempler/scenarioer/`
|
||||
|
||||
| Fil | Spørsmål |
|
||||
|---|---|
|
||||
| `avviklingstid_koordinert.json` | hvor raskt bør restansen avvikles i den koordinerte regelen (T_clear fra 6 til 96 mnd og uendelig)? |
|
||||
| `stabilitet_reaktivitet_forsinkelse.json` | hvor går grensen for svingninger, over reaktivitet og forsinkelse, sammenlignet med den kritiske forsinkelsen? |
|
||||
| `sjokkstorrelse_kjede.json` | hvor mye betyr størrelsen på sjokket når domstolen er en flaskehals? |
|
||||
| `kvalitetssloyfe.json` | hva gjør kvalitetssløyfen med desentraliserte og koordinerte regler? |
|
||||
| `utvidelse_bolge.json` | hva etterlater en forbigående bølge i pådraget, med og uten flaskehals (utvidelsen `paadrag_tidsserie`)? |
|
||||
229
docs/utvidelser.md
Normal file
229
docs/utvidelser.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Utvidelser: å legge til en mekanisme
|
||||
|
||||
En utvidelse er en mekanisme modellen ikke har, lagt ved siden av kjernen i stedet for inn i den. Den ligger i
|
||||
`modell/utvidelser/<navn>.py`, kjøres med en scenariofil (`"modell": "utvidelse:<navn>"`) og kontrolleres av
|
||||
`kontroll`. Kjernen (`modell/kjerne/`) forblir notatets kode, så det er alltid klart hva som er notatets modell og hva
|
||||
som er lagt til.
|
||||
|
||||
## Regelen
|
||||
|
||||
Samme regel som i notatets kode (`sim.py`, Del 3: «Alle utvidelser har grensetilfeller som gjenskaper Del 1 og 2
|
||||
eksakt»): **en utvidelse skal ha et grensetilfelle som gjenskaper kjernen eksakt, og en test som viser det.** Grensetilfellet er en verdi av utvidelsens parametere der mekanismen forsvinner,
|
||||
for eksempel en faktor lik 1, et ledd lik 0 eller en kapasitet lik uendelig. Med den verdien skal utvidelsen gi
|
||||
nøyaktig de samme tallene som simulatoren i kjernen den bygger på: største absolutte avvik over alle tidsserier skal
|
||||
være `0.0`, ikke bare lite.
|
||||
|
||||
Hvorfor eksakt: da vet både du og leseren at alt utvidelsen viser utover kjernen, kommer fra mekanismen, ikke fra en
|
||||
annen tallbehandling, en annen tidsstegning eller en feil i kopien av løkka. Avvik på 10⁻¹⁵ betyr at regnestegene er
|
||||
gjort i en annen rekkefølge, og da er det ikke lenger kjernens modell.
|
||||
|
||||
## Grensesnittet
|
||||
|
||||
| Navn | Type | Krav |
|
||||
|---|---|---|
|
||||
| `NAVN` | tekst | lik filnavnet uten `.py` |
|
||||
| `BESKRIVELSE` | tekst | én setning; står i sammendraget etter modellnavnet |
|
||||
| `PARAMETERE` | dict | utvidelsens parametere med standardverdier. Standardverdiene bør være grensetilfellet |
|
||||
| `simuler(scenario) -> dict` | funksjon | kjører modellen; se under |
|
||||
| `grensetilfelle() -> list[tuple[str, float]]` | funksjon | minst ett tilfelle: (beskrivelse, største absolutte avvik mot kjernen). Hvert avvik skal være `0.0` |
|
||||
| `BRUKER` | tuple, valgfri | hvilke av kjernens dicts (`"P"`, `"CH0"`, `"CH2"`, `"ENDO"`, `"SOC"`) som kan overstyres under `parametere`. Uten den: alle fem |
|
||||
| `sjekk(scenario) -> None` | funksjon, valgfri | kalles før kjøringen; en `ValueError("felt: hva som er galt")` gir exit-kode 2 med meldingen din |
|
||||
|
||||
### Hva `simuler` får
|
||||
|
||||
Scenariokjøreren validerer scenariofila og gir `simuler` én kjøring om gangen (ett sveip gir flere kall), som en dict:
|
||||
|
||||
```python
|
||||
{
|
||||
"navn": "T_clear=24", # kjøringens navn ("<navn>" uten sveip)
|
||||
"styring": {"regulator": "P", "forsinkelse": 3.0, "reaktivitet": 3.0},
|
||||
"slutt": 120.0, # horisont i måneder
|
||||
"tiltak": {...}, # tiltakene slik de står i fila (oppløst, med standardverdier)
|
||||
"parametere": { # kjernens dicts, fullt utfylt med overstyringene
|
||||
"P": {...}, # som sim.P, med det fila endrer
|
||||
"CH0": {...}, # som sim.CH0, med Zfun, xifun og dbr satt fra tiltak
|
||||
... # bare dictene i BRUKER
|
||||
},
|
||||
"utvidelse": {...}, # PARAMETERE med verdiene fra fila
|
||||
}
|
||||
```
|
||||
|
||||
Dictene kan sendes rett til kjernens funksjoner (`sim.deriv_chain(..., p, ch, ...)`). Typene i `"utvidelse"` er
|
||||
sjekket mot standardverdiene: tall (også `"inf"`), heltall, true/false, tekst eller liste. Innholdet i en liste
|
||||
sjekker du selv i `sjekk`. Feltet `sjokk` i scenariofila er ikke tillatt for utvidelser: pådraget er utvidelsens sak.
|
||||
|
||||
### Hva `simuler` returnerer
|
||||
|
||||
En dict med tidsserien `"t"` (numpy-array) og andre tidsserier med samme lengde, med kjernens navn der det gir mening
|
||||
(`K`, `U`, `L`, `T`, `Q`, `tauD`, `JD`, `RD`, `Z`, `Z_rel`, `xi`, …). Kjøreren behandler dem som en kjøring av
|
||||
kjernen: nøkkeltallene velges etter hvilke tidsserier som finnes, alle kan skrives til CSV, og `utdata.kolonner` kan
|
||||
velge blant dem. Tall (ikke arrays) blir «skalarer» i `resultat.json`; en liste med tekst under `"log"` blir
|
||||
vedtakslinjer i sammendraget.
|
||||
|
||||
## Hvordan utvidelser kontrolleres
|
||||
|
||||
- `python3 modell/kjor.py kontroll --rask` kjører `grensetilfelle()` i hver utvidelse (hver fil i
|
||||
`modell/utvidelser/` som ikke begynner med `_`) og sier «Utvidelser: grensetilfellene gjenskaper kjernen eksakt
|
||||
(...)», eller hvilket tilfelle som ikke gjør det.
|
||||
- `tests/test_utvidelser.py` går gjennom alle utvidelsene: grensesnittet finnes, og hvert avvik er `0.0`. En ny
|
||||
utvidelse er med uten at testen endres.
|
||||
- `resultat.json` har sha256 for utvidelsens fil, så det er sporbart hvilken versjon en kjøring brukte.
|
||||
|
||||
## Gjennomført eksempel: `paadrag_tidsserie`
|
||||
|
||||
I kjernen er det strukturelle pådraget Ω₀ før måned 12 og Ω₀ · 1,3 etterpå (`Omega` i `sim.py`): ett varig sjokk.
|
||||
Utvidelsen lar pådraget følge Ω₀ · f(t), der f er en tidsserie du oppgir. Da kan en forbigående bølge, et sjokk i
|
||||
flere trinn eller en gradvis endring kjøres.
|
||||
|
||||
| Parameter | Standard | Hva |
|
||||
|---|---|---|
|
||||
| `form` | `"trapp"` | `"trapp"`: f holder verdien fra hvert punkt til neste; `"lineaer"`: rett linje mellom punktene |
|
||||
| `punkter` | `[[12.0, 1.3]]` | `[måned, faktor]`, stigende måneder. Før første punkt er f = 1, etter siste er f lik siste verdi |
|
||||
|
||||
- Styring: P-regelen (`styring.regulator` må være `"P"`).
|
||||
- Med kjeden (`parametere.CH0.Z` endelig, standard): regnestegene i `simulate_chain`, med kjernens `deriv_chain` og
|
||||
pådraget gitt som `Om0`. Tiltakene virker som i `simulate_chain` (`andel_utenom`, `kapasitet`,
|
||||
`trommel_buffer_tau`).
|
||||
- Uten kjeden (`"CH0": {"Z": "inf"}`): regnestegene i `simulate`. Utvidelsen har en egen løkke for dette i stedet
|
||||
for å la `simulate_chain` med uendelig kapasitet gjøre jobben, fordi den bare er eksakt lik `simulate` for regime A
|
||||
(for regime B–D er avviket opptil 3,8·10⁻¹⁵, se [modellkart.md](modellkart.md), del (e)).
|
||||
- `P.shock` og `P.t_shock` brukes ikke (pådraget følger `punkter`); endres de, er det en feil.
|
||||
- Tidsserien `Omega_faktor` (f(t)) skrives med i CSV-fila.
|
||||
|
||||
Grensetilfellene, som `grensetilfelle()` returnerer:
|
||||
|
||||
| Tilfelle | Kjernen | Avvik |
|
||||
|---|---|---|
|
||||
| trapp med ett punkt `[12, 1.3]`, notatets `CH0` | `simulate_chain("P", 3.0, CH0)` | 0.0 (t, K, U, L, T, Q, tauD, JD, RD og tau_ref) |
|
||||
| samme trapp, `CH0.Z` = inf | `simulate("P", 3.0)` | 0.0 (t, K, U, L, T) |
|
||||
|
||||
Testene viser i tillegg at utvidelsen kjørt gjennom scenariokjøreren med standardverdiene gir notatets
|
||||
`kjede_s2.dat` og `p3.dat` byte for byte, og med tiltakene fra scenario 5 notatets `kjede_s5.dat`.
|
||||
|
||||
Eksempel: `eksempler/scenarioer/utvidelse_bolge.json` kjører en bølge (40 % høyere pådrag fra måned 18 til 36, med
|
||||
lineær stigning og fall) med og uten flaskehals i domstolen.
|
||||
|
||||
## Mal
|
||||
|
||||
Malen under er en hel, kjørbar utvidelse: politiets effektivitet (`gamma` i fjerningsleddet) endres med en faktor
|
||||
fra en gitt måned. Med faktor 1 forsvinner mekanismen, og utvidelsen gjenskaper `simulate("P", ...)` eksakt. Kopier
|
||||
den til `modell/utvidelser/<ditt_navn>.py` og bytt ut mekanismen.
|
||||
|
||||
<!-- mal:start -->
|
||||
```python
|
||||
"""Utvidelse: politiets effektivitet endres med en faktor fra en gitt måned.
|
||||
|
||||
Bygger på simulate i sim.py (grunnmodellen uten kjede). Grensetilfelle: faktor = 1
|
||||
gjenskaper simulate("P", tau) eksakt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
NAVN = "mal_effektivitet" # samme som filnavnet uten .py
|
||||
BESKRIVELSE = "politiets effektivitet ganges med en faktor fra en gitt måned (mal)"
|
||||
PARAMETERE: dict[str, object] = { # standardverdiene er grensetilfellet
|
||||
"faktor": 1.0, # effektiviteten ganges med dette ...
|
||||
"fra": 24.0, # ... fra denne måneden
|
||||
}
|
||||
BRUKER = ("P",) # kjernens dicts som kan overstyres under "parametere"
|
||||
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
|
||||
|
||||
|
||||
def _sim() -> Any:
|
||||
"""Kjernen fra samme modellmappe som utvidelsen."""
|
||||
if str(KJERNE) not in sys.path:
|
||||
sys.path.insert(0, str(KJERNE))
|
||||
return importlib.import_module("sim")
|
||||
|
||||
|
||||
def sjekk(scenario: dict[str, Any]) -> None:
|
||||
"""Valgfri: stopp med en norsk melding før kjøringen."""
|
||||
if scenario["styring"]["regulator"] != "P":
|
||||
raise ValueError('styring.regulator: denne utvidelsen bruker bare "P".')
|
||||
if scenario["utvidelse"]["faktor"] < 0:
|
||||
raise ValueError("utvidelse.faktor: må være minst 0.")
|
||||
|
||||
|
||||
def simuler(scenario: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Løkka fra simulate i sim.py, med mekanismen lagt inn i fjerningsleddet."""
|
||||
sim = _sim()
|
||||
p = scenario["parametere"]["P"]
|
||||
tau = scenario["styring"]["forsinkelse"]
|
||||
kp = scenario["styring"]["reaktivitet"]
|
||||
u = scenario["utvidelse"]
|
||||
dt = sim.DT
|
||||
n = int(scenario["slutt"] / dt) + 1
|
||||
t = np.arange(n) * dt
|
||||
K = np.ones(n); U = np.full(n, p["U0"]); L = np.full(n, p["Lam0"]); T = np.full(n, p["T0"])
|
||||
lag = int(round(tau / dt))
|
||||
for k in range(n - 1):
|
||||
Kobs = K[k - lag] if k - lag >= 0 else 1.0
|
||||
T[k] = np.clip(p["T0"] + kp * (Kobs - p["Kstar"]), 0.0, p["Tmax"])
|
||||
# Mekanismen: med faktor 1.0 er F * 1.0 nøyaktig F, så grensetilfellet blir eksakt.
|
||||
f = u["faktor"] if t[k] >= u["fra"] else 1.0
|
||||
F = sim.removal(K[k], L[k], T[k], p) * f
|
||||
# Resten er deriv() i sim.py, med F i stedet for removal(...). Samme rekkefølge på regnestegene.
|
||||
dK = sim.Omega(t[k], p) - F - p["delta"] * K[k]
|
||||
Ueq = np.clip(p["U0"] - p["psiU"] * (K[k] - 1.0), 0.0, 1.0)
|
||||
dU = p["kU"] * (Ueq - U[k])
|
||||
over = max(0.0, T[k] - p["Tprop"] * K[k])
|
||||
dL = p["kL"] * (p["Lam0"] + p["mu1"] * (U[k] - p["U0"]) - L[k]) - p["mu2"] * L[k] * over ** 2
|
||||
K[k + 1] = K[k] + dt * dK
|
||||
U[k + 1] = U[k] + dt * dU
|
||||
L[k + 1] = np.clip(L[k] + dt * dL, 1e-4, 1 - 1e-4)
|
||||
T[-1] = T[-2]
|
||||
return {"t": t, "K": K, "U": U, "L": L, "T": T}
|
||||
|
||||
|
||||
def grensetilfelle() -> list[tuple[str, float]]:
|
||||
"""Faktor 1 gjenskaper simulate i kjernen; avvikene skal være eksakt 0.0."""
|
||||
sim = _sim()
|
||||
ut = []
|
||||
for tau in (3.0, 11.0):
|
||||
scenario = {"styring": {"regulator": "P", "forsinkelse": tau, "reaktivitet": 3.0},
|
||||
"slutt": float(sim.TEND), "parametere": {"P": dict(sim.P)},
|
||||
"utvidelse": {"faktor": 1.0, "fra": 24.0}}
|
||||
egen = simuler(scenario)
|
||||
kjerne = dict(zip(("t", "K", "U", "L", "T"), sim.simulate("P", tau)))
|
||||
avvik = max(float(np.abs(egen[n] - kjerne[n]).max()) for n in kjerne)
|
||||
ut.append((f'mal_effektivitet: faktor 1 = simulate("P", {tau:g})', avvik))
|
||||
return ut
|
||||
```
|
||||
<!-- mal:slutt -->
|
||||
|
||||
`tests/test_utvidelser.py` legger malen inn i en kopi av modellen og kontrollerer at grensetilfellene er `0.0` og at
|
||||
den kan kjøres med en scenariofil, så malen virker slik den står her.
|
||||
|
||||
## Trinn for trinn
|
||||
|
||||
1. **Lag en arbeidskopi** du kan endre: `python3 modell/kjor.py arbeidskopi rettsstat-modell`. Resten gjøres i
|
||||
kopien.
|
||||
2. **Finn stedet i modellen** mekanismen hører til: hvilken ligning, hvilken funksjon i `sim.py`, hvilke parametere
|
||||
([modellkart.md](modellkart.md)). Velg simulatoren du bygger på: den enkleste som har det mekanismen trenger.
|
||||
3. **Kopier malen** til `modell/utvidelser/<navn>.py`. Sett `NAVN` lik filnavnet, skriv `BESKRIVELSE`, og legg
|
||||
utvidelsens parametere i `PARAMETERE` med grensetilfellet som standardverdier.
|
||||
4. **Kopier løkka** fra simulatoren du bygger på inn i `simuler`, uendret, og legg mekanismen inn slik at den
|
||||
forsvinner i grensetilfellet. Det som gjør avviket eksakt 0: gang med `1.0`, legg til `0.0`, eller velg en gren
|
||||
(`if`) som gjør nøyaktig det kjernen gjør. Bruk kjernens funksjoner (`removal`, `deriv_chain`, `court`,
|
||||
`chain_at`, …) der du kan, i stedet for å skrive dem av. Endre ikke rekkefølgen på regnestegene: `a - b - c` og
|
||||
`a - (b + c)` kan gi forskjellig siste siffer.
|
||||
5. **Skriv `grensetilfelle()`**: kjør `simuler` med grenseverdiene og kjernen med de samme forutsetningene, og
|
||||
returner største absolutte avvik over alle tidsseriene. Ta gjerne med flere tilfeller (andre forsinkelser, med og
|
||||
uten kjede).
|
||||
6. **Kontroller**: `python3 modell/kjor.py kontroll --rask` skal si at grensetilfellene gjenskaper kjernen eksakt.
|
||||
Kjør testene (`python3 -m pytest tests/test_utvidelser.py`); utvidelsen er med automatisk. Skriv gjerne en egen
|
||||
test for det mekanismen skal gjøre utenfor grensetilfellet.
|
||||
7. **Bruk den**: skriv en scenariofil med `"modell": "utvidelse:<navn>"` og parametrene under `"utvidelse"`, og kjør
|
||||
`python3 modell/kjor.py scenario <fil.json>`. Sveip virker som ellers, også over `utvidelse.<parameter>`.
|
||||
8. **Send den tilbake** hvis du vil: se `BIDRA.md` i pluginen.
|
||||
|
||||
Vil du heller endre kjernen selv, kan du det i arbeidskopien. Da sier `kontroll` hvilke av notatets resultater som
|
||||
flytter seg, og hvor mye. En utvidelse er likevel å foretrekke når mekanismen kan stå ved siden av: notatets
|
||||
resultater ligger da fast, og grensetilfellet viser at de gjør det.
|
||||
11
eksempler/scenarioer/avviklingstid_koordinert.json
Normal file
11
eksempler/scenarioer/avviklingstid_koordinert.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"navn": "avviklingstid_koordinert",
|
||||
"beskrivelse": "Hvor raskt bør restansen avvikles når påtale og budsjett koordineres? Den koordinerte regelen (avsnitt 8.5) leder saker utenom og bestiller kapasitet slik at restansen over målet avvikles over T_clear måneder. Notatet har 24, 48 og uendelig i tabellen over endogene tiltak og nevner 12 i teksten; her er spennet fra 6 til 96 måneder i én kjøring, med kriminalitet, tillit, saksbehandlingstid, kapasitet og andel utenom side om side.",
|
||||
"modell": "simulate_endo",
|
||||
"regel": "koord",
|
||||
"slutt": 180,
|
||||
"sveip": {
|
||||
"parameter": "parametere.ENDO.T_clear",
|
||||
"verdier": [6, 12, 24, 48, 96, "inf"]
|
||||
}
|
||||
}
|
||||
17
eksempler/scenarioer/kvalitetssloyfe.json
Normal file
17
eksempler/scenarioer/kvalitetssloyfe.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"navn": "kvalitetssloyfe",
|
||||
"beskrivelse": "Hva gjør kvalitetssløyfen (prosessuell kvalitet q styres etter tillitsundersøkelsen, avsnitt 8.5) med desentraliserte og koordinerte regler? Notatets tabell over endogene tiltak har den koordinerte regelen med kvalitetssløyfe; her er alle fire kombinasjonene side om side over 180 måneder.",
|
||||
"modell": "simulate_endo",
|
||||
"slutt": 180,
|
||||
"regel": "koord",
|
||||
"sveip": [
|
||||
{
|
||||
"parameter": "regel",
|
||||
"verdier": ["desentral", "koord"]
|
||||
},
|
||||
{
|
||||
"parameter": "parametere.ENDO.qloop",
|
||||
"verdier": [false, true]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
eksempler/scenarioer/notatet/endo_des.json
Normal file
17
eksempler/scenarioer/notatet/endo_des.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"navn": "endo_des",
|
||||
"beskrivelse": "Gjenskaper data/endo_des.dat: desentraliserte regler (observert strøm), 180 måneder, regime A (avsnitt 8.5).",
|
||||
"modell": "simulate_endo",
|
||||
"regel": "desentral",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"slutt": 180,
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "L", "T", "Z_rel", "xi", "tauD", "q"],
|
||||
"overskrift": "t K L T Z xi tauD q"
|
||||
}
|
||||
}
|
||||
17
eksempler/scenarioer/notatet/endo_desp.json
Normal file
17
eksempler/scenarioer/notatet/endo_desp.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"navn": "endo_desp",
|
||||
"beskrivelse": "Gjenskaper data/endo_desp.dat: desentraliserte regler med prognose, 180 måneder, regime A (avsnitt 8.5).",
|
||||
"modell": "simulate_endo",
|
||||
"regel": "desentral_prog",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"slutt": 180,
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "L", "T", "Z_rel", "xi", "tauD", "q"],
|
||||
"overskrift": "t K L T Z xi tauD q"
|
||||
}
|
||||
}
|
||||
28
eksempler/scenarioer/notatet/endo_e5.json
Normal file
28
eksempler/scenarioer/notatet/endo_e5.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"navn": "endo_e5",
|
||||
"beskrivelse": "Gjenskaper data/endo_e5.dat: faste vedtak i måned 24 (scenario 5 som tidsplan), 180 måneder, regime A (avsnitt 8.5).",
|
||||
"modell": "simulate_endo",
|
||||
"regel": "sched",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"andel_utenom": {
|
||||
"andel": 0.2,
|
||||
"fra": 24
|
||||
},
|
||||
"kapasitet": {
|
||||
"faktor": 1.34,
|
||||
"fra": 24,
|
||||
"over": 24
|
||||
}
|
||||
},
|
||||
"slutt": 180,
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "L", "T", "Z_rel", "xi", "tauD", "q"],
|
||||
"overskrift": "t K L T Z xi tauD q"
|
||||
}
|
||||
}
|
||||
17
eksempler/scenarioer/notatet/endo_ko.json
Normal file
17
eksempler/scenarioer/notatet/endo_ko.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"navn": "endo_ko",
|
||||
"beskrivelse": "Gjenskaper data/endo_ko.dat: koordinert regel (T_clear = 24), 180 måneder, regime A (avsnitt 8.5).",
|
||||
"modell": "simulate_endo",
|
||||
"regel": "koord",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"slutt": 180,
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "L", "T", "Z_rel", "xi", "tauD", "q"],
|
||||
"overskrift": "t K L T Z xi tauD q"
|
||||
}
|
||||
}
|
||||
19
eksempler/scenarioer/notatet/kjede_s1.json
Normal file
19
eksempler/scenarioer/notatet/kjede_s1.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"navn": "kjede_s1",
|
||||
"beskrivelse": "Gjenskaper data/kjede_s1.dat: scenario 1: ingen flaskehals (= regime A). Regime A (forsinkelse 3, reaktivitet 3), avsnitt 8.2.",
|
||||
"modell": "simulate_chain",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"parametere": {
|
||||
"CH0": {
|
||||
"Z": "inf"
|
||||
}
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]
|
||||
}
|
||||
}
|
||||
14
eksempler/scenarioer/notatet/kjede_s2.json
Normal file
14
eksempler/scenarioer/notatet/kjede_s2.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "kjede_s2",
|
||||
"beskrivelse": "Gjenskaper data/kjede_s2.dat: scenario 2: flaskehals i domstolen, ingen tiltak. Regime A (forsinkelse 3, reaktivitet 3), avsnitt 8.2.",
|
||||
"modell": "simulate_chain",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]
|
||||
}
|
||||
}
|
||||
20
eksempler/scenarioer/notatet/kjede_s3.json
Normal file
20
eksempler/scenarioer/notatet/kjede_s3.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"navn": "kjede_s3",
|
||||
"beskrivelse": "Gjenskaper data/kjede_s3.dat: scenario 3: underordne, 20 % av sakene ledes utenom fra måned 24. Regime A (forsinkelse 3, reaktivitet 3), avsnitt 8.2.",
|
||||
"modell": "simulate_chain",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"andel_utenom": {
|
||||
"andel": 0.2,
|
||||
"fra": 24
|
||||
}
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]
|
||||
}
|
||||
}
|
||||
21
eksempler/scenarioer/notatet/kjede_s4.json
Normal file
21
eksempler/scenarioer/notatet/kjede_s4.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"navn": "kjede_s4",
|
||||
"beskrivelse": "Gjenskaper data/kjede_s4.dat: scenario 4: utvide, domstolens kapasitet +34 % lineært over måned 24-48. Regime A (forsinkelse 3, reaktivitet 3), avsnitt 8.2.",
|
||||
"modell": "simulate_chain",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"kapasitet": {
|
||||
"faktor": 1.34,
|
||||
"fra": 24,
|
||||
"over": 24
|
||||
}
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]
|
||||
}
|
||||
}
|
||||
25
eksempler/scenarioer/notatet/kjede_s5.json
Normal file
25
eksempler/scenarioer/notatet/kjede_s5.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"navn": "kjede_s5",
|
||||
"beskrivelse": "Gjenskaper data/kjede_s5.dat: scenario 5: underordne og utvide. Regime A (forsinkelse 3, reaktivitet 3), avsnitt 8.2.",
|
||||
"modell": "simulate_chain",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"andel_utenom": {
|
||||
"andel": 0.2,
|
||||
"fra": 24
|
||||
},
|
||||
"kapasitet": {
|
||||
"faktor": 1.34,
|
||||
"fra": 24,
|
||||
"over": 24
|
||||
}
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]
|
||||
}
|
||||
}
|
||||
13
eksempler/scenarioer/notatet/mpc11.json
Normal file
13
eksempler/scenarioer/notatet/mpc11.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"navn": "mpc11",
|
||||
"beskrivelse": "Gjenskaper data/mpc11.dat: regime D, modellprediktiv styring (MPC) med forsinkelse 11 mnd. Tar om lag 7 sekunder.",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "MPC",
|
||||
"forsinkelse": 11
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T"]
|
||||
}
|
||||
}
|
||||
14
eksempler/scenarioer/notatet/p11.json
Normal file
14
eksempler/scenarioer/notatet/p11.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "p11",
|
||||
"beskrivelse": "Gjenskaper data/p11.dat i notatet: regime C, P-regel med reaktivitet 3 og forsinkelse 11 mnd, uten straffesakskjede (avsnitt 8.1).",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 11,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T"]
|
||||
}
|
||||
}
|
||||
14
eksempler/scenarioer/notatet/p11d.json
Normal file
14
eksempler/scenarioer/notatet/p11d.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "p11d",
|
||||
"beskrivelse": "Gjenskaper data/p11d.dat: regime E, avdempet P-regel (reaktivitet 1,2) med samme forsinkelse som regime C (11 mnd).",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 11,
|
||||
"reaktivitet": 1.2
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T"]
|
||||
}
|
||||
}
|
||||
14
eksempler/scenarioer/notatet/p3.json
Normal file
14
eksempler/scenarioer/notatet/p3.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "p3",
|
||||
"beskrivelse": "Gjenskaper data/p3.dat i notatet: regime A, P-regel med reaktivitet 3 og forsinkelse 3 mnd, uten straffesakskjede (avsnitt 8.1).",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T"]
|
||||
}
|
||||
}
|
||||
14
eksempler/scenarioer/notatet/p6.json
Normal file
14
eksempler/scenarioer/notatet/p6.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "p6",
|
||||
"beskrivelse": "Gjenskaper data/p6.dat i notatet: regime B, P-regel med reaktivitet 3 og forsinkelse 6 mnd, uten straffesakskjede (avsnitt 8.1).",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 6,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "U", "L", "T"]
|
||||
}
|
||||
}
|
||||
18
eksempler/scenarioer/notatet/prio_s2.json
Normal file
18
eksempler/scenarioer/notatet/prio_s2.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"navn": "prio_s2",
|
||||
"beskrivelse": "Gjenskaper kolonnene i data/prio_s2.dat: domstol med to sakstyper, scenario 2, ingen tiltak. Kjøringen med forrang (prio = true) gir kolonnene t, K, KA, KB, tauA og tauB; kjøringen uten (FIFO, prio = false) gir tauF (= tauA), KF (= K) og KAF (= KA).",
|
||||
"modell": "simulate_chain2",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.CH2.prio",
|
||||
"verdier": [true, false]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "KA", "KB", "tauA", "tauB"]
|
||||
}
|
||||
}
|
||||
24
eksempler/scenarioer/notatet/prio_s3.json
Normal file
24
eksempler/scenarioer/notatet/prio_s3.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"navn": "prio_s3",
|
||||
"beskrivelse": "Gjenskaper kolonnene i data/prio_s3.dat: domstol med to sakstyper, scenario 3, 20 % av alle saker (en tredjedel av klasse B) ledes utenom fra måned 24. Kjøringen med forrang (prio = true) gir kolonnene t, K, KA, KB, tauA og tauB; kjøringen uten (FIFO, prio = false) gir tauF (= tauA), KF (= K) og KAF (= KA).",
|
||||
"modell": "simulate_chain2",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"andel_utenom": {
|
||||
"andel": 0.2,
|
||||
"fra": 24
|
||||
}
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.CH2.prio",
|
||||
"verdier": [true, false]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "KA", "KB", "tauA", "tauB"]
|
||||
}
|
||||
}
|
||||
29
eksempler/scenarioer/notatet/prio_s5.json
Normal file
29
eksempler/scenarioer/notatet/prio_s5.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"navn": "prio_s5",
|
||||
"beskrivelse": "Gjenskaper kolonnene i data/prio_s5.dat: domstol med to sakstyper, scenario 5, underordne og utvide. Kjøringen med forrang (prio = true) gir kolonnene t, K, KA, KB, tauA og tauB; kjøringen uten (FIFO, prio = false) gir tauF (= tauA), KF (= K) og KAF (= KA).",
|
||||
"modell": "simulate_chain2",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 3,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"tiltak": {
|
||||
"andel_utenom": {
|
||||
"andel": 0.2,
|
||||
"fra": 24
|
||||
},
|
||||
"kapasitet": {
|
||||
"faktor": 1.34,
|
||||
"fra": 24,
|
||||
"over": 24
|
||||
}
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.CH2.prio",
|
||||
"verdier": [true, false]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K", "KA", "KB", "tauA", "tauB"]
|
||||
}
|
||||
}
|
||||
18
eksempler/scenarioer/notatet/sat_B.json
Normal file
18
eksempler/scenarioer/notatet/sat_B.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"navn": "sat_B",
|
||||
"beskrivelse": "Gjenskaper kolonnene i data/sat_B.dat: regime B (forsinkelse 6 mnd) uten politimetning og med halvmetningsnivå K_sat = 3 og 1,5 (avsnitt 8.3). Kolonnene K, Ks3 og Ks15 i notatets fil er K fra hver av de tre kjøringene.",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 6,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.P.ksat",
|
||||
"verdier": ["inf", 3, 1.5]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K"]
|
||||
}
|
||||
}
|
||||
18
eksempler/scenarioer/notatet/sat_C.json
Normal file
18
eksempler/scenarioer/notatet/sat_C.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"navn": "sat_C",
|
||||
"beskrivelse": "Gjenskaper kolonnene i data/sat_C.dat: regime C (forsinkelse 11 mnd) uten politimetning og med halvmetningsnivå K_sat = 3 og 1,5 (avsnitt 8.3). Kolonnene K, Ks3 og Ks15 i notatets fil er K fra hver av de tre kjøringene.",
|
||||
"modell": "simulate",
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"forsinkelse": 11,
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.P.ksat",
|
||||
"verdier": ["inf", 3, 1.5]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K"]
|
||||
}
|
||||
}
|
||||
24
eksempler/scenarioer/notatet/vippe.json
Normal file
24
eksempler/scenarioer/notatet/vippe.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"navn": "vippe",
|
||||
"beskrivelse": "Gjenskaper kolonnene fast, p3 og p6 i data/vippe.dat: rekruttering med vippepunkt og et forbigående sjokk (Omega x 1,6 i måned 12-36), avsnitt 8.4. Rutenettet gir fast innsats (to like kjøringer, forsinkelsen brukes ikke) og P-regel med forsinkelse 3 og 6.",
|
||||
"modell": "simulate_soc",
|
||||
"sjokk": {
|
||||
"faktor": 1.6,
|
||||
"fra": 12,
|
||||
"til": 36
|
||||
},
|
||||
"sveip": [
|
||||
{
|
||||
"parameter": "styring.regulator",
|
||||
"verdier": ["fast", "P"]
|
||||
},
|
||||
{
|
||||
"parameter": "styring.forsinkelse",
|
||||
"verdier": [3, 6]
|
||||
}
|
||||
],
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K"]
|
||||
}
|
||||
}
|
||||
28
eksempler/scenarioer/notatet/vippe_tak.json
Normal file
28
eksempler/scenarioer/notatet/vippe_tak.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"navn": "vippe_tak",
|
||||
"beskrivelse": "Gjenskaper kolonnene p3cap og p6cap i data/vippe.dat: som vippe.json, men med tak på innsatsen (Tmax = 1,5) og politimetning (K_sat = 1,5).",
|
||||
"modell": "simulate_soc",
|
||||
"parametere": {
|
||||
"P": {
|
||||
"Tmax": 1.5,
|
||||
"ksat": 1.5
|
||||
}
|
||||
},
|
||||
"sjokk": {
|
||||
"faktor": 1.6,
|
||||
"fra": 12,
|
||||
"til": 36
|
||||
},
|
||||
"styring": {
|
||||
"regulator": "P",
|
||||
"reaktivitet": 3
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "styring.forsinkelse",
|
||||
"verdier": [3, 6]
|
||||
},
|
||||
"utdata": {
|
||||
"format": "notat",
|
||||
"kolonner": ["t", "K"]
|
||||
}
|
||||
}
|
||||
9
eksempler/scenarioer/sjokkstorrelse_kjede.json
Normal file
9
eksempler/scenarioer/sjokkstorrelse_kjede.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navn": "sjokkstorrelse_kjede",
|
||||
"beskrivelse": "Hvor mye betyr størrelsen på sjokket når domstolen er en flaskehals og ingen tiltak settes inn? Notatet bruker 30 % økning i det strukturelle pådraget (scenario 2, avsnitt 8.2); her går økningen fra 10 til 60 %. Se på saksbehandlingstiden og tilliten ved slutt, og på om kriminaliteten ved slutt er lik den høyeste: da har forløpet ikke funnet en ny balanse innen 120 måneder.",
|
||||
"modell": "simulate_chain",
|
||||
"sveip": {
|
||||
"parameter": "sjokk.faktor",
|
||||
"verdier": [1.1, 1.2, 1.3, 1.45, 1.6]
|
||||
}
|
||||
}
|
||||
16
eksempler/scenarioer/stabilitet_reaktivitet_forsinkelse.json
Normal file
16
eksempler/scenarioer/stabilitet_reaktivitet_forsinkelse.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"navn": "stabilitet_reaktivitet_forsinkelse",
|
||||
"beskrivelse": "Hvor går grensen for når styringen selv skaper svingninger? Rutenett over reaktivitet (kp) og forsinkelse (tau) i modellen uten straffesakskjede, 240 måneder. Tabellen nederst viser spennet i K de siste 60 månedene: null betyr at svingningene har dødd ut, et tall over null at de holder seg. Sammenlign med notatets kritiske forsinkelse (proposisjon 1, Hayes), som med notatets tall er uendelig for kp = 1, 27,8 mnd for kp = 1,5, 13,8 for kp = 2, 7,2 for kp = 3, 4,2 for kp = 4,5 og 3,0 for kp = 6. Nær grensen dør svingningene sakte ut, så små tall rett under grensen er rester av innsvinget; sammenlign med spennet etter måned 60 i hovedtabellen. Over grensen vokser svingningene til taket på innsatsen (Tmax = 3) og gulvet på 0 holder dem i sjakk. Den lineære analysen gjelder små avvik fra likevekten før sjokket, simuleringen hele forløpet.",
|
||||
"modell": "simulate",
|
||||
"slutt": 240,
|
||||
"sveip": [
|
||||
{
|
||||
"parameter": "styring.reaktivitet",
|
||||
"verdier": [1, 1.5, 2, 3, 4.5, 6]
|
||||
},
|
||||
{
|
||||
"parameter": "styring.forsinkelse",
|
||||
"verdier": [2, 4, 6, 8, 11, 14]
|
||||
}
|
||||
]
|
||||
}
|
||||
14
eksempler/scenarioer/utvidelse_bolge.json
Normal file
14
eksempler/scenarioer/utvidelse_bolge.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"navn": "utvidelse_bolge",
|
||||
"beskrivelse": "Hva etterlater en forbigående bølge i kriminalitetspresset? Pådraget stiger lineært med 40 % fra måned 12 til 18, ligger der til måned 36 og er tilbake på utgangsnivået i måned 48 (utvidelsen paadrag_tidsserie). Kjøres med domstolen som flaskehals (notatets CH0.Z; null i sveipet betyr notatets verdi) og uten flaskehals (\"inf\"). Spørsmålet er om køen og tillitstapet blir værende etter at presset er borte.",
|
||||
"modell": "utvidelse:paadrag_tidsserie",
|
||||
"slutt": 180,
|
||||
"utvidelse": {
|
||||
"form": "lineaer",
|
||||
"punkter": [[12, 1.0], [18, 1.4], [36, 1.4], [48, 1.0]]
|
||||
},
|
||||
"sveip": {
|
||||
"parameter": "parametere.CH0.Z",
|
||||
"verdier": [null, "inf"]
|
||||
}
|
||||
}
|
||||
208
modell/arbeidskopi.py
Normal file
208
modell/arbeidskopi.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# 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:]))
|
||||
1508
modell/scenario.py
Normal file
1508
modell/scenario.py
Normal file
File diff suppressed because it is too large
Load diff
8
modell/utvidelser/__init__.py
Normal file
8
modell/utvidelser/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Utvidelser: mekanismer utenfor kjernen.
|
||||
|
||||
Hver modul her har NAVN, BESKRIVELSE, PARAMETERE, simuler(scenario) og
|
||||
grensetilfelle(). Regelen er den samme som i notatet: en utvidelse skal ha et
|
||||
grensetilfelle som gjenskaper kjernen eksakt, og en test som viser det. Se
|
||||
docs/utvidelser.md.
|
||||
"""
|
||||
258
modell/utvidelser/paadrag_tidsserie.py
Normal file
258
modell/utvidelser/paadrag_tidsserie.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Utvidelse: det strukturelle pådraget følger en tidsserie.
|
||||
|
||||
I kjernen (sim.py, Omega) er pådraget Omega0 før måned t_shock og Omega0 * shock
|
||||
etterpå: ett varig sjokk. Her følger pådraget Omega0 * f(t), der f er en
|
||||
tidsserie brukeren oppgir, som trappetrinn eller stykkevis lineær. Da kan en
|
||||
forbigående bølge, et sjokk i flere trinn eller en gradvis endring kjøres.
|
||||
|
||||
Styringen er P-regelen fra kjernen. Med kjeden (parametere.CH0.Z endelig) er
|
||||
regnestegene de samme som i simulate_chain, med tiltakene i kjeden (andel
|
||||
utenom, kapasitet, trommel-buffer-tau). Uten kjeden (CH0.Z = "inf") er de de
|
||||
samme som i simulate.
|
||||
|
||||
Grensetilfelle: et trappetrinn lik notatets sjokk (faktor 1,3 fra måned 12)
|
||||
gjenskaper simulate_chain("P", 3.0, CH0) og, med CH0.Z = inf, simulate("P", 3.0)
|
||||
bit for bit. Regnestegene under er kopier av løkkene i sim.py; endres de der,
|
||||
må de endres likt her, og grensetilfellet viser om det er glemt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
Arr = npt.NDArray[np.float64]
|
||||
|
||||
NAVN = "paadrag_tidsserie"
|
||||
BESKRIVELSE = ("det strukturelle pådraget Omega følger en tidsserie (trappetrinn eller stykkevis "
|
||||
"lineær) i stedet for ett varig sjokk; P-regel, med eller uten straffesakskjeden")
|
||||
PARAMETERE: dict[str, object] = {
|
||||
"form": "trapp", # "trapp" eller "lineaer"
|
||||
"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
|
||||
FORMER = ("trapp", "lineaer")
|
||||
MAKS_PUNKTER = 1000
|
||||
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
|
||||
|
||||
|
||||
def _sim() -> ModuleType:
|
||||
"""Kjernen (sim.py) fra modellmappa denne utvidelsen ligger i."""
|
||||
sim = sys.modules.get("sim")
|
||||
if sim is not None and Path(str(getattr(sim, "__file__", ""))).resolve().parent == KJERNE:
|
||||
return sim
|
||||
sys.modules.pop("sim", None)
|
||||
if str(KJERNE) in sys.path:
|
||||
sys.path.remove(str(KJERNE))
|
||||
sys.path.insert(0, str(KJERNE))
|
||||
return importlib.import_module("sim")
|
||||
|
||||
|
||||
def _punkter(raa: object) -> list[tuple[float, float]]:
|
||||
"""Valider punktene: [[måned, faktor], ...] med stigende måneder."""
|
||||
hvor = "utvidelse.punkter"
|
||||
if not isinstance(raa, list) or not 1 <= len(raa) <= MAKS_PUNKTER:
|
||||
melding = f"{hvor}: må være en liste med 1 til {MAKS_PUNKTER} punkter [måned, faktor]."
|
||||
raise ValueError(melding)
|
||||
ut: list[tuple[float, float]] = []
|
||||
for nr, p in enumerate(raa):
|
||||
ok = (isinstance(p, list) and len(p) == 2 # noqa: PLR2004 - et punkt er et par
|
||||
and all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in p))
|
||||
if not ok:
|
||||
melding = f"{hvor}[{nr}]: må være et par [måned, faktor] med tall, ikke {p!r}."
|
||||
raise ValueError(melding)
|
||||
tid, faktor = float(p[0]), float(p[1])
|
||||
if not (math.isfinite(tid) and math.isfinite(faktor)) or tid < 0 or faktor < 0:
|
||||
melding = f"{hvor}[{nr}]: måned og faktor må være tall fra og med 0."
|
||||
raise ValueError(melding)
|
||||
if ut and tid <= ut[-1][0]:
|
||||
melding = f"{hvor}[{nr}]: månedene må stige (måned {tid:g} kommer etter {ut[-1][0]:g})."
|
||||
raise ValueError(melding)
|
||||
ut.append((tid, faktor))
|
||||
return ut
|
||||
|
||||
|
||||
def faktor(tid: float, form: str, punkter: list[tuple[float, float]]) -> float:
|
||||
"""f(t): 1 før første punkt; trapp holder verdien til neste punkt, lineaer interpolerer."""
|
||||
if tid < punkter[0][0]:
|
||||
return 1.0
|
||||
if form == "trapp":
|
||||
verdi = 1.0
|
||||
for t0, f0 in punkter:
|
||||
if tid >= t0:
|
||||
verdi = f0
|
||||
else:
|
||||
break
|
||||
return verdi
|
||||
for (t0, f0), (t1, f1) in zip(punkter, punkter[1:]):
|
||||
if t0 <= tid < t1:
|
||||
return f0 + (f1 - f0) * (tid - t0) / (t1 - t0)
|
||||
return punkter[-1][1]
|
||||
|
||||
|
||||
def sjekk(scenario: dict[str, object]) -> None:
|
||||
"""Kalles av scenariokjøreren før kjøringen; ValueError gir en norsk feilmelding."""
|
||||
sim = _sim()
|
||||
styring = cast("dict[str, Any]", scenario["styring"])
|
||||
if styring["regulator"] != "P":
|
||||
melding = 'styring.regulator: utvidelsen paadrag_tidsserie bruker bare "P".'
|
||||
raise ValueError(melding)
|
||||
par = cast("dict[str, dict[str, Any]]", scenario["parametere"])
|
||||
for nokkel in ("shock", "t_shock"):
|
||||
if par["P"][nokkel] != sim.P[nokkel]:
|
||||
melding = (f"parametere.P.{nokkel}: brukes ikke av paadrag_tidsserie. Pådraget følger "
|
||||
"utvidelse.punkter.")
|
||||
raise ValueError(melding)
|
||||
if math.isinf(par["CH0"]["Z"]) and scenario.get("tiltak"):
|
||||
melding = "tiltak: virker bare i kjeden, og parametere.CH0.Z er inf (ingen kjede)."
|
||||
raise ValueError(melding)
|
||||
u = cast("dict[str, Any]", scenario["utvidelse"])
|
||||
if u["form"] not in FORMER:
|
||||
melding = f'utvidelse.form: må være "trapp" eller "lineaer", ikke {u["form"]!r}.'
|
||||
raise ValueError(melding)
|
||||
_punkter(u["punkter"])
|
||||
|
||||
|
||||
def _deriv(tilstand: tuple[float, float, float, float], om: float,
|
||||
p: dict[str, Any]) -> tuple[float, float, float]:
|
||||
"""Som deriv i sim.py, med pådraget om i stedet for Omega(t, p). Tilstand: K, U, L, T."""
|
||||
sim = _sim()
|
||||
krim, trygg, tillit, innsats = tilstand
|
||||
d_krim = om - sim.removal(krim, tillit, innsats, p) - p["delta"] * krim
|
||||
u_eq = np.clip(p["U0"] - p["psiU"] * (krim - 1.0), 0.0, 1.0)
|
||||
d_trygg = p["kU"] * (u_eq - trygg)
|
||||
over = max(0.0, innsats - p["Tprop"] * krim)
|
||||
d_tillit = (p["kL"] * (p["Lam0"] + p["mu1"] * (trygg - p["U0"]) - tillit)
|
||||
- p["mu2"] * tillit * over ** 2)
|
||||
return d_krim, d_trygg, d_tillit
|
||||
|
||||
|
||||
def _uten_kjede(t: Arr, om: Arr, styring: tuple[float, float], p: dict[str, Any]) -> dict[str, object]:
|
||||
"""Løkka i simulate (P-regelen), med pådraget fra tidsserien. Styring: (tau, kp)."""
|
||||
tau, kp = styring
|
||||
sim = _sim()
|
||||
dt = sim.DT
|
||||
n = len(t)
|
||||
krim = np.ones(n)
|
||||
trygg = np.full(n, p["U0"])
|
||||
tillit = np.full(n, p["Lam0"])
|
||||
innsats = np.full(n, p["T0"])
|
||||
lag = round(tau / dt)
|
||||
for k in range(n - 1):
|
||||
k_obs = krim[k - lag] if k - lag >= 0 else 1.0
|
||||
innsats[k] = np.clip(p["T0"] + kp * (k_obs - p["Kstar"]), 0.0, p["Tmax"])
|
||||
d_krim, d_trygg, d_tillit = _deriv((krim[k], trygg[k], tillit[k], innsats[k]), om[k], p)
|
||||
krim[k + 1] = krim[k] + dt * d_krim
|
||||
trygg[k + 1] = trygg[k] + dt * d_trygg
|
||||
tillit[k + 1] = np.clip(tillit[k] + dt * d_tillit, 1e-4, 1 - 1e-4)
|
||||
innsats[-1] = innsats[-2]
|
||||
return {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats}
|
||||
|
||||
|
||||
def _med_kjede(t: Arr, om: Arr, styring: tuple[float, float], p: dict[str, Any],
|
||||
ch: dict[str, Any]) -> dict[str, object]:
|
||||
"""Løkka i simulate_chain (P-regelen og trommel-buffer-tau), med pådraget fra tidsserien."""
|
||||
tau, kp = styring
|
||||
sim = _sim()
|
||||
dt = sim.DT
|
||||
n = len(t)
|
||||
krim = np.ones(n)
|
||||
trygg = np.full(n, p["U0"])
|
||||
tillit = np.full(n, p["Lam0"])
|
||||
innsats = np.full(n, p["T0"])
|
||||
ko, tid_d, inn_d, ut_d = np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)
|
||||
q0, tau_ref = sim.init_chain(ch, p)
|
||||
ko[0] = q0
|
||||
lag = round(tau / dt)
|
||||
for k in range(n - 1):
|
||||
k_obs = krim[k - lag] if k - lag >= 0 else 1.0
|
||||
innsats[k] = np.clip(p["T0"] + kp * (k_obs - p["Kstar"]), 0.0, p["Tmax"])
|
||||
if ch["dbr"] is not None:
|
||||
z, xi = sim.chain_at(ch, t[k])
|
||||
t_tak = ch["dbr"] * z / (xi * p["gamma"] * tillit[k] ** p["eta"] * k_obs)
|
||||
innsats[k] = min(innsats[k], max(t_tak, 0.0))
|
||||
d_krim, d_trygg, d_tillit, d_ko, tid_d[k], inn_d[k], ut_d[k] = sim.deriv_chain(
|
||||
krim[k], trygg[k], tillit[k], innsats[k], ko[k], t[k], p, ch, tau_ref, Om0=om[k])
|
||||
krim[k + 1] = krim[k] + dt * d_krim
|
||||
trygg[k + 1] = trygg[k] + dt * d_trygg
|
||||
tillit[k + 1] = np.clip(tillit[k] + dt * d_tillit, 1e-4, 1 - 1e-4)
|
||||
ko[k + 1] = max(ko[k] + dt * d_ko, 0.0)
|
||||
innsats[-1] = innsats[-2]
|
||||
tid_d[-1] = tid_d[-2]
|
||||
inn_d[-1] = inn_d[-2]
|
||||
ut_d[-1] = ut_d[-2]
|
||||
par = [sim.chain_at(ch, float(x)) for x in t]
|
||||
z_t = np.array([float(a) for a, _ in par])
|
||||
return {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats, "Q": ko, "tauD": tid_d,
|
||||
"JD": inn_d, "RD": ut_d, "Z": z_t, "Z_rel": z_t / ch["Z"],
|
||||
"xi": np.array([float(b) for _, b in par]), "tau_ref": tau_ref}
|
||||
|
||||
|
||||
def simuler(scenario: dict[str, object]) -> dict[str, object]:
|
||||
"""Kjør modellen med pådraget fra tidsserien. Nøkler som i kjernen (t, K, U, L, T, ...)."""
|
||||
sim = _sim()
|
||||
styring = cast("dict[str, Any]", scenario["styring"])
|
||||
par = cast("dict[str, dict[str, Any]]", scenario["parametere"])
|
||||
u = cast("dict[str, Any]", scenario["utvidelse"])
|
||||
p, ch = par["P"], par["CH0"]
|
||||
punkter = _punkter(u["punkter"])
|
||||
slutt = float(cast("float", scenario["slutt"]))
|
||||
n = int(slutt / sim.DT) + 1
|
||||
t = np.arange(n) * sim.DT
|
||||
f = np.array([faktor(x, u["form"], punkter) for x in t])
|
||||
om = np.array([p["Omega0"] * x for x in f])
|
||||
tau_kp = (float(styring["forsinkelse"]), float(styring["reaktivitet"]))
|
||||
ut = _uten_kjede(t, om, tau_kp, p) if math.isinf(ch["Z"]) else _med_kjede(t, om, tau_kp, p, ch)
|
||||
ut["Omega_faktor"] = f
|
||||
return ut
|
||||
|
||||
|
||||
def _scenario(p: dict[str, Any], ch: dict[str, Any], slutt: float) -> dict[str, object]:
|
||||
return {
|
||||
"styring": {"regulator": "P", "forsinkelse": 3.0, "reaktivitet": 3.0},
|
||||
"slutt": slutt,
|
||||
"parametere": {"P": p, "CH0": ch},
|
||||
"tiltak": {},
|
||||
"utvidelse": {"form": "trapp", "punkter": [[float(p["t_shock"]), float(p["shock"])]]},
|
||||
}
|
||||
|
||||
|
||||
def _avvik(egen: dict[str, object], fasit: dict[str, Any], navn: tuple[str, ...]) -> float:
|
||||
"""Største absolutte avvik over tidsseriene (inf hvis lengdene er ulike)."""
|
||||
storst = 0.0
|
||||
for n in navn:
|
||||
a, b = np.asarray(egen[n]), np.asarray(fasit[n])
|
||||
if a.shape != b.shape:
|
||||
return math.inf
|
||||
storst = max(storst, float(np.abs(a - b).max()))
|
||||
return storst
|
||||
|
||||
|
||||
def grensetilfelle() -> list[tuple[str, float]]:
|
||||
"""Trappetrinn lik notatets sjokk gjenskaper kjernen; avvikene skal være eksakt 0."""
|
||||
sim = _sim()
|
||||
slutt = float(sim.TEND)
|
||||
egen = simuler(_scenario(dict(sim.P), dict(sim.CH0), slutt))
|
||||
fasit = sim.simulate_chain("P", 3.0, sim.CH0)
|
||||
d_kjede = max(_avvik(egen, fasit, ("t", "K", "U", "L", "T", "Q", "tauD", "JD", "RD")),
|
||||
abs(float(cast("float", egen["tau_ref"])) - float(fasit["tau_ref"])))
|
||||
egen0 = simuler(_scenario(dict(sim.P), dict(sim.CH0, Z=math.inf), slutt))
|
||||
t, krim, trygg, tillit, innsats = sim.simulate("P", 3.0)
|
||||
d_uten = _avvik(egen0, {"t": t, "K": krim, "U": trygg, "L": tillit, "T": innsats},
|
||||
("t", "K", "U", "L", "T"))
|
||||
return [
|
||||
('paadrag_tidsserie: trapp lik notatets sjokk, med kjeden = simulate_chain("P", 3, CH0)',
|
||||
d_kjede),
|
||||
('paadrag_tidsserie: trapp lik notatets sjokk, CH0.Z = inf = simulate("P", 3)', d_uten),
|
||||
]
|
||||
102
tests/test_arbeidskopi.py
Normal file
102
tests/test_arbeidskopi.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""arbeidskopi.py: kopien lages, nekter en mappe som ikke er tom, og kan kjøres med --modell."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from conftest import EKSEMPLER, FASIT, ROT, kjor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
KJEDE_S2 = EKSEMPLER / "scenarioer" / "notatet" / "kjede_s2.json"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def arbeidskopi(tmp_path: Path) -> Path:
|
||||
"""En arbeidskopi laget med kjor.py arbeidskopi."""
|
||||
mal = tmp_path / "rettsstat-modell"
|
||||
prosess = kjor("arbeidskopi", str(mal))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert f"Arbeidskopien er laget i {mal}" in prosess.stdout
|
||||
return mal
|
||||
|
||||
|
||||
def test_kopien_har_mappene_og_les_meg(arbeidskopi: Path) -> None:
|
||||
for navn in ("modell", "eksempler", "fasit", "tests"):
|
||||
assert (arbeidskopi / navn).is_dir(), navn
|
||||
for navn in ("modell/kjor.py", "modell/scenario.py", "modell/kjerne/sim.py", "modell/kjerne/KILDE.json",
|
||||
"modell/utvidelser/paadrag_tidsserie.py", "fasit/data/p3.dat",
|
||||
"eksempler/scenarioer/notatet/p3.json"):
|
||||
assert (arbeidskopi / navn).is_file(), navn
|
||||
for navn in ("modell/kjerne/sim.py", "fasit/data/endo_ko.dat"):
|
||||
assert (arbeidskopi / navn).read_bytes() == (ROT / navn).read_bytes()
|
||||
if (ROT / "docs").is_dir():
|
||||
assert sorted(p.name for p in (arbeidskopi / "docs").iterdir()) == \
|
||||
sorted(p.name for p in (ROT / "docs").iterdir() if p.is_file())
|
||||
les_meg = (arbeidskopi / "LES-MEG.md").read_text(encoding="utf-8")
|
||||
for tekst in ("Alt i mappa kan endres, også kjernen", f"python3 {arbeidskopi / 'modell' / 'kjor.py'} scenario",
|
||||
"kontroll", "«bit for bit som notatet»", "«endret: ...»", "docs/", "BIDRA.md"):
|
||||
assert tekst in les_meg, tekst
|
||||
|
||||
|
||||
def test_kopien_har_ikke_mellomlagre_eller_resultater(arbeidskopi: Path) -> None:
|
||||
uonsket = {".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "resultater"}
|
||||
funnet = [p for p in arbeidskopi.rglob("*") if p.name in uonsket or p.suffix == ".pyc"]
|
||||
assert funnet == []
|
||||
|
||||
|
||||
def test_nekter_mappe_som_ikke_er_tom(tmp_path: Path) -> None:
|
||||
mal = tmp_path / "opptatt"
|
||||
mal.mkdir()
|
||||
(mal / "notat.txt").write_text("mitt", encoding="utf-8")
|
||||
prosess = kjor("arbeidskopi", str(mal))
|
||||
assert prosess.returncode == 2
|
||||
assert "finnes og er ikke tom" in prosess.stderr
|
||||
assert sorted(p.name for p in mal.iterdir()) == ["notat.txt"]
|
||||
|
||||
|
||||
def test_tom_mappe_er_greit(tmp_path: Path) -> None:
|
||||
mal = tmp_path / "tom"
|
||||
mal.mkdir()
|
||||
assert kjor("arbeidskopi", str(mal)).returncode == 0
|
||||
assert (mal / "LES-MEG.md").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [[], ["a", "b"]])
|
||||
def test_feil_bruk(args: list[str]) -> None:
|
||||
prosess = kjor("arbeidskopi", *args)
|
||||
assert prosess.returncode == 2
|
||||
assert "arbeidskopi <mappe>" in prosess.stderr
|
||||
|
||||
|
||||
def test_nekter_mappe_inne_i_det_som_kopieres(tmp_path: Path, arbeidskopi: Path) -> None:
|
||||
mal = arbeidskopi / "eksempler" / "ny"
|
||||
prosess = kjor("--modell", str(arbeidskopi), "arbeidskopi", str(mal))
|
||||
assert prosess.returncode == 2, prosess.stdout
|
||||
assert "ligger inne i" in prosess.stderr
|
||||
assert not mal.exists()
|
||||
assert not (tmp_path / "ny").exists()
|
||||
|
||||
|
||||
def test_scenario_kjorer_fra_kopien_med_modell(arbeidskopi: Path, tmp_path: Path) -> None:
|
||||
ut = tmp_path / "ut"
|
||||
prosess = kjor("--modell", str(arbeidskopi), "scenario", str(KJEDE_S2), "--ut", str(ut))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert (ut / "kjede_s2.dat").read_bytes() == (FASIT / "data" / "kjede_s2.dat").read_bytes()
|
||||
|
||||
|
||||
def test_endring_i_kopiens_kjerne_brukes(arbeidskopi: Path, tmp_path: Path) -> None:
|
||||
sim = arbeidskopi / "modell" / "kjerne" / "sim.py"
|
||||
tekst = sim.read_text(encoding="utf-8")
|
||||
assert "shock=1.30," in tekst
|
||||
sim.write_text(tekst.replace("shock=1.30,", "shock=1.40,", 1), encoding="utf-8")
|
||||
ut = tmp_path / "ut"
|
||||
prosess = kjor("--modell", str(arbeidskopi), "scenario", str(KJEDE_S2), "--ut", str(ut))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert (ut / "kjede_s2.dat").read_bytes() != (FASIT / "data" / "kjede_s2.dat").read_bytes()
|
||||
plugin = kjor("scenario", str(KJEDE_S2), "--ut", str(tmp_path / "plugin"))
|
||||
assert plugin.returncode == 0, plugin.stderr
|
||||
assert (tmp_path / "plugin" / "kjede_s2.dat").read_bytes() == (FASIT / "data" / "kjede_s2.dat").read_bytes()
|
||||
304
tests/test_scenario.py
Normal file
304
tests/test_scenario.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""scenario.py: gjenskaping av notatets datafiler, validering, sveip, utdata og determinisme."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import scenario
|
||||
from conftest import EKSEMPLER, FASIT, kjor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
NOTATET = EKSEMPLER / "scenarioer" / "notatet"
|
||||
NYE = EKSEMPLER / "scenarioer"
|
||||
EKSAKT_RASKE = ("p3", "p6", "p11", "p11d", "kjede_s1", "kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5",
|
||||
"endo_e5", "endo_des", "endo_desp", "endo_ko")
|
||||
|
||||
# Kolonnene i notatets fil: (scenariofil, kjøring, kolonne i kjøringens fil).
|
||||
KOLONNEVIS: dict[str, dict[str, tuple[str, str, str]]] = {
|
||||
"sat_B": {"t": ("sat_B", "ksat=inf", "t"), "K": ("sat_B", "ksat=inf", "K"),
|
||||
"Ks3": ("sat_B", "ksat=3", "K"), "Ks15": ("sat_B", "ksat=1.5", "K")},
|
||||
"sat_C": {"t": ("sat_C", "ksat=inf", "t"), "K": ("sat_C", "ksat=inf", "K"),
|
||||
"Ks3": ("sat_C", "ksat=3", "K"), "Ks15": ("sat_C", "ksat=1.5", "K")},
|
||||
"vippe": {"t": ("vippe", "regulator=fast_forsinkelse=3", "t"),
|
||||
"fast": ("vippe", "regulator=fast_forsinkelse=3", "K"),
|
||||
"p3": ("vippe", "regulator=P_forsinkelse=3", "K"), "p6": ("vippe", "regulator=P_forsinkelse=6", "K"),
|
||||
"p3cap": ("vippe_tak", "forsinkelse=3", "K"), "p6cap": ("vippe_tak", "forsinkelse=6", "K")},
|
||||
}
|
||||
for _s in ("prio_s2", "prio_s3", "prio_s5"):
|
||||
KOLONNEVIS[_s] = {
|
||||
**{c: (_s, "prio=true", c) for c in ("t", "K", "KA", "KB", "tauA", "tauB")},
|
||||
"tauF": (_s, "prio=false", "tauA"), "KF": (_s, "prio=false", "K"), "KAF": (_s, "prio=false", "KA"),
|
||||
}
|
||||
|
||||
|
||||
def kjor_fil(fil: Path, ut: Path, capsys: pytest.CaptureFixture[str]) -> tuple[int, str, str]:
|
||||
"""Kjør scenario.main i denne prosessen."""
|
||||
kode = scenario.main([str(fil), "--ut", str(ut)])
|
||||
fanget = capsys.readouterr()
|
||||
return kode, fanget.out, fanget.err
|
||||
|
||||
|
||||
def skriv(tmp_path: Path, data: object, navn: str = "s.json") -> Path:
|
||||
"""Skriv en scenariofil."""
|
||||
fil = tmp_path / navn
|
||||
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
return fil
|
||||
|
||||
|
||||
def kolonner(fil: Path) -> dict[str, list[str]]:
|
||||
"""Kolonnene i en fil i notatets format, som tekst."""
|
||||
linjer = fil.read_text(encoding="utf-8").splitlines()
|
||||
hode = linjer[0].split()
|
||||
rader = [linje.split(" ") for linje in linjer[1:]]
|
||||
return {h: [r[i] for r in rader] for i, h in enumerate(hode)}
|
||||
|
||||
|
||||
# ---------------- notatets datafiler ----------------
|
||||
|
||||
@pytest.mark.parametrize("navn", EKSAKT_RASKE)
|
||||
def test_gjenskaper_notatets_fil_eksakt(navn: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, _, err = kjor_fil(NOTATET / f"{navn}.json", tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
assert (tmp_path / f"{navn}.dat").read_bytes() == (FASIT / "data" / f"{navn}.dat").read_bytes()
|
||||
|
||||
|
||||
@pytest.mark.treg
|
||||
def test_gjenskaper_mpc11_eksakt(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, _, err = kjor_fil(NOTATET / "mpc11.json", tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
assert (tmp_path / "mpc11.dat").read_bytes() == (FASIT / "data" / "mpc11.dat").read_bytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("navn", sorted(KOLONNEVIS))
|
||||
def test_gjenskaper_notatets_fil_kolonnevis(navn: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kjort: dict[str, Path] = {}
|
||||
for scenariofil, _, _ in KOLONNEVIS[navn].values():
|
||||
if scenariofil not in kjort:
|
||||
kode, _, err = kjor_fil(NOTATET / f"{scenariofil}.json", tmp_path / scenariofil, capsys)
|
||||
assert kode == 0, err
|
||||
kjort[scenariofil] = tmp_path / scenariofil
|
||||
fasit = kolonner(FASIT / "data" / f"{navn}.dat")
|
||||
assert set(fasit) == set(KOLONNEVIS[navn])
|
||||
for kolonne, (scenariofil, kjoring, kilde) in KOLONNEVIS[navn].items():
|
||||
egen = kolonner(kjort[scenariofil] / f"{scenariofil}__{kjoring}.dat")
|
||||
assert egen[kilde] == fasit[kolonne], f"{navn}: kolonne {kolonne}"
|
||||
|
||||
|
||||
def test_alle_notatets_simuleringer_har_en_scenariofil() -> None:
|
||||
simulerte = {f.stem for f in (FASIT / "data").glob("*.dat")} - {"stab", "thumb"}
|
||||
dekket = set(EKSAKT_RASKE) | {"mpc11"} | set(KOLONNEVIS)
|
||||
assert simulerte == dekket
|
||||
|
||||
|
||||
# ---------------- utdata ----------------
|
||||
|
||||
def test_sammendrag_csv_og_resultatjson(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "test", "modell": "simulate_chain",
|
||||
"tiltak": {"kapasitet": {"faktor": 1.34}}})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
mappe = tmp_path / "ut"
|
||||
assert sorted(p.name for p in mappe.iterdir()) == ["resultat.json", "sammendrag.md", "test.csv"]
|
||||
assert ut.startswith((mappe / "sammendrag.md").read_text(encoding="utf-8"))
|
||||
assert f"Resultatene ligger i {mappe}" in ut
|
||||
assert "ikke kalibrert på norske data" in ut
|
||||
assert "ikke prognoser" in ut
|
||||
assert "tiltak.kapasitet.faktor" in ut
|
||||
assert "Slik leses tallene: 1,00 er nivået" in ut
|
||||
csv = (mappe / "test.csv").read_text(encoding="utf-8").splitlines()
|
||||
assert csv[0] == "t,K,U,L,T,Q,tauD,JD,RD,xi,Z,Z_rel"
|
||||
assert len(csv) == 1 + 121 # én rad per måned, 0-120
|
||||
assert csv[13].split(",")[0] == "12"
|
||||
data = json.loads((mappe / "resultat.json").read_text(encoding="utf-8"))
|
||||
assert set(data["versjoner"]) == {"python", "numpy", "scipy"}
|
||||
assert set(data["kjerne_sha256"]) == {"sim.py", "diagnose.py", "forlop.py"}
|
||||
brukt = data["kjoringer"][0]["brukt"]
|
||||
assert brukt["parametere"]["P"]["ksat"] == "inf"
|
||||
assert brukt["tiltak"]["kapasitet"] == {"faktor": 1.34, "fra": 24.0, "over": 24.0}
|
||||
assert data["kjoringer"][0]["nokkeltall"]["Z_slutt"] == pytest.approx(1.34)
|
||||
|
||||
|
||||
def test_standard_mappe_er_resultater_under_arbeidsmappa(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "Første forsøk", "modell": "simulate"})
|
||||
monkeypatch.chdir(tmp_path)
|
||||
assert scenario.main([str(fil)]) == 0
|
||||
capsys.readouterr()
|
||||
assert (tmp_path / "resultater" / "Forste-forsok" / "Forste-forsok.csv").is_file()
|
||||
|
||||
|
||||
def test_samme_inndata_gir_samme_bytes(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = NYE / "utvidelse_bolge.json"
|
||||
for mappe in ("a", "b"):
|
||||
assert kjor_fil(fil, tmp_path / mappe, capsys)[0] == 0
|
||||
a = sorted((tmp_path / "a").iterdir())
|
||||
assert [p.name for p in a] == sorted(p.name for p in (tmp_path / "b").iterdir())
|
||||
for p in a:
|
||||
assert p.read_bytes() == (tmp_path / "b" / p.name).read_bytes(), p.name
|
||||
|
||||
|
||||
def test_ny_kjoring_i_samme_mappe_rydder_bare_egne_filer(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
sveip = skriv(tmp_path, {"navn": "x", "modell": "simulate",
|
||||
"sveip": {"parameter": "styring.forsinkelse", "verdier": [3, 6]}})
|
||||
enkel = skriv(tmp_path, {"navn": "x", "modell": "simulate"}, "enkel.json")
|
||||
ut = tmp_path / "ut"
|
||||
assert kjor_fil(sveip, ut, capsys)[0] == 0
|
||||
(ut / "notat.txt").write_text("min egen fil", encoding="utf-8")
|
||||
assert kjor_fil(enkel, ut, capsys)[0] == 0
|
||||
assert sorted(p.name for p in ut.iterdir()) == ["notat.txt", "resultat.json", "sammendrag.md", "x.csv"]
|
||||
|
||||
|
||||
def test_nekter_mappe_med_andre_filer(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate"})
|
||||
ut = tmp_path / "ut"
|
||||
ut.mkdir()
|
||||
(ut / "viktig.txt").write_text("x", encoding="utf-8")
|
||||
kode, _, err = kjor_fil(fil, ut, capsys)
|
||||
assert kode == 2
|
||||
assert "ikke er et tidligere resultat" in err
|
||||
assert sorted(p.name for p in ut.iterdir()) == ["viktig.txt"]
|
||||
|
||||
|
||||
def test_valgte_kolonner_og_hvert(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "k", "modell": "simulate_endo", "regel": "koord", "slutt": 36,
|
||||
"utdata": {"kolonner": ["t", "Z_rel", "xi"], "hvert": 240}})
|
||||
assert kjor_fil(fil, tmp_path / "ut", capsys)[0] == 0
|
||||
linjer = (tmp_path / "ut" / "k.csv").read_text(encoding="utf-8").splitlines()
|
||||
assert linjer[0] == "t,Z_rel,xi"
|
||||
assert [linje.split(",")[0] for linje in linjer[1:]] == ["0", "12", "24", "36"]
|
||||
assert linjer[1] == "0,1,1"
|
||||
|
||||
|
||||
# ---------------- sveip ----------------
|
||||
|
||||
def test_sveip_over_avviklingstid(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "ko", "modell": "simulate_endo", "regel": "koord", "slutt": 180,
|
||||
"sveip": {"parameter": "parametere.ENDO.T_clear", "verdier": [12, None, "inf"]}})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
brukt = [k["brukt"]["parametere"]["ENDO"]["T_clear"] for k in data["kjoringer"]]
|
||||
assert brukt == [12.0, 24.0, "inf"] # null = notatets verdi
|
||||
assert [k["fil"] for k in data["kjoringer"]] == [
|
||||
"ko__T_clear=12.csv", "ko__T_clear=standard.csv", "ko__T_clear=inf.csv"]
|
||||
standard = data["kjoringer"][1]
|
||||
assert standard["vedtak"] == ["mnd 24: bestill +19 % (F_pred=0.134 Q_exc=0.27)",
|
||||
"mnd 36: bestill +12 % (F_pred=0.134 Q_exc=0.56)"]
|
||||
assert round(standard["nokkeltall"]["kapasitetsaar"], 1) == 3.5 # notatets tabell i avsnitt 8.5
|
||||
assert round(standard["nokkeltall"]["frafall"], 1) == 8.3
|
||||
assert "T_clear=standard" in ut
|
||||
|
||||
|
||||
def test_rutenett_gir_alle_kombinasjoner_og_matrise(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "g", "modell": "simulate", "slutt": 150,
|
||||
"sveip": [{"parameter": "styring.reaktivitet", "verdier": [1, 3]},
|
||||
{"parameter": "styring.forsinkelse", "verdier": [3, 11]}]})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
par = [(k["brukt"]["styring"]["reaktivitet"], k["brukt"]["styring"]["forsinkelse"]) for k in data["kjoringer"]]
|
||||
assert par == [(1.0, 3.0), (1.0, 11.0), (3.0, 3.0), (3.0, 11.0)]
|
||||
spenn = {p: k["nokkeltall"]["spenn_K_sist"] for p, k in zip(par, data["kjoringer"])}
|
||||
assert spenn[3.0, 11.0] > 0.3 > spenn[3.0, 3.0] # regime C svinger, regime A ikke
|
||||
assert "rader: styring.reaktivitet, kolonner: styring.forsinkelse" in ut
|
||||
|
||||
|
||||
def test_sveip_gjennom_kjor_py(tmp_path: Path) -> None:
|
||||
prosess = kjor("scenario", str(NOTATET / "sat_B.json"), "--ut", str(tmp_path))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert "ksat=1,5" in prosess.stdout
|
||||
assert len(list(tmp_path.glob("*.dat"))) == 3
|
||||
|
||||
|
||||
# ---------------- validering ----------------
|
||||
|
||||
@pytest.mark.parametrize(("data", "tekster"), [
|
||||
({"modell": "simulate", "styrng": {}}, ["styrng: ukjent felt", 'Mente du "styring"?', "Gyldige felt her:"]),
|
||||
({"modell": "simulat"}, ["modell: må være ett av", 'Mente du "simulate"?']),
|
||||
({"modell": "simulate", "parametere": {"P": {"gama": 0.1}}},
|
||||
["parametere.P.gama: ukjent parameter i P", 'Mente du "gamma"?', "Gyldige: gamma, eta"]),
|
||||
({"modell": "simulate", "parametere": {"CH0": {"Z": 1}}},
|
||||
["parametere.CH0: brukes ikke av modellen simulate", "bruker: P"]),
|
||||
({"modell": "simulate_chain", "parametere": {"CH0": {"Zfun": 1}}}, ["settes med tiltak.kapasitet"]),
|
||||
({"modell": "simulate", "styring": {"forsinkelse": "tre"}},
|
||||
["styring.forsinkelse: må være et tall minst 0", '"tre"']),
|
||||
({"modell": "simulate", "parametere": {"P": {"Lam0": 1.2}}}, ["parametere.P.Lam0", "mindre enn 1"]),
|
||||
({"modell": "simulate", "parametere": {"P": {"gamma": "inf"}}}, ["uendelig er ikke tillatt her"]),
|
||||
({"modell": "simulate_chain2", "parametere": {"CH2": {"c": 2.5}}}, ["parametere.CH2.c: må være et heltall"]),
|
||||
({"modell": "simulate_endo", "regel": "koord", "parametere": {"ENDO": {"qloop": 1}}},
|
||||
["parametere.ENDO.qloop: må være true eller false"]),
|
||||
({"modell": "simulate_endo"}, ["regel: mangler", "sched, desentral, desentral_prog, koord"]),
|
||||
({"modell": "simulate", "regel": "koord"}, ["regel: gjelder bare simulate_endo"]),
|
||||
({"modell": "simulate_chain2", "styring": {"regulator": "MPC"}}, ["styring.regulator", '"P"']),
|
||||
({"modell": "simulate", "sjokk": {"til": 36}}, ["sjokk.til: gjelder bare simulate_soc", "paadrag_tidsserie"]),
|
||||
({"modell": "simulate", "tiltak": {"kapasitet": {"faktor": 1.2}}},
|
||||
["tiltak.kapasitet: brukes ikke av modellen simulate", "simulate_chain, simulate_chain2, simulate_endo"]),
|
||||
({"modell": "simulate_endo", "regel": "koord", "tiltak": {"kapasitet": {"faktor": 1.2}}},
|
||||
['bare regel "sched"']),
|
||||
({"modell": "simulate_chain", "parametere": {"CH0": {"Z": "inf"}}, "tiltak": {"kapasitet": {"faktor": 1.2}}},
|
||||
["tiltak.kapasitet: har ingen mening"]),
|
||||
({"modell": "simulate", "sjokk": {"faktor": 1.2}, "parametere": {"P": {"shock": 1.2}}},
|
||||
["sjokk.faktor: er oppgitt to ganger"]),
|
||||
({"modell": "simulate", "sveip": {"parameter": "styring.reaktivtet", "verdier": [1, 2]}},
|
||||
["sveip[0].parameter", 'ukjent felt "reaktivtet" under styring', 'Mente du "reaktivitet"?']),
|
||||
({"modell": "simulate", "sveip": {"parameter": "parametere.P", "verdier": [1]}},
|
||||
["er en gruppe, ikke én verdi", "parametere.P.gamma"]),
|
||||
({"modell": "simulate", "sveip": {"parameter": "navn", "verdier": ["a"]}},
|
||||
["begynner ikke med et felt som kan sveipes"]),
|
||||
({"modell": "simulate", "sveip": {"parameter": "styring.forsinkelse", "verdier": [3, -1]}},
|
||||
["styring.forsinkelse: må være et tall minst 0", "(i sveipet: styring.forsinkelse = -1)"]),
|
||||
({"modell": "simulate", "sveip": {"parameter": "styring.forsinkelse", "verdier": [3, 3.0]}},
|
||||
["har samme verdi to ganger"]),
|
||||
({"modell": "simulate", "utdata": {"kolonner": ["t", "Q"]}}, ['utdata.kolonner: "Q" finnes ikke', "t, K, U, L, T"]),
|
||||
({"modell": "simulate", "utdata": {"overskrift": "t K"}}, ['gjelder bare "format": "notat"']),
|
||||
({"modell": "utvidelse:finnes_ikke"}, ['fant ikke utvidelsen "finnes_ikke"', "utvidelse:paadrag_tidsserie"]),
|
||||
])
|
||||
def test_feil_i_scenariofila_gir_kode_2_og_navngir_feltet(
|
||||
data: object, tekster: list[str], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, data)
|
||||
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert ut == ""
|
||||
assert err.startswith(f"Feil i scenariofila {fil}: ")
|
||||
for t in tekster:
|
||||
assert t in err, err
|
||||
assert not (tmp_path / "ut").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("innhold", "tekst"), [
|
||||
('{"modell": "simulate",\n "slutt": 120,,}', "ikke gyldig JSON: feil på linje 2, kolonne 15"),
|
||||
('{"modell": "simulate", "parametere": {"P": {"ksat": Infinity}}}', 'Skriv uendelig som "inf"'),
|
||||
('{"modell": "simulate", "slutt": 120, "slutt": 60}', "slutt: feltet står to ganger"),
|
||||
("[1, 2]", "må være ett JSON-objekt"),
|
||||
])
|
||||
def test_feil_json(innhold: str, tekst: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = tmp_path / "s.json"
|
||||
fil.write_text(innhold, encoding="utf-8")
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert tekst in err, err
|
||||
|
||||
|
||||
def test_fil_som_mangler_og_feil_bruk(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert kjor_fil(tmp_path / "finnes_ikke.json", tmp_path / "ut", capsys)[2].startswith("Fant ikke fila")
|
||||
assert scenario.main([]) == 2
|
||||
assert "Mangler scenariofil" in capsys.readouterr().err
|
||||
assert scenario.main(["a.json", "--tull"]) == 2
|
||||
assert "Ukjent valg --tull" in capsys.readouterr().err
|
||||
assert scenario.main(["--hjelp"]) == 0
|
||||
assert capsys.readouterr().out.startswith("Bruk:")
|
||||
|
||||
|
||||
def test_alle_eksempelscenarioer_er_gyldige() -> None:
|
||||
filer = sorted(NYE.glob("*.json")) + sorted(NOTATET.glob("*.json"))
|
||||
assert len(filer) >= 26
|
||||
for fil in filer:
|
||||
sc = scenario.les_scenario(fil)
|
||||
assert sc.kjoringer, fil.name
|
||||
if fil.parent == NYE:
|
||||
assert sc.beskrivelse, f"{fil.name} mangler beskrivelse"
|
||||
158
tests/test_utvidelser.py
Normal file
158
tests/test_utvidelser.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
||||
"""Utvidelsene: grensetilfellene gjenskaper kjernen eksakt, og paadrag_tidsserie virker i scenariokjøreren."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import scenario
|
||||
from conftest import FASIT, MODELL, ROT, kjor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
UTVIDELSER = sorted(f.stem for f in (MODELL / "utvidelser").glob("*.py") if not f.stem.startswith("_"))
|
||||
|
||||
|
||||
def kjor_fil(data: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> tuple[int, str]:
|
||||
"""Skriv og kjør en scenariofil; returnerer exit-kode og feilmelding."""
|
||||
fil = tmp_path / "s.json"
|
||||
fil.write_text(json.dumps(data), encoding="utf-8")
|
||||
kode = scenario.main([str(fil), "--ut", str(tmp_path / "ut")])
|
||||
return kode, capsys.readouterr().err
|
||||
|
||||
|
||||
def test_det_finnes_minst_en_utvidelse() -> None:
|
||||
assert "paadrag_tidsserie" in UTVIDELSER
|
||||
|
||||
|
||||
@pytest.mark.parametrize("navn", UTVIDELSER)
|
||||
def test_grensetilfellene_gjenskaper_kjernen_eksakt(navn: str) -> None:
|
||||
modul = scenario.last_utvidelse(navn)
|
||||
tilfeller = modul.grensetilfelle()
|
||||
assert tilfeller, f"{navn} har ingen grensetilfeller"
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
assert isinstance(beskrivelse, str)
|
||||
assert avvik == 0.0, f"{navn}: {beskrivelse}: {avvik}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("navn", UTVIDELSER)
|
||||
def test_grensesnittet(navn: str) -> None:
|
||||
modul = scenario.last_utvidelse(navn)
|
||||
assert isinstance(modul.NAVN, str)
|
||||
assert navn == modul.NAVN
|
||||
assert isinstance(modul.BESKRIVELSE, str)
|
||||
assert isinstance(modul.PARAMETERE, dict)
|
||||
assert callable(modul.simuler)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("ch0", "fasitfil"), [({}, "kjede_s2"), ({"Z": "inf"}, "p3")])
|
||||
def test_standardverdiene_gjenskaper_notatets_fil_gjennom_kjoreren(
|
||||
ch0: dict[str, object], fasitfil: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kolonner = ["t", "K", "U", "L", "T", "Q", "tauD"] if fasitfil.startswith("kjede") else ["t", "K", "U", "L", "T"]
|
||||
data = {"navn": "u", "modell": "utvidelse:paadrag_tidsserie", "parametere": {"CH0": ch0},
|
||||
"utdata": {"format": "notat", "kolonner": kolonner}}
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
assert (tmp_path / "ut" / "u.dat").read_bytes() == (FASIT / "data" / f"{fasitfil}.dat").read_bytes()
|
||||
|
||||
|
||||
def test_tiltak_i_kjeden_virker_som_i_kjernen(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
data = {"navn": "u", "modell": "utvidelse:paadrag_tidsserie",
|
||||
"tiltak": {"andel_utenom": {"andel": 0.2}, "kapasitet": {"faktor": 1.34}},
|
||||
"utdata": {"format": "notat", "kolonner": ["t", "K", "U", "L", "T", "Q", "tauD"]}}
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
assert (tmp_path / "ut" / "u.dat").read_bytes() == (FASIT / "data" / "kjede_s5.dat").read_bytes()
|
||||
|
||||
|
||||
def test_faktor_trapp_og_lineaer() -> None:
|
||||
modul = importlib.import_module("utvidelser.paadrag_tidsserie")
|
||||
punkter = [(12.0, 1.0), (18.0, 1.4), (36.0, 1.4), (48.0, 1.0)]
|
||||
assert [modul.faktor(t, "lineaer", punkter) for t in (0.0, 12.0, 15.0, 18.0, 30.0, 42.0, 60.0)] == \
|
||||
pytest.approx([1.0, 1.0, 1.2, 1.4, 1.4, 1.2, 1.0])
|
||||
trapp = [(12.0, 1.3), (24.0, 1.1)]
|
||||
assert [modul.faktor(t, "trapp", trapp) for t in (11.95, 12.0, 23.95, 24.0, 100.0)] == [1.0, 1.3, 1.3, 1.1, 1.1]
|
||||
|
||||
|
||||
def test_bolge_gir_forbigaaende_paadrag(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
data = {"navn": "u", "modell": "utvidelse:paadrag_tidsserie", "slutt": 120,
|
||||
"parametere": {"CH0": {"Z": "inf"}},
|
||||
"utvidelse": {"form": "lineaer", "punkter": [[12, 1.0], [18, 1.4], [36, 1.4], [48, 1.0]]},
|
||||
"utdata": {"kolonner": ["t", "K", "Omega_faktor"], "hvert": 240}}
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
rader = [linje.split(",") for linje in (tmp_path / "ut" / "u.csv").read_text(encoding="utf-8").splitlines()[1:]]
|
||||
assert [r[2] for r in rader] == ["1", "1", "1.4", "1.4", "1", "1", "1", "1", "1", "1", "1"]
|
||||
k = [float(r[1]) for r in rader]
|
||||
assert max(k) > 1.05
|
||||
assert abs(k[-1] - 1.0) < 0.01 # uten flaskehals går K tilbake når presset er borte
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("ekstra", "tekst"), [
|
||||
({"utvidelse": {"punkter": [[12, 1.3], [6, 1.0]]}}, "utvidelse.punkter[1]: månedene må stige"),
|
||||
({"utvidelse": {"punkter": [[12]]}}, "utvidelse.punkter[0]: må være et par"),
|
||||
({"utvidelse": {"punkter": 1.3}}, "utvidelse.punkter: må være en liste"),
|
||||
({"utvidelse": {"form": "glatt"}}, 'utvidelse.form: må være "trapp" eller "lineaer"'),
|
||||
({"utvidelse": {"fart": 2}}, "utvidelse.fart: ukjent felt"),
|
||||
({"styring": {"regulator": "MPC"}}, 'bruker bare "P"'),
|
||||
({"sjokk": {"faktor": 1.2}}, "sjokk: brukes ikke med utvidelser"),
|
||||
({"parametere": {"P": {"shock": 1.5}}}, "parametere.P.shock: brukes ikke av paadrag_tidsserie"),
|
||||
({"parametere": {"CH2": {"c": 2}}}, "parametere.CH2: brukes ikke av modellen"),
|
||||
({"parametere": {"CH0": {"Z": "inf"}}, "tiltak": {"kapasitet": {"faktor": 1.2}}}, "tiltak: virker bare i kjeden"),
|
||||
])
|
||||
def test_feil_i_utvidelsens_parametere(ekstra: dict[str, object], tekst: str, tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, err = kjor_fil({"modell": "utvidelse:paadrag_tidsserie", **ekstra}, tmp_path, capsys)
|
||||
assert kode == 2
|
||||
assert tekst in err, err
|
||||
|
||||
|
||||
# ---------------- malen i docs/utvidelser.md ----------------
|
||||
|
||||
def malen() -> str:
|
||||
"""Python-koden mellom markørene i docs/utvidelser.md."""
|
||||
tekst = (ROT / "docs" / "utvidelser.md").read_text(encoding="utf-8")
|
||||
blokk = tekst.split("<!-- mal:start -->", 1)[1].split("<!-- mal:slutt -->", 1)[0]
|
||||
return blokk.split("```python\n", 1)[1].rsplit("```", 1)[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def modell_med_mal(tmp_path: Path) -> Path:
|
||||
"""En kopi av modell/ og fasit/ med malen lagt inn som modell/utvidelser/mal_effektivitet.py."""
|
||||
rot = tmp_path / "med_mal"
|
||||
for navn in ("modell", "fasit"):
|
||||
shutil.copytree(ROT / navn, rot / navn, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
(rot / "modell" / "utvidelser" / "mal_effektivitet.py").write_text(malen(), encoding="utf-8")
|
||||
return rot
|
||||
|
||||
|
||||
def test_malen_har_eksakt_grensetilfelle(modell_med_mal: Path) -> None:
|
||||
modell = modell_med_mal / "modell"
|
||||
kode = ("import importlib, json, sys; sys.path[:0] = [sys.argv[1], sys.argv[2]]; "
|
||||
"m = importlib.import_module('utvidelser.mal_effektivitet'); print(json.dumps(m.grensetilfelle()))")
|
||||
prosess = subprocess.run([sys.executable, "-c", kode, str(modell / "kjerne"), str(modell)],
|
||||
capture_output=True, text=True, check=False)
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
tilfeller = json.loads(prosess.stdout)
|
||||
assert len(tilfeller) == 2
|
||||
assert all(avvik == 0.0 for _, avvik in tilfeller), tilfeller
|
||||
|
||||
|
||||
def test_malen_kjorer_med_scenariofil(modell_med_mal: Path, tmp_path: Path) -> None:
|
||||
fil = tmp_path / "mal.json"
|
||||
for faktor, navn in ((1.0, "en"), (1.2, "annen")):
|
||||
fil.write_text(json.dumps({"navn": navn, "modell": "utvidelse:mal_effektivitet",
|
||||
"utvidelse": {"faktor": faktor},
|
||||
"utdata": {"format": "notat", "kolonner": ["t", "K", "U", "L", "T"]}}),
|
||||
encoding="utf-8")
|
||||
prosess = kjor("--modell", str(modell_med_mal), "scenario", str(fil), "--ut", str(tmp_path / navn))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert (tmp_path / "en" / "en.dat").read_bytes() == (FASIT / "data" / "p3.dat").read_bytes()
|
||||
assert (tmp_path / "annen" / "annen.dat").read_bytes() != (FASIT / "data" / "p3.dat").read_bytes()
|
||||
Loading…
Add table
Add a link
Reference in a new issue