diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/coastal/Makefile b/coastal/Makefile new file mode 100644 index 0000000..cb9771b --- /dev/null +++ b/coastal/Makefile @@ -0,0 +1,18 @@ +TARGETS = format lint test typecheck + +.PHONY: $(TARGETS) + +all: + $(error Valid targets are: $(TARGETS)) + +format: + black *.py + isort *.py + +lint: + pylint *.py + +test: lint typecheck + +typecheck: + mypy *.py diff --git a/coastal/README.md b/coastal/README.md new file mode 100644 index 0000000..7127824 --- /dev/null +++ b/coastal/README.md @@ -0,0 +1,46 @@ +# Coastal + +To execute this driver, in a conda environment where [`uwtools`](https://github.com/ufs-community/uwtools) is [installed](https://uwtools.readthedocs.io/en/stable/sections/user_guide/installation.html): + +``` +uw execute --module coastal.py --classname Coastal --task provisioned_rundir --config-file coastal.yaml --cycle 2024-08-05T12 --batch +``` + +Afterwards: + +``` +$ tree run +run +├── albedo.gr3 -> /path/to/data/albedo.gr3 +├── bctides.in -> /path/to/data/bctides.in +├── datm_in +├── datm.streams +├── fd_ufs.yaml -> /path/to/model/tests/parm/fd_ufs.yaml +├── hgrid.gr3 -> /path/to/data/hgrid.gr3 +├── hgrid.ll -> /path/to/data/hgrid.ll +├── INPUT +│   ├── data.nc -> /path/to/data/INPUT/wind_atm_fin_ch_time_vec_STR_fixed.nc +│   └── esmf_mesh.nc -> /path/to/data/INPUT/wind_atm_fin_ch_time_vec_ESMFmesh.nc +├── manning.gr3 -> /path/to/data/manning.gr3 +├── model_configure -> /path/to/data/model_configure +├── noahmptable.tbl -> /path/to/data/noahmptable.tbl +├── param.nml +├── RESTART +├── runscript.coastal +├── station.in -> /path/to/data/station.in +├── ufs.configure -> /path/to/data/ufs.configure +├── vgrid.in -> /path/to/data/vgrid.in +├── watertype.gr3 -> /path/to/data/watertype.gr3 +└── windrot_geo2proj.gr3 -> /path/to/data/windrot_geo2proj.gr3 + +2 directories, 19 files +``` + +To be run in a conda environment with [`uwtools`](https://github.com/ufs-community/uwtools) installed. + +For development/testing, with conda active in your shell: + +``` +$ conda env create -f environment.yml +$ conda activate coastal +``` diff --git a/coastal/coastal.jsonschema b/coastal/coastal.jsonschema new file mode 100644 index 0000000..e6307dc --- /dev/null +++ b/coastal/coastal.jsonschema @@ -0,0 +1,3 @@ +{ + "type": "object" +} diff --git a/coastal/coastal.py b/coastal/coastal.py new file mode 100644 index 0000000..bd7f734 --- /dev/null +++ b/coastal/coastal.py @@ -0,0 +1,62 @@ +""" +A driver for the Coastal App coupled executable. +""" + +from iotaa import asset, task, tasks +from uwtools.api.cdeps import CDEPS +from uwtools.api.driver import DriverCycleBased +from uwtools.api.file import link +from uwtools.api.logging import use_uwtools_logger +from uwtools.api.schism import SCHISM + +use_uwtools_logger() + + +class Coastal(DriverCycleBased): + """ + A driver for the Coastal App coupled executable. + """ + + @task + def linked_files(self): + """ + Data files linked into the run directory. + """ + links = self.config["links"] + path = lambda fn: self.rundir / fn + yield self.taskname("Linked files") + yield [asset(path(fn), path(fn).is_file) for fn in links.keys()] + yield None + link(config=links, target_dir=self.rundir) + + @tasks + def provisioned_rundir(self): + """ + The run directory provisioned with all required content. + """ + cdeps = CDEPS(config=self.config_full, cycle=self.cycle, controller=self.driver_name) + schism = SCHISM(config=self.config_full, cycle=self.cycle, controller=self.driver_name) + yield self.taskname("Provisioned run directory") + yield [ + cdeps.atm_nml(), + cdeps.atm_stream(), + schism.namelist_file(), + self.linked_files(), + self.restart_dir(), + self.runscript(), + ] + + @task + def restart_dir(self): + """ + RESTART directory in run directory. + """ + path = self.rundir / "RESTART" + yield self.taskname("RESTART directory") + yield asset(path, path.is_dir) + yield None + path.mkdir(parents=True) + + @property + def driver_name(self): + return "coastal" diff --git a/coastal/coastal.yaml b/coastal/coastal.yaml new file mode 100644 index 0000000..a75f569 --- /dev/null +++ b/coastal/coastal.yaml @@ -0,0 +1,82 @@ +cdeps: + atm_in: + update_values: + datm_nml: + datamode: ATMMESH + export_all: true + factorfn_data: 'null' + factorfn_mesh: 'null' + flds_co2: false + flds_presaero: false + flds_wiso: false + iradsw: 1 + model_maskfile: INPUT/esmf_mesh.nc + model_meshfile: INPUT/esmf_mesh.nc + nx_global: 101 + ny_global: 101 + restfilm: 'null' + atm_streams: + streams: + stream01: + dtlimit: 1.5 + mapalgo: redist + readmode: single + stream_data_files: + - INPUT/data.nc + stream_data_variables: + - uwnd Sa_u10m + - vwnd Sa_v10m + - P Sa_pslv + stream_lev_dimname: 'null' + stream_mesh_file: INPUT/esmf_mesh.nc + stream_offset: 0 + stream_vectors: 'null' + taxmode: limit + tinterpalgo: linear + yearAlign: 2008 + yearFirst: 2008 + yearLast: 2008 + template_file: stream.jinja2 +coastal: + execution: + batchargs: + cores: 6 + export: NONE + jobname: coastal + partition: hercules + queue: batch + walltime: '00:30:00' + envcmds: + - module use {{ dir.model }}/modulefiles + - module load ufs_hercules.intel + executable: /path/to/ufs_model + mpiargs: + - '--export=ALL' + mpicmd: srun + links: + INPUT/data.nc: '{{ dir.data }}/INPUT/wind_atm_fin_ch_time_vec_STR_fixed.nc' + INPUT/esmf_mesh.nc: '{{ dir.data }}/INPUT/wind_atm_fin_ch_time_vec_ESMFmesh.nc' + albedo.gr3: '{{ dir.data }}/albedo.gr3' + bctides.in: '{{ dir.data }}/bctides.in' + fd_ufs.yaml: '{{ dir.model }}/tests/parm/fd_ufs.yaml' + hgrid.gr3: '{{ dir.data }}/hgrid.gr3' + hgrid.ll: '{{ dir.data }}/hgrid.ll' + manning.gr3: '{{ dir.data }}/manning.gr3' + model_configure: '{{ dir.data }}/model_configure' + noahmptable.tbl: '{{ dir.data }}/noahmptable.tbl' + station.in: '{{ dir.data }}/station.in' + ufs.configure: '{{ dir.data }}/ufs.configure' + vgrid.in: '{{ dir.data }}/vgrid.in' + watertype.gr3: '{{ dir.data }}/watertype.gr3' + windrot_geo2proj.gr3: '{{ dir.data }}/windrot_geo2proj.gr3' + rundir: run +dir: + test: /path/to/coastal/test/input + data: '{{ dir.test }}//data' + model: '{{ dir.test }}//model' +platform: + account: me + scheduler: slurm +schism: + namelist: + template_file: '{{ dir.test }}/param.nml' diff --git a/coastal/environment.yml b/coastal/environment.yml new file mode 100644 index 0000000..8be7068 --- /dev/null +++ b/coastal/environment.yml @@ -0,0 +1,9 @@ +name: coastal +channels: + - ufs-community + - conda-forge +dependencies: + - black + - mypy + - pylint + - uwtools >=2.4.0 diff --git a/coastal/pyproject.toml b/coastal/pyproject.toml new file mode 100644 index 0000000..76b1404 --- /dev/null +++ b/coastal/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +line-length = 100 + +[tool.isort] +line_length = 100 +profile = "black" + +[tool.mypy] +check_untyped_defs = true +pretty = true +warn_return_any = true + +[tool.pylint."messages control"] +disable = ["unnecessary-lambda-assignment"] diff --git a/coastal/stream.jinja2 b/coastal/stream.jinja2 new file mode 100644 index 0000000..28ac7cf --- /dev/null +++ b/coastal/stream.jinja2 @@ -0,0 +1,33 @@ +{% set stream_info = [] -%} +{% for key, val in streams.items() -%} +{% set _ = stream_info.append( key ) -%} +{% endfor -%} +stream_info: {{ stream_info | join(' ') }} +{% for key, val in streams.items() %} +taxmode{{'%02d' % loop.index}}: {{ val['taxmode'] }} +mapalgo{{'%02d' % loop.index}}: {{ val['mapalgo'] }} +tInterpAlgo{{'%02d' % loop.index}}: {{ val['tinterpalgo'] }} +readMode{{'%02d' % loop.index}}: {{ val['readmode'] }} +dtlimit{{'%02d' % loop.index}}: {{ val['dtlimit'] }} +stream_offset{{'%02d' % loop.index}}: {{ val['stream_offset'] }} +yearFirst{{'%02d' % loop.index}}: {{ val['yearFirst'] }} +yearLast{{'%02d' % loop.index}}: {{ val['yearLast'] }} +yearAlign{{'%02d' % loop.index}}: {{ val['yearAlign'] }} +{% if val['stream_vectors'] is string -%} +stream_vectors{{'%02d' % loop.index}}: {{ val['stream_vectors'] }} +{% else -%} +stream_vectors{{'%02d' % loop.index}}: "{{ val['stream_vectors'] | join(':') }}" +{% endif -%} +stream_mesh_file{{'%02d' % loop.index}}: {{ val['stream_mesh_file'] }} +stream_lev_dimname{{'%02d' % loop.index}}: {{ val['stream_lev_dimname'] }} +{% if val['stream_data_files'] | length > 1 -%} +stream_data_files{{'%02d' % loop.index}}: ""{{ val['stream_data_files'] | join('" "') }}"" +{% else -%} +stream_data_files{{'%02d' % loop.index}}: "{{ val['stream_data_files'] | first }}" +{% endif -%} +{% if val['stream_data_variables'] is string -%} +stream_data_variables{{'%02d' % loop.index}}: "{{ val['stream_data_variables'] }}" +{% else -%} +stream_data_variables{{'%02d' % loop.index}}: "{{ val['stream_data_variables'] | join('" "') }}" +{% endif -%} +{% endfor %}