Skip to content

Commit f859e12

Browse files
authored
Ungrib update (#779)
1 parent b42a490 commit f859e12

File tree

18 files changed

+351
-165
lines changed

18 files changed

+351
-165
lines changed
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
{
2+
"$defs": {
3+
"iso8601": {
4+
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}$",
5+
"type": [
6+
"datetime",
7+
"string"
8+
]
9+
}
10+
},
211
"properties": {
312
"ungrib": {
413
"additionalProperties": false,
@@ -9,12 +18,3 @@
918
"batchargs": {
1019
"additionalProperties": true,
1120
"properties": {
12-
"cores": {
13-
"type": "integer"
14-
},
15-
"debug": {
16-
"type": "boolean"
17-
},
18-
"exclusive": {
19-
"type": "boolean"
20-
},

docs/sections/user_guide/yaml/components/ungrib.rst

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,27 @@ See :ref:`this page <execution_yaml>` for details.
2323
gribfiles:
2424
^^^^^^^^^^
2525

26-
Describes the GRIB-formatted files to be processed by ``ungrib``.
26+
A list of paths to the GRIB-formatted files to be processed by ``ungrib``.
2727

28-
**interval_hours:**
29-
30-
Frequency interval of the given files, in integer hours.
31-
32-
**max_leadtime:**
28+
rundir:
29+
^^^^^^^
3330

34-
The maximum forecast leadtime to process. This may be the same as the forecast length, or a lower leadtime.
31+
The path to the run directory.
3532

36-
**offset:**
33+
start:
34+
^^^^^^
3735

38-
How many hours earlier the external model used for boundary conditions started compared to the desired forecast cycle, in integer hours.
36+
The validtime of the initial element of ``gribfiles`` as an ISO8601 timestamp.
3937

40-
**path:**
38+
step:
39+
^^^^^
4140

42-
An absolute-path template to the GRIB-formatted files to be processed by ``ungrib``. The Python ``int`` variables ``cycle_hour`` and ``forecast_hour`` will be interpolated into, e.g., ``/path/to/gfs.t{cycle_hour:02d}z.pgrb2.0p25.f{forecast_hour:03d}``. Note that this is a Python string template rather than a Jinja2 template.
41+
The interval between successive elements of ``gribfiles`` as integer hours or as a string of the form ``hours[:minutes[:seconds]]``, where the ``hours``, ``minutes``, and ``seconds`` components are (possibly zero-padded) integers.
4342

44-
rundir:
45-
^^^^^^^
43+
stop:
44+
^^^^^
4645

47-
The path to the run directory.
46+
The validtime of the final element of ``gribfiles`` as an ISO8601 timestamp.
4847

4948
vtable:
5049
^^^^^^^

docs/sections/user_guide/yaml/tags.rst

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,11 @@ Converts the tagged node to a Python ``bool`` object. For example, given ``input
3838
3939
.. code-block:: text
4040
41-
$ uw config realize -i ../input.yaml --output-format yaml
41+
$ uw config realize -i input.yaml --output-format yaml
4242
flag1: True
4343
flag2: True
4444
flag3: False
4545
46-
4746
``!datetime``
4847
^^^^^^^^^^^^^
4948

@@ -56,9 +55,9 @@ Converts the tagged node to a Python ``datetime`` object. For example, given ``i
5655
5756
.. code-block:: text
5857
59-
$ uw config realize -i ../input.yaml --output-format yaml
58+
$ uw config realize -i input.yaml --output-format yaml
6059
date1: 2024-09-01
61-
date2: 2024-09-01 00:00:00
60+
date2: 2024-09-01T00:00:00
6261
6362
The value provided to the tag must be in :python:`ISO 8601 format<datetime.html#datetime.datetime.fromisoformat>` to be interpreted correctly by the ``!datetime`` tag.
6463

@@ -76,7 +75,7 @@ Converts the tagged node to a Python ``dict`` value. For example, given ``input.
7675
7776
.. code-block:: text
7877
79-
$ uw config realize --input-file input.yaml --output-format yaml
78+
$ uw config realize -i input.yaml --output-format yaml
8079
d1: {'k0': 0, 'k1': 1, 'k2': 2}
8180
d2: {'k0': 0, 'k1': 1, 'k2': 2}
8281
d3: {'k0': 0, 'k1': 1, 'k2': 2}
@@ -93,7 +92,7 @@ Converts the tagged node to a Python ``float`` value. For example, given ``input
9392
9493
.. code-block:: text
9594
96-
$ uw config realize --input-file input.yaml --output-format yaml
95+
$ uw config realize -i input.yaml --output-format yaml
9796
f2: 5.859
9897
9998
``!glob``
@@ -119,7 +118,7 @@ and ``constants.yaml``:
119118
120119
.. code-block:: text
121120
122-
$ uw config realize --input-file numbers.yaml --output-format yaml
121+
$ uw config realize -i numbers.yaml --output-format yaml
123122
values:
124123
e: 2.718
125124
pi: 3.141
@@ -146,7 +145,7 @@ and ``pi.yaml``:
146145
147146
.. code-block:: text
148147
149-
$ uw config realize --input-file numbers.yaml --output-format yaml
148+
$ uw config realize -i numbers.yaml --output-format yaml
150149
values:
151150
constants:
152151
pi: 3.141
@@ -164,7 +163,7 @@ Converts the tagged node to a Python ``int`` value. For example, given ``input.y
164163
165164
.. code-block:: text
166165
167-
$ uw config realize --input-file input.yaml --output-format yaml
166+
$ uw config realize -i input.yaml --output-format yaml
168167
f1: 3
169168
f2: 11
170169
f2: 140
@@ -182,7 +181,7 @@ Converts the tagged node to a Python ``list`` value. For example, given ``input.
182181
183182
.. code-block:: text
184183
185-
$ uw config realize --input-file input.yaml --output-format yaml
184+
$ uw config realize -i input.yaml --output-format yaml
186185
l1: [1, 2, 3]
187186
l2: ['a0', 'a1', 'a2']
188187
l3: ['a0', 'a1', 'a2']
@@ -205,5 +204,26 @@ and ``update.yaml``:
205204
206205
.. code-block:: text
207206
208-
$ uw config realize --input-file input.yaml --update-file update.yaml --output-format yaml
207+
$ uw config realize -i input.yaml --update-file update.yaml --output-format yaml
209208
pi: 3.141
209+
210+
``!timedelta``
211+
^^^^^^^^^^^^^^
212+
213+
Converts the tagged node to a Python ``timedelta`` object. For example:
214+
215+
216+
.. code-block:: yaml
217+
218+
base: !datetime 2025-07-31T12:00:00
219+
offset: !timedelta 6
220+
cycle: !datetime "{{ base + offset }}"
221+
222+
.. code-block:: text
223+
224+
$ uw config realize -i input.yaml --output-format yaml
225+
base: 2025-07-31T12:00:00
226+
offset: !timedelta '6:00:00'
227+
cycle: 2025-07-31T18:00:00
228+
229+
The value provided to the tag may be an integer number of hours, or a string of the form ``hours[:minutes[:seconds]]``, where the ``hours``, ``minutes``, and ``seconds`` components are (possibly zero-padded) integers.

docs/shared/ungrib.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ ungrib:
55
walltime: "00:01:00"
66
executable: /path/to/ungrib.exe
77
gribfiles:
8-
interval_hours: 6
9-
max_leadtime: 24
10-
offset: 0
11-
path: /path/to/dir/gfs.t{cycle_hour:02d}z.pgrb2.0p25.f{forecast_hour:03d}
8+
- /path/to/dir/rap.t00z.wrfnatf00.grib2
9+
- /path/to/dir/rap.t00z.wrfnatf06.grib2
10+
- /path/to/dir/rap.t00z.wrfnatf12.grib2
1211
rundir: /path/to/run/dir
12+
start: 2025-07-31T00:00:00
13+
step: 6
14+
stop: 2025-07-31T12:00:00
1315
vtable: /path/to/Vtable.GFS
1416
platform:
1517
account: me

src/uwtools/config/formats/yaml.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections import OrderedDict
4+
from datetime import datetime, timedelta
45
from types import SimpleNamespace as ns
56
from typing import TYPE_CHECKING
67

@@ -21,6 +22,7 @@
2122
from uwtools.exceptions import UWConfigError
2223
from uwtools.strings import FORMAT
2324
from uwtools.utils.file import readable, writable
25+
from uwtools.utils.time import to_iso8601
2426

2527
if TYPE_CHECKING:
2628
from pathlib import Path
@@ -66,6 +68,16 @@ def _add_yaml_representers(cls) -> None:
6668
"""
6769
yaml.add_representer(Namelist, cls._represent_namelist)
6870
yaml.add_representer(OrderedDict, cls._represent_ordereddict)
71+
yaml.add_representer(
72+
datetime,
73+
lambda dumper, data: dumper.represent_scalar(
74+
"tag:yaml.org,2002:timestamp", to_iso8601(data)
75+
),
76+
)
77+
yaml.add_representer(
78+
timedelta,
79+
lambda dumper, data: dumper.represent_scalar("!timedelta", str(data)),
80+
)
6981
for tag_class in [UWYAMLConvert, UWYAMLGlob, UWYAMLRemove]:
7082
yaml.add_representer(tag_class, tag_class.represent)
7183

src/uwtools/config/jinja2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
import os
8-
from datetime import datetime
8+
from datetime import datetime, timedelta
99
from functools import cached_property
1010
from pathlib import Path
1111
from typing import Union
@@ -25,7 +25,7 @@
2525
from uwtools.utils.file import get_file_format, readable, writable
2626

2727
_ConfigVal = Union[
28-
bool, datetime, dict, float, int, list, str, UWYAMLConvert, UWYAMLGlob, UWYAMLRemove
28+
bool, datetime, dict, float, int, list, str, timedelta, UWYAMLConvert, UWYAMLGlob, UWYAMLRemove
2929
]
3030

3131

src/uwtools/config/support.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import math
4-
from datetime import datetime
4+
from datetime import datetime, timedelta
55
from functools import partial
66
from importlib import import_module
77
from typing import TYPE_CHECKING, Callable, Union
@@ -11,6 +11,7 @@
1111
from uwtools.exceptions import UWConfigError
1212
from uwtools.logging import log
1313
from uwtools.strings import FORMAT
14+
from uwtools.utils.time import to_datetime, to_timedelta
1415

1516
if TYPE_CHECKING:
1617
from collections import OrderedDict
@@ -114,10 +115,7 @@ def __repr__(self) -> str:
114115
return ("%s %s" % (self.tag, self.value)).strip()
115116

116117
@staticmethod
117-
def represent(
118-
_dumper: yaml.Dumper,
119-
data: UWYAMLTag,
120-
) -> yaml.nodes.Node:
118+
def represent(_dumper: yaml.Dumper, data: UWYAMLTag) -> yaml.nodes.Node:
121119
"""
122120
Serialize a scalar value as "!type value".
123121
@@ -153,8 +151,8 @@ class UWYAMLConvert(UWYAMLTaggedStr):
153151
Support for YAML tags that specify type conversions.
154152
"""
155153

156-
TAGS = ("!bool", "!datetime", "!dict", "!float", "!int", "!list")
157-
TaggedValT = Union[bool, datetime, dict, float, int, list]
154+
TAGS = ("!bool", "!datetime", "!dict", "!float", "!int", "!list", "!timedelta")
155+
TaggedValT = Union[bool, datetime, dict, float, int, list, timedelta]
158156

159157
def __repr__(self) -> str:
160158
return "%s %s" % (self.tag, self.converted)
@@ -171,12 +169,13 @@ def converted(self) -> UWYAMLConvert.TaggedValT:
171169
"""
172170
load_as = lambda t, v: t(yaml.safe_load(v))
173171
converters: list[Callable[..., UWYAMLConvert.TaggedValT]] = [
174-
partial(load_as, bool),
175-
datetime.fromisoformat,
176-
partial(load_as, dict),
177-
float,
178-
int,
179-
partial(load_as, list),
172+
partial(load_as, bool), # !bool
173+
to_datetime, # !datetime
174+
partial(load_as, dict), # !dict
175+
float, # !float
176+
int, # !int
177+
partial(load_as, list), # !list
178+
to_timedelta, # !timedelta
180179
]
181180
return dict(zip(UWYAMLConvert.TAGS, converters))[self.tag](self.value)
182181

src/uwtools/config/validator.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
import json
8-
from datetime import datetime
8+
from datetime import datetime, timedelta
99
from functools import cache
1010
from pathlib import Path
1111
from typing import TYPE_CHECKING, Union
@@ -196,9 +196,13 @@ def _validation_errors(config: JSONValueT, schema: dict) -> list[ValidationError
196196
:return: Any validation errors.
197197
"""
198198
base = Draft202012Validator
199-
type_checker = base.TYPE_CHECKER.redefine(
200-
"fs_src", lambda _, x: any(isinstance(x, t) for t in [str, UWYAMLGlob])
201-
).redefine("datetime", lambda _, x: isinstance(x, datetime))
199+
type_checker = (
200+
base.TYPE_CHECKER.redefine(
201+
"fs_src", lambda _, x: any(isinstance(x, t) for t in [str, UWYAMLGlob])
202+
)
203+
.redefine("datetime", lambda _, x: isinstance(x, datetime))
204+
.redefine("timedelta", lambda _, x: isinstance(x, timedelta))
205+
)
202206
uwvalidator = validators.extend(base, type_checker=type_checker)
203207
validator = uwvalidator(schema, registry=_registry())
204208
return list(validator.iter_errors(config))

0 commit comments

Comments
 (0)