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 c73e17ec..ebf7a97a 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.3 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 5389ee1e..4fdb7fcc 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.11", "3.12", "3.13"] 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 973a16ab..fc4ff196 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.3 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 507b2726..cdc95266 100755 --- a/build_tools/build_iqtree.sh +++ b/build_tools/build_iqtree.sh @@ -9,6 +9,26 @@ 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." cmake -DBUILD_LIB=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. @@ -16,4 +36,10 @@ else 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/changelog.d/20250521_122741_robert.mcarthur_windows_support.md b/changelog.d/20250521_122741_robert.mcarthur_windows_support.md new file mode 100644 index 00000000..1e6354c9 --- /dev/null +++ b/changelog.d/20250521_122741_robert.mcarthur_windows_support.md @@ -0,0 +1,42 @@ + + + +### Contributors + +- @rmcar17 and @thomaskf enabled windows support for piqtree! + + + +### ENH + +- Added windows support! + + + + + + diff --git a/iqtree2 b/iqtree2 index 1320c4cc..3ad863ad 160000 --- a/iqtree2 +++ b/iqtree2 @@ -1 +1 @@ -Subproject commit 1320c4cc360bbacacc592911459aaa06cd4a7961 +Subproject commit 3ad863ad7e26d46aa549327e6c3003b75d288ff0 diff --git a/pyproject.toml b/pyproject.toml index 1b0788cc..4cd09b6e 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] @@ -7,8 +7,14 @@ name = "piqtree" dependencies = ["cogent3>=2025.5.8a2", "pyyaml", "requests"] requires-python = ">=3.11, <3.14" -authors = [{name="Gavin Huttley"}, {name="Robert McArthur"}, {name="Bui Quang Minh "}, {name="Richard Morris"}, {name="Thomas Wong"}] -description="Python bindings for IQTree" +authors = [ + { name = "Gavin Huttley" }, + { name = "Robert McArthur" }, + { name = "Bui Quang Minh " }, + { name = "Richard Morris" }, + { name = "Thomas Wong" }, +] +description = "Python bindings for IQTree" readme = "README.md" dynamic = ["version"] @@ -32,7 +38,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Typing :: Typed" + "Typing :: Typed", ] [project.urls] @@ -41,7 +47,15 @@ Documentation = "https://piqtree.readthedocs.io" [project.optional-dependencies] -dev = ["cibuildwheel", "pybind11", "scriv", "piqtree[test]", "piqtree[lint]", "piqtree[typing]"] +dev = [ + "cibuildwheel", + "pybind11", + "delvewheel", + "scriv", + "piqtree[test]", + "piqtree[lint]", + "piqtree[typing]", +] test = ["pytest", "pytest-cov", "nox"] lint = ["ruff==0.11.10"] typing = ["mypy==1.15.0", "piqtree[stubs]", "piqtree[test]"] @@ -57,8 +71,8 @@ doc = [ "cogent3[extra]", "diverse-seq", "jupyter", - "ipywidgets" - ] + "ipywidgets", +] [project.entry-points."cogent3.app"] piqtree_phylo = "piqtree._app:piqtree_phylo" @@ -72,7 +86,7 @@ piqtree_mfinder = "piqtree._app:piqtree_mfinder" quick_tree = "piqtree._app:piqtree_nj" [tool.setuptools.dynamic] -version = {attr = "piqtree.__version__"} +version = { attr = "piqtree.__version__" } [tool.ruff] exclude = [ @@ -127,32 +141,34 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = [ - "S101", # asserts allowed in tests... + "S101", # asserts allowed in tests... "INP001", # __init__.py files are not required... - "N802", # allow non snake_case function names for fixtures - "N803", # allow use of fixture constants + "N802", # allow non snake_case function names for fixtures + "N803", # allow use of fixture constants "SLF001", # private member access is useful for testing "FBT001", # allow bool pos args for parameterisation - "D", # don't require docstrings -] -"docs/**/*.py" = [ - "B018", - "E402", - "ERA001", - "INP001" + "D", # don't require docstrings ] +"docs/**/*.py" = ["B018", "E402", "ERA001", "INP001"] "noxfile.py" = [ - "S101", # asserts allowed in tests... + "S101", # asserts allowed in tests... "INP001", # __init__.py files are not required... "ANN", "N802", "N803", - "D" + "D", ] "src/piqtree/_app/__init__.py" = [ "N801", # apps follow function naming convention ] -"src/piqtree/model/_substitution_model.py" = ["N815"] # use IQ-TREE naming scheme +"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] # Like Black, use double quotes for strings. @@ -182,14 +198,21 @@ docstring-code-format = false docstring-code-line-length = "dynamic" [tool.scriv] -format="md" -categories=["Contributors", "ENH", "BUG", "DOC", "Deprecations", "Discontinued"] -output_file="changelog.md" +format = "md" +categories = [ + "Contributors", + "ENH", + "BUG", + "DOC", + "Deprecations", + "Discontinued", +] +output_file = "changelog.md" version = "literal: src/piqtree/__init__.py: __version__" -skip_fragments="README.*" -new_fragment_template="file: changelog.d/templates/new.md.j2" -entry_title_template="file: changelog.d/templates/title.md.j2" +skip_fragments = "README.*" +new_fragment_template = "file: changelog.d/templates/new.md.j2" +entry_title_template = "file: changelog.d/templates/title.md.j2" [[tool.mypy.overrides]] module = ['cogent3.*', "_piqtree"] -ignore_missing_imports = true \ No newline at end of file +ignore_missing_imports = true 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 028c2b51..5d7abd5e 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..218648ee 100644 --- a/src/piqtree/_libiqtree/_piqtree.cpp +++ b/src/piqtree/_libiqtree/_piqtree.cpp @@ -1,89 +1,170 @@ +#include "_piqtree.h" +#include #include #include #include #include #include - -using namespace std; +#include "_piqtree.h" 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(); +void checkError(char* errorStr) { + if (errorStr && std::strlen(errorStr) > 0) { + string msg(errorStr); + free(errorStr); + throw std::runtime_error(msg); + } + if (errorStr) + free(errorStr); +} + +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(); + + tmpDoubles.assign(arr.data(), arr.data() + value.length); + value.doubles = tmpDoubles.data(); + + return true; + } + + // Conversion from C++ to Python + static handle cast(DoubleArray src, return_value_policy, handle) { + throw std::runtime_error("Unsupported operation"); + } + + private: + vector tmpDoubles; +}; + +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(IntegerResult, _("IntegerResult")); + + // Conversion from Python to C++ + bool load(handle /* src */, bool /* convert */) { + throw std::runtime_error("Unsupported operation"); + } + + // Conversion from C++ to Python + static handle cast(const IntegerResult& src, return_value_policy, handle) { + checkError(src.errorStr); + + return py::int_(src.value).release(); + } +}; + +template <> +struct type_caster { + public: + // Indicate that this caster only supports conversion from C++ to Python + PYBIND11_TYPE_CASTER(StringResult, _("StringResult")); + + // Reject Python to C++ conversion + bool load(handle src, bool) { + throw std::runtime_error("Unsupported operation"); + } + + // Conversion from C++ to Python + static handle cast(const StringResult& src, return_value_policy, handle) { + checkError(src.errorStr); + + PyObject* py_str = PyUnicode_FromString(src.value); + if (!py_str) + throw error_already_set(); + + free(src.value); + + return handle(py_str); + } +}; + +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(DoubleArrayResult, _("DoubleArrayResult")); + + // Conversion from Python to C++ + bool load(handle src, bool) { + throw std::runtime_error("Unsupported operation"); + } + + // Conversion from C++ to Python + static handle cast(DoubleArrayResult src, return_value_policy, handle) { + checkError(src.errorStr); + + auto result = py::array_t(src.length); + + std::memcpy(result.mutable_data(), src.value, src.length * sizeof(double)); + free(src.value); + + 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..5e377249 --- /dev/null +++ b/src/piqtree/_libiqtree/_piqtree.h @@ -0,0 +1,133 @@ +#ifndef _PIQTREE_H +#define _PIQTREE_H + +#include +#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; + +typedef struct { + int value; + char* errorStr; +} IntegerResult; + +typedef struct { + char* value; + char* errorStr; +} StringResult; + +typedef struct { + double* value; + size_t length; + char* errorStr; +} DoubleArrayResult; + +#ifdef _MSC_VER +#pragma pack(pop) +#else +#pragma pack() +#endif + +/* + * Calculates the robinson fould distance between two trees + */ +extern "C" IntegerResult 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" StringResult 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" StringResult 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" StringResult 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" StringResult 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" DoubleArrayResult 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" StringResult build_njtree(StringArray& names, + DoubleArray& distances); + +/* + * verion number + */ +extern "C" StringResult 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