-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
WASM: JupyterLite environment config and browser smoke suite #938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+486
−0
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
41948c2
WASM: add JupyterLite environment config and browser smoke suite (#928)
kp992 97e6d21
CI: add native smoke run for ci/wasm/smoke_test.py
kp992 c96ff33
CI: add Playwright-based Emscripten/JupyterLite WASM smoke job
kp992 5daae94
CI: fix missing quantecon install and micromamba for WASM job
kp992 a18c110
CI: scope default pytest collection to quantecon/
mmcky 3baf546
CI: move Emscripten runner job to a follow-up under #933
mmcky 564dc8d
TST: drive JupyterLite cells by completion sentinel, not kernel status
mmcky 1e0c598
MAINT: drop unused pytest from wasm env; soften smoke_test docstring
mmcky 3508851
Merge branch 'main' into wasm-phase0-smoke-suite
mmcky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| name: WASM smoke suite | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
| branches: | ||
| - main | ||
|
|
||
| jobs: | ||
| # ------------------------------------------------------------------ | ||
| # Native Python gate: ensures the smoke suite itself is correct and | ||
| # all tests pass against this repo's source. The Emscripten / | ||
| # JupyterLite job that consumes the same suite lands with issue #933. | ||
| # ------------------------------------------------------------------ | ||
| native: | ||
| name: Smoke suite (native) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
|
|
||
| - name: Cache conda | ||
| uses: actions/cache@v6 | ||
| env: | ||
| CACHE_NUMBER: 0 | ||
| with: | ||
| path: ~/conda_pkgs_dir | ||
| key: ${{ runner.os }}-3.13-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} | ||
|
|
||
| - uses: conda-incubator/setup-miniconda@v4 | ||
| with: | ||
| auto-update-conda: true | ||
| miniforge-version: latest | ||
| environment-file: environment.yml | ||
| python-version: "3.13" | ||
| auto-activate-base: false | ||
| use-only-tar-bz2: true | ||
| activate-environment: qe | ||
|
|
||
| - name: Install quantecon from source | ||
| shell: bash -l {0} | ||
| run: pip install -e . --no-deps | ||
|
|
||
| - name: Run smoke suite (native) | ||
| shell: bash -l {0} | ||
| run: pytest ci/wasm/smoke_test.py -v |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| name: qe-lite | ||
| channels: | ||
| - https://prefix.dev/emscripten-forge-4x | ||
| - https://prefix.dev/conda-forge | ||
| dependencies: | ||
| - xeus-python | ||
| - numba | ||
| - quantecon | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| """ | ||
| Browser smoke suite for QuantEcon.py on the JupyterLite xeus-python kernel. | ||
|
|
||
| One representative function per Numba feature class used in the library. | ||
| Run natively with pytest to validate the suite itself; the WASM CI job | ||
| (issue #933) will consume the same file once it is wired to ship it into | ||
| the JupyterLite site. Until then the Emscripten-only branches below | ||
| (IS_EMSCRIPTEN, xfail) are inert but document the expected behaviour. | ||
|
|
||
| Findings from a WASM run should be recorded in issue #928 as a results | ||
| table: function name -> works / fails / notes. | ||
| """ | ||
| import sys | ||
| import time | ||
| import warnings | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| from numba import njit | ||
|
|
||
| IS_EMSCRIPTEN = sys.platform == "emscripten" | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Jitted helpers required by optimize tests (must be at module scope) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| @njit | ||
| def _rosenbrock(x): | ||
| return -(100 * (x[1] - x[0] ** 2) ** 2 + (1 - x[0]) ** 2) | ||
|
|
||
|
|
||
| @njit | ||
| def _parabola(x): | ||
| return -(x + 2.0) ** 2 + 1.0 | ||
|
|
||
|
|
||
| @njit | ||
| def _cubic(x): | ||
| return x ** 3 - 1.0 | ||
|
|
||
|
|
||
| @njit | ||
| def _cubic_prime(x): | ||
| return 3.0 * x ** 2 | ||
|
|
||
|
|
||
| @njit | ||
| def _linalg_solve(A, b): | ||
| return np.linalg.solve(A, b) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 1. Import timing — cold vs warm cache (feeds issue #930) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_import_time(): | ||
| t0 = time.perf_counter() | ||
| import quantecon # noqa: F401 | ||
| elapsed = time.perf_counter() - t0 | ||
| # 30 s is generous for a cold WASM JIT cache; native should be <1 s. | ||
| assert elapsed < 30, f"import took {elapsed:.1f} s" | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 2. Plain lazy @njit — tauchen and rouwenhorst | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_tauchen(): | ||
| import quantecon as qe | ||
| mc = qe.tauchen(5, 0.9, 0.1) | ||
| assert mc.P.shape == (5, 5) | ||
| assert np.allclose(mc.P.sum(axis=1), 1.0) | ||
|
|
||
|
|
||
| def test_rouwenhorst(): | ||
| import quantecon as qe | ||
| mc = qe.rouwenhorst(5, 0.9, 0.1) | ||
| assert mc.P.shape == (5, 5) | ||
| assert np.allclose(mc.P.sum(axis=1), 1.0) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 3. MarkovChain.simulate — jitted simulation with NRT-allocated arrays | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_markov_simulate(): | ||
| import quantecon as qe | ||
| mc = qe.tauchen(5, 0.9, 0.1) | ||
| sim = mc.simulate_indices(ts_length=200, init=0, random_state=42) | ||
| assert len(sim) == 200 | ||
| assert np.all((sim >= 0) & (sim < 5)) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 4. probvec — parallel guvectorize; on Emscripten patch 0007 falls back | ||
| # to 'cpu' target silently, so the result must still be correct | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_probvec(): | ||
| import quantecon as qe | ||
| result = qe.random.probvec(4, 3, random_state=42) | ||
| assert result.shape == (4, 3) | ||
| assert np.allclose(result.sum(axis=1), 1.0) | ||
| assert np.all(result >= 0) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 5. sample_without_replacement — eager guvectorize with explicit i8 sig | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_sample_without_replacement(): | ||
| import quantecon as qe | ||
| result = qe.random.sample_without_replacement(10, 4, random_state=42) | ||
| assert len(result) == 4 | ||
| assert len(set(result.tolist())) == 4 | ||
| assert np.all((result >= 0) & (result < 10)) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 6. Optimize: nelder_mead, brent_max, newton | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_nelder_mead(): | ||
| from quantecon.optimize import nelder_mead | ||
| result = nelder_mead(_rosenbrock, np.array([-1.0, 1.0])) | ||
| assert result.success | ||
| assert np.allclose(result.x, [1.0, 1.0], atol=1e-4) | ||
|
|
||
|
|
||
| def test_brent_max(): | ||
| from quantecon.optimize import brent_max | ||
| xf, fval, info = brent_max(_parabola, -4.0, 0.0) | ||
| assert abs(xf - (-2.0)) < 1e-4 | ||
| assert abs(fval - 1.0) < 1e-4 | ||
|
|
||
|
|
||
| def test_newton(): | ||
| from quantecon.optimize import newton | ||
| result = newton(_cubic, 2.0, _cubic_prime) | ||
| assert abs(result.root - 1.0) < 1e-6 | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 7. game_theory.lemke_howson | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_lemke_howson(): | ||
| import quantecon as qe | ||
| bimatrix = [[(3, 3), (3, 2)], | ||
| [(2, 2), (5, 6)], | ||
| [(0, 3), (6, 1)]] | ||
| g = qe.game_theory.NormalFormGame(bimatrix) | ||
| NE = qe.game_theory.lemke_howson(g, init_pivot=0) | ||
| assert len(NE) == 2 | ||
| assert np.allclose(NE[0].sum(), 1.0, atol=1e-6) | ||
| assert np.allclose(NE[1].sum(), 1.0, atol=1e-6) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 8. game_theory.vertex_enumeration — exercises numba.typed.Dict | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_vertex_enumeration(): | ||
| import quantecon as qe | ||
| bimatrix = [[(3, 3), (3, 2)], | ||
| [(2, 2), (5, 6)], | ||
| [(0, 3), (6, 1)]] | ||
| g = qe.game_theory.NormalFormGame(bimatrix) | ||
| NEs = qe.game_theory.vertex_enumeration(g) | ||
| assert len(NEs) == 3 | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 9. np.linalg.solve inside @njit — isolates the _LAPACK mechanism (#927) | ||
| # independently of QuantEcon's own overload. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_np_linalg_solve_jit(): | ||
| A = np.array([[3.0, 2.0], [1.0, -1.0]]) | ||
| b = np.array([8.0, 1.0]) | ||
| x = _linalg_solve(A, b) | ||
| assert np.allclose(x, np.linalg.solve(A, b)) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 10. game_theory.support_enumeration — end-to-end _LAPACK test (#927) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_support_enumeration(): | ||
| import quantecon as qe | ||
| bimatrix = [[(3, 3), (3, 2)], | ||
| [(2, 2), (5, 6)], | ||
| [(0, 3), (6, 1)]] | ||
| g = qe.game_theory.NormalFormGame(bimatrix) | ||
| NEs = qe.game_theory.support_enumeration(g) | ||
| assert len(NEs) == 3 | ||
| assert np.allclose(NEs[0][0], [1.0, 0.0, 0.0], atol=1e-6) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 11. gini_coefficient — @njit(parallel=True) + prange; expected to fail | ||
| # at first call on Emscripten because the ParallelAccelerator pass is | ||
| # not supported (issue #926). | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| @pytest.mark.xfail( | ||
| IS_EMSCRIPTEN, | ||
| reason="@njit(parallel=True) not supported on Emscripten (#926)", | ||
| strict=True, | ||
| ) | ||
| def test_gini_coefficient(): | ||
| import quantecon as qe | ||
| y = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) | ||
| g = qe.gini_coefficient(y) | ||
| assert 0.0 < g < 1.0 | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 12. simplex_grid — 32-bit intp boundary behaviour on wasm32 (#929) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_simplex_grid(): | ||
| import quantecon as qe | ||
| grid = qe.simplex_grid(3, 4) | ||
| # shape: (L, m) where L = C(4+3-1, 3-1) = 15 | ||
| assert grid.shape == (15, 3) | ||
| assert np.all(grid.sum(axis=1) == 4) | ||
| assert np.all(grid >= 0) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 13. searchsorted — objmode() shim (deprecated helper) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def test_searchsorted(): | ||
| from quantecon.util.array import searchsorted | ||
| a = np.array([0.2, 0.4, 1.0]) | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("ignore", DeprecationWarning) | ||
| assert searchsorted(a, 0.1) == 0 | ||
| assert searchsorted(a, 0.4) == 2 | ||
| assert searchsorted(a, 2.0) == 3 |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.