Skip to content

Make WhyNot run on current Python and dependencies - #38

Merged
mrtzh merged 13 commits into
masterfrom
modernize-2026
Aug 8, 2026
Merged

Make WhyNot run on current Python and dependencies#38
mrtzh merged 13 commits into
masterfrom
modernize-2026

Conversation

@mrtzh

@mrtzh mrtzh commented Aug 8, 2026

Copy link
Copy Markdown
Member

Moritz's note: Claude update to make WhyNot run again.

Gets WhyNot installing and running again on current Python. gym==0.21.0 could
not be built by modern setuptools, and eager simulator imports meant that one
dependency broke import whynot entirely.

Released as 0.13.0. Full detail in CHANGELOG.md.

Two behaviour changes that the diff does not make obvious:

  • world3-v0 produces different trajectories. It had been restarting the
    engine 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.
  • Python 3.12+ required, up from 3.8, because current numpy and scipy
    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-v0
being 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-latest will take. This PR
is the first CI run.

4f7384b is a black reformat, formatting only, 61 files — skip it.

MH and others added 13 commits August 3, 2026 17:05
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>
@mrtzh mrtzh self-assigned this Aug 8, 2026
@mrtzh
mrtzh merged commit 680ad41 into master Aug 8, 2026
7 checks passed
This was referenced Aug 8, 2026
@mrtzh
mrtzh deleted the modernize-2026 branch August 11, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant