Make re-running the plots cheap, and convert units exactly once (#425) - #482
Make re-running the plots cheap, and convert units exactly once (#425)#482brianpm-ucar wants to merge 5 commits into
Conversation
Re-running the ADF with redo_plot false was doing far more than deciding
there was nothing to do. Measured on a two-case, ten-variable run whose
plots were all already present, the plotting stage took 13.6 s and
rewrote five files.
zonal_mean built the log-pressure file name two different ways: the
pre-flight pass looked for '{var}_{season}_Zonal_logp_Mean.png' while the
plotting pass wrote '{var}_logp_{season}_Zonal_Mean.png'. The name it
looked for never existed, so every log-pressure plot was redrawn on every
run whatever redo_plot said -- the five rewritten files, and the 3.6 s in
matplotlib's _update_ticks and 3.1 s in mathtext.parse that dominated the
profile. It also computed both seasonal means before finding out whether
either plot was wanted.
meridional_mean now follows zonal_mean, which is what NCAR#425 asks for: a
pre-flight pass that registers the plots that already exist, and data read
through AdfData rather than a hand-rolled glob of cam_regrid_loc. Because
none of its file names depend on whether the variable has a 'lev'
dimension, a variable or a case whose plots are all present is settled
from the names alone, without opening a file.
Reading through AdfData also stops the plots being scaled twice. The
regridding stage applies the variable-defaults conversion when it writes
and stamps 'transformed' on the result; this script applied scale_factor
and add_offset again on top. PRECT was drawn 86400000 times too large and
PS a hundred times too small. AdfData's _regrid_converters exists to make
exactly that decision.
The no-op run now takes 1.3 s and rewrites nothing. Of the thirty
meridional plots, the fifteen for variables with no unit conversion are
byte-identical; PRECT and PS change because they were wrong; RESTOM
changes only its units label, from the variable defaults' "W m$^{-2}$" to
the file's own "W/m2", because AdfData applies new_unit only alongside a
conversion -- which is what zonal_mean has always shown.
Fixes NCAR#425.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ADF converts units with the scale_factor, add_offset and new_unit in
the variable defaults, and it has to apply that conversion exactly once.
Whether it had already been applied was being decided seven different
ways.
load_da and the time series loaders applied it unconditionally.
load_climo_ds applied it unconditionally and stamped 'transformed' on the
result. load_reference_climo_ds checked that stamp, and also compared the
file's units to new_unit -- with ==, which almost never matched, because
the two strings come from different hands: CAM writes 'W/m2' where the
defaults say 'Wm$^{-2}$'. load_reference_climo_da checked nothing, on the
same files. Only the regrid loaders had a working guard, and it read the
stamp alone.
There is now one decision, AdfData.already_converted, used by all of them:
the stamp if it is there, and otherwise whether the file's units already
say what the conversion is converting to.
adf_units carries that comparison. It reduces a unit to a canonical form
first, so the many spellings of one unit agree: 'W/m2', 'W m-2', 'Wm^-2',
'W m**-2', 'W m⁻²' and the LaTeX 'W m$^{-2}$' and 'Wm$^{-2}$' -- both of
which the shipped defaults contain -- are one unit written seven ways. It
folds a solidus into negative exponents, splits a run-together factor into
known symbols, reads superscripts, and treats a unit whose factors cancel
as dimensionless, so 'kg/kg', 'kg kg-1' and 'fraction' agree too. It
imports nothing but re, so CI runs its tests.
Two plotting scripts scaled by hand on data the regridding stage had
already converted, the same bug meridional_mean had: global_latlon_vect_map
on regridded climatologies, and tem. Both now go through
AdfData.apply_conversion, which makes the same decision the loaders do.
global_latlon_vect_map also relabelled units from the defaults whether or
not it had converted, claiming units the data was not in.
Nothing that was right changes: the three AMWG tables are byte-identical
to what main produces, and a plotting re-run still rewrites nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
polar_map already worked out that every plot existed and skipped the
reference data, but it opened a regridded file for each case first, only
to see whether the variable has a 'lev' dimension. A 2-D variable is
plotted as {var}_{season}_{hemisphere}_Mean and a 3-D one as
{var}_{pressure}hpa_{season}_{hemisphere}_Mean, so which of the two it is
looking at can be read off the names, and the file does not have to be
opened at all when nothing is missing.
The plots still reach the website: the names are registered before the
variable is skipped, which is what the loop below did with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
global_latlon_vect_map opened four regridded files for a vector pair, and global_mean_timeseries opened the reference time series and every case's and took global means of them, before either found out that the plot it would draw was already there. aod_latlon opened both observation files and every case's climatology up front and only then checked each panel. All three name their output from things known before any data is read -- the vector's name and the configured pressure levels, the field name, the observation source and the season -- so a re-run can be settled from the names, and the plots still reach the website. The check that a named set is complete is now plotting_utils. first_complete_plot_set, shared with polar_map: a 2-D variable and a 3-D one are named differently, so trying both spellings says both that nothing needs drawing and which kind of variable it was, which is the only thing the data was being opened for. Verified against a run with U and V added: ten vector plots and five global-mean plots on the first pass, and a second pass that draws nothing, rewrites nothing and reports every variable as already done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tem.py called adfobj.data.apply_conversion, but the entry point's argument is named adf, so the first TEM variable that needed drawing raised NameError and took the rest of the plots with it. A re-run whose plots all existed skipped the line, which is how the end-to-end testing missed it. normalize_units split a run-together factor by greedily matching known symbols, after lower-casing. That reported 'Nm-2' and 'nm-2' -- newton per metre squared and per square nanometre -- as the same unit, which is exactly the false match the module exists to prevent, and it shredded unit names that happen to spell other symbols: 'Sv' became siemens-volt, 'cal' became c-a-l. The splitting is now an explicit table of the forms that occur, applied while the case is still intact, and unit names it does not know are left whole. Multi-factor units written with several solidi -- 'kg/m2/s', 'W/m2/K' -- work either way round. A ratio whose factors cancel now keeps what cancelled, so 'kg/kg' and 'kg kg-1' still agree but 'kg/kg' and 'mol/mol' do not: a mass mixing ratio and a volume mixing ratio are both dimensionless and are not the same number. Also from the review: - load_reference_regrid_da did not pass 'field', so for observations the guard in load_da looked the obs variable name up in the variable defaults, found nothing, and could never fire. - load_reference_climo_ds stamped 'transformed' but never set the new units, so a converted reference kept the units it arrived with: the regridded baseline files on disk say 'm/s' over values in mm/day. - polar_map and global_latlon_vect_map passed the layouts to first_complete_plot_set least specific first, against what its docstring says, so a 3-D variable with stale 2-D-named plots could be declared complete and its levelled plots never drawn. - adf_units imports nothing but re and scores 10.00/10, so it goes in the set of files CI lints. - plot_meridional_mean_and_save assigned the reference's time mean to adata, throwing away the latitude weighting just applied to the test case. Latent, because seasonal_mean removes 'time' first, and fixed here because NCAR#425 is about the meridional plots. The AMWG tables are still byte-identical to main, a plotting re-run still rewrites nothing, and the thirty meridional plots are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round: a crash, a wrong unit comparison, and six smaller things (
|
Closes #425, and covers a good part of #409.
Two things turned out to be tangled together. Re-running the ADF with
redo_plot: falsewas doing far more than deciding there was nothing to do, and the reason a plotting script had to open its data before it could decide was often the same reason it then handled units wrong.Measured
A two-case, ten-variable run whose plots all already existed,
redo_plot: false:The five rewritten files
zonal_meanbuilt the log-pressure file name two different ways. The pre-flight pass looked for{var}_{season}_Zonal_logp_Mean.png; the plotting pass wrote{var}_logp_{season}_Zonal_Mean.png. The name it looked for never existed, so every log-pressure plot was redrawn on every run whateverredo_plotsaid — and that redraw was the 3.6 s in matplotlib's_update_ticksand 3.1 s inmathtext.parsethat dominated the profile. One 3-D variable here; a run with many would redraw five plots per variable, every time.zonal_meanalso computed both seasonal means before finding out whether either plot was wanted.Deciding without opening the data
A script could not tell what it would produce without knowing whether the variable has a
levdimension, which is a property of the data — so it opened the data. But a 2-D variable and a 3-D one are named differently, so trying both spellings answers both questions at once: whether anything is missing, and which kind of variable it was. That isplotting_utils.first_complete_plot_set, used bypolar_mapandglobal_latlon_vect_map.Scripts changed:
meridional_mean— rewritten to followzonal_mean, which is what Clean up Meridional plots to mimic code of Zonal plots #425 asks for: a pre-flight pass, and data read throughAdfDatarather than a hand-rolled glob ofcam_regrid_loc. None of its names depend onlev, so a variable or case whose plots exist is settled from the names alone.polar_map— worked out that everything existed, but opened a regridded file per case first, only to look forlev.global_latlon_vect_map— opened four regridded files for a vector pair before checking.global_mean_timeseries— opened the reference time series and every case's, and took global means of them, beforeconditional_savedecided not to write.aod_latlon— opened both observation files and every case's climatology up front, then checked each panel.Already correct and left alone:
qbo,tape_recorder,adf_histogram,cam_taylor_diagram.Units: one decision instead of seven
Migrating
meridional_meantoAdfDataexposed a second bug, and pulling on it found the general case. Whether a conversion had already been applied was being decided seven different ways:load_da, both time series loadersload_climo_dstransformedload_reference_climo_dsunits == new_unitload_reference_climo_daThat
units == new_unitwas a literal string comparison, so it almost never fired: CAM writesW/m2where the defaults sayWm$^{-2}$. There is now one decision —AdfData.already_converted— used by all of them: the stamp if it is there, otherwise whether the file's units already say what the conversion is converting to.lib/adf_units.pycarries that comparison. It reduces a unit to a canonical form first, so the spellings agree:It folds a solidus into negative exponents, splits a run-together factor into known symbols, reads superscripts, and strips LaTeX.
hPais deliberately not split into hour-pascal. This is not hypothetical —adf_variable_defaults.yamlcontains bothWm$^{-2}$andW m$^{-2}$today. The module imports nothing butre, so CI runs its tests.Three scripts were scaling twice
The regridding stage applies the variable-defaults conversion when it writes and stamps
transformed; these appliedscale_factorandadd_offsetagain on top of it:meridional_mean— PRECT was drawn 86400000 times too large and PS a hundred times too smallglobal_latlon_vect_map— on regridded climatologies, and it also relabelled units from the defaults whether or not it had converted, claiming units the data was not intemAll three now go through
AdfData.apply_conversion, which makes the same decision the loaders do.What changes in the output
Of the thirty meridional plots, the fifteen for variables with no unit conversion (TS, ICEFRAC, Q) are byte-identical. PRECT and PS change because they were wrong.
RESTOM changes for a third reason, and it is not a bug fix.
load_daappliesnew_unitonly alongside a conversion, so RESTOM's label goes from the defaults'W m$^{-2}$to the file's ownW/m2— the same units, rendered differently. That is whatzonal_meanhas always shown, so this makes meridional agree with it rather than introducing something new. If the preference is thatnew_unitshould relabel even when nothing is converted, that is a one-line change inload_da— but it would relabel plots across every script, so it belongs in its own PR.Testing
New CI-runnable tests:
test_adf_units.py(8 tests, 25 subtests) covering the spelling families, the pairs that actually occur in this repo, units that must stay different, dimensionless forms, missing units matching nothing, andhPanot being split.test_regrid_scaling.pygains two cases for the units evidence and now exercises the real shared decision rather than a literal stamp check.End to end on Casper, model vs model, two dissimilar cases:
mainproducesaod_latlon's pre-flight tested directly with synthetic panels (no AOD data on this machine): all present → registers exactly 8; one missing → registers none;redo_plot: true→ does not firenew_unitand so rely on the stamp alone.pytest lib/test/unit_tests: 167 passed in a full environment; 76 passed, 68 skipped in a virtualenv holding only PyYAML and pytest, the way CI builds it.darker --check --revision mainclean.Not in this PR
regional_map_multicasereadsredo_plotand never uses it, so it always regenerates;cloud_regime_analysishas no redo handling at all.tem,MOPITTandenso_comparison_plotsstill open their data before checking. Left deliberately for a follow-up on #409.🤖 Generated with Claude Code