From 5a35c95e4b2ff7022a0af97f10219d23429c8f62 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 30 Jun 2026 16:45:23 +0200 Subject: [PATCH 1/5] initial commit --- recipes/scipy/meta.yaml | 34 ++++++++++++++++++++++++++++++++++ recipes/scipy/test_scipy.py | 9 +++++++++ src/forge/build.py | 16 +++++++++++++--- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 recipes/scipy/meta.yaml create mode 100644 recipes/scipy/test_scipy.py diff --git a/recipes/scipy/meta.yaml b/recipes/scipy/meta.yaml new file mode 100644 index 0000000..f6ac67e --- /dev/null +++ b/recipes/scipy/meta.yaml @@ -0,0 +1,34 @@ +package: + name: scipy + version: '1.18.0' + +requirements: + build: + - ninja + - meson + - cython + - pythran + host: + - numpy 2.4.6 + - pybind11 +# {% if sdk == 'android' %} + - flet-libcpp-shared >=27.2.12479018 +# {% endif %} + +build: + number: 1 + script_env: + # meson introspects numpy via the cross-python from scipy's source dir; + # drop the implicit cwd from sys.path so it doesn't shadow stdlib (cf. pandas). + PYTHONSAFEPATH: "1" + backend-args: + # scipy 1.18 builds without a Fortran compiler — only the deprecated + # scipy.odr is dropped. Removes the cross-Fortran blocker entirely. + - -Csetup-args=-D_without-fortran=true +# {% if sdk in ['iphoneos', 'iphonesimulator'] %} + # iOS has no OpenBLAS; Apple's Accelerate framework provides BLAS/LAPACK. + - -Csetup-args=-Dblas=accelerate + - -Csetup-args=-Dlapack=accelerate +# {% endif %} + - -Csetup-args=--cross-file + - -Csetup-args={MESON_CROSS_FILE} diff --git a/recipes/scipy/test_scipy.py b/recipes/scipy/test_scipy.py new file mode 100644 index 0000000..e5eafe1 --- /dev/null +++ b/recipes/scipy/test_scipy.py @@ -0,0 +1,9 @@ +def test_basic(): + """Load scipy and exercise the BLAS/LAPACK backend (Accelerate on iOS).""" + import numpy as np + from scipy import linalg + + a = np.array([[3.0, 1.0], [1.0, 2.0]]) + b = np.array([9.0, 8.0]) + x = linalg.solve(a, b) # LAPACK getrf/getrs -> proves BLAS/LAPACK works + assert np.allclose(a @ x, b) diff --git a/src/forge/build.py b/src/forge/build.py index 3d99063..1c7da0b 100644 --- a/src/forge/build.py +++ b/src/forge/build.py @@ -458,9 +458,19 @@ def compile_env(self, **kwargs) -> dict[str, str]: / "site-packages" ) if site_dir.is_dir(): - for share_pkgconfig in site_dir.glob("*/share/pkgconfig"): - if share_pkgconfig.is_dir(): - pkg_config_paths.append(str(share_pkgconfig)) + # Wheels bundle their .pc files in assorted spots under + # site-packages: pybind11 -> /share/pkgconfig, while + # numpy ships numpy/_core/lib/pkgconfig/numpy.pc. Search the + # known locations so meson's pkg-config method resolves them + # (e.g. scipy's `dependency('numpy')`, which uses numpy.pc). + for pattern in ( + "*/share/pkgconfig", + "*/lib/pkgconfig", + "*/_core/lib/pkgconfig", + ): + for wheel_pc in site_dir.glob(pattern): + if wheel_pc.is_dir(): + pkg_config_paths.append(str(wheel_pc)) pkg_config_path = ":".join(pkg_config_paths) env = { From 04e8d602a5a025fadcae1026fdae6485c61540a8 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 30 Jun 2026 16:51:19 +0200 Subject: [PATCH 2/5] patch --- recipes/scipy/meta.yaml | 7 +++---- recipes/scipy/patches/accelerate-ios.patch | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 recipes/scipy/patches/accelerate-ios.patch diff --git a/recipes/scipy/meta.yaml b/recipes/scipy/meta.yaml index f6ac67e..daebd85 100644 --- a/recipes/scipy/meta.yaml +++ b/recipes/scipy/meta.yaml @@ -15,12 +15,11 @@ requirements: - flet-libcpp-shared >=27.2.12479018 # {% endif %} +patches: + - accelerate-ios.patch + build: number: 1 - script_env: - # meson introspects numpy via the cross-python from scipy's source dir; - # drop the implicit cwd from sys.path so it doesn't shadow stdlib (cf. pandas). - PYTHONSAFEPATH: "1" backend-args: # scipy 1.18 builds without a Fortran compiler — only the deprecated # scipy.odr is dropped. Removes the cross-Fortran blocker entirely. diff --git a/recipes/scipy/patches/accelerate-ios.patch b/recipes/scipy/patches/accelerate-ios.patch new file mode 100644 index 0000000..365e8a5 --- /dev/null +++ b/recipes/scipy/patches/accelerate-ios.patch @@ -0,0 +1,20 @@ +# scipy's Accelerate gate (scipy/meson.build) only computes `macOS13_3_or_later` +# when host_machine.system() == 'darwin', then errors out for blas=accelerate +# otherwise. forge's iOS cross-file sets system='iphoneos'/'iphonesimulator', so +# the gate stays false and the build aborts. The Accelerate framework is the same +# on iOS, and its modern (macOS-13.3-equivalent) LAPACK is available from iOS 16.4 +# onward — well below the active iOS SDK — so enable the new-Accelerate path for +# iOS systems too. Only affects iOS (Android uses blas=openblas). +--- a/scipy/meson.build ++++ b/scipy/meson.build +@@ -268,6 +268,10 @@ + r = run_command('xcrun', '-sdk', 'macosx', '--show-sdk-version', check: true) + sdkVersion = r.stdout().strip() + macOS13_3_or_later = sdkVersion.version_compare('>=13.3') ++elif host_machine.system() in ['ios', 'iphoneos', 'iphonesimulator'] ++ # iOS Accelerate provides the modern (macOS 13.3-equivalent) LAPACK from ++ # iOS 16.4 onward; the active iOS SDK is well past that, so enable it. ++ macOS13_3_or_later = true + endif + + _args_blas_lp64 = [] From 9ba322b49588b29a8bb7f8b712e26d770255e578 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 30 Jun 2026 16:56:59 +0200 Subject: [PATCH 3/5] update Accelerate support checks for macOS 13.3 and iOS 16.4 SDKs --- recipes/scipy/patches/accelerate-ios.patch | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/recipes/scipy/patches/accelerate-ios.patch b/recipes/scipy/patches/accelerate-ios.patch index 365e8a5..8086807 100644 --- a/recipes/scipy/patches/accelerate-ios.patch +++ b/recipes/scipy/patches/accelerate-ios.patch @@ -18,3 +18,17 @@ endif _args_blas_lp64 = [] +--- a/scipy/_build_utils/src/scipy_blas_defines.h ++++ b/scipy/_build_utils/src/scipy_blas_defines.h +@@ -16,8 +16,10 @@ + + + #ifdef ACCELERATE_NEW_LAPACK +- #if __MAC_OS_X_VERSION_MAX_ALLOWED < 130300 ++ #if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED < 130300 + #error "Accelerate support is only available with macOS 13.3 SDK or later" ++ #elif defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 160400 ++ #error "Accelerate support is only available with iOS 16.4 SDK or later" + #else + /* New Accelerate suffix is always $NEWLAPACK or $NEWLAPACK$ILP64 (no underscore appended) */ + #ifdef HAVE_BLAS_ILP64 From 8c901d07e88b1f44813afe7d98129d3274409b89 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 30 Jun 2026 22:30:24 +0200 Subject: [PATCH 4/5] recipe: scipy 1.18.0 + flet-libopenblas 0.3.33 (OpenBLAS, iOS+Android) scipy via OpenBLAS everywhere (NOFORTRAN f2c-LAPACK) + -D_without-fortran. iOS: no patches. Android: 3 patches (clog/cpow shim, ducc no-affinity, boost binary128 long double on x86_64) + use-pythran=false. flet-libopenblas builds BLAS+LAPACK static, openblas.pc relocatable. NOTE: android still needs the libpython.so symlink fix (not yet in repo) - expected to fail at the scipy link step on CI until that lands. --- recipes/flet-libopenblas/build.sh | 51 +++++++++++++++++++ recipes/flet-libopenblas/meta.yaml | 11 ++++ recipes/scipy/meta.yaml | 19 +++++-- recipes/scipy/patches/accelerate-ios.patch | 34 ------------- .../patches/android-bionic-clog-cpow.patch | 28 ++++++++++ .../patches/android-ducc-no-affinity.patch | 34 +++++++++++++ .../android-x86_64-boost-longdouble.patch | 24 +++++++++ src/forge/build.py | 8 +-- 8 files changed, 167 insertions(+), 42 deletions(-) create mode 100755 recipes/flet-libopenblas/build.sh create mode 100644 recipes/flet-libopenblas/meta.yaml delete mode 100644 recipes/scipy/patches/accelerate-ios.patch create mode 100644 recipes/scipy/patches/android-bionic-clog-cpow.patch create mode 100644 recipes/scipy/patches/android-ducc-no-affinity.patch create mode 100644 recipes/scipy/patches/android-x86_64-boost-longdouble.patch diff --git a/recipes/flet-libopenblas/build.sh b/recipes/flet-libopenblas/build.sh new file mode 100755 index 0000000..4f76860 --- /dev/null +++ b/recipes/flet-libopenblas/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -eu + +# OpenBLAS is make-based (not autotools). Cross-compile knobs: +# TARGET/BINARY — required for cross (OpenBLAS can't probe the target CPU) +# NOFORTRAN=1 — build BLAS + f2c-converted LAPACK (no Fortran compiler) +# CROSS=1 — never run target binaries during the build +# HOSTCC=clang — build-machine compiler for getarch and friends +case "$HOST_ARCH" in + arm64-v8a|arm64) TARGET=ARMV8; BINARY=64 ;; + armeabi-v7a) TARGET=ARMV7; BINARY=32 ;; + x86_64) TARGET=NEHALEM; BINARY=64 ;; + *) echo "flet-libopenblas: unsupported arch '$HOST_ARCH'"; exit 1 ;; +esac + +# `libs netlib` builds the BLAS + f2c-LAPACK static lib but skips OpenBLAS's +# self-test binaries (the `tests` target), whose ARMV7 link pulls a hard-float +# -lm_hard that the softfp NDK lacks. The .a is identical; tests aren't needed. +make libs netlib \ + TARGET="$TARGET" BINARY="$BINARY" \ + HOSTCC=clang CC="$CC" AR="$AR" RANLIB=echo \ + NOFORTRAN=1 CROSS=1 \ + USE_THREAD=0 NUM_THREADS=1 \ + NO_SHARED=1 \ + CFLAGS="$CFLAGS" \ + -j"$CPU_COUNT" + +make PREFIX="$PREFIX" TARGET="$TARGET" NO_SHARED=1 install +rm -rf "$PREFIX/lib/cmake" + +# OpenBLAS installs libopenblas.a as a symlink to the versioned archive; forge's +# copytree dereferences it into two 60 MB files. Collapse to one real libopenblas.a. +ver_a=$(ls "$PREFIX"/lib/libopenblas_*.a 2>/dev/null | head -1 || true) +if [ -n "${ver_a:-}" ]; then + rm -f "$PREFIX/lib/libopenblas.a" + mv "$ver_a" "$PREFIX/lib/libopenblas.a" +fi + +# Fix openblas.pc for consumers (scipy's meson `dependency('openblas')`): +# - relocatable libdir/includedir (the baked-in absolute build paths are gone +# by the time scipy builds against the installed host wheel) +# - drop extralib's `-lgfortran` (we built NOFORTRAN; the f2c LAPACK lives inside +# libopenblas and there is no libgfortran on iOS/Android) and `-lpthread` +# (USE_THREAD=0; pthread is in libc on both platforms). +pc="$PREFIX/lib/pkgconfig/openblas.pc" +sed -i.bak \ + -e 's|^libdir=.*|libdir=${pcfiledir}/..|' \ + -e 's|^includedir=.*|includedir=${pcfiledir}/../../include|' \ + -e 's|^extralib=.*|extralib=|' \ + "$pc" +rm -f "$pc.bak" diff --git a/recipes/flet-libopenblas/meta.yaml b/recipes/flet-libopenblas/meta.yaml new file mode 100644 index 0000000..5de5da9 --- /dev/null +++ b/recipes/flet-libopenblas/meta.yaml @@ -0,0 +1,11 @@ +{% set version = "0.3.33" %} + +package: + name: flet-libopenblas + version: '{{ version }}' + +build: + number: 0 + +source: + url: https://github.com/OpenMathLib/OpenBLAS/releases/download/v{{ version }}/OpenBLAS-{{ version }}.tar.gz diff --git a/recipes/scipy/meta.yaml b/recipes/scipy/meta.yaml index daebd85..4775631 100644 --- a/recipes/scipy/meta.yaml +++ b/recipes/scipy/meta.yaml @@ -11,12 +11,15 @@ requirements: host: - numpy 2.4.6 - pybind11 + - flet-libopenblas 0.3.33 # {% if sdk == 'android' %} - flet-libcpp-shared >=27.2.12479018 # {% endif %} patches: - - accelerate-ios.patch + - android-bionic-clog-cpow.patch + - android-ducc-no-affinity.patch + - android-x86_64-boost-longdouble.patch build: number: 1 @@ -24,10 +27,16 @@ build: # scipy 1.18 builds without a Fortran compiler — only the deprecated # scipy.odr is dropped. Removes the cross-Fortran blocker entirely. - -Csetup-args=-D_without-fortran=true -# {% if sdk in ['iphoneos', 'iphonesimulator'] %} - # iOS has no OpenBLAS; Apple's Accelerate framework provides BLAS/LAPACK. - - -Csetup-args=-Dblas=accelerate - - -Csetup-args=-Dlapack=accelerate + # BLAS/LAPACK from flet-libopenblas (OpenBLAS, NOFORTRAN f2c-LAPACK) on both + # platforms, resolved via its openblas.pc. Standard Fortran mangling. + - -Csetup-args=-Dblas=openblas + - -Csetup-args=-Dlapack=openblas +# {% if sdk == 'android' %} + # pythran 0.18.1's pythonic/types/ndarray.hpp has an ill-formed ref-qualifier + # overload that the newer NDK clang rejects (Apple clang accepts it, so iOS + # keeps pythran). pythran is an optional scipy build dep — disable it on + # Android; the affected modules fall back to their cython/numpy versions. + - -Csetup-args=-Duse-pythran=false # {% endif %} - -Csetup-args=--cross-file - -Csetup-args={MESON_CROSS_FILE} diff --git a/recipes/scipy/patches/accelerate-ios.patch b/recipes/scipy/patches/accelerate-ios.patch deleted file mode 100644 index 8086807..0000000 --- a/recipes/scipy/patches/accelerate-ios.patch +++ /dev/null @@ -1,34 +0,0 @@ -# scipy's Accelerate gate (scipy/meson.build) only computes `macOS13_3_or_later` -# when host_machine.system() == 'darwin', then errors out for blas=accelerate -# otherwise. forge's iOS cross-file sets system='iphoneos'/'iphonesimulator', so -# the gate stays false and the build aborts. The Accelerate framework is the same -# on iOS, and its modern (macOS-13.3-equivalent) LAPACK is available from iOS 16.4 -# onward — well below the active iOS SDK — so enable the new-Accelerate path for -# iOS systems too. Only affects iOS (Android uses blas=openblas). ---- a/scipy/meson.build -+++ b/scipy/meson.build -@@ -268,6 +268,10 @@ - r = run_command('xcrun', '-sdk', 'macosx', '--show-sdk-version', check: true) - sdkVersion = r.stdout().strip() - macOS13_3_or_later = sdkVersion.version_compare('>=13.3') -+elif host_machine.system() in ['ios', 'iphoneos', 'iphonesimulator'] -+ # iOS Accelerate provides the modern (macOS 13.3-equivalent) LAPACK from -+ # iOS 16.4 onward; the active iOS SDK is well past that, so enable it. -+ macOS13_3_or_later = true - endif - - _args_blas_lp64 = [] ---- a/scipy/_build_utils/src/scipy_blas_defines.h -+++ b/scipy/_build_utils/src/scipy_blas_defines.h -@@ -16,8 +16,10 @@ - - - #ifdef ACCELERATE_NEW_LAPACK -- #if __MAC_OS_X_VERSION_MAX_ALLOWED < 130300 -+ #if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED < 130300 - #error "Accelerate support is only available with macOS 13.3 SDK or later" -+ #elif defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 160400 -+ #error "Accelerate support is only available with iOS 16.4 SDK or later" - #else - /* New Accelerate suffix is always $NEWLAPACK or $NEWLAPACK$ILP64 (no underscore appended) */ - #ifdef HAVE_BLAS_ILP64 diff --git a/recipes/scipy/patches/android-bionic-clog-cpow.patch b/recipes/scipy/patches/android-bionic-clog-cpow.patch new file mode 100644 index 0000000..4ea9b37 --- /dev/null +++ b/recipes/scipy/patches/android-bionic-clog-cpow.patch @@ -0,0 +1,28 @@ +# Android bionic only exposes the C99 complex functions clog() and cpow() from +# API level 26 (verified via llvm-nm: API 24's libm has cabs/carg/cexp/csin/ +# ccos/csqrt but not clog/cpow). scipy.special's _complexstuff.h assumes libc +# provides them. Define them for __ANDROID_API__ < 26 using functions present +# since API 24. No-op on iOS and Android >= 26 (so harmless to apply always). +--- a/scipy/special/_complexstuff.h ++++ b/scipy/special/_complexstuff.h +@@ -16,6 +16,20 @@ + #include + #include "scipy_complex_support.h" + ++/* bionic (Android libc) only declares the C99 complex log/pow from API level ++ * 26; scipy.special assumes libc provides them. Supply them for older Android ++ * targets in terms of functions bionic has had since API 24 (log/cabs/carg/ ++ * cexp). The guard makes this a no-op on iOS and on Android API >= 26. */ ++#if defined(__ANDROID__) && __ANDROID_API__ < 26 ++#include ++static inline double complex clog(double complex z) { ++ return log(cabs(z)) + carg(z) * I; ++} ++static inline double complex cpow(double complex x, double complex y) { ++ return cexp(y * clog(x)); ++} ++#endif ++ + #if defined(_MSC_VER) + typedef _Dcomplex _scipy_dz; + #else diff --git a/recipes/scipy/patches/android-ducc-no-affinity.patch b/recipes/scipy/patches/android-ducc-no-affinity.patch new file mode 100644 index 0000000..d41ee3c --- /dev/null +++ b/recipes/scipy/patches/android-ducc-no-affinity.patch @@ -0,0 +1,34 @@ +# scipy.fft vendors ducc0, whose threading.cc pins threads via the glibc-only +# pthread_{get,set}affinity_np, guarded on `__linux__ && _GNU_SOURCE`. Android is +# __linux__ but bionic lacks those _np functions, so the guard wrongly fires. +# Exclude __ANDROID__ so ducc falls back to its no-affinity path (CPU pinning is +# only a perf optimization). No-op on iOS (not __linux__) and desktop Linux. +--- a/subprojects/duccfft/ducc0/infra/threading.cc ++++ b/subprojects/duccfft/ducc0/infra/threading.cc +@@ -69,7 +69,7 @@ + #include + #if __has_include() + #include +-#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) ++#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) && !defined(__ANDROID__) + #include + #endif + #endif +@@ -111,7 +111,7 @@ + { + static const size_t available_hardware_threads_ = []() + { +-#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) ++#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) && !defined(__ANDROID__) + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + pthread_getaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); +@@ -202,7 +202,7 @@ + bool empty() const { return size_==0; } + }; + +-#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) ++#if __has_include() && defined(__linux__) && defined(_GNU_SOURCE) && !defined(__ANDROID__) + static void do_pinning(int ithread) + { + if (pin_info()==-1) return; diff --git a/recipes/scipy/patches/android-x86_64-boost-longdouble.patch b/recipes/scipy/patches/android-x86_64-boost-longdouble.patch new file mode 100644 index 0000000..ede4822 --- /dev/null +++ b/recipes/scipy/patches/android-x86_64-boost-longdouble.patch @@ -0,0 +1,24 @@ +# Android x86_64 (bionic) uses 128-bit IEEE-quad long double (LDBL_MANT_DIG==113), +# unlike desktop x86_64's 80-bit extended. scipy's vendored boost.math selects its +# long-double bit layout by CPU macro: the `defined(__x86_64__)` branch assumes the +# 80-bit format and hard-asserts LDBL_MANT_DIG==64, which fails on android x86_64. +# boost already has the correct path right below it — `#elif (LDBL_MANT_DIG == 113)` +# ("all other processors", IEEE binary128). Gate the x86 branch on ==64 so android +# x86_64 falls through to that 113 path. Desktop x86_64 (==64) is unaffected; arm/iOS +# never match the x86 branch. (arm64 long double is also 113 but takes the native +# path; armeabi-v7a long double is 64-bit/double → the ==53 branch.) +--- a/subprojects/boost_math/math/include/boost/math/special_functions/detail/fp_traits.hpp ++++ b/subprojects/boost_math/math/include/boost/math/special_functions/detail/fp_traits.hpp +@@ -305,9 +305,10 @@ + + // long double (>64 bits), x86 and x64 ----------------------------------------- + +-#elif defined(__i386) || defined(__i386__) || defined(_M_IX86) \ ++#elif (defined(__i386) || defined(__i386__) || defined(_M_IX86) \ + || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64) \ +- || defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) ++ || defined(__x86_64) || defined(__x86_64__) || defined(_M_X64)) \ ++ && (LDBL_MANT_DIG == 64) + + // Intel extended double precision format (80 bits) + diff --git a/src/forge/build.py b/src/forge/build.py index 1c7da0b..574a3de 100644 --- a/src/forge/build.py +++ b/src/forge/build.py @@ -261,7 +261,10 @@ def prepare(self, clean=True): # host_build deps install into the cross env like host deps (so the build # can link them), but are NOT promoted to the wheel's Requires-Dist (that # loop below only walks "host"). For statically-linked native libs. - log(self.log_file, f"\n[{self.cross_venv}] Install forge host_build requirements") + log( + self.log_file, + f"\n[{self.cross_venv}] Install forge host_build requirements", + ) self.install_requirements("host_build") self.fix_host_tool_shims() @@ -461,8 +464,7 @@ def compile_env(self, **kwargs) -> dict[str, str]: # Wheels bundle their .pc files in assorted spots under # site-packages: pybind11 -> /share/pkgconfig, while # numpy ships numpy/_core/lib/pkgconfig/numpy.pc. Search the - # known locations so meson's pkg-config method resolves them - # (e.g. scipy's `dependency('numpy')`, which uses numpy.pc). + # known locations so meson's pkg-config method resolves them. for pattern in ( "*/share/pkgconfig", "*/lib/pkgconfig", From e0ac8aacd9daf3aeff8a0bbde85c875d3f7f2cdd Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 1 Jul 2026 01:25:25 +0200 Subject: [PATCH 5/5] more tests --- recipes/scipy/test_scipy.py | 116 ++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/recipes/scipy/test_scipy.py b/recipes/scipy/test_scipy.py index e5eafe1..d04f83d 100644 --- a/recipes/scipy/test_scipy.py +++ b/recipes/scipy/test_scipy.py @@ -1,9 +1,117 @@ -def test_basic(): - """Load scipy and exercise the BLAS/LAPACK backend (Accelerate on iOS).""" - import numpy as np +import numpy as np + + +def test_linalg_solve(): + """linalg.solve -> LAPACK getrf/getrs through OpenBLAS.""" from scipy import linalg a = np.array([[3.0, 1.0], [1.0, 2.0]]) b = np.array([9.0, 8.0]) - x = linalg.solve(a, b) # LAPACK getrf/getrs -> proves BLAS/LAPACK works + x = linalg.solve(a, b) assert np.allclose(a @ x, b) + assert np.allclose(x, [2.0, 3.0]) + + +def test_linalg_svd(): + """linalg.svd -> LAPACK gesdd; reconstruct the matrix from U S Vt.""" + from scipy import linalg + + m = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + u, s, vt = linalg.svd(m, full_matrices=False) + assert np.allclose(u @ np.diag(s) @ vt, m) + assert s[0] > s[1] > 0 + + +def test_linalg_eigh(): + """eigvalsh -> symmetric eigensolver (LAPACK syevd) through OpenBLAS.""" + from scipy import linalg + + a = np.array([[2.0, 1.0], [1.0, 2.0]]) + w = np.sort(linalg.eigvalsh(a)) + assert np.allclose(w, [1.0, 3.0]) + + +def test_linalg_cholesky(): + """cholesky -> LAPACK potrf of an SPD matrix; L @ L.T reconstructs it.""" + from scipy import linalg + + a = np.array([[4.0, 2.0], [2.0, 3.0]]) + L = linalg.cholesky(a, lower=True) + assert np.allclose(L @ L.T, a) + + +def test_fft(): + """scipy.fft (vendored ducc) -> fft/ifft roundtrip; DC term equals the sum.""" + from scipy import fft + + x = np.array([1.0, 2.0, 1.0, -1.0, 1.5, 0.0, 0.0, 0.0]) + y = fft.fft(x) + assert np.allclose(y[0].real, x.sum()) + assert np.allclose(fft.ifft(y).real, x) + + +def test_special_real(): + """special.gamma(0.5) == sqrt(pi); gamma(5) == 4!; erf(0) == 0.""" + import math + from scipy import special + + assert abs(special.gamma(0.5) - math.sqrt(math.pi)) < 1e-12 + assert abs(special.gamma(5.0) - 24.0) < 1e-9 + assert abs(special.erf(0.0)) < 1e-15 + + +def test_special_complex(): + """Complex special function -> exercises scipy.special's C99 complex math + (clog/cpow in _complexstuff.h). loggamma(1) == 0; loggamma(1+1j) is finite.""" + from scipy import special + + assert abs(special.loggamma(1.0)) < 1e-12 + z = special.loggamma(1 + 1j) + assert np.isfinite(z.real) and np.isfinite(z.imag) + + +def test_sparse_spsolve(): + """sparse CSC matrix + sparse.linalg.spsolve (SuperLU).""" + from scipy import sparse + from scipy.sparse.linalg import spsolve + + a = sparse.csc_matrix([[3.0, 1.0], [1.0, 2.0]]) + b = np.array([9.0, 8.0]) + x = spsolve(a, b) + assert np.allclose(x, [2.0, 3.0]) + + +def test_optimize(): + """optimize.minimize (BFGS) on a quadratic -> minimum at (1, 2.5).""" + from scipy import optimize + + res = optimize.minimize( + lambda p: (p[0] - 1.0) ** 2 + (p[1] - 2.5) ** 2, [0.0, 0.0], method="BFGS" + ) + assert res.success + assert np.allclose(res.x, [1.0, 2.5], atol=1e-4) + + +def test_integrate(): + """integrate.quad of x^2 over [0, 1] == 1/3 (QUADPACK, f2c).""" + from scipy import integrate + + val, _ = integrate.quad(lambda x: x**2, 0.0, 1.0) + assert abs(val - 1.0 / 3.0) < 1e-10 + + +def test_interpolate(): + """interpolate.interp1d -> linear interp of a known line (FITPACK, f2c).""" + from scipy import interpolate + + x = np.array([0.0, 1.0, 2.0]) + f = interpolate.interp1d(x, 2.0 * x + 1.0) + assert abs(float(f(0.5)) - 2.0) < 1e-12 + + +def test_stats(): + """stats.norm -> standard-normal cdf(0) == 0.5, pdf(0) == 1/sqrt(2*pi).""" + from scipy import stats + + assert abs(stats.norm.cdf(0.0) - 0.5) < 1e-12 + assert abs(stats.norm.pdf(0.0) - 1.0 / np.sqrt(2.0 * np.pi)) < 1e-12