Skip to content

Commit

Permalink
UW-585: Build driver for SCHISM (ufs-community#506)
Browse files Browse the repository at this point in the history
* Build Schism driver framework

* Formatting fix

* fix schema errors

* Update src/uwtools/drivers/schism.py

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

* had to add logging to failing test

* fix copy/paste name error

* Apply suggestions from code review

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

* open template_values to all JSON types

---------

Co-authored-by: Paul Madden <[email protected]>
  • Loading branch information
WeirAE and maddenp-noaa authored Jun 13, 2024
1 parent 3509e23 commit dbe1224
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 4 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 @@ -13,6 +13,7 @@ UW YAML for Components
make_solo_mosaic
mpas
mpas_init
schism
sfc_climo_gen
shave
ungrib
Expand Down
32 changes: 32 additions & 0 deletions docs/sections/user_guide/yaml/components/schism.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.. _schism_yaml:

schism
======

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

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

.. highlight:: yaml
.. literalinclude:: /shared/schism.yaml

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

namelist:
^^^^^^^^^

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

**template_file:**

The path to the input template file containing Jinja2 expressions (perhaps named ``param.nml.IN``), based on the ``param.nml`` file from the SCHISM build.

**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/schism.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
schism:
namelist:
template_file: /path/to/schism/param.nml.IN
template_values:
dt: 100
run_dir: /path/to/run/directory
platform:
account: me
scheduler: slurm
78 changes: 78 additions & 0 deletions src/uwtools/drivers/schism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
An assets driver for SCHISM.
"""

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 SCHISM(Assets):
"""
An assets driver for SCHISM.
"""

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 = "param.nml"
yield self._taskname(fn)
path = self._rundir / fn
yield asset(path, path.is_file)
template_file = Path(self._driver_config["namelist"]["template_file"])
yield file(path=template_file)
render(
input_file=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()

# Private helper methods

@property
def _driver_name(self) -> str:
"""
Returns the name of this driver.
"""
return STR.schism
33 changes: 33 additions & 0 deletions src/uwtools/resources/jsonschema/schism.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"properties": {
"schism": {
"additionalProperties": false,
"properties": {
"namelist": {
"additionalProperties": false,
"properties": {
"template_file": {
"type": "string"
},
"template_values": {
"minProperties": 1,
"type": "object"
}
},
"required": [
"template_file"
],
"type": "object"
},
"run_dir": {
"type": "string"
}
},
"required": [
"namelist",
"run_dir"
],
"type": "object"
}
}
}
4 changes: 1 addition & 3 deletions src/uwtools/resources/jsonschema/ww3.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
"type": "string"
},
"template_values": {
"additionalProperties": {
"type": "string"
},
"minProperties": 1,
"type": "object"
}
},
Expand Down
1 change: 1 addition & 0 deletions src/uwtools/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class STR:
rocoto: str = "rocoto"
run: str = "run"
schemafile: str = "schema_file"
schism: str = "schism"
searchpath: str = "search_path"
sfcclimogen: str = "sfc_climo_gen"
shave: str = "shave"
Expand Down
1 change: 1 addition & 0 deletions src/uwtools/tests/config/test_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ def test__supplement_values_priority(supplemental_values):


def test__values_needed(caplog):
log.setLevel(logging.DEBUG)
undeclared_variables = {"roses_color", "lavender_smell"}
jinja2._values_needed(undeclared_variables)
assert logged(caplog, "Value(s) needed to render this template are:")
Expand Down
74 changes: 74 additions & 0 deletions src/uwtools/tests/drivers/test_schism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# pylint: disable=missing-function-docstring,protected-access,redefined-outer-name
"""
SCHISM driver tests.
"""
import datetime as dt
from unittest.mock import DEFAULT as D
from unittest.mock import patch

import yaml
from pytest import fixture

from uwtools.drivers import schism

# Fixtures


@fixture
def config(tmp_path):
return {
"schism": {
"namelist": {
"template_file": str(tmp_path / "param.nml.IN"),
"template_values": {
"dt": 100,
},
},
"run_dir": str(tmp_path),
},
}


@fixture
def cycle():
return dt.datetime(2024, 2, 1, 18)


@fixture
def driverobj(config, cycle):
return schism.SCHISM(config=config, cycle=cycle, batch=True)


# Tests


def test_SCHISM(driverobj):
assert isinstance(driverobj, schism.SCHISM)


def test_SCHISM_namelist_file(driverobj):
src = driverobj._driver_config["namelist"]["template_file"]
with open(src, "w", encoding="utf-8") as f:
yaml.dump({}, f)
dst = driverobj._rundir / "param.nml"
assert not dst.is_file()
driverobj.namelist_file()
assert dst.is_file()


def test_SCHISM_provisioned_run_directory(driverobj):
with patch.multiple(
driverobj,
namelist_file=D,
) as mocks:
driverobj.provisioned_run_directory()
for m in mocks:
mocks[m].assert_called_once_with()


def test_SCHISM__driver_config(driverobj):
assert driverobj._driver_config == driverobj._config["schism"]


def test_SCHISM__validate(driverobj):
driverobj._validate()
52 changes: 51 additions & 1 deletion src/uwtools/tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def sfc_climo_gen_prop():
return partial(schema_validator, "sfc-climo-gen", "properties", "sfc_climo_gen", "properties")


@fixture
def schism_prop():
return partial(schema_validator, "schism", "properties", "schism", "properties")


@fixture
def shave_prop():
return partial(schema_validator, "shave", "properties", "shave", "properties")
Expand Down Expand Up @@ -1131,6 +1136,51 @@ def test_schema_rocoto_workflow_cycledef():
assert "'foo' is not valid" in errors([{"attrs": {"activation_offset": "foo"}, "spec": spec}])


# schism


def test_schema_schism():
config = {
"namelist": {
"template_file": "/tmp/param.nml",
"template_values": {
"dt": 100,
},
},
"run_dir": "/tmp",
}
errors = schema_validator("schism", "properties", "schism")
# Basic correctness:
assert not errors(config)
# All top-level keys are required:
for key in ("namelist", "run_dir"):
assert f"'{key}' is a required property" in errors(with_del(config, key))
# Additional top-level keys are not allowed:
assert "Additional properties are not allowed" in errors({**config, "foo": "bar"})


def test_schema_schism_namelist(schism_prop):
errors = schism_prop("namelist")
# At least template_file is required:
assert "'template_file' is a required property" in errors({})
# Just template_file is ok:
assert not errors({"template_file": "/path/to/param.nml"})
# Both template_file and template_values are ok:
assert not errors(
{
"template_file": "/path/to/param.nml",
"template_values": {"dt": 100},
}
)


def test_schema_schism_run_dir(schism_prop):
errors = schism_prop("run_dir")
# Must be a string:
assert not errors("/some/path")
assert "88 is not of type 'string'" in errors(88)


# sfc-climo-gen


Expand Down Expand Up @@ -1399,7 +1449,7 @@ def test_schema_upp_run_dir(upp_prop):
assert "88 is not of type 'string'" in errors(88)


# ungrib
# ww3


def test_schema_ww3():
Expand Down

0 comments on commit dbe1224

Please sign in to comment.