From 794427698c4852ce40862ac342b2bce6b35fc9b7 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:05:38 -0700 Subject: [PATCH 01/68] Update FTetWildWrapper.cpp --- src/FTetWildWrapper.cpp | 63 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index a4fb9cb..c97ca2a 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -1 +1,62 @@ -// placeholder for FTetWildWrapper.cpp +#include +#include +#include +#include + +// Function to generate tetrahedral mesh using fTetWild +// vertices: Input array of surface vertices, each vertex is represented by 3 consecutive doubles (x, y, z) +// numVertices: Number of vertices in the input surface mesh +// faces: Input array of surface triangle faces, each face is represented by 3 consecutive integers (indices to vertices) +// numFaces: Number of faces in the input surface mesh +// tetPoints: Output array of tetrahedral mesh vertices, each vertex is represented by 3 consecutive doubles (x, y, z) +// numTetPoints: Output number of vertices in the tetrahedral mesh +// tetCells: Output array of tetrahedral mesh cells, each cell is represented by 4 consecutive integers (indices to tetPoints) +// numTetCells: Output number of cells in the tetrahedral mesh +void generateTetMesh(const double* vertices, int numVertices, const int* faces, int numFaces, double** tetPoints, int* numTetPoints, int** tetCells, int* numTetCells) { + // Convert input arrays to Eigen matrices for fTetWild + Eigen::MatrixXd V(numVertices, 3); + Eigen::MatrixXi F(numFaces, 3); + for(int i = 0; i < numVertices; ++i) { + V(i, 0) = vertices[i * 3]; + V(i, 1) = vertices[i * 3 + 1]; + V(i, 2) = vertices[i * 3 + 2]; + } + for(int i = 0; i < numFaces; ++i) { + F(i, 0) = faces[i * 3]; + F(i, 1) = faces[i * 3 + 1]; + F(i, 2) = faces[i * 3 + 2]; + } + + // Parameters for tetrahedralization + floatTetWild::Parameters params; + // Set any required parameters here + // For example, params.ideal_edge_length = 1.0; + + // Mesh object to store the output tetrahedral mesh + floatTetWild::Mesh mesh; + + // Generate the tetrahedral mesh + floatTetWild::tetrahedralize(V, F, mesh, params); + + // Extract tetrahedral mesh vertices and cells + auto& vertices_out = mesh.get_vertices(); + auto& tets_out = mesh.get_tets(); + + // Assuming memory for tetPoints and tetCells is allocated externally + *numTetPoints = vertices_out.size(); + *numTetCells = tets_out.size(); + + for(int i = 0; i < vertices_out.size(); ++i) { + (*tetPoints)[i * 3] = vertices_out[i][0]; + (*tetPoints)[i * 3 + 1] = vertices_out[i][1]; + (*tetPoints)[i * 3 + 2] = vertices_out[i][2]; + } + + for(int i = 0; i < tets_out.size(); ++i) { + if(tets_out[i].is_removed) continue; // Skip removed tets + (*tetCells)[i * 4] = tets_out[i][0]; + (*tetCells)[i * 4 + 1] = tets_out[i][1]; + (*tetCells)[i * 4 + 2] = tets_out[i][2]; + (*tetCells)[i * 4 + 3] = tets_out[i][3]; + } +} From 734c0021cd7f49e86a53eae83e60474f7e254221 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:26:06 -0700 Subject: [PATCH 02/68] Add workflow --- .../.github/workflows/build-and-deploy.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/.github/workflows/build-and-deploy.yml diff --git a/.github/workflows/.github/workflows/build-and-deploy.yml b/.github/workflows/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..28aca88 --- /dev/null +++ b/.github/workflows/.github/workflows/build-and-deploy.yml @@ -0,0 +1,40 @@ +name: Build and upload + +on: + pull_request: + push: + tags: + - "*" + branches: + - "main" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Build + run: pipx run build --sdist + + - name: Validate + run: | + pip install twine + twine check dist/* + + - name: Install and test + run: | + pip install --find-links=dist pytetwild[dev] + pytest -x + + - uses: actions/upload-artifact@v4 + with: + path: dist/*.tar.gz + name: pytetwild-sdist From 5b7a457d44e047fcf4f39267d8c968de5bfd0699 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:40:24 -0700 Subject: [PATCH 03/68] Update setup.py --- setup.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3392c47..513b4f4 100644 --- a/setup.py +++ b/setup.py @@ -1 +1,80 @@ -# placeholder for setup.py +"""Setup for pyminiply.""" +from io import open as io_open +import os +import sys + +from Cython.Build import cythonize +import numpy as np +from setuptools import Extension, setup + +filepath = os.path.dirname(__file__) + +# Define macros for cython +macros = [] +if os.name == "nt": # windows + extra_compile_args = ["/O2", "/w", "/GS"] +elif os.name == "posix": # linux org mac os + if sys.platform == "linux": + extra_compile_args = ["-std=gnu++11", "-O3", "-w"] + else: # probably mac os + extra_compile_args = ["-std=c++11", "-O3", "-w"] +else: + raise Exception(f"Unsupported OS {os.name}") + + +# Get version from version info +__version__ = None +version_file = os.path.join(filepath, "pyminiply", "_version.py") +with io_open(version_file, mode="r") as fd: + exec(fd.read()) + +# readme file +readme_file = os.path.join(filepath, "README.rst") + + +setup( + name="pytetwild", + packages=["pytetwild"], + version=__version__, + description="Tetrahedralize surfaces using fTetWild", + long_description=open(readme_file).read(), + long_description_content_type="text/x-rst", + author="PyVista Developers", + author_email="info@pyvista.org", + license="MPLv2", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.8", + url="https://github.com/pyvista/pytetwild", + # Build cython modules + ext_modules=cythonize( + [ + Extension( + "pytetwild._wrapper", + [ + "pytetwild/src/FTetWildWrapper.cpp", + "pytetwild/_wrapper.pyx", + "pytetwild/fTetWild/src.cpp", + ], + language="c++", + extra_compile_args=extra_compile_args, + define_macros=macros, + include_dirs=[np.get_include()], + ) + ] + ), + package_data={ + "pytetwild": ["*.pyx", "*.hpp"], + "pytetwild/wrapper": ["*.c", "*.h"], + }, + keywords="tetrahedralize tetwild ftetwild", + install_requires=["numpy>1.11.0"], +) From cc0c5faea0c27d803cb205ec6a1db17cc40381fd Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:40:53 -0700 Subject: [PATCH 04/68] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 513b4f4..c1c658c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -"""Setup for pyminiply.""" +"""Setup for pytetwild.""" from io import open as io_open import os import sys From b5722b2587a4759e978f6984f2e857707a7168fa Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:42:22 -0700 Subject: [PATCH 05/68] Create _version.py --- _version.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 _version.py diff --git a/_version.py b/_version.py new file mode 100644 index 0000000..355c0d8 --- /dev/null +++ b/_version.py @@ -0,0 +1,10 @@ +"""pytetwild version. + +On the ``main`` branch, use 'dev0' to denote a development version. +For example: + +version_info = 0, 27, 'dev0' + +""" +version_info = 0, 1, 'dev0' +__version__ = ".".join(map(str, version_info)) From 28b5c167b6d961b6770c480096e585069bf311ea Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:43:10 -0700 Subject: [PATCH 06/68] Create _version.py --- src/_version.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/_version.py diff --git a/src/_version.py b/src/_version.py new file mode 100644 index 0000000..355c0d8 --- /dev/null +++ b/src/_version.py @@ -0,0 +1,10 @@ +"""pytetwild version. + +On the ``main`` branch, use 'dev0' to denote a development version. +For example: + +version_info = 0, 27, 'dev0' + +""" +version_info = 0, 1, 'dev0' +__version__ = ".".join(map(str, version_info)) From 3ab06b5af76c5c34e8ea2bde0b9a5ca01e6932ba Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:43:46 -0700 Subject: [PATCH 07/68] Update README.rst --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index c3d0abe..92c6f5c 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,6 @@ pytetwild ######### -**This repository is not ready and the content here is a placeholder.** - |pypi| |MPL| .. |pypi| image:: https://img.shields.io/pypi/v/pytetwild.svg?logo=python&logoColor=white From b4d14a3c8a5bd807bc45c1d77074c4c54e694a2d Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:53:59 -0700 Subject: [PATCH 08/68] Update build-and-deploy.yml --- .../.github/workflows/build-and-deploy.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/.github/workflows/build-and-deploy.yml b/.github/workflows/.github/workflows/build-and-deploy.yml index 28aca88..a7d3613 100644 --- a/.github/workflows/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/.github/workflows/build-and-deploy.yml @@ -29,6 +29,19 @@ jobs: pip install twine twine check dist/* + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install \ + libblas-dev \ + libboost-filesystem-dev \ + libboost-system-dev \ + libboost-thread-dev \ + libglu1-mesa-dev \ + libsuitesparse-dev \ + xorg-dev \ + ccache + - name: Install and test run: | pip install --find-links=dist pytetwild[dev] From 053b78acd226491a0d567021b83563988ea87be8 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:54:48 -0700 Subject: [PATCH 09/68] Create build-and-deploy.yml --- .github/workflows/build-and-deploy.yml | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/build-and-deploy.yml diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..a7d3613 --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,53 @@ +name: Build and upload + +on: + pull_request: + push: + tags: + - "*" + branches: + - "main" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Build + run: pipx run build --sdist + + - name: Validate + run: | + pip install twine + twine check dist/* + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install \ + libblas-dev \ + libboost-filesystem-dev \ + libboost-system-dev \ + libboost-thread-dev \ + libglu1-mesa-dev \ + libsuitesparse-dev \ + xorg-dev \ + ccache + + - name: Install and test + run: | + pip install --find-links=dist pytetwild[dev] + pytest -x + + - uses: actions/upload-artifact@v4 + with: + path: dist/*.tar.gz + name: pytetwild-sdist From e11c5b66d307b739998036daff1dde81e3284f48 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:55:16 -0700 Subject: [PATCH 10/68] Delete .github/workflows/.github/workflows/build-and-deploy.yml --- .../.github/workflows/build-and-deploy.yml | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 .github/workflows/.github/workflows/build-and-deploy.yml diff --git a/.github/workflows/.github/workflows/build-and-deploy.yml b/.github/workflows/.github/workflows/build-and-deploy.yml deleted file mode 100644 index a7d3613..0000000 --- a/.github/workflows/.github/workflows/build-and-deploy.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Build and upload - -on: - pull_request: - push: - tags: - - "*" - branches: - - "main" - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - name: Build - run: pipx run build --sdist - - - name: Validate - run: | - pip install twine - twine check dist/* - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install \ - libblas-dev \ - libboost-filesystem-dev \ - libboost-system-dev \ - libboost-thread-dev \ - libglu1-mesa-dev \ - libsuitesparse-dev \ - xorg-dev \ - ccache - - - name: Install and test - run: | - pip install --find-links=dist pytetwild[dev] - pytest -x - - - uses: actions/upload-artifact@v4 - with: - path: dist/*.tar.gz - name: pytetwild-sdist From 48d4feef53abaa88aa194ea12fca709a9aefd4c3 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 10:55:27 -0700 Subject: [PATCH 11/68] Delete _version.py --- _version.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 _version.py diff --git a/_version.py b/_version.py deleted file mode 100644 index 355c0d8..0000000 --- a/_version.py +++ /dev/null @@ -1,10 +0,0 @@ -"""pytetwild version. - -On the ``main`` branch, use 'dev0' to denote a development version. -For example: - -version_info = 0, 27, 'dev0' - -""" -version_info = 0, 1, 'dev0' -__version__ = ".".join(map(str, version_info)) From b1a559a72451434b3ad3a935d597305d4dfee836 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Wed, 28 Feb 2024 21:35:52 -0700 Subject: [PATCH 12/68] use cmake; fix wrapper --- src/CMakeLists.txt | 20 +++++++++ src/FTetWildWrapper.cpp | 95 ++++++++++++++++++++--------------------- 2 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 src/CMakeLists.txt diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..66c9c30 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.8) +project(fTetWildWrapper) + +# Set the path to the fTetWild project +set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/fTetWild") + +# Include the fTetWild project as a subdirectory +add_subdirectory(${fTetWild_DIR}) + +# Define your shared library that wraps the fTetWild code +add_library(PyfTetWildWrapper SHARED FTetWildWrapper.cpp) + +# Include directories from fTetWild required for the wrapper +target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) + +# Link the FloatTetwild library (from fTetWild) to your wrapper library +target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) + +# Specify any additional compile features or definitions as needed +target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index c97ca2a..aab50ad 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -1,62 +1,59 @@ #include +#include +#include #include -#include + #include +#include -// Function to generate tetrahedral mesh using fTetWild -// vertices: Input array of surface vertices, each vertex is represented by 3 consecutive doubles (x, y, z) -// numVertices: Number of vertices in the input surface mesh -// faces: Input array of surface triangle faces, each face is represented by 3 consecutive integers (indices to vertices) -// numFaces: Number of faces in the input surface mesh -// tetPoints: Output array of tetrahedral mesh vertices, each vertex is represented by 3 consecutive doubles (x, y, z) -// numTetPoints: Output number of vertices in the tetrahedral mesh -// tetCells: Output array of tetrahedral mesh cells, each cell is represented by 4 consecutive integers (indices to tetPoints) -// numTetCells: Output number of cells in the tetrahedral mesh -void generateTetMesh(const double* vertices, int numVertices, const int* faces, int numFaces, double** tetPoints, int* numTetPoints, int** tetCells, int* numTetCells) { - // Convert input arrays to Eigen matrices for fTetWild - Eigen::MatrixXd V(numVertices, 3); - Eigen::MatrixXi F(numFaces, 3); - for(int i = 0; i < numVertices; ++i) { - V(i, 0) = vertices[i * 3]; - V(i, 1) = vertices[i * 3 + 1]; - V(i, 2) = vertices[i * 3 + 2]; - } - for(int i = 0; i < numFaces; ++i) { - F(i, 0) = faces[i * 3]; - F(i, 1) = faces[i * 3 + 1]; - F(i, 2) = faces[i * 3 + 2]; +// You must include other necessary headers and namespaces used in your project + +void tetrahedralizeAndWriteMesh(const std::vector& vertices, + const std::vector& faces, + const std::string& outputPath) { + using namespace floatTetWild; + using namespace Eigen; + + // Convert input vertices and faces to the format expected by FloatTetWild + std::vector input_vertices(vertices.size()); + for (size_t i = 0; i < vertices.size(); ++i) { + input_vertices[i] = vertices[i].cast(); } - // Parameters for tetrahedralization - floatTetWild::Parameters params; - // Set any required parameters here - // For example, params.ideal_edge_length = 1.0; + std::vector input_faces(faces.begin(), faces.end()); - // Mesh object to store the output tetrahedral mesh - floatTetWild::Mesh mesh; + // Initialize placeholders for flags and epsr_flags, if needed + std::vector flags; + std::vector epsr_flags; - // Generate the tetrahedral mesh - floatTetWild::tetrahedralize(V, F, mesh, params); + // Initialize GEO::Mesh and load the mesh data into it + GEO::Mesh geo_mesh; + MeshIO::load_mesh(input_vertices, input_faces, geo_mesh, flags, epsr_flags); - // Extract tetrahedral mesh vertices and cells - auto& vertices_out = mesh.get_vertices(); - auto& tets_out = mesh.get_tets(); + // Initialize AABBWrapper with the loaded GEO::Mesh for collision checking + AABBWrapper tree(geo_mesh); - // Assuming memory for tetPoints and tetCells is allocated externally - *numTetPoints = vertices_out.size(); - *numTetCells = tets_out.size(); + // Create an instance of Mesh to hold the output tetrahedral mesh + Mesh mesh; - for(int i = 0; i < vertices_out.size(); ++i) { - (*tetPoints)[i * 3] = vertices_out[i][0]; - (*tetPoints)[i * 3 + 1] = vertices_out[i][1]; - (*tetPoints)[i * 3 + 2] = vertices_out[i][2]; - } + // Prepare a vector to track the insertion status of faces + std::vector is_face_inserted(input_faces.size(), false); - for(int i = 0; i < tets_out.size(); ++i) { - if(tets_out[i].is_removed) continue; // Skip removed tets - (*tetCells)[i * 4] = tets_out[i][0]; - (*tetCells)[i * 4 + 1] = tets_out[i][1]; - (*tetCells)[i * 4 + 2] = tets_out[i][2]; - (*tetCells)[i * 4 + 3] = tets_out[i][3]; - } + // Perform tetrahedralization + FloatTetDelaunay::tetrahedralize(input_vertices, input_faces, tree, mesh, is_face_inserted); + + // Write the tetrahedralized mesh to a file + std::vector colors; // Optional: For visualizing quality or other metrics + MeshIO::write_mesh(outputPath, mesh, false, colors); +} + +// Example usage: +int main() { + std::vector vertices = {/* Fill with your vertices */}; + std::vector faces = {/* Fill with your faces */}; + std::string outputPath = "output_mesh.msh"; + + tetrahedralizeAndWriteMesh(vertices, faces, outputPath); + + return 0; } From 9497cca13f292061033ec021cd259aa488a04090 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 09:35:11 -0700 Subject: [PATCH 13/68] Update setup.py --- setup.py | 97 +++++++++++++++----------------------------------------- 1 file changed, 25 insertions(+), 72 deletions(-) diff --git a/setup.py b/setup.py index c1c658c..3fe98a5 100644 --- a/setup.py +++ b/setup.py @@ -1,80 +1,33 @@ """Setup for pytetwild.""" -from io import open as io_open -import os import sys -from Cython.Build import cythonize -import numpy as np -from setuptools import Extension, setup +from setuptools import setup, find_packages +from setuptools.command.test import test as TestCommand -filepath = os.path.dirname(__file__) - -# Define macros for cython -macros = [] -if os.name == "nt": # windows - extra_compile_args = ["/O2", "/w", "/GS"] -elif os.name == "posix": # linux org mac os - if sys.platform == "linux": - extra_compile_args = ["-std=gnu++11", "-O3", "-w"] - else: # probably mac os - extra_compile_args = ["-std=c++11", "-O3", "-w"] -else: - raise Exception(f"Unsupported OS {os.name}") - - -# Get version from version info -__version__ = None -version_file = os.path.join(filepath, "pyminiply", "_version.py") -with io_open(version_file, mode="r") as fd: - exec(fd.read()) - -# readme file -readme_file = os.path.join(filepath, "README.rst") +try: + from skbuild import setup +except ImportError: + print("Please update pip to pip 10 or greater, or a manually install the PEP 518 requirements in pyproject.toml", file=sys.stderr) + raise +cmake_args = [] +debug = False +cfg = 'Debug' if debug else 'Release' setup( - name="pytetwild", - packages=["pytetwild"], - version=__version__, - description="Tetrahedralize surfaces using fTetWild", - long_description=open(readme_file).read(), - long_description_content_type="text/x-rst", - author="PyVista Developers", - author_email="info@pyvista.org", - license="MPLv2", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - python_requires=">=3.8", - url="https://github.com/pyvista/pytetwild", - # Build cython modules - ext_modules=cythonize( - [ - Extension( - "pytetwild._wrapper", - [ - "pytetwild/src/FTetWildWrapper.cpp", - "pytetwild/_wrapper.pyx", - "pytetwild/fTetWild/src.cpp", - ], - language="c++", - extra_compile_args=extra_compile_args, - define_macros=macros, - include_dirs=[np.get_include()], - ) - ] - ), - package_data={ - "pytetwild": ["*.pyx", "*.hpp"], - "pytetwild/wrapper": ["*.c", "*.h"], - }, - keywords="tetrahedralize tetwild ftetwild", - install_requires=["numpy>1.11.0"], + name='pytetwild', + version='0.1.dev0', + author='Alex Kaszynski', + author_email='akascap@gmail.com', + description='Python wrapper of fTetWild', + long_description=open("README.rst").read(), + long_description_content_type="text/rst", + packages=find_packages('src'), + package_dir={'':'src'}, + zip_safe=False, + include_package_data=False, + cmake_args=cmake_args, + cmake_install_dir="src/", + cmake_install_target='install', + install_requires="numpy", ) From 37679f7ddca7cf6758bbd4119df122bebb4084c1 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 09:37:02 -0700 Subject: [PATCH 14/68] Create pyproject.toml --- pyproject.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01676cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "cmake>=3.18", + "scikit-build>=0.13", + "ninja>=1.10.0", +] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = 'tests' + +[tool.cibuildwheel] +archs = ["auto64"] # 64-bit only +build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels +skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels +test-requires = "pytest" +test-command = "pytest {project}/tests -v" From 3d571dfa7270bc92c079b04388af4a1918fe20f1 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:04:51 -0700 Subject: [PATCH 15/68] Update setup.py --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 3fe98a5..be9c6f2 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ """Setup for pytetwild.""" import sys -from setuptools import setup, find_packages -from setuptools.command.test import test as TestCommand +from setuptools import find_packages try: from skbuild import setup @@ -26,7 +25,7 @@ package_dir={'':'src'}, zip_safe=False, include_package_data=False, - cmake_args=cmake_args, + cmake_args=cmake_args=cmake_args + ['-DCMAKE_BUILD_TYPE=' + cfg], cmake_install_dir="src/", cmake_install_target='install', install_requires="numpy", From 76072e57328c50d223571362aee4b8f06967c962 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 17:12:35 +0000 Subject: [PATCH 16/68] fix setup.py --- setup.py | 2 +- src/_wrapper.pyx | 1 - src/pytetwild/__init__.py | 0 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/_wrapper.pyx create mode 100644 src/pytetwild/__init__.py diff --git a/setup.py b/setup.py index be9c6f2..3f9a536 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ package_dir={'':'src'}, zip_safe=False, include_package_data=False, - cmake_args=cmake_args=cmake_args + ['-DCMAKE_BUILD_TYPE=' + cfg], + cmake_args=cmake_args + ['-DCMAKE_BUILD_TYPE=' + cfg], cmake_install_dir="src/", cmake_install_target='install', install_requires="numpy", diff --git a/src/_wrapper.pyx b/src/_wrapper.pyx deleted file mode 100644 index a7838ef..0000000 --- a/src/_wrapper.pyx +++ /dev/null @@ -1 +0,0 @@ -# placeholder for ftetwild.pyx diff --git a/src/pytetwild/__init__.py b/src/pytetwild/__init__.py new file mode 100644 index 0000000..e69de29 From ec249b5eedb845047165fae12232e709b4ffeb3c Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 17:21:37 +0000 Subject: [PATCH 17/68] fix skbuild --- .gitignore | 3 +++ src/CMakeLists.txt => CMakeLists.txt | 4 ++-- src/{ => pytetwild}/_version.py | 0 3 files changed, 5 insertions(+), 2 deletions(-) rename src/CMakeLists.txt => CMakeLists.txt (80%) rename src/{ => pytetwild}/_version.py (100%) diff --git a/.gitignore b/.gitignore index e9f2d24..b70f6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ venv/ # VSCode .vscode/ + +# skbuild +_skbuild/ \ No newline at end of file diff --git a/src/CMakeLists.txt b/CMakeLists.txt similarity index 80% rename from src/CMakeLists.txt rename to CMakeLists.txt index 66c9c30..576776a 100644 --- a/src/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.8) project(fTetWildWrapper) # Set the path to the fTetWild project -set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/fTetWild") +set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") # Include the fTetWild project as a subdirectory add_subdirectory(${fTetWild_DIR}) # Define your shared library that wraps the fTetWild code -add_library(PyfTetWildWrapper SHARED FTetWildWrapper.cpp) +add_library(PyfTetWildWrapper SHARED "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") # Include directories from fTetWild required for the wrapper target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) diff --git a/src/_version.py b/src/pytetwild/_version.py similarity index 100% rename from src/_version.py rename to src/pytetwild/_version.py From ceb2fc56ba6be2df4b6c02467a50bb2a6cec2dff Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:28:53 -0700 Subject: [PATCH 18/68] Update build-and-deploy.yml --- .github/workflows/build-and-deploy.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index a7d3613..12a3547 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -42,9 +42,10 @@ jobs: xorg-dev \ ccache - - name: Install and test + - name: Install inplace run: | - pip install --find-links=dist pytetwild[dev] + pip install -e .[dev] + ls -R pytest -x - uses: actions/upload-artifact@v4 From f3741e6cdb2bbe69ce6846910b7a89ae891b2082 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:34:54 -0700 Subject: [PATCH 19/68] Update setup.py --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3f9a536..b0ab00c 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,10 @@ zip_safe=False, include_package_data=False, cmake_args=cmake_args + ['-DCMAKE_BUILD_TYPE=' + cfg], - cmake_install_dir="src/", + cmake_install_dir="src/pytetwild", cmake_install_target='install', install_requires="numpy", + extras_require={ + 'dev': ['pytest'], + }, ) From 0d9592be6c6b2c65bfb02353f1b823b88dbdd029 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:45:58 -0700 Subject: [PATCH 20/68] Update CMakeLists.txt --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 576776a..ee79250 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,3 +18,5 @@ target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) # Specify any additional compile features or definitions as needed target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) + +install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION {CMAKE_INSTALL_PREFIX}) From 848edb631cb32ce7124684cd23dfe6065da2381f Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:48:15 -0700 Subject: [PATCH 21/68] Update build-and-deploy.yml --- .github/workflows/build-and-deploy.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 12a3547..74a9908 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -45,8 +45,10 @@ jobs: - name: Install inplace run: | pip install -e .[dev] - ls -R - pytest -x + ls src/pytetwild + + - name: Test + run: pytest -vv - uses: actions/upload-artifact@v4 with: From d0f25199e8a9ac72ea075c98a470439fb7b08fa4 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 10:58:22 -0700 Subject: [PATCH 22/68] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ee79250..05c3b20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,4 +19,4 @@ target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) # Specify any additional compile features or definitions as needed target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) -install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION {CMAKE_INSTALL_PREFIX}) +install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}) From 00aaa83f2e41d07aea59f1f5c12b563f0f7b3692 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 11:05:58 -0700 Subject: [PATCH 23/68] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05c3b20..06984ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,4 +19,4 @@ target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) # Specify any additional compile features or definitions as needed target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) -install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}) +install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION .) From 3162c9643c13f831cc9fd2f7445d79be5ff62d64 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 11:10:37 -0700 Subject: [PATCH 24/68] Update build-and-deploy.yml --- .github/workflows/build-and-deploy.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 74a9908..469beb9 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -42,10 +42,17 @@ jobs: xorg-dev \ ccache - - name: Install inplace + - name: Build wheel run: | - pip install -e .[dev] - ls src/pytetwild + pipx run build --wheel + + - name: Validate wheel + run: | + pip install twine + twine check dist/* + + - name: Install from wheel + run: pip install --find-links=dist/ pytetwild[dev] - name: Test run: pytest -vv From a518948b9e6aa8db69723e21facae13a53e90be4 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 11:23:44 -0700 Subject: [PATCH 25/68] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 06984ab..d64ccd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(fTetWildWrapper) set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") # Include the fTetWild project as a subdirectory -add_subdirectory(${fTetWild_DIR}) +add_subdirectory(${fTetWild_DIR} EXCLUDE_FROM_ALL) # Define your shared library that wraps the fTetWild code add_library(PyfTetWildWrapper SHARED "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") From 09fd10574a7f6dc741ad58e8083793a85b77e3c0 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 12:49:55 -0700 Subject: [PATCH 26/68] Update FTetWildWrapper.cpp --- src/FTetWildWrapper.cpp | 93 ++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index aab50ad..9674a9e 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -5,12 +5,40 @@ #include #include +#include +#include +#include +#include +namespace py = pybind11; + +#include +#include + +std::pair>, std::vector>> extractMeshData(const floatTetWild::Mesh& mesh) { + std::vector> vertices; + std::vector> tetrahedra; + + // Extract vertices + for (const auto& vertex : mesh.tet_vertices) { + if (!vertex.is_removed) { + vertices.push_back({{static_cast(vertex.pos.x()), static_cast(vertex.pos.y()), static_cast(vertex.pos.z())}}); + } + } + + // Extract tetrahedra + for (const auto& tet : mesh.tets) { + if (!tet.is_removed) { + tetrahedra.push_back({{tet.indices[0], tet.indices[1], tet.indices[2], tet.indices[3]}}); + } + } + + return {vertices, tetrahedra}; +} -// You must include other necessary headers and namespaces used in your project -void tetrahedralizeAndWriteMesh(const std::vector& vertices, - const std::vector& faces, - const std::string& outputPath) { +std::pair, std::vector> tetrahedralizeAndWriteMesh( + const std::vector& vertices, + const std::vector& faces) { using namespace floatTetWild; using namespace Eigen; @@ -42,18 +70,55 @@ void tetrahedralizeAndWriteMesh(const std::vector& vertices, // Perform tetrahedralization FloatTetDelaunay::tetrahedralize(input_vertices, input_faces, tree, mesh, is_face_inserted); - // Write the tetrahedralized mesh to a file - std::vector colors; // Optional: For visualizing quality or other metrics - MeshIO::write_mesh(outputPath, mesh, false, colors); + return extractMeshData(mesh); } -// Example usage: -int main() { - std::vector vertices = {/* Fill with your vertices */}; - std::vector faces = {/* Fill with your faces */}; - std::string outputPath = "output_mesh.msh"; +PYBIND11_MODULE(_wrapper, m) { + m.doc() = "Pybind11 plugin for FloatTetWild mesh tetrahedralization"; - tetrahedralizeAndWriteMesh(vertices, faces, outputPath); + m.def("tetrahedralize_mesh", [](py::array_t vertices, py::array_t faces) { + // Convert numpy arrays to Eigen matrices for vertices and faces + auto verts = vertices.unchecked<2>(); // Access numpy array data without bounds checking + auto fcs = faces.unchecked<2>(); - return 0; + std::vector vert_vector(verts.shape(0)); + std::vector face_vector(fcs.shape(0)); + + for (py::ssize_t i = 0; i < verts.shape(0); ++i) { + vert_vector[i] = Eigen::Vector3d(verts(i, 0), verts(i, 1), verts(i, 2)); + } + + for (py::ssize_t i = 0; i < fcs.shape(0); ++i) { + face_vector[i] = Eigen::Vector3i(fcs(i, 0), fcs(i, 1), fcs(i, 2)); + } + + // Call the tetrahedralization function + auto [vertices_result, tetrahedra_result] = tetrahedralizeAndWriteMesh(vert_vector, face_vector); + + // Convert results back to numpy arrays + size_t num_vertices = vertices_result.size(); + size_t num_tetrahedra = tetrahedra_result.size(); + + // Prepare numpy array (points) + py::array_t np_vertices({num_vertices, 3}); + auto np_verts_access = np_vertices.mutable_unchecked<2>(); + for (size_t i = 0; i < num_vertices; ++i) { + for (size_t j = 0; j < 3; ++j) { + np_verts_access(i, j) = vertices_result[i][j]; + } + } + + // Prepare numpy array (tetrahedra) + py::array_t np_tetrahedra({num_tetrahedra, 4}); + auto np_tets_access = np_tetrahedra.mutable_unchecked<2>(); + for (size_t i = 0; i < num_tetrahedra; ++i) { + for (size_t j = 0; j < 4; ++j) { + np_tets_access(i, j) = tetrahedra_result[i][j]; + } + } + + return std::make_pair(np_vertices, np_tetrahedra); + }, "Tetrahedralizes a mesh given vertices and faces arrays, returning numpy arrays of tetrahedra and points."); } + + From d3d431381e0ab3609b8fdac4a200e869af5868ef Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 20:12:05 +0000 Subject: [PATCH 27/68] add pybind --- .gitmodules | 3 +++ src/pybind11 | 1 + 2 files changed, 4 insertions(+) create mode 160000 src/pybind11 diff --git a/.gitmodules b/.gitmodules index 61a6918..9520583 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/fTetWild"] path = src/fTetWild url = https://github.com/wildmeshing/fTetWild +[submodule "src/pybind11"] + path = src/pybind11 + url = https://github.com/pybind/pybind11.git diff --git a/src/pybind11 b/src/pybind11 new file mode 160000 index 0000000..8b48ff8 --- /dev/null +++ b/src/pybind11 @@ -0,0 +1 @@ +Subproject commit 8b48ff878c168b51fe5ef7b8c728815b9e1a9857 From de03224e4c77d3118e5df58d1a4b39dbccf30923 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 29 Feb 2024 13:23:53 -0700 Subject: [PATCH 28/68] Update CMakeLists.txt --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d64ccd7..7e2f180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,16 @@ project(fTetWildWrapper) # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") +# Add the pybind11 submodule +set(pybind11_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/pybind11") +add_subdirectory(${pybind11_DIR}) + # Include the fTetWild project as a subdirectory add_subdirectory(${fTetWild_DIR} EXCLUDE_FROM_ALL) # Define your shared library that wraps the fTetWild code -add_library(PyfTetWildWrapper SHARED "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") +pybind11_add_module(PyfTetWildWrapper "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") + # Include directories from fTetWild required for the wrapper target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) From 5fdeed073717c2120332305553672b7126d864ab Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 13:18:39 -0700 Subject: [PATCH 29/68] update wrapper --- CMakeLists.txt | 4 +- src/FTetWildWrapper.cpp | 134 +++++++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e2f180..549ce98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,7 @@ add_subdirectory(${pybind11_DIR}) # Include the fTetWild project as a subdirectory add_subdirectory(${fTetWild_DIR} EXCLUDE_FROM_ALL) -# Define your shared library that wraps the fTetWild code -pybind11_add_module(PyfTetWildWrapper "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") - +pybind11_add_module(PyfTetWildWrapper MODULE "${CMAKE_CURRENT_SOURCE_DIR}/src/FTetWildWrapper.cpp") # Include directories from fTetWild required for the wrapper target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index 9674a9e..00766ea 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -11,8 +12,17 @@ #include namespace py = pybind11; +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include + std::pair>, std::vector>> extractMeshData(const floatTetWild::Mesh& mesh) { std::vector> vertices; @@ -35,90 +45,140 @@ std::pair>, std::vector>> ex return {vertices, tetrahedra}; } +template +GEO::vector array_to_geo_vector(py::array_t array) { + auto info = array.request(); + T* ptr = static_cast(info.ptr); + GEO::vector geo_vec(info.size); // Allocate space for info.size elements. + + // Populate the GEO::vector with elements from the array. + for (size_t i = 0; i < info.size; ++i) { + geo_vec.data()[i] = ptr[i]; // Copy each element. + } + + return geo_vec; +} -std::pair, std::vector> tetrahedralizeAndWriteMesh( - const std::vector& vertices, - const std::vector& faces) { +std::pair>, std::vector>> tetrahedralize( + GEO::vector& vertices, + GEO::vector& faces) { using namespace floatTetWild; using namespace Eigen; - // Convert input vertices and faces to the format expected by FloatTetWild - std::vector input_vertices(vertices.size()); - for (size_t i = 0; i < vertices.size(); ++i) { - input_vertices[i] = vertices[i].cast(); - } - - std::vector input_faces(faces.begin(), faces.end()); + std::cout << "Starting tetrahedralization..." << std::endl; // Initialize placeholders for flags and epsr_flags, if needed std::vector flags; - std::vector epsr_flags; // Initialize GEO::Mesh and load the mesh data into it - GEO::Mesh geo_mesh; - MeshIO::load_mesh(input_vertices, input_faces, geo_mesh, flags, epsr_flags); + GEO::Mesh sf_mesh; + std::cout << "Loading mesh..." << std::endl; + + sf_mesh.facets.assign_triangle_mesh(3, vertices, faces, false); + if(sf_mesh.cells.nb() != 0 && sf_mesh.facets.nb() == 0) { + sf_mesh.cells.compute_borders(); + } + sf_mesh.show_stats("I/O"); + + std::cout << "Vertices (Points):" << std::endl; + for (size_t i = 0; i < sf_mesh.vertices.nb(); ++i) { + const GEO::vec3& p = sf_mesh.vertices.point(i); // Access the point at index i + std::cout << "Vertex " << i << ": (" + << p[0] << ", " << p[1] << ", " << p[2] << ")" << std::endl; + } + + // Print all facets (faces) + std::cout << "\nFacets (Faces):" << std::endl; + for (size_t i = 0; i < sf_mesh.facets.nb(); ++i) { + std::cout << "Face " << i << ":"; + for (size_t j = 0; j < sf_mesh.facets.nb_vertices(i); ++j) { + // Print each vertex index that makes up the facet + std::cout << " " << sf_mesh.facets.vertex(i, j); + } + std::cout << std::endl; + } + + GEO::mesh_reorder(sf_mesh, GEO::MESH_ORDER_MORTON); // segfault here... + std::cout << "Loaded mesh data into GEO::Mesh." << std::endl; // Initialize AABBWrapper with the loaded GEO::Mesh for collision checking - AABBWrapper tree(geo_mesh); + AABBWrapper tree(sf_mesh); + std::cout << "Initialized AABBWrapper." << std::endl; // Create an instance of Mesh to hold the output tetrahedral mesh Mesh mesh; + std::cout << "Created Mesh instance for output." << std::endl; // Prepare a vector to track the insertion status of faces + std::vector> input_points; + std::vector> input_faces; + + input_points.resize(sf_mesh.vertices.nb()); + for (size_t i = 0; i < input_points.size(); i++) + input_points[i] << (sf_mesh.vertices.point(i))[0], (sf_mesh.vertices.point(i))[1], + (sf_mesh.vertices.point(i))[2]; + + input_faces.resize(sf_mesh.facets.nb()); + for (size_t i = 0; i < input_faces.size(); i++) + input_faces[i] << sf_mesh.facets.vertex(i, 0), sf_mesh.facets.vertex(i, 1), sf_mesh.facets.vertex(i, 2); + std::vector is_face_inserted(input_faces.size(), false); // Perform tetrahedralization - FloatTetDelaunay::tetrahedralize(input_vertices, input_faces, tree, mesh, is_face_inserted); + std::cout << "Starting tetrahedralization..." << std::endl; + FloatTetDelaunay::tetrahedralize(input_points, input_faces, tree, mesh, is_face_inserted); + std::cout << "Tetrahedralization performed." << std::endl; + std::cout << "Tetrahedralization completed. Extracting mesh data..." << std::endl; return extractMeshData(mesh); } -PYBIND11_MODULE(_wrapper, m) { +PYBIND11_MODULE(PyfTetWildWrapper, m) { m.doc() = "Pybind11 plugin for FloatTetWild mesh tetrahedralization"; + + m.def("tetrahedralize_mesh", [](py::array_t vertices, py::array_t faces) { - m.def("tetrahedralize_mesh", [](py::array_t vertices, py::array_t faces) { - // Convert numpy arrays to Eigen matrices for vertices and faces - auto verts = vertices.unchecked<2>(); // Access numpy array data without bounds checking - auto fcs = faces.unchecked<2>(); + // GEO::Logger* geo_logger = GEO::Logger::instance(); + // geo_logger-->initialize(); + GEO::initialize(); - std::vector vert_vector(verts.shape(0)); - std::vector face_vector(fcs.shape(0)); + py::print("Starting tetrahedralization process..."); - for (py::ssize_t i = 0; i < verts.shape(0); ++i) { - vert_vector[i] = Eigen::Vector3d(verts(i, 0), verts(i, 1), verts(i, 2)); - } - - for (py::ssize_t i = 0; i < fcs.shape(0); ++i) { - face_vector[i] = Eigen::Vector3i(fcs(i, 0), fcs(i, 1), fcs(i, 2)); - } - - // Call the tetrahedralization function - auto [vertices_result, tetrahedra_result] = tetrahedralizeAndWriteMesh(vert_vector, face_vector); + // Convert numpy arrays to vectors and call the tetrahedralization function + GEO::vector vertices_vec = array_to_geo_vector(vertices); + GEO::vector faces_vec = array_to_geo_vector(faces); + auto [vertices_result, tetrahedra_result] = tetrahedralize(vertices_vec, faces_vec); + py::print("Tetrahedralization complete."); // Convert results back to numpy arrays size_t num_vertices = vertices_result.size(); size_t num_tetrahedra = tetrahedra_result.size(); + py::print("Number of vertices:", num_vertices); + py::print("Number of tetrahedra:", num_tetrahedra); // Prepare numpy array (points) - py::array_t np_vertices({num_vertices, 3}); + size_t shape[2]{num_vertices, 3}; + auto np_vertices = py::array_t(shape); auto np_verts_access = np_vertices.mutable_unchecked<2>(); for (size_t i = 0; i < num_vertices; ++i) { for (size_t j = 0; j < 3; ++j) { np_verts_access(i, j) = vertices_result[i][j]; } } + py::print("Prepared numpy array for points."); // Prepare numpy array (tetrahedra) - py::array_t np_tetrahedra({num_tetrahedra, 4}); + size_t shape_tet[2]{num_tetrahedra, 4}; + auto np_tetrahedra = py::array_t(shape_tet); auto np_tets_access = np_tetrahedra.mutable_unchecked<2>(); for (size_t i = 0; i < num_tetrahedra; ++i) { for (size_t j = 0; j < 4; ++j) { np_tets_access(i, j) = tetrahedra_result[i][j]; } } + py::print("Prepared numpy array for tetrahedra."); + py::print("Tetrahedralization process completed successfully."); return std::make_pair(np_vertices, np_tetrahedra); }, "Tetrahedralizes a mesh given vertices and faces arrays, returning numpy arrays of tetrahedra and points."); } - - From 75a591f08f356b1b350416d1b11da9c8408ae293 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 15:29:31 -0700 Subject: [PATCH 30/68] Create pytetwild.py --- src/pytetwild/pytetwild.py | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/pytetwild/pytetwild.py diff --git a/src/pytetwild/pytetwild.py b/src/pytetwild/pytetwild.py new file mode 100644 index 0000000..20c50a0 --- /dev/null +++ b/src/pytetwild/pytetwild.py @@ -0,0 +1,46 @@ +import numpy as np +import pyvista as pv +from pytetwild import PyfTetWildWrapper +from typing import Tuple + +def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: + """ + Converts a PyVista surface mesh to a PyVista unstructured grid. + + Parameters + ---------- + mesh : pv.PolyData + The input surface mesh. + + Returns + ------- + pv.UnstructuredGrid + The converted unstructured grid. + """ + vertices = np.array(mesh.points, dtype=np.float64) + faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) + + tetrahedral_mesh_vertices, tetrahedral_mesh_tetrahedra = PyfTetWildWrapper.tetrahedralize_mesh(vertices, faces) + + cells = np.hstack([np.full((tetrahedral_mesh_tetrahedra.shape[0], 1), 4, dtype=np.int32), tetrahedral_mesh_tetrahedra]) + cell_types = np.full(tetrahedral_mesh_tetrahedra.shape[0], 10, dtype=np.uint8) + + return pv.UnstructuredGrid(cells, cell_types, tetrahedral_mesh_vertices) + +def tetrahedralize(vertices: np.ndarray, faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """ + Converts mesh vertices and faces to a tetrahedral mesh using PyfTetWildWrapper. + + Parameters + ---------- + vertices : np.ndarray + The vertices of the mesh. + faces : np.ndarray + The faces of the mesh. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + A tuple containing the vertices and tetrahedra of the tetrahedral mesh. + """ + return PyfTetWildWrapper.tetrahedralize_mesh(vertices, faces) From 42ad1eb2d692d701bd79a2bd5c6e61a3463cd28b Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 15:30:22 -0700 Subject: [PATCH 31/68] Update __init__.py --- src/pytetwild/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pytetwild/__init__.py b/src/pytetwild/__init__.py index e69de29..0220b40 100644 --- a/src/pytetwild/__init__.py +++ b/src/pytetwild/__init__.py @@ -0,0 +1,2 @@ +from ._version import __version__ +from .pytetwild import tetrahedralize tetrahedralize_pv From cf749ff32cfd8a4ebfea63845466a1ed3cf3e013 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 15:45:03 -0700 Subject: [PATCH 32/68] Create test_pytetwild.py --- tests/test_pytetwild.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_pytetwild.py diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py new file mode 100644 index 0000000..dab5634 --- /dev/null +++ b/tests/test_pytetwild.py @@ -0,0 +1,26 @@ +import numpy as np +import pyvista as pv +import pytest +from your_module import tetrahedralize_pv, tetrahedralize # Replace 'your_module' with the name of your Python file + +# Parameterized test for tetrahedralize_pv function +@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) +def test_tetrahedralize_pv(mesh_generator): + mesh = mesh_generator() + result = tetrahedralize_pv(mesh) + assert isinstance(result, pv.UnstructuredGrid), "The result should be a PyVista UnstructuredGrid" + assert result.n_cells > 0, "The resulting mesh should have more than 0 cells" + assert result.n_points > 0, "The resulting mesh should have more than 0 points" + +# Parameterized test for tetrahedralize function +@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) +def test_tetrahedralize(mesh_generator): + mesh = mesh_generator() + vertices = np.array(mesh.points, dtype=np.float64) + faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) + + vertices_result, tetrahedra_result = tetrahedralize(vertices, faces) + assert isinstance(vertices_result, np.ndarray), "The vertices result should be a numpy array" + assert isinstance(tetrahedra_result, np.ndarray), "The tetrahedra result should be a numpy array" + assert len(vertices_result) > 0, "There should be more than 0 vertices in the result" + assert len(tetrahedra_result) > 0, "There should be more than 0 tetrahedra in the result" From 9ef4530a25097dac3d9a2b203e2c6fd4699da48c Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 15:56:11 -0700 Subject: [PATCH 33/68] Update test_pytetwild.py --- tests/test_pytetwild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py index dab5634..91c2287 100644 --- a/tests/test_pytetwild.py +++ b/tests/test_pytetwild.py @@ -1,7 +1,7 @@ import numpy as np import pyvista as pv import pytest -from your_module import tetrahedralize_pv, tetrahedralize # Replace 'your_module' with the name of your Python file +from pytetwild import tetrahedralize_pv, tetrahedralize # Replace 'your_module' with the name of your Python file # Parameterized test for tetrahedralize_pv function @pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) From 1ce1997087b777727691421586ac08f87a53fd2f Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 16:00:28 -0700 Subject: [PATCH 34/68] Create .pre-commit-config.yaml --- .pre-commit-config.yaml | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..48d4927 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,66 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.11 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + exclude: ^(docs|tests) + - id: ruff-format + +- repo: https://github.com/keewis/blackdoc + rev: v0.3.9 + hooks: + - id: blackdoc + exclude: README.rst + +- repo: https://github.com/numpy/numpydoc + rev: v1.6.0 + hooks: + - id: numpydoc-validation + files: ^src/pytetwild + +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: ["--skip=*.vt*"] + +- repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + additional_dependencies: [tomli==2.0.1] + files: ^src/pytetwild/.*\.py + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: trailing-whitespace + +- repo: https://github.com/pre-commit/mirrors-clang-format + rev: v17.0.6 + hooks: + - id: clang-format + files: | + (?x)^( + src/FTetWildWrapper.cpp + )$ + +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.7.0 +# hooks: +# - id: mypy +# exclude: ^(docs/|tests) +# additional_dependencies: [ +# "mypy-extensions==1.0.0", +# "toml==0.10.2", +# "types-PyYAML", +# "numpy", +# ] + +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.27.3 + hooks: + - id: check-github-workflows From b7b837040f97a14f53fa3d24d1e1791b4f6623cb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:00:37 +0000 Subject: [PATCH 35/68] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- LICENSE | 2 +- setup.py | 29 ++--- src/FTetWildWrapper.cpp | 232 +++++++++++++++++++------------------ src/pytetwild/_version.py | 2 +- src/pytetwild/pytetwild.py | 24 +++- tests/test_pytetwild.py | 37 ++++-- 6 files changed, 185 insertions(+), 141 deletions(-) diff --git a/LICENSE b/LICENSE index f4bbcd2..fa0086a 100644 --- a/LICENSE +++ b/LICENSE @@ -35,7 +35,7 @@ Mozilla Public License Version 2.0 means any form of the work other than Source Code Form. 1.7. "Larger Work" - means a work that combines Covered Software with other material, in + means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" diff --git a/setup.py b/setup.py index b0ab00c..386b1ed 100644 --- a/setup.py +++ b/setup.py @@ -6,30 +6,33 @@ try: from skbuild import setup except ImportError: - print("Please update pip to pip 10 or greater, or a manually install the PEP 518 requirements in pyproject.toml", file=sys.stderr) + print( + "Please update pip to pip 10 or greater, or a manually install the PEP 518 requirements in pyproject.toml", + file=sys.stderr, + ) raise cmake_args = [] debug = False -cfg = 'Debug' if debug else 'Release' +cfg = "Debug" if debug else "Release" setup( - name='pytetwild', - version='0.1.dev0', - author='Alex Kaszynski', - author_email='akascap@gmail.com', - description='Python wrapper of fTetWild', + name="pytetwild", + version="0.1.dev0", + author="Alex Kaszynski", + author_email="akascap@gmail.com", + description="Python wrapper of fTetWild", long_description=open("README.rst").read(), long_description_content_type="text/rst", - packages=find_packages('src'), - package_dir={'':'src'}, + packages=find_packages("src"), + package_dir={"": "src"}, zip_safe=False, include_package_data=False, - cmake_args=cmake_args + ['-DCMAKE_BUILD_TYPE=' + cfg], + cmake_args=cmake_args + ["-DCMAKE_BUILD_TYPE=" + cfg], cmake_install_dir="src/pytetwild", - cmake_install_target='install', + cmake_install_target="install", install_requires="numpy", extras_require={ - 'dev': ['pytest'], - }, + "dev": ["pytest"], + }, ) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index 00766ea..5c14fb7 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -1,153 +1,161 @@ -#include -#include #include +#include +#include #include #include #include -#include -#include #include #include +#include #include +#include namespace py = pybind11; #include #include +#include +#include #include #include #include #include #include -#include -#include - -std::pair>, std::vector>> extractMeshData(const floatTetWild::Mesh& mesh) { - std::vector> vertices; - std::vector> tetrahedra; - - // Extract vertices - for (const auto& vertex : mesh.tet_vertices) { - if (!vertex.is_removed) { - vertices.push_back({{static_cast(vertex.pos.x()), static_cast(vertex.pos.y()), static_cast(vertex.pos.z())}}); - } +std::pair>, std::vector>> +extractMeshData(const floatTetWild::Mesh &mesh) { + std::vector> vertices; + std::vector> tetrahedra; + + // Extract vertices + for (const auto &vertex : mesh.tet_vertices) { + if (!vertex.is_removed) { + vertices.push_back({{static_cast(vertex.pos.x()), + static_cast(vertex.pos.y()), + static_cast(vertex.pos.z())}}); } + } - // Extract tetrahedra - for (const auto& tet : mesh.tets) { - if (!tet.is_removed) { - tetrahedra.push_back({{tet.indices[0], tet.indices[1], tet.indices[2], tet.indices[3]}}); - } + // Extract tetrahedra + for (const auto &tet : mesh.tets) { + if (!tet.is_removed) { + tetrahedra.push_back( + {{tet.indices[0], tet.indices[1], tet.indices[2], tet.indices[3]}}); } + } - return {vertices, tetrahedra}; -} - -template -GEO::vector array_to_geo_vector(py::array_t array) { - auto info = array.request(); - T* ptr = static_cast(info.ptr); - GEO::vector geo_vec(info.size); // Allocate space for info.size elements. - - // Populate the GEO::vector with elements from the array. - for (size_t i = 0; i < info.size; ++i) { - geo_vec.data()[i] = ptr[i]; // Copy each element. - } - - return geo_vec; + return {vertices, tetrahedra}; } -std::pair>, std::vector>> tetrahedralize( - GEO::vector& vertices, - GEO::vector& faces) { - using namespace floatTetWild; - using namespace Eigen; +template GEO::vector array_to_geo_vector(py::array_t array) { + auto info = array.request(); + T *ptr = static_cast(info.ptr); + GEO::vector geo_vec(info.size); // Allocate space for info.size elements. - std::cout << "Starting tetrahedralization..." << std::endl; + // Populate the GEO::vector with elements from the array. + for (size_t i = 0; i < info.size; ++i) { + geo_vec.data()[i] = ptr[i]; // Copy each element. + } - // Initialize placeholders for flags and epsr_flags, if needed - std::vector flags; - - // Initialize GEO::Mesh and load the mesh data into it - GEO::Mesh sf_mesh; - std::cout << "Loading mesh..." << std::endl; - - sf_mesh.facets.assign_triangle_mesh(3, vertices, faces, false); - if(sf_mesh.cells.nb() != 0 && sf_mesh.facets.nb() == 0) { - sf_mesh.cells.compute_borders(); - } - sf_mesh.show_stats("I/O"); - - std::cout << "Vertices (Points):" << std::endl; - for (size_t i = 0; i < sf_mesh.vertices.nb(); ++i) { - const GEO::vec3& p = sf_mesh.vertices.point(i); // Access the point at index i - std::cout << "Vertex " << i << ": (" - << p[0] << ", " << p[1] << ", " << p[2] << ")" << std::endl; - } + return geo_vec; +} - // Print all facets (faces) - std::cout << "\nFacets (Faces):" << std::endl; - for (size_t i = 0; i < sf_mesh.facets.nb(); ++i) { - std::cout << "Face " << i << ":"; - for (size_t j = 0; j < sf_mesh.facets.nb_vertices(i); ++j) { - // Print each vertex index that makes up the facet - std::cout << " " << sf_mesh.facets.vertex(i, j); - } - std::cout << std::endl; +std::pair>, std::vector>> +tetrahedralize(GEO::vector &vertices, GEO::vector &faces) { + using namespace floatTetWild; + using namespace Eigen; + + std::cout << "Starting tetrahedralization..." << std::endl; + + // Initialize placeholders for flags and epsr_flags, if needed + std::vector flags; + + // Initialize GEO::Mesh and load the mesh data into it + GEO::Mesh sf_mesh; + std::cout << "Loading mesh..." << std::endl; + + sf_mesh.facets.assign_triangle_mesh(3, vertices, faces, false); + if (sf_mesh.cells.nb() != 0 && sf_mesh.facets.nb() == 0) { + sf_mesh.cells.compute_borders(); + } + sf_mesh.show_stats("I/O"); + + std::cout << "Vertices (Points):" << std::endl; + for (size_t i = 0; i < sf_mesh.vertices.nb(); ++i) { + const GEO::vec3 &p = + sf_mesh.vertices.point(i); // Access the point at index i + std::cout << "Vertex " << i << ": (" << p[0] << ", " << p[1] << ", " << p[2] + << ")" << std::endl; + } + + // Print all facets (faces) + std::cout << "\nFacets (Faces):" << std::endl; + for (size_t i = 0; i < sf_mesh.facets.nb(); ++i) { + std::cout << "Face " << i << ":"; + for (size_t j = 0; j < sf_mesh.facets.nb_vertices(i); ++j) { + // Print each vertex index that makes up the facet + std::cout << " " << sf_mesh.facets.vertex(i, j); } + std::cout << std::endl; + } - GEO::mesh_reorder(sf_mesh, GEO::MESH_ORDER_MORTON); // segfault here... - std::cout << "Loaded mesh data into GEO::Mesh." << std::endl; + GEO::mesh_reorder(sf_mesh, GEO::MESH_ORDER_MORTON); // segfault here... + std::cout << "Loaded mesh data into GEO::Mesh." << std::endl; - // Initialize AABBWrapper with the loaded GEO::Mesh for collision checking - AABBWrapper tree(sf_mesh); - std::cout << "Initialized AABBWrapper." << std::endl; + // Initialize AABBWrapper with the loaded GEO::Mesh for collision checking + AABBWrapper tree(sf_mesh); + std::cout << "Initialized AABBWrapper." << std::endl; - // Create an instance of Mesh to hold the output tetrahedral mesh - Mesh mesh; - std::cout << "Created Mesh instance for output." << std::endl; + // Create an instance of Mesh to hold the output tetrahedral mesh + Mesh mesh; + std::cout << "Created Mesh instance for output." << std::endl; - // Prepare a vector to track the insertion status of faces - std::vector> input_points; - std::vector> input_faces; + // Prepare a vector to track the insertion status of faces + std::vector> input_points; + std::vector> input_faces; - input_points.resize(sf_mesh.vertices.nb()); - for (size_t i = 0; i < input_points.size(); i++) - input_points[i] << (sf_mesh.vertices.point(i))[0], (sf_mesh.vertices.point(i))[1], - (sf_mesh.vertices.point(i))[2]; + input_points.resize(sf_mesh.vertices.nb()); + for (size_t i = 0; i < input_points.size(); i++) + input_points[i] << (sf_mesh.vertices.point(i))[0], + (sf_mesh.vertices.point(i))[1], (sf_mesh.vertices.point(i))[2]; - input_faces.resize(sf_mesh.facets.nb()); - for (size_t i = 0; i < input_faces.size(); i++) - input_faces[i] << sf_mesh.facets.vertex(i, 0), sf_mesh.facets.vertex(i, 1), sf_mesh.facets.vertex(i, 2); + input_faces.resize(sf_mesh.facets.nb()); + for (size_t i = 0; i < input_faces.size(); i++) + input_faces[i] << sf_mesh.facets.vertex(i, 0), sf_mesh.facets.vertex(i, 1), + sf_mesh.facets.vertex(i, 2); - std::vector is_face_inserted(input_faces.size(), false); + std::vector is_face_inserted(input_faces.size(), false); - // Perform tetrahedralization - std::cout << "Starting tetrahedralization..." << std::endl; - FloatTetDelaunay::tetrahedralize(input_points, input_faces, tree, mesh, is_face_inserted); - std::cout << "Tetrahedralization performed." << std::endl; + // Perform tetrahedralization + std::cout << "Starting tetrahedralization..." << std::endl; + FloatTetDelaunay::tetrahedralize(input_points, input_faces, tree, mesh, + is_face_inserted); + std::cout << "Tetrahedralization performed." << std::endl; - std::cout << "Tetrahedralization completed. Extracting mesh data..." << std::endl; - return extractMeshData(mesh); + std::cout << "Tetrahedralization completed. Extracting mesh data..." + << std::endl; + return extractMeshData(mesh); } PYBIND11_MODULE(PyfTetWildWrapper, m) { - m.doc() = "Pybind11 plugin for FloatTetWild mesh tetrahedralization"; - - m.def("tetrahedralize_mesh", [](py::array_t vertices, py::array_t faces) { + m.doc() = "Pybind11 plugin for FloatTetWild mesh tetrahedralization"; - // GEO::Logger* geo_logger = GEO::Logger::instance(); - // geo_logger-->initialize(); - GEO::initialize(); + m.def( + "tetrahedralize_mesh", + [](py::array_t vertices, py::array_t faces) { + // GEO::Logger* geo_logger = GEO::Logger::instance(); + // geo_logger-->initialize(); + GEO::initialize(); py::print("Starting tetrahedralization process..."); - // Convert numpy arrays to vectors and call the tetrahedralization function + // Convert numpy arrays to vectors and call the tetrahedralization + // function GEO::vector vertices_vec = array_to_geo_vector(vertices); - GEO::vector faces_vec = array_to_geo_vector(faces); - auto [vertices_result, tetrahedra_result] = tetrahedralize(vertices_vec, faces_vec); + GEO::vector faces_vec = array_to_geo_vector(faces); + auto [vertices_result, tetrahedra_result] = + tetrahedralize(vertices_vec, faces_vec); py::print("Tetrahedralization complete."); // Convert results back to numpy arrays @@ -161,9 +169,9 @@ PYBIND11_MODULE(PyfTetWildWrapper, m) { auto np_vertices = py::array_t(shape); auto np_verts_access = np_vertices.mutable_unchecked<2>(); for (size_t i = 0; i < num_vertices; ++i) { - for (size_t j = 0; j < 3; ++j) { - np_verts_access(i, j) = vertices_result[i][j]; - } + for (size_t j = 0; j < 3; ++j) { + np_verts_access(i, j) = vertices_result[i][j]; + } } py::print("Prepared numpy array for points."); @@ -172,13 +180,15 @@ PYBIND11_MODULE(PyfTetWildWrapper, m) { auto np_tetrahedra = py::array_t(shape_tet); auto np_tets_access = np_tetrahedra.mutable_unchecked<2>(); for (size_t i = 0; i < num_tetrahedra; ++i) { - for (size_t j = 0; j < 4; ++j) { - np_tets_access(i, j) = tetrahedra_result[i][j]; - } + for (size_t j = 0; j < 4; ++j) { + np_tets_access(i, j) = tetrahedra_result[i][j]; + } } py::print("Prepared numpy array for tetrahedra."); py::print("Tetrahedralization process completed successfully."); return std::make_pair(np_vertices, np_tetrahedra); - }, "Tetrahedralizes a mesh given vertices and faces arrays, returning numpy arrays of tetrahedra and points."); + }, + "Tetrahedralizes a mesh given vertices and faces arrays, returning numpy " + "arrays of tetrahedra and points."); } diff --git a/src/pytetwild/_version.py b/src/pytetwild/_version.py index 355c0d8..75e9205 100644 --- a/src/pytetwild/_version.py +++ b/src/pytetwild/_version.py @@ -6,5 +6,5 @@ version_info = 0, 27, 'dev0' """ -version_info = 0, 1, 'dev0' +version_info = 0, 1, "dev0" __version__ = ".".join(map(str, version_info)) diff --git a/src/pytetwild/pytetwild.py b/src/pytetwild/pytetwild.py index 20c50a0..09e6c60 100644 --- a/src/pytetwild/pytetwild.py +++ b/src/pytetwild/pytetwild.py @@ -3,6 +3,7 @@ from pytetwild import PyfTetWildWrapper from typing import Tuple + def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: """ Converts a PyVista surface mesh to a PyVista unstructured grid. @@ -19,15 +20,26 @@ def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: """ vertices = np.array(mesh.points, dtype=np.float64) faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) - - tetrahedral_mesh_vertices, tetrahedral_mesh_tetrahedra = PyfTetWildWrapper.tetrahedralize_mesh(vertices, faces) - - cells = np.hstack([np.full((tetrahedral_mesh_tetrahedra.shape[0], 1), 4, dtype=np.int32), tetrahedral_mesh_tetrahedra]) + + ( + tetrahedral_mesh_vertices, + tetrahedral_mesh_tetrahedra, + ) = PyfTetWildWrapper.tetrahedralize_mesh(vertices, faces) + + cells = np.hstack( + [ + np.full((tetrahedral_mesh_tetrahedra.shape[0], 1), 4, dtype=np.int32), + tetrahedral_mesh_tetrahedra, + ] + ) cell_types = np.full(tetrahedral_mesh_tetrahedra.shape[0], 10, dtype=np.uint8) - + return pv.UnstructuredGrid(cells, cell_types, tetrahedral_mesh_vertices) -def tetrahedralize(vertices: np.ndarray, faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + +def tetrahedralize( + vertices: np.ndarray, faces: np.ndarray +) -> Tuple[np.ndarray, np.ndarray]: """ Converts mesh vertices and faces to a tetrahedral mesh using PyfTetWildWrapper. diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py index 91c2287..3109bf3 100644 --- a/tests/test_pytetwild.py +++ b/tests/test_pytetwild.py @@ -1,26 +1,45 @@ import numpy as np import pyvista as pv import pytest -from pytetwild import tetrahedralize_pv, tetrahedralize # Replace 'your_module' with the name of your Python file +from pytetwild import ( + tetrahedralize_pv, + tetrahedralize, +) # Replace 'your_module' with the name of your Python file + # Parameterized test for tetrahedralize_pv function -@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) +@pytest.mark.parametrize( + "mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse] +) def test_tetrahedralize_pv(mesh_generator): mesh = mesh_generator() result = tetrahedralize_pv(mesh) - assert isinstance(result, pv.UnstructuredGrid), "The result should be a PyVista UnstructuredGrid" + assert isinstance( + result, pv.UnstructuredGrid + ), "The result should be a PyVista UnstructuredGrid" assert result.n_cells > 0, "The resulting mesh should have more than 0 cells" assert result.n_points > 0, "The resulting mesh should have more than 0 points" + # Parameterized test for tetrahedralize function -@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) +@pytest.mark.parametrize( + "mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse] +) def test_tetrahedralize(mesh_generator): mesh = mesh_generator() vertices = np.array(mesh.points, dtype=np.float64) faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) - + vertices_result, tetrahedra_result = tetrahedralize(vertices, faces) - assert isinstance(vertices_result, np.ndarray), "The vertices result should be a numpy array" - assert isinstance(tetrahedra_result, np.ndarray), "The tetrahedra result should be a numpy array" - assert len(vertices_result) > 0, "There should be more than 0 vertices in the result" - assert len(tetrahedra_result) > 0, "There should be more than 0 tetrahedra in the result" + assert isinstance( + vertices_result, np.ndarray + ), "The vertices result should be a numpy array" + assert isinstance( + tetrahedra_result, np.ndarray + ), "The tetrahedra result should be a numpy array" + assert ( + len(vertices_result) > 0 + ), "There should be more than 0 vertices in the result" + assert ( + len(tetrahedra_result) > 0 + ), "There should be more than 0 tetrahedra in the result" From 81be1773e77a2dd5587e2e94805363966f82af98 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 16:09:34 -0700 Subject: [PATCH 36/68] Delete src/FTetWildWrapper.h --- src/FTetWildWrapper.h | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/FTetWildWrapper.h diff --git a/src/FTetWildWrapper.h b/src/FTetWildWrapper.h deleted file mode 100644 index 51e7b82..0000000 --- a/src/FTetWildWrapper.h +++ /dev/null @@ -1 +0,0 @@ -// placeholder for FTetWildWrapper.h From 2e04e21e40ef6fdf4d6acb39146326ddf4196bf8 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 16:47:19 -0700 Subject: [PATCH 37/68] pre-commit fixes --- pyproject.toml | 94 ++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- src/pytetwild/__init__.py | 5 +- src/pytetwild/_version.py | 2 +- src/pytetwild/pytetwild.py | 11 ++--- tests/test_pytetwild.py | 20 ++------ 6 files changed, 109 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01676cf..c5fab52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,97 @@ build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wh skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels test-requires = "pytest" test-command = "pytest {project}/tests -v" + +[tool.blackdoc] +# From https://numpydoc.readthedocs.io/en/latest/format.html +# Extended discussion: https://github.com/pyvista/pyvista/pull/4129 +# The length of docstring lines should be kept to 75 characters to facilitate +# reading the docstrings in text terminals. +line-length = 75 + +[tool.pydocstyle] +match = '(?!coverage).*.py' +convention = "numpy" +add-ignore = ["D404"] + +[tool.mypy] +ignore_missing_imports = true +disallow_any_generics = true +pretty = true +show_error_context = true +warn_unused_ignores = true +plugins = ['numpy.typing.mypy_plugin','npt_promote'] + +[tool.numpydoc_validation] +checks = [ + "all", # all but the following: + "GL01", # Contradicts numpydoc examples + "GL02", # Permit a blank line after the end of our docstring + "GL03", # Considering enforcing + "GL06", # Found unknown section + "GL07", # "Sections are in the wrong order. Correct order is: {correct_sections}", + "GL09", # Deprecation warning should precede extended summary (check broken) + "SA01", # Not all docstrings need a see also + "SA04", # See also section does not need descriptions + "SS05", # Appears to be broken. + "ES01", # Not all docstrings need an extend summary. + "EX01", # Examples: Will eventually enforce + "YD01", # Yields: No plan to enforce +] + +[tool.ruff] +exclude = [ + '.git', + '__pycache__', + 'build', + 'dist', +] +line-length = 100 +indent-width = 4 +target-version = 'py38' + +[tool.ruff.lint] +external = ["E131", "D102", "D105"] +ignore = [ + # whitespace before ':' + "E203", + # line break before binary operator + # "W503", + # line length too long + "E501", + # do not assign a lambda expression, use a def + "E731", + # too many leading '#' for block comment + "E266", + # ambiguous variable name + "E741", + # module level import not at top of file + "E402", + # Quotes (temporary) + "Q0", + # bare excepts (temporary) + # "B001", "E722", + "E722", + # we already check black + # "BLK100", + # 'from module import *' used; unable to detect undefined names + "F403", +] +fixable = ["ALL"] +unfixable = [] +extend-select = [ + "B007", + "B010", + "C4", + "F", + "FLY", + "NPY", + "PGH004", + "PIE", + "PT", + "RSE", + "RUF005", + "RUF010", + "RUF100", +] + diff --git a/setup.py b/setup.py index 386b1ed..9e85d9e 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ package_dir={"": "src"}, zip_safe=False, include_package_data=False, - cmake_args=cmake_args + ["-DCMAKE_BUILD_TYPE=" + cfg], + cmake_args=[*cmake_args, "-DCMAKE_BUILD_TYPE=", cfg], cmake_install_dir="src/pytetwild", cmake_install_target="install", install_requires="numpy", diff --git a/src/pytetwild/__init__.py b/src/pytetwild/__init__.py index 0220b40..091ebc6 100644 --- a/src/pytetwild/__init__.py +++ b/src/pytetwild/__init__.py @@ -1,2 +1,3 @@ -from ._version import __version__ -from .pytetwild import tetrahedralize tetrahedralize_pv +"""Wrapper for fTetWild.""" +from ._version import __version__ # noqa: F401 +from .pytetwild import tetrahedralize, tetrahedralize_pv # noqa: F401 diff --git a/src/pytetwild/_version.py b/src/pytetwild/_version.py index 75e9205..109edec 100644 --- a/src/pytetwild/_version.py +++ b/src/pytetwild/_version.py @@ -1,4 +1,4 @@ -"""pytetwild version. +"""Contains the pytetwild version. On the ``main`` branch, use 'dev0' to denote a development version. For example: diff --git a/src/pytetwild/pytetwild.py b/src/pytetwild/pytetwild.py index 09e6c60..1e666f7 100644 --- a/src/pytetwild/pytetwild.py +++ b/src/pytetwild/pytetwild.py @@ -1,3 +1,4 @@ +"""Wrapper for fTetWild.""" import numpy as np import pyvista as pv from pytetwild import PyfTetWildWrapper @@ -6,7 +7,7 @@ def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: """ - Converts a PyVista surface mesh to a PyVista unstructured grid. + Convert a PyVista surface mesh to a PyVista unstructured grid. Parameters ---------- @@ -37,15 +38,13 @@ def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: return pv.UnstructuredGrid(cells, cell_types, tetrahedral_mesh_vertices) -def tetrahedralize( - vertices: np.ndarray, faces: np.ndarray -) -> Tuple[np.ndarray, np.ndarray]: +def tetrahedralize(vertices: np.ndarray, faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """ - Converts mesh vertices and faces to a tetrahedral mesh using PyfTetWildWrapper. + Convert mesh vertices and faces to a tetrahedral mesh. Parameters ---------- - vertices : np.ndarray + vertices : np.ndarray[double] The vertices of the mesh. faces : np.ndarray The faces of the mesh. diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py index 3109bf3..395a842 100644 --- a/tests/test_pytetwild.py +++ b/tests/test_pytetwild.py @@ -8,9 +8,7 @@ # Parameterized test for tetrahedralize_pv function -@pytest.mark.parametrize( - "mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse] -) +@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) def test_tetrahedralize_pv(mesh_generator): mesh = mesh_generator() result = tetrahedralize_pv(mesh) @@ -22,24 +20,16 @@ def test_tetrahedralize_pv(mesh_generator): # Parameterized test for tetrahedralize function -@pytest.mark.parametrize( - "mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse] -) +@pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) def test_tetrahedralize(mesh_generator): mesh = mesh_generator() vertices = np.array(mesh.points, dtype=np.float64) faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) vertices_result, tetrahedra_result = tetrahedralize(vertices, faces) - assert isinstance( - vertices_result, np.ndarray - ), "The vertices result should be a numpy array" + assert isinstance(vertices_result, np.ndarray), "The vertices result should be a numpy array" assert isinstance( tetrahedra_result, np.ndarray ), "The tetrahedra result should be a numpy array" - assert ( - len(vertices_result) > 0 - ), "There should be more than 0 vertices in the result" - assert ( - len(tetrahedra_result) > 0 - ), "There should be more than 0 tetrahedra in the result" + assert len(vertices_result) > 0, "There should be more than 0 vertices in the result" + assert len(tetrahedra_result) > 0, "There should be more than 0 tetrahedra in the result" From 84928671ebccb716d4db02df642c71a303af8911 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 21:35:11 -0700 Subject: [PATCH 38/68] use scikit-build-core --- CMakeLists.txt | 9 ++++----- pyproject.toml | 25 +++++++++++++++++-------- setup.py | 38 -------------------------------------- 3 files changed, 21 insertions(+), 51 deletions(-) delete mode 100644 setup.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 549ce98..5a5c0a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") # Add the pybind11 submodule +set(PYBIND11_NEWPYTHON ON) set(pybind11_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/pybind11") add_subdirectory(${pybind11_DIR}) @@ -16,10 +17,8 @@ pybind11_add_module(PyfTetWildWrapper MODULE "${CMAKE_CURRENT_SOURCE_DIR}/src/FT # Include directories from fTetWild required for the wrapper target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) -# Link the FloatTetwild library (from fTetWild) to your wrapper library +# Link the FloatTetwild library (from fTetWild) target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) - -# Specify any additional compile features or definitions as needed target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) -install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION .) +install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ./pytetwild) diff --git a/pyproject.toml b/pyproject.toml index c5fab52..d7eb9c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,21 @@ [build-system] -requires = [ - "setuptools>=42", - "wheel", - "cmake>=3.18", - "scikit-build>=0.13", - "ninja>=1.10.0", -] -build-backend = "setuptools.build_meta" +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "pytetwild" +version = "0.1.dev0" +description = "Python wrapper of fTetWild" +readme = { file = "README.rst", content-type = "text/x-rst" } +authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] +dependencies = ["numpy"] +optional-dependencies = { dev = ["pytest"] } +dynamic = ["classifiers", "urls", "license"] + +[tool.scikit-build] +cmake.build-type = "Release" +editable.mode = "inplace" +build-dir = "./build" [tool.pytest.ini_options] testpaths = 'tests' diff --git a/setup.py b/setup.py deleted file mode 100644 index 9e85d9e..0000000 --- a/setup.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Setup for pytetwild.""" -import sys - -from setuptools import find_packages - -try: - from skbuild import setup -except ImportError: - print( - "Please update pip to pip 10 or greater, or a manually install the PEP 518 requirements in pyproject.toml", - file=sys.stderr, - ) - raise - -cmake_args = [] -debug = False -cfg = "Debug" if debug else "Release" - -setup( - name="pytetwild", - version="0.1.dev0", - author="Alex Kaszynski", - author_email="akascap@gmail.com", - description="Python wrapper of fTetWild", - long_description=open("README.rst").read(), - long_description_content_type="text/rst", - packages=find_packages("src"), - package_dir={"": "src"}, - zip_safe=False, - include_package_data=False, - cmake_args=[*cmake_args, "-DCMAKE_BUILD_TYPE=", cfg], - cmake_install_dir="src/pytetwild", - cmake_install_target="install", - install_requires="numpy", - extras_require={ - "dev": ["pytest"], - }, -) From d2cb1d50aadd919710408b7760333b763d6f46a7 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 21:51:00 -0700 Subject: [PATCH 39/68] fix inplace build --- CMakeLists.txt | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a5c0a6..cb65d7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") @@ -22,3 +23,4 @@ target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ./pytetwild) + diff --git a/pyproject.toml b/pyproject.toml index d7eb9c3..9581868 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,8 @@ dynamic = ["classifiers", "urls", "license"] [tool.scikit-build] cmake.build-type = "Release" -editable.mode = "inplace" build-dir = "./build" +editable.mode = "inplace" [tool.pytest.ini_options] testpaths = 'tests' From 67043afa712dd193fe4731c5f19db21f44fcdb30 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 21:56:47 -0700 Subject: [PATCH 40/68] update .gitignore --- .gitignore | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b70f6eb..06d3be1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,16 @@ venv/ .vscode/ # skbuild -_skbuild/ \ No newline at end of file +_skbuild/ + +# cmake +.ninja_deps +.ninja_log +CMakeCache.txt +CMakeFiles/ +CMakeInit.txt +CPackConfig.cmake +CPackSourceConfig.cmake +build.ninja +cmake_install.cmake +lib/ From adb80e2f171d3b8aa6238c09424154ada8c21a2d Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 22:13:26 -0700 Subject: [PATCH 41/68] add in pyvista to dev dependencies --- CONTRIBUTING.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 ++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4294571 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to Our Project + +Thank you for considering contributions to our project! Here's how you can help: + +## Setting Up for Development + +- We're using [scikit-build-core](https://github.com/scikit-build/scikit-build-core) for our build system. +- To set up the project in editable mode: + 1. Clone the repository: + ``` + git clone https://github.com/pyvista/pytetwild + ``` + 2. Initialize submodules: + ``` + git submodule update --init --recursive + ``` + 3. Install the project in editable mode and include development dependencies: + ``` + pip install -e .[dev] + ``` + +## Code Style and Quality + +- **Documentation**: Follow the `numpydoc` style for docstrings. +- **Linting**: We use `ruff` for code styling to ensure consistency. +- **Pre-commit**: Utilize `pre-commit` hooks to automate checks before commits. Set up your local environment with: + ``` + pip install pre-commit + pre-commit install + ``` +- **Testing**: Write tests for new features or bug fixes and run them using `pytest`. Tests are located in the `tests` directory. + +## How to Contribute + +- **Bugs and Feature Requests**: Submit them through our [issues page](https://github.com/pyvista/pytetwild/issues). +- **Code Contributions**: Make changes in a fork of this repository and submit a pull request (PR) through our [PR page](https://github.com/pyvista/pytetwild/pulls). Follow the standard fork-and-PR workflow for contributions. + +## Community and Conduct + +- We expect all contributors to follow our [Code of Conduct](https://github.com/pyvista/pyvista/blob/main/CODE_OF_CONDUCT.md) to maintain a welcoming and inclusive community. + +## License + +- Contributions are subject to the project's license as found in the [LICENSE.md](./LICENSE.md) file. + +We welcome your contributions and look forward to collaborating with you! diff --git a/pyproject.toml b/pyproject.toml index 9581868..7b4667f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,12 @@ description = "Python wrapper of fTetWild" readme = { file = "README.rst", content-type = "text/x-rst" } authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] dependencies = ["numpy"] -optional-dependencies = { dev = ["pytest"] } dynamic = ["classifiers", "urls", "license"] +[project.optional-dependencies] +all = ['pyvista'] +dev = ["pytest", "pre-commit", "pyvista"] + [tool.scikit-build] cmake.build-type = "Release" build-dir = "./build" From dc1f98c2856f872adb88cf3c5eb845342902b072 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 22:50:52 -0700 Subject: [PATCH 42/68] get version from metadata --- src/pytetwild/_version.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pytetwild/_version.py b/src/pytetwild/_version.py index 109edec..0a854c6 100644 --- a/src/pytetwild/_version.py +++ b/src/pytetwild/_version.py @@ -1,10 +1,4 @@ -"""Contains the pytetwild version. +"""Contains the pytetwild version.""" +from importlib import metadata -On the ``main`` branch, use 'dev0' to denote a development version. -For example: - -version_info = 0, 27, 'dev0' - -""" -version_info = 0, 1, "dev0" -__version__ = ".".join(map(str, version_info)) +__version__ = metadata.version("pytetwild") From ab408485bd8e7799dcac9892e18acb30e1742657 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 23:02:37 -0700 Subject: [PATCH 43/68] update contributing, test sdist --- .github/workflows/build-and-deploy.yml | 16 ++++------------ CONTRIBUTING.md | 11 +++++++++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 469beb9..91b56c4 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -21,14 +21,6 @@ jobs: with: submodules: true - - name: Build - run: pipx run build --sdist - - - name: Validate - run: | - pip install twine - twine check dist/* - - name: Install system dependencies run: | sudo apt-get update @@ -42,16 +34,16 @@ jobs: xorg-dev \ ccache - - name: Build wheel + - name: Build source distribution run: | - pipx run build --wheel + pipx run build --sdist - - name: Validate wheel + - name: Validate run: | pip install twine twine check dist/* - - name: Install from wheel + - name: Install from dist/ run: pip install --find-links=dist/ pytetwild[dev] - name: Test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4294571..d36cda0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,15 @@ Thank you for considering contributions to our project! Here's how you can help: ## Setting Up for Development -- We're using [scikit-build-core](https://github.com/scikit-build/scikit-build-core) for our build system. -- To set up the project in editable mode: +We're using [scikit-build-core](https://github.com/scikit-build/scikit-build-core) for our build system and building using cmake. You'll need a handful of system dependencies to build locally. On Linux, install them with: + +``` +sudo apt-get update +sudo apt-get install libblas-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libglu1-mesa-dev libsuitesparse-dev xorg-dev ccache +``` + +To install the library in editable mode: + 1. Clone the repository: ``` git clone https://github.com/pyvista/pytetwild From 3c44ad47925e77bc1af3bd1899158d6d3a1e7c84 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 23:04:25 -0700 Subject: [PATCH 44/68] add cibuildwheel --- .github/workflows/build-and-deploy.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 91b56c4..89169dc 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -13,6 +13,28 @@ concurrency: cancel-in-progress: true jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + + - name: List generated wheels + run: ls ./wheelhouse/* + + - uses: actions/upload-artifact@v4 + with: + path: ./wheelhouse/*.whl + name: pytetwild-wheel-${{ matrix.os }} + build_sdist: name: Build source distribution runs-on: ubuntu-latest From 547e8f8f0f219dac23ab016037fd18590706af82 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 23:07:05 -0700 Subject: [PATCH 45/68] add conditional linux apt packages --- .github/workflows/build-and-deploy.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 89169dc..7eaacdc 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -24,6 +24,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install \ + libblas-dev \ + libboost-filesystem-dev \ + libboost-system-dev \ + libboost-thread-dev \ + libglu1-mesa-dev \ + libsuitesparse-dev \ + xorg-dev \ + ccache + - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 From a1ffc71934fa0caa5fbd974ba91252d9a528c974 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 1 Mar 2024 23:13:24 -0700 Subject: [PATCH 46/68] submodules --- .github/workflows/build-and-deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 7eaacdc..cab4435 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -23,6 +23,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install system dependencies if: runner.os == 'Linux' From 05b13c761ba200633e39532d9c23a3e020472767 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 08:31:41 -0700 Subject: [PATCH 47/68] fix linux wheel build --- .github/workflows/build-and-deploy.yml | 14 -------------- .gitignore | 3 +++ pyproject.toml | 20 +++++++++++++++++++- src/pytetwild/pytetwild.py | 23 ++++++++++++++++++++--- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index cab4435..10e3b70 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -26,20 +26,6 @@ jobs: with: submodules: true - - name: Install system dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install \ - libblas-dev \ - libboost-filesystem-dev \ - libboost-system-dev \ - libboost-thread-dev \ - libglu1-mesa-dev \ - libsuitesparse-dev \ - xorg-dev \ - ccache - - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 diff --git a/.gitignore b/.gitignore index 06d3be1..6cbdde7 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ CPackSourceConfig.cmake build.ninja cmake_install.cmake lib/ + +# cibuildwheel +wheelhouse \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7b4667f..809e745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,21 @@ readme = { file = "README.rst", content-type = "text/x-rst" } authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] dependencies = ["numpy"] dynamic = ["classifiers", "urls", "license"] +classifiers = [ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Science/Research', + 'Topic :: Scientific/Engineering :: Information Analysis', + 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', +] + [project.optional-dependencies] all = ['pyvista'] @@ -27,9 +42,12 @@ testpaths = 'tests' archs = ["auto64"] # 64-bit only build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels -test-requires = "pytest" +test-requires = "pytest pyvista" test-command = "pytest {project}/tests -v" +[tool.cibuildwheel.linux] +before-all = "yum install gmp gmp-devel -y" + [tool.blackdoc] # From https://numpydoc.readthedocs.io/en/latest/format.html # Extended discussion: https://github.com/pyvista/pyvista/pull/4129 diff --git a/src/pytetwild/pytetwild.py b/src/pytetwild/pytetwild.py index 1e666f7..e75a225 100644 --- a/src/pytetwild/pytetwild.py +++ b/src/pytetwild/pytetwild.py @@ -1,11 +1,14 @@ """Wrapper for fTetWild.""" +import warnings import numpy as np -import pyvista as pv from pytetwild import PyfTetWildWrapper -from typing import Tuple +from typing import Tuple, TYPE_CHECKING +if TYPE_CHECKING: + import pyvista as pv -def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: + +def tetrahedralize_pv(mesh: "pv.PolyData") -> "pv.UnstructuredGrid": """ Convert a PyVista surface mesh to a PyVista unstructured grid. @@ -19,6 +22,20 @@ def tetrahedralize_pv(mesh: pv.PolyData) -> pv.UnstructuredGrid: pv.UnstructuredGrid The converted unstructured grid. """ + try: + import pyvista as pv + except: + raise ModuleNotFoundError( + "Install PyVista to use this feature with:\n\n" "pip install pytetwild[all]" + ) + + if not mesh.is_all_triangles: + warnings.warn( + "Input mesh is not all triangles. Either call `.triangulate()`" + " beforehand to suppress this warning or use an all triangle mesh." + ) + mesh = mesh.triangulate() + vertices = np.array(mesh.points, dtype=np.float64) faces = np.array(mesh.faces.reshape((-1, 4))[:, 1:4], dtype=np.int32) From bc7b9a09c4d48a8931a549ea40314a3bd774ddab Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 08:05:36 -0800 Subject: [PATCH 48/68] attempt windows build --- .github/workflows/build-and-deploy.yml | 16 ++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 10e3b70..0422720 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -26,6 +26,22 @@ jobs: with: submodules: true + - name: Setup Miniconda + if: runner.os == 'Windows' + uses: conda-incubator/setup-miniconda@v3.0.3 + + - name: Install Dependencies (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + conda install -c conda-forge mpir -y + + - name: Set env + run: | + echo "appdata=$env:LOCALAPPDATA" >> ${env:GITHUB_ENV} + echo "GMP_INC=C:\Miniconda\Library\include" >> ${env:GITHUB_ENV} + echo "GMP_LIB=C:\Miniconda\Library\lib" >> ${env:GITHUB_ENV} + - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 diff --git a/pyproject.toml b/pyproject.toml index 809e745..a651ca1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Python wrapper of fTetWild" readme = { file = "README.rst", content-type = "text/x-rst" } authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] dependencies = ["numpy"] -dynamic = ["classifiers", "urls", "license"] +dynamic = ["urls", "license"] classifiers = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', From bc4d34835020a18054eab03d4ae8a9a4db318f91 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 11:41:33 -0800 Subject: [PATCH 49/68] fix windows build --- .github/workflows/build-and-deploy.yml | 13 +++++++++---- .gitignore | 14 ++++++++++++++ CMakeLists.txt | 12 +++++++++++- src/FTetWildWrapper.cpp | 5 +++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 0422720..82a473a 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -26,17 +26,22 @@ jobs: with: submodules: true - - name: Setup Miniconda + - name: Setup Miniconda (Windows) if: runner.os == 'Windows' uses: conda-incubator/setup-miniconda@v3.0.3 - name: Install Dependencies (Windows) if: runner.os == 'Windows' shell: powershell - run: | - conda install -c conda-forge mpir -y + run: conda install -c conda-forge mpir -y + + - name: Install Dependencies (Windows) + if: runner.os == 'Windows' + shell: powershell + run: Get-ChildItem -Path C:\Miniconda -Recurse -Filter gmp.lib - - name: Set env + - name: Set env (Windows) + if: runner.os == 'Windows' run: | echo "appdata=$env:LOCALAPPDATA" >> ${env:GITHUB_ENV} echo "GMP_INC=C:\Miniconda\Library\include" >> ${env:GITHUB_ENV} diff --git a/.gitignore b/.gitignore index 6cbdde7..772b51a 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,20 @@ CPackSourceConfig.cmake build.ninja cmake_install.cmake lib/ +ALL_BUILD.vcxproj +ALL_BUILD.vcxproj.filters +INSTALL.vcxproj +INSTALL.vcxproj.filters +PACKAGE.vcxproj +PACKAGE.vcxproj.filters +PyfTetWildWrapper.dir/ +PyfTetWildWrapper.vcxproj +PyfTetWildWrapper.vcxproj.filters +ZERO_CHECK.vcxproj +ZERO_CHECK.vcxproj.filters +fTetWildWrapper.sln +x64/ +Release/ # cibuildwheel wheelhouse \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cb65d7c..95a833f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,5 +22,15 @@ target_include_directories(PyfTetWildWrapper PUBLIC ${fTetWild_DIR}/src) target_link_libraries(PyfTetWildWrapper PRIVATE FloatTetwild) target_compile_features(PyfTetWildWrapper PUBLIC cxx_std_11) -install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ./pytetwild) +if(WIN32) + set_target_properties(PyfTetWildWrapper PROPERTIES SUFFIX ".pyd") + foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG_UPPER) + set_target_properties(PyfTetWildWrapper PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG_UPPER} "${CMAKE_CURRENT_SOURCE_DIR}/src/pytetwild" + LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG_UPPER} "${CMAKE_CURRENT_SOURCE_DIR}/src/pytetwild" + ) + endforeach() +endif() +install(TARGETS PyfTetWildWrapper LIBRARY DESTINATION ./pytetwild) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index 5c14fb7..20380c6 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -154,8 +154,9 @@ PYBIND11_MODULE(PyfTetWildWrapper, m) { // function GEO::vector vertices_vec = array_to_geo_vector(vertices); GEO::vector faces_vec = array_to_geo_vector(faces); - auto [vertices_result, tetrahedra_result] = - tetrahedralize(vertices_vec, faces_vec); + auto result = tetrahedralize(vertices_vec, faces_vec); + auto vertices_result = result.first; + auto tetrahedra_result = result.second; py::print("Tetrahedralization complete."); // Convert results back to numpy arrays From c321c9e02d532996a745ccd0d28a12261f99e532 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 11:44:04 -0800 Subject: [PATCH 50/68] fix library path --- .github/workflows/build-and-deploy.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 82a473a..0316058 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -35,17 +35,12 @@ jobs: shell: powershell run: conda install -c conda-forge mpir -y - - name: Install Dependencies (Windows) - if: runner.os == 'Windows' - shell: powershell - run: Get-ChildItem -Path C:\Miniconda -Recurse -Filter gmp.lib - - name: Set env (Windows) if: runner.os == 'Windows' run: | echo "appdata=$env:LOCALAPPDATA" >> ${env:GITHUB_ENV} - echo "GMP_INC=C:\Miniconda\Library\include" >> ${env:GITHUB_ENV} - echo "GMP_LIB=C:\Miniconda\Library\lib" >> ${env:GITHUB_ENV} + echo "GMP_INC=C:\Miniconda\envs\test\Library\include" >> ${env:GITHUB_ENV} + echo "GMP_LIB=C:\Miniconda\envs\test\Library\lib >> ${env:GITHUB_ENV} - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 From b03e87c3817d73f76bf5ff91361c407fe3190a7c Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 11:53:40 -0800 Subject: [PATCH 51/68] fix contributing; fix env var windows ci --- .github/workflows/build-and-deploy.yml | 2 +- CONTRIBUTING.md | 30 ++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 0316058..4edc7c6 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -40,7 +40,7 @@ jobs: run: | echo "appdata=$env:LOCALAPPDATA" >> ${env:GITHUB_ENV} echo "GMP_INC=C:\Miniconda\envs\test\Library\include" >> ${env:GITHUB_ENV} - echo "GMP_LIB=C:\Miniconda\envs\test\Library\lib >> ${env:GITHUB_ENV} + echo "GMP_LIB=C:\Miniconda\envs\test\Library\lib" >> ${env:GITHUB_ENV} - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d36cda0..1390efe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,13 +4,39 @@ Thank you for considering contributions to our project! Here's how you can help: ## Setting Up for Development -We're using [scikit-build-core](https://github.com/scikit-build/scikit-build-core) for our build system and building using cmake. You'll need a handful of system dependencies to build locally. On Linux, install them with: +We're using [scikit-build-core](https://github.com/scikit-build/scikit-build-core) for our build system and building using ``cmake``. You'll need a handful of system dependencies to build locally. + + +### Linux +On Linux, install the following OS dependencies with: ``` sudo apt-get update -sudo apt-get install libblas-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libglu1-mesa-dev libsuitesparse-dev xorg-dev ccache +sudo apt-get install libblas-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libglu1-mesa-dev libsuitesparse-dev xorg-dev ccache -y ``` +### Windows + +For contributors using Windows, we also have a setup process to ensure you can compile and test changes locally. Here's how to set up your development environment on Windows: + +1. **Install Miniconda**: We use Miniconda to manage dependencies. Install it from [here](https://docs.anaconda.com/free/miniconda/index.html). + +2. **Setup Miniconda and Install Dependencies**: + ``` + conda install -c conda-forge mpir -y + ``` + +3. **Configure Environment Variables**: + Set the required environment variables for the build. This assumes PowerShell. + + ``` + $Env:GMP_INC = "C:\Miniconda\Library\include" + $Env:GMP_LIB = "C:\Miniconda\Library\lib" + ``` + + Note: You may need to adjust these paths depending on if you use a non-global conda environment. + +### Installation To install the library in editable mode: 1. Clone the repository: From d93eed68a5187575d02412caf7dbaa83878fdfb0 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 13:16:03 -0700 Subject: [PATCH 52/68] clean before build on windows --- CMakeLists.txt | 7 +++++++ pyproject.toml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95a833f..c4fa6a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,13 @@ cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) +# https://github.com/wildmeshing/fTetWild/issues/10 +if(UNIX AND NOT APPLE) + # Use -mavx flag for C and C++ compilers instead of -mavx2 + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") +endif() + # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") diff --git a/pyproject.toml b/pyproject.toml index a651ca1..5c622c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,13 @@ test-command = "pytest {project}/tests -v" [tool.cibuildwheel.linux] before-all = "yum install gmp gmp-devel -y" +[tool.cibuildwheel.windows] +before-build = "python -c \"import shutil; import os; build_dir = 'build'; shutil.rmtree(build_dir, ignore_errors=True); os.makedirs(build_dir, exist_ok=True)\"" + +[tool.cibuildwheel.macos] +before-all = "brew install suite-sparse ccache gmp" +archs = ["universal2"] + [tool.blackdoc] # From https://numpydoc.readthedocs.io/en/latest/format.html # Extended discussion: https://github.com/pyvista/pyvista/pull/4129 From e713d52a3e878c0cf47a015ab4367728e0b2052f Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 13:01:57 -0800 Subject: [PATCH 53/68] fix mac build --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c622c0..88c81c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,11 +49,11 @@ test-command = "pytest {project}/tests -v" before-all = "yum install gmp gmp-devel -y" [tool.cibuildwheel.windows] -before-build = "python -c \"import shutil; import os; build_dir = 'build'; shutil.rmtree(build_dir, ignore_errors=True); os.makedirs(build_dir, exist_ok=True)\"" +before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; try: os.remove(file_path) except FileNotFoundError: pass\"" [tool.cibuildwheel.macos] before-all = "brew install suite-sparse ccache gmp" -archs = ["universal2"] +archs = ["x86_64"] [tool.blackdoc] # From https://numpydoc.readthedocs.io/en/latest/format.html From 77d53a514e45d2be6ae09770200247a757fb0aee Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 13:34:08 -0800 Subject: [PATCH 54/68] fix before-build command for windows --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88c81c5..8e77605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ test-command = "pytest {project}/tests -v" before-all = "yum install gmp gmp-devel -y" [tool.cibuildwheel.windows] -before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; try: os.remove(file_path) except FileNotFoundError: pass\"" +before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; os.remove(file_path) if os.path.exists(file_path) else None\"" [tool.cibuildwheel.macos] before-all = "brew install suite-sparse ccache gmp" From 9b8c8619a3b3b9a553a4e663a04dddfa7107d6fe Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 14:47:18 -0700 Subject: [PATCH 55/68] attempt source dist fix --- CMakeLists.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c4fa6a0..02bcd83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,16 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) # https://github.com/wildmeshing/fTetWild/issues/10 if(UNIX AND NOT APPLE) - # Use -mavx flag for C and C++ compilers instead of -mavx2 - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("-dumpversion" COMPILER_SUPPORTS_DUMPVERSION) + if(COMPILER_SUPPORTS_DUMPVERSION) + execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) + if(GCC_VERSION VERSION_LESS 9.0) + # Use -mavx flag for C and C++ compilers instead of -mavx2 + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") + endif() + endif() endif() # Set the path to the fTetWild project From 1860e91174e486956e7fd36e35c0e20a7573235f Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 15:05:39 -0700 Subject: [PATCH 56/68] drop macOS build --- .github/workflows/build-and-deploy.yml | 2 +- CMakeLists.txt | 2 +- pyproject.toml | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 4edc7c6..7a6dd0e 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 02bcd83..c771431 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ if(UNIX AND NOT APPLE) CHECK_CXX_COMPILER_FLAG("-dumpversion" COMPILER_SUPPORTS_DUMPVERSION) if(COMPILER_SUPPORTS_DUMPVERSION) execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) - if(GCC_VERSION VERSION_LESS 9.0) + if(GCC_VERSION VERSION_LESS 10.0) # Use -mavx flag for C and C++ compilers instead of -mavx2 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") diff --git a/pyproject.toml b/pyproject.toml index 8e77605..e7cb271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,6 @@ before-all = "yum install gmp gmp-devel -y" [tool.cibuildwheel.windows] before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; os.remove(file_path) if os.path.exists(file_path) else None\"" -[tool.cibuildwheel.macos] -before-all = "brew install suite-sparse ccache gmp" -archs = ["x86_64"] - [tool.blackdoc] # From https://numpydoc.readthedocs.io/en/latest/format.html # Extended discussion: https://github.com/pyvista/pyvista/pull/4129 From a57cbcf16cb993c164cea28b2812edcdc30ac0db Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 15:22:56 -0700 Subject: [PATCH 57/68] move env settings to gh actions --- .github/workflows/build-and-deploy.yml | 2 ++ CMakeLists.txt | 14 -------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 7a6dd0e..8faf4d4 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -44,6 +44,8 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 + env: # https://github.com/wildmeshing/fTetWild/issues/10 + CIBW_ENVIRONMENT_LINUX: CMAKE_C_FLAGS=-mavx CMAKE_CXX_FLAGS=-mavx - name: List generated wheels run: ls ./wheelhouse/* diff --git a/CMakeLists.txt b/CMakeLists.txt index c771431..95a833f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,20 +2,6 @@ cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) -# https://github.com/wildmeshing/fTetWild/issues/10 -if(UNIX AND NOT APPLE) - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("-dumpversion" COMPILER_SUPPORTS_DUMPVERSION) - if(COMPILER_SUPPORTS_DUMPVERSION) - execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) - if(GCC_VERSION VERSION_LESS 10.0) - # Use -mavx flag for C and C++ compilers instead of -mavx2 - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") - endif() - endif() -endif() - # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") From 0c4583344490800ae3f768c75900bc6c513169b7 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 15:36:10 -0700 Subject: [PATCH 58/68] pass var on linux --- .github/workflows/build-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 8faf4d4..c4b3df1 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -45,7 +45,7 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 env: # https://github.com/wildmeshing/fTetWild/issues/10 - CIBW_ENVIRONMENT_LINUX: CMAKE_C_FLAGS=-mavx CMAKE_CXX_FLAGS=-mavx + CIBW_ENVIRONMENT_PASS_LINUX: CMAKE_C_FLAGS=-mavx CMAKE_CXX_FLAGS=-mavx - name: List generated wheels run: ls ./wheelhouse/* From 1ef51863b2ee1d3e22ac9378cdbba15786206201 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 2 Mar 2024 15:48:06 -0700 Subject: [PATCH 59/68] try gcc < 11 --- .github/workflows/build-and-deploy.yml | 2 -- CMakeLists.txt | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index c4b3df1..7a6dd0e 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -44,8 +44,6 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 - env: # https://github.com/wildmeshing/fTetWild/issues/10 - CIBW_ENVIRONMENT_PASS_LINUX: CMAKE_C_FLAGS=-mavx CMAKE_CXX_FLAGS=-mavx - name: List generated wheels run: ls ./wheelhouse/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 95a833f..15991b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,20 @@ cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) +# https://github.com/wildmeshing/fTetWild/issues/10 +if(UNIX AND NOT APPLE) + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("-dumpversion" COMPILER_SUPPORTS_DUMPVERSION) + if(COMPILER_SUPPORTS_DUMPVERSION) + execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) + if(GCC_VERSION VERSION_LESS 11.0) + # Use -mavx flag for C and C++ compilers instead of -mavx2 + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") + endif() + endif() +endif() + # Set the path to the fTetWild project set(fTetWild_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/fTetWild") From 486009ff6e1bc392ac514aace4261458588f0450 Mon Sep 17 00:00:00 2001 From: Daniel Pak Date: Sun, 3 Mar 2024 14:23:53 -0500 Subject: [PATCH 60/68] output reproduces default settings in fTetWild - preparing for switches --- src/FTetWildWrapper.cpp | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/FTetWildWrapper.cpp b/src/FTetWildWrapper.cpp index 20380c6..357c25e 100644 --- a/src/FTetWildWrapper.cpp +++ b/src/FTetWildWrapper.cpp @@ -2,6 +2,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -28,6 +32,19 @@ extractMeshData(const floatTetWild::Mesh &mesh) { std::vector> vertices; std::vector> tetrahedra; + // Remap tet indices for removed vertices + std::map old_2_new; + int cnt_v = 0; + const auto skip_vertex = [&mesh](const int i) { + return mesh.tet_vertices[i].is_removed; + }; + for (int i = 0; i < mesh.tet_vertices.size(); i++) { + if (!skip_vertex(i)) { + old_2_new[i] = cnt_v; + cnt_v++; + } + } + // Extract vertices for (const auto &vertex : mesh.tet_vertices) { if (!vertex.is_removed) { @@ -41,7 +58,8 @@ extractMeshData(const floatTetWild::Mesh &mesh) { for (const auto &tet : mesh.tets) { if (!tet.is_removed) { tetrahedra.push_back( - {{tet.indices[0], tet.indices[1], tet.indices[2], tet.indices[3]}}); + {{old_2_new[tet.indices[0]], old_2_new[tet.indices[1]], + old_2_new[tet.indices[2]], old_2_new[tet.indices[3]]}}); } } @@ -125,16 +143,61 @@ tetrahedralize(GEO::vector &vertices, GEO::vector &faces) { input_faces[i] << sf_mesh.facets.vertex(i, 0), sf_mesh.facets.vertex(i, 1), sf_mesh.facets.vertex(i, 2); - std::vector is_face_inserted(input_faces.size(), false); + Parameters ¶ms = mesh.params; + if (!params.init(tree.get_sf_diag())) { + throw std::runtime_error( + "FTetWildWrapper.cpp: Parameters initialization failed"); + } + const size_t MB = 1024 * 1024; + const size_t stack_size = 64 * MB; + unsigned int max_threads = std::numeric_limits::max(); + unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency()); + num_threads = std::min(max_threads, num_threads); + params.num_threads = num_threads; + + std::vector input_tags; + if (input_tags.size() != input_faces.size()) { + input_tags.resize(input_faces.size()); + std::fill(input_tags.begin(), input_tags.end(), 0); + } + bool skip_simplify = false; + simplify(input_points, input_faces, input_tags, tree, params, skip_simplify); + tree.init_b_mesh_and_tree(input_points, input_faces, mesh); // Perform tetrahedralization + std::vector is_face_inserted(input_faces.size(), false); std::cout << "Starting tetrahedralization..." << std::endl; FloatTetDelaunay::tetrahedralize(input_points, input_faces, tree, mesh, is_face_inserted); std::cout << "Tetrahedralization performed." << std::endl; + insert_triangles(input_points, input_faces, input_tags, mesh, + is_face_inserted, tree, false); + optimization(input_points, input_faces, input_tags, is_face_inserted, mesh, + tree, {{1, 1, 1, 1}}); + correct_tracked_surface_orientation(mesh, tree); + + // filter elements + if (params.smooth_open_boundary) { + smooth_open_boundary(mesh, tree); + for (auto &t : mesh.tets) { + if (t.is_outside) + t.is_removed = true; + } + } else { + if (!params.disable_filtering) { + if (params.use_floodfill) { + filter_outside_floodfill(mesh); + } else if (params.use_input_for_wn) { + filter_outside(mesh, input_points, input_faces); + } else + filter_outside(mesh); + } + } + std::cout << "Tetrahedralization completed. Extracting mesh data..." << std::endl; + return extractMeshData(mesh); } From 7a0ed1bc7fa0e07392d59c8c57c8eb147fe845cb Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 13:20:36 -0700 Subject: [PATCH 61/68] fix compiler settings --- CMakeLists.txt | 15 +++------------ pyproject.toml | 5 ++++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 15991b0..abead66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,18 +2,9 @@ cmake_minimum_required(VERSION 3.15...3.26) project(fTetWildWrapper) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ./src/pytetwild) -# https://github.com/wildmeshing/fTetWild/issues/10 -if(UNIX AND NOT APPLE) - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("-dumpversion" COMPILER_SUPPORTS_DUMPVERSION) - if(COMPILER_SUPPORTS_DUMPVERSION) - execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) - if(GCC_VERSION VERSION_LESS 11.0) - # Use -mavx flag for C and C++ compilers instead of -mavx2 - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") - endif() - endif() +if($ENV{USE_MAVX}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx") endif() # Set the path to the fTetWild project diff --git a/pyproject.toml b/pyproject.toml index e7cb271..33430e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,13 +40,16 @@ testpaths = 'tests' [tool.cibuildwheel] archs = ["auto64"] # 64-bit only -build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels +# build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels +build = "cp311-* cp312-*" # Only build Python 3.8-3.12 wheels skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels test-requires = "pytest pyvista" test-command = "pytest {project}/tests -v" [tool.cibuildwheel.linux] before-all = "yum install gmp gmp-devel -y" +environment = "USE_MAVX='true'" +environment-pass = ["USE_MAVX"] [tool.cibuildwheel.windows] before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; os.remove(file_path) if os.path.exists(file_path) else None\"" From e7a2f2a363e5691f9d7aba884954e7542ef80369 Mon Sep 17 00:00:00 2001 From: Daniel Pak Date: Sun, 3 Mar 2024 18:25:32 -0500 Subject: [PATCH 62/68] adding unit tests for output mesh accuracy --- pyproject.toml | 4 +-- tests/test_data/test_surf.ply | Bin 0 -> 853 bytes tests/test_data/test_tets.msh | Bin 0 -> 239721 bytes tests/test_pytetwild.py | 46 +++++++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/test_data/test_surf.ply create mode 100644 tests/test_data/test_tets.msh diff --git a/pyproject.toml b/pyproject.toml index 33430e1..fe65818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ [project.optional-dependencies] all = ['pyvista'] -dev = ["pytest", "pre-commit", "pyvista"] +dev = ["pytest", "pre-commit", "pyvista", "scipy", "meshio"] [tool.scikit-build] cmake.build-type = "Release" @@ -43,7 +43,7 @@ archs = ["auto64"] # 64-bit only # build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels build = "cp311-* cp312-*" # Only build Python 3.8-3.12 wheels skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels -test-requires = "pytest pyvista" +test-requires = "pytest pyvista scipy meshio" test-command = "pytest {project}/tests -v" [tool.cibuildwheel.linux] diff --git a/tests/test_data/test_surf.ply b/tests/test_data/test_surf.ply new file mode 100644 index 0000000000000000000000000000000000000000..8cf7626e28f3d2536757a2b9a2fdadbd86c12ec2 GIT binary patch literal 853 zcmaKo!A` zEc?%wnScImq3bxemVxvV9_HJ}0rdu03VUyj0@TMrHaPh>C_v%( E16OIk!vFvP literal 0 HcmV?d00001 diff --git a/tests/test_data/test_tets.msh b/tests/test_data/test_tets.msh new file mode 100644 index 0000000000000000000000000000000000000000..9c8a3c8f85b8e8b5d30afbdfb61ad44829f5fb3a GIT binary patch literal 239721 zcmZ_Wc|4Tg+cpi)U)*%A>^BuOfg7F!{eiWG`Mltdeql1jF*T}qZg zmKn>8ZJ4psB83+GuFv=T?Rh=F-|ubeQkeu$x!~c!sb~qija}!fpy>9itn7*;ae|-cS_@Y` zeHe`q!R%?{YB(^HY<`(a?abnLz&uo5W_qjvo#k%mS}tsaS!6@+jGv72ZvvCB*d0mG z4)G$AH?CfNjI+tU4TQ-=h&RHH&SUzl(k>`%F;Rc^E*s~N!E9lx^_+ST4*x7A8&Quv z?2fcornMkRwxZOeDDo^H?{-SLG`{5EgPgBIzv2pUE}8!+Vej@Yd@R24=y|JKEl$kg zF;6HR!+B(iQM1(>#tHC>bVpj}3@&ONh|3t)RtQpLD_iw=+C6+6>~}TTo?eI_c!^ug z2e>Fr=K1}L2Rnt2Es56{WtvP3oG3BKvzkQ0Pj>TP^;Tfj9W|J9Tr`@ z3uVZ>CCfkQZ5H5r-|srB_vNDkTPBvZoe8pJ%d-snzt;0{p@X#EXS)I{XEr%r3FN>6 zveO?&<~{S{<6OHf9`mJK@H#Hn5INj}3&}>42A}_Y%g4`U4^zr@cWU(kSCk9BlpdmL_Xd#s8^LgX$Wf$)s0>e<-!s&>6kL7 z!B{>nU1@!-JD?C`UT;|0yOVBD?CL(%^EEkCUhF8WVrB7A}mgs9&I1h|9^0Hw@fMplvD+ zUAF1B;X>+>r>diJTW|%L<;;NbCVhOYjZYV?NU8;uuqm2_Ma8g^EM|QII}17RB0Os2 z%nLPGFS(D?b*vRvk=bzXwC+A$1;r8i=C-m;*i#t%-aVfW%4Dmw?X}jw<>LO^%!O?2 zPL!zN6ev&Lk1Aw~KI!eL3*=yP`qG=*r3JXGXwj0a6NRWsHdQI8;>L}7JlZ8H(lM}_%q!3@`%g$Me)ag_Qbtx_XZtksP64bTYYr8U|FM;e)why$eth5}_i}r9 zj6Mg~k~wZJ(JMd7#k5*InFXaB?7VU+eb?F|R3p2(%jxE^U3F-`@XV?4uQ=FIm8sLk z_Jwt1UAt#L(a+|flJ+dOOlJ<>kMdkqkjaGgWQDty?b?^m!7;}cACwX2V&vA~CmX)k z!UnRA?@}v5t!pvgc=qZ3YY0yEfl-qkt`{Qa;!QKSfu-ah(h!D}vV zBr`k}+i-{jKf4(d$!XJbC*9^E9qXya3`%m^5oaSOsy1(*eAts()_qL@`)*d#I z^%Rb|8@rQ>13N@&EsBfq!5ty9UALLAnGC=CT|HdE#b*{bxF?<#V$$5+@_Vve_$Nr$ zj^5JqH{hbXd_lq0`4k)7$2u-L#D^_pCtL6DWRK_K2TPHG7X{Y%s(h{HeBnCOBs-gZ z(sa5P7w;apDm~_<70j4g+^c8SZ=J263rEcVKh^H$TiXc*(HD8k+XN%5;HOWtrmmn`>l$b8+U zTnvu#RZ|RA#K>=3$|8OFu$4?Wbp5{lx?Bu2l=jRyQV4gqPWHKTpO1QE&e-(^zLzS8<|VW@ObIZ9L&x7@F{W*6GRxvYuRI%s880AW5)KP>mn=r z;M<=^Oel#qO0`>l2n@(RzOzq!w1kVIK0fy}beQlMFYR1(nF-s;-fAtmHCu{{l5LXf zcE=Qf;=4Qcja?itB%80zw9@V7V4lxOu8kfO^3RotMN9L+i0tBs!Abik92|N`&nOAxezNo=i{7)A~YuZbxgnDQ!58~gY&kf9OZ)l zhsZ-!52v9C*}S95gHeyUD0Xw!Ro4K*o(on)Uzf&(=V)M3nptBZE5;lPCh zF^{8F>cEuDXs!Iv$5*vjls4L%eLw&h5Yz2fQiNt?Sy$MPeM&g!T|d5$RmO);t)3~P zw>)7Nnb;2A;XAW9D6c!%&iuiL9DDuKl?8>co6PKb(y(?*9flb1&vy0e0A;;3`=BcZ z&B=;pUtTKN(FzIsmgveTGI5dfkgyID!Gdhk;Lk~$D)L}VZ`m|adoH|~_$U5}A0PIR z_2qAh4(=$$4BmbfxtxehNa_0&V?t7i^z1u)++=U2Xk}D~d&!ng zm-u4gRfsR5Qlqg?fPBIETW#TmXhn8mpLP60;S$`k?9v}>umjbK z@w-r?t=&s5KAyRMzRC(N+LA4o5ZU}=RtYZb%{AY&iHlO=A2b(Qmf%4$XXmHEPcAVr z&EZ2TPoIldSI63A>lwizvYPq3cRCIgp_oN`%IPs&G`Ryx%Hd2nOg3z2HCchK8BcPC zf^HQTGaoyDcwN8+JF?61V-6m1D@Ly))0Q6iK{0Zwo$<0&e6S}QSibu7z2`OHvF(F> zxqA)%zH`V$g3}B~$ZX1^KYN@hL5tarqgUwOGR~@B3%x2p2eNw^$%7O7n3x%IEAO!s z6EA!$P&z}g?rf0MVFZv=df}{oj()9 zWc9DeFHpl{WS7rtnAYB5qD(U{WZ6k3I^X&Eh2g=3<79IJcc1_3ZbdlS-5zm1t_W8(6drT9D}a+^3d_<~i)7G#HlA_+m2H5(D?Rjb zP72V8?4*g4)77U;bgQx2H4v~Bl&c=LwH9&FnauE$%i&LIE6!N;m&f|!WvE$zG+^Kfo&orBfY#pp_QNZ+nd`z{mt(yI1K6P0k&lpiuO0Zecs zV^|wINSH8jpAFNaTVyX>Sev3)yMqg-$R3Vg@xJ>=A;w!Bbq}l9iruSTb02ROz-cl? zG3hbvN3=h^vGXNmnXo$GMcxi4iu+`u?J>KJ4lwb-NXMedrzoCf9vE_TWugb!4}F8N z^RF}U^3`|eCtqPg`c7Ate1-yek{PyNPjoxQ#Gq~bUpdZ9_~bb*eW5E8&XC#cbMYRg zd)m6+{lxiuOqjH3rf;(n7re+cQ=%T8DWK~h95&|q>H^Se>iaVO86VD)-5H+mtdZ3W zJH}|G2S~DEjjiixVVn1Oj%>HFe6@iIm&i`39rZXNUxadc(P80q z{eAqfw0n9T_>eu7Ty;Q`>U8kNs?fsg94K#b5Bzdb0KR03LEqK+TZ^#&p6`^-Xg-Xy zYB+d3o`aXkR5#~8p1Pfhso(5ct|$tieX7#$({IATk8G_b&%r&X2rr0r&x?@Y!$!wB zs|Pk0!4)#8+UbYlGN@l2<)$NP!H0eJ`y@|jbK&Yg_PM{>ePS~{_0}mLZ&QrMTldt> zV>hEenRL}>?cj%1a6DaKYj;R9e!tQrJSCiq0b~eghBgoE$GU%_WuJMdt_x$DnP zAHzVhgRI@bbZ}syl65!MA&PhB5l^-&xCrdq7%3Q-Q>sHFS5A$)&9xeZ$d7%(Y zb}`;CXpG_<_n`rbzY=X^10UDo^dQdLLft&yA zk6U!(-BJlpv)sMfjmO}67;e^*21k!C)IlBqn< zGHj^5h5RWKeZtps(W!@*6w*_NVPsmKt~-}V>VeKh8Ko=wbtv(f_i*|9LcB%hAQB+{ zL&FTGOBCFHM0M+>*KnK`m_s;Oy6HrI_no>>+l}gm}wQYdV$WU9db*C zIm$$X`eWvkzcVqK?5&6*aW>h5j>Yc&NLn)rO_zc~*6|uo{>fjlfx#DY%|IhOjmmK`}a)RMmyw0EI zD}0D2n<8+QK5pa-mF06=---ud^3E~V(B1*h$@(AV`zmZ^L)htn^*TBntVvR@+!)En z7i3zcc5dx!gP`HF+DV`*2gGv2a0!kdYSWXmljzph``zgaG3&Jq`2Cg`d3oc`{nim%Aj z!)0G;vxy0{&c0%p$w8L}bcx`E4-Kti1QbasenG8tKtrQ3Tm!=k5grHXljHgDz6_Umn)M zW{DxTYi$eUkXgJO=5!0%FyOx3jSIp|IH}bC{D^rrAX$pNp`1oTA=I-?ELXL6V9&iH zUYARKAeSsNVf$(=yH2R#?3Z32@evm&v5z=CD};Asqt(umVKt=?&AiZAZp+6FX`b&; zk%RBazP({?AIhRW*T`h6qZbAExb?(Do=^$qk*#j)JbB{+6B0l?W>+yERjv2E9o&5Y zK9Ie?rQ|;Rs1zO=o1H@r<(cLO#(sQ4<0P`i)wu@-sE=_e@rc*RMLxQ$TeM5plmi81 zR`&4^8iWeq(&ob=8CF$@<;DJY7O{@^p~mxDe`VX8cysmrCy1TtVi(>`rFX&0<(~ zXs_Zl+hW{%tAD@i!eXc-vkEZKSE7Dd=#ikSTMjYt^TB)V{?s>OkxiPoDa7$`F_@lH zjvbOO#)Z7^t8y+0fK6r)vp%NlR0-t0X7-#Iy$?J`!RFy@0<0ptX?nMZwU-HL{l*S& z*=o?6e0hV_Dayx??QZ;16F_lgHp)gVs+$OM!Umse>3D0%PPprPZMw#U98JB59inPr zaMHB>?o}-#YKgWea?ZEHEVivgDX&I{_z>C*)32dE>`4$MAbPz<6!*(QtaA^fJ5&VQ)*RH^PVJY!Ygpn! z5iT~8ZLsw-F*?bF@9uq1swtoGwN3r5!wDuf{j)#eyQPCqnXp&JMd?r!6NFVfG>pZp zkw+GuE^NYG&V-9?K9V6xN3ibM5N)4j5CaY~Ja(NZl0?*VVZyJ9N?4^Nv$6pD$XLdsD!HC>a6reft!mX$0jk=M{yETD2>oQ|%k9;3k~#46%JVKuYYsjv z-81XRGA`u^goO4E4=vEWzy<3cKZUy~k9OPAd;0A2O#DG6I%~Yvsy+_-*g+e=5UwOkOJuOFLG6%)o#$sL^&{I2P@BOV1-?9p$*V8%u zArm>BG%)UXEi5zLS-?7)k2X;yd4g&_43oV#%eDMMZGLZ6)c$}sflyZT%>EmV6-LO| z&%V4cIl+ZTj+34bTwe^Gca<78QVwvGY^>1Sz5QNXs8`CIF!sbIoaBDx`d2e13XP#D zz$@(&ABJ%u+9}uabfq@tcCQDmUB)nm>_^zDiHV+E7&x^&O#Q=Yh)ittw3BASShCQO zv`0IaaUdW}*X&Mz5JZ-}$uRs{3&Q`{j`QkTja-m3Y3J66dST14R?DJZK8TPx*>K{{ zFQB^B^!-k^Yd-MCxoO8==HocBZRtO+o_olJnm-nG*V>CAAs)rmLPv))L`f)-N z7o@|LXEh#ULhFU~?5lf=aRS+?ict&i33Z^|@c!Z%>0sJ61$*S5*<1+8n!bY9;568WWAhP-MqYyi~pJWs6s?9mRtqvqq zLQX7s_5r>ekgi@bTL6>DQYFfCCfw)1uA72^h`k&T9C42)WX$-BADO%AU?#KeF*l#a%nMGjlZX6q3R&RUsv$k;IuMy@8f&7< zff^;>R1;1SW1!`Euz``IhK>9(z4)~odXdtW5XAmcp}U9gSH#9VaV^Rko=nruna zHF{+rLAK!I<}drP7L-%1M*4nrK)*#t@9|GAIFoFbCC_WZ*czC*PrfjCQWYA;4R5|G z(gCx`&dJwV_Q$orrq8RmiyZmr>slTa_dWz?lj$wL=w&v64}Z?=eXbi^i=Ok>m@K|W zc?Yr!1~a6V)3FN{XGVle*W%{0OHx%89)Kj-N(bTbp3(wPUoG`s#gU1*xo-2@slPjy ztYOwjXx|DxOi&4ZYdD^X*IvEZAxd+f^T-s;4pzu_^FccDmd4S;MOemJG1Yzg5tJfx zE$|6hBP4*0ua~6Pjuv9S?d^{bmUBRwj9QEEw5xn@H<a>E730(vM9_s@D8md3nk;=>93hvRaWxj>P zb7_2d&>eZaZB96>UvprW53>jskgb#X`Q1WS00m!N^P_F8;l$)2$Mu3*SV$IJlDSEHOeEmHs{-e(FXFQ!~q>?g%}jx_&A)&)l-Z%h`z zy6Fe0?Jt7$V-8IbU+V*MWZTmgKANg2fFY;((#omSADGxU@A|?5kSB9k@w@xiNdZhb zbN3m;A|IZ5^(@E>r@BM7^_+E|_w`pq1j=+yJ zH*{r8)$F?kph&iRVabE($_#vV3*J0Se>$H za;%Swoj_Pdw&+MpciiqYxI6ZkK{VxE%-R|oO{BU}iEM}3dAr?nJAu;I$3#jEVbLB< z{3Lr3my;bD+QR0mbwd2gaD(xu9KbL}^~b$499Tit>!Nq@mTfEc#tqDQFuN2ZTQj(` zVw+(lnJs7iH~B+#KrysIQ|AdBaW&x|UD=MS$h5b%O-S3#f+Uuw!_C@O@Ge~#`Mjze zmB~K8j*O}BYyg>&eCjo9mx{ zS%Gdba}7V8>q3*6hr_+)-@^tnr?sa8G%u#Y{4n|FQ-+(tj&sOnW4-{@$u_D!&vVdC z#YFwLW&@U0_~)g(4c0iy4zTWKqmtX36-MV+xS6asb)l=1I1g@I z7;CyRvJHE~lFZ(`Dghviy)ag;{9_BOofx=g&lUmRZ}>Pr;d=>eAu}B`K0IHS2m7WU z7(9BNi+?|BlBt;*zSnwC1EY+i9eMF=Z2u`Y$+*28waENrU+&Sa;KH@1GZ)6)7Jz-o zQu#S^YEheP^ZSxJO$D{EGCZpH^s*MTml!sx`_%|KWWFHh$i+x86#}d3Og@Wj9A97U@O`A#M3$%I|Xpde^s`@I~KalYdh2S zrU~@ODw*OAp5|rfs2kdsEnNc3s!zEp92MX;vQhS>9xcOWuy+a$d%LI=9KPlFI?=4M zK3Q{ZV29~-{<+Qkqt@1Ul;5%euq;-Jo&3nbl`Tf zqq$Wbd2=-6E??fqlrgZOuh4Zc_Qx=lhHWJIPqu2L&;g zYoPmX-Zx))7mG#^dzt>uhxnm`V zsx_Ly=zZSd-PHozMK(IqSH5~`Y}h}o6{H2?!sq^!;chaan;k5nIsxd4syhVU zE8VOSRDlKA)x?IU9E)mrgAb0>O|8VV zD-YK_TO`0eWJAABTF%>h6&!Ru4n|tGpligD+o_Ulv?Obu6H~H7KMQ7EyZq*ZO$)YP zA0PH3f{S~}e(reHawfYOULLzKt7k0_8*_w`ew->sE3(CdQQ7%3`QQ-WZP_bag{BUT z-OIapxQ}dQe*KXL0nH$CnV-3v%>_kq*%vi26=?m>{urg5g7@vXLn>$XBv&)=+ETw@ zt$8u*CwtarpgXgG1AjKD94fyhfCDk6$$d*WU_&;fGX2VX$#zs;xB7jbO99R0A3Q2y z-V6uG#;)8Fp|*>S+jN9)ruDX=^^&hSmBnwsmdx?&&+2(4@1Ve3Rdb;!8-|VF-uN=1 z4GxkC#~pk=fyaYi={ejxtaOOn_3cHzdmSDk+p^iC@zxL@Pbn_dof}aLldU?@_C_5X zCfn39smY?b1$wsm1#3Cfpm^dpqiXYNv?KdZ-N)E}mj(Dw*#LX84UPO28<*2_QX`4d zpB_{Bwkkelt=)o0$RrNr%@va7;Op@Z=8Rd&h5Lp(3M(B1;6V0zz~0PvG6zO4Kc9_N zT_9APb1Fe98;}0C{r!zO|A|51NX9bjy1QMc8Aa^QzfyM*fO)X0(w!F-aEz>Wx|d97 zaxi$8&tJvog54wSWgsWNHWgTnQXsj}Lacd@;=qf#y=y?zL0f z(V1*dtH_$Q5p3N2?5kPE;T(YPS>_#H9q2-4B|dh(S$H;zid#*o_*I39GS3yimUW>k zna=eGq9(rY@ZaZ5bAA7czg_4?7WlProzMd&-ZWJ&cPqxMy0#nvr*0{Jp$#Y3n^6_>8>_HvwL%^iEWKVJcbtViFJn5~-tPhJs9vk$&GO~I-#8OYmZUSfp(c!n^RC)2aa)>#vn!nB z6B4LClC@jwPPF9lu<%gNVDgG|+qg#uV@C9mHH~pMkNqRRweR1zo}h-e`CPC^r=2Kd-EWSY{zO_ zjro;4ytDDsK9)i{#@w-T2z6(`Ewb`QVR?7`d8lmby_Yp54G*oHJ7IGH3&P2Mwv7|> zY~^FSL-MYQk8Lm^V1<}TQ3gbiF_!MD5wmE*>^(Af3qHKTbLWL-3FS4xZL(oMnGZ9j zRzT9voamylwcznVXs~A!&Hs_5r`*|QD%6C}ebpBPTEB$eTG76v2F(~rmQdCho@wB+% zXu3i&JW%inIzQA1F=P{R7f*H^XvAFt?d7M_D&fYYv2T~=RO0=Aw&`0QJGQC`7bP<; z%BQh_w=}oODk=>hkeN+fuq5L(4-ZeX5NTgt1#+?vXH{osz(caXb^mYsKwF}*LOVVp zi>Vv4*uJ3&pB1f@@Ml*-a|pw1PB#l4lZ{gi?F7)xfN zr*V63W+PfJlTv?VzyskvAMU5BB#0wB_R^x!j_$)%J?qr8taj+kz8rJn`UiYMW+263 zo1SWdTa}`J)*og=NWfH2aao$*Ci_p^gTKf5w;g>(cHeS^%J-E52%aG1J8LQ%)O6$O zp2Rm{Jehy4|ECv=SZG?mOFh+_hY~Cy<|_Ltd`>oT(v^1=87#b2dTF)CcR zIT>G&y}oJ(K2|IoxA&)YT16AiFs-~T5tI%IWXB@>J+^7G==q7J!NJ5POkEPYBf78x zUXl$zoxW$W4;$w_8~%NJKMPeprR;vcn}>;HC*-mtZysQy=$1G;t~?7T)?aKM1vb7S z+o5W4Cqjdbg*m)sR%{ktIQP<3-Gza#$&PwwKb<_4ji-vFwC<;}@aMt#CsoTB@P=$s z)x2_MrDJ92@m8_JY)m0r7U9%) zdnp@V&YW^rd>I4pMh6F|l`}AvOu}(oi}^e@CUV}|#K<$SLul~xnyU;*BU6spcVgKj zHXd*9n(?D49Yr@q^TgU%kWO|gXOo_MEDIHG+_tmZk%~q3ItlgiJYbNm7Qgdo!7vLW zM}9UgA54R%dxZPlrZi&)*~qP*mPUgtROgN>+o!>R81G1*D{I)8NtShC%D|RsY+Tm+ zeN3z~1M*MCUA@1ZfmvkDFDA(Ub3L<+o=!UB!2o-`xz9a48IVo3b+N{EEX*UDr~kHisEUQrH`{9(hFEZOva*iaKstUP`+9H9lLXm( zd=VY9BtobY)x8gbMqLHulLgOE5B>dxg>hzFmC^}psJK1!IQs|#3dk1w#y1H4pj>@u z0b|dCDp)P$Vc$5J4TWS5zl#ggx4ps8`-S1ZdRlSnkA*Vn=bNF3%yZ*J^Pn+od@$rI z9vaR9QHw{LOpY`GlWbv!h3m0ETG{aDGT5{t=9*3a;mnL>4}dg@(K z9?Qu7+wy<)smjS%;RCZ96|+|%`z*sW1?)_{UbPM6}8EoLbpD(Y^=vhvT275Y^`lvpewsX z`@M|-!xlZWns}`hSY+QtX8P?v!v~klY1(Di*tl`m=#-?MYG9K&{n%>G73D)&O1L&t zyc3ohu08l??<=e#`x}$#c>k~CtR_1;d3XkQN(PFra+;>lPV%q)^QLHC z%|w;x8jaCUJXE;)tGKK)9l2yS)7=jj6f>~%-Px{*B~931dFq$uxeTZyD_d1iG+>sF zcP1qVJyhf2#DyoK6|b|Po=p42y5C*Wh<+OLe(hV`j2<$dXPuXAfd;a_vHbr%zechd z@nb4l0yD6W9rkUW5er!!<80$-jM7BbEp0MyQ(q<;D4#p+F@=o>2Q}jiUD(JYJNr7) zEo(sr=FWc?yFrzW6`zh-w;%(X$!w24>NtKX9cK^CH>+Dn_d%yyCCWAfTF697qw2hJ zGf){6LQh;_;cvT+_CNPnz$bH`@10@(i-Ad}zFXN^vT?S9<+-@*CJ>Op7vr_7UNi90 z3CEEp-ArTzPEVV)s~KC#Mk4)!7IETXUgPMDy{s18%2beZkLrXrGBGpnKb2!MaQye8 zg*SaDeqMg_$C>&Q?PRWVJB6k`WT0sG5x0kN4Ac({zt@yXagqIC?eR~~PVMC5=ASaEM?bVd7n%P^YUeWQZ^o9|ziSz0 z;F<&<4;^hbbd#NHm=xHjRt?LN*Dsr7+kwiNJ}vj=S3wV1wQj|+y3|ZOJIAEvMNc{k zdPeUqpV|z)WR5yFx(s4VVbkhU{jZ_zzs7o>$mHBliO+JagNoT18yiMC(Ib7OXQELd zd?qvU@!9cmS}L;mVvhnUQgN!9o@YjG8+;*4bN+Vy+_nsqi}rf|N{ayx_eB?#rtQ?Ir_P^!`{MeTD%mv!-hE4=|vgtjl;7`}!0H9JorI2^7(Pnm#73B%8XT>*O3k*E2|zP{BDB*vahHA zxSXak7vdOurT#@Fl-zrFV01z=4wBt<;NALFla8C8h@RG%%Z8b;U!{5|$1+6r(p}Nl zFEk(axD8ttEl$Ahaa-e7EbYRdWPukRZCFC>)?klKqTEC_l&XKL(%-_sUu1Ju_&*3# zs)KLeE#8N|XTSyawxiwqDBnkRBKNzF#eN1dGDjBt8pj5eSc$yxTN&_&?3wYI>KPIY zlrtMQ&TS4GZtbu<)-k|>VY2ETk7*ItvaoEAXGLT+3*@Mv7*1@45i;%RyOk2YbZr@)>aZ>3MC2IL+7Zf%x2B9te#U5}IskELr?J6*=!V z9(kX^gS58kG7alkIED=OPqJEmA_uvK%IELuGz061Z_DC`890{g_qkf>Ffj(|e%H7c zXvKpU8<@}T()|}E+ngZu{@2b_OjGGJ4|Cwb^f9V389!MdLNu35+vCD=DWc4$R!*rj#fp1b-Q8^zPabVS)q=dOV6eat;{Ah8$ZyB6DDQD=^_!v$V zM~@BL(TNkuymelM`cpYEi@rATIL%3XO}G`fJ1`Z+$Oa1I6W8u%fZcuJZr%+Zu1j;< z(0w5lCXw-f%0~S%Oo!Pf?G7U6X^hBd&DwCf3dG44KS^_NkYU5(H&(vwnuXBWQXE_q z(t(r76r)}o3ao5Iv7SF7M{c*mTXVOk5kd@{LYD69k=QgX4bComyIdf}#yehv0^?C0 zP9+mC+&=AZNrzF#lmSJ`m6Sx@(+fr;M8!pP6h|~|9^V(bE>bwkm+>>X4i!yKyndWCL+0bSN zsCLY#6pGHk`r83fzmH~sBw5fSM@N@^jo@jRoHK8C8a5;>>=lV_!?|RY#(GPX_N9aL z&COolmZssBU(=O#M6+NXnRfeWhfn3X5WgXE%+akWShYqd{|3F!DMjWpnPYcUCzYO6 zPI>UQHxnKw^sbZ3ZbNCZIu(z!q%9e6q^-iv5;9=$yyy}KTOQ6Qdn45QrQ#^XsM__* z&9^Y%;M2UTQ6g-VA#?Xum~>pE8$WiPyCt`lfscwpF8k&-f-G60x6#)JAq;x&Giq;y z9|K+{9#N>h#lQt*7FAWr-yb#ucV6(Bv6T$C>8-taaZ@WjU`1`}x}ml|#~GkyJugV& z3ImoIybC|~BOMlzaYl{i`0~?fzG!F4upk4B`*vQt(!+wqWODlzcRt^o2`({tD`X<_ zpw`P@URSjlnEHfVep6kX0~LU}SH{f_C$wbZZP)a^N}#|HZY zJANh#Sh$32P&buh@P+|S%7@%^$FL#lNmGTL5(5>;1j#mxQe*1JFI@7~e<>SgCw
&m-r(d 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

RMqpFk@@>(>gG9aa)hUd>L)eJ`BWiQELP9oaSbK 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 {C@vUiI(RRo>^i*rIlrQMH=>KMtMi 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 0, "The resulting mesh should have more than 0 cells" assert result.n_points > 0, "The resulting mesh should have more than 0 points" - + # Parameterized test for tetrahedralize function @pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) @@ -33,3 +44,36 @@ def test_tetrahedralize(mesh_generator): ), "The tetrahedra result should be a numpy array" assert len(vertices_result) > 0, "There should be more than 0 vertices in the result" assert len(tetrahedra_result) > 0, "There should be more than 0 tetrahedra in the result" + + +def _sample_points_vtk(mesh_pv, dist_btw_pts=0.01): + point_sampler = vtk.vtkPolyDataPointSampler() + point_sampler.SetInputData(mesh_pv) + point_sampler.SetDistance(dist_btw_pts) + point_sampler.SetPointGenerationMode(point_sampler.REGULAR_GENERATION) + point_sampler.Update() + points_sampled = pv.PolyData(point_sampler.GetOutput()).points + return points_sampled + +def _symmetric_surf_dist(pts0, pts1): + d_kdtree0, _ = KDTree(pts0).query(pts1) + d_kdtree1, _ = KDTree(pts1).query(pts0) + return (np.mean(d_kdtree0) + np.mean(d_kdtree1))/2 + +@pytest.mark.parametrize("mesh_generator", [pv.Icosphere]) # pv.examples.download_bunny_coarse is not closed, so select_enclosed_points fails +def test_output_points_enclosed(mesh_generator): + input_pv = mesh_generator() + py_output_pv = tetrahedralize_pv(input_pv) + additional_input_scaling = 0.01 + enclosed_pv = py_output_pv.select_enclosed_points(input_pv.scale(1 + additional_input_scaling)) + enclosed_ratio = enclosed_pv.point_data["SelectedPoints"].sum() / input_pv.points.shape[0] + assert enclosed_ratio > 0.99, "all output vertices should be within some threshold of the input surf" + +def test_default_output_surf_dist(default_test_data): + input_pv = default_test_data["input"] + output_pv = default_test_data["output"] + py_output_pv = tetrahedralize_pv(input_pv) + pts0 = _sample_points_vtk(py_output_pv.extract_surface()) + pts1 = _sample_points_vtk(output_pv.extract_surface()) + surf_dist = _symmetric_surf_dist(pts0, pts1) + assert surf_dist < 1e-2, "surfs of outputs from c++/py should be similar" \ No newline at end of file From 21527f52a7d082d2505f46029b4b36816021b4e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:25:42 +0000 Subject: [PATCH 63/68] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_pytetwild.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py index b83b686..1c2b575 100644 --- a/tests/test_pytetwild.py +++ b/tests/test_pytetwild.py @@ -10,14 +10,17 @@ ) # Replace 'your_module' with the name of your Python file THIS_PATH = os.path.dirname(os.path.abspath(__file__)) + + @pytest.fixture def default_test_data(): data = { "input": pv.read(os.path.join(THIS_PATH, "test_data/test_surf.ply")), - "output": pv.read(os.path.join(THIS_PATH, "test_data/test_tets.msh")) + "output": pv.read(os.path.join(THIS_PATH, "test_data/test_tets.msh")), } return data + # Parameterized test for tetrahedralize_pv function @pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) def test_tetrahedralize_pv(mesh_generator): @@ -28,7 +31,7 @@ def test_tetrahedralize_pv(mesh_generator): ), "The result should be a PyVista UnstructuredGrid" assert result.n_cells > 0, "The resulting mesh should have more than 0 cells" assert result.n_points > 0, "The resulting mesh should have more than 0 points" - + # Parameterized test for tetrahedralize function @pytest.mark.parametrize("mesh_generator", [pv.Icosphere, pv.examples.download_bunny_coarse]) @@ -55,19 +58,26 @@ def _sample_points_vtk(mesh_pv, dist_btw_pts=0.01): points_sampled = pv.PolyData(point_sampler.GetOutput()).points return points_sampled + def _symmetric_surf_dist(pts0, pts1): d_kdtree0, _ = KDTree(pts0).query(pts1) d_kdtree1, _ = KDTree(pts1).query(pts0) - return (np.mean(d_kdtree0) + np.mean(d_kdtree1))/2 + return (np.mean(d_kdtree0) + np.mean(d_kdtree1)) / 2 + -@pytest.mark.parametrize("mesh_generator", [pv.Icosphere]) # pv.examples.download_bunny_coarse is not closed, so select_enclosed_points fails +@pytest.mark.parametrize( + "mesh_generator", [pv.Icosphere] +) # pv.examples.download_bunny_coarse is not closed, so select_enclosed_points fails def test_output_points_enclosed(mesh_generator): input_pv = mesh_generator() py_output_pv = tetrahedralize_pv(input_pv) additional_input_scaling = 0.01 enclosed_pv = py_output_pv.select_enclosed_points(input_pv.scale(1 + additional_input_scaling)) enclosed_ratio = enclosed_pv.point_data["SelectedPoints"].sum() / input_pv.points.shape[0] - assert enclosed_ratio > 0.99, "all output vertices should be within some threshold of the input surf" + assert ( + enclosed_ratio > 0.99 + ), "all output vertices should be within some threshold of the input surf" + def test_default_output_surf_dist(default_test_data): input_pv = default_test_data["input"] @@ -76,4 +86,4 @@ def test_default_output_surf_dist(default_test_data): pts0 = _sample_points_vtk(py_output_pv.extract_surface()) pts1 = _sample_points_vtk(output_pv.extract_surface()) surf_dist = _symmetric_surf_dist(pts0, pts1) - assert surf_dist < 1e-2, "surfs of outputs from c++/py should be similar" \ No newline at end of file + assert surf_dist < 1e-2, "surfs of outputs from c++/py should be similar" From e456dc9b205ef4ca4d6c690abc97ec43e60c109f Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 21:06:33 -0800 Subject: [PATCH 64/68] fix windows build --- .gitignore | 1 + CONTRIBUTING.md | 5 +++++ pyproject.toml | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 772b51a..5e2814e 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ ZERO_CHECK.vcxproj.filters fTetWildWrapper.sln x64/ Release/ +mpir.dll # cibuildwheel wheelhouse \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1390efe..9c006c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,11 @@ To install the library in editable mode: pip install -e .[dev] ``` + **Note:** On windows, you'll need to copy the `mpir.dll` to your source directory with: + ``` + python -c "import shutil, os; shutil.copy(os.path.join(os.getenv('GMP_LIB'), '..', 'bin', 'mpir.dll'), './src/pytetwild')" + ``` + ## Code Style and Quality - **Documentation**: Follow the `numpydoc` style for docstrings. diff --git a/pyproject.toml b/pyproject.toml index fe65818..ec7da13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ testpaths = 'tests' [tool.cibuildwheel] archs = ["auto64"] # 64-bit only # build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels -build = "cp311-* cp312-*" # Only build Python 3.8-3.12 wheels +build = "cp310-*" # Only build Python 3.8-3.12 wheels skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels test-requires = "pytest pyvista scipy meshio" test-command = "pytest {project}/tests -v" @@ -51,8 +51,11 @@ before-all = "yum install gmp gmp-devel -y" environment = "USE_MAVX='true'" environment-pass = ["USE_MAVX"] +# pip install delvewheel && [tool.cibuildwheel.windows] -before-build = "python -c \"import os; file_path = 'build/CMakeCache.txt'; os.remove(file_path) if os.path.exists(file_path) else None\"" +before-build = "pip install delvewheel && python -c \"import os; file_path = 'build/CMakeCache.txt'; os.remove(file_path) if os.path.exists(file_path) else None\"" +repair-wheel-command = "python -c \"import shutil, os; shutil.copy(os.path.join(os.getenv('GMP_LIB'), '..', 'bin', 'mpir.dll'), '.')\" && delvewheel repair -w {dest_dir} {wheel} --add-path ." + [tool.blackdoc] # From https://numpydoc.readthedocs.io/en/latest/format.html From fa94e28e245bd25235f92d42885eceeea0d432fe Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 22:27:55 -0700 Subject: [PATCH 65/68] add release --- .github/workflows/build-and-deploy.yml | 26 ++++++++++++++++++++++++++ .gitignore | 1 + tests/test_pytetwild.py | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 7a6dd0e..a54fff2 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -93,3 +93,29 @@ jobs: with: path: dist/*.tar.gz name: pytetwild-sdist + + release: + name: Release + # if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pytetwild + permissions: + id-token: write # this permission is mandatory for trusted publishing + steps: + - uses: actions/download-artifact@v4 + - name: Flatten directory structure + run: | + mkdir -p dist/ + find . -name '*.whl' -exec mv {} dist/ \; + find . -name '*.tar.gz' -exec mv {} dist/ \; + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # - name: Create GitHub Release + # uses: softprops/action-gh-release@v1 + # with: + # generate_release_notes: true + # files: | + # ./**/*.whl diff --git a/.gitignore b/.gitignore index 5e2814e..c951985 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ dist/ # MISC .mypy_cache +__tracked_surface.stl # Mac .DS_Store diff --git a/tests/test_pytetwild.py b/tests/test_pytetwild.py index 1c2b575..f98c9d7 100644 --- a/tests/test_pytetwild.py +++ b/tests/test_pytetwild.py @@ -7,7 +7,7 @@ from pytetwild import ( tetrahedralize_pv, tetrahedralize, -) # Replace 'your_module' with the name of your Python file +) THIS_PATH = os.path.dirname(os.path.abspath(__file__)) From 118c2408dc9c52fcb097ca8b2cdaa9578d14d22c Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 22:39:53 -0700 Subject: [PATCH 66/68] fix dynamic tag; fix readme; build all --- README.rst | 13 +++++-------- pyproject.toml | 5 ++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 92c6f5c..a21a07f 100644 --- a/README.rst +++ b/README.rst @@ -17,20 +17,17 @@ Python wrapper around the efficient C++ library for tetrahedral meshing provided Installation ************ +We have pre-built wheels for Python 3.8 - Python 3.12 for Windows and Linux x64. + The recommended way to install ``pytetwild`` is via PyPI: .. code:: sh pip install pytetwild -You can also clone the repository and install it from source: - -.. code:: sh - - git clone https://github.com/pyvista/pytetwild.git - cd pytetwild - git submodule update --init --recursive - pip install . +You can also clone the repository and install it from source, but since there's +C++ involved, the build is a bit more complicated. See ``CONTRIBUTING.md`` for +more details. Usage diff --git a/pyproject.toml b/pyproject.toml index ec7da13..00b9a74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Python wrapper of fTetWild" readme = { file = "README.rst", content-type = "text/x-rst" } authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] dependencies = ["numpy"] -dynamic = ["urls", "license"] +dynamic = ["license"] classifiers = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', @@ -40,8 +40,7 @@ testpaths = 'tests' [tool.cibuildwheel] archs = ["auto64"] # 64-bit only -# build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels -build = "cp310-*" # Only build Python 3.8-3.12 wheels +build = "cp38-* cp39-* cp310-* cp311-* cp312-*" # Only build Python 3.8-3.12 wheels skip = ["pp*", "*musllinux*"] # disable PyPy and musl-based wheels test-requires = "pytest pyvista scipy meshio" test-command = "pytest {project}/tests -v" From 300d3e8d0397589bada5bccb15733b546cd041f0 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 23:02:01 -0700 Subject: [PATCH 67/68] remove dynamic license in pyproject --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00b9a74..14c93ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ description = "Python wrapper of fTetWild" readme = { file = "README.rst", content-type = "text/x-rst" } authors = [{ name = "Alex Kaszynski", email = "akascap@gmail.com" }] dependencies = ["numpy"] -dynamic = ["license"] classifiers = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', From 6666eeac845b7b1a532e4454c9a157fa2cfdd334 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 3 Mar 2024 23:25:36 -0700 Subject: [PATCH 68/68] Make release conditional --- .github/workflows/build-and-deploy.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index a54fff2..baa3193 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -96,7 +96,7 @@ jobs: release: name: Release - # if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') needs: [build_wheels, build_sdist] runs-on: ubuntu-latest environment: @@ -113,9 +113,9 @@ jobs: find . -name '*.tar.gz' -exec mv {} dist/ \; - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - # - name: Create GitHub Release - # uses: softprops/action-gh-release@v1 - # with: - # generate_release_notes: true - # files: | - # ./**/*.whl + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + ./**/*.whl