mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
extras/stepper_resonance_tester.py opens with a bare `import numpy as np`; klippy.py:122 walks EVERY config section through a load_object whose importlib.import_module is not wrapped in except ImportError; printer.base.cfg includes FlashForge's printer.vibration.cfg, which declares [stepper_resonance_tester]. Since klippy moved to our own CPython 3.13 that has not been a printer missing resonance testing. It has been a klippy that never reaches ready. So this is not a scientific extra. anvil-klipper depends on anvil-python-numpy the way it depends on cffi, except that the others mean "klippy imports this" and this one means "klippy starts". It also gives back SHAPER_CALIBRATE and TEST_RESONANCES, dead since the same switch, and the VFA calibration behind STEPPER_RESONANCE_FACTORY_CALIBRATE that docs/notes/44-vfa-calibration.md traces end to end. WHAT WAS NOT OBVIOUS, in the order it bit. numpy 2.x builds with meson-python, and meson has never heard of _PYTHON_SYSCONFIGDATA_NAME -- the trick every other python-* recipe here leans on. pkg_mesoncross writes a real cross file instead, naming the $PKG_XW wrappers rather than the raw toolchain: the wrappers carry -EL -mnan=2008, and a cross file that reaches past them builds a tree that passes every gate which only reads symbols and is refused at exec() on the printer. pkg_pywheel grew a PKG_PY_PIP_ARGS branch to pass the file as --config-settings, which is the one thing PEP 517 does standardise. The cross file's `python =` is the BUILD python, and that is not a mistake: meson introspects an interpreter by running it and asking sysconfig, and pkg_pytarget has already made that sysconfig answer for mipsel. The old trick does reach meson, just not by its own mechanism. numpy's meson.build hard-errors below gcc 8.4 and ours is 7.2. The gate is the SciPy Toolchain Roadmap's support statement, not a requirement of the code: this toolchain compiles if constexpr, structured bindings, std::optional, std::string_view, and pocketfft_hdronly.h -- 3635 lines of the heaviest template C++ in the tree, and the half np.fft.rfft needs. Probed before the floor was lowered, and lowered rather than removed, behind a guard that fails loudly if upstream moves the check. 19 extension modules, every one at e_flags 0x70001405/0x70001407, 14MB shipped after pruning tests/, f2py/, stubs and static libs. meson-python is self-hosting, so pkg_buildpython runs meson and ninja just to install the backend -- hence ninja-build in the image and PYPKG_HOST_LIST in dependency order rather than alphabetical. And pip puts meson and cython in work/.py-host/bin, which nothing had ever looked in: without that on PATH the backend fails to install with `meson executable "meson" not found` while pip list shows it right there. THE GATE THAT WOULD HAVE CAUGHT ALL OF IT is qa/replica/test_klippy_extras_import.py: walk the real /usr/data/config include graph, map every section to its extra, import each on the printer's own interpreter. It needs no MCU, because config load happens before klippy:mcu_identify. Nothing asked that question before, which is how two comments in this repo came to assert the module guarded its own import. test_numpy.py asks for numeric answers rather than a module object -- linalg.lstsq specifically, because -Dallow-noblas selects the bundled lapack_lite and _r2_sinusoidal_fit calls it every calibration round. And test_install.py's include check read only ff-*.cfg, so FlashForge's four -- printer.vibration.cfg among them -- were never asked about at all. ff-toolchange.cfg wraps the two search commands in _FF_SHAPER_PREP, as SHAPER_CALIBRATE already was. STEPPER_RESONANCE_FACTORY_CALIBRATE is deliberately NOT wrapped: it is a gcode_macro, not a registered command, and same-named sections merge under RawConfigParser(strict=False) -- a wrapper would not wrap it, it would BE it, with rename_existing pointing at itself. It needs none, since its body is two calls to a command that is wrapped. No SAVE_CONFIG in any macro either; it restarts klippy and stays the operator's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8
136 lines
5.2 KiB
Python
136 lines
5.2 KiB
Python
"""numpy on the printer's interpreter: does it import, and do the four things klippy asks of it work.
|
|
|
|
WHY A SMOKE TEST AND NOT JUST AN IMPORT
|
|
|
|
anvil-python-numpy is the one package in this feed built with meson-python
|
|
rather than setuptools, against a cross file this repo writes itself
|
|
(pkg_mesoncross), with `-Dallow-noblas=true` and `-Ddisable-optimization=true`.
|
|
Each of those is a place where a build can succeed, install cleanly, import
|
|
fine, and be wrong underneath:
|
|
|
|
* allow-noblas selects numpy's bundled `lapack_lite` instead of a real
|
|
LAPACK. `np.linalg.lstsq` is the call that goes through it, and it is not
|
|
hypothetical -- `_r2_sinusoidal_fit` in extras/stepper_resonance_tester.py
|
|
makes it on every VFA calibration round.
|
|
* a cross build that silently produced host objects would fail at import,
|
|
which test_klippy_extras_import.py already covers -- but one that lost an
|
|
accelerated path while keeping the module would not.
|
|
|
|
So this asks for numeric answers, not for a module object. The four calls
|
|
below are exactly the ones the calibration path makes: rfft and hanning in
|
|
`harmonic_extract_using_fft`, polyfit in `parabolic_interpolation`, lstsq in
|
|
`_r2_sinusoidal_fit`.
|
|
|
|
WHY NOT THE ABI
|
|
|
|
test_abi.py already sweeps every ELF object on the filesystem, so numpy's
|
|
extensions are covered there the moment they ship, and duplicating it here
|
|
would only make that sweep's failures arrive twice. This is the other half:
|
|
the ABI test proves the kernel will load them, this proves they compute.
|
|
"""
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.replica
|
|
|
|
MODDIR = "/usr/data/anvil"
|
|
|
|
# Ours, not FlashForge's 3.8.2. FlashForge's rootfs HAD a numpy; that one is
|
|
# built for 3.8 and is not importable here, which is the whole reason this
|
|
# package exists.
|
|
PY = MODDIR + "/bin/python3.13"
|
|
ENV = MODDIR + "/anvil-env.sh"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def box(printer):
|
|
"""The installed machine, with the interpreter this asks about."""
|
|
if not printer.file(PY).exists:
|
|
pytest.fail(
|
|
"there is no %s, so there is no interpreter to ask -- "
|
|
"`make build` first." % PY)
|
|
return printer
|
|
|
|
|
|
def _py(box, body, timeout=300):
|
|
return box.sh(". %s\nexec %s - <<'PYEOF'\n%s\nPYEOF\n" % (ENV, PY, body),
|
|
timeout=timeout)
|
|
|
|
|
|
def test_numpy_imports_at_all(box):
|
|
"""The dependency anvil-klipper declares, present on the machine.
|
|
|
|
Separate from the maths below so a missing package reads as a missing
|
|
package rather than as four failed calculations.
|
|
"""
|
|
res = _py(box, "import numpy; print(numpy.__version__)")
|
|
assert res.ok, (
|
|
"numpy does not import on %s, so klippy cannot start: klippy.py:122 "
|
|
"loads [stepper_resonance_tester] and klippy.py:103 does not catch "
|
|
"ImportError.\nexit=%s\n%s\n%s"
|
|
% (PY, res.code, res.out[-2000:], res.err[-2000:]))
|
|
assert res.out.strip(), "numpy imported but reported no version"
|
|
|
|
|
|
def test_the_four_calls_the_calibration_makes(box):
|
|
"""rfft, hanning, polyfit, lstsq -- with answers checked, not just no traceback.
|
|
|
|
The values are the ones a correct numpy must produce, computed against
|
|
closed forms rather than against a recorded run of this same code, so a
|
|
subtly wrong build cannot agree with its own output.
|
|
"""
|
|
body = r'''
|
|
import numpy as np
|
|
|
|
fails = []
|
|
|
|
def check(name, got, want, tol=1e-6):
|
|
if abs(got - want) > tol:
|
|
fails.append("%s: got %r, want %r" % (name, got, want))
|
|
|
|
# rfft on a pure tone: a 16-sample signal at exactly bin 3 must put all its
|
|
# energy in that bin and (near) none elsewhere.
|
|
n = 16
|
|
sig = np.cos(2 * np.pi * 3 * np.arange(n) / n)
|
|
spec = np.abs(np.fft.rfft(sig))
|
|
check("rfft peak bin", float(np.argmax(spec)), 3.0)
|
|
check("rfft peak amplitude", float(spec[3]), n / 2.0, 1e-9)
|
|
|
|
# hanning: a symmetric window, zero at both ends, 1.0 in the middle for odd N.
|
|
w = np.hanning(9)
|
|
check("hanning[0]", float(w[0]), 0.0, 1e-12)
|
|
check("hanning[-1]", float(w[-1]), 0.0, 1e-12)
|
|
check("hanning mid", float(w[4]), 1.0, 1e-12)
|
|
|
|
# polyfit deg 2 on points taken FROM a known parabola: 2x^2 - 3x + 1.
|
|
x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
|
|
y = 2 * x ** 2 - 3 * x + 1
|
|
c = np.polyfit(x, y, 2)
|
|
check("polyfit a", float(c[0]), 2.0, 1e-6)
|
|
check("polyfit b", float(c[1]), -3.0, 1e-6)
|
|
check("polyfit c", float(c[2]), 1.0, 1e-6)
|
|
|
|
# lstsq: THE lapack_lite CALL. Overdetermined exact-fit system, y = 2a + 3b.
|
|
A = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
|
|
rhs = np.array([2.0, 3.0, 5.0])
|
|
sol, _res, _rank, _sv = np.linalg.lstsq(A, rhs, rcond=None)
|
|
check("lstsq x0", float(sol[0]), 2.0, 1e-6)
|
|
check("lstsq x1", float(sol[1]), 3.0, 1e-6)
|
|
|
|
if fails:
|
|
print("FAILURES")
|
|
for f in fails:
|
|
print(" " + f)
|
|
else:
|
|
print("ALL OK")
|
|
'''
|
|
res = _py(box, body)
|
|
assert res.ok, (
|
|
"the numpy smoke test did not run to completion on %s.\nexit=%s\n%s\n%s"
|
|
% (PY, res.code, res.out[-3000:], res.err[-3000:]))
|
|
assert "ALL OK" in res.out, (
|
|
"numpy is installed and imports, but computes wrong answers -- so the "
|
|
"cross build produced something that loads and does not work. lstsq "
|
|
"failing on its own points at -Dallow-noblas and the bundled "
|
|
"lapack_lite; the rest points at the cross file.\n%s"
|
|
% res.out[-3000:])
|