Make WhyNot run on current Python and dependencies - #38
Merged
Conversation
The package no longer installed or imported: gym 0.21 cannot be built by modern setuptools, and a single unavailable simulator dependency took down `import whynot` entirely. Dependencies: - Port whynot.gym from OpenAI Gym to Gymnasium. Gym was abandoned in 2022 and does not support NumPy 2. Environments now use the 5-tuple step API and the keyword-only reset. Reaching the end of the simulation horizon is reported as truncation rather than termination, since it is a time limit and not an absorbing state. EnvRegistry has no Gymnasium equivalent and is reimplemented. - Replace the abandoned py_mini_racer with the maintained mini-racer fork, which ships aarch64 wheels. Drop the __del__ override that worked around a deadlock in py_mini_racer 0.6; the fork reworked context lifetimes and suppressing cleanup now leaks a V8 context per simulation. - Keep mesa pinned at 0.8.7, which still runs on current interpreters, and pass Python ints as model seeds. Since 3.11, random.seed rejects numpy integers. - Replace Index.get_loc(method="nearest"), removed in pandas 2, with get_indexer. - Drop the dead `import distutils.version`, removed in Python 3.12. Packaging and CI: - Replace setup.py with pyproject.toml and drop the python_requires ceiling. - Replace Travis, dead for open source, with GitHub Actions. - Prefer an IPOPT on PATH over the bundled x86-64 binaries, which cannot run on Apple Silicon, and skip the DICE tests when no solver is available. Structural: - Import simulators lazily so an unavailable dependency affects only the simulator that needs it. Environments register as a side effect of those imports, so the registry forces them when an id is looked up. - Import traceable_numpy explicitly rather than relying on a simulator to import it first, which causal_graphs depends on. Two bugs predating the dependency rot, both from a3caaf5: - Restore the Credit dataset as the default state. It had been replaced with empty arrays to fix a mutable default, which broke Credit-v0. - Fix test_basestate, which asserted a three-field count against a four-field class, and rewrite its shallow-copy check to not rely on ragged arrays that NumPy 2 rejects. Co-Authored-By: Claude <noreply@anthropic.com>
The pre-commit config pinned black 19.10b0 from the ambv/black repository, which no longer resolves and is incompatible with modern click. Moving to current black reformats the tree, almost entirely by adding the blank line after a module docstring that black 24 introduced. Formatting only, no behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
Rewrite the environment examples in the README and docs for the 5-tuple step and the keyword-only reset, and point the prose at Gymnasium rather than the retired OpenAI Gym. Every snippet in the README and quickstart was executed against the ported environments. Also: - Set bibtex_bibfiles, required since sphinxcontrib-bibtex 2.0. The docs did not build without it. - Drop the removed seed method from the ODEEnvBuilder autodoc members. - Point the build badge at GitHub Actions instead of Travis. - Document that DICE needs an IPOPT on PATH where the bundled x86-64 binaries cannot run. Co-Authored-By: Claude <noreply@anthropic.com>
Migrate the environment calls to the 5-tuple step and the keyword-only reset, and reach through .unwrapped for config and initial_state, since Gymnasium 1.0 removed attribute forwarding on wrappers. Executed end to end and passing: performative_prediction, walkthrough, zika_simulator. hiv_simulator was still running when this was committed. world3_simulator fails partway through a 400 step rollout with a JS ReferenceError, "nan is not defined": the simulator interpolates state values into JavaScript source, and a Python nan is not a JS literal. The API migration in this commit is independent of that bug, which is in the simulator rather than the notebook. Co-Authored-By: Claude <noreply@anthropic.com>
The last cell calls wn.hiv.State.variable_names(), but the notebook only ever imported whynot.gym, so wn was never bound. Predates the Gymnasium migration: the same NameError is reachable on master. Found by executing the notebook. It failed only at the final plotting cell, after the training loop completed, so the migrated environment calls are fine. Co-Authored-By: Claude <noreply@anthropic.com>
plot_sample_trajectory takes env as its first argument, but the notebook omitted it, so policies bound to env and max_episode_length went unfilled. Predates the Gymnasium migration: the same TypeError is reachable on master. This is the second bug in that one cell, after the missing whynot import in 5d8ca13, which suggests it was never run in its committed form. Verified by executing the cell against a trivial policy rather than repeating the 300 iteration training loop. Co-Authored-By: Claude <noreply@anthropic.com>
State and config values reach the world3 engine by being interpolated into JavaScript source. Python spells its non-finite floats nan, inf and -inf, none of which are JavaScript literals, so a non-finite value produced a ReferenceError from deep inside the engine rather than anything diagnosable. Render numbers through to_js_number, which spells them NaN, Infinity and -Infinity. Finite values are unaffected: f-string formatting of a float already used repr. Also raise a clear ValueError when asked to resume from a non-finite state. World3 cannot be meaningfully resumed from one, and emitting NaN into the engine would trade a crash for silently non-finite observations. Surfaced by world3_simulator.ipynb, whose rollout diverges. The divergence itself is a separate, pre-existing problem and is not addressed here. Co-Authored-By: Claude <noreply@anthropic.com>
world3-v0 restarted the engine from the twelve stocks on every step. World3
carries internal state beyond those stocks, in its smoothed and delayed
quantities, and that state is built by fastRun's hundred iteration warmup
rather than derived from the stocks, so restarting discarded it.
The resulting trajectories were not world3. Measured against a continuous run
at the environment's own delta_t, and taking action 4, the action that changes
nothing:
deviation after 10 steps 32%
deviation after 200 steps 55x
nonrenewable_resources negative from around step 170
observations in obs_space no, Box declares low=0
random rollouts completing 1 of 8, the rest reaching a non-finite state
The engine turns out to support incremental stepping: timeStep advances by one
delta_t without reinitialising. So keep one engine alive for the episode and
advance it in place, applying each action by setting a parameter's before and
after values, which takes effect whatever the current time is.
Against the same reference, under the same action, deviation is now exactly
zero over all 200 steps, observations stay inside the observation space, and
8 of 8 random rollouts complete.
This makes world3-v0 a stateful environment, so it no longer builds on
ODEEnvBuilder, whose contract is to re-simulate from state. That contract is
exact for the four ODE simulators and only ever wrong for world3. The
simulate() path is untouched: restarting from a true initial state is what it
does, and its resources bookkeeping is correct there.
Co-Authored-By: Claude <noreply@anthropic.com>
The environment silently produced trajectories that were not world3, with nothing failing to say so. Pin the property that broke: - start_engine plus step_engine matches simulate() exactly, so the smoothed and delayed quantities survive being advanced in place. - world3-v0 under action 4, which changes no parameter, follows an unintervened run exactly and stays inside its observation space. Both count the timesteps they compare and assert the count, so neither can pass by matching nothing. Confirmed that the second fails when step is reverted to rebuilding the engine from the stocks. Co-Authored-By: Claude <noreply@anthropic.com>
The custom environment guide presents ODEEnvBuilder as wrapping an arbitrary simulator. It advances one by re-simulating from its current state, which is exact for the ODE simulators and wrong for a simulator carrying internal state its published state does not capture. Point at world3 as the worked example, since that is exactly how world3-v0 came to produce trajectories that were not world3. Co-Authored-By: Claude <noreply@anthropic.com>
requires-python said >=3.9, which the dependencies cannot honour: current numpy and scipy require 3.12, pandas and scikit-learn 3.11, gymnasium and pyomo 3.10. On anything below 3.12 pip would not fail, it would quietly backtrack to an older, untested scientific stack. Raise the floor to 3.12 and add the version classifiers. The CI matrix had the same problem, listing 3.10. Run 3.12, 3.13 and 3.14 instead. All three were run locally first, 92 tests passing on each, resolving to the same dependency versions, so the matrix reflects something observed rather than assumed. Install IPOPT on the macOS runners. The binaries bundled for DICE are x86-64 only, so without it the three DICE tests would skip there rather than run. Co-Authored-By: Claude <noreply@anthropic.com>
The Gymnasium migration changes step and reset for every caller, the Python floor moves from 3.8 to 3.12, setup.py is gone, and world3-v0 no longer produces the trajectories it used to. None of that was signalled: the version had stayed at 0.12.0, so anyone pinning whynot would have taken the breakage without a version to notice it by. Chose 0.13.0 rather than 1.0.0. Breaking changes in the minor position are the convention below 1.0, and releasing 1.0 would make a stability promise that is the maintainers' to make, not this change's. Add a changelog, which the project did not have. It starts here and does not reconstruct earlier releases. The migration example in it was executed rather than written from memory. Co-Authored-By: Claude <noreply@anthropic.com>
Cutting things I added that nothing needed: - EnvRegistry.__contains__ and __repr__: written on the assumption something would want them. Nothing does. - build_world3_env: the registry calls entry_point(**kwargs) and World3Env's constructor already takes those keywords, so the wrapper only added a hop. - ACTION_PARAMETERS: named the two keys that intervention.updates already holds, and would have gone stale if get_intervention changed. Iterate the updates instead. - World3Env.metadata: identical to Gymnasium's default. - IPOPT_INSTALL_HINT: a module level constant for one error message. - __all__ in the gym.utils shim, which the original did not have and which a one symbol re-export does not need. Also trim the World3Env docstring. It recounted the measurements behind the change, which belong to the commit that made it and to the changelog; the class needs the design rationale, not the forensics. No behaviour change: world3-v0 still matches a continuous run exactly, config still routes through make, 92 tests pass. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Aug 8, 2026
Merged
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.
Moritz's note: Claude update to make WhyNot run again.
Gets WhyNot installing and running again on current Python.
gym==0.21.0couldnot be built by modern setuptools, and eager simulator imports meant that one
dependency broke
import whynotentirely.Released as 0.13.0. Full detail in
CHANGELOG.md.Two behaviour changes that the diff does not make obvious:
world3-v0produces different trajectories. It had been restarting theengine from the twelve stocks each step, discarding the smoothed and delayed
state built by the warmup. Under the no-op action it deviated 55x from a
continuous run by step 200 and drove resources negative; it now matches
exactly. Old results do not reproduce against it. See
8a140eb.require it. Below that pip would silently resolve an older untested stack
rather than fail.
Also fixes five bugs that predate the dependency rot, including
Credit-v0being unconstructible since June 2023.
Verified on macOS arm64: 92 tests passing on 3.12, 3.13 and 3.14, clean install,
docs building, all five example notebooks executing. Linux is untested — in
particular the bundled x86-64 IPOPT path that
ubuntu-latestwill take. This PRis the first CI run.
4f7384bis a black reformat, formatting only, 61 files — skip it.