Correction (2026-08-11): the claim below that numba cannot run in a browser is out of date. numba now builds for emscripten-wasm32 via emscripten-forge and works under JupyterLite. See this comment for what still holds and what changes. The Phase 1 shim is still worth doing; its justification is different.
Goal
Make HARK importable and usable in the browser: Pyodide, JupyterLite, and anything else running CPython on WebAssembly. The target is that a student or referee can open a notebook link and run a HARK model with no local install.
This issue enumerates the specific changes required. Findings below were measured against main at e7909203, not assumed.
The blocker is numba, and today it is total
HARK.core imports numba transitively, so HARK does not merely run slowly in Pyodide. It does not import at all.
import HARK.core -> ImportError: No module named 'numba'
Three modules put it on the core path:
| Module |
How numba enters |
HARK/utilities.py |
import numba |
HARK/simulator.py |
from numba import njit |
HARK/SSJutils.py |
from numba import njit |
numba cannot be fixed upstream on any useful timescale. It requires llvmlite, which requires LLVM itself compiled to WebAssembly. The Pyodide request has been open since 2020 (pyodide/pyodide#621), the one attempt to build it was closed (pyodide/pyodide#618), and numba's own WASM-target issue (numba/numba#3284) is unshipped. This is not something to wait for.
The same is true of JAX, for a different reason: jaxlib is a native XLA binary with no WebAssembly build. Any accelerator that ships compiled artifacts is out. What does work in Pyodide is NumPy, SciPy, pandas, matplotlib, xarray and the rest of the standard scientific stack, which means vectorized NumPy is the accelerator available in the browser.
What we are lucky about
The dependency situation is much better than it looks:
- HARK uses exactly two numba APIs:
njit (54 sites) and prange (3 sites). There is no typed.List, no guvectorize, no vectorize, and no numba.types objects. cache=True (33 sites) and parallel=True are kwargs that a fallback can ignore.
interpolation (econforge) and quantecon both pull numba, but neither is on the core import path. They are confined to econforgeinterp.py, dcegm.py, and ConsIndShockModelFast.py.
joblib and xarray are on the core path and both work in Pyodide.
So the core path has one blocker, not a web of them.
Phase 1: make numba optional so HARK imports
Add a single compatibility module, HARK/_numba.py, holding a guarded import with a no-op fallback:
try:
from numba import njit, prange
HAS_NUMBA = True
except ImportError:
HAS_NUMBA = False
prange = range
def njit(*args, **kwargs):
if len(args) == 1 and callable(args[0]) and not kwargs:
return args[0] # bare @njit
return lambda func: func # @njit(...) and @njit("sig", cache=True)
njit is three calling conventions under one name (decorator, decorator factory, factory with a positional signature string), so the callable(args[0]) and not kwargs guard is load-bearing. A naive def njit(f): return f silently breaks @njit(cache=True) by returning True as the function. This prototype has been verified against all four forms HARK uses, with numba absent: bare, cache=True, njit("float64(float64[:])", cache=True), and parallel=True with prange.
prange = range is a correct fallback. numba's prange is range plus a parallelism hint, and the loops in question are order independent, so serial execution is correct and only slower.
Then change the eight importing modules to import from HARK._numba instead of numba:
| File |
@njit |
prange / parallel |
explicit sigs |
HARK/ConsumptionSaving/ConsIndShockModelFast.py |
14 |
4 |
0 |
HARK/numba_tools.py |
10 |
0 |
0 |
HARK/mat_methods.py |
8 |
0 |
0 |
HARK/simulator.py |
8 |
0 |
0 |
HARK/SSJutils.py |
4 |
0 |
0 |
HARK/utilities.py |
4 |
5 |
0 |
HARK/dcegm.py |
3 |
0 |
2 |
HARK/interpolation.py |
3 |
0 |
0 |
Finally, move numba out of requirements/base.txt into an extra, so a default install is browser compatible and pip install econ-ark[fast] restores compiled speed.
This phase is mechanical and changes no numerical behavior when numba is present.
Phase 2: prove the fallback in CI, or it will rot
With numba installed, CI never executes the fallback path. A numba-only semantic (integer overflow, division behavior, or a loop that is only correct because it was compiled) can diverge silently and nobody finds out until a browser user does.
Add one CI job that installs without the fast extra and runs the suite with numba absent. This is the difference between numba being genuinely optional and being nominally optional.
Phase 3: decide what "usable in the browser" means
Phase 1 makes HARK import and produce correct answers. It does not make it fast. Every @njit path becomes interpreted Python, and the two parallel=True loops in HARK/utilities.py iterate over dist_mGrid x dist_pGrid, which in pure Python is likely minutes rather than seconds.
Two defensible targets, and they cost very different amounts:
- Importable and usable for teaching-scale models. Phase 1 plus Phase 2 gets this. Small
AgentCount, short T_sim, coarse grids. Good enough for a course notebook or a referee reproducing a figure.
- Performant in the browser. Requires rewriting the hot loops as vectorized NumPy, which Pyodide runs at native-ish speed. This is real work and should be scoped from measurement, not guesswork: profile the pure-Python path first, then rewrite only what dominates. The
utilities.py prange loops are the obvious first candidates.
Vectorized NumPy is worth preferring over numba even outside the browser, since it removes a compiled dependency from the hot path entirely.
Phase 4: the three modules that stay unavailable
econforgeinterp.py, dcegm.py, and ConsIndShockModelFast.py import interpolation and quantecon, both of which pull numba themselves. Making HARK's own numba use optional does not help these, because the dependency is external.
Options, in rough order of appeal:
- Leave them unavailable in the browser and raise a clear error on import. Core HARK still works, and these are specialist modules.
- Replace
interpolation.interp with a NumPy or SciPy equivalent where the call is simple enough. dcegm.py uses only interp.
ConsIndShockModelFast.py is the numba-accelerated variant of a model that already has a pure-Python implementation, so the browser can simply use the non-Fast path.
quantecon.optimize.newton_secant in ConsIndShockModelFast.py is a single scalar root finder and has a straightforward SciPy replacement if that module is ever wanted in the browser.
Phase 5: parallelism
HARK/core.py and HARK/estimation.py use joblib.Parallel. joblib imports fine under Pyodide but there are no worker processes in a browser tab, so n_jobs must resolve to 1. Confirm this degrades gracefully rather than hanging or raising.
Non-goals
- Getting numba or JAX to run in the browser. Neither is achievable, and this issue does not depend on either.
- Removing numba as an option for local use. It stays, as an extra, and remains the fast path off the browser.
- Adding JAX. Separate discussion, and it has the same WebAssembly problem, so it would work against this goal.
Open questions
- Which of the two Phase 3 targets are we buying? That decides whether this is a small change or a sustained vectorization effort.
- Should the default install be browser compatible (numba in an extra) or should the browser build be the special case (numba stays in base, browser installs a subset)? Putting numba in an extra makes the browser path the default and is the stronger statement, but it changes what
pip install econ-ark gives existing users.
- Do we want a pinned JupyterLite deployment in CI so browser compatibility is continuously tested rather than asserted?
Verification notes
Everything above was measured on main at e7909203:
- numba absence was simulated with a
sys.meta_path finder that raises on numba*, with a rejection test confirming the blocker actually blocks. An earlier attempt using the legacy find_module protocol silently did nothing on Python 3.13 and produced a false pass.
- API usage counts come from a grep sweep across
HARK/.
- Core-path membership was determined by importing
HARK.core and inspecting sys.modules.
Goal
Make HARK importable and usable in the browser: Pyodide, JupyterLite, and anything else running CPython on WebAssembly. The target is that a student or referee can open a notebook link and run a HARK model with no local install.
This issue enumerates the specific changes required. Findings below were measured against
mainate7909203, not assumed.The blocker is numba, and today it is total
HARK.coreimports numba transitively, so HARK does not merely run slowly in Pyodide. It does not import at all.Three modules put it on the core path:
HARK/utilities.pyimport numbaHARK/simulator.pyfrom numba import njitHARK/SSJutils.pyfrom numba import njitnumba cannot be fixed upstream on any useful timescale. It requires
llvmlite, which requires LLVM itself compiled to WebAssembly. The Pyodide request has been open since 2020 (pyodide/pyodide#621), the one attempt to build it was closed (pyodide/pyodide#618), and numba's own WASM-target issue (numba/numba#3284) is unshipped. This is not something to wait for.The same is true of JAX, for a different reason:
jaxlibis a native XLA binary with no WebAssembly build. Any accelerator that ships compiled artifacts is out. What does work in Pyodide is NumPy, SciPy, pandas, matplotlib, xarray and the rest of the standard scientific stack, which means vectorized NumPy is the accelerator available in the browser.What we are lucky about
The dependency situation is much better than it looks:
njit(54 sites) andprange(3 sites). There is notyped.List, noguvectorize, novectorize, and nonumba.typesobjects.cache=True(33 sites) andparallel=Trueare kwargs that a fallback can ignore.interpolation(econforge) andquanteconboth pull numba, but neither is on the core import path. They are confined toeconforgeinterp.py,dcegm.py, andConsIndShockModelFast.py.joblibandxarrayare on the core path and both work in Pyodide.So the core path has one blocker, not a web of them.
Phase 1: make numba optional so HARK imports
Add a single compatibility module,
HARK/_numba.py, holding a guarded import with a no-op fallback:njitis three calling conventions under one name (decorator, decorator factory, factory with a positional signature string), so thecallable(args[0]) and not kwargsguard is load-bearing. A naivedef njit(f): return fsilently breaks@njit(cache=True)by returningTrueas the function. This prototype has been verified against all four forms HARK uses, with numba absent: bare,cache=True,njit("float64(float64[:])", cache=True), andparallel=Truewithprange.prange = rangeis a correct fallback. numba'sprangeisrangeplus a parallelism hint, and the loops in question are order independent, so serial execution is correct and only slower.Then change the eight importing modules to import from
HARK._numbainstead of numba:@njitprange/parallelHARK/ConsumptionSaving/ConsIndShockModelFast.pyHARK/numba_tools.pyHARK/mat_methods.pyHARK/simulator.pyHARK/SSJutils.pyHARK/utilities.pyHARK/dcegm.pyHARK/interpolation.pyFinally, move
numbaout ofrequirements/base.txtinto an extra, so a default install is browser compatible andpip install econ-ark[fast]restores compiled speed.This phase is mechanical and changes no numerical behavior when numba is present.
Phase 2: prove the fallback in CI, or it will rot
With numba installed, CI never executes the fallback path. A numba-only semantic (integer overflow, division behavior, or a loop that is only correct because it was compiled) can diverge silently and nobody finds out until a browser user does.
Add one CI job that installs without the
fastextra and runs the suite with numba absent. This is the difference between numba being genuinely optional and being nominally optional.Phase 3: decide what "usable in the browser" means
Phase 1 makes HARK import and produce correct answers. It does not make it fast. Every
@njitpath becomes interpreted Python, and the twoparallel=Trueloops inHARK/utilities.pyiterate overdist_mGrid x dist_pGrid, which in pure Python is likely minutes rather than seconds.Two defensible targets, and they cost very different amounts:
AgentCount, shortT_sim, coarse grids. Good enough for a course notebook or a referee reproducing a figure.utilities.pyprangeloops are the obvious first candidates.Vectorized NumPy is worth preferring over numba even outside the browser, since it removes a compiled dependency from the hot path entirely.
Phase 4: the three modules that stay unavailable
econforgeinterp.py,dcegm.py, andConsIndShockModelFast.pyimportinterpolationandquantecon, both of which pull numba themselves. Making HARK's own numba use optional does not help these, because the dependency is external.Options, in rough order of appeal:
interpolation.interpwith a NumPy or SciPy equivalent where the call is simple enough.dcegm.pyuses onlyinterp.ConsIndShockModelFast.pyis the numba-accelerated variant of a model that already has a pure-Python implementation, so the browser can simply use the non-Fast path.quantecon.optimize.newton_secantinConsIndShockModelFast.pyis a single scalar root finder and has a straightforward SciPy replacement if that module is ever wanted in the browser.Phase 5: parallelism
HARK/core.pyandHARK/estimation.pyusejoblib.Parallel. joblib imports fine under Pyodide but there are no worker processes in a browser tab, son_jobsmust resolve to 1. Confirm this degrades gracefully rather than hanging or raising.Non-goals
Open questions
pip install econ-arkgives existing users.Verification notes
Everything above was measured on
mainate7909203:sys.meta_pathfinder that raises onnumba*, with a rejection test confirming the blocker actually blocks. An earlier attempt using the legacyfind_moduleprotocol silently did nothing on Python 3.13 and produced a false pass.HARK/.HARK.coreand inspectingsys.modules.