jacobian='jax' emits jnp.jnp.log for any model using ln(): double substitution in _translate_expr_jax
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
Research direction
Read python/bngsim/_jax_rhs.py around lines 143-144 and run the minimal _translate_expr_jax reproduction. Check python/tests/test_jax_jacobian.py for the expression-translation tests, then add coverage for ln() so it produces jnp.log once and verify the JAX run matches the default trajectory.
Written by the indexing model from the issue text.
Description
Summary
_translate_expr_jax applies re.sub(r"\bln\b", "jnp.log", c) on line 143 and then re.sub(r"\blog\b", "jnp.log", c) on line 144. The second substitution re-matches the log it just produced (the . in jnp.log is a non-word char, so \blog\b has a word boundary there), so ln(x) becomes jnp.jnp.log(x). The generated source is eval'd in rhs() with {"__builtins__": {}}, so the very first RHS evaluation raises AttributeError. Any .net model with an ln() in a functions block is broken on the JAX paths: bngsim.Simulator(model, method='ode', jacobian='jax', net_path=...) and bngsim._diffrax_solver.run_diffrax, which both call generate_jax_rhs. The same model integrates fine with the default jacobian, so this is purely the translator's substitution ordering.
Location: python/bngsim/_jax_rhs.py:143
Reproduction
cd /private/tmp/claude-11919/-Users-hlavacek-Code-bngsim/c49bfe51-f320-4eef-88bb-4025a472c19f/scratchpad && /Users/hlavacek/Code/bngsim/.venv/bin/python verif_ln_e2e.py # (model at verif_ln_model.net, identical content to the claim's int_ln_model.net). Minimal one-liner that needs no .net file: /Users/hlavacek/Code/bngsim/.venv/bin/python -c "from bngsim._jax_rhs import _translate_expr_jax; print(_translate_expr_jax('k1*ln(1+B)', {'k1':0}, {'B':0}, set(), []))" -> params[0]*jnp.jnp.log(1+obs[0])
Repro script
# int_ln_model.net (written to the scratchpad dir):
# Created by BioNetGen 2.9.3
begin parameters
1 k1 0.5 # Constant
2 kd 0.1 # Constant
end parameters
begin functions
1 growth() k1*ln(1+B)
end functions
begin species
1 A() 10
2 B() 1
end species
begin reactions
1 1 2 growth #_R1
2 2 1 kd #_R2
end reactions
begin groups
1 A_tot 1
2 B 2
end groups
# int_ln_e2e.py
import warnings, bngsim
NET = "/private/tmp/claude-11919/-Users-hlavacek-Code-bngsim/c49bfe51-f320-4eef-88bb-4025a472c19f/scratchpad/int_ln_model.net"
m = bngsim.Model.from_net(NET)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
sim = bngsim.Simulator(m, method="ode", jacobian="jax", net_path=NET)
try:
r = sim.run(t_span=(0, 10), n_points=11)
print("ran ok", r.species[-1])
except Exception as e:
print("E2E RAISED:", type(e).__name__, e)
Observed
$ .venv/bin/python int_ln_e2e.py
E2E RAISED: AttributeError module 'jax.numpy' has no attribute 'jnp'
$ .venv/bin/python -c "from bngsim._jax_rhs import _translate_expr_jax; print(_translate_expr_jax('k1*ln(1+Btot)', {'k1':0}, {'Btot':0}, set(), []))"
params[0]*jnp.jnp.log(1+obs[0])
$ .venv/bin/python -c "..." # same model, default jacobian
baseline (jacobian=auto) OK: [ 0.84228104 10.15771896]
Expected
ln(x) should translate to jnp.log(x) and the JAX-Jacobian run should produce the same trajectory as the default jacobian ([0.84228104, 10.15771896] at t=10). Fix by making the substitutions non-re-entrant, e.g. map ln and log in a single pass (re.sub(r"\b(?:ln|log)\b", "jnp.log", c)) or use a placeholder/\b(?<!\.) guard so an already-emitted jnp.log is not rewritten.
Verification notes
The repro was independently re-run and the analysis re-checked against existing tests, git blame/git log -S and CHANGELOG.md (confidence: high).
The claim survives every refutation I could mount.
MECHANISM CONFIRMED BY READING. /Users/hlavacek/Code/bngsim/python/bngsim/_jax_rhs.py:143-144:
c = re.sub(r"\bln\b", "jnp.log", c)
c = re.sub(r"\blog\b", "jnp.log", c)
The second sub re-matches the log the first one just emitted (. is a non-word char, so \blog\b has boundaries inside jnp.log), giving jnp.jnp.log. No other entry in that block is re-entrant: exp->jnp.exp, min->jnp.minimum, etc. never re-match, which my probe confirms — only ln is affected.
NOT INTENDED. The function's own docstring at _jax_rhs.py:95 states the contract it violates: "- ln() -> jnp.log()". There is no test asserting the doubled form: python/tests/test_jax_jacobian.py's TestExpressionTranslation covers only simple/time/exponentiation cases (lines 219-246) and no test file mentions ln at all. git blame -L 141,146 puts both lines in the file's original commit 764f4b1; git log -S finds no later commit that deliberately shaped this. CHANGELOG.md has no entry for it (its _jax_rhs.py mentions are about dense-solver routing and a de-duplicated expression, not the math-function map).
INPUT IS VALID, NOT MISUSE. ln is a first-class .net function everywhere else in this codebase: src/bngsim_api.cpp:191 and src/expression.cpp:1191 list it, src/expression.cpp:961 registers ln in the symbol table, and python/bngsim/_jacobian.py:121, _saturable_jacobian.py:58-59, _codegen.py:5921 all map ln->log. So the whole rest of the system consumes ln( correctly — CHANGELOG.md:3871 even quotes a real run's logterm() = ln(1 - Atot). My identical .net integrates cleanly on the default jacobian ([0.84228104, 10.15771896]); only the JAX translator breaks on it.
NOT A DELIBERATE FALLBACK / NOT SWALLOWED. The jax branch in python/bngsim/_simulator.py:1060-1087 raises on every other failure mode (no net_path -> ValueError, no jax -> ImportError) and has no try/except around prepare_jax_jacobian, so the AttributeError propagates out of the public run. I also confirmed the second, independent entry point fails: generate_jax_rhs(net_path) — used by python/bngsim/_diffrax_solver.py:95 — raises the same AttributeError.
… (truncated)
Severity assessed as medium. Reviewed against main at commit c3ab2ee.
- Dominant language
- Python
- Stars
- 2
- Forks
- 3
- Avg merge
- 6h 57m
- Merged PRs (30d)
- 75
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lanl/bngsim
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
correctness concern latent bug low priority
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
enhancement
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100