From 22513095e5990174662e88a48abce2701ef99c21 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 16:45:24 -0300 Subject: [PATCH 1/2] release: v0.2.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WHohDPhCtXJvVZ7jTGwK8e --- CHANGELOG.md | 6 +++++- CITATION.cff | 4 ++-- pyproject.toml | 2 +- src/dissmodel_sysdyn/__init__.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d5799..040251e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## [Unreleased] +## [0.2.0] — 2026-10-22 + +Every deterministic model now gives the same doubles as TerraME at every +step (checked against `terrame-goldens` v0.1.0). The trajectories of +`Daisyworld` and `MonoLake` change. - **Fix:** `Daisyworld` now follows TerraME: after updating the white area, it computes the planet albedo again for the black daisies. The trajectory diff --git a/CITATION.cff b/CITATION.cff index f3d9f67..03de5d1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -13,8 +13,8 @@ authors: - family-names: "Vale" given-names: "Mateus Dutra" orcid: "https://orcid.org/0009-0005-1301-879X" -version: 0.1.1 -date-released: "2026-10-08" +version: 0.2.0 +date-released: "2026-10-22" license: MIT url: "https://github.com/DisSModel/dissmodel-sysdyn" repository-code: "https://github.com/DisSModel/dissmodel-sysdyn" diff --git a/pyproject.toml b/pyproject.toml index 15a646c..b5a150d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dissmodel-sysdyn" -version = "0.1.1" +version = "0.2.0" description = "System dynamics models for DisSModel" readme = "README.md" license = { text = "MIT" } diff --git a/src/dissmodel_sysdyn/__init__.py b/src/dissmodel_sysdyn/__init__.py index 485f44a..d3ec452 100644 --- a/src/dissmodel_sysdyn/__init__.py +++ b/src/dissmodel_sysdyn/__init__.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.2.0" From 636c021ff6c3621a15f3243ed8fbe3023c7097aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:38:59 -0300 Subject: [PATCH 2/2] fix(tests): Daisyworld against the goldens generated in terrame-docker terrame-goldens v0.1.0 is now generated in the terrame-docker image (glibc 2.27). Daisyworld's temperatures take a fourth root through the C library's pow, whose last bit differs from newer glibc versions, so the model agrees to about 15 significant digits there (at most 2e-15 relative). The test keeps bit-for-bit equality for the other 15 cases and compares Daisyworld with a relative tolerance of 1e-13; the extract of the goldens is copied again. README and CHANGELOG say so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WHohDPhCtXJvVZ7jTGwK8e --- CHANGELOG.md | 13 +++++++++---- README.md | 7 +++++-- tests/goldens/daisyworld.csv.gz | Bin 5299 -> 5299 bytes tests/goldens/reference.json | 2 +- tests/test_goldens.py | 20 ++++++++++++++++++-- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040251e..6b59e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## [0.2.0] — 2026-10-22 Every deterministic model now gives the same doubles as TerraME at every -step (checked against `terrame-goldens` v0.1.0). The trajectories of +step, except Daisyworld, which agrees to about 15 significant digits (checked +against `terrame-goldens` v0.1.0). The trajectories of `Daisyworld` and `MonoLake` change. - **Fix:** `Daisyworld` now follows TerraME: after updating the white area, @@ -20,9 +21,13 @@ step (checked against `terrame-goldens` v0.1.0). The trajectories of - `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. + 2.0.1 runs from `terrame-goldens` v0.1.0, generated in the + `terrame-docker` image. 15 of the 16 deterministic cases (13 models, some + with two parameter sets) give **the same doubles as TerraME at every + step**. Daisyworld is compared with a relative tolerance of 10⁻¹³: its + temperatures use the C library's `pow`, whose last bit differs between + glibc 2.27 (the image) and newer versions. 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. diff --git a/README.md b/README.md index f66aad3..5c59ba7 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,11 @@ 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 +bit, for every variable TerraME records, except Daisyworld, whose fourth +roots go through the C library's `pow`: its last bit depends on the system +(the goldens come from the `terrame-docker` image, with glibc 2.27), so +Daisyworld agrees to about 15 significant digits and is compared with a +relative tolerance of 10⁻¹³. RandomWalk is compared by distribution. `tests/test_goldens.py` runs these comparisons (`pytest -m golden`, under a second). ## Examples diff --git a/tests/goldens/daisyworld.csv.gz b/tests/goldens/daisyworld.csv.gz index f9a7383d085b12dbc46725f0ef2e39408d8660a3..4a6995a364eec83f75c6c5eaced7a5d7c37751bd 100644 GIT binary patch delta 2174 zcmV-^2!Z#rDYGfCO$>jvJ%ODwAfx$?b8@dw*I;U{Z{Qj`*|qi?xP~+zWB(v42_4tw zxM<}0EBK6oMaqLR>J!>zG1faD(7i^UjRV@{q$=|5ZiN61t>ky4Au=OMaIfBgo_cvI z-$~Fi$`g7ik{3x?PiR=5U+L=&%_F+&%y*JeC=}Z*n_kcg00w`lysxen8(~CtkAe;z z&-8XX;(g2`()@x0@)`#;!q;hJ&o}h&>&u&gH#8c<(30QdmVrGQeu5k!M zgobk&uIl0N=xrSIa5xHY>+{3m;E*IR>D4FDEg34aP8aoOnsryMc-O7x8ycpDl=5jc z?#{6(c|%WSv$}uzfTqj2ufu9bN{6pWbIDHkqda5X-R1E!`0Ruh4VeChhS}_F<#6~) zGzOBbzM(q=p`Kb-kEv3%f=sz{KhGz0^Uldl{OR!F1=Ki=R$|i`M?a9=D5a3YKgdqO zOpE;*3FPcpqzrd4b1VJ>*;^4eEoq!(&G??oiUU0vlIk0|wQ&}`wf2Xc>TY>{Q%)tncRE+R~Eti8Kl?&O~2z~ zXR{w@nm4=(dtGDx8ebin%-&d$YklSqWb_iwEsy+0d~EqFAIRI4(;>>Xm!i6M9KhAh zA^yFnx1r5UBhitEL&OI%ndJ3VK8qR@s&UUZlX*okg+G986JzG1ra6X35!s>AOL3lu z|CWD>@L5>iZrONNdmvL&*_FzHOygMIcD<3+T2S0k(gd0UqRU*Ng4slX90K<#_e)`*O#@t7*Y}xq&HU;r4cjC?1gPy=tywk~kq_?-9wxrj>pqYP zk_n}CYiqUc_ z*j?)~E5WWGz$?dH+X=g@f>zHc`({W@CyiBiKt$z?GMe8jM}T9pB&q0-!$mu#@{!x# zTJY78A2jAVgoHRvc8lpc?1X=&51{f+Xcf9Fb1f%Jnpfvh^h3;F@;Un>U7c()2`$pDW1(XpUZbiAv!2Qp1{Jwt8HIF+B!uuIGBG};S3rrL3> z7%Yp#wn86p>Q88u=IiCIA841(&q&98^{K}Al5gV8Gsk>Y$QK;06=P-1W^nL(-s3aS zgrS5*58w&!taTuFX%>H(eA?_*82_HsP2DaZaP62Y@rdtGP}s@A_5*s>crEmlCb3!~ zBhv$#>8{*&&@X6*Bdg|s-a2cyYsXm0b6uo5a(O58f$aO8?~V5h+4lijZ!H*7qSy_y@yK+KLmB_IG_MIkgOZPmX z-RPp0}jc zi-T>(lHL6SIWE delta 2174 zcmV-^2!Z#rDYGfCO$>ibB2Q!o4cKVD1D#y#(>55J>l?YoQg*HVMy{dFhjQT?T21J@ zHpfLL&tE}kOe}I99ily<%@%{b^8wv!B-%Kj-A*ba-~Lt!(9ueMhZ-g`q(t}X4d|(x zr~19QcYB7qEmiU&E9(gj?ei;ry`gzTcc1yrGAe~?yLHnGS_OZ=B$fBo6=Nfe$SzVG z*vbRi^@tZTk5KaqPRMH<&?sNWkv-qg!>=2{0~(QGY)SBO>%blzKf#Vr+7mjPd_%Ew zLPNStSM_jsG&c@)P`T4OPQR`Lr5$ z=UA1zp{Kf8;e3BU)8<^*VYM@*)7PZCWXF53pU^Jz_!)$DLW>4We?voUwzhIOd?h*q z$#!IThanVH>*_I9Dp$}cckZWWaS;k{5UB0b;lmrKaT=}0rZtX!AiGjZp@)Bv9fKJc z`!y2C*||s^?q&vNeXfk8duEE=%x7yC_0u zz#+{n*WvDwKA;y4q6h7>wondDR%r(`QCJ z1@wADOVxbd@&lT2#S4u61KrJ+m4h^Ypxpy1pblvAp*gBtxMzJiupzwND4_;(enN*7 zL{XQM%$R@4i`QlIfTp1+bzP?lF!nvaGwC^Cr?wgejiKhtbO$x8V-@L>^H#2aTebQjPV@ z0qwlV{|#&3U8=7c6f5YizNdR)t(#SUot9UnWK z{Xo;b;Z@k{8uQoq>d<8N%8Fb={e2)Km~d`+e_JtS67Gl z_oCj0HZzVyM|Oo+Kaj~J@2~P%)Sys}d%l^>JBlg%0c@KXGaoh0F+7UMPL^iH`H|!3VNQ z>_+;GGqWOTPvRfQ)4Q44kL-rblb!yAM(8XD{s%E>jfjD_n{Cl7Q6BHHJvLNJ!nb}zQfz@Phj7n*j^wVI`r#hu=oVYjafxE3Q&`44}khdLlo!Z37i6++9HJvS) z(jiFMGr8CJ$ZooW%a3(ZHvq$%|9<-0b;jIBuWWh>e?5#|@j3zOd&X=@ruu3R1%z^( zpGGex_Z}PN1KM%!zRCK5zHc+z30;4(Lu-Dg_xAcoCzIX|mkw~Tv&;+_?b)wJbs5^c zL_d+;MUs?9debB5v~frOgIv`NGpz$TyX&C_FX=$>O{ifu^A7Y6=;}_(3)U0*UNnva z*$cF`_KDo$QsI#ObRdRRj(sg>%cdTl<<7gAKC;_s&f#P~vfJBFyVHSOz4?EibsxyY zNCKP(vMxj+u4~0?iCul$VQpdS6B;5JR?QPy>~S=W7TPPHCXW)$<2d;01)O4~~bq7RL&L|`Ky>bLN21}B3AjDm?lbs*9 zo0+}%4#fl6#y|3Du}e(1VJCkw-QCIQKvtj2G1qdgq;+*3MYp7rQ~FP6vbx58p`OsL znMu;qXgJ!JK+d9~FJ+f$2lQ~AjD6RNxpD|Vb}T#Mr`65NEv52lwXasW5I%EzdR11& z>F@3O_im>@&=_iexK_-S_LVuj1W#U>en2DRe8a7^2ej+@nNF+eE@*$CgSqJ~45dAt zDdzqBfb*{vV`aQr#pu)2&HE#_(P5}~H*F8(O$?u*wq%^iPiV-c<#zhp`#ombajh6D zi?p^v9k_=5fL3R|-tGEg4XSMD3=7qpX+MRP!JeYM-QW2WS}u2G%0R{7*Ghx23K{X+I_fSr%z#x=A9mvwJo zUu8{ooWL%^nE|ftNV!s65*7C-aH=EDK9Ny(3W-Nq`Qn2ckrRK}OW%hq;U7E`Z(Y2g zy$GZ*Xp>A2`^%x8T#dWxL5+Pt`%Y)P=JJ5Hb(eWU`_?Eu{)G0;m2xhr_}dFkX25GZ zQqtmljV|AJ2C=R=ph+Iz|6I=`^Ah+BGZYAM&wS##EAPj`#D!!`kHp!5l1JiG-yLxN z-s2P3t+ayafeU|AQz83=cH$lF%rM_jv&|jQXcBp#U)zDQgl?I0_;$=}pU}C+pp)_g z+LvS7sRGqK_O-LFLJH1fj8=?#aig~#D5tA-b8)O@bNhteZgpqx59np5O=osjx-8-E zCp2>3Mdb;Nz+PdsBPBLdkg@jTmp@Vrt%NE@$v?8YrAT(Waw1Q4$g%zPjVA9(_dJnZ z=PClXBPFw{?=kuUQF8nJ3C&P4J?R6w#LH%<(ewzi`;2sly1lhKkjdt_`MKzL-jP diff --git a/tests/goldens/reference.json b/tests/goldens/reference.json index a7cbaf6..17f0b5d 100644 --- a/tests/goldens/reference.json +++ b/tests/goldens/reference.json @@ -17,7 +17,7 @@ "files": { "goldens/sysdyn/chaotic_growth.csv.gz": "6328b3248918b88ec4adfc1002a99ec6c4d252fa1e6a3beaa0c1477faa528c75", "goldens/sysdyn/coffee.csv.gz": "cf32769b784339a21489a54ef05f1e59c403da6857fa7421dd28e05f1581a4c1", - "goldens/sysdyn/daisyworld.csv.gz": "da394371bf4dc84114ae5c5815797d20aaa7fd85b6e7b182446e180f7d747c6c", + "goldens/sysdyn/daisyworld.csv.gz": "1b8668e090f51550b12562d5af517d2fe4b2845e1ec68381c4eb21e9bee516b4", "goldens/sysdyn/homeostasis.csv.gz": "2cb49a621d7153e98632f7f0e11bfdc47ea04a105a2f659d6976bfa3dfc0fd4f", "goldens/sysdyn/homeostasis_100.csv.gz": "6896a8faed843dc0c42f58194b284381fad971e793bed8de531ef26282ac280b", "goldens/sysdyn/limited_growth.csv.gz": "9972047016a135b12c2e70e26209d438e1b911dd44ac927c47f4b8eab5f28b87", diff --git a/tests/test_goldens.py b/tests/test_goldens.py index 376950c..d22d2ef 100644 --- a/tests/test_goldens.py +++ b/tests/test_goldens.py @@ -8,6 +8,14 @@ **the same doubles** as TerraME at every step (bit for bit), for every variable TerraME records. +The exception is Daisyworld, whose temperatures take a fourth root +(``x ** 0.25`` in Python, ``x ^ 0.25`` in Lua). Both languages call the C +library's ``pow``, and its last bit depends on the library: the goldens come +from the ``terrame-docker`` image (glibc 2.27), and the same TerraME binary +on glibc 2.39 differs from the 16th significant digit on (see the README of +``terrame-goldens``). Python on any recent system behaves like the latter, so +Daisyworld is compared with a relative tolerance of ``POW_RTOL``. + RandomWalk draws random numbers, so it is compared by distribution with the 50 TerraME runs. """ @@ -28,6 +36,9 @@ GOLDENS = Path(__file__).resolve().parent / "goldens" +#: Cases that use the C library's pow, and their relative tolerance (see above). +POW_RTOL = {"daisyworld": 1e-13} + with open(GOLDENS / "cases.csv", newline="") as _f: CASES = [c for c in csv.DictReader(_f) if not c["seeds"]] @@ -63,7 +74,7 @@ def execute(self) -> None: @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: +def test_trajectory_matches_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"])) @@ -75,7 +86,12 @@ def test_trajectory_is_bit_identical_to_terrame(case: dict[str, str]) -> None: 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}" + rtol = POW_RTOL.get(case["case"]) + if rtol is None: + assert g == e, f"{case['case']}.{var} at step {t}: {g!r} != TerraME {e!r}" + else: + assert abs(g - e) <= rtol * abs(e), ( + f"{case['case']}.{var} at step {t}: {g!r} vs TerraME {e!r}") def test_random_walk_matches_terrame_distribution() -> None: