From 359fedfcccf241c0b620b43a146dca285ef9278e Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:02:10 +0200 Subject: [PATCH 1/9] build: switch project metadata to pyproject.toml; remove setup.py/MANIFEST.in - Add pyproject.toml with [build-system] (meson-python), [project] metadata (description, license, authors, classifiers, urls), runtime dependencies, and PEP 735 [dependency-groups] (test + dev). - Remove legacy setup.py / setup.cfg / MANIFEST.in. - Add .env to .gitignore. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 2 + MANIFEST.in | 11 - pyproject.toml | 46 ++++ setup.cfg | 16 -- setup.py | 722 ------------------------------------------------- 5 files changed, 48 insertions(+), 749 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index e1d4710a..88ccccdf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ build.sh build/ +dist/ *.pyc .venv +.env diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 73872c1c..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include CHANGELOG -include INSTALL -include README.md -include assimulo/*.pxd -include assimulo/*.pxi -include assimulo/lib/*.pxd -include assimulo/lib/*.pxi -include assimulo/thirdparty/dasp3/* -include assimulo/examples/* -recursive-include assimulo/thirdparty *.pyf -recursive-include assimulo *.pyx diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c02a68fe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = [ + "meson-python>=0.16", + "meson>=1.4", + "ninja", + "Cython>=3.0.7", + "numpy>=2.1", +] +build-backend = "mesonpy" + +[project] +name = "Assimulo" +dynamic = ["version"] +description = "A package for solving ordinary and differential-algebraic equations." +readme = "README.md" +license = { text = "LGPL-3.0-only" } +authors = [{ name = "Modelon AB" }] +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: Microsoft :: Windows", + "Operating System :: Unix", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", +] + +dependencies = [ + "numpy>=2.1", + "scipy>=1.14.0", + "matplotlib>=3.0", +] + +[project.urls] +Homepage = "https://github.com/modelon-community/Assimulo" + +[dependency-groups] +test = ["pytest>=7.4.4"] +dev = [ + {include-group = "test"}, + "meson>=1.4", + "meson-python>=0.16", + "ninja", + "Cython>=3.0.7", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cc8a2a52..00000000 --- a/setup.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[options] -setup_requires = - setuptools - numpy >= 1.19.5 - cython >= 3.0.7 - -python_requires = >=3.9 - -install_requires = - numpy >= 1.19.5 - scipy >= 1.10.1 - cython >= 3.0.7 - matplotlib > 3 - -tests_require = - pytest >= 7.4.4 diff --git a/setup.py b/setup.py deleted file mode 100644 index c77fdc8d..00000000 --- a/setup.py +++ /dev/null @@ -1,722 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2023 Modelon AB -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -#from distutils.core import setup, Extension -import logging -import sys -import os -import shutil -import ctypes.util -import argparse -from os.path import isfile, join -import numpy as np -try: - from numpy.distutils.core import setup - import numpy.distutils as nd - from numpy.distutils.fcompiler import intel - have_nd = True -except ImportError: - from setuptools import setup - have_nd = False -import Cython -from Cython.Build import cythonize - -def str2bool(v): - return v.lower() in ("yes", "true", "t", "1") - -def remove_prefix(name, prefix): - if name.startswith(prefix): - return name[len(prefix):] - return name - -parser = argparse.ArgumentParser(description='Assimulo setup script.') -parser.register('type','bool',str2bool) -package_arguments=['plugins','sundials','blas','superlu','lapack','mkl'] -package_arguments.sort() -for pg in package_arguments: - parser.add_argument("--{}-home".format(pg), - help="Location of the {} directory".format(pg.upper()),type=str,default='') -parser.add_argument("--blas-name", help="name of the blas package",default='blas') -parser.add_argument("--mkl-name", help="name of the mkl package",default='mkl') -parser.add_argument("--extra-c-flags", help='Extra C-flags (a list enclosed in " ")',default='') -parser.add_argument("--with_openmp", type='bool', help="set to true if present",default=False) -parser.add_argument("--is_static", type='bool', help="set to true if present",default=False) -parser.add_argument("--sundials-with-superlu", type='bool', help="(DEPRECATED) set to true if Sundials has been compiled with SuperLU",default=None) -parser.add_argument("--debug", type='bool', help="set to true if present",default=False) -parser.add_argument("--force-32bit", type='bool', help="set to true if present",default=False) -parser.add_argument("--no-msvcr", type='bool', help="set to true if present",default=False) -parser.add_argument("--log",choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),default='NOTSET') -parser.add_argument("--log_file",default=None,type=str,help='Path of a logfile') -parser.add_argument("--prefix",default=None,type=str,help='Path to destination directory') -parser.add_argument("--extra-fortran-link-flags", help='Extra Fortran link flags (a list enclosed in " ")', default='') -parser.add_argument("--extra-fortran-link-files", help='Extra Fortran link files (a list enclosed in " ")', default='') -parser.add_argument("--extra-fortran-compile-flags", help='Extra Fortran compile flags (a list enclosed in " ")', default='--std=legacy') -parser.add_argument("--version", help='Package version number', default='Default') - -args = parser.parse_known_args() -version_number_arg = args[0].version - -logging.basicConfig(level=getattr(logging,args[0].log),format='%(levelname)s:%(message)s',filename=args[0].log_file) -logging.debug('setup.py called with the following optional args\n %s\n argument parsing completed.',vars(args[0])) - -#If prefix is set, we want to allow installation in a directory that is not on PYTHONPATH -#and this is only possible with distutils, not setuptools -if args[0].prefix is not None and not have_nd: - raise ValueError("Cannot handle prefix argument without distutils") - -#Verify Cython version -cython_version = Cython.__version__.split(".") -if not cython_version[0] >= '3': - msg="Please upgrade to a newer Cython version, >= 3" - logging.error(msg) - raise Exception(msg) - -logging.debug('Python version used: {}'.format(sys.version.split()[0])) - -thirdparty_methods= ["hairer","glimda", "odepack","odassl","dasp3","radau5"] - -class Assimulo_prepare(object): -# helper functions - def create_dir(self,d): - try: - os.makedirs(d) #Create the build directory - except OSError: - pass #Directory already exists - def copy_file(self,fi, to_dir): - # copies only files not directories - if not os.path.isdir(fi): - shutil.copy2(fi, to_dir) - def copy_all_files(self,file_list, from_dir, to_dir): - logging.debug('fromdir {} todir {}'.format(from_dir,to_dir)) - for f in file_list: - if from_dir: - self.copy_file(os.path.join(from_dir,f),to_dir) - else: - self.copy_file(f,to_dir) - def __init__(self,args, thirdparty_methods): - # args[0] are optional arguments given above - # args[1] are arguments passed to distutils - self.distutil_args=args[1] - if args[0].prefix: - self.prefix = args[0].prefix.replace('/',os.sep) # required in this way for cygwin etc. - self.distutil_args.append('--prefix={}'.format(self.prefix)) - self.SLUdir = args[0].superlu_home - self.BLASdir = args[0].blas_home - self.sundialsdir = args[0].sundials_home - self.MKLdir = args[0].mkl_home - self.BLASname_t = args[0].blas_name if args[0].blas_name.startswith('lib') else 'lib'+args[0].blas_name - self.BLASname = self.BLASname_t[3:] # the name without "lib" - self.MKLname_t = args[0].mkl_name if args[0].mkl_name.startswith('lib') else 'lib'+args[0].mkl_name - self.MKLname = self.MKLname_t[3:] # the name without "lib" - self.debug_flag = args[0].debug - self.LAPACKdir = args[0].lapack_home - self.LAPACKname = "" - self.PLUGINSdir = args[0].plugins_home - self.static = args[0].is_static - self.static_link_gcc = ["-static-libgcc"] if self.static else [] - self.static_link_gfortran = ["-static-libgfortran"] if self.static else [] - self.force_32bit = args[0].force_32bit - self.flag_32bit = ["-m32"] if self.force_32bit else [] - self.no_mvscr = args[0].no_msvcr - self.extra_c_flags = args[0].extra_c_flags.split() - self.extra_fortran_compile_flags = args[0].extra_fortran_compile_flags.split() - self.extra_fortran_link_flags = args[0].extra_fortran_link_flags.split() - self.extra_fortran_link_files = args[0].extra_fortran_link_files.split() - self.thirdparty_methods = thirdparty_methods - self.with_openmp = args[0].with_openmp - self.sundials_with_msvc = False - self.msvcSLU = False - - if self.no_mvscr and have_nd: - # prevent the MSVCR* being added to the DLLs passed to the linker - def msvc_runtime_library_mod(): - return None - nd.misc_util.msvc_runtime_library = msvc_runtime_library_mod - logging.debug('numpy.distutils.misc_util.msvc_runtime_library overwritten.') - - if have_nd: - # prevent Fortran to link dynamically - # Are there any additional flags needed for e.g. MKL, see https://software.intel.com/en-us/articles/intel-mkl-link-line-advisor - def fortran_compiler_flags(self): - opt = ['/nologo', '/MT', '/nbs', '/names:lowercase', '/assume:underscore'] - return opt - intel.IntelVisualFCompiler.get_flags=fortran_compiler_flags - - self.platform = 'linux' - if 'win' in sys.platform: - self.platform = 'win' - if 'darwin' in sys.platform: - self.platform = 'mac' - - logging.debug('Platform {}'.format(self.platform)) - - if args[0].sundials_home: - self.incdirs = os.path.join(self.sundialsdir, 'include') - self.libdirs = os.path.join(self.sundialsdir, 'lib') - elif 'win' in self.platform: - self.incdirs = '' - self.libdirs = '' - else: - self.incdirs = os.path.sep + os.path.join('usr', 'local', 'include') - self.libdirs = os.path.sep + os.path.join('usr', 'local', 'lib') - - self.assimulo_lib = os.path.join('assimulo','lib') - - # check packages - self.check_BLAS() - self.check_SuperLU() - self.check_SUNDIALS() - self.check_LAPACK() - self.check_MKL() - - def _set_directories(self): - # directory paths - self.curdir = os.path.dirname(os.path.abspath(__file__)) - # build directories - self.build_assimulo = os.path.join("build", "assimulo") - self.build_assimulo_thirdparty = os.path.join(self.build_assimulo, 'thirdparty') - # destination directories - self.desSrc = os.path.join(self.curdir,self.build_assimulo) - self.desLib = os.path.join(self.desSrc,"lib") - self.desSolvers = os.path.join(self.desSrc,"solvers") - self.desExamples = os.path.join(self.desSrc,"examples") - self.desMain = os.path.join(self.curdir,"build") - self.desThirdParty=dict([(thp,os.path.join(self.curdir,self.build_assimulo_thirdparty,thp)) - for thp in self.thirdparty_methods]) - # file lists - self.fileSrc = os.listdir("src") - self.fileLib = os.listdir(os.path.join("src","lib")) - self.fileSolvers = os.listdir(os.path.join("src","solvers")) - self.fileExamples= os.listdir("examples") - self.fileMain = ["setup.py","README.md","INSTALL","CHANGELOG","MANIFEST.in"] - self.fileMainIncludes = ["README.md","CHANGELOG", "LICENSE"] - self.filelist_thirdparty=dict([(thp,os.listdir(os.path.join("thirdparty",thp))) - for thp in self.thirdparty_methods]) - - def create_assimulo_dirs_and_populate(self): - self._set_directories() - - for subdir in ["lib", "solvers", "examples"]: - self.create_dir(os.path.join(self.build_assimulo,subdir)) - for pck in self.thirdparty_methods: - self.create_dir(os.path.join(self.build_assimulo_thirdparty, pck)) - - self.copy_all_files(self.fileSrc, "src", self.desSrc) - self.copy_all_files(self.fileLib, "src/lib", self.desLib) - self.copy_all_files(self.fileSolvers, os.path.join("src", "solvers"), self.desSolvers) - self.copy_all_files(self.fileExamples, "examples", self.desExamples) - self.copy_all_files(self.fileMain, None, self.desMain) - self.copy_all_files(self.fileMainIncludes, None, self.desSrc) - - for f in self.filelist_thirdparty.items(): - logging.debug('Thirdparty method {} file {} copied'.format(f[0],f[1])) - self.copy_all_files(f[1],os.path.join("thirdparty", f[0]), self.desThirdParty[f[0]]) - license_name = f[0].upper() if f[0].upper() != "RADAU5" else "HAIRER" - try: - shutil.copy2(os.path.join("thirdparty", f[0], "LICENSE_{}".format(license_name)), self.desLib) - except IOError: - logging.warning('No license file {} found.'.format("LICENSE_{}".format(f[0].upper()))) - - #Delete OLD renamed files - delFiles = [("lib","sundials_kinsol_core_wSLU.pxd")] - for item in delFiles: - dirDel = self.desSrc - for f in item[:-1]: - dirDel = os.path.join(dirDel, f) - dirDel = os.path.join(dirDel, item[-1]) - if os.path.exists(dirDel): - try: - os.remove(dirDel) - except Exception: - logging.debug("Could not remove: "+str(dirDel)) - - if self.extra_fortran_link_files: - for extra_fortran_lib in self.extra_fortran_link_files: - path_extra_fortran_lib = ctypes.util.find_library(extra_fortran_lib) - if path_extra_fortran_lib is not None: - shutil.copy2(path_extra_fortran_lib,self.desSrc) - else: - logging.debug("Could not find Fortran link file: "+str(extra_fortran_lib)) - - def check_BLAS(self): - """ - Check if BLAS can be found - """ - self.with_BLAS = True - msg=", disabling support. View more information using --log=DEBUG" - if self.BLASdir == "": - logging.warning("No path to BLAS supplied" + msg) - logging.debug("usage: --blas-home=path") - logging.debug("Note: the path required is to where the static library lib is found") - self.with_BLAS = False - else: - suffix = ".so" - if "win" in self.platform: - suffix = ".lib" - if "mac" in self.platform: - suffix = ".dylib" - - if not os.path.exists(os.path.join(self.BLASdir,self.BLASname_t+'.a')) and not os.path.exists(os.path.join(self.BLASdir,self.BLASname_t+suffix)): - logging.warning("Could not find BLAS"+msg) - logging.debug("Could not find BLAS at the given path {}.".format(self.BLASdir)) - logging.debug("usage: --blas-home=path") - self.with_BLAS = False - else: - logging.debug("BLAS found at "+self.BLASdir) - self.with_BLAS = True - - def check_MKL(self): - """ - Check if MKL can be found - """ - self.with_MKL = True - msg=", disabling support. View more information using --log=DEBUG" - if self.MKLdir == "": - logging.warning("No path to MKL supplied" + msg) - logging.debug("usage: --mkl-home=path") - logging.debug("Note: the path required is to where the static library lib is found") - self.with_MKL = False - else: - if not os.path.exists(os.path.join(self.MKLdir,self.MKLname_t+'.a')) and not os.path.exists(os.path.join(self.MKLdir,self.MKLname+'.lib')): - logging.warning("Could not find MKL"+msg) - logging.debug("Could not find MKL at the given path {}.".format(self.MKLdir)) - logging.debug("Searched for: {} and {}".format(self.MKLname_t+'.a', self.MKLname+'.lib')) - logging.debug("usage: --mkl-home=path") - self.with_MKL = False - else: - logging.debug("MKL found at "+self.MKLdir) - self.with_MKL = True - # To make sure that when MKL is found, BLAS and/or LAPACK aren't used - self.with_BLAS = False - self.with_LAPACK = False - - def check_SuperLU(self): - """ - Check if SuperLU package installed - """ - self.with_SLU = True - slu_missing_msg='SUNDIALS&Radau5 will not be compiled with support for SuperLU.' - - if self.SLUdir != "": - self.SLUincdir = os.path.join(self.SLUdir,'SRC') - if not os.path.exists(os.path.join(self.SLUincdir,'supermatrix.h')): - self.SLUincdir = os.path.join(self.SLUdir,'include') - self.SLUlibdir = os.path.join(self.SLUdir,'lib') - if not os.path.exists(os.path.join(self.SLUincdir,'supermatrix.h')): - self.with_SLU = False - logging.warning("Could not find SuperLU, disabling support. View more information using --log=DEBUG") - logging.debug("Could not find SuperLU at the given path {}.".format(self.SLUdir)) - logging.debug("usage: --superlu-home path") - logging.debug(slu_missing_msg) - else: - logging.debug("SuperLU found in {} and {}: ".format(self.SLUincdir, self.SLUlibdir)) - - potential_files = [remove_prefix(f.rsplit(".",1)[0],"lib") for f in os.listdir(self.SLUlibdir) if isfile(join(self.SLUlibdir, f)) and f.endswith(".a")] - self.msvcSLU = False - if not potential_files: - msvs_lib_suffix=".lib" - self.msvcSLU = True - potential_files = [f[:-len(msvs_lib_suffix)] for f in os.listdir(self.SLUlibdir) if isfile(join(self.SLUlibdir, f)) and f.endswith(msvs_lib_suffix)] - potential_files.sort(reverse=True) - logging.debug("Potential SuperLU files: "+str(potential_files)) - - self.superLUFiles = [] - for f in potential_files: - if "superlu" in f: - self.superLUFiles.append(f) - #if self.with_BLAS == False and "blas" in f: - # self.superLUFiles.append(f) - if "blas" in f: - self.superLUFiles.append(f) - - #if self.with_BLAS: - # self.superLUFiles.append(self.BLASname) - - logging.debug("SuperLU files: "+str(self.superLUFiles)) - - else: - logging.warning("No path to SuperLU supplied, disabling support. View more information using --log=DEBUG") - logging.debug("No path to SuperLU supplied, SUNDIALS&Radau5 will not be compiled with support for SuperLU.") - logging.debug("usage: --superlu-home=path") - logging.debug("Note: the path required is to the folder where the folders 'SRC' and 'lib' are found.") - self.with_SLU = False - - def check_SUNDIALS(self): - """ - Check if Sundials installed - """ - if os.path.exists(os.path.join(os.path.join(self.incdirs,'cvodes'), 'cvodes.h')): - self.with_SUNDIALS=True - logging.debug('SUNDIALS found.') - sundials_version = None - sundials_vector_type_size = None - sundials_with_superlu = False - sundials_with_msvc = False - sundials_cvode_with_rtol_vec = False - try: - if os.path.exists(os.path.join(os.path.join(self.incdirs,'sundials'), 'sundials_config.h')): - with open(os.path.join(os.path.join(self.incdirs,'sundials'), 'sundials_config.h')) as f: - for line in f: - if "SUNDIALS_PACKAGE_VERSION" in line or "SUNDIALS_VERSION" in line: - sundials_version = tuple([int(f) for f in line.split()[-1][1:-1].split('-dev')[0].split(".")]) - logging.debug('SUNDIALS %d.%d found.'%(sundials_version[0], sundials_version[1])) - break - with open(os.path.join(os.path.join(self.incdirs,'sundials'), 'sundials_config.h')) as f: - for line in f: - if "SUNDIALS_INT32_T" in line and line.startswith("#define"): - sundials_vector_type_size = "32" - logging.debug('SUNDIALS vector type size %s bit found.'%(sundials_vector_type_size)) - break - if "SUNDIALS_INT64_T" in line and line.startswith("#define"): - sundials_vector_type_size = "64" - logging.debug('SUNDIALS vector type size %s bit found.'%(sundials_vector_type_size)) - if self.with_SLU: - logging.warning("It is recommended to set the SUNDIALS_INDEX_TYPE to an 32bit integer when using SUNDIALS together with SuperLU (or make sure that SuperLU is configured to use the same int size).") - logging.warning("SuperLU may not function properly.") - break - with open(os.path.join(os.path.join(self.incdirs,'sundials'), 'sundials_config.h')) as f: - for line in f: - if "SUNDIALS_SUPERLUMT" in line and line.startswith("#define"): #Sundials compiled with support for SuperLU - sundials_with_superlu = True - logging.debug('SUNDIALS found to be compiled with support for SuperLU.') - break - with open(os.path.join(os.path.join(self.incdirs,'sundials'), 'sundials_config.h')) as f: - for line in f: - if "SUNDIALS_CVODE_RTOL_VEC" in line and line.startswith("#define"): #Sundials with CVode support for rtol vectors - sundials_cvode_with_rtol_vec = True - logging.debug('SUNDIALS found with CVode supporting rtol vectors.') - break - if os.path.exists(os.path.join(self.libdirs,'sundials_nvecserial.lib')) and not os.path.exists(os.path.join(self.libdirs,'libsundials_nvecserial.a')): - sundials_with_msvc = True - except Exception: - if os.path.exists(os.path.join(os.path.join(self.incdirs,'arkode'), 'arkode.h')): #This was added in 2.6 - sundials_version = (2,6,0) - logging.debug('SUNDIALS 2.6 found.') - else: - sundials_version = (2,5,0) - logging.debug('SUNDIALS 2.5 found.') - - self.SUNDIALS_version = sundials_version - self.SUNDIALS_vector_size = sundials_vector_type_size - self.sundials_with_superlu = sundials_with_superlu - self.sundials_with_msvc = sundials_with_msvc - self.sundials_cvode_with_rtol_vec = sundials_cvode_with_rtol_vec - if not self.sundials_with_superlu: - logging.debug("Could not detect SuperLU support with Sundials, disabling support for SuperLU.") - else: - logging.warning(("Could not find Sundials, check the provided path (--sundials-home={}) "+ - "to see that it actually points to Sundials.").format(self.sundialsdir)) - logging.debug("Could not find cvodes.h in " + os.path.join(self.incdirs,'cvodes')) - self.with_SUNDIALS=False - - def check_LAPACK(self): - """ - Check if LAPACK installed - """ - msg=", disabling support. View more information using --log=DEBUG" - self.with_LAPACK=False - if self.LAPACKdir != "": - if not os.path.exists(self.LAPACKdir): - logging.warning('LAPACK directory {} not found'.format(self.LAPACKdir)) - else: - logging.debug("LAPACK found at "+self.LAPACKdir) - self.with_LAPACK = True - else: - """ - name = ctypes.util.find_library("lapack") - if name != "": - logging.debug('LAPACK found in standard library path as {}'.format(name)) - self.with_LAPACK=True - self.LAPACKname = name - else: - """ - logging.warning("No path to LAPACK supplied" + msg) - logging.debug("usage: --lapack-home=path") - logging.debug("Note: the path required is to where the static library lib is found") - self.with_LAPACK = False - - def cython_extensionlists(self): - extra_link_flags = self.static_link_gcc + self.flag_32bit - - # Cythonize main modules - ext_list = cythonize([os.path.join("assimulo", "explicit_ode.pyx")], - include_path=[".", "assimulo"], - force = True, - compiler_directives={'language_level' : "3str"}) - ext_list[-1].include_dirs += ["assimulo", self.incdirs] - ext_list[-1].sources += [os.path.join("assimulo", "ode_event_locator.c")] - - remaining_pyx = ["algebraic", "implicit_ode", "ode", "problem", "special_systems", "support"] - ext_list += cythonize([os.path.join("assimulo", "{}.pyx".format(x)) for x in remaining_pyx], - include_path=[".", "assimulo"], - force = True, - compiler_directives={'language_level' : "3str"}) - - # Cythonize Solvers - # Euler - ext_list += cythonize([os.path.join("assimulo", "solvers", "euler.pyx")], - include_path=[".", "assimulo", os.path.join("assimulo", "solvers")], - force = True, - compiler_directives={'language_level' : "3str"},) - for ext in ext_list: - ext.include_dirs += [np.get_include()] - - # SUNDIALS - if self.with_SUNDIALS: - compile_time_env = {'SUNDIALS_VERSION': self.SUNDIALS_version, - 'SUNDIALS_WITH_SUPERLU': self.sundials_with_superlu and self.with_SLU, - 'SUNDIALS_VECTOR_SIZE': self.SUNDIALS_vector_size, - 'SUNDIALS_CVODE_RTOL_VEC': self.sundials_cvode_with_rtol_vec} - #CVode and IDA - ext_list += cythonize(["assimulo" + os.path.sep + "solvers" + os.path.sep + "sundials.pyx"], - include_path=[".","assimulo","assimulo" + os.sep + "lib"], - compile_time_env=compile_time_env, - force=True, - compiler_directives={'language_level' : "3str"}) - ext_list[-1].include_dirs = [np.get_include(), "assimulo","assimulo"+os.sep+"lib", self.incdirs] - ext_list[-1].library_dirs = [self.libdirs] - - if self.SUNDIALS_version >= (3,0,0): - ext_list[-1].libraries = ["sundials_cvodes", "sundials_nvecserial", "sundials_idas", "sundials_sunlinsoldense", "sundials_sunlinsolspgmr", "sundials_sunmatrixdense", "sundials_sunmatrixsparse"] - if self.SUNDIALS_version >= (7,0,0): - ext_list[-1].libraries.extend(["sundials_core"]) - else: - ext_list[-1].libraries = ["sundials_cvodes", "sundials_nvecserial", "sundials_idas"] - if self.sundials_with_superlu and self.with_SLU: #If SUNDIALS is compiled with support for SuperLU - if self.SUNDIALS_version >= (3,0,0): - ext_list[-1].libraries.extend(["sundials_sunlinsolsuperlumt"]) - - ext_list[-1].include_dirs.append(self.SLUincdir) - ext_list[-1].library_dirs.append(self.SLUlibdir) - ext_list[-1].libraries.extend(self.superLUFiles) - - #Kinsol - ext_list += cythonize(["assimulo"+os.path.sep+"solvers"+os.path.sep+"kinsol.pyx"], - include_path=[".","assimulo","assimulo"+os.sep+"lib"], - compile_time_env=compile_time_env, - force=True, - compiler_directives={'language_level' : "3str"}) - ext_list[-1].include_dirs = [np.get_include(), "assimulo","assimulo"+os.sep+"lib", self.incdirs] - ext_list[-1].library_dirs = [self.libdirs] - ext_list[-1].libraries = ["sundials_kinsol", "sundials_nvecserial"] - if self.SUNDIALS_version >= (7,0,0): - ext_list[-1].libraries.extend(["sundials_core"]) - - if self.sundials_with_superlu and self.with_SLU: #If SUNDIALS is compiled with support for SuperLU - ext_list[-1].include_dirs.append(self.SLUincdir) - ext_list[-1].library_dirs.append(self.SLUlibdir) - ext_list[-1].libraries.extend(self.superLUFiles) - - ## Radau5 - ext_list += cythonize([os.path.join("assimulo","thirdparty","radau5","radau5ode.pyx")], - include_path=[".", "assimulo", os.path.join("assimulo", "lib")], - force = True, - compiler_directives={'language_level' : "3str"}) - ext_list[-1].include_dirs = [np.get_include(), "assimulo", os.path.join("assimulo", "lib"), - os.path.join("assimulo","thirdparty","radau5"), - self.incdirs] - extra_sources = ["radau5.c", "radau5_io.c"] - if self.with_SLU: - ext_list[-1].include_dirs += [self.SLUincdir] - extra_sources += ["superlu_double.c", "superlu_complex.c", "superlu_util.c"] - ext_list[-1].sources = ext_list[-1].sources + [os.path.join("assimulo","thirdparty","radau5", file) for file in extra_sources] - ext_list[-1].name = "assimulo.lib.radau5ode" - - if self.with_SLU: - if 'win' in self.platform: - ext_list[-1].library_dirs = [os.path.join(self.SLUincdir, "..", "lib"), self.libdirs] - ext_list[-1].libraries = ['superlu_mt_OPENMP', 'blas_OPENMP'] - ext_list[-1].extra_compile_args += ["-D__OPENMP", "-D__RADAU5_WITH_SUPERLU"] - else: - ext_list[-1].library_dirs = [os.path.join(self.SLUincdir, "..", "lib"), self.BLASdir] - ext_list[-1].libraries = ['superlu_mt_OPENMP', 'blas_OPENMP', 'blas', 'm', 'gomp'] - ext_list[-1].extra_compile_args = ["-D__RADAU5_WITH_SUPERLU"] - else: - if 'win' not in self.platform: - ext_list[-1].libraries = ['m'] - - for el in ext_list: - #Debug - if self.debug_flag: - if self.sundials_with_msvc: - el.extra_compile_args += ["/DEBUG"] - el.extra_link_args += ["/DEBUG"] - else: - el.extra_compile_args += ["-g","-fno-strict-aliasing"] - el.extra_link_args += ["-g"] - else: - if self.sundials_with_msvc: - el.extra_compile_args += ["/O2"] - else: - el.extra_compile_args += ["-O2", "-fno-strict-aliasing"] - if self.platform == "mac": - el.extra_compile_args += ["-Wno-error=return-type"] - if self.with_openmp: - if self.msvcSLU: - openmp_arg = "/openmp" - else: - openmp_arg = "-fopenmp" - el.extra_link_args.append(openmp_arg) - el.extra_compile_args.append(openmp_arg) - el.extra_compile_args += self.flag_32bit + self.extra_c_flags - - for el in ext_list: - el.extra_link_args += extra_link_flags - return ext_list - - def fortran_extensionlists(self): - """ - Adds the Fortran extensions using Numpy's distutils extension. - """ - extra_link_flags = self.static_link_gfortran + self.static_link_gcc + self.flag_32bit + self.extra_fortran_link_flags - extra_compile_flags = self.flag_32bit + self.extra_c_flags - extra_fortran_compile_flags = self.flag_32bit + self.extra_fortran_compile_flags - - config = np.distutils.misc_util.Configuration() - extraargs={'extra_link_args':extra_link_flags[:], 'extra_compile_args':extra_compile_flags[:]} - - extraargs['extra_f77_compile_args'] = extra_fortran_compile_flags[:] - extraargs['extra_f90_compile_args'] = extra_fortran_compile_flags[:] - - #Hairer - sources='assimulo'+os.sep+'thirdparty'+os.sep+'hairer'+os.sep+'{0}.f','assimulo'+os.sep+'thirdparty'+os.sep+'hairer'+os.sep+'{0}.pyf' - config.add_extension('assimulo.lib.dopri5', sources=[s.format('dopri5') for s in sources], **extraargs) - config.add_extension('assimulo.lib.rodas', sources=[s.format('rodas_decsol') for s in sources], include_dirs=[np.get_include()],**extraargs) - config.add_extension('assimulo.lib.radau5', sources=[s.format('radau_decsol') for s in sources], include_dirs=[np.get_include()],**extraargs) - - radar_list=['contr5.f90', 'radar5_int.f90', 'radar5.f90', 'dontr5.f90', 'decsol.f90', 'dc_decdel.f90', 'radar5.pyf'] - src=['assimulo'+os.sep+'thirdparty'+os.sep+'hairer'+os.sep+code for code in radar_list] - config.add_extension('assimulo.lib.radar5', sources= src, include_dirs=[np.get_include()],**extraargs) - - #ODEPACK - odepack_list = ['opkdmain.f', 'opkda1.f', 'opkda2.f', 'odepack_aux.f90','odepack.pyf'] - src=['assimulo'+os.sep+'thirdparty'+os.sep+'odepack'+os.sep+code for code in odepack_list] - config.add_extension('assimulo.lib.odepack', sources= src, include_dirs=[np.get_include()],**extraargs) - - #ODASSL - odassl_list=['odassl.pyf','odassl.f','odastp.f','odacor.f','odajac.f','d1mach.f','daxpy.f','ddanrm.f','ddatrp.f','ddot.f', - 'ddwats.f','dgefa.f','dgesl.f','dscal.f','idamax.f','xerrwv.f'] - src=['assimulo'+os.sep+'thirdparty'+os.sep+'odassl'+os.sep+code for code in odassl_list] - config.add_extension('assimulo.lib.odassl', sources= src, include_dirs=[np.get_include()],**extraargs) - - dasp3_f77_compile_flags = ["-fdefault-double-8","-fdefault-real-8"] - dasp3_f77_compile_flags += extra_fortran_compile_flags - - #NOTE, THERE IS A PROBLEM WITH PASSING F77 COMPILER ARGS FOR NUMPY LESS THAN 1.6.1 - dasp3_list = ['dasp3dp.pyf', 'DASP3.f', 'ANORM.f','CTRACT.f','DECOMP.f', 'HMAX.f','INIVAL.f','JACEST.f','PDERIV.f','PREPOL.f','SOLVE.f','SPAPAT.f'] - src=['assimulo'+os.sep+'thirdparty'+os.sep+'dasp3'+os.sep+code for code in dasp3_list] - config.add_extension('assimulo.lib.dasp3dp', - sources= src, - include_dirs=[np.get_include()], extra_link_args=extra_link_flags[:],extra_f77_compile_args=dasp3_f77_compile_flags[:], - extra_compile_args=extra_compile_flags[:],extra_f90_compile_args=extra_fortran_compile_flags[:]) - - #GLIMDA - glimda_list = ['glimda_complete.f','glimda_complete.pyf'] - src=['assimulo'+os.sep+'thirdparty'+os.sep+'glimda'+os.sep+code for code in glimda_list] - if self.with_BLAS and self.with_LAPACK: - extraargs_glimda={'extra_link_args':extra_link_flags[:], 'extra_compile_args':extra_compile_flags[:], 'library_dirs':[self.BLASdir, self.LAPACKdir], 'libraries':['lapack', self.BLASname]} - extraargs_glimda["extra_f77_compile_args"] = extra_fortran_compile_flags[:] - config.add_extension('assimulo.lib.glimda', sources= src,include_dirs=[np.get_include()],**extraargs_glimda) - extra_link_flags=extra_link_flags[:-2] # remove LAPACK flags after GLIMDA - elif self.with_MKL: #assuming windows and Intel fortran compiler - config.add_extension('assimulo.lib.glimda', sources= src,include_dirs=[np.get_include()], library_dirs=[self.MKLdir], libraries=[self.MKLname]) - else: - logging.warning("Could not find Blas or Lapack, disabling support for the solver GLIMDA.") - - return config.todict()["ext_modules"] - -prepare=Assimulo_prepare(args, thirdparty_methods) -curr_dir=os.getcwd() -if not os.path.isdir("assimulo"): - prepare.create_assimulo_dirs_and_populate() - os.chdir("build") #Change dir - change_dir = True -else: - change_dir = False - -ext_list = prepare.cython_extensionlists() -if have_nd: - ext_list += prepare.fortran_extensionlists() - -# distutils part - - -NAME = "Assimulo" -AUTHOR = u"C. Winther (Andersson), C. Führer, J. Åkesson, M. Gäfvert" -AUTHOR_EMAIL = "christian.winther@modelon.com" -VERSION = "3.7.0.dev0" if version_number_arg == "Default" else version_number_arg -LICENSE = "LGPL" -URL = "https://github.com/modelon-community/Assimulo" -DOWNLOAD_URL = "https://github.com/modelon-community/Assimulo/releases" -DESCRIPTION = "A package for solving ordinary differential equations and differential algebraic equations." -PLATFORMS = ["Linux", "Windows", "MacOS X"] -CLASSIFIERS = [ 'Programming Language :: Python', - 'Programming Language :: Cython', - 'Programming Language :: C', - 'Programming Language :: Fortran', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: Unix'] - -LONG_DESCRIPTION = """ -Assimulo is a Cython / Python based simulation package that allows for -simulation of both ordinary differential equations (ODEs), f(t,y), and -differential algebraic equations (DAEs), f(t,y,yd). It combines a -variety of different solvers written in C, FORTRAN and Python via a -common high-level interface. - -Assimulo supports Explicit Euler, adaptive Runge-Kutta of -order 4 and Runge-Kutta of order 4. It also wraps the popular SUNDIALS -(https://computation.llnl.gov/casc/sundials/main.html) solvers CVode -(for ODEs) and IDA (for DAEs). Ernst Hairer's -(http://www.unige.ch/~hairer/software.html) codes Radau5, Rodas and -Dopri5 are also available. For the full list, see the documentation. - -The package requires Numpy, Scipy and Matplotlib and additionally for -compiling from source, Cython >=3, Sundials 2.6/2.7/3.1/4.1, BLAS and LAPACK -together with a C-compiler and a FORTRAN-compiler. -""" - -version_txt = os.path.join('assimulo', 'version.txt') -with open(version_txt, 'w') as f: - f.write(VERSION + '\n') - -license_info=[place+os.sep+pck+os.sep+'LICENSE_{}'.format(pck.upper()) - for pck in thirdparty_methods for place in ['thirdparty','lib']] -logging.debug(license_info) - -setup(name=NAME, - version=VERSION, - license=LICENSE, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - url=URL, - download_url=DOWNLOAD_URL, - platforms=PLATFORMS, - classifiers=CLASSIFIERS, - package_dir = {'assimulo':'assimulo'}, - packages=['assimulo', 'assimulo.lib', 'assimulo.solvers', 'assimulo.examples'], - #cmdclass = {'build_ext': build_ext}, - ext_modules = ext_list, - package_data={'assimulo': ['*.pxd', 'version.txt', 'CHANGELOG', 'README.md', 'LICENSE']+license_info+['examples'+os.sep+'kinsol_ors_matrix.mtx', - 'examples'+os.sep+'kinsol_ors_matrix.mtx'] + (['lib'+os.sep+f for f in prepare.extra_fortran_link_files] if prepare.extra_fortran_link_files else [])}, - script_args=prepare.distutil_args) - - -if change_dir: - os.chdir(curr_dir) #Change back to original directory From 7f4ff8ec4c0e175e28cdc9c1068911561c63f373 Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:02:41 +0200 Subject: [PATCH 2/9] build: port build to meson; restructure src/ into src/assimulo/ package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add meson.build and meson.options driving the meson-python backend. - Move src/* into src/assimulo/ so meson-python installs as a regular package; move examples/ into src/assimulo/examples/ so they're shipped with the wheel. - Wire up Cython extensions (sundials.pyx, kinsol.pyx, euler.pyx, sdirk_dae.pyx, special_systems.pyx, support.pyx, etc.) via meson. - Add tools/f2py_wrapper.py so meson can drive numpy.f2py for the Fortran solvers (dopri5, rodas, radau5, radar5, odepack, odassl, dasp3, glimda). - thirdparty: minor portability fixes — ASCII-clean glimda_complete.f, fix radar5.f90 intent warning that's an error with modern gfortran. Co-Authored-By: Claude Opus 4.7 --- meson.build | 383 ++++++++++++++++++ meson.options | 7 + src/{ => assimulo}/__init__.py | 0 src/{ => assimulo}/algebraic.pxd | 0 src/{ => assimulo}/algebraic.pyx | 0 src/{ => assimulo}/constants.pxi | 0 .../assimulo/examples}/__init__.py | 0 .../assimulo/examples}/cvode_basic.py | 0 .../examples}/cvode_basic_backward.py | 0 .../assimulo/examples}/cvode_gyro.py | 0 .../assimulo/examples}/cvode_stability.py | 0 .../assimulo/examples}/cvode_with_disc.py | 0 .../cvode_with_initial_sensitivity.py | 0 .../assimulo/examples}/cvode_with_jac.py | 0 .../examples}/cvode_with_jac_sparse.py | 0 .../examples}/cvode_with_jac_spgmr.py | 0 .../examples}/cvode_with_parameters.py | 0 .../examples}/cvode_with_parameters_fcn.py | 0 .../cvode_with_parameters_modified.py | 0 .../examples}/cvode_with_preconditioning.py | 0 .../assimulo/examples}/dasp3_basic.py | 0 .../assimulo/examples}/dopri5_basic.py | 0 .../assimulo/examples}/dopri5_with_disc.py | 0 .../assimulo/examples}/euler_basic.py | 0 .../assimulo/examples}/euler_vanderpol.py | 0 .../assimulo/examples}/euler_with_disc.py | 0 .../assimulo/examples}/glimda_vanderpol.py | 0 .../assimulo/examples}/ida_basic_backward.py | 0 .../assimulo/examples}/ida_with_disc.py | 0 .../examples}/ida_with_initial_sensitivity.py | 0 .../assimulo/examples}/ida_with_jac.py | 0 .../assimulo/examples}/ida_with_jac_spgmr.py | 0 .../assimulo/examples}/ida_with_parameters.py | 0 .../ida_with_user_defined_handle_result.py | 0 .../assimulo/examples}/kinsol_basic.py | 0 .../assimulo/examples}/kinsol_ors.py | 0 .../assimulo/examples}/kinsol_ors_matrix.mtx | 0 .../assimulo/examples}/kinsol_with_jac.py | 0 .../examples}/lsodar_bouncing_ball.py | 0 .../assimulo/examples}/lsodar_vanderpol.py | 0 .../assimulo/examples}/lsodar_with_disc.py | 0 .../examples}/mech_system_pendulum.py | 0 .../assimulo/examples}/radar_basic.py | 0 .../examples}/radau5dae_time_events.py | 0 .../assimulo/examples}/radau5dae_vanderpol.py | 0 .../assimulo/examples}/radau5ode_vanderpol.py | 0 .../assimulo/examples}/radau5ode_with_disc.py | 0 .../examples}/radau5ode_with_disc_sparse.py | 0 .../examples}/radau5ode_with_jac_sparse.py | 0 .../assimulo/examples}/rodasode_vanderpol.py | 0 .../assimulo/examples}/rungekutta34_basic.py | 0 .../examples}/rungekutta34_with_disc.py | 0 .../assimulo/examples}/rungekutta4_basic.py | 0 src/{ => assimulo}/exception.py | 0 src/{ => assimulo}/explicit_ode.pxd | 0 src/{ => assimulo}/explicit_ode.pyx | 0 src/{ => assimulo}/implicit_ode.pxd | 0 src/{ => assimulo}/implicit_ode.pyx | 0 src/{ => assimulo}/lib/__init__.py | 0 src/{ => assimulo}/lib/radau_core.py | 0 src/{ => assimulo}/lib/sundials_callbacks.pxi | 1 + .../lib/sundials_callbacks_ida_cvode.pxi | 1 + .../lib/sundials_callbacks_kinsol.pxi | 1 + src/{ => assimulo}/lib/sundials_constants.pxi | 0 src/{ => assimulo}/lib/sundials_includes.pxd | 1 + src/{ => assimulo}/ode.pxd | 0 src/{ => assimulo}/ode.pyx | 0 src/{ => assimulo}/ode_event_locator.c | 0 src/{ => assimulo}/ode_event_locator.h | 0 src/{ => assimulo}/problem.pxd | 0 src/{ => assimulo}/problem.pyx | 0 src/{ => assimulo}/problem_algebraic.py | 0 src/{ => assimulo}/solvers/__init__.py | 0 src/{ => assimulo}/solvers/dasp3.py | 0 src/{ => assimulo}/solvers/euler.pyx | 2 +- src/{ => assimulo}/solvers/glimda.py | 0 src/{ => assimulo}/solvers/kinsol.pyx | 3 +- src/{ => assimulo}/solvers/odassl.py | 0 src/{ => assimulo}/solvers/odepack.py | 0 src/{ => assimulo}/solvers/radar5.py | 0 src/{ => assimulo}/solvers/radau5.py | 0 src/{ => assimulo}/solvers/rosenbrock.py | 0 src/{ => assimulo}/solvers/runge_kutta.py | 0 src/{ => assimulo}/solvers/sdirk_dae.pyx | 0 src/{ => assimulo}/solvers/sundials.pyx | 3 +- src/{ => assimulo}/special_systems.pyx | 0 src/assimulo/sundials.pxi.in | 5 + src/{ => assimulo}/support.pxd | 0 src/{ => assimulo}/support.pyx | 0 thirdparty/glimda/glimda_complete.f | 18 +- thirdparty/hairer/radar5.f90 | 4 +- tools/f2py_wrapper.py | 47 +++ 92 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 meson.build create mode 100644 meson.options rename src/{ => assimulo}/__init__.py (100%) rename src/{ => assimulo}/algebraic.pxd (100%) rename src/{ => assimulo}/algebraic.pyx (100%) rename src/{ => assimulo}/constants.pxi (100%) rename {examples => src/assimulo/examples}/__init__.py (100%) rename {examples => src/assimulo/examples}/cvode_basic.py (100%) rename {examples => src/assimulo/examples}/cvode_basic_backward.py (100%) rename {examples => src/assimulo/examples}/cvode_gyro.py (100%) rename {examples => src/assimulo/examples}/cvode_stability.py (100%) rename {examples => src/assimulo/examples}/cvode_with_disc.py (100%) rename {examples => src/assimulo/examples}/cvode_with_initial_sensitivity.py (100%) rename {examples => src/assimulo/examples}/cvode_with_jac.py (100%) rename {examples => src/assimulo/examples}/cvode_with_jac_sparse.py (100%) rename {examples => src/assimulo/examples}/cvode_with_jac_spgmr.py (100%) rename {examples => src/assimulo/examples}/cvode_with_parameters.py (100%) rename {examples => src/assimulo/examples}/cvode_with_parameters_fcn.py (100%) rename {examples => src/assimulo/examples}/cvode_with_parameters_modified.py (100%) rename {examples => src/assimulo/examples}/cvode_with_preconditioning.py (100%) rename {examples => src/assimulo/examples}/dasp3_basic.py (100%) rename {examples => src/assimulo/examples}/dopri5_basic.py (100%) rename {examples => src/assimulo/examples}/dopri5_with_disc.py (100%) rename {examples => src/assimulo/examples}/euler_basic.py (100%) rename {examples => src/assimulo/examples}/euler_vanderpol.py (100%) rename {examples => src/assimulo/examples}/euler_with_disc.py (100%) rename {examples => src/assimulo/examples}/glimda_vanderpol.py (100%) rename {examples => src/assimulo/examples}/ida_basic_backward.py (100%) rename {examples => src/assimulo/examples}/ida_with_disc.py (100%) rename {examples => src/assimulo/examples}/ida_with_initial_sensitivity.py (100%) rename {examples => src/assimulo/examples}/ida_with_jac.py (100%) rename {examples => src/assimulo/examples}/ida_with_jac_spgmr.py (100%) rename {examples => src/assimulo/examples}/ida_with_parameters.py (100%) rename {examples => src/assimulo/examples}/ida_with_user_defined_handle_result.py (100%) rename {examples => src/assimulo/examples}/kinsol_basic.py (100%) rename {examples => src/assimulo/examples}/kinsol_ors.py (100%) rename {examples => src/assimulo/examples}/kinsol_ors_matrix.mtx (100%) rename {examples => src/assimulo/examples}/kinsol_with_jac.py (100%) rename {examples => src/assimulo/examples}/lsodar_bouncing_ball.py (100%) rename {examples => src/assimulo/examples}/lsodar_vanderpol.py (100%) rename {examples => src/assimulo/examples}/lsodar_with_disc.py (100%) rename {examples => src/assimulo/examples}/mech_system_pendulum.py (100%) rename {examples => src/assimulo/examples}/radar_basic.py (100%) rename {examples => src/assimulo/examples}/radau5dae_time_events.py (100%) rename {examples => src/assimulo/examples}/radau5dae_vanderpol.py (100%) rename {examples => src/assimulo/examples}/radau5ode_vanderpol.py (100%) rename {examples => src/assimulo/examples}/radau5ode_with_disc.py (100%) rename {examples => src/assimulo/examples}/radau5ode_with_disc_sparse.py (100%) rename {examples => src/assimulo/examples}/radau5ode_with_jac_sparse.py (100%) rename {examples => src/assimulo/examples}/rodasode_vanderpol.py (100%) rename {examples => src/assimulo/examples}/rungekutta34_basic.py (100%) rename {examples => src/assimulo/examples}/rungekutta34_with_disc.py (100%) rename {examples => src/assimulo/examples}/rungekutta4_basic.py (100%) rename src/{ => assimulo}/exception.py (100%) rename src/{ => assimulo}/explicit_ode.pxd (100%) rename src/{ => assimulo}/explicit_ode.pyx (100%) rename src/{ => assimulo}/implicit_ode.pxd (100%) rename src/{ => assimulo}/implicit_ode.pyx (100%) rename src/{ => assimulo}/lib/__init__.py (100%) rename src/{ => assimulo}/lib/radau_core.py (100%) rename src/{ => assimulo}/lib/sundials_callbacks.pxi (99%) rename src/{ => assimulo}/lib/sundials_callbacks_ida_cvode.pxi (99%) rename src/{ => assimulo}/lib/sundials_callbacks_kinsol.pxi (99%) rename src/{ => assimulo}/lib/sundials_constants.pxi (100%) rename src/{ => assimulo}/lib/sundials_includes.pxd (99%) rename src/{ => assimulo}/ode.pxd (100%) rename src/{ => assimulo}/ode.pyx (100%) rename src/{ => assimulo}/ode_event_locator.c (100%) rename src/{ => assimulo}/ode_event_locator.h (100%) rename src/{ => assimulo}/problem.pxd (100%) rename src/{ => assimulo}/problem.pyx (100%) rename src/{ => assimulo}/problem_algebraic.py (100%) rename src/{ => assimulo}/solvers/__init__.py (100%) rename src/{ => assimulo}/solvers/dasp3.py (100%) rename src/{ => assimulo}/solvers/euler.pyx (99%) rename src/{ => assimulo}/solvers/glimda.py (100%) rename src/{ => assimulo}/solvers/kinsol.pyx (99%) rename src/{ => assimulo}/solvers/odassl.py (100%) rename src/{ => assimulo}/solvers/odepack.py (100%) rename src/{ => assimulo}/solvers/radar5.py (100%) rename src/{ => assimulo}/solvers/radau5.py (100%) rename src/{ => assimulo}/solvers/rosenbrock.py (100%) rename src/{ => assimulo}/solvers/runge_kutta.py (100%) rename src/{ => assimulo}/solvers/sdirk_dae.pyx (100%) rename src/{ => assimulo}/solvers/sundials.pyx (99%) rename src/{ => assimulo}/special_systems.pyx (100%) create mode 100644 src/assimulo/sundials.pxi.in rename src/{ => assimulo}/support.pxd (100%) rename src/{ => assimulo}/support.pyx (100%) create mode 100644 tools/f2py_wrapper.py diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..99a1c774 --- /dev/null +++ b/meson.build @@ -0,0 +1,383 @@ +project( + 'Assimulo', + ['c', 'cython', 'fortran'], + version: '3.9.0b1', + meson_version: '>=1.4' +) + +py = import('python').find_installation(pure: false) +fs = import('fs') +cc = meson.get_compiler('c') + +# --- NumPy include dir --- +numpy_inc = run_command( + py, ['-c', 'import numpy; print(numpy.get_include())'], + check: true +).stdout().strip() + +# --- Common flags --- +# numpy_inc is passed via -I because it may live inside the source tree (e.g. in a +# local venv), which meson's include_directories() disallows for absolute paths. +if cc.get_argument_syntax() == 'msvc' + common_c_args = ['-I' + numpy_inc] + if get_option('debug') + common_c_args += ['/Zi'] + common_link_args = ['/DEBUG'] + else + common_c_args += ['/O2'] + common_link_args = [] + endif +else + common_c_args = ['-fno-strict-aliasing', '-I' + numpy_inc] + if get_option('debug') + common_c_args += ['-g'] + common_link_args = ['-g'] + else + common_c_args += ['-O2'] + common_link_args = [] + endif +endif + +omp_dep = dependency('openmp', required: get_option('openmp')) + +# --- Install pure python package tree --- +# After `git mv src assimulo`, this installs assimulo/** into site-packages/assimulo/** +install_subdir('src/assimulo', install_dir: py.get_install_dir()) + +# --- Helper: find include/lib dirs from a prefix or from standard locations --- +sundials_prefix = get_option('sundials_prefix') +superlu_prefix = get_option('superlu_prefix') + +default_inc_candidates = ['/usr/include', '/usr/local/include'] +default_lib_candidates = ['/usr/lib', '/usr/lib64', '/usr/local/lib', '/usr/lib/x86_64-linux-gnu', '/usr/local/lib/x86_64-linux-gnu'] +if host_machine.system() == 'windows' + default_lib_candidates += ['C:/deps/lib'] +endif + +sundials_inc_candidates = [] +sundials_lib_candidates = [] +if sundials_prefix != '' + sundials_inc_candidates += [join_paths(sundials_prefix, 'include')] + foreach d : ['lib', 'lib64', 'lib/x86_64-linux-gnu'] + sundials_lib_candidates += [join_paths(sundials_prefix, d)] + endforeach +else + sundials_inc_candidates += default_inc_candidates + sundials_lib_candidates += default_lib_candidates +endif + +sundials_incdir = '' +foreach d : sundials_inc_candidates + if fs.is_file(join_paths(d, 'cvodes', 'cvodes.h')) + sundials_incdir = d + break + endif +endforeach +if sundials_incdir == '' + error('SUNDIALS not found: could not locate cvodes/cvodes.h. Set -Dsundials_prefix=...') +endif + +sundials_inc_dir = include_directories(sundials_incdir) + +sundials_libdirs = [] +foreach d : sundials_lib_candidates + if fs.is_dir(d) + sundials_libdirs += d + endif +endforeach + +# SUNDIALS 2.7.0 libraries — prefer shared so extensions don't duplicate the code +sundials_cvodes_libs = [ + cc.find_library('sundials_cvodes', dirs: sundials_libdirs, required: true), + cc.find_library('sundials_idas', dirs: sundials_libdirs, required: true), + cc.find_library('sundials_nvecserial', dirs: sundials_libdirs, required: true), +] + +sundials_kinsol_libs = [ + cc.find_library('sundials_kinsol', dirs: sundials_libdirs, required: true), + cc.find_library('sundials_nvecserial', dirs: sundials_libdirs, required: true), +] + +# --- SuperLU detection (optional; used by radau5ode + possibly sundials superlu solver) --- +with_superlu = false +superlu_incdir = '' +superlu_libdirs = [] + +if superlu_prefix != '' + foreach d : [join_paths(superlu_prefix, 'include'), join_paths(superlu_prefix, 'SRC')] + if fs.is_file(join_paths(d, 'supermatrix.h')) + superlu_incdir = d + break + endif + endforeach + + foreach d : ['lib', 'lib64', 'lib/x86_64-linux-gnu'] + p = join_paths(superlu_prefix, d) + if fs.is_dir(p) + superlu_libdirs += p + endif + endforeach + + if superlu_incdir != '' and superlu_libdirs.length() > 0 + with_superlu = true + endif +endif + +# With shared SUNDIALS libs, SuperLU and BLAS are embedded in the shared libs +# themselves (via target_link_libraries in the Dockerfiles), so extensions do +# not need to link against them separately. + +# --- Cython targets --- +cython_include_dirs = [ + '-I', meson.current_build_dir(), + '-I', join_paths(meson.current_source_dir(), 'src/assimulo'), + '-I', join_paths(meson.current_source_dir(), 'src/assimulo/lib'), +] +cython_common_args = ['-3', '-X', 'language_level=3str'] + cython_include_dirs + +# Core modules in assimulo/ +py.extension_module( + 'explicit_ode', + sources: ['src/assimulo/explicit_ode.pyx', 'src/assimulo/ode_event_locator.c'], + subdir: 'assimulo', + include_directories: include_directories('src/assimulo'), + c_args: common_c_args, + dependencies: [], + install: true +) + +foreach mod : ['algebraic', 'implicit_ode', 'ode', 'problem', 'special_systems', 'support'] + py.extension_module( + mod, + sources: ['src/assimulo/' + mod + '.pyx'], + subdir: 'assimulo', + include_directories: include_directories('src/assimulo'), + c_args: common_c_args, + dependencies: [], + install: true + ) +endforeach + +# Euler solver +py.extension_module( + 'euler', + sources: ['src/assimulo/solvers/euler.pyx'], + subdir: 'assimulo/solvers', + include_directories: include_directories('src/assimulo', 'src/assimulo/solvers'), + c_args: common_c_args, + dependencies: [], + install: true +) + +sundials_conf = configuration_data() +sundials_conf.set('SUNDIALS_WITH_SUPERLU', with_superlu ? 'True' : 'False') + +sundials_pxi = configure_file( + input: 'src/assimulo/sundials.pxi.in', + output: 'sundials.pxi', + configuration: sundials_conf, + install: false, +) + +py.extension_module( + 'sundials', + sources: ['src/assimulo/solvers/sundials.pyx', sundials_pxi], + subdir: 'assimulo/solvers', + include_directories: [include_directories('src/assimulo', 'src/assimulo/lib'), sundials_inc_dir], + c_args: common_c_args, + dependencies: [omp_dep] + sundials_cvodes_libs, + cython_args: cython_common_args, + install: true +) + +py.extension_module( + 'kinsol', + sources: ['src/assimulo/solvers/kinsol.pyx', sundials_pxi], + subdir: 'assimulo/solvers', + include_directories: [include_directories('src/assimulo', 'src/assimulo/lib'), sundials_inc_dir], + c_args: common_c_args, + dependencies: [omp_dep] + sundials_kinsol_libs, + cython_args: cython_common_args, + install: true +) + +# radau5ode (Cython + C sources, optional SuperLU) +radau5_inc_dirs = [ + include_directories('src/assimulo', 'src/assimulo/lib'), + sundials_inc_dir, + include_directories('thirdparty/radau5'), +] +radau5_c_args = common_c_args +libm = cc.find_library('m', required: host_machine.system() != 'windows') +radau5_deps = [omp_dep, libm] + +radau5_extra_sources = [ + 'thirdparty/radau5/radau5.c', + 'thirdparty/radau5/radau5_io.c', +] + +if with_superlu + radau5_c_args += ['-D__RADAU5_WITH_SUPERLU'] + radau5_inc_dirs += [include_directories(superlu_incdir)] + radau5_extra_sources += [ + 'thirdparty/radau5/superlu_double.c', + 'thirdparty/radau5/superlu_complex.c', + 'thirdparty/radau5/superlu_util.c', + ] + + superlu_mt = cc.find_library('superlu_mt_OPENMP', dirs: superlu_libdirs, static: true, required: true) + openblas = cc.find_library('openblas', dirs: default_lib_candidates, required: true) + radau5_deps += [superlu_mt, openblas] + + # gomp is typically needed when linking OpenMP with gcc + gomp = cc.find_library('gomp', required: false) + if gomp.found() + radau5_deps += [gomp] + endif +endif + +py.extension_module( + 'radau5ode', + sources: ['thirdparty/radau5/radau5ode.pyx'] + radau5_extra_sources, + subdir: 'assimulo/lib', + include_directories: radau5_inc_dirs, + c_args: radau5_c_args, + dependencies: radau5_deps, + cython_args: cython_common_args, + install: true +) + +f2py_src = run_command( + py, + ['-c', 'import numpy.f2py, os; print(os.path.join(os.path.dirname(numpy.f2py.__file__), "src"))'], + check: true +).stdout().strip() +fortranobject_c = join_paths(f2py_src, 'fortranobject.c') + +# BLAS/LAPACK for glimda — OpenBLAS dynamic on both platforms. The same +# libopenblas.so/.dll is reused by SUNDIALS (built against it inside the +# Docker manylinux image / MSYS2 build step), so the wheel ships exactly +# one OpenBLAS bundled by auditwheel (Linux) / delvewheel (Windows). +blas_dep = cc.find_library('openblas', dirs: default_lib_candidates, required: true) +glimda_deps = [blas_dep] +fortran_solvers = [ + { + 'name': 'dopri5', + 'pyf': files('thirdparty/hairer/dopri5.pyf'), + 'fortran_srcs': files('thirdparty/hairer/dopri5.f'), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'rodas', + 'pyf': files('thirdparty/hairer/rodas_decsol.pyf'), + 'fortran_srcs': files('thirdparty/hairer/rodas_decsol.f'), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'radau5', + 'pyf': files('thirdparty/hairer/radau_decsol.pyf'), + 'fortran_srcs': files('thirdparty/hairer/radau_decsol.f'), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'radar5', + 'pyf': files('thirdparty/hairer/radar5.pyf'), + 'fortran_srcs': files( + 'thirdparty/hairer/contr5.f90', 'thirdparty/hairer/radar5_int.f90', + 'thirdparty/hairer/radar5.f90', 'thirdparty/hairer/dontr5.f90', + 'thirdparty/hairer/decsol.f90', 'thirdparty/hairer/dc_decdel.f90', + ), + 'outputs': ['module.c', '-f2pywrappers.f', '-f2pywrappers2.f90'], + }, + { + 'name': 'odepack', + 'pyf': files('thirdparty/odepack/odepack.pyf'), + 'fortran_srcs': files( + 'thirdparty/odepack/opkdmain.f', 'thirdparty/odepack/opkda1.f', + 'thirdparty/odepack/opkda2.f', 'thirdparty/odepack/odepack_aux.f90', + ), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'odassl', + 'pyf': files('thirdparty/odassl/odassl.pyf'), + 'fortran_srcs': files( + 'thirdparty/odassl/odassl.f', 'thirdparty/odassl/odastp.f', + 'thirdparty/odassl/odacor.f', 'thirdparty/odassl/odajac.f', + 'thirdparty/odassl/d1mach.f', 'thirdparty/odassl/daxpy.f', + 'thirdparty/odassl/ddanrm.f', 'thirdparty/odassl/ddatrp.f', + 'thirdparty/odassl/ddot.f', 'thirdparty/odassl/ddwats.f', + 'thirdparty/odassl/dgefa.f', 'thirdparty/odassl/dgesl.f', + 'thirdparty/odassl/dscal.f', 'thirdparty/odassl/idamax.f', + 'thirdparty/odassl/xerrwv.f', + ), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'dasp3dp', + 'pyf': files('thirdparty/dasp3/dasp3dp.pyf'), + 'fortran_srcs': files( + 'thirdparty/dasp3/DASP3.f', 'thirdparty/dasp3/ANORM.f', + 'thirdparty/dasp3/CTRACT.f', 'thirdparty/dasp3/DECOMP.f', + 'thirdparty/dasp3/HMAX.f', 'thirdparty/dasp3/INIVAL.f', + 'thirdparty/dasp3/JACEST.f', 'thirdparty/dasp3/PDERIV.f', + 'thirdparty/dasp3/PREPOL.f', 'thirdparty/dasp3/SOLVE.f', + 'thirdparty/dasp3/SPAPAT.f', + ), + 'outputs': ['module.c', '-f2pywrappers.f'], + }, + { + 'name': 'glimda', + 'pyf': files('thirdparty/glimda/glimda_complete.pyf'), + 'fortran_srcs': files('thirdparty/glimda/glimda_complete.f'), + 'outputs': ['module.c', '-f2pywrappers.f'], + 'extra_deps': glimda_deps, + }, +] + +# --- f2py / Fortran solver modules --- +if get_option('enable_fortran') + foreach solver : fortran_solvers + solver_name = solver['name'] + fortran_srcs = solver['fortran_srcs'] + + solver_outputs = [] + foreach suf : solver['outputs'] + solver_outputs += solver_name + suf + endforeach + + wrappers = custom_target( + 'f2py_wrap_' + solver_name, + input: solver['pyf'], + output: solver_outputs, + command: [py, + join_paths(meson.current_source_dir(), 'tools', 'f2py_wrapper.py'), + '@OUTDIR@', solver_name, '@INPUT@'], + build_by_default: false, + ) + + # Compile Fortran sources as a static library first so that any Fortran + # module files (.mod) are generated before the f2py wrapper files are + # compiled (e.g. radar5-f2pywrappers2.f90 uses the ip_array module from + # radar5.f90, and meson does not scan custom_target outputs for 'use'). + solver_lib = static_library( + solver_name + '_impl', + sources: fortran_srcs, + fortran_args: ['-std=legacy'], + pic: true, + install: false, + ) + + py.extension_module( + solver_name, + sources: [wrappers] + [fortranobject_c], + subdir: 'assimulo/lib', + include_directories: include_directories('src/assimulo'), + c_args: common_c_args + ['-I' + f2py_src], + fortran_args: ['-std=legacy'], + dependencies: solver.get('extra_deps', []), + link_with: solver_lib, + install: true, + ) + endforeach +endif diff --git a/meson.options b/meson.options new file mode 100644 index 00000000..92ab41a7 --- /dev/null +++ b/meson.options @@ -0,0 +1,7 @@ +option('sundials_prefix', type: 'string', value: '', description: 'Prefix where SUNDIALS is installed (e.g. /usr, /opt/sundials)') +option('superlu_prefix', type: 'string', value: '', description: 'Prefix where SuperLU_MT is installed (e.g. /usr, /opt/superlu)') + +option('openmp', type: 'boolean', value: true, description: 'Enable OpenMP flags/deps') + +# f2py/Fortran modules (experimental in this first Meson pass) +option('enable_fortran', type: 'boolean', value: true, description: 'Build f2py-based Fortran solver extension modules') diff --git a/src/__init__.py b/src/assimulo/__init__.py similarity index 100% rename from src/__init__.py rename to src/assimulo/__init__.py diff --git a/src/algebraic.pxd b/src/assimulo/algebraic.pxd similarity index 100% rename from src/algebraic.pxd rename to src/assimulo/algebraic.pxd diff --git a/src/algebraic.pyx b/src/assimulo/algebraic.pyx similarity index 100% rename from src/algebraic.pyx rename to src/assimulo/algebraic.pyx diff --git a/src/constants.pxi b/src/assimulo/constants.pxi similarity index 100% rename from src/constants.pxi rename to src/assimulo/constants.pxi diff --git a/examples/__init__.py b/src/assimulo/examples/__init__.py similarity index 100% rename from examples/__init__.py rename to src/assimulo/examples/__init__.py diff --git a/examples/cvode_basic.py b/src/assimulo/examples/cvode_basic.py similarity index 100% rename from examples/cvode_basic.py rename to src/assimulo/examples/cvode_basic.py diff --git a/examples/cvode_basic_backward.py b/src/assimulo/examples/cvode_basic_backward.py similarity index 100% rename from examples/cvode_basic_backward.py rename to src/assimulo/examples/cvode_basic_backward.py diff --git a/examples/cvode_gyro.py b/src/assimulo/examples/cvode_gyro.py similarity index 100% rename from examples/cvode_gyro.py rename to src/assimulo/examples/cvode_gyro.py diff --git a/examples/cvode_stability.py b/src/assimulo/examples/cvode_stability.py similarity index 100% rename from examples/cvode_stability.py rename to src/assimulo/examples/cvode_stability.py diff --git a/examples/cvode_with_disc.py b/src/assimulo/examples/cvode_with_disc.py similarity index 100% rename from examples/cvode_with_disc.py rename to src/assimulo/examples/cvode_with_disc.py diff --git a/examples/cvode_with_initial_sensitivity.py b/src/assimulo/examples/cvode_with_initial_sensitivity.py similarity index 100% rename from examples/cvode_with_initial_sensitivity.py rename to src/assimulo/examples/cvode_with_initial_sensitivity.py diff --git a/examples/cvode_with_jac.py b/src/assimulo/examples/cvode_with_jac.py similarity index 100% rename from examples/cvode_with_jac.py rename to src/assimulo/examples/cvode_with_jac.py diff --git a/examples/cvode_with_jac_sparse.py b/src/assimulo/examples/cvode_with_jac_sparse.py similarity index 100% rename from examples/cvode_with_jac_sparse.py rename to src/assimulo/examples/cvode_with_jac_sparse.py diff --git a/examples/cvode_with_jac_spgmr.py b/src/assimulo/examples/cvode_with_jac_spgmr.py similarity index 100% rename from examples/cvode_with_jac_spgmr.py rename to src/assimulo/examples/cvode_with_jac_spgmr.py diff --git a/examples/cvode_with_parameters.py b/src/assimulo/examples/cvode_with_parameters.py similarity index 100% rename from examples/cvode_with_parameters.py rename to src/assimulo/examples/cvode_with_parameters.py diff --git a/examples/cvode_with_parameters_fcn.py b/src/assimulo/examples/cvode_with_parameters_fcn.py similarity index 100% rename from examples/cvode_with_parameters_fcn.py rename to src/assimulo/examples/cvode_with_parameters_fcn.py diff --git a/examples/cvode_with_parameters_modified.py b/src/assimulo/examples/cvode_with_parameters_modified.py similarity index 100% rename from examples/cvode_with_parameters_modified.py rename to src/assimulo/examples/cvode_with_parameters_modified.py diff --git a/examples/cvode_with_preconditioning.py b/src/assimulo/examples/cvode_with_preconditioning.py similarity index 100% rename from examples/cvode_with_preconditioning.py rename to src/assimulo/examples/cvode_with_preconditioning.py diff --git a/examples/dasp3_basic.py b/src/assimulo/examples/dasp3_basic.py similarity index 100% rename from examples/dasp3_basic.py rename to src/assimulo/examples/dasp3_basic.py diff --git a/examples/dopri5_basic.py b/src/assimulo/examples/dopri5_basic.py similarity index 100% rename from examples/dopri5_basic.py rename to src/assimulo/examples/dopri5_basic.py diff --git a/examples/dopri5_with_disc.py b/src/assimulo/examples/dopri5_with_disc.py similarity index 100% rename from examples/dopri5_with_disc.py rename to src/assimulo/examples/dopri5_with_disc.py diff --git a/examples/euler_basic.py b/src/assimulo/examples/euler_basic.py similarity index 100% rename from examples/euler_basic.py rename to src/assimulo/examples/euler_basic.py diff --git a/examples/euler_vanderpol.py b/src/assimulo/examples/euler_vanderpol.py similarity index 100% rename from examples/euler_vanderpol.py rename to src/assimulo/examples/euler_vanderpol.py diff --git a/examples/euler_with_disc.py b/src/assimulo/examples/euler_with_disc.py similarity index 100% rename from examples/euler_with_disc.py rename to src/assimulo/examples/euler_with_disc.py diff --git a/examples/glimda_vanderpol.py b/src/assimulo/examples/glimda_vanderpol.py similarity index 100% rename from examples/glimda_vanderpol.py rename to src/assimulo/examples/glimda_vanderpol.py diff --git a/examples/ida_basic_backward.py b/src/assimulo/examples/ida_basic_backward.py similarity index 100% rename from examples/ida_basic_backward.py rename to src/assimulo/examples/ida_basic_backward.py diff --git a/examples/ida_with_disc.py b/src/assimulo/examples/ida_with_disc.py similarity index 100% rename from examples/ida_with_disc.py rename to src/assimulo/examples/ida_with_disc.py diff --git a/examples/ida_with_initial_sensitivity.py b/src/assimulo/examples/ida_with_initial_sensitivity.py similarity index 100% rename from examples/ida_with_initial_sensitivity.py rename to src/assimulo/examples/ida_with_initial_sensitivity.py diff --git a/examples/ida_with_jac.py b/src/assimulo/examples/ida_with_jac.py similarity index 100% rename from examples/ida_with_jac.py rename to src/assimulo/examples/ida_with_jac.py diff --git a/examples/ida_with_jac_spgmr.py b/src/assimulo/examples/ida_with_jac_spgmr.py similarity index 100% rename from examples/ida_with_jac_spgmr.py rename to src/assimulo/examples/ida_with_jac_spgmr.py diff --git a/examples/ida_with_parameters.py b/src/assimulo/examples/ida_with_parameters.py similarity index 100% rename from examples/ida_with_parameters.py rename to src/assimulo/examples/ida_with_parameters.py diff --git a/examples/ida_with_user_defined_handle_result.py b/src/assimulo/examples/ida_with_user_defined_handle_result.py similarity index 100% rename from examples/ida_with_user_defined_handle_result.py rename to src/assimulo/examples/ida_with_user_defined_handle_result.py diff --git a/examples/kinsol_basic.py b/src/assimulo/examples/kinsol_basic.py similarity index 100% rename from examples/kinsol_basic.py rename to src/assimulo/examples/kinsol_basic.py diff --git a/examples/kinsol_ors.py b/src/assimulo/examples/kinsol_ors.py similarity index 100% rename from examples/kinsol_ors.py rename to src/assimulo/examples/kinsol_ors.py diff --git a/examples/kinsol_ors_matrix.mtx b/src/assimulo/examples/kinsol_ors_matrix.mtx similarity index 100% rename from examples/kinsol_ors_matrix.mtx rename to src/assimulo/examples/kinsol_ors_matrix.mtx diff --git a/examples/kinsol_with_jac.py b/src/assimulo/examples/kinsol_with_jac.py similarity index 100% rename from examples/kinsol_with_jac.py rename to src/assimulo/examples/kinsol_with_jac.py diff --git a/examples/lsodar_bouncing_ball.py b/src/assimulo/examples/lsodar_bouncing_ball.py similarity index 100% rename from examples/lsodar_bouncing_ball.py rename to src/assimulo/examples/lsodar_bouncing_ball.py diff --git a/examples/lsodar_vanderpol.py b/src/assimulo/examples/lsodar_vanderpol.py similarity index 100% rename from examples/lsodar_vanderpol.py rename to src/assimulo/examples/lsodar_vanderpol.py diff --git a/examples/lsodar_with_disc.py b/src/assimulo/examples/lsodar_with_disc.py similarity index 100% rename from examples/lsodar_with_disc.py rename to src/assimulo/examples/lsodar_with_disc.py diff --git a/examples/mech_system_pendulum.py b/src/assimulo/examples/mech_system_pendulum.py similarity index 100% rename from examples/mech_system_pendulum.py rename to src/assimulo/examples/mech_system_pendulum.py diff --git a/examples/radar_basic.py b/src/assimulo/examples/radar_basic.py similarity index 100% rename from examples/radar_basic.py rename to src/assimulo/examples/radar_basic.py diff --git a/examples/radau5dae_time_events.py b/src/assimulo/examples/radau5dae_time_events.py similarity index 100% rename from examples/radau5dae_time_events.py rename to src/assimulo/examples/radau5dae_time_events.py diff --git a/examples/radau5dae_vanderpol.py b/src/assimulo/examples/radau5dae_vanderpol.py similarity index 100% rename from examples/radau5dae_vanderpol.py rename to src/assimulo/examples/radau5dae_vanderpol.py diff --git a/examples/radau5ode_vanderpol.py b/src/assimulo/examples/radau5ode_vanderpol.py similarity index 100% rename from examples/radau5ode_vanderpol.py rename to src/assimulo/examples/radau5ode_vanderpol.py diff --git a/examples/radau5ode_with_disc.py b/src/assimulo/examples/radau5ode_with_disc.py similarity index 100% rename from examples/radau5ode_with_disc.py rename to src/assimulo/examples/radau5ode_with_disc.py diff --git a/examples/radau5ode_with_disc_sparse.py b/src/assimulo/examples/radau5ode_with_disc_sparse.py similarity index 100% rename from examples/radau5ode_with_disc_sparse.py rename to src/assimulo/examples/radau5ode_with_disc_sparse.py diff --git a/examples/radau5ode_with_jac_sparse.py b/src/assimulo/examples/radau5ode_with_jac_sparse.py similarity index 100% rename from examples/radau5ode_with_jac_sparse.py rename to src/assimulo/examples/radau5ode_with_jac_sparse.py diff --git a/examples/rodasode_vanderpol.py b/src/assimulo/examples/rodasode_vanderpol.py similarity index 100% rename from examples/rodasode_vanderpol.py rename to src/assimulo/examples/rodasode_vanderpol.py diff --git a/examples/rungekutta34_basic.py b/src/assimulo/examples/rungekutta34_basic.py similarity index 100% rename from examples/rungekutta34_basic.py rename to src/assimulo/examples/rungekutta34_basic.py diff --git a/examples/rungekutta34_with_disc.py b/src/assimulo/examples/rungekutta34_with_disc.py similarity index 100% rename from examples/rungekutta34_with_disc.py rename to src/assimulo/examples/rungekutta34_with_disc.py diff --git a/examples/rungekutta4_basic.py b/src/assimulo/examples/rungekutta4_basic.py similarity index 100% rename from examples/rungekutta4_basic.py rename to src/assimulo/examples/rungekutta4_basic.py diff --git a/src/exception.py b/src/assimulo/exception.py similarity index 100% rename from src/exception.py rename to src/assimulo/exception.py diff --git a/src/explicit_ode.pxd b/src/assimulo/explicit_ode.pxd similarity index 100% rename from src/explicit_ode.pxd rename to src/assimulo/explicit_ode.pxd diff --git a/src/explicit_ode.pyx b/src/assimulo/explicit_ode.pyx similarity index 100% rename from src/explicit_ode.pyx rename to src/assimulo/explicit_ode.pyx diff --git a/src/implicit_ode.pxd b/src/assimulo/implicit_ode.pxd similarity index 100% rename from src/implicit_ode.pxd rename to src/assimulo/implicit_ode.pxd diff --git a/src/implicit_ode.pyx b/src/assimulo/implicit_ode.pyx similarity index 100% rename from src/implicit_ode.pyx rename to src/assimulo/implicit_ode.pyx diff --git a/src/lib/__init__.py b/src/assimulo/lib/__init__.py similarity index 100% rename from src/lib/__init__.py rename to src/assimulo/lib/__init__.py diff --git a/src/lib/radau_core.py b/src/assimulo/lib/radau_core.py similarity index 100% rename from src/lib/radau_core.py rename to src/assimulo/lib/radau_core.py diff --git a/src/lib/sundials_callbacks.pxi b/src/assimulo/lib/sundials_callbacks.pxi similarity index 99% rename from src/lib/sundials_callbacks.pxi rename to src/assimulo/lib/sundials_callbacks.pxi index e970aea8..be0caa62 100644 --- a/src/lib/sundials_callbacks.pxi +++ b/src/assimulo/lib/sundials_callbacks.pxi @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +include "sundials.pxi" from numpy cimport PyArray_DATA diff --git a/src/lib/sundials_callbacks_ida_cvode.pxi b/src/assimulo/lib/sundials_callbacks_ida_cvode.pxi similarity index 99% rename from src/lib/sundials_callbacks_ida_cvode.pxi rename to src/assimulo/lib/sundials_callbacks_ida_cvode.pxi index b6eb2ca3..0a21622f 100644 --- a/src/lib/sundials_callbacks_ida_cvode.pxi +++ b/src/assimulo/lib/sundials_callbacks_ida_cvode.pxi @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +include "sundials.pxi" import cython import traceback diff --git a/src/lib/sundials_callbacks_kinsol.pxi b/src/assimulo/lib/sundials_callbacks_kinsol.pxi similarity index 99% rename from src/lib/sundials_callbacks_kinsol.pxi rename to src/assimulo/lib/sundials_callbacks_kinsol.pxi index 3066c293..7d6e4020 100644 --- a/src/lib/sundials_callbacks_kinsol.pxi +++ b/src/assimulo/lib/sundials_callbacks_kinsol.pxi @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +include "sundials.pxi" import cython import traceback diff --git a/src/lib/sundials_constants.pxi b/src/assimulo/lib/sundials_constants.pxi similarity index 100% rename from src/lib/sundials_constants.pxi rename to src/assimulo/lib/sundials_constants.pxi diff --git a/src/lib/sundials_includes.pxd b/src/assimulo/lib/sundials_includes.pxd similarity index 99% rename from src/lib/sundials_includes.pxd rename to src/assimulo/lib/sundials_includes.pxd index 671bc3db..5abfbd0d 100644 --- a/src/lib/sundials_includes.pxd +++ b/src/assimulo/lib/sundials_includes.pxd @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +include "sundials.pxi" """ Cython Wrapper for interfacing Python with CVode and IDA (Sundials Version 2.4.0) diff --git a/src/ode.pxd b/src/assimulo/ode.pxd similarity index 100% rename from src/ode.pxd rename to src/assimulo/ode.pxd diff --git a/src/ode.pyx b/src/assimulo/ode.pyx similarity index 100% rename from src/ode.pyx rename to src/assimulo/ode.pyx diff --git a/src/ode_event_locator.c b/src/assimulo/ode_event_locator.c similarity index 100% rename from src/ode_event_locator.c rename to src/assimulo/ode_event_locator.c diff --git a/src/ode_event_locator.h b/src/assimulo/ode_event_locator.h similarity index 100% rename from src/ode_event_locator.h rename to src/assimulo/ode_event_locator.h diff --git a/src/problem.pxd b/src/assimulo/problem.pxd similarity index 100% rename from src/problem.pxd rename to src/assimulo/problem.pxd diff --git a/src/problem.pyx b/src/assimulo/problem.pyx similarity index 100% rename from src/problem.pyx rename to src/assimulo/problem.pyx diff --git a/src/problem_algebraic.py b/src/assimulo/problem_algebraic.py similarity index 100% rename from src/problem_algebraic.py rename to src/assimulo/problem_algebraic.py diff --git a/src/solvers/__init__.py b/src/assimulo/solvers/__init__.py similarity index 100% rename from src/solvers/__init__.py rename to src/assimulo/solvers/__init__.py diff --git a/src/solvers/dasp3.py b/src/assimulo/solvers/dasp3.py similarity index 100% rename from src/solvers/dasp3.py rename to src/assimulo/solvers/dasp3.py diff --git a/src/solvers/euler.pyx b/src/assimulo/solvers/euler.pyx similarity index 99% rename from src/solvers/euler.pyx rename to src/assimulo/solvers/euler.pyx index 201f5ca4..3ced2b0e 100644 --- a/src/solvers/euler.pyx +++ b/src/assimulo/solvers/euler.pyx @@ -24,7 +24,7 @@ import scipy.sparse as sps from assimulo.explicit_ode cimport Explicit_ODE from assimulo.exception import AssimuloException -include "constants.pxi" #Includes the constants (textual include) +include "../constants.pxi" #Includes the constants (textual include) cdef class ImplicitEuler(Explicit_ODE): """ diff --git a/src/solvers/glimda.py b/src/assimulo/solvers/glimda.py similarity index 100% rename from src/solvers/glimda.py rename to src/assimulo/solvers/glimda.py diff --git a/src/solvers/kinsol.pyx b/src/assimulo/solvers/kinsol.pyx similarity index 99% rename from src/solvers/kinsol.pyx rename to src/assimulo/solvers/kinsol.pyx index 341f75a1..e0f25886 100644 --- a/src/solvers/kinsol.pyx +++ b/src/assimulo/solvers/kinsol.pyx @@ -16,6 +16,7 @@ # along with this program. If not, see . # distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION +include "sundials.pxi" import numpy as np cimport numpy as np @@ -35,7 +36,7 @@ IF SUNDIALS_VERSION >= (6,0,0): ELSE: from sundials_includes cimport N_VDestroy_Serial as N_VDestroy -include "constants.pxi" #Includes the constants (textual include) +include "../constants.pxi" #Includes the constants (textual include) include "../lib/sundials_constants.pxi" #Sundials related constants include "../lib/sundials_callbacks.pxi" include "../lib/sundials_callbacks_kinsol.pxi" diff --git a/src/solvers/odassl.py b/src/assimulo/solvers/odassl.py similarity index 100% rename from src/solvers/odassl.py rename to src/assimulo/solvers/odassl.py diff --git a/src/solvers/odepack.py b/src/assimulo/solvers/odepack.py similarity index 100% rename from src/solvers/odepack.py rename to src/assimulo/solvers/odepack.py diff --git a/src/solvers/radar5.py b/src/assimulo/solvers/radar5.py similarity index 100% rename from src/solvers/radar5.py rename to src/assimulo/solvers/radar5.py diff --git a/src/solvers/radau5.py b/src/assimulo/solvers/radau5.py similarity index 100% rename from src/solvers/radau5.py rename to src/assimulo/solvers/radau5.py diff --git a/src/solvers/rosenbrock.py b/src/assimulo/solvers/rosenbrock.py similarity index 100% rename from src/solvers/rosenbrock.py rename to src/assimulo/solvers/rosenbrock.py diff --git a/src/solvers/runge_kutta.py b/src/assimulo/solvers/runge_kutta.py similarity index 100% rename from src/solvers/runge_kutta.py rename to src/assimulo/solvers/runge_kutta.py diff --git a/src/solvers/sdirk_dae.pyx b/src/assimulo/solvers/sdirk_dae.pyx similarity index 100% rename from src/solvers/sdirk_dae.pyx rename to src/assimulo/solvers/sdirk_dae.pyx diff --git a/src/solvers/sundials.pyx b/src/assimulo/solvers/sundials.pyx similarity index 99% rename from src/solvers/sundials.pyx rename to src/assimulo/solvers/sundials.pyx index e0843152..db80d7f0 100644 --- a/src/solvers/sundials.pyx +++ b/src/assimulo/solvers/sundials.pyx @@ -16,6 +16,7 @@ # along with this program. If not, see . # distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION +include "sundials.pxi" import numpy as np cimport numpy as np @@ -40,7 +41,7 @@ ELSE: from sundials_includes cimport N_VCloneVectorArray_Serial as N_VCloneVectorArray from sundials_includes cimport N_VDestroy_Serial as N_VDestroy -include "constants.pxi" #Includes the constants (textual include) +include "../constants.pxi" #Includes the constants (textual include) include "../lib/sundials_constants.pxi" #Sundials related constants include "../lib/sundials_callbacks.pxi" include "../lib/sundials_callbacks_ida_cvode.pxi" diff --git a/src/special_systems.pyx b/src/assimulo/special_systems.pyx similarity index 100% rename from src/special_systems.pyx rename to src/assimulo/special_systems.pyx diff --git a/src/assimulo/sundials.pxi.in b/src/assimulo/sundials.pxi.in new file mode 100644 index 00000000..14fccfef --- /dev/null +++ b/src/assimulo/sundials.pxi.in @@ -0,0 +1,5 @@ +# This file is generated by Meson +DEF SUNDIALS_VERSION = (2, 7, 0) +DEF SUNDIALS_WITH_SUPERLU = @SUNDIALS_WITH_SUPERLU@ +DEF SUNDIALS_VECTOR_SIZE = '32' +DEF SUNDIALS_CVODE_RTOL_VEC = False diff --git a/src/support.pxd b/src/assimulo/support.pxd similarity index 100% rename from src/support.pxd rename to src/assimulo/support.pxd diff --git a/src/support.pyx b/src/assimulo/support.pyx similarity index 100% rename from src/support.pyx rename to src/assimulo/support.pyx diff --git a/thirdparty/glimda/glimda_complete.f b/thirdparty/glimda/glimda_complete.f index 6d3f1d0b..c4262716 100644 --- a/thirdparty/glimda/glimda_complete.f +++ b/thirdparty/glimda/glimda_complete.f @@ -1494,13 +1494,13 @@ subroutine glimda(m , n , fevl , qevl , dfyevl, dfxevl, C integer P3METH [-3] .. the particular order 3 method to use C C P3METH = 1 use a method with s=r=p=q=3 and -C A(alpha) stability for alpha=74° +C A(alpha) stability for alpha=74 deg C but being unconditionally stable C at zero and at infinity C P3METH = 2 use a method with s=r=p=q=3 and -C A(alpha) stability for alpha=88.1° +C A(alpha) stability for alpha=88.1 deg C -C default: 1 +C default: 1 C SEE ALSO C iopt C @@ -2397,11 +2397,11 @@ subroutine glimda(m , n , fevl , qevl , dfyevl, dfxevl, C delta depends on the method currently used C integer type .. the particular order 3 method to use C type = 1 use a method with s=r=p=q=3 and -C A(alpha) stability for alpha=74° +C A(alpha) stability for alpha=74 deg C but being unconditionally stable C at zero and at infinity -C type = 2 use a method with s=r=p=q=3 and -C A(alpha) stability for alpha=88.1° +C type = 2 use a method with s=r=p=q=3 and +C A(alpha) stability for alpha=88.1 deg C integer ierr .. return code (0 signals success) C C COPYRIGHT @@ -2655,9 +2655,9 @@ subroutine p3s3(p,MD,A,U,B,V,c,c1beta,delta,ierr) real*8 lambda,A(MD,MD),U(MD,MD),B(MD,MD),V(MD,MD),c(MD), $ c1beta(MD),delta(MD+1,2) -C .. alpha = 61.7° .. +C .. alpha = 61.7 deg .. lambda = 0.15d0 -C .. alpha = 74° .. +C .. alpha = 74 deg .. lambda = 4.d0/25.d0 A(1,1) = lambda @@ -2733,7 +2733,7 @@ subroutine p3s3(p,MD,A,U,B,V,c,c1beta,delta,ierr) C method is not A-stable but A(alpha) stable and the only eigenvalue C of M(\infty) is zero. Compared to p3s3, the method is not C unconditionally stable but the region of A(alpha) stability is -C considerably enlarged to alpha=88.1°. +C considerably enlarged to alpha=88.1 deg. C C COPYRIGHT C diff --git a/thirdparty/hairer/radar5.f90 b/thirdparty/hairer/radar5.f90 index 5c33c7e1..429221e7 100644 --- a/thirdparty/hairer/radar5.f90 +++ b/thirdparty/hairer/radar5.f90 @@ -925,8 +925,8 @@ SUBROUTINE RADCOR(N,X,Y,XEND,H,FCN,PHI,ARGLAG,RTOL,ATOL,ITOL, & RPAR INTEGER, dimension(1), intent(in) :: & IPAR,IPAST - REAL(kind=DP), dimension(1), intent(in) :: & - GRID + REAL(kind=DP), dimension(1), intent(inout) :: & + GRID REAL(kind=DP), dimension(:,:), allocatable :: & FJACL,XLAG REAL(kind=DP), dimension(:), allocatable :: & diff --git a/tools/f2py_wrapper.py b/tools/f2py_wrapper.py new file mode 100644 index 00000000..b6432ba2 --- /dev/null +++ b/tools/f2py_wrapper.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Generate f2py C/Fortran wrapper files (no compilation). + +Called by meson custom_target. Produces in : + module.c - C/Python glue + -f2pywrappers.f - Fortran 77 shims (placeholder if f2py emits none) + -f2pywrappers2.f90 - Fortran 90 shims (radar5 only; placeholder otherwise) + +Usage: python3 f2py_wrapper.py +""" + +import subprocess +import sys +from pathlib import Path + + +def main(): + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + outdir = Path(sys.argv[1]).resolve() + modname = sys.argv[2] + pyf = Path(sys.argv[3]).resolve() + outdir.mkdir(parents=True, exist_ok=True) + + # Mirror what run_compile does for .pyf + meson backend (f2py2e.py:734): + # invoke f2py with just the .pyf (no -m, no --build-dir) and let the .pyf's + # own `python module ` declaration drive the output filename. Passing + # -m here would force every `python module` block in the .pyf to share the + # same module.c output, last write wins. + subprocess.run( + [sys.executable, "-m", "numpy.f2py", str(pyf)], + cwd=outdir, + check=True, + ) + + # meson custom_target declares fixed outputs per solver; create empty + # placeholders for the wrapper files f2py chose not to emit for this .pyf. + for name in (f"{modname}-f2pywrappers.f", f"{modname}-f2pywrappers2.f90"): + path = outdir / name + if not path.exists(): + path.write_text("! f2py placeholder\n") + + +if __name__ == "__main__": + main() From cd1de500fb3772130c7fa5543e5608b04cc7db8d Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:02:58 +0200 Subject: [PATCH 3/9] build: containerized SUNDIALS/SuperLU + Makefile for dev and manylinux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: build SUNDIALS (Modelon fork v2.7.0-3) and SuperLU_MT from source into /usr, with OpenBLAS as the BLAS provider. Embed SuperLU_MT + OpenBLAS into the SUNDIALS shared libs so .so files are self-contained. uv binary copied in for `make compile-deps`. - Dockerfile.manylinux: parallel manylinux image with the same SUNDIALS + SuperLU pre-installed, so cibuildwheel can produce manylinux wheels without rebuilding the deps each run. - tools/wheels/build_dependencies.sh: shared dep-build script reused by both Dockerfiles and the manylinux wheel pipeline. - tools/wheels/build_dependencies_windows.sh + repair_windows.ps1: Windows equivalents — build SUNDIALS under MSYS2 UCRT64; repair wheels via delvewheel. - Makefile: docker-based build/test/shell/wheel/wheel-cibw/wheel-portable targets; build-dev for incremental editable installs; build-dev-image / build-manylinux-image to (re)build the images; compile-deps regenerates requirements.lock via uv. Co-Authored-By: Claude Opus 4.7 --- Dockerfile | 121 +++++++++++++-------- Dockerfile.manylinux | 8 ++ Makefile | 73 +++++++++---- tools/wheels/build_dependencies.sh | 79 ++++++++++++++ tools/wheels/build_dependencies_windows.sh | 89 +++++++++++++++ tools/wheels/repair_windows.ps1 | 8 ++ 6 files changed, 313 insertions(+), 65 deletions(-) create mode 100644 Dockerfile.manylinux create mode 100755 tools/wheels/build_dependencies.sh create mode 100755 tools/wheels/build_dependencies_windows.sh create mode 100644 tools/wheels/repair_windows.ps1 diff --git a/Dockerfile b/Dockerfile index 7ce3f0ed..7777a63d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,76 @@ -FROM python:3.11 - -ARG SUNDIALS_VERSION=2.7.0 - -# System deps -RUN apt-get update && apt-get -y install cmake liblapack-dev libsuitesparse-dev libhypre-dev curl git make vim bash-completion -RUN cp -v /usr/lib/x86_64-linux-gnu/libblas.so /usr/lib/x86_64-linux-gnu/libblas_OPENMP.so - -# Python packages -RUN pip install Cython numpy scipy matplotlib setuptools==69.1.0 - -# SuperLU MT 4.0.1 -RUN cd /tmp && \ - curl -fSsL https://github.com/xiaoyeli/superlu_mt/archive/refs/tags/v4.0.1.tar.gz | tar xz && \ - cd superlu_mt-4.0.1 && \ - cmake -Denable_examples=OFF -Denable_tests=OFF -DPLAT="_OPENMP" \ - -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib \ - -DSUPERLUMT_INSTALL_INCLUDEDIR=include . && \ - make -j4 && make install - -# Sundials (version-parameterized; v2.7.0 needs CMakeLists patch) -RUN git clone --depth 1 -b v${SUNDIALS_VERSION} https://github.com/LLNL/sundials /tmp/sundials && \ - cd /tmp/sundials && \ - if [ "${SUNDIALS_VERSION}" = "2.7.0" ]; then \ - echo "target_link_libraries(sundials_idas_shared lapack blas superlu_mt_OPENMP)" >> src/idas/CMakeLists.txt; \ - echo "target_link_libraries(sundials_kinsol_shared lapack blas superlu_mt_OPENMP)" >> src/kinsol/CMakeLists.txt; \ - fi && \ - mkdir build && cd build && \ - cmake -LAH \ - -DSUPERLUMT_BLAS_LIBRARIES=blas \ - -DSUPERLUMT_LIBRARIES=blas \ - -DSUPERLUMT_INCLUDE_DIR=/usr/include \ - -DSUPERLUMT_LIBRARY=/usr/lib/libsuperlu_mt_OPENMP.a \ - -DSUPERLUMT_THREAD_TYPE=OpenMP \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DSUPERLUMT_ENABLE=ON \ - -DLAPACK_ENABLE=ON \ - -DEXAMPLES_ENABLE=OFF \ - -DEXAMPLES_ENABLE_C=OFF \ - -DBUILD_STATIC_LIBS=OFF \ - -DSUNDIALS_INDEX_SIZE=32 \ - .. && \ - make -j4 && make install - -ARG PYTHON_VENV=/src/.venv -ENV PATH=${PYTHON_VENV}/bin:$PATH +FROM ubuntu:24.04 + +# ------------------------------------------------------------ +# System tooling and Python +# ------------------------------------------------------------ +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + python3-dev \ + python3-venv \ + git \ + curl \ + libopenblas-dev \ + cmake \ + build-essential \ + gfortran \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# uv handles requirements.lock generation in `make compile-deps`. +COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /uvx /usr/local/bin/ + +# ------------------------------------------------------------ +# SuperLU +# ------------------------------------------------------------ +WORKDIR /tmp +RUN curl -fSsL https://github.com/xiaoyeli/superlu_mt/archive/refs/tags/v4.0.1.tar.gz \ + | tar xz + +RUN cmake -S /tmp/superlu_mt-4.0.1 -B /tmp/superlu_mt-4.0.1/build \ + -Denable_examples=OFF \ + -Denable_tests=OFF \ + -DPLAT="_OPENMP" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DSUPERLUMT_INSTALL_INCLUDEDIR=include \ + && make -C /tmp/superlu_mt-4.0.1/build -j$(nproc) \ + && make -C /tmp/superlu_mt-4.0.1/build install \ + && rm -rf /tmp/superlu_mt-4.0.1 + +# ------------------------------------------------------------ +# SUNDIALS +# ------------------------------------------------------------ +ARG SUNDIALS_VERSION=2.7.0-3 + +RUN git clone --depth 1 -b v${SUNDIALS_VERSION} https://github.com/modelon-community/sundials /tmp/sundials + +# Embed SuperLU_MT + OpenBLAS into SUNDIALS shared libs so .so files are +# self-contained. LAPACK_ENABLE=OFF: Assimulo never calls CVLapack* APIs. +RUN echo "target_link_libraries(sundials_cvodes_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/cvodes/CMakeLists.txt && \ + echo "target_link_libraries(sundials_idas_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/idas/CMakeLists.txt && \ + echo "target_link_libraries(sundials_kinsol_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/kinsol/CMakeLists.txt + +RUN cmake -S /tmp/sundials -B /tmp/sundials/build \ + -DSUPERLUMT_INCLUDE_DIR=/usr/include \ + -DSUPERLUMT_LIBRARY=/usr/lib/libsuperlu_mt_OPENMP.a \ + -DSUPERLUMT_THREAD_TYPE=OpenMP \ + -DSUPERLUMT_ENABLE=ON \ + -DLAPACK_ENABLE=OFF \ + -DEXAMPLES_ENABLE=OFF \ + -DEXAMPLES_ENABLE_C=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_INSTALL_PREFIX=/usr \ + && make -C /tmp/sundials/build -j$(nproc) \ + && make -C /tmp/sundials/build install \ + && rm -rf /tmp/sundials + +# ------------------------------------------------------------ +# Final image state +# ------------------------------------------------------------ WORKDIR /src +CMD ["/bin/bash"] diff --git a/Dockerfile.manylinux b/Dockerfile.manylinux new file mode 100644 index 00000000..f844e148 --- /dev/null +++ b/Dockerfile.manylinux @@ -0,0 +1,8 @@ +FROM quay.io/pypa/manylinux_2_28_x86_64 + +# Build SuperLU_MT and SUNDIALS using the same script as cibuildwheel. +COPY tools/wheels/build_dependencies.sh /tmp/build_dependencies.sh +RUN bash /tmp/build_dependencies.sh && rm /tmp/build_dependencies.sh + +WORKDIR /src +CMD ["/bin/bash"] diff --git a/Makefile b/Makefile index be6dcb0d..5bb9c0fb 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -.PHONY: build build-dev-image test shell clean +.PHONY: build wheel build-dev-image build-manylinux-image test shell compile-deps check-meson.build build-dev wheel-portable wheel-cibw +DOCKER_IMAGE := assimulo-dev +MANYLINUX_IMAGE := assimulo-manylinux +MESON_BUILD_DIR := builddir +PYTHON_VERSIONS ?= 3.12 +IN_DOCKER_IMG := $(shell test -f /.dockerenv && echo 1 || echo 0) -SUNDIALS_VERSION ?= 2.7.0 -DOCKER_IMAGE := assimulo-dev-sundials_$(SUNDIALS_VERSION) - -IN_DOCKER_IMG := $(shell test -f /.dockerenv && echo 1 || echo 0) - -FORTRAN_FLAGS := "-std=legacy" -SETUPTOOLS_JFLAG=-j$(shell nproc) +MESON_SETUP_ARGS := -Dsundials_prefix=/usr -Dsuperlu_prefix=/usr -Dopenmp=true +PIP_SETUP_ARGS := $(addprefix -Csetup-args=,$(MESON_SETUP_ARGS)) define _run @if [ $(IN_DOCKER_IMG) -eq 1 ]; then \ @@ -15,26 +15,61 @@ define _run docker run \ --rm $(2) \ -v $(CURDIR):/src \ - $(DOCKER_IMAGE) \ + ${DOCKER_IMAGE} \ $(1); \ fi endef +define _run_with_venv + $(call _run, bash -c '. .venv/bin/activate && $(1)') +endef + build-dev-image: - docker build --build-arg SUNDIALS_VERSION=$(SUNDIALS_VERSION) -t $(DOCKER_IMAGE) . + docker build -t ${DOCKER_IMAGE} . + +build-manylinux-image: + docker build -f Dockerfile.manylinux -t ${MANYLINUX_IMAGE} . -.venv: - $(call _run, python3.11 -m venv .venv --system-site-packages) - $(call _run, pip install pytest) +.venv: requirements.lock + $(call _run, python3 -m venv .venv) + $(call _run_with_venv, pip install -r requirements.lock) + $(call _run, touch .venv) build: .venv - $(call _run, python3.11 setup.py build_ext ${SETUPTOOLS_JFLAG} install --sundials-home=/usr --blas-home=/usr/lib/x86_64-linux-gnu/ --lapack-home=/usr/lib/x86_64-linux-gnu/ --superlu-home=/usr --extra-fortran-compile-flags=$(FORTRAN_FLAGS)) + $(call _run_with_venv, pip install . -v $(PIP_SETUP_ARGS)) -test: build - $(call _run, pytest) +wheel: .venv + $(call _run_with_venv, pip wheel . --no-deps $(PIP_SETUP_ARGS) -w dist) + +build-dev: .venv + $(call _run_with_venv, pip install --no-build-isolation -e . -v -Cbuild-dir=$(MESON_BUILD_DIR) $(PIP_SETUP_ARGS)) + +test: .venv + $(call _run_with_venv, pytest) shell: - $(call _run, /bin/bash,-it) + $(call _run, /bin/bash, -it) + +check-meson.build: .venv + $(call _run_with_venv, meson setup $(MESON_BUILD_DIR) $(MESON_SETUP_ARGS) --wipe) + +# Regenerate requirements.lock from pyproject.toml. Run after changing +# build-system requires or runtime dependencies; commit the resulting file. +compile-deps: + $(call _run, uv pip compile --python python3 --group=dev --output-file=requirements.lock pyproject.toml) + +wheel-cibw: + pip install cibuildwheel + cibuildwheel --platform linux --output-dir dist/ -clean: - rm -rf build/ \ No newline at end of file +wheel-portable: + mkdir -p dist + docker run --rm -v $(CURDIR):/src $(MANYLINUX_IMAGE) bash -c '\ + mkdir -p /src/dist/raw; \ + for pyver in $(PYTHON_VERSIONS); do \ + pydir="cp$${pyver//./}-cp$${pyver//./}"; \ + /opt/python/$$pydir/bin/pip wheel /src --no-deps $(PIP_SETUP_ARGS) -w /src/dist/raw; \ + done; \ + for whl in /src/dist/raw/assimulo-*.whl; do \ + auditwheel repair $$whl --plat manylinux_2_27_x86_64 -w /src/dist; \ + done' diff --git a/tools/wheels/build_dependencies.sh b/tools/wheels/build_dependencies.sh new file mode 100755 index 00000000..e6f044eb --- /dev/null +++ b/tools/wheels/build_dependencies.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# tools/wheels/build_dependencies.sh +# Builds SuperLU_MT and SUNDIALS from source into /usr. +# Runs inside quay.io/pypa/manylinux_2_28_x86_64 (AlmaLinux 8, dnf available). +# Used by both Dockerfile.manylinux (local testing) and cibuildwheel (CI). +set -eux + +# Ensure standard system paths are in PATH (cibuildwheel's before-all +# environment may have a minimal PATH that omits /usr/bin). +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:${PATH}}" + +SUPERLU_VERSION="4.0.1" +SUNDIALS_VERSION="2.7.0-3" +NPROC=$(nproc) + +# Both SUNDIALS 2.7 and SuperLU_MT 4.0.1 use `cmake_minimum_required` values +# that modern cmake (>= 4.x, shipped in manylinux_2_28) rejects. Export as +# an env var so it propagates to cmake's `try_compile` sub-invocations. +export CMAKE_POLICY_VERSION_MINIMUM=3.5 + +# --- System packages --- +# OpenBLAS provides both BLAS and LAPACK symbols. SUNDIALS is built with +# LAPACK_ENABLE=OFF (Assimulo never calls CVLapackDense/Band APIs), so no +# netlib reference BLAS/LAPACK is needed. SuperLU_MT and glimda both link +# against OpenBLAS. +yum install -y \ + gcc-gfortran \ + openblas-devel +yum clean all + +# --- SuperLU_MT --- +curl -fSsL \ + "https://github.com/xiaoyeli/superlu_mt/archive/refs/tags/v${SUPERLU_VERSION}.tar.gz" \ + | tar xz -C /tmp + +cmake -S "/tmp/superlu_mt-${SUPERLU_VERSION}" \ + -B "/tmp/superlu_mt-${SUPERLU_VERSION}/build" \ + -Denable_examples=OFF \ + -Denable_tests=OFF \ + -DPLAT="_OPENMP" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DSUPERLUMT_INSTALL_INCLUDEDIR=include + +make -C "/tmp/superlu_mt-${SUPERLU_VERSION}/build" -j"${NPROC}" +make -C "/tmp/superlu_mt-${SUPERLU_VERSION}/build" install +rm -rf "/tmp/superlu_mt-${SUPERLU_VERSION}" + +# --- SUNDIALS (Modelon community fork) --- +# Embed SuperLU + OpenBLAS into the SUNDIALS shared libs so the .so files +# are self-contained. SuperLU_MT itself calls dgemm_/dgetrf_ → OpenBLAS +# must be listed as a target_link_libraries dep too. LAPACK_ENABLE is OFF +# because Assimulo never calls SUNDIALS' CVLapack* APIs. +git clone --depth 1 -b "v${SUNDIALS_VERSION}" \ + https://github.com/modelon-community/sundials /tmp/sundials + +echo "target_link_libraries(sundials_cvodes_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/cvodes/CMakeLists.txt +echo "target_link_libraries(sundials_idas_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/idas/CMakeLists.txt +echo "target_link_libraries(sundials_kinsol_shared superlu_mt_OPENMP openblas)" \ + >> /tmp/sundials/src/kinsol/CMakeLists.txt + +cmake -S /tmp/sundials -B /tmp/sundials/build \ + -DSUPERLUMT_INCLUDE_DIR=/usr/include \ + -DSUPERLUMT_LIBRARY=/usr/lib/libsuperlu_mt_OPENMP.a \ + -DSUPERLUMT_THREAD_TYPE=OpenMP \ + -DSUPERLUMT_ENABLE=ON \ + -DLAPACK_ENABLE=OFF \ + -DEXAMPLES_ENABLE=OFF \ + -DEXAMPLES_ENABLE_C=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=lib + +make -C /tmp/sundials/build -j"${NPROC}" +make -C /tmp/sundials/build install +rm -rf /tmp/sundials diff --git a/tools/wheels/build_dependencies_windows.sh b/tools/wheels/build_dependencies_windows.sh new file mode 100755 index 00000000..d0cf7820 --- /dev/null +++ b/tools/wheels/build_dependencies_windows.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# tools/wheels/build_dependencies_windows.sh +# Builds SuperLU_MT and SUNDIALS from source using MSYS2 UCRT64 toolchain. +# Run from an MSYS2 UCRT64 shell (shell: msys2 {0} in GHA). +# Installs to /c/deps (= C:\deps on Windows). +# +# OpenBLAS provides BLAS (and LAPACK, though Assimulo doesn't use SUNDIALS' +# CVLapack* APIs). SuperLU_MT + glimda link against the MSYS2 mingw-w64 +# OpenBLAS shared library; delvewheel bundles libopenblas.dll into the wheel. +set -eux + +SUPERLU_VERSION="4.0.1" +SUNDIALS_VERSION="2.7.0-3" +INSTALL_PREFIX="/c/deps" +NPROC=$(nproc 2>/dev/null || echo 4) + +mkdir -p "${INSTALL_PREFIX}/bin" "${INSTALL_PREFIX}/lib" "${INSTALL_PREFIX}/include" + +# --- OpenBLAS (shared) for SUNDIALS+SuperLU+glimda --- +pacman -S --noconfirm mingw-w64-ucrt-x86_64-openblas +# Stage the import lib + DLL into the same C:/deps tree we use for SUNDIALS, +# so meson's default_lib_candidates ([..., 'C:/deps/lib']) finds it and the +# delvewheel --add-path C:\deps\bin entry picks the DLL. +cp /ucrt64/lib/libopenblas.dll.a "${INSTALL_PREFIX}/lib/" +cp /ucrt64/bin/libopenblas.dll "${INSTALL_PREFIX}/bin/" +OPENBLAS_LIB="${INSTALL_PREFIX}/lib/libopenblas.dll.a" + +# Both SUNDIALS 2.7 and SuperLU_MT 4.0.1 use `cmake_minimum_required` values +# that the bundled MSYS2 cmake (>= 4.x) rejects. Export as an env var so it +# propagates to cmake's `try_compile` sub-invocations (the -D form does not). +export CMAKE_POLICY_VERSION_MINIMUM=3.5 + +# --- SuperLU_MT --- +curl -fSsL \ + "https://github.com/xiaoyeli/superlu_mt/archive/refs/tags/v${SUPERLU_VERSION}.tar.gz" \ + | tar xz -C /tmp + +cmake -S "/tmp/superlu_mt-${SUPERLU_VERSION}" \ + -B "/tmp/superlu_mt-${SUPERLU_VERSION}/build" \ + -G Ninja \ + -Denable_examples=OFF \ + -Denable_tests=OFF \ + -DPLAT="_OPENMP" \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DSUPERLUMT_INSTALL_INCLUDEDIR=include + +cmake --build "/tmp/superlu_mt-${SUPERLU_VERSION}/build" --parallel "${NPROC}" +cmake --install "/tmp/superlu_mt-${SUPERLU_VERSION}/build" +rm -rf "/tmp/superlu_mt-${SUPERLU_VERSION}" + +# --- SUNDIALS --- +# Embed SuperLU + OpenBLAS into the shared libs so the .dll files are +# self-contained. LAPACK_ENABLE=OFF because Assimulo doesn't call CVLapack*. +git clone --depth 1 -b "v${SUNDIALS_VERSION}" \ + https://github.com/modelon-community/sundials /tmp/sundials + +# Use absolute paths in Windows form (C:/deps/...): /c/deps/lib is not on +# the mingw linker's default search path, and ninja interprets msys2-style +# /c/... paths as Unix paths that don't exist on Windows. +# +# All six SUNDIALS solver shared libs compile in a *_superlumt.c source file +# when SUPERLUMT_ENABLE=ON; each needs explicit linkage. On Linux this is +# masked by /usr/lib being on the default ld path, so it's only needed here. +SUPERLU_LIB_WIN=$(cygpath -m "${INSTALL_PREFIX}/lib/libsuperlu_mt_OPENMP.a") +OPENBLAS_LIB_WIN=$(cygpath -m "${OPENBLAS_LIB}") +for solver in cvode cvodes ida idas kinsol arkode; do + echo "target_link_libraries(sundials_${solver}_shared ${SUPERLU_LIB_WIN} ${OPENBLAS_LIB_WIN})" \ + >> "/tmp/sundials/src/${solver}/CMakeLists.txt" +done + +cmake -S /tmp/sundials -B /tmp/sundials/build \ + -G Ninja \ + -DSUPERLUMT_INCLUDE_DIR="${INSTALL_PREFIX}/include" \ + -DSUPERLUMT_LIBRARY="${INSTALL_PREFIX}/lib/libsuperlu_mt_OPENMP.a" \ + -DSUPERLUMT_BLAS_LIBRARIES="${OPENBLAS_LIB}" \ + -DSUPERLUMT_THREAD_TYPE=OpenMP \ + -DSUPERLUMT_ENABLE=ON \ + -DLAPACK_ENABLE=OFF \ + -DEXAMPLES_ENABLE=OFF \ + -DEXAMPLES_ENABLE_C=OFF \ + -DBUILD_STATIC_LIBS=OFF \ + -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \ + -DCMAKE_BUILD_TYPE=Release + +cmake --build /tmp/sundials/build --parallel "${NPROC}" +cmake --install /tmp/sundials/build +rm -rf /tmp/sundials diff --git a/tools/wheels/repair_windows.ps1 b/tools/wheels/repair_windows.ps1 new file mode 100644 index 00000000..c67ab234 --- /dev/null +++ b/tools/wheels/repair_windows.ps1 @@ -0,0 +1,8 @@ +# tools/wheels/repair_windows.ps1 +# Wraps delvewheel to bundle SUNDIALS and MSYS2 UCRT64 runtime DLLs into the wheel. +# Called by cibuildwheel repair-wheel-command. +param([string]$Wheel, [string]$DestDir) +delvewheel repair ` + --add-path "C:\msys64\ucrt64\bin;C:\deps\bin;C:\deps\lib" ` + -w $DestDir ` + $Wheel From ec32f7a85bebd874964e8209b8e5dfc4b330e877 Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:03:23 +0200 Subject: [PATCH 4/9] ci: cibuildwheel pipeline for Linux + Windows wheels - pyproject.toml [tool.cibuildwheel] + per-platform sections: build cp311/312/313; skip win32/i686/musllinux; smoke-test each built wheel via pytest (test-groups = ["test"]). - Linux: use the assimulo-manylinux image (SuperLU + SUNDIALS already installed at /usr); auditwheel repair for portability. - Windows: build SUNDIALS via MSYS2 UCRT64 in the workflow first, then cibuildwheel; delvewheel repair via repair_windows.ps1. - .github/workflows/wheels.yml: per-Python matrix (sparse PR / full master via `on: all` vs `on: master`); push restricted to master/main/beta/tags; workflow_dispatch supports publish + test_release (rewrites name to assimulo-testing). - upload_pypi: OIDC Trusted Publisher to PyPI on tag pushes. - RELEASING.md: documented release flow. - Remove legacy .github/workflows/build.yml. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build.yml | 26 ------ .github/workflows/wheels.yml | 154 +++++++++++++++++++++++++++++++++++ RELEASING.md | 28 +++++++ pyproject.toml | 31 +++++++ 4 files changed, 213 insertions(+), 26 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/wheels.yml create mode 100644 RELEASING.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 2db00fa8..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,26 +0,0 @@ -on: - push: - pull_request: - -permissions: - contents: read - -jobs: - linux: - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - sundials_version: [2.7.0, 3.2.0, 7.1.1] - steps: - - uses: actions/checkout@v4 - - uses: docker/build-push-action@v6 - - name: Install make - run: sudo apt install make -y - - name: Build image - run: make build-dev-image SUNDIALS_VERSION=${{ matrix.sundials_version }} - - name: Build - run: make build SUNDIALS_VERSION=${{ matrix.sundials_version }} - - name: Test - run: make test SUNDIALS_VERSION=${{ matrix.sundials_version }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000..05fdd0bb --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,154 @@ +name: Build wheels + +on: + push: + branches: [master, main, beta] + tags: ['*'] + pull_request: + workflow_dispatch: + inputs: + publish: + description: "Publish wheels to PyPI after building" + type: boolean + default: false + test_release: + description: "When publishing, rewrite the package name to assimulo-testing (fork-only; remove before merging upstream)" + type: boolean + default: false + +permissions: + contents: read + +jobs: + linux: + name: Linux ${{ matrix.arch }} (${{ matrix.python }}) + # Skip `on: master` rows on PRs for fast feedback. + if: matrix.on == 'all' || github.event_name != 'pull_request' + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-latest, python: cp311, on: all } + - { arch: x86_64, runner: ubuntu-latest, python: cp312, on: master } + - { arch: x86_64, runner: ubuntu-latest, python: cp313, on: master } + steps: + - uses: actions/checkout@v4 + + - name: Rewrite package name to assimulo-testing + if: github.event_name == 'workflow_dispatch' && inputs.publish && inputs.test_release + run: | + sed -i 's/^name = "Assimulo"$/name = "assimulo-testing"/' pyproject.toml + sed -i "s/^ 'Assimulo',$/ 'assimulo-testing',/" meson.build + grep '^name = ' pyproject.toml + grep -n "assimulo-testing" meson.build + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - run: pip install cibuildwheel + + - name: Build manylinux image (SuperLU + SUNDIALS pre-installed) + run: docker build -f Dockerfile.manylinux -t assimulo-manylinux . + + - name: Build wheel + env: + CIBW_BUILD: "${{ matrix.python }}-*" + run: cibuildwheel --platform linux --output-dir dist/ + + - uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.arch }}-${{ matrix.python }} + path: dist/*.whl + + windows: + name: Windows ${{ matrix.arch }} (${{ matrix.python }}) + # Skip `on: master` rows on PRs for fast feedback. + if: matrix.on == 'all' || github.event_name != 'pull_request' + runs-on: windows-2022 + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { arch: amd64, python: cp311, on: all } + - { arch: amd64, python: cp312, on: master } + - { arch: amd64, python: cp313, on: master } + steps: + - uses: actions/checkout@v4 + + - name: Rewrite package name to assimulo-testing + if: github.event_name == 'workflow_dispatch' && inputs.publish && inputs.test_release + shell: bash + run: | + sed -i 's/^name = "Assimulo"$/name = "assimulo-testing"/' pyproject.toml + sed -i "s/^ 'Assimulo',$/ 'assimulo-testing',/" meson.build + grep '^name = ' pyproject.toml + grep -n "assimulo-testing" meson.build + + - name: Set up MSYS2 UCRT64 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + git + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-gcc-fortran + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-pkg-config + + - name: Build SUNDIALS + shell: msys2 {0} + run: bash tools/wheels/build_dependencies_windows.sh + + # Add UCRT64 and SUNDIALS to PATH so cibuildwheel/meson find gcc and DLLs. + # Set CC/CXX/FC so meson uses MSYS2 GCC instead of MSVC. + - name: Configure compiler environment + shell: pwsh + run: | + echo "C:\msys64\ucrt64\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + echo "C:\deps\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + echo "CC=gcc" | Out-File -FilePath $env:GITHUB_ENV -Append + echo "CXX=g++" | Out-File -FilePath $env:GITHUB_ENV -Append + echo "FC=gfortran" | Out-File -FilePath $env:GITHUB_ENV -Append + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - run: pip install cibuildwheel + shell: pwsh + + - name: Build wheel + shell: pwsh + env: + CIBW_BUILD: "${{ matrix.python }}-*" + run: cibuildwheel --platform windows --output-dir dist/ + + - uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.arch }}-${{ matrix.python }} + path: dist/*.whl + + upload_pypi: + name: Upload to PyPI + needs: [linux, windows] + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' && inputs.publish && startsWith(github.ref, 'refs/tags/v') + environment: + name: pypi + url: https://pypi.org/p/Assimulo + permissions: + id-token: write # required for OIDC Trusted Publisher + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist/ + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..cca0b92e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,28 @@ +# Releasing Assimulo + +The release process is manual. The long-term goal is to adopt +[python-semantic-release](https://python-semantic-release.readthedocs.io/) so +that versioning, changelog, tagging, and upload are driven by commit messages. + +## Prerequisites + +The repository must have a GitHub Actions environment named `pypi`. PyPI must +have a Trusted Publisher configured for the `Assimulo` project bound to this +repository, workflow `wheels.yml`, and environment `pypi`. See the +[PyPI Trusted Publishers documentation](https://docs.pypi.org/trusted-publishers/) +for setup. + +## Cutting a release + +1. Update the `version:` field in `meson.build`. +2. Prepend a section for the new version to `CHANGELOG`. +3. Commit the changes on the release branch. +4. Create and push the tag: + ``` + git tag vX.Y.Z + git push origin vX.Y.Z + ``` +5. Run the `Build wheels` workflow via `workflow_dispatch` against the new + tag with `publish: true`. The `upload_pypi` job runs only when dispatched + against a `v*` tag. It builds wheels for Linux and Windows and uploads + them to . diff --git a/pyproject.toml b/pyproject.toml index c02a68fe..3862a804 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,3 +44,34 @@ dev = [ "ninja", "Cython>=3.0.7", ] + +# --------------------------------------------------------------------------- +# cibuildwheel configuration +# --------------------------------------------------------------------------- +[tool.cibuildwheel] +build = "cp311-* cp312-* cp313-*" +skip = "*-win32 *-manylinux_i686 *-musllinux*" +# Smoke each built wheel against pytest before it leaves the runner. +# Runs for every built CPython (3.11/3.12/3.13) on both Linux and Windows. +# test-groups pulls the `test` dependency group from [dependency-groups] +# above, keeping pytest's pin in a single place. +test-groups = ["test"] +test-command = "pytest {project}/tests" + +[tool.cibuildwheel.windows] +# SUNDIALS is built in the GHA workflow (tools/wheels/build_dependencies_windows.sh) +# using MSYS2 UCRT64 before cibuildwheel runs. MSYS2 bin dirs are added to PATH +# so meson picks up gcc/gfortran instead of MSVC. +before-build = "pip install delvewheel" +repair-wheel-command = "pwsh {project}/tools/wheels/repair_windows.ps1 {wheel} {dest_dir}" +config-settings = {"setup-args" = ["-Dsundials_prefix=C:/deps", "-Dsuperlu_prefix=C:/deps", "-Dopenmp=true"]} + +[tool.cibuildwheel.linux] +# Use the pre-built assimulo-manylinux image which has SuperLU + SUNDIALS already +# compiled (via Dockerfile.manylinux → tools/wheels/build_dependencies.sh). +# The official manylinux images are intentionally stripped of their package +# manager, so before-all cannot install system packages. +# Build the image first: make build-manylinux-image +manylinux-x86_64-image = "assimulo-manylinux" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" +config-settings = {"setup-args" = ["-Dsundials_prefix=/usr", "-Dsuperlu_prefix=/usr", "-Dopenmp=true"]} From 542a1fd67a2fe4056e83126e7be20f4590defa7d Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:03:34 +0200 Subject: [PATCH 5/9] chore: requirements.lock + CLAUDE.md + CHANGELOG for new build system - requirements.lock: uv-compiled dev/test environment pin so the dev image reproducibly installs the same versions. - CLAUDE.md: document the meson-python build, key constraints (thirdparty/ untouched, .pyx files need rebuild, always use `make build`), test command, dependencies. - CHANGELOG: note the build-system migration. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG | 30 ++++++++++++++++++++++++++ CLAUDE.md | 10 +++++++-- requirements.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 requirements.lock diff --git a/CHANGELOG b/CHANGELOG index ef8bd334..0f1f614a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,34 @@ --- CHANGELOG --- +--- Assimulo-3.9.0b1 --- + Beta release of the modernized build system. No functional or API changes + relative to 3.8.0. + + Build system: + * Switched from distutils/setup.py to meson-python (PEP 517 build backend). + * Bumped minimum requirements: Python >= 3.11, NumPy >= 2.1, SciPy >= 1.14, + Cython >= 3.0.7. + * Build configuration is now declared in pyproject.toml + meson.build + + meson.options. + * Fortran solvers (Radau5, Dopri5, Rodas, Odassl, Odepack, Dasp3, Glimda, + Kvaernoe) are now wrapped via numpy.f2py invoked from meson; the previous + f2py glue has been replaced by tools/f2py_wrapper.py. + * SUNDIALS is sourced from the Modelon SUNDIALS fork as the canonical + dependency; CVode and IDA are statically linked into the wrapper modules + by default. + * BLAS/LAPACK unified to dynamic OpenBLAS on both Linux and Windows. + * SuperLU support enabled on Windows wheels. + * Examples directory moved under src/assimulo for consistency with the + installed package layout. + + Packaging and distribution: + * Added manylinux and Windows wheel builds via cibuildwheel. + * Added GitHub Actions workflow (.github/workflows/wheels.yml) for building + and publishing wheels to PyPI via a Trusted Publisher. + * Added Docker images for reproducible local builds (Dockerfile, + Dockerfile.manylinux) and a Makefile target for incremental development + builds. + * Added RELEASING.md describing the manual release procedure. + --- Assimulo-3.8.0--- * Enabled interpolated output support for ExplicitEuler and ImplicitEuler. diff --git a/CLAUDE.md b/CLAUDE.md index fd4975b5..0b699bd9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,11 +16,17 @@ Python/Cython library providing a unified interface to ODE and DAE solvers. Solv ## Build +Build backend is `meson-python`; options in `meson.options`. All targets run inside Docker. + ``` -make build +make build # full install into .venv +make build-dev # incremental editable install (keeps builddir/) +make check-meson.build # validate meson config without building ``` -Runs inside Docker (builds a dev image if needed). Dependencies: Python ≥ 3.9, NumPy ≥ 1.19.5, SciPy ≥ 1.10.1, Cython ≥ 3.0.7, SUNDIALS v2.7.0. +Dependencies: Python ≥ 3.11, NumPy ≥ 2.1, SciPy ≥ 1.14, Cython ≥ 3.0.7, SUNDIALS v2.7.0. Linux requires `libopenblas-dev`; Windows links BLAS statically. + +Fortran solvers are wrapped via `tools/f2py_wrapper.py` (generates C glue from `.pyf` files). ## Testing diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..d68f6de8 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,54 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python python3 --group=dev --output-file=requirements.lock pyproject.toml +contourpy==1.3.3 + # via matplotlib +cycler==0.12.1 + # via matplotlib +cython==3.2.4 + # via assimulo (pyproject.toml:dev) +fonttools==4.61.1 + # via matplotlib +iniconfig==2.3.0 + # via pytest +kiwisolver==1.4.9 + # via matplotlib +matplotlib==3.10.8 + # via assimulo (pyproject.toml) +meson==1.10.1 + # via + # assimulo (pyproject.toml:dev) + # meson-python +meson-python==0.19.0 + # via assimulo (pyproject.toml:dev) +ninja==1.13.0 + # via assimulo (pyproject.toml:dev) +numpy==2.4.2 + # via + # assimulo (pyproject.toml) + # contourpy + # matplotlib + # scipy +packaging==26.0 + # via + # matplotlib + # meson-python + # pyproject-metadata + # pytest +pillow==12.1.0 + # via matplotlib +pluggy==1.6.0 + # via pytest +pygments==2.19.2 + # via pytest +pyparsing==3.3.2 + # via matplotlib +pyproject-metadata==0.11.0 + # via meson-python +pytest==9.0.2 + # via assimulo (pyproject.toml:dev) +python-dateutil==2.9.0.post0 + # via matplotlib +scipy==1.17.0 + # via assimulo (pyproject.toml) +six==1.17.0 + # via python-dateutil From 3b9009caadccaac4692131579acf67585c6a7e00 Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 19:37:57 +0200 Subject: [PATCH 6/9] ci: add cp314 + Linux aarch64 to wheel matrix (dynamic matrix via fromJSON) Matrix changes (.github/workflows/wheels.yml): - Add cp314 for Linux x86_64, Linux aarch64, and Windows amd64. - Add Linux aarch64 lane (cp311-cp314) on ubuntu-22.04-arm runners. - PR matrix: cp311 + cp314 on Linux x86_64 and Windows (oldest+newest, fast feedback). All other rows run on push to master/beta/tag and on workflow_dispatch. The earlier attempt used `if: matrix.on == 'all' || ...` on the job itself, but GitHub Actions does not expose the `matrix` context inside `jobs..if` (only inside env, runs-on, steps.*, etc), so the file failed to parse outright with `Unrecognized named-value: 'matrix'`. PyFMI's wheels.yml has the same broken pattern and was the source of the copy. Replaced with a `config` job that emits the matrix JSON based on github.event_name; `linux` and `windows` jobs consume it via `include: ${{ fromJSON(needs.config.outputs.X) }}`. Single source of truth for the matrix; no duplicated job definitions. Dockerfile.manylinux: introduce `ARG ARCH=x86_64` so the same Dockerfile builds either arch; the linux job passes `--build-arg ARCH=${{ matrix.arch }}` to docker build. pyproject.toml: add `cp314-*` to [tool.cibuildwheel].build so cibw picks up the new Python. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/wheels.yml | 67 +++++++++++++++++++++++++++++------- Dockerfile.manylinux | 3 +- pyproject.toml | 4 +-- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 05fdd0bb..d3119920 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -20,19 +20,64 @@ permissions: contents: read jobs: + # Picks a small matrix for PRs and a full matrix for everything else. + # The `matrix` context is not available in `jobs..if`, so the + # row selection cannot be done inline on the build jobs. + config: + name: Resolve matrix + runs-on: ubuntu-latest + outputs: + linux: ${{ steps.set.outputs.linux }} + windows: ${{ steps.set.outputs.windows }} + steps: + - id: set + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "pull_request" ]; then + LINUX='[ + {"arch":"x86_64","runner":"ubuntu-latest","python":"cp311"}, + {"arch":"x86_64","runner":"ubuntu-latest","python":"cp314"} + ]' + WINDOWS='[ + {"arch":"amd64","python":"cp311"}, + {"arch":"amd64","python":"cp314"} + ]' + else + LINUX='[ + {"arch":"x86_64", "runner":"ubuntu-latest", "python":"cp311"}, + {"arch":"x86_64", "runner":"ubuntu-latest", "python":"cp312"}, + {"arch":"x86_64", "runner":"ubuntu-latest", "python":"cp313"}, + {"arch":"x86_64", "runner":"ubuntu-latest", "python":"cp314"}, + {"arch":"aarch64","runner":"ubuntu-22.04-arm","python":"cp311"}, + {"arch":"aarch64","runner":"ubuntu-22.04-arm","python":"cp312"}, + {"arch":"aarch64","runner":"ubuntu-22.04-arm","python":"cp313"}, + {"arch":"aarch64","runner":"ubuntu-22.04-arm","python":"cp314"} + ]' + WINDOWS='[ + {"arch":"amd64","python":"cp311"}, + {"arch":"amd64","python":"cp312"}, + {"arch":"amd64","python":"cp313"}, + {"arch":"amd64","python":"cp314"} + ]' + fi + { + echo "linux<> "$GITHUB_OUTPUT" + linux: name: Linux ${{ matrix.arch }} (${{ matrix.python }}) - # Skip `on: master` rows on PRs for fast feedback. - if: matrix.on == 'all' || github.event_name != 'pull_request' + needs: config runs-on: ${{ matrix.runner }} timeout-minutes: 60 strategy: fail-fast: false matrix: - include: - - { arch: x86_64, runner: ubuntu-latest, python: cp311, on: all } - - { arch: x86_64, runner: ubuntu-latest, python: cp312, on: master } - - { arch: x86_64, runner: ubuntu-latest, python: cp313, on: master } + include: ${{ fromJSON(needs.config.outputs.linux) }} steps: - uses: actions/checkout@v4 @@ -51,7 +96,7 @@ jobs: - run: pip install cibuildwheel - name: Build manylinux image (SuperLU + SUNDIALS pre-installed) - run: docker build -f Dockerfile.manylinux -t assimulo-manylinux . + run: docker build --build-arg ARCH=${{ matrix.arch }} -f Dockerfile.manylinux -t assimulo-manylinux . - name: Build wheel env: @@ -65,17 +110,13 @@ jobs: windows: name: Windows ${{ matrix.arch }} (${{ matrix.python }}) - # Skip `on: master` rows on PRs for fast feedback. - if: matrix.on == 'all' || github.event_name != 'pull_request' + needs: config runs-on: windows-2022 timeout-minutes: 90 strategy: fail-fast: false matrix: - include: - - { arch: amd64, python: cp311, on: all } - - { arch: amd64, python: cp312, on: master } - - { arch: amd64, python: cp313, on: master } + include: ${{ fromJSON(needs.config.outputs.windows) }} steps: - uses: actions/checkout@v4 diff --git a/Dockerfile.manylinux b/Dockerfile.manylinux index f844e148..49d01d79 100644 --- a/Dockerfile.manylinux +++ b/Dockerfile.manylinux @@ -1,4 +1,5 @@ -FROM quay.io/pypa/manylinux_2_28_x86_64 +ARG ARCH=x86_64 +FROM quay.io/pypa/manylinux_2_28_${ARCH} # Build SuperLU_MT and SUNDIALS using the same script as cibuildwheel. COPY tools/wheels/build_dependencies.sh /tmp/build_dependencies.sh diff --git a/pyproject.toml b/pyproject.toml index 3862a804..7780df2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,10 +49,10 @@ dev = [ # cibuildwheel configuration # --------------------------------------------------------------------------- [tool.cibuildwheel] -build = "cp311-* cp312-* cp313-*" +build = "cp311-* cp312-* cp313-* cp314-*" skip = "*-win32 *-manylinux_i686 *-musllinux*" # Smoke each built wheel against pytest before it leaves the runner. -# Runs for every built CPython (3.11/3.12/3.13) on both Linux and Windows. +# Runs for every built CPython (3.11/3.12/3.13/3.14) on both Linux and Windows. # test-groups pulls the `test` dependency group from [dependency-groups] # above, keeping pytest's pin in a single place. test-groups = ["test"] From 63927e1857e95c8af871bbe1c22eaa7999b6deaa Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 23:00:35 +0200 Subject: [PATCH 7/9] test: use setup_method xunit hook instead of @classmethod+@pytest.fixture The test classes had: @classmethod @pytest.fixture(autouse=True) def setup_class(cls): cls.X = ... Two problems: 1. @pytest.fixture on top of a method whose name is the xunit setup_class hook is non-idiomatic and was driving pytest into a code path that crashes on CPython 3.14: pytest 8.4+ wraps fixture-decorated functions in a FixtureFunctionDefinition, and pytest's xunit setup code does `func.__code__.co_argcount` on it. 3.14 attribute lookup no longer falls through the wrapper, so `AttributeError: 'FixtureFunctionDefinition' object has no attribute '__code__'` -> 271 errors on the cibw cp314 smoke test. 2. setup_class runs once per class, but the tests mutate self.simulator (e.g. simulator.h = 1.0, simulator.simulate(...)), so per-class setup leaks state between tests. The original autouse=True was masking this by re-running the function before every test. The documented pytest xunit hook for per-test setup is setup_method. Drop both decorators, rename to setup_method(self), and replace cls with self in the body. Behaves the same as before (fresh per-test state) and parses cleanly on cp314. Verified locally: 331 passed, 4 skipped on both CPython 3.12 (make test) and CPython 3.14.5 with pytest 9.0.3 (built assimulo for cp314 inside the dev container). Co-Authored-By: Claude Opus 4.7 --- tests/solvers/test_euler.py | 16 +++---- tests/solvers/test_glimda.py | 18 ++++--- tests/solvers/test_odassl.py | 8 ++-- tests/solvers/test_odepack.py | 16 +++---- tests/solvers/test_radau5.py | 80 ++++++++++++++------------------ tests/solvers/test_rosenbrock.py | 8 ++-- tests/solvers/test_rungekutta.py | 24 ++++------ tests/solvers/test_sundials.py | 26 ++++------- tests/test_ode.py | 8 ++-- tests/test_solvers.py | 16 +++---- 10 files changed, 91 insertions(+), 129 deletions(-) diff --git a/tests/solvers/test_euler.py b/tests/solvers/test_euler.py index 0351f33a..31ad748e 100644 --- a/tests/solvers/test_euler.py +++ b/tests/solvers/test_euler.py @@ -27,17 +27,15 @@ class Test_Explicit_Euler: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y: 1.0 y0 = 1.0 - cls.problem = Explicit_Problem(f, y0) - cls.simulator = ExplicitEuler(cls.problem) + self.problem = Explicit_Problem(f, y0) + self.simulator = ExplicitEuler(self.problem) def test_event_localizer(self, extended_problem): exp_sim = ExplicitEuler(extended_problem) #Create the solver @@ -181,17 +179,15 @@ def test_interpolated_output_ncp(self): class Test_Implicit_Euler: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y: 1.0 y0 = 1.0 - cls.problem = Explicit_Problem(f, y0) - cls.simulator = ImplicitEuler(cls.problem) + self.problem = Explicit_Problem(f, y0) + self.simulator = ImplicitEuler(self.problem) def test_reset_statistics(self): assert self.simulator.statistics["nsteps"] == 0 diff --git a/tests/solvers/test_glimda.py b/tests/solvers/test_glimda.py index d8ec85d8..ac3f6746 100644 --- a/tests/solvers/test_glimda.py +++ b/tests/solvers/test_glimda.py @@ -26,9 +26,7 @@ class Test_GLIMDA: """ Tests the GLIMDA solver. """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -48,17 +46,17 @@ def f(t,y,yd): yd0 = [-.6,-200000.] #Define an Assimulo problem - cls.mod = Implicit_Problem(f,y0,yd0) - cls.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) + self.mod = Implicit_Problem(f,y0,yd0) + self.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) #Define an explicit solver - cls.sim = GLIMDA(cls.mod) #Create a Radau5 solve - cls.sim_t0 = GLIMDA(cls.mod_t0) + self.sim = GLIMDA(self.mod) #Create a Radau5 solve + self.sim_t0 = GLIMDA(self.mod_t0) #Sets the parameters - cls.sim.atol = 1e-4 #Default 1e-6 - cls.sim.rtol = 1e-4 #Default 1e-6 - cls.sim.inith = 1.e-4 #Initial step-size + self.sim.atol = 1e-4 #Default 1e-6 + self.sim.rtol = 1e-4 #Default 1e-6 + self.sim.inith = 1.e-4 #Initial step-size def test_simulate_explicit(self): """ diff --git a/tests/solvers/test_odassl.py b/tests/solvers/test_odassl.py index 0560fbfd..50664f18 100644 --- a/tests/solvers/test_odassl.py +++ b/tests/solvers/test_odassl.py @@ -23,9 +23,7 @@ class Test_ODASSL: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ @@ -33,8 +31,8 @@ def setup_class(cls): y0 = [1.0, 1.0, 1.0] yd0 = [-1.0, -1.0, -1.0] - cls.problem = Overdetermined_Problem(f,y0, yd0) - cls.simulator = ODASSL(cls.problem) + self.problem = Overdetermined_Problem(f,y0, yd0) + self.simulator = ODASSL(self.problem) def test_overdetermined(self): f = lambda t,y,yd: np.hstack((yd + 1, yd +1)) diff --git a/tests/solvers/test_odepack.py b/tests/solvers/test_odepack.py index df89ff0e..802ff567 100644 --- a/tests/solvers/test_odepack.py +++ b/tests/solvers/test_odepack.py @@ -32,9 +32,7 @@ class Test_LSODAR: """ Tests the LSODAR solver. """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -79,16 +77,16 @@ def jac_sparse(t,y): exp_mod.jac = jac exp_mod_sp.jac = jac_sparse - cls.mod = exp_mod + self.mod = exp_mod #Define an explicit solver - cls.sim = LSODAR(exp_mod) #Create a LSODAR solve - cls.sim_sp = LSODAR(exp_mod_sp) + self.sim = LSODAR(exp_mod) #Create a LSODAR solve + self.sim_sp = LSODAR(exp_mod_sp) #Sets the parameters - cls.sim.atol = 1e-6 #Default 1e-6 - cls.sim.rtol = 1e-6 #Default 1e-6 - cls.sim.usejac = False + self.sim.atol = 1e-6 #Default 1e-6 + self.sim.rtol = 1e-6 #Default 1e-6 + self.sim.usejac = False def test_event_localizer(self, extended_problem): exp_sim = LSODAR(extended_problem) #Create the solver diff --git a/tests/solvers/test_radau5.py b/tests/solvers/test_radau5.py index 79e396b5..f4a7e18f 100644 --- a/tests/solvers/test_radau5.py +++ b/tests/solvers/test_radau5.py @@ -33,9 +33,7 @@ class Test_Explicit_Radau5_Py: """ Tests the explicit Radau solver (Python implementation). """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -66,17 +64,17 @@ def jac(t,y): exp_mod_t0 = Explicit_Problem(f,y0,1.0) exp_mod.jac = jac - cls.mod = exp_mod + self.mod = exp_mod #Define an explicit solver - cls.sim = _Radau5ODE(exp_mod) #Create a Radau5 solve - cls.sim_t0 = _Radau5ODE(exp_mod_t0) + self.sim = _Radau5ODE(exp_mod) #Create a Radau5 solve + self.sim_t0 = _Radau5ODE(exp_mod_t0) #Sets the parameters - cls.sim.atol = 1e-4 #Default 1e-6 - cls.sim.rtol = 1e-4 #Default 1e-6 - cls.sim.inith = 1.e-4 #Initial step-size - cls.sim.usejac = False + self.sim.atol = 1e-4 #Default 1e-6 + self.sim.rtol = 1e-4 #Default 1e-6 + self.sim.inith = 1.e-4 #Initial step-size + self.sim.usejac = False @pytest.mark.skip("Does not support state events") def test_event_localizer(self, extended_problem): @@ -278,9 +276,7 @@ class Test_Explicit_Radau5: """ Tests the explicit Radau solver. """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -325,18 +321,18 @@ def jac_sparse(t,y): exp_mod.jac = jac exp_mod_sp.jac = jac_sparse - cls.mod = exp_mod + self.mod = exp_mod #Define an explicit solver - cls.sim = Radau5ODE(exp_mod) #Create a Radau5 solve - cls.sim_t0 = Radau5ODE(exp_mod_t0) - cls.sim_sp = Radau5ODE(exp_mod_sp) + self.sim = Radau5ODE(exp_mod) #Create a Radau5 solve + self.sim_t0 = Radau5ODE(exp_mod_t0) + self.sim_sp = Radau5ODE(exp_mod_sp) #Sets the parameters - cls.sim.atol = 1e-4 #Default 1e-6 - cls.sim.rtol = 1e-4 #Default 1e-6 - cls.sim.inith = 1.e-4 #Initial step-size - cls.sim.usejac = False + self.sim.atol = 1e-4 #Default 1e-6 + self.sim.rtol = 1e-4 #Default 1e-6 + self.sim.inith = 1.e-4 #Initial step-size + self.sim.usejac = False def test_event_localizer(self, extended_problem): exp_sim = Radau5ODE(extended_problem) #Create the solver @@ -979,9 +975,7 @@ class Test_Implicit_Radau5: """ Tests the implicit Radau solver. """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -1001,17 +995,17 @@ def f(t,y,yd): yd0 = [-.6,-200000.] #Define an Assimulo problem - cls.mod = Implicit_Problem(f,y0,yd0) - cls.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) + self.mod = Implicit_Problem(f,y0,yd0) + self.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) #Define an implicit solver - cls.sim = Radau5DAE(cls.mod) #Create a Radau5 solve - cls.sim_t0 = Radau5DAE(cls.mod_t0) + self.sim = Radau5DAE(self.mod) #Create a Radau5 solve + self.sim_t0 = Radau5DAE(self.mod_t0) #Sets the parameters - cls.sim.atol = 1e-4 #Default 1e-6 - cls.sim.rtol = 1e-4 #Default 1e-6 - cls.sim.inith = 1.e-4 #Initial step-size + self.sim.atol = 1e-4 #Default 1e-6 + self.sim.rtol = 1e-4 #Default 1e-6 + self.sim.inith = 1.e-4 #Initial step-size def test_implementation_get(self): """ @@ -1260,9 +1254,7 @@ class Test_Implicit_Radau5_Py: """ Tests the implicit Radau solver (Python implementation). """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -1282,17 +1274,17 @@ def f(t,y,yd): yd0 = [-.6,-200000.] #Define an Assimulo problem - cls.mod = Implicit_Problem(f,y0,yd0) - cls.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) + self.mod = Implicit_Problem(f,y0,yd0) + self.mod_t0 = Implicit_Problem(f,y0,yd0,1.0) #Define an explicit solver - cls.sim = _Radau5DAE(cls.mod) #Create a Radau5 solve - cls.sim_t0 = _Radau5DAE(cls.mod_t0) + self.sim = _Radau5DAE(self.mod) #Create a Radau5 solve + self.sim_t0 = _Radau5DAE(self.mod_t0) #Sets the parameters - cls.sim.atol = 1e-4 #Default 1e-6 - cls.sim.rtol = 1e-4 #Default 1e-6 - cls.sim.inith = 1.e-4 #Initial step-size + self.sim.atol = 1e-4 #Default 1e-6 + self.sim.rtol = 1e-4 #Default 1e-6 + self.sim.inith = 1.e-4 #Initial step-size def test_time_event(self): f = lambda t,y,yd: y-yd @@ -1405,9 +1397,7 @@ class Test_Radau_Common: """ Tests the common attributes of the Radau solvers. """ - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -1417,7 +1407,7 @@ def setup_class(cls): #Define an explicit Assimulo problem y0 = [2.0,-0.6] #Initial conditions exp_mod = Explicit_Problem(f,y0) - cls.sim = Radau5ODE(exp_mod) + self.sim = Radau5ODE(exp_mod) def test_fac1(self): """ diff --git a/tests/solvers/test_rosenbrock.py b/tests/solvers/test_rosenbrock.py index f4d10eb0..2e0857df 100644 --- a/tests/solvers/test_rosenbrock.py +++ b/tests/solvers/test_rosenbrock.py @@ -25,9 +25,7 @@ float_regex = r"[\s]*[\d]*.[\d]*((e|E)(\+|\-)\d\d|)" class Test_RodasODE: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): #Define the rhs def f(t,y): eps = 1.e-6 @@ -63,8 +61,8 @@ def jac_sparse(t,y): exp_mod_sp = Explicit_Problem(f,y0, name = 'Van der Pol (explicit)') exp_mod.jac = jac exp_mod_sp.jac = jac_sparse - cls.mod = exp_mod - cls.mod_sp = exp_mod_sp + self.mod = exp_mod + self.mod_sp = exp_mod_sp def test_nbr_fcn_evals_due_to_jac(self): sim = RodasODE(self.mod) diff --git a/tests/solvers/test_rungekutta.py b/tests/solvers/test_rungekutta.py index 48f09020..92f8f4e9 100644 --- a/tests/solvers/test_rungekutta.py +++ b/tests/solvers/test_rungekutta.py @@ -25,17 +25,15 @@ class Test_Dopri5: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y:1.0 y0 = 1 - cls.problem = Explicit_Problem(f,y0) - cls.simulator = Dopri5(cls.problem) + self.problem = Explicit_Problem(f,y0) + self.simulator = Dopri5(self.problem) def test_integrator(self): """ @@ -121,17 +119,15 @@ def f(t, y): class Test_RungeKutta34: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y:1.0 y0 = 1 - cls.problem = Explicit_Problem(f,y0) - cls.simulator = RungeKutta34(cls.problem) + self.problem = Explicit_Problem(f,y0) + self.simulator = RungeKutta34(self.problem) def test_integrator(self): """ @@ -252,17 +248,15 @@ def f(t, y): class Test_RungeKutta4: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y:1.0 y0 = 1 - cls.problem = Explicit_Problem(f,y0) - cls.simulator = RungeKutta4(cls.problem) + self.problem = Explicit_Problem(f,y0) + self.simulator = RungeKutta4(self.problem) def test_time_event(self): f = lambda t,y: [1.0] diff --git a/tests/solvers/test_sundials.py b/tests/solvers/test_sundials.py index 436205e7..4f207332 100644 --- a/tests/solvers/test_sundials.py +++ b/tests/solvers/test_sundials.py @@ -26,18 +26,16 @@ class Test_CVode: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ f = lambda t,y:np.array(y) y0 = [1.0] - cls.problem = Explicit_Problem(f,y0) - cls.simulator = CVode(cls.problem) - cls.simulator.verbosity = 0 + self.problem = Explicit_Problem(f,y0) + self.simulator = CVode(self.problem) + self.simulator.verbosity = 0 def test_backward_integration(self): def f(t, y): @@ -906,9 +904,7 @@ def test_final_step_skip_due_to_t_final_rounding(self, scale_factor): class Test_IDA: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This function sets up the test case. """ @@ -916,8 +912,8 @@ def setup_class(cls): y0 = [1.0] yd0 = [1.0] - cls.problem = Implicit_Problem(f,y0,yd0) - cls.simulator = IDA(cls.problem) + self.problem = Implicit_Problem(f,y0,yd0) + self.simulator = IDA(self.problem) def test_time_limit(self): f = lambda t,y,yd: yd-y @@ -1370,9 +1366,7 @@ def test_backwards_report_continuously(self): class Test_Sundials: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): + def setup_method(self): """ This sets up the test case. """ @@ -1393,7 +1387,7 @@ class Prob_CVode(Explicit_Problem): f = Prob_CVode() - cls.simulators = [IDA(res), CVode(f)] + self.simulators = [IDA(res), CVode(f)] f = lambda t,y,yd,p: np.array([0.0]) @@ -1402,7 +1396,7 @@ class Prob_CVode(Explicit_Problem): p0 = [1.0] mod = Implicit_Problem(f, y0,yd0,p0=p0) - cls.sim = IDA(mod) + self.sim = IDA(mod) def test_atol(self): """ diff --git a/tests/test_ode.py b/tests/test_ode.py index 12a4779c..5a21baae 100644 --- a/tests/test_ode.py +++ b/tests/test_ode.py @@ -21,11 +21,9 @@ from assimulo.exception import AssimuloException class Test_ODE: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): - cls.problem = Explicit_Problem(y0=4.0) - cls.simulator = ODE(cls.problem) + def setup_method(self): + self.problem = Explicit_Problem(y0=4.0) + self.simulator = ODE(self.problem) def test_init(self): """ diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 98942680..122c3d27 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -32,16 +32,14 @@ def handle_event(solver, event_info): pass class Test_Solvers: - @classmethod - @pytest.fixture(autouse=True) - def setup_class(cls): - cls.problem = Implicit_Problem(res, [1.0], [-1.0]) - cls.problem.state_events = state_events - cls.problem.handle_event = handle_event + def setup_method(self): + self.problem = Implicit_Problem(res, [1.0], [-1.0]) + self.problem.state_events = state_events + self.problem.handle_event = handle_event - cls.eproblem = Explicit_Problem(rhs, [1.0]) - cls.eproblem.state_events = estate_events - cls.eproblem.handle_event = handle_event + self.eproblem = Explicit_Problem(rhs, [1.0]) + self.eproblem.state_events = estate_events + self.eproblem.handle_event = handle_event def test_radau5dae_state_events(self): solver = Radau5DAE(self.problem) From 9abe69226e4a0f5c5e2c1bd61d47cdc7ee4a256d Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 23:19:56 +0200 Subject: [PATCH 8/9] ci: tell cibw to use assimulo-manylinux for aarch64 too The matrix commit set manylinux-x86_64-image only, so cibw silently fell back to the upstream quay.io/pypa/manylinux_2_28_aarch64 for the new aarch64 lane. That image doesn't have SUNDIALS preinstalled, so the meson build immediately failed with `SUNDIALS not found: could not locate cvodes/cvodes.h`. The Dockerfile.manylinux already builds for either arch (via ARG ARCH); the wheels.yml step tags it `assimulo-manylinux` regardless of arch on the aarch64 runner. Just needed to point cibw at it. Co-Authored-By: Claude Opus 4.7 --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7780df2c..17eed60f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ config-settings = {"setup-args" = ["-Dsundials_prefix=C:/deps", "-Dsuperlu_prefi # The official manylinux images are intentionally stripped of their package # manager, so before-all cannot install system packages. # Build the image first: make build-manylinux-image -manylinux-x86_64-image = "assimulo-manylinux" +manylinux-x86_64-image = "assimulo-manylinux" +manylinux-aarch64-image = "assimulo-manylinux" repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" config-settings = {"setup-args" = ["-Dsundials_prefix=/usr", "-Dsuperlu_prefix=/usr", "-Dopenmp=true"]} From e91a02c633143c63c1d9561e3bdf502c4c9181f9 Mon Sep 17 00:00:00 2001 From: Emil Fredriksson Date: Sun, 17 May 2026 23:20:09 +0200 Subject: [PATCH 9/9] chore: prepare 3.9.0b2 release Bump version to 3.9.0b2 and add a CHANGELOG section covering changes since 3.9.0b1: cp314 + Linux aarch64 wheel matrix and the setup_method test-fixture cleanup. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG | 20 ++++++++++++++++++++ meson.build | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0f1f614a..6d0bb8f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,24 @@ --- CHANGELOG --- +--- Assimulo-3.9.0b2 --- + Second beta of the modernized build system. No functional or API changes + relative to 3.9.0b1. + + Packaging and distribution: + * Wheel matrix expanded to CPython 3.14 and Linux aarch64 + (ubuntu-22.04-arm runners). Pull requests build cp311 + cp314 on + Linux x86_64 and Windows; master/tag/workflow_dispatch builds the + full matrix. + * wheels.yml restructured: a `config` job emits the matrix as JSON and + the linux/windows jobs consume it via fromJSON, working around the + fact that the `matrix` context is not available in `jobs..if`. + + Tests: + * Replaced the non-idiomatic `@classmethod` + `@pytest.fixture(autouse=True)` + stack on `setup_class` with the documented `setup_method` xunit hook. + The previous pattern crashed pytest on CPython 3.14 + (`AttributeError: 'FixtureFunctionDefinition' object has no attribute + '__code__'`); `setup_method` runs cleanly on every supported CPython. + --- Assimulo-3.9.0b1 --- Beta release of the modernized build system. No functional or API changes relative to 3.8.0. diff --git a/meson.build b/meson.build index 99a1c774..72826c7d 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'Assimulo', ['c', 'cython', 'fortran'], - version: '3.9.0b1', + version: '3.9.0b2', meson_version: '>=1.4' )