From 27c34ffba0c9cdebddffb6934051f24ecfaf1950 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:44:26 +0000 Subject: [PATCH 01/14] Export ring helpers and add ergodic-component diagnostics --- jump_diffusion_engine/__init__.py | 6 ++- jump_diffusion_engine/engine.py | 84 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/jump_diffusion_engine/__init__.py b/jump_diffusion_engine/__init__.py index 200382a..7bcf6e6 100644 --- a/jump_diffusion_engine/__init__.py +++ b/jump_diffusion_engine/__init__.py @@ -1,3 +1,5 @@ -from .engine import JumpDiffusionEngine +from .engine import (JumpDiffusionEngine, ergodic_components, + ring_generator, reduce_ring) -__all__ = ["JumpDiffusionEngine"] +__all__ = ["JumpDiffusionEngine", "ergodic_components", + "ring_generator", "reduce_ring"] diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index 7b43a62..04441b6 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -1,8 +1,82 @@ +import warnings + import numpy as np from scipy.optimize import root_scalar import matplotlib.pyplot as plt from typing import Callable, List, Optional, Tuple, Dict + +def ergodic_components(L: np.ndarray, rtol: float = 1e-9) -> int: + """Number of ergodic components of a column-generator matrix. + + For a conservative generator (columns sum to zero), dim ker(L) equals + the number of closed communicating classes: each component carries its + own stationary distribution, so the zero eigenvalue has one copy per + component. Computed via SVD (robust to non-normality): the count of + singular values below ``rtol * s_max``. + + Discrete check of the gcd law: for a ring of N sites with jump channels + {k_i}, dim ker(L) = gcd(k_1, ..., k_m, N). Anything downstream that + assumes a *unique* stationary distribution (stationary densities, + transfer times, spectral gaps) silently requires this to return 1. + """ + s = np.linalg.svd(np.asarray(L, dtype=float), compute_uv=False) + if s.size == 0: + return 0 + smax = s[0] if s[0] > 0 else 1.0 + return int(np.sum(s < rtol * smax)) + + +def ring_generator(N: int, channels: Dict[int, float]) -> np.ndarray: + """Conservative column-generator for jump channels on the ring Z_N. + + ``channels`` maps a (signed) step dn to a rate. Steps are taken mod N; + dn ≡ 0 (mod N) is a no-op. Columns sum to zero by construction. + """ + L = np.zeros((N, N)) + for i in range(N): + for dn, r in channels.items(): + j = (i + dn) % N + if j != i and r > 0: + L[j, i] += r + L[i, i] -= r + return L + + +def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: + """Exact decimation of a ring jump generator to its coprime core. + + Since gcd(m·k, N) = m·gcd(k, N/m) for m | N, the cosets mod + g = gcd(all steps, N) are exactly invariant, and + + L_N(channels) ≅ I_g ⊗ L_{N/g}(channels/g) (permutation-similar) + + so the full spectrum is the core spectrum with multiplicity g, and + dim ker(L_N) = g. Returns the core parameters; analyses (spectra, + mixing rates, transfer times) can be run on the (N/g)-site core and + replicated g times at zero error. + + Returns + ------- + dict with keys 'g' (sector count / multiplicity), 'N_core', + 'channels_core', 'L_core'. + """ + from math import gcd as _gcd + g = N + for dn in channels: + g = _gcd(g, dn % N) + if g == 0: # all steps were multiples of N: nothing moves + return {'g': N, 'N_core': 1, 'channels_core': {}, 'L_core': np.zeros((1, 1))} + N_core = N // g + ch_core: Dict[int, float] = {} + for dn, r in channels.items(): + dn_c = ((dn % N) // g) % N_core + if dn_c != 0 and r > 0: + ch_core[dn_c] = ch_core.get(dn_c, 0.0) + r + return {'g': g, 'N_core': N_core, 'channels_core': ch_core, + 'L_core': ring_generator(N_core, ch_core)} + + class JumpDiffusionEngine: """ Universal Jump-Diffusion Simulator & Stochastic Stability Framework. @@ -595,12 +669,22 @@ def markov_generator(self, lambda_val: float, L[n - 1, n] = rate_down[n] # downward neighbour receives this rate L[n, n] = -(rate_up[n] + rate_down[n]) # diagonal: exact negative sum + n_comp = ergodic_components(L) + if n_comp != 1: + warnings.warn( + f"markov_generator: generator has {n_comp} ergodic components; " + "stationary quantities and transfer times are ill-defined " + "(initial-condition dependent).", + RuntimeWarning + ) + return { 'L': L, 'x': x, 'dx': float(dx), 'rate_up': rate_up, 'rate_down': rate_down, + 'n_components': n_comp, } def stationary_density(self, lambda_val: float, x_range: Tuple[float, float] = (-10, 10), n_points: int = 2000): From 0f4cd176f21dfd59a8a9be6e51a4e4e0495fcaa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:45:40 +0000 Subject: [PATCH 02/14] Refine reduce_ring logic per review feedback --- jump_diffusion_engine/engine.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index 04441b6..d2d329c 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -65,12 +65,10 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: g = N for dn in channels: g = _gcd(g, dn % N) - if g == 0: # all steps were multiples of N: nothing moves - return {'g': N, 'N_core': 1, 'channels_core': {}, 'L_core': np.zeros((1, 1))} N_core = N // g - ch_core: Dict[int, float] = {} + ch_core = {} for dn, r in channels.items(): - dn_c = ((dn % N) // g) % N_core + dn_c = (dn % N) // g if dn_c != 0 and r > 0: ch_core[dn_c] = ch_core.get(dn_c, 0.0) + r return {'g': g, 'N_core': N_core, 'channels_core': ch_core, From 8a5ab27a647d6feb279360e1bdfa21140b7db3ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:46:28 +0000 Subject: [PATCH 03/14] Polish reduce_ring docs and imports after validation review --- jump_diffusion_engine/engine.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index d2d329c..d04bba1 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -1,4 +1,5 @@ import warnings +from math import gcd as _gcd import numpy as np from scipy.optimize import root_scalar @@ -47,21 +48,23 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: """Exact decimation of a ring jump generator to its coprime core. Since gcd(m·k, N) = m·gcd(k, N/m) for m | N, the cosets mod - g = gcd(all steps, N) are exactly invariant, and + g = GCD(all steps, N) are exactly invariant, and L_N(channels) ≅ I_g ⊗ L_{N/g}(channels/g) (permutation-similar) so the full spectrum is the core spectrum with multiplicity g, and dim ker(L_N) = g. Returns the core parameters; analyses (spectra, mixing rates, transfer times) can be run on the (N/g)-site core and - replicated g times at zero error. + replicated g times at zero error. If ``channels`` is empty then no jumps + occur and the reduction yields ``g = N`` and ``N_core = 1``. + When multiple channels map to the same reduced step, their rates are + aggregated in ``channels_core``. Returns ------- dict with keys 'g' (sector count / multiplicity), 'N_core', 'channels_core', 'L_core'. """ - from math import gcd as _gcd g = N for dn in channels: g = _gcd(g, dn % N) From 78acb10dee4e7463d3340977469b5ca5b6626e8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:47:28 +0000 Subject: [PATCH 04/14] Normalize reduced ring step handling explicitly --- jump_diffusion_engine/engine.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index d04bba1..7361ed3 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -67,11 +67,13 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: """ g = N for dn in channels: - g = _gcd(g, dn % N) + dn_mod = ((dn % N) + N) % N + g = _gcd(g, dn_mod) N_core = N // g ch_core = {} for dn, r in channels.items(): - dn_c = (dn % N) // g + dn_mod = ((dn % N) + N) % N + dn_c = dn_mod // g if dn_c != 0 and r > 0: ch_core[dn_c] = ch_core.get(dn_c, 0.0) + r return {'g': g, 'N_core': N_core, 'channels_core': ch_core, From d6b8e4290885080055af95ce6648074d22598db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:48:15 +0000 Subject: [PATCH 05/14] Tighten ergodic tolerance and dedupe modular reduction logic --- jump_diffusion_engine/engine.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index 7361ed3..9aeef70 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -25,7 +25,7 @@ def ergodic_components(L: np.ndarray, rtol: float = 1e-9) -> int: if s.size == 0: return 0 smax = s[0] if s[0] > 0 else 1.0 - return int(np.sum(s < rtol * smax)) + return int(np.sum(s <= rtol * smax)) def ring_generator(N: int, channels: Dict[int, float]) -> np.ndarray: @@ -66,13 +66,14 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: 'channels_core', 'L_core'. """ g = N + mod_n = lambda dn: ((dn % N) + N) % N for dn in channels: - dn_mod = ((dn % N) + N) % N + dn_mod = mod_n(dn) g = _gcd(g, dn_mod) N_core = N // g ch_core = {} for dn, r in channels.items(): - dn_mod = ((dn % N) + N) % N + dn_mod = mod_n(dn) dn_c = dn_mod // g if dn_c != 0 and r > 0: ch_core[dn_c] = ch_core.get(dn_c, 0.0) + r From 65c1621e0208333be98fa2b7eb85afeaa9de597e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:18:58 +0000 Subject: [PATCH 06/14] Fix reduce_ring to ignore inactive channels in gcd reduction --- jump_diffusion_engine/engine.py | 31 +++++++++++++++++++------------ tests/test_engine.py | 21 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index 9aeef70..a6cbee4 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -55,8 +55,10 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: so the full spectrum is the core spectrum with multiplicity g, and dim ker(L_N) = g. Returns the core parameters; analyses (spectra, mixing rates, transfer times) can be run on the (N/g)-site core and - replicated g times at zero error. If ``channels`` is empty then no jumps - occur and the reduction yields ``g = N`` and ``N_core = 1``. + replicated g times at zero error. If no *active* channels remain after + applying the same acceptance rule as ``ring_generator`` (positive rate and + non-zero modular step), then no jumps occur and the reduction yields + ``g = N`` and ``N_core = 1``. When multiple channels map to the same reduced step, their rates are aggregated in ``channels_core``. @@ -65,18 +67,23 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: dict with keys 'g' (sector count / multiplicity), 'N_core', 'channels_core', 'L_core'. """ - g = N mod_n = lambda dn: ((dn % N) + N) % N - for dn in channels: - dn_mod = mod_n(dn) - g = _gcd(g, dn_mod) - N_core = N // g - ch_core = {} - for dn, r in channels.items(): - dn_mod = mod_n(dn) - dn_c = dn_mod // g - if dn_c != 0 and r > 0: + active = [(mod_n(dn), r) for dn, r in channels.items() if r > 0 and mod_n(dn) != 0] + + if not active: + g = N + N_core = 1 + ch_core = {} + else: + g = N + for dn_mod, _ in active: + g = _gcd(g, dn_mod) + N_core = N // g + ch_core = {} + for dn_mod, r in active: + dn_c = dn_mod // g ch_core[dn_c] = ch_core.get(dn_c, 0.0) + r + return {'g': g, 'N_core': N_core, 'channels_core': ch_core, 'L_core': ring_generator(N_core, ch_core)} diff --git a/tests/test_engine.py b/tests/test_engine.py index 7b98779..6a69d62 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from jump_diffusion_engine import JumpDiffusionEngine +from jump_diffusion_engine import JumpDiffusionEngine, reduce_ring # --------------------------------------------------------------------------- @@ -500,6 +500,25 @@ def test_accepts_single_realization(self, engine, monkeypatch): assert fig is not None +# --------------------------------------------------------------------------- +# reduce_ring +# --------------------------------------------------------------------------- + +class TestReduceRing: + def test_gcd_uses_only_active_channels(self): + out = reduce_ring(12, {2: 1.0, 3: -5.0, 6: 0.0}) + assert out['g'] == 2 + assert out['N_core'] == 6 + assert out['channels_core'] == {1: 1.0} + + def test_effectively_empty_channels_reduce_to_trivial_core(self): + out = reduce_ring(12, {1: 0.0, -1: -2.0, 12: 4.0}) + assert out['g'] == 12 + assert out['N_core'] == 1 + assert out['channels_core'] == {} + np.testing.assert_allclose(out['L_core'], np.zeros((1, 1))) + + # --------------------------------------------------------------------------- # markov_generator # --------------------------------------------------------------------------- From f03cd434c1f97dbdac4e3a427d08f53645ff64df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:20:50 +0000 Subject: [PATCH 07/14] Refactor reduce_ring active-channel filtering loop --- jump_diffusion_engine/engine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jump_diffusion_engine/engine.py b/jump_diffusion_engine/engine.py index a6cbee4..81e99cd 100644 --- a/jump_diffusion_engine/engine.py +++ b/jump_diffusion_engine/engine.py @@ -68,7 +68,11 @@ def reduce_ring(N: int, channels: Dict[int, float]) -> Dict: 'channels_core', 'L_core'. """ mod_n = lambda dn: ((dn % N) + N) % N - active = [(mod_n(dn), r) for dn, r in channels.items() if r > 0 and mod_n(dn) != 0] + active = [] + for dn, r in channels.items(): + dn_mod = mod_n(dn) + if r > 0 and dn_mod != 0: + active.append((dn_mod, r)) if not active: g = N From 91302d338bbd7fc1bfea82042bcad0e3eb150116 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:56:24 +0000 Subject: [PATCH 08/14] Add entropy production current-affinity test --- tests/test_engine.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 6a69d62..04ca809 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from jump_diffusion_engine import JumpDiffusionEngine, reduce_ring +from jump_diffusion_engine import JumpDiffusionEngine, reduce_ring, ring_generator # --------------------------------------------------------------------------- @@ -519,6 +519,32 @@ def test_effectively_empty_channels_reduce_to_trivial_core(self): np.testing.assert_allclose(out['L_core'], np.zeros((1, 1))) +# --------------------------------------------------------------------------- +# entropy production +# --------------------------------------------------------------------------- + +class TestEntropyProduction: + def test_entropy_production_equals_current_times_affinity(self): + N = 7 + rate_fwd = 1.7 + rate_bwd = 0.4 + L = ring_generator(N, {1: rate_fwd, -1: rate_bwd}) + p = np.full(N, 1.0 / N) + + ep = 0.0 + for i in range(N): + j = (i + 1) % N + f_ij = L[j, i] * p[i] + f_ji = L[i, j] * p[j] + J = f_ij - f_ji + A = np.log(f_ij / f_ji) + ep += J * A + + current = (rate_fwd - rate_bwd) / N + affinity = N * np.log(rate_fwd / rate_bwd) + np.testing.assert_allclose(ep, current * affinity, rtol=0, atol=1e-12) + + # --------------------------------------------------------------------------- # markov_generator # --------------------------------------------------------------------------- From a5079f4410e59fbf6f46ca8a69bbb2b75b3bbe1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:56:54 +0000 Subject: [PATCH 09/14] Harden entropy production test flow assumptions --- tests/test_engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_engine.py b/tests/test_engine.py index 04ca809..e696de7 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -536,6 +536,7 @@ def test_entropy_production_equals_current_times_affinity(self): j = (i + 1) % N f_ij = L[j, i] * p[i] f_ji = L[i, j] * p[j] + assert f_ij > 0.0 and f_ji > 0.0 J = f_ij - f_ji A = np.log(f_ij / f_ji) ep += J * A From 5a35c068da6f76c31a9f339e6e33d1041a74e5fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:57:34 +0000 Subject: [PATCH 10/14] Use stationary distribution in entropy production test --- tests/test_engine.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index e696de7..a199f4a 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -529,9 +529,16 @@ def test_entropy_production_equals_current_times_affinity(self): rate_fwd = 1.7 rate_bwd = 0.4 L = ring_generator(N, {1: rate_fwd, -1: rate_bwd}) - p = np.full(N, 1.0 / N) + w, v = np.linalg.eig(L) + p = np.real(v[:, np.argmin(np.abs(w))]) + if p.sum() < 0: + p = -p + p = np.clip(p, 0.0, None) + p /= p.sum() ep = 0.0 + bond_currents = [] + affinity = 0.0 for i in range(N): j = (i + 1) % N f_ij = L[j, i] * p[i] @@ -539,10 +546,12 @@ def test_entropy_production_equals_current_times_affinity(self): assert f_ij > 0.0 and f_ji > 0.0 J = f_ij - f_ji A = np.log(f_ij / f_ji) + bond_currents.append(J) + affinity += A ep += J * A - current = (rate_fwd - rate_bwd) / N - affinity = N * np.log(rate_fwd / rate_bwd) + current = float(np.mean(bond_currents)) + np.testing.assert_allclose(bond_currents, current, rtol=0, atol=1e-12) np.testing.assert_allclose(ep, current * affinity, rtol=0, atol=1e-12) From 3cb85d179bab66ab1f5a41eefe437f992f4ffc89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:58:13 +0000 Subject: [PATCH 11/14] Compute stationary state via constrained solve in entropy test --- tests/test_engine.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index a199f4a..50ed08d 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -529,12 +529,13 @@ def test_entropy_production_equals_current_times_affinity(self): rate_fwd = 1.7 rate_bwd = 0.4 L = ring_generator(N, {1: rate_fwd, -1: rate_bwd}) - w, v = np.linalg.eig(L) - p = np.real(v[:, np.argmin(np.abs(w))]) - if p.sum() < 0: - p = -p - p = np.clip(p, 0.0, None) - p /= p.sum() + A = np.vstack([L, np.ones(N)]) + b = np.zeros(N + 1) + b[-1] = 1.0 + p, *_ = np.linalg.lstsq(A, b, rcond=None) + np.testing.assert_allclose(L @ p, 0.0, rtol=0, atol=1e-12) + np.testing.assert_allclose(p.sum(), 1.0, rtol=0, atol=1e-12) + assert np.all(p > 0.0) ep = 0.0 bond_currents = [] From d78a0c29876eb4409894a34acca423427a185a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:58:49 +0000 Subject: [PATCH 12/14] Clarify bond variable names in entropy test --- tests/test_engine.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 50ed08d..25b41a6 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -545,11 +545,11 @@ def test_entropy_production_equals_current_times_affinity(self): f_ij = L[j, i] * p[i] f_ji = L[i, j] * p[j] assert f_ij > 0.0 and f_ji > 0.0 - J = f_ij - f_ji - A = np.log(f_ij / f_ji) - bond_currents.append(J) - affinity += A - ep += J * A + bond_current = f_ij - f_ji + bond_affinity = np.log(f_ij / f_ji) + bond_currents.append(bond_current) + affinity += bond_affinity + ep += bond_current * bond_affinity current = float(np.mean(bond_currents)) np.testing.assert_allclose(bond_currents, current, rtol=0, atol=1e-12) From 04f1dad2e91bd6f26fc83d66569c1cf2eb860ca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:59:16 +0000 Subject: [PATCH 13/14] Rename entropy variable for test clarity --- tests/test_engine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 25b41a6..0ab0104 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -537,7 +537,7 @@ def test_entropy_production_equals_current_times_affinity(self): np.testing.assert_allclose(p.sum(), 1.0, rtol=0, atol=1e-12) assert np.all(p > 0.0) - ep = 0.0 + entropy_production = 0.0 bond_currents = [] affinity = 0.0 for i in range(N): @@ -549,11 +549,11 @@ def test_entropy_production_equals_current_times_affinity(self): bond_affinity = np.log(f_ij / f_ji) bond_currents.append(bond_current) affinity += bond_affinity - ep += bond_current * bond_affinity + entropy_production += bond_current * bond_affinity current = float(np.mean(bond_currents)) np.testing.assert_allclose(bond_currents, current, rtol=0, atol=1e-12) - np.testing.assert_allclose(ep, current * affinity, rtol=0, atol=1e-12) + np.testing.assert_allclose(entropy_production, current * affinity, rtol=0, atol=1e-12) # --------------------------------------------------------------------------- From e828b88beee445e6641b5f77e8d336c7abf0f12c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:59:42 +0000 Subject: [PATCH 14/14] Simplify stationary solve unpacking in entropy test --- tests/test_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 0ab0104..9505bf2 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -532,7 +532,7 @@ def test_entropy_production_equals_current_times_affinity(self): A = np.vstack([L, np.ones(N)]) b = np.zeros(N + 1) b[-1] = 1.0 - p, *_ = np.linalg.lstsq(A, b, rcond=None) + p = np.linalg.lstsq(A, b, rcond=None)[0] np.testing.assert_allclose(L @ p, 0.0, rtol=0, atol=1e-12) np.testing.assert_allclose(p.sum(), 1.0, rtol=0, atol=1e-12) assert np.all(p > 0.0)