CoursesHub includes dozens of interactive Python simulations that run directly in the browser via Pyodide (CPython compiled to WebAssembly). These simulations let students adjust parameters and see physics in action — from orbital mechanics to quantum wavefunctions. Last week, we completed a comprehensive audit and fixed 81 broken simulations across the entire site.
The Problem: NumPy 2.0 Breaking Changes
When Pyodide updated its bundled NumPy from 1.x to 2.0, several deprecated functions were removed. The most widespread breakage came from np.trapz(), which was renamed to np.trapezoid() in NumPy 2.0. This single change affected 38 simulations across courses in physics, chemistry, and mathematics.
The fix was straightforward — a global search-and-replace from np.trapz to np.trapezoid — but each simulation needed to be tested individually to confirm the output remained correct. We verified every plot, every numerical result, and every animation frame.
The Bigger Problem: SciPy Unavailability
A more serious issue affected 43 simulations that imported SciPy. While Pyodide supports SciPy in principle, loading it adds significant download time (tens of megabytes) and occasionally fails on slower connections. We decided to eliminate the SciPy dependency entirely by replacing every SciPy function call with a pure NumPy or custom implementation.
Replacing scipy.integrate.odeint
The most common SciPy import was scipy.integrate.odeint for solving ordinary differential equations. We replaced it with a custom fourth-order Runge–Kutta (RK4) integrator written in pure NumPy. The implementation is compact — about 15 lines — and produces results that agree with SciPy's adaptive solver to within plotting resolution for all our simulations.
The RK4 function takes the same signature as odeint (rk4(func, y0, t)), so the calling code needed minimal changes. For stiff systems where RK4 required very small timesteps, we added an adaptive step-size variant using Richardson extrapolation.
Replacing scipy.optimize.brentq
Several simulations used scipy.optimize.brentq for root-finding. We replaced it with a simple bisection method that converges reliably for all the continuous functions in our simulations. While Brent's method is faster in general, the bisection approach is transparent, easy to debug, and more than fast enough for interactive use.
Replacing scipy.special (Bessel functions, etc.)
A handful of simulations required Bessel functions (scipy.special.jn, scipy.special.yn) and the error function (scipy.special.erf). For the error function, we used the Abramowitz and Stegun rational approximation (accurate to 1.5 × 10&sup-7;). For Bessel functions, we implemented the Miller backward recurrence algorithm, which is stable and accurate for the orders and arguments used in our simulations.
Lessons for Scientific Python Maintainers
If you maintain scientific Python code, here are the takeaways:
- Pin your NumPy version or test against NumPy 2.0 before deploying. The deprecation warnings in NumPy 1.25+ gave advance notice, but they're easy to miss if you suppress warnings in production.
- Minimize heavy dependencies for browser-based code. SciPy is excellent in server environments, but for Pyodide/WASM targets, a lean NumPy-only approach reduces load times from 10+ seconds to under 2 seconds.
- Write your own RK4. For educational simulations, a custom ODE integrator is not only lighter but also pedagogically valuable — students can read and understand the integration algorithm itself.
- Test every simulation after dependency updates. Automated visual regression tests (comparing plot outputs pixel-by-pixel) would have caught these issues immediately.
Current Status
All 81 simulations are now working correctly with the latest Pyodide release. Page load times have improved significantly for the 43 simulations that previously loaded SciPy. If you encounter any remaining issues, please use the suggestion form at the bottom of any page to let us know.