From 26bc639cb0c61ddfeb05c7806e91df617d1ee2ef Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 19:14:18 +0000 Subject: [PATCH 1/8] Set version to 0.7.1 --- recipe/meta.json | 2 +- src/wxvx/resources/info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/recipe/meta.json b/recipe/meta.json index 0840374..14b78a1 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -48,5 +48,5 @@ "zarr ==3.1.*" ] }, - "version": "0.7.0" + "version": "0.7.1" } diff --git a/src/wxvx/resources/info.json b/src/wxvx/resources/info.json index e279444..3eb8efe 100644 --- a/src/wxvx/resources/info.json +++ b/src/wxvx/resources/info.json @@ -1,4 +1,4 @@ { "buildnum": "0", - "version": "0.7.0" + "version": "0.7.1" } From 19db1a33bc80d7fb37bb2fd2cee3b5e668e0bdb8 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 19:14:31 +0000 Subject: [PATCH 2/8] Make forecast local paths absolute --- src/wxvx/tests/test_util.py | 5 ++++- src/wxvx/util.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wxvx/tests/test_util.py b/src/wxvx/tests/test_util.py index 9c686ab..89bc5d6 100644 --- a/src/wxvx/tests/test_util.py +++ b/src/wxvx/tests/test_util.py @@ -75,11 +75,14 @@ def test_util_classify_data_format__pass_zarr(tmp_path): ("http://example.com/path/to/gfs.t00z.pgrb2.0p25.f001", util.Proximity.REMOTE), ("file:///path/to/gfs.t00z.pgrb2.0p25.f001", util.Proximity.LOCAL), ("/path/to/gfs.t00z.pgrb2.0p25.f001", util.Proximity.LOCAL), + ("gfs.t00z.pgrb2.0p25.f001", util.Proximity.LOCAL), ], ) def test_workflow_classify_url(expected_scheme, url): - scheme, _ = util.classify_url(url) + scheme, new = util.classify_url(url) assert scheme == expected_scheme + if scheme == util.Proximity.LOCAL: + assert new.is_absolute() def test_workflow_classify_url_unsupported(): diff --git a/src/wxvx/util.py b/src/wxvx/util.py index 350026f..b353ab8 100644 --- a/src/wxvx/util.py +++ b/src/wxvx/util.py @@ -111,7 +111,7 @@ def classify_url(url: str) -> tuple[Proximity, str | Path]: if p.scheme in {"http", "https"}: return Proximity.REMOTE, url if p.scheme in {"file", ""}: - return Proximity.LOCAL, Path(p.path if p.scheme else url) + return Proximity.LOCAL, Path(p.path if p.scheme else url).resolve() msg = f"Scheme '{p.scheme}' in '{url}' not supported." raise WXVXError(msg) From b58e126be74e4d4b484bc2d0259636d2341552fd Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 19:21:29 +0000 Subject: [PATCH 3/8] Help typechecker --- src/wxvx/tests/test_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wxvx/tests/test_util.py b/src/wxvx/tests/test_util.py index 89bc5d6..a153f5f 100644 --- a/src/wxvx/tests/test_util.py +++ b/src/wxvx/tests/test_util.py @@ -82,6 +82,7 @@ def test_workflow_classify_url(expected_scheme, url): scheme, new = util.classify_url(url) assert scheme == expected_scheme if scheme == util.Proximity.LOCAL: + assert isinstance(new, Path) assert new.is_absolute() From cdaee15f0029734e51ea337ead0a6e070b8be937 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 19:48:32 +0000 Subject: [PATCH 4/8] Make grid_stat, point_stat, pb2nc input file paths absolute --- src/wxvx/workflow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wxvx/workflow.py b/src/wxvx/workflow.py index 1e0b79f..ebf2997 100644 --- a/src/wxvx/workflow.py +++ b/src/wxvx/workflow.py @@ -425,7 +425,7 @@ def _netcdf_from_obs(c: Config, tc: TimeCoords): yield {"cfgfile": cfgfile, "prepbufr": prepbufr} runscript = cfgfile.ref.with_suffix(".sh") content = "exec time pb2nc -v 4 {prepbufr} {netcdf} {config} >{log} 2>&1".format( - prepbufr=prepbufr.ref, + prepbufr=prepbufr.ref.resolve(), netcdf=path, config=cfgfile.ref, log=f"{path.stem}.log", @@ -515,7 +515,7 @@ def _stats_vs_grid(c: Config, varname: str, tc: TimeCoords, var: Var, prefix: st export OMP_NUM_THREADS=1 exec time grid_stat -v 4 {fcst} {obs} {config} >{log} 2>&1 """.format( - fcst=fcst.ref, + fcst=fcst.ref.resolve(), obs=obs.ref, config=config.ref, log=f"{path.stem}.log", @@ -552,7 +552,7 @@ def _stats_vs_obs(c: Config, varname: str, tc: TimeCoords, var: Var, prefix: str yield reqs runscript = path.with_suffix(".sh") content = "exec time point_stat -v 4 {fcst} {obs} {config} -outdir {rundir} >{log} 2>&1".format( - fcst=fcst.ref, + fcst=fcst.ref.resolve(), obs=obs.ref, config=config.ref, rundir=rundir, From 359ee70295903af8ad6d2126736b2b1131d4e411 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 20:39:09 +0000 Subject: [PATCH 5/8] Handle spaces in forecast dataset names --- src/wxvx/tests/conftest.py | 2 +- src/wxvx/tests/test_types.py | 2 +- src/wxvx/tests/test_workflow.py | 8 ++++---- src/wxvx/workflow.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wxvx/tests/conftest.py b/src/wxvx/tests/conftest.py index 60be8f5..52dae44 100644 --- a/src/wxvx/tests/conftest.py +++ b/src/wxvx/tests/conftest.py @@ -81,7 +81,7 @@ def config_data(): [21.138123, 275.0], [21.138123, 225.90452027], ], - S.name: "Forecast", + S.name: "Forecast Model", S.path: "/path/to/forecast-{{ yyyymmdd }}-{{ hh }}-{{ '%03d' % fh }}.nc", S.projection: { "a": 6371229, diff --git a/src/wxvx/tests/test_types.py b/src/wxvx/tests/test_types.py index 989fb17..b8ce12e 100644 --- a/src/wxvx/tests/test_types.py +++ b/src/wxvx/tests/test_types.py @@ -248,7 +248,7 @@ def test_types_Forecast(config_data, forecast): assert obj.coords.time.inittime == "time" assert obj.coords.time.leadtime == "lead_time" assert obj.format is None - assert obj.name == "Forecast" + assert obj.name == "Forecast Model" assert obj.path == "/path/to/forecast-{{ yyyymmdd }}-{{ hh }}-{{ '%03d' % fh }}.nc" cfg = config_data[S.forecast] other1 = types.Forecast(**cfg) diff --git a/src/wxvx/tests/test_workflow.py b/src/wxvx/tests/test_workflow.py index dba3be1..8cd1d77 100644 --- a/src/wxvx/tests/test_workflow.py +++ b/src/wxvx/tests/test_workflow.py @@ -275,7 +275,7 @@ def test_workflow__config_point_stat__atm(c, fakefs, fmt, testvars, tidy): val = "ADPSFC"; } ]; - model = "Forecast"; + model = "Forecast Model"; obs = { field = [ { @@ -348,7 +348,7 @@ def test_workflow__config_point_stat__sfc(c, fakefs, fmt, testvars, tidy): val = "ADPSFC"; } ]; - model = "Forecast"; + model = "Forecast Model"; obs = { field = [ { @@ -879,7 +879,7 @@ def test_workflow__stat_reqs(baseline_name, c, statkit, cycle): args = (c, statkit.varname, statkit.tc, statkit.var) assert _stats_vs_grid.call_args_list[0].args == ( *args, - f"forecast_gh_{statkit.level_type}_{statkit.level:04d}", + f"forecast_model_gh_{statkit.level_type}_{statkit.level:04d}", Source.FORECAST, ) @@ -984,7 +984,7 @@ def statkit(tc, testvars): return ns( level=level, level_type=level_type, - prefix=f"forecast_gh_{level_type}_{level:04d}", + prefix=f"forecast_model_gh_{level_type}_{level:04d}", source=Source.FORECAST, tc=tc, var=testvars[EC.gh], diff --git a/src/wxvx/workflow.py b/src/wxvx/workflow.py index ebf2997..d4881fc 100644 --- a/src/wxvx/workflow.py +++ b/src/wxvx/workflow.py @@ -670,7 +670,7 @@ def _stat_args( cycles = c.cycles sections = {Source.BASELINE: c.baseline, Source.FORECAST: c.forecast, Source.TRUTH: c.truth} name = cast(Named, sections[source]).name.lower() - prefix = lambda var: "%s_%s" % (name, str(var).replace("-", "_")) + prefix = lambda var: "%s_%s" % (name.replace(" ", "_"), str(var).replace("-", "_")) args = [ (c, vn, tc, var, prefix(var), source) for (var, vn), tc in product(_vxvars(c).items(), gen_timecoords(cycles, c.leadtimes)) From 47c8fafaf4d4c2cb8545a014783189ccf7721537 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 20:45:40 +0000 Subject: [PATCH 6/8] Spaces to underscores in model name in MET configs --- src/wxvx/metconf.py | 5 ++++- src/wxvx/tests/test_workflow.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wxvx/metconf.py b/src/wxvx/metconf.py index 6cb8b7e..d2fc37f 100644 --- a/src/wxvx/metconf.py +++ b/src/wxvx/metconf.py @@ -226,8 +226,11 @@ def _top(k: str, v: Any, level: int) -> list[str]: case MET.quality_mark_thresh: return _kvpair(k, _bare(v), level) # Scalar: quoted. - case MET.model | MET.obtype | MET.output_prefix | MET.tmp_dir: + case MET.obtype | MET.output_prefix | MET.tmp_dir: return _kvpair(k, _quoted(v), level) + # Scalar: quoted with no whitespace. + case MET.model: + return _kvpair(k, _quoted(v.replace(" ", "_")), level) # Sequence: quoted. case MET.message_type | MET.obs_bufr_var: return _sequence(k, v, _quoted, level) diff --git a/src/wxvx/tests/test_workflow.py b/src/wxvx/tests/test_workflow.py index 8cd1d77..7148c37 100644 --- a/src/wxvx/tests/test_workflow.py +++ b/src/wxvx/tests/test_workflow.py @@ -275,7 +275,7 @@ def test_workflow__config_point_stat__atm(c, fakefs, fmt, testvars, tidy): val = "ADPSFC"; } ]; - model = "Forecast Model"; + model = "Forecast_Model"; obs = { field = [ { @@ -348,7 +348,7 @@ def test_workflow__config_point_stat__sfc(c, fakefs, fmt, testvars, tidy): val = "ADPSFC"; } ]; - model = "Forecast Model"; + model = "Forecast_Model"; obs = { field = [ { From 44052fbe1456e87703148b9d93ea7a8a43af144c Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 21:26:00 +0000 Subject: [PATCH 7/8] Make paths.grids.forecast (conditionally) optional --- README.md | 2 +- src/wxvx/resources/config.jsonschema | 3 --- src/wxvx/tests/test_schema.py | 6 +----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 732abf7..81a1138 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ Where to store grids extracted from baseline datasets. When `baseline.name` is ` ### paths.grids.forecast -Where to store grids extracted from netCDF or Zarr forecast datasets. GRIB forecast datasets currently must be local, and grids are processed directly from their containing files without being extracted into separate files. +Where to store grids extracted from netCDF or Zarr forecast datasets. GRIB forecast datasets currently must be local, and grids are processed directly from their containing files without being extracted into separate files. This value is required for netCDF and Zarr datasets, and ignored for local GRIB datasets. ### paths.grids.truth diff --git a/src/wxvx/resources/config.jsonschema b/src/wxvx/resources/config.jsonschema index 220f84a..d9c2c72 100644 --- a/src/wxvx/resources/config.jsonschema +++ b/src/wxvx/resources/config.jsonschema @@ -334,9 +334,6 @@ "type": "string" } }, - "required": [ - "forecast" - ], "type": "object" }, "obs": { diff --git a/src/wxvx/tests/test_schema.py b/src/wxvx/tests/test_schema.py index b651693..1241060 100644 --- a/src/wxvx/tests/test_schema.py +++ b/src/wxvx/tests/test_schema.py @@ -271,12 +271,8 @@ def test_schema_paths_grids(config_data, fs, logged): for key in [S.forecast, S.truth]: assert not ok(with_set(config, None, key)) assert logged("None is not of type 'string'") - # Some values are required: - for key in [S.forecast]: - assert not ok(with_del(config, key)) - assert logged(f"'{key}' is a required property") # Some values are optional: - for key in [S.baseline]: + for key in [S.baseline, S.forecast]: assert ok(with_del(config, key)) From 1940135be23209b2f2d76edc0dd580763935e215 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Thu, 26 Mar 2026 21:41:25 +0000 Subject: [PATCH 8/8] Add GDAS class, synonymous with GFS --- src/wxvx/strings.py | 1 + src/wxvx/tests/test_variables.py | 2 +- src/wxvx/variables.py | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/wxvx/strings.py b/src/wxvx/strings.py index 810b538..7b6f18f 100644 --- a/src/wxvx/strings.py +++ b/src/wxvx/strings.py @@ -132,6 +132,7 @@ class _S(_ValsMatchKeys): Strings defined by wxvx, plus strings from various other sources. """ + GDAS: str = _ GFS: str = _ HRRR: str = _ OBS: str = _ diff --git a/src/wxvx/tests/test_variables.py b/src/wxvx/tests/test_variables.py index 553bbb4..8eb5f92 100644 --- a/src/wxvx/tests/test_variables.py +++ b/src/wxvx/tests/test_variables.py @@ -248,7 +248,7 @@ class C1(B): ... class C2(B): ... assert variables.model_names(A) == {"B", "C1", "C2"} - assert variables.model_names() == {S.GFS, S.HRRR, S.PREPBUFR} + assert variables.model_names() == {S.GDAS, S.GFS, S.HRRR, S.PREPBUFR} def test_variables__da_val__fail_unparesable(da_flat): diff --git a/src/wxvx/variables.py b/src/wxvx/variables.py index 901ed8a..f1a3e9d 100644 --- a/src/wxvx/variables.py +++ b/src/wxvx/variables.py @@ -220,6 +220,12 @@ def _levinfo(levstr: str) -> tuple[str, float | int | None]: return (UNKNOWN, None) +class GDAS(GFS): + """ + A GDAS variable. + """ + + class HRRR(GFS): """ A HRRR variable.