Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ configuration is built, so they raise ``FileNotFoundError`` whether or not the
input is validated; with the validation off the other missing inputs surface
only when the run reaches them.

Pass ``allow_blocking_problems=True`` to either loader to run past those
problems, the counterpart of ``rubem run --allow-blocking-problems``: the
checks still run, every problem is kept in ``model.configuration.problems``,
the blocking ones are logged as errors followed by one error line stating that
the simulation continues despite them, and the model loads and runs instead of
raising. The failures that are not problems (a document that does not match
the schema, a missing file) raise as before. Pass ``validate_input=False``
instead to skip the checks altogether.

An already loaded ``ModelConfiguration`` may be passed to ``Model.from_config``
as well, and is used as it is, neither validated again nor re-anchored.

Expand Down Expand Up @@ -106,7 +115,8 @@ and the raster memory of a run is not reclaimed. Two consequences follow for
``Model.run_isolated`` avoids both: it runs the simulation in a subprocess
started with the ``spawn`` method, used for that one run and shut down before
the call returns. The configuration crosses as its document, its base
directory and the validation flag, and is rebuilt on the other side; the result
directory and the validation flags (``validate_input`` and
``allow_blocking_problems``), and is rebuilt on the other side; the result
crosses as plain data. A ``ConfigurationError`` raised while the subprocess
rebuilds the configuration reaches the caller as the same exception, with its
problems; any other exception of the run propagates as itself, and a subprocess
Expand Down
11 changes: 11 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ The format follows `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`__.
Added
`````

- Added ``rubem run --allow-blocking-problems``, which runs the model even when the input
validation finds blocking problems: the same checks run, every problem is still reported
(the non-blocking ones as warnings, the blocking ones as errors, followed by one error line
stating that the simulation continues despite them) and the run goes on instead of stopping
with exit code 1. The option cannot be combined with ``-s``, which skips the checks it
reports (usage error, exit code 2), and the deprecated ``rubem -c <config>`` spelling does
not take it. ``ModelConfiguration(..., allow_blocking_problems=False)`` is the library
counterpart and keeps the full list in ``ModelConfiguration.problems``; ``Model.from_file``
and ``Model.from_config`` of ``rubem.api`` take the same keyword and an isolated run
rebuilds the configuration with it
(`#352 <https://github.com/LabSid-USP/RUBEM/issues/352>`__).
- Added ``rubem.api``, the public Python surface: ``Model.from_file`` and
``Model.from_config`` load a configuration file or document, ``Model.run()``
runs the simulation in the current process and ``Model.run_isolated()`` in a
Expand Down
37 changes: 37 additions & 0 deletions doc/source/userguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,9 @@ Use ``-h`` or ``--help`` to get a brief description of each command and its argu
[required]
-s, --skip-inputs-validation Disable input files validation before running
the model.
--allow-blocking-problems Run the model even if the input validation
finds blocking problems; they are logged as
errors instead of stopping the run.
-h, --help Show this message and exit.

Use ``-V`` or ``--version`` to get the version of the RUBEM.
Expand Down Expand Up @@ -1266,3 +1269,37 @@ Use ``-c`` or ``--configfile`` to set the path of the RUBEM configuration file.
.## Timestep 22 of 24
.## Timestep 23 of 24
.## Timestep 24 of 24

Use ``-s`` or ``--skip-inputs-validation`` to skip the validation of the
content of the input files: the checks do not run at all, so the run neither
stops on a blocking problem nor reports one. Use ``--allow-blocking-problems``
instead to keep the checks and their report, but not the stop: every problem is
still logged, the non-blocking ones as warnings and the blocking ones as
errors, followed by one error line stating that the simulation continues
despite them, and the run goes on. Exploring a dataset, or reproducing a
published run whose inputs fail a check, are the cases the option is meant
for. The two options cannot be combined, since ``-s`` skips the very checks
the other one reports: doing so is a usage error (exit code 2). A run forced
this way exits with 0 when it completes and with 1 when it fails, as it does
when a structural problem (a missing member of a raster series, a member on
another grid) is reached by the simulation. Failures that are not validation
problems still stop the run before it starts: a configuration file that does
not match the schema, a lookup table file that is not there, no raster format
enabled, or a DEM, clone and georeference that do not share their geometry.

.. code-block:: console

$ rubem run -c project-config.json --allow-blocking-problems
Loading configuration and validating inputs...
2026-09-22T10:27:30-0300.119 [WRN] rubem.configuration.model_configuration: Configuration problems found: 1
2026-09-22T10:27:30-0300.119 [ERR] rubem.configuration.model_configuration: Configuration problem: Rainy days lookup table does not cover every month.: Missing months: [12]. The simulation cannot run with this table. /path/to/rainydays.txt
2026-09-22T10:27:30-0300.119 [ERR] rubem.configuration.model_configuration: Simulation continues despite 1 blocking problem(s).
Simulation started...
.## Timestep 1 of 24
.## Timestep 2 of 24

-- Omitted for brevity --

.## Timestep 24 of 24
Simulation finished successfully!
Elapsed time: 12 seconds
68 changes: 54 additions & 14 deletions rubem/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,23 @@ class Model:
and their content when it rebuilds the configuration. It has no effect
on ``configuration``, which is already loaded; pass the value the
configuration was loaded with.
:param allow_blocking_problems: Whether an isolated run keeps going when the
configuration it rebuilds carries blocking problems, as
:meth:`from_file` and :meth:`from_config` do with the same keyword. It
has no effect on ``configuration`` either; pass the value the
configuration was loaded with.
"""

def __init__(self, configuration: "ModelConfiguration", *, validate_input: bool = True) -> None:
def __init__(
self,
configuration: "ModelConfiguration",
*,
validate_input: bool = True,
allow_blocking_problems: bool = False,
) -> None:
self._configuration = configuration
self._validate_input = bool(validate_input)
self._allow_blocking_problems = bool(allow_blocking_problems)

@classmethod
def from_file(
Expand All @@ -127,25 +139,38 @@ def from_file(
*,
validate_input: bool = True,
base_dir: PathInput | None = None,
allow_blocking_problems: bool = False,
) -> "Model":
"""Load a configuration file and return the model it configures.

:param path: The JSON configuration file, legacy or format 1.0.
:param validate_input: Whether to validate the input files and their content.
:param base_dir: Directory the relative paths of the configuration are
anchored on. Defaults to the directory of the file.
:param allow_blocking_problems: Whether to load, and run, a configuration
whose inputs carry blocking problems. The checks still run and every
problem is kept in ``configuration.problems``; the blocking ones are
logged as errors instead of raising :class:`ConfigurationError`. An
isolated run rebuilds the configuration the same way.
:raises ImportError: If PCRaster or GDAL are not installed.
:raises FileNotFoundError: If ``path``, or a raster or table it names,
is not there.
:raises json.JSONDecodeError: If the file is not JSON.
:raises pydantic.ValidationError: If the document does not match the schema.
:raises ConfigurationError: If the inputs carry blocking problems.
:raises ConfigurationError: If the inputs carry blocking problems, unless
``allow_blocking_problems`` is set.
"""
_require_runtime_deps()
from .configuration.model_configuration import ModelConfiguration

configuration = ModelConfiguration(path, validate_input, base_dir)
return cls(configuration, validate_input=validate_input)
configuration = ModelConfiguration(
path, validate_input, base_dir, allow_blocking_problems=allow_blocking_problems
)
return cls(
configuration,
validate_input=validate_input,
allow_blocking_problems=allow_blocking_problems,
)

@classmethod
def from_config(
Expand All @@ -154,6 +179,7 @@ def from_config(
*,
validate_input: bool = True,
base_dir: PathInput | None = None,
allow_blocking_problems: bool = False,
) -> "Model":
"""Return the model a configuration document, or a loaded configuration, configures.

Expand All @@ -168,19 +194,26 @@ def from_config(
:param base_dir: Directory the relative paths of the document are
anchored on. A dictionary has no anchor unless this is passed.
Ignored when ``config`` is already loaded.
:param allow_blocking_problems: Whether to load, and run, a document
whose inputs carry blocking problems, as in :meth:`from_file`. For an
already loaded configuration the flag only says whether an isolated
run keeps going when it rebuilds the configuration.
:raises ImportError: If PCRaster or GDAL are not installed.
:raises FileNotFoundError: If a raster or table the document names is
not there.
:raises pydantic.ValidationError: If the document does not match the schema.
:raises ConfigurationError: If the inputs carry blocking problems.
:raises ConfigurationError: If the inputs carry blocking problems, unless
``allow_blocking_problems`` is set.
"""
_require_runtime_deps()
from .configuration.model_configuration import ModelConfiguration

if isinstance(config, ModelConfiguration):
return cls(config, validate_input=validate_input)
if not isinstance(config, ModelConfiguration):
config = ModelConfiguration(
config, validate_input, base_dir, allow_blocking_problems=allow_blocking_problems
)
return cls(
ModelConfiguration(config, validate_input, base_dir), validate_input=validate_input
config, validate_input=validate_input, allow_blocking_problems=allow_blocking_problems
)

@property
Expand Down Expand Up @@ -214,8 +247,8 @@ def run_isolated(self) -> RunResult:
"""Run the simulation in a fresh spawned subprocess.

The configuration crosses as its document (the copy taken when it was
loaded) plus its base directory and the validation flag, and is rebuilt
on the other side; the result crosses as plain data. The subprocess is
loaded) plus its base directory and the validation flags, and is
rebuilt on the other side; the result crosses as plain data. The subprocess is
started for this call only and the executor is shut down before
returning, so the PCRaster state of the run leaves nothing behind. Every
call therefore pays a full interpreter start-up.
Expand All @@ -233,8 +266,9 @@ def run_isolated(self) -> RunResult:

A configuration problem found while the subprocess rebuilds the
configuration reaches the caller as the same
:class:`ConfigurationError`, with its problems; any other exception of
the run propagates as itself.
:class:`ConfigurationError`, with its problems, unless the model was
built with ``allow_blocking_problems``; any other exception of the run
propagates as itself.

The log records of the simulation are emitted in the subprocess, which
starts from the default logging configuration: the handlers of the
Expand All @@ -257,6 +291,7 @@ def run_isolated(self) -> RunResult:
self._configuration.config,
self._configuration.base_dir,
self._validate_input,
self._allow_blocking_problems,
)
document = future.result()
except BrokenProcessPool as error:
Expand All @@ -274,7 +309,9 @@ def run_isolated(self) -> RunResult:
return _result_from_json(document)


def _run_document(document: dict, base_dir: str | None, validate_input: bool) -> dict[str, Any]:
def _run_document(
document: dict, base_dir: str | None, validate_input: bool, allow_blocking_problems: bool
) -> dict[str, Any]:
"""Rebuild a configuration, run it, and return the result as plain data.

The entry point of the subprocess of :meth:`Model.run_isolated`. It lives at
Expand All @@ -284,12 +321,15 @@ def _run_document(document: dict, base_dir: str | None, validate_input: bool) ->
:param document: The configuration document, legacy or format 1.0.
:param base_dir: Directory the relative paths of the document are anchored on.
:param validate_input: Whether to validate the input files and their content.
:param allow_blocking_problems: Whether to run past blocking problems.
:return: The :class:`RunResult` of the run, as a JSON-able dictionary.
"""
from .configuration.model_configuration import ModelConfiguration
from .core import DynamicFrameworkWrapper

configuration = ModelConfiguration(document, validate_input, base_dir)
configuration = ModelConfiguration(
document, validate_input, base_dir, allow_blocking_problems=allow_blocking_problems
)
started = time.perf_counter()
DynamicFrameworkWrapper(configuration).run()
elapsed = time.perf_counter() - started
Expand Down
29 changes: 24 additions & 5 deletions rubem/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""The ``rubem`` command line.

``rubem run -c <config.json> [-s]`` runs a simulation; ``rubem config schema``
prints the JSON Schema of the configuration file. The former ``rubem -c
<config.json>`` spelling still works for one minor release and emits a
``DeprecationWarning``.
``rubem run -c <config.json> [-s | --allow-blocking-problems]`` runs a
simulation; ``rubem config schema`` prints the JSON Schema of the configuration
file. The former ``rubem -c <config.json>`` spelling still works for one minor
release and emits a ``DeprecationWarning``.
"""

import json
Expand Down Expand Up @@ -122,8 +122,25 @@ def run(
help="Disable input files validation before running the model.",
),
] = False,
allow_blocking_problems: Annotated[
bool,
typer.Option(
"--allow-blocking-problems",
help=(
"Run the model even if the input validation finds blocking problems; "
"they are logged as errors instead of stopping the run."
),
),
] = False,
) -> None:
"""Run a simulation from a configuration file."""
if skip_inputs_validation and allow_blocking_problems:
# ``-s`` skips the very checks this option exists to report.
raise typer.BadParameter(
"cannot be combined with '-s' / '--skip-inputs-validation': the problems "
"it allows are found by the validation that option skips.",
param_hint="'--allow-blocking-problems'",
)
require_runtime_deps()

validate_input = not skip_inputs_validation
Expand All @@ -140,7 +157,9 @@ def run(
# embedded run stays silent unless its host configures logging.
validating = " and validating inputs" if validate_input else ""
print(f"Loading configuration{validating}...", flush=True)
model_config = ModelConfiguration(configfile, validate_input)
model_config = ModelConfiguration(
configfile, validate_input, allow_blocking_problems=allow_blocking_problems
)
model = DynamicFrameworkWrapper.load(model_config)
except (ConfigurationError, ValidationError, ValueError) as e:
# A configuration the user can fix: no traceback, the message says what.
Expand Down
Loading
Loading