diff --git a/.github/actions/build-iqtree/action.yml b/.github/actions/build-iqtree/action.yml index 6ce03727..9f457639 100644 --- a/.github/actions/build-iqtree/action.yml +++ b/.github/actions/build-iqtree/action.yml @@ -19,19 +19,68 @@ runs: IQ_TREE_2_SHA=$(git rev-parse HEAD) echo "iqtree2-sha=${IQ_TREE_2_SHA}" >> "$GITHUB_OUTPUT" - - uses: actions/cache@v4 - id: cache + - name: Cache IQ-TREE 2 (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v4 + id: cache-windows + with: + key: libiqtree-${{ inputs.os }}-${{ steps.iqtree2-sha.outputs.iqtree2-sha }} + path: | + src/piqtree/_libiqtree/iqtree2.lib + src/piqtree/_libiqtree/iqtree2.dll + lookup-only: true + + - name: Cache IQ-TREE 2 (Linux/macOS) + if: runner.os != 'Windows' + uses: actions/cache@v4 + id: cache-unix with: key: libiqtree-${{ inputs.os }}-${{ steps.iqtree2-sha.outputs.iqtree2-sha }} - path: src/piqtree/_libiqtree/libiqtree2.a + path: | + src/piqtree/_libiqtree/libiqtree2.a lookup-only: true + - name: Combine Cache Hits + id: cache + shell: bash + run: | + if [[ "${{ steps.cache-windows.outputs.cache-hit }}" == 'true' || "${{ steps.cache-unix.outputs.cache-hit }}" == 'true' ]]; then + echo "cache-hit=true" >> "$GITHUB_OUTPUT" + else + echo "cache-hit=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Boost + if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true' + uses: MarkusJx/install-boost@v2.4.5 + id: install-boost + with: + boost_version: 1.84.0 + platform_version: 2022 + toolset: mingw + + - name: Set Boost Environment Variables + if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + echo "Boost_INCLUDE_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }}/include" >> "$GITHUB_ENV" + echo "Boost_LIBRARY_DIRS=${{ steps.install-boost.outputs.BOOST_ROOT }}/lib" >> "$GITHUB_ENV" + + - name: Setup MSVC Developer Command Prompt + if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true' + uses: ilammy/msvc-dev-cmd@v1 + - name: Build IQ-TREE shell: bash if: steps.cache.outputs.cache-hit != 'true' run: | - if [[ "${{ inputs.os }}" == "ubuntu-latest" ]]; then - sudo ./build_tools/before_all_linux.sh + if [[ "${{ runner.os }}" == "Linux" ]]; then + sudo ./build_tools/before_all_linux.sh + elif [[ "${{ runner.os }}" == "macOS" ]]; then + ./build_tools/before_all_mac.sh + elif [[ "${{ runner.os }}" == "Windows" ]]; then + ./build_tools/before_all_windows.sh else - ./build_tools/before_all_mac.sh - fi + echo "Unrecognized OS: '${{ inputs.os }}'." + exit 1 + fi \ No newline at end of file diff --git a/.github/actions/setup-piqtree/action.yml b/.github/actions/setup-piqtree/action.yml index 00ae89b0..fb72ec8a 100644 --- a/.github/actions/setup-piqtree/action.yml +++ b/.github/actions/setup-piqtree/action.yml @@ -13,9 +13,24 @@ runs: - uses: "actions/setup-python@v5" with: python-version: ${{ inputs.python-version }} - - - uses: actions/cache/restore@v4 + + - name: Cache IQ-TREE 2 (Windows) + if: runner.os == 'Windows' + uses: actions/cache/restore@v4 + id: cache-windows with: key: ${{ inputs.cache-key }} - path: src/piqtree/_libiqtree/libiqtree2.a - fail-on-cache-miss: true \ No newline at end of file + path: | + src/piqtree/_libiqtree/iqtree2.lib + src/piqtree/_libiqtree/iqtree2.dll + fail-on-cache-miss: true + + - name: Cache IQ-TREE 2 (Linux/macOS) + if: runner.os != 'Windows' + uses: actions/cache/restore@v4 + id: cache-unix + with: + key: ${{ inputs.cache-key }} + path: | + src/piqtree/_libiqtree/libiqtree2.a + fail-on-cache-miss: true diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 66dc0819..5d56787d 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: include: - # manylinux (x86) + # manylinux x86_64 - os: ubuntu-latest platform_id: manylinux_x86_64 @@ -22,6 +22,10 @@ jobs: - os: macos-14 platform_id: macosx_arm64 + # Windows x86_64 + - os: windows-latest + platform_id: win_amd64 + steps: - uses: "actions/checkout@v4" with: @@ -35,7 +39,7 @@ jobs: platforms: arm64 - name: Set macOS Deployment Target - if: ${{startsWith(matrix.os, 'macos')}} + if: runner.os == 'macOS' run: | if [[ "${{ matrix.os }}" == "macos-13" ]]; then echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV @@ -43,13 +47,29 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV fi + - name: Install Boost + if: runner.os == 'Windows' + uses: MarkusJx/install-boost@v2.4.5 + id: install-boost + with: + boost_version: 1.84.0 + platform_version: 2022 + toolset: mingw + + - name: Setup MSVC Developer Command Prompt + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + - name: Build wheels uses: pypa/cibuildwheel@v2.23.0 env: # Can specify per os - e.g. CIBW_BEFORE_ALL_LINUX, CIBW_BEFORE_ALL_MACOS, CIBW_BEFORE_ALL_WINDOWS CIBW_BEFORE_ALL_LINUX: ./build_tools/before_all_linux.sh CIBW_BEFORE_ALL_MACOS: ./build_tools/before_all_mac.sh + CIBW_BEFORE_ALL_WINDOWS: bash ./build_tools/before_all_windows.sh + CIBW_ENVIRONMENT_WINDOWS: Boost_INCLUDE_DIR='${{ steps.install-boost.outputs.BOOST_ROOT }}/include' Boost_LIBRARY_DIRS='${{ steps.install-boost.outputs.BOOST_ROOT }}/lib' CIBW_ARCHS_LINUX: ${{endsWith(matrix.platform_id, '_x86_64') && 'x86_64' || 'aarch64'}} CIBW_ARCHS_MACOS: ${{endsWith(matrix.platform_id, 'universal2') && 'universal2' || 'auto'}} + CIBW_ARCHS_WINDOWS: ${{endsWith(matrix.platform_id, '_amd64') && 'AMD64' || 'ARM64'}} CIBW_BUILD: "*${{matrix.platform_id}}" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: pytest {package}/tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71e763b9..acf50784 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-13, macos-14] # Intel linux, Intel Mac, ARM Mac + os: [ubuntu-latest, macos-13, macos-14, windows-latest] # Intel linux, Intel Mac, ARM Mac, Windows steps: - uses: "actions/checkout@v4" @@ -35,7 +35,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-13, macos-14] # Intel linux, Intel Mac, ARM Mac + os: [ubuntu-latest, macos-13, macos-14, windows-latest] # Intel linux, Intel Mac, ARM Mac, Windows python-version: ["3.10", "3.11", "3.12"] steps: - uses: "actions/checkout@v4" @@ -48,10 +48,15 @@ jobs: python-version: ${{ matrix.python-version }} cache-key: libiqtree-${{ matrix.os }}-${{ needs.build-iqtree.outputs.iqtree2-sha }} - - name: Install llvm - if: matrix.os != 'ubuntu-latest' + - name: Install llvm (macOS) + if: runner.os == 'macOS' run: | brew install llvm + + - name: Install llvm (Windows) + if: runner.os == 'Windows' + run: | + choco install -y llvm --version=14.0.6 --allow-downgrade - name: Run Nox Testing run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17158bca..500c3127 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: include: - # manylinux (x86) + # manylinux x86_64 - os: ubuntu-latest platform_id: manylinux_x86_64 @@ -22,6 +22,10 @@ jobs: - os: macos-14 platform_id: macosx_arm64 + # Windows x86_64 + - os: windows-latest + platform_id: win_amd64 + steps: - uses: "actions/checkout@v4" with: @@ -35,23 +39,41 @@ jobs: platforms: arm64 - name: Set macOS Deployment Target - if: ${{startsWith(matrix.os, 'macos')}} + if: runner.os == 'macOS' run: | if [[ "${{ matrix.os }}" == "macos-13" ]]; then echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV elif [[ "${{ matrix.os }}" == "macos-14" ]]; then echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV fi - + + - name: Install Boost + if: runner.os == 'Windows' + uses: MarkusJx/install-boost@v2.4.5 + id: install-boost + with: + boost_version: 1.84.0 + platform_version: 2022 + toolset: mingw + + - name: Setup MSVC Developer Command Prompt + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + - name: Build wheels uses: pypa/cibuildwheel@v2.23.0 env: # Can specify per os - e.g. CIBW_BEFORE_ALL_LINUX, CIBW_BEFORE_ALL_MACOS, CIBW_BEFORE_ALL_WINDOWS CIBW_BEFORE_ALL_LINUX: ./build_tools/before_all_linux.sh CIBW_BEFORE_ALL_MACOS: ./build_tools/before_all_mac.sh + CIBW_BEFORE_ALL_WINDOWS: bash ./build_tools/before_all_windows.sh + CIBW_ENVIRONMENT_WINDOWS: Boost_INCLUDE_DIR='${{ steps.install-boost.outputs.BOOST_ROOT }}/include' Boost_LIBRARY_DIRS='${{ steps.install-boost.outputs.BOOST_ROOT }}/lib' CIBW_ARCHS_LINUX: ${{endsWith(matrix.platform_id, '_x86_64') && 'x86_64' || 'aarch64'}} + CIBW_ARCHS_MACOS: ${{endsWith(matrix.platform_id, 'universal2') && 'universal2' || 'auto'}} + CIBW_ARCHS_WINDOWS: ${{endsWith(matrix.platform_id, '_amd64') && 'AMD64' || 'ARM64'}} CIBW_BUILD: "*${{matrix.platform_id}}" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: pytest {package}/tests + CIBW_TEST_SKIP: "*-macosx_universal2:x86_64" # skip x86 on m1 mac CIBW_SKIP: pp* # Disable building PyPy wheels on all platforms - name: Upload wheels diff --git a/.gitignore b/.gitignore index 87df248a..0ea37ac7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ # piqtree specific ignores src/piqtree/_libiqtree/**/*.a +src/piqtree/_libiqtree/**/*.dll +src/piqtree/_libiqtree/**/*.lib +src/*.dll # docs data diff --git a/build_tools/before_all_windows.sh b/build_tools/before_all_windows.sh new file mode 100644 index 00000000..763536ea --- /dev/null +++ b/build_tools/before_all_windows.sh @@ -0,0 +1,13 @@ +# Install dependencies using choco + +export Boost_INCLUDE_DIR=$(echo $Boost_INCLUDE_DIR | sed 's|\\|/|g') +export Boost_LIBRARY_DIRS=$(echo $Boost_LIBRARY_DIRS | sed 's|\\|/|g') + +echo "Boost_INCLUDE_DIR: $Boost_INCLUDE_DIR" +echo "Boost_LIBRARY_DIRS: $Boost_LIBRARY_DIRS" + +choco install -y llvm --version=14.0.6 --allow-downgrade +choco install -y eigen + +# Build IQ-TREE +bash build_tools/build_iqtree.sh \ No newline at end of file diff --git a/build_tools/build_iqtree.sh b/build_tools/build_iqtree.sh index 38bb7afe..5aedccd6 100755 --- a/build_tools/build_iqtree.sh +++ b/build_tools/build_iqtree.sh @@ -9,11 +9,37 @@ if [[ "$OSTYPE" == "darwin"* ]]; then echo $CXXFLAGS cmake -DBUILD_LIB=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. gmake -j +elif [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "cygwin"* ]]; then + echo "Building for Windows." + + if [[ -n "$BOOST_ROOT" ]]; then + export Boost_INCLUDE_DIR="${BOOST_ROOT}" + export Boost_LIBRARY_DIRS="${BOOST_ROOT}" + fi + + cmake -G "MinGW Makefiles" \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_C_FLAGS=--target=x86_64-pc-windows-gnu \ + -DCMAKE_CXX_FLAGS=--target=x86_64-pc-windows-gnu \ + -DCMAKE_MAKE_PROGRAM=make \ + -DBoost_INCLUDE_DIR=$Boost_INCLUDE_DIR \ + -DBoost_LIBRARY_DIRS=$Boost_LIBRARY_DIRS \ + -DIQTREE_FLAGS="cpp14" \ + -DBUILD_LIB=ON \ + .. + make -j else - echo "Building for linux." + echo "Building for Linux." cmake -DBUILD_LIB=ON .. make -j fi cd ../.. -mv iqtree2/build/libiqtree2.a src/piqtree/_libiqtree/ \ No newline at end of file + +if [[ "$OSTYPE" == "darwin"* || "$OSTYPE" == "linux"* ]]; then + mv iqtree2/build/libiqtree2.a src/piqtree/_libiqtree/ +elif [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "cygwin"* ]]; then + mv iqtree2/build/iqtree2.lib src/piqtree/_libiqtree/ + mv iqtree2/build/iqtree2.dll src/piqtree/_libiqtree/ +fi diff --git a/pyproject.toml b/pyproject.toml index 28f4cb7f..19871515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 61.0", "pybind11 >= 2.12"] +requires = ["setuptools >= 61.0", "pybind11 >= 2.12", "delvewheel >= 1.10"] build-backend = "setuptools.build_meta" [project] @@ -152,6 +152,11 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "src/piqtree/_app/__init__.py" = [ "N801", # apps follow function naming convention ] +"src/piqtree/__init__.py" = [ + "E402", # handle DLLs before imports + "PTH118", # os operations for DLL path + "PTH120", # os operations for DLL path +] "src/piqtree/model/_substitution_model.py" = ["N815"] # use IQ-TREE naming scheme [tool.ruff.format] diff --git a/setup.py b/setup.py index fcd22a34..62a6b4c4 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ from pybind11.setup_helpers import Pybind11Extension, build_ext from setuptools import setup -LIBRARY_DIR = "src/piqtree/_libiqtree" +LIBRARY_DIR = "src/piqtree/_libiqtree/" +IQTREE_LIB_NAME = "iqtree2" def get_brew_prefix(package: str) -> Path: @@ -18,7 +19,36 @@ def get_brew_prefix(package: str) -> Path: ) -if platform.system() == "Darwin": +extra_libs = [] +extra_compile_args = [] +include_dirs = [] +library_dirs = [] + + +def include_dlls() -> None: + import shutil + + from delvewheel._dll_utils import get_all_needed + + needed_dll_paths, _, _, _ = get_all_needed( + LIBRARY_DIR + f"{IQTREE_LIB_NAME}.dll", + set(), + None, + "raise", + False, # noqa: FBT003 + False, # noqa: FBT003 + 0, + ) + + for dll_path in needed_dll_paths: + shutil.copy(dll_path, LIBRARY_DIR) + + +def setup_windows() -> None: + include_dlls() + + +def setup_macos() -> None: brew_prefix_llvm = get_brew_prefix("llvm") brew_prefix_libomp = get_brew_prefix("libomp") @@ -27,20 +57,34 @@ def get_brew_prefix(package: str) -> Path: os.environ["CXX"] = str(brew_prefix_llvm / "bin" / "clang++") # Define OpenMP flags and libraries for macOS - openmp_flags = ["-Xpreprocessor", "-fopenmp"] - openmp_libs = ["omp"] + extra_compile_args.extend(["-Xpreprocessor", "-fopenmp"]) + extra_libs.extend(["z", "omp"]) # Use the paths from Homebrew for libomp - openmp_include = str(brew_prefix_libomp / "include") - library_dirs = [ - str(brew_prefix_libomp / "lib"), - str(brew_prefix_llvm / "lib"), - ] -else: - openmp_flags = ["-fopenmp"] - openmp_libs = ["gomp"] - openmp_include = None - library_dirs = [] + include_dirs.extend([str(brew_prefix_libomp / "include")]) + library_dirs.extend( + [ + str(brew_prefix_libomp / "lib"), + str(brew_prefix_llvm / "lib"), + ], + ) + + +def setup_linux() -> None: + extra_compile_args.extend(["-fopenmp"]) + extra_libs.extend(["z", "gomp"]) + + +match system := platform.system(): + case "Windows": + setup_windows() + case "Darwin": + setup_macos() + case "Linux": + setup_linux() + case _: + msg = f"Unsupported platform: {system}" + raise ValueError(msg) ext_modules = [ Pybind11Extension( @@ -50,14 +94,16 @@ def get_brew_prefix(package: str) -> Path: *library_dirs, LIBRARY_DIR, ], - libraries=["iqtree2", "z", *openmp_libs], - extra_compile_args=openmp_flags, - include_dirs=[openmp_include] if openmp_include else [], + libraries=[IQTREE_LIB_NAME, *extra_libs], + extra_compile_args=extra_compile_args, + include_dirs=include_dirs, ), ] setup( + name="piqtree", ext_modules=ext_modules, cmdclass={"build_ext": build_ext}, zip_safe=False, + package_data={"piqtree": ["_libiqtree/*.dll"]}, ) diff --git a/src/piqtree/__init__.py b/src/piqtree/__init__.py index 5461c950..5a91c957 100644 --- a/src/piqtree/__init__.py +++ b/src/piqtree/__init__.py @@ -1,5 +1,18 @@ """piqtree - access the power of IQ-TREE within Python.""" + +def _add_dll_path() -> None: + import os + + if "add_dll_directory" in dir(os): + dll_folder = os.path.join(os.path.dirname(__file__), "_libiqtree") + os.add_dll_directory(dll_folder) # type: ignore[attr-defined] + + +_add_dll_path() +del _add_dll_path + + from _piqtree import __iqtree_version__ from piqtree._data import dataset_names, download_dataset diff --git a/src/piqtree/_libiqtree/_piqtree.cpp b/src/piqtree/_libiqtree/_piqtree.cpp index 52210fee..5101edd1 100644 --- a/src/piqtree/_libiqtree/_piqtree.cpp +++ b/src/piqtree/_libiqtree/_piqtree.cpp @@ -1,3 +1,5 @@ +#include "_piqtree.h" +#include #include #include #include @@ -8,82 +10,84 @@ using namespace std; namespace py = pybind11; -/* - * Calculates the robinson fould distance between two trees - */ -extern int robinson_fould(const string& tree1, const string& tree2); - -/* - * Generates a set of random phylogenetic trees - * tree_gen_mode allows:"YULE_HARDING", "UNIFORM", "CATERPILLAR", "BALANCED", - * "BIRTH_DEATH", "STAR_TREE" output: a newick tree (in string format) - */ -extern string random_tree(int num_taxa, - string tree_gen_mode, - int num_trees, - int rand_seed = 0); - -/* - * Perform phylogenetic analysis on the input alignment - * With estimation of the best topology - * output: results in YAML format with the tree and the details of parameters - */ -extern string build_tree(vector& names, - vector& seqs, - string model, - int rand_seed = 0, - int bootstrap_rep = 0, - int num_thres = 1); - -/* - * Perform phylogenetic analysis on the input alignment - * With restriction to the input toplogy - * output: results in YAML format with the details of parameters - */ -extern string fit_tree(vector& names, - vector& seqs, - string model, - string intree, - int rand_seed = 0, - int num_thres = 1); - -/* - * Perform phylogenetic analysis with ModelFinder - * on the input alignment (in string format) - * model_set -- a set of models to consider - * freq_set -- a set of frequency types - * rate_set -- a set of RHAS models - * rand_seed -- random seed, if 0, then will generate a new random seed - * output: modelfinder results in YAML format - */ -extern string modelfinder(vector& names, - vector& seqs, - int rand_seed = 0, - string model_set = "", - string freq_set = "", - string rate_set = "", - int num_thres = 1); - -/* - * Build pairwise JC distance matrix - * output: set of distances - * (n * i + j)-th element of the list represents the distance between i-th and - * j-th sequence, where n is the number of sequences - */ -extern vector build_distmatrix(vector& names, - vector& seqs, - int num_thres); - -/* - * Using Rapid-NJ to build tree from a distance matrix - * output: a newick tree (in string format) - */ -extern string build_njtree(vector& names, vector& distances); - -/* - * verion number - */ -extern string version(); +namespace PYBIND11_NAMESPACE { +namespace detail { +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(StringArray, const_name("StringArray")); + + // Conversion from Python to C++ + bool load(handle src, bool) { + /* Extract PyObject from handle */ + PyObject* source = src.ptr(); + if (!py::isinstance(source)) { + return false; + } + + auto seq = reinterpret_borrow(src); + value.length = seq.size(); + + tmpStrings.reserve(value.length); + tmpCStrs.reserve(value.length); + + for (size_t i = 0; i < seq.size(); ++i) { + auto item = seq[i]; + if (!py::isinstance(item)) { + return false; + } + + tmpStrings.push_back(item.cast()); + tmpCStrs.push_back(tmpStrings[i].c_str()); + } + + value.strings = tmpCStrs.data(); + + return true; + } + + // Conversion from C++ to Python + static handle cast(StringArray src, return_value_policy, handle) { + throw std::runtime_error("Unsupported operation"); + } + + private: + vector tmpStrings; + vector tmpCStrs; +}; + +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(DoubleArray, _("DoubleArray")); + + // Conversion from Python to C++ + bool load(handle src, bool) { + if (!py::isinstance>(src)) { + return false; // Only accept numpy arrays of float64 + } + + auto arr = py::cast>(src); + if (arr.ndim() != 1) { + return false; // Only accept 1D arrays + } + + value.length = arr.size(); + value.doubles = new double[value.length]; + std::memcpy(value.doubles, arr.data(), value.length * sizeof(double)); + return true; + } + + // Conversion from C++ to Python + static handle cast(DoubleArray src, return_value_policy, handle) { + auto result = py::array_t(src.length); + std::memcpy(result.mutable_data(), src.doubles, + src.length * sizeof(double)); + return result.release(); + } +}; +} // namespace detail +} // namespace PYBIND11_NAMESPACE int mine() { return 42; diff --git a/src/piqtree/_libiqtree/_piqtree.h b/src/piqtree/_libiqtree/_piqtree.h new file mode 100644 index 00000000..070c621b --- /dev/null +++ b/src/piqtree/_libiqtree/_piqtree.h @@ -0,0 +1,115 @@ +#ifndef _PIQTREE_H +#define _PIQTREE_H + +#include + +using namespace std; + +#ifdef _MSC_VER +#pragma pack(push, 1) +#else +#pragma pack(1) +#endif + +typedef struct { + const char** strings; + size_t length; +} StringArray; + +typedef struct { + double* doubles; + size_t length; +} DoubleArray; + +#ifdef _MSC_VER +#pragma pack(pop) +#else +#pragma pack() +#endif + +/* + * Calculates the robinson fould distance between two trees + */ +extern "C" int robinson_fould(const char* ctree1, const char* ctree2); + +/* + * Generates a set of random phylogenetic trees + * tree_gen_mode allows:"YULE_HARDING", "UNIFORM", "CATERPILLAR", "BALANCED", + * "BIRTH_DEATH", "STAR_TREE" output: a newick tree (in string format) + */ +extern "C" char* random_tree(int num_taxa, + const char* tree_gen_mode, + int num_trees, + int rand_seed = 0); + +/* + * Perform phylogenetic analysis on the input alignment + * With estimation of the best topology + * num_thres -- number of cpu threads to be used, default: 1; 0 - auto detection + * of the optimal number of cpu threads output: results in YAML format with the + * tree and the details of parameters + */ +extern "C" char* build_tree(StringArray& names, + StringArray& seqs, + const char* model, + int rand_seed = 0, + int bootstrap_rep = 0, + int num_thres = 1); + +/* + * Perform phylogenetic analysis on the input alignment + * With restriction to the input toplogy + * num_thres -- number of cpu threads to be used, default: 1; 0 - auto detection + * of the optimal number of cpu threads output: results in YAML format with the + * details of parameters + */ +extern "C" char* fit_tree(StringArray& names, + StringArray& seqs, + const char* model, + const char* intree, + int rand_seed = 0, + int num_thres = 1); + +/* + * Perform phylogenetic analysis with ModelFinder + * on the input alignment (in string format) + * model_set -- a set of models to consider + * freq_set -- a set of frequency types + * rate_set -- a set of RHAS models + * rand_seed -- random seed, if 0, then will generate a new random seed + * num_thres -- number of cpu threads to be used, default: 1; 0 - auto detection + * of the optimal number of cpu threads output: modelfinder results in YAML + * format + */ +extern "C" char* modelfinder(StringArray& names, + StringArray& seqs, + int rand_seed = 0, + const char* model_set = "", + const char* freq_set = "", + const char* rate_set = "", + int num_thres = 1); + +/* + * Build pairwise JC distance matrix + * output: set of distances + * (n * i + j)-th element of the list represents the distance between i-th and + * j-th sequence, where n is the number of sequences num_thres -- number of cpu + * threads to be used, default: 1; 0 - use all available cpu threads on the + * machine + */ +extern "C" DoubleArray build_distmatrix(StringArray& names, + StringArray& seqs, + int num_thres = 1); + +/* + * Using Rapid-NJ to build tree from a distance matrix + * output: a newick tree (in string format) + */ +extern "C" char* build_njtree(StringArray& names, DoubleArray& distances); + +/* + * verion number + */ +extern "C" char* version(); + +#endif /* LIBIQTREE2_FUN */ diff --git a/src/piqtree/iqtree/_decorator.py b/src/piqtree/iqtree/_decorator.py index d7f4fd61..4d59e101 100644 --- a/src/piqtree/iqtree/_decorator.py +++ b/src/piqtree/iqtree/_decorator.py @@ -84,7 +84,7 @@ def wrapper_iqtree_func(*args: Param.args, **kwargs: Param.kwargs) -> RetType: os.close(devnull_fd) if hide_files: - tempdir.cleanup() os.chdir(original_dir) + tempdir.cleanup() return wrapper_iqtree_func diff --git a/tests/test_iqtree/test_build_tree.py b/tests/test_iqtree/test_build_tree.py index 90cbf8ab..d8232d15 100644 --- a/tests/test_iqtree/test_build_tree.py +++ b/tests/test_iqtree/test_build_tree.py @@ -1,3 +1,4 @@ +import platform import re import pytest @@ -87,6 +88,10 @@ def test_rate_model_build_tree( ) +@pytest.mark.skipif( + platform.system() == "Windows", + reason="IQ-TREE errors can't be caught yet on Windows", +) def test_build_tree_inadequate_bootstrapping(four_otu: ArrayAlignment) -> None: with pytest.raises(IqTreeError, match=re.escape("#replicates must be >= 1000")): piqtree.build_tree(four_otu, Model(DnaModel.GTR), bootstrap_replicates=10) diff --git a/tests/test_iqtree/test_random_trees.py b/tests/test_iqtree/test_random_trees.py index 60c09ce1..82ce8f4a 100644 --- a/tests/test_iqtree/test_random_trees.py +++ b/tests/test_iqtree/test_random_trees.py @@ -1,3 +1,5 @@ +import platform + import pytest import piqtree @@ -45,6 +47,10 @@ def test_random_trees_no_seed( @pytest.mark.parametrize("num_taxa", [-1, 0, 1, 2]) @pytest.mark.parametrize("tree_mode", list(piqtree.TreeGenMode)) +@pytest.mark.skipif( + platform.system() == "Windows", + reason="IQ-TREE errors can't be caught yet on Windows", +) def test_invalid_taxa( num_taxa: int, tree_mode: piqtree.TreeGenMode, diff --git a/tests/test_iqtree/test_segmentation_fault.py b/tests/test_iqtree/test_segmentation_fault.py index 701cc406..1fb58575 100644 --- a/tests/test_iqtree/test_segmentation_fault.py +++ b/tests/test_iqtree/test_segmentation_fault.py @@ -1,5 +1,7 @@ """Test combinations of calls which under previous versions resulted in a segmentation fault.""" +import platform + import pytest from cogent3 import make_aligned_seqs, make_tree @@ -8,6 +10,10 @@ from piqtree.model import DiscreteGammaModel, DnaModel, FreeRateModel, Model +@pytest.mark.skipif( + platform.system() == "Windows", + reason="IQ-TREE errors can't be caught yet on Windows", +) def test_two_build_random_trees() -> None: """ Calling build tree twice followed by random trees with a bad input @@ -22,6 +28,10 @@ def test_two_build_random_trees() -> None: random_trees(3, 2, TreeGenMode.BALANCED, 1) +@pytest.mark.skipif( + platform.system() == "Windows", + reason="IQ-TREE errors can't be caught yet on Windows", +) def test_two_fit_random_trees() -> None: """ Calling fit tree twice followed by random trees with a bad input @@ -39,6 +49,10 @@ def test_two_fit_random_trees() -> None: @pytest.mark.parametrize("rate_model_class", [DiscreteGammaModel, FreeRateModel]) @pytest.mark.parametrize("categories", [0, -4]) +@pytest.mark.skipif( + platform.system() == "Windows", + reason="IQ-TREE errors can't be caught yet on Windows", +) def test_two_invalid_models( rate_model_class: type[DiscreteGammaModel] | type[FreeRateModel], categories: int,