Skip to content

Fix reversible reaction Jacobian#114

Open
parkyr wants to merge 2 commits into
masterfrom
fix/issue-23-reversible-jacobian
Open

Fix reversible reaction Jacobian#114
parkyr wants to merge 2 commits into
masterfrom
fix/issue-23-reversible-jacobian

Conversation

@parkyr

@parkyr parkyr commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #23.

Summary

  • Add the missing reverse-term concentration derivative for reversible RxnKinetics state Jacobians.
  • Preserve the forward-only irreversible Jacobian path.
  • Make elementary state-derivative broadcasting explicit for batched concentration/time inputs.

Triage / Acceptance Criteria

Uses the maintainer triage artifact at #23 (comment).

Covered:

  • derivatives and get_rxn_rates(..., jac=True) match a finite-difference Jacobian of the reversible net species rates.
  • The irreversible, forward-only Jacobian remains unchanged.
  • Batched 2-D concentration/time inputs produce per-time Jacobians matching finite differences of the vectorized net-rate path.
  • Existing kinetics and assimulo-free tests still pass.

Related Issues

Tests

  • Red first: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py -q failed on unchanged origin/master with the missing reverse-term C_B column.
  • Green after fix: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py -q
  • Green after fix: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py tests/test_cryst_kinetics.py -q
  • Green after fix: .venv/bin/python -m pytest tests/ -m "not assimulo" -q
  • Green after fix: .venv/bin/python -m pytest --collect-only -q
  • Green after fix: git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,cr-at-eol diff --cached --check

Local environment: repository .venv with Python 3.11.15. The changed kinetics code is pure NumPy and does not require the assimulo backend.

Branch Hygiene

@parkyr
parkyr marked this pull request as ready for review July 13, 2026 14:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22c62c9250

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread PharmaPy/Kinetics.py Outdated
conc = np.asarray(conc)
conc_correc = np.maximum(eps, conc)

keq_temp = self.equil_term(temp, self.delta_hrxn)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the runtime ΔH into the reversible Jacobian

For reversible calls where the rate path supplies a temperature-corrected delta_hrxn (e.g. Reactors.material_balances computes deltah_rxn with Liquid_1.getHeatOfRxn(...) and passes it to get_rxn_rates), this new reverse derivative always recomputes Keq from self.delta_hrxn instead. When heat-capacity corrections make deltah_rxn differ from the reference value, the Jacobian is no longer the derivative of the residual used by the solver, so non-isothermal reversible reactors get the wrong reverse-term concentration columns; derivatives/get_rxn_rates(..., jac=True) needs a way to use the same per-call ΔH as the rate evaluation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6421028: runtime delta_hrxn is now accepted by get_rxn_rates(..., jac=True) / derivatives, _reverse_df_dstates uses it for Keq, and _BaseReactor.get_jacobians passes the reactor's corrected heat of reaction into the Jacobian path. Added tests for the runtime-DeltaH mismatch and reactor handoff.

@parkyr

parkyr commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Branch hygiene note: PR #111 also touches PharmaPy/Kinetics.py. Depending on merge order, this PR may need a rebase after #111 lands.

@parkyr parkyr left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review — COMMENT (posted by the PR author)

The reverse-term math is correct and the fix is a clear improvement: the added _reverse_df_dstates computes ∂/∂C_k [∏ C_prod^β / Keq] = β_k · r_term / C_k, derivatives now returns df − dr (verified against a finite-difference Jacobian for the isothermal A↔B case — matches within FD tolerance). The forward-only irreversible path is preserved, batched 2-D concentration/time Jacobians are supported via einsum, and the elem_df_dstates rewrite incidentally fixes a latent 2-D broadcasting bug (the old / (conc + eps) would misalign for conc.ndim == 2). CI on the head is green (Core tests + Assimulo integration).

One correctness gap remains, described below. It does not regress master (the base Jacobian omitted the reverse term entirely), so I am not treating it as blocking — but it is the natural follow-up and the PR body's "match a finite-difference Jacobian of the reversible net species rates" claim only holds for constant ΔH.

Nonblocking: reverse-term Keq uses reference ΔH, not the runtime ΔH the solver integrates

_reverse_df_dstates recomputes Keq from self.equil_term(temp, self.delta_hrxn) — the reference ΔH stored at construction. But the reactor residual passes a temperature-corrected ΔH into the rate path: Reactors.material_balances / energy_balances compute deltah_rxn = self.Liquid_1.getHeatOfRxn(..., self.Kinetics.delta_hrxn, self.Kinetics.tref_hrxn) and call get_rxn_rates(..., delta_hrxn=deltah_rxn), while get_jacobians calls self.Kinetics.derivatives(conc, temp) with no ΔH. When Δcp ≠ 0 (non-isothermal reversible reactor) the Jacobian is no longer the derivative of the residual CVode integrates. This matters most for the sensitivity system (dsens_dt = jac_states.dot(sens) + jac_params in Reactors.get_jacobians), which uses jac_states directly, and secondarily degrades Newton convergence.

Demonstration (same fix, keq_params=[2], delta_hrxn=[-5e4], tref_hrxn=298.15, temp=350; residual fed a runtime delta_hrxn=[-4e4] as getHeatOfRxn would):

Jacobian (uses self.delta_hrxn = -5e4):   B-column = [ 9.92, -9.92]
FD of residual (uses runtime = -4e4):     B-column = [ 5.46, -5.46]
max abs diff over (A,B) block: 4.47

Concrete fix: thread the per-call ΔH through get_rxn_rates(..., jac=True, delta_hrxn=...)derivatives(conc, temp, delta_hrxn=...)_reverse_df_dstates, and pass the reactor's already-computed deltah_rxn from get_jacobians (mirroring material_balances). If that plumbing is out of scope for this PR, document the limitation in the docstring and open a follow-up, since derivatives never accepted runtime ΔH — this PR is the first to make the Jacobian ΔH-dependent.

Nonblocking: test coverage does not exercise the runtime-ΔH / Keq(T) path

All three new tests use delta_hrxn=[0.0], so Keq is constant and the reference-vs-runtime ΔH question above is invisible to them. Add a case with nonzero ΔH at temp ≠ tref_hrxn that compares derivatives against a finite difference of the same residual the reactor integrates (i.e. get_rxn_rates(..., delta_hrxn=deltah_runtime)), so the gap above is either guarded or explicitly documented as deferred.

Review summary

  • Blocking: none.
  • Nonblocking: (1) reverse-term Keq uses reference ΔH instead of the runtime ΔH the residual uses — wrong Jacobian/sensitivities for non-isothermal reversible reactors with Δcp ≠ 0 (strict improvement over base, not a regression); (2) tests only cover constant ΔH.
  • Questions: none.
  • Tests run (repo .venv, Python 3.11.15, at head 22c62c9): pytest tests/test_reversible_jacobian_reverse_term.py tests/test_cryst_kinetics.py → 8 passed; pytest tests/ -m "not assimulo" → 36 passed, 8 deselected; --collect-only → 44 collected. Independently reproduced the ΔH mismatch shown above.
  • Merge-readiness: No blocking issues; head CI green (Core tests, Assimulo integration). This review is a COMMENT because this account authored the PR and GitHub rejects author APPROVE; an eligible maintainer's approval is required only if branch protection reports a review gate.

Comment thread PharmaPy/Kinetics.py Outdated
@parkyr

parkyr commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the runtime heat-of-reaction review feedback in 6421028.

Commits pushed:

  • 6421028 Thread runtime heat of reaction into reversible Jacobian

Changes made:

  • Threaded per-call delta_hrxn through RxnKinetics.get_rxn_rates(..., jac=True) and RxnKinetics.derivatives.
  • Updated _reverse_df_dstates to compute Keq from runtime delta_hrxn when supplied, falling back to self.delta_hrxn for existing callers.
  • Updated _BaseReactor.get_jacobians to compute the same runtime heat of reaction used by the reactor residual and pass it into Kinetics.derivatives.
  • Added regression coverage for non-constant runtime delta_hrxn and for the reactor-to-kinetics Jacobian handoff.

Tests run:

  • Red first: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py::test_reversible_jacobian_uses_runtime_delta_hrxn -q failed before the fix with max difference 4.4650123.
  • Green: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py -q
  • Green: .venv/bin/python -m pytest tests/test_reversible_jacobian_reverse_term.py tests/test_cryst_kinetics.py -q
  • Green: .venv/bin/python -m pytest --collect-only -q (46 tests collected)
  • Green: .venv/bin/python -m pytest tests/ -m "not assimulo" -q (38 passed, 8 deselected)
  • Green: git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,cr-at-eol diff --check
  • CI after push: Core tests passed; Assimulo integration tests passed.

Intentionally not addressed: none.

Remaining notes:

  • PR Fix kinetics custom params and equilibrium indexing #111 still touches PharmaPy/Kinetics.py; this PR may need a rebase depending on merge order.
  • This PR is authored by parkyr, so an eligible maintainer approval is still separate from these replies if branch protection requires it.

Comment thread PharmaPy/Kinetics.py

df_dconc = f_term[..., np.newaxis] * self.params_f / (conc + eps)
if conc.ndim == 1:
conc_term = conc + eps

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this eps a constant across the whole code? What’s its value? Is it only added to avoid division by zero is a concentration is small? I’m not very satisfied with this from a numerical point of view

Comment thread PharmaPy/Kinetics.py
@@ -532,12 +532,55 @@ def equilibrium_model(self, conc, temp, deltah_rxn):

def elem_df_dstates(self, conc):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s use the chance to document this and all new and exisiting functions in this file according to the numpy format

Comment thread PharmaPy/Kinetics.py

return jac_states
else: # --------------- wrt parameters
dk_dphi = self.dk_dkparams(temp).T

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need units for all these, espcially since we are computing derivatives

@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
"""Regression test for issue #23.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this format for the tests. Commenting each test function sounds unnecessary but this is a nice summary. We should keep applying it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

High: [Kinetics] Reversible-reaction analytical Jacobian ignores the equilibrium reverse term

2 participants