Feat: Support the current JAX release (0.10.x), test Python 3.9–3.12, and drop jaxopt#211
Open
ChiahsinChu wants to merge 4 commits into
Open
Feat: Support the current JAX release (0.10.x), test Python 3.9–3.12, and drop jaxopt#211ChiahsinChu wants to merge 4 commits into
ChiahsinChu wants to merge 4 commits into
Conversation
DMFF called a handful of JAX/optax APIs that have since been removed, so a fresh install (which resolves the newest JAX) broke. All replacements below are valid across the whole supported range, so no version-sniffing is needed. - jax.tree_map -> jax.tree_util.tree_map (removed in jax 0.6.0) - isinstance(..., jnp.ndarray) -> jax.Array (jnp.ndarray is a deprecated alias) - optax._src.base -> public optax.EmptyState / optax.GradientTransformation jax's default PRNG impl changed (jax_threefry_partitionable now defaults to True), which silently shifted the reference energy in test_difftraj. Derive the initial velocities from numpy's seeded Generator instead, whose stream is stable across versions. The resulting energy and gradients agree between jax 0.4.35 and 0.10.2 to ~1e-13 relative. jaxopt is archived upstream and only the QEq module needs it, so it moves from install_requires to an optional [qeq] extra. It does still work on jax 0.10. Also lift the numpy<2 cap and the untrue rdkit==2022.03.2 pin, and extend CI to a matrix covering both ends of the supported JAX range. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI previously tested a single Python (3.9 before this branch, 3.11 after), while setup.py claimed 3.9-3.12. Expand the matrix so the metadata is actually backed by test runs. The Python and JAX axes are not independent: upstream JAX drops old Pythons over time, so each Python has a different newest installable JAX (3.9 -> 0.4.30, 3.10 -> 0.6.2, 3.11/3.12 -> current). A "latest JAX" job on 3.9 would silently resolve an old JAX, so the matrix pins each leg explicitly and the install docs spell out the table. Installing on 3.9 was broken outright: setup.py declared setup_requires= ['setuptools_scm'], which resolves via the legacy easy_install path and fetches setuptools_scm as an egg *without* its transitive dependencies. setuptools_scm 10.x moved Configuration into a separate vcs_versioning package, so the egg failed to import and metadata generation died. Declare the build deps in a PEP 517 pyproject.toml instead, which makes pip install them properly in an isolated build env, and drop setup_requires. Verified locally: full suite passes on py3.9/jax0.4.30, py3.10/jax0.6.2, py3.11/jax0.10.2 and py3.12/jax0.10.2 (test_dimer and 3 test_frontend tests fail in every env for lack of ambertools, both before and after this branch). The test_difftraj reference energy now holds across jax 0.4.30, 0.4.35, 0.6.2, 0.10.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pip` Two unrelated CI failures. 1. The OpenMM plugin build fails at `make PythonInstall` with "Error: Unknown directive '%factory'". OpenMM's bundled include/swig/OpenMMSwigHeaders.i does `%include "factory.i"` and `%factory(...)`, but SWIG removed the factory.i library in 4.1.0. CMakeLists.txt locates swig with FIND_PROGRAM, and ubuntu-latest now ships swig 4.2+, so the wrapper generation broke without any change on our side. Pin "swig<4.1" into the conda env and pass -DSWIG_EXECUTABLE explicitly so FIND_PROGRAM cannot pick the system one. Documented in the plugin README, since manual builds hit the same. 2. `python -c "import jax"` failed in a step right after `pip install .` reported success. That only happens when `pip` and `python` resolve to different interpreters -- a bare `pip` can come from the setup-python installation ahead of the conda env on PATH. Use `python -m pip` (and `python -m pytest`) throughout so the installing and importing interpreters are the same by construction, add `pip` explicitly to the conda env, and print `which python` plus a `pip list` on failure so a recurrence is self-diagnosing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jaxopt is archived (last release Nov 2023) and was DMFF's only use of it. QEq's inner charge optimizer used jaxopt.LBFGS on a projected gradient. Port it to optax.lbfgs (added in optax 0.2.3), which is maintained and already in the dependency tree, so jaxopt is dropped entirely rather than merely made optional. Adds a small `lbfgs_minimize` helper that mirrors the jaxopt.LBFGS contract used here -- minimize over the first argument, converge on the gradient L2 norm, and stay jittable via lax.while_loop. The projected gradient from get_proj_grad is used both as the descent direction and the stopping criterion; the Armijo backtracking line search only evaluates (never differentiates) the objective, so the charge-constraint projection is preserved. Numerically validated against the jaxopt implementation on a perturbed-charge case that actually exercises the LBFGS branch (test_qeq.py only hits the initial linear-solve branch): both converge to the same projected minimum (81.33959058 vs 81.33959057, final |q| identical to 1e-8). lbfgs_minimize also verified jittable and converging to machine precision on optax 0.2.4/jax 0.4.30 and optax 0.2.8/jax 0.10.2. Full test_admp (23 tests) passes on both. The qeq extra now points at optax>=0.2.3 instead of jaxopt; CI installs optax>=0.2.3 and no longer installs jaxopt. Co-Authored-By: Claude Opus 4.8 <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.
Support the current JAX release (0.10.x), test Python 3.9–3.12, and drop jaxopt
Motivation
DMFF calls a few JAX and
optaxAPIs that upstream has since removed. Becausesetup.pydeclares onlyjax>=0.4.1with no upper bound, a freshpip install .resolves the newest JAX and then fails. Anyone installing DMFF today on Python
3.11+ hits this.
This PR makes DMFF work on current JAX without dropping support for older
versions. The declared floor stays at
jax>=0.4.1: every replacement below isvalid across the whole range, so no version-sniffing or compat shim is needed.
What was broken (removed / private JAX & optax APIs)
jax.tree_map(removed in jax 0.6.0)jax.tree_util.tree_mapdmff/optimize.py,dmff/api/paramset.pyisinstance(x, jnp.ndarray)(deprecated alias)isinstance(x, jax.Array)dmff/utils.py,dmff/classical/vsite.py,dmff/torch_tools.pyoptax._src.base.{EmptyState,GradientTransformation,NO_PARAMS_MSG}(private)optax.EmptyState/optax.GradientTransformation; message inlineddmff/optimize.pyjax.tree_util.tree_mapandjax.Arrayexist as far back as jax 0.4.1, andoptax.EmptyState/optax.GradientTransformationare public in every optax from0.1.4 onward, so the floor is unchanged. (
jax.tree.mapwas deliberately notused — it requires jax>=0.4.25 and would have raised the floor.) Two dead
# from jax.config import configcomments (referencing a module deleted in jax0.4.27) were also removed.
test_difftrajand the PRNG defaulttest_difftrajfailed on new JAX with5401.08336042— which sits in the filetoday as a commented-out alternative expected value, suggesting this has been
papered over before by swapping the constant.
The cause is not DMFF. JAX flipped
jax_threefry_partitionablefromFalsetoTrueby default, sojax.random.normal(PRNGKey(0))yields different initialvelocities. Rather than pin a legacy flag JAX intends to remove, the test now
draws its initial velocities from
numpy.random.default_rng(seed), whose streamnumpy guarantees stable across versions. With that fixture the reference energy
5412.24832246and gradients reproduce identically on jax 0.4.30, 0.4.35,0.6.2 and 0.10.2 (~1e-13 relative) — a stronger check than the old constant,
which only pinned one version's RNG output.
Drop jaxopt: port QEq's L-BFGS to optax
jaxoptis archived upstream (last release 0.8.5, Nov 2023) and was DMFF's onlyuse of it — QEq's inner charge optimizer (
dmff/admp/qeq.py) usedjaxopt.LBFGSon a projected gradient. It is ported tooptax.lbfgs(added inoptax 0.2.3, already in the dependency tree), so jaxopt is removed entirely
rather than merely made optional.
A small
lbfgs_minimizehelper mirrors the jaxopt contract used here: minimizeover the first argument, converge on the gradient L2 norm, stay jittable via
lax.while_loop. QEq feeds L-BFGS a projected gradient to keep charges on theconstraint plane; that is preserved by using the projected gradient as both the
descent direction and the stopping criterion, while the Armijo backtracking line
search only evaluates (never differentiates) the objective, so it cannot
reintroduce the unprojected gradient.
Validated against the jaxopt implementation on a perturbed-charge case that
actually exercises the L-BFGS branch — note
test_qeq.pyonly ever hits theinitial linear-solve branch (
hessinv_stride=1), so the test suite alone doesnot cover this swap:
lbfgs_minimizewas additionally checked jittable and converging to machineprecision on optax 0.2.4 / jax 0.4.30 and optax 0.2.8 / jax 0.10.2, and
test_qeq.pypasses in an environment with no jaxopt installed at all. Theqeqextra now requiresoptax>=0.2.3(above the coreoptax>=0.1.4floor, soold-JAX non-QEq users aren't forced up).
Python 3.9–3.12
CI tested a single Python (3.9) while
setup.pysaidpython_requires="~=3.8".The matrix now covers 3.9–3.12 and the classifiers match.
The two axes are not independent: JAX drops old Pythons over time, so each Python
has a different newest installable JAX. Critically,
pip install jaxon 3.9silently resolves an old release rather than failing, so a naive
python × [oldest, latest]matrix would contain jobs that test nothing they claimto. Each leg is therefore pinned explicitly:
Documented in
docs/user_guide/2.installation.mdso users on 3.9/3.10 understandwhy they cannot get current JAX.
Installing on Python 3.9 was broken outright
Independently of JAX:
setup.pydeclaredsetup_requires=['setuptools_scm'],which resolves through the legacy
easy_installpath and fetches setuptools_scmas an egg without its transitive dependencies. setuptools_scm 10.x moved
Configurationinto a separatevcs_versioningpackage, so the egg fails toimport and metadata generation dies:
Fixed by declaring build dependencies in a PEP 517
pyproject.toml(
requires = ["setuptools>=64", "setuptools_scm>=8", "wheel"]), which makes pipinstall them correctly in an isolated build environment, and dropping
setup_requires. CI gainsfetch-depth: 0so setuptools_scm can derive aversion.
Two CI failures fixed along the way
Both are pre-existing breakage caused by the environment moving under us, not by
this PR's code changes.
test_openmm_dmff_plugin.ymlfails atmake PythonInstall:OpenMM's bundled
include/swig/OpenMMSwigHeaders.idoes%include "factory.i"then
%factory(...). SWIG removed thefactory.ilibrary in 4.1.0.backend/openmm_dmff_plugin/CMakeLists.txt:138locates the tool withFIND_PROGRAM(SWIG_EXECUTABLE swig), and theubuntu-latestrunner image nowships SWIG 4.2+, so wrapper generation broke with no change on our side. Fixed by
pinning
"swig<4.1"into the conda env (4.0.2 solves cleanly withopenmm=7.7/python=3.9/numpy=1.*) and passing-DSWIG_EXECUTABLE=${CONDA_PREFIX}/bin/swigso
FIND_PROGRAMcannot pick up the system one. Same note added to the pluginREADME, since manual builds hit this identically.
ut.ymlfails in the version-report step withModuleNotFoundError: No module named 'jax', immediately afterpip install .reported success. Thatcombination only occurs when
pipandpythonresolve to differentinterpreters: a bare
pipcan come from theactions/setup-pythoninstallationahead of the conda env on
PATH, so JAX is installed into one interpreter andimported from another. Fixed by driving everything through
python -m pip/python -m pytest, addingpipexplicitly to the conda env, and printingwhich pythonplus apip liston failure so any recurrence is self-diagnosing.Other dependency cleanups
requirements.txt: drop thenumpy<2cap (current JAX supports numpy 2; thesource has no numpy-2-removed API usage). Verified under numpy 2.4.
requirements.txt: drop the stalerdkit==2022.03.2pin — the suite passesagainst current rdkit, and
setup.pynever listed rdkit, so the pin only everapplied to people installing from this file.
requirements.txt: separate runtime deps from the mkdocs tooling; docs extrasare also exposed as
extras_require["docs"].docs/user_guide/2.installation.mdhad a duplicated dependency block; consolidated.Verification
Full test suite run on five Python × JAX combinations:
tests/test_dimer(6) and 3 oftests/test_frontendfail in every environment,before and after this branch, because
antechamber/sqm(ambertools) are absent.They are unrelated to this change.
Beyond the suite,
examples/water_fullpol/run.pyruns end to end on both jax0.4.35 and 0.10.2; across ~27k printed energies and gradients the max relative
difference is 1.9e-12 — float64 round-off from different XLA codegen, not a
semantic change. This matters because
tests/test_admp/test_gradDisp.pyenablesjax_debug_nans, so a clean ADMP suite plus a cleanwater_fullpolrun togetherare the real signal that autodiff still behaves.
Notes for reviewers
on conda-forge for 3.13 was not verified, so the classifiers stop at 3.12.
pytest tests/as a whole still fails at collection:tests/test_ase/test_energy.pyand
tests/test_sgnn/test_energy.pyshare a basename with no__init__.py.Pre-existing, not touched here;
Makefileand CI run per-module and are fine.import dmff.admp.qeqas the very first import raises acircular-import error; it works via the normal path (package init first), which
is why the suite is unaffected. Left as-is.