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.
Maintainer triage (June 29, 2026)
mastersource in this fork.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.pydefects, grouped as one kinetics-correctness fix.1. (High)
RxnKineticswith a user-definedkinetic_modelcrashes at constructionLocation:
PharmaPy/Kinetics.py:337-363In the dict branch of
set_params, the localsparams_f/ordersare assigned only insideif self.elem_flag:. For a customkinetic_model,elem_flag=False, so the block is skipped and execution hitselif params_f is None:(359) referencing an unbound local →UnboundLocalError. Even guarded, line 363self.params_f = ordersreferences undefinedorders. The constructor setselem_flag=Falsewheneverkinetic_modelis not None (262-272) and unconditionally callsset_params(params_dict)(288).RxnKinetics(..., kinetic_model=my_func, params_f=array, df_dstates=..., df_dtheta=...).UnboundLocalErrorimmediately; the entire user-defined rate-law feature is unusable.self.params_fwhenelem_flag=False, freezing custom f-parameters during parameter estimation.elem_flag=Falsepath setparams_f = params.get('params_f', None)before theelif, thenself.params_f = np.asarray(params_f)(raise the intendedRuntimeErrorwhen None); handleelem_flag=Falsein the array branch too.2. (High) Vectorized equilibrium reverse term divides by
Keqindexed on the time axis, not the reaction axisLocation:
PharmaPy/Kinetics.py:519-531(withequil_term453-463)The 2-D branch builds
r_term[:, ind] = np.prod(conc**orders[ind], axis=1) / keq_temp[ind]forind in range(num_rxns)(524-527). For arraytemp,equil_termreturns shape(n_temp, n_rxns)(vianp.outer(inv_temp, deltah_temp/gas_ct), 460), sokeq_temp[ind]indexes the first (time) axis — the per-reaction Keq vector of rowind— not the scalar Keq of reactionind. Worse, whendeltah_rxnis 2-D (asgetHeatOfRxnreturns for array temp),np.outerflattens it to(n_temp, n_temp*n_rxns), sokeq_tempis malformed and the divide can raise a broadcast error. The 1-D ODE-integration branch (519-522) is fine.keq_paramsnot None) in a CSTR/PFR/Batch reactor, then retrieve results / build heat profiles (2-Dmole_conc+ 1-Dtempinto the 2-D branch, e.g.CSTR.energy_balanceswithheat_prof=True).deltah_rxnit can raiseValueError: operands could not be broadcast together.keq_temp[:, ind]) in the 2-D branch; and fixequil_termto avoidnp.outerflattening whendeltah_rxnis already 2-D (broadcastinv_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=Truepath.