diff --git a/setup.py b/setup.py index dfdd455a16..1917b62728 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import os +import sys import glob import logging import subprocess @@ -13,12 +14,173 @@ from distutils.core import setup, find_packages from distutils.command.install import install -import CMakeBuild +# import CMakeBuild + +# ---------- cmakebuild was integrated into setup.py directly -------------------------- + +try: + from setuptools.command.build_ext import build_ext +except ImportError: + from distutils.command.build_ext import build_ext default_lib_dir = ( "" if system() == "Windows" else os.path.join(os.getenv("HOME"), ".local") ) + +def set_vcpkg_environment_variables(): + if not os.getenv("VCPKG_ROOT_DIR"): + raise EnvironmentError("Environment variable 'VCPKG_ROOT_DIR' is undefined.") + if not os.getenv("VCPKG_DEFAULT_TRIPLET"): + raise EnvironmentError( + "Environment variable 'VCPKG_DEFAULT_TRIPLET' is undefined." + ) + if not os.getenv("VCPKG_FEATURE_FLAGS"): + raise EnvironmentError( + "Environment variable 'VCPKG_FEATURE_FLAGS' is undefined." + ) + return ( + os.getenv("VCPKG_ROOT_DIR"), + os.getenv("VCPKG_DEFAULT_TRIPLET"), + os.getenv("VCPKG_FEATURE_FLAGS"), + ) + + +class CMakeBuild(build_ext): + user_options = build_ext.user_options + [ + ("suitesparse-root=", None, "suitesparse source location"), + ("sundials-root=", None, "sundials source location"), + ] + + def initialize_options(self): + build_ext.initialize_options(self) + self.suitesparse_root = None + self.sundials_root = None + + def finalize_options(self): + build_ext.finalize_options(self) + # Determine the calling command to get the + # undefined options from. + # If build_ext was called directly then this + # doesn't matter. + try: + self.get_finalized_command("install", create=0) + calling_cmd = "install" + except AttributeError: + calling_cmd = "bdist_wheel" + self.set_undefined_options( + calling_cmd, + ("suitesparse_root", "suitesparse_root"), + ("sundials_root", "sundials_root"), + ) + if not self.suitesparse_root: + self.suitesparse_root = os.path.join(default_lib_dir) + if not self.sundials_root: + self.sundials_root = os.path.join(default_lib_dir) + + def get_build_directory(self): + # distutils outputs object files in directory self.build_temp + # (typically build/temp.*). This is our CMake build directory. + # On Windows, distutils is too smart and appends "Release" or + # "Debug" to self.build_temp. So in this case we want the + # build directory to be the parent directory. + if system() == "Windows": + return Path(self.build_temp).parents[0] + return self.build_temp + + def run(self): + if not self.extensions: + return + + if system() == "Windows": + use_python_casadi = False + else: + use_python_casadi = True + + build_type = os.getenv("PYBAMM_CPP_BUILD_TYPE", "RELEASE") + cmake_args = [ + "-DCMAKE_BUILD_TYPE={}".format(build_type), + "-DPYTHON_EXECUTABLE={}".format(sys.executable), + "-DUSE_PYTHON_CASADI={}".format("TRUE" if use_python_casadi else "FALSE"), + ] + if self.suitesparse_root: + cmake_args.append( + "-DSuiteSparse_ROOT={}".format(os.path.abspath(self.suitesparse_root)) + ) + if self.sundials_root: + cmake_args.append( + "-DSUNDIALS_ROOT={}".format(os.path.abspath(self.sundials_root)) + ) + + build_dir = self.get_build_directory() + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # The CMakeError.log file is generated by cmake is the configure step + # encounters error. In the following the existence of this file is used + # to determine whether or not the cmake configure step went smoothly. + # So must make sure this file does not remain from a previous failed build. + if os.path.isfile(os.path.join(build_dir, "CMakeError.log")): + os.remove(os.path.join(build_dir, "CMakeError.log")) + + build_env = os.environ + if os.getenv("PYBAMM_USE_VCPKG"): + ( + vcpkg_root_dir, + vcpkg_default_triplet, + vcpkg_feature_flags, + ) = set_vcpkg_environment_variables() + build_env["vcpkg_root_dir"] = vcpkg_root_dir + build_env["vcpkg_default_triplet"] = vcpkg_default_triplet + build_env["vcpkg_feature_flags"] = vcpkg_feature_flags + + cmake_list_dir = os.path.abspath(os.path.dirname(__file__)) + print("-" * 10, "Running CMake for idaklu solver", "-" * 40) + subprocess.run( + ["cmake", cmake_list_dir] + cmake_args, cwd=build_dir, env=build_env + ) + + if os.path.isfile(os.path.join(build_dir, "CMakeError.log")): + msg = ( + "cmake configuration steps encountered errors, and the idaklu module" + " could not be built. Make sure dependencies are correctly " + "installed. See " + "https://github.com/pybamm-team/PyBaMM/tree/develop" + "INSTALL-LINUX-MAC.md" + ) + raise RuntimeError(msg) + else: + print("-" * 10, "Building idaklu module", "-" * 40) + subprocess.run( + ["cmake", "--build", ".", "--config", "Release"], + cwd=build_dir, + env=build_env, + ) + + # Move from build temp to final position + for ext in self.extensions: + self.move_output(ext) + + def move_output(self, ext): + # Copy built module to dist/ directory + build_temp = Path(self.build_temp).resolve() + # Get destination location + # self.get_ext_fullpath(ext.name) --> + # build/lib.linux-x86_64-3.5/idaklu.cpython-37m-x86_64-linux-gnu.so + # using resolve() with python < 3.6 will result in a FileNotFoundError + # since the location does not yet exists. + dest_path = Path(self.get_ext_fullpath(ext.name)).resolve() + source_path = build_temp / os.path.basename(self.get_ext_filename(ext.name)) + dest_directory = dest_path.parents[0] + dest_directory.mkdir(parents=True, exist_ok=True) + self.copy_file(source_path, dest_path) + +# ---------- end of cmakebuild steps --------------------------------------------------- + +# default_lib_dir = ( +# "" if system() == "Windows" else os.path.join(os.getenv("HOME"), ".local") +# ) + log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" logger = logging.getLogger("PyBaMM setup") @@ -123,6 +285,7 @@ def compile_KLU(): # Build the list of package data files to be included in the PyBaMM package. # These are mainly the parameter files located in the input/parameters/ subdirectories. +# TODO: might be possible to include in pyproject.toml with data configuration values pybamm_data = [] for file_ext in ["*.csv", "*.py", "*.md", "*.txt"]: # Get all the files ending in file_ext in pybamm/input dir. @@ -162,144 +325,32 @@ def compile_KLU(): ext_modules = [idaklu_ext] if compile_KLU() else [] # Defines __version__ +# TODO: might not be needed anymore, because we define it in pyproject.toml +# and can therefore access it with importlib.metadata.version("pybamm") (python 3.8+) +# The version.py file can then be imported with attr: pybamm.__version__ dynamically root = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(root, "pybamm", "version.py")) as f: exec(f.read()) # Load text for description and license +# TODO: might not be needed anymore, because we define the description and license +# in pyproject.toml +# TODO: add long description there and remove it from setup() with open("README.md", encoding="utf-8") as f: readme = f.read() +# Project metadata was moved to pyproject.toml (which is read by pip). +# However, custom build commands and setuptools extension modules are still defined here setup( - name="pybamm", - version=__version__, # noqa: F821 - description="Python Battery Mathematical Modelling.", long_description=readme, long_description_content_type="text/markdown", url="https://github.com/pybamm-team/PyBaMM", packages=find_packages(include=("pybamm", "pybamm.*")), ext_modules=ext_modules, cmdclass={ - "build_ext": CMakeBuild.CMakeBuild, + "build_ext": CMakeBuild, "bdist_wheel": bdist_wheel, "install": CustomInstall, }, package_data={"pybamm": pybamm_data}, - # Python version - python_requires=">=3.8,<3.12", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Scientific/Engineering", - ], - # List of dependencies - install_requires=[ - "numpy>=1.16", - "scipy>=1.3", - "casadi>=3.6.0", - "xarray", - ], - extras_require={ - "docs": [ - "sphinx>=6", - "sphinx_rtd_theme>=0.5", - "pydata-sphinx-theme", - "sphinx_design", - "sphinx-copybutton", - "myst-parser", - "sphinx-inline-tabs", - "sphinxcontrib-bibtex", - "sphinx-autobuild", - "sphinx-last-updated-by-git", - "nbsphinx", - "ipykernel", - "ipywidgets", - "sphinx-gallery", - "sphinx-hoverxref", - "sphinx-docsearch", - ], # For doc generation - "examples": [ - "jupyter", # For example notebooks - ], - "plot": [ - "imageio>=2.9.0", - # Note: Matplotlib is loaded for debug plots, but to ensure pybamm runs - # on systems without an attached display, it should never be imported - # outside of plot() methods. - # Should not be imported - "matplotlib>=2.0", - ], - "cite": [ - "pybtex>=0.24.0", - ], - "latexify": [ - "sympy>=1.8", - ], - "bpx": [ - "bpx", - ], - "tqdm": [ - "tqdm", - ], - "dev": [ - "pre-commit", # For code style checking - "ruff", # For code style auto-formatting - "nox", # For running testing sessions - ], - "pandas": [ - "pandas>=0.24", - ], - "jax": [ - "jax==0.4.8", - "jaxlib==0.4.7", - ], - "odes": ["scikits.odes"], - "all": [ - "anytree>=2.4.3", - "autograd>=1.2", - "pandas>=0.24", - "scikit-fem>=0.2.0", - "imageio>=2.9.0", - "pybtex>=0.24.0", - "sympy>=1.8", - "bpx", - "tqdm", - "matplotlib>=2.0", - "jupyter", - ], - }, - entry_points={ - "console_scripts": [ - "pybamm_edit_parameter = pybamm.parameters_cli:edit_parameter", - "pybamm_add_parameter = pybamm.parameters_cli:add_parameter", - "pybamm_rm_parameter = pybamm.parameters_cli:remove_parameter", - "pybamm_install_odes = pybamm.install_odes:main", - "pybamm_install_jax = pybamm.util:install_jax", - ], - "pybamm_parameter_sets": [ - "Sulzer2019 = pybamm.input.parameters.lead_acid.Sulzer2019:get_parameter_values", # noqa: E501 - "Ai2020 = pybamm.input.parameters.lithium_ion.Ai2020:get_parameter_values", # noqa: E501 - "Chen2020 = pybamm.input.parameters.lithium_ion.Chen2020:get_parameter_values", # noqa: E501 - "Chen2020_composite = pybamm.input.parameters.lithium_ion.Chen2020_composite:get_parameter_values", # noqa: E501 - "Ecker2015 = pybamm.input.parameters.lithium_ion.Ecker2015:get_parameter_values", # noqa: E501 - "Marquis2019 = pybamm.input.parameters.lithium_ion.Marquis2019:get_parameter_values", # noqa: E501 - "Mohtat2020 = pybamm.input.parameters.lithium_ion.Mohtat2020:get_parameter_values", # noqa: E501 - "NCA_Kim2011 = pybamm.input.parameters.lithium_ion.NCA_Kim2011:get_parameter_values", # noqa: E501 - "OKane2022 = pybamm.input.parameters.lithium_ion.OKane2022:get_parameter_values", # noqa: E501 - "ORegan2022 = pybamm.input.parameters.lithium_ion.ORegan2022:get_parameter_values", # noqa: E501 - "Prada2013 = pybamm.input.parameters.lithium_ion.Prada2013:get_parameter_values", # noqa: E501 - "Ramadass2004 = pybamm.input.parameters.lithium_ion.Ramadass2004:get_parameter_values", # noqa: E501 - "Xu2019 = pybamm.input.parameters.lithium_ion.Xu2019:get_parameter_values", # noqa: E501 - "ECM_Example = pybamm.input.parameters.ecm.example_set:get_parameter_values", # noqa: E501 - ], - }, )