import os
import subprocess
from logging import debug, info
from ....utils import path
from ....utils.check.errclass import CifRuntimeError
from .utils import compile as _compile
[docs]
def run(self, runsubdir, mode, workdir, ddi, nbproc=1, do_simu=True,
approx_transf=False, ref_fwd_dir="", overlap=False, datastore=None,
**kwargs):
"""Compile (if needed) and run the D&B executable for one sub-period.
D&B is a self-contained Fortran executable (not a Python model like
``satwetch4``): this function launches it as a subprocess in
``runsubdir``, where :func:`~.io.inputs.make_auxiliary.make_auxiliary`
and :func:`~.io.native2inputs.native2inputs` are expected to have
already staged everything the executable needs (``input/*.nc``,
``input/*-params.csv``, ``opt.nml``, ``obs.nml``), following the
fixed, hard-wired relative paths used throughout the D&B sources (see
e.g. ``src/prior.f90``, ``src/ncread_forcing.f90``: ``input/...``,
``mode==adj`` is not implemented yet.
This function also triggers :func:`~.compile.compile` itself, on the
first call only (i.e. once ``self.workdir/model/runmodel.x`` exists,
later calls skip straight to running it). It is deliberately *not*
triggered any earlier -- e.g. by the generic, input-agnostic
``run_model`` transform init (see the ``__init__.py`` note) -- because
D&B's build needs ``runsubdir/input/{static,dyn}forcing.nc`` to already
exist (see :func:`~.compile.compile`); by the time ``run`` is first
called, pyCIF has finished initialising the model (domain, mapper,
periods) and staging its input data (via
:func:`~.io.native2inputs.native2inputs`), so that precondition holds.
Args:
self: the dalecbethy model Plugin.
runsubdir (str): working directory for the current run; the D&B
executable and its ``input/`` directory must be staged here.
mode (str): 'fwd', 'tl' or 'adj'.
workdir (str): pyCIF working directory.
ddi (datetime.datetime): start date of the current sub-simulation.
do_simu (bool): re-run or not an existing simulation.
.. warning::
TODO / open questions for a D&B developer:
* D&B ships a hand-coded adjoint (``src/cost_bw.f90``,
``runassi.x``) but it is wired to D&B's *own* L-BFGS-B
minimiser (``mini/``), driving D&B's internal control vector
(``src/prior.f90``) against D&B's own observation operator
(``src/obsop.f90``), not to a per-timestep tangent-linear/adjoint
call pyCIF could drive itself (unlike e.g. CHIMERE's
``tlchimere.e``/``achimere.e``). Whether/how a pyCIF-driven
``mode='tl'``/``'adj'`` run should be implemented here (e.g. by
building and calling ``runassi.x`` or a new dedicated driver) is
left as a follow-up.
* D&B currently only supports full-domain runs at hourly internal
time steps between ``yyyymmdd_start``/``yyyymmdd_end``
(``src/dimensions.f90``); how that maps onto pyCIF's
sub-simulation windows (``ddi``/``ddf``) still needs to be
decided, in particular since those dates are presently compiled
into the executable rather than passed at runtime.
"""
if mode != "fwd":
raise CifRuntimeError(
"The D&B plugin does not implement a tangent-linear/adjoint "
f"run yet (mode={mode!r} requested); see run.py TODOs."
)
if not do_simu:
return
# Compile lazily, only once per model instance (see docstring above):
# later sub-periods reuse the executable from the first call, even if
# 'force-recompile' is set (that only forces *one* recompilation here,
# not one per sub-period).
built_exe = f"{workdir}/model/runmodel.x"
if not getattr(self, "_dalecbethy_compiled", False):
if getattr(self, "force-recompile") or not os.path.isfile(built_exe):
_compile(self, runsubdir)
self._dalecbethy_compiled = True
exe = f"{runsubdir}/runmodel.x"
path.link(built_exe, exe)
info(f"Running D&B in {runsubdir} for sub-period starting {ddi}")
with open(f"{runsubdir}/log.std", "w") as log:
process = subprocess.Popen(
"./runmodel.x",
cwd=runsubdir,
stdout=log,
stderr=subprocess.PIPE,
)
_, stderr = process.communicate()
if process.returncode != 0:
debug("D&B returned errors during execution:")
debug(stderr.decode())
raise CifRuntimeError(
f"D&B execution failed in {runsubdir}; see log.std for details"
)