From 1dd82f0ba411fb716f3bd7b10ab5d349fa75113c Mon Sep 17 00:00:00 2001 From: Gabriel Bernardino Date: Wed, 9 Sep 2026 10:05:54 +0200 Subject: [PATCH] Add GitHub Actions CI: test suite, Python version matrix, and a Py3.9 import bug fix Adds a pytest suite (import/smoke tests, txdelay/getparam sanity checks, and golden-master regression tests for simus/pfield with a dB-bucketed tolerance that tolerates BLAS-backend rounding noise without masking real regressions) plus a GitHub Actions workflow that installs the package and runs it across Python 3.9-3.13. Testing against the declared minimum Python version (3.9) surfaced a real bug: txdelay3.py used PEP 604 `X|Y` union type hints without deferred annotation evaluation, crashing `import pymust` on 3.9 and 3.8. Fixed by adding `from __future__ import annotations`, consistent with the rest of the codebase. requires-python is bumped to >=3.9 to match what's actually tested (NumPy 2.0+, needed by simus.py's np.concat usage, dropped 3.8 support). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/tests.yml | 32 +++++++++++++ pyproject.toml | 5 +- src/pymust/txdelay3.py | 1 + tests/conftest.py | 12 +++++ tests/data/pfield.npz | Bin 0 -> 6534 bytes tests/data/simus.npz | Bin 0 -> 63904 bytes tests/generate_golden_data.py | 23 +++++++++ tests/golden_scenarios.py | 52 ++++++++++++++++++++ tests/test_getparam.py | 18 +++++++ tests/test_imports.py | 38 +++++++++++++++ tests/test_regression.py | 87 ++++++++++++++++++++++++++++++++++ tests/test_simus_pipeline.py | 28 +++++++++++ tests/test_txdelay.py | 23 +++++++++ tests/test_utils.py | 40 ++++++++++++++++ 14 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/conftest.py create mode 100644 tests/data/pfield.npz create mode 100644 tests/data/simus.npz create mode 100644 tests/generate_golden_data.py create mode 100644 tests/golden_scenarios.py create mode 100644 tests/test_getparam.py create mode 100644 tests/test_imports.py create mode 100644 tests/test_regression.py create mode 100644 tests/test_simus_pipeline.py create mode 100644 tests/test_txdelay.py create mode 100644 tests/test_utils.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..1e0bdf57 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package and test dependencies + run: python -m pip install -e ".[test]" + + - name: Run tests + run: python -m pytest tests/ -v diff --git a/pyproject.toml b/pyproject.toml index 0d0bda0f..0c4941a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ ] description = "Python port of the MUST toolbox for ultrasound signal processing and generation of simulated images" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", @@ -24,6 +24,9 @@ dependencies = [ ] dynamic = ["license", "version"] +[project.optional-dependencies] +test = ["pytest"] + [project.urls] Homepage = "https://www.biomecardio.com/MUST" Repository = "https://github.com/creatis-ULTIM/PyMUST" diff --git a/src/pymust/txdelay3.py b/src/pymust/txdelay3.py index 712abeaf..50b72b00 100644 --- a/src/pymust/txdelay3.py +++ b/src/pymust/txdelay3.py @@ -1,3 +1,4 @@ +from __future__ import annotations from . import utils import numpy as np import scipy.optimize diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..8ade035b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import numpy as np +import pytest + + +@pytest.fixture(params=["L11-5v", "L12-3v", "C5-2v", "P4-2v"]) +def probe_name(request): + return request.param + + +@pytest.fixture +def rng(): + return np.random.default_rng(0) diff --git a/tests/data/pfield.npz b/tests/data/pfield.npz new file mode 100644 index 0000000000000000000000000000000000000000..40f15ded2d1270cfbd62e0bf6d2802d7371084c3 GIT binary patch literal 6534 zcmd6rc{J5)+s9GVp^zaulBtYgi*TaTrOAoNoO#~nVVlP?XUIGcdz(uXGDI|*GDM|P zM3W@5IAtn4m*+WWt@oUD-se5*{pkRf`qh$3(1d~ikT{Z(|_Lm`#nWn zS53o!n#!9hK**ZtVdXA#h*wC_MqX$?uaJ$ayQjN_ix6lWbm)_dW{7pkf;ZK2l85i514G{gU28%zoFU%b z!L1h$4xiuCMw&)Lmts~y#r+7dD=HpQVnPo2E z_k;YEatSA3IeE+Z#JwDd3tUe)FPjcYo_Ve-5uq>@l(Bs>%NtBB9z`)6b_1FK&IaZ+ zCvbX^YdVo(4~J-E3KI_5!jw3t!fr1jSbOfV*~@GVaj%rvCcVfo{J} ze<2ubIJmxPot1)CGgvb#H5}LVr+O>Jv&C_X7Xf z$24YH?ofKADa!dE365HJl~fEmK}VOir~sb>TxTiPZ{W^>E~c}Ia|ZDcAf@m~D=8W@ zu2|g`DZT)$23KBBW`%=n;h;{FXc$QIwN0DThr%9Wo`Do+2zcv!PF6k{404Tn%Z z;S>YC1^nNlLLy=7R9TmjP6SL8CwNv~4uj`SiI$^hLP6R4ur@_D1e%SA!j6rT(cWp88d%HEkjpn62%LW~?*d`t6V|@vVVi#G-rd=zt51-Oj2yXK4f1 zWrt|)p0tFdwB0YeO3k3Tx58%0%oyZzycaxI3_y(_nfa1WA8wAYW}Xoy1JjAZSUTN& z7~Yo1%5oqPl9;I#y!?Ve$$_`0mh1)JM3iOjLBCatdA2Vbii9WwTT=62X>GS52`5P2>&V3knRxiz^tSfe zt_$#Rbi_saR0vFJZp!QN4+L{z=b~H-UzpRm^w81Y3rYfA%A=dfz{4fZ6l_a|g6dM2 z3xnA(kt@e~tCa%2DhpfJgYqEZQhz};DHp1n6Ygi(km0m*MbWrYHb@lK_qeHL!d3%q z)*D&ra6@o!N#{8-9FXb0{fw0iF~9zBuv?P?=_DV7&5Y-0y<>=dxfr zWS^C>aXN4waC;JKlLGxpO`3I^lR%8^dSUOuczBTG@A^KI3{OTwdtX$NAu~Qo%ikym z^t_JcZsAFSUltgyCahe5lGwvl5^Nzb@o+I($36f)w8p=>NAQ7%tK=xTv!0;+(!?kD zv^$8*u4a$$QhxGF`VhG%DIxQQXX6 zTu}Fh&s8#%JfoEEAYo;<$d_#la$l>*-j|Vq{7gIjgl7n_<(PiE&)@@$NsQ^c zEj*x&f7H^6+6}%3_3xalc7dw$#8QP5PSEnkAuh_&0q(Y~B>gaG2UFgraobXGlK$bu z$2eVz{yQgLj37byr=IwGS#~RmQWO|ok6w>_7YR!Id10-ONT6axx4P5M25fH!+e=Sd z0Hd$w9r|`txLnch%HeGU8EtDG9&85iM5;1v*U_`k+GJR9t49}D7<(GEd&n?xi11DC zWIFuPGA+f=5eSdWnjY~Olc3t}gW6KE9lUGeaeonD4fe*ujS4Fkux2SdY7OR)x8-Gl z*Jl%GKTWVWKWhXRuhxf_Pm&=bksQZ)Vtm>@C6gBZ@Y_W;)*P<7W*59{exFBq{Km>nfZ5W-`!R zzL9XPAQv=msaU)5q{4n%_Xp8b7r~SzkVUFJ49pKIyRZ;~pwph>xb0Uz@HMmhMV{mh zJd#G!hvq%NfsH)Td6Nv=zZ#|uZKJ?|-TgU2IvMVd8m7~j<-?Vk+JO|6d{_%C3fkR_ z9ww~g43kGTxaspZMQu^^|jQ7^sHXL_Ta4z6gUo~elV z^hQ0t4+j`ivEG-(&h?FYU(>=ZH|p*B&eXM0&u*sHf1}>(OY@=|^^A#9MH}ngscMm6 zi|b7ixxBps*PGIK%#4KV4f8e$i^TP6kVZEXaJ|!0`WZ~PUfzYWBzj!0l3m{cSzIq@ z66Alv^|sd=AHIhBVcMWr)q?xMn;$R3j{8Br&NqA+_v7og^`d&*4=RVwrERz$D-NAh zk+>h!x(07+aX(@!*wqwpKNffSg?ZwB{L)FgtcCr;J+-q%2m7V4r>&PA`{k-PkX`nkq6M#D3{lzAj>h{Ss^N$=4bC>YK}88FpgJr1ZQGL!(Do5 zoQXy%3*G@Z6Gz$iJ)^F1fm((AdTKZm#b@f)({U!cukmhT$C*eubt(;J%nt)o<<1U*B`oXB_!dWLwx365s;480@saWBy`obNm* zFM*!HWZ~3zA@mGVZSD#N=oxOmrM?t{oI9n$gnArDgOq+wFYAE6~$u-LN^8j-DpxaO3nSdYbvgyD6^d zX~r$y4OgS55%J%~P>!BUzHl+&HF~Z)#k6H>DS0p&lKI{qJ(r%@bi@brTn430wk7Df zh~iRKlIXc|Ee5}Jpyzs0(y}KTJy&>*)^0oWThb1fcg>igr^7sEBnwB9wZ#@>P1gTJ0f5snoc#?6+P>yIg>~&de#coh9iyWS%0nX zEt*8n%9s5*+8#Y?2w?|f4|>)=p7`%eoV)(!iT}suc%`vBAvKtL6qQfjlOZQSVQ10V zA9><|cBpr57;{hiwG|pN=AI{{bJJ%q_pBbVC~m~ubL6eLn%w@yg{2w+;L_#n_`N)>s z2p~wSPkh5%X7Z!?PzmO;U7HU_&0sE*0YZGNnyNof{wv?6xW?`oc>@(yRYtmyoLXcY&r=RILh1_B$I9VzhxkcSgs@|Q* zEsSq9y|Y1X0rPPs7DHgIvYlExO(UxzCrt zlIupueWpBmJd%+6l+QGkb>ReI(Ad$hj@;+j83JJjxsPx3^p(BHeN29C><&QgL$lAi z?mlv#yKgV@*dh0cd&~c%9=R0jHJ?lOkV_Rm4pnz#&VhiS`fuFGrI-R^;1hDGO4To8 zqR6H6Muh8pkxNlKQd@)~my#pQdNd=KYMd?Hb_u!EA5T10eOLJJC!X`qIN>OI@=G;xI#MZ_L;z17hrMvZo+=s8EocL3Jwl9!O;+lxCLq_ z_{JMSJ8?S)M0*u{WAs9yYOWy6DBlgZ>E+UnCELR<5m9d_O+=6i?2;ghd3&$Z))N$epgz_oNCKUmd%nubIYIWZ$wl>B_E1Q- zg==`-7G8V}tbSBs19}c8k3{AX;d!ZXZaZo*t@Y6oXSaMX^m`dR^)&?s%MVEn@By^g{2Abnk~Unm3l%v=B| zi>naI*fQQ7xRnIayis+d4#7YTQ-kMscmado%yNYf34W3iK1vmc-g4r5kJF+(7_`oO zO*6Iw$CTD5MTc$Sefw1Tq6G!~l+0wKg|onjKDL^lI{*fmkBM?ooZ)Z_xIQAKu^C z{l+brdgJ&1#q9rFZG!;*b+vh{|6i*8J1hLT+6GuMug{`YFXvxYxc+ej{dUF`+K zf35bH-)+Mc{dKhtrvF;)-yZ4D-)&>!{$7o9;~)Rq6-?d8`lo9DWBzKW($a1GAss%t LaleW=HlF&kZKZA*(Rk3A=#5%$jrKD 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%0A{Bl(qn3l38+hUI?UWB2D@5^j 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 literal 0 HcmV?d00001 diff --git a/tests/generate_golden_data.py b/tests/generate_golden_data.py new file mode 100644 index 00000000..c2087bed --- /dev/null +++ b/tests/generate_golden_data.py @@ -0,0 +1,23 @@ +"""Regenerate the golden reference .npz files used by test_regression.py. + +Run this deliberately after a change that intentionally alters simus/pfield +numerics, then review the diff of the resulting .npz files: + + python tests/generate_golden_data.py +""" +import numpy as np + +from golden_scenarios import DATA_DIR, SCENARIOS + + +def main(): + DATA_DIR.mkdir(exist_ok=True) + for name, build in SCENARIOS.items(): + outputs = build() + path = DATA_DIR / f"{name}.npz" + np.savez(path, **outputs) + print(f"wrote {path}") + + +if __name__ == "__main__": + main() diff --git a/tests/golden_scenarios.py b/tests/golden_scenarios.py new file mode 100644 index 00000000..0aa150f4 --- /dev/null +++ b/tests/golden_scenarios.py @@ -0,0 +1,52 @@ +"""Fixed input scenarios shared by the golden-data generator and the regression +tests, so both sides always compute from the exact same inputs. +""" +import numpy as np +import pymust + +DATA_DIR = __import__("pathlib").Path(__file__).parent / "data" + + +PLANE_WAVE_TILTS = [-0.3, -0.1, 0.0, 0.1, 0.3] # radians, several steering directions + + +def pfield_scenario(): + param = pymust.getparam("L11-5v") + param.Nelements = 16 + + x, z = pymust.impolgrid(10, 0.04, np.pi / 4, param) + y = np.zeros_like(x) + + outputs = {} + for tilt in PLANE_WAVE_TILTS: + delays = pymust.txdelayPlane(param, tilt) + rp, _, _ = pymust.pfield(x, y, z, delays, param) + outputs[f"rms_pressure_plane_tilt_{tilt:+.1f}"] = rp + + focused_delays = pymust.txdelayFocused(param, 0.005, 0.03) + rp_focused, _, _ = pymust.pfield(x, y, z, focused_delays, param) + outputs["rms_pressure_focused"] = rp_focused + + return outputs + + +def simus_scenario(): + param = pymust.getparam("L11-5v") + param.Nelements = 8 + + x = np.array([0.0, 0.3e-2, -0.3e-2]) + y = np.array([0.0, 0.0, 0.0]) + z = np.array([0.2e-2, 0.4e-2, 0.6e-2]) + rc = np.array([1.0, 0.7, 1.3]) + delays = pymust.txdelayPlane(param, 0.05) + + rf, _ = pymust.simus(x, y, z, rc, delays, param) + iq = pymust.rf2iq(rf, param) + img = pymust.bmode(iq) + return {"rf": rf, "iq": iq, "bmode": img} + + +SCENARIOS = { + "pfield": pfield_scenario, + "simus": simus_scenario, +} diff --git a/tests/test_getparam.py b/tests/test_getparam.py new file mode 100644 index 00000000..c01d68ae --- /dev/null +++ b/tests/test_getparam.py @@ -0,0 +1,18 @@ +import numpy as np +import pymust + + +def test_getparam_returns_sane_probe_parameters(probe_name): + param = pymust.getparam(probe_name) + + assert param.Nelements > 0 + assert param.fc > 0 + assert param.pitch > 0 + assert param.bandwidth > 0 + + +def test_getparam_unknown_probe_raises(): + import pytest + + with pytest.raises(Exception): + pymust.getparam("not-a-real-probe") diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 00000000..c6dc81b8 --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,38 @@ +import pymust + + +EXPECTED_FUNCTIONS = [ + "bmode", + "dasmtx", + "dasmtx3", + "getparam", + "impolgrid", + "iq2doppler", + "getNyquistVelocity", + "pfield", + "pfield3", + "rf2iq", + "simus", + "simus3", + "tgc", + "txdelay", + "txdelayCircular", + "txdelayPlane", + "txdelayFocused", + "txdelay3", + "txdelay3Plane", + "txdelay3Diverging", + "txdelay3Focused", + "getDopplerColorMap", + "genscat", + "mkmovie", + "getpulse", + "smoothn", + "sptrack", +] + + +def test_all_public_functions_are_importable(): + for name in EXPECTED_FUNCTIONS: + assert hasattr(pymust, name), f"pymust.{name} is missing" + assert callable(getattr(pymust, name)) diff --git a/tests/test_regression.py b/tests/test_regression.py new file mode 100644 index 00000000..6ba29f53 --- /dev/null +++ b/tests/test_regression.py @@ -0,0 +1,87 @@ +"""Golden-master regression tests. + +These compare simus/pfield output, computed fresh on every test run, against +reference arrays saved in tests/data/. simus and pfield are deterministic +within one environment (no randomness - see tests/golden_scenarios.py), but +NOT bit-reproducible across environments: pfield accumulates results in +single precision (complex64), and different BLAS/LAPACK backends (e.g. +OpenBLAS vs Apple's Accelerate) round matrix reductions differently. simus +then runs a sharp threshold ("zero out samples below -100dB relative to the +peak", see the RelThresh/tanh step in simus.py) on top of that, which turns +tiny backend-dependent noise into visibly different values for the handful +of samples that sit right at the threshold. This was caught in practice by +comparing a conda-env-generated reference against a plain pip install: exact +comparison failed on ~36% of RF samples even though every value was +numerically negligible (all well under 1e-4 of the peak amplitude). + +So a sample's relative tolerance depends on how loud it is compared to the +signal's own peak (in dB, 20*log10(|value|/peak)): + - at or above NOISE_FLOOR_DB: tight rtol - this is "real" signal, and a + regression here (wrong scaling, wrong timing/shape, wrong physics) + should still fail loudly. + - below NOISE_FLOOR_DB: loose rtol - down in the noise floor, relative + comparison is meaningless (dividing by a near-zero reference blows up + the ratio) and this is exactly where backend rounding causes samples to + snap across simus's threshold. A small absolute tolerance (scaled to + the peak) still catches a sample that shouldn't be near-zero at all. + +If you intentionally change the numerics of simus/pfield (e.g. a bug fix that +changes the output), regenerate the references and review the diff: + + python tests/generate_golden_data.py +""" +import numpy as np +import pytest + +from golden_scenarios import DATA_DIR, SCENARIOS + +NOISE_FLOOR_DB = -30.0 +TIGHT_RTOL = 1e-4 +LOOSE_RTOL = 1.0 +ATOL_FRACTION_OF_PEAK = 1e-5 + + +def _load_reference(name): + path = DATA_DIR / f"{name}.npz" + if not path.exists(): + pytest.skip(f"no golden reference at {path}; run generate_golden_data.py") + with np.load(path) as data: + return {key: data[key] for key in data.files} + + +def _assert_matches_reference(fresh_value, ref_value, label): + fresh_value = np.asarray(fresh_value, dtype=np.complex128 if np.iscomplexobj(fresh_value) else np.float64) + ref_value = np.asarray(ref_value, dtype=np.complex128 if np.iscomplexobj(ref_value) else np.float64) + assert fresh_value.shape == ref_value.shape, label + + peak = np.max(np.abs(ref_value)) + atol = max(peak * ATOL_FRACTION_OF_PEAK, 1e-9) + + with np.errstate(divide="ignore"): + ref_db = 20 * np.log10(np.abs(ref_value) / peak) + rtol = np.where(ref_db < NOISE_FLOOR_DB, LOOSE_RTOL, TIGHT_RTOL) + + diff = np.abs(fresh_value - ref_value) + allowed = atol + rtol * np.abs(ref_value) + bad = diff > allowed + + if bad.any(): + worst = np.unravel_index(np.argmax(diff - allowed), diff.shape) + raise AssertionError( + f"{label}: {bad.sum()}/{bad.size} elements exceed tolerance " + f"(no longer matches the golden reference); worst at {worst}: " + f"fresh={fresh_value[worst]!r} ref={ref_value[worst]!r} " + f"diff={diff[worst]:.3g} allowed={allowed[worst]:.3g} " + f"({ref_db[worst]:.1f} dB relative to peak)" + ) + + +@pytest.mark.parametrize("scenario_name", sorted(SCENARIOS)) +def test_matches_golden_reference(scenario_name): + reference = _load_reference(scenario_name) + fresh = SCENARIOS[scenario_name]() + + assert set(fresh) == set(reference), "scenario outputs changed - regenerate golden data" + + for key, fresh_value in fresh.items(): + _assert_matches_reference(fresh_value, reference[key], f"{scenario_name}.{key}") diff --git a/tests/test_simus_pipeline.py b/tests/test_simus_pipeline.py new file mode 100644 index 00000000..e45adcf5 --- /dev/null +++ b/tests/test_simus_pipeline.py @@ -0,0 +1,28 @@ +import numpy as np +import pymust + + +def test_simus_rf2iq_bmode_pipeline(): + param = pymust.getparam("L11-5v") + param.Nelements = 8 + + x = np.array([0.0, 0.0]) + y = np.array([0.0, 0.0]) + z = np.array([0.2e-2, 0.5e-2]) + rc = np.array([1.0, 1.0]) + tx_delays = np.zeros(param.Nelements).reshape((1, -1)) + + rf, _ = pymust.simus(x, y, z, rc, tx_delays, param) + assert rf.ndim == 2 + assert rf.shape[1] == param.Nelements + assert np.isfinite(rf).all() + assert np.any(rf != 0) + + iq = pymust.rf2iq(rf, param) + assert iq.shape == rf.shape + assert np.iscomplexobj(iq) + assert np.isfinite(iq).all() + + img = pymust.bmode(iq) + assert img.shape == rf.shape + assert img.dtype == np.uint8 diff --git a/tests/test_txdelay.py b/tests/test_txdelay.py new file mode 100644 index 00000000..46aaab12 --- /dev/null +++ b/tests/test_txdelay.py @@ -0,0 +1,23 @@ +import numpy as np +import pymust + + +def test_txdelay_plane_and_focused(probe_name): + param = pymust.getparam(probe_name) + + plane = pymust.txdelayPlane(param, 0.1) + focused = pymust.txdelayFocused(param, 0, 0.03) + + for delays in (plane, focused): + assert delays.shape[-1] == param.Nelements + assert np.isfinite(delays).all() + + +def test_txdelay_circular_wave_on_linear_array(): + # txdelayCircular is only defined for linear (non-curved) arrays. + param = pymust.getparam("L11-5v") + + circular = pymust.txdelayCircular(param, 0.1, np.pi / 3) + + assert circular.shape[-1] == param.Nelements + assert np.isfinite(circular).all() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..acd7a76c --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,40 @@ +import numpy as np +import pymust + + +def test_genscat_generates_scatterers_within_roi(): + roi_width, roi_depth = 0.02, 0.02 + x, y, z, rc = pymust.genscat(np.array([roi_width, roi_depth]), 0.001) + + assert x.shape == y.shape == z.shape == rc.shape + assert x.size > 0 + # The ROI is centered on x=0 with its top edge at z=0, per genscat's docstring. + assert np.abs(x).max() <= roi_width / 2 + 1e-9 + assert (z >= -1e-9).all() + assert z.max() <= roi_depth + 1e-9 + + +def test_smoothn_reduces_noise(rng): + t = np.linspace(0, 10, 200) + clean = np.sin(t) + noisy = clean + 0.3 * rng.standard_normal(t.size) + + smoothed, _, _ = pymust.smoothn(noisy) + + assert smoothed.shape == noisy.shape + assert np.isfinite(smoothed).all() + error_before = np.mean((noisy - clean) ** 2) + error_after = np.mean((smoothed - clean) ** 2) + assert error_after < error_before + + +def test_impolgrid_returns_grid_coordinates(): + param = pymust.getparam("L11-5v") + + grid = pymust.impolgrid(50, 0.05, np.pi / 3, param) + + assert len(grid) == 2 + x, z = grid + assert x.shape == z.shape + assert np.isfinite(x).all() + assert np.isfinite(z).all()