Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UW-585: Build driver for SCHISM #506

Merged
merged 10 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
WeirAE marked this conversation as resolved.
Show resolved Hide resolved

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.
WeirAE marked this conversation as resolved.
Show resolved Hide resolved

**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
77 changes: 77 additions & 0 deletions src/uwtools/drivers/schism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
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):
"""
A library driver for SCHISM.
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
"""

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"
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
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"]),
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
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
41 changes: 41 additions & 0 deletions src/uwtools/resources/jsonschema/schism.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"properties": {
"schism": {
"additionalProperties": false,
"properties": {
"namelist": {
"additionalProperties": false,
"properties": {
"template_file": {
"type": "string"
},
"template_values": {
"additionalProperties": {
"type": [
"array",
"boolean",
"number",
"string"
]
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
},
"minProperties": 1,
"type": "object"
}
},
"required": [
"template_file"
],
"type": "object"
},
"run_dir": {
"type": "string"
}
},
"required": [
"namelist",
"run_dir"
],
"type": "object"
}
}
}
8 changes: 7 additions & 1 deletion src/uwtools/resources/jsonschema/ww3.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
},
"template_values": {
"additionalProperties": {
"type": "string"
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
"type": [
"array",
"boolean",
"number",
"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
82 changes: 82 additions & 0 deletions src/uwtools/tests/drivers/test_schism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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 config_file(config, tmp_path):
path = tmp_path / "config.yaml"
with open(path, "w", encoding="utf-8") as f:
yaml.dump(config, f)
return path
WeirAE marked this conversation as resolved.
Show resolved Hide resolved


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


@fixture
def driverobj(config_file, cycle):
return schism.SCHISM(config=config_file, cycle=cycle, batch=True)
WeirAE marked this conversation as resolved.
Show resolved Hide resolved


# Tests


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


def test_WaveWatchIII_namelist_file(driverobj):
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
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
# wavewatch III
WeirAE marked this conversation as resolved.
Show resolved Hide resolved
WeirAE marked this conversation as resolved.
Show resolved Hide resolved


def test_schema_ww3():
Expand Down
Loading