Batchrun draaien

Een reeks zichtjaren voor meerdere varianten duurt uren en past niet in een GUI-sessie. Daarvoor draait het model in batch, met GeoDmsRun.exe in plaats van de GUI en met een PowerShell-script dat de stappen op volgorde zet, elke stap toetst en een spoor achterlaat waarmee je achteraf kunt zien wat er is gebeurd.

Deze pagina beschrijft hoe je zo’n reeks start, ook op een machine waar het nog niet eerder is gedaan. Voor het draaien vanuit de GUI, zie Installatie tutorial.

Welk script

In batch/ staan meerdere scripts. Ze zijn niet gelijkwaardig.

Script Waarvoor
Run2120.ps1 de reeks zelf: basisdata, variantdata en de allocatie per zichtjaar, met het diagnoseharnas achter de gekozen zichtjaren
RunIndicatoren.ps1 de indicatorenexport, leest de stand uit de tifs en hoeft dus niet te alloceren
ToetsOplevering.ps1 beoordeelt na afloop of de uitkomst opleverbaar is, zonder opnieuw te rekenen
MaakOplevering.ps1 stelt uit de ruwe uitvoer een opleveringsmap samen
RunAll.cmd en de andere .cmd-scripts een enkele variant in een proces, met de stand in het geheugen; niet geschikt voor een reeks

De .cmd-scripts missen drie dingen die de PowerShell-scripts wel hebben. Ze weten niet dat een variant de stand van een andere variant kan lenen, dus ze alloceren zo’n variant alsnog en dat kost uren voor een uitkomst die identiek wordt aan die van de uitlener. Ze toetsen alleen de exitcode en niet het staplog, en ze schrijven alle stappen in een gedeeld logbestand. En ze zetten LocalDataProjDir op een vast pad, wat leest alsof het de uitvoer stuurt terwijl die variabele wordt genegeerd: de werkelijke uitvoermap volgt uit de mapnaam boven cfg. Gebruik ze niet voor een productiereeks.

De naam Run2120 komt uit de NL2120-toepassing, maar het script is daar alleen in zijn standaardwaarden aan gebonden. Scenario, varianten, zichtjaren en paden zijn allemaal parameters.

Wat er op de machine moet staan

GeoDMS

Dezelfde build als waarop de rest van de reeks draait, geinstalleerd onder C:\Program Files\ObjectVision. Niet een build uit Visual Studio: die wordt opnieuw gecompileerd zonder dat de configuratie verandert, dus een run kan halverwege van engine wisselen en dan is een verschil in uitkomst niet meer toe te wijzen.

De configuratie bewaakt zelf een ondergrens. In cfg/main.dms staat een IntegrityCheck die GeoDMSVersion() >= 17.0904 eist, dus alles onder 17.9.4 valt meteen om met een melding die zegt dat je een nieuwere versie nodig hebt. Dat is de ondergrens en niet het advies: neem de build die als standaardwaarde van -Exe in de scripts staat, want daarop draaien de productieruns.

Draai je een reeks op twee machines tegelijk, controleer dan dat beide op dezelfde build staan. Dat is de eerste vraag die je achteraf niet meer kunt beantwoorden.

De werkkopie

Een clone van de repo, op de branch van de toepassing. De naam van de map boven cfg is niet vrij: GeoDMS leidt %LocalDataProjDir% af uit LocalDataDir plus die mapnaam, dus een werkkopie in C:\ProjDir\RSopen_NL2120_productie schrijft naar C:\LocalData\RSopen_NL2120_productie. Kies dezelfde naam als op de andere machine, anders komen de ontkoppelde bestanden en de standtifs ergens anders terecht en begint de machine van voren af aan.

De brondata en de instellingen

GeoDMS bewaart LocalDataDir en SourceDataDir per gebruiker en per machine in het register, onder HKCU:\SOFTWARE\ObjectVision\<MACHINENAAM>\GeoDMS. Zet ze via de GUI, Tools en dan Options; GeoDmsRun leest dezelfde sleutel, dus de batchrun neemt ze vanzelf over. Nakijken kan zo:

Get-ItemProperty "HKCU:\SOFTWARE\ObjectVision\$env:COMPUTERNAME\GeoDMS" |
  Select-Object LocalDataDir, SourceDataDir

Staat de sleutel er niet, dan is de GUI op die machine nog nooit met een configuratie gestart. Doe dat eerst, want zonder deze twee paden zoekt de run de brondata op een standaardplek.

Ruimte en geheugen

De brondata is de grootste post. Daarnaast schrijft het model zijn ontkoppelde bestanden, standtifs, ketentifs en indicatoren in LocalData: voor een volle reeks met drie varianten tot 2120 is dat orde vijftig gigabyte. Reken op minstens 64 GB werkgeheugen; de machine waarop de NL2120-productieruns draaien heeft er 128.

PowerShell 7

Zie de valkuil verderop. Kort: Run2120.ps1 draait wel onder Windows PowerShell 5.1, maar handelt daar een gefaalde stap verkeerd af.

De reeks starten

Alle padparameters hebben een standaardwaarde die naar de productiemachine wijst. Op een andere machine kloppen die niet, dus geef ze expliciet mee:

pwsh -NoProfile -File C:\ProjDir\RSopen_NL2120_productie\batch\Run2120.ps1 `
  -Exe       'C:\Program Files\ObjectVision\GeoDms20.17.0.m\GeoDmsRun.exe' `
  -Cfg       'C:\ProjDir\RSopen_NL2120_productie\cfg\main.dms' `
  -LocalData 'C:\LocalData\RSopen_NL2120_productie' `
  -LogDir    'C:\ProjDir\RSopen_NL2120_productie\batch\log\run2120' `
  -Scenario  'WLO_hoog' `
  -Varianten BAU,BAU2,NbSGenuanceerd `
  -DiagnoseNaZichtjaar Y2040,Y2120

Scenario en varianten in dit voorbeeld horen bij de NL2120-toepassing; zie Toepassing: NL2120 voor wat die namen betekenen.

Twee parameters verdienen toelichting.

-LocalData stuurt niet waar GeoDMS schrijft. Dat volgt uit de mapnaam hierboven en is niet met een omgevingsvariabele te verleggen: LocalDataProjDir als omgevingsvariabele wordt genegeerd, en het script wist die variabele dan ook bij de start. Het script gebruikt -LocalData alleen om te kijken of de ontkoppelde invoer er staat, of er al standen van deze reeks staan, en voor de dictionary-controle. Geef je er een andere map aan dan waar GeoDMS werkelijk schrijft, dan kijken die controles naar de verkeerde plek en melden ze niets.

-DiagnoseNaZichtjaar laat het diagnoseharnas meedraaien na de genoemde zichtjaren, en daarnaast een keer per variant de basisjaarcontroles. Dat kost ongeveer acht minuten per zichtjaar per variant en het is de moeite waard: wat er tijdens de run niet is weggeschreven, kun je achteraf niet beoordelen zonder opnieuw te rekenen. Het eerste zichtjaar vangt een systematische fout na een uur in plaats van na zeven, het laatste is het zichtjaar dat wordt opgeleverd. Een omgevallen diagnosestap stopt de reeks niet: hij komt als mislukt in status.tsv en het volgende zichtjaar gaat door, want de allocatie hangt van de stand af en niet van de meting.

De zichtjaren zelf hoef je niet op te geven. Het script haalt ze uit de configuratie, zodat de lijst niet uit de pas kan lopen met Model_FirstZichtjaar en Model_FinalYear.

Wat het script controleert voordat het rekent

Vijf toetsen, allemaal bedoeld om een reeks van uren niet op een verkeerde aanname te laten beginnen.

De schakelaar ModelParameters/OntkoppelStedelijkeKlasses wordt naast de bedoeling van de aanroeper gelegd. Staat die op TRUE, dan alloceert alleen de landbouw en nemen de stedelijke sectoren hun ingangsstand over. Dat is een geldige route, maar niets in het log zegt het, dus het script eist dat je die bedoeling met -AlleenLandbouw bevestigt.

Leent een variant zijn stand van een andere, dan wordt de aanname daaronder omgekeerd getoetst: de kolommen waarop de twee varianten verschillen moeten precies de bekende zijn, en de sector landbouw mag niet alloceren. Komt er ooit een afwijkende kolom bij, dan stopt de reeks in plaats van stil een verkeerde stand te lenen. De uitlener moet wel in dezelfde aanroep meedraaien.

Staan er al standen van deze reeks, dan weigert het script de basisdata of de variantdata opnieuw weg te schrijven. Verse invoer halverwege een reeks laat de vroege en de late zichtjaren met verschillende gegevens rekenen, en dat is achteraf niet aan de uitkomst te zien. Ververs die bestanden dus tussen twee complete reeksen door en nooit ertussenin. Bewust opnieuw beginnen kan met -HerbouwBasedata.

Voordat de allocatie begint, en bij -SkipBasedata ook al voor de variantdata, kijkt het script of de ontkoppelde invoer in LocalData staat: een representatief bestand per stap, zoals de pandtypering, de verwervingskosten, de stand van het basisjaar en per variant de opbrengsten per pakket. Ontbreekt er iets, dan zegt het script welk item onder WriteBasedata of WriteVariantData het maakt, in plaats van na een uur rekenen om te vallen op een onbekende identifier.

Na de basisdata worden de dictionarybestanden gecontroleerd op relatieve paden. Een vers geschreven 0Dictionary.dms kan een relatief pad krijgen en is dan bij het teruglezen onvindbaar.

Bij de start toont het script daarnaast de schakelaars uit de configuratie waar de run van afhangt zonder ze zelf te zetten, zoals AlleenEindjaar, Model_FirstZichtjaar en Model_FinalYear, zodat in het schermlog staat waarmee is gerekend.

Meekijken en herstarten

Elke stap krijgt een eigen logbestand met tijdstempel in de logmap, plus een regel in status.tsv met tijd, stapnaam, itempad, exitcode en het aantal seconden.

Get-Content C:\ProjDir\RSopen_NL2120_productie\batch\log\run2120\status.tsv -Tail 10 -Wait

De exitcode alleen is geen bewijs. Een ontbrekend bronbestand komt als GDAL-fout in het log terwijl de exitcode 0 blijft, en de gevraagde waarde komt dan leeg terug. Het script toetst daarom ook op regels met [E] in het staplog, en meldt een stap als mislukt zodra een van beide iets zegt. Draai je een stap met de hand, doe dat dan ook.

Het decimaalteken in status.tsv volgt de landinstelling van de shell die de reeks startte. In een bestand dat over meerdere runs is volgeschreven kunnen dus komma’s en punten door elkaar staan. Lees het niet met een vast scheidingsteken uit.

Stopt de reeks halverwege, dan pak je hem op met de stapnaam uit status.tsv:

... -StartBij allocatie-BAU-Y2070 -SkipBasedata -SkipVariantData

De stapnamen zijn basedata-allocatie en basedata-indicatoren, variantdata-<variant>-run1 en -run2, diagnose-<variant>-basisjaar, allocatie-<variant>-<zichtjaar> en diagnose-<variant>-<zichtjaar>. Alles voor de genoemde stap wordt overgeslagen.

Rekentijd

Ter indicatie de tijden van een productiereeks van de NL2120-toepassing, negen zichtjaren van 2040 tot en met 2120, op de machine met 128 GB hierboven. Ze horen bij dat project en die machine en niet bij de scripts.

Stap Tijd
basisdata, eenmalig 8 minuten
variantdata, per variant 5,5 minuten
allocatie, per zichtjaar tot en met 2060 43 tot 52 minuten
allocatie, per zichtjaar vanaf 2070 11 tot 21 minuten
allocatie, hele reeks per allocerende variant 3,5 tot 4,5 uur
indicatorenexport in een proces (-Ontkoppeld:$false), per variant 44 tot 70 minuten
indicatorenexport ontkoppeld: de reeks van acht zichtjaren voor het exportzichtjaar, per variant 68 minuten, eenmalig per allocatie
indicatorenexport ontkoppeld: het exportzichtjaar zelf, per variant en per herhaling 18 minuten

De late zichtjaren zijn sneller doordat ze met een verkorte allocatieopzet rekenen; die staat in ModelParameters/Skeleton en geldt vanaf het zichtjaar dat daar is ingesteld, in deze reeks 2070. De spreiding komt verder vooral van wat er nog meer op de machine draait en niet van de vordering van de reeks: op een drukke avond loopt een zichtjaar op tot het dubbele van wat het op een lege machine kost. Een variant die zijn stand leent, kost niets.

Na de reeks

De indicatorenexport draait los van de allocatie, want die leest de stand uit de tifs:

pwsh -NoProfile -File ...\batch\RunIndicatoren.ps1 -Varianten BAU,BAU2 -Zichtjaren Y2120 -IndicatorRegio Landschappen

Voordat het script rekent toetst het of alle invoer er staat: de stand van elk zichtjaar tot en met het exportzichtjaar, de basisdata voor de indicatoren en de variantdata. Ontbreekt er iets, dan zegt het welke stap dat maakt.

Landschappen draait de vier aangeleverde landschapsgebieden van #760 na elkaar, een proces per gebied; de eerste draagt de volledige export, de drie volgende alleen hun tabel, want de kaarten, de landelijke tabel en de claimrealisatie hangen niet van de indeling af. De andere waarden van -IndicatorRegio zijn NL, Provincie, COROP, Gemeente, NVM en elk van de vier Landschap_<naam> los.

De ketens tussen de zichtjaren lopen standaard ontkoppeld (#824). De cumulatieve indicatoren (contante waarden per periode, koolstof, methaan, sterfte, SOMERS) lezen het vorige zichtjaar; in een proces, met -Ontkoppeld:$false, rekent een export van 2120 daarom alle zichtjaren vanaf 2040 mee, en elke tabel die je opnieuw wilt is dan weer drie kwartier. Ontkoppeld draait het script per variant eerst de zichtjaren voor het exportzichtjaar, elk in een eigen proces: het item Zichtjaren/<jaar>/Tijdreeks schrijft de grootheden die het volgende zichtjaar nodig heeft als tif in Indicatoren/<casus>/Ketens/ (de lijst staat in Templates/Indicatoren/Ketens.dms, enkele gigabytes per variant), plus de kaarten van de tijdreeks. De export van het exportzichtjaar leest dan alleen de ketentifs van zijn voorganger. Voor en na elk zichtjaar toetst het script of de ketentifs er staan, want een ontbrekende ketentif geeft exit 0 met alleen een GDAL-waarschuwing.

De export van het exportzichtjaar gaat zelf ook in twee processen: eerst alle kaarten, daarna de tabellen, die de geschreven kaarten teruglezen in plaats van de indicatoren opnieuw te rekenen (ModelParameters/TabellenUitExport, #831). Een lezer kent zijn schrijver niet, dus in een proces zou een tabel een tif kunnen openen voordat hij geschreven is; vandaar de twee processen. Zo kost een tabel per indeling geen volledige export meer. Uit met -TabellenUitExport:$false; dat is ook nodig voor een tabel op een export waarin niet alle kaarten zijn geschreven.

De reeks hoort opnieuw zodra de allocatie opnieuw is gedaan. Er is met opzet geen fingerprint: een bestaande ketentif wordt gelezen, en het script waarschuwt alleen als een ketentif ouder is dan de stand van dat zichtjaar. Voor een herhaling van het exportzichtjaar alleen, bijvoorbeeld een tabel na een reparatie, is er -AlleenExportZichtjaar; -AlleenLandschapstabellen doet alleen de regionale tabel per gebied, op de kaarten van een eerdere export van hetzelfde zichtjaar.

Daarna beoordelen met ToetsOplevering.ps1, dat de diagnose-uitdraaien van de run leest en per toets PASS, FAIL of GEEN DATA meldt. GEEN DATA is nadrukkelijk geen PASS. Geef met -RunGestartOp het starttijdstip van de run mee, anders telt een uitdraai van een eerdere run gewoon mee. Samenstellen van de opleveringsmap gaat met MaakOplevering.ps1.

Zet ten slotte een tag op de commit die heeft gedraaid. Een reeks beslaat uren en loopt vaak op meerdere machines tegelijk, terwijl de branch ondertussen doorloopt; zonder tag is achteraf niet meer vast te stellen welke code een uitdraai heeft gemaakt. Draaiden er meerdere machines, zet dan in de tagboodschap welke variant waar draaide, en gebruik git rev-parse HEAD op elke machine als bewijs.

Waar het misgaat

Windows PowerShell 5.1 in plaats van PowerShell 7

Windows brengt van huis uit powershell.exe mee, versie 5.1. PowerShell 7 is een aparte installatie en heet pwsh.exe. Welke je hebt:

$PSVersionTable.PSVersion

De scripts in batch/ worden door beide versies foutloos ingelezen, dus je merkt het verschil niet aan de start. Het verschil zit in wat er gebeurt als een stap faalt. De scripts draaien met $ErrorActionPreference op Stop en vangen de uitvoer van GeoDmsRun op met 2>&1. Onder 5.1 maakt PowerShell van elke regel die een programma naar de foutuitvoer schrijft een afbrekende fout, onder 7 niet. En GeoDmsRun schrijft daar wel degelijk naartoe: een mislukte aanroep meldt op de foutuitvoer welke configuratie hij niet kon lezen.

Onder 5.1 klapt het script daardoor uit met een kale uitzondering op precies het moment dat het zijn werk zou gaan doen. Je krijgt de regel MISLUKT niet, niet het pad naar het staplog, niet de eerste twintig foutregels, en er komt ook geen regel in status.tsv. Een stap die als niet-fataal is bedoeld, zoals de diagnose, neemt onder 5.1 de hele reeks mee in zijn val.

Installeer dus PowerShell 7:

winget install --id Microsoft.PowerShell

Werkt winget niet op de machine, dan staat er een MSI-installatie op de releasepagina van PowerShell op GitHub, en er is een zip-versie die je zonder beheerrechten kunt uitpakken en direct aanroepen.

Kan het echt niet, dan moet het script mee. Schermt de aanroep zelf af met een eigen voorkeur, dan gedragen 5.1 en 7 zich hetzelfde en blijft de exitcode bewaard:

$oud = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
& $Exe "/L$log" '/S1' '/S2' '/S3' $Cfg $Item 2>&1 | Out-Null
$code = $LASTEXITCODE
$ErrorActionPreference = $oud

Loopt het aanroepen van een .ps1 stuk op het uitvoerbeleid van de machine, start dan met -ExecutionPolicy Bypass erbij. Gebruik -NoProfile sowieso, zodat een profiel op die machine de run niet stilletjes anders instelt.

De brondata staat op een andere plek dan op de buurmachine

In cfg/main/ConfigSettings.dms staan twee paden dubbel, met een machinenaam als commentaar erachter en telkens een van de twee uitgeschakeld. De reden is dat de bronmap op de ene machine een niveau dieper hangt dan op de andere. Voor je begint controleer je dus of de actieve regel op jouw machine naar een bestaande map wijst.

Een omgevingsvariabele met dezelfde naam helpt hier niet: zet je RSo_DataDir als omgevingsvariabele, dan wint de waarde in de configuratie. Het is dus of de mappenstructuur gelijktrekken, of de regel in de configuratie omzetten. Dat bestand staat in .gitignore, want het is een machine-instelling en geen modelwijziging; elke machine heeft zijn eigen exemplaar.

De brondata is een synchronisatiemap

De bronmap synchroniseert met een cloudmap in de virtuele-bestandenmodus van Windows. Twee gevolgen.

Een bestand dat nog een placeholder is, wordt bij het eerste gebruik opgehaald, en de run wacht daarop. Bij honderdduizenden bestanden gaat elke opening door het cloudfilter, en een run die tientallen bestanden per seconde opent verzadigt de synchronisatieclient. Pauzeer de synchronisatie voor een zware run.

De map is bovendien schrijf-eenmaal: een eenmaal geplaatst bestand mag niet worden gewijzigd of verwijderd, en overschrijven levert een conflictkopie op. Elk bronbestand draagt daarom een datum in de naam en het volledige pad staat als parameter in ModelParameters. Loopt een run vast op cannot open dataset, zoek het bestand dan eerst met een recursieve zoekopdracht op een deel van de naam voordat je in de configuratie gaat zoeken. Het staat vaker ergens anders dan dat het echt weg is, en de vervolgfouten noemen namen die niets met de bron te maken hebben.

Twee runs op dezelfde LocalData

Draai geen stap die ontkoppelde bestanden genereert zolang er een ander GeoDMS-proces op dezelfde %LocalDataProjDir% actief is, ook geen open GUI. De generator verwijdert het doelbestand en schrijft het opnieuw; houdt een ander proces het open, dan blijft er een stub achter en falen latere runs op een IntegrityCheck van een heel ander item. Andersom valt een run die zo’n bestand alleen leest om met een melding over een klassetabel, die naar de configuratie wijst terwijl er niets mis is.

Kijken wat er draait:

Get-CimInstance Win32_Process -Filter "Name='GeoDmsRun.exe'" | Select-Object ProcessId, CommandLine

Ruim daarbij nooit op procesnaam op. Get-Process -Name GeoDmsRun | Stop-Process raakt elk GeoDmsRun-proces op de machine, ongeacht welke werkkopie eronder draait, en op een gedeelde machine kost dat een ander uren. Filter op het volledige configuratiepad tot cfg, of gebruik het procesnummer.

Let er ook op dat een lege processenlijst niet betekent dat de machine vrij is. Tussen twee stappen zit een gat van enkele seconden waarin er niets draait. Kijk naar de wijzigingstijd van het logbestand van de ander, of vraag het.

De werkkopie verversen tijdens een run

Doe dat niet. Elke stap laadt de configuratie opnieuw bij het starten, dus een pull halverwege laat opeenvolgende zichtjaren of varianten op verschillende code draaien zonder dat iets dat meldt. Moet het toch, bepaal dan eerst of de gewijzigde items boven of onder de ontkoppellijn liggen: boven betekent de ontkoppelde bestanden opnieuw maken en de allocatie over.

De sessie afsluiten

Een reeks van uren draait in de sessie waarin je hem hebt gestart. Werk je via een extern bureaublad, koppel de sessie dan los en meld je niet af: afmelden beeindigt de processen. Wil je er helemaal niet bij hoeven blijven, start de reeks dan als geplande taak.

Een sector aan- of uitzetten vlak voor een run

De bestandsnaam van de standtifs bevat het aantal subsectoren van de actieve sectoren. Zet je een hele sector aan of uit in ModelParameters/SectorAllocRegio, dan schuift dat getal en is elke bestaande allocatie in een klap onbereikbaar, ook voor de indicatorenkant. Dat is dus geen instelling die je vlak voor een levering nog even meeneemt.

Verantwoording

De werkwijze hierboven is gevormd in de volgende issues; daar staan de metingen en de afwegingen.

  • #705: een landsdekkende partitie in landschappen als indeling voor de regionale tabel; de vier aangeleverde gebieden van #760 zijn er de opvolger van.
  • #724: het diagnoseharnas draait mee achter de gekozen zichtjaren, omdat wat tijdens de run niet is weggeschreven achteraf niet te beoordelen is zonder opnieuw te rekenen.
  • #760: de vier aangeleverde, overlappende landschapsgebieden als indeling voor de regionale tabel, een proces per gebied.
  • #816: de toets vooraf op ontbrekende ontkoppelde invoer, met de stap die haar maakt.
  • #824: de ketens tussen de zichtjaren via tifs, een proces per zichtjaar, met het bewijs dat de export bit voor bit gelijk blijft aan die in een proces.
  • #831: de tabellen lezen de geschreven kaarten terug, in een eigen proces na de kaarten.