Skip to content

Commit

Permalink
UW-586: Build driver for WaveWatchIII (ufs-community#496)
Browse files Browse the repository at this point in the history
* base skeleton for the wavewatch3 component

* split driver base class
cycle subclassing still has issues

* Many fixes, some coverage missing

* test coverage fix

* move drivers together, add directory
tests need work

* fix logic for multiple files, still errors

* ww3 modified to match existing namelist behavior
directory test still failing
pending driver naming decisions

* fix namelist_file errors

* Apply suggestions from code review

Co-authored-by: Paul Madden <[email protected]>

* rename base to Assets and revert to Driver(Assets)
Combine all driver tests to one file

* re-remove test_standalonedriver

* multiple fixes to account for namelist changes

* Update src/uwtools/drivers/driver.py

Co-authored-by: Paul Madden <[email protected]>

* Update docs/sections/user_guide/yaml/components/ww3.rst

Co-authored-by: NaureenBharwaniNOAA <[email protected]>

* Next set of feedback comments
Needs template render update and fix schema

* convert from realizing to rendering in ww3

* fix test bug

* Apply suggestions from code review

Co-authored-by: Paul Madden <[email protected]>

* Move resources, fix validation, add schema tests

* add cycle, remove non-nml, rename to template
documentation updated
test currently failing, in progress

* Fix input file ref and doc error

* Add'l doc change

* Apply suggestions from code review

Co-authored-by: Paul Madden <[email protected]>

* Update src/uwtools/resources/jsonschema/ww3.jsonschema

Co-authored-by: Paul Madden <[email protected]>

* Update docs/shared/ww3.yaml

Co-authored-by: Paul Madden <[email protected]>

* test and doc fixes

* Apply suggestions from code review

Co-authored-by: Paul Madden <[email protected]>

* small doc format

* revert function name to namelist_file()

---------

Co-authored-by: Paul Madden <[email protected]>
Co-authored-by: NaureenBharwaniNOAA <[email protected]>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent 8303d82 commit bddcf4d
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 96 deletions.
1 change: 1 addition & 0 deletions docs/sections/user_guide/yaml/components/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ UW YAML for Components
shave
ungrib
upp
ww3
32 changes: 32 additions & 0 deletions docs/sections/user_guide/yaml/components/ww3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.. _ww3_yaml:

ww3
===

Structured YAML to configure WaveWatchIII as part of a compiled coupled executable is validated by JSON Schema and requires the ``ww3:`` block, described below.

Here is a prototype UW YAML ``ww3:`` block, explained in detail below:

.. highlight:: yaml
.. literalinclude:: ../../../../shared/ww3.yaml

UW YAML for the ``ww3:`` Block
------------------------------

namelist:
^^^^^^^^^

.. important:: The WaveWatchIII namelist file is provisioned by rendering an input template file containing Jinja2 expressions. Unlike namelist files provisioned by ``uwtools`` for other components, the WaveWatchIII namelist file will not be validated.

**template_file:**

The path to the input template file containing Jinja2 expressions (perhaps named ``ww3_shel.nml.IN``), based on the ``ww3_shel.nml`` file from the WaveWatchIII build. Note that the non-namelist ``ww3_shel.inp`` file may not be used as the basis for the input template file.

**template_values:**

Key-value pairs necessary to render all Jinja2 expressions in the input template file named by ``template_file:``.

run_dir:
^^^^^^^^

The path to the run directory.
9 changes: 9 additions & 0 deletions docs/shared/ww3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ww3:
namelist:
template_file: /path/to/ww3/ww3_shel.nml.IN
template_values:
input_forcing_winds: "C"
run_dir: /path/to/run/directory
platform:
account: me
scheduler: slurm
159 changes: 88 additions & 71 deletions src/uwtools/drivers/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
from uwtools.utils.processing import execute


class Driver(ABC):
class Assets(ABC):
"""
An abstract class for component drivers.
An abstract class to provision assets for component drivers.
"""

def __init__(
Expand Down Expand Up @@ -74,14 +74,6 @@ def provisioned_run_directory(self):
Run directory provisioned with all required content.
"""

@tasks
def run(self):
"""
A run.
"""
yield self._taskname("run")
yield (self._run_via_batch_submission() if self._batch else self._run_via_local_execution())

@external
def validate(self):
"""
Expand All @@ -90,29 +82,6 @@ def validate(self):
yield self._taskname("valid schema")
yield asset(None, lambda: True)

@task
def _run_via_batch_submission(self):
"""
A run executed via the batch system.
"""
yield self._taskname("run via batch submission")
path = Path("%s.submit" % self._runscript_path)
yield asset(path, path.is_file)
yield self.provisioned_run_directory()
self._scheduler.submit_job(runscript=self._runscript_path, submit_file=path)

@task
def _run_via_local_execution(self):
"""
A run executed directly on the local system.
"""
yield self._taskname("run via local execution")
path = self._rundir / f"done.{self._driver_name}"
yield asset(path, path.is_file)
yield self.provisioned_run_directory()
cmd = "{x} >{x}.out 2>&1".format(x=self._runscript_path)
execute(cmd=cmd, cwd=self._rundir, log_output=True)

# Private helper methods

@staticmethod
Expand Down Expand Up @@ -191,10 +160,95 @@ def _namelist_schema(
schema = schema[schema_key]
return schema

@property
def _rundir(self) -> Path:
"""
The path to the component's run directory.
"""
return Path(self._driver_config["run_dir"])

def _taskname(self, suffix: str) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s" % (self._driver_name, suffix)

def _taskname_with_cycle(self, cycle: datetime, suffix: str) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s %s" % (cycle.strftime("%Y%m%d %HZ"), self._driver_name, suffix)

def _taskname_with_cycle_and_leadtime(
self, cycle: datetime, leadtime: timedelta, suffix: str
) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s %s" % (
(cycle + leadtime).strftime("%Y%m%d %H:%M:%S"),
self._driver_name,
suffix,
)

def _validate(self) -> None:
"""
Perform all necessary schema validation.
"""
schema_name = self._driver_name.replace("_", "-")
validate_internal(schema_name=schema_name, config=self._config)


class Driver(Assets):
"""
An abstract class for standalone component drivers.
"""

# Workflow tasks

@tasks
def run(self):
"""
A run.
"""
yield self._taskname("run")
yield (self._run_via_batch_submission() if self._batch else self._run_via_local_execution())

@task
def _run_via_batch_submission(self):
"""
A run executed via the batch system.
"""
yield self._taskname("run via batch submission")
path = Path("%s.submit" % self._runscript_path)
yield asset(path, path.is_file)
yield self.provisioned_run_directory()
self._scheduler.submit_job(runscript=self._runscript_path, submit_file=path)

@task
def _run_via_local_execution(self):
"""
A run executed directly on the local system.
"""
yield self._taskname("run via local execution")
path = self._rundir / f"done.{self._driver_name}"
yield asset(path, path.is_file)
yield self.provisioned_run_directory()
cmd = "{x} >{x}.out 2>&1".format(x=self._runscript_path)
execute(cmd=cmd, cwd=self._rundir, log_output=True)

# Private helper methods

@property
def _resources(self) -> Dict[str, Any]:
"""
Returns configuration data for the runscript.
Returns platform configuration data.
"""
try:
platform = self._config["platform"]
Expand Down Expand Up @@ -222,13 +276,6 @@ def _runcmd(self) -> str:
]
return " ".join(filter(None, components))

@property
def _rundir(self) -> Path:
"""
The path to the component's run directory.
"""
return Path(self._driver_config["run_dir"])

def _runscript(
self,
execution: List[str],
Expand Down Expand Up @@ -279,36 +326,6 @@ def _scheduler(self) -> JobScheduler:
"""
return JobScheduler.get_scheduler(self._resources)

def _taskname(self, suffix: str) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s" % (self._driver_name, suffix)

def _taskname_with_cycle(self, cycle: datetime, suffix: str) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s %s" % (cycle.strftime("%Y%m%d %HZ"), self._driver_name, suffix)

def _taskname_with_cycle_and_leadtime(
self, cycle: datetime, leadtime: timedelta, suffix: str
) -> str:
"""
Returns a common tag for graph-task log messages.
:param suffix: Log-string suffix.
"""
return "%s %s %s" % (
(cycle + leadtime).strftime("%Y%m%d %H:%M:%S"),
self._driver_name,
suffix,
)

def _validate(self) -> None:
"""
Perform all necessary schema validation.
Expand Down
88 changes: 88 additions & 0 deletions src/uwtools/drivers/ww3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
An assets driver for ww3.
"""

from datetime import datetime
from pathlib import Path
from typing import List, Optional

from iotaa import asset, task, tasks

from uwtools.api.template import render
from uwtools.drivers.driver import Assets
from uwtools.strings import STR
from uwtools.utils.tasks import file


class WaveWatchIII(Assets):
"""
A library driver for ww3.
"""

def __init__(
self,
cycle: datetime,
config: Optional[Path] = None,
dry_run: bool = False,
batch: bool = False,
key_path: Optional[List[str]] = None,
):
"""
The driver.
:param cycle: The cycle.
:param config: Path to config file (read stdin if missing or None).
:param dry_run: Run in dry-run mode?
:param batch: Run component via the batch system?
:param key_path: Keys leading through the config to the driver's configuration block.
"""
super().__init__(
config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path
)
self._cycle = cycle

# Workflow tasks

@task
def namelist_file(self):
"""
Render the namelist from the template file.
"""
fn = "ww3_shel.nml"
yield self._taskname(fn)
path = self._rundir / fn
yield asset(path, path.is_file)
yield file(path=Path(self._driver_config["namelist"]["template_file"]))
render(
input_file=Path(self._driver_config["namelist"]["template_file"]),
output_file=path,
overrides=self._driver_config["namelist"]["template_values"],
)

@tasks
def provisioned_run_directory(self):
"""
Run directory provisioned with all required content.
"""
yield self._taskname("provisioned run directory")
yield [self.namelist_file(), self.restart_directory()]

@task
def restart_directory(self):
"""
The restart directory.
"""
yield self._taskname("restart directory")
path = self._rundir / "restart_wave"
yield asset(path, path.is_dir)
yield None
path.mkdir(parents=True)

# Private helper methods

@property
def _driver_name(self) -> str:
"""
Returns the name of this driver.
"""
return STR.ww3
35 changes: 35 additions & 0 deletions src/uwtools/resources/jsonschema/ww3.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"properties": {
"ww3": {
"additionalProperties": false,
"properties": {
"namelist": {
"additionalProperties": false,
"properties": {
"template_file": {
"type": "string"
},
"template_values": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"required": [
"template_file"
],
"type": "object"
},
"run_dir": {
"type": "string"
}
},
"required": [
"namelist",
"run_dir"
],
"type": "object"
}
}
}
1 change: 1 addition & 0 deletions src/uwtools/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ class STR:
valsneeded: str = "values_needed"
verbose: str = "verbose"
version: str = "version"
ww3: str = "ww3"
Loading

0 comments on commit bddcf4d

Please sign in to comment.