From d700167b0debbca9e78b9c3ed56f94484b00962e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 07:38:47 -0300 Subject: [PATCH] fix: Daisyworld and MonoLake as in TerraME; every model bit-identical to TerraME runs Compared with TerraME 2.0.1 runs of the sysdyn package (terrame-goldens v0.1.0): - Daisyworld: the planet albedo for the black daisies is computed again after the white area is updated, as in the Lua model. New attributes temp_near_white and temp_near_black. - MonoLake: lake surface and elevation through TerraME's sci.Spline (a Catmull-Rom curve sampled at 1000 points per segment, read linearly; new models._spline.Spline) instead of scipy's CubicSpline, an undeclared dependency. As in TerraME, a volume outside the curve raises an error. - PredatorPrey, RoomTemperature, SIR and MonoLake: arithmetic in TerraME's order. All 16 deterministic cases now give the same doubles as TerraME at every step. tests/test_goldens.py (marker golden) checks it bit for bit from a copy of the goldens in tests/goldens (SHA-256 provenance and the script that copies them), and RandomWalk by distribution. The strict xfails of Daisyworld and MonoLake are removed. The Mono Lake notebook uses the new Spline. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WHohDPhCtXJvVZ7jTGwK8e --- CHANGELOG.md | 21 ++++ README.md | 10 ++ examples/notebooks/sysdyn_mono_lake.ipynb | 20 ++-- pyproject.toml | 3 + src/dissmodel_sysdyn/models/_spline.py | 87 ++++++++++++++++ src/dissmodel_sysdyn/models/daisyworld.py | 23 +++-- src/dissmodel_sysdyn/models/mono_lake.py | 31 ++++-- src/dissmodel_sysdyn/models/predatorprey.py | 10 +- .../models/room_temperature.py | 2 +- src/dissmodel_sysdyn/models/sir.py | 13 +-- tests/goldens/README.md | 26 +++++ tests/goldens/cases.csv | 18 ++++ tests/goldens/chaotic_growth.csv.gz | Bin 0 -> 3608 bytes tests/goldens/coffee.csv.gz | Bin 0 -> 242 bytes tests/goldens/daisyworld.csv.gz | Bin 0 -> 5299 bytes tests/goldens/homeostasis.csv.gz | Bin 0 -> 301 bytes tests/goldens/homeostasis_100.csv.gz | Bin 0 -> 315 bytes tests/goldens/limited_growth.csv.gz | Bin 0 -> 1183 bytes tests/goldens/lorenz.csv.gz | Bin 0 -> 6336 bytes tests/goldens/make_reference.py | 60 +++++++++++ tests/goldens/mono_lake.csv.gz | Bin 0 -> 1101 bytes tests/goldens/mono_lake_export_50.csv.gz | Bin 0 -> 1041 bytes tests/goldens/population_growth.csv.gz | Bin 0 -> 101 bytes tests/goldens/predator_prey.csv.gz | Bin 0 -> 10997 bytes tests/goldens/random_walk.csv.gz | Bin 0 -> 14311 bytes tests/goldens/reference.json | 37 +++++++ tests/goldens/room_temperature.csv.gz | Bin 0 -> 361 bytes tests/goldens/sir.csv.gz | Bin 0 -> 1008 bytes tests/goldens/tub.csv.gz | Bin 0 -> 69 bytes tests/goldens/yeast.csv.gz | Bin 0 -> 138 bytes tests/goldens/yeast_rate_2.csv.gz | Bin 0 -> 110 bytes tests/test_goldens.py | 96 ++++++++++++++++++ tests/test_spline.py | 46 +++++++++ tests/test_terrame_parity.py | 21 +--- 34 files changed, 467 insertions(+), 57 deletions(-) create mode 100644 src/dissmodel_sysdyn/models/_spline.py create mode 100644 tests/goldens/README.md create mode 100644 tests/goldens/cases.csv create mode 100644 tests/goldens/chaotic_growth.csv.gz create mode 100644 tests/goldens/coffee.csv.gz create mode 100644 tests/goldens/daisyworld.csv.gz create mode 100644 tests/goldens/homeostasis.csv.gz create mode 100644 tests/goldens/homeostasis_100.csv.gz create mode 100644 tests/goldens/limited_growth.csv.gz create mode 100644 tests/goldens/lorenz.csv.gz create mode 100644 tests/goldens/make_reference.py create mode 100644 tests/goldens/mono_lake.csv.gz create mode 100644 tests/goldens/mono_lake_export_50.csv.gz create mode 100644 tests/goldens/population_growth.csv.gz create mode 100644 tests/goldens/predator_prey.csv.gz create mode 100644 tests/goldens/random_walk.csv.gz create mode 100644 tests/goldens/reference.json create mode 100644 tests/goldens/room_temperature.csv.gz create mode 100644 tests/goldens/sir.csv.gz create mode 100644 tests/goldens/tub.csv.gz create mode 100644 tests/goldens/yeast.csv.gz create mode 100644 tests/goldens/yeast_rate_2.csv.gz create mode 100644 tests/test_goldens.py create mode 100644 tests/test_spline.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bad9034..37d5799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ ## [Unreleased] +- **Fix:** `Daisyworld` now follows TerraME: after updating the white area, + it computes the planet albedo again for the black daisies. The trajectory + changes (up to 0.61 in area); the equilibrium is the same. New attributes + `temp_near_white` and `temp_near_black`, as in TerraME. +- **Fix:** `MonoLake` reads the lake surface and elevation with TerraME's + `sci.Spline` (a Catmull-Rom curve sampled at 1000 points per segment and + read linearly; new `dissmodel_sysdyn.models._spline.Spline`) instead of + scipy's `CubicSpline`, which differed by up to 26 KAF in 50 steps. As in + TerraME, a volume outside the curve raises an error instead of + extrapolating. scipy, which was used without being a declared + dependency, is no longer needed. +- `PredatorPrey`, `RoomTemperature`, `SIR` and `MonoLake` do their + arithmetic in TerraME's order, so that the results are the same doubles. +- `tests/test_goldens.py` (marker `golden`): every model against TerraME + 2.0.1 runs from `terrame-goldens` v0.1.0. All 16 deterministic cases + (13 models, some with two parameter sets) give **the same doubles as + TerraME at every step**; RandomWalk is compared by distribution. + `tests/goldens/` holds the goldens, their SHA-256 provenance and the + script that copies them. The strict `xfail`s of `Daisyworld` and + `MonoLake` in `test_terrame_parity.py` are removed. + - **Fix:** `Tub` kept its counter in `_step`, the attribute where dissmodel's `Model` stores the interval between ticks, so the interval grew at every execution (a run from t=1 to 30 executed 8 times). The counter is now diff --git a/README.md b/README.md index e802c52..f66aad3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ The library includes a wide range of System Dynamics models: * **Environment**: Mono Lake Water Balance. * **Stochasticity**: Random Walk. +## Verification against TerraME + +Every model was run in TerraME 2.0.1 with the `sysdyn` package +([`terrame-goldens`](https://github.com/LambdaGeo/terrame-goldens) v0.1.0). +For the 16 deterministic cases (13 models, some with two parameter sets), +the Python models give **the same doubles as TerraME at every step**, bit for +bit, for every variable TerraME records; RandomWalk is compared by +distribution. `tests/test_goldens.py` runs these comparisons +(`pytest -m golden`, under a second). + ## Examples The project includes several examples demonstrating how to run the models via CLI, Streamlit, or Jupyter Notebooks. diff --git a/examples/notebooks/sysdyn_mono_lake.ipynb b/examples/notebooks/sysdyn_mono_lake.ipynb index 3d66b34..9f137fc 100644 --- a/examples/notebooks/sysdyn_mono_lake.ipynb +++ b/examples/notebooks/sysdyn_mono_lake.ipynb @@ -88,17 +88,17 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "from scipy.interpolate import CubicSpline\n", "from dissmodel.core import Model\n", "from dissmodel.visualization import track_plot\n", + "from dissmodel_sysdyn.models._spline import Spline # TerraME's sci.Spline\n", "\n", - "_VOLUME_KAF = np.array([0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000], dtype=float)\n", - "_SURFACE_KM2 = np.array([0, 24.7, 35.3, 48.6, 54.3, 57.2, 61.6, 66.0, 69.9], dtype=float)\n", - "_ELEV_FT = np.array([6224, 6335, 6369, 6392, 6412, 6430, 6447, 6463, 6477], dtype=float)\n", + "_VOLUME_KAF = [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000]\n", + "_SURFACE_KM2 = [0, 24.7, 35.3, 48.6, 54.3, 57.2, 61.6, 66.0, 69.9]\n", + "_ELEV_FT = [6224, 6335, 6369, 6392, 6412, 6430, 6447, 6463, 6477]\n", "\n", - "_water_surface = CubicSpline(_VOLUME_KAF, _SURFACE_KM2, extrapolate=True)\n", - "_water_elevation = CubicSpline(_VOLUME_KAF, _ELEV_FT, extrapolate=True)\n", + "# a Catmull-Rom curve sampled at 1000 points per segment, read linearly\n", + "_water_surface = Spline(_VOLUME_KAF, _SURFACE_KM2, steps=1000)\n", + "_water_elevation = Spline(_VOLUME_KAF, _ELEV_FT, steps=1000)\n", "\n", "@track_plot(\"level\", \"blue\")\n", "@track_plot(\"water_in_lake\", \"cyan\")\n", @@ -109,11 +109,11 @@ " self.evap_rate, self.other_out, self.export = evap_rate, other_out, export\n", "\n", " def execute(self) -> None:\n", - " surface = float(_water_surface(self.water_in_lake))\n", + " surface = _water_surface.value(self.water_in_lake)\n", " total_in = surface * self.prec_rate + self.runoff + self.other_in\n", " total_out = surface * self.evap_rate + self.export + self.other_out\n", - " self.water_in_lake += total_in - total_out\n", - " self.level = float(_water_elevation(self.water_in_lake))" + " self.water_in_lake = self.water_in_lake + total_in - total_out\n", + " self.level = _water_elevation.value(self.water_in_lake)" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 808a232..15a646c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,6 @@ Homepage = "https://github.com/DisSModel/dissmodel-sysdyn" [tool.pytest.ini_options] testpaths = ["tests"] +markers = [ + "golden: compares the models with TerraME runs (tests/goldens)", +] diff --git a/src/dissmodel_sysdyn/models/_spline.py b/src/dissmodel_sysdyn/models/_spline.py new file mode 100644 index 0000000..f292b76 --- /dev/null +++ b/src/dissmodel_sysdyn/models/_spline.py @@ -0,0 +1,87 @@ +""" +The ``Spline`` of TerraME's ``sci`` package (commit 42b0b3e, ``lua/Spline.lua``). + +``sci.Spline`` is not a cubic spline through the points: it samples a +Catmull-Rom curve at ``steps`` points per segment (the first and last points +repeated as end controls) and reads it by linear interpolation between the +samples. :class:`Spline` reproduces both steps, with the same floating-point +operations, so that a model that uses it matches TerraME to the last bit. + +One consequence of following Lua: the loop over ``t`` adds ``1 / steps`` +to ``t`` and, for some ``steps`` (1000 among them), the sum ends just below +1, so the last sample stops short of the last point (at 7999.5 instead of +8000 in MonoLake) and a larger ``x`` raises an error, in TerraME as here. +""" +from __future__ import annotations + +from bisect import bisect_left +from collections.abc import Sequence + + +class Spline: + """ + TerraME ``sci.Spline``: a sampled Catmull-Rom curve read linearly. + + Parameters + ---------- + xs, ys : sequence of float + Control points, with increasing ``xs``. + steps : int, optional + Samples per segment, by default 10 (as in ``sci``). + + Raises + ------ + ValueError + From :meth:`value`, for an ``x`` outside the range of the points, as + TerraME does. + """ + + def __init__(self, xs: Sequence[float], ys: Sequence[float], steps: int = 10) -> None: + points = list(zip((float(x) for x in xs), (float(y) for y in ys), strict=True)) + self.values = self._build(points, steps) + self.xs = [p[0] for p in self.values] + + @staticmethod + def _build(points: list[tuple[float, float]], steps: int) -> list[tuple[float, float]]: + if len(points) < 3: + return points + count = len(points) - 1 + values: list[tuple[float, float]] = [] + for i in range(1, count + 1): # 1-based, as in Lua + if i == 1: + p0, p1, p2, p3 = points[0], points[0], points[1], points[2] + elif i == count: + p0, p1, p2, p3 = points[-3], points[-2], points[-1], points[-1] + else: + p0, p1, p2, p3 = points[i - 2], points[i - 1], points[i], points[i + 1] + # Lua's ``for t = 0, 1, 1 / steps`` adds the step to t each time + t = 0.0 + dt = 1 / steps + while t <= 1: + x = 0.5 * ((2 * p1[0]) + (p2[0] - p0[0]) * t + + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t * t + + (3 * p1[0] - p0[0] - 3 * p2[0] + p3[0]) * t * t * t) + y = 0.5 * ((2 * p1[1]) + (p2[1] - p0[1]) * t + + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t * t + + (3 * p1[1] - p0[1] - 3 * p2[1] + p3[1]) * t * t * t) + if not values or (values[-1][0] != x and values[-1][1] != y): + values.append((x, y)) + t += dt + return values + + def value(self, x: float) -> float: + """Linear interpolation between the two samples around ``x``.""" + xs = self.xs + if not xs[0] <= x <= xs[-1]: + raise ValueError(f"Spline: {x} is outside the range [{xs[0]}, {xs[-1]}].") + i = bisect_left(xs, x) + if xs[i] == x: + return self.values[i][1] + (x0, y0), (x1, y1) = self.values[i - 1], self.values[i] + w = (x - x0) / (x1 - x0) + return (1 - w) * y0 + w * y1 + + __call__ = value + + +__all__ = ["Spline"] diff --git a/src/dissmodel_sysdyn/models/daisyworld.py b/src/dissmodel_sysdyn/models/daisyworld.py index bc56784..f80f9af 100644 --- a/src/dissmodel_sysdyn/models/daisyworld.py +++ b/src/dissmodel_sysdyn/models/daisyworld.py @@ -65,6 +65,10 @@ class Daisyworld(Model): Notes ----- + The step follows the TerraME ``sysdyn`` model line by line: the white + area is updated first, and the planet albedo used for the black daisies + is computed again with the new white area. + The ``@track_plot`` decorators register ``white_area``, ``black_area``, ``empty_area``, and ``ave_temp`` for automatic live plotting. @@ -89,6 +93,12 @@ class Daisyworld(Model): #: Current average planet temperature (K). ave_temp: float + #: Temperature near the white daisies in the last step (K). + temp_near_white: float + + #: Temperature near the black daisies in the last step (K). + temp_near_black: float + #: Solar luminosity (fractional). sun_luminosity: float @@ -138,15 +148,16 @@ def _planet_albedo(self) -> float: ) def execute(self) -> None: - pa = self._planet_albedo() - self.ave_temp = _planet_temp(self.sun_luminosity, pa) + self.ave_temp = _planet_temp(self.sun_luminosity, self._planet_albedo()) - temp_white = _local_temp(self.ave_temp, pa, self.white_albedo) - white_growth = _daisy_growth_rate(temp_white) * self.empty_area + self.temp_near_white = _local_temp(self.ave_temp, self._planet_albedo(), self.white_albedo) + white_growth = _daisy_growth_rate(self.temp_near_white) * self.empty_area self.white_area += self.white_area * (white_growth - self.decay_rate) - temp_black = _local_temp(self.ave_temp, pa, self.black_albedo) - black_growth = _daisy_growth_rate(temp_black) * self.empty_area + # As in TerraME, the planet albedo is computed again here, with the + # new white area (and the black and empty areas of the start of the step) + self.temp_near_black = _local_temp(self.ave_temp, self._planet_albedo(), self.black_albedo) + black_growth = _daisy_growth_rate(self.temp_near_black) * self.empty_area self.black_area += self.black_area * (black_growth - self.decay_rate) self.empty_area = self.planet_area - (self.white_area + self.black_area) diff --git a/src/dissmodel_sysdyn/models/mono_lake.py b/src/dissmodel_sysdyn/models/mono_lake.py index fddf159..259f86b 100644 --- a/src/dissmodel_sysdyn/models/mono_lake.py +++ b/src/dissmodel_sysdyn/models/mono_lake.py @@ -1,17 +1,17 @@ from __future__ import annotations -from scipy.interpolate import CubicSpline -import numpy as np - from dissmodel.core import Model from dissmodel.visualization import track_plot -_VOLUME_KAF = np.array([0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000], dtype=float) -_SURFACE_KM2 = np.array([0, 24.7, 35.3, 48.6, 54.3, 57.2, 61.6, 66.0, 69.9], dtype=float) -_ELEV_FT = np.array([6224, 6335, 6369, 6392, 6412, 6430, 6447, 6463, 6477], dtype=float) +from ._spline import Spline + +_VOLUME_KAF = [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000] +_SURFACE_KM2 = [0, 24.7, 35.3, 48.6, 54.3, 57.2, 61.6, 66.0, 69.9] +_ELEV_FT = [6224, 6335, 6369, 6392, 6412, 6430, 6447, 6463, 6477] -_water_surface = CubicSpline(_VOLUME_KAF, _SURFACE_KM2, extrapolate=True) -_water_elevation = CubicSpline(_VOLUME_KAF, _ELEV_FT, extrapolate=True) +# As in TerraME: sci.Spline{points = ..., steps = 1000} +_water_surface = Spline(_VOLUME_KAF, _SURFACE_KM2, steps=1000) +_water_elevation = Spline(_VOLUME_KAF, _ELEV_FT, steps=1000) @track_plot("level", "blue") @@ -43,6 +43,14 @@ class MonoLake(Model): export : float, optional Annual water exported in KAF/year, by default 100.0. + Notes + ----- + The lake surface and elevation are read from the volume through + TerraME's ``sci.Spline`` (:class:`~dissmodel_sysdyn.models._spline.Spline`): + a Catmull-Rom curve sampled at 1000 points per segment and read by + linear interpolation, as in the TerraME model. As there, a volume + outside 0 to about 7999.5 KAF raises an error instead of extrapolating. + Examples -------- >>> from dissmodel.core import Environment @@ -96,7 +104,7 @@ def setup( self.export = export def _surface(self) -> float: - return float(_water_surface(self.water_in_lake)) + return _water_surface.value(self.water_in_lake) def _total_input(self) -> float: return self._surface() * self.prec_rate + self.runoff + self.other_in @@ -105,8 +113,9 @@ def _total_output(self) -> float: return self._surface() * self.evap_rate + self.export + self.other_out def execute(self) -> None: - self.water_in_lake += self._total_input() - self._total_output() - self.level = float(_water_elevation(self.water_in_lake)) + # same order of operations as TerraME: (water + input) - output + self.water_in_lake = self.water_in_lake + self._total_input() - self._total_output() + self.level = _water_elevation.value(self.water_in_lake) __all__ = ["MonoLake"] diff --git a/src/dissmodel_sysdyn/models/predatorprey.py b/src/dissmodel_sysdyn/models/predatorprey.py index 54ce963..c2ae607 100644 --- a/src/dissmodel_sysdyn/models/predatorprey.py +++ b/src/dissmodel_sysdyn/models/predatorprey.py @@ -116,12 +116,14 @@ def execute(self) -> None: Updates prey and predator populations according to the discrete Lotka-Volterra equations. """ - self.prey += ( - self.prey_growth * self.prey + # same order of operations as TerraME, so that the results match it + # to the last bit; the predators use the prey already updated + self.prey = ( + self.prey + self.prey_growth * self.prey - self.prey_death_pred * self.prey * self.predator ) - self.predator += ( - -self.pred_death * self.predator + self.predator = ( + self.predator - self.pred_death * self.predator + self.pred_growth_kills * self.prey * self.predator ) diff --git a/src/dissmodel_sysdyn/models/room_temperature.py b/src/dissmodel_sysdyn/models/room_temperature.py index c9ce46d..cb2928a 100644 --- a/src/dissmodel_sysdyn/models/room_temperature.py +++ b/src/dissmodel_sysdyn/models/room_temperature.py @@ -92,7 +92,7 @@ def execute(self) -> None: self.outside = climate(t) inflow = self.thermal_inertia * (self.temp_set - self.inside) outflow = self.loss_to_outside * (self.inside - self.outside) - self.inside += inflow - outflow + self.inside = self.inside + inflow - outflow # TerraME's order of operations __all__ = ["RoomTemperature"] diff --git a/src/dissmodel_sysdyn/models/sir.py b/src/dissmodel_sysdyn/models/sir.py index 94825e2..ce05f42 100644 --- a/src/dissmodel_sysdyn/models/sir.py +++ b/src/dissmodel_sysdyn/models/sir.py @@ -119,15 +119,16 @@ def execute(self) -> None: Computes new infections and recoveries, then updates the susceptible, infected, and recovered compartments. """ - total = self.susceptible + self.infected + self.recovered - alpha = self.contacts * self.probability + # same order of operations as TerraME, so that the results match it + # to the last bit + proportion = self.susceptible / (self.susceptible + self.infected + self.recovered) - new_infected = self.infected * alpha * (self.susceptible / total) + new_infected = self.infected * self.contacts * self.probability * proportion new_recovered = self.infected / self.duration - self.susceptible -= new_infected - self.infected += new_infected - new_recovered - self.recovered += new_recovered + self.susceptible = self.susceptible - new_infected + self.recovered = self.recovered + new_recovered + self.infected = self.infected + new_infected - new_recovered __all__ = ["SIR"] diff --git a/tests/goldens/README.md b/tests/goldens/README.md new file mode 100644 index 0000000..64b6219 --- /dev/null +++ b/tests/goldens/README.md @@ -0,0 +1,26 @@ +# TerraME reference values for the sysdyn models + +The `sysdyn` suite of +[`terrame-goldens`](https://github.com/LambdaGeo/terrame-goldens) v0.1.0: +what TerraME 2.0.1 computed running the models of the +[`sysdyn`](https://github.com/TerraME/sysdyn) package (commit `9e671cb`; `sci` +commit `42b0b3e` for MonoLake's spline). + +- `.csv.gz`: a column `step` and one column per variable, from step 0 + (the state after `init`) to `finalTime`, written with 17 significant digits, + so every double is recovered exactly. A variable that does not exist yet + at step 0 is empty. Lorenz keeps steps 0 to 100 and then every 100th step. +- `random_walk.csv.gz`: `seed, step, value` for seeds 1 to 50. +- `cases.csv`: model, arguments, final time and variables of each case. +- `reference.json`: the source version, the package commits and the SHA-256 + of each source file. + +To copy them again from a copy of `terrame-goldens`: + +```bash +python tests/goldens/make_reference.py /path/to/terrame-goldens +``` + +The values are the output of TerraME and of the `sysdyn` and `sci` packages +(INPE and TerraLAB/UFOP); they are offered here under CC-BY-4.0, as the +goldens they come from. diff --git a/tests/goldens/cases.csv b/tests/goldens/cases.csv new file mode 100644 index 0000000..5f100f5 --- /dev/null +++ b/tests/goldens/cases.csv @@ -0,0 +1,18 @@ +case,model,arguments,seeds,final_time,variables +chaotic_growth,ChaoticGrowth,,,300,pop +coffee,Coffee,,,20,temperature +daisyworld,Daisyworld,,,100,whiteArea;blackArea;emptyArea;aveTemp;tempNearWhite;tempNearBlack +homeostasis,Homeostasis,,,30,stock +homeostasis_100,Homeostasis,stock=100.0,,30,stock +limited_growth,LimitedGrowth,,,100,pop +lorenz,Lorenz,,,10000,x;y;z +mono_lake,MonoLake,,,50,waterInLake;level +mono_lake_export_50,MonoLake,export=50,,50,waterInLake;level +population_growth,PopulationGrowth,,,7,population;growth +predator_prey,PredatorPrey,,,500,prey;predator +room_temperature,RoomTemperature,,,24,inside;outside +sir,SIR,,,30,susceptible;infected;recovered;contacts +tub,Tub,,,8,water +yeast,Yeast,,,9,cells +yeast_rate_2,Yeast,rate=2.0,,9,cells +random_walk,RandomWalk,,50,100,value diff --git a/tests/goldens/chaotic_growth.csv.gz b/tests/goldens/chaotic_growth.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..2f4fb3b15780c72e27163a33eaa68e31117346da GIT binary patch literal 3608 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 literal 0 HcmV?d00001 diff --git a/tests/goldens/coffee.csv.gz b/tests/goldens/coffee.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ebf3080c5e564018f8e2f8a4a38aeb09b0645394 GIT binary patch literal 242 zcmVF3o&Mq4D)Q!> zYQ=}y{M*DM^=j>s1MT86=`I@J5Z0JRVi1-F>lpDG0$9 zs^gP0SHI15!tQD*6<#*mWpZf{j1lmxvB>Jd&X5FU)mBplR8<^Hiy$nLkP)4_I?U20 sn~h#t)xG;zEgiCEl^sGLDX5A42SQeCbG?`C7;ueQzjq{$n6&`_0Evfk!2kdN literal 0 HcmV?d00001 diff --git a/tests/goldens/daisyworld.csv.gz b/tests/goldens/daisyworld.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..f9a7383d085b12dbc46725f0ef2e39408d8660a3 GIT binary patch literal 5299 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 F001~9YV-gA literal 0 HcmV?d00001 diff --git a/tests/goldens/homeostasis.csv.gz b/tests/goldens/homeostasis.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..00f5b5d613eb48f66b05d7fad58a6e6c770ae5cc GIT binary patch literal 301 zcmV+|0n+{-iwFP!00002|5edRZiGM#1<<|Ef?A%3^F&$oB9Xf9uY3wf0s>TeGQaHN z_5S?cUhl7uC-KH4o04w!P`+JC1{|5&MZWpN;#O-Ez!cHj=LxLL(*KnO?8NF%ofua3OHn8_xc4nl z8%ryFxPq&&Ws>C5jvn`5Pc3PZS~?PjQCX`}(QhB>qnJ}AGYUF~<^;wU-3x-04@C=@ zAG@KzN#)dGlTgo^#&6O?F^6D6Z${gsc4~5nd3Spysh?VD+&qRia^uvFYVQavkejC> zvv|Y;4{2SBObu@dM{Zx5qZ8Uairl#rRXDLSizee=#i^kWX9@ZNhaAMjfC2yjuyTtE literal 0 HcmV?d00001 diff --git a/tests/goldens/homeostasis_100.csv.gz b/tests/goldens/homeostasis_100.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..968bd67ea052734f77cae372657bd55ae8749646 GIT binary patch literal 315 zcmV-B0mS|viwFP!00002|5edDZp1JQ1kj#m5e#zp)p>$cDH34!{>a!~ARBdh5@tmE z@%;Q*kLUNtm*_H8u~iT(-v{)4x%fgJiT@bO`fz(!lU#y%vwh4Urrc#mV5)6jYs8G|5tR+~n_CQ2X!T~h-9;hhR6}T=^_I*G ziZV0UowLoZ%siEu)htgB)-khAMU84kaO82L_Ni7n>ZoYM-fWsvbytk-obeRf7iAA? zI-RGif?moB_tdn};zMDVvLe(t6|1TQxYUpuS~iu+U*#8Sa}(YifE|LkHBv1KX!Fg0 zycMlF-Ckg>pl-E-CP%QY^KRd&yTMO2$wunDG{=2*@^Ni~{Zfvg?yUvhb$u!990l;k N$S=fR5$SOP001$BpL+lR literal 0 HcmV?d00001 diff --git a/tests/goldens/limited_growth.csv.gz b/tests/goldens/limited_growth.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..c2d721b561c1244148fefa7db69c81ae0ea33e8e GIT binary patch literal 1183 zcmV;Q1Yr9giwFP!00002|236M4rMnEL+4)0g`xjvK|SsQlKmscFGvki6+K^Dq)6(o zzyJ5=`}6-lM?M!h`nl^B>)8MP^s&z?uc^pZ)pqmO@h`;gWUYyY&8Y8*FCwl>OvX?& zQ#<*^?B!VW%!pdFpZYT7s_?N}Wr}cTf4Mv~94Vbi>zp$mKl6F|j7_^{qeI%&TZ_6B$&&2(j3{5c+-#w+ZNE zSLLqgvG{Cu?h4O&ta`s{ey+Kf?y?iWOvaAY=izf1Y`RK7aL2L!yuEKQZGpq_Evkbw zcrEFud2iv9&h`s_z97gA8Ec1LmoJ*LW~&PG(U5&yzf`X~FbR^XfxF}O@p#{sLS>u; zVITJ=nxWrO^ocy4yga`0r8Ch7I-oANH@`xe*UCGr!H_k_>ua~ZP6Lr;blp_@>(RJS z9?DSAa!{RboN5Ow_QVQeJ-#2RVo+)80thWB3&HLvP7rD3<}#*yIWO z-v0|GOIVPm&5?~Me)nMY~{M+;-!05+twMhoQL)uA@B?G_cu>{?cLEH;6G|=}cBncl(z`Y== z?7IEBCbKfJWdf}rOe+zIIoG~~J@q+x3$k)(V-Z1v{e zN(_3uo6kIKtz>sSK}m$-zj@gDZ5bpMB#gAOP9C-{ln&x1_^@4_dDwdCmnb7)5i2P& z4;u!*0Q7=JGM#%LHhliVg_t7rhOy1V2G^`a237dGg|iRa^E+B*=5HqjuRUzjd%;TP zVz{>_=V5~owjyg$kdbGceb|WT>qJ);0!;C<4;zVngT3Skhwk(2!?tfTqQ1ATVYAOZ zZ0iD(ZEwW%t!Lv`MP`r6eoN1|;bGgi!}|dzvx;4_4;$lT13`ll?s(vA|Egd^Lg)r} xlHL2TEqhehEhtC21Qh$Ll0@u9mQr9D)~Niz;Q;K;Oqkts%RdpMLxq$G006DyT9*I- literal 0 HcmV?d00001 diff --git a/tests/goldens/lorenz.csv.gz b/tests/goldens/lorenz.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..5a5d5fc98e7bc8b59a25724cc949b29ea072e957 GIT binary patch literal 6336 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.csv.gz``: a column ``step`` and one per +variable, values with 17 significant digits), except Lorenz (10000 steps), +which keeps steps 0 to 100 and then every 100th step. ``reference.json`` +records the source version, the package commits and the SHA-256 of each +source file. + + python tests/goldens/make_reference.py /path/to/terrame-goldens +""" +from __future__ import annotations + +import gzip +import hashlib +import io +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent + + +def write_gz(text: str, dest: Path) -> None: + buf = io.BytesIO() + with gzip.GzipFile(filename="", mode="wb", fileobj=buf, mtime=0, compresslevel=9) as gz: + gz.write(text.encode()) + dest.write_bytes(buf.getvalue()) + + +def main(root: Path) -> None: + src = root / "goldens" / "sysdyn" + sources = {} + for path in sorted(src.glob("*.csv.gz")): + sources[path.relative_to(root).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest() + with gzip.open(path, "rt", newline="") as f: + lines = f.read().splitlines() + if path.name == "lorenz.csv.gz": + lines = [lines[0]] + [ + ln for ln in lines[1:] + if (s := int(ln.split(",", 1)[0])) <= 100 or s % 100 == 0 + ] + write_gz("\n".join(lines) + "\n", HERE / path.name) + (HERE / "cases.csv").write_bytes((src / "cases.csv").read_bytes()) + manifest = json.loads((root / "manifest.json").read_text()) + meta = { + "source": "https://github.com/LambdaGeo/terrame-goldens", + "version": manifest["version"], + "terrame": manifest["terrame"], + "packages": [p for p in manifest["packages"] if p["name"] in ("sysdyn", "sci")], + "files": sources, + "lorenz_steps": "0 to 100, then every 100th step", + } + (HERE / "reference.json").write_text(json.dumps(meta, indent=2) + "\n") + print(f"{len(sources)} goldens copied") + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/tests/goldens/mono_lake.csv.gz b/tests/goldens/mono_lake.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ed6a7ebbeb63892b56a661eae9c6820c7ca0740f GIT binary patch literal 1101 zcmV-T1hV@diwFP!00002|0R`44r4bAME5$&hrm%Hj}Rc2=!I8l1VPeBuAj&6g$-CP zajGinpI@K9p8tOQ`uzRdpTGb32t40)miyyUk-^xWqCcN2UnY7lfTr7sOy09v4u)KGt#W73$M&u8Oc5PomfEAIdy}%V zC(k+Y1%jwjk~0#34v;)2v$|m*)!tro+v!ZF`=Feq0nln!R~7`^dvvsLWGAYq=nGDJ zDXe7YSMuTxY#(C;!B@j~dgNQ|gmU$Fw5P{Hu2 z!|pN|@$`KsmYLL*Gj`-ikKBo>%$bNVf+yi=Y!}50^aY|g%X9h^-?-Rd#6@?uU?)R7 z_Ki3kqd2lE-1;&Yv3oX^7-~mp_MM<0Y=AvlmjObFk%u!zPnW24W*7_tMyUMeF9{ed2};PN0LO11>uUwdO5_Pi~Wuk=R=MUgArSYTBcK^&gBrJ zcG!mPPA#1J+l+4*n$eP{?_FG1j5$hSz2FSdw?w>WFDDHBIGN#vk!UW9sT zL~?{YUokpYZ(Gnf#dj=+7Y4W4b1+kUx7{yLU>2JwX1r$wykIXxk84MaDCjNm%y1wt zPze)T0WVl_GcU870;1p4@Iqt4Lqbsj(4H<_&k|?QsEqU@36E`LaDuTG&RyY8-{Q3( zR^QoUtdjxy_M!|IVHUM8FhkfZ5eCb`8n)jOjrQ1bbJ={JpL{w;cmfs8RrcGc+jY+q z6AdR2+bhmPu$@2{Anz2bZU}VQ-_^oCB`+2~UvXAF??a$XjmmS}+NPWBEzZFa4WYG*^EZ++LZom?qT!;8bcuujO2^u9pe TQu>Ae@#y*=zCAd;4+sDNqEjDo literal 0 HcmV?d00001 diff --git a/tests/goldens/mono_lake_export_50.csv.gz b/tests/goldens/mono_lake_export_50.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..0e3a23aad0e138bd0dae1951b8af0abb5f48bee7 GIT binary patch literal 1041 zcmV+s1n&DEiwFP!00002|2>sSZUi?HMfW_5Km=pv5e)beUTD=u!>|qa_4|b~%Yq=4 zQa?$?pZM3`-+z4l@3+6d|NP~@zy9{0@2}s#|M~uX$(O43S6an7^Vzr9iQ}f_>N@}N zeZwT%``);^ba1lO!lExfkM)D==!_#rPsCjY?&MCfD)UYijR#lR$JOLomn|Qh?W5PS zr1gsUg9B!L+C0p11vt68CFJ&%liu&Xaa)gi9cIy$gJWNgAF095YcK9FL~@u4ja7rQ z9WCxAKfm62`5NrIcOZ=E3QO8ELyG zFFeZtV=F6}xqDQ6fOa%J1OaZXa|Wn8qu4rlyW|COhO&hJWa)KufJk!CqCEUY)(aF^ zI@RtXwho${o-^uYE!a&x50E)fQ}1QxUdIO*J5AYCw5>D;X!~~8oR9HL*NFkz_AX|O z5)AO-%r#j*4Ib9X!JX~qme5pY4?DKIRS>B!1aU-UId!)q{tbumzVC`{ym+$?V);5f zz(T*^OAO;gKfq$8D+Z6evCarn!BcH4C&*VHpdr>h2QIRfjxa+-q(gRShrB~4R%M{T zv;}a4Dcvrn_c)%)e}Qpw24(IOOGlWJ&4i3to|*OzlNd=9UF+IeI>O}LMp%Yyt5kh} zQK~5scon{Ngh>=cf)u<~`qD@mS&t{md`m}{OlZUaI1~%qcWA#T7syTKb!5pPd>k3$ zF>$=>Iirv#5fa!-N0zkN_qLNQ!I>`*^OH0TN`RM+EQx86gDFDkk{8EPJ=36*Ege-7 zL!h5(okrLfXeH&u9J11ujVdXd3ZRmLT)EsA=y_9%nop zmDpn0s1m*B*o8x!)_HK+m8TFvF~G7BC2A}4QW#Sm{{j&ib0;MTIX0q1q8=qSWt0r? z0x7@xS?+c%8&x8wEF-l2THI_|B9zRzbw_I3@24)RGgjOn!TS3@v&7%h&s9Lf E0q24#m;e9( literal 0 HcmV?d00001 diff --git a/tests/goldens/predator_prey.csv.gz b/tests/goldens/predator_prey.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..fe25cc16145523650bfb52098d5ba72c1c047418 GIT binary patch literal 10997 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 literal 0 HcmV?d00001 diff --git a/tests/goldens/random_walk.csv.gz b/tests/goldens/random_walk.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..10f99313f1907bfd12d91a7858146247bf73e26b GIT binary patch literal 14311 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 literal 0 HcmV?d00001 diff --git a/tests/goldens/sir.csv.gz b/tests/goldens/sir.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..aa25677f3163992a070cb727d982bbb2866be98c GIT binary patch literal 1008 zcmVKK}Z6fBpRY_Uj+7_3iubulLW_Z|l$3`;Wh0fAZh^kMEzqynlYYoz>m7 zbe)xNG9R(eInn?AeA(uY^?$!y!d>NVHFw>+rGfamT#Y)X4nUqLTRQ0cAG*LA*n9U; z>xzt{_ws1$zU5-=e!cJECg7WtIDG5VO};tcuq`5P0L{{Sne0Atqsv?CTJoOnuB>y7 zOMWX1Z~#8By-f1T6)}&Mw&S*VgeKT9U3PZcov$O%3!Aocb&2dFQEqiacz=oJ*2wA< zGrOv~Quo!XG@jvFcBbvSIyJT)6w>e#&sge39lxhddKy*Ui(L`W7gIPo@4eM(P0o+% zrLGE!E=cUUo_Xg@5BH^UwyB*|_#nR~;9eDNnyATIl6-4IZR~C5`3URYNKSrS`lE4KPWz8b=FYAt-^$pk_RT!;-HO4kx=+K$+#Hh z1v>7&9bs|I5xdYabmQG|xS+{H+lTgc_#Dc(2Dzkhz1xtd=*7>)!Azou;`rG@rp{A3 z7E`em_sn5i88M=fyh&6LYIUUqQQu7yb7WZF1I|Tb4A$jYy1i&g<_R8wC+;YWud=ZI zZu~f$1m1mBzDpZt&-BM_3F%l&*p|x(BohXSS7+QPJbB}U z>?H4?3|AQ$ybcm^2pC`YQe?oR4}B-5k`!TZvYEn>TP9WGp3!AT4;K_@VN*m-X+4BD zCZuPCpaVt%fp#)KXs{-12aLc&S0X3JEIk-{`pw24wH!1#A>PUoBWGS?wZ?-!7_Aen z8q?XBDzgGKC5<9I-_X^FML7|uFkGtBa#7dDpXw29tqQT038k|}oCd206-a@KV` zr}aVKFSHcXKqwA-cF>Fz=Sa&!L%gYtv}C}F_>q?fTo0HsccQ4pxc53c2sp6V~=Y61rJ)s2_}&p09@I|>!-~N2D`+LpCHqOw=TEVB% XG<>yZIVbv=PCvtcD*H>F8Bj9-r(YZ% literal 0 HcmV?d00001 diff --git a/tests/goldens/yeast.csv.gz b/tests/goldens/yeast.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..a9c725a7589ee7bdddaf681acbc0ef2dea93f6c6 GIT binary patch literal 138 zcmV;50CoQ#iwFP!00002|5c4U3IkCL1ACvv82N9XCv3MxU~_-+NEa1qMvR`%_2$=g z-w%OTa{p4KY!IL`W2Q|9QDht{p5Cfjs*S>s^MpX-jMor>)}>&rkWSx-axYCJB|B6w sUZrc*)@Uk@F>x#8p*D$KYAzc=t#uz^p0A literal 0 HcmV?d00001 diff --git a/tests/goldens/yeast_rate_2.csv.gz b/tests/goldens/yeast_rate_2.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..b018e74a3e9f475a7c5478da4f58fbb70188302f GIT binary patch literal 110 zcmV-!0FnP6iwFP!00002|5eH{4#Q9g1JIqbpa}fIzu`A4pgpV%E=%!TI}__!Vi*#dzmhhrwh9CwsbmVb Qwe9U}zh6E`qmcjr0G-$|oB#j- literal 0 HcmV?d00001 diff --git a/tests/test_goldens.py b/tests/test_goldens.py new file mode 100644 index 0000000..376950c --- /dev/null +++ b/tests/test_goldens.py @@ -0,0 +1,96 @@ +""" +The models against TerraME itself. + +``goldens/`` holds the ``sysdyn`` suite of ``terrame-goldens`` (see +``goldens/README.md``): what TerraME 2.0.1 computed running the models of the +``sysdyn`` package, written with 17 significant digits so that every double +is recovered exactly. Each model runs with the same parameters and must give +**the same doubles** as TerraME at every step (bit for bit), for every +variable TerraME records. + +RandomWalk draws random numbers, so it is compared by distribution with the +50 TerraME runs. +""" +from __future__ import annotations + +import csv +import gzip +import re +from pathlib import Path + +import numpy as np +import pytest +from dissmodel.core import Environment, Model + +import dissmodel_sysdyn.models as sd + +pytestmark = pytest.mark.golden + +GOLDENS = Path(__file__).resolve().parent / "goldens" + +with open(GOLDENS / "cases.csv", newline="") as _f: + CASES = [c for c in csv.DictReader(_f) if not c["seeds"]] + + +def snake(name: str) -> str: + """TerraME's camelCase attribute name in DisSModel's snake_case.""" + return re.sub(r"(? dict[str, float]: + return {snake(k): float(v) for k, v in (a.split("=") for a in text.split(";") if a)} + + +def load(case: str) -> tuple[list[str], dict[int, list[float]]]: + with gzip.open(GOLDENS / f"{case}.csv.gz", "rt", newline="") as f: + rows = list(csv.reader(f)) + nan = float("nan") + return rows[0][1:], {int(r[0]): [float(v) if v else nan for v in r[1:]] for r in rows[1:]} + + +class _Recorder(Model): + """Reads the model after each step (created after the model, so it runs after it).""" + + def setup(self, model: Model, attrs: list[str]) -> None: # type: ignore[override] + self.model, self.attrs, self.rows = model, attrs, {} + self.rows[0] = self.read() + + def read(self) -> list[float]: + return [float(getattr(self.model, a, float("nan"))) for a in self.attrs] + + def execute(self) -> None: + self.rows[int(self.env.now())] = self.read() + + +@pytest.mark.parametrize("case", CASES, ids=[c["case"] for c in CASES]) +def test_trajectory_is_bit_identical_to_terrame(case: dict[str, str]) -> None: + variables, ref = load(case["case"]) + attrs = [snake(v) for v in variables] + env = Environment(start_time=1, end_time=int(case["final_time"])) + model = getattr(sd, case["model"])(**parse_arguments(case["arguments"])) + rec = _Recorder(model=model, attrs=attrs) + env.run() + for t, expected in ref.items(): + got = rec.rows[t] + for var, e, g in zip(variables, expected, got, strict=True): + if np.isnan(e): # not defined yet in TerraME (Daisyworld at step 0) + continue + assert g == e, f"{case['case']}.{var} at step {t}: {g!r} != TerraME {e!r}" + + +def test_random_walk_matches_terrame_distribution() -> None: + with gzip.open(GOLDENS / "random_walk.csv.gz", "rt", newline="") as f: + rows = list(csv.DictReader(f)) + final_tm = np.array([int(r["value"]) for r in rows if r["step"] == "100"]) + ours = [] + for seed in range(1, 201): + env = Environment(start_time=1, end_time=100) + model = sd.RandomWalk(seed=seed) + env.run() + ours.append(model.value) + ours_a = np.array(ours) + # a walk of 100 steps of +-1: mean 0, standard deviation 10 + se = np.sqrt(final_tm.var(ddof=1) / len(final_tm) + ours_a.var(ddof=1) / len(ours_a)) + assert abs(ours_a.mean() - final_tm.mean()) < 4 * se + assert 7 < ours_a.std() < 13 and 7 < final_tm.std() < 13 + assert set(np.unique(ours_a % 2)) == {0} # even after an even number of steps diff --git a/tests/test_spline.py b/tests/test_spline.py new file mode 100644 index 0000000..82bda16 --- /dev/null +++ b/tests/test_spline.py @@ -0,0 +1,46 @@ +"""The ``sci.Spline`` of TerraME used by MonoLake.""" +from __future__ import annotations + +import pytest + +from dissmodel_sysdyn.models._spline import Spline + +XS = [0, 1000, 2000, 3000] +YS = [0, 24.7, 35.3, 48.6] + + +def test_passes_through_the_points(): + s = Spline(XS, YS, steps=1000) + for x, y in zip(XS[:-1], YS[:-1], strict=True): + assert s.value(x) == pytest.approx(y, abs=1e-12) + + +def test_last_sample_falls_short_of_the_last_point_as_in_lua(): + # Lua's ``for t = 0, 1, 1 / steps`` adds 1/1000 a thousand times and ends + # just below 1, so the curve stops before the last point; TerraME then + # refuses x beyond the last sample + s = Spline(XS, YS, steps=1000) + assert 2999 < s.xs[-1] < 3000 + with pytest.raises(ValueError): + s.value(3000) + assert Spline(XS, YS, steps=10).xs[-1] == 3000 + + +def test_linear_between_samples(): + s = Spline(XS, YS, steps=10) + (x0, y0), (x1, y1) = s.values[3], s.values[4] + mid = (x0 + x1) / 2 + assert s.value(mid) == pytest.approx((y0 + y1) / 2) + + +def test_outside_the_range_raises_as_in_terrame(): + s = Spline(XS, YS) + with pytest.raises(ValueError): + s.value(-1) + with pytest.raises(ValueError): + s.value(3000.5) + + +def test_two_points_are_kept_as_given(): + s = Spline([0, 10], [1, 3]) + assert s.value(5) == 2 diff --git a/tests/test_terrame_parity.py b/tests/test_terrame_parity.py index eeff55d..fd6be5e 100644 --- a/tests/test_terrame_parity.py +++ b/tests/test_terrame_parity.py @@ -15,16 +15,7 @@ ``maximum`` event and the PredatorPrey ``period`` option (not ported, and inactive with the default values). -Known differences, kept as strict ``xfail`` so that a fix is noticed: - -* ``MonoLake``: TerraME's ``sci.Spline`` is a Catmull-Rom curve sampled at - ``steps`` points per segment and read by linear interpolation; the Python - model uses ``scipy.interpolate.CubicSpline``. Up to 26 KAF of difference in - 50 steps. -* ``Daisyworld``: the Lua model recomputes the planet albedo for the black - daisies after updating the white area; the Python model uses the albedo - from the start of the step. The trajectories differ (up to 0.61 in area) - and reach the same equilibrium. +``test_goldens.py`` compares the models with the output of TerraME itself. """ from __future__ import annotations @@ -241,11 +232,6 @@ def ref_mono_lake(final_time=50): # --------------------------------------------------------------------------- -KNOWN_DIFFERENCE = { - "MonoLake": "TerraME uses a sampled Catmull-Rom spline; the model uses scipy CubicSpline", - "Daisyworld": "TerraME recomputes the planet albedo after updating the white area", -} - CASES = [ # name, class, finalTime, attributes, reference, parameters ("Coffee", sd.Coffee, 20, ["temperature"], ref_coffee, {}), @@ -266,10 +252,7 @@ def ref_mono_lake(final_time=50): def _param(case): - name = case[0] - marks = ([pytest.mark.xfail(reason=KNOWN_DIFFERENCE[name], strict=True)] - if name in KNOWN_DIFFERENCE else []) - return pytest.param(*case, id=name, marks=marks) + return pytest.param(*case, id=case[0]) @pytest.mark.parametrize("name, cls, final_time, attrs, reference, params",