mxlpy-native 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. mxlpy_native-0.3.0/LICENSE +21 -0
  2. mxlpy_native-0.3.0/PKG-INFO +58 -0
  3. mxlpy_native-0.3.0/README.md +36 -0
  4. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/mxn_solvers.c +5 -0
  5. mxlpy_native-0.3.0/pyproject.toml +58 -0
  6. mxlpy_native-0.3.0/setup.py +113 -0
  7. mxlpy_native-0.3.0/src/mxlpy_native/__init__.py +76 -0
  8. mxlpy_native-0.3.0/src/mxlpy_native/__main__.py +35 -0
  9. mxlpy_native-0.3.0/src/mxlpy_native.egg-info/PKG-INFO +58 -0
  10. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/src/mxlpy_native.egg-info/SOURCES.txt +7 -1
  11. mxlpy_native-0.3.0/src/mxlpy_native.egg-info/requires.txt +1 -0
  12. mxlpy_native-0.3.0/tests/test_build_errors.py +48 -0
  13. mxlpy_native-0.3.0/tests/test_solvers.py +60 -0
  14. mxlpy_native-0.3.0/tests/test_vendor.py +32 -0
  15. mxlpy_native-0.2.0/PKG-INFO +0 -6
  16. mxlpy_native-0.2.0/README.md +0 -5
  17. mxlpy_native-0.2.0/pyproject.toml +0 -18
  18. mxlpy_native-0.2.0/setup.py +0 -26
  19. mxlpy_native-0.2.0/src/mxlpy_native/__init__.py +0 -18
  20. mxlpy_native-0.2.0/src/mxlpy_native.egg-info/PKG-INFO +0 -6
  21. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/MANIFEST.in +0 -0
  22. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/HAIRER-LICENCE.txt +0 -0
  23. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/SCIPY-LICENCE.txt +0 -0
  24. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/VENDORED +0 -0
  25. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/blaslapack_declarations.h +0 -0
  26. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/dc_decsol.c +0 -0
  27. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/decsol.c +0 -0
  28. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/f2c.h +0 -0
  29. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/libf2c_stubs.c +0 -0
  30. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/lsoda.c +0 -0
  31. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/lsoda.h +0 -0
  32. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/mxs_lapack.c +0 -0
  33. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/c/vendor/radau5.c +0 -0
  34. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/setup.cfg +0 -0
  35. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/src/mxlpy_native.egg-info/dependency_links.txt +0 -0
  36. {mxlpy_native-0.2.0 → mxlpy_native-0.3.0}/src/mxlpy_native.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [Marvin van Aalst, Anna Matuszyńska]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: mxlpy-native
3
+ Version: 0.3.0
4
+ Summary: Compiled ODE solvers for MxlPy's Native integrator
5
+ Author-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Computational-Biology-Aachen/mxlpy-native
8
+ Project-URL: Issues, https://github.com/Computational-Biology-Aachen/mxlpy-native/issues
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: C
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ License-File: c/vendor/HAIRER-LICENCE.txt
19
+ License-File: c/vendor/SCIPY-LICENCE.txt
20
+ Requires-Dist: llvmlite>=0.45
21
+ Dynamic: license-file
22
+
23
+ # mxlpy-native
24
+
25
+ The compiled Radau and LSODA solvers behind [MxlPy](https://github.com/Computational-Biology-Aachen/MxlPy)'s `Native` integrator, plus `llvmlite`, which compiles models to machine code for them. Install it through MxlPy rather than on its own:
26
+
27
+ ```bash
28
+ pip install "mxlpy[native]"
29
+ ```
30
+
31
+ ## Platforms
32
+
33
+ Wheels are published for Linux (x86_64, aarch64; glibc 2.28+), macOS (arm64, 12+) and Windows (amd64, arm64), the platforms `llvmlite` has wheels for. On anything else, `mxlpy[native]` installs the WASM backend (`wasmtime`) instead, which runs the same solvers without a compiler, a little slower.
34
+
35
+ If pip has to build from the sdist anyway, it needs a C11 compiler (GCC 8+, Clang 7+ or MSVC 2019 16.8+), and nothing else: no Fortran, BLAS/LAPACK or Python headers. The build says what to install if the compiler is missing or fails.
36
+
37
+ ## Troubleshooting
38
+
39
+ ```bash
40
+ python -m mxlpy_native
41
+ ```
42
+
43
+ prints the installed version, the solver ABI, and whether the compiled solvers and `llvmlite` load, with a fix for each failure.
44
+
45
+ ## Development
46
+
47
+ ```bash
48
+ uv sync # builds the extension in place
49
+ uv run pytest
50
+ ```
51
+
52
+ `c/vendor/` is copied from [mxl-solvers](https://github.com/Computational-Biology-Aachen/mxl-solvers) by its `scripts/vendor.sh`. `vendor/VENDORED` records the commit and file hashes, which `tests/test_vendor.py` checks. Fix solvers there, not here. `c/mxn_solvers.c` is the glue MxlPy calls, compiled unchanged into the wheel and into MxlPy's `solvers.wasm` (`scripts/build_native_wasm.sh` in MxlPy).
53
+
54
+ When an exported function's signature or meaning changes, bump `MXN_ABI_VERSION` in `c/mxn_solvers.c`, `ABI_VERSION` in `src/mxlpy_native/__init__.py` and the ABI MxlPy expects, then release both. MxlPy refuses a mismatched pair with an upgrade hint instead of crashing.
55
+
56
+ ## Releasing
57
+
58
+ Bump `version` in `pyproject.toml` and publish a GitHub release `v<version>`. The release workflow builds the wheels with cibuildwheel plus an sdist and uploads them to PyPI via trusted publishing.
@@ -0,0 +1,36 @@
1
+ # mxlpy-native
2
+
3
+ The compiled Radau and LSODA solvers behind [MxlPy](https://github.com/Computational-Biology-Aachen/MxlPy)'s `Native` integrator, plus `llvmlite`, which compiles models to machine code for them. Install it through MxlPy rather than on its own:
4
+
5
+ ```bash
6
+ pip install "mxlpy[native]"
7
+ ```
8
+
9
+ ## Platforms
10
+
11
+ Wheels are published for Linux (x86_64, aarch64; glibc 2.28+), macOS (arm64, 12+) and Windows (amd64, arm64), the platforms `llvmlite` has wheels for. On anything else, `mxlpy[native]` installs the WASM backend (`wasmtime`) instead, which runs the same solvers without a compiler, a little slower.
12
+
13
+ If pip has to build from the sdist anyway, it needs a C11 compiler (GCC 8+, Clang 7+ or MSVC 2019 16.8+), and nothing else: no Fortran, BLAS/LAPACK or Python headers. The build says what to install if the compiler is missing or fails.
14
+
15
+ ## Troubleshooting
16
+
17
+ ```bash
18
+ python -m mxlpy_native
19
+ ```
20
+
21
+ prints the installed version, the solver ABI, and whether the compiled solvers and `llvmlite` load, with a fix for each failure.
22
+
23
+ ## Development
24
+
25
+ ```bash
26
+ uv sync # builds the extension in place
27
+ uv run pytest
28
+ ```
29
+
30
+ `c/vendor/` is copied from [mxl-solvers](https://github.com/Computational-Biology-Aachen/mxl-solvers) by its `scripts/vendor.sh`. `vendor/VENDORED` records the commit and file hashes, which `tests/test_vendor.py` checks. Fix solvers there, not here. `c/mxn_solvers.c` is the glue MxlPy calls, compiled unchanged into the wheel and into MxlPy's `solvers.wasm` (`scripts/build_native_wasm.sh` in MxlPy).
31
+
32
+ When an exported function's signature or meaning changes, bump `MXN_ABI_VERSION` in `c/mxn_solvers.c`, `ABI_VERSION` in `src/mxlpy_native/__init__.py` and the ABI MxlPy expects, then release both. MxlPy refuses a mismatched pair with an upgrade hint instead of crashing.
33
+
34
+ ## Releasing
35
+
36
+ Bump `version` in `pyproject.toml` and publish a GitHub release `v<version>`. The release workflow builds the wheels with cibuildwheel plus an sdist and uploads them to PyPI via trusted publishing.
@@ -15,6 +15,11 @@
15
15
  #define MXN_API
16
16
  #endif
17
17
 
18
+ /* Bump whenever an exported signature or its meaning changes, together with ABI_VERSION
19
+ * in mxlpy_native/__init__.py and the version MxlPy's Native integrator expects. */
20
+ #define MXN_ABI_VERSION 1
21
+ MXN_API int mxn_abi_version(void) { return MXN_ABI_VERSION; }
22
+
18
23
  extern int radau5_(integer *n, U_fp fcn, doublereal *x, doublereal *y, doublereal *xend, doublereal *h,
19
24
  doublereal *rtol, doublereal *atol, integer *itol, U_fp jac, integer *ijac, integer *mljac,
20
25
  integer *mujac, U_fp mas, integer *imas, integer *mlmas, integer *mumas, U_fp solout,
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=77", "wheel"]
4
+
5
+ [project]
6
+ authors = [{ email = "marvin.vanaalst@gmail.com", name = "Marvin van Aalst" }]
7
+ classifiers = [
8
+ "Operating System :: MacOS",
9
+ "Operating System :: Microsoft :: Windows",
10
+ "Operating System :: POSIX :: Linux",
11
+ "Programming Language :: C",
12
+ "Programming Language :: Python :: 3",
13
+ "Topic :: Scientific/Engineering",
14
+ ]
15
+ # llvmlite JIT-compiles models for these solvers. It only has wheels for linux
16
+ # x86_64/aarch64, macOS arm64 and windows amd64/arm64; MxlPy's `native` extra
17
+ # installs the WASM backend everywhere else, so this is never pulled in there.
18
+ dependencies = ["llvmlite>=0.45"]
19
+ description = "Compiled ODE solvers for MxlPy's Native integrator"
20
+ license = "MIT"
21
+ license-files = ["LICENSE", "c/vendor/*LICENCE*.txt"]
22
+ name = "mxlpy-native"
23
+ readme = "README.md"
24
+ requires-python = ">=3.12"
25
+ version = "0.3.0"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/Computational-Biology-Aachen/mxlpy-native"
29
+ Issues = "https://github.com/Computational-Biology-Aachen/mxlpy-native/issues"
30
+
31
+ [dependency-groups]
32
+ dev = ["pytest>=8", "ruff>=0.9", "setuptools>=77"]
33
+
34
+ [tool.cibuildwheel]
35
+ # One abi3 wheel per platform covers Python 3.12+.
36
+ build = "cp312-*"
37
+ # Only platforms llvmlite publishes wheels for.
38
+ skip = ["*-musllinux_*", "*-win32", "*-manylinux_i686", "*-macosx_x86_64"]
39
+ test-command = "python -m mxlpy_native"
40
+
41
+ [tool.cibuildwheel.macos]
42
+ environment = { MACOSX_DEPLOYMENT_TARGET = "12.0" }
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+
47
+ [tool.ruff.lint]
48
+ select = ["ALL"]
49
+ ignore = ["COM812", "D203", "D213", "S101", "T201", "PLR2004"]
50
+
51
+ [tool.ruff.lint.per-file-ignores]
52
+ "tests/*" = ["D", "INP001", "ANN"]
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
56
+
57
+ [tool.uv]
58
+ exclude-newer = "3 days"
@@ -0,0 +1,113 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """Build the solver as one stable-ABI extension, so a wheel covers all Python versions.
4
+
5
+ Wheels are published for the common platforms, so this only runs when pip has to build
6
+ from the sdist. That needs a C11 compiler and nothing else: no Fortran, BLAS or Python
7
+ headers. When it fails, the error says what to install instead of showing a raw
8
+ compiler traceback.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import platform
14
+ import shutil
15
+ import sys
16
+
17
+ from setuptools import Extension, setup
18
+ from setuptools.command.build_ext import build_ext
19
+ from setuptools.errors import CCompilerError, ExecError, PlatformError
20
+
21
+ _NO_COMPILER_HINTS = {
22
+ "linux": (
23
+ " Debian/Ubuntu: sudo apt install build-essential\n"
24
+ " Fedora/RHEL: sudo dnf install gcc\n"
25
+ " Alpine: apk add build-base\n"
26
+ " conda/pixi: conda install -c conda-forge c-compiler"
27
+ ),
28
+ "darwin": " macOS: xcode-select --install",
29
+ "win32": (
30
+ ' Windows: install "Build Tools for Visual Studio" (2019 16.8 or newer)\n'
31
+ ' with the "Desktop development with C++" workload'
32
+ ),
33
+ }
34
+ _ALTERNATIVE = (
35
+ "If you can't install a compiler, use the WASM backend instead, which needs none:\n"
36
+ ' pip install "mxlpy[native-wasm]"'
37
+ )
38
+
39
+
40
+ def _platform() -> str:
41
+ return f"{sys.platform} {platform.machine()}, Python {platform.python_version()}"
42
+
43
+
44
+ def _no_compiler(detail: str) -> str:
45
+ hints = _NO_COMPILER_HINTS.get(sys.platform, " Install a C11 compiler such as gcc or clang.")
46
+ return (
47
+ f"mxlpy-native: no C compiler found ({detail}).\n\n"
48
+ f"There is no prebuilt mxlpy-native wheel for {_platform()}, so pip is building it\n"
49
+ "from source, which needs a C11 compiler (no Fortran, BLAS or Python headers):\n"
50
+ f"{hints}\n\n{_ALTERNATIVE}"
51
+ )
52
+
53
+
54
+ def _compile_failed(detail: str) -> str:
55
+ return (
56
+ f"mxlpy-native: compiling the solvers failed ({detail}).\n\n"
57
+ f"There is no prebuilt mxlpy-native wheel for {_platform()}, so pip is building it\n"
58
+ "from source. The solvers need C11 support: GCC 8+, Clang 7+ or MSVC 2019 16.8+.\n"
59
+ "Check that your compiler is recent enough and that CC points to it. If it is, please\n"
60
+ "report this at https://github.com/Computational-Biology-Aachen/mxlpy-native/issues\n"
61
+ f"with the compiler output above.\n\n{_ALTERNATIVE}"
62
+ )
63
+
64
+
65
+ class _BuildError(PlatformError):
66
+ """A build error that already explains itself."""
67
+
68
+
69
+ class BuildExt(build_ext):
70
+ """build_ext that explains missing or unsuitable compilers."""
71
+
72
+ def run(self) -> None:
73
+ """Explain a missing MSVC."""
74
+ try:
75
+ super().run()
76
+ except _BuildError:
77
+ raise
78
+ except PlatformError as e: # MSVC isn't installed
79
+ raise _BuildError(_no_compiler(str(e))) from e
80
+
81
+ def build_extension(self, ext: Extension) -> None:
82
+ """Explain a missing or failing compiler."""
83
+ compiler = getattr(self.compiler, "compiler_so", None)
84
+ if compiler and shutil.which(compiler[0]) is None:
85
+ raise _BuildError(_no_compiler(f"{compiler[0]!r} is not on PATH"))
86
+ try:
87
+ super().build_extension(ext)
88
+ except (ExecError, FileNotFoundError) as e: # the compiler couldn't be started
89
+ raise _BuildError(_no_compiler(str(e))) from e
90
+ except CCompilerError as e: # it ran, but compiling or linking failed
91
+ msg = _compile_failed("the compiler returned an error, see its output above")
92
+ raise _BuildError(msg) from e
93
+
94
+
95
+ vendored = ["radau5.c", "dc_decsol.c", "decsol.c", "libf2c_stubs.c", "lsoda.c", "mxs_lapack.c"]
96
+ setup(
97
+ ext_modules=[
98
+ Extension(
99
+ "mxlpy_native._solvers",
100
+ sources=["c/mxn_solvers.c", *(f"c/vendor/{s}" for s in vendored)],
101
+ include_dirs=["c/vendor"],
102
+ py_limited_api=True,
103
+ define_macros=[("Py_LIMITED_API", "0x030C0000")],
104
+ # lsoda.c needs C11 (restrict, compound literals), which MSVC only enables on request.
105
+ extra_compile_args=["/std:c11"]
106
+ if sys.platform == "win32"
107
+ else ["-std=gnu17", "-w", "-O2"],
108
+ libraries=[] if sys.platform == "win32" else ["m"],
109
+ )
110
+ ],
111
+ cmdclass={"build_ext": BuildExt},
112
+ options={"bdist_wheel": {"py_limited_api": "cp312"}},
113
+ )
@@ -0,0 +1,76 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """Compiled solvers for MxlPy's Native integrator.
4
+
5
+ `load()` returns the solver library for ctypes. Everything that can go wrong with an
6
+ installation raises `NativeInstallError` with a message saying how to fix it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ from functools import lru_cache
13
+ from importlib.machinery import EXTENSION_SUFFIXES
14
+ from importlib.metadata import PackageNotFoundError, version
15
+ from pathlib import Path
16
+
17
+ __all__ = ["ABI_VERSION", "NativeInstallError", "__version__", "library_path", "load"]
18
+
19
+ # Must match MXN_ABI_VERSION in c/mxn_solvers.c. MxlPy checks it before calling into
20
+ # the library, so a mismatched pair fails with an upgrade hint instead of crashing.
21
+ ABI_VERSION = 1
22
+
23
+ try:
24
+ __version__ = version("mxlpy-native")
25
+ except PackageNotFoundError: # running from a source tree without installing
26
+ __version__ = "0+unknown"
27
+
28
+ _REINSTALL = 'pip install --force-reinstall --no-cache-dir "mxlpy-native"'
29
+
30
+
31
+ class NativeInstallError(ImportError):
32
+ """mxlpy-native is installed, but its compiled solvers can't be used."""
33
+
34
+
35
+ def library_path() -> Path:
36
+ """Return the path of the compiled solvers, for loading with ctypes."""
37
+ here = Path(__file__).parent
38
+ for suffix in EXTENSION_SUFFIXES:
39
+ if (path := here / f"_solvers{suffix}").exists():
40
+ return path
41
+ found = sorted(p.name for p in here.glob("_solvers*"))
42
+ detail = f"found only {found}, none built for this Python" if found else "none found"
43
+ msg = (
44
+ f"mxlpy-native {__version__} is installed without its compiled solvers in {here} "
45
+ f"({detail}).\nThis usually means a source checkout was installed without building "
46
+ f"it, or the wheel belongs to a different platform. Reinstall it with:\n {_REINSTALL}"
47
+ )
48
+ raise NativeInstallError(msg)
49
+
50
+
51
+ @lru_cache(maxsize=1)
52
+ def load() -> ctypes.CDLL:
53
+ """Load the compiled solvers and check that they match this package."""
54
+ path = library_path()
55
+ try:
56
+ lib = ctypes.CDLL(str(path))
57
+ except OSError as e:
58
+ msg = (
59
+ f"mxlpy-native {__version__} can't load its compiled solvers {path}: {e}\n"
60
+ "The file may be corrupt or built for another platform or architecture. "
61
+ f"Reinstall it with:\n {_REINSTALL}"
62
+ )
63
+ raise NativeInstallError(msg) from e
64
+ try:
65
+ lib.mxn_abi_version.restype = ctypes.c_int
66
+ abi = lib.mxn_abi_version()
67
+ except AttributeError:
68
+ abi = 0 # built before the ABI was versioned
69
+ if abi != ABI_VERSION:
70
+ msg = (
71
+ f"mxlpy-native {__version__}'s compiled solvers {path} have ABI {abi}, but its "
72
+ f"Python code expects {ABI_VERSION}. The build is stale; rebuild it with:\n"
73
+ f" {_REINSTALL}"
74
+ )
75
+ raise NativeInstallError(msg)
76
+ return lib
@@ -0,0 +1,35 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """Diagnose an installation: python -m mxlpy_native."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import platform
8
+ import sys
9
+
10
+ import mxlpy_native
11
+
12
+
13
+ def main() -> int:
14
+ """Print what's installed and whether the compiled solvers load."""
15
+ print(f"mxlpy-native {mxlpy_native.__version__} (ABI {mxlpy_native.ABI_VERSION})")
16
+ print(f"platform {sys.platform} {platform.machine()}, Python {platform.python_version()}")
17
+ ok = True
18
+ try:
19
+ print(f"solvers {mxlpy_native.load()._name}: ok") # noqa: SLF001
20
+ except mxlpy_native.NativeInstallError as e:
21
+ print(f"solvers FAILED\n{e}")
22
+ ok = False
23
+ try:
24
+ import llvmlite.binding as llvm # noqa: PLC0415
25
+
26
+ llvm.initialize_native_target()
27
+ print(f"llvmlite {llvm.llvm_version_info} targeting {llvm.get_default_triple()}: ok")
28
+ except Exception as e: # noqa: BLE001
29
+ print(f"llvmlite FAILED: {e}")
30
+ ok = False
31
+ return 0 if ok else 1
32
+
33
+
34
+ if __name__ == "__main__":
35
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: mxlpy-native
3
+ Version: 0.3.0
4
+ Summary: Compiled ODE solvers for MxlPy's Native integrator
5
+ Author-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Computational-Biology-Aachen/mxlpy-native
8
+ Project-URL: Issues, https://github.com/Computational-Biology-Aachen/mxlpy-native/issues
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: C
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ License-File: c/vendor/HAIRER-LICENCE.txt
19
+ License-File: c/vendor/SCIPY-LICENCE.txt
20
+ Requires-Dist: llvmlite>=0.45
21
+ Dynamic: license-file
22
+
23
+ # mxlpy-native
24
+
25
+ The compiled Radau and LSODA solvers behind [MxlPy](https://github.com/Computational-Biology-Aachen/MxlPy)'s `Native` integrator, plus `llvmlite`, which compiles models to machine code for them. Install it through MxlPy rather than on its own:
26
+
27
+ ```bash
28
+ pip install "mxlpy[native]"
29
+ ```
30
+
31
+ ## Platforms
32
+
33
+ Wheels are published for Linux (x86_64, aarch64; glibc 2.28+), macOS (arm64, 12+) and Windows (amd64, arm64), the platforms `llvmlite` has wheels for. On anything else, `mxlpy[native]` installs the WASM backend (`wasmtime`) instead, which runs the same solvers without a compiler, a little slower.
34
+
35
+ If pip has to build from the sdist anyway, it needs a C11 compiler (GCC 8+, Clang 7+ or MSVC 2019 16.8+), and nothing else: no Fortran, BLAS/LAPACK or Python headers. The build says what to install if the compiler is missing or fails.
36
+
37
+ ## Troubleshooting
38
+
39
+ ```bash
40
+ python -m mxlpy_native
41
+ ```
42
+
43
+ prints the installed version, the solver ABI, and whether the compiled solvers and `llvmlite` load, with a fix for each failure.
44
+
45
+ ## Development
46
+
47
+ ```bash
48
+ uv sync # builds the extension in place
49
+ uv run pytest
50
+ ```
51
+
52
+ `c/vendor/` is copied from [mxl-solvers](https://github.com/Computational-Biology-Aachen/mxl-solvers) by its `scripts/vendor.sh`. `vendor/VENDORED` records the commit and file hashes, which `tests/test_vendor.py` checks. Fix solvers there, not here. `c/mxn_solvers.c` is the glue MxlPy calls, compiled unchanged into the wheel and into MxlPy's `solvers.wasm` (`scripts/build_native_wasm.sh` in MxlPy).
53
+
54
+ When an exported function's signature or meaning changes, bump `MXN_ABI_VERSION` in `c/mxn_solvers.c`, `ABI_VERSION` in `src/mxlpy_native/__init__.py` and the ABI MxlPy expects, then release both. MxlPy refuses a mismatched pair with an upgrade hint instead of crashing.
55
+
56
+ ## Releasing
57
+
58
+ Bump `version` in `pyproject.toml` and publish a GitHub release `v<version>`. The release workflow builds the wheels with cibuildwheel plus an sdist and uploads them to PyPI via trusted publishing.
@@ -1,3 +1,4 @@
1
+ LICENSE
1
2
  MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
@@ -16,7 +17,12 @@ c/vendor/lsoda.h
16
17
  c/vendor/mxs_lapack.c
17
18
  c/vendor/radau5.c
18
19
  src/mxlpy_native/__init__.py
20
+ src/mxlpy_native/__main__.py
19
21
  src/mxlpy_native.egg-info/PKG-INFO
20
22
  src/mxlpy_native.egg-info/SOURCES.txt
21
23
  src/mxlpy_native.egg-info/dependency_links.txt
22
- src/mxlpy_native.egg-info/top_level.txt
24
+ src/mxlpy_native.egg-info/requires.txt
25
+ src/mxlpy_native.egg-info/top_level.txt
26
+ tests/test_build_errors.py
27
+ tests/test_solvers.py
28
+ tests/test_vendor.py
@@ -0,0 +1 @@
1
+ llvmlite>=0.45
@@ -0,0 +1,48 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """Building from source without a working compiler explains what to install."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ ROOT = Path(__file__).parents[1]
16
+
17
+ pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="MSVC ignores CC")
18
+
19
+
20
+ def _build(tmp_path: Path, cc: str) -> str:
21
+ for name in ("setup.py", "pyproject.toml", "c", "src"):
22
+ src = ROOT / name
23
+ if src.is_dir():
24
+ shutil.copytree(src, tmp_path / name, ignore=shutil.ignore_patterns("*.so", "*.pyd"))
25
+ else:
26
+ shutil.copy(src, tmp_path / name)
27
+ proc = subprocess.run(
28
+ [sys.executable, "setup.py", "-q", "build_ext", "--inplace"],
29
+ cwd=tmp_path,
30
+ env={**os.environ, "CC": cc},
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+ assert proc.returncode != 0
36
+ return proc.stdout + proc.stderr
37
+
38
+
39
+ def test_missing_compiler(tmp_path: Path) -> None:
40
+ out = _build(tmp_path, "/nonexistent/cc")
41
+ assert "no C compiler found" in out
42
+ assert "mxlpy[native-wasm]" in out
43
+
44
+
45
+ def test_failing_compiler(tmp_path: Path) -> None:
46
+ out = _build(tmp_path, "false")
47
+ assert "compiling the solvers failed" in out
48
+ assert "C11" in out
@@ -0,0 +1,60 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """The compiled solvers load, match the Python package and solve dy/dt = -k y."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes as ct
8
+ import math
9
+
10
+ import pytest
11
+
12
+ import mxlpy_native
13
+
14
+ _DP = ct.POINTER(ct.c_double)
15
+ _MODEL_FN = ct.CFUNCTYPE(None, ct.c_int, ct.c_double, _DP, _DP, _DP)
16
+
17
+
18
+ @_MODEL_FN
19
+ def _decay(n: int, t: float, y: _DP, dydt: _DP, pars: _DP) -> None: # noqa: ARG001
20
+ for i in range(n):
21
+ dydt[i] = -pars[0] * y[i]
22
+
23
+
24
+ def _arr(*values: float) -> ct.Array:
25
+ return (ct.c_double * len(values))(*values)
26
+
27
+
28
+ def test_abi_matches() -> None:
29
+ assert mxlpy_native.load().mxn_abi_version() == mxlpy_native.ABI_VERSION
30
+
31
+
32
+ @pytest.mark.parametrize("method", ["radau", "lsoda"])
33
+ def test_solve_exponential_decay(method: str) -> None:
34
+ lib = mxlpy_native.load()
35
+ t_out = [0.0, 0.5, 1.0, 2.0]
36
+ y, pars, t, out = _arr(1.0, 2.0), _arr(0.7), _arr(*t_out), _arr(*[0.0] * 8)
37
+ lib.mxn_set_model.argtypes = [ct.c_void_p]
38
+ lib.mxn_set_model(ct.cast(_decay, ct.c_void_p))
39
+ head = (2, y, pars, ct.c_double(1e-10), ct.c_double(1e-10), 100_000)
40
+ if method == "radau":
41
+ rc = lib.mxn_radau(*head, 0, len(t_out), t, out)
42
+ assert rc == 1
43
+ else:
44
+ rc = lib.mxn_lsoda(*head, len(t_out), t, out)
45
+ assert rc == 2
46
+ for i, ti in enumerate(t_out):
47
+ assert out[2 * i] == pytest.approx(math.exp(-0.7 * ti), rel=1e-7)
48
+ assert out[2 * i + 1] == pytest.approx(2 * math.exp(-0.7 * ti), rel=1e-7)
49
+
50
+
51
+ def test_missing_library_explains_reinstall(monkeypatch: pytest.MonkeyPatch) -> None:
52
+ monkeypatch.setattr(mxlpy_native, "EXTENSION_SUFFIXES", [".nope"])
53
+ with pytest.raises(mxlpy_native.NativeInstallError, match="force-reinstall"):
54
+ mxlpy_native.library_path()
55
+
56
+
57
+ def test_diagnostics_pass() -> None:
58
+ from mxlpy_native.__main__ import main # noqa: PLC0415
59
+
60
+ assert main() == 0
@@ -0,0 +1,32 @@
1
+ # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
+
3
+ """The solvers in c/vendor must be exactly what mxl-solvers' vendor.sh copied.
4
+
5
+ Fix solvers in https://github.com/Computational-Biology-Aachen/mxl-solvers and re-run
6
+ its scripts/vendor.sh, rather than editing the copies here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from pathlib import Path
13
+
14
+ VENDOR = Path(__file__).parents[1] / "c" / "vendor"
15
+
16
+
17
+ def _manifest() -> dict[str, str]:
18
+ lines = (VENDOR / "VENDORED").read_text().splitlines()
19
+ return {
20
+ name: digest
21
+ for digest, name in (line.split() for line in lines if line and not line.startswith("#"))
22
+ }
23
+
24
+
25
+ def test_vendored_solvers_are_unmodified() -> None:
26
+ manifest = _manifest()
27
+ actual = {
28
+ p.name: hashlib.sha256(p.read_bytes()).hexdigest()
29
+ for p in VENDOR.iterdir()
30
+ if p.name != "VENDORED"
31
+ }
32
+ assert actual == manifest
@@ -1,6 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mxlpy-native
3
- Version: 0.2.0
4
- Summary: Compiled ODE solvers for MxlPy's Native integrator
5
- License-Expression: MIT
6
- Requires-Python: >=3.12
@@ -1,5 +0,0 @@
1
- # mxlpy-native
2
-
3
- The compiled Radau and LSODA solvers behind MxlPy's `Native` integrator. Install it with `pip install "mxlpy[native]"`, not on its own.
4
-
5
- `c/vendor/` is copied from [mxl-solvers](https://github.com/Computational-Biology-Aachen/mxl-solvers) by its `scripts/vendor.sh`. `vendor/VENDORED` records the commit and file hashes, which `tests/integrators/test_native_vendor.py` checks. Fix solvers there, not here. `c/mxn_solvers.c` is MxlPy's own glue, compiled unchanged into both the wheel and `solvers.wasm`.
@@ -1,18 +0,0 @@
1
- [build-system]
2
- build-backend = "setuptools.build_meta"
3
- requires = ["setuptools>=77", "wheel"]
4
-
5
- [project]
6
- description = "Compiled ODE solvers for MxlPy's Native integrator"
7
- license = "MIT"
8
- name = "mxlpy-native"
9
- requires-python = ">=3.12"
10
- version = "0.2.0"
11
-
12
- [tool.cibuildwheel]
13
- build = "cp312-*"
14
- skip = ["*-musllinux_*", "*-win32", "*-manylinux_i686"]
15
- test-command = "python -c \"import mxlpy_native; assert mxlpy_native.library_path().exists()\""
16
-
17
- [tool.setuptools.packages.find]
18
- where = ["src"]
@@ -1,26 +0,0 @@
1
- # Copyright 2025 Marvin van Aalst, Anna Matuszyńska # noqa: INP001
2
-
3
- """Build the solver as one stable-ABI extension, so a wheel covers all Python versions."""
4
-
5
- import sys
6
-
7
- from setuptools import Extension, setup
8
-
9
- vendored = ["radau5.c", "dc_decsol.c", "decsol.c", "libf2c_stubs.c", "lsoda.c", "mxs_lapack.c"]
10
- setup(
11
- ext_modules=[
12
- Extension(
13
- "mxlpy_native._solvers",
14
- sources=["c/mxn_solvers.c", *(f"c/vendor/{s}" for s in vendored)],
15
- include_dirs=["c/vendor"],
16
- py_limited_api=True,
17
- define_macros=[("Py_LIMITED_API", "0x030C0000")],
18
- # lsoda.c needs C11 (restrict, compound literals), which MSVC only enables on request.
19
- extra_compile_args=["/std:c11"]
20
- if sys.platform == "win32"
21
- else ["-std=gnu17", "-w", "-O2"],
22
- libraries=[] if sys.platform == "win32" else ["m"],
23
- )
24
- ],
25
- options={"bdist_wheel": {"py_limited_api": "cp312"}},
26
- )
@@ -1,18 +0,0 @@
1
- # Copyright 2025 Marvin van Aalst, Anna Matuszyńska
2
-
3
- """Compiled solvers for MxlPy's Native integrator."""
4
-
5
- from importlib.machinery import EXTENSION_SUFFIXES
6
- from pathlib import Path
7
-
8
- __all__ = ["library_path"]
9
-
10
-
11
- def library_path() -> Path:
12
- """Return the path of the compiled solvers, for loading with ctypes."""
13
- here = Path(__file__).parent
14
- for suffix in EXTENSION_SUFFIXES:
15
- if (path := here / f"_solvers{suffix}").exists():
16
- return path
17
- msg = f"compiled solvers not found in {here}"
18
- raise FileNotFoundError(msg)
@@ -1,6 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mxlpy-native
3
- Version: 0.2.0
4
- Summary: Compiled ODE solvers for MxlPy's Native integrator
5
- License-Expression: MIT
6
- Requires-Python: >=3.12
File without changes
File without changes