From 3c87ba75f857d0cba6254610f5894d3490027410 Mon Sep 17 00:00:00 2001 From: Zhongwang Wei Date: Thu, 16 Jul 2026 22:31:05 +0800 Subject: [PATCH 1/2] Restore CI contracts after registry merge Constraint: Preserve OPENBENCH_HOME station-list behavior introduced by PR #134. Rejected: Reverting station-list output to reference/catalog roots | would discard read-only reference-root support. Confidence: high Scope-risk: moderate Directive: Keep bundled station entries paired with fulllist or station_matching. Tested: ruff check src/ tests/; ruff format --check src/ tests/; pytest tests/ -q --tb=short (1572 passed, 10 skipped) Not-tested: GitHub Actions matrix after push --- src/openbench/cli/model.py | 12 +- src/openbench/core/metrics.py | 2 +- .../data/registry/model_catalog.yaml | 7 +- .../data/registry/reference_catalog.yaml | 214 +++--------------- .../test_scanner_registration.py | 19 +- 5 files changed, 57 insertions(+), 197 deletions(-) diff --git a/src/openbench/cli/model.py b/src/openbench/cli/model.py index 4bdf31f0..d2708fac 100644 --- a/src/openbench/cli/model.py +++ b/src/openbench/cli/model.py @@ -125,6 +125,7 @@ def _validate_expression(expr: str, *, label: str, allowed_names: set[str] | Non return from openbench.data.compute import ( ComputeError, + _split_assignment, ) from openbench.data.compute import ( _validate_expression as validate_one, @@ -137,12 +138,11 @@ def _validate_expression(expr: str, *, label: str, allowed_names: set[str] | Non # unknown/dangerous identifiers. active_allowed_names = {"value", "np", *(allowed_names or set())} for part in parts: - if "=" in part and not part.startswith("("): - target, rhs = part.split("=", 1) - validate_one(rhs.strip(), allowed_names=active_allowed_names) - target = target.strip() - if target.isidentifier(): - active_allowed_names.add(target) + assignment = _split_assignment(part) + if assignment is not None: + target, rhs = assignment + validate_one(rhs, allowed_names=active_allowed_names) + active_allowed_names.add(target) else: validate_one(part, allowed_names=active_allowed_names) except ComputeError as exc: diff --git a/src/openbench/core/metrics.py b/src/openbench/core/metrics.py index 18bb5e3c..4e34260d 100644 --- a/src/openbench/core/metrics.py +++ b/src/openbench/core/metrics.py @@ -1183,7 +1183,7 @@ def FFT_component(sim, obs): fft_sim = np.fft.rfft(sim) # Selects the strongest observed Fourier component representing at least two cycles across the record - dominant_freq_idx = np.argmax(np.abs(fft_obs[2 : ])) + 2 + dominant_freq_idx = np.argmax(np.abs(fft_obs[2:])) + 2 # Calculate phase difference phase_obs = np.angle(fft_obs) diff --git a/src/openbench/data/registry/model_catalog.yaml b/src/openbench/data/registry/model_catalog.yaml index 3d150449..6e648c1b 100644 --- a/src/openbench/data/registry/model_catalog.yaml +++ b/src/openbench/data/registry/model_catalog.yaml @@ -2582,12 +2582,9 @@ TE: name: TE tim_res: Month variables: - Surface_Albedo: - varname: alb + Albedo: + varname: ALB varunit: percentage - fallbacks: - - varname: ALB - varunit: percentage Canopy_Evaporation: varname: EIFLX varunit: kg m-2 s-1 diff --git a/src/openbench/data/registry/reference_catalog.yaml b/src/openbench/data/registry/reference_catalog.yaml index 92225435..69fc8d2e 100644 --- a/src/openbench/data/registry/reference_catalog.yaml +++ b/src/openbench/data/registry/reference_catalog.yaml @@ -3352,8 +3352,8 @@ OpenBench_FLUX_30min: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/30min prefix: OpenBench_FLUX_30min_full -# station_matching: -# dataset_file: OpenBench_FLUX_30min_full.nc + station_matching: + dataset_file: OpenBench_FLUX_30min_full.nc variables: &openbench_flux_catalog_variables Ground_Heat: sub_dir: '' @@ -3444,8 +3444,8 @@ OpenBench_FLUX_Hourly: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/hourly prefix: OpenBench_FLUX_hourly_full -# station_matching: -# dataset_file: OpenBench_FLUX_hourly_full.nc + station_matching: + dataset_file: OpenBench_FLUX_hourly_full.nc variables: *openbench_flux_catalog_variables _provenance: data_groupby: profile @@ -3460,8 +3460,8 @@ OpenBench_FLUX_Daily: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/daily prefix: OpenBench_FLUX_daily_full -# station_matching: -# dataset_file: OpenBench_FLUX_daily_full.nc + station_matching: + dataset_file: OpenBench_FLUX_daily_full.nc variables: *openbench_flux_catalog_variables _provenance: data_groupby: profile @@ -3476,8 +3476,8 @@ OpenBench_FLUX_Daily_Benchmark: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/daily_benchmark prefix: OpenBench_FLUX_daily_benchmark_full -# station_matching: -# dataset_file: OpenBench_FLUX_daily_benchmark_full.nc + station_matching: + dataset_file: OpenBench_FLUX_daily_benchmark_full.nc variables: *openbench_flux_catalog_variables _provenance: data_groupby: profile @@ -3492,8 +3492,8 @@ OpenBench_FLUX_Hourly_Benchmark: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/hourly_benchmark prefix: OpenBench_FLUX_hourly_benchmark_full -# station_matching: -# dataset_file: OpenBench_FLUX_hourly_benchmark_full.nc + station_matching: + dataset_file: OpenBench_FLUX_hourly_benchmark_full.nc variables: *openbench_flux_catalog_variables _provenance: data_groupby: profile @@ -3508,8 +3508,8 @@ OpenBench_FLUX_Monthly_Benchmark: timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Composite/OpenBench_FLUX/monthly_benchmark prefix: OpenBench_FLUX_monthly_benchmark_full -# station_matching: -# dataset_file: OpenBench_FLUX_monthly_benchmark_full.nc + station_matching: + dataset_file: OpenBench_FLUX_monthly_benchmark_full.nc variables: *openbench_flux_catalog_variables _provenance: data_groupby: profile @@ -3523,8 +3523,8 @@ UrbanFlux_30min: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Anth/Urban/UrbanFlux/30min -# station_matching: -# dataset_file: OpenBench_UrbanFlux_30min.nc + station_matching: + dataset_file: OpenBench_UrbanFlux_30min.nc variables: &urbanflux_catalog_variables Sensible_Heat: sub_dir: '' @@ -3590,8 +3590,8 @@ UrbanFlux_Hourly: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Anth/Urban/UrbanFlux/hourly -# station_matching: -# dataset_file: OpenBench_UrbanFlux_Hourly.nc + station_matching: + dataset_file: OpenBench_UrbanFlux_Hourly.nc variables: *urbanflux_catalog_variables _provenance: data_groupby: profile @@ -3605,8 +3605,8 @@ UrbanFlux_Daily: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Anth/Urban/UrbanFlux/daily -# station_matching: -# dataset_file: OpenBench_UrbanFlux_Daily.nc + station_matching: + dataset_file: OpenBench_UrbanFlux_Daily.nc variables: *urbanflux_catalog_variables _provenance: data_groupby: profile @@ -3620,8 +3620,8 @@ LAI_Yuan2011_8Day: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Bio/Leaf_Area_Index/Yuan2011/8day -# station_matching: -# dataset_file: OpenBench_LAI_8day_Yuan2011.nc + station_matching: + dataset_file: OpenBench_LAI_8day_Yuan2011.nc variables: Leaf_Area_Index: sub_dir: '' @@ -3639,8 +3639,8 @@ LAI_Yuan2011_Monthly: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Bio/Leaf_Area_Index/Yuan2011/monthly -# station_matching: -# dataset_file: OpenBench_LAI_monthly_Yuan2011.nc + station_matching: + dataset_file: OpenBench_LAI_monthly_Yuan2011.nc variables: Leaf_Area_Index: sub_dir: '' @@ -3658,8 +3658,8 @@ Transpiration_OpenBench_Daily: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Water/Transpiration -# station_matching: -# dataset_file: OpenBench_Transpiration_Daily_redist.nc + station_matching: + dataset_file: OpenBench_Transpiration_Daily_redist.nc variables: &transpiration_daily_catalog_variables Canopy_Transpiration: sub_dir: '' @@ -3681,8 +3681,8 @@ Transpiration_SAPFLUXNET_Daily: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Water/Transpiration/SAPFLUXNET -# station_matching: -# dataset_file: OpenBench_Transpiration_Benchmark_Daily.nc + station_matching: + dataset_file: OpenBench_Transpiration_Benchmark_Daily.nc variables: *transpiration_daily_catalog_variables _provenance: data_groupby: profile @@ -3696,8 +3696,8 @@ Rodai2025_NPP: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Bio/Net_Primary_Production/Rodai2025 -# station_matching: -# dataset_file: OpenBench_NPP_Rodai2025.nc + station_matching: + dataset_file: OpenBench_NPP_Rodai2025.nc variables: Net_Primary_Production: sub_dir: '' @@ -3715,8 +3715,8 @@ G_REALM_LakeLevel: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Lake/Lake_Level/G_REALM -# station_matching: -# dataset_file: OpenBench_LAKE_mixed_G-REALM_LakeLevel.nc + station_matching: + dataset_file: OpenBench_LAKE_mixed_G-REALM_LakeLevel.nc variables: Lake_Level: sub_dir: '' @@ -3734,8 +3734,8 @@ GLAST_LakeSurfaceWaterTemperature: data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Lake/Lake_Surface_Water_Temperature/GLAST -# station_matching: -# dataset_file: OpenBench_LAKE_daily_GLAST_LakeSurfaceWaterTemperature.nc + station_matching: + dataset_file: OpenBench_LAKE_daily_GLAST_LakeSurfaceWaterTemperature.nc variables: Lake_Surface_Water_Temperature: sub_dir: '' @@ -3749,12 +3749,12 @@ ReaLSAT_LakeArea: description: ReaLSAT station lake area observations category: Lake data_type: stn - tim_res: Daily + tim_res: Month data_groupby: single timezone: 0 root_dir: ${OPENBENCH_REF_ROOT}/Station/Lake/Lake_Area/ReaLSAT -# station_matching: -# dataset_file: OpenBench_LAKE_monthly_ReaLSAT_LakeArea.nc + station_matching: + dataset_file: OpenBench_LAKE_monthly_ReaLSAT_LakeArea.nc variables: Lake_Area: sub_dir: '' @@ -3829,147 +3829,3 @@ GRFR_HigRes: data_groupby: profile tim_res: profile grid_res: nc -OpenBench_Heat_Monthly: - name: OpenBench_Heat_Monthly - description: OpenBench_Heat_Monthly reference dataset (Station) - category: Energy - data_type: stn - tim_res: Month - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Composite: - varname: Rn - varunit: W m-2 - sub_dir: Heat/Composite/OpenBench_Heat_Monthly - prefix: OpenBench_Heat_Monthly_full - _provenance: - data_groupby: scan - tim_res: nc -OpenBench_LH_Daily: - name: OpenBench_LH_Daily - description: OpenBench_LH_Daily reference dataset (Station) - category: Energy - data_type: stn - tim_res: Day - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Latent_Heat: - varname: Ta - varunit: degC - sub_dir: Heat/Latent_Heat/OpenBench_LH_Daily - prefix: OpenBench_LH_Daily_full - _provenance: - data_groupby: scan - tim_res: nc -OpenBench_SM_Daily: - name: OpenBench_SM_Daily - description: OpenBench_SM_Daily reference dataset (Station) - category: Water - data_type: stn - tim_res: Month - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Surface_Soil_Moisture: - varname: sm - varunit: m3/m3 - sub_dir: Water/Surface_Soil_Moisture/OpenBench_SM_Daily - prefix: OpenBench_SM_Daily_full - _provenance: - data_groupby: scan - tim_res: default -OpenBench_SM_Daily_ZYG: - name: OpenBench_SM_Daily_ZYG - description: OpenBench_SM_Daily_ZYG reference dataset (Station) - category: Water - data_type: stn - tim_res: Month - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Surface_Soil_Moisture: - varname: sm - varunit: m3/m3 - sub_dir: Water/Surface_Soil_Moisture/OpenBench_SM_Daily_ZYG - prefix: OpenBench_SM_Daily_full - _provenance: - data_groupby: scan - tim_res: default -OpenBench_SnowWE_Daily: - name: OpenBench_SnowWE_Daily - description: OpenBench_SnowWE_Daily reference dataset (Station) - category: Water - data_type: stn - tim_res: Day - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Snow_Water_Equivalent: - varname: swe - varunit: mm - sub_dir: Water/Snow_Water_Equivalent/OpenBench_SnowWE_Daily - prefix: OpenBench_SnowWE_Daily_redist - _provenance: - data_groupby: scan - tim_res: nc -OpenBench_WSE_Daily: - name: OpenBench_WSE_Daily - description: OpenBench_WSE_Daily reference dataset (Station) - category: Water - data_type: stn - tim_res: Day - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Water_Surface_Elevation: - varname: wse - varunit: m - sub_dir: Water/Water_Surface_Elevation/OpenBench_WSE_Daily - prefix: OpenBench_WSE_Daily_cgls-hydrosat-hydroweb_redist - _provenance: - data_groupby: scan - tim_res: nc -OpenBench_WTD_Daily: - name: OpenBench_WTD_Daily - description: OpenBench_WTD_Daily reference dataset (Station) - category: Water - data_type: stn - tim_res: Day - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Water_Table_Depth: - varname: wtd - varunit: m - sub_dir: Water/Water_Table_Depth/OpenBench_WTD_Daily - prefix: OpenBench_WTD_Daily_full - _provenance: - data_groupby: scan - tim_res: nc -OpenBench_WTD_Monthly: - name: OpenBench_WTD_Monthly - description: OpenBench_WTD_Monthly reference dataset (Station) - category: Water - data_type: stn - tim_res: Month - data_groupby: Single - timezone: 0 - root_dir: ${OPENBENCH_REF_ROOT}/Station - variables: - Water_Table_Depth: - varname: wtd - varunit: m - sub_dir: Water/Water_Table_Depth/OpenBench_WTD_Monthly - prefix: OpenBench_WTD_Monthly_full - _provenance: - data_groupby: scan - tim_res: nc diff --git a/tests/test_registry/test_scanner_registration.py b/tests/test_registry/test_scanner_registration.py index 65d5f7b1..5bb0a8e0 100644 --- a/tests/test_registry/test_scanner_registration.py +++ b/tests/test_registry/test_scanner_registration.py @@ -410,12 +410,17 @@ def fake_register_batch(datasets, on_multi_var=None, on_progress=None, catalog_p assert selected == [] -def test_register_scanned_dataset_writes_station_list_next_to_catalog(tmp_path: Path): +def test_register_scanned_dataset_writes_station_list_to_openbench_home( + tmp_path: Path, + monkeypatch, +): import numpy as np import xarray as xr from openbench.data.registry.scanner import register_scanned_dataset + home = tmp_path / "home" + monkeypatch.setenv("OPENBENCH_HOME", str(home)) nc_root = tmp_path / "station_nc" nc_root.mkdir() ds = xr.Dataset( @@ -442,8 +447,8 @@ def test_register_scanned_dataset_writes_station_list_next_to_catalog(tmp_path: register_scanned_dataset(scanned, catalog_path=catalog_path) catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) - expected = catalog_path.parent / "station_lists" / "DemoStation.csv" - assert catalog["DemoStation"]["fulllist"] == expected.as_posix() + expected = home / "station_lists" / "DemoStation.csv" + assert catalog["DemoStation"]["fulllist"] == "${OPENBENCH_HOME}/station_lists/DemoStation.csv" assert expected.exists() @@ -3825,7 +3830,7 @@ def test_profile_scan_unknown_layout_warns(monkeypatch, tmp_path: Path, caplog): assert "Unknown scan layout 'staion_direct' in profile 'TypoProfile'" in caplog.text -def test_station_generated_fulllist_uses_portable_reference_root( +def test_station_generated_fulllist_uses_portable_openbench_home( monkeypatch, tmp_path: Path, ): @@ -3847,6 +3852,8 @@ def test_station_generated_fulllist_uses_portable_reference_root( ) ds.to_netcdf(nc_dir / "S01_2010.nc") monkeypatch.setenv("OPENBENCH_REF_ROOT", str(ref_root)) + home = tmp_path / "home" + monkeypatch.setenv("OPENBENCH_HOME", str(home)) scanned = ScannedDataset( name="DemoStation", @@ -3861,8 +3868,8 @@ def test_station_generated_fulllist_uses_portable_reference_root( register_scanned_datasets_batch([scanned], catalog_path=catalog_path) entry = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))["DemoStation"] - assert entry["fulllist"] == "${OPENBENCH_REF_ROOT}/station_lists/DemoStation.csv" - assert (ref_root / "station_lists" / "DemoStation.csv").exists() + assert entry["fulllist"] == "${OPENBENCH_HOME}/station_lists/DemoStation.csv" + assert (home / "station_lists" / "DemoStation.csv").exists() def test_profile_file_glob_is_used_for_registration_inspection(monkeypatch, tmp_path: Path): From a183f578e7ee3771116a4eafb67ba8cc3c0f10b1 Mon Sep 17 00:00:00 2001 From: Zhongwang Wei Date: Thu, 16 Jul 2026 22:44:06 +0800 Subject: [PATCH 2/2] Prepare beta 9 for a reproducible release Constraint: PyPI release versions are immutable and 3.0.0b8 is already published. Rejected: Publishing from an unversioned working tree | would make source and package metadata diverge. Confidence: high Scope-risk: moderate Directive: Publish and tag only the merged main commit carrying version 3.0.0b9. Tested: version consistency tests; package build; twine check; build artifact tests; ruff check and format check. Not-tested: GitHub Actions matrix and PyPI upload. --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 2 +- conda/meta.yaml | 2 +- src/openbench/__init__.py | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d8f252..08ef3e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [3.0.0b9] - 2026-07-16 + +Beta release over 3.0.0b8 for registry coverage and CI stability. + +### Changed +- Station fulllists generated by scans are stored under `OPENBENCH_HOME`, + allowing shared reference roots to remain read-only. +- Expanded bundled model and reference metadata. + +### Fixed +- Compute validation now accepts keyword arguments such as `isel(lake=0)`. +- Restored required station-matching metadata and removed incomplete generated + station catalog entries. +- Simulation scanning retains the TE `Albedo` compatibility key. +- NetCDF evaluation writes no longer retain lazy references to closed source + datasets. +- Ruff formatting for the FFT component metric. + ## [3.0.0b8] - 2026-07-08 Patch release over 3.0.0b7 for model coverage, audit fixes, and CI stability. diff --git a/README.md b/README.md index 8a64d082..20d43835 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ conda install -c conda-forge colm-openbench mamba install -c conda-forge colm-openbench # or the faster mamba ``` -> **Note:** the current release is a Beta pre-release (`3.0.0b8`) and is **not on +> **Note:** the current release is a Beta pre-release (`3.0.0b9`) and is **not on > conda-forge yet**. Until it is, use one of the two paths below. If your platform does not have wheels for geospatial dependencies such as diff --git a/conda/meta.yaml b/conda/meta.yaml index b9462ea0..c5091660 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -1,6 +1,6 @@ {% set name = "colm-openbench" %} {% set dist_name = name|replace("-", "_") %} -{% set version = "3.0.0b8" %} +{% set version = "3.0.0b9" %} package: name: {{ name|lower }} diff --git a/src/openbench/__init__.py b/src/openbench/__init__.py index 06456961..61ffc83d 100644 --- a/src/openbench/__init__.py +++ b/src/openbench/__init__.py @@ -1,6 +1,6 @@ """OpenBench: Open Source Land Surface Model Benchmarking System.""" -__version__ = "3.0.0b8" +__version__ = "3.0.0b9" __author__ = "Zhongwang Wei, CoLM-SYSU" __title__ = "OpenBench" __description__ = "Land Surface Model Benchmarking System"