From 37ceb865d77d2d3586041933737d110ebb7f4fb4 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Wed, 30 Sep 2026 14:55:29 -0400 Subject: [PATCH 1/6] r.sim: Write time series maps at their output step times 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 first iteration whose simulated time reaches j times output_step, and name and timestamp the maps by that time. When one iteration reaches several steps, write only the latest one and warn that the time step is longer than output_step. 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 up to one time step after 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. Written with the help of Claude Code. --- raster/r.sim/r.sim.water/r.sim.water.html | 40 ++++++++++++-------- raster/r.sim/r.sim.water/r.sim.water.md | 45 ++++++++++++++--------- raster/r.sim/simlib/hydro.c | 43 +++++++++++++++------- raster/r.sim/simlib/input.c | 8 ++-- raster/r.sim/simlib/output.c | 8 ++-- raster/r.sim/simlib/simlib.h | 7 ++-- 6 files changed, 92 insertions(+), 59 deletions(-) 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 7bf3d256d39..8bdd9d68f23 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.html +++ b/raster/r.sim/r.sim.water/r.sim.water.html @@ -88,7 +88,14 @@

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 maps of an output step are written at the first iteration which reaches its time, +so their simulated time can exceed their name by up to one time step. +When the time step is longer than output_step, one iteration can reach several +output steps and only the latest of them is written. +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 @@ -201,12 +208,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. @@ -214,6 +221,7 @@

Run summary

Summary of a time series run with two output steps in JSON:
+g.region n=224000 s=223000 e=637000 w=636000 res=10
 r.sim.water elevation=elevation depth=depth discharge=discharge rain_value=50 \
     man_value=0.05 nwalkers=100000 duration=20 output_step=10 random_seed=3 \
     -t -p format=json
@@ -222,33 +230,33 @@ 

Run summary

 {
     "walkers_requested": 100000,
-    "walkers_generated": 120000,
-    "walkers_remaining": 112724,
+    "walkers_generated": 110000,
+    "walkers_remaining": 92085,
     "duration": 1200,
-    "simulated_time": 1199.2085202681737,
-    "time_step": 1.0631281208051186,
-    "iterations_planned": 1128,
-    "iterations_completed": 1128,
+    "simulated_time": 1198.8235235863349,
+    "time_step": 1.8908888384642506,
+    "iterations_planned": 634,
+    "iterations_completed": 634,
     "stopped_early": false,
-    "mean_velocity": 9.4062040165270862,
+    "mean_velocity": 5.288518180752412,
     "mean_mannings_n": 0.050000000000000003,
-    "mean_source_rate": 1.390000000000819e-05,
+    "mean_source_rate": 1.3899999999999379e-05,
     "mean_infiltration": 0,
     "threads": 1,
     "outputs": [
         {
-            "simulated_time": 599.60426013408687,
+            "simulated_time": 601.30265063163165,
             "timestamp": "10 minutes",
-            "walkers_remaining": 113464,
+            "walkers_remaining": 92121,
             "depth": "depth.10",
             "discharge": "discharge.10",
             "error": null,
             "walkers": null
         },
         {
-            "simulated_time": 1199.2085202681737,
+            "simulated_time": 1198.8235235863349,
             "timestamp": "20 minutes",
-            "walkers_remaining": 112724,
+            "walkers_remaining": 92085,
             "depth": "depth.20",
             "discharge": "discharge.20",
             "error": null,
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 460e35e34c9..e9a6a055dee 100644
--- a/raster/r.sim/r.sim.water/r.sim.water.md
+++ b/raster/r.sim/r.sim.water/r.sim.water.md
@@ -78,7 +78,15 @@ 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 maps of an
+output step are written at the first iteration which reaches its time,
+so their simulated time can exceed their name by up to one time step.
+When the time step is longer than **output_step**, one iteration can
+reach several output steps and only the latest of them is written. 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
@@ -190,16 +198,17 @@ 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:
 
 ```sh
+g.region n=224000 s=223000 e=637000 w=636000 res=10
 r.sim.water elevation=elevation depth=depth discharge=discharge rain_value=50 \
     man_value=0.05 nwalkers=100000 duration=20 output_step=10 random_seed=3 \
     -t -p format=json
@@ -208,33 +217,33 @@ r.sim.water elevation=elevation depth=depth discharge=discharge rain_value=50 \
 ```json
 {
     "walkers_requested": 100000,
-    "walkers_generated": 120000,
-    "walkers_remaining": 112724,
+    "walkers_generated": 110000,
+    "walkers_remaining": 92085,
     "duration": 1200,
-    "simulated_time": 1199.2085202681737,
-    "time_step": 1.0631281208051186,
-    "iterations_planned": 1128,
-    "iterations_completed": 1128,
+    "simulated_time": 1198.8235235863349,
+    "time_step": 1.8908888384642506,
+    "iterations_planned": 634,
+    "iterations_completed": 634,
     "stopped_early": false,
-    "mean_velocity": 9.4062040165270862,
+    "mean_velocity": 5.288518180752412,
     "mean_mannings_n": 0.050000000000000003,
-    "mean_source_rate": 1.390000000000819e-05,
+    "mean_source_rate": 1.3899999999999379e-05,
     "mean_infiltration": 0,
     "threads": 1,
     "outputs": [
         {
-            "simulated_time": 599.60426013408687,
+            "simulated_time": 601.30265063163165,
             "timestamp": "10 minutes",
-            "walkers_remaining": 113464,
+            "walkers_remaining": 92121,
             "depth": "depth.10",
             "discharge": "discharge.10",
             "error": null,
             "walkers": null
         },
         {
-            "simulated_time": 1199.2085202681737,
+            "simulated_time": 1198.8235235863349,
             "timestamp": "20 minutes",
-            "walkers_remaining": 112724,
+            "walkers_remaining": 92085,
             "depth": "depth.20",
             "discharge": "discharge.20",
             "error": null,
diff --git a/raster/r.sim/simlib/hydro.c b/raster/r.sim/simlib/hydro.c
index 0c03eb2476d..72bf3c953f4 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;
@@ -315,13 +311,26 @@ 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 first iteration reaching
+            // j * output_step and named by that time. When one iteration
+            // reaches several steps, only the latest one is written.
+            bool write_series = false;
+            if (settings->ts && settings->iterout > 0) {
+                double reached =
+                    settings->iterout *
+                    floor(simulated_seconds(setup, i) / settings->iterout);
+                if (reached > series_time) {
+                    series_time = reached;
+                    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++) {
@@ -352,15 +361,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"));
             }
@@ -453,13 +465,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 0ee0f8ecf41..ac69267cd28 100644
--- a/raster/r.sim/simlib/input.c
+++ b/raster/r.sim/simlib/input.c
@@ -333,10 +333,10 @@ 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 && time_step_seconds(setup) > settings->iterout)
+        G_warning(_("Time step of %.2f s is longer than output_step of %d s, "
+                    "some output steps will 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 4251e47cc472f097ed4792dd5c83a15222e5a9ed Mon Sep 17 00:00:00 2001
From: Vaclav Petras 
Date: Wed, 30 Sep 2026 14:58:28 -0400
Subject: [PATCH 2/6] r.sim: Test the time series schedule of r.sim.water

Check on a slope where the output step does not fall on an iteration
that each output step is written once under its name and timestamp,
that walker maps follow the same schedule, and that the series ends with
the maps named by the duration when the duration is not a multiple of
output_step, when the time step is longer than output_step, and when
all walkers left the domain early.

Check the timestamps in the summary of a time series run against the
output steps instead of the rounded simulated time, and update the
description of the series written in the duration test.

Written with the help of Claude Code.
---
 .../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     | 142 ++++++++++++++++++
 3 files changed, 152 insertions(+), 10 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/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..c624e0392d8
--- /dev/null
+++ b/raster/r.sim/r.sim.water/tests/r_sim_water_time_series_test.py
@@ -0,0 +1,142 @@
+# 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.2 s, so a 1-minute output step
+# does not fall on an iteration and the old schedule, which wrote every
+# (int)(60 / 10.2) = 5 iterations and named maps by the rounded simulated
+# time, wrote dep.03 and dep.08 twice and no dep.10.
+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 500 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, epsg=3358)
+    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=500, 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 first iteration reaching 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 (
+            minute * 60 <= output["simulated_time"] < minute * 60 + summary["time_step"]
+        )
+    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])
+
+
+def test_time_step_longer_than_output_step(slope_tools):
+    """An iteration passing several output steps writes the latest one
+
+    A minimum time step of 17.5 s makes the simulated time per iteration
+    70 s (the time step times the time coefficient 4), so the iteration
+    ending at 420 s passes both 6 and 7 minutes. The old schedule wrote
+    every (int)(60 / 70) = 0 iterations, that is, no maps at all.
+    """
+    result = slope_tools.r_sim_water(
+        **SLOPE,
+        depth="dep",
+        output_step=1,
+        mintimestep=17.5,
+        flags="tp",
+        format="json",
+    )
+    summary = result.json
+    assert summary["time_step"] == 70
+    check_series(slope_tools, summary, [1, 2, 3, 4, 5, 7, 8, 9, 10])
+    assert "longer than output_step" in result.stderr
+
+
+def test_final_step_after_early_stop(session_tools):
+    """The series ends with the duration also when all walkers left early
+
+    With the default Manning's n, all walkers leave the small domain from
+    the shared fixture before the first output step.
+    """
+    summary = session_tools.r_sim_water(
+        elevation="rows_raster",
+        depth="depth",
+        duration=2,
+        output_step=1,
+        random_seed=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"]] == ["depth.02"]
+    assert summary["outputs"][0]["timestamp"] == "2 minutes"

From aed8984f6a2a0bd68dab4e7f9c2bbe63e2680efa Mon Sep 17 00:00:00 2001
From: Vaclav Petras 
Date: Thu, 1 Oct 2026 17:14:39 -0400
Subject: [PATCH 3/6] r.sim: Write time series maps at the closest iteration

The series maps of an output step were written at the first iteration
which reached the time of the step, so a map held the state up to one
time step after the time in its name. With a time step close to
output_step, that was almost a whole output step: with a time step of
59 s and output_step of 1 minute, the map named 01 held the state at
118 s.

Write the maps of an output step at the iteration closest to its time
instead, so that a map holds the state at most half a time step before
or after the time in its name; in the example above, at 59 s. An
iteration is the closest one to the steps up to half a time step ahead
of it. That range is limited to the duration, because with a time step
at least twice as long as output_step it would otherwise include steps
past the duration. As before, when one iteration is the closest one to
several steps, only the latest step is written, and the series ends
with the maps named by the duration.

Describe the rule in the manual with the time in the map name instead
of the name, and update the example summary, where the first output is
now written one iteration earlier. Check the new rule in the tests and
add a test with a time step much longer than output_step, which fails
without the limit.

Written with the help of Claude Code.
---
 raster/r.sim/r.sim.water/r.sim.water.html     | 11 ++++---
 raster/r.sim/r.sim.water/r.sim.water.md       | 20 ++++++-----
 .../tests/r_sim_water_time_series_test.py     | 33 ++++++++++++++-----
 raster/r.sim/simlib/hydro.c                   | 15 ++++++---
 4 files changed, 52 insertions(+), 27 deletions(-)

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 8bdd9d68f23..aa19ac1984e 100644
--- a/raster/r.sim/r.sim.water/r.sim.water.html
+++ b/raster/r.sim/r.sim.water/r.sim.water.html
@@ -89,10 +89,11 @@ 

DESCRIPTION

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) and are timestamped with that time. -The maps of an output step are written at the first iteration which reaches its time, -so their simulated time can exceed their name by up to one time step. -When the time step is longer than output_step, one iteration can reach several -output steps and only the latest of them is written. +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, one time step can be the closest one +to several output times and only the map for the latest of them is written. 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. @@ -245,7 +246,7 @@

Run summary

"threads": 1, "outputs": [ { - "simulated_time": 601.30265063163165, + "simulated_time": 599.41176179316744, "timestamp": "10 minutes", "walkers_remaining": 92121, "depth": "depth.10", 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 e9a6a055dee..e50f927ae66 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.md +++ b/raster/r.sim/r.sim.water/r.sim.water.md @@ -78,14 +78,16 @@ 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) and are timestamped with that time. The maps of an -output step are written at the first iteration which reaches its time, -so their simulated time can exceed their name by up to one time step. -When the time step is longer than **output_step**, one iteration can -reach several output steps and only the latest of them is written. 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. +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**, one time step can be the +closest one to several output times and only the map for the latest of +them is written. 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 @@ -232,7 +234,7 @@ r.sim.water elevation=elevation depth=depth discharge=discharge rain_value=50 \ "threads": 1, "outputs": [ { - "simulated_time": 601.30265063163165, + "simulated_time": 599.41176179316744, "timestamp": "10 minutes", "walkers_remaining": 92121, "depth": "depth.10", 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 index c624e0392d8..52afcf00357 100644 --- 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 @@ -54,7 +54,7 @@ def minutes_of_timestamp(tools, name): def check_series(tools, summary, minutes): """Series maps are named and timestamped by the given minutes. - Each map is written at the first iteration reaching its time, except the + 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"] @@ -65,9 +65,7 @@ def check_series(tools, summary, minutes): 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 ( - minute * 60 <= output["simulated_time"] < minute * 60 + summary["time_step"] - ) + 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"] @@ -100,12 +98,13 @@ def test_final_step_named_by_duration(slope_tools): def test_time_step_longer_than_output_step(slope_tools): - """An iteration passing several output steps writes the latest one + """An iteration closest to several output steps writes the latest one A minimum time step of 17.5 s makes the simulated time per iteration 70 s (the time step times the time coefficient 4), so the iteration - ending at 420 s passes both 6 and 7 minutes. The old schedule wrote - every (int)(60 / 70) = 0 iterations, that is, no maps at all. + ending at 210 s is the closest one to both 3 and 4 minutes. The old + schedule wrote every (int)(60 / 70) = 0 iterations, that is, no maps + at all. """ result = slope_tools.r_sim_water( **SLOPE, @@ -117,7 +116,7 @@ def test_time_step_longer_than_output_step(slope_tools): ) summary = result.json assert summary["time_step"] == 70 - check_series(slope_tools, summary, [1, 2, 3, 4, 5, 7, 8, 9, 10]) + check_series(slope_tools, summary, [1, 2, 4, 5, 6, 7, 8, 9, 10]) assert "longer than output_step" in result.stderr @@ -140,3 +139,21 @@ def test_final_step_after_early_stop(session_tools): assert summary["simulated_time"] < 60 assert [output["depth"] for output in summary["outputs"]] == ["depth.02"] assert summary["outputs"][0]["timestamp"] == "2 minutes" + + +def test_no_output_step_past_duration(slope_tools): + """The series ends with the duration also with a very long time step + + With a time step of 290 s, the last iteration ends at 580 s and is the + closest one also to 11 and 12 minutes, which are past the duration. + """ + summary = slope_tools.r_sim_water( + **SLOPE, + depth="dep", + output_step=1, + mintimestep=72.5, + flags="tp", + format="json", + ).json + assert summary["time_step"] == 290 + check_series(slope_tools, summary, [7, 10]) diff --git a/raster/r.sim/simlib/hydro.c b/raster/r.sim/simlib/hydro.c index 72bf3c953f4..03abeab36ed 100644 --- a/raster/r.sim/simlib/hydro.c +++ b/raster/r.sim/simlib/hydro.c @@ -311,14 +311,19 @@ 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 first iteration reaching - // j * output_step and named by that time. When one iteration - // reaches several steps, only the latest one is written. + // Output step j is written at the iteration closest to + // j * output_step and named by that time. When one iteration is + // the closest one to several steps, only the latest one is + // written. bool write_series = false; if (settings->ts && settings->iterout > 0) { + // An iteration is the closest one to the steps up to half a + // time step ahead of it. No step is past the duration. + double ahead = fmin(simulated_seconds(setup, i) + + 0.5 * time_step_seconds(setup), + settings->timesec); double reached = - settings->iterout * - floor(simulated_seconds(setup, i) / settings->iterout); + settings->iterout * floor(ahead / settings->iterout); if (reached > series_time) { series_time = reached; write_series = true; From 0b5d57f080d74d80b6ecafe6ff2b0f91a78042f4 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Thu, 1 Oct 2026 18:04:07 -0400 Subject: [PATCH 4/6] r.sim: Warn about skipped output steps only for a positive output_step The warning about a time step longer than output_step was also printed for output_step of zero or less, as "longer than output_step of 0 s", although no series step is written at all in that case, only the final maps. Print it only for a positive output_step, as the series writes themselves are limited. Say that output steps may be skipped, because in a short run every step can still be written, for example with a time step of 61 s, output_step of 1 minute and duration of 2 minutes. Written with the help of Claude Code. --- raster/r.sim/simlib/input.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/raster/r.sim/simlib/input.c b/raster/r.sim/simlib/input.c index 4dc3bd571cb..ddb6298fbe8 100644 --- a/raster/r.sim/simlib/input.c +++ b/raster/r.sim/simlib/input.c @@ -339,9 +339,10 @@ 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 */ - if (settings->ts && time_step_seconds(setup) > settings->iterout) + 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 will be skipped"), + "some output steps may be skipped"), time_step_seconds(setup), settings->iterout); fprintf(stderr, "\n"); From 248de603b0e4cea556a8b180017cebbad46d955d Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 2 Oct 2026 11:05:30 -0400 Subject: [PATCH 5/6] r.sim.water: Make the time series tests faster Run the time series tests on a slope 5 cells wide instead of 50. The run which writes ten walker vector maps took 0.37 s on the wide slope and takes 0.10 s on the narrow one, because there are ten times fewer walkers to write. The time step is now about 10.7 s, which still does not divide the output step; the old schedule wrote dep.04 twice there. Use a project without a coordinate reference system, which is created without running a tool, and make the walkers leave early on the same slope with a very low Manning's n instead of setting up the shared project for one test. The file now takes 0.9 s instead of 1.4 s. All five tests still fail with the old schedule. Written with the help of Claude Code. --- .../tests/r_sim_water_time_series_test.py | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) 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 index 52afcf00357..85ff807ea08 100644 --- 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 @@ -10,10 +10,10 @@ from grass.experimental.mapset import TemporaryMapsetSession from grass.tools import Tools -# On this slope, the time step is about 10.2 s, so a 1-minute output step +# 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.2) = 5 iterations and named maps by the rounded simulated -# time, wrote dep.03 and dep.08 twice and no dep.10. +# (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, @@ -25,12 +25,12 @@ @pytest.fixture(scope="module") def slope_project(tmp_path_factory): - """Project with a 500 m by 500 m plane at 10 m rising 0.1 m per row""" + """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, epsg=3358) + 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=500, res=10, flags="s") + 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 @@ -120,24 +120,22 @@ def test_time_step_longer_than_output_step(slope_tools): assert "longer than output_step" in result.stderr -def test_final_step_after_early_stop(session_tools): +def test_final_step_after_early_stop(slope_tools): """The series ends with the duration also when all walkers left early - With the default Manning's n, all walkers leave the small domain from - the shared fixture before the first output step. + With a very low Manning's n, all walkers leave the slope before the + first output step. """ - summary = session_tools.r_sim_water( - elevation="rows_raster", - depth="depth", - duration=2, + summary = slope_tools.r_sim_water( + **(SLOPE | {"man_value": 0.002, "duration": 2}), + depth="dep", output_step=1, - random_seed=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"]] == ["depth.02"] + assert [output["depth"] for output in summary["outputs"]] == ["dep.02"] assert summary["outputs"][0]["timestamp"] == "2 minutes" From e54cc218f59aaf37df202d1c2318bd1dd1873cff Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 2 Oct 2026 16:57:26 -0400 Subject: [PATCH 6/6] r.sim: Write the output step closest to the iteration With a time step longer than output_step, an iteration is the closest one to several output steps, and it wrote the latest of them. When the time step was a multiple of output_step, the iterations fell exactly on output steps, but the maps were named by later ones: with a time step of 120 s and output_step of 1 minute, the series was 03, 05, 07, 09, 10 with the state at 120 s in the map named 03. Write the output step closest to the iteration instead. The series in the example is now 02, 04, 06, 08, 10, and with a time step of 180 s it is 03, 06, 09, 10. A step more than half a time step ahead of the iteration is left to the next iteration, so with a time step shorter than output_step the maps are the same as before. A map now holds the state at most half of the shorter of the time step and output_step before or after the time in its name. A step past the duration is not written, which can only happen when the duration is not a multiple of output_step. Test time steps of two and three times output_step, and replace the test of a step past the duration, which the 290 s time step no longer reaches, with a duration of 11 minutes and output_step of 3 minutes. The problem was reported in the review of the pull request. Written with the help of Claude Code. --- raster/r.sim/r.sim.water/r.sim.water.html | 4 +- raster/r.sim/r.sim.water/r.sim.water.md | 6 +-- .../tests/r_sim_water_time_series_test.py | 50 ++++++++++++------- raster/r.sim/simlib/hydro.c | 26 +++++----- 4 files changed, 50 insertions(+), 36 deletions(-) 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 b1ddd41fb4d..7e125f78243 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.html +++ b/raster/r.sim/r.sim.water/r.sim.water.html @@ -94,8 +94,8 @@

DESCRIPTION

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, one time step can be the closest one -to several output times and only the map for the latest of them is written. +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. 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 6feda572c14..ec2fded33af 100644 --- a/raster/r.sim/r.sim.water/r.sim.water.md +++ b/raster/r.sim/r.sim.water/r.sim.water.md @@ -84,9 +84,9 @@ 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**, one time step can be the -closest one to several output times and only the map for the latest of -them is written. The series always ends with maps named by 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. 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 index 85ff807ea08..dbcdd628559 100644 --- 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 @@ -97,26 +97,37 @@ def test_final_step_named_by_duration(slope_tools): check_series(slope_tools, summary, [3, 6, 9, 10]) -def test_time_step_longer_than_output_step(slope_tools): - """An iteration closest to several output steps writes the latest one - - A minimum time step of 17.5 s makes the simulated time per iteration - 70 s (the time step times the time coefficient 4), so the iteration - ending at 210 s is the closest one to both 3 and 4 minutes. The old - schedule wrote every (int)(60 / 70) = 0 iterations, that is, no maps - at all. +@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=17.5, + mintimestep=mintimestep, flags="tp", format="json", ) summary = result.json - assert summary["time_step"] == 70 - check_series(slope_tools, summary, [1, 2, 4, 5, 6, 7, 8, 9, 10]) + assert summary["time_step"] == time_step + check_series(slope_tools, summary, minutes) assert "longer than output_step" in result.stderr @@ -140,18 +151,19 @@ def test_final_step_after_early_stop(slope_tools): def test_no_output_step_past_duration(slope_tools): - """The series ends with the duration also with a very long time step + """An output step past the duration is not written - With a time step of 290 s, the last iteration ends at 580 s and is the - closest one also to 11 and 12 minutes, which are past the duration. + 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, + **(SLOPE | {"duration": 11}), depth="dep", - output_step=1, - mintimestep=72.5, + output_step=3, + mintimestep=40, flags="tp", format="json", ).json - assert summary["time_step"] == 290 - check_series(slope_tools, summary, [7, 10]) + 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 03abeab36ed..cfecbbfa1ec 100644 --- a/raster/r.sim/simlib/hydro.c +++ b/raster/r.sim/simlib/hydro.c @@ -312,20 +312,22 @@ void main_loop(const Setup *setup, const Geometry *geometry, sim->nwalka = nwalka; // Output step j is written at the iteration closest to - // j * output_step and named by that time. When one iteration is - // the closest one to several steps, only the latest one is - // written. + // 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) { - // An iteration is the closest one to the steps up to half a - // time step ahead of it. No step is past the duration. - double ahead = fmin(simulated_seconds(setup, i) + - 0.5 * time_step_seconds(setup), - settings->timesec); - double reached = - settings->iterout * floor(ahead / settings->iterout); - if (reached > series_time) { - series_time = reached; + 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; } }