fix: findings from the independent review
Scenario runner: stops when overridden base parameters leave the start out of balance (new field startlikevekt: krev, kalibrer, ubalanse); stops on non-finite results; says that models with a court queue drift slowly even without a shock and prints the drift before the shock; queue in months of inflow; never deletes files it did not write; stricter limits. Check: anchored on the note's own checksums for core and reference files, runs the scenario files too, exact comparison for the diagnosis, honest wording when parts were skipped or an extension is broken. Launcher: unknown field names in input are rejected with a suggestion; unwritable folders give a message, not a traceback. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
parent
2669759f5a
commit
776325c680
14 changed files with 1524 additions and 199 deletions
|
|
@ -36,12 +36,13 @@ regime A og scenario 2.
|
|||
| `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 |
|
||||
| `startlikevekt` | `"krev"`, `"kalibrer"` eller `"ubalanse"` | `"krev"` | alle | hva som skjer når parametrene gjør at startpunktet ikke er en likevekt, 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) |
|
||||
| `slutt` | tall | 120 | alle | horisont i måneder (minst 1, 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 |
|
||||
|
|
@ -101,7 +102,9 @@ Typen følger kjernens standardverdi: tall, heltall (`CH2.c`, `SOC.pexp`) eller
|
|||
`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`.
|
||||
- `ENDO.L1` < `ENDO.L2` og `ENDO.ups_lo` < `ENDO.ups_hi`;
|
||||
- `ENDO.period` (budsjettsyklusen) er et helt antall måneder, minst 1: kjernen gjør budsjettvedtak bare i hele
|
||||
måneder (`sim.py:614`), så en annen verdi ville gitt en regel som aldri gjør vedtak.
|
||||
|
||||
`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
|
||||
|
|
@ -109,9 +112,60 @@ 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.
|
||||
Endrer du en av parametrene som bestemmer likevekten før sjokket, sjekker kjøreren om startpunktet fortsatt er en
|
||||
likevekt; se `startlikevekt` under.
|
||||
|
||||
### `startlikevekt`
|
||||
|
||||
Notatets parametere er kalibrert slik at kriminaliteten K = 1 og innsatsen T = T₀ er en likevekt i grunnmodellen, og
|
||||
slik at domstolen er 95 % utnyttet med 3 måneders saksbehandlingstid. I modellene med kø i domstolen er
|
||||
startpunktet nesten, men ikke helt, en likevekt (se under). Endrer du for eksempel `P.gamma` («hva om politiet blir
|
||||
20 % mer effektivt»), stemmer kalibreringen ikke lenger: modellen driver bort fra startpunktet før sjokket kommer, og
|
||||
det ser ut som en virkning av endringen. Derfor sjekker kjøreren startpunktet for hver kjøring, med kjernens egne
|
||||
funksjoner:
|
||||
|
||||
| Hva sjekkes | Betingelse | Kan kalibreres |
|
||||
|---|---|---|
|
||||
| pådraget mot fjerning og frafall | `Omega0` = `removal(1, Lam0, T0, P)` + `delta` (tillatt rest 10⁻¹²); berøres av `gamma`, `eta`, `delta`, `Omega0`, `T0`, `Lam0` og `ksat` | ja, `P.Omega0` regnes ut på nytt (ikke hvis fila setter `Omega0` selv) |
|
||||
| målet for styringen | `Kstar` = 1 (når regelen virker: P med reaktivitet over 0, eller MPC) | nei |
|
||||
| taket på innsatsen | `T0` ≤ `Tmax` | nei |
|
||||
| forholdsmessighet | `T0` ≤ `Tprop` · 1 (ellers faller tilliten fra start) | nei |
|
||||
| domstolen mot strømmen (`simulate_chain`, `simulate_chain2`, `simulate_endo`, utvidelser med kjede) | `CH0.Z`/`CH0.m` (eller `CH2.Z`/`CH2.m`) er notatets, regnet for strømmen J0 = `gamma`·`Lam0`·`T0` = 0,1. Endres `gamma`, `Lam0` eller `T0` uten at fila setter både `Z` og `m`, passer de ikke lenger | ja: `Z` = J0/0,95 og `m` = 3 − J0/(Z·(1/3 + `varsigma`)), som `sim.py:151-155` (for `CH2` med flere spor: `calibrate_m`); bare det fila ikke setter selv |
|
||||
| domstolen er ikke overbelastet ved start | strømmen til domstolen ved start er mindre enn kapasiteten `Z` | bare når `Z` kalibreres |
|
||||
| `eta` i kjedemodellene | `eta` = 1: kjernen regner strømmen til domstolen ved start uten η (`init_chain`, `simulate_chain2`) | nei |
|
||||
|
||||
`ksat` og rekrutteringen i `simulate_soc` (`SOC`) rekalibrerer kjernen selv (`removal` og `Omega0 − eps·H(1)`), og
|
||||
sjekken bekrefter det.
|
||||
|
||||
**Modellene med kø i domstolen står ikke helt i ro, heller ikke med notatets verdier.** Ved start er
|
||||
dK/dt = ϑ·ς·Q₀ > 0: det som faller fra køen (henleggelse, foreldelse), blir aldri fjernet fra kriminaliteten. Notatets
|
||||
koblede modell uten sjokk (`eksempler/scenarioer/uten_sjokk_kjede.json`) gir K 1,007 i måned 11, 1,015 i måned 120 og
|
||||
1,018 i måned 240, og saksbehandlingstiden 3,1, 4,2 og 4,6 måneder, fortsatt stigende. Det gjelder `simulate_chain`,
|
||||
`simulate_chain2`, `simulate_endo` og utvidelser med kjede, og det er notatets referanse; sjekken over gjelder
|
||||
kalibreringen, ikke denne driften. Derfor:
|
||||
|
||||
- sammendraget for disse modellene kaller 1,00 nivået ved start (notatets referansetilstand), sier at det er nesten,
|
||||
men ikke helt, en likevekt, og viser hvor langt K, T, tilliten og saksbehandlingstiden har drevet fra start til
|
||||
måneden sjokket begynner. Uten sjokk, eller med sjokk fra måned 0, sier det i én setning at modellen kryper sakte
|
||||
oppover;
|
||||
- når små virkninger betyr noe, sammenlign med en kjøring uten sjokk (`"sjokk": {"faktor": 1}`, samme parametere og
|
||||
horisont) og trekk driften fra.
|
||||
|
||||
| Verdi | Hva skjer når startpunktet ikke er en likevekt |
|
||||
|---|---|
|
||||
| `"krev"` (standard) | kjøringen stopper (exit 2) med en melding som sier hva som er galt og de to veiene ut under |
|
||||
| `"kalibrer"` | kjøreren regner om det som kan kalibreres (tabellen over) som i notatets kalibrering, slik at startpunktet blir notatets referansetilstand for de nye verdiene (med samme lille drift i modellene med kø), og sammendraget sier hvilke verdier som ble endret, fra hva og til hva. Er noe som ikke kan kalibreres galt, stopper den |
|
||||
| `"ubalanse"` | kjøringen går fra ubalansen med vilje. Forklaringen under tabellen («1,00 er nivået i referanselikevekten …») byttes ut med hva som er galt og hvor langt K, T, tilliten og saksbehandlingstiden har drevet fram til måneden sjokket begynner (måned 12 når det ikke er noe sjokk, høyst `slutt`) |
|
||||
|
||||
`resultat.json` har for hver kjøring `startlikevekt` med valget, de kalibrerte verdiene (`for` og `etter`) og grunnene
|
||||
til ubalansen.
|
||||
|
||||
**`simulate_endo` har én grense til.** Regelen i kjernen bruker 0,1 som observert strøm de første månedene
|
||||
(`sim.py:588`). Er strømmen ved start (`removal(1, Lam0, T0, P)`) en annen, stopper kjøringen uansett valg. Målt: med
|
||||
`gamma` 0,11 og 0,10 (og `Omega0`, `CH0.Z` og `CH0.m` kalibrert) gir den koordinerte regelen en annen bane enn med
|
||||
riktig observasjon (største avvik 0,33 og 0,44 i tidsseriene, andre budsjettvedtak); de desentraliserte reglene og
|
||||
`sched` endret seg ikke. Å endre `gamma`, `Lam0`, `T0`, `eta` eller `ksat` i `simulate_endo` er derfor en endring i
|
||||
kjernen (arbeidskopi og ferdigheten `forbedre`).
|
||||
|
||||
### `regel` (bare `simulate_endo`)
|
||||
|
||||
|
|
@ -162,9 +216,12 @@ Hvor tiltakene virker:
|
|||
|
||||
### `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
|
||||
Horisont i måneder, minst 1 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.
|
||||
Er horisonten kortere enn forsinkelsen i styringen, stopper kjøreren med en melding om `slutt`. Kommer sjokket ved
|
||||
eller etter `slutt` (eller varer det i `simulate_soc` lenger enn `slutt`), kjøres det, men sammendraget har en
|
||||
merknad om det.
|
||||
|
||||
### `sveip`
|
||||
|
||||
|
|
@ -184,7 +241,7 @@ To sveip i en liste gir et rutenett (alle kombinasjoner, den første parameteren
|
|||
```
|
||||
|
||||
`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:
|
||||
`sjokk`, `tiltak`, `slutt`, `utvidelse` eller `startlikevekt`, og må ende i én verdi, for eksempel:
|
||||
|
||||
| Sti | Hva |
|
||||
|---|---|
|
||||
|
|
@ -229,7 +286,7 @@ LaTeX-kilden leser filene direkte).
|
|||
|---|---|
|
||||
| `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_chain2` | `t K KA KB U L T QA QB tauA tauB RA RB Z Z_rel xi` |
|
||||
| `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` |
|
||||
|
||||
|
|
@ -238,8 +295,10 @@ politiets innsats (1 = før sjokket), `Q` køen i domstolen, `tauD` saksbehandli
|
|||
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`.
|
||||
`simulate_chain` regnes ut av kjøreren fra kjernens `chain_at`, og i `simulate_chain2` fra `Zfun` og `xiBfun` slik
|
||||
`court2` bruker dem (der er `xi` andelen av alle saker som går til domstolen, `piA`·`xiA` + (1 − `piA`)·ξ_B).
|
||||
`Z_rel` i `simulate_endo` er det notatets `endo_*.dat` kaller `Z`. I datafilene står køen i modellens enheter; i
|
||||
sammendraget står den i måneder av innstrømningen før sjokket (se under).
|
||||
|
||||
## Hva kjøreren skriver
|
||||
|
||||
|
|
@ -248,15 +307,22 @@ I mappa `resultater/<navn>/` under mappa du står i, eller i mappa du gir med `-
|
|||
- `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.
|
||||
`simulate_endo` budsjettvedtakene fra regelen. Med `"kalibrer"` står de kalibrerte verdiene øverst, og med
|
||||
`"ubalanse"` driften før sjokket. Under «Merknader» står forhold som ikke stopper kjøringen: at K går utenfor
|
||||
0,01–12 (området kjernens `soc_equilibria` leter i; notatets egne kjøringer ligger mellom 0,83 og 9,82), og at
|
||||
sjokket ligger utenfor horisonten. 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"`.
|
||||
fila ikke satte), nøkkeltallene med full presisjon, budsjettvedtakene og `startlikevekt`. 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.
|
||||
tidspunkter i dem. Finnes mappa fra før med et tidligere resultat (en `resultat.json` med kjørerens egne nøkler), tas
|
||||
bare filene det resultatet laget bort: sammendraget, `resultat.json` og `.csv`/`.dat`-filer uten mappe i navnet. Alle
|
||||
andre filer blir stående. Finnes mappa med andre filer og uten et slikt resultat, stopper kjøreren uten å skrive noe.
|
||||
Kan mappa ikke lages eller skrives i, stopper den med «Kan ikke skrive i <mappe>: <grunn>. Velg en annen mappe med
|
||||
--ut.» (exit 2).
|
||||
|
||||
## Nøkkeltallene
|
||||
|
||||
|
|
@ -273,15 +339,17 @@ nøkkeltall vises bare når modellen har tidsserien det bygger på.
|
|||
| `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 |
|
||||
| `Q_slutt_mnd` (og `QA_slutt_mnd`, `QB_slutt_mnd`) | kø ved slutt, i måneder av innstrømningen før sjokket (Q delt på J0 = `gamma`·`Lam0`·`T0`, 0,1 med notatets tall; køen før sjokket er om lag 2,9) |
|
||||
| `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.
|
||||
Under tabellen står: 1,00 er nivået i referanselikevekten før sjokket for K og T (i modellene med kø: nivået ved
|
||||
start, nesten, men ikke helt, en likevekt, med driften før sjokket), tilliten er `Lam0` der (0,80 med notatets tall),
|
||||
kapasitet 1,00 er kapasiteten ved start, tider er i måneder, og (når modellen har kø) at køen er målt i måneder av
|
||||
innstrømningen før sjokket. Med `"startlikevekt": "ubalanse"` byttes den første delen ut, se over.
|
||||
|
||||
## Feil og exit-koder
|
||||
|
||||
|
|
@ -289,13 +357,15 @@ Exit-kode 0 når alt gikk, 2 ved feil i scenariofila eller bruken. Meldingen nev
|
|||
|
||||
```
|
||||
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: styring.forsinkelse: må være et tall minst 0, 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.
|
||||
Stopper en kjøring i kjernen (en regnefeil, for eksempel en kombinasjon der kjernen ikke finner køen ved start), eller
|
||||
gir den verdier som ikke er endelige tall (uendelig eller NaN, en løsning som har vokst over alle grenser), stopper
|
||||
kjøreren med exit 2. Meldingen sier hvilken kjøring det var og hvilke tidsserier, og ingenting skrives. Store, men
|
||||
endelige utslag stopper ikke; de får en merknad i sammendraget.
|
||||
|
||||
## Eksempelfilene
|
||||
|
||||
|
|
@ -325,3 +395,4 @@ simuleringer, og har ingen scenariofil.
|
|||
| `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`)? |
|
||||
| `uten_sjokk_kjede.json` | hvor mye driver modellen med kø av seg selv uten sjokk (240 mnd)? Nullinjen å sammenligne små virkninger med |
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ gjort i en annen rekkefølge, og da er det ikke lenger kjernens modell.
|
|||
| `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 |
|
||||
| `GJENSKAPER_NOTATET` | tuple, valgfri | navn på scenariofiler i `eksempler/scenarioer/notatet/` (uten `.json`) som utvidelsen med standardverdiene skal gjenskape byte for byte når `"modell"` byttes til `"utvidelse:<navn>"`. Testene kjører dem gjennom kjøreren og sammenligner med `fasit/data` |
|
||||
|
||||
### Hva `simuler` får
|
||||
|
||||
|
|
@ -52,6 +53,11 @@ Dictene kan sendes rett til kjernens funksjoner (`sim.deriv_chain(..., p, ch, ..
|
|||
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.
|
||||
|
||||
Før `simuler` kalles, sjekker kjøreren at startpunktet er en likevekt for dictene i `BRUKER`, på samme måte som for
|
||||
kjernens modeller (`startlikevekt` i [scenarioformat.md](scenarioformat.md)): `P` for grunnmodellen, og `CH0` eller
|
||||
`CH2` for domstolen når kapasiteten er endelig. Med `"startlikevekt": "kalibrer"` får `simuler` de kalibrerte
|
||||
verdiene i `parametere`.
|
||||
|
||||
### Hva `simuler` returnerer
|
||||
|
||||
En dict med tidsserien `"t"` (numpy-array) og andre tidsserier med samme lengde, med kjernens navn der det gir mening
|
||||
|
|
@ -67,6 +73,10 @@ vedtakslinjer i sammendraget.
|
|||
(...)», 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.
|
||||
- Avviket `grensetilfelle()` rapporterer, er utvidelsens egen beregning; en utvidelse som bare returnerer `0.0`,
|
||||
ville gått gjennom. Derfor bør utvidelsen oppgi `GJENSKAPER_NOTATET`: da kjører testen de scenariofilene gjennom
|
||||
scenariokjøreren med utvidelsen og sammenligner datafilene byte for byte med `fasit/data`, uavhengig av
|
||||
`grensetilfelle()`. Mangler den, sier testen fra (hoppet over, med grunn).
|
||||
- `resultat.json` har sha256 for utvidelsens fil, så det er sporbart hvilken versjon en kjøring brukte.
|
||||
|
||||
## Gjennomført eksempel: `paadrag_tidsserie`
|
||||
|
|
@ -97,8 +107,9 @@ Grensetilfellene, som `grensetilfelle()` returnerer:
|
|||
| 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`.
|
||||
Utvidelsen oppgir `GJENSKAPER_NOTATET = ("kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5")`: kjørt gjennom
|
||||
scenariokjøreren med standardverdiene (og tiltakene fra scenario 3–5) gir den notatets datafiler byte for byte. Testene
|
||||
viser i tillegg at den med `CH0.Z` = inf gir `p3.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.
|
||||
|
|
@ -218,8 +229,10 @@ den kan kjøres med en scenariofil, så malen virker slik den står her.
|
|||
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.
|
||||
Kjør testene (`python3 -m pytest tests/test_utvidelser.py`); utvidelsen er med automatisk. Oppgi
|
||||
`GJENSKAPER_NOTATET` hvis standardverdiene gjenskaper en av notatets scenariofiler, så sjekkes grensetilfellet
|
||||
også uavhengig av din egen `grensetilfelle()`. 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.
|
||||
|
|
|
|||
7
eksempler/scenarioer/uten_sjokk_kjede.json
Normal file
7
eksempler/scenarioer/uten_sjokk_kjede.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"navn": "uten_sjokk_kjede",
|
||||
"beskrivelse": "Hvor mye driver modellen med kø i domstolen av seg selv, uten sjokk? Notatets koblede modell (avsnitt 8.2) med alle notatets verdier og sjokket slått av, over 240 måneder. Startpunktet er notatets referansetilstand, men ikke helt en likevekt: det som faller fra køen, blir aldri fjernet fra kriminaliteten, så K og saksbehandlingstiden kryper sakte oppover. Bruk kjøringen som nullinje når små virkninger i andre scenarioer betyr noe: trekk denne driften fra før du tolker dem.",
|
||||
"modell": "simulate_chain",
|
||||
"sjokk": {"faktor": 1.0},
|
||||
"slutt": 240
|
||||
}
|
||||
|
|
@ -14,8 +14,10 @@ eller resultater/. Bare standardbiblioteket.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -35,7 +37,24 @@ DOKUMENTER = (
|
|||
("inndata.md", "inndatafila til diagnose og forlop"),
|
||||
("forbehold.md", "hva modellen kan og ikke kan brukes til"),
|
||||
)
|
||||
BRUK = "Bruk: python3 modell/kjor.py arbeidskopi <mappe>"
|
||||
|
||||
|
||||
def paa_windows(*, windows: bool | None = None) -> bool:
|
||||
"""Sann på Windows (eller når testene ber om det)."""
|
||||
return os.name == "nt" if windows is None else windows
|
||||
|
||||
|
||||
def python_navn(*, windows: bool | None = None) -> str:
|
||||
"""Tolken heter python på Windows og python3 ellers."""
|
||||
return "python" if paa_windows(windows=windows) else "python3"
|
||||
|
||||
|
||||
def sitert(sti: Path | str, *, windows: bool | None = None) -> str:
|
||||
"""En sti slik den må skrives på kommandolinja: cmd/PowerShell på Windows, POSIX-skall ellers."""
|
||||
return subprocess.list2cmdline([str(sti)]) if paa_windows(windows=windows) else shlex.quote(str(sti))
|
||||
|
||||
|
||||
BRUK = f"Bruk: {python_navn()} modell/kjor.py arbeidskopi <mappe>"
|
||||
|
||||
|
||||
class KopiFeil(Exception):
|
||||
|
|
@ -60,11 +79,11 @@ def sjekk_mal(mal: Path) -> None:
|
|||
melding = (f"Mappa {mal} finnes og er ikke tom. Velg en ny mappe; arbeidskopi skriver aldri over noe "
|
||||
"som finnes.")
|
||||
raise KopiFeil(melding)
|
||||
for navn in MAPPER:
|
||||
kilde = (ROT / navn).resolve()
|
||||
if mal == kilde or kilde in mal.parents:
|
||||
melding = f"{mal} ligger inne i {kilde}, som skal kopieres. Velg en mappe utenfor."
|
||||
raise KopiFeil(melding)
|
||||
rot = ROT.resolve()
|
||||
if mal == rot or rot in mal.parents:
|
||||
melding = (f"{mal} ligger inne i {rot}, modellen som skal kopieres. Velg en mappe utenfor, for eksempel "
|
||||
"i arbeidsmappa di.")
|
||||
raise KopiFeil(melding)
|
||||
|
||||
|
||||
def kildeversjon() -> str:
|
||||
|
|
@ -80,13 +99,14 @@ def kildeversjon() -> str:
|
|||
return f"{repo} @ {commit[:7]}" if repo else commit[:7]
|
||||
|
||||
|
||||
def les_meg(mal: Path, docs: list[str]) -> str:
|
||||
def les_meg(mal: Path, docs: list[str], *, windows: bool | None = None) -> str:
|
||||
"""Innholdet i LES-MEG.md."""
|
||||
kjor = f"python3 {shlex.quote(str(mal / 'modell' / 'kjor.py'))}"
|
||||
py = python_navn(windows=windows)
|
||||
kjor = f"{py} {sitert(mal / 'modell' / 'kjor.py', windows=windows)}"
|
||||
eks = mal / "eksempler"
|
||||
|
||||
def q(sti: Path) -> str:
|
||||
return shlex.quote(str(sti))
|
||||
return sitert(sti, windows=windows)
|
||||
|
||||
linjer = [
|
||||
"# Arbeidskopi av modellen bak «Den dynamiske rettsstaten»",
|
||||
|
|
@ -115,7 +135,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
"```",
|
||||
"",
|
||||
"Fra pluginen kan du peke på kopien med `--modell`:",
|
||||
"`python3 <plugin>/modell/kjor.py --modell " + q(mal) + " scenario <fil.json>`. Ferdighetene i "
|
||||
f"`{py} <plugin>/modell/kjor.py --modell " + q(mal) + " scenario <fil.json>`. Ferdighetene i "
|
||||
"pluginen bruker kopien selv når mappa du jobber i, har en `rettsstat-modell/`.",
|
||||
"",
|
||||
("Scenario, forløp og kontroll trenger numpy og scipy. Mangler de, sier kommandoen fra; "
|
||||
|
|
@ -137,7 +157,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
"sier hvilke datafiler og utskrifter som er endret, og største avvik per kolonne."),
|
||||
"",
|
||||
"Kontrollen kjører også `grensetilfelle()` i hver utvidelse; hvert avvik skal være eksakt 0.",
|
||||
"Testene kjøres med `python3 -m pytest` i mappa (krever pytest; de trege med `-m treg`).",
|
||||
f"Testene kjøres med `{py} -m pytest` i mappa (krever pytest; de trege med `-m treg`).",
|
||||
"",
|
||||
"## Dokumentasjon",
|
||||
"",
|
||||
|
|
@ -154,7 +174,7 @@ def les_meg(mal: Path, docs: list[str]) -> str:
|
|||
("Se `BIDRA.md` i pluginen. Kort: lag en patch av endringene (for eksempel med `git diff` hvis du har lagt "
|
||||
"mappa i git) og send den på e-post med en forklaring og utskriften fra kontrollen."),
|
||||
"",
|
||||
"Tekst og kode er under lisensene i pluginen (MIT for kode og data, se `LICENSE`).",
|
||||
"Alt i pluginen er under MIT-lisensen (se `LICENSE`).",
|
||||
"",
|
||||
]
|
||||
return "\n".join(linjer)
|
||||
|
|
@ -195,12 +215,12 @@ def main(argv: list[str]) -> int:
|
|||
except OSError as e:
|
||||
sys.stderr.write(f"Kunne ikke lage arbeidskopien i {mal}: {e}\n")
|
||||
return 2
|
||||
kjor = shlex.quote(str(mal / "modell" / "kjor.py"))
|
||||
kjor = f"{python_navn()} {sitert(mal / 'modell' / 'kjor.py')}"
|
||||
sys.stdout.write(f"Arbeidskopien er laget i {mal} ({antall} filer).\n"
|
||||
f"Les {mal / 'LES-MEG.md'} først. Kjør den med:\n"
|
||||
f" python3 {kjor} scenario <fil.json>\n"
|
||||
f" {kjor} scenario <fil.json>\n"
|
||||
f"og kontroller endringer med:\n"
|
||||
f" python3 {kjor} kontroll\n")
|
||||
f" {kjor} kontroll\n")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
120
modell/kjor.py
120
modell/kjor.py
|
|
@ -19,10 +19,18 @@ hver har ``main(argv: list[str]) -> int``, der argv er argumentene etter kommand
|
|||
Før main kalles, ligger modell-mappa og modell-mappa/kjerne først i sys.path, slik at
|
||||
``import sim`` gir kjernen i samme modell-mappe. ``sys.exit("melding")`` gir exit-kode 2
|
||||
og meldingen uendret; ``sys.exit(n)`` gir exit-kode n.
|
||||
|
||||
Før diagnose og forlop kalles, avvises ukjente feltnavn i hvert ledd og i "styring" (exit-kode
|
||||
2, med «Mente du ...?»), fordi kjernen ser bort fra felt den ikke kjenner, og en skrivefeil
|
||||
ellers gir en stille standardverdi. Gyldige felt i et ledd: "navn", kjernens diagnose.TALLFELT
|
||||
(lest fra kjernen i modell-mappa når kommandoen kjøres) og merknadsfeltene i ANNOTASJONSFELT.
|
||||
Gyldige felt i "styring": forlop.STYRINGSFELT hvis kjernen har den, ellers STYRINGSFELT her,
|
||||
pluss merknadsfeltene. Andre felt på øverste nivå (som "merknad" og "kilder") er tillatt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
|
|
@ -68,6 +76,8 @@ Exit-koder: 0 ok, 1 kontroll fant forskjeller, 2 feil i inndata eller bruk,
|
|||
"""
|
||||
|
||||
TREGER_REGNEMILJO = ("forlop", "scenario", "notatet")
|
||||
ANNOTASJONSFELT = ("merknad", "kilde", "kilder") # fri tekst i et ledd eller i styring; leses ikke av regningen
|
||||
STYRINGSFELT = ("forsinkelse", "reaktivitet") # det forlop.les_styring leser
|
||||
KOMMANDOER = ("diagnose", "forlop", "scenario", "notatet", "kontroll", "miljo", "arbeidskopi")
|
||||
|
||||
|
||||
|
|
@ -252,12 +262,23 @@ def kjor_paa_nytt(python: Path, argv: list[str]) -> int:
|
|||
return subprocess.run([str(python), str(Path(__file__).resolve()), *argv], env=miljo, check=False).returncode
|
||||
|
||||
|
||||
def kommando_tekst(valg: Valg, *args: str) -> str:
|
||||
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data."""
|
||||
deler = ["python3", str(Path(__file__).resolve())]
|
||||
def python_navn(system: str = os.name) -> str:
|
||||
"""Navnet brukeren skriver for Python: «python» på Windows, ellers «python3»."""
|
||||
return "python" if system == "nt" else "python3"
|
||||
|
||||
|
||||
def kommando_tekst(valg: Valg, *args: str, system: str = os.name) -> str:
|
||||
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data.
|
||||
|
||||
På Windows (system "nt") med «python» og Windows-sitering, ellers med «python3» og POSIX-sitering.
|
||||
"""
|
||||
deler = [python_navn(system), str(Path(__file__).resolve())]
|
||||
if valg.data_oppgitt:
|
||||
deler += ["--data", str(valg.data)]
|
||||
return " ".join(shlex.quote(d) for d in [*deler, *args])
|
||||
deler += list(args)
|
||||
if system == "nt":
|
||||
return subprocess.list2cmdline(deler)
|
||||
return " ".join(shlex.quote(d) for d in deler)
|
||||
|
||||
|
||||
def melding_mangler(valg: Valg, tolk: Tolk) -> str:
|
||||
|
|
@ -358,12 +379,63 @@ def kjor_modul(valg: Valg, navn: str) -> int:
|
|||
|
||||
# ---------------- kommandoene ----------------
|
||||
|
||||
def feltmelding(hvor: str, felt: str, gyldige: tuple[str, ...]) -> str:
|
||||
"""Melding om et ukjent feltnavn, med forslag når det ligner et gyldig."""
|
||||
treff = difflib.get_close_matches(felt, gyldige, n=1, cutoff=0.6)
|
||||
forslag = f' Mente du "{treff[0]}"?' if treff else ""
|
||||
return (f'Feil i inndata, {hvor}: feltet "{felt}" finnes ikke.{forslag} '
|
||||
f"Gyldige felt: {', '.join(gyldige)}.")
|
||||
|
||||
|
||||
def ukjent_felt(fil: str, diagnose: object, forlop: object = None) -> str | None:
|
||||
"""Melding om det første ukjente feltet i et ledd eller i styring, ellers None.
|
||||
|
||||
Fila leses bare for å finne feltnavn. Kan den ikke leses, eller har den feil form, sier
|
||||
kjernen fra om det selv, med sin egen melding.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(Path(fil).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
tallfelt = tuple(str(f) for f in getattr(diagnose, "TALLFELT", ()))
|
||||
gyldige_ledd = ("navn", *tallfelt, *ANNOTASJONSFELT)
|
||||
alle_ledd = data.get("ledd")
|
||||
for nr, ledd in enumerate(alle_ledd if isinstance(alle_ledd, list) else [], 1):
|
||||
if not isinstance(ledd, dict):
|
||||
continue
|
||||
for felt in ledd:
|
||||
if felt not in gyldige_ledd:
|
||||
return feltmelding(f"ledd {nr} ({ledd.get('navn', 'uten navn')})", felt, gyldige_ledd)
|
||||
styring = data.get("styring")
|
||||
if isinstance(styring, dict):
|
||||
gyldige_styring = (*(str(f) for f in getattr(forlop, "STYRINGSFELT", STYRINGSFELT)), *ANNOTASJONSFELT)
|
||||
for felt in styring:
|
||||
if felt not in gyldige_styring:
|
||||
return feltmelding("styring", felt, gyldige_styring)
|
||||
return None
|
||||
|
||||
|
||||
def avvis_ukjente_felt(fil: str, diagnose: object, forlop: object = None) -> int | None:
|
||||
"""Skriv meldingen og gi exit-kode 2 når fila har et ukjent felt; None når den er i orden."""
|
||||
melding = ukjent_felt(fil, diagnose, forlop)
|
||||
if melding is None:
|
||||
return None
|
||||
sys.stdout.flush()
|
||||
print(melding, file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
def kommando_diagnose(valg: Valg) -> int:
|
||||
"""Kjernens diagnose.main."""
|
||||
if len(valg.args) != 1:
|
||||
melding = "Bruk: kjor.py diagnose <inndata.json>"
|
||||
raise Bruksfeil(melding)
|
||||
diagnose = last_kjerne(valg.modell, "diagnose")
|
||||
feil = avvis_ukjente_felt(valg.args[0], diagnose)
|
||||
if feil is not None:
|
||||
return feil
|
||||
return fang(lambda: diagnose.main(valg.args[0]), "diagnose.py")
|
||||
|
||||
|
||||
|
|
@ -373,9 +445,19 @@ def kommando_forlop(valg: Valg) -> int:
|
|||
melding = 'Bruk: kjor.py forlop <inndata.json> ["navn på ledd"]'
|
||||
raise Bruksfeil(melding)
|
||||
forlop = last_kjerne(valg.modell, "forlop")
|
||||
feil = avvis_ukjente_felt(valg.args[0], getattr(forlop, "diagnose", None), forlop)
|
||||
if feil is not None:
|
||||
return feil
|
||||
return fang(lambda: forlop.main(*valg.args), "forlop.py")
|
||||
|
||||
|
||||
def kan_ikke_skrive(mappe: Path, feil: OSError) -> int:
|
||||
"""Norsk melding og exit-kode 2 når ut-mappa ikke kan lages eller skrives i."""
|
||||
sys.stdout.flush()
|
||||
print(f"Kan ikke skrive i {mappe}: {feil.strerror or feil}. Velg en annen mappe med --ut.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
def kommando_notatet(valg: Valg) -> int:
|
||||
"""Kjør kjernens sim.py uendret, med ut-mappa som arbeidsmappe (sim.py skriver data/ der)."""
|
||||
ut = Path.cwd() / "resultater" / "notatet"
|
||||
|
|
@ -392,21 +474,33 @@ def kommando_notatet(valg: Valg) -> int:
|
|||
if ut.exists() and not ut.is_dir():
|
||||
melding = f"{ut} finnes og er ikke en mappe. Velg en annen med --ut <mappe>."
|
||||
raise Bruksfeil(melding)
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
utskrift = ut / "sim_utskrift.txt"
|
||||
try:
|
||||
(ut / "data").mkdir(parents=True, exist_ok=True)
|
||||
prove = ut / "data" / ".skriveprove"
|
||||
prove.write_text("", encoding="utf-8")
|
||||
prove.unlink()
|
||||
logg = utskrift.open("w", encoding="utf-8")
|
||||
except OSError as feil:
|
||||
return kan_ikke_skrive(ut, feil)
|
||||
print(f"Kjører notatets simuleringer (kjerne/sim.py, uendret) i {ut}. Det tar om lag 1-2 minutter.\n",
|
||||
flush=True)
|
||||
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1", PYTHONUNBUFFERED="1")
|
||||
utskrift = ut / "sim_utskrift.txt"
|
||||
with utskrift.open("w", encoding="utf-8") as logg, subprocess.Popen(
|
||||
with logg, subprocess.Popen(
|
||||
[sys.executable, str(valg.modell / "kjerne" / "sim.py")],
|
||||
cwd=str(ut), stdout=subprocess.PIPE, text=True, encoding="utf-8", env=miljo,
|
||||
) as prosess:
|
||||
if prosess.stdout is not None:
|
||||
for linje in prosess.stdout:
|
||||
try:
|
||||
for linje in prosess.stdout or ():
|
||||
sys.stdout.write(linje)
|
||||
logg.write(linje)
|
||||
except OSError as feil:
|
||||
prosess.kill()
|
||||
return kan_ikke_skrive(ut, feil)
|
||||
if prosess.returncode != 0:
|
||||
print(f"sim.py stoppet med exit-kode {prosess.returncode}.", file=sys.stderr)
|
||||
sys.stdout.flush()
|
||||
print(f"Kjøringen av sim.py stoppet underveis (exit-kode {prosess.returncode}); se meldingene over. "
|
||||
f"Datafilene i {ut / 'data'} er ikke fullstendige.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
antall = len(list((ut / "data").glob("*.dat")))
|
||||
print(f"\nFerdig: {antall} datafiler i {ut / 'data'}, utskriften i {utskrift}.")
|
||||
|
|
@ -581,7 +675,11 @@ def main(argv: list[str]) -> int:
|
|||
return utfor(tolk_argumenter(argv))
|
||||
except Bruksfeil as feil:
|
||||
sys.stdout.flush()
|
||||
print(f"{feil}\nSkriv «python3 kjor.py --hjelp» for bruk.", file=sys.stderr)
|
||||
print(f"{feil}\nSkriv «{python_navn()} kjor.py --hjelp» for bruk.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
except OSError as feil: # exit-kode 1 er forbeholdt kontroll som finner forskjeller
|
||||
sys.stdout.flush()
|
||||
print(f"Kan ikke lese eller skrive {feil.filename or ''}: {feil.strerror or feil}.", file=sys.stderr)
|
||||
return FEIL_BRUK
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,30 +5,48 @@ Bruk:
|
|||
python3 kjor.py kontroll [--rask]
|
||||
python3 kontroll.py [--rask] (med en Python som har numpy og scipy)
|
||||
|
||||
Fire kontroller, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen: sha256 for filene i kjerne/ mot kjerne/KILDE.json.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst).
|
||||
Kontrollene, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen og fasit: sha256 for filene i kjerne/ og fasit/ mot notatets verdier, som står som
|
||||
konstanter i denne fila (NOTATETS_KJERNE og NOTATETS_FASIT). Ankeret er notatet, ikke
|
||||
filene i samme mappe: kjerne/KILDE.json er bare en opplysning.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst, eksakt: diagnose.py bruker bare
|
||||
standardbiblioteket, så ingen toleranse).
|
||||
3. forlop.py på begge eksempelfilene mot fasit/ (tekst; trenger numpy og scipy).
|
||||
4. Full kjøring av sim.py i en midlertidig mappe: datafilene mot fasit/data/ byte for byte,
|
||||
med største absolutte avvik per kolonne når en fil er ulik, og utskriften mot
|
||||
fasit/sim_utskrift.txt. Om lag 1-2 min; --rask hopper over den.
|
||||
I tillegg kjøres grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
5. grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
6. Scenariokjøreren: scenariofilene i eksempler/scenarioer/notatet/ som gjenskaper notatets
|
||||
datafiler byte for byte, kjørt gjennom scenario.py i denne modell-mappa, mot fasit/data/.
|
||||
De 13 raske er med i --rask; mpc11 (MPC, treg) bare i full kjøring.
|
||||
Er en fasit-fil endret i forhold til notatet, sier en sammenligning mot den ingenting om
|
||||
notatet; den regnes da som endret.
|
||||
|
||||
Nivået til slutt:
|
||||
- «bit for bit som notatet»: alt er likt byte for byte.
|
||||
- «bit for bit som notatet»: alt er kjørt og likt byte for byte (med --rask: alt unntatt den
|
||||
fulle kjøringen, og det står i linja).
|
||||
- «innenfor toleranse»: teksten er lik, og hvert tall avviker med høyst én enhet i siste
|
||||
viste siffer (det som kan skje med andre versjoner av numpy og scipy, eller en annen maskin).
|
||||
- «endret»: alt annet. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som
|
||||
arbeidskopi.py lager) er det en opplysning, ikke en feil: detaljene sier hvilke av
|
||||
notatets resultater som har flyttet seg, og hvor mye.
|
||||
- Mangler numpy eller scipy, sies bare at det som ble kjørt, var uten avvik, og hva som ikke
|
||||
er kontrollert.
|
||||
- «endret»: alt annet. Detaljene sier hvilke av notatets resultater som har flyttet seg, og
|
||||
hvor mye. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som arbeidskopi.py lager) er
|
||||
det en opplysning, ikke en feil; ellers avhenger det av om modellen er endret med vilje.
|
||||
En utvidelse som ikke gjenskaper kjernen, er alltid en feil i utvidelsen.
|
||||
|
||||
Exit-kode: 0 for bit for bit og innenfor toleranse, 1 for endret, 3 når numpy eller scipy
|
||||
mangler for noe av kontrollen (det som kunne kjøres, er kjørt), 2 for feil bruk.
|
||||
|
||||
Når kjernen synkroniseres fra notatets repo (ny commit), må disse oppdateres sammen:
|
||||
kjerne/*.py, kjerne/KILDE.json, fasit/ (lages på nytt med notatets eget miljø), og
|
||||
konstantene NOTATETS_COMMIT, NOTATETS_KJERNE og NOTATETS_FASIT her. Sha256 for fasit/data/
|
||||
skal være lik notatets paper/data/ ved samme commit.
|
||||
|
||||
Grensesnitt: main(argv: list[str]) -> int, som kjor.py kaller.
|
||||
Utvidelser: utvidelser/<navn>.py (ikke filer som begynner med _) med
|
||||
grensetilfelle() -> list av (beskrivelse, største absolutte avvik mot kjernen). Hver
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path.
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path. Scenariokjøreren
|
||||
kjøres på samme måte: scenario.main([fil, "--ut", mappe]) skal skrive <navn>.dat i mappa.
|
||||
Bare standardbiblioteket her; kjernen kjøres som egne prosesser med samme Python.
|
||||
"""
|
||||
|
||||
|
|
@ -52,13 +70,57 @@ KJERNE = MODELL / "kjerne"
|
|||
ROT = MODELL.parent
|
||||
FASIT = ROT / "fasit"
|
||||
EKSEMPLER = ROT / "eksempler"
|
||||
SCENARIOER = EKSEMPLER / "scenarioer" / "notatet"
|
||||
EKSEMPELFILER = ("eksempel_domstol", "norge_2025")
|
||||
ARBEIDSKOPI_MERKE = "LES-MEG.md"
|
||||
|
||||
# Notatet: https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten @ 2badc25 (se synkroniseringen over).
|
||||
NOTATETS_REPO = "https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten"
|
||||
NOTATETS_COMMIT = "2badc25e5134ee26309bd8c3bd008ac64afe55a4"
|
||||
NOTATETS_KJERNE = {
|
||||
"sim.py": "96857a92492f5768bc5d1d98bec30f4292b603c1273913e69901b6a1b220a01e",
|
||||
"diagnose.py": "e5a20c68615836dd854c1414346c7932037eefaf1adb29a18458dbd590a3367a",
|
||||
"forlop.py": "5b369e30f98783b712f76e5dc90bcea4f711603944690c4f19da01efd29c6e21",
|
||||
}
|
||||
NOTATETS_FASIT = {
|
||||
"data/endo_des.dat": "cc200766c80dd486611b4f0a0e20e229c6f1842763b205e86760bf17458f594f",
|
||||
"data/endo_desp.dat": "66794618783bd098a16ddbb427c20d46795f6805e5a14f14122f86e3f8825847",
|
||||
"data/endo_e5.dat": "de137561fe952336f94008378f1bed4b72bbec91ca9613a054c47860086c5671",
|
||||
"data/endo_ko.dat": "ebbf9a096d04a5a52977303b221ee6d8144fc771354c86b7d0ba85262e543378",
|
||||
"data/kjede_s1.dat": "5f7597aea8a39909cfa3784f841a4e4f11cff8822721fa42a5ee90e3ffcd9ce2",
|
||||
"data/kjede_s2.dat": "9dedcc6821a286eca9f05d1a908e377f11c1e542baf7a30c3658b1128f7aacf9",
|
||||
"data/kjede_s3.dat": "a88c964f3a98c2eb962fa2066b3c1d1f4d092b0abfadc2b93c8cd6d747b9a68d",
|
||||
"data/kjede_s4.dat": "6b7dccdcade9eead1e483d046e745a1291b2238c2027f05ee38ee248787cd407",
|
||||
"data/kjede_s5.dat": "cdf936372eed8f3e1117508172f63e8c72f89294cb06caf994262652d044fe59",
|
||||
"data/mpc11.dat": "2609b5bb73efb12e31adf3d4b40c6798e93d40e6bd68afb2583a565d60cb3be0",
|
||||
"data/p11.dat": "526a839a393c2020fff0280a39c263e0ea73d7131ff86d3d9b734be919e884da",
|
||||
"data/p11d.dat": "dfa920ebeafef640735ce27eb98e50f6977e20b643a9c140a1672df86d8e8bf3",
|
||||
"data/p3.dat": "448def8fa662a3b68152d184a33888f7d5a0cfcdfe619064a51a47ff07d6037c",
|
||||
"data/p6.dat": "fcb78c473b4927aaf1a8360a6e2652d0363eaa6b2ac2a80ef551eb577ca4390d",
|
||||
"data/prio_s2.dat": "38389ac7a3d4cc2b5ad4cf54248f0a98d89bb21eb5c8f4f1eba799c6b4b5c7fc",
|
||||
"data/prio_s3.dat": "b675f87eefbcd4097732a258b6e81d06b5b5d96d4bcfbc8599270cc2079baf7e",
|
||||
"data/prio_s5.dat": "dd8744bd75ee27eceb9f312c4740058d13973cdcabf499fef895d2fe32c41813",
|
||||
"data/sat_B.dat": "6a24ce8f69964a055deaaa9260375b8e4cb1c6c09f1a113fd20f6f96a4251847",
|
||||
"data/sat_C.dat": "851d218a3a7772da65e3b19301cdf2006582358df873bab9fcaf7c3da0236fa7",
|
||||
"data/stab.dat": "9a09e14290634beafa1560a31097f76d26c592e7be46b7590e1480a01b60974e",
|
||||
"data/thumb.dat": "49c07f7ae6c23c9f728be27c422e76e8afa895b9bdd46c43ea2f5d5ebe7c767d",
|
||||
"data/vippe.dat": "7185a3edff28dede529a230504d63d85e4821096730acd293855cd74bd005ed1",
|
||||
"diagnose_eksempel_domstol.txt": "226212b0177b54a5402c2fd83d2df334184c7533b295bb3a72a277a4b99f82f0",
|
||||
"diagnose_norge_2025.txt": "3764c9236ec5afde9a47ebbeacec3e0d42cde98123a7d5a6e11c3fbec56a696e",
|
||||
"forlop_eksempel_domstol.txt": "b56cf2a7408d9611088eebaba787ab5d5a1dcdd3ee6116ce73dc226e1cea88c4",
|
||||
"forlop_norge_2025.txt": "ec75dae235031743816ad4b2d63830fcee6050e9e1477acb68269e7e9ded456a",
|
||||
"sim_utskrift.txt": "6225f9427845d915914a0bf78acf4359f6935d437f48cb94e1da48220a99eafd",
|
||||
}
|
||||
# Scenariofilene som gjenskaper notatets datafil med samme navn byte for byte.
|
||||
SCENARIO_RASKE = ("p3", "p6", "p11", "p11d", "kjede_s1", "kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5",
|
||||
"endo_e5", "endo_des", "endo_desp", "endo_ko")
|
||||
SCENARIO_TREGE = ("mpc11",)
|
||||
|
||||
LIK, TOLERANSE, ENDRET, IKKE_KJORT = "lik", "toleranse", "endret", "ikke kjørt"
|
||||
RANG = {LIK: 0, TOLERANSE: 1, IKKE_KJORT: 2, ENDRET: 3}
|
||||
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning
|
||||
SLINGRING = 1e-9 # relativ slingring når avviket sammenlignes med én enhet i siste siffer
|
||||
UTVIDELSER = "Utvidelser" # merkelappen for kontroll 5
|
||||
|
||||
TALL = re.compile(r"[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?")
|
||||
|
||||
|
|
@ -75,6 +137,32 @@ print()
|
|||
print(json.dumps([[str(b), float(a)] for b, a in modul.grensetilfelle()]))
|
||||
"""
|
||||
|
||||
SCENARIO_KJORER = """\
|
||||
import contextlib, importlib.util, io, json, os, sys, traceback
|
||||
modell, ut, filer = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
sys.path[:0] = [os.path.join(modell, "kjerne"), modell]
|
||||
spec = importlib.util.spec_from_file_location("scenario", os.path.join(modell, "scenario.py"))
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
sys.modules["scenario"] = modul
|
||||
spec.loader.exec_module(modul)
|
||||
svar = []
|
||||
for fil in filer:
|
||||
navn = os.path.splitext(os.path.basename(fil))[0]
|
||||
feil = io.StringIO()
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(feil):
|
||||
try:
|
||||
kode = modul.main([fil, "--ut", os.path.join(ut, navn)])
|
||||
except SystemExit as stopp:
|
||||
kode = stopp.code if isinstance(stopp.code, int) else 2
|
||||
feil.write(str(stopp.code or ""))
|
||||
except Exception:
|
||||
kode = 2
|
||||
feil.write(traceback.format_exc())
|
||||
svar.append([navn, kode, feil.getvalue()[-3000:]])
|
||||
print()
|
||||
print(json.dumps(svar))
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dom:
|
||||
|
|
@ -84,6 +172,7 @@ class Dom:
|
|||
linje: str
|
||||
detaljer: list[str] = field(default_factory=list)
|
||||
avvik: float = 0.0
|
||||
ikke_kjort: str = "" # hva som ikke ble kontrollert, når status er IKKE_KJORT
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -226,44 +315,77 @@ def sha256(fil: Path) -> str:
|
|||
return hashlib.sha256(fil.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
|
||||
def lest_fasit(rel: str, endret_fasit: set[str]) -> str | None:
|
||||
"""Innholdet i fasit/<rel>, eller None når fila mangler eller er endret i forhold til notatet."""
|
||||
fil = FASIT / rel
|
||||
if rel in endret_fasit or not fil.is_file():
|
||||
return None
|
||||
return fil.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def ugyldig_fasit(rel: str) -> str:
|
||||
"""Detaljen når en fasit-fil ikke kan brukes som notatets verdi."""
|
||||
if not (FASIT / rel).is_file():
|
||||
return f"fant ikke fasit/{rel}"
|
||||
return f"fasit/{rel} er endret i forhold til notatet, så sammenligningen sier ingenting om notatet"
|
||||
|
||||
|
||||
# ---------------- kontrollene ----------------
|
||||
|
||||
def kontroll_kjerne() -> Dom:
|
||||
"""1. Kjernen mot sha256 i KILDE.json."""
|
||||
kildefil = KJERNE / "KILDE.json"
|
||||
if not kildefil.is_file():
|
||||
return Dom(ENDRET, f"Kjernen: fant ikke {kildefil}, så kjernen kan ikke sammenlignes med notatet.")
|
||||
kilde = json.loads(kildefil.read_text(encoding="utf-8"))
|
||||
"""1. Kjernen mot notatets sha256 (konstantene over, ikke KILDE.json)."""
|
||||
endret = []
|
||||
for navn, info in kilde["filer"].items():
|
||||
for navn, verdi in NOTATETS_KJERNE.items():
|
||||
fil = KJERNE / navn
|
||||
if not fil.is_file():
|
||||
endret.append(f"{navn} mangler")
|
||||
elif sha256(fil) != info["sha256"]:
|
||||
elif sha256(fil) != verdi:
|
||||
endret.append(f"{navn} er endret")
|
||||
versjon = f"{kilde['repo']} @ {kilde['commit'][:7]}"
|
||||
versjon = f"{NOTATETS_REPO} @ {NOTATETS_COMMIT[:7]}"
|
||||
if not endret:
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(kilde['filer'])} filer, {versjon}).")
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(NOTATETS_KJERNE)} filer, {versjon}).")
|
||||
return Dom(ENDRET, f"Kjernen: endret fra notatet ({versjon}): {', '.join(endret)}.")
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit."""
|
||||
def endrede_fasitfiler() -> set[str]:
|
||||
"""Filene i fasit/ som mangler eller ikke har notatets sha256."""
|
||||
return {rel for rel, verdi in NOTATETS_FASIT.items()
|
||||
if not (FASIT / rel).is_file() or sha256(FASIT / rel) != verdi}
|
||||
|
||||
|
||||
def kontroll_fasit(endret_fasit: set[str]) -> Dom:
|
||||
"""1. Fasit mot notatets sha256."""
|
||||
if not endret_fasit:
|
||||
return Dom(LIK, f"Fasit: uendret fra notatet ({len(NOTATETS_FASIT)} filer).")
|
||||
detaljer = [f"fasit/{rel}: " + ("mangler" if not (FASIT / rel).is_file() else "har ikke notatets sha256")
|
||||
for rel in sorted(endret_fasit)]
|
||||
return Dom(ENDRET, f"Fasit: fasit er endret i forhold til notatet ({len(endret_fasit)} av "
|
||||
f"{len(NOTATETS_FASIT)} filer).", detaljer, math.inf)
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path, endret_fasit: set[str], *, toleranse: bool) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit.
|
||||
|
||||
toleranse=False: bare byte-lik tekst godtas (diagnose.py avhenger ikke av numpy og scipy).
|
||||
"""
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for eksempel in EKSEMPELFILER:
|
||||
fasitfil = FASIT / f"{skript}_{eksempel}.txt"
|
||||
rel = f"{skript}_{eksempel}.txt"
|
||||
fasit = lest_fasit(rel, endret_fasit)
|
||||
inndata = EKSEMPLER / f"{eksempel}.json"
|
||||
mangler = [str(f) for f in (fasitfil, inndata) if not f.is_file()]
|
||||
if mangler:
|
||||
if fasit is None or not inndata.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{eksempel}: fant ikke {', '.join(mangler)}")
|
||||
detaljer.append(f"{eksempel}: " + (ugyldig_fasit(rel) if fasit is None else f"fant ikke {inndata}"))
|
||||
continue
|
||||
prosess = kjor_python([str(KJERNE / f"{skript}.py"), str(inndata)], mappe)
|
||||
if prosess.returncode != 0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{eksempel}:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
continue
|
||||
resultat = sammenlign_tekst(fasitfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
resultat = sammenlign_tekst(fasit, prosess.stdout)
|
||||
if resultat.status == TOLERANSE and not toleranse:
|
||||
resultat.status = ENDRET
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status != LIK:
|
||||
|
|
@ -278,23 +400,27 @@ def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kontroll_full(mappe: Path) -> Dom:
|
||||
def kontroll_full(mappe: Path, endret_fasit: set[str]) -> Dom:
|
||||
"""4. Full kjøring av sim.py i en midlertidig mappe mot fasit/data og fasit/sim_utskrift.txt."""
|
||||
print(" (kjører sim.py, om lag 1-2 minutter ...)", flush=True)
|
||||
prosess = kjor_python([str(KJERNE / "sim.py")], mappe)
|
||||
if prosess.returncode != 0:
|
||||
return Dom(ENDRET, "Full kjøring: sim.py stoppet med en feil.", feilmelding(prosess), math.inf)
|
||||
fasit_data = FASIT / "data"
|
||||
navn_fasit = sorted(f.name for f in fasit_data.glob("*.dat"))
|
||||
navn_fasit = sorted(rel[len("data/"):] for rel in NOTATETS_FASIT if rel.startswith("data/"))
|
||||
navn_naa = sorted(f.name for f in (mappe / "data").glob("*.dat"))
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for navn in sorted(set(navn_fasit) | set(navn_naa)):
|
||||
if navn not in navn_naa or navn not in navn_fasit:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa else "finnes ikke i fasit"))
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa
|
||||
else "er ikke en av notatets datafiler"))
|
||||
continue
|
||||
resultat = sammenlign_dat((fasit_data / navn).read_text(encoding="utf-8"),
|
||||
(mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
fasit = lest_fasit(f"data/{navn}", endret_fasit)
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: {ugyldig_fasit(f'data/{navn}')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, (mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
|
|
@ -302,11 +428,11 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{navn} ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
utskriftfil = FASIT / "sim_utskrift.txt"
|
||||
if utskriftfil.is_file():
|
||||
utskrift = sammenlign_tekst(utskriftfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
fasit_utskrift = lest_fasit("sim_utskrift.txt", endret_fasit)
|
||||
if fasit_utskrift is None:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [ugyldig_fasit("sim_utskrift.txt")])
|
||||
else:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [f"fant ikke {utskriftfil}"])
|
||||
utskrift = sammenlign_tekst(fasit_utskrift, prosess.stdout)
|
||||
statuser.append(utskrift.status)
|
||||
storst = max(storst, utskrift.avvik)
|
||||
if utskrift.status != LIK:
|
||||
|
|
@ -318,40 +444,112 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def grensetilfeller(fil: Path, mappe: Path) -> tuple[list[str], list[str], float]:
|
||||
"""Kjør grensetilfelle() i én utvidelse. Returnerer statuser, detaljer og største avvik."""
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() kunne ikke kjøres",
|
||||
*(" " + linje for linje in feilmelding(prosess))], math.inf
|
||||
if not tilfeller:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett."], 0.0
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik == 0.0:
|
||||
statuser.append(LIK)
|
||||
continue
|
||||
statuser.append(ENDRET)
|
||||
hvem = beskrivelse if str(beskrivelse).startswith(fil.stem) else f"{fil.stem}: {beskrivelse}"
|
||||
detaljer.append(f"{hvem}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
return statuser, detaljer, storst
|
||||
|
||||
|
||||
def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
||||
"""Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
"""5. Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
utvidelser = sorted(f for f in (MODELL / "utvidelser").glob("*.py") if not f.name.startswith("_"))
|
||||
if not utvidelser:
|
||||
return Dom(LIK, "Utvidelser: ingen å kontrollere.")
|
||||
return Dom(LIK, f"{UTVIDELSER}: ingen å kontrollere.")
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, f"Utvidelser: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.")
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
return Dom(IKKE_KJORT, f"{UTVIDELSER}: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.",
|
||||
ikke_kjort="utvidelsenes grensetilfeller")
|
||||
statuser, detaljer, storst, feilet = [], [], 0.0, []
|
||||
for fil in utvidelser:
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{fil.stem}: grensetilfelle() kunne ikke kjøres", *(" " + linje for linje in
|
||||
feilmelding(prosess))]
|
||||
continue
|
||||
if not tilfeller:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett.")
|
||||
continue
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik != 0.0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: {beskrivelse}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
else:
|
||||
statuser.append(LIK)
|
||||
status = verst(statuser)
|
||||
egne, egne_detaljer, avvik = grensetilfeller(fil, mappe)
|
||||
statuser += egne
|
||||
detaljer += egne_detaljer
|
||||
storst = max(storst, avvik)
|
||||
if ENDRET in egne:
|
||||
feilet.append(fil.stem)
|
||||
navn = ", ".join(f.stem for f in utvidelser)
|
||||
linje = (f"Utvidelser: grensetilfellene gjenskaper kjernen eksakt ({navn})." if status == LIK
|
||||
else f"Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt ({navn}; se under).")
|
||||
if not feilet:
|
||||
return Dom(LIK, f"{UTVIDELSER}: grensetilfellene gjenskaper kjernen eksakt ({navn}).", [], storst)
|
||||
linje = (f"{UTVIDELSER}: {', '.join(feilet)} gjenskaper ikke kjernen i grensetilfellet. Det er en feil i "
|
||||
"utvidelsen, og resultatene den gir, kan ikke brukes (se under).")
|
||||
return Dom(ENDRET, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kjor_scenarioer(navn: tuple[str, ...], mappe: Path) -> tuple[dict[str, tuple[int, str]], list[str]]:
|
||||
"""Kjør scenariofilene gjennom scenario.py i en egen prosess.
|
||||
|
||||
Returnerer {navn: (exit-kode, stderr)} og feillinjer når prosessen selv stoppet.
|
||||
"""
|
||||
ut = mappe / "scenarioer"
|
||||
filer = [str(SCENARIOER / f"{n}.json") for n in navn]
|
||||
prosess = kjor_python(["-c", SCENARIO_KJORER, str(MODELL), str(ut), *filer], mappe)
|
||||
try:
|
||||
svar = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
svar = None
|
||||
if svar is None:
|
||||
return {}, ["scenario.py kunne ikke lastes:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
return {str(n): (int(kode), str(feil)) for n, kode, feil in svar}, []
|
||||
|
||||
|
||||
def kontroll_scenario(mappe: Path, endret_fasit: set[str], *, rask: bool, regnemiljo: bool) -> Dom:
|
||||
"""6. Scenariokjøreren gjenskaper notatets datafiler fra scenariofilene i eksempler/scenarioer/notatet/."""
|
||||
navn = SCENARIO_RASKE if rask else SCENARIO_RASKE + SCENARIO_TREGE
|
||||
if not (MODELL / "scenario.py").is_file():
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {MODELL / 'scenario.py'}, så den er ikke kontrollert.",
|
||||
[], math.inf)
|
||||
mangler = [n for n in navn if not (SCENARIOER / f"{n}.json").is_file()]
|
||||
if mangler:
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {len(mangler)} av {len(navn)} scenariofiler i "
|
||||
f"{SCENARIOER}, så den er ikke kontrollert.", [f"mangler: {', '.join(mangler)}"], math.inf)
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, "Scenariokjøreren: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="scenariokjøreren")
|
||||
resultater, feil = kjor_scenarioer(navn, mappe)
|
||||
if feil:
|
||||
return Dom(ENDRET, "Scenariokjøreren: scenario.py stoppet før noen fil var kjørt.", feil, math.inf)
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for n in navn:
|
||||
kode, melding = resultater.get(n, (2, "ble ikke kjørt"))
|
||||
fasit = lest_fasit(f"data/{n}.dat", endret_fasit)
|
||||
laget = mappe / "scenarioer" / n / f"{n}.dat"
|
||||
if kode != 0 or not laget.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{n}.json: stoppet med exit-kode {kode}" if kode != 0 else f"{n}.json: laget ikke {n}.dat",
|
||||
*(" " + linje for linje in melding.strip().splitlines()[-MAKS_LINJER // 2:])]
|
||||
continue
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{n}.dat: {ugyldig_fasit(f'data/{n}.dat')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, laget.read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
like += 1
|
||||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{n}.dat ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
status = verst(statuser)
|
||||
linje = (f"Scenariokjøreren: {like} av {len(navn)} av notatets datafiler gjenskapt byte for byte fra "
|
||||
"eksempler/scenarioer/notatet/" + (" (mpc11 bare i full kjøring)." if rask else "."))
|
||||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
|
|
@ -360,25 +558,35 @@ def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
|||
def niva(dommer: list[Dom], *, rask: bool, arbeidskopi: bool) -> list[str]:
|
||||
"""Linjene om nivået som er oppnådd."""
|
||||
kjort = [d for d in dommer if d.status != IKKE_KJORT]
|
||||
hoppet_over = [d.ikke_kjort for d in dommer if d.status == IKKE_KJORT]
|
||||
status = verst([d.status for d in kjort])
|
||||
deler = (["uten full kjøring, --rask"] if rask else []) + (
|
||||
["bare det som er kjørt"] if any(d.status == IKKE_KJORT for d in dommer) else [])
|
||||
hva = f" ({'; '.join(deler)})" if deler else ""
|
||||
if status == LIK:
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
hva = " (uten full kjøring, --rask)" if rask else ""
|
||||
if status == ENDRET:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if UTVIDELSER in endret:
|
||||
linjer.append("En utvidelse som ikke gjenskaper kjernen i grensetilfellet, har en feil. Resultatene den "
|
||||
"gir, kan ikke brukes før den er rettet.")
|
||||
if [e for e in endret if e != UTVIDELSER]:
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Er dette pluginen slik den er installert, er det en feil: pluginens modell skal regne "
|
||||
"som notatet. Er modellen endret med vilje (for eksempel i en klone av repoet), er det "
|
||||
"en opplysning: detaljene over viser hvilke av notatets resultater som har flyttet "
|
||||
"seg, og hvor mye.")
|
||||
elif hoppet_over:
|
||||
kjorte = [d.linje.split(":")[0] for d in kjort]
|
||||
linjer = [(f"Nivå{hva}: ingen avvik i det som ble kjørt ({', '.join(kjorte)}). Ikke kontrollert: "
|
||||
f"{', '.join(hoppet_over)}.")]
|
||||
elif status == TOLERANSE:
|
||||
storst = max(d.avvik for d in kjort)
|
||||
linjer = [(f"Nivå{hva}: innenfor toleranse (største avvik {vis_tall(storst)}): hvert tall avviker med "
|
||||
"høyst én enhet i siste viste siffer.")]
|
||||
else:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Pluginens modell skal regne som notatet, så dette er en feil.")
|
||||
if any(d.status == IKKE_KJORT for d in dommer):
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
if hoppet_over:
|
||||
linjer.append(f"Ikke kjørt fordi numpy eller scipy mangler i {sys.executable}. "
|
||||
"«kjor.py miljo --opprett» installerer dem i en egen mappe.")
|
||||
return linjer
|
||||
|
|
@ -402,21 +610,25 @@ def main(argv: list[str]) -> int:
|
|||
print(f" {linje}")
|
||||
sys.stdout.flush()
|
||||
|
||||
endret_fasit = endrede_fasitfiler()
|
||||
with tempfile.TemporaryDirectory(prefix="dynamisk-rettsstat-") as tmp:
|
||||
mappe = Path(tmp)
|
||||
vis("1.", kontroll_kjerne())
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe))
|
||||
vis("1.", kontroll_fasit(endret_fasit))
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe, endret_fasit, toleranse=False))
|
||||
if regnemiljo:
|
||||
vis("3.", kontroll_utskrift("forlop", mappe))
|
||||
vis("3.", kontroll_utskrift("forlop", mappe, endret_fasit, toleranse=True))
|
||||
else:
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler.", ikke_kjort="forlop"))
|
||||
if rask:
|
||||
print("4. Full kjøring: hoppet over (--rask).")
|
||||
elif regnemiljo:
|
||||
vis("4.", kontroll_full(mappe))
|
||||
vis("4.", kontroll_full(mappe, endret_fasit))
|
||||
else:
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="full kjøring av sim.py"))
|
||||
vis("5.", kontroll_utvidelser(mappe, regnemiljo=regnemiljo))
|
||||
vis("6.", kontroll_scenario(mappe, endret_fasit, rask=rask, regnemiljo=regnemiljo))
|
||||
print()
|
||||
print("\n".join(niva(dommer, rask=rask, arbeidskopi=arbeidskopi)))
|
||||
statuser = [d.status for d in dommer]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import importlib.util
|
|||
import itertools
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -87,9 +88,10 @@ SATT_AV_TILTAK = {
|
|||
("CH2", "xiBfun"): "tiltak.andel_utenom",
|
||||
}
|
||||
|
||||
TOPPFELT = ("navn", "beskrivelse", "merknad", "modell", "styring", "parametere", "regel", "sjokk",
|
||||
"tiltak", "slutt", "sveip", "utdata", "utvidelse")
|
||||
SVEIPBARE = ("styring", "parametere", "regel", "sjokk", "tiltak", "slutt", "utvidelse")
|
||||
TOPPFELT = ("navn", "beskrivelse", "merknad", "modell", "startlikevekt", "styring", "parametere", "regel",
|
||||
"sjokk", "tiltak", "slutt", "sveip", "utdata", "utvidelse")
|
||||
STARTVALG = ("krev", "kalibrer", "ubalanse")
|
||||
SVEIPBARE = ("styring", "parametere", "regel", "sjokk", "tiltak", "slutt", "utvidelse", "startlikevekt")
|
||||
STYRINGSFELT = ("regulator", "forsinkelse", "reaktivitet", "mpc_kjenner_kjeden")
|
||||
SJOKKFELT = ("faktor", "fra", "til")
|
||||
TILTAKSFELT = ("andel_utenom", "kapasitet", "trommel_buffer_tau")
|
||||
|
|
@ -105,6 +107,7 @@ STANDARD_TILTAK_FRA = 24.0 # T_TILTAK i sim.py
|
|||
STANDARD_TILTAK_OVER = 24.0 # T_UTV i sim.py
|
||||
STANDARD_SOC_SJOKK = (1.6, 12.0, 36.0) # simulate_soc: shock, t_on, t_off
|
||||
MAKS_SLUTT = 2400.0
|
||||
MIN_SLUTT = 1.0 # kjernen trenger minst to tidssteg; én måned er minste meningsfulle horisont
|
||||
MAKS_KJORINGER = 400
|
||||
RUTENETT = 2 # høyst to parametere i et sveip
|
||||
SEN_FASE = 60.0 # spennet i K måles etter denne måneden (som amp(K,t>60) i sim.py)
|
||||
|
|
@ -112,6 +115,11 @@ SNITT_FRA = 12.0 # summary_endo: gjennomsnitt fra måned 12
|
|||
MAKS_KOLONNER_I_TABELL = 6
|
||||
CSV_HVERT = 20 # standard: én rad per måned (DT = 0,05)
|
||||
NOTAT_HVERT = 10 # write_csv i sim.py
|
||||
LIKEVEKT_TOL = 1e-12 # største tillatte rest i dK/dt ved start (pådraget er om lag 0,12 per måned)
|
||||
FLYTPARAMETERE = ("gamma", "Lam0", "T0") # gir strømmen J0 = gamma Lam0 T0 som domstolen er kalibrert for
|
||||
ENDO_FAST_STROM = 0.1 # simulate_endo: observert strøm de første lagD månedene (sim.py:588), fast i kjernen
|
||||
UTEN_SJOKK_MND = 12.0 # måneden driften vises for når det ikke er noe sjokk (notatets sjokkmåned)
|
||||
K_OMRADE = (0.01, 12.0) # merknad utenfor: området soc_equilibria i sim.py leter i (notatets K: 0,83-9,82)
|
||||
|
||||
|
||||
class ScenarioFeil(Exception):
|
||||
|
|
@ -156,12 +164,13 @@ GRENSER: dict[tuple[str, str], Grense] = {
|
|||
("ENDO", "k_xi"): IKKE_NEG, ("ENDO", "tau_xi"): POSITIV, ("ENDO", "ups_star"): POSITIV,
|
||||
("ENDO", "ups_hi"): POSITIV, ("ENDO", "ups_lo"): IKKE_NEG, ("ENDO", "kZ"): IKKE_NEG,
|
||||
("ENDO", "L1"): IKKE_NEG, ("ENDO", "L2"): POSITIV, ("ENDO", "tau_down"): POSITIV,
|
||||
("ENDO", "T_clear"): POSITIV_INF, ("ENDO", "H_pred"): POSITIV, ("ENDO", "period"): POSITIV,
|
||||
("ENDO", "T_clear"): POSITIV_INF, ("ENDO", "H_pred"): POSITIV, ("ENDO", "period"): Grense(1.0),
|
||||
("ENDO", "mu3"): IKKE_NEG, ("ENDO", "Lam_star"): APEN_ANDEL, ("ENDO", "k_q"): IKKE_NEG,
|
||||
("ENDO", "q_max"): IKKE_NEG, ("ENDO", "lag_survey"): IKKE_NEG,
|
||||
("SOC", "eps"): IKKE_NEG, ("SOC", "Kh"): POSITIV,
|
||||
}
|
||||
HELTALLSGRENSER = {("CH2", "c"): (1, 50), ("SOC", "pexp"): (1, 20)}
|
||||
HELE_MAANEDER = {("ENDO", "period")} # budsjettsyklusen: vedtak når t % period == 0 i hele måneder (sim.py:614)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- kjernen
|
||||
|
|
@ -455,6 +464,9 @@ class Kjoring:
|
|||
tiltak: dict[str, object]
|
||||
dicts: dict[str, dict[str, object]]
|
||||
utvidelse: dict[str, object] = field(default_factory=dict)
|
||||
startlikevekt: str = "krev"
|
||||
ubalanse: list[str] = field(default_factory=list) # hvorfor starten ikke er en likevekt
|
||||
kalibrert: list[tuple[str, float, float]] = field(default_factory=list) # (sti, notatets, ny)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -545,7 +557,10 @@ def parameterverdi(navn: str, nokkel: str, standard: object, verdi: object, sti:
|
|||
if isinstance(standard, int):
|
||||
nedre, ovre = HELTALLSGRENSER.get((navn, nokkel), (-10**9, 10**9))
|
||||
return heltall(verdi, sti, nedre, ovre)
|
||||
return tallverdi(verdi, sti, GRENSER.get((navn, nokkel), Grense()))
|
||||
x = tallverdi(verdi, sti, GRENSER.get((navn, nokkel), Grense()))
|
||||
if (navn, nokkel) in HELE_MAANEDER and not x.is_integer():
|
||||
feil(sti, f"må være et helt antall måneder (kjernen gjør vedtak bare i hele måneder), ikke {vis(verdi)}.")
|
||||
return x
|
||||
|
||||
|
||||
def les_styring(raa: object, modell: str, slutt: float) -> tuple[str, float, float, bool | None]:
|
||||
|
|
@ -553,8 +568,10 @@ def les_styring(raa: object, modell: str, slutt: float) -> tuple[str, float, flo
|
|||
s = objekt(raa if raa is not None else {}, "styring", STYRINGSFELT)
|
||||
regulatorer = REGULATORER.get(modell, ("P", "MPC", "fast"))
|
||||
regulator = tekst(s.get("regulator", "P"), "styring.regulator", regulatorer)
|
||||
forsinkelse = tallverdi(s.get("forsinkelse", STANDARD_FORSINKELSE), "styring.forsinkelse",
|
||||
Grense(0.0, ovre=slutt))
|
||||
forsinkelse = tallverdi(s.get("forsinkelse", STANDARD_FORSINKELSE), "styring.forsinkelse", IKKE_NEG)
|
||||
if forsinkelse > slutt:
|
||||
feil("slutt", f"horisonten ({tall_kort(slutt)} mnd) er kortere enn forsinkelsen i styringen "
|
||||
f"(styring.forsinkelse = {tall_kort(forsinkelse)} mnd). Øk slutt eller kort ned forsinkelsen.")
|
||||
reaktivitet = tallverdi(s.get("reaktivitet", STANDARD_REAKTIVITET), "styring.reaktivitet",
|
||||
Grense(0.0, ovre=100.0))
|
||||
mpc: bool | None = None
|
||||
|
|
@ -725,6 +742,166 @@ def sjekk_kombinasjoner(k: Kjoring, sim: ModuleType, utv: ModuleType | None) ->
|
|||
sjekk_endo(k)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- startlikevekten
|
||||
|
||||
|
||||
def tall_g(x: float) -> str:
|
||||
"""Et tall med fire gjeldende sifre, norsk desimaltegn."""
|
||||
return f"{x:.4g}".replace(".", ",")
|
||||
|
||||
|
||||
def oppgitte_nokler(parametere: object) -> dict[str, set[str]]:
|
||||
"""Nøklene fila setter selv under parametere, per gruppe."""
|
||||
ut: dict[str, set[str]] = {}
|
||||
if isinstance(parametere, dict):
|
||||
for navn, d in parametere.items():
|
||||
if isinstance(d, dict):
|
||||
ut[str(navn)] = {str(n) for n in d}
|
||||
return ut
|
||||
|
||||
|
||||
def paadragsrest(sim: ModuleType, k: Kjoring) -> float:
|
||||
"""dK/dt ved start (K = 1, Lambda = Lam0, T = T0, før sjokket), regnet med kjernens funksjoner."""
|
||||
p = k.dicts["P"]
|
||||
om = tallfelt(p, "Omega0")
|
||||
if k.modell == "simulate_soc": # kjernen rekalibrerer Omega0 for rekrutteringsleddet (sim.py:455)
|
||||
s = k.dicts["SOC"]
|
||||
om = om - tallfelt(s, "eps") * float(sim.hill(1.0, s)) + tallfelt(s, "eps") * float(sim.hill(1.0, s))
|
||||
fjernet = float(sim.removal(1.0, tallfelt(p, "Lam0"), tallfelt(p, "T0"), p))
|
||||
return om - fjernet - tallfelt(p, "delta") * 1.0
|
||||
|
||||
|
||||
def strom_j0(p: dict[str, object]) -> float:
|
||||
"""Strømmen til domstolen ved start slik kjernen kalibrerer den (J0 i sim.py:154, uten eta)."""
|
||||
return tallfelt(p, "gamma") * tallfelt(p, "Lam0") * tallfelt(p, "T0") * 1.0
|
||||
|
||||
|
||||
class Startfunn(NamedTuple):
|
||||
"""Det som gjør at starten ikke er en likevekt."""
|
||||
|
||||
kan_kalibreres: list[str]
|
||||
kan_ikke: list[str]
|
||||
rettinger: list[tuple[str, str, float, float]] # (gruppe, nøkkel, gammel, ny)
|
||||
|
||||
|
||||
def funn_grunnmodell(sim: ModuleType, k: Kjoring, oppgitt: dict[str, set[str]], f: Startfunn) -> None:
|
||||
"""Pådrag, mål, tak og proporsjonalitet i grunnmodellen."""
|
||||
p = k.dicts["P"]
|
||||
rest = paadragsrest(sim, k)
|
||||
if abs(rest) > LIKEVEKT_TOL:
|
||||
ny = tallfelt(p, "Omega0") - rest
|
||||
tekst_ = (f"pådraget Omega0 ({tall_g(tallfelt(p, 'Omega0'))}) er ikke lik det politiets innsats fjerner "
|
||||
f"pluss det som faller fra av seg selv ved K = 1 ({tall_g(ny)}), så kriminaliteten ville "
|
||||
"drevet bort fra 1,00 også uten sjokk")
|
||||
if "Omega0" in oppgitt.get("P", set()):
|
||||
f.kan_ikke.append(tekst_ + " (Omega0 er satt i fila)")
|
||||
else:
|
||||
f.kan_kalibreres.append(tekst_)
|
||||
f.rettinger.append(("P", "Omega0", tallfelt(p, "Omega0"), ny))
|
||||
t0, kstar = tallfelt(p, "T0"), tallfelt(p, "Kstar")
|
||||
regel_virker = k.regulator == "MPC" or (k.regulator == "P" and k.reaktivitet > 0)
|
||||
if regel_virker and kstar != 1.0:
|
||||
f.kan_ikke.append(f"målet Kstar er {tall_g(kstar)}, ikke 1, så styringen flytter innsatsen bort fra T0 "
|
||||
"allerede ved start")
|
||||
if k.regulator != "fast" and t0 > tallfelt(p, "Tmax"):
|
||||
f.kan_ikke.append(f"T0 ({tall_g(t0)}) er over taket Tmax ({tall_g(tallfelt(p, 'Tmax'))})")
|
||||
if t0 > tallfelt(p, "Tprop") * 1.0:
|
||||
f.kan_ikke.append(f"T0 ({tall_g(t0)}) er over Tprop ({tall_g(tallfelt(p, 'Tprop'))}) ganger K, så "
|
||||
"innsatsen regnes som uforholdsmessig og tilliten faller fra start")
|
||||
|
||||
|
||||
def funn_domstol(sim: ModuleType, k: Kjoring, navn: str, oppgitt: dict[str, set[str]], f: Startfunn) -> None:
|
||||
"""Domstolens kapasitet og behandlingstid mot strømmen ved start (CH0 eller CH2)."""
|
||||
p, ch = k.dicts["P"], k.dicts[navn]
|
||||
if tallfelt(p, "eta") != 1.0:
|
||||
f.kan_ikke.append(f"eta er {tall_g(tallfelt(p, 'eta'))}, men kjernen regner strømmen til domstolen ved "
|
||||
"start uten eta (init_chain og simulate_chain2 i sim.py), så køen starter ikke i balanse")
|
||||
j0 = strom_j0(p)
|
||||
if j0 <= 0:
|
||||
f.kan_ikke.append("strømmen til domstolen er 0 ved start (gamma, Lam0 eller T0 er 0), så domstolen kan "
|
||||
"ikke kalibreres")
|
||||
return
|
||||
satt = oppgitt.get(navn, set())
|
||||
flyt_endret = any(n in oppgitt.get("P", set()) and p[n] != sim.P[n] for n in FLYTPARAMETERE)
|
||||
z = tallfelt(ch, "Z")
|
||||
if flyt_endret and not {"Z", "m"} <= satt:
|
||||
z_ny = z if "Z" in satt else j0 / float(sim.UPS0)
|
||||
f.kan_kalibreres.append(
|
||||
f"domstolens kapasitet og minste behandlingstid ({navn}.Z, {navn}.m) er notatets, regnet for en "
|
||||
f"strøm på {tall_g(strom_j0(sim.P))} saker per måned, men strømmen er nå {tall_g(j0)}, så "
|
||||
"kapasitet og saksbehandlingstid passer ikke lenger til strømmen")
|
||||
if "Z" not in satt:
|
||||
f.rettinger.append((navn, "Z", z, z_ny))
|
||||
if "m" not in satt:
|
||||
f.rettinger.append((navn, "m", tallfelt(ch, "m") if ch["m"] != "kalibrer" else math.nan, math.nan))
|
||||
z = z_ny
|
||||
andel = (tallfelt(ch, "xi") if navn == "CH0"
|
||||
else tallfelt(ch, "piA") * tallfelt(ch, "xiA") + (1 - tallfelt(ch, "piA")) * tallfelt(ch, "xiB"))
|
||||
if andel * j0 >= z:
|
||||
f.kan_ikke.append(f"domstolen får {tall_g(andel * j0)} saker per måned ved start, minst like mye som "
|
||||
f"kapasiteten {navn}.Z = {tall_g(z)}, så køen kan ikke stå i ro")
|
||||
|
||||
|
||||
def kalibrert_m(sim: ModuleType, k: Kjoring, navn: str) -> float:
|
||||
"""Minste behandlingstid m som gir saksbehandlingstid TAU_REF ved start.
|
||||
|
||||
Som MD i sim.py:155, eller calibrate_m for flere spor.
|
||||
"""
|
||||
ch, p = k.dicts[navn], k.dicts["P"]
|
||||
j0, z, tau = strom_j0(p), tallfelt(ch, "Z"), float(sim.TAU_REF)
|
||||
if navn == "CH2" and ch["c"] != 1:
|
||||
return float(sim.calibrate_m(dict(ch, m=None), p))
|
||||
return tau - j0 / (z * (1.0 / tau + tallfelt(ch, "varsigma")))
|
||||
|
||||
|
||||
def startmelding(f: Startfunn, kan_kalibrere: bool) -> str: # noqa: FBT001 - intern hjelper
|
||||
"""Melding når starten ikke er en likevekt og fila ikke har valgt hva som skal skje."""
|
||||
grunner = "; ".join([*f.kan_kalibreres, *f.kan_ikke])
|
||||
ut = (f"startpunktet er ikke en likevekt: {grunner}. Tallene ville da vist en drift som ikke kommer av "
|
||||
"sjokket eller tiltakene. To veier: ")
|
||||
if kan_kalibrere:
|
||||
ut += ('"startlikevekt": "kalibrer" regner referansetilstanden om (' + ", ".join(
|
||||
f"{g}.{n}" for g, n, _, _ in f.rettinger) + ") slik at startpunktet blir notatets referansetilstand; ")
|
||||
else:
|
||||
ut += '"startlikevekt": "kalibrer" kan ikke rette dette (verdiene er satt i fila eller ligger fast); '
|
||||
return ut + ('"startlikevekt": "ubalanse" kjører fra ubalansen med vilje og viser i sammendraget hvor '
|
||||
"langt modellen driver før sjokket. Se docs/scenarioformat.md.")
|
||||
|
||||
|
||||
def vurder_start(sim: ModuleType, k: Kjoring, parametere: object) -> None:
|
||||
"""Stopp, kalibrer eller merk kjøringen når startpunktet ikke er en likevekt ("startlikevekt")."""
|
||||
oppgitt = oppgitte_nokler(parametere)
|
||||
f = Startfunn([], [], [])
|
||||
funn_grunnmodell(sim, k, oppgitt, f)
|
||||
kjede = next((n for n in ("CH0", "CH2") if n in k.dicts), None)
|
||||
if kjede is not None and not math.isinf(tallfelt(k.dicts[kjede], "Z")):
|
||||
funn_domstol(sim, k, kjede, oppgitt, f)
|
||||
if k.modell == "simulate_endo":
|
||||
f0 = float(sim.removal(1.0, tallfelt(k.dicts["P"], "Lam0"), tallfelt(k.dicts["P"], "T0"), k.dicts["P"]))
|
||||
if abs(f0 - ENDO_FAST_STROM) > LIKEVEKT_TOL:
|
||||
feil("parametere.P", f"simulate_endo i kjernen bruker {tall_g(ENDO_FAST_STROM)} som observert strøm de "
|
||||
f"første månedene (sim.py:588), men med disse parametrene er strømmen ved start {tall_g(f0)}. Det "
|
||||
"endrer hva den koordinerte regelen gjør, og kan ikke rettes fra scenariofila. Å endre gamma, "
|
||||
"Lam0, T0, eta eller ksat i simulate_endo er derfor en endring i kjernen (arbeidskopi og "
|
||||
"ferdigheten forbedre).")
|
||||
if not f.kan_kalibreres and not f.kan_ikke:
|
||||
return
|
||||
if k.startlikevekt == "krev":
|
||||
feil("startlikevekt", startmelding(f, kan_kalibrere=not f.kan_ikke))
|
||||
if k.startlikevekt == "ubalanse":
|
||||
k.ubalanse = [*f.kan_kalibreres, *f.kan_ikke]
|
||||
return
|
||||
if f.kan_ikke:
|
||||
feil("startlikevekt", '"kalibrer" kan ikke gjøre starten til en likevekt: ' + "; ".join(f.kan_ikke)
|
||||
+ '. Endre verdiene, eller bruk "startlikevekt": "ubalanse".')
|
||||
for gruppe, nokkel, gammel, ny in f.rettinger:
|
||||
verdi = kalibrert_m(sim, k, gruppe) if (gruppe, nokkel) in {("CH0", "m"), ("CH2", "m")} else ny
|
||||
k.dicts[gruppe][nokkel] = verdi
|
||||
k.kalibrert.append((f"{gruppe}.{nokkel}", gammel, verdi))
|
||||
if abs(paadragsrest(sim, k)) > LIKEVEKT_TOL:
|
||||
feil("startlikevekt", "kalibreringen ga ikke en likevekt; dette er en feil i scenariokjøreren.")
|
||||
|
||||
|
||||
def les_utvidelse(raa: object, utv: ModuleType | None) -> dict[str, object]:
|
||||
"""Valider utvidelsens parametere mot PARAMETERE (samme type som standardverdien)."""
|
||||
if utv is None:
|
||||
|
|
@ -766,7 +943,7 @@ def los_opp(sim: ModuleType, raa: dict[str, object], modell: str, utv: ModuleTyp
|
|||
variant: Variant) -> Kjoring:
|
||||
"""Valider én variant av scenariofila og lag en kjøring."""
|
||||
objekt(raa, "", TOPPFELT)
|
||||
slutt = tallverdi(raa.get("slutt", float(sim.TEND)), "slutt", Grense(0.0, nedre_med=False, ovre=MAKS_SLUTT))
|
||||
slutt = tallverdi(raa.get("slutt", float(sim.TEND)), "slutt", Grense(MIN_SLUTT, ovre=MAKS_SLUTT))
|
||||
regel = les_regel(raa, modell)
|
||||
regulator, forsinkelse, reaktivitet, mpc = les_styring(raa.get("styring"), modell, slutt)
|
||||
dicts = les_parametere(sim, raa.get("parametere"), parameterdicts(sim, modell, utv), modell)
|
||||
|
|
@ -781,6 +958,8 @@ def los_opp(sim: ModuleType, raa: dict[str, object], modell: str, utv: ModuleTyp
|
|||
mpc_kjenner_kjeden=mpc, regel=regel, slutt=slutt, sjokk=sjokk,
|
||||
tiltak=les_tiltak(raa.get("tiltak"), modell, regel), dicts=dicts,
|
||||
utvidelse=les_utvidelse(raa.get("utvidelse"), utv))
|
||||
k.startlikevekt = tekst(raa.get("startlikevekt", "krev"), "startlikevekt", STARTVALG)
|
||||
vurder_start(sim, k, raa.get("parametere"))
|
||||
sjekk_kombinasjoner(k, sim, utv)
|
||||
return k
|
||||
|
||||
|
|
@ -1008,7 +1187,9 @@ def kjor(sim: ModuleType, k: Kjoring, utv: ModuleType | None) -> Resultat:
|
|||
mpc_knows_chain=bool(k.mpc_kjenner_kjeden)))
|
||||
avledet_kjede(sim, ch, raa)
|
||||
elif k.modell == "simulate_chain2":
|
||||
raa = dict(sim.simulate_chain2(tau, bygg_tiltak(k, "CH2", k.dicts["CH2"]), p=p, kp=kp))
|
||||
ch2 = bygg_tiltak(k, "CH2", k.dicts["CH2"])
|
||||
raa = dict(sim.simulate_chain2(tau, ch2, p=p, kp=kp))
|
||||
avledet_kjede2(ch2, raa)
|
||||
elif k.modell == "simulate_soc":
|
||||
raa = fra_tuppel(sim.simulate_soc(k.regulator, tau, p, s=k.dicts["SOC"], kp=kp,
|
||||
shock=k.sjokk["faktor"], t_on=k.sjokk["fra"],
|
||||
|
|
@ -1031,6 +1212,20 @@ def avledet_kjede(sim: ModuleType, ch: dict[str, object], raa: dict[str, object]
|
|||
raa["Z_rel"] = z / tallfelt(ch, "Z")
|
||||
|
||||
|
||||
def avledet_kjede2(ch2: dict[str, object], raa: dict[str, object]) -> None:
|
||||
"""Kapasitet og andelen av alle saker som går til domstolen over tid i simulate_chain2 (som court2)."""
|
||||
t = raa["t"]
|
||||
if not isinstance(t, np.ndarray):
|
||||
return
|
||||
zfun, xibfun = ch2.get("Zfun"), ch2.get("xiBfun")
|
||||
z = np.array([float(zfun(float(x))) if callable(zfun) else tallfelt(ch2, "Z") for x in t])
|
||||
xib = np.array([float(xibfun(float(x))) if callable(xibfun) else tallfelt(ch2, "xiB") for x in t])
|
||||
pia = tallfelt(ch2, "piA")
|
||||
raa["Z"] = z
|
||||
raa["Z_rel"] = z / tallfelt(ch2, "Z")
|
||||
raa["xi"] = pia * tallfelt(ch2, "xiA") + (1.0 - pia) * xib
|
||||
|
||||
|
||||
def del_opp(raa: dict[str, object], k: Kjoring) -> Resultat:
|
||||
"""Skill tidsserier, skalarer og logg."""
|
||||
t = raa.get("t")
|
||||
|
|
@ -1052,13 +1247,19 @@ def del_opp(raa: dict[str, object], k: Kjoring) -> Resultat:
|
|||
|
||||
def kjor_trygt(sim: ModuleType, k: Kjoring, utv: ModuleType | None) -> Resultat:
|
||||
"""Som kjor, men regnefeil i kjernen blir en norsk melding (exit-kode 2)."""
|
||||
hvem = f" ({kolonneoverskrift(k, 1)})" if k.sveipverdier else ""
|
||||
try:
|
||||
return kjor(sim, k, utv)
|
||||
r = kjor(sim, k, utv)
|
||||
except (ArithmeticError, ValueError) as e:
|
||||
hvem = f" ({kolonneoverskrift(k, 1)})" if k.sveipverdier else ""
|
||||
melding = (f"Kjøringen{hvem} stoppet i kjernen: {e}. Parameterkombinasjonen gir trolig ingen "
|
||||
"likevekt ved start eller en løsning som vokser over alle grenser.")
|
||||
"likevekt ved start eller en løsning som vokser over alle grenser. Ingenting er skrevet.")
|
||||
raise ScenarioFeil(melding) from None
|
||||
ikke_tall = [navn for navn, v in r.serier.items() if not np.all(np.isfinite(v))]
|
||||
if ikke_tall:
|
||||
melding = (f"Kjøringen{hvem} ga verdier som ikke er endelige tall (uendelig eller NaN) i "
|
||||
f"{', '.join(ikke_tall)}: løsningen har vokst over alle grenser. Ingenting er skrevet.")
|
||||
raise ScenarioFeil(melding)
|
||||
return r
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- nøkkeltall
|
||||
|
|
@ -1115,16 +1316,17 @@ def tall_innsats_tillit(n: Nokkeltall, s: dict[str, Arr]) -> None:
|
|||
n.legg("L_slutt", "Tillit, ved slutt", s["L"][-1], 3)
|
||||
|
||||
|
||||
def tall_kjede(n: Nokkeltall, s: dict[str, Arr], endo: bool) -> None: # noqa: FBT001 - intern hjelper
|
||||
"""Saksbehandlingstid, kø og strøm i kjeden (summary, summary2)."""
|
||||
def tall_kjede(n: Nokkeltall, s: dict[str, Arr], endo: bool, j0: float) -> None: # noqa: FBT001 - intern hjelper
|
||||
"""Saksbehandlingstid, kø og strøm i kjeden (summary, summary2). Køen i måneder av strømmen J0."""
|
||||
for navn, tekst_ in (("tauD", "Saksbehandlingstid i domstolen"), ("tauA", "Saksbehandlingstid klasse A"),
|
||||
("tauB", "Saksbehandlingstid klasse B")):
|
||||
if navn in s:
|
||||
n.legg(f"{navn}_maks", f"{tekst_}, lengste (mnd)", s[navn].max(), 1)
|
||||
n.legg(f"{navn}_slutt", f"{tekst_}, ved slutt (mnd)", s[navn][-1], 1)
|
||||
for navn in ("Q", "QA", "QB"):
|
||||
for navn, hva in (("Q", "Kø i domstolen"), ("QA", "Kø klasse A"), ("QB", "Kø klasse B")):
|
||||
if navn in s:
|
||||
n.legg(f"{navn}_slutt", f"Kø {navn}, ved slutt", s[navn][-1], 2)
|
||||
n.legg(f"{navn}_slutt_mnd", f"{hva} ved slutt (mnd innstrømning før sjokket)",
|
||||
float(s[navn][-1]) / j0 if j0 > 0 else None, 1)
|
||||
if "JD" in s and "RD" in s and not endo:
|
||||
rd = float(s["RD"][-1])
|
||||
n.legg("inn_ut_slutt", "Saker inn / avgjort, ved slutt", float(s["JD"][-1]) / rd if rd > 0 else None, 2)
|
||||
|
|
@ -1146,7 +1348,7 @@ def tall_endo(n: Nokkeltall, s: dict[str, Arr], ch: dict[str, object], p: dict[s
|
|||
"""Frafall og kapasitetsår (summary_endo; frafallet i måneder av innstrømningen, som i notatets tabell)."""
|
||||
t = s["t"]
|
||||
z0 = tallfelt(ch, "Z")
|
||||
j0 = tallfelt(p, "gamma") * tallfelt(p, "Lam0") * tallfelt(p, "T0") # J0 i sim.py
|
||||
j0 = strom_j0(p)
|
||||
n.legg("frafall", "Frafall fra køen, i måneder av innstrømningen før sjokket",
|
||||
float(np.trapezoid(tallfelt(ch, "varsigma") * s["Q"], t)) / j0 if j0 > 0 else None, 1)
|
||||
n.legg("kapasitetsaar", "Kapasitetsår over utgangsnivået",
|
||||
|
|
@ -1161,7 +1363,7 @@ def nokkeltall(r: Resultat, k: Kjoring) -> list[Tall]:
|
|||
if "K" in s:
|
||||
tall_krim(n, s, endo)
|
||||
tall_innsats_tillit(n, s)
|
||||
tall_kjede(n, s, endo)
|
||||
tall_kjede(n, s, endo, strom_j0(k.dicts["P"]))
|
||||
tall_tiltak(n, s)
|
||||
if endo and "Z" in s and "Q" in s:
|
||||
tall_endo(n, s, k.dicts["CH0"], k.dicts["P"])
|
||||
|
|
@ -1308,6 +1510,16 @@ def hode(u: Utfall) -> list[str]:
|
|||
linjer += ["", ("Parametrene er notatets stiliserte verdier (tabell 3 i notatet), ikke kalibrert på norske "
|
||||
f"data, unntatt det scenariofila setter selv: {valgt}. Tallene viser modellens dynamikk, "
|
||||
"ikke prognoser."), ""]
|
||||
kalibrert = [(k, k.kalibrert) for k in sc.kjoringer if k.kalibrert]
|
||||
if kalibrert:
|
||||
linjer += [('Referansetilstanden er kalibrert på nytt ("startlikevekt": "kalibrer"): pådraget og domstolen '
|
||||
"er regnet om som i notatets kalibrering, for de nye verdiene:"), ""]
|
||||
for k, endr in kalibrert:
|
||||
hvem = f"{kolonneoverskrift(k, len(sc.sveip))}: " if sc.sveip else ""
|
||||
linjer.append("- " + hvem + "; ".join(
|
||||
f"{sti} {tall_g(ny)}" + ("" if math.isnan(gammel) else f" (var {tall_g(gammel)})")
|
||||
for sti, gammel, ny in endr))
|
||||
linjer.append("")
|
||||
return linjer
|
||||
|
||||
|
||||
|
|
@ -1325,14 +1537,130 @@ def vedtakslinjer(u: Utfall) -> list[str]:
|
|||
return linjer
|
||||
|
||||
|
||||
def sjokkstart(k: Kjoring, r: Resultat) -> float | None:
|
||||
"""Måneden sjokket (eller pådraget fra utvidelsen) begynner; None uten sjokk."""
|
||||
s = r.serier
|
||||
if k.modell == "simulate_soc":
|
||||
return k.sjokk["fra"] if k.sjokk["faktor"] != 1.0 else None
|
||||
if "Omega_faktor" in s:
|
||||
endret = np.flatnonzero(s["Omega_faktor"] != 1.0)
|
||||
return float(s["t"][endret[0]]) if endret.size else None
|
||||
p = k.dicts["P"]
|
||||
return tallfelt(p, "t_shock") if tallfelt(p, "shock") != 1.0 else None
|
||||
|
||||
|
||||
def drift_maaned(k: Kjoring, r: Resultat) -> float:
|
||||
"""Måneden driften før sjokket vises for: sjokkets start, eller måned 12 uten sjokk (høyst slutt)."""
|
||||
m = sjokkstart(k, r)
|
||||
return min(UTEN_SJOKK_MND if m is None else m, k.slutt)
|
||||
|
||||
|
||||
def har_ko(r: Resultat) -> bool:
|
||||
"""Sann for modeller med kø i domstolen (simulate_chain, simulate_chain2, simulate_endo, utvidelse med kjede)."""
|
||||
return "Q" in r.serier or "QA" in r.serier
|
||||
|
||||
|
||||
def driftlinje(k: Kjoring, r: Resultat, dt: float, antall_sveip: int, *, med_grunner: bool) -> str:
|
||||
"""Hvor langt en kjøring har drevet fra start til sjokket (ubalanse, eller kø i domstolen)."""
|
||||
s = r.serier
|
||||
m = drift_maaned(k, r)
|
||||
i = min(round(m / dt), len(s["t"]) - 1)
|
||||
deler = [f"{hva} fra {tall(float(s[navn][0]), d)} til {tall(float(s[navn][i]), d)}"
|
||||
for navn, hva, d in (("K", "K", 3), ("T", "innsatsen T", 2), ("L", "tilliten", 3),
|
||||
("tauD", "saksbehandlingstiden (mnd)", 1), ("tauA", "tiden i klasse A (mnd)", 1),
|
||||
("tauB", "tiden i klasse B (mnd)", 1))
|
||||
if navn in s]
|
||||
hvem = f"{kolonneoverskrift(k, antall_sveip)}: " if antall_sveip else ""
|
||||
grunner = f"{'; '.join(k.ubalanse)}. " if med_grunner else ""
|
||||
deler[0] = deler[0].replace(" fra ", " gått fra ", 1)
|
||||
return f"- {hvem}{grunner}Fram til måned {tall_kort(m)} har " + ", ".join(deler) + "."
|
||||
|
||||
|
||||
KO_DRIFT = ("I modeller med kø i domstolen er dette nesten, men ikke helt, en likevekt: det som faller fra køen, "
|
||||
"blir aldri fjernet fra kriminaliteten, så K og saksbehandlingstiden kryper sakte oppover også uten sjokk.")
|
||||
|
||||
|
||||
def ko_driftlinjer(u: Utfall, kjoringer: list[tuple[Kjoring, Resultat]]) -> list[str]:
|
||||
"""Driften før sjokket i kjøringer med kø som starter i notatets referansetilstand."""
|
||||
med_sjokk = [(k, r) for k, r in kjoringer if (sjokkstart(k, r) or 0.0) > 0.0]
|
||||
uten = len(med_sjokk) < len(kjoringer)
|
||||
linjer: list[str] = []
|
||||
if med_sjokk:
|
||||
linjer += ["", "Slik har modellen drevet før sjokket:", ""]
|
||||
linjer += [driftlinje(k, r, u.dt, len(u.sc.sveip), med_grunner=False) for k, r in med_sjokk]
|
||||
if uten:
|
||||
linjer += ["", ("Uten sjokk (eller med sjokk fra måned 0) kryper modellen med kø sakte oppover av seg selv; en "
|
||||
'kjøring uten sjokk ("sjokk": {"faktor": 1}) viser hvor mye.')]
|
||||
return linjer
|
||||
|
||||
|
||||
def forklaring(u: Utfall) -> list[str]:
|
||||
"""Hvordan tallene leses; byttes ut når starten ikke er en likevekt."""
|
||||
k0 = u.sc.kjoringer[0]
|
||||
lam0 = {tallfelt(k.dicts["P"], "Lam0") for k in u.sc.kjoringer}
|
||||
tillit = f"tilliten er {tall(lam0.pop(), 2)} der" if len(lam0) == 1 else "tilliten er Lam0 der"
|
||||
ko = any(har_ko(r) for r in u.resultater)
|
||||
resten = ("Kapasitet: 1,00 er kapasiteten ved start. Tider i måneder."
|
||||
+ (f" Køen er målt i måneder av innstrømningen til domstolen før sjokket (J0 = "
|
||||
f"{tall_g(strom_j0(k0.dicts['P']))} i modellens enheter); i datafilene står køen i modellens "
|
||||
"enheter." if ko else ""))
|
||||
alle = list(zip(u.sc.kjoringer, u.resultater))
|
||||
skjeve = [(k, r) for k, r in alle if k.ubalanse]
|
||||
rette_med_ko = [(k, r) for k, r in alle if not k.ubalanse and har_ko(r)]
|
||||
if not skjeve and not ko:
|
||||
return [(f"Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket for kriminaliteten K og "
|
||||
f"politiets innsats T; {tillit}. {resten}")]
|
||||
if not skjeve:
|
||||
return [(f"Slik leses tallene: 1,00 er nivået ved start for kriminaliteten K og politiets innsats T "
|
||||
f"(notatets referansetilstand); {tillit}. {KO_DRIFT} {resten}"), *ko_driftlinjer(u, rette_med_ko)]
|
||||
like = len({tuple(k.ubalanse) for k, _ in skjeve}) == 1
|
||||
linjer = [('Slik leses tallene: startpunktet er ikke en likevekt ("startlikevekt": "ubalanse")'
|
||||
+ (": " + "; ".join(skjeve[0][0].ubalanse) if like else "") + ". Tallene er målt fra "
|
||||
"startverdiene (K og T er 1,00 ved start), men modellen ville drevet bort fra dem også uten sjokk, "
|
||||
f"så 1,00 er ikke et nivå den holder seg på. {resten} Slik har modellen drevet før sjokket:"), ""]
|
||||
linjer += [driftlinje(k, r, u.dt, len(u.sc.sveip), med_grunner=not like) for k, r in skjeve]
|
||||
if len(skjeve) < len(u.sc.kjoringer):
|
||||
linjer += ["", ("Kjøringene som ikke er nevnt, starter i notatets referansetilstand." + (f" {KO_DRIFT}" if
|
||||
rette_med_ko else ""))]
|
||||
linjer += ko_driftlinjer(u, rette_med_ko) if rette_med_ko else []
|
||||
return linjer
|
||||
|
||||
|
||||
def merknader(u: Utfall) -> list[str]:
|
||||
"""Merknader (ikke stopp): K langt utenfor modellens område, og sjokk utenfor horisonten."""
|
||||
ut: list[str] = []
|
||||
for k, r in zip(u.sc.kjoringer, u.resultater):
|
||||
hvem = f" ({kolonneoverskrift(k, len(u.sc.sveip))})" if u.sc.sveip else ""
|
||||
if "K" in r.serier:
|
||||
kmax, kmin = float(r.serier["K"].max()), float(r.serier["K"].min())
|
||||
if kmax > K_OMRADE[1] or kmin < K_OMRADE[0]:
|
||||
ut.append(f"Kriminaliteten{hvem} går fra {tall(kmin, 3)} til {tall(kmax, 3)}, utenfor området "
|
||||
f"kjernen selv regner på (K mellom {tall(K_OMRADE[0], 2)} og {tall(K_OMRADE[1], 0)}, der "
|
||||
"soc_equilibria i sim.py leter etter likevekter; notatets egne kjøringer ligger mellom "
|
||||
"0,83 og 9,82). Tallene er regnet slik modellen sier, men ligningene er ikke laget for "
|
||||
"så store utslag.")
|
||||
if k.modell == "simulate_soc":
|
||||
if k.sjokk["fra"] >= k.slutt:
|
||||
ut.append(f"Sjokket{hvem} begynner i måned {tall_kort(k.sjokk['fra'])}, ikke før slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ingen virkning av det.")
|
||||
elif k.sjokk["til"] > k.slutt:
|
||||
ut.append(f"Sjokket{hvem} varer til måned {tall_kort(k.sjokk['til'])}, etter slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ikke at det går over.")
|
||||
elif k.sjokk and k.sjokk["faktor"] != 1.0 and k.sjokk["fra"] >= k.slutt:
|
||||
ut.append(f"Sjokket{hvem} kommer i måned {tall_kort(k.sjokk['fra'])}, ikke før slutt "
|
||||
f"({tall_kort(k.slutt)}): kjøringen viser ingen virkning av det.")
|
||||
return list(dict.fromkeys(ut))
|
||||
|
||||
|
||||
def sammendrag(u: Utfall) -> str:
|
||||
"""Teksten i sammendrag.md (og på skjermen)."""
|
||||
linjer = [*hode(u), "## Nøkkeltall", "", *nokkeltabeller(u.sc, u.alle)]
|
||||
if len(u.sc.sveip) == RUTENETT:
|
||||
linjer += matrise(u.sc, u.alle)
|
||||
linjer.append("Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket for kriminaliteten K "
|
||||
"og politiets innsats T; tilliten er 0,80 der. Kapasitet: 1,00 er kapasiteten ved start. "
|
||||
"Tider i måneder.")
|
||||
linjer += forklaring(u)
|
||||
merk = merknader(u)
|
||||
if merk:
|
||||
linjer += ["", "## Merknader (kjøringen er ikke stoppet)", "", *(f"- {m}" for m in merk)]
|
||||
linjer += vedtakslinjer(u)
|
||||
linjer += ["", "## Filer", ""]
|
||||
linjer.extend(f"- {navn}: {', '.join(kolonner)}" for navn, kolonner in u.filer)
|
||||
|
|
@ -1378,6 +1706,11 @@ def resultatjson(sim: ModuleType, u: Utfall) -> str:
|
|||
post["skalarer"] = r.skalarer
|
||||
if r.logg:
|
||||
post["vedtak"] = r.logg
|
||||
post["startlikevekt"] = {
|
||||
"valg": k.startlikevekt,
|
||||
"kalibrert": {sti: {"for": gammel, "etter": ny} for sti, gammel, ny in k.kalibrert},
|
||||
"ubalanse": k.ubalanse,
|
||||
}
|
||||
kjoringer.append(post)
|
||||
forklaring: dict[str, str] = {}
|
||||
for tallene in u.alle:
|
||||
|
|
@ -1403,33 +1736,64 @@ def resultatjson(sim: ModuleType, u: Utfall) -> str:
|
|||
return json.dumps(json_verdi(data), ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
|
||||
EGNE_NOKLER = ("scenario", "scenariofil_sha256", "kjerne_sha256", "kjoringer") # i resultat.json
|
||||
EGEN_DATAFIL = re.compile(r"[^/\\]+\.(csv|dat)")
|
||||
|
||||
|
||||
def egne_filer(gammel: Path) -> list[str] | None:
|
||||
"""Filene et tidligere resultat laget, eller None hvis resultat.json ikke er kjørerens."""
|
||||
try:
|
||||
data = json.loads(gammel.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if not isinstance(data, dict) or not all(n in data for n in EGNE_NOKLER):
|
||||
return None
|
||||
kjoringer = data["kjoringer"]
|
||||
if not isinstance(kjoringer, list):
|
||||
return None
|
||||
filer = [str(x.get("fil", "")) for x in kjoringer if isinstance(x, dict)]
|
||||
return [f for f in filer if EGEN_DATAFIL.fullmatch(f) and f not in {".csv", ".dat"}]
|
||||
|
||||
|
||||
def klargjor_mappe(ut: Path) -> None:
|
||||
"""Lag mappa; fjern bare filer et tidligere resultat i samme mappe har laget."""
|
||||
"""Lag mappa; fjern bare datafilene, sammendraget og resultat.json et tidligere resultat laget."""
|
||||
if ut.exists() and not ut.is_dir():
|
||||
melding = f"--ut: {ut} finnes og er ikke en mappe."
|
||||
raise ScenarioFeil(melding)
|
||||
if ut.exists() and any(ut.iterdir()):
|
||||
gammel = ut / "resultat.json"
|
||||
if not gammel.is_file():
|
||||
melding = (f"--ut: mappa {ut} finnes og har filer som ikke er et tidligere resultat. Velg en "
|
||||
"annen mappe med --ut.")
|
||||
filer = egne_filer(ut / "resultat.json")
|
||||
if filer is None:
|
||||
melding = (f"--ut: mappa {ut} finnes og har filer som ikke er et tidligere resultat fra "
|
||||
"scenariokjøreren. Velg en annen mappe med --ut.")
|
||||
raise ScenarioFeil(melding)
|
||||
try:
|
||||
data = json.loads(gammel.read_text(encoding="utf-8"))
|
||||
filer = [str(x["fil"]) for x in data.get("kjoringer", [])]
|
||||
except (OSError, ValueError, TypeError, KeyError, AttributeError):
|
||||
filer = []
|
||||
for navn in [*filer, "sammendrag.md", "resultat.json"]:
|
||||
sti = ut / navn
|
||||
if sti.parent == ut and sti.is_file():
|
||||
if sti.parent == ut and sti.is_file() and not sti.is_symlink():
|
||||
sti.unlink()
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def skriv_alt(sim: ModuleType, sc: Scenario, fil: Path, mappe: Path,
|
||||
resultater: list[Resultat]) -> str:
|
||||
"""Skriv datafilene, sammendraget og resultat.json. Returnerer sammendraget."""
|
||||
alle = [nokkeltall(r, k) for r, k in zip(resultater, sc.kjoringer)]
|
||||
klargjor_mappe(mappe)
|
||||
filer: list[tuple[str, list[str]]] = []
|
||||
for k, r in zip(sc.kjoringer, resultater):
|
||||
navn = filnavn(sc, k)
|
||||
filer.append((navn, skriv_csv(sim, sc, r, mappe / navn)))
|
||||
u = Utfall(sc=sc, fil=fil, alle=alle, resultater=resultater, filer=filer, dt=float(sim.DT))
|
||||
tekst_ = sammendrag(u)
|
||||
(mappe / "sammendrag.md").write_text(tekst_, encoding="utf-8")
|
||||
(mappe / "resultat.json").write_text(resultatjson(sim, u), encoding="utf-8")
|
||||
return tekst_
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- inngang
|
||||
|
||||
|
||||
BRUK = ("Bruk: python3 modell/kjor.py scenario <fil.json> [--ut <mappe>]\n"
|
||||
PYTHON = "python" if os.name == "nt" else "python3" # tolken heter python på Windows
|
||||
BRUK = (f"Bruk: {PYTHON} modell/kjor.py scenario <fil.json> [--ut <mappe>]\n"
|
||||
"Formatet er beskrevet i docs/scenarioformat.md.")
|
||||
|
||||
|
||||
|
|
@ -1468,18 +1832,14 @@ def kjor_scenario(fil: Path, ut: Path | None) -> tuple[str, Path]:
|
|||
sim = last_kjerne()
|
||||
mappe = ut if ut is not None else Path.cwd() / "resultater" / slug(sc.navn)
|
||||
resultater = [kjor_trygt(sim, k, sc.utvidelse) for k in sc.kjoringer]
|
||||
alle = [nokkeltall(r, k) for r, k in zip(resultater, sc.kjoringer)]
|
||||
for r in resultater:
|
||||
kolonner_for(sc, r)
|
||||
klargjor_mappe(mappe)
|
||||
filer: list[tuple[str, list[str]]] = []
|
||||
for k, r in zip(sc.kjoringer, resultater):
|
||||
navn = filnavn(sc, k)
|
||||
filer.append((navn, skriv_csv(sim, sc, r, mappe / navn)))
|
||||
u = Utfall(sc=sc, fil=fil, alle=alle, resultater=resultater, filer=filer, dt=float(sim.DT))
|
||||
tekst_ = sammendrag(u)
|
||||
(mappe / "sammendrag.md").write_text(tekst_, encoding="utf-8")
|
||||
(mappe / "resultat.json").write_text(resultatjson(sim, u), encoding="utf-8")
|
||||
try:
|
||||
tekst_ = skriv_alt(sim, sc, fil, mappe, resultater)
|
||||
except OSError as e:
|
||||
grunn = e.strerror or str(e)
|
||||
melding = f"Kan ikke skrive i {mappe}: {grunn}. Velg en annen mappe med --ut."
|
||||
raise ScenarioFeil(melding) from None
|
||||
return tekst_, mappe
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ PARAMETERE: dict[str, object] = {
|
|||
"punkter": [[12.0, 1.3]], # [måned, faktor på Omega0]; før første punkt er faktoren 1
|
||||
}
|
||||
BRUKER = ("P", "CH0") # kjernens dicts som kan overstyres under parametere
|
||||
# Scenariofiler i eksempler/scenarioer/notatet/ som utvidelsen med standardverdiene skal gjenskape byte for byte
|
||||
# når "modell" byttes til "utvidelse:paadrag_tidsserie" (tests/test_utvidelser.py kjører dem gjennom kjøreren).
|
||||
GJENSKAPER_NOTATET = ("kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5")
|
||||
FORMER = ("trapp", "lineaer")
|
||||
MAKS_PUNKTER = 1000
|
||||
KJERNE = Path(__file__).resolve().parents[1] / "kjerne"
|
||||
|
|
|
|||
|
|
@ -70,10 +70,11 @@ def hjem(tmp_path: Path) -> Path:
|
|||
|
||||
@pytest.fixture
|
||||
def kopi(tmp_path: Path) -> Path:
|
||||
"""En kopi av modell/, fasit/ og eksempler/ som testen kan endre. Returnerer kopiens rot."""
|
||||
"""En kopi av modell/, fasit/ og eksempler/ som testen kan endre. Returnerer kopiens rot.
|
||||
|
||||
Kopien er ikke merket som arbeidskopi (ingen LES-MEG.md), som en klone av repoet.
|
||||
"""
|
||||
rot = tmp_path / "kopi"
|
||||
for navn in ("modell", "fasit", "eksempler"):
|
||||
shutil.copytree(ROT / navn, rot / navn, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
for navn in ("scenario.py", "arbeidskopi.py"):
|
||||
(rot / "modell" / navn).unlink(missing_ok=True)
|
||||
return rot
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import arbeidskopi as arbeidskopi_modul
|
||||
import pytest
|
||||
from conftest import EKSEMPLER, FASIT, ROT, kjor
|
||||
|
||||
|
|
@ -100,3 +101,28 @@ def test_endring_i_kopiens_kjerne_brukes(arbeidskopi: Path, tmp_path: Path) -> N
|
|||
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()
|
||||
|
||||
|
||||
def test_nekter_mappe_inne_i_pluginen(tmp_path: Path) -> None:
|
||||
mal = ROT / "nyttsted-test-arbeidskopi"
|
||||
assert not mal.exists()
|
||||
prosess = kjor("arbeidskopi", str(mal))
|
||||
assert prosess.returncode == 2, prosess.stdout
|
||||
assert "ligger inne i" in prosess.stderr
|
||||
assert not mal.exists()
|
||||
assert not (tmp_path / "x").exists()
|
||||
|
||||
|
||||
def test_lisenssetningen(arbeidskopi: Path) -> None:
|
||||
assert "Alt i pluginen er under MIT-lisensen (se `LICENSE`)." in (arbeidskopi / "LES-MEG.md").read_text(
|
||||
encoding="utf-8")
|
||||
|
||||
|
||||
def test_kommandoene_skrives_for_windows(tmp_path: Path) -> None:
|
||||
mal = tmp_path / "Kari Nordmann" / "rettsstat-modell"
|
||||
tekst = arbeidskopi_modul.les_meg(mal, [], windows=True)
|
||||
assert f'python "{mal / "modell" / "kjor.py"}" scenario' in tekst
|
||||
assert "python3" not in tekst
|
||||
assert "'" not in tekst.split("## Slik kjører du kopien", 1)[1].split("## Slik kontrollerer", 1)[0]
|
||||
posix = arbeidskopi_modul.les_meg(mal, [], windows=False)
|
||||
assert f"python3 '{mal / 'modell' / 'kjor.py'}' scenario" in posix
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import kjor as kjor_modul
|
||||
import pytest
|
||||
from conftest import DOMSTOL, EKSEMPLER, FASIT, MODELL, NORGE, ROT, fasit, kjor, lag_venv_omvei
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
UTEN_SITE = [sys.executable, "-S"] # samme Python uten site-packages: numpy og scipy mangler
|
||||
|
||||
|
||||
|
|
@ -119,6 +124,74 @@ def test_ingen_pycache_i_kjernen(kopi: Path) -> None:
|
|||
assert not (kopi / "modell" / "kjerne" / "__pycache__").exists()
|
||||
|
||||
|
||||
# ---------------- ukjente feltnavn (funn 5) ----------------
|
||||
|
||||
def skriv_variant(tmp_path: Path, endre: Callable[[dict[str, Any]], None], navn: str = "variant.json") -> Path:
|
||||
"""En kopi av eksempel_domstol.json med en endring."""
|
||||
data = json.loads(DOMSTOL.read_text(encoding="utf-8"))
|
||||
endre(data)
|
||||
fil = tmp_path / navn
|
||||
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
return fil
|
||||
|
||||
|
||||
def skrivefeil_i_ledd(data: dict[str, Any]) -> None:
|
||||
domstol = data["ledd"][2]
|
||||
domstol["prognose_innstrømning"] = domstol.pop("prognose_innstromning")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kommando", ["diagnose", "forlop"])
|
||||
def test_skrivefeil_i_ledd_avvises(kommando: str, tmp_path: Path) -> None:
|
||||
fil = skriv_variant(tmp_path, skrivefeil_i_ledd)
|
||||
prosess = kjor(kommando, str(fil))
|
||||
assert prosess.returncode == 2
|
||||
assert prosess.stdout == ""
|
||||
assert prosess.stderr.startswith(
|
||||
'Feil i inndata, ledd 3 (Domstol): feltet "prognose_innstrømning" finnes ikke. '
|
||||
'Mente du "prognose_innstromning"? Gyldige felt: navn, innstromning, kapasitet, restanse, maal_tid,')
|
||||
|
||||
|
||||
def test_skrivefeil_i_styring_avvises(tmp_path: Path) -> None:
|
||||
def endre(data: dict[str, Any]) -> None:
|
||||
data["styring"]["reaktivitett"] = data["styring"].pop("reaktivitet")
|
||||
|
||||
prosess = kjor("forlop", str(skriv_variant(tmp_path, endre)))
|
||||
assert prosess.returncode == 2
|
||||
assert prosess.stderr == ('Feil i inndata, styring: feltet "reaktivitett" finnes ikke. Mente du "reaktivitet"? '
|
||||
"Gyldige felt: forsinkelse, reaktivitet, merknad, kilde, kilder.\n")
|
||||
|
||||
|
||||
def test_merknadsfelt_og_ukjente_toppfelt_er_tillatt(tmp_path: Path) -> None:
|
||||
def endre(data: dict[str, Any]) -> None:
|
||||
data["ledd"][2].update(merknad="fri tekst", kilde="SSB", kilder=["a", "b"])
|
||||
data["styring"]["merknad"] = "regime A"
|
||||
data["noe_annet"] = {"fritt": True}
|
||||
|
||||
prosess = kjor("diagnose", str(skriv_variant(tmp_path, endre)))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
assert prosess.stdout == fasit("diagnose_eksempel_domstol.txt")
|
||||
|
||||
|
||||
def test_feltene_leses_fra_kjernen_i_modellen(kopi: Path, tmp_path: Path) -> None:
|
||||
diagnose = kopi / "modell" / "kjerne" / "diagnose.py"
|
||||
tekst = diagnose.read_text(encoding="utf-8")
|
||||
assert '"innstromning_for_okning")' in tekst
|
||||
diagnose.write_text(tekst.replace('"innstromning_for_okning")', '"innstromning_for_okning", "nytt_felt")', 1),
|
||||
encoding="utf-8")
|
||||
fil = skriv_variant(tmp_path, lambda data: data["ledd"][0].update(nytt_felt=1))
|
||||
assert kjor("diagnose", str(fil)).returncode == 2
|
||||
prosess = kjor("--modell", str(kopi), "diagnose", str(fil))
|
||||
assert prosess.returncode == 0, prosess.stderr
|
||||
|
||||
|
||||
def test_feil_form_overlates_til_kjernen(tmp_path: Path) -> None:
|
||||
fil = tmp_path / "feil.json"
|
||||
fil.write_text('{"ledd": [1, {"navn": "A"}]}', encoding="utf-8")
|
||||
prosess = kjor("diagnose", str(fil))
|
||||
assert prosess.returncode == 2
|
||||
assert prosess.stderr == "Feil i inndata, ledd 1: hvert ledd må være et objekt med felt og verdier.\n"
|
||||
|
||||
|
||||
# ---------------- --data og valg av Python ----------------
|
||||
|
||||
@pytest.mark.parametrize("verdi", [None, "", " ", "${CLAUDE_PLUGIN_DATA}", "/a/${CLAUDE_PLUGIN_DATA}/b"])
|
||||
|
|
@ -229,6 +302,18 @@ def test_velg_tolk(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
assert kjor_modul.velg_tolk(tmp_path).hvor == "mangler"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("system", "start", "sitert"), [
|
||||
("nt", "python ", '"/data med mellomrom"'), ("posix", "python3 ", "'/data med mellomrom'"),
|
||||
])
|
||||
def test_foreslatt_kommando_per_system(system: str, start: str, sitert: str) -> None:
|
||||
valg = kjor_modul.Valg(argv=[], data=Path("/data med mellomrom"), data_oppgitt=True, modell=MODELL,
|
||||
kommando="forlop", args=[])
|
||||
tekst = kjor_modul.kommando_tekst(valg, "miljo", "--opprett", system=system)
|
||||
assert tekst.startswith(start)
|
||||
assert f"--data {sitert} miljo --opprett" in tekst
|
||||
assert kjor_modul.python_navn(system) == start.strip()
|
||||
|
||||
|
||||
# ---------------- miljo --opprett (uten å installere) ----------------
|
||||
|
||||
def test_opprett_proever_laaste_versjoner_forst(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
@ -302,6 +387,7 @@ def test_modell_i_arbeidskopi(kopi: Path, undermappe: str) -> None:
|
|||
|
||||
@pytest.mark.parametrize("kommando", ["scenario", "arbeidskopi"])
|
||||
def test_modul_som_ikke_finnes_ennaa(kopi: Path, kommando: str) -> None:
|
||||
(kopi / "modell" / f"{kommando}.py").unlink()
|
||||
prosess = kjor("--modell", str(kopi), kommando, "x")
|
||||
assert prosess.returncode == 2
|
||||
assert f"Kommandoen «{kommando}» er ikke på plass i denne modellen ennå" in prosess.stderr
|
||||
|
|
@ -359,6 +445,29 @@ def test_notatet_nekter_ut_mappe_i_modellen() -> None:
|
|||
assert not (MODELL / "kjerne" / "data").exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt" or os.geteuid() == 0, reason="skrivebeskyttelse virker ikke for root")
|
||||
def test_notatet_i_mappe_som_ikke_kan_skrives(tmp_path: Path) -> None:
|
||||
sperret = tmp_path / "sperret"
|
||||
sperret.mkdir()
|
||||
sperret.chmod(0o555)
|
||||
try:
|
||||
prosess = kjor("notatet", "--ut", str(sperret / "ut"))
|
||||
finally:
|
||||
sperret.chmod(0o755)
|
||||
assert prosess.returncode == 2
|
||||
assert prosess.stderr.startswith(f"Kan ikke skrive i {(sperret / 'ut').resolve()}: ")
|
||||
assert prosess.stderr.endswith(". Velg en annen mappe med --ut.\n")
|
||||
assert "Traceback" not in prosess.stderr
|
||||
|
||||
|
||||
def test_notatet_naar_sim_stopper(kopi: Path, tmp_path: Path) -> None:
|
||||
(kopi / "modell" / "kjerne" / "sim.py").write_text("print('start')\nraise SystemExit(5)\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(kopi), "notatet", "--ut", str(tmp_path / "ut"))
|
||||
assert prosess.returncode == 2
|
||||
assert "Kjøringen av sim.py stoppet underveis (exit-kode 5)" in prosess.stderr
|
||||
assert "start\n" in prosess.stdout
|
||||
|
||||
|
||||
@pytest.mark.treg
|
||||
def test_notatet_gjenskaper_datafilene(tmp_path: Path) -> None:
|
||||
ut = tmp_path / "ut"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import kontroll
|
||||
import pytest
|
||||
from conftest import kjor
|
||||
from conftest import FASIT, kjor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
|
@ -70,23 +73,53 @@ def test_dat_ulik_form(naa: str) -> None:
|
|||
|
||||
# ---------------- kontrollen som helhet ----------------
|
||||
|
||||
FALSK_SCENARIO = """\
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv):
|
||||
fil, ut = Path(argv[0]), Path(argv[2])
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
fasit = Path(__file__).resolve().parent.parent / "fasit" / "data" / (fil.stem + ".dat")
|
||||
shutil.copyfile(fasit, ut / (fil.stem + ".dat"))
|
||||
return 0
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lett_kopi(kopi: Path) -> Path:
|
||||
"""En kopi der scenario.py er byttet mot en som kopierer fasit, så kontroll 6 går fort.
|
||||
|
||||
Brukes av testene som gjelder andre kontroller; scenariokjøreren selv prøves på pluginen og i egne tester.
|
||||
"""
|
||||
(kopi / "modell" / "scenario.py").write_text(FALSK_SCENARIO, encoding="utf-8")
|
||||
return kopi
|
||||
|
||||
|
||||
def test_rask_kontroll_av_pluginen(hjem: Path) -> None:
|
||||
prosess = kjor("kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "1. Kjernen: uendret fra notatet (3 filer," in prosess.stdout
|
||||
assert "1. Fasit: uendret fra notatet (27 filer)." in prosess.stdout
|
||||
assert "2. diagnose: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
|
||||
assert "3. forlop: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
|
||||
assert "4. Full kjøring: hoppet over (--rask)." in prosess.stdout
|
||||
assert ("6. Scenariokjøreren: 13 av 13 av notatets datafiler gjenskapt byte for byte fra "
|
||||
"eksempler/scenarioer/notatet/ (mpc11 bare i full kjøring).") in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): bit for bit som notatet." in prosess.stdout
|
||||
|
||||
|
||||
def test_kontroll_uten_numpy_gjor_det_den_kan(hjem: Path) -> None:
|
||||
def test_kontroll_uten_numpy_skryter_ikke(hjem: Path) -> None:
|
||||
prosess = kjor("kontroll", python=[sys.executable, "-S"], hjem=hjem)
|
||||
assert prosess.returncode == 3
|
||||
assert "2. diagnose: lik notatet" in prosess.stdout
|
||||
assert "3. forlop: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert "4. Full kjøring: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert "Nivå (bare det som er kjørt): bit for bit som notatet." in prosess.stdout
|
||||
assert "6. Scenariokjøreren: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert ("Nivå: ingen avvik i det som ble kjørt (Kjernen, Fasit, diagnose). Ikke kontrollert: forlop, full "
|
||||
"kjøring av sim.py, utvidelsenes grensetilfeller, scenariokjøreren.") in prosess.stdout
|
||||
assert "bit for bit som notatet" not in prosess.stdout
|
||||
|
||||
|
||||
def endre_diagnose(kopi: Path) -> None:
|
||||
|
|
@ -97,32 +130,91 @@ def endre_diagnose(kopi: Path) -> None:
|
|||
fil.write_text(tekst.replace("UPS_STAR = 0.95", "UPS_STAR = 0.90"), encoding="utf-8")
|
||||
|
||||
|
||||
def test_endret_kjerne_i_pluginen_er_en_feil(kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(kopi)
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_utenfor_arbeidskopi_er_noytral(lett_kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(lett_kopi)
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "diagnose.py er endret" in prosess.stdout
|
||||
assert "2. diagnose: endret (se under)." in prosess.stdout
|
||||
assert "- Kapasitet: (0,80 x 134,3 + 273/24) / 0,95 = 125,1 saker/mnd, det vil si 19 % mer." in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): endret: Kjernen, diagnose, forlop." in prosess.stdout
|
||||
assert "Pluginens modell skal regne som notatet, så dette er en feil." in prosess.stdout
|
||||
assert "Er dette pluginen slik den er installert, er det en feil" in prosess.stdout
|
||||
assert "Er modellen endret med vilje (for eksempel i en klone av repoet), er det en opplysning" in prosess.stdout
|
||||
assert "så dette er en feil." not in prosess.stdout
|
||||
|
||||
|
||||
def test_endret_kjerne_i_arbeidskopi_er_en_opplysning(kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(kopi)
|
||||
(kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_i_arbeidskopi_er_en_opplysning(lett_kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(lett_kopi)
|
||||
(lett_kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "(arbeidskopi)" in prosess.stdout
|
||||
assert "det er en opplysning, ikke en feil" in prosess.stdout
|
||||
assert "+ Kapasitet: (0,80 x 134,3 + 273/24) / 0,90" in prosess.stdout
|
||||
|
||||
|
||||
def test_manglende_fasit(kopi: Path, hjem: Path) -> None:
|
||||
(kopi / "fasit" / "diagnose_norge_2025.txt").unlink()
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_med_ny_fasit_og_kilde_er_fortsatt_endret(lett_kopi: Path, hjem: Path) -> None:
|
||||
"""Funn 3: kontrollen ankres i notatet, ikke i KILDE.json og fasit/ i samme mappe."""
|
||||
endre_diagnose(lett_kopi)
|
||||
kjerne = lett_kopi / "modell" / "kjerne"
|
||||
for eksempel in ("eksempel_domstol", "norge_2025"):
|
||||
ny = subprocess.run([sys.executable, str(kjerne / "diagnose.py"), str(lett_kopi / "eksempler" /
|
||||
f"{eksempel}.json")], capture_output=True, text=True, encoding="utf-8", check=True)
|
||||
(lett_kopi / "fasit" / f"diagnose_{eksempel}.txt").write_text(ny.stdout, encoding="utf-8")
|
||||
kilde = json.loads((kjerne / "KILDE.json").read_text(encoding="utf-8"))
|
||||
kilde["filer"]["diagnose.py"]["sha256"] = hashlib.sha256((kjerne / "diagnose.py").read_bytes()).hexdigest()
|
||||
(kjerne / "KILDE.json").write_text(json.dumps(kilde), encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "norge_2025: fant ikke" in prosess.stdout
|
||||
assert "1. Kjernen: endret fra notatet" in prosess.stdout
|
||||
assert "diagnose.py er endret" in prosess.stdout
|
||||
assert "1. Fasit: fasit er endret i forhold til notatet (2 av 27 filer)." in prosess.stdout
|
||||
assert "fasit/diagnose_eksempel_domstol.txt: har ikke notatets sha256" in prosess.stdout
|
||||
assert "2. diagnose: endret (se under)." in prosess.stdout
|
||||
assert "er endret i forhold til notatet, så sammenligningen sier ingenting om notatet" in prosess.stdout
|
||||
assert "bit for bit" not in prosess.stdout.split("Nivå")[-1]
|
||||
|
||||
|
||||
def test_manglende_fasit(lett_kopi: Path, hjem: Path) -> None:
|
||||
(lett_kopi / "fasit" / "diagnose_norge_2025.txt").unlink()
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "fasit/diagnose_norge_2025.txt: mangler" in prosess.stdout
|
||||
assert "norge_2025: fant ikke fasit/diagnose_norge_2025.txt" in prosess.stdout
|
||||
|
||||
|
||||
def test_diagnose_krever_eksakt_likhet(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Funn 10: én enhet i siste siffer i diagnosen er endret, ikke innenfor toleranse."""
|
||||
fasit = tmp_path / "fasit"
|
||||
fasit.mkdir()
|
||||
for navn in ("eksempel_domstol", "norge_2025"):
|
||||
tekst = (FASIT / f"diagnose_{navn}.txt").read_text(encoding="utf-8")
|
||||
(fasit / f"diagnose_{navn}.txt").write_text(tekst.replace("1,28", "1,29", 1), encoding="utf-8")
|
||||
monkeypatch.setattr(kontroll, "FASIT", fasit)
|
||||
assert kontroll.kontroll_utskrift("diagnose", tmp_path, set(), toleranse=False).status == kontroll.ENDRET
|
||||
assert kontroll.kontroll_utskrift("diagnose", tmp_path, set(), toleranse=True).status == kontroll.TOLERANSE
|
||||
|
||||
|
||||
def test_scenariokjoreren_som_ikke_gjenskaper_notatet(kopi: Path, hjem: Path) -> None:
|
||||
"""Funn 4: en endring i det scenariokjøreren regner, blir oppdaget av kontroll --rask."""
|
||||
fil = kopi / "eksempler" / "scenarioer" / "notatet" / "p3.json"
|
||||
data = json.loads(fil.read_text(encoding="utf-8"))
|
||||
data["styring"]["reaktivitet"] = 4
|
||||
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "6. Scenariokjøreren: 12 av 13 av notatets datafiler gjenskapt byte for byte" in prosess.stdout
|
||||
assert "p3.dat (endret): største absolutte avvik per kolonne:" in prosess.stdout
|
||||
assert "endret: Scenariokjøreren." in prosess.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fjern", ["modell/scenario.py", "eksempler/scenarioer/notatet/endo_ko.json"])
|
||||
def test_scenariokjoreren_som_mangler_sies_fra(lett_kopi: Path, hjem: Path, fjern: str) -> None:
|
||||
(lett_kopi / fjern).unlink()
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "så den er ikke kontrollert." in prosess.stdout
|
||||
assert "endret: Scenariokjøreren." in prosess.stdout
|
||||
|
||||
|
||||
def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
|
||||
|
|
@ -130,36 +222,41 @@ def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
|
|||
mappe = kopi / "modell" / "utvidelser"
|
||||
mappe.mkdir(exist_ok=True)
|
||||
for gammel in mappe.glob("*.py"):
|
||||
gammel.unlink()
|
||||
if gammel.name != "__init__.py":
|
||||
gammel.unlink()
|
||||
for navn, innhold in filer.items():
|
||||
(mappe / f"{navn}.py").write_text(innhold, encoding="utf-8")
|
||||
|
||||
|
||||
def test_utvidelse_som_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(kopi, {
|
||||
def test_utvidelse_som_gjenskaper_kjernen(lett_kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(lett_kopi, {
|
||||
"eksakt": "import sim\n\ndef grensetilfelle():\n return [('p3 med trappetrinn', 0.0)]\n",
|
||||
"_hjelper": "raise RuntimeError('filer som begynner med _ skal ikke kjøres')\n",
|
||||
})
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "5. Utvidelser: grensetilfellene gjenskaper kjernen eksakt (eksakt)." in prosess.stdout
|
||||
|
||||
|
||||
def test_utvidelser_som_ikke_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(kopi, {
|
||||
"avvik": "def grensetilfelle():\n return [('p3 med trappetrinn', 1e-12), ('p6', 0.0)]\n",
|
||||
def test_utvidelser_som_ikke_gjenskaper_kjernen(lett_kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(lett_kopi, {
|
||||
"avvik": "def grensetilfelle():\n return [('avvik: trapp lik notatets sjokk', 1e-12), ('p6', 0.0)]\n",
|
||||
"tom": "def grensetilfelle():\n return []\n",
|
||||
"odelagt": "import sim\nraise RuntimeError('i stykker')\n",
|
||||
})
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
(lett_kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "5. Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt (avvik, odelagt, tom; se under)." in (
|
||||
prosess.stdout)
|
||||
assert "avvik: p3 med trappetrinn: avvik 1,0e-12, skal være eksakt 0" in prosess.stdout
|
||||
assert ("5. Utvidelser: avvik, odelagt, tom gjenskaper ikke kjernen i grensetilfellet. Det er en feil i "
|
||||
"utvidelsen, og resultatene den gir, kan ikke brukes (se under).") in prosess.stdout
|
||||
assert " avvik: trapp lik notatets sjokk: avvik 1,0e-12, skal være eksakt 0" in prosess.stdout
|
||||
assert "avvik: avvik:" not in prosess.stdout
|
||||
assert "tom: grensetilfelle() ga ingen tilfeller" in prosess.stdout
|
||||
assert "odelagt: grensetilfelle() kunne ikke kjøres" in prosess.stdout
|
||||
assert "RuntimeError: i stykker" in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): endret: Utvidelser." in prosess.stdout
|
||||
assert "har en feil. Resultatene den gir, kan ikke brukes før den er rettet." in prosess.stdout
|
||||
assert "opplysning" not in prosess.stdout
|
||||
|
||||
|
||||
@pytest.mark.treg
|
||||
|
|
@ -167,4 +264,5 @@ def test_full_kontroll(hjem: Path) -> None:
|
|||
prosess = kjor("kontroll", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "4. Full kjøring: 22 av 22 datafiler like byte for byte med notatet; utskriften lik." in prosess.stdout
|
||||
assert "6. Scenariokjøreren: 14 av 14 av notatets datafiler gjenskapt byte for byte" in prosess.stdout
|
||||
assert "\nNivå: bit for bit som notatet.\n" in prosess.stdout
|
||||
|
|
|
|||
|
|
@ -302,3 +302,271 @@ def test_alle_eksempelscenarioer_er_gyldige() -> None:
|
|||
assert sc.kjoringer, fil.name
|
||||
if fil.parent == NYE:
|
||||
assert sc.beskrivelse, f"{fil.name} mangler beskrivelse"
|
||||
|
||||
|
||||
# ---------------- startlikevekten (gjennomgang E, funn 1) ----------------
|
||||
|
||||
def test_endret_grunnparameter_stopper_med_to_veier_ut(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_chain", "parametere": {"P": {"gamma": 0.15}}, "sjokk": {"faktor": 1.0}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
for t in ("startlikevekt: startpunktet er ikke en likevekt", "pådraget Omega0 (0,12)",
|
||||
"ville drevet bort fra 1,00 også uten sjokk", "passer ikke lenger til strømmen",
|
||||
'"startlikevekt": "kalibrer"', "(P.Omega0, CH0.Z, CH0.m)", '"startlikevekt": "ubalanse"'):
|
||||
assert t in err, err
|
||||
assert not (tmp_path / "ut").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("p", "modell"), [({"Lam0": 0.6}, "simulate"), ({"delta": 0.03}, "simulate"),
|
||||
({"T0": 1.2}, "simulate"), ({"eta": 2.0}, "simulate"),
|
||||
({"Omega0": 0.13}, "simulate"), ({"Kstar": 1.1}, "simulate"),
|
||||
({"Tprop": 0.9}, "simulate"), ({"Tmax": 0.9}, "simulate"),
|
||||
({"gamma": 0.1}, "simulate_soc"), ({"Lam0": 0.7}, "simulate_chain2")])
|
||||
def test_hver_grunnparameter_i_likevekten_sjekkes(p: dict[str, float], modell: str, tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": modell, "parametere": {"P": p}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert "startpunktet er ikke en likevekt" in err, err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("modell", "par"), [("simulate", {"P": {"ksat": 1.5}}),
|
||||
("simulate_soc", {"SOC": {"eps": 0.5}}),
|
||||
("simulate_chain", {"CH0": {"Z": 0.12}}),
|
||||
("simulate_chain", {"CH0": {"varsigma": 0.02}})])
|
||||
def test_parametere_kjernen_selv_rekalibrerer_stopper_ikke(
|
||||
modell: str, par: dict[str, object], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": modell, "parametere": par})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0, err
|
||||
|
||||
|
||||
def test_kalibrer_gir_ro_for_sjokket(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"navn": "k", "modell": "simulate", "startlikevekt": "kalibrer",
|
||||
"parametere": {"P": {"Lam0": 0.6, "gamma": 0.2}}, "sjokk": {"faktor": 1.0}})
|
||||
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0, err
|
||||
assert "Referansetilstanden er kalibrert på nytt" in ut
|
||||
assert "P.Omega0 0,14 (var 0,12)" in ut
|
||||
assert "tilliten er 0,60 der" in ut
|
||||
rader = (tmp_path / "ut" / "k.csv").read_text(encoding="utf-8").splitlines()[1:]
|
||||
assert all(abs(float(r.split(",")[1]) - 1.0) < 1e-9 for r in rader) # K står i ro
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
kalibrert = data["kjoringer"][0]["startlikevekt"]["kalibrert"]
|
||||
assert kalibrert["P.Omega0"]["for"] == 0.12
|
||||
assert data["kjoringer"][0]["brukt"]["parametere"]["P"]["Omega0"] == kalibrert["P.Omega0"]["etter"]
|
||||
|
||||
|
||||
def test_kalibrer_kjeden_regner_om_z_og_m(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "kalibrer",
|
||||
"parametere": {"P": {"gamma": 0.15}}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0, err
|
||||
brukt = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))["kjoringer"][0]["brukt"]
|
||||
j0 = 0.15 * 0.8 * 1.0
|
||||
assert brukt["parametere"]["CH0"]["Z"] == j0 / 0.95 # sim.py:151
|
||||
assert brukt["parametere"]["CH0"]["m"] == 3.0 - j0 / (j0 / 0.95 * (1 / 3.0 + 0.01)) # sim.py:155
|
||||
assert brukt["parametere"]["P"]["Omega0"] == pytest.approx(j0 + 0.02, abs=1e-15)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("p", [{"Kstar": 1.1}, {"Omega0": 0.13}, {"Tprop": 0.9}])
|
||||
def test_kalibrer_retter_ikke_det_som_er_satt_eller_ligger_fast(
|
||||
p: dict[str, float], tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate", "startlikevekt": "kalibrer", "parametere": {"P": p}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert '"kalibrer" kan ikke gjøre starten til en likevekt' in err
|
||||
|
||||
|
||||
def test_ubalanse_kjorer_og_viser_driften(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate", "startlikevekt": "ubalanse", "parametere": {"P": {"Lam0": 0.6}},
|
||||
"sjokk": {"faktor": 1.0}})
|
||||
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0, err
|
||||
assert "1,00 er nivået i referanselikevekten" not in ut
|
||||
assert 'startpunktet er ikke en likevekt ("startlikevekt": "ubalanse"): pådraget Omega0' in ut
|
||||
assert "Fram til måned 12 har K gått fra 1,000 til 1,0" in ut
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
assert data["kjoringer"][0]["startlikevekt"]["ubalanse"]
|
||||
|
||||
|
||||
def test_ubalanse_med_sjokk_viser_driften_til_sjokkmaaneden(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "ubalanse",
|
||||
"parametere": {"P": {"gamma": 0.15}}, "sjokk": {"fra": 18}})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "Fram til måned 18 har K gått fra 1,000 til" in ut
|
||||
assert "saksbehandlingstiden (mnd) fra 15,7 til" in ut
|
||||
|
||||
|
||||
@pytest.mark.parametrize("valg", ["krev", "kalibrer", "ubalanse"])
|
||||
def test_simulate_endo_med_annen_strom_ved_start_stopper_alltid(
|
||||
valg: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_endo", "regel": "koord", "startlikevekt": valg,
|
||||
"parametere": {"P": {"gamma": 0.11}}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert "sim.py:588" in err
|
||||
assert "endring i kjernen" in err
|
||||
|
||||
|
||||
def test_eta_i_kjeden_kan_ikke_kalibreres(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_chain", "startlikevekt": "kalibrer", "parametere": {"P": {"eta": 2.0}}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert "uten eta" in err
|
||||
|
||||
|
||||
# ---------------- funn 2, 7, 8, 12, 13, 14 og kapasitet i simulate_chain2 ----------------
|
||||
|
||||
def test_kjoring_som_eksploderer_stopper_for_noe_skrives(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "utvidelse:paadrag_tidsserie",
|
||||
"utvidelse": {"punkter": [[12, 1e308], [13, 1e308]]}})
|
||||
kode, ut, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert ut == ""
|
||||
assert "ikke er endelige tall" in err
|
||||
assert "Ingenting er skrevet" in err
|
||||
assert not (tmp_path / "ut").exists()
|
||||
|
||||
|
||||
def test_absurd_k_gir_merknad(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate", "sjokk": {"faktor": 1e6}})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "## Merknader (kjøringen er ikke stoppet)" in ut
|
||||
assert "utenfor området kjernen selv regner på" in ut
|
||||
|
||||
|
||||
def test_notatets_vippekjoring_gir_ingen_merknad(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert kjor_fil(NOTATET / "vippe.json", tmp_path / "ut", capsys)[1].count("Merknader") == 0
|
||||
|
||||
|
||||
def test_mappe_som_ikke_kan_skrives(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate"})
|
||||
laast = tmp_path / "laast"
|
||||
laast.mkdir()
|
||||
laast.chmod(0o500)
|
||||
try:
|
||||
kode, _, err = kjor_fil(fil, laast / "ut", capsys)
|
||||
finally:
|
||||
laast.chmod(0o700)
|
||||
assert kode == 2
|
||||
assert err.startswith(f"Kan ikke skrive i {laast / 'ut'}: ")
|
||||
assert err.rstrip().endswith("Velg en annen mappe med --ut.")
|
||||
assert "Traceback" not in err
|
||||
|
||||
|
||||
def test_koen_har_enhet(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, ut, _ = kjor_fil(NOTATET / "kjede_s2.json", tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "Kø i domstolen ved slutt (mnd innstrømning før sjokket)" in ut
|
||||
assert "Køen er målt i måneder av innstrømningen til domstolen før sjokket" in ut
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
assert data["kjoringer"][0]["nokkeltall"]["Q_slutt_mnd"] == pytest.approx(56.4, abs=0.05) # Qend 5,64 / 0,1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("periode", "tekst"), [(0.07, "minst 1"), (1.5, "helt antall måneder")])
|
||||
def test_budsjettsyklus_er_hele_maaneder(periode: float, tekst: str, tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_endo", "regel": "koord", "parametere": {"ENDO": {"period": periode}}})
|
||||
kode, _, err = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert "parametere.ENDO.period" in err
|
||||
assert tekst in err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("data", "tekst"), [
|
||||
({"modell": "simulate", "slutt": 0.04, "styring": {"forsinkelse": 0}}, "slutt: må være et tall minst 1"),
|
||||
({"modell": "simulate", "slutt": 2}, "slutt: horisonten (2 mnd) er kortere enn forsinkelsen"),
|
||||
({"modell": "simulate", "sveip": {"parameter": "slutt", "verdier": [2]}}, "slutt: horisonten (2 mnd)"),
|
||||
])
|
||||
def test_for_kort_horisont_navngir_slutt(data: dict[str, object], tekst: str, tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, _, err = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
|
||||
assert kode == 2
|
||||
assert tekst in err, err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("data", "tekst"), [
|
||||
({"modell": "simulate_chain", "slutt": 6}, "Sjokket kommer i måned 12, ikke før slutt (6)"),
|
||||
({"modell": "simulate_soc", "slutt": 30}, "Sjokket varer til måned 36, etter slutt (30)"),
|
||||
])
|
||||
def test_sjokk_utenfor_horisonten_gir_merknad(data: dict[str, object], tekst: str, tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, ut, _ = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert tekst in ut
|
||||
|
||||
|
||||
@pytest.mark.parametrize("innhold", [{"kjoringer": [{"fil": "viktig.docx"}]},
|
||||
{"scenario": "x", "scenariofil_sha256": "", "kjerne_sha256": {},
|
||||
"kjoringer": [{"fil": "viktig.docx"}, {"fil": "../utenfor.csv"}]}])
|
||||
def test_sletter_aldri_andres_filer(innhold: dict[str, object], tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
ut = tmp_path / "ut"
|
||||
ut.mkdir()
|
||||
(ut / "resultat.json").write_text(json.dumps(innhold), encoding="utf-8")
|
||||
(ut / "viktig.docx").write_text("x", encoding="utf-8")
|
||||
(tmp_path / "utenfor.csv").write_text("x", encoding="utf-8")
|
||||
kjor_fil(skriv(tmp_path, {"navn": "n", "modell": "simulate"}), ut, capsys)
|
||||
assert (ut / "viktig.docx").is_file()
|
||||
assert (tmp_path / "utenfor.csv").is_file()
|
||||
|
||||
|
||||
def test_kapasitet_i_simulate_chain2_vises(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
fil = skriv(tmp_path, {"modell": "simulate_chain2", "tiltak": {"kapasitet": {"faktor": 1.34},
|
||||
"andel_utenom": {"andel": 0.2}}})
|
||||
kode, ut, _ = kjor_fil(fil, tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
tall = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))["kjoringer"][0]["nokkeltall"]
|
||||
assert tall["Z_slutt"] == pytest.approx(1.34)
|
||||
assert tall["utenom_slutt_pst"] == pytest.approx(20.0)
|
||||
assert "Kapasitet, ved slutt (1,00 = ved start)" in ut
|
||||
|
||||
|
||||
def test_bruk_nevner_riktig_python() -> None:
|
||||
assert scenario.BRUK.startswith(f"Bruk: {scenario.PYTHON} modell/kjor.py scenario")
|
||||
|
||||
|
||||
# ---------------- modeller med kø driver sakte før sjokket ----------------
|
||||
|
||||
def test_modell_med_ko_viser_driften_for_sjokket(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, ut, _ = kjor_fil(NOTATET / "kjede_s2.json", tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "1,00 er nivået ved start for kriminaliteten K og politiets innsats T (notatets referansetilstand)" in ut
|
||||
assert "nesten, men ikke helt, en likevekt" in ut
|
||||
assert "referanselikevekten" not in ut
|
||||
assert "i ro" not in ut
|
||||
assert "Fram til måned 12 har K gått fra 1,000 til 1,007, innsatsen T fra 1,00 til 1,02" in ut
|
||||
|
||||
|
||||
def test_modell_med_ko_uten_sjokk_sier_at_den_kryper(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, ut, _ = kjor_fil(NYE / "uten_sjokk_kjede.json", tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "Fram til måned" not in ut
|
||||
assert 'kryper modellen med kø sakte oppover av seg selv; en kjøring uten sjokk ("sjokk": {"faktor": 1})' in ut
|
||||
data = json.loads((tmp_path / "ut" / "resultat.json").read_text(encoding="utf-8"))
|
||||
tall = data["kjoringer"][0]["nokkeltall"]
|
||||
assert round(tall["K_slutt"], 3) == 1.018
|
||||
assert round(tall["tauD_slutt"], 1) == 4.6
|
||||
|
||||
|
||||
@pytest.mark.parametrize("modell", ["simulate_chain2", "simulate_endo", "utvidelse:paadrag_tidsserie"])
|
||||
def test_alle_modeller_med_ko_far_driftlinjen(modell: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
data: dict[str, object] = {"modell": modell}
|
||||
if modell == "simulate_endo":
|
||||
data["regel"] = "koord"
|
||||
kode, ut, _ = kjor_fil(skriv(tmp_path, data), tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "nesten, men ikke helt, en likevekt" in ut
|
||||
assert "Fram til måned 12 har K gått fra 1,000 til" in ut
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fil", ["p3", "vippe"])
|
||||
def test_modell_uten_ko_har_ingen_driftlinje(fil: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
kode, ut, _ = kjor_fil(NOTATET / f"{fil}.json", tmp_path / "ut", capsys)
|
||||
assert kode == 0
|
||||
assert "Slik leses tallene: 1,00 er nivået i referanselikevekten før sjokket" in ut
|
||||
assert "Fram til måned" not in ut
|
||||
assert "nesten, men ikke helt" not in ut
|
||||
|
|
|
|||
|
|
@ -156,3 +156,42 @@ def test_malen_kjorer_med_scenariofil(modell_med_mal: Path, tmp_path: Path) -> N
|
|||
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()
|
||||
|
||||
|
||||
# ---------------- uavhengig av utvidelsens egen beregning: gjennom kjøreren mot fasit ----------------
|
||||
|
||||
NOTATFILER = [(navn, fil) for navn in UTVIDELSER
|
||||
for fil in getattr(scenario.last_utvidelse(navn), "GJENSKAPER_NOTATET", ())]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("navn", UTVIDELSER)
|
||||
def test_utvidelsen_oppgir_notatfiler(navn: str) -> None:
|
||||
filer = getattr(scenario.last_utvidelse(navn), "GJENSKAPER_NOTATET", None)
|
||||
if filer is None:
|
||||
pytest.skip(f"{navn} oppgir ikke GJENSKAPER_NOTATET; grensetilfellet er bare sjekket med egen beregning")
|
||||
assert filer, f"{navn}: GJENSKAPER_NOTATET er tom"
|
||||
for fil in filer:
|
||||
assert (ROT / "eksempler" / "scenarioer" / "notatet" / f"{fil}.json").is_file(), fil
|
||||
assert (FASIT / "data" / f"{fil}.dat").is_file(), fil
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("navn", "fil"), NOTATFILER)
|
||||
def test_standardverdiene_gjenskaper_notatfilene_gjennom_kjoreren(
|
||||
navn: str, fil: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
data = json.loads((ROT / "eksempler" / "scenarioer" / "notatet" / f"{fil}.json").read_text(encoding="utf-8"))
|
||||
data["modell"] = f"utvidelse:{navn}"
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
assert (tmp_path / "ut" / f"{fil}.dat").read_bytes() == (FASIT / "data" / f"{fil}.dat").read_bytes()
|
||||
|
||||
|
||||
def test_utvidelsen_stoppes_ved_ubalanse_og_kan_kalibreres(tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
data: dict[str, object] = {"navn": "u", "modell": "utvidelse:paadrag_tidsserie",
|
||||
"parametere": {"P": {"gamma": 0.15}}}
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 2
|
||||
assert "startpunktet er ikke en likevekt" in err
|
||||
data["startlikevekt"] = "kalibrer"
|
||||
kode, err = kjor_fil(data, tmp_path, capsys)
|
||||
assert kode == 0, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue