diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ba9752c1..16734afc0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,48 +7,86 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-22.04, ubuntu-latest, macos-13, macos-latest] + os: [ubuntu-22.04, ubuntu-latest, macos-13, macos-latest, windows-latest] version: [13] # GCC version fail-fast: false env: GCC_V: ${{ matrix.version }} + CONDA_ENV: zerod + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@v4 - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true + activate-environment: ${{env.CONDA_ENV}} + python-version: "3.11.4" - name: Install ubuntu dependencies if: startsWith(matrix.os, 'ubuntu') run: sudo apt update && sudo apt install build-essential cmake lcov - - name: Create conda environment - run: | - #export PATH="/usr/share/miniconda/bin:$PATH" - #alias conda="$CONDA/bin/conda" - conda create -n zerod python=3.11.4 + - name: Install dependencies to get correct version numbers (Ubuntu) if: startsWith(matrix.os, 'ubuntu') - run: conda install -n zerod -c conda-forge libstdcxx-ng=${GCC_V} gcc=${GCC_V} + run: conda install -c conda-forge libstdcxx-ng=${GCC_V} gcc=${GCC_V} + - name: Install dependencies to get correct version numbers (MacOS) if: startsWith(matrix.os, 'macos') run: | brew install gcc@${GCC_V} ln -s /usr/local/bin/gcc-${GCC_V} /usr/local/bin/gcc - - name: Install svZeroDSolver - run: conda run -n zerod pip install -e ".[dev]" + + - name: Install dependencies for windows + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + choco install mingw --no-progress + conda install -y -c conda-forge cmake graphviz python-graphviz pydot + pip install --upgrade cmake-setuptools + + - name: Install POISX-like svZeroDSolver + if: ${{!startsWith(matrix.os, 'windows')}} + run: conda run pip install -e ".[dev]" + + - name: Install Windows svZeroDSolver + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + $Env:CMAKE_GENERATOR = 'MinGW Makefiles' + Write-Host "→ Using CMAKE_GENERATOR = $Env:CMAKE_GENERATOR" + pip install --no-build-isolation -v .[dev] + pip show pysvzerod + - name: Install Networkx run: | - conda run -n zerod pip install networkx + conda run pip install networkx + - name: Test the build run: | cd tests - conda run -n zerod pytest -v --durations=0 --ignore=test_dirgraph.py - - name: Build using CMake + conda run pytest -v --durations=0 --ignore=test_dirgraph.py + + - name: Build using CMake for POISX-like Systems + if: ${{!startsWith(matrix.os, 'windows')}} run: | mkdir Release cd Release cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_DISTRIBUTION=ON .. make -j2 - - name: Test interface + + - name: Build using CMake for Windows Systems + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + mkdir Release + cd Release + cmake -G "MinGW Makefiles" ` + -DCMAKE_BUILD_TYPE=Release ` + -DENABLE_DISTRIBUTION=ON ` + .. + cmake --build . --parallel 2 + + - name: Test interface POISX-like Systems + if: ${{!startsWith(matrix.os, 'windows')}} run: | cd tests/test_interface mkdir build_tests @@ -59,6 +97,26 @@ jobs: ./svZeroD_interface_test01 ../../../../Release ../../test_01/svzerod_3Dcoupling.json cd ../test_02 ./svZeroD_interface_test02 ../../../../Release ../../test_02/svzerod_tuned.json + + - name: Test interface Windows Systems + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + cd tests/test_interface + mkdir build_tests + cd build_tests + cmake -G "MinGW Makefiles" .. + cmake --build . --parallel 2 + cd test_01 + ./svZeroD_interface_test01.exe ` + ../../../../Release ` + ../../test_01/svzerod_3Dcoupling.json + + cd ../test_02 + ./svZeroD_interface_test02 ` + ../../../../Release ` + ../../test_02/svzerod_tuned.json + - name: Generate code coverage if: startsWith(matrix.os, 'ubuntu-22.04') run: | @@ -66,25 +124,38 @@ jobs: cmake -DENABLE_COVERAGE=ON .. make -j2 cd ../tests - conda run -n zerod pytest -v --durations=0 --coverage --ignore=test_dirgraph.py + conda run pytest -v --durations=0 --coverage --ignore=test_dirgraph.py cd ../Release make coverage + - name: Save coverage report if: startsWith(matrix.os, 'ubuntu-22.04') uses: actions/upload-artifact@v4 with: name: coverage_report path: Release/coverage + - name: Upload coverage reports to Codecov if: startsWith(matrix.os, 'ubuntu-22.04') uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - name: Build installer + + - name: Build installer POISX-like Systems + if: ${{!startsWith(matrix.os, 'windows')}} run: | cd Release cpack cp distribution/svZeroDSolver_* .. + + - name: Build installer Windows Systems + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + cd Release + cpack + Copy-Item distribution\svZeroDSolver_* -Destination ..\ + - name: Upload installer uses: actions/upload-artifact@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index a8708e798..93358496e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,22 @@ if(ENABLE_COVERAGE) WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endif() +if (WIN32 AND MSVC) + # CMake ≥ 3.15 has a proper variable + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.15") + set(CMAKE_MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$:Debug>") + else() + # CMake < 3.15: manually swap /MD → /MT in all flags for static + # versions of the runtime libraries + foreach(_var + CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_SHARED_LINKER_FLAGS) + string(REPLACE "/MD" "/MT" ${_var} "${${_var}}") + endforeach() + endif() +endif() # ----------------------------------------------------------------------------- # Set the location to store the binaries and libraries created by this project. # ----------------------------------------------------------------------------- @@ -159,7 +175,38 @@ add_executable(svzerodcalibrator applications/svzerodcalibrator.cpp # ----------------------------------------------------------------------------- # Replace EXCLUDE_FROM_ALL with SHARED to test building the Python # shared library. -pybind11_add_module(pysvzerod EXCLUDE_FROM_ALL applications/pysvzerod.cpp) + +pybind11_add_module(pysvzerod applications/pysvzerod.cpp) +if(WIN32 AND "${CMAKE_GENERATOR}" STREQUAL "MinGW Makefiles") + message(STATUS ">> Applying static‑link flags to pysvzerod") + + include(CheckCXXCompilerFlag) + set(_static_flags + -static + -static-libgcc + -static-libstdc++ + ) + # test for winpthread support before adding + check_cxx_compiler_flag("-static-libwinpthread" HAVE_WINPTHREAD) + if(HAVE_WINPTHREAD) + list(APPEND _static_flags -static-libwinpthread) + else() + find_package(Threads REQUIRED) + target_link_libraries(pysvzerod PRIVATE Threads::Threads) + endif() + # apply to compile *and* link + foreach(_f IN LISTS _static_flags) + target_compile_options(pysvzerod PRIVATE ${_f}) + # CMake ≥3.13: + target_link_options (pysvzerod PRIVATE ${_f}) + endforeach() + + # verify in the configure log for static compilation: + get_target_property(_ccopts pysvzerod COMPILE_OPTIONS) + message(STATUS ">>> pysvzerod COMPILE_OPTIONS = ${_ccopts}") + get_target_property(_lnkopts pysvzerod LINK_OPTIONS) + message(STATUS ">>> pysvzerod LINK_OPTIONS = ${_lnkopts}") +endif() # ----------------------------------------------------------------------------- # Add source sub-directories. @@ -232,3 +279,22 @@ add_custom_target(codeformat # check code format add_custom_target(codecheck COMMAND find ${SDIR}/*.h ${SDIR}/*.cpp | xargs clang-format -style=file:${PROJECT_SOURCE_DIR}/.clang-format --dry-run --Werror) + +set_target_properties(pysvzerod PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/python +) + +# standard install target locations +install(TARGETS pysvzerod + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_LIBDIR}" +) + +# append correct suffix for windows python runtime files +if (WIN32) + set_target_properties(pysvzerod PROPERTIES + PREFIX "" + SUFFIX ".pyd" + ) +endif() \ No newline at end of file diff --git a/setup.py b/setup.py index c6f07fc59..b55ec646d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,38 @@ +import os +import shutil from setuptools import setup from cmake_setuptools import CMakeExtension, CMakeBuildExt +class CustomCMakeBuild(CMakeBuildExt): + def run(self): + # ------------------------------------------------- + # 1. Build the C++ extension *without* the default + # setuptools copy step (set inplace False) + # ------------------------------------------------- + inplace_orig = self.inplace # remember + self.inplace = False # inhibit copy_extensions_to_source + super().run() # runs CMake + self.inplace = inplace_orig # restore flag + + # ------------------------------------------------- + # 2. Locate the compiled library + # ------------------------------------------------- + build_temp = os.path.abspath(self.build_temp) + search_root = os.path.join(build_temp, "python") + dest_dir = os.path.dirname(self.get_ext_fullpath("pysvzerod")) + + for root, _, files in os.walk(search_root): + for f in files: + if f.startswith("pysvzerod") and f.endswith((".so", ".pyd", ".dll", ".dylib")): + src = os.path.join(root, f) + os.makedirs(dest_dir, exist_ok=True) + shutil.copy2(src, os.path.join(dest_dir, f)) + print(f"[INFO] copied {src} -> {dest_dir}") + return + + raise RuntimeError("pysvzerod binary not found in build tree") + setup( ext_modules=[CMakeExtension("pysvzerod")], - cmdclass={"build_ext": CMakeBuildExt}, + cmdclass={"build_ext": CustomCMakeBuild}, ) diff --git a/src/model/ClosedLoopHeartPulmonary.cpp b/src/model/ClosedLoopHeartPulmonary.cpp index ce0c63eda..26899cead 100644 --- a/src/model/ClosedLoopHeartPulmonary.cpp +++ b/src/model/ClosedLoopHeartPulmonary.cpp @@ -207,7 +207,7 @@ void ClosedLoopHeartPulmonary::get_activation_and_elastance_functions( AA = 0.0; if (t_in_cycle <= tpwave) { AA = (0.5) * (1.0 - cos(2.0 * M_PI * (t_in_cycle - tpwave + Tsa) / Tsa)); - } else if ((t_in_cycle >= (T_cardiac - Tsa) + tpwave) and + } else if ((t_in_cycle >= (T_cardiac - Tsa) + tpwave) && (t_in_cycle < T_cardiac)) { AA = (0.5) * (1.0 - cos(2.0 * M_PI * (t_in_cycle - tpwave - (T_cardiac - Tsa)) / Tsa)); @@ -280,14 +280,14 @@ void ClosedLoopHeartPulmonary::get_valve_positions( auto pressure_ra = y[global_var_ids[0]]; auto pressure_rv = y[global_var_ids[6]]; auto outflow_ra = y[global_var_ids[5]]; - if ((pressure_ra <= pressure_rv) and (outflow_ra <= 0.0)) { + if ((pressure_ra <= pressure_rv) && (outflow_ra <= 0.0)) { valves[5] = 0.0; } // RV to pulmonary auto pressure_pulmonary = y[global_var_ids[9]]; auto outflow_rv = y[global_var_ids[8]]; - if ((pressure_rv <= pressure_pulmonary) and (outflow_rv <= 0.0)) { + if ((pressure_rv <= pressure_pulmonary) && (outflow_rv <= 0.0)) { valves[8] = 0.0; } @@ -295,14 +295,14 @@ void ClosedLoopHeartPulmonary::get_valve_positions( auto pressure_la = y[global_var_ids[10]]; auto pressure_lv = y[global_var_ids[13]]; auto outflow_la = y[global_var_ids[12]]; - if ((pressure_la <= pressure_lv) and (outflow_la <= 0.0)) { + if ((pressure_la <= pressure_lv) && (outflow_la <= 0.0)) { valves[12] = 0.0; } // LV to aorta auto pressure_aorta = y[global_var_ids[2]]; auto outflow_lv = y[global_var_ids[15]]; - if ((pressure_lv <= pressure_aorta) and (outflow_lv <= 0.0)) { + if ((pressure_lv <= pressure_aorta) && (outflow_lv <= 0.0)) { valves[15] = 0.0; } } diff --git a/src/model/Parameter.h b/src/model/Parameter.h index c3112ed6b..a4789ba84 100644 --- a/src/model/Parameter.h +++ b/src/model/Parameter.h @@ -34,8 +34,8 @@ #ifndef SVZERODSOLVER_MODEL_PARAMETER_HPP_ #define SVZERODSOLVER_MODEL_PARAMETER_HPP_ -#include - +#include +#include #include #include #include diff --git a/tests/test_interface/LPNSolverInterface/LPNSolverInterface.cpp b/tests/test_interface/LPNSolverInterface/LPNSolverInterface.cpp index 098435a29..829e818a6 100644 --- a/tests/test_interface/LPNSolverInterface/LPNSolverInterface.cpp +++ b/tests/test_interface/LPNSolverInterface/LPNSolverInterface.cpp @@ -1,5 +1,4 @@ #include "LPNSolverInterface.h" -#include #include #include diff --git a/tests/test_interface/LPNSolverInterface/LPNSolverInterface.h b/tests/test_interface/LPNSolverInterface/LPNSolverInterface.h index 513d5a2e5..a3a568204 100644 --- a/tests/test_interface/LPNSolverInterface/LPNSolverInterface.h +++ b/tests/test_interface/LPNSolverInterface/LPNSolverInterface.h @@ -1,5 +1,59 @@ +#if defined(_WIN32) || defined(_WIN64) +/* ---------- Windows implementation ---------- */ +#include +#ifdef interface + #undef interface +#endif + +using dl_handle_t = HMODULE; +// Define windows flags to emulate +#ifndef RTLD_LAZY + #define RTLD_LAZY 0 + #define RTLD_NOW 0 + #define RTLD_GLOBAL 0 + #define RTLD_LOCAL 0 +#endif + +inline dl_handle_t dlopen(const char* file, int /*flags*/) +{ + /* LoadLibraryA allows UTF-8 compatible narrow strings under MSVC ≥ 2015 */ + return ::LoadLibraryA(file); +} + +inline void* dlsym(dl_handle_t handle, const char* symbol) +{ + return reinterpret_cast(::GetProcAddress(handle, symbol)); +} + +inline int dlclose(dl_handle_t handle) +{ + return ::FreeLibrary(handle) ? 0 : 1; // 0 = success, POSIX-style +} + +/* Store the last error message in a local static buffer and return a C-string + * (roughly mimicking the POSIX API). + */ +inline const char* dlerror() +{ + static char buf[256]; + DWORD code = ::GetLastError(); + if (code == 0) return nullptr; + + ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + buf, sizeof(buf), nullptr); + return buf; +} +#else +/* ---------- POSIX / Unix-like ---------- */ #include +using dl_handle_t = void*; +#endif + #include #include #include @@ -62,7 +116,7 @@ class LPNSolverInterface std::string lpn_set_external_step_size_name_; void (*lpn_set_external_step_size_)(const int, double); - void* library_handle_ = nullptr; + dl_handle_t library_handle_ = nullptr; int problem_id_ = 0; int system_size_ = 0; int num_cycles_ = 0; diff --git a/tests/test_interface/test_01/main.cpp b/tests/test_interface/test_01/main.cpp index e6e29bc3c..3df83a1c3 100644 --- a/tests/test_interface/test_01/main.cpp +++ b/tests/test_interface/test_01/main.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +namespace fs = std::filesystem; //------ // main @@ -22,19 +24,20 @@ int main(int argc, char** argv) // Load shared library and get interface functions. // File extension of the shared library depends on the system - std::string svzerod_build_path = std::string(argv[1]); - std::string interface_lib_path = svzerod_build_path + "/src/interface/libsvzero_interface"; - std::string interface_lib_so = interface_lib_path + ".so"; - std::string interface_lib_dylib = interface_lib_path + ".dylib"; - std::ifstream lib_so_exists(interface_lib_so); - std::ifstream lib_dylib_exists(interface_lib_dylib); - if (lib_so_exists) { - interface.load_library(interface_lib_so); - } else if (lib_dylib_exists) { - interface.load_library(interface_lib_dylib); + fs::path build_dir = argv[1]; + fs::path iface_dir = build_dir / "src" / "interface"; + fs::path lib_so = iface_dir / "libsvzero_interface.so"; + fs::path lib_dylib = iface_dir / "libsvzero_interface.dylib"; + fs::path lib_dll = iface_dir / "libsvzero_interface.dll"; + if (fs::exists(lib_so)) { + interface.load_library(lib_so.string()); + } else if (fs::exists(lib_dylib)) { + interface.load_library(lib_dylib.string()); + } else if (fs::exists(lib_dll)) { + interface.load_library(lib_dll.string()); } else { - throw std::runtime_error("Could not find shared libraries " + interface_lib_so + " or " + interface_lib_dylib); - } + throw std::runtime_error("Could not find shared libraries " + lib_so.string() + " or " + lib_dylib.string() + " or " + lib_dll.string() + " !"); + } // Set up the svZeroD model std::string file_name = std::string(argv[2]); diff --git a/tests/test_interface/test_02/main.cpp b/tests/test_interface/test_02/main.cpp index b6856e314..57bd803d7 100644 --- a/tests/test_interface/test_02/main.cpp +++ b/tests/test_interface/test_02/main.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +namespace fs = std::filesystem; //--------------------------------------------------------------------------------------- // Compare mean flow/pressure in aorta and coronary with pre-computed ("correct") values @@ -52,19 +54,20 @@ int main(int argc, char** argv) // Load shared library and get interface functions. // File extension of the shared library depends on the system - std::string svzerod_build_path = std::string(argv[1]); - std::string interface_lib_path = svzerod_build_path + "/src/interface/libsvzero_interface"; - std::string interface_lib_so = interface_lib_path + ".so"; - std::string interface_lib_dylib = interface_lib_path + ".dylib"; - std::ifstream lib_so_exists(interface_lib_so); - std::ifstream lib_dylib_exists(interface_lib_dylib); - if (lib_so_exists) { - interface.load_library(interface_lib_so); - } else if (lib_dylib_exists) { - interface.load_library(interface_lib_dylib); + fs::path build_dir = argv[1]; + fs::path iface_dir = build_dir / "src" / "interface"; + fs::path lib_so = iface_dir / "libsvzero_interface.so"; + fs::path lib_dylib = iface_dir / "libsvzero_interface.dylib"; + fs::path lib_dll = iface_dir / "libsvzero_interface.dll"; + if (fs::exists(lib_so)) { + interface.load_library(lib_so.string()); + } else if (fs::exists(lib_dylib)) { + interface.load_library(lib_dylib.string()); + } else if (fs::exists(lib_dll)) { + interface.load_library(lib_dll.string()); } else { - throw std::runtime_error("Could not find shared libraries " + interface_lib_so + " or " + interface_lib_dylib); - } + throw std::runtime_error("Could not find shared libraries " + lib_so.string() + " or " + lib_dylib.string() + " or " + lib_dll.string() + " !"); + } // Set up the svZeroD model std::string file_name = std::string(argv[2]); diff --git a/tests/test_interface/test_03/main.cpp b/tests/test_interface/test_03/main.cpp index 6fd45224d..be47ab45d 100644 --- a/tests/test_interface/test_03/main.cpp +++ b/tests/test_interface/test_03/main.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +namespace fs = std::filesystem; //------ // main @@ -22,19 +24,20 @@ int main(int argc, char** argv) // Load shared library and get interface functions. // File extension of the shared library depends on the system - std::string svzerod_build_path = std::string(argv[1]); - std::string interface_lib_path = svzerod_build_path + "/src/interface/libsvzero_interface"; - std::string interface_lib_so = interface_lib_path + ".so"; - std::string interface_lib_dylib = interface_lib_path + ".dylib"; - std::ifstream lib_so_exists(interface_lib_so); - std::ifstream lib_dylib_exists(interface_lib_dylib); - if (lib_so_exists) { - interface.load_library(interface_lib_so); - } else if (lib_dylib_exists) { - interface.load_library(interface_lib_dylib); + fs::path build_dir = argv[1]; + fs::path iface_dir = build_dir / "src" / "interface"; + fs::path lib_so = iface_dir / "libsvzero_interface.so"; + fs::path lib_dylib = iface_dir / "libsvzero_interface.dylib"; + fs::path lib_dll = iface_dir / "libsvzero_interface.dll"; + if (fs::exists(lib_so)) { + interface.load_library(lib_so.string()); + } else if (fs::exists(lib_dylib)) { + interface.load_library(lib_dylib.string()); + } else if (fs::exists(lib_dll)) { + interface.load_library(lib_dll.string()); } else { - throw std::runtime_error("Could not find shared libraries " + interface_lib_so + " or " + interface_lib_dylib); - } + throw std::runtime_error("Could not find shared libraries " + lib_so.string() + " or " + lib_dylib.string() + " or " + lib_dll.string() + " !"); + } // Set up the svZeroD model std::string file_name = std::string(argv[2]);