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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
tests/test_data_temp/elev_lid792_1m.gtiff filter=lfs diff=lfs merge=lfs -text
tests/test_data/tutorial_files/elev_lid792_1m.gtiff filter=lfs diff=lfs merge=lfs -text
8 changes: 5 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ jobs:
- name: Install dependencies
run: |
apt-get update
apt-get install -y ca-certificates
apt-get install -y ca-certificates git-lfs

- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
lfs: true

- name: Install uv and Python
run: |
Expand All @@ -36,7 +38,7 @@ jobs:
$HOME/.local/bin/uv run --all-extras --python ${{ matrix.python-version }} pytest --cov=src --junitxml=junit/test-temporal-overwrite-results.xml --cov-report=xml --cov-report=html tests/grass/test_temporal_overwrite.py

- name: Upload coverage
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: coverage-report-${{ matrix.python-version }}
path: |
Expand Down
11 changes: 11 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ These are the major changes made in each release.
For more details please see the commit log of the git repository.


Itzï 26.8
---------

*Unreleased*

**Internals**

- Rely on itzi-core for the computing part.
Itzï is the user-facing interface, including GRASS.


Itzï 26.6
---------

Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "itzi"
version = "26.7"
version = "26.8"
description = "A distributed dynamic flood model."
authors = [
{name = "Laurent Courty", email = "lrntct@gmail.com"},
]
dependencies = [
"numpy>=2.5",
"itzi-core>=0.2.0",
"itzi-core>=0.4.0",
"bmipy>=2.0.1",
"pydantic>=2.10"
]
Expand Down Expand Up @@ -40,7 +40,6 @@ dev = [
"pytest>=9.1",
"pytest-cov==6.*",
"pandas>=3.0.3",
"requests>=2.34",
"pre-commit>=4.6",
"ruff>=0.15",
"ty>=0.0.55",
Expand Down
6 changes: 5 additions & 1 deletion src/itzi/bmi_itzi.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ def initialize(self, filename=None) -> None:
self.grass_session_manager = GrassSessionManager(grass_params)
self.grass_session_manager.open()

self.itzi = SimulationRunner(sim_params, grass_params)
self.itzi = SimulationRunner(
sim_params,
grass_params,
stats_file=conf_data.get_stats_file(),
)

def update(self):
"""Advance model by one time step."""
Expand Down
11 changes: 7 additions & 4 deletions src/itzi/configreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,9 @@ def __init__(self, filename: str | None) -> None:
self.raw_input_times = _read_time_values(params)
self.input_map_names = _read_input_map_names(params)
self.out_prefix, self.out_values, self.output_map_names = _read_output_config(params)
self.stats_file = (
_read_optional_value(params, "statistics", "stats_file", params.get) or None
)
self.sim_times = SimulationTimes.from_raw_values(self.raw_input_times)
self._check_general_input(self.input_map_names)
infiltration_model = self._resolve_infiltration_model(self.input_map_names)
Expand All @@ -416,10 +419,6 @@ def __init__(self, filename: str | None) -> None:
if self.hotstart_config is not None:
simulation_kwargs["hotstart_config"] = self.hotstart_config

stats_file = _read_optional_value(params, "statistics", "stats_file", params.get)
if stats_file is not None:
simulation_kwargs["stats_file"] = stats_file

simulation_kwargs.update(_read_simulation_option_values(params))
simulation_kwargs.update(_read_simulation_drainage_values(params))
if "swmm_inp" in simulation_kwargs:
Expand Down Expand Up @@ -473,3 +472,7 @@ def get_sim_params(self) -> SimulationConfig:
def get_grass_params(self) -> GrassParams:
"""Return validated GRASS GIS session parameters."""
return self.grass_params

def get_stats_file(self) -> str | None:
"""Return the CSV statistics output file name, if configured."""
return self.stats_file
9 changes: 7 additions & 2 deletions src/itzi/itzi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from typing import TYPE_CHECKING, Callable

import numpy as np
from itzi_core.providers.csv_mass_balance_output import CSVMassBalanceOutputProvider
from itzi_core.simulation_builder import SimulationBuilder

from itzi.configreader import ConfigReader
Expand Down Expand Up @@ -76,7 +77,8 @@ def __init__(
sim_config: SimulationConfig,
grass_params: GrassParams,
hotstart_path: str | None = None,
):
stats_file: str | None = None,
) -> None:
self.grass_required_version = "8.4.0"
self.g_interface: GrassInterface
self.sim: Simulation
Expand Down Expand Up @@ -149,6 +151,8 @@ def __init__(
.with_raster_output_provider(raster_output_provider)
.with_vector_output_provider(vector_output_provider)
)
if stats_file:
sim_builder.with_mass_balance_output_provider(CSVMassBalanceOutputProvider(stats_file))
if hotstart_path:
sim_builder.with_hotstart(hotstart_path)
self.sim: Simulation = sim_builder.build()
Expand Down Expand Up @@ -212,7 +216,8 @@ def sim_runner_worker(conf_file: str, hotstart_file: str | None) -> None:
sim_runner = SimulationRunner(
sim_params,
grass_params,
hotstart_file,
hotstart_path=hotstart_file,
stats_file=conf_data.get_stats_file(),
)
sim_runner.run().finalize()
except msgr.FatalError:
Expand Down
54 changes: 54 additions & 0 deletions tests/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,60 @@ def fail(_):
assert "WARNING: Simulation terminated with exit status 1" in itzi_stderr.getvalue()


def test_worker_passes_statistics_file_to_simulation_runner(monkeypatch):
sim_params = object()
grass_params = object()
runner_arguments = {}

class FakeConfigReader:
def __init__(self, _):
pass

def get_sim_params(self):
return sim_params

def get_grass_params(self):
return grass_params

def get_stats_file(self):
return "statistics.csv"

class FakeGrassSessionManager:
def __init__(self, received_grass_params):
assert received_grass_params is grass_params

def __enter__(self):
return self

def __exit__(self, *_):
pass

class FakeSimulationRunner:
def __init__(self, *args, **kwargs):
runner_arguments["args"] = args
runner_arguments["kwargs"] = kwargs

def run(self):
return self

def finalize(self):
return self

monkeypatch.setattr("itzi.itzi.ConfigReader", FakeConfigReader)
monkeypatch.setattr("itzi.itzi.GrassSessionManager", FakeGrassSessionManager)
monkeypatch.setattr("itzi.itzi.SimulationRunner", FakeSimulationRunner)

sim_runner_worker("a.ini", "hotstart.zip")

assert runner_arguments == {
"args": (sim_params, grass_params),
"kwargs": {
"hotstart_path": "hotstart.zip",
"stats_file": "statistics.csv",
},
}


def test_run_one_reports_worker_signal(monkeypatch, itzi_stderr):
class FailedProcess:
exitcode = -11
Expand Down
26 changes: 25 additions & 1 deletion tests/cli/test_configreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_reader_uses_defaults_when_optional_sections_are_missing(tmp_path):

assert sim_config.hotstart_config is None
assert sim_config.surface_flow_parameters == SurfaceFlowParameters()
assert sim_config.stats_file is None
assert reader.get_stats_file() is None
assert sim_config.dtinf == DefaultValues.DTINF
assert sim_config.swmm_inp is None
assert sim_config.drainage_output is None
Expand All @@ -81,6 +81,30 @@ def test_reader_uses_defaults_when_optional_sections_are_missing(tmp_path):
}


@pytest.mark.parametrize(
("statistics", "expected_stats_file"),
[
(None, None),
({"stats_file": ""}, None),
({"stats_file": "statistics.csv"}, "statistics.csv"),
],
)
def test_reader_exposes_stats_file_separately_from_simulation_config(
tmp_path,
statistics,
expected_stats_file,
):
config_file = write_config_file(
tmp_path,
make_config_dict(statistics=statistics),
)

reader = ConfigReader(config_file)

assert reader.get_stats_file() == expected_stats_file
assert "stats_file" not in type(reader.get_sim_params()).model_fields


def test_reader_normalizes_deprecated_aliases(tmp_path, caplog):
config_file = write_config_file(
tmp_path,
Expand Down
9 changes: 0 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import hashlib
import os

import pytest
Expand All @@ -14,14 +13,6 @@ def roughness(timeseries):
normed_f = (f - f.mean()) / f.std()
return (normed_f.diff() ** 2).sum()

@staticmethod
def md5(file_path):
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()


@pytest.fixture(scope="session")
def helpers():
Expand Down
6 changes: 5 additions & 1 deletion tests/grass/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ def grass_5by5_sim(grass_5by5, test_data_path):
""" """
config_file = os.path.join(test_data_path, "5by5", "5by5.ini")
conf_data = ConfigReader(config_file)
sim_runner = SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params())
sim_runner = SimulationRunner(
conf_data.get_sim_params(),
conf_data.get_grass_params(),
stats_file=conf_data.get_stats_file(),
)
assert isinstance(sim_runner, SimulationRunner)
sim_runner.run().finalize()
return sim_runner
18 changes: 15 additions & 3 deletions tests/grass/test_itzi.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ def _build_timed_rain_runner(
parser.write(file_handle)

conf_data = ConfigReader(config_file)
return SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params())
return SimulationRunner(
conf_data.get_sim_params(),
conf_data.get_grass_params(),
stats_file=conf_data.get_stats_file(),
)


@pytest.mark.forked
Expand Down Expand Up @@ -143,7 +147,11 @@ def test_region_mask(test_data_path):
conf_data = ConfigReader(config_file)
sim_params = conf_data.get_sim_params()
grass_params = conf_data.get_grass_params()
sim_runner = SimulationRunner(sim_params, grass_params)
sim_runner = SimulationRunner(
sim_params,
grass_params,
stats_file=conf_data.get_stats_file(),
)
# Run simulation
sim_runner.run().finalize()
# Check temporary mask and region
Expand Down Expand Up @@ -192,7 +200,11 @@ def test_fails_when_region_has_no_dem_data(test_data_temp_path):
conf_data = ConfigReader(config_file)

with pytest.raises(RuntimeError, match=r"input map <dem> contains only NULL/NaN cells"):
SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params())
SimulationRunner(
conf_data.get_sim_params(),
conf_data.get_grass_params(),
stats_file=conf_data.get_stats_file(),
)


@pytest.mark.forked
Expand Down
6 changes: 5 additions & 1 deletion tests/grass/test_temporal_overwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ def _build_runner(
parser.write(file_handle)

conf_data = ConfigReader(config_file)
return SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params())
return SimulationRunner(
conf_data.get_sim_params(),
conf_data.get_grass_params(),
stats_file=conf_data.get_stats_file(),
)


@pytest.mark.forked
Expand Down
Loading