diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 058bbd416d..d1ace99aad 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -100,7 +100,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-13] + os: [ubuntu-latest, macos-12] steps: - uses: actions/checkout@v4 name: Check out PyBaMM repository @@ -113,6 +113,15 @@ jobs: - name: Clone pybind11 repo (no history) run: git clone --depth 1 --branch v2.11.1 https://github.com/pybind/pybind11.git + # sometimes gfortran cannot be found, so reinstall gcc just to be sure + - name: Install SuiteSparse and SUNDIALS on macOS + if: matrix.os == 'macos-12' + run: | + brew install graphviz libomp + brew reinstall gcc + python -m pip install cmake wget + python scripts/install_KLU_Sundials.py + - name: Build wheels on Linux run: pipx run cibuildwheel --output-dir wheelhouse if: matrix.os == 'ubuntu-latest' @@ -126,50 +135,11 @@ jobs: CIBW_TEST_COMMAND: python -c "import pybamm; pybamm.IDAKLUSolver()" - name: Build wheels on macOS amd64 - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-12' run: pipx run cibuildwheel --output-dir wheelhouse env: - MACOSX_DEPLOYMENT_TARGET: 11.0 - # Sourced from - # License: BSD-3-Clause - # https://github.com/scipy/scipy/blob/f2d4775e7762fad984f8f0acd8227c725ff21630/tools/wheels/cibw_before_build_macos.sh#L23-L49 - CIBW_BEFORE_ALL_MACOS: | - set -e -x - - # download gfortran with proper macos minimum version (11.0) - curl -L https://github.com/isuruf/gcc/releases/download/gcc-11.3.0-2/gfortran-darwin-x86_64-native.tar.gz -o gfortran.tar.gz - - GFORTRAN_SHA256=$(shasum --algorithm 256 gfortran.tar.gz) - KNOWN_SHA256="981367dd0ad4335613e91bbee453d60b6669f5d7e976d18c7bdb7f1966f26ae4 gfortran.tar.gz" - if [ "$GFORTRAN_SHA256" != "$KNOWN_SHA256" ]; then - echo "SHA256 mismatch for gfortran.tar.gz" - echo "expected: $KNOWN_SHA256" - echo "got: $GFORTRAN_SHA256" - exit 1 - fi - - mkdir -p gfortran_installed/ - tar -xv -C gfortran_installed/ -f gfortran.tar.gz - - export FC=$(pwd)/gfortran_installed/gfortran-darwin-x86_64-native/bin/gfortran - export PATH=$(pwd)/gfortran_installed/gfortran-darwin-x86_64-native/bin:$PATH - - # link libgfortran.5.dylib, libgfortran.dylib, libquadmath.0.dylib, libquadmath.dylib, libgcc_s.1.dylib, libgcc_s.1.1.dylib - # and place them in $HOME/.local/lib, and then change rpath - # to $HOME/.local/lib - - mkdir -p $HOME/.local/lib - lib_dir=$(pwd)/gfortran_installed/gfortran-darwin-x86_64-native/lib - for lib in libgfortran.5.dylib libgfortran.dylib libquadmath.0.dylib libquadmath.dylib libgcc_s.1.dylib libgcc_s.1.1.dylib; do - cp $lib_dir/$lib $HOME/.local/lib/ - install_name_tool -id $HOME/.local/lib/$lib $HOME/.local/lib/$lib - done - - export SDKROOT=${SDKROOT:-$(xcrun --show-sdk-path)} - - python -m pip install cmake wget - python scripts/install_KLU_Sundials.py - CIBW_BEFORE_BUILD_MACOS: python -m pip install --upgrade pip cmake casadi setuptools wheel delocate + CIBW_BEFORE_BUILD_MACOS: > + python -m pip install --upgrade cmake casadi setuptools wheel CIBW_REPAIR_WHEEL_COMMAND_MACOS: delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} CIBW_TEST_COMMAND: python -c "import pybamm; pybamm.IDAKLUSolver()" @@ -193,57 +163,18 @@ jobs: - name: Clone pybind11 repo (no history) run: git clone --depth 1 --branch v2.11.1 https://github.com/pybind/pybind11.git - - name: Build wheels on macOS arm64 + - name: Install SuiteSparse and SUNDIALS on macOS run: | - python -m pip install cibuildwheel - python -m cibuildwheel --output-dir wheelhouse - env: - MACOSX_DEPLOYMENT_TARGET: 11.0 - # Sourced from - # License: BSD-3-Clause - # https://github.com/scipy/scipy/blob/f2d4775e7762fad984f8f0acd8227c725ff21630/tools/wheels/cibw_before_build_macos.sh#L23-L49 - CIBW_BEFORE_ALL_MACOS: | - set -e -x - - # download gfortran with proper macos minimum version (11.0) - curl -L https://github.com/isuruf/gcc/releases/download/gcc-11.3.0-2/gfortran-darwin-arm64-native.tar.gz -o gfortran.tar.gz - - GFORTRAN_SHA256=$(shasum --algorithm 256 gfortran.tar.gz) - KNOWN_SHA256="84364eee32ba843d883fb8124867e2bf61a0cd73b6416d9897ceff7b85a24604 gfortran.tar.gz" - if [ "$GFORTRAN_SHA256" != "$KNOWN_SHA256" ]; then - echo "SHA256 mismatch for gfortran.tar.gz" - echo "expected: $KNOWN_SHA256" - echo "got: $GFORTRAN_SHA256" - exit 1 - fi - - mkdir -p gfortran_installed/ - tar -xv -C gfortran_installed/ -f gfortran.tar.gz - - export FC=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/bin/gfortran - export PATH=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/bin:$PATH + brew install graphviz libomp + brew reinstall gcc + python -m pip install cmake pipx + python scripts/install_KLU_Sundials.py - # link libgfortran.5.dylib, libgfortran.dylib, libquadmath.0.dylib, libquadmath.dylib, libgcc_s.1.1.dylib - # and place them in $HOME/.local/lib, and then change rpath - # note: libgcc_s.1.dylib not present for arm64, skip for now - # to $HOME/.local/lib - - mkdir -p $HOME/.local/lib - lib_dir=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/lib - for lib in libgfortran.5.dylib libgfortran.dylib libquadmath.0.dylib libquadmath.dylib libgcc_s.1.1.dylib; do - cp $lib_dir/$lib $HOME/.local/lib/ - install_name_tool -id $HOME/.local/lib/$lib $HOME/.local/lib/$lib - done - - export SDKROOT=${SDKROOT:-$(xcrun --show-sdk-path)} - - python -m pip install cmake wget - python scripts/install_KLU_Sundials.py - CIBW_BEFORE_BUILD_MACOS: python -m pip install --upgrade pip cmake casadi setuptools wheel delocate - # Use higher macOS target for now: https://github.com/casadi/casadi/issues/3698 - CIBW_REPAIR_WHEEL_COMMAND_MACOS: | - delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} --require-target-macos-version 11.1 - for file in {dest_dir}/*.whl; do mv "$file" "${file//macosx_11_1/macosx_11_0}"; done + - name: Build wheels on macOS arm64 + run: python -m pipx run cibuildwheel --output-dir wheelhouse + env: + CIBW_BEFORE_BUILD: python -m pip install cmake casadi setuptools wheel delocate + CIBW_REPAIR_WHEEL_COMMAND: delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} CIBW_TEST_COMMAND: python -c "import pybamm; pybamm.IDAKLUSolver()" - name: Upload wheels for macOS arm64 diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 48ba046646..426ee6b5b7 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -57,7 +57,7 @@ jobs: # sometimes gfortran cannot be found, so reinstall gcc just to be sure run: | brew analytics off - brew install graphviz + brew install graphviz libomp brew reinstall gcc - name: Install Windows system dependencies diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index 0b8b3118ba..5701b555e0 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -74,7 +74,7 @@ jobs: # sometimes gfortran cannot be found, so reinstall gcc just to be sure run: | brew analytics off - brew install graphviz + brew install graphviz libomp brew reinstall gcc - name: Install Windows system dependencies @@ -209,7 +209,7 @@ jobs: # sometimes gfortran cannot be found, so reinstall gcc just to be sure run: | brew analytics off - brew install graphviz + brew install graphviz libomp brew reinstall gcc - name: Install Windows system dependencies diff --git a/.gitignore b/.gitignore index 6f1f201617..03750e18b2 100644 --- a/.gitignore +++ b/.gitignore @@ -137,7 +137,3 @@ results/ # tests test_callback.log - -# openmp downloads -install_KLU_Sundials/openmp-* -install_KLU_Sundials/usr/ diff --git a/docs/source/user_guide/installation/install-from-source.rst b/docs/source/user_guide/installation/install-from-source.rst index a2a7637c9e..ea664b4a5b 100644 --- a/docs/source/user_guide/installation/install-from-source.rst +++ b/docs/source/user_guide/installation/install-from-source.rst @@ -43,11 +43,11 @@ You can install the above with Where ``X`` is the version sub-number. -.. tab:: macOS +.. tab:: MacOS .. code:: bash - brew install python openblas gcc gfortran graphviz cmake pandoc + brew install python openblas gcc gfortran graphviz libomp cmake pandoc .. note:: @@ -82,9 +82,8 @@ If you are running windows, you can simply skip this section and jump to :ref:`p # in the PyBaMM/ directory nox -s pybamm-requires -This will download, compile and install the SuiteSparse and SUNDIALS (with OpenMP) libraries -and the ``pybind11`` headers. -SuiteSparse and SUNDIALS are installed in ``~/.local`` by default. +This will download, compile and install the SuiteSparse and SUNDIALS libraries. +Both libraries are installed in ``~/.local``. For users requiring more control over the installation process, the ``pybamm-requires`` session supports additional command-line arguments: @@ -111,7 +110,7 @@ If you'd rather do things yourself, 1. Make sure you have CMake installed 2. Compile and install SuiteSparse (PyBaMM only requires the ``KLU`` component). -3. Compile and install SUNDIALS with `OpenMP support `_. +3. Compile and install SUNDIALS. 4. Clone the pybind11 repository in the ``PyBaMM/`` directory (make sure the directory is named ``pybind11``). @@ -316,12 +315,12 @@ source files to your current directory. ``ValueError: Integrator name ida does not exist``, or ``ValueError: Integrator name cvode does not exist``. -**Solution:** This could mean that you have not linked to -SUNDIALS correctly, check the instructions given above and make +**Solution:** This could mean that you have not installed +``scikits.odes`` correctly, check the instructions given above and make sure each command was successful. One possibility is that you have not set your ``LD_LIBRARY_PATH`` to -point to the SUNDIALS library, type ``echo $LD_LIBRARY_PATH`` and make +point to the sundials library, type ``echo $LD_LIBRARY_PATH`` and make sure one of the directories printed out corresponds to where the SUNDIALS libraries are located. diff --git a/scripts/install_KLU_Sundials.py b/scripts/install_KLU_Sundials.py index c0e046c48d..6224e40776 100755 --- a/scripts/install_KLU_Sundials.py +++ b/scripts/install_KLU_Sundials.py @@ -14,35 +14,18 @@ SUITESPARSE_VERSION = "6.0.3" SUNDIALS_VERSION = "6.5.0" - SUITESPARSE_URL = f"https://github.com/DrTimothyAldenDavis/SuiteSparse/archive/v{SUITESPARSE_VERSION}.tar.gz" SUNDIALS_URL = f"https://github.com/LLNL/sundials/releases/download/v{SUNDIALS_VERSION}/sundials-{SUNDIALS_VERSION}.tar.gz" - SUITESPARSE_CHECKSUM = ( "7111b505c1207f6f4bd0be9740d0b2897e1146b845d73787df07901b4f5c1fb7" ) SUNDIALS_CHECKSUM = "4e0b998dff292a2617e179609b539b511eb80836f5faacf800e688a886288502" - -# universal binaries for macOS 11.0 and later; sourced from https://mac.r-project.org/openmp/ -OPENMP_VERSION = "16.0.4" -OPENMP_URL = ( - f"https://mac.r-project.org/openmp/openmp-{OPENMP_VERSION}-darwin20-Release.tar.gz" -) -OPENMP_CHECKSUM = "a763f0bdc9115c4f4933accc81f514f3087d56d6528778f38419c2a0d2231972" - - DEFAULT_INSTALL_DIR = os.path.join(os.getenv("HOME"), ".local") def safe_remove_dir(path): - """Remove a directory or file if it exists.""" - try: - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path) - except Exception as e: - print(f"Error while removing {path}: {e}") + if os.path.exists(path): + shutil.rmtree(path) def install_suitesparse(download_dir): @@ -68,11 +51,11 @@ def install_suitesparse(download_dir): env = os.environ.copy() for libdir in ["SuiteSparse_config", "AMD", "COLAMD", "BTF", "KLU"]: build_dir = os.path.join(suitesparse_src, libdir) - # Set an RPATH in order for libsuitesparseconfig.dylib to find libomp.dylib + # We want to ensure that libsuitesparseconfig.dylib is not repeated in + # multiple paths at the time of wheel repair. Therefore, it should not be + # built with an RPATH since it is copied to the install prefix. if libdir == "SuiteSparse_config": - env["CMAKE_OPTIONS"] = ( - f"-DCMAKE_INSTALL_PREFIX={install_dir} -DCMAKE_INSTALL_RPATH={install_dir}/lib" - ) + env["CMAKE_OPTIONS"] = f"-DCMAKE_INSTALL_PREFIX={install_dir}" else: # For AMD, COLAMD, BTF and KLU; do not set a BUILD RPATH but use an # INSTALL RPATH in order to ensure that the dynamic libraries are found @@ -110,9 +93,20 @@ def install_sundials(download_dir, install_dir): # try to find OpenMP on mac if platform.system() == "Darwin": # flags to find OpenMP on mac - OpenMP_C_FLAGS = f"-Xpreprocessor -fopenmp -lomp -L{os.path.join(KLU_LIBRARY_DIR)} -I{os.path.join(KLU_INCLUDE_DIR)}" - OpenMP_C_LIB_NAMES = "omp" - OpenMP_omp_LIBRARY = os.path.join(KLU_LIBRARY_DIR, "libomp.dylib") + if platform.processor() == "arm": + OpenMP_C_FLAGS = ( + "-Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include" + ) + OpenMP_C_LIB_NAMES = "omp" + OpenMP_omp_LIBRARY = "/opt/homebrew/opt/libomp/lib/libomp.dylib" + elif platform.processor() == "i386": + OpenMP_C_FLAGS = "-Xpreprocessor -fopenmp -I/usr/local/opt/libomp/include" + OpenMP_C_LIB_NAMES = "omp" + OpenMP_omp_LIBRARY = "/usr/local/opt/libomp/lib/libomp.dylib" + else: + raise NotImplementedError( + f"Unsupported processor architecture: {platform.processor()}. Only 'arm' and 'i386' architectures are supported." + ) cmake_args += [ "-DOpenMP_C_FLAGS=" + OpenMP_C_FLAGS, @@ -136,47 +130,6 @@ def install_sundials(download_dir, install_dir): subprocess.run(make_cmd, cwd=build_dir, check=True) -# Relevant for macOS only because recent Xcode Clang versions do not include OpenMP headers. -# Other compilers (e.g. GCC) include the OpenMP specification by default. -def set_up_openmp(download_dir, install_dir): - print("-" * 10, "Extracting OpenMP archive", "-" * 40) - - openmp_dir = f"openmp-{OPENMP_VERSION}" - openmp_src = os.path.join(download_dir, openmp_dir) - - # extract OpenMP archive - with tarfile.open( - os.path.join(download_dir, f"{openmp_dir}-darwin20-Release.tar.gz") - ) as tar: - tar.extractall(openmp_src) - - # create directories - os.makedirs(os.path.join(install_dir, "lib"), exist_ok=True) - os.makedirs(os.path.join(install_dir, "include"), exist_ok=True) - - # copy files - shutil.copy( - os.path.join(openmp_src, "usr", "local", "lib", "libomp.dylib"), - os.path.join(install_dir, "lib"), - ) - for file in os.listdir(os.path.join(openmp_src, "usr", "local", "include")): - shutil.copy( - os.path.join(openmp_src, "usr", "local", "include", file), - os.path.join(install_dir, "include"), - ) - - # fix rpath; for some reason the downloaded dylib has an absolute path - # to /usr/local/lib/, so use self-referential rpath - subprocess.check_call( - [ - "install_name_tool", - "-id", - "@rpath/libomp.dylib", - f"{os.path.join(install_dir, 'lib', 'libomp.dylib')}", - ] - ) - - def check_libraries_installed(install_dir): # Define the directories to check for SUNDIALS and SuiteSparse libraries lib_dirs = [install_dir] @@ -224,7 +177,7 @@ def check_libraries_installed(install_dir): suitesparse_files = [file + ".dylib" for file in suitesparse_files] else: raise NotImplementedError( - f"Unsupported operating system: {platform.system()}. This script supports only Linux and macOS." + f"Unsupported operating system: {platform.system()}. This script currently supports only Linux and macOS." ) suitesparse_lib_found = True @@ -246,16 +199,6 @@ def check_libraries_installed(install_dir): return sundials_lib_found, suitesparse_lib_found -def check_openmp_installed_on_macos(install_dir): - openmp_lib_found = isfile(join(install_dir, "lib", "libomp.dylib")) - openmp_headers_found = isfile(join(install_dir, "include", "omp.h")) - if not openmp_lib_found or not openmp_headers_found: - print("libomp.dylib or omp.h not found. Proceeding with OpenMP installation.") - else: - print(f"libomp.dylib and omp.h found in {install_dir}.") - return openmp_lib_found - - def calculate_sha256(file_path): sha256_hash = hashlib.sha256() with open(file_path, "rb") as f: @@ -331,7 +274,7 @@ def parallel_download(urls, download_dir): # Get installation location parser = argparse.ArgumentParser( - description="Download, compile and install SUNDIALS and SuiteSparse." + description="Download, compile and install Sundials and SuiteSparse." ) parser.add_argument( "--force", @@ -353,20 +296,10 @@ def parallel_download(urls, download_dir): safe_remove_dir(os.path.join(download_dir, "build_sundials")) safe_remove_dir(os.path.join(download_dir, f"SuiteSparse-{SUITESPARSE_VERSION}")) safe_remove_dir(os.path.join(download_dir, f"sundials-{SUNDIALS_VERSION}")) - if platform.system() == "Darwin": - safe_remove_dir(os.path.join(install_dir, "lib", "libomp.dylib")) - safe_remove_dir(os.path.join(install_dir, "include", "omp.h")) - sundials_found, suitesparse_found, openmp_found = False, False, False - else: - sundials_found, suitesparse_found = False, False + sundials_found, suitesparse_found = False, False else: # Check whether the libraries are installed - if platform.system() == "Darwin": - sundials_found, suitesparse_found = check_libraries_installed(install_dir) - openmp_found = check_openmp_installed_on_macos(install_dir) - else: # Linux - sundials_found, suitesparse_found = check_libraries_installed(install_dir) - + sundials_found, suitesparse_found = check_libraries_installed(install_dir) if __name__ == "__main__": # Determine which libraries to download based on whether they were found @@ -379,22 +312,12 @@ def parallel_download(urls, download_dir): ], download_dir, ) - - if platform.system() == "Darwin" and not openmp_found: - download_extract_library(OPENMP_URL, OPENMP_CHECKSUM, download_dir) - set_up_openmp(download_dir, install_dir) - install_suitesparse(download_dir) install_sundials(download_dir, install_dir) - else: if not sundials_found: # Only SUNDIALS is missing, download and install it parallel_download([(SUNDIALS_URL, SUNDIALS_CHECKSUM)], download_dir) - if platform.system() == "Darwin" and not openmp_found: - download_extract_library(OPENMP_URL, OPENMP_CHECKSUM, download_dir) - set_up_openmp(download_dir, install_dir) - # openmp needed for SUNDIALS on macOS install_sundials(download_dir, install_dir) if not suitesparse_found: # Only SuiteSparse is missing, download and install it