Browser app for JAXSR at /jaxsr/app - #12
Merged
Merged
Conversation
Adds a zero-install front end published alongside the docs on GitHub Pages: upload a spreadsheet, assign column roles, pick which function families the search may draw from, and get a ranked table of candidate equations with diagnostics and exports. Everything runs client-side through Pyodide, so no data leaves the browser and there is no server to operate. jaxlib has no WebAssembly build, but jaxsr barely uses JAX: ~1300 jnp.* calls that NumPy also provides, plus a short tail (jit, grad, lax.erf, .at[], random). webapp/py/jax_shim.py supplies that tail on NumPy and registers itself before `import jaxsr`, so the library needs no changes. The whole test suite runs through the shim (scripts/test_under_numpy.py) as a CI job, so a change that only breaks the browser path is caught at PR time. Measured in Pyodide: ~6 s to boot, 9-26 ms per fit. Validating the shim surfaced three pre-existing float64 bugs. They are not shim artifacts -- all three reproduce on real JAX with jax_enable_x64, which docs/guides/performance.md recommends: - basis.py: _safe_exp clipped to a hardcoded +/-500. At float32 that overflows to inf and the non-finite column filter drops the term; at float64 it gives a finite ~1e217 that survives selection and overflows Phi.T @ Phi. On real JAX in x64 this silently returned a garbage 2-term model instead of recovering exp(x0*x1). The bound is now derived from np.finfo(dtype), and only the upper side is clipped. - metrics.py: every information criterion returned +inf for mse <= 0, scoring a perfect fit as the worst possible model. Exact zeros are routine in float64 because selection.py computes MSE by a closed form that cancels, so the correct model was being rejected outright. MSE is now floored so the log-likelihood stays finite and fewer parameters still wins ties. - metrics.py: the IC functions did scalar math with jnp, so every AIC/BIC in jaxsr was computed in float32. Switched to `math`, which is both exact and dtype-independent. Also adds SymbolicRegressor.selection_path_, a public accessor for the candidates the search evaluated, so the app does not reach into a private attribute. Its docstring records a subtlety worth knowing: the path predates the pruning and constraint refitting in fit(), so path.best can carry terms the returned model does not. New tests: tests/test_metrics.py (closes a gap listed in CLAUDE.md), tests/test_numpy_shim.py, plus coverage for _safe_exp and selection_path_. 656 tests pass on both backends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a zero-install front end published alongside the docs on GitHub Pages, at
https://kitchingroup.cheme.cmu.edu/jaxsr/app/: upload a spreadsheet, assign columnroles, pick which function families the search may draw from, and get a ranked table of
candidate equations with diagnostics and exports. Everything runs client-side through
Pyodide, so no data leaves the browser and there is no server to operate.
How it runs without JAX
jaxlibis a compiled XLA wheel with no Emscripten build, so JAX cannot run underPyodide. JAXSR barely needs it: roughly 1300
jnp.*calls that NumPy also provides,plus a short tail of genuinely JAX-specific API (
jit,grad,lax.erf,.at[]index updates,
random).webapp/py/jax_shim.pysupplies that tail on top of NumPy and registers itself insys.modulesbeforeimport jaxsr, so the library itself is unmodified. Thatsubstitution is only trustworthy if JAXSR behaves the same on top of it, so the whole
test suite runs through the shim as a new CI job (
numpy-backend). A library changethat only breaks the browser path is caught at PR time.
Measured in real Pyodide: ~6 s to boot, 9-26 ms per fit, SymPy lazy-loaded in
377 ms and only when someone asks for LaTeX.
Three pre-existing bugs this surfaced
None of these are shim artifacts. All three reproduce on real JAX with
jax_enable_x64, whichdocs/guides/performance.mdactively recommends.basis.py—_safe_expclipped to a hardcoded ±500. At float32 that overflowsto
infand the non-finite column filter drops the term; at float64 it yields afinite ~1e217 that survives selection and overflows
Phi.T @ Phi. On real JAX in x64this silently returned a garbage 2-term model instead of recovering
exp(x0*x1). Thebound is now derived from
np.finfo(dtype), and only the upper side is clipped —large negative arguments underflowing to zero is a legitimate result, not an
out-of-domain one.
metrics.py— every information criterion returned+infformse <= 0, scoringa perfect fit as the worst possible model. Exact zeros are routine in float64
because
selection.pycomputes MSE by a closed form that cancels, so the correctmodel was being rejected outright. MSE is now floored so the log-likelihood stays
finite and fewer parameters still wins ties.
metrics.py— the IC functions did scalar math withjnp, so every AIC/BIC injaxsr was computed in float32. Switched to
math: exact, and dtype-independent.One API addition
SymbolicRegressor.selection_path_, a public accessor for the candidates the searchevaluated, so the app does not have to reach into
_selection_path. Its docstringrecords a subtlety worth knowing independently of this PR: the path predates the
pruning and constraint refitting in
fit(), sopath.bestcan carry terms thereturned model does not.
Verification
tests/test_metrics.pycloses a gap listed inCLAUDE.md; newtests/test_numpy_shim.pycovers the JAX contract and checks a fit through the shimagrees with the same fit on real JAX.
.xlsxwith an IDcolumn, a text column and failed runs, fits, and checks the rendered output. No
console errors.
key. It includes
stir_rpm, a variable drawn independently of the response — thewinning model correctly excludes it, all four true coefficients land inside their 95%
intervals, and the intercept is selected but flagged not significant, which is right
since the data has no constant term.
Worth knowing
docs.ymluploads the Pages artifact onpushes to
mainonly; this PR exercises the doc build and both test backends but doesnot publish.
browser session. Worth a manual look before merging.
jupyterandnbconvertare declared runtime dependencies of jaxsr but are importednowhere in
src/. They are whymicropiptried to pull in tornado and pyzmq. Harmlesshere since the app installs with
deps=False, but they make everypip install jaxsrheavier than it needs to be. Not addressed in this PR.
CLAUDE.mdchange also carries a pre-existing Crucible block that was alreadyuncommitted in the working tree.
🤖 Generated with Claude Code