Repository navigation
Feat/cmip7 awiesm3 veg hr - #266
JanStreffing wants to merge 526 commits into
Conversation
1a42875 to
5617a18
Compare
|
The single failing test ( Something to worry about? @pgierz |
|
@pgierz, will you review this or should I go for it? |
|
I will look |
| return ds | ||
|
|
||
|
|
||
| def _attach_bounds_from_mesh(ds, rule, coord_names): |
There was a problem hiding this comment.
Check if generic enough for backend
| return any(v.chunks is not None for v in ds.data_vars.values()) | ||
|
|
||
|
|
||
| def _encoding_from_dask_chunks(ds, rule): |
There was a problem hiding this comment.
Deferred for later review
|
|
||
| if table_id is None: | ||
| # Fallback to user-provided | ||
| table_id = self.rule_dict.get("table_id", None) |
There was a problem hiding this comment.
Should be checked. Why deleted?
| """ | ||
| return self.rule_dict.get("Conventions", "CF-1.11") | ||
|
|
||
| # ======================================================================== |
There was a problem hiding this comment.
Should be a class. Do we need it?
| return "hdl:21.14100/" + str(uuid.uuid4()) | ||
| """Generate a unique tracking ID (prefix overridable via rule_dict). | ||
|
|
||
| The CMIP7 tracking_id CV requires the ``hdl:21.14107/<uuid>`` prefix |
There was a problem hiding this comment.
Does that mean cmip6 wont work anymore?
| approx_interval = drv.table_header.approx_interval | ||
| frequency_str = _frequency_from_approx_interval(approx_interval) | ||
| logger.debug(f"{approx_interval=} {frequency_str=}") | ||
| # attach the frequency_str to rule, it is referenced when creating file name | ||
| rule.frequency_str = frequency_str | ||
| time_method = _get_time_method(drv.frequency) | ||
| rule.time_method = time_method | ||
| # FESOM yearly files and concat'd hemispheric selects can yield a |
There was a problem hiding this comment.
Hallucination? xarray can sort itself?
Die HR-Submission verarbeitet ein Modelljahr pro Job und pinnt jedes
Eingabemuster auf dieses Jahr. Die neun dec-Regeln haben deshalb bisher
ein einziges Jahr gemittelt und das Ergebnis als ganze Dekade
ausgewiesen; jedes weitere Jahr derselben Dekade schrieb dieselbe Datei
mit einem anderen Einjahresmittel.
Neuer Schritt decadal_gate am Anfang jeder dec-Pipeline, ohne Zustand:
er zaehlt das erste Jahr des Laufs jedes Mal neu aus den Eingabedateien,
zaehlt Dekaden ab diesem Jahr und laesst die Regel nur in dem Jahr durch,
das eine Dekade schliesst. Dann weitet er das Muster auf alle Jahre und
setzt year_start/year_end auf die Dekade, load_mfdataset liest genau die
zehn Jahre. In jedem anderen Jahr gibt er RuleSkipped zurueck; die
Pipeline-Runner halten dort an und _process_rule zaehlt die Regel als
erledigt, ohne Ausgabe. Fehlt ein Jahr einer schliessenden Dekade, ist
das ein Fehler, kein Skip.
Dekaden ab Laufbeginn statt Kalenderdekade, damit ein Szenario ab 2024
keine sechsjaehrige erste Zelle bekommt. Die Grenzen folgen deshalb der
vom Gate gewaehlten Dekade. Eine unvollstaendige letzte Dekade wird nie
geschrieben: cc-plugin-wcrp#81 meldet Zellen, die nicht zehn Jahre lang
sind, als HIGH (nachgemessen mit 5a4239e fuer 2024-2030 und 2040-2046;
volle Dekaden 1850-1860, 1851-1861 und 2024-2034 bestehen).
Nachgemessen: timeavg ('10YS') ergibt fuer zehn Jahre genau einen Wert,
fuer Start 1850 und 2024, datetime64 und cftime. Elf neue Unit-Tests,
Rest unveraendert (70 wie vorher).
d7468f7 to
01a9526
Compare
- Sanity-Check-Artefakte (1464 PNG, HTML, CSS, JSONL) aus dem Index genommen. Die Dateien bleiben auf der Platte und werden von tools/sanity_check neu erzeugt. Die markdown-Berichte bleiben getrackt. - Notizen und Design-Dokumente nach notes/ verschoben, damit die Wurzel nur noch CLAUDE.md, GETTING_STARTED.md und QUICK_START_CMIP7.md enthaelt. Session-Notizen liegen in notes/scratch/ und sind ignoriert. - Wrapper-Skripte der Sanity-Checks und die Benchmark-Skripte aufgenommen, damit die Laeufe reproduzierbar sind.
Ausloeser war cli120: der extra_atm-Shard wurde nach 2 h vom eigenen Inaktivitaets-Watchdog abgebrochen, obwohl er noch schrieb, 17 Dateien fehlten. Fuer ~200 Jahre pro Experiment muss das ohne Handarbeit gehen. - Ursache: der Save-Heartbeat sah nur das Ausgabeverzeichnis. Die Dateien entstehen aber zuerst im knotenlokalen /tmp und landen erst am Ende im Ziel, also meldeten alle 17 Saves nach 15 min einen Stillstand und verstummten. Jetzt zaehlen die Staging-Dateien als Fortschritt, und der Heartbeat tickt nach einem Stillstand langsamer weiter statt aufzuhoeren. - Der Shard-Watchdog prueft Log, Ausgabeverzeichnis und /tmp-Staging und bricht nur ab, wenn alle drei stehen. - Regelfehler blieben in serial_process haengen, ein Shard ohne eine einzige Datei endete mit Exit 0. Jetzt schreibt pycmor pro Shard ein Manifest (ok, skipped, no_output, failed) und endet mit Exit 1, sobald eine Regel nichts erzeugt hat. Ueber das Dekaden-Gate abgeschaltete Regeln zaehlen als vollstaendig. - examples/run_year_driver.py treibt ein Jahr bis zur Vollstaendigkeit: einreichen, warten, Manifeste pruefen, nur die betroffenen Tiers neu einreichen, nach drei Versuchen eine Mail schicken. Bei Erfolg entsteht <WORKDIR>/<JAHR>.done.
examples/run_year_chain.sbatch treibt ein Jahr durch run_year_driver.py, reicht danach das naechste Jahr ein und endet mit dem Ergebnis des Treibers. Laeuft auf `shared` (1 Kern, 3 Tage), also ohne Login-Sitzung. - Mail kommt von SLURM (--mail-type=FAIL,TIME_LIMIT) und nur vom Kettenjob, die Shard-Jobs mailen nie. Die drei Versuche laufen in diesem einen Job, der erst nach dem letzten mit Exit 1 endet: eine Mail pro Jahr, das nicht fertig wurde, keine pro Wiederholung. TIME_LIMIT steht extra drin, weil FAIL keinen Timeout abdeckt. - Die Einzelheiten stehen in <WORKDIR>/<JAHR>.FAILED, SLURMs Mail nennt nur den Job (pycmor-y<JAHR>). - Ein gescheitertes Jahr haelt die Kette nicht an, spaetere Jahre brauchen nur Modellausgabe. Nach zwei gescheiterten Jahren in Folge stoppt sie, weil das auf etwas Systematisches deutet. - Fertige Jahre (<JAHR>.done) werden beim Neustart sofort uebersprungen.
sbatch --mail-user ist keine Umgebungsvariable und ging ab dem zweiten Jahr verloren. Der Kettenjob liest die Adresse jetzt per scontrol aus sich selbst und gibt sie beim naechsten Einreichen weiter.
Beides kam im cli121-Test der Jahreskette heraus. - run_hr_shard.sh stellt jede Shard-Yaml auf parallel/dask um, das Manifest hing aber nur in serial_process. Kein Shard schrieb eines, der Treiber hielt ein vollstaendiges Jahr fuer leer und reichte alles neu ein, und der Exit-Code-Schutz griff ebenso wenig. Unter Dask laeuft die Regel auf einem Worker mit einer Kopie des Rule-Objekts, deshalb gibt _process_rule jetzt eine kleine Zusammenfassung (geschriebene Dateien, Skip-Grund) zurueck, und der Dask-Pfad ordnet Futures ihren Regeln zu. Neuer Test faehrt den echten Dask-Pfad mit einer Worker-Kopie. - Das DRS-Versionsverzeichnis war das Datum jedes einzelnen Schreibens. Ein Lauf ueber Mitternacht landete in zwei Versionen, jede Wiederholung an einem spaeteren Tag verdoppelte jeden Datensatz, und eine Jahreskette haette die 196 Jahre einer Variable auf ~100 Versionen verteilt. Jetzt per directory_date (Regel/inherit) oder PYCMOR_DRS_VERSION festlegbar; die Kette setzt es einmal und reicht es durch, der Treiber haelt es ueber alle Versuche fest.
…immt run_hr_shard.sh setzt pipeline_orchestrator: dask, aber der Schluessel steht nicht im Config-Schema. process() faellt deshalb auf _parallel_process_prefect zurueck, und dort fehlte der Manifest-Aufruf. In cli122 schrieb kein Shard ein Manifest. Der neue Test geht ueber die echte Config und process(), damit er dem Pfad folgt, den die Produktion nimmt. Regeln, die nie zurueckmelden, zaehlen jetzt als failed, damit ein abgebrochener Lauf kein sauber aussehendes Manifest hinterlaesst.
Variablen mit Zeitachse werden ueber xr.save_mfdataset geschrieben und nicht ueber _atomic_to_netcdf, deshalb landeten sie nie in rule._written_files. In cli122 meldeten 509 von 529 Regeln no_output, obwohl alle Dateien auf der Platte lagen; nur fx-Felder wurden erkannt. Der Treiber rechnete das Jahr daraufhin dreimal und gab auf. Geprueft mit zwei echten Shards auf Levante (core_land, cap7_aerosol): vorher 2/10 und 0/5 ok, jetzt 10/10 und 5/5.
…itert Voruebergehende Probleme (Knoten faellt aus, langsames Dateisystem, Watchdog) treffen einzelne Stufen. Scheitern alle zusammen, liegt die Ursache bei allen gemeinsam: Code, Eingabedaten, voller Plattenplatz. Wiederholen verbrennt dann nur Knotenstunden. In cli122 hat der Treiber so ein komplettes Jahr zweimal umsonst neu gerechnet. Jetzt schreibt er nach dem ersten Versuch .FAILED und beendet sich mit 1, was die eine SLURM-Mail ausloest. Ein Jahr mit nur einer Stufe ist nicht eindeutig und wird weiter wiederholt. Gegen die echten cli122-Manifeste nachgespielt: 17 von 17 Stufen schlecht, der Treiber haette nach Versuch 1 aufgehoert.
…i pro Experiment Mehrere Experimente sollen gleichzeitig cmorisiert werden koennen, ohne dass Skriptkopien auseinanderlaufen. Bisher lief jeder Job aus dem lebenden Checkout, ein Fix mitten in einer 150-Jahre-Kette hat also den Code auf halber Strecke geaendert, und experiment_id: piControl stand fest in allen 17 Stufen-yamls. Eine Kampagne ist ein Verzeichnis mit campaign.yaml. start_campaign.sh klont pycmor nach <kampagne>/code auf code_ref und pinnt den Commit. Jeder Jahresjob prueft vor dem Start, dass Code und die datenbestimmenden Felder (run_root, output_root, drs_version, inherit) noch dem Pin entsprechen, und laeuft mit PYTHONPATH auf die gepinnte Kopie. Bewusstes Umpinnen: setup --repin, wird in pin_history.log protokolliert. campaign.py apply schreibt die inherit-Werte der Kampagne in die erzeugten Stufen-yamls und biegt die absoluten Pfade in den Checkout auf die gepinnte Kopie um; das Ergebnis wird strukturell gegen das Original geprueft. Fertige Jahre verschiebt der Treiber in output_root, nur vollstaendige Jahre landen also im ESGF-Baum. Ein Neustart eines schon vollstaendigen Jahres veroeffentlicht nur noch, statt neu zu rechnen. Jobnamen tragen den Kampagnennamen, Manifeste den Commit.
Every FESOM sea-ice field is written by XIOS as _FillValue where it is physically zero and never as 0 — measured at 73-93 % fill with zero exact zeros — and runoff behaves the same way at 54 %. Rules branded hxy-u or hxy-sea must publish 0 there, while hxy-si rules must keep the fill. This extends affd468 from siconc to the other eight. cap7_ocean: nan_to_zero in scale_pipeline, for friver (source runoff). lrcs_seaice: same step in scale_pipeline (siflfwbot_omon; also reaches vsfsit, a no-op there since virtsalt has no fill), sfdsi_from_fw_ice_pipeline, sihc_pipeline and ice_mass_transport_pipeline (sidmasstranx/y, whose product of uice and m_ice is NaN wherever there is no ice); plus a new nan_to_zero_pipeline — a copy of DefaultPipeline.STEPS with the step — for sidconcdyn, sidconcth and sitimefrac_day, which ran on <default> and could not gain it there: that pipeline is shared with 17 rules in the tier, most of them hxy-si. Placement is always before timeavg. timeavg skips NaN, so averaging first would average only the ice-covered days instead of averaging the zeros in — the monthly mean of a flux differs. sitimefrac_day was writing raw a_ice, a concentration, under a fraction-of-time standard_name; it now uses a sitimefrac_pipeline like the monthly rule, so a cell at 30 % concentration reads 1.0 rather than 0.3. compute_sitimefrac itself no longer collapses fill: xr.where(data > 0, 1.0, 0.0) sent NaN down the 0.0 branch, which is right on an ocean-only mesh but publishes "ice never present" over every continent on gr. It now preserves missingness and the pipelines apply the policy with nan_to_zero, which generate_gr_yaml swaps for nan_to_zero_over_ocean. That also fixes the monthly sitimefrac on gr. Checked: the 13 rules now carrying the step are all u- or sea-branded, none masked; the gr generator injects the sst reference for all of them without per-rule configuration; yamllint on the touched files is back to the two pre-existing duplicate-grid errors on siconca, reported separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cmip7-awiesm3-veg-hr
Mit #281 gibt es Experiment-Metadaten zweimal: die Profile in repoint_hr_year.py und den inherit-Block der Kampagne. Jetzt eine Quelle, die Profile. Sie liegen in git und werden mit dem Code gepinnt. campaign.yaml nennt nur noch `experiment: 1pctCO2`, die Kette gibt das als EXPERIMENT an den Repointer weiter. Ohne das hielte der Laufname-Schutz aus #281 jede 1pctCO2-Kampagne an. inherit in der Kampagne bleibt fuer Werte, die kein Profil abdeckt, und darf keinen Profilschluessel wiederholen. apply prueft, dass jeder Profilwert in jeder Stufen-yaml angekommen ist. Getestet fuer alle vier Profile ueber die 17 echten Stufen-yamls.
…the campaign Found by the 1pctCO2 1850 test campaign, which came up short on five shards. Land walltime: cap7_land_03, core_land_00 and extra_land_00/01 were all still saving when SLURM cancelled them at exactly 3h (TIMEOUT, no error, watchdog quiet). cap7_land, core_land and extra_land now get 6h, like extra_atm and cap7_atm. pfull_mon: the climatology accumulator directory was hardcoded to /work/bb1469/a270092/cmorized/climatologies, in the rule and as the default, so anyone but its owner hit PermissionError, and every campaign would have pooled its years into one shared accumulator. campaign.py now exports PYCMOR_CLIMATOLOGY_STATE_DIR=<campaign>/climatologies, and custom_steps.py reads the rule's setting first, then that variable, then the old default. The rule no longer sets it, so runs outside a campaign are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AWI-ESM3 has no CO2 tracer, so co2 was listed as blocked. But in a concentration-driven run the CO2 the model used is known exactly, and in 1pctCO2 and abrupt-4xCO2 it is the forcing that matters. co2mass_mon (atmos.co2mass.tavg-u-hm-u.mon.glb) reproduces ece_cmip_ghg.F90: the input4MIPs value of NCMIPFIXYR (forcing_year), grown 1%/yr (L1pctCO2), times RNxCO2=4 (LANxCO2), or the file's own year (historical), with the model's monthly interpolation between annual values. Mass = that mole fraction * M(CO2)/M(air) * the atmosphere's mass from the model's monthly global-mean surface pressure (OpenIFS RA, RG). The experiment profiles in repoint_hr_year.py set co2_scenario (historical, 1pct, abrupt4x); piControl keeps the tier default, fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GHG scalar rules read forcing_year 1850 for every experiment. Right for piControl, 1pctCO2 and abrupt-4xCO2, which hold the gases at 1850; wrong for historical, which would have shown 1850 values in every year. forcing_year now mirrors the model's NCMIPFIXYR for all gases: the historical profile sets it to null, so the values follow the model year, interpolated monthly between the annual ones as ece_cmip_ghg.F90 does (year y's value reached in June). With a fixed year the output is unchanged. co2_scenario now only changes CO2, like L1pctCO2/LANxCO2: file (default; piControl and historical), 1pct, abrupt4x. Historical CO2 needs no scenario of its own any more; forcing_year null covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
broadcast_forcing_year_to_monthly passed the open_mfdataset series, a dask array, to oifs_ghg_mole_fraction, whose .item() xarray refuses on dask. ch4, n2o, cfc11 and cfc12 failed in the test-1pctCO2 1850 run (3536ff2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMIP7 definiert realization_index, initialization_index, physics_index und forcing_index mit ihrem Buchstaben: cmor-cvs.json verlangt ^r[0-9]+$ usw., esgvoc cmip7 ebenso. pycmor schrieb wie bei CMIP6 die nackte Zahl. Der alte cc-plugin-wcrp hat das nicht geprueft; mit 2.4.0b1 faellt jede Datei viermal mit ATTR004 (HIGH) durch. Geprueft an einer echten cli122-Datei mit korrigierten Attributen: cf, wcrp_cmip7 und aicc jeweils 0 Befunde. CMIP6 bleibt unveraendert.
Beides kam im Testjahr test-1pctCO2 y1850 (5.10.) heraus: extra_land_shard_01
hing mit drei Saves 5,8 h fest, lief ins 6-h-Walltime-Limit, und das Jahr
wurde trotzdem als vollstaendig veroeffentlicht.
- Der Watchdog wertete jede neue Logzeile als Fortschritt. Seit dem
cli120-Fix schreibt der Heartbeat auch im Stillstand weiter ("no I/O
progress yet"), ein verklemmter Shard hielt sich so selbst am Leben. Diese
Zeilen zaehlen jetzt nicht mehr; Fortschritt sind andere Logzeilen oder
wachsende Staging-Dateien im knotenlokalen /tmp. Das gemeinsame
Ausgabeverzeichnis wird nicht mehr beobachtet, dort schreiben alle Shards
des Jahres. Mit dem echten Block in vier Faellen getestet.
- Das Manifest des Shards vom Lauf drei Tage zuvor lag noch da und sagte
"alles ok". Der Treiber loescht jetzt die Manifeste einer Stufe, bevor er
sie neu einreicht, und akzeptiert kein Manifest, das von anderem Code als
dem gepinnten stammt.
…omises a resume clear_manifests matched the tier exactly, but submit_hr_year_shards.sh selects TIER by *TIER*, so with WITH_GR=yes a retry of <tier> also reruns <tier>_gr and its old manifests stayed. The .FAILED text said a rerun picks up from existing outputs; it resubmits every tier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eans up a failed one With WITH_GR=yes veg_seaice_gr has no rules (its only rule needs the FESOM mesh). ls on its empty shard dir failed under pipefail, so submit_hr_year_shards.sh died after queuing 22 of 24 tiers and the 'no shards; skipping' branch was never reached. Count with find instead. The driver then exited and left the 22 arrays running with nothing to assess or publish them, and wrote no .FAILED although the chain log points to it. A failed submission now cancels the jobs it queued and writes .FAILED with the tail of the submit output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMIP7 cmorization for AWI-ESM3-VEG-HR
Adds full CMIP7 support targeting AWI-ESM3-VEG-HR, including a native compound-name
architecture that replaces the legacy cmip6-table-based data request lookup.
Key changes
CMIP7 data request
DataRequestfromCMIP7_DReq_metadataJSON instead of cmip6 tablesocean.tos.tavg-u-hxy-sea.mon.GLB)cmip6_table→cmip6_cmor_tablein vendored metadatacompound_namematching againstcmip6_compound_nameandcmip7_compound_nameattributestable_idfrom compound name when not set explicitlyValueErroron zero DRV matches (instead of silent skip)Pipeline
vertical_integratecustom pipeline stepconvert()step fromDefaultPipelineStateobjects not being unwrapped to actual results in parallel runsStandard library
src/pycmor/std_lib/time_bounds.py)getattr+_pycmor_cfgfallbackglobal_attributesto derivetable_idfrom CMIP6/CMIP7 compound namesXarray accessor API
StdLibAccessorwith.process()Test infrastructure
pycmor.fixtures.model_runs)pycmor.tutorialdataset system (xarray.tutorial-style API)Misc fixes
entry_points()compatibilitypyfesom2imports for environments without itTest plan
pytest tests/unit/pycmor process examples/awiesm3-cmip7-minimal.yamlruns successfully on Levante