Skip to content

High: [Kinetics] Custom rate-law setup and vectorized equilibrium reverse term crash #71

Description

@sergey-gusev94

Maintainer triage (June 29, 2026)

  • Status: Verified bug against the current master source in this fork.
  • Severity: High.
  • Area labels: area:kinetics.
  • Tackle batch: P3 - documented-mode crashes and tooling breakages.

The source review confirmed that the cited code path and failure mode are still present. The original audit details are preserved below for reproduction notes and suggested fixes.


Original audit details

Severity: High · Category: crash + indexing · Part of #67

Two Kinetics.py defects, grouped as one kinetics-correctness fix.


1. (High) RxnKinetics with a user-defined kinetic_model crashes at construction

Location: PharmaPy/Kinetics.py:337-363

In the dict branch of set_params, the locals params_f/orders are assigned only inside if self.elem_flag:. For a custom kinetic_model, elem_flag=False, so the block is skipped and execution hits elif params_f is None: (359) referencing an unbound local → UnboundLocalError. Even guarded, line 363 self.params_f = orders references undefined orders. The constructor sets elem_flag=False whenever kinetic_model is not None (262-272) and unconditionally calls set_params(params_dict) (288).

  • Trigger: the documented custom-rate-law workflow, RxnKinetics(..., kinetic_model=my_func, params_f=array, df_dstates=..., df_dtheta=...).
  • Wrong result: constructor raises UnboundLocalError immediately; the entire user-defined rate-law feature is unusable.
  • Secondary: the array branch (366-375) also never updates self.params_f when elem_flag=False, freezing custom f-parameters during parameter estimation.
  • Fix: on the elem_flag=False path set params_f = params.get('params_f', None) before the elif, then self.params_f = np.asarray(params_f) (raise the intended RuntimeError when None); handle elem_flag=False in the array branch too.

2. (High) Vectorized equilibrium reverse term divides by Keq indexed on the time axis, not the reaction axis

Location: PharmaPy/Kinetics.py:519-531 (with equil_term 453-463)

The 2-D branch builds r_term[:, ind] = np.prod(conc**orders[ind], axis=1) / keq_temp[ind] for ind in range(num_rxns) (524-527). For array temp, equil_term returns shape (n_temp, n_rxns) (via np.outer(inv_temp, deltah_temp/gas_ct), 460), so keq_temp[ind] indexes the first (time) axis — the per-reaction Keq vector of row ind — not the scalar Keq of reaction ind. Worse, when deltah_rxn is 2-D (as getHeatOfRxn returns for array temp), np.outer flattens it to (n_temp, n_temp*n_rxns), so keq_temp is malformed and the divide can raise a broadcast error. The 1-D ODE-integration branch (519-522) is fine.

  • Trigger: a reversible reaction (keq_params not None) in a CSTR/PFR/Batch reactor, then retrieve results / build heat profiles (2-D mole_conc + 1-D temp into the 2-D branch, e.g. CSTR.energy_balances with heat_prof=True).
  • Wrong result: reverse (and net) rates for reversible reactions use the wrong Keq per time point → incorrect rate profiles and heat-of-reaction source terms; with 2-D deltah_rxn it can raise ValueError: operands could not be broadcast together.
  • Fix: index the reaction axis (keq_temp[:, ind]) in the 2-D branch; and fix equil_term to avoid np.outer flattening when deltah_rxn is already 2-D (broadcast inv_temp[:,None]*deltah/gas_ct).

Related (not duplicates): #36 (reverse-term exponents vs custom orders) and #23 (analytical Jacobian ignores the reverse term) are different defects in the same function; #44 targets integer-dtype truncation on the elem_flag=True path.

Metadata

Metadata

Assignees

Labels

area:kineticsReaction and crystallization kineticsbugSomething isn't workingcorrectnessModel/numerical correctness defectseverity:highConfirmed bug with major correctness impact or broken documented workflowstatus:verifiedSource-reviewed against the current codebase

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions