From 1037fdb6a4e86e72bd00b5ddc13ec6cb44208720 Mon Sep 17 00:00:00 2001 From: Laurent Courty Date: Tue, 23 Jun 2026 16:47:02 -0600 Subject: [PATCH 1/5] add --resume-from hotstart in CLI --- src/itzi/itzi.py | 93 +++++++++++++++++++++++++++++++++++++------ src/itzi/parser.py | 20 ++++++++++ tests/cli/test_cli.py | 76 +++++++++++++++++++++++++++++++++-- 3 files changed, 173 insertions(+), 16 deletions(-) diff --git a/src/itzi/itzi.py b/src/itzi/itzi.py index 51979ef..d19cd26 100644 --- a/src/itzi/itzi.py +++ b/src/itzi/itzi.py @@ -68,14 +68,15 @@ def main(argv=None): class SimulationRunner: """Provide the necessary tools to run one simulation.""" - def __init__(self, sim_config: SimulationConfig, grass_params: GrassParams): + def __init__( + self, + sim_config: SimulationConfig, + grass_params: GrassParams, + hotstart_path: str | None = None, + ): self.grass_required_version = "8.4.0" self.g_interface: GrassInterface self.sim: Simulation - self._initialize(sim_config, grass_params) - - def _initialize(self, sim_config: SimulationConfig, grass_params: GrassParams): - """Set GRASS and initialize the simulation.""" # display parameters (if verbose) sim_config.display_sim_param() @@ -139,13 +140,15 @@ def _initialize(self, sim_config: SimulationConfig, grass_params: GrassParams): } ) - self.sim = ( + sim_builder = ( SimulationBuilder(sim_config, self.g_interface.get_npmask(), data_type) .with_input_provider(raster_input_provider) .with_raster_output_provider(raster_output_provider) .with_vector_output_provider(vector_output_provider) - .build() ) + if hotstart_path: + sim_builder.with_hotstart(hotstart_path) + self.sim: Simulation = sim_builder.build() # Initialize the simulation self.sim.initialize() @@ -192,7 +195,7 @@ def __del__(self): self.g_interface.cleanup() -def sim_runner_worker(conf_file): +def sim_runner_worker(conf_file: str, hotstart_file: str | None): """Run one simulation""" msgr.raise_on_error = True msgr._itzi_logger.set_verbosity(msgr.verbosity()) @@ -204,7 +207,11 @@ def sim_runner_worker(conf_file): grass_params = conf_data.get_grass_params() with GrassSessionManager(grass_params): with profile_context(): - sim_runner = SimulationRunner(sim_params, grass_params) + sim_runner = SimulationRunner( + sim_params, + grass_params, + hotstart_file, + ) sim_runner.run().finalize() except itzi_error.ItziError: # if an Itzï error, only print the last line of the traceback @@ -214,9 +221,9 @@ def sim_runner_worker(conf_file): msgr.warning("Error during execution: {}".format(traceback.format_exc())) -def itzi_run_one(conf_file): +def itzi_run_one(conf_file: str, hotstart_file: str | None): """Run a simulation in a subprocess""" - worker_args = (conf_file,) + worker_args = (conf_file, hotstart_file) p = Process(target=sim_runner_worker, args=worker_args) p.start() p.join() @@ -225,6 +232,62 @@ def itzi_run_one(conf_file): p.close() +def reconcile_hotstart_commands( + config_file_list: list[str], + resume_from_list: list[tuple[str | None, str]], +) -> list[tuple[str, str | None]]: + """Output a list of tuples in the form (config_file, hotstart_file).""" + + # No hotstart requested: run every config from scratch. + if not resume_from_list: + return [(config_file, None) for config_file in config_file_list] + + # A single hotstart only makes sense when there is a single config. + if len(resume_from_list) == 1: + if len(config_file_list) != 1: + msgr.fatal("A single --resume-from value can only be used with a single config file") + return [(config_file_list[0], resume_from_list[0][1])] + + # In batch mode every hotstart must be explicitly mapped. + if any(config_key is None for config_key, _ in resume_from_list): + msgr.fatal("When using multiple --resume-from values, each one must be CONFIG=PATH") + + # Accept a normalized config path first, then a unique basename like "a.ini". + exact_lookup = { + os.path.abspath(os.path.normpath(config_file)): config_file + for config_file in config_file_list + } + basename_lookup: dict[str, str | None] = {} + resolved_hotstarts = {config_file: None for config_file in config_file_list} + + # Build basename lookup; Store None when a basename is shared. + for config_file in config_file_list: + basename = os.path.basename(config_file) + if basename not in basename_lookup: + basename_lookup[basename] = config_file + elif basename_lookup[basename] != config_file: + basename_lookup[basename] = None + + # Resolve each CONFIG=PATH pair onto its target config file. + for config_key, hotstart_file in resume_from_list: + assert config_key is not None + normalized_key = os.path.abspath(os.path.normpath(config_key)) + target_config = exact_lookup.get(normalized_key) + if target_config is None: + basename = os.path.basename(config_key) + if basename not in basename_lookup: + msgr.fatal(f"--resume-from config {config_key!r} does not match any config file") + target_config = basename_lookup[basename] + if target_config is None: + msgr.fatal(f"--resume-from config {config_key!r} is ambiguous") + + if resolved_hotstarts[target_config] is not None: + msgr.fatal(f"Multiple hotstart files were given for {target_config!r}") + resolved_hotstarts[target_config] = hotstart_file + + return [(config_file, resolved_hotstarts[config_file]) for config_file in config_file_list] + + def itzi_run(cli_args): """Run one or multiple simulations from the command line.""" # set environment variables @@ -259,10 +322,14 @@ def itzi_run(cli_args): total_sim_start = time.time() # dictionary to store computation times times_list = [] - for conf_file in cli_args.config_file: + run_commands = reconcile_hotstart_commands( + cli_args.config_file, + getattr(cli_args, "resume_from", []), + ) + for conf_file, hotstart_file in run_commands: sim_start = time.time() # Run the simulation - itzi_run_one(conf_file) + itzi_run_one(conf_file, hotstart_file) # store computational time comp_time = timedelta(seconds=int(time.time() - sim_start)) list_elem = (os.path.basename(conf_file), comp_time) diff --git a/src/itzi/parser.py b/src/itzi/parser.py index cb63aa8..d142599 100644 --- a/src/itzi/parser.py +++ b/src/itzi/parser.py @@ -1,11 +1,23 @@ """parse command line""" +from __future__ import annotations + import argparse DESCR = "A dynamic, fully distributed hydraulic and hydrologic model." +def parse_resume_from(arg_value: str) -> tuple[str | None, str]: + if "=" not in arg_value: + return None, arg_value + + key, path = arg_value.split("=", 1) + if not key or not path: + raise argparse.ArgumentTypeError("--resume-from must be PATH or CONFIG=PATH") + return key, path + + def build_parser() -> argparse.ArgumentParser: arg_parser = argparse.ArgumentParser(description=DESCR) subparsers = arg_parser.add_subparsers(dest="command", required=True) @@ -21,6 +33,14 @@ def build_parser() -> argparse.ArgumentParser: verbosity_parser = run_parser.add_mutually_exclusive_group() verbosity_parser.add_argument("-v", action="count", help="increase verbosity") verbosity_parser.add_argument("-q", action="count", help="decrease verbosity") + run_parser.add_argument( + "--resume-from", + action="append", + type=parse_resume_from, + metavar="PATH|CONFIG=PATH", + default=[], + help="resume a simulation from a hotstart file", + ) # display version subparsers.add_parser("version", help="display software version number") diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index c421bb0..6bfd264 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -6,7 +6,8 @@ import pytest from itzi.const import VerbosityLevel -from itzi.itzi import main, itzi_run +from itzi.itzi import main, itzi_run, reconcile_hotstart_commands +from itzi.itzi_error import ItziFatal from itzi.parser import build_parser @@ -18,6 +19,21 @@ def test_run_parser_accepts_multiple_config_files(): assert args.q is None +def test_run_parser_accepts_resume_from_args(): + args = build_parser().parse_args( + [ + "run", + "a.ini", + "b.ini", + "--resume-from", + "a.ini=restart_a.zip", + "--resume-from", + "b.ini=restart_b.zip", + ] + ) + assert args.resume_from == [("a.ini", "restart_a.zip"), ("b.ini", "restart_b.zip")] + + def test_run_parser_rejects_v_and_q_together(): with pytest.raises(SystemExit): build_parser().parse_args(["run", "a.ini", "-v", "-q"]) @@ -29,11 +45,64 @@ def test_prints_version(monkeypatch, capsys): assert capsys.readouterr().out.strip() == "22.2" +def test_reconcile_hotstart_commands_accepts_single_resume_for_single_config(): + assert reconcile_hotstart_commands(["/tmp/a.ini"], [(None, "restart_a.zip")]) == [ + ("/tmp/a.ini", "restart_a.zip"), + ] + + +def test_reconcile_hotstart_commands_matches_multiple_named_values(): + config_file_list = ["/tmp/a.ini", "/tmp/b.ini", "/tmp/c.ini"] + resume_from_list = [("c.ini", "restart_c.zip"), ("a.ini", "restart_a.zip")] + + assert reconcile_hotstart_commands(config_file_list, resume_from_list) == [ + ("/tmp/a.ini", "restart_a.zip"), + ("/tmp/b.ini", None), + ("/tmp/c.ini", "restart_c.zip"), + ] + + +def test_reconcile_hotstart_commands_accepts_duplicate_basenames_with_paths(): + config_file_list = ["./sim1/config.ini", "sim2/config.ini"] + resume_from_list = [ + ("./sim1/config.ini", "sim1/hotstart.zip"), + ("sim2/config.ini", "sim2/hotstart.zip"), + ] + + assert reconcile_hotstart_commands(config_file_list, resume_from_list) == [ + ("./sim1/config.ini", "sim1/hotstart.zip"), + ("sim2/config.ini", "sim2/hotstart.zip"), + ] + + +def test_reconcile_hotstart_commands_rejects_single_resume_for_multiple_configs(): + with pytest.raises(ItziFatal): + reconcile_hotstart_commands(["/tmp/a.ini", "/tmp/b.ini"], [(None, "restart.zip")]) + + +def test_reconcile_hotstart_commands_rejects_unnamed_values_in_batch_mode(): + with pytest.raises(ItziFatal): + reconcile_hotstart_commands( + ["/tmp/a.ini", "/tmp/b.ini"], + [("a.ini", "restart_a.zip"), (None, "restart_b.zip")], + ) + + +def test_reconcile_hotstart_commands_rejects_unknown_config_key(): + with pytest.raises(ItziFatal): + reconcile_hotstart_commands( + ["/tmp/a.ini", "/tmp/b.ini"], + [("missing.ini", "restart.zip"), ("b.ini", "restart_b.zip")], + ) + + def test_itzi_run_sets_env_and_dispatches(monkeypatch): calls = [] messages = [] - monkeypatch.setattr("itzi.itzi.itzi_run_one", calls.append) + monkeypatch.setattr( + "itzi.itzi.itzi_run_one", lambda conf, hotstart: calls.append((conf, hotstart)) + ) monkeypatch.setattr("itzi.itzi.msgr.message", messages.append) args = argparse.Namespace( @@ -41,11 +110,12 @@ def test_itzi_run_sets_env_and_dispatches(monkeypatch): o=True, v=1, q=None, + resume_from=[("a.ini", "restart_a.zip"), ("b.ini", "restart_b.zip")], ) itzi_run(args) - assert calls == ["a.ini", "b.ini"] + assert calls == [("a.ini", "restart_a.zip"), ("b.ini", "restart_b.zip")] assert os.environ["GRASS_OVERWRITE"] == "1" assert os.environ["ITZI_VERBOSE"] == str(VerbosityLevel.VERBOSE) assert os.environ["GRASS_VERBOSE"] == "2" From 66e7fd492347cb5ac80dcfd19d088a1e4e14f682 Mon Sep 17 00:00:00 2001 From: Laurent Courty Date: Tue, 23 Jun 2026 22:50:56 -0600 Subject: [PATCH 2/5] Add CLI documentation --- docs/cli.rst | 17 ++-- docs/conf_file.rst | 2 +- docs/prog_manual.rst | 9 ++- pyproject.toml | 3 + src/itzi/parser.py | 24 +++--- uv.lock | 181 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 18 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index cb0e07f..6ac86f7 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,20 +1,27 @@ Command line usage ------------------- +================== Run a simulation -~~~~~~~~~~~~~~~~ +---------------- .. argparse:: :filename: ../src/itzi/parser.py - :func: arg_parser + :func: build_parser :prog: itzi :path: run + :nodefault: + + +Hotstart usage +~~~~~~~~~~~~~~ +.. versionadded:: 26.6 + Get the version number -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- .. argparse:: :filename: ../src/itzi/parser.py - :func: arg_parser + :func: build_parser :prog: itzi :path: version diff --git a/docs/conf_file.rst b/docs/conf_file.rst index a62ec46..0ec704f 100644 --- a/docs/conf_file.rst +++ b/docs/conf_file.rst @@ -37,7 +37,7 @@ Valid combinations: ---------- .. versionadded:: 26.6 - The hotstart feature is added. + A hotstart file saves the current state of the simulation at a give point in time, allowing to restart a simulation from a given point. diff --git a/docs/prog_manual.rst b/docs/prog_manual.rst index caf15a3..551a908 100644 --- a/docs/prog_manual.rst +++ b/docs/prog_manual.rst @@ -24,7 +24,7 @@ Once the itzi repository is cloned and uv installed, you can run itzi with: uv run itzi -This will create a virtual environment, install all the dependencies listed in the *pyproject.toml* file, and build the Cython extensions. +This will create a virtual environment, install all the dependencies listed in the *pyproject.toml* file, including dev dependencies, and build the Cython extensions. Now, every change you make to the Python code will be directly reflected when running the tests or *uv run itzi* . @@ -71,12 +71,14 @@ To estimate the test coverage: The GRASS-specific tests could be sped up a bit by running them separately: .. code:: sh + uv run pytest tests/grass/test_itzi.py && uv run pytest tests/grass/test_bmi.py && uv run pytest tests/grass/test_tutorial.py The tests not relying on GRASS can be run directly: .. code:: sh + uv run pytest tests/core @@ -99,8 +101,9 @@ Documentation The documentation is written in reStructuredText and is built with Sphinx. It is located in the *docs* directory. It is automatically built and published on `readthedocs `__. -To build the documentation locally, you first need to install *sphinx*, along with *sphinx-argparse* and *sphinx_rtd_theme*. -You can then build the documentation locally: +The packages *sphinx*, *sphinx-argparse*, and *sphinx_rtd_theme* are needed to build the docs locally. +They are normally installed automatically with the rest of the dev dependencies. +You can then build the documentation: .. code:: sh diff --git a/pyproject.toml b/pyproject.toml index 9424c13..358fc2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,9 @@ dev = [ "ty>=0.0.51", "pyinstrument==5.*", "rioxarray>=0.19.0", + "sphinx>=9.1.0", + "sphinx-argparse>=0.5.2", + "sphinx-rtd-theme>=3.1.0", ] [build-system] diff --git a/src/itzi/parser.py b/src/itzi/parser.py index d142599..1b42dd6 100644 --- a/src/itzi/parser.py +++ b/src/itzi/parser.py @@ -14,7 +14,9 @@ def parse_resume_from(arg_value: str) -> tuple[str | None, str]: key, path = arg_value.split("=", 1) if not key or not path: - raise argparse.ArgumentTypeError("--resume-from must be PATH or CONFIG=PATH") + raise argparse.ArgumentTypeError( + "--resume-from must be HOTSTART_PATH or CONFIG_PATH=HOTSTART_PATH" + ) return key, path @@ -23,26 +25,30 @@ def build_parser() -> argparse.ArgumentParser: subparsers = arg_parser.add_subparsers(dest="command", required=True) # run a simulation - run_parser = subparsers.add_parser("run", help="run a simulation") + run_parser = subparsers.add_parser("run", help="Run a simulation.") run_parser.add_argument( "config_file", nargs="+", - help=("an Itzï configuration file (if several given, run in batch mode)"), + help=("An Itzï configuration file (if several given, run in batch mode.)"), ) - run_parser.add_argument("-o", action="store_true", help="overwrite files if exist") + run_parser.add_argument("-o", action="store_true", help="Overwrite files if exist.") verbosity_parser = run_parser.add_mutually_exclusive_group() - verbosity_parser.add_argument("-v", action="count", help="increase verbosity") - verbosity_parser.add_argument("-q", action="count", help="decrease verbosity") + verbosity_parser.add_argument("-v", action="count", help="Increase verbosity.") + verbosity_parser.add_argument("-q", action="count", help="Decrease verbosity.") run_parser.add_argument( "--resume-from", action="append", type=parse_resume_from, - metavar="PATH|CONFIG=PATH", + metavar="HOTSTART_PATH | CONFIG_PATH=HOTSTART_PATH", default=[], - help="resume a simulation from a hotstart file", + help=( + "Resume a simulation from a hotstart file. " + "If only the path to a hotstart file is given, batch is not allowed. " + "For batch processing, use the 'CONFIG_PATH=HOTSTART_PATH' construct." + ), ) # display version - subparsers.add_parser("version", help="display software version number") + subparsers.add_parser("version", help="Display software version number.") return arg_parser diff --git a/uv.lock b/uv.lock index c428701..4cfb328 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, ] +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -42,6 +51,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.2" @@ -459,6 +477,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/77/2ff7aefc09cf1306a81cd7a46af34f80ebefef81a2e8329b94b58ad813ae/distributed-2026.3.0-py3-none-any.whl", hash = "sha256:52518f4b3e6795e87b442e8f57788ba1ddc750c62d0835669c85927280d38f07", size = 1009769, upload-time = "2026-03-18T07:10:21.241Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "donfig" version = "0.8.1.post1" @@ -584,6 +611,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.0" @@ -639,6 +675,9 @@ dev = [ { name = "rioxarray" }, { name = "ruff" }, { name = "scipy" }, + { name = "sphinx" }, + { name = "sphinx-argparse" }, + { name = "sphinx-rtd-theme" }, { name = "ty" }, ] @@ -670,6 +709,9 @@ dev = [ { name = "rioxarray", specifier = ">=0.19.0" }, { name = "ruff", specifier = ">=0.15" }, { name = "scipy", specifier = ">=1.15.2" }, + { name = "sphinx", specifier = ">=9.1.0" }, + { name = "sphinx-argparse", specifier = ">=0.5.2" }, + { name = "sphinx-rtd-theme", specifier = ">=3.1.0" }, { name = "ty", specifier = ">=0.0.51" }, ] @@ -1624,6 +1666,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/2f/63d2cacc0e525f8e3398bcf32bd3620385f22cd1600834ec49d7f3597a7b/rioxarray-0.19.0-py3-none-any.whl", hash = "sha256:494ee4fff1781072d55ee5276f5d07b63d93b05093cb33b926a12186ba5bb8ef", size = 62151, upload-time = "2025-04-21T17:46:52.801Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "ruff" version = "0.15.18" @@ -1708,6 +1759,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1726,6 +1786,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, ] +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-argparse" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/a8c64e6633652111e6e4f89703182a53cbc3ed67233523e47472101358b6/sphinx_argparse-0.5.2.tar.gz", hash = "sha256:e5352f8fa894b6fb6fda0498ba28a9f8d435971ef4bbc1a6c9c6414e7644f032", size = 27838, upload-time = "2024-07-17T12:08:08.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/43/9f0e9bfb3ce02cbf7747aa2185c48a9d6e42ba95736a5e8f511a5054d976/sphinx_argparse-0.5.2-py3-none-any.whl", hash = "sha256:d771b906c36d26dee669dbdbb5605c558d9440247a5608b810f7fa6e26ab1fd3", size = 12547, upload-time = "2024-07-17T12:08:06.307Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "swmm-toolkit" version = "0.16.0" From 976c0f773e870a8241a474e4f0e4e66d09855c5e Mon Sep 17 00:00:00 2001 From: Laurent Courty Date: Tue, 23 Jun 2026 23:22:54 -0600 Subject: [PATCH 3/5] update documentation for hotstart --- docs/cli.rst | 43 ++++++++++++++++++ docs/conf_file.rst | 107 +++++++++++++++++++++++++++++++++++++++++++-- docs/faq.rst | 22 +++++++++- 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 6ac86f7..c2b7d67 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -16,6 +16,49 @@ Hotstart usage ~~~~~~~~~~~~~~ .. versionadded:: 26.6 +Use ``--resume-from`` to resume a run from a hotstart file created by a +previous simulation. The hotstart file does not replace the configuration file: +you still pass the normal Itzi configuration file(s), and Itzi validates the +resumed configuration against the hotstart before starting. + +Checkpoint creation and resume-time configuration constraints are documented in +:doc:`conf_file`. +Known restart limitations are summarized in :doc:`faq`. + +Single simulation +^^^^^^^^^^^^^^^^^ + +Resume a single configuration file from one hotstart file: + +.. code-block:: bash + + itzi run my_case.ini --resume-from checkpoints/latest_hotstart.zip + +Batch mode +^^^^^^^^^^ + +For batch runs, map each resumed simulation explicitly: + +.. code-block:: bash + + itzi run a.ini b.ini \ + --resume-from a.ini=checkpoints/a_hotstart.zip \ + --resume-from b.ini=checkpoints/b_hotstart.zip + +Rules +^^^^^ + +- With a single configuration file, ``--resume-from HOTSTART_PATH`` is valid. +- With multiple configuration files, each ``--resume-from`` value must use the + ``CONFIG_PATH=HOTSTART_PATH`` form. +- Current limitation: when several configuration files are given, a single + ``--resume-from`` value is rejected even if it uses ``CONFIG_PATH=HOTSTART_PATH``. +- The ``CONFIG_PATH`` part may be either the config path or a basename such as + ``a.ini``. When basenames are not unique, use the config path. +- At most one hotstart file may be mapped to a given configuration file. +- When multiple ``CONFIG_PATH=HOTSTART_PATH`` mappings are supplied, any batch + configuration file left unmapped still runs from scratch. + Get the version number ---------------------- diff --git a/docs/conf_file.rst b/docs/conf_file.rst index 0ec704f..4e1ea9d 100644 --- a/docs/conf_file.rst +++ b/docs/conf_file.rst @@ -39,7 +39,8 @@ Valid combinations: .. versionadded:: 26.6 -A hotstart file saves the current state of the simulation at a give point in time, allowing to restart a simulation from a given point. +A hotstart file saves the current state of the simulation at a given point in +time, allowing the run to be resumed later with ``itzi run --resume-from``. .. list-table:: :header-rows: 1 @@ -49,12 +50,112 @@ A hotstart file saves the current state of the simulation at a give point in tim - Description - Format * - wallclock_step - - Wall clock duration between records. + - Wall-clock duration between checkpoint writes. - HH:MM:SS * - save_file - - File name for the hotstart file. Each new file overwrites the anterior. Only the last created file is kept. + - Path to the hotstart file written by the simulation. - Text string +Current behavior and limitations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The ``[hotstart]`` section is optional. If it is absent, no checkpoint is + written. +- When the ``[hotstart]`` section is present, both ``wallclock_step`` and + ``save_file`` must be provided. +- ``wallclock_step`` is measured in wall-clock time, not simulation time. +- ``save_file`` is overwritten every time a new checkpoint is written. Only the + most recent checkpoint is kept automatically. +- There is no extra automatic checkpoint at the very end of the simulation. +- If the run finishes before ``wallclock_step`` has elapsed in real time, no + hotstart file is written. + +Resume requirements +^^^^^^^^^^^^^^^^^^^ + +To resume from a hotstart file, pass the configuration file together with the +``--resume-from`` CLI option described in :doc:`cli`. + +Before resuming, Itzi validates the hotstart against the current configuration. +Resume is rejected when any of the following checks fail: + +- the hotstart archive is missing or invalid; +- the resumed domain bounds, grid size, or coordinate reference system do not + match the archived domain; +- the current GRASS mask does not match the archived mask; +- the archived run contains drainage state but the resumed configuration has no + drainage model, or the reverse; +- the infiltration model changes between the archived run and the resumed run; +- a surface-flow parameter changes outside the set explicitly allowed on + resume; +- ``end_time`` is changed to a value that is not strictly after the archived + checkpoint time; + + +Config changes allowed on resume +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The table below reflects the current implementation. + +.. list-table:: + :header-rows: 1 + :widths: 30 15 55 + + * - Config option(s) + - Status + - Notes + * - ``[time] end_time`` + - Allowed + - May stay unchanged or be extended. If changed, it must be strictly after + the checkpoint time stored in the hotstart. + * - ``[time] record_step`` + - Allowed + - Changes the output cadence from the resume point onward. It must stay + positive. + * - ``[options] dtinf`` + - Allowed + - The resumed hydrology schedule uses the new value. + * - ``[options] cfl``, ``theta``, ``dtmax``, ``slope_threshold``, + ``max_slope`` + - Allowed + - These are the only surface-flow options explicitly allowed to change on + resume. + * - ``[hotstart] wallclock_step``, ``[hotstart] save_file`` + - Allowed + - These control only new checkpoints creation after the resume. The input + hotstart file itself is selected with ``--resume-from``. + * - ``[output] prefix``, ``[output] values``, ``[statistics] stats_file`` + - Allowed + - Output targets come from the resumed configuration file. + * - ``[options] hmin``, ``g``, ``vrouting``, ``max_error`` + - Must match + - Changing any of these raises a hotstart compatibility error. + * - Infiltration model selection from ``[input]`` + - Must match + - The model type must stay the same: no infiltration, constant + infiltration, and Green-Ampt are not interchangeable on resume. + * - Drainage enabled or disabled + - Must match + - A run with drainage can only resume from a hotstart that also contains + drainage state, and vice versa. + * - ``[time] start_time`` + - Keep unchanged + - The archived simulation clock and scheduler are restored from the + hotstart. They are not remapped to a new start time. + * - Input map names, ``[drainage] swmm_inp``, and other forcing paths + - Not cross-checked + - Itzi validates the resumed domain and mask, but it does not verify that + these files are the same as in the archived run. Changing them changes + the forcing applied after the resume. + * - ``[drainage] output``, ``orifice_coeff``, ``free_weir_coeff``, + ``submerged_weir_coeff`` + - Not cross-checked + - These values are taken from the resumed configuration, but Itzi does not + compare them with the archived configuration before resuming. + +If you want a pure restart, keep every option unchanged except for the options +listed as ``Allowed`` above. + [input] ------- diff --git a/docs/faq.rst b/docs/faq.rst index aa76b52..e66eafe 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -26,16 +26,34 @@ which could be achieved by changing two options: The second one is by reducing the *theta* option. Please note however that a value below 0.7 could be counter-productive. +Why does a resumed run differ from an uninterrupted run? +-------------------------------------------------------- + +.. versionadded:: 26.6 + +Hotstart resume is a continuation mechanism, not a guarantee of bitwise +identity with an uninterrupted run. + +For surface-only cases, resumed runs should normally be very close to the +original run. With drainage enabled, the current SWMM hotstart behavior is not +always restart-exact. +A resumed run can therefore show small differences relative to an uninterrupted run +even when the hotstart file and configuration are valid. + +See :doc:`conf_file` for the configuration constraints enforced when resuming. + Performances and computer resources usage ----------------------------------------- -*Itzï* is parallelized using OpenMP. +*Itzï* is parallelized using OpenMP and makes use of compiler-level vector optimizations. By default, it will try to use all available hardware threads on the machine. The number of threads used can be changed by setting the environment variable OMP\_NUM\_THREADS. Given the type of numerical scheme, using a computer with more cores and faster RAM will likely decrease the computation time. -No parallel efficiency test has been performed so far, though. +Additionally, it is recommended to disable multithreading (SMT) if your CPU support it. +SMT is counterproductive for the type of computing *Itzï* does. + For an example of expected performance, a 24h simulation of urban floods with direct rainfall on a 5m DEM of 3.5 millions cells takes around 3 hours with an Intel Core i7-4790 (4 cores, 8 threads). From c9c963a1508255574b6e41e90824cc9442c164e0 Mon Sep 17 00:00:00 2001 From: Laurent Courty Date: Tue, 23 Jun 2026 23:31:46 -0600 Subject: [PATCH 4/5] a single, mapped --resume-from is now allowed. max_error and vrouting are allowed to change in a resumed run --- docs/cli.rst | 2 -- docs/conf_file.rst | 4 +-- src/itzi/itzi.py | 12 +++++--- src/itzi/simulation_builder.py | 2 ++ tests/cli/test_cli.py | 10 +++++++ tests/core/test_hotstart_state_loading.py | 36 +++++++++++++++++------ 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index c2b7d67..80c476d 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -51,8 +51,6 @@ Rules - With a single configuration file, ``--resume-from HOTSTART_PATH`` is valid. - With multiple configuration files, each ``--resume-from`` value must use the ``CONFIG_PATH=HOTSTART_PATH`` form. -- Current limitation: when several configuration files are given, a single - ``--resume-from`` value is rejected even if it uses ``CONFIG_PATH=HOTSTART_PATH``. - The ``CONFIG_PATH`` part may be either the config path or a basename such as ``a.ini``. When basenames are not unique, use the config path. - At most one hotstart file may be mapped to a given configuration file. diff --git a/docs/conf_file.rst b/docs/conf_file.rst index 4e1ea9d..89075a7 100644 --- a/docs/conf_file.rst +++ b/docs/conf_file.rst @@ -116,7 +116,7 @@ The table below reflects the current implementation. - Allowed - The resumed hydrology schedule uses the new value. * - ``[options] cfl``, ``theta``, ``dtmax``, ``slope_threshold``, - ``max_slope`` + ``max_slope``, ``vrouting``, ``max_error`` - Allowed - These are the only surface-flow options explicitly allowed to change on resume. @@ -127,7 +127,7 @@ The table below reflects the current implementation. * - ``[output] prefix``, ``[output] values``, ``[statistics] stats_file`` - Allowed - Output targets come from the resumed configuration file. - * - ``[options] hmin``, ``g``, ``vrouting``, ``max_error`` + * - ``[options] hmin``, ``g`` - Must match - Changing any of these raises a hotstart compatibility error. * - Infiltration model selection from ``[input]`` diff --git a/src/itzi/itzi.py b/src/itzi/itzi.py index d19cd26..6d9a606 100644 --- a/src/itzi/itzi.py +++ b/src/itzi/itzi.py @@ -242,11 +242,15 @@ def reconcile_hotstart_commands( if not resume_from_list: return [(config_file, None) for config_file in config_file_list] - # A single hotstart only makes sense when there is a single config. + # An unnamed single hotstart only makes sense when there is a single config. if len(resume_from_list) == 1: - if len(config_file_list) != 1: - msgr.fatal("A single --resume-from value can only be used with a single config file") - return [(config_file_list[0], resume_from_list[0][1])] + config_key, hotstart_file = resume_from_list[0] + if config_key is None: + if len(config_file_list) != 1: + msgr.fatal( + "A single unnamed --resume-from value can only be used with a single config file" + ) + return [(config_file_list[0], hotstart_file)] # In batch mode every hotstart must be explicitly mapped. if any(config_key is None for config_key, _ in resume_from_list): diff --git a/src/itzi/simulation_builder.py b/src/itzi/simulation_builder.py index ad449cd..5921522 100644 --- a/src/itzi/simulation_builder.py +++ b/src/itzi/simulation_builder.py @@ -59,6 +59,8 @@ class SimulationBuilder: "dtmax", "slope_threshold", "max_slope", + "vrouting", + "max_error", } def __init__( diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 6bfd264..b4874d2 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -80,6 +80,16 @@ def test_reconcile_hotstart_commands_rejects_single_resume_for_multiple_configs( reconcile_hotstart_commands(["/tmp/a.ini", "/tmp/b.ini"], [(None, "restart.zip")]) +def test_reconcile_hotstart_commands_accepts_single_named_resume_for_multiple_configs(): + assert reconcile_hotstart_commands( + ["/tmp/a.ini", "/tmp/b.ini"], + [("a.ini", "restart_a.zip")], + ) == [ + ("/tmp/a.ini", "restart_a.zip"), + ("/tmp/b.ini", None), + ] + + def test_reconcile_hotstart_commands_rejects_unnamed_values_in_batch_mode(): with pytest.raises(ItziFatal): reconcile_hotstart_commands( diff --git a/tests/core/test_hotstart_state_loading.py b/tests/core/test_hotstart_state_loading.py index c9d5c06..47a777c 100644 --- a/tests/core/test_hotstart_state_loading.py +++ b/tests/core/test_hotstart_state_loading.py @@ -398,13 +398,14 @@ def test_build_allows_dtinf_change( assert simulation.hydrology_model.dt == timedelta(seconds=resumed_dtinf) @pytest.mark.parametrize( - ("parameter", "value"), + ("parameter", "value", "target_attr"), [ - ("cfl", 0.15), - ("theta", 0.9), - ("dtmax", 0.2), - ("slope_threshold", 1e-5), - ("max_slope", 5.0), + ("cfl", 0.15, "cfl"), + ("theta", 0.9, "theta"), + ("dtmax", 0.2, "dtmax"), + ("slope_threshold", 1e-5, "slope_threshold"), + ("max_slope", 5.0, "max_slope"), + ("vrouting", 2.0, "v_routing"), ], ) def test_build_allows_surface_flow_resume_parameter_change( @@ -414,6 +415,7 @@ def test_build_allows_surface_flow_resume_parameter_change( valid_hotstart_bytes: io.BytesIO, parameter: str, value: float, + target_attr: str, ) -> None: """build() should allow selected surface-flow tuning changes on resume.""" resumed_surface_flow_parameters = sim_config.surface_flow_parameters.model_copy( @@ -425,15 +427,31 @@ def test_build_allows_surface_flow_resume_parameter_change( simulation = self._build_with_hotstart(domain_5by5, resumed_config, valid_hotstart_bytes) - assert getattr(simulation.surface_flow, parameter) == value + assert getattr(simulation.surface_flow, target_attr) == value + + def test_build_allows_max_error_change( + self, + domain_5by5, + sim_config: SimulationConfig, + valid_hotstart_bytes: io.BytesIO, + ) -> None: + """build() should allow changing the mass-balance threshold on resume.""" + resumed_surface_flow_parameters = sim_config.surface_flow_parameters.model_copy( + update={"max_error": 0.5} + ) + resumed_config = sim_config.model_copy( + update={"surface_flow_parameters": resumed_surface_flow_parameters} + ) + + simulation = self._build_with_hotstart(domain_5by5, resumed_config, valid_hotstart_bytes) + + assert simulation.mass_balance_error_threshold == 0.5 @pytest.mark.parametrize( ("parameter", "value"), [ ("hmin", 0.0002), ("g", 9.9), - ("vrouting", 2.0), - ("max_error", 0.5), ], ) def test_build_rejects_surface_flow_parameter_mismatch( From 626a583e8a49e5e20ab29790e6c597bced5b3d7e Mon Sep 17 00:00:00 2001 From: Laurent Courty Date: Wed, 24 Jun 2026 23:27:56 -0600 Subject: [PATCH 5/5] update faq --- docs/faq.rst | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index e66eafe..44f717f 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -26,6 +26,7 @@ which could be achieved by changing two options: The second one is by reducing the *theta* option. Please note however that a value below 0.7 could be counter-productive. + Why does a resumed run differ from an uninterrupted run? -------------------------------------------------------- @@ -42,6 +43,7 @@ even when the hotstart file and configuration are valid. See :doc:`conf_file` for the configuration constraints enforced when resuming. + Performances and computer resources usage ----------------------------------------- @@ -49,13 +51,13 @@ Performances and computer resources usage By default, it will try to use all available hardware threads on the machine. The number of threads used can be changed by setting the environment variable OMP\_NUM\_THREADS. -Given the type of numerical scheme, using a computer with more cores and -faster RAM will likely decrease the computation time. -Additionally, it is recommended to disable multithreading (SMT) if your CPU support it. +Using a computer with more cores and faster RAM will likely decrease the computation time. +Additionally, it is recommended to disable multithreading (SMT) if your CPU support it; SMT is counterproductive for the type of computing *Itzï* does. -For an example of expected performance, a 24h simulation of urban floods with direct -rainfall on a 5m DEM of 3.5 millions cells takes around 3 hours with an Intel Core i7-4790 (4 cores, 8 threads). +For an example of expected performance with *Itzï* version 26.6, a 24h simulation of urban floods with direct +rainfall on a 5m DEM of 3.5 millions cells takes around 2 hours on a 16 cores AMD Ryzen 5900XT. + How to decrease computation time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -66,20 +68,20 @@ The factors that influence the computation time are: - The number of cells in the domain. - The number of wet cells in the domain. Direct rainfall is more demanding. -- The cell size. A smaller cell size decreases the time-step. +- The cell size. A smaller cell size decreases the time-step duration. - The maximum water depth in the domain. - The higher the water, the smaller the time-step. -- The amount and frequency of result maps. Disk operations being slow - and not yet parallelized (as of version 17.1), writing more maps to - the disk will slow the simulation down. + The higher the water depth, the shorter the time-step. +- The number and frequency of result maps. + Disk operations are slow; writing more maps to the disk will slow down the simulation. As we can see, they are two main categories of factors. -Those that increase the raw computation load (more cells), +Those that increase the raw computation load (more cells, longer simulations), and those that lower the simulation time-step. -For the same study area, increasing the cell size is the more efficient way to make a simulation faster, -because it influence both the number of cells and the time-step. +For the same study area, increasing the cell size is the easiest way to make a simulation run faster, +because it influences both the number of cells and the time-step. + Memory usage ~~~~~~~~~~~~ -On average, *Itzï* 17.1 uses around 250 MB of RAM for each million cells in the domain. +*Itzï* 26.6 uses around 275 MB of RAM for each million cells in the domain.