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/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/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/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/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" } 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/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_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)) 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_util.py b/src/wxvx/tests/test_util.py index 9c686ab..a153f5f 100644 --- a/src/wxvx/tests/test_util.py +++ b/src/wxvx/tests/test_util.py @@ -75,11 +75,15 @@ 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 isinstance(new, Path) + assert new.is_absolute() def test_workflow_classify_url_unsupported(): 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/tests/test_workflow.py b/src/wxvx/tests/test_workflow.py index dba3be1..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 = "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/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) 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. diff --git a/src/wxvx/workflow.py b/src/wxvx/workflow.py index 1e0b79f..d4881fc 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, @@ -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))