Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a114b70
CI(Ubuntu): Install latest version of Python for full config
echoix Sep 24, 2024
9b05e37
CI(Ubuntu): Install packages from pip
echoix Sep 24, 2024
01e6667
Merge branch 'main' into ubuntu-max-py313
echoix Sep 25, 2024
1c335d7
Merge branch 'main' into ubuntu-max-py313
echoix Sep 25, 2024
234761f
Merge branch 'main' into ubuntu-max-py313
echoix Sep 30, 2024
aec46a0
Merge branch 'main' into ubuntu-max-py313
echoix Nov 1, 2024
90f1653
Merge branch 'main' into ubuntu-max-py313
echoix Dec 19, 2024
bd8a685
Merge branch 'main' into ubuntu-max-py313
echoix Mar 9, 2025
ae45cc7
Merge branch 'main' into ubuntu-max-py313
echoix May 17, 2025
bcaa567
Merge branch 'main' into ubuntu-max-py313
echoix Jul 23, 2025
cb4703c
Merge branch 'main' into ubuntu-max-py313
echoix Oct 18, 2025
331bb54
Update PR for Ubuntu 24.04
echoix Oct 18, 2025
fd816fc
Ignore zizmor cache poisoning warning for ubuntu workflow
echoix Oct 18, 2025
c8d52f8
Merge branch 'main' into ubuntu-max-py313
echoix Oct 18, 2025
d242fc2
Merge branch 'main' into ubuntu-max-py313
echoix Dec 2, 2025
caedbb2
Merge branch 'main' into ubuntu-max-py313
echoix May 11, 2026
4d2da1c
r.sim: Lock the infiltration capacity of a cell (#8001)
wenzeslaus Oct 2, 2026
8b60e9f
r.sim: Write time series maps at their output step times (#8003)
wenzeslaus Oct 3, 2026
4d8e578
wxGUI: Store history region in g.region JSON format (#7973)
saket0187 Oct 3, 2026
034a7a0
CI(deps): Update ubuntu:26.04 Docker digest to 88a381d (main) (#7999)
renovate[bot] Oct 3, 2026
0083972
CI(deps): Update dependency pylint to v4.1.2 (main) (#7996)
renovate[bot] Oct 3, 2026
f420f0f
Merge branch 'main' into ubuntu-max-py313
echoix Oct 3, 2026
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 .github/workflows/python-code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ jobs:
env:
STEPS_GET_EXCLUDE_OUTPUTS_EXTRA_EXCLUDE: ${{ steps.get-exclude.outputs.extra-exclude }}

- name: Set up Python
if: ${{ matrix.config == 'ubuntu-24.04' }}
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
python-version: 3.13
cache: pip # zizmor: ignore[cache-poisoning] This workflow is not used for release artifacts
allow-prereleases: true

- name: Get dependencies
run: |
sudo apt-get update -y
Expand All @@ -116,6 +124,13 @@ 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-24.04' }}
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: |
mkdir "${HOME}/install"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/ubuntu/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions gui/wxpython/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion gui/wxpython/gui_core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
42 changes: 31 additions & 11 deletions gui/wxpython/history/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@author Tomas Zigo
"""

import math
from datetime import datetime

import wx
Expand All @@ -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

Expand All @@ -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:"),
Expand All @@ -69,13 +70,26 @@ 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)

self.parent = parent
self.giface = giface
self.title = title
self.tools = Tools()

self.region_settings = None

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion gui/wxpython/tplot/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion python/grass/grassdb/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion python/grass/pygrass/raster/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions python/grass/temporal/c_libraries_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 10 additions & 10 deletions python/grass/temporal/temporal_extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
16 changes: 12 additions & 4 deletions raster/r.sim/r.sim.water/r.sim.water.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,15 @@ <h2>DESCRIPTION</h2>
the time series flag <b>-t</b> and <b>output_step</b> 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 <b>output_step</b>, 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 <b>duration</b> which hold the state
at the end of the run, also when the duration is not a multiple of <b>output_step</b>
or when the simulation stopped early.
<!-- TODO add example of registering outputs into temporal framework when #2146 is fixed -->
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
Expand Down Expand Up @@ -209,12 +217,12 @@ <h3>Run summary</h3>
<tr><td><code>mean_source_rate</code></td><td>Mean rainfall excess</td><td>m/s</td></tr>
<tr><td><code>mean_infiltration</code></td><td>Mean infiltration rate, 0 without infiltration input</td><td>m/s</td></tr>
<tr><td><code>threads</code></td><td>Threads used for the computation</td><td>count</td></tr>
<tr><td><code>outputs</code></td><td>One entry per set of written maps: one per <b>output_step</b> with <b>-t</b>, otherwise a single entry</td><td></td></tr>
<tr><td><code>outputs</code></td><td>One entry per set of written maps: with <b>-t</b>, one per written output step, the last one named by <b>duration</b>, otherwise a single entry</td><td></td></tr>
</table>

<p>
Each entry of <code>outputs</code> contains the <code>simulated_time</code> (s) and <code>timestamp</code>
of the written maps, the number of <code>walkers_remaining</code> at that time, and
Each entry of <code>outputs</code> contains the <code>simulated_time</code> (s) when the maps
were written, their <code>timestamp</code>, the number of <code>walkers_remaining</code> at that time, and
the names of the <code>depth</code>, <code>discharge</code>, <code>error</code> and <code>walkers</code> maps, or
<code>null</code> for maps which were not requested.

Expand Down
22 changes: 16 additions & 6 deletions raster/r.sim/r.sim.water/r.sim.water.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
21 changes: 21 additions & 0 deletions raster/r.sim/r.sim.water/tests/r_sim_water_nprocs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Loading
Loading