From a114b7005cafb27a88151c1aa4ba5990678060d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Tue, 24 Sep 2024 00:33:38 +0000 Subject: [PATCH 1/9] CI(Ubuntu): Install latest version of Python for full config --- .github/workflows/ubuntu.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 57b0821eec4..3ad145115e7 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -99,6 +99,14 @@ jobs: }}:g' .gunittest.cfg > .gunittest.extra.cfg cat .gunittest.extra.cfg + - name: Set up Python + if: ${{ matrix.config == 'ubuntu-22.04' }} + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: 3.13 + cache: pip + allow-prereleases: true + - name: Get dependencies run: | sudo apt-get update -y @@ -106,6 +114,12 @@ jobs: xargs -a <(awk '! /^ *(#|$)/' ".github/workflows/apt.txt") -r -- \ sudo apt-get install -y --no-install-recommends --no-install-suggests + - name: Install Python dependencies + if: ${{ matrix.config == 'ubuntu-22.04' }} + run: | + python -m pip install --upgrade pip + pip install -r .github/workflows/python_requirements.txt + - name: Create installation directory run: | mkdir $HOME/install @@ -132,7 +146,6 @@ jobs: # TODO: -pedantic-errors here won't compile CXXFLAGS: -fPIC run: .github/workflows/build_${{ matrix.config }}.sh $HOME/install -Werror - - name: Add the bin directory to PATH run: | echo "$HOME/install/bin" >> $GITHUB_PATH From 9b05e37aa005a13589d4900e90439068a8d6b632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Tue, 24 Sep 2024 01:06:51 +0000 Subject: [PATCH 2/9] CI(Ubuntu): Install packages from pip --- .github/workflows/ubuntu.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 3ad145115e7..aea97c989fd 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -119,6 +119,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r .github/workflows/python_requirements.txt + pip install -r .github/workflows/optional_requirements.txt - name: Create installation directory run: | From 331bb54a9664664fb6a1d1e819a631438128409c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:16:20 -0400 Subject: [PATCH 3/9] Update PR for Ubuntu 24.04 --- .github/workflows/ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 290c11a370a..88bfa684c90 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -105,7 +105,7 @@ jobs: STEPS_GET_EXCLUDE_OUTPUTS_EXTRA_EXCLUDE: ${{ steps.get-exclude.outputs.extra-exclude }} - name: Set up Python - if: ${{ matrix.config == 'ubuntu-22.04' }} + if: ${{ matrix.config == 'ubuntu-24.04' }} uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: 3.13 @@ -122,7 +122,7 @@ jobs: sudo apt-get install -y --no-install-recommends --no-install-suggests - name: Install Python dependencies - if: ${{ matrix.config == 'ubuntu-22.04' }} + if: ${{ matrix.config == 'ubuntu-24.04' }} run: | python -m pip install --upgrade pip pip install -r .github/workflows/python_requirements.txt From fd816fcf2c5e4a741e5ba10e911690bed255c616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:02:06 +0000 Subject: [PATCH 4/9] Ignore zizmor cache poisoning warning for ubuntu workflow --- .github/workflows/ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 88bfa684c90..d8f2b8fead9 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -109,7 +109,7 @@ jobs: uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: 3.13 - cache: pip + cache: pip # zizmor: ignore[cache-poisoning] This workflow is not used for release artifacts allow-prereleases: true - name: Get dependencies From 4d2da1c18552037c2fbba1ad4847fd9ebf2bbe8e Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 2 Oct 2026 19:27:35 -0400 Subject: [PATCH 5/9] r.sim: Lock the infiltration capacity of a cell (#8001) With nprocs > 1, walkers on different threads checked and reduced the remaining infiltration capacity of a cell without synchronization. This is a data race and undefined behavior in C: two walkers could both be absorbed by the capacity left for one, or a reduction could be lost. Run the unchanged infiltration code in a critical section, entered only when an atomic read shows capacity left in the cell. Within the lock, the code checks the capacity again, as another thread may have used it up. The capacity is written atomically, since it is also read outside of the lock. After the first step, few walkers are in a cell with capacity left, so the lock is rarely taken. The arithmetic and the conditions are unchanged. With one thread, the depth maps computed from the elev_lid792_1m elevation, with and without infiltration, are byte-identical to those before this change, and the run time is the same within noise with 1 and 4 threads. With more threads, the order in which walkers reach a cell still depends on thread scheduling, as before. A new test checks that infiltration removes exactly the capacity of the cells at 1 and 4 threads. Written with the help of Claude Code. --- .../tests/r_sim_water_nprocs_test.py | 21 ++++++ raster/r.sim/simlib/hydro.c | 73 +++++++++++-------- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/raster/r.sim/r.sim.water/tests/r_sim_water_nprocs_test.py b/raster/r.sim/r.sim.water/tests/r_sim_water_nprocs_test.py index 8510050fa6c..73f4fe40412 100644 --- a/raster/r.sim/r.sim.water/tests/r_sim_water_nprocs_test.py +++ b/raster/r.sim/r.sim.water/tests/r_sim_water_nprocs_test.py @@ -18,6 +18,8 @@ RAIN = np.where( (abs(row - SIZE // 2) <= SIZE // 4) & (abs(col - SIZE // 2) <= SIZE // 4), 50, 0 ) +# Infiltration only where it rains, at a lower rate than the rain +INFILTRATION = np.where(RAIN > 0, 20, 0) @pytest.fixture(scope="module") @@ -76,3 +78,22 @@ def test_all_walkers_move_with_nprocs(session): single = total_water(depth(session, nprocs=1, **options)) multiple = total_water(depth(session, nprocs=4, **options)) assert multiple == pytest.approx(single, rel=1e-6) + + +def test_infiltration_takes_exactly_the_capacity(session): + """Infiltration removes exactly the capacity of the cells, also in parallel. + + The walkers created in a cell outweigh its infiltration capacity, so the + capacity is used up in the first step, before any walker moves, and it + is not restored. The water left for the rest of the simulation is then + the rain minus the infiltration, so the total is that fraction of the + total without infiltration. + """ + expected = total_water( + depth(session, nprocs=1, infil_value=0, diffusion_coeff=0) + ) * (1 - INFILTRATION.sum() / RAIN.sum()) + for nprocs in (1, 4): + actual = total_water( + depth(session, nprocs=nprocs, infil=INFILTRATION, diffusion_coeff=0) + ) + assert actual == pytest.approx(expected, rel=1e-6) diff --git a/raster/r.sim/simlib/hydro.c b/raster/r.sim/simlib/hydro.c index 2bcc53bcc88..1cd732ade7c 100644 --- a/raster/r.sim/simlib/hydro.c +++ b/raster/r.sim/simlib/hydro.c @@ -178,39 +178,54 @@ void main_loop(const Setup *setup, const Geometry *geometry, } if (grids->zz[k][l] != UNDEF) { - if (grids->inf[k][l] != UNDEF && - grids->inf[k][l] > 0) { - // Walker's contribution to water depth in this - // cell for this timestep [m] - double decr = factor * sim->w[lw].m; - // Compare with the depth the cell can absorb - // this timestep [m] - if (grids->inf[k][l] * setup->deltap > decr) { - // The cell can absorb the full walker. - // Reduce infiltration rate [m/s]. - grids->inf[k][l] -= decr / setup->deltap; - // Eliminate the walker - sim->w[lw].m = 0.; - continue; - } - else { - // The cell can't absorb the full walker. - // Reduce the walker mass by the equivalent - // of what an infiltration-rate source would - // generate as walker weight. - sim->w[lw].m -= sim->rwalk * - grids->inf[k][l] / - setup->sisum; - // Cell's infiltration capacity is fully - // exhausted - grids->inf[k][l] = 0.; - // Eliminate walker if needed - if (sim->w[lw].m < 0.) { + /* Lock only cells which may have capacity left. */ + double inf; +#pragma omp atomic read + inf = grids->inf[k][l]; + bool eliminated = false; + if (inf > 0) { +#pragma omp critical(infiltration) + if (grids->inf[k][l] != UNDEF && + grids->inf[k][l] > 0) { + // Walker's contribution to water depth in + // this cell for this timestep [m] + double decr = factor * sim->w[lw].m; + // Compare with the depth the cell can + // absorb this timestep [m] + if (grids->inf[k][l] * setup->deltap > + decr) { + // The cell can absorb the full walker. + // Reduce infiltration rate [m/s]. +#pragma omp atomic + grids->inf[k][l] -= + decr / setup->deltap; + // Eliminate the walker sim->w[lw].m = 0.; - continue; + eliminated = true; + } + else { + // The cell can't absorb the full + // walker. Reduce the walker mass by the + // equivalent of what an + // infiltration-rate source would + // generate as walker weight. + sim->w[lw].m -= sim->rwalk * + grids->inf[k][l] / + setup->sisum; + // Cell's infiltration capacity is fully + // exhausted +#pragma omp atomic write + grids->inf[k][l] = 0.; + // Eliminate walker if needed + if (sim->w[lw].m < 0.) { + sim->w[lw].m = 0.; + eliminated = true; + } } } } + if (eliminated) + continue; /* Add walker weight to water depth or * concentration. The captured sum includes the From 8b60e9ff0d0ec8dd9cbd4222f9159852a7b21081 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 2 Oct 2026 23:08:47 -0400 Subject: [PATCH 6/9] r.sim: Write time series maps at their output step times (#8003) With -t, r.sim.water wrote the series maps every (int)(output_step / time step) iterations and named them by the simulated time rounded to whole minutes. The truncated interval made the write times drift away from the output steps, so two writes could round to the same minute and the first map was overwritten, names could skip a minute or land on odd minutes with an even output_step, and the last step was often missing. When the time step was longer than output_step, the interval was zero and no map was written at all while the tool still succeeded. Write output step j at the iteration whose simulated time is closest to j times output_step, and name and timestamp the maps by that time. The time step comes from the cell size and the mean velocity, so iterations usually do not fall on the output times. When one iteration is the closest one to several steps, write only the step closest to it and warn that the time step is longer than output_step. Do not write steps past the duration. Always end the series with maps named by the duration which hold the state at the end of the run, also when the duration is not a multiple of output_step or when the run stopped early. Walker vector maps follow the same schedule. For users, each output step now appears once, named and timestamped by a multiple of output_step, and the maps named by the duration are always present. A series map holds the state at most half a time step before or after the time in its name instead of up to several minutes before it. The depth and discharge values are unchanged and so are the outputs without -t. In the run summary, simulated_time of an output is the time the maps were written and timestamp the time they are named by. New tests check the names, timestamps and write times of the series, a duration which is not a multiple of output_step, time steps longer than output_step including its multiples, a step past the duration, and an early stop. Written with the help of Claude Code. --- raster/r.sim/r.sim.water/r.sim.water.html | 16 +- raster/r.sim/r.sim.water/r.sim.water.md | 22 ++- .../tests/r_sim_water_summary_test.py | 4 +- .../r.sim.water/tests/r_sim_water_test.py | 16 +- .../tests/r_sim_water_time_series_test.py | 169 ++++++++++++++++++ raster/r.sim/simlib/hydro.c | 50 ++++-- raster/r.sim/simlib/input.c | 9 +- raster/r.sim/simlib/output.c | 8 +- raster/r.sim/simlib/simlib.h | 7 +- 9 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 raster/r.sim/r.sim.water/tests/r_sim_water_time_series_test.py diff --git a/raster/r.sim/r.sim.water/r.sim.water.html b/raster/r.sim/r.sim.water/r.sim.water.html index 5e280a77876..7e125f78243 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.html +++ b/raster/r.sim/r.sim.water/r.sim.water.html @@ -90,7 +90,15 @@

DESCRIPTION

the time series flag -t and output_step parameter defining the time step in minutes for writing output files. Files are saved with a suffix representing time since the start of simulation in minutes -(e.g. wdepth.05, wdepth.10). +(e.g. wdepth.05, wdepth.10) and are timestamped with that time. +The simulation advances in time steps which usually do not fall exactly on the output times. +A map holds the state at the time step closest to the time in its name, +so at most half a time step earlier or later. +When the time step is longer than output_step, there are fewer time steps than +output times, and a time step writes only the maps for the output time closest to it. +The series always ends with maps named by the duration which hold the state +at the end of the run, also when the duration is not a multiple of output_step +or when the simulation stopped early. Monitoring of water depth at specific points is supported. A vector map with observation points and a path to a logfile must be provided. For each point in the vector map which is located in @@ -209,12 +217,12 @@

Run summary

mean_source_rateMean rainfall excessm/s mean_infiltrationMean infiltration rate, 0 without infiltration inputm/s threadsThreads used for the computationcount - outputsOne entry per set of written maps: one per output_step with -t, otherwise a single entry + outputsOne entry per set of written maps: with -t, one per written output step, the last one named by duration, otherwise a single entry

-Each entry of outputs contains the simulated_time (s) and timestamp -of the written maps, the number of walkers_remaining at that time, and +Each entry of outputs contains the simulated_time (s) when the maps +were written, their timestamp, the number of walkers_remaining at that time, and the names of the depth, discharge, error and walkers maps, or null for maps which were not requested. diff --git a/raster/r.sim/r.sim.water/r.sim.water.md b/raster/r.sim/r.sim.water/r.sim.water.md index 692f0d06c90..ec2fded33af 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.md +++ b/raster/r.sim/r.sim.water/r.sim.water.md @@ -80,7 +80,17 @@ water depth and discharge maps can be saved during simulation using the time series flag **-t** and **output_step** parameter defining the time step in minutes for writing output files. Files are saved with a suffix representing time since the start of simulation in minutes (e.g. -wdepth.05, wdepth.10). Monitoring of water depth at specific points is +wdepth.05, wdepth.10) and are timestamped with that time. The +simulation advances in time steps which usually do not fall exactly on +the output times. A map holds the state at the time step closest to the +time in its name, so at most half a time step earlier or later. When the +time step is longer than **output_step**, there are fewer time steps +than output times, and a time step writes only the maps for the output +time closest to it. The series always ends with maps named by the +**duration** which hold the state at the end of the run, also when the +duration is not a multiple of **output_step** or when the simulation +stopped early. +Monitoring of water depth at specific points is supported. A vector map with observation points and a path to a logfile must be provided. For each point in the vector map which is located in the computational region the water depth is logged each time step in the @@ -197,12 +207,12 @@ stored in the history of the output raster maps under the same keys (see | `mean_source_rate` | Mean rainfall excess | m/s | | `mean_infiltration` | Mean infiltration rate, 0 without infiltration input | m/s | | `threads` | Threads used for the computation | count | -| `outputs` | One entry per set of written maps: one per **output_step** with **-t**, otherwise a single entry | | +| `outputs` | One entry per set of written maps: with **-t**, one per written output step, the last one named by **duration**, otherwise a single entry | | -Each entry of `outputs` contains the `simulated_time` (s) and `timestamp` -of the written maps, the number of `walkers_remaining` at that time, and -the names of the `depth`, `discharge`, `error` and `walkers` maps, or -`null` for maps which were not requested. +Each entry of `outputs` contains the `simulated_time` (s) when the maps +were written, their `timestamp`, the number of `walkers_remaining` at +that time, and the names of the `depth`, `discharge`, `error` and +`walkers` maps, or `null` for maps which were not requested. Summary of a time series run with two output steps in JSON: diff --git a/raster/r.sim/r.sim.water/tests/r_sim_water_summary_test.py b/raster/r.sim/r.sim.water/tests/r_sim_water_summary_test.py index a7c3f0fd74f..d62e3b5abc6 100644 --- a/raster/r.sim/r.sim.water/tests/r_sim_water_summary_test.py +++ b/raster/r.sim/r.sim.water/tests/r_sim_water_summary_test.py @@ -120,8 +120,8 @@ def test_json_summary_time_series(session_tools): assert [output["depth"] for output in outputs] == ["depth.01", "depth.02"] assert outputs[0]["simulated_time"] < outputs[1]["simulated_time"] assert summary["simulated_time"] >= outputs[1]["simulated_time"] - for output in outputs: - assert output["timestamp"] == timestamp_for(output["simulated_time"]) + for minute, output in enumerate(outputs, start=1): + assert output["timestamp"] == f"{minute} minutes" assert output["discharge"] is None assert session_tools.r_info(map=output["depth"], format="json")["rows"] == 5 diff --git a/raster/r.sim/r.sim.water/tests/r_sim_water_test.py b/raster/r.sim/r.sim.water/tests/r_sim_water_test.py index 8dcf2f02736..0e6f67a61f3 100644 --- a/raster/r.sim/r.sim.water/tests/r_sim_water_test.py +++ b/raster/r.sim/r.sim.water/tests/r_sim_water_test.py @@ -529,12 +529,12 @@ def test_duration_affects_time_series_progression(long_slope_session): """A longer duration must create more time-series output maps. With output_step=5, duration=10 produces maps at t=5,10 while - duration=20 produces maps at t=5,10,15 and, if walkers survive to the - end, also t=20. The exact count of the longer run is not pinned: whether - the final t=20 map appears depends on whether the last walkers leave the - domain before the final step, which varies with seed and platform. We - assert the robust lower bound (at least three maps) and that the longer - simulation yields strictly more maps than the shorter one. + duration=20 produces maps at t=5,10,15,20. The exact count of the longer + run is not pinned: when the last walkers leave the domain early, which + varies with seed and platform, the steps after that are not written, + only the final t=20 map. We assert the robust lower bound (at least + three maps) and that the longer simulation yields strictly more maps + than the shorter one. """ tools = Tools(session=long_slope_session) @@ -573,8 +573,8 @@ def test_duration_affects_time_series_progression(long_slope_session): f"10-min simulation with output_step=5 should produce 2 maps, got {len(maps_10)}" ) - # 20-min run should produce at least 3 time-series maps (t=5, t=10, t=15; - # t=20 as well when the last walkers survive to the final step). + # 20-min run should produce at least 3 time-series maps (t=5, t=10 and + # t=20, and t=15 as well when the last walkers stay past it). maps_20 = list(tools.g_list(type="raster", pattern="depth_20min*", format="json")) assert len(maps_20) >= 3, ( f"20-min simulation with output_step=5 should produce at least 3 maps, " diff --git a/raster/r.sim/r.sim.water/tests/r_sim_water_time_series_test.py b/raster/r.sim/r.sim.water/tests/r_sim_water_time_series_test.py new file mode 100644 index 00000000000..dbcdd628559 --- /dev/null +++ b/raster/r.sim/r.sim.water/tests/r_sim_water_time_series_test.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Tests of when and under which names r.sim.water writes time series maps""" + +import os + +import pytest + +import grass.script as gs +from grass.experimental.mapset import TemporaryMapsetSession +from grass.tools import Tools + +# On this slope, the time step is about 10.7 s, so a 1-minute output step +# does not fall on an iteration and the old schedule, which wrote every +# (int)(60 / 10.7) = 5 iterations and named maps by the rounded simulated +# time, wrote dep.04 twice. +SLOPE = { + "elevation": "elevation", + "man_value": 0.1, + "duration": 10, + "random_seed": 1, + "nprocs": 1, +} + + +@pytest.fixture(scope="module") +def slope_project(tmp_path_factory): + """Project with a 50 m by 500 m plane at 10 m rising 0.1 m per row""" + project = tmp_path_factory.mktemp("simwe_time_series") / "simwe" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + tools = Tools(session=session) + tools.g_region(s=0, n=500, w=0, e=50, res=10, flags="s") + tools.r_mapcalc(expression="elevation = row() * 0.1") + yield session + + +@pytest.fixture +def slope_tools(slope_project): + with ( + TemporaryMapsetSession(env=slope_project.env) as session, + Tools(session=session, capture_stderr=True) as tools, + ): + yield tools + + +def minutes_of_timestamp(tools, name): + """Minutes of a raster timestamp such as '1 minute' or '10 minutes'""" + number, unit = tools.r_timestamp(map=name).text.split() + assert unit in {"minute", "minutes"} + return int(number) + + +def check_series(tools, summary, minutes): + """Series maps are named and timestamped by the given minutes. + + Each map is written at the iteration closest to its time, except the + last one which is written at the end of the run. + """ + outputs = summary["outputs"] + names = [f"dep.{minute:02d}" for minute in minutes] + assert [output["depth"] for output in outputs] == names + listed = [item["name"] for item in tools.g_list(type="raster", format="json")] + assert sorted(listed) == sorted([*names, "elevation"]) + for minute, output in zip(minutes[:-1], outputs[:-1], strict=True): + assert output["timestamp"] == f"{minute} minutes" + assert minutes_of_timestamp(tools, output["depth"]) == minute + assert abs(output["simulated_time"] - minute * 60) <= summary["time_step"] / 2 + assert outputs[-1]["timestamp"] == f"{minutes[-1]} minutes" + assert minutes_of_timestamp(tools, names[-1]) == minutes[-1] + assert outputs[-1]["simulated_time"] == summary["simulated_time"] + + +def test_every_output_step_written_once(slope_tools): + """Each output step gets one map named and timestamped by its time""" + summary = slope_tools.r_sim_water( + **SLOPE, + depth="dep", + walkers_output="walkers", + output_step=1, + flags="tp", + format="json", + ).json + check_series(slope_tools, summary, list(range(1, 11))) + # Walker maps follow the same schedule with an underscore separator. + walkers = [output["walkers"] for output in summary["outputs"]] + assert walkers == [f"walkers_{minute:02d}" for minute in range(1, 11)] + listed = slope_tools.g_list(type="vector", format="json") + assert sorted(item["name"] for item in listed) == walkers + + +def test_final_step_named_by_duration(slope_tools): + """Duration which is not a multiple of output_step still ends the series""" + summary = slope_tools.r_sim_water( + **SLOPE, depth="dep", output_step=3, flags="tp", format="json" + ).json + check_series(slope_tools, summary, [3, 6, 9, 10]) + + +@pytest.mark.parametrize( + ("mintimestep", "time_step", "minutes"), + [ + (17.5, 70, [1, 2, 4, 5, 6, 7, 8, 9, 10]), + (30, 120, [2, 4, 6, 8, 10]), + (45, 180, [3, 6, 9, 10]), + ], +) +def test_time_step_longer_than_output_step( + slope_tools, mintimestep, time_step, minutes +): + """An iteration writes only the output step closest to it + + The simulated time per iteration is the minimum time step times the + time coefficient 4. With 70 s, the iteration ending at 210 s is equally + close to 3 and 4 minutes and writes the later one. With 120 s and 180 s, + the iterations fall exactly on every second and third output step. The + old schedule wrote every (int)(60 / time step) = 0 iterations, that is, + no maps at all. + """ + result = slope_tools.r_sim_water( + **SLOPE, + depth="dep", + output_step=1, + mintimestep=mintimestep, + flags="tp", + format="json", + ) + summary = result.json + assert summary["time_step"] == time_step + check_series(slope_tools, summary, minutes) + assert "longer than output_step" in result.stderr + + +def test_final_step_after_early_stop(slope_tools): + """The series ends with the duration also when all walkers left early + + With a very low Manning's n, all walkers leave the slope before the + first output step. + """ + summary = slope_tools.r_sim_water( + **(SLOPE | {"man_value": 0.002, "duration": 2}), + depth="dep", + output_step=1, + flags="tp", + format="json", + ).json + assert summary["stopped_early"] is True + assert summary["simulated_time"] < 60 + assert [output["depth"] for output in summary["outputs"]] == ["dep.02"] + assert summary["outputs"][0]["timestamp"] == "2 minutes" + + +def test_no_output_step_past_duration(slope_tools): + """An output step past the duration is not written + + With a time step of 160 s and a duration of 11 minutes, the last + iteration ends at 640 s. The output step closest to it is 12 minutes, + which is past the duration. + """ + summary = slope_tools.r_sim_water( + **(SLOPE | {"duration": 11}), + depth="dep", + output_step=3, + mintimestep=40, + flags="tp", + format="json", + ).json + assert summary["time_step"] == 160 + check_series(slope_tools, summary, [3, 6, 9, 11]) diff --git a/raster/r.sim/simlib/hydro.c b/raster/r.sim/simlib/hydro.c index 1cd732ade7c..457872cf865 100644 --- a/raster/r.sim/simlib/hydro.c +++ b/raster/r.sim/simlib/hydro.c @@ -44,6 +44,8 @@ void main_loop(const Setup *setup, const Geometry *geometry, int iblock; double conn = 1.0; double addac; + // Time of the last time series output step written [s] + double series_time = 0.; // nblock is reserved for Monte Carlo replicas. A future // change will allow nblock > 1, give each replica an @@ -124,12 +126,6 @@ void main_loop(const Setup *setup, const Geometry *geometry, for (i = 1; i <= setup->miter; i++) { /* iteration loop depending on simulation time and deltap */ G_percent(i, setup->miter, 1); - if (setup->iterout > 0 && i % setup->iterout == 0) { - /* nfiterw = i / iterout + 10; - nfiterh = i / iterout + 40; */ - G_debug(2, "iblock=%d i=%d miter=%d nwalk=%d nwalka=%d", iblock, - i, setup->miter, sim->nwalk, sim->nwalka); - } if (sim->nwalka == 0 && i > 1) { summary->stopped_early = true; @@ -319,13 +315,33 @@ void main_loop(const Setup *setup, const Geometry *geometry, /* Total remaining walkers for this iteration */ sim->nwalka = nwalka; + // Output step j is written at the iteration closest to + // j * output_step and named by that time. With a time step + // longer than output_step, an iteration is the closest one to + // several steps and writes the step closest to it. + bool write_series = false; + if (settings->ts && settings->iterout > 0) { + double iteration_time = simulated_seconds(setup, i); + double step_time = + settings->iterout * + floor(iteration_time / settings->iterout + 0.5); + // A step more than half a time step ahead is closer to the + // next iteration. No step is past the duration. + if (step_time > series_time && + step_time <= + iteration_time + 0.5 * time_step_seconds(setup) && + step_time <= settings->timesec) { + series_time = step_time; + write_series = true; + } + } + /* Changes made by Soeren 8. Mar 2011 to replace the site walker * output implementation */ /* Save all walkers located within the computational region and with valid z coordinates */ if (outputs->outwalk != NULL && - (i == setup->miter || - (setup->iterout > 0 && i % setup->iterout == 0))) { + (i == setup->miter || write_series)) { sim->nstack = 0; for (lw = 0; lw < sim->nwalk; lw++) { @@ -356,15 +372,18 @@ void main_loop(const Setup *setup, const Geometry *geometry, } /* lw loop */ } - if (settings->ts && setup->iterout > 0 && i % setup->iterout == 0) { + if (write_series) { + G_debug(2, "iblock=%d i=%d miter=%d nwalk=%d nwalka=%d", iblock, + i, setup->miter, sim->nwalk, sim->nwalka); /* call output for iteration output */ if (outputs->erdep != NULL) erod(grids->gama, setup, geometry, grids); /* divergence of gama field */ double itime = simulated_seconds(setup, i); - int ii = output_data(itime, conn, setup, geometry, settings, - sim, inputs, outputs, grids, summary); + int ii = + output_data(itime, series_time, conn, setup, geometry, + settings, sim, inputs, outputs, grids, summary); if (ii != 1) G_fatal_error(_("Unable to write raster maps")); } @@ -457,13 +476,16 @@ void main_loop(const Setup *setup, const Geometry *geometry, } /* Write final maps here because we know the last time stamp here */ - if (!settings->ts) { + // A time series always ends with a step named by the duration, also when + // the duration is not a multiple of output_step or the run stopped early. + if (!settings->ts || series_time < settings->timesec) { // All blocks have completed; gama is the eventual cumulative total, // so no extrapolation is needed. conn = 1.0; double itime = simulated_seconds(setup, summary->iterations_completed); - int ii = output_data(itime, conn, setup, geometry, settings, sim, - inputs, outputs, grids, summary); + double name_time = settings->ts ? settings->timesec : itime; + int ii = output_data(itime, name_time, conn, setup, geometry, settings, + sim, inputs, outputs, grids, summary); if (ii != 1) G_fatal_error(_("Cannot write raster maps")); } diff --git a/raster/r.sim/simlib/input.c b/raster/r.sim/simlib/input.c index 9987711e71d..ddb6298fbe8 100644 --- a/raster/r.sim/simlib/input.c +++ b/raster/r.sim/simlib/input.c @@ -339,10 +339,11 @@ int grad_check(Setup *setup, const Geometry *geometry, const Settings *settings, (int)(settings->timesec / (setup->deltap * setup->timec)); /* number of iterations = number of cells to pass */ - setup->iterout = - (int)(settings->iterout / - (setup->deltap * setup->timec)); /* number of cells to pass for - time series output */ + if (settings->ts && settings->iterout > 0 && + time_step_seconds(setup) > settings->iterout) + G_warning(_("Time step of %.2f s is longer than output_step of %d s, " + "some output steps may be skipped"), + time_step_seconds(setup), settings->iterout); fprintf(stderr, "\n"); G_message(_("Min elevation \t= %.2f m\nMax elevation \t= %.2f m\n"), zmin, diff --git a/raster/r.sim/simlib/output.c b/raster/r.sim/simlib/output.c index f322ac65221..393e2b12eb7 100644 --- a/raster/r.sim/simlib/output.c +++ b/raster/r.sim/simlib/output.c @@ -143,12 +143,14 @@ static void write_history(const char *name, double tt, const Setup *setup, G_write_raster_timestamp(name, timestamp); } -/* conn is the sequential-block extrapolation factor nblock/iblock: scales +/* tt is the simulated time reached [s] and name_time the time [s] the maps + * are named and timestamped by, rounded to minutes. + * conn is the sequential-block extrapolation factor nblock/iblock: scales * the cumulative partial sum in gama into an estimator of the eventual total * for snapshots taken before all blocks have run. With nblock = 1 (or after * the final block) conn = 1.0 and the output formulas are unchanged. err is * written from gammas, which already has conn baked into its accumulator. */ -int output_data(double tt, double conn, const Setup *setup, +int output_data(double tt, double name_time, double conn, const Setup *setup, const Geometry *geometry, const Settings *settings, const Simulation *sim, const Inputs *inputs, const Outputs *outputs, const Grids *grids, Summary *summary) @@ -187,7 +189,7 @@ int output_data(double tt, double conn, const Setup *setup, ndigit = 5; /* Convert to minutes */ - tt_minutes = (int)(tt / 60. + 0.5); + tt_minutes = (int)(name_time / 60. + 0.5); /* Create timestamp */ snprintf(timestamp_buf, sizeof(timestamp_buf), "%d minutes", tt_minutes); diff --git a/raster/r.sim/simlib/simlib.h b/raster/r.sim/simlib/simlib.h index be11d77350b..fe71bea6012 100644 --- a/raster/r.sim/simlib/simlib.h +++ b/raster/r.sim/simlib/simlib.h @@ -33,14 +33,13 @@ typedef struct { double hbeta; // Weighting factor for water flow velocity vector double hhmax; // Threshold water depth [m] double frac; // Water diffusion constant - int iterout; // Time interval for creating output maps [minutes] - int timesec; // Time how long the simulation runs [minutes] + int iterout; // Time interval for creating output maps [seconds] + int timesec; // Time how long the simulation runs [seconds] bool ts; // Time series output double mintimestep; // Minimum time step for the simulation [seconds] } Settings; typedef struct { - int iterout; // Number of iterations for creating output maps int miter; // Total number of iterations double chmean; // Mean Manning's n double si0; // Mean rainfall excess (or sediment concentration?) @@ -177,7 +176,7 @@ void main_loop(const Setup *setup, const Geometry *geometry, const Settings *settings, Simulation *sim, ObservationPoints *points, const Inputs *inputs, const Outputs *outputs, Grids *grids, Summary *summary); -int output_data(double tt, double conn, const Setup *setup, +int output_data(double tt, double name_time, double conn, const Setup *setup, const Geometry *geometry, const Settings *settings, const Simulation *sim, const Inputs *inputs, const Outputs *outputs, const Grids *grids, Summary *summary); From 4d8e578e4818d7c83b997864841c2a309c86abd8 Mon Sep 17 00:00:00 2001 From: SAKET KUMAR MALL <72020337+saket0187@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:15:36 +0530 Subject: [PATCH 7/9] wxGUI: Store history region in g.region JSON format (#7973) Store history region in g.region JSON format, use Tools API for region operations --- gui/wxpython/history/browser.py | 42 ++++++++++++++++++++++++--------- python/grass/grassdb/history.py | 4 +++- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/gui/wxpython/history/browser.py b/gui/wxpython/history/browser.py index 98db94cdfc8..6f0817accc4 100644 --- a/gui/wxpython/history/browser.py +++ b/gui/wxpython/history/browser.py @@ -16,6 +16,7 @@ @author Tomas Zigo """ +import math from datetime import datetime import wx @@ -25,7 +26,7 @@ from history.tree import HistoryBrowserTree from icons.icon import MetaIcon -import grass.script as gs +from grass.tools import Tools from grass.grassdb import history @@ -40,10 +41,10 @@ "status": _("Status:"), "mask2d": _("Mask 2D:"), "mask3d": _("Mask 3D:"), - "n": _("North:"), - "s": _("South:"), - "w": _("West:"), - "e": _("East:"), + "north": _("North:"), + "south": _("South:"), + "west": _("West:"), + "east": _("East:"), "nsres": _("North-south resolution:"), "ewres": _("East-west resolution:"), "rows": _("Number of rows:"), @@ -69,6 +70,18 @@ def make_label(key): return TRANSLATION_KEYS.get(key, "") +# Region keys in the shell format of g.region, which are also the names of the g.region parameters +REGION_KEYS = {"n": "north", "s": "south", "w": "west", "e": "east"} + + +def normalize_region(region): + """Return region settings with the keys used by g.region format=json. + + History entries recorded by older versions use the keys of the shell format. + """ + return {REGION_KEYS.get(key, key): value for key, value in region.items()} + + class HistoryInfoPanel(SP.ScrolledPanel): def __init__(self, parent, giface, title=("Command Info"), style=wx.TAB_TRAVERSAL): super().__init__(parent=parent, id=wx.ID_ANY, style=style) @@ -76,6 +89,7 @@ def __init__(self, parent, giface, title=("Command Info"), style=wx.TAB_TRAVERSA self.parent = parent self.giface = giface self.title = title + self.tools = Tools() self.region_settings = None @@ -166,7 +180,7 @@ def _general_info_filter(self, key, value): return key in filter_keys or ((key in {"mask2d", "mask3d"}) and value is True) def _region_settings_filter(self, key): - return key not in {"projection", "zone", "cells"} + return key not in {"crs", "projection", "zone", "cells"} def _updateGeneralInfoBox(self, command_info): """Update a static box for displaying general info about the command. @@ -213,7 +227,7 @@ def _updateRegionSettingsGrid(self, command_info): """ self.sizer_region_settings_grid.Clear(True) - self.region_settings = command_info["region"] + self.region_settings = normalize_region(command_info["region"]) idx = 0 for key, value in self.region_settings.items(): if self._region_settings_filter(key): @@ -248,9 +262,12 @@ def _updateRegionSettingsMatch(self): self.sizer_region_settings_match.Clear(True) # Region condition - history_region = self.region_settings + history_region = self._get_history_region() current_region = self._get_current_region() - region_matches = history_region == current_region + region_matches = all( + math.isclose(value, current_region[key], rel_tol=0, abs_tol=1e-8) + for key, value in history_region.items() + ) # Icon and button according to the condition if region_matches: @@ -320,7 +337,7 @@ def hideCommandInfo(self): def _get_current_region(self): """Get current computational region settings.""" - return gs.region() + return self.tools.g_region(flags="p", format="json").json def _get_history_region(self): """Get computational region settings of executed command.""" @@ -332,8 +349,11 @@ def _get_history_region(self): def OnUpdateRegion(self, event): """Set current region to the region of executed command.""" + parameters = {value: key for key, value in REGION_KEYS.items()} history_region = self._get_history_region() - gs.run_command("g.region", **history_region) + self.tools.g_region( + **{parameters.get(key, key): value for key, value in history_region.items()} + ) self.giface.updateMap.emit(render=False, renderVector=False) self._updateRegionSettingsMatch() self.Layout() diff --git a/python/grass/grassdb/history.py b/python/grass/grassdb/history.py index 5608f792274..3c3cba5e264 100644 --- a/python/grass/grassdb/history.py +++ b/python/grass/grassdb/history.py @@ -264,7 +264,9 @@ def get_initial_command_info(env_run): mask3d_name = f"RASTER3D_MASK@{env['MAPSET']}" # Computational region settings - region_settings = gs.region(env=env_run) + region_settings = gs.parse_command( + "g.region", flags="p", format="json", env=env_run + ) # Finalize the command info dictionary return { From 034a7a07bffe44bd6e081561bc876d89de01a65b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:49:52 -0400 Subject: [PATCH 8/9] CI(deps): Update ubuntu:26.04 Docker digest to 88a381d (main) (#7999) CI(deps): Update ubuntu:26.04 Docker digest to 88a381d Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- docker/ubuntu/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78f6a5f50d3..65936b9304f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # Changes to this file must be copied over to the other file. ARG GUI=without -FROM ubuntu:26.04@sha256:61ebaa5cc23ca45450db85eac015435199ec569e28ec222ea13f2aed2110b8a6 AS common_start +FROM ubuntu:26.04@sha256:88a381d5b5eeb2b35d3ad70925a362c37ce569daf43ede89ff818ec20e4d3794 AS common_start ARG BASE_NAME="ubuntu:26.04" ARG PYTHON_VERSION=3.14 diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 78f6a5f50d3..65936b9304f 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -4,7 +4,7 @@ # Changes to this file must be copied over to the other file. ARG GUI=without -FROM ubuntu:26.04@sha256:61ebaa5cc23ca45450db85eac015435199ec569e28ec222ea13f2aed2110b8a6 AS common_start +FROM ubuntu:26.04@sha256:88a381d5b5eeb2b35d3ad70925a362c37ce569daf43ede89ff818ec20e4d3794 AS common_start ARG BASE_NAME="ubuntu:26.04" ARG PYTHON_VERSION=3.14 From 0083972ede97356d6bdce7633dfc8fd50b3fbfcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:55:51 -0400 Subject: [PATCH 9/9] CI(deps): Update dependency pylint to v4.1.2 (main) (#7996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CI(deps): Update dependency pylint to v4.1.1 * Python: Fix new pylint chained-comparison and comparison-with-itself messages Pylint 4.1 reports R1716 (chained-comparison) and R0124 (comparison-with-itself). Rewrite the flagged conditions as chained comparisons, with unchanged logic, and use math.isnan() for the NaN checks in c_libraries_interface. Needed for the pylint update in #7996. Drafted with AI assistance (Claude Code); the changes are mechanical and were checked with pylint 4.1.1 and ruff. * Update pylint to 4.1.2 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Edouard Choinière <27212526+echoix@users.noreply.github.com> --- .github/workflows/python-code-quality.yml | 2 +- gui/wxpython/core/utils.py | 6 +----- gui/wxpython/gui_core/forms.py | 2 +- gui/wxpython/tplot/frame.py | 2 +- python/grass/pygrass/raster/segment.py | 2 +- .../grass/temporal/c_libraries_interface.py | 5 +++-- python/grass/temporal/temporal_extent.py | 20 +++++++++---------- scripts/i.pansharpen/i.pansharpen.py | 2 +- scripts/r.in.wms/wms_drv.py | 10 ++++++---- scripts/r.tileset/r.tileset.py | 4 ++-- 10 files changed, 27 insertions(+), 28 deletions(-) diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index e38fec4d2c5..242068a9f0d 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -32,7 +32,7 @@ jobs: # renovate: datasource=pypi depName=flake8 FLAKE8_VERSION: "7.3.0" # renovate: datasource=pypi depName=pylint - PYLINT_VERSION: "4.0.6" + PYLINT_VERSION: "4.1.2" # renovate: datasource=pypi depName=bandit BANDIT_VERSION: "1.9.4" # renovate: datasource=pypi depName=ruff diff --git a/gui/wxpython/core/utils.py b/gui/wxpython/core/utils.py index 305b6c1bc3d..addda95d614 100644 --- a/gui/wxpython/core/utils.py +++ b/gui/wxpython/core/utils.py @@ -230,11 +230,7 @@ def GetValidLayerName(name): cIdx += 1 retName = "".join(retNameList) - if ( - retName - and not (retName[0] >= "A" and retName[0] <= "Z") - and not (retName[0] >= "a" and retName[0] <= "z") - ): + if retName and not ("A" <= retName[0] <= "Z") and not ("a" <= retName[0] <= "z"): retName = "x" + retName[1:] if mapset: diff --git a/gui/wxpython/gui_core/forms.py b/gui/wxpython/gui_core/forms.py index c172b54a8c9..2cdbcce334c 100644 --- a/gui/wxpython/gui_core/forms.py +++ b/gui/wxpython/gui_core/forms.py @@ -3115,7 +3115,7 @@ def OnVerbosity(self, event): def OnPageChange(self, event): sel = self.notebook.GetSelection() if not event else event.GetSelection() idx = self.notebook.GetPageIndexByName("manual") - if idx > -1 and sel == idx: + if -1 < idx == sel: # calling LoadPage() is strangely time-consuming (only first call) # FIXME: move to helpPage.__init__() if not self.manualTab.IsLoaded(): diff --git a/gui/wxpython/tplot/frame.py b/gui/wxpython/tplot/frame.py index 9f7f025a50f..5788badd52e 100755 --- a/gui/wxpython/tplot/frame.py +++ b/gui/wxpython/tplot/frame.py @@ -493,7 +493,7 @@ def _getSTRDdata(self, timeseries): r.open() val = r.get_value(self.poi) r.close() - if val == -2147483648 and val < minmin: + if -2147483648 == val < minmin: self.timeDataR[name][row[0]]["value"] = None else: self.timeDataR[name][row[0]]["value"] = val diff --git a/python/grass/pygrass/raster/segment.py b/python/grass/pygrass/raster/segment.py index cc301cc3cef..3f8603f761e 100644 --- a/python/grass/pygrass/raster/segment.py +++ b/python/grass/pygrass/raster/segment.py @@ -33,7 +33,7 @@ def nseg(self): ) def segments_in_mem(self): - if self.maxmem > 0 and self.maxmem < 100: + if 0 < self.maxmem < 100: seg_in_mem = (self.maxmem * self.nseg()) / 100 else: seg_in_mem = 4 * (self.rows() / self.srows + self.cols() / self.scols + 2) diff --git a/python/grass/temporal/c_libraries_interface.py b/python/grass/temporal/c_libraries_interface.py index 3804296b9d3..be9e1b2d400 100644 --- a/python/grass/temporal/c_libraries_interface.py +++ b/python/grass/temporal/c_libraries_interface.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +import math import sys from ctypes import CFUNCTYPE, POINTER, byref, c_int, c_void_p, cast from datetime import datetime @@ -825,11 +826,11 @@ def _read_raster3d_info(name, mapset): return None libraster3d.Rast3d_range_min_max(g3map, byref(min), byref(max)) - if min.value != min.value: + if math.isnan(min.value): kvp["min"] = None else: kvp["min"] = float(min.value) - if max.value != max.value: + if math.isnan(max.value): kvp["max"] = None else: kvp["max"] = float(max.value) diff --git a/python/grass/temporal/temporal_extent.py b/python/grass/temporal/temporal_extent.py index ef3ab3a8feb..c6ed6d3b5a5 100644 --- a/python/grass/temporal/temporal_extent.py +++ b/python/grass/temporal/temporal_extent.py @@ -737,8 +737,7 @@ def during(self, extent) -> bool: # Check single point of time in interval if self.D["end_time"] is None: return bool( - self.D["start_time"] >= extent.D["start_time"] - and self.D["start_time"] < extent.D["end_time"] + extent.D["start_time"] <= self.D["start_time"] < extent.D["end_time"] ) return bool( @@ -776,8 +775,7 @@ def contains(self, extent) -> bool: # Check single point of time in interval if extent.D["end_time"] is None: return bool( - self.D["start_time"] <= extent.D["start_time"] - and self.D["end_time"] > extent.D["start_time"] + self.D["start_time"] <= extent.D["start_time"] < self.D["end_time"] ) return bool( @@ -853,9 +851,10 @@ def overlaps(self, extent) -> bool: return bool( self.D["end_time"] is not None and extent.D["end_time"] is not None - and self.D["start_time"] < extent.D["start_time"] - and self.D["end_time"] < extent.D["end_time"] - and self.D["end_time"] > extent.D["start_time"] + and self.D["start_time"] + < extent.D["start_time"] + < self.D["end_time"] + < extent.D["end_time"] ) def overlapped(self, extent) -> bool: @@ -893,9 +892,10 @@ def overlapped(self, extent) -> bool: return bool( self.D["end_time"] is not None and extent.D["end_time"] is not None - and self.D["start_time"] > extent.D["start_time"] - and self.D["end_time"] > extent.D["end_time"] - and self.D["start_time"] < extent.D["end_time"] + and extent.D["start_time"] + < self.D["start_time"] + < extent.D["end_time"] + < self.D["end_time"] ) def temporal_relation(self, extent): diff --git a/scripts/i.pansharpen/i.pansharpen.py b/scripts/i.pansharpen/i.pansharpen.py index 535fd032532..703f876c9e0 100755 --- a/scripts/i.pansharpen/i.pansharpen.py +++ b/scripts/i.pansharpen/i.pansharpen.py @@ -776,7 +776,7 @@ def matchhist(original, target, matched): for j in arrays[target]: # find the grey value in target that corresponds to the cdf # closest to the original cdf - if j[1] <= i[1] + min_difference and j[1] >= i[1] - min_difference: + if i[1] - min_difference <= j[1] <= i[1] + min_difference: # build a reclass rules file from the original grey value and # corresponding grey value from target out_line = "%d = %d\n" % (i[0], j[0]) diff --git a/scripts/r.in.wms/wms_drv.py b/scripts/r.in.wms/wms_drv.py index 754c6acb1f7..be171f97865 100644 --- a/scripts/r.in.wms/wms_drv.py +++ b/scripts/r.in.wms/wms_drv.py @@ -348,11 +348,13 @@ def _computeRequestData(self, bbox, tl_corner, tile_span, tile_size, mat_num_bbo for col in ["min_col", "max_col"]: for row in ["min_row", "max_row"]: if ( - self.t_num_bbox["min_row"] <= self.t_num_bbox[row] - and self.t_num_bbox[row] <= mat_num_bbox["max_row"] + self.t_num_bbox["min_row"] + <= self.t_num_bbox[row] + <= mat_num_bbox["max_row"] ) and ( - self.t_num_bbox["min_col"] <= self.t_num_bbox[col] - and self.t_num_bbox[col] <= mat_num_bbox["max_col"] + self.t_num_bbox["min_col"] + <= self.t_num_bbox[col] + <= mat_num_bbox["max_col"] ): self.intersects = True diff --git a/scripts/r.tileset/r.tileset.py b/scripts/r.tileset/r.tileset.py index 5397a850e3b..fd71601bf80 100644 --- a/scripts/r.tileset/r.tileset.py +++ b/scripts/r.tileset/r.tileset.py @@ -225,9 +225,9 @@ def bboxesIntersect(bbox_1, bbox_2) -> bool: cin = [False, False] for i in (0, 1): if ( - (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b1[i]) + (bi_a1[i] <= bi_b1[i] <= bi_a2[i]) or (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b2[i]) - or (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a1[i]) + or (bi_b1[i] <= bi_a1[i] <= bi_b2[i]) or (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a2[i]) ): cin[i] = True