Repository navigation
Refactor and clean up dapper, and some bug fixes - #50
Conversation
|
It's all the stuff I always mean to do but never get around to! Plus extras! For posterity, the initial development of dapper was done without LLMs, then partly with LLMs, and finally fully with LLMs. So it's nice to see a major cleanup effort here to make it more consistent, robust, and hopefully usable. |
Pin current behavior of weakly covered code before refactoring: domain export/round trip, landuse sampling, ELM packing/humidity, surface customization, and geo sampling helpers. Assertions marked 'PINS BUG Bn' document known-incorrect behavior logged in REFACTOR_NOTES.md.
ruff: py311 target, line-length 88, rules E4/E7/E9/F/I/UP/B; excludes dev/ scratch scripts and read-only tests. mypy: ignore_missing_imports, excludes dev/. No pytest configuration touched.
Formatting only (AST-identical for all 39 files); dev/ and tests/ excluded.
Sort imports, drop unused and duplicate imports, move module-level imports above the lazy Earth Engine blocks (gee_utils, topomake) and fastparquet helper (exporter), and remove function-local re-imports of modules already imported at module level.
PEP 585/604 syntax (dict/list/tuple, X | None) and unquoted annotations where postponed evaluation is active. Annotations and typing imports only.
- cmip_utils: drop 77 unreachable lines after sample_bbox_means_for_aois returns; unused locals - exporter: drop never-called _pass1_to_parquet_raw and _zone_mappings_path, unused writer params (years_span, filename_template), an unreachable mode check, and redundant pack-scope branches (same results) - zonal: drop unused _reduce_da and per-gid guards that cannot trigger after the global candidate-count check - temporal: drop always-empty accum_vars - sfile: drop unused locals - era5 adapter: drop wind_direction, which was computed then discarded - gee_utils: drop unused _ROOT_DIR/_DATA_DIR - remove commented-out code and an impossible netCDF4 None check
- Exporter: document the real constructor parameters and layouts - ERA5Adapter: describe the actual preprocess steps (Feb 29 kept until temporal alignment, one-hour lookahead) - customize_surface: drop nonexistent validate_units, document units_policy and engine - sample_e5lh/parse_geometry_objects: drop references to nonexistent AOI and Domain.to_geometries - schema: move the module docstring to the top; drop references to nonexistent dapper.surf.write - SurfaceValidator: correct V-105, list V-108/V-109 - replace 27 'dapper module: x.y' placeholder docstrings, drop path header comments, conversational and 'NEW' comments No code changes (verified AST-identical after stripping docstrings).
geo.sampling.write_netcdf, sfile.write_surface_nc, and SurfaceFile.to_netcdf each re-implemented the same attr precedence (append_attrs > source attrs > dapper defaults). Move it to io.attrs.merge_global_attrs. Replace deprecated datetime.utcnow() with io.attrs.utc_timestamp(), which yields the identical string format.
- is_feb29() replaces six hand-written Feb-29 masks (temporal, fluxnet) - create_dtime reuses _numeric_dtime instead of a duplicate copy - one _LINEAR_VARS / _FFILL_VARS definition Verified identical outputs and exceptions against the previous implementation across 292 input combinations.
- one LonWrap definition (geo.lonwrap), re-used by zonal and landuse - zonal and landuse use lonwrap.normalize_lons instead of hand-rolled list comprehensions (same NaN handling) - merge _median_step/_median_step_lon in infer_grid_metadata - drop sfile._latlon_dim_names, unused since the previous cleanup
- share SAMPLING_PROVENANCE_COLUMNS between Domain.to_df_loc and Exporter._site_attrs - read domain.mode directly (domain is always a Domain here) - initialize filename_prefix in __init__ instead of getattr fallbacks - rename misleading import aliases (utils -> fs, dt -> temporal) - simplify zone-mapping lon column selection (same values) - write the tab separator as '\t' instead of a literal tab - type hints on Exporter.__init__ and run
- zip(..., strict=False) to make existing lenient behavior explicit - f-strings instead of str.format; datetime.UTC alias - attribute access instead of constant getattr - immutable default for elm_var_packing_params(data=()) - rename ambiguous loop variables - ruff config: ignore B028 (stacklevel would move warning attribution) and per-file B023 where closures run within the same iteration
from_file re-applied the CRS normalization that from_provided already performs; from_file/from_geometry converted path_out and run_group before from_provided converted them again. Add return types to iter_runs and topounits_for_gid.
_dtype_nbytes' alias table was unreachable (np.dtype accepts every alias it listed) and the per-pattern 'lock axis to 1' branches in _compute_auto_chunks reassigned values that were already 1. Verified identical chunk tuples across 225 combinations.
- collapse duplicated topounits-is-None checks - drop the unused parameter of _parse_spec - use a separate set for skipped columns in add_params_from_df instead of rebinding the list argument - ds.sizes instead of deprecated ds.dims[...] lookup in resize_dim
Required-var lists, the ERA5 raw->ELM name map, and units now come from schemas.elm / config.metsources.era5 where values are identical. Differing values (DTBOT units 'unsure', list-typed ranges, band order) stay literal. Verified identical keys, order, values, and types.
None of these were referenced in src, tests, docs, or tutorials: - integrations.earthengine.gee_utils: split_into_dfs, infer_id_field, featurecollection_to_df_loc - elm.utils: validate_met_vars (needed unshipped docs/data), gen_zone_mappings; elm.utils no longer imports Domain (§5.8) - io.attrs.apply_append_attrs, io.fs.make_directory, io.provenance - surf.schema: register_many, expand_registry, propose_export_policy, EXPORT_POLICIES, validate_against_schema - met.cmip_utils: extract_vars_from_files, cftime_date, summarize_search - surf.sfile: build_surface_dataset, build_surface_dataset_cellset - met.validation: raw-site-parquet/raw-site-csv quicklook modes, whose inputs nothing produces any more BREAKING CHANGE: the names above are no longer importable.
…B13) make_topounits caught RuntimeError, printed it, and returned None despite its '-> Domain' annotation, so 'dom = dom.make_topounits(...)' silently discarded the Domain. The error now propagates. BREAKING CHANGE: Domain.make_topounits raises RuntimeError instead of returning None when a topounit build fails.
…§5.4)
Importing dapper.met.validation called matplotlib.use('Agg'), switching
the backend for the whole process (e.g. breaking inline/interactive
plots in notebooks). Quicklooks only save PNGs and close their figures,
so the backend switch is unnecessary; matplotlib already picks Agg when
no display is available. Smoke-tested sites and cellset quicklooks.
BREAKING CHANGE: importing dapper.met.validation no longer changes the
matplotlib backend.
__version__ was hardcoded to 0.1.0 while pyproject declares 1.0. BREAKING CHANGE: dapper.__version__ now reports the installed distribution version (currently '1.0'), or '0+unknown' when not installed.
- add directly imported but undeclared deps: shapely, pyproj, fsspec, python-dateutil, intake - drop pip; move jupyter, jupytext, geemap, rioxarray (not imported by dapper) to a 'notebooks' extra; add a 'plot' extra with folium - sync environment.yml and document the extras Verified the full suite passes on a fresh install without extras. BREAKING CHANGE: jupyter, jupytext, geemap, and rioxarray are no longer installed by default; use 'pip install dapper-elm[notebooks]'.
…column_for_csv (§5.7) Both remain functional but emit DeprecationWarning. engine now defaults to None (it was never used).
In sites-mode MET export, pack_params receives the raw per-site series. data.min()/max() propagated any NaN into add_offset/scale_factor, and the exporter then fell back to scale_factor=1.0, destroying small variables such as QBOT and PRECTmms under int16 packing. Use np.nanmin/np.nanmax (as the cellset global path already does). Updates the T-7 pin for B3 into a regression test.
Saturation vapor pressure used latent heat of vaporization for T >= 273.15 K, but actual vapor pressure used vaporization for T <= 273.15 K (the opposite branches), yielding RH > 100% when the dewpoint equals temperature and biased RH/QBOT for ERA5 inputs. Both now use the temperature-based phase choice. Updates the T-7 pins for B2 into regression tests.
xv/yv were written as [minx,maxx,minx,maxx]/[miny,miny,maxy,maxy], a Z order rather than the counter-clockwise (ll, lr, ur, ul) order used by CIME/ELM domain files. Domain.from_elm_domain therefore read dapper-written domain files back as self-intersecting zero-area cells. Updates the T-1 pins for B4 into regression tests.
export_landuse_timeseries called kwargs.setdefault('targets', ...)
on the shared kwargs dict inside the per-run loop, so in sites mode with
sampling_method='zonal' every site after the first was sampled over the
first site's polygon. Use a per-run copy of kwargs.
Updates the T-2 pin for B5 into a regression test.
sample_gridded_dataset_polygons reordered each per-target slice before xr.concat(..., dim=lat_dim), and the concat then prepended lsmlat, producing e.g. (lsmlat, time, natpft, lsmlon) instead of ELM's (time, natpft, lsmlat, lsmlon). Reorder after the concat, as sample_gridded_dataset_points does. Affects zonal landuse and zonal surface outputs. Updates the T-2 pin for B18 into a regression test.
…s (B19) resize_dim assigned the new-length coordinate before resizing data variables, so any dim used by a data variable raised 'conflicting sizes'. Truncation now uses isel; padding builds the padded variables first, then installs the extended coordinate, preserving dtype, attrs, and variable order. Updates the T-8 pin for B19 into a regression test.
…(B15) - units_policy='warn' now emits a UserWarning (it behaved like 'ignore') - overwriting an existing variable keeps its file dtype unless a dtype override is given; registry dtypes are only the float32 default (the specs carry no dtype), which made every integer variable such as URBAN_REGION_ID uneditable Updates the T-8 pins for B15 into regression tests.
|
Wonder if we should have a test to see if any of this has changed answers (like a gold file test or something) |
0528c0e to
d66fa7a
Compare
|
@rfiorella What is B7? It's apparently unresolved but I didn't see the description anywhere here. Otherwise I'm glad you put an eye on this because many of the current design decisions were inferred from conversations, example files, and outdated docs. |
Some of the existing tests were developed after bugs had been found, so we wouldn't want to lose those. I wouldn't build a "gold file" test until the repo is more hardened and demonstrated via use--I'm sure more will surface. I guess for a PR this sweeping it might be good to have an LLM do a poor-man's version of it instead of formalizing something? You've already doubled the number of tests and provided much more coverage--I guess the question is if the new tests are correct. We don't know that the old ones were, either. |
matthewhoffman
left a comment
There was a problem hiding this comment.
@rfiorella , thanks for this sweeping update. Obviously it is too big to review in any detail, but I did look over all the commit messages individually, and all the changes seem worthwhile. Given the low maturity level of the codebase and little active development, I see no reason not to go ahead and merge this. My only concern would be if anyone has active branches that would be a nightmare to rebase after this gets merged. I do not.
All of the B* references relate to an intermediate LLM spec markdown file that I used to help guide the refactoring. I excluded the md file in the PR since it's essentially clutter at this point but never went back and updated the PR message. I'll do that and add the bug list this pr claims to have fixed to an additional message here. |
|
Updated the main PR message to be a bit more descriptive and remove cryptic references to intermediate files that weren't pushed. @jonschwenk @jsta @matthewhoffman Anyone feel free to merge if you're happy with it now. |
This PR removes some of the duplication left over from some LLM code generation steps without changing behavior. It also adds characterization tests, applies a set of approved breaking changes, and fixes seven bugs. Each concern is its own commit, so no commit mixes a refactor, a breaking change and a bug fix.
Summary
8b43332)The six original test files are byte-identical to the baseline. The only change under
tests/is five new files,test_characterize_*.py, all approved in advance.Refactor (no behavior change)
ruff format. The formatted code is AST-identical to the original, and the formatting commit is listed in.git-blame-ignore-revsso it doesn't clutter blame. Also tidied imports and modernized type annotations.cmip_utils, the exporter's raw path (never called), unused private helpers, and guards that could never trigger.datetime.utcnow_numeric_dtimeLonWrapnormalize_lonscreate_dtimeover 292 cases, chunking over 225 cases, andelm_data_dicts).Potentially breaking changes
integrations.earthengine.gee_utils,elm.utils,io.attrs/fs/provenance,surf.schema,met.cmip_utils,surf.sfileandmet.validation. None of them is referenced insrc, tests, docs or tutorials. The full list is in commit0ec1528.Domain.make_topounitsnow raisesRuntimeErrorwhen a topounit build fails. It is annotated-> Domain, but it used to catch theRuntimeError, print it and returnNone. A call likedom = dom.make_topounits(...)therefore replaced the Domain withNonewithout any error. This is also a bug fix.dapper.met.validationno longer callsmatplotlib.use("Agg"). Forcing that backend at import time broke inline and interactive plotting in notebooks. It wasn't needed, because quicklooks only save PNGs and close their figures.dapper.__version__now comes from package metadata, so it reads1.0(the version inpyproject.toml) instead of the hardcoded0.1.0.shapely,pyproj,fsspec,python-dateutilandintake, which the code imports but were never declared.pip.jupyter,jupytext,geemapandrioxarraymove to anotebooksextra;foliumgoes in a newplotextra.customize_surface(engine=)andERA5Adapter.id_column_for_csv. Both still work but now emit aDeprecationWarning.enginewas never used and now defaults toNone.Bug fixes (one commit each, each with a regression test)
compute_humidities(elm/utils.py,3a3b684): saturation vapor pressure used the latent heat of vaporization for T ≥ 273.15 K. Actual vapor pressure used it for T ≤ 273.15 K, which is the opposite choice. When the dewpoint equalled the air temperature, the two came out different, so RH went above 100%, and RH/QBOT were biased for ERA5 inputs. Both now choose the phase from temperature the same way.elm/utils.py,517b0e4): in sites-mode MET export,pack_paramsgets each site's raw series. It useddata.min()/data.max(), so a single NaN madeadd_offsetandscale_factorNaN. The exporter then fell back toscale_factor=1.0, which under int16 packing destroys small-magnitude variables such as QBOT and PRECTmms. The fix usesnp.nanmin/np.nanmax, as the cellset global path already did.domain.nccorners in the wrong order (domains/elm_domain.py,65bd3b5): cell cornersxv/yvwere written as[minx, maxx, minx, maxx]/[miny, miny, maxy, maxy]. That is a Z order, not the counter-clockwise order (ll, lr, ur, ul) that CIME/ELM domain files use. As a result,Domain.from_elm_domainread dapper-written domain files back as self-intersecting, zero-area cells. Corners are now written counter-clockwise.landuse/landuse.py,0491500):export_landuse_timeseriescalledkwargs.setdefault("targets", ...)on akwargsdict shared across the per-run loop. In sites mode withsampling_method="zonal", the first site's target stuck, so every later site was sampled over the first site's polygon. Each run now gets its own copy ofkwargs.geo/zonal.py,46a3dd4):sample_gridded_dataset_polygonsreordered each per-target slice beforexr.concat(..., dim=lat_dim), and the concat then putlsmlatfirst. The output came out as e.g.(lsmlat, time, natpft, lsmlon)instead of ELM's(time, natpft, lsmlat, lsmlon). The reorder now happens after the concat, assample_gridded_dataset_pointsalready does. This affects zonal landuse and zonal surface outputs.SurfaceFile.resize_dimfailed for any dimension used by a data variable (surf/sfile.py,3869a9a): it assigned the new-length coordinate before resizing the data variables, so any dimension used by a variable raised "conflicting sizes". Truncation now usesisel. Padding builds the padded variables first and then installs the extended coordinate, keeping dtype, attrs and variable order.units_policy="warn"never warned, and integer surface variables could not be edited (surf/sfile.py,d66fa7a):"warn"behaved exactly like"ignore"; it now emits aUserWarning.URBAN_REGION_ID) uneditable.