From: Drew Parsons Date: Wed, 26 Jan 2022 16:21:20 +0000 (+0100) Subject: Import pygalmesh_0.10.6.orig.tar.xz X-Git-Tag: archive/raspbian/0.10.6-5+rpi1~5 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=6e87577fa8465b2bff4fc9dc85f22775af9a9f86;p=pygalmesh.git Import pygalmesh_0.10.6.orig.tar.xz [dgit import orig pygalmesh_0.10.6.orig.tar.xz] --- 6e87577fa8465b2bff4fc9dc85f22775af9a9f86 diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..a052f98 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1 @@ +comment: no diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c321e71 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503 +max-line-length = 80 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2c8f9bb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.inr filter=lfs diff=lfs merge=lfs -text +*.off filter=lfs diff=lfs merge=lfs -text +*.vtu filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3a1f550 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Run pre-commit + uses: pre-commit/action@v2.0.3 + + linux: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-python@v2 + with: + python-version: "3.x" + + - name: Checkout code + uses: nschloe/action-cached-lfs-checkout@v1 + + - name: Install CGAL 5 + run: | + # Leave that here in case we ever need a PPA again + # sudo apt-get install -y software-properties-common + # sudo apt-add-repository -y ppa:nschloe/cgal-backports + # sudo apt update + sudo apt install -y libcgal-dev + - name: Install other dependencies + run: | + sudo apt-get install -y libeigen3-dev python3-pip clang + - name: Test with tox + run: | + pip install tox + CC="clang" tox -- --cov pygalmesh --cov-report xml --cov-report term + - uses: codecov/codecov-action@v1 + + macos: + runs-on: macos-latest + steps: + - uses: actions/setup-python@v2 + with: + python-version: "3.x" + + - name: Checkout code + uses: nschloe/action-cached-lfs-checkout@v1 + + - name: Install system dependencies + run: | + brew install cgal eigen + - name: Test with tox + run: | + pip install tox + CC="clang" tox -- --cov pygalmesh --cov-report xml --cov-report term diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0038c24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.mesh +.cache/ +build/ +dist/ +MANIFEST +README.rst +do-configure.sh +.pytest_cache/ +*.egg-info/ +.tox/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1e8b2f7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 21.9b0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..5cb3e1a --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Schlömer" + given-names: "Nico" + orcid: "https://orcid.org/0000-0001-5228-0946" +title: "pygalmesh: Python interface for CGAL's meshing tools" +doi: 10.5281/zenodo.5564818 +url: https://github.com/nschloe/pygalmesh +license: GPL-3.0 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..56b6e55 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +# CMake is used for debugging in pygalmesh. Like every other Python package, the +# production build system is setuptools. +cmake_minimum_required(VERSION 3.0) + +project(pygalmesh CXX) + +add_subdirectory(src) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..28cf337 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,14 @@ +include src/domain.hpp +include src/generate.hpp +include src/generate_2d.hpp +include src/generate_from_inr.hpp +include src/generate_from_off.hpp +include src/generate_periodic.hpp +include src/generate_surface_mesh.hpp +include src/polygon2d.hpp +include src/primitives.hpp +include src/remesh_surface.hpp +include src/sizing_field.hpp + +include tests/*.py +recursive-include tests/meshes * diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1b05fe --- /dev/null +++ b/README.md @@ -0,0 +1,584 @@ +

+ pygalmesh +

Create high-quality meshes with ease.

+

+ +[![PyPi Version](https://img.shields.io/pypi/v/pygalmesh.svg?style=flat-square)](https://pypi.org/project/pygalmesh) +[![Anaconda Cloud](https://anaconda.org/conda-forge/pygalmesh/badges/version.svg?=style=flat-square)](https://anaconda.org/conda-forge/pygalmesh/) +[![Packaging status](https://repology.org/badge/tiny-repos/pygalmesh.svg)](https://repology.org/project/pygalmesh/versions) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/pygalmesh.svg?style=flat-square)](https://pypi.org/pypi/pygalmesh/) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5564818.svg?style=flat-square)](https://doi.org/10.5281/zenodo.5564818) +[![GitHub stars](https://img.shields.io/github/stars/nschloe/pygalmesh.svg?style=flat-square&label=Stars&logo=github)](https://github.com/nschloe/pygalmesh) +[![Downloads](https://pepy.tech/badge/pygalmesh/month?style=flat-square)](https://pepy.tech/project/pygalmesh) + + +[![Discord](https://img.shields.io/static/v1?logo=discord&label=chat&message=on%20discord&color=7289da&style=flat-square)](https://discord.gg/Z6DMsJh4Hr) + +[![gh-actions](https://img.shields.io/github/workflow/status/nschloe/pygalmesh/ci?style=flat-square)](https://github.com/nschloe/pygalmesh/actions?query=workflow%3Aci) +[![codecov](https://img.shields.io/codecov/c/github/nschloe/pygalmesh.svg?style=flat-square)](https://codecov.io/gh/nschloe/pygalmesh) +[![LGTM](https://img.shields.io/lgtm/grade/python/github/nschloe/pygalmesh.svg?style=flat-square)](https://lgtm.com/projects/g/nschloe/pygalmesh) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) + +pygalmesh is a Python frontend to [CGAL](https://www.cgal.org/)'s +[2D](https://doc.cgal.org/latest/Mesh_2/index.html) and [3D mesh generation +capabilities](https://doc.cgal.org/latest/Mesh_3/index.html). pygalmesh makes it easy +to create high-quality 2D, 3D volume meshes, periodic volume meshes, and surface meshes. + +### Examples + +#### 2D meshes + + + +CGAL generates 2D meshes from linear constraints. + +```python +import numpy as np +import pygalmesh + +points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]) +constraints = [[0, 1], [1, 2], [2, 3], [3, 0]] + +mesh = pygalmesh.generate_2d( + points, + constraints, + max_edge_size=1.0e-1, + num_lloyd_steps=10, +) +# mesh.points, mesh.cells +``` + +The quality of the mesh isn't very good, but can be improved with +[optimesh](https://github.com/nschloe/optimesh). + +#### A simple ball + + + +```python +import pygalmesh + +s = pygalmesh.Ball([0, 0, 0], 1.0) +mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.2) + +# mesh.points, mesh.cells, ... +``` + +You can write the mesh with + + + +```python +mesh.write("out.vtk") +``` + +You can use any format supported by [meshio](https://github.com/nschloe/meshio). + +The mesh generation comes with many more options, described +[here](https://doc.cgal.org/latest/Mesh_3/). Try, for example, + + + +```python +mesh = pygalmesh.generate_mesh( + s, max_cell_circumradius=0.2, odt=True, lloyd=True, verbose=False +) +``` + +#### Other primitive shapes + + + +pygalmesh provides out-of-the-box support for balls, cuboids, ellipsoids, tori, cones, +cylinders, and tetrahedra. Try for example + +```python +import pygalmesh + +s0 = pygalmesh.Tetrahedron( + [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0] +) +mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, +) +``` + +#### Domain combinations + + + +Supported are unions, intersections, and differences of all domains. As mentioned above, +however, the sharp intersections between two domains are not automatically handled. Try +for example + +```python +import pygalmesh + +radius = 1.0 +displacement = 0.5 +s0 = pygalmesh.Ball([displacement, 0, 0], radius) +s1 = pygalmesh.Ball([-displacement, 0, 0], radius) +u = pygalmesh.Difference(s0, s1) +``` + +To sharpen the intersection circle, add it as a feature edge polygon line, e.g., + +```python +import numpy as np +import pygalmesh + +radius = 1.0 +displacement = 0.5 +s0 = pygalmesh.Ball([displacement, 0, 0], radius) +s1 = pygalmesh.Ball([-displacement, 0, 0], radius) +u = pygalmesh.Difference(s0, s1) + +# add circle +a = np.sqrt(radius ** 2 - displacement ** 2) +max_edge_size_at_feature_edges = 0.15 +n = int(2 * np.pi * a / max_edge_size_at_feature_edges) +alpha = np.linspace(0.0, 2 * np.pi, n + 1) +circ = a * np.column_stack([np.zeros(n + 1), np.cos(alpha), np.sin(alpha)]) + +mesh = pygalmesh.generate_mesh( + u, + extra_feature_edges=[circ], + max_cell_circumradius=0.15, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=25, + max_radius_surface_delaunay_ball=0.15, + max_circumradius_edge_ratio=2.0, +) +``` + +Note that the length of the polygon legs are kept in sync with +`max_edge_size_at_feature_edges` of the mesh generation. This makes sure that it fits in +nicely with the rest of the mesh. + +#### Domain deformations + + + +You can of course translate, rotate, scale, and stretch any domain. Try, for example, + +```python +import pygalmesh + +s = pygalmesh.Stretch(pygalmesh.Ball([0, 0, 0], 1.0), [1.0, 2.0, 0.0]) + +mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.1) +``` + +#### Extrusion of 2D polygons + + + +pygalmesh lets you extrude any polygon into a 3D body. It even supports rotation +alongside! + +```python +import pygalmesh + +p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]]) +max_edge_size_at_feature_edges = 0.1 +domain = pygalmesh.Extrude( + p, + [0.0, 0.0, 1.0], + 0.5 * 3.14159265359, + max_edge_size_at_feature_edges, +) +mesh = pygalmesh.generate_mesh( + domain, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, +) +``` + +Feature edges are automatically preserved here, which is why an edge length needs to be +given to `pygalmesh.Extrude`. + +#### Rotation bodies + + + +Polygons in the x-z-plane can also be rotated around the z-axis to yield a rotation +body. + +```python +import pygalmesh + +p = pygalmesh.Polygon2D([[0.5, -0.3], [1.5, -0.3], [1.0, 0.5]]) +max_edge_size_at_feature_edges = 0.1 +domain = pygalmesh.RingExtrude(p, max_edge_size_at_feature_edges) +mesh = pygalmesh.generate_mesh( + domain, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, +) +``` + +#### Your own custom level set function + + + +If all of the variety is not enough for you, you can define your own custom level set +function. You simply need to subclass `pygalmesh.DomainBase` and specify a function, +e.g., + +```python +import pygalmesh + + +class Heart(pygalmesh.DomainBase): + def __init__(self): + super().__init__() + + def eval(self, x): + return ( + (x[0] ** 2 + 9.0 / 4.0 * x[1] ** 2 + x[2] ** 2 - 1) ** 3 + - x[0] ** 2 * x[2] ** 3 + - 9.0 / 80.0 * x[1] ** 2 * x[2] ** 3 + ) + + def get_bounding_sphere_squared_radius(self): + return 10.0 + + +d = Heart() +mesh = pygalmesh.generate_mesh(d, max_cell_circumradius=0.1) +``` + +Note that you need to specify the square of a bounding sphere radius, used as an input +to CGAL's mesh generator. + +#### Local refinement + + + +Use `generate_mesh` with a function (regular or lambda) as `max_cell_circumradius`. The +same goes for `max_edge_size_at_feature_edges`, `max_radius_surface_delaunay_ball`, and +`max_facet_distance`. + +```python +import numpy as np +import pygalmesh + +mesh = pygalmesh.generate_mesh( + pygalmesh.Ball([0.0, 0.0, 0.0], 1.0), + min_facet_angle=30.0, + max_radius_surface_delaunay_ball=0.1, + max_facet_distance=0.025, + max_circumradius_edge_ratio=2.0, + max_cell_circumradius=lambda x: abs(np.sqrt(np.dot(x, x)) - 0.5) / 5 + 0.025, +) +``` + +#### Surface meshes + +If you're only after the surface of a body, pygalmesh has `generate_surface_mesh` for +you. It offers fewer options (obviously, `max_cell_circumradius` is gone), but otherwise +works the same way: + +```python +import pygalmesh + +s = pygalmesh.Ball([0, 0, 0], 1.0) +mesh = pygalmesh.generate_surface_mesh( + s, + min_facet_angle=30.0, + max_radius_surface_delaunay_ball=0.1, + max_facet_distance=0.1, +) +``` + +Refer to [CGAL's +documentation](https://doc.cgal.org/latest/Surface_mesher/index.html) for the +options. + +#### Periodic volume meshes + + + +pygalmesh also interfaces CGAL's [3D periodic +mesh generation](https://doc.cgal.org/latest/Periodic_3_mesh_3/index.html). Besides a +domain, one needs to specify a bounding box, and optionally the number of copies in the +output (1, 2, 4, or 8). Example: + +```python +import numpy as np +import pygalmesh + + +class Schwarz(pygalmesh.DomainBase): + def __init__(self): + super().__init__() + + def eval(self, x): + x2 = np.cos(x[0] * 2 * np.pi) + y2 = np.cos(x[1] * 2 * np.pi) + z2 = np.cos(x[2] * 2 * np.pi) + return x2 + y2 + z2 + + +mesh = pygalmesh.generate_periodic_mesh( + Schwarz(), + [0, 0, 0, 1, 1, 1], + max_cell_circumradius=0.05, + min_facet_angle=30, + max_radius_surface_delaunay_ball=0.05, + max_facet_distance=0.025, + max_circumradius_edge_ratio=2.0, + number_of_copies_in_output=4, + # odt=True, + # lloyd=True, + verbose=False, +) +``` + +#### Volume meshes from surface meshes + + + +If you have a surface mesh at hand (like +[elephant.vtu](http://nschloe.github.io/pygalmesh/elephant.vtu)), pygalmesh generates a +volume mesh on the command line via + +``` +pygalmesh volume-from-surface elephant.vtu out.vtk --cell-size 1.0 --odt +``` + +(See `pygalmesh volume-from-surface -h` for all options.) + +In Python, do + + + +```python +import pygalmesh + +mesh = pygalmesh.generate_volume_mesh_from_surface_mesh( + "elephant.vtu", + min_facet_angle=25.0, + max_radius_surface_delaunay_ball=0.15, + max_facet_distance=0.008, + max_circumradius_edge_ratio=3.0, + verbose=False, +) +``` + +#### Meshes from INR voxel files + + + +It is also possible to generate meshes from INR voxel files, e.g., +[here](https://github.com/CGAL/cgal/tree/master/Mesh_3/examples/Mesh_3/data) either on +the command line + +``` +pygalmesh from-inr skull_2.9.inr out.vtu --cell-size 5.0 --odt +``` + +(see `pygalmesh from-inr -h` for all options) or from Python + + + +```python +import pygalmesh + +mesh = pygalmesh.generate_from_inr( + "skull_2.9.inr", + max_cell_circumradius=5.0, + verbose=False, +) +``` + +#### Meshes from numpy arrays representing 3D images + +| | | +| :------------------------------------------------------------------------: | :---------------------------------------------------------------------: | + +pygalmesh can help generating unstructed meshes from 3D numpy int arrays specifying the +subdomains. Subdomains with key `0` are not meshed. + +```python +import pygalmesh +import numpy as np + +x_ = np.linspace(-1.0, 1.0, 50) +y_ = np.linspace(-1.0, 1.0, 50) +z_ = np.linspace(-1.0, 1.0, 50) +x, y, z = np.meshgrid(x_, y_, z_) + +vol = np.empty((50, 50, 50), dtype=np.uint8) +idx = x ** 2 + y ** 2 + z ** 2 < 0.5 ** 2 +vol[idx] = 1 +vol[~idx] = 0 + +voxel_size = (0.1, 0.1, 0.1) + +mesh = pygalmesh.generate_from_array( + vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=0.1 +) +mesh.write("ball.vtk") +``` + +The code below creates a mesh from the 3D breast phantom from [Lou et +al](http://biomedicaloptics.spiedigitallibrary.org/article.aspx?articleid=2600985) +available +[here](https://wustl.app.box.com/s/rqivtin0xcofjwlkz43acou8jknsbfx8/file/127108205145). +The phantom comprises four tissue types (background, fat, fibrograndular, skin, vascular +tissues). The generated mesh conforms to tissues interfaces. + + + +```python +import pygalmesh +import meshio + +with open("MergedPhantom.DAT", "rb") as fid: + vol = np.fromfile(fid, dtype=np.uint8) + +vol = vol.reshape((722, 411, 284)) +voxel_size = (0.2, 0.2, 0.2) + +mesh = pygalmesh.generate_from_array( + vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=1.0 +) +mesh.write("breast.vtk") +``` + +In addition, we can specify different mesh sizes for each tissue type. The code below +sets the mesh size to _1 mm_ for the skin tissue (label `4`), _0.5 mm_ for the vascular +tissue (label `5`), and _2 mm_ for all other tissues (`default`). + + + +```python +mesh = pygalmesh.generate_from_array( + vol, + voxel_size, + max_facet_distance=0.2, + max_cell_circumradius={"default": 2.0, 4: 1.0, 5: 0.5}, +) +mesh.write("breast_adapted.vtk") +``` + +#### Surface remeshing + +| | | +| :-------------------------------------------------------------------------: | :-------------------------------------------------------------------------: | + +pygalmesh can help remeshing an existing surface mesh, e.g., +[`lion-head.off`](https://github.com/nschloe/pygalmesh/raw/gh-pages/lion-head.off). On +the command line, use + +``` +pygalmesh remesh-surface lion-head.off out.vtu -e 0.025 -a 25 -s 0.1 -d 0.001 +``` + +(see `pygalmesh remesh-surface -h` for all options) or from Python + + + +```python +import pygalmesh + +mesh = pygalmesh.remesh_surface( + "lion-head.off", + max_edge_size_at_feature_edges=0.025, + min_facet_angle=25, + max_radius_surface_delaunay_ball=0.1, + max_facet_distance=0.001, + verbose=False, +) +``` + +### Installation + +For installation, pygalmesh needs [CGAL](https://www.cgal.org/) and +[Eigen](http://eigen.tuxfamily.org/index.php?title=Main_Page) installed on your +system. They are typically available on your Linux distribution, e.g., on +Ubuntu + +``` +sudo apt install libcgal-dev libeigen3-dev +``` + +On MacOS with homebrew, + +``` +brew install cgal eigen +``` + +After that, pygalmesh can be [installed from the Python Package +Index](https://pypi.org/project/pygalmesh/), so with + +``` +pip install -U pygalmesh +``` + +you can install/upgrade. + +#### Troubleshooting + +If pygalmesh fails to build due to `fatal error: 'Eigen/Dense' file not found` +you will need to create a symbolic link for Eigen to be detected, e.g. + +``` +cd /usr/local/include +sudo ln -sf eigen3/Eigen Eigen +``` + +It's possible that `eigen3` could be in `/usr/include` instead of +`/usr/local/install`. + +#### Manual installation + +For manual installation (if you're a developer or just really keen on getting +the bleeding edge version of pygalmesh), there are two possibilities: + +- Get the sources, type `python3 setup.py install`. This does the trick + most the time. +- As a fallback, there's a CMake-based installation. Simply go `cmake /path/to/sources/` and `make`. + +### Testing + +To run the pygalmesh unit tests, check out this repository and type + +``` +pytest +``` + +### Background + +CGAL offers two different approaches for mesh generation: + +1. Meshes defined implicitly by level sets of functions. +2. Meshes defined by a set of bounding planes. + +pygalmesh provides a front-end to the first approach, which has the following advantages +and disadvantages: + +- All boundary points are guaranteed to be in the level set within any specified + residual. This results in smooth curved surfaces. +- Sharp intersections of subdomains (e.g., in unions or differences of sets) need to be + specified manually (via feature edges, see below), which can be tedious. + +On the other hand, the bounding-plane approach (realized by +[mshr](https://bitbucket.org/fenics-project/mshr)), has the following properties: + +- Smooth, curved domains are approximated by a set of bounding planes, resulting in more + of less visible edges. +- Intersections of domains can be computed automatically, so domain unions etc. have + sharp edges where they belong. + +See [here](https://github.com/nschloe/awesome-scientific-computing#meshing) for other +mesh generation tools. + +### License + +pygalmesh is published under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html). diff --git a/justfile b/justfile new file mode 100644 index 0000000..642fc1e --- /dev/null +++ b/justfile @@ -0,0 +1,28 @@ +version := `python3 -c "from configparser import ConfigParser; p = ConfigParser(); p.read('setup.cfg'); print(p['metadata']['version'])"` + +default: + @echo "\"make publish\"?" + +tag: + @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi + curl -H "Authorization: token `cat ~/.github-access-token`" -d '{"tag_name": "v{{version}}"}' https://api.github.com/repos/nschloe/pygalmesh/releases + +upload: clean + @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi + python3 -m build --sdist . + twine upload dist/*.tar.gz + +publish: tag upload + +clean: + @find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf + @rm -rf src/*.egg-info/ build/ dist/ .tox/ pygalmesh.egg-info// + +format: + isort . + black . + blacken-docs README.md + +lint: + black --check . + flake8 setup.py pygalmesh/ test/*.py diff --git a/pygalmesh/__about__.py b/pygalmesh/__about__.py new file mode 100644 index 0000000..8c2ff90 --- /dev/null +++ b/pygalmesh/__about__.py @@ -0,0 +1,14 @@ +from _pygalmesh import _CGAL_VERSION_STR + +try: + # Python 3.8 + from importlib import metadata +except ImportError: + import importlib_metadata as metadata + +try: + __version__ = metadata.version("pygalmesh") +except Exception: + __version__ = "unknown" + +__cgal_version__ = _CGAL_VERSION_STR diff --git a/pygalmesh/__init__.py b/pygalmesh/__init__.py new file mode 100644 index 0000000..3a73598 --- /dev/null +++ b/pygalmesh/__init__.py @@ -0,0 +1,73 @@ +# https://github.com/pybind/pybind11/issues/1004 +from _pygalmesh import ( + Ball, + Cone, + Cuboid, + Cylinder, + Difference, + DomainBase, + Ellipsoid, + Extrude, + HalfSpace, + Intersection, + Polygon2D, + RingExtrude, + Rotate, + Scale, + Stretch, + Tetrahedron, + Torus, + Translate, + Union, +) + +from . import _cli +from .__about__ import __cgal_version__, __version__ +from .main import ( + generate_2d, + generate_from_array, + generate_from_inr, + generate_mesh, + generate_periodic_mesh, + generate_surface_mesh, + generate_volume_mesh_from_surface_mesh, + remesh_surface, + save_inr, +) + +__all__ = [ + "__version__", + "__cgal_version__", + # + "_cli", + # + "DomainBase", + "Translate", + "Rotate", + "Scale", + "Stretch", + "Intersection", + "Union", + "Difference", + "Extrude", + "Ball", + "Cuboid", + "Ellipsoid", + "Tetrahedron", + "Cone", + "Cylinder", + "Torus", + "HalfSpace", + "Polygon2D", + "RingExtrude", + # + "generate_mesh", + "generate_2d", + "generate_periodic_mesh", + "generate_surface_mesh", + "generate_volume_mesh_from_surface_mesh", + "generate_from_array", + "generate_from_inr", + "remesh_surface", + "save_inr", +] diff --git a/pygalmesh/_cli.py b/pygalmesh/_cli.py new file mode 100644 index 0000000..c9307cf --- /dev/null +++ b/pygalmesh/_cli.py @@ -0,0 +1,352 @@ +import argparse +from sys import version_info + +import meshio +from _pygalmesh import _CGAL_VERSION_STR + +from .__about__ import __version__ +from .main import generate_from_inr, generate_volume_mesh_from_surface_mesh +from .main import remesh_surface as rms + + +def cli(argv=None): + parser = argparse.ArgumentParser( + description="pygalmesh command-line tools", + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument( + "--version", + "-v", + action="version", + version=get_version_text(), + help="display version information", + ) + + subparsers = parser.add_subparsers(title="subcommands") + + parser_inr = subparsers.add_parser( + "from-inr", help="Create volume mesh from INR voxel files" + ) + _cli_inr(parser_inr) + parser_inr.set_defaults(func=inr) + + parser_remesh = subparsers.add_parser("remesh-surface", help="Remesh surface mesh") + _cli_remesh(parser_remesh) + parser_remesh.set_defaults(func=remesh_surface) + + parser_volume_from_surface = subparsers.add_parser( + "volume-from-surface", help="Generate volume mesh from surface mesh" + ) + _cli_volume_from_surface(parser_volume_from_surface) + parser_volume_from_surface.set_defaults(func=volume_from_surface) + + args = parser.parse_args(argv) + return args.func(args) + + +def get_version_text(): + python_ver = f"{version_info.major}.{version_info.minor}.{version_info.micro}" + return "\n".join( + [ + f"pygalmesh {__version__} [Python {python_ver}, CGAL {_CGAL_VERSION_STR}]" + + "Copyright (c) 2016-2021, Nico Schlömer ", + ] + ) + + +def inr(args): + mesh = generate_from_inr( + args.infile, + lloyd=args.lloyd, + odt=args.odt, + perturb=args.perturb, + exude=args.exude, + max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges, + min_facet_angle=args.min_facet_angle, + max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball, + max_facet_distance=args.max_facet_distance, + max_circumradius_edge_ratio=args.max_circumradius_edge_ratio, + max_cell_circumradius=args.max_cell_circumradius, + verbose=not args.quiet, + ) + meshio.write(args.outfile, mesh) + + +def _cli_inr(parser): + parser.add_argument("infile", type=str, help="input INR file") + + parser.add_argument("outfile", type=str, help="output mesh file") + + parser.add_argument( + "--lloyd", + "-l", + type=bool, + default=False, + help="Lloyd smoothing (default: false)", + ) + + parser.add_argument( + "--odt", + "-o", + action="store_true", + default=False, + help="ODT smoothing (default: false)", + ) + + parser.add_argument( + "--perturb", + "-p", + action="store_true", + default=False, + help="perturb (default: false)", + ) + + parser.add_argument( + "--exude", + "-x", + action="store_true", + default=False, + help="exude (default: false)", + ) + + parser.add_argument( + "--max-edge-size-at-feature-edges", + "-e", + type=float, + default=0.0, + help="maximum edge size at feature edges (default: 0.0)", + ) + + parser.add_argument( + "--min-facet-angle", + "-a", + type=float, + default=0.0, + help="minimum facet angle (default: 0.0)", + ) + + parser.add_argument( + "--max-radius-surface-delaunay-ball", + "-s", + type=float, + default=0.0, + help="maximum radius of the surface facet Delaunay ball (default: 0.0)", + ) + + parser.add_argument( + "--max-facet-distance", + "-d", + type=float, + default=0.0, + help="maximum facet distance (default: 0.0)", + ) + + parser.add_argument( + "--max-circumradius-edge-ratio", + "-r", + type=float, + default=0.0, + help="cell radius/edge ratio (default: 0.0)", + ) + + parser.add_argument( + "--max-cell-circumradius", + "-c", + type=float, + default=0.0, + help="maximum cell circumradius (default: 0.0)", + ) + + parser.add_argument( + "--quiet", + "-q", + action="store_true", + default=False, + help="quiet mode (default: False)", + ) + return parser + + +def remesh_surface(args): + mesh = rms( + args.infile, + # lloyd=args.lloyd, + # odt=args.odt, + # perturb=args.perturb, + # exude=args.exude, + max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges, + min_facet_angle=args.min_facet_angle, + max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball, + max_facet_distance=args.max_facet_distance, + verbose=not args.quiet, + ) + meshio.write(args.outfile, mesh) + + +def _cli_remesh(parser): + parser.add_argument("infile", type=str, help="input mesh file") + parser.add_argument("outfile", type=str, help="output mesh file") + + parser.add_argument( + "--max-edge-size-at-feature-edges", + "-e", + type=float, + default=0.0, + help="maximum edge size at feature edges (default: 0.0)", + ) + + parser.add_argument( + "--min-facet-angle", + "-a", + type=float, + default=0.0, + help="minimum facet angle (default: 0.0)", + ) + + parser.add_argument( + "--max-radius-surface-delaunay-ball", + "-s", + type=float, + default=0.0, + help="maximum radius of the surface facet Delaunay ball (default: 0.0)", + ) + + parser.add_argument( + "--max-facet-distance", + "-d", + type=float, + default=0.0, + help="maximum facet distance (default: 0.0)", + ) + + parser.add_argument( + "--quiet", + "-q", + action="store_true", + default=False, + help="quiet mode (default: False)", + ) + return parser + + +def volume_from_surface(args): + mesh = generate_volume_mesh_from_surface_mesh( + args.infile, + lloyd=args.lloyd, + odt=args.odt, + perturb=args.perturb, + exude=args.exude, + max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges, + min_facet_angle=args.min_facet_angle, + max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball, + max_facet_distance=args.max_facet_distance, + max_circumradius_edge_ratio=args.max_circumradius_edge_ratio, + max_cell_circumradius=args.max_cell_circumradius, + reorient=args.reorient, + verbose=not args.quiet, + ) + meshio.write(args.outfile, mesh) + + +def _cli_volume_from_surface(parser): + parser.add_argument("infile", type=str, help="input mesh file") + + parser.add_argument("outfile", type=str, help="output mesh file") + + parser.add_argument( + "--lloyd", + "-l", + type=bool, + default=False, + help="Lloyd smoothing (default: false)", + ) + + parser.add_argument( + "--odt", + "-o", + action="store_true", + default=False, + help="ODT smoothing (default: false)", + ) + + parser.add_argument( + "--perturb", + "-p", + action="store_true", + default=False, + help="perturb (default: false)", + ) + + parser.add_argument( + "--exude", + "-x", + action="store_true", + default=False, + help="exude (default: false)", + ) + + parser.add_argument( + "--max-edge-size-at-feature-edges", + "-e", + type=float, + default=0.0, + help="maximum edge size at feature edges (default: 0.0)", + ) + + parser.add_argument( + "--min-facet-angle", + "-a", + type=float, + default=0.0, + help="minimum facet angle (default: 0.0)", + ) + + parser.add_argument( + "--max-radius-surface-delaunay-ball", + "-s", + type=float, + default=0.0, + help="maximum radius of the surface facet Delaunay ball (default: 0.0)", + ) + + parser.add_argument( + "--max-facet-distance", + "-d", + type=float, + default=0.0, + help="maximum facet distance (default: 0.0)", + ) + + parser.add_argument( + "--max-circumradius-edge-ratio", + "-r", + type=float, + default=0.0, + help="cell radius/edge ratio (default: 0.0)", + ) + + parser.add_argument( + "--max-cell-circumradius", + "-c", + type=float, + default=0.0, + help="maximum cell circumradius (default: 0.0)", + ) + + parser.add_argument( + "--reorient", + "-t", + action="store_true", + default=False, + help="automatically fix face orientation (default: False)", + ) + + parser.add_argument( + "--quiet", + "-q", + action="store_true", + default=False, + help="quiet mode (default: False)", + ) + return parser diff --git a/pygalmesh/main.py b/pygalmesh/main.py new file mode 100644 index 0000000..e161e3c --- /dev/null +++ b/pygalmesh/main.py @@ -0,0 +1,478 @@ +from __future__ import annotations + +import math +import os +import tempfile +from typing import Callable + +import meshio +import numpy +from _pygalmesh import ( + SizingFieldBase, + _generate_2d, + _generate_from_inr, + _generate_from_inr_with_subdomain_sizing, + _generate_from_off, + _generate_mesh, + _generate_periodic_mesh, + _generate_surface_mesh, + _remesh_surface, +) + + +class Wrapper(SizingFieldBase): + def __init__(self, f): + self.f = f + super().__init__() + + def eval(self, x): + return self.f(x) + + +def generate_mesh( + domain, + extra_feature_edges: list | None = None, + bounding_sphere_radius: float = 0.0, + lloyd: bool = False, + odt: bool = False, + perturb: bool = True, + exude: bool = True, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float | Callable[..., float] = 0.0, + max_facet_distance: float = 0.0, + max_circumradius_edge_ratio: float = 0.0, + max_cell_circumradius: float | Callable[..., float] = 0.0, + exude_time_limit: float = 0.0, + exude_sliver_bound: float = 0.0, + verbose: bool = True, + seed: int = 0, +): + """ + From : + + max_edge_size_at_feature_edges: + a scalar field (resp. a constant) providing a space varying (resp. a uniform) + upper bound for the lengths of curve edges. This parameter has to be set to a + positive value when 1-dimensional features protection is used. + min_facet_angle: + a lower bound for the angles (in degrees) of the surface mesh facets. + max_radius_surface_delaunay_ball: + a scalar field (resp. a constant) describing a space varying (resp. a uniform) + upper-bound or for the radii of the surface Delaunay balls. + max_facet_distance: + a scalar field (resp. a constant) describing a space varying (resp. a uniform) + upper bound for the distance between the facet circumcenter and the center of + its surface Delaunay ball. + max_circumradius_edge_ratio: + an upper bound for the radius-edge ratio of the mesh tetrahedra. + max_cell_circumradius: + a scalar field (resp. a constant) describing a space varying (resp. a uniform) + upper-bound for the circumradii of the mesh tetrahedra. + """ + extra_feature_edges = [] if extra_feature_edges is None else extra_feature_edges + + fh, outfile = tempfile.mkstemp(suffix=".mesh") + os.close(fh) + + def _select(obj): + if isinstance(obj, float): + return obj, None + assert callable(obj) + return -1.0, Wrapper(obj) + + ( + max_edge_size_at_feature_edges_value, + max_edge_size_at_feature_edges_field, + ) = _select(max_edge_size_at_feature_edges) + max_cell_circumradius_value, max_cell_circumradius_field = _select( + max_cell_circumradius + ) + ( + max_radius_surface_delaunay_ball_value, + max_radius_surface_delaunay_ball_field, + ) = _select(max_radius_surface_delaunay_ball) + max_facet_distance_value, max_facet_distance_field = _select(max_facet_distance) + + # if feature_edges: + # if max_edge_size_at_feature_edges == 0.0: + # raise ValueError( + # "Need a positive max_edge_size_at_feature_edges bound if feature_edges are present." + # ) + # elif max_edge_size_at_feature_edges != 0.0: + # warnings.warn( + # "No feature edges. The max_edge_size_at_feature_edges argument has no effect." + # ) + + _generate_mesh( + domain, + outfile, + extra_feature_edges=extra_feature_edges, + bounding_sphere_radius=bounding_sphere_radius, + lloyd=lloyd, + odt=odt, + perturb=perturb, + exude=exude, + max_edge_size_at_feature_edges_value=max_edge_size_at_feature_edges_value, + max_edge_size_at_feature_edges_field=max_edge_size_at_feature_edges_field, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball_value=max_radius_surface_delaunay_ball_value, + max_radius_surface_delaunay_ball_field=max_radius_surface_delaunay_ball_field, + max_facet_distance_value=max_facet_distance_value, + max_facet_distance_field=max_facet_distance_field, + max_circumradius_edge_ratio=max_circumradius_edge_ratio, + max_cell_circumradius_value=max_cell_circumradius_value, + max_cell_circumradius_field=max_cell_circumradius_field, + exude_time_limit=exude_time_limit, + exude_sliver_bound=exude_sliver_bound, + verbose=verbose, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(outfile) + return mesh + + +def generate_2d( + points, + constraints, + B: float = math.sqrt(2), + max_edge_size: float = 0.0, + num_lloyd_steps: int = 0, +): + # some sanity checks + points = numpy.asarray(points) + constraints = numpy.asarray(constraints) + assert numpy.all(constraints >= 0) + assert numpy.all(constraints < len(points)) + # make sure there are no edges of 0 length + edges = points[constraints[:, 0]] - points[constraints[:, 1]] + length2 = numpy.einsum("ij,ij->i", edges, edges) + if numpy.any(length2 < 1.0e-15): + raise RuntimeError("Constraint of (near)-zero length.") + + points, cells = _generate_2d( + points, + constraints, + B, + max_edge_size, + num_lloyd_steps, + ) + return meshio.Mesh(numpy.array(points), {"triangle": numpy.array(cells)}) + + +def generate_periodic_mesh( + domain, + bounding_cuboid, + lloyd: bool = False, + odt: bool = False, + perturb: bool = True, + exude: bool = True, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_facet_distance: float = 0.0, + max_circumradius_edge_ratio: float = 0.0, + max_cell_circumradius: float = 0.0, + number_of_copies_in_output: int = 1, + verbose: bool = True, + seed: int = 0, +): + fh, outfile = tempfile.mkstemp(suffix=".mesh") + os.close(fh) + + assert number_of_copies_in_output in [1, 2, 4, 8] + + _generate_periodic_mesh( + domain, + outfile, + bounding_cuboid, + lloyd=lloyd, + odt=odt, + perturb=perturb, + exude=exude, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + max_circumradius_edge_ratio=max_circumradius_edge_ratio, + max_cell_circumradius=max_cell_circumradius, + number_of_copies_in_output=number_of_copies_in_output, + verbose=verbose, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(outfile) + return mesh + + +def generate_surface_mesh( + domain, + bounding_sphere_radius: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_facet_distance: float = 0.0, + verbose: bool = True, + seed: int = 0, +): + fh, outfile = tempfile.mkstemp(suffix=".off") + os.close(fh) + + _generate_surface_mesh( + domain, + outfile, + bounding_sphere_radius=bounding_sphere_radius, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + verbose=verbose, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(outfile) + return mesh + + +def generate_volume_mesh_from_surface_mesh( + filename: str, + lloyd: bool = False, + odt: bool = False, + perturb: bool = True, + exude: bool = True, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_facet_distance: float = 0.0, + max_circumradius_edge_ratio: float = 0.0, + max_cell_circumradius: float = 0.0, + exude_time_limit: float = 0.0, + exude_sliver_bound: float = 0.0, + verbose: bool = True, + reorient: bool = False, + seed: int = 0, +): + mesh = meshio.read(filename) + + fh, off_file = tempfile.mkstemp(suffix=".off") + os.close(fh) + meshio.write(off_file, mesh) + + fh, outfile = tempfile.mkstemp(suffix=".mesh") + os.close(fh) + + _generate_from_off( + off_file, + outfile, + lloyd=lloyd, + odt=odt, + perturb=perturb, + exude=exude, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + max_circumradius_edge_ratio=max_circumradius_edge_ratio, + max_cell_circumradius=max_cell_circumradius, + exude_time_limit=exude_time_limit, + exude_sliver_bound=exude_sliver_bound, + verbose=verbose, + reorient=reorient, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(off_file) + os.remove(outfile) + return mesh + + +def generate_from_inr( + inr_filename: str, + lloyd: bool = False, + odt: bool = False, + perturb: bool = True, + exude: bool = True, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_facet_distance: float = 0.0, + max_circumradius_edge_ratio: float = 0.0, + max_cell_circumradius: float | dict[int | str, float] = 0.0, + exude_time_limit: float = 0.0, + exude_sliver_bound: float = 0.0, + verbose: bool = True, + seed: int = 0, +): + fh, outfile = tempfile.mkstemp(suffix=".mesh") + os.close(fh) + + if isinstance(max_cell_circumradius, float): + _generate_from_inr( + inr_filename, + outfile, + lloyd=lloyd, + odt=odt, + perturb=perturb, + exude=exude, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + max_circumradius_edge_ratio=max_circumradius_edge_ratio, + max_cell_circumradius=max_cell_circumradius, + exude_time_limit=exude_time_limit, + exude_sliver_bound=exude_sliver_bound, + verbose=verbose, + seed=seed, + ) + else: + assert isinstance(max_cell_circumradius, dict) + if "default" in max_cell_circumradius.keys(): + default_max_cell_circumradius = max_cell_circumradius.pop("default") + else: + default_max_cell_circumradius = 0.0 + + max_cell_circumradiuss = list(max_cell_circumradius.values()) + subdomain_labels = list(max_cell_circumradius.keys()) + + _generate_from_inr_with_subdomain_sizing( + inr_filename, + outfile, + default_max_cell_circumradius, + max_cell_circumradiuss, + subdomain_labels, + lloyd=lloyd, + odt=odt, + perturb=perturb, + exude=exude, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + max_circumradius_edge_ratio=max_circumradius_edge_ratio, + verbose=verbose, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(outfile) + return mesh + + +def remesh_surface( + filename: str, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_facet_distance: float = 0.0, + verbose: bool = True, + seed: int = 0, +): + mesh = meshio.read(filename) + + fh, off_file = tempfile.mkstemp(suffix=".off") + os.close(fh) + meshio.write(off_file, mesh) + + fh, outfile = tempfile.mkstemp(suffix=".off") + os.close(fh) + + _remesh_surface( + off_file, + outfile, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=min_facet_angle, + max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball, + max_facet_distance=max_facet_distance, + verbose=verbose, + seed=seed, + ) + + mesh = meshio.read(outfile) + os.remove(off_file) + os.remove(outfile) + return mesh + + +def save_inr(vol, voxel_size: tuple[float, float, float], fname: str): + """ + Save a volume (described as a numpy array) to INR format. + Code inspired by iso2mesh (http://iso2mesh.sf.net) by Q. Fang + INPUTS: + - vol: volume as a 3D numpy array + - h: voxel sizes + - fname: filename for saving the inr file + """ + fid = open(fname, "wb") + + btype, bitlen = { + "uint8": ("unsigned fixed", 8), + "uint16": ("unsigned fixed", 16), + "float32": ("float", 32), + "float64": ("float", 64), + }[vol.dtype.name] + + xdim, ydim, zdim = vol.shape + header = "\n".join( + [ + "#INRIMAGE-4#{", + f"XDIM={xdim}", + f"YDIM={ydim}", + f"ZDIM={zdim}", + "VDIM=1", + f"TYPE={btype}", + f"PIXSIZE={bitlen} bits", + "CPU=decm", + f"VX={voxel_size[0]:f}", + f"VY={voxel_size[1]:f}", + f"VZ={voxel_size[2]:f}", + ] + ) + header += "\n" + + header = header + "\n" * (256 - 4 - len(header)) + "##}\n" + + fid.write(header.encode("ascii")) + fid.write(vol.tobytes(order="F")) + + +def generate_from_array( + vol, + voxel_size: tuple[float, float, float], + lloyd: bool = False, + odt: bool = False, + perturb: bool = True, + exude: bool = True, + max_edge_size_at_feature_edges: float = 0.0, + min_facet_angle: float = 0.0, + max_radius_surface_delaunay_ball: float = 0.0, + max_cell_circumradius: float | dict[int | str, float] = 0.0, + max_facet_distance: float = 0.0, + max_circumradius_edge_ratio: float = 0.0, + verbose: bool = True, + seed: int = 0, +): + assert vol.dtype in ["uint8", "uint16"] + fh, inr_filename = tempfile.mkstemp(suffix=".inr") + os.close(fh) + save_inr(vol, voxel_size, inr_filename) + mesh = generate_from_inr( + inr_filename, + lloyd, + odt, + perturb, + exude, + max_edge_size_at_feature_edges, + min_facet_angle, + max_radius_surface_delaunay_ball, + max_facet_distance, + max_circumradius_edge_ratio, + max_cell_circumradius, + verbose, + seed, + ) + os.remove(inr_filename) + return mesh diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2ac57ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "pybind11>=2.6.0"] +build-backend = "setuptools.build_meta" + +[tool.isort] +profile = "black" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..56feff8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = pygalmesh +version = 0.10.6 +author = Nico Schlömer +author_email = nico.schloemer@gmail.com +description = Python frontend to CGAL's mesh generation capabilities +url = https://github.com/nschloe/pygalmesh +project_urls = + Code=https://github.com/nschloe/pygalmesh + Issues=https://github.com/nschloe/pygalmesh/issues + Funding=https://github.com/sponsors/nschloe +long_description = file: README.md +long_description_content_type = text/markdown +license = GPL-3.0-or-later +classifiers = + Development Status :: 4 - Beta + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Topic :: Scientific/Engineering + Topic :: Scientific/Engineering :: Mathematics + Topic :: Scientific/Engineering :: Physics + Topic :: Scientific/Engineering :: Visualization +keywords = + mathematics + physics + engineering + cgal + mesh + mesh generation + +[options] +packages = find: +install_requires = + importlib_metadata;python_version<"3.8" + meshio >= 4.0.0, < 6.0.0 + numpy +python_requires = >=3.7 + +[options.entry_points] +console_scripts = + pygalmesh = pygalmesh._cli:cli diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5a75a1c --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +import os + +from pybind11.setup_helpers import Pybind11Extension, build_ext +from setuptools import setup + +# https://github.com/pybind/python_example/ +ext_modules = [ + Pybind11Extension( + "_pygalmesh", + # Sort input source files to ensure bit-for-bit reproducible builds + # (https://github.com/pybind/python_example/pull/53) + sorted( + [ + "src/generate.cpp", + "src/generate_2d.cpp", + "src/generate_from_inr.cpp", + "src/generate_from_off.cpp", + "src/generate_periodic.cpp", + "src/generate_surface_mesh.cpp", + "src/remesh_surface.cpp", + "src/pybind11.cpp", + ] + ), + include_dirs=[ + os.environ.get("EIGEN_INCLUDE_DIR", "/usr/include/eigen3/"), + # macos/brew: + "/usr/local/include/eigen3", + ], + # no CGAL libraries necessary from CGAL 5.0 onwards + libraries=["gmp", "mpfr"], + ) +] + +if __name__ == "__main__": + setup( + cmdclass={"build_ext": build_ext}, + ext_modules=ext_modules, + zip_safe=False, + ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d214588 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,31 @@ +FIND_PACKAGE(pybind11 REQUIRED) +# include_directories(${PYBIND11_INCLUDE_DIR}) + +FIND_PACKAGE(Eigen3 REQUIRED) +include_directories(${EIGEN3_INCLUDE_DIR}) + +FIND_PACKAGE(CGAL REQUIRED) +include(${CGAL_USE_FILE}) + +FILE(GLOB pygalmesh_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +# FILE(GLOB pygalmesh_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp") + +pybind11_add_module(pygalmesh ${pygalmesh_SRCS}) + +# ADD_LIBRARY(pygalmesh ${pygalmesh_SRCS}) +target_link_libraries(pygalmesh PRIVATE ${CGAL_LIBRARIES}) + +# https://github.com/CGAL/cgal/issues/6002 +# find_program(iwyu_path NAMES include-what-you-use iwyu REQUIRED) +# set_property(TARGET pygalmesh PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${iwyu_path}) + +# execute_process( +# COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()" +# OUTPUT_VARIABLE PYTHON_SITE_PACKAGES +# OUTPUT_STRIP_TRAILING_WHITESPACE +# ) +# install(TARGETS _pygalmesh DESTINATION ${PYTHON_SITE_PACKAGES}) +# install( +# FILES ${CMAKE_BINARY_DIR}/src/pygalmesh.py +# DESTINATION ${PYTHON_SITE_PACKAGES} +# ) diff --git a/src/domain.hpp b/src/domain.hpp new file mode 100644 index 0000000..ff8cd9c --- /dev/null +++ b/src/domain.hpp @@ -0,0 +1,490 @@ +#ifndef DOMAIN_HPP +#define DOMAIN_HPP + +#include +#include +#include +#include +#include + +namespace pygalmesh { + +class DomainBase +{ + public: + + virtual ~DomainBase() = default; + + virtual + double + eval(const std::array & x) const = 0; + + virtual + double + get_bounding_sphere_squared_radius() const = 0; + + virtual + std::vector>> + get_features() const + { + return {}; + }; +}; + +class Translate: public pygalmesh::DomainBase +{ + public: + Translate( + const std::shared_ptr & domain, + const std::array & direction + ): + domain_(domain), + direction_(Eigen::Vector3d(direction.data())), + translated_features_(translate_features(domain->get_features(), direction_)) + { + } + + virtual ~Translate() = default; + + std::vector>> + translate_features( + const std::vector>> & features, + const Eigen::Vector3d & direction + ) const + { + std::vector>> translated_features; + for (const auto & feature: features) { + std::vector> translated_feature; + for (const auto & point: feature) { + const std::array translated_point = { + point[0] + direction[0], + point[1] + direction[1], + point[2] + direction[2] + }; + translated_feature.push_back(translated_point); + } + translated_features.push_back(translated_feature); + } + return translated_features; + } + + virtual + double + eval(const std::array & x) const + { + const std::array d = { + x[0] - direction_[0], + x[1] - direction_[1], + x[2] - direction_[2] + }; + return domain_->eval(d); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + const double radius = sqrt(domain_->get_bounding_sphere_squared_radius()); + const double dir_norm = direction_.norm(); + return (radius + dir_norm)*(radius + dir_norm); + } + + virtual + std::vector>> + get_features() const + { + return translated_features_; + }; + + private: + const std::shared_ptr domain_; + const Eigen::Vector3d direction_; + const std::vector>> translated_features_; +}; + +class Rotate: public pygalmesh::DomainBase +{ + public: + Rotate( + const std::shared_ptr & domain, + const std::array & axis, + const double angle + ): + domain_(domain), + normalized_axis_(Eigen::Vector3d(axis.data()).normalized()), + sinAngle_(sin(angle)), + cosAngle_(cos(angle)), + rotated_features_(rotate_features(domain_->get_features())) + { + } + + virtual ~Rotate() = default; + + Eigen::Vector3d + rotate( + const Eigen::Vector3d & vec, + const Eigen::Vector3d & axis, + const double sinAngle, + const double cosAngle + ) const + { + // Rotate a vector `v` by the angle `theta` in the plane perpendicular + // to the axis given by `u`. + // Refer to + // http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle + // + // cos(theta) * I * v + // + sin(theta) u\cross v + // + (1-cos(theta)) (u*u^T) * v + return cosAngle * vec + + sinAngle * axis.cross(vec) + + (1.0-cosAngle) * axis.dot(vec) * axis; + } + + virtual + double + eval(const std::array & x) const + { + // rotate with negative angle + const auto p2 = rotate( + Eigen::Vector3d(x.data()), + normalized_axis_, + -sinAngle_, + cosAngle_ + ); + return domain_->eval({p2[0], p2[1], p2[2]}); + } + + std::vector>> + rotate_features( + const std::vector>> & features + ) const + { + std::vector>> rotated_features; + for (const auto & feature: features) { + std::vector> rotated_feature; + for (const auto & point: feature) { + const auto p2 = rotate( + Eigen::Vector3d(point.data()), + normalized_axis_, + sinAngle_, + cosAngle_ + ); + rotated_feature.push_back({p2[0], p2[1], p2[2]}); + } + rotated_features.push_back(rotated_feature); + } + return rotated_features; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return domain_->get_bounding_sphere_squared_radius(); + } + + virtual + std::vector>> + get_features() const + { + return rotated_features_; + }; + + private: + const std::shared_ptr domain_; + const Eigen::Vector3d normalized_axis_; + const double sinAngle_; + const double cosAngle_; + const std::vector>> rotated_features_; +}; + +class Scale: public pygalmesh::DomainBase +{ + public: + Scale( + std::shared_ptr & domain, + const double alpha + ): + domain_(domain), + alpha_(alpha), + scaled_features_(scale_features(domain_->get_features())) + { + assert(alpha_ > 0.0); + } + + virtual ~Scale() = default; + + virtual + double + eval(const std::array & x) const + { + return domain_->eval({x[0]/alpha_, x[1]/alpha_, x[2]/alpha_}); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return alpha_*alpha_ * domain_->get_bounding_sphere_squared_radius(); + } + + std::vector>> + scale_features( + const std::vector>> & features + ) const + { + std::vector>> scaled_features; + for (const auto & feature: features) { + std::vector> scaled_feature; + for (const auto & point: feature) { + scaled_feature.push_back({ + alpha_ * point[0], + alpha_ * point[1], + alpha_ * point[2] + }); + } + scaled_features.push_back(scaled_feature); + } + return scaled_features; + } + + virtual + std::vector>> + get_features() const + { + return scaled_features_; + }; + + private: + std::shared_ptr domain_; + const double alpha_; + const std::vector>> scaled_features_; +}; + +class Stretch: public pygalmesh::DomainBase +{ + public: + Stretch( + std::shared_ptr & domain, + const std::array & direction + ): + domain_(domain), + normalized_direction_(Eigen::Vector3d(direction.data()).normalized()), + alpha_(Eigen::Vector3d(direction.data()).norm()), + stretched_features_(stretch_features(domain_->get_features())) + { + assert(alpha_ > 0.0); + } + + virtual ~Stretch() = default; + + virtual + double + eval(const std::array & x) const + { + const Eigen::Vector3d v(x.data()); + const double beta = normalized_direction_.dot(v); + // scale the component of normalized_direction_ by 1/alpha_ + const auto v2 = beta/alpha_ * normalized_direction_ + + (v - beta * normalized_direction_); + return domain_->eval({v2[0], v2[1], v2[2]}); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return alpha_*alpha_ * domain_->get_bounding_sphere_squared_radius(); + } + + std::vector>> + stretch_features( + const std::vector>> & features + ) const + { + std::vector>> stretched_features; + for (const auto & feature: features) { + std::vector> stretched_feature; + for (const auto & point: feature) { + // scale the component of normalized_direction_ by alpha_ + const Eigen::Vector3d v(point.data()); + const double beta = normalized_direction_.dot(v); + const auto v2 = beta * alpha_ * normalized_direction_ + + (v - beta * normalized_direction_); + stretched_feature.push_back({v2[0], v2[1], v2[2]}); + } + stretched_features.push_back(stretched_feature); + } + return stretched_features; + } + + virtual + std::vector>> + get_features() const + { + return stretched_features_; + }; + + private: + std::shared_ptr domain_; + const Eigen::Vector3d normalized_direction_; + const double alpha_; + const std::vector>> stretched_features_; +}; + +class Intersection: public pygalmesh::DomainBase +{ + public: + explicit Intersection( + std::vector> & domains + ): + domains_(domains) + { + } + + virtual ~Intersection() = default; + + virtual + double + eval(const std::array & x) const + { + // TODO find a differentiable expression + double maxval = std::numeric_limits::lowest(); + for (const auto & domain: domains_) { + maxval = std::max(maxval, domain->eval(x)); + } + return maxval; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + double min = std::numeric_limits::max(); + for (const auto & domain: domains_) { + min = std::min(min, domain->get_bounding_sphere_squared_radius()); + } + return min; + } + + virtual + std::vector>> + get_features() const + { + std::vector>> features; + for (const auto & domain: domains_) { + const auto f = domain->get_features(); + features.insert(std::end(features), std::begin(f), std::end(f)); + } + return features; + }; + + private: + std::vector> domains_; +}; + +class Union: public pygalmesh::DomainBase +{ + public: + explicit Union( + std::vector> & domains + ): + domains_(domains) + { + } + + virtual ~Union() = default; + + virtual + double + eval(const std::array & x) const + { + // TODO find a differentiable expression + double minval = std::numeric_limits::max(); + for (const auto & domain: domains_) { + minval = std::min(minval, domain->eval(x)); + } + return minval; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + double max = 0.0; + for (const auto & domain: domains_) { + max = std::max(max, domain->get_bounding_sphere_squared_radius()); + } + return max; + } + + virtual + std::vector>> + get_features() const + { + std::vector>> features; + for (const auto & domain: domains_) { + const auto f = domain->get_features(); + features.insert(std::end(features), std::begin(f), std::end(f)); + } + return features; + }; + + private: + std::vector> domains_; +}; + +class Difference: public pygalmesh::DomainBase +{ + public: + Difference( + std::shared_ptr & domain0, + std::shared_ptr & domain1 + ): + domain0_(domain0), + domain1_(domain1) + { + } + + virtual ~Difference() = default; + + virtual + double + eval(const std::array & x) const + { + // TODO find a continuous (perhaps even differentiable) expression + const double val0 = domain0_->eval(x); + const double val1 = domain1_->eval(x); + return (val0 < 0.0 && val1 >= 0.0) ? val0 : std::max(val0, -val1); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return domain0_->get_bounding_sphere_squared_radius(); + } + + virtual + std::vector>> + get_features() const + { + std::vector>> features; + + const auto f0 = domain0_->get_features(); + features.insert(std::end(features), std::begin(f0), std::end(f0)); + + const auto f1 = domain1_->get_features(); + features.insert(std::end(features), std::begin(f1), std::end(f1)); + + return features; + }; + + private: + std::shared_ptr domain0_; + std::shared_ptr domain1_; +}; + +} // namespace pygalmesh +#endif // DOMAIN_HPP diff --git a/src/generate.cpp b/src/generate.cpp new file mode 100644 index 0000000..64b0ba5 --- /dev/null +++ b/src/generate.cpp @@ -0,0 +1,191 @@ +#define CGAL_MESH_3_VERBOSE 1 + +#include "generate.hpp" + +#include + +#include +#include +#include + +#include +#include +#include + +namespace pygalmesh { + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +typedef CGAL::Mesh_domain_with_polyline_features_3> Mesh_domain; + +// Triangulation +typedef CGAL::Mesh_triangulation_3::type Tr; +typedef CGAL::Mesh_complex_3_in_triangulation_3 C3t3; + +// Mesh Criteria +typedef CGAL::Mesh_criteria_3 Mesh_criteria; +typedef Mesh_criteria::Edge_criteria Edge_criteria; +typedef Mesh_criteria::Facet_criteria Facet_criteria; +typedef Mesh_criteria::Cell_criteria Cell_criteria; + +// translate vector> to list> +std::list> +translate_feature_edges( + const std::vector>> & feature_edges + ) +{ + std::list> polylines; + for (const auto & feature_edge: feature_edges) { + std::vector polyline; + for (const auto & point: feature_edge) { + polyline.push_back(K::Point_3(point[0], point[1], point[2])); + } + polylines.push_back(polyline); + } + return polylines; +} + +void +generate_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const std::vector>> & extra_feature_edges, + const double bounding_sphere_radius, + const bool lloyd, + const bool odt, + const bool perturb, + const bool exude, + // + const double max_edge_size_at_feature_edges_value, + const std::shared_ptr & max_edge_size_at_feature_edges_field, + // + const double min_facet_angle, + // + const double max_radius_surface_delaunay_ball_value, + const std::shared_ptr & max_radius_surface_delaunay_ball_field, + // + const double max_facet_distance_value, + const std::shared_ptr & max_facet_distance_field, + // + const double max_circumradius_edge_ratio, + // + const double max_cell_circumradius_value, + const std::shared_ptr & max_cell_circumradius_field, + // + const double exude_time_limit, + const double exude_sliver_bound, + // + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + const double bounding_sphere_radius2 = bounding_sphere_radius > 0 ? + bounding_sphere_radius*bounding_sphere_radius : + // some wiggle room + 1.01 * domain->get_bounding_sphere_squared_radius(); + + // wrap domain + const auto d = [&](K::Point_3 p) { + return domain->eval({p.x(), p.y(), p.z()}); + }; + + Mesh_domain cgal_domain = Mesh_domain::create_implicit_mesh_domain( + d, + K::Sphere_3(CGAL::ORIGIN, bounding_sphere_radius2) + ); + + // cgal_domain.detect_features(); + + const auto native_features = translate_feature_edges(domain->get_features()); + cgal_domain.add_features(native_features.begin(), native_features.end()); + + const auto polylines = translate_feature_edges(extra_feature_edges); + cgal_domain.add_features(polylines.begin(), polylines.end()); + + // perhaps there's a more elegant solution here + // see + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + + // Build the float/field values according to + // . + + // nested ternary operator + const auto facet_criteria = max_radius_surface_delaunay_ball_field ? ( + max_facet_distance_field ? + Facet_criteria( + min_facet_angle, + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_radius_surface_delaunay_ball_field->eval({p.x(), p.y(), p.z()}); + }, + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_facet_distance_field->eval({p.x(), p.y(), p.z()}); + } + ) : Facet_criteria( + min_facet_angle, + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_radius_surface_delaunay_ball_field->eval({p.x(), p.y(), p.z()}); + }, + max_facet_distance_value + ) + ) : ( + max_facet_distance_field ? + Facet_criteria( + min_facet_angle, + max_radius_surface_delaunay_ball_value, + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_facet_distance_field->eval({p.x(), p.y(), p.z()}); + } + ) : Facet_criteria( + min_facet_angle, + max_radius_surface_delaunay_ball_value, + max_facet_distance_value + ) + ); + + const auto edge_criteria = max_edge_size_at_feature_edges_field ? + Edge_criteria( + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_edge_size_at_feature_edges_field->eval({p.x(), p.y(), p.z()}); + }) : Edge_criteria(max_edge_size_at_feature_edges_value); + + const auto cell_criteria = max_cell_circumradius_field ? + Cell_criteria( + max_circumradius_edge_ratio, + [&](K::Point_3 p, const int, const Mesh_domain::Index&) { + return max_cell_circumradius_field->eval({p.x(), p.y(), p.z()}); + }) : Cell_criteria(max_circumradius_edge_ratio, max_cell_circumradius_value); + + const auto criteria = Mesh_criteria(edge_criteria, facet_criteria, cell_criteria); + + // Mesh generation + C3t3 c3t3 = CGAL::make_mesh_3( + cgal_domain, + criteria, + lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(), + odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(), + perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(), + exude ? + CGAL::parameters::exude( + CGAL::parameters::time_limit = exude_time_limit, + CGAL::parameters::sliver_bound = exude_sliver_bound + ) : + CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + + // Output + std::ofstream medit_file(outfile); + c3t3.output_to_medit(medit_file); + medit_file.close(); + + return; +} + +} // namespace pygalmesh diff --git a/src/generate.hpp b/src/generate.hpp new file mode 100644 index 0000000..eb188d3 --- /dev/null +++ b/src/generate.hpp @@ -0,0 +1,49 @@ +#ifndef GENERATE_HPP +#define GENERATE_HPP + +#include "domain.hpp" +#include "sizing_field.hpp" + +#include +#include +#include +#include + +namespace pygalmesh { + +void generate_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const std::vector>> & extra_feature_edges = {}, + const double bounding_sphere_radius = 0.0, + const bool lloyd = false, + const bool odt = false, + const bool perturb = true, + const bool exude = true, + // + const double max_edge_size_at_feature_edges_value = 0.0, // std::numeric_limits::max(), + const std::shared_ptr & max_edge_size_at_feature_edges_field = nullptr, + // + const double min_facet_angle = 0.0, + // + const double max_radius_surface_delaunay_ball_value = 0.0, + const std::shared_ptr & max_radius_surface_delaunay_ball_field = nullptr, + // + const double max_facet_distance_value = 0.0, + const std::shared_ptr & max_facet_distance_field = nullptr, + // + const double max_circumradius_edge_ratio = 0.0, + // + const double max_cell_circumradius_value = 0.0, + const std::shared_ptr & max_cell_circumradius_field = nullptr, + // + const double exude_time_limit = 0.0, + const double exude_sliver_bound = 0.0, + // + const bool verbose = true, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // GENERATE_HPP diff --git a/src/generate_2d.cpp b/src/generate_2d.cpp new file mode 100644 index 0000000..5b573ce --- /dev/null +++ b/src/generate_2d.cpp @@ -0,0 +1,96 @@ +#define CGAL_MESH_3_VERBOSE 1 + +#include "generate_2d.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace pygalmesh { + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Delaunay_mesh_vertex_base_2 Vb; +typedef CGAL::Delaunay_mesh_face_base_2 Fb; +typedef CGAL::Triangulation_data_structure_2 Tds; +typedef CGAL::Constrained_Delaunay_triangulation_2 CDT; +typedef CGAL::Delaunay_mesh_size_criteria_2 Criteria; +typedef CDT::Vertex_handle Vertex_handle; +typedef CDT::Point Point; + +std::tuple>, std::vector>> +generate_2d( + const std::vector> & points, + const std::vector> & constraints, + // See + // https://doc.cgal.org/latest/Mesh_2/classCGAL_1_1Delaunay__mesh__size__criteria__2.html#a58b0186eae407ba76b8f4a3d0aa85a1a + // for what the bounds mean. Spoiler: + // B = circumradius / shortest_edge, + // relates to the smallest angle via sin(alpha_min) = 1 / (2B) + // edge_size S: "all segments of all triangles must be shorter than a bound S." + const double max_circumradius_shortest_edge_ratio, + const double max_edge_size, + const int num_lloyd_steps +) +{ + CDT cdt; + // construct a constrained triangulation + std::vector vertices(points.size()); + int k = 0; + for (auto pt: points) { + vertices[k] = cdt.insert(Point(pt[0], pt[1])); + k++; + } + for (auto c: constraints) { + cdt.insert_constraint(vertices[c[0]], vertices[c[1]]); + } + + // create proper mesh + CGAL::refine_Delaunay_mesh_2( + cdt, + Criteria( + 0.25 / (max_circumradius_shortest_edge_ratio * max_circumradius_shortest_edge_ratio), + max_edge_size + ) + ); + + if (num_lloyd_steps > 0) { + CGAL::lloyd_optimize_mesh_2( + cdt, + CGAL::parameters::max_iteration_number = num_lloyd_steps + ); + } + + // convert points to vector of arrays + std::map vertex_index; + std::vector> out_points(cdt.number_of_vertices()); + k = 0; + for (auto vit: cdt.finite_vertex_handles()) { + out_points[k][0] = vit->point()[0]; + out_points[k][1] = vit->point()[1]; + vertex_index[vit] = k; + k++; + } + + // https://github.com/CGAL/cgal/issues/5068#issuecomment-706213606 + auto nb_faces = 0u; + for (auto fit: cdt.finite_face_handles()) { + if(fit->is_in_domain()) ++nb_faces; + } + std::vector> out_cells(nb_faces); + k = 0; + for (auto fit: cdt.finite_face_handles()) { + if(!fit->is_in_domain()) continue; + out_cells[k][0] = vertex_index[fit->vertex(0)]; + out_cells[k][1] = vertex_index[fit->vertex(1)]; + out_cells[k][2] = vertex_index[fit->vertex(2)]; + k++; + } + + return std::make_tuple(out_points, out_cells); +} + +} // namespace pygalmesh diff --git a/src/generate_2d.hpp b/src/generate_2d.hpp new file mode 100644 index 0000000..6bcdf1e --- /dev/null +++ b/src/generate_2d.hpp @@ -0,0 +1,24 @@ +#ifndef GENERATE_2D_HPP +#define GENERATE_2D_HPP + +#include +#include + +namespace pygalmesh { + +std::tuple>, std::vector>> +generate_2d( + const std::vector> & points, + const std::vector> & constraints, + const double max_circumradius_shortest_edge_ratio, + // https://github.com/CGAL/cgal/issues/5061#issuecomment-705520984 + // the "default" size criterion for a triangle in the 2D mesh generator refers to its + // edge lengths. In the output mesh, all segments of all triangles must be shorter + // than the given bound. + const double max_edge_size, + const int num_lloyd_steps +); + +} // namespace pygalmesh + +#endif // GENERATE_2D_HPP diff --git a/src/generate_from_inr.cpp b/src/generate_from_inr.cpp new file mode 100644 index 0000000..1d63fe6 --- /dev/null +++ b/src/generate_from_inr.cpp @@ -0,0 +1,178 @@ +#define CGAL_MESH_3_VERBOSE 1 + +#include "generate_from_inr.hpp" + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace pygalmesh { + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +typedef CGAL::Labeled_mesh_domain_3 Mesh_domain; + +// Triangulation +typedef CGAL::Mesh_triangulation_3::type Tr; +typedef CGAL::Mesh_complex_3_in_triangulation_3 C3t3; + +// Mesh Criteria +typedef CGAL::Mesh_criteria_3 Mesh_criteria; +typedef Mesh_criteria::Facet_criteria Facet_criteria; +typedef Mesh_criteria::Cell_criteria Cell_criteria; + +typedef CGAL::Mesh_constant_domain_field_3 Sizing_field_cell; + +void +generate_from_inr( + const std::string & inr_filename, + const std::string & outfile, + const bool lloyd, + const bool odt, + const bool perturb, + const bool exude, + const double max_edge_size_at_feature_edges, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const double max_circumradius_edge_ratio, + const double max_cell_circumradius, + const double exude_time_limit, + const double exude_sliver_bound, + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + CGAL::Image_3 image; + const bool success = image.read(inr_filename.c_str()); + if (!success) { + throw "Could not read image file"; + } + Mesh_domain cgal_domain = Mesh_domain::create_labeled_image_mesh_domain(image); + + Mesh_criteria criteria( + CGAL::parameters::edge_size=max_edge_size_at_feature_edges, + CGAL::parameters::facet_angle=min_facet_angle, + CGAL::parameters::facet_size=max_radius_surface_delaunay_ball, + CGAL::parameters::facet_distance=max_facet_distance, + CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio, + CGAL::parameters::cell_size=max_cell_circumradius + ); + + // Mesh generation + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + C3t3 c3t3 = CGAL::make_mesh_3( + cgal_domain, + criteria, + lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(), + odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(), + perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(), + exude ? + CGAL::parameters::exude( + CGAL::parameters::time_limit = exude_time_limit, + CGAL::parameters::sliver_bound = exude_sliver_bound + ) : + CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + + // Output + std::ofstream medit_file(outfile); + c3t3.output_to_medit(medit_file); + medit_file.close(); + return; +} + + +void +generate_from_inr_with_subdomain_sizing( + const std::string & inr_filename, + const std::string & outfile, + const double default_max_cell_circumradius, + const std::vector & max_cell_circumradiuss, + const std::vector & cell_labels, + const bool lloyd, + const bool odt, + const bool perturb, + const bool exude, + const double max_edge_size_at_feature_edges, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const double max_circumradius_edge_ratio, + const double exude_time_limit, + const double exude_sliver_bound, + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + CGAL::Image_3 image; + const bool success = image.read(inr_filename.c_str()); + if (!success) { + throw "Could not read image file"; + } + Mesh_domain cgal_domain = Mesh_domain::create_labeled_image_mesh_domain(image); + + Sizing_field_cell max_cell_circumradius(default_max_cell_circumradius); + const int ndimensions = 3; + for(std::vector::size_type i(0); i < max_cell_circumradiuss.size(); ++i) + max_cell_circumradius.set_size(max_cell_circumradiuss[i], ndimensions, cgal_domain.index_from_subdomain_index(cell_labels[i])); + + Mesh_criteria criteria( + CGAL::parameters::edge_size=max_edge_size_at_feature_edges, + CGAL::parameters::facet_angle=min_facet_angle, + CGAL::parameters::facet_size=max_radius_surface_delaunay_ball, + CGAL::parameters::facet_distance=max_facet_distance, + CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio, + CGAL::parameters::cell_size=max_cell_circumradius + ); + + // Mesh generation + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + C3t3 c3t3 = CGAL::make_mesh_3( + cgal_domain, + criteria, + lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(), + odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(), + perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(), + exude ? + CGAL::parameters::exude( + CGAL::parameters::time_limit = exude_time_limit, + CGAL::parameters::sliver_bound = exude_sliver_bound + ) : + CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + + // Output + std::ofstream medit_file(outfile); + c3t3.output_to_medit(medit_file); + medit_file.close(); + return; +} + +} // namespace pygalmesh diff --git a/src/generate_from_inr.hpp b/src/generate_from_inr.hpp new file mode 100644 index 0000000..79b04b7 --- /dev/null +++ b/src/generate_from_inr.hpp @@ -0,0 +1,52 @@ +#ifndef GENERATE_FROM_INR_HPP +#define GENERATE_FROM_INR_HPP + +#include +#include + +namespace pygalmesh { + +void generate_from_inr( + const std::string & inr_filename, + const std::string & outfile, + const bool lloyd = false, + const bool odt = false, + const bool perturb = true, + const bool exude = true, + const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits::max(), + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const double max_circumradius_edge_ratio = 0.0, + const double max_cell_circumradius = 0.0, + const double exude_time_limit = 0.0, + const double exude_sliver_bound = 0.0, + const bool verbose = true, + const int seed = 0 + ); + +void +generate_from_inr_with_subdomain_sizing( + const std::string & inr_filename, + const std::string & outfile, + const double default_max_cell_circumradius, + const std::vector & max_cell_circumradiuss, + const std::vector & cell_labels, + const bool lloyd = false, + const bool odt = false, + const bool perturb = true, + const bool exude = true, + const double max_edge_size_at_feature_edges = 0.0, + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const double max_circumradius_edge_ratio = 0.0, + const double exude_time_limit = 0.0, + const double exude_sliver_bound = 0.0, + const bool verbose = true, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // GENERATE_FROM_INR_HPP diff --git a/src/generate_from_off.cpp b/src/generate_from_off.cpp new file mode 100644 index 0000000..fcebef5 --- /dev/null +++ b/src/generate_from_off.cpp @@ -0,0 +1,159 @@ +#include "generate_from_off.hpp" + +#include +#include +#include +#include +#include +#include +#include + +// IO +#include + +// for re-orientation +#include +#include +#include + +#include + +#if CGAL_VERSION_MAJOR >= 5 && CGAL_VERSION_MINOR < 3 + #include +#endif + +// for sharp features +//#include + +namespace pygalmesh { + +// Domain +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Polyhedron_3 Polyhedron; +typedef CGAL::Polyhedral_mesh_domain_3 Mesh_domain; +// for sharp features +//typedef CGAL::Polyhedral_mesh_domain_with_features_3 Mesh_domain; +//typedef CGAL::Mesh_polyhedron_3::type Polyhedron; + +// Triangulation +typedef CGAL::Mesh_triangulation_3::type Tr; + +typedef CGAL::Mesh_complex_3_in_triangulation_3 C3t3; + +// Criteria +typedef CGAL::Mesh_criteria_3 Mesh_criteria; + +// To avoid verbose function and named parameters call +using namespace CGAL::parameters; + +void generate_from_off( + const std::string& infile, + const std::string& outfile, + const bool lloyd, + const bool odt, + const bool perturb, + const bool exude, + const double max_edge_size_at_feature_edges, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const double max_circumradius_edge_ratio, + const double max_cell_circumradius, + const double exude_time_limit, + const double exude_sliver_bound, + const bool verbose, + const bool reorient, + const int seed +) { + CGAL::get_default_random() = CGAL::Random(seed); + + std::ifstream input(infile); + Polyhedron polyhedron; + // fix the orientation of the faces of the input file + if (reorient) { + std::stringstream msg; + msg << "fixing face orientation for \"" << infile <<"\""<< std::endl; + std::vector points; + std::vector > polygons; + + if( + !input || +#if CGAL_VERSION_MAJOR >= 5 && CGAL_VERSION_MINOR >= 3 + !CGAL::IO::read_OFF(input, points, polygons) || +#else + !CGAL::read_OFF(input, points, polygons) || +#endif + points.empty() + ) + { + std::stringstream msg; + msg << "Cannot read .off file \"" << infile <<"\""<< std::endl; + throw std::runtime_error(msg.str()); + } + // orient the polygons + CGAL::Polygon_mesh_processing::orient_polygon_soup(points, polygons); + // create the polyhedron + CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(points, polygons, polyhedron); + + } else { + + // Create input polyhedron + input >> polyhedron; + if (!input.good()) { + // Even if the mesh exists, it may not be valid, see + // + std::stringstream msg; + msg << "Invalid input file \"" << infile << "\"" << std::endl; + msg << "If this is due to wrong face orientation, retry with the option --reorient \"" << std::endl; + throw std::runtime_error(msg.str()); + } + } + + input.close(); + + // Create domain + Mesh_domain cgal_domain(polyhedron); + + // Get sharp features + // cgal_domain.detect_features(); + + + // Mesh criteria + Mesh_criteria criteria( + CGAL::parameters::edge_size = max_edge_size_at_feature_edges, + CGAL::parameters::facet_angle = min_facet_angle, + CGAL::parameters::facet_size = max_radius_surface_delaunay_ball, + CGAL::parameters::facet_distance = max_facet_distance, + CGAL::parameters::cell_radius_edge_ratio = max_circumradius_edge_ratio, + CGAL::parameters::cell_size = max_cell_circumradius); + + // Mesh generation + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + C3t3 c3t3 = CGAL::make_mesh_3( + cgal_domain, criteria, + lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(), + odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(), + perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(), + exude ? + CGAL::parameters::exude( + CGAL::parameters::time_limit = exude_time_limit, + CGAL::parameters::sliver_bound = exude_sliver_bound + ) : + CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + + // Output + std::ofstream medit_file(outfile); + c3t3.output_to_medit(medit_file); + medit_file.close(); + + return; +} + +} // namespace pygalmesh diff --git a/src/generate_from_off.hpp b/src/generate_from_off.hpp new file mode 100644 index 0000000..96368f7 --- /dev/null +++ b/src/generate_from_off.hpp @@ -0,0 +1,32 @@ +#ifndef GENERATE_FROM_OFF_HPP +#define GENERATE_FROM_OFF_HPP + +#include +#include + +namespace pygalmesh { + +void +generate_from_off( + const std::string & infile, + const std::string & outfile, + const bool lloyd = false, + const bool odt = false, + const bool perturb = true, + const bool exude = true, + const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits::max(), + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const double max_circumradius_edge_ratio = 0.0, + const double max_cell_circumradius = 0.0, + const double exude_time_limit = 0.0, + const double exude_sliver_bound = 0.0, + const bool verbose = true, + const bool reorient = false, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // GENERATE_FROM_OFF_HPP diff --git a/src/generate_periodic.cpp b/src/generate_periodic.cpp new file mode 100644 index 0000000..2a4e8ce --- /dev/null +++ b/src/generate_periodic.cpp @@ -0,0 +1,123 @@ +#define CGAL_MESH_3_VERBOSE 1 + +#include "generate_periodic.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // CGAL_PI +#include +#include +#include + + +namespace pygalmesh { + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +typedef CGAL::Labeled_mesh_domain_3 Periodic_mesh_domain; + +// Triangulation +typedef CGAL::Periodic_3_mesh_triangulation_3::type Tr; +typedef CGAL::Mesh_complex_3_in_triangulation_3 C3t3; + +// Mesh Criteria +typedef CGAL::Mesh_criteria_3 Mesh_criteria; +typedef Mesh_criteria::Facet_criteria Facet_criteria; +typedef Mesh_criteria::Cell_criteria Cell_criteria; + + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef K::FT FT; +typedef K::Point_3 Point; +typedef K::Iso_cuboid_3 Iso_cuboid; +// Domain +typedef FT (Function)(const Point&); +typedef CGAL::Labeled_mesh_domain_3 Periodic_mesh_domain; +// Triangulation +typedef CGAL::Periodic_3_mesh_triangulation_3::type Tr; +typedef CGAL::Mesh_complex_3_in_triangulation_3 C3t3; +// Criteria +typedef CGAL::Mesh_criteria_3 Periodic_mesh_criteria; +// To avoid verbose function and named parameters call +using namespace CGAL::parameters; + +void +generate_periodic_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const std::array bounding_cuboid, + const bool lloyd, + const bool odt, + const bool perturb, + const bool exude, + const double max_edge_size_at_feature_edges, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const double max_circumradius_edge_ratio, + const double max_cell_circumradius, + const int number_of_copies_in_output, + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + K::Iso_cuboid_3 cuboid( + bounding_cuboid[0], + bounding_cuboid[1], + bounding_cuboid[2], + bounding_cuboid[3], + bounding_cuboid[4], + bounding_cuboid[5] + ); + + // wrap domain + const auto d = [&](K::Point_3 p) { + return domain->eval({p.x(), p.y(), p.z()}); + }; + Periodic_mesh_domain cgal_domain = + Periodic_mesh_domain::create_implicit_mesh_domain(d, cuboid); + + Mesh_criteria criteria( + CGAL::parameters::edge_size=max_edge_size_at_feature_edges, + CGAL::parameters::facet_angle=min_facet_angle, + CGAL::parameters::facet_size=max_radius_surface_delaunay_ball, + CGAL::parameters::facet_distance=max_facet_distance, + CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio, + CGAL::parameters::cell_size=max_cell_circumradius + ); + + // Mesh generation + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + C3t3 c3t3 = CGAL::make_periodic_3_mesh_3( + cgal_domain, + criteria, + lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(), + odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(), + perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(), + exude ? CGAL::parameters::exude() : CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + + // Output + std::ofstream medit_file(outfile); + CGAL::output_periodic_mesh_to_medit(medit_file, c3t3, number_of_copies_in_output); + medit_file.close(); + + return; +} + +} // namespace pygalmesh diff --git a/src/generate_periodic.hpp b/src/generate_periodic.hpp new file mode 100644 index 0000000..25ad5ca --- /dev/null +++ b/src/generate_periodic.hpp @@ -0,0 +1,33 @@ +#ifndef GENERATE_PERIODIC_HPP +#define GENERATE_PERIODIC_HPP + +#include "domain.hpp" + +#include +#include +#include + +namespace pygalmesh { + +void generate_periodic_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const std::array bounding_cuboid, + const bool lloyd = false, + const bool odt = false, + const bool perturb = true, + const bool exude = true, + const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits::max(), + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const double max_circumradius_edge_ratio = 0.0, + const double max_cell_circumradius = 0.0, + const int number_of_copies_in_output = 1, + const bool verbose = true, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // GENERATE_PERIODIC_HPP diff --git a/src/generate_surface_mesh.cpp b/src/generate_surface_mesh.cpp new file mode 100644 index 0000000..4d3f367 --- /dev/null +++ b/src/generate_surface_mesh.cpp @@ -0,0 +1,102 @@ +#define CGAL_SURFACE_MESHER_VERBOSE 1 + +#include "generate_surface_mesh.hpp" + +#include +#include +#include +#include +#include +#include + +namespace pygalmesh { + +// default triangulation for Surface_mesher +typedef CGAL::Surface_mesh_default_triangulation_3 Tr; +// c2t3 +typedef CGAL::Complex_2_in_triangulation_3 C2t3; +typedef Tr::Geom_traits GT; + +// Wrapper for DomainBase for translating to GT. +class CgalDomainWrapper +{ + public: + explicit CgalDomainWrapper(const std::shared_ptr & domain): + domain_(domain) + { + } + + virtual ~CgalDomainWrapper() = default; + + virtual + GT::FT + operator()(GT::Point_3 p) const + { + return domain_->eval({p.x(), p.y(), p.z()}); + } + + private: + const std::shared_ptr domain_; +}; + +typedef CGAL::Implicit_surface_3 Surface_3; + +void +generate_surface_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const double bounding_sphere_radius, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + const double bounding_sphere_radius2 = bounding_sphere_radius > 0 ? + bounding_sphere_radius*bounding_sphere_radius : + // add a little wiggle room + 1.01 * domain->get_bounding_sphere_squared_radius(); + + Tr tr; // 3D-Delaunay triangulation + C2t3 c2t3 (tr); // 2D-complex in 3D-Delaunay triangulation + + const auto d = CgalDomainWrapper(domain); + Surface_3 surface( + d, + GT::Sphere_3(CGAL::ORIGIN, bounding_sphere_radius2) + ); + + CGAL::Surface_mesh_default_criteria_3 criteria( + min_facet_angle, + max_radius_surface_delaunay_ball, + max_facet_distance + ); + + if (!verbose) { + // suppress output + std::cout.setstate(std::ios_base::failbit); + std::cerr.setstate(std::ios_base::failbit); + } + CGAL::make_surface_mesh( + c2t3, + surface, + criteria, + CGAL::Non_manifold_tag() + ); + if (!verbose) { + std::cout.clear(); + std::cerr.clear(); + } + + // Output + std::ofstream off_file(outfile); + CGAL::output_surface_facets_to_off(off_file, c2t3); + off_file.close(); + + return; +} + +} // namespace pygalmesh diff --git a/src/generate_surface_mesh.hpp b/src/generate_surface_mesh.hpp new file mode 100644 index 0000000..df88da4 --- /dev/null +++ b/src/generate_surface_mesh.hpp @@ -0,0 +1,24 @@ +#ifndef GENERATE_SURFACE_MESH_HPP +#define GENERATE_SURFACE_MESH_HPP + +#include "domain.hpp" + +#include +#include + +namespace pygalmesh { + +void generate_surface_mesh( + const std::shared_ptr & domain, + const std::string & outfile, + const double bounding_sphere_radius = 0.0, + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const bool verbose = true, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // GENERATE_SURFACE_MESH_HPP diff --git a/src/polygon2d.hpp b/src/polygon2d.hpp new file mode 100644 index 0000000..40144b1 --- /dev/null +++ b/src/polygon2d.hpp @@ -0,0 +1,308 @@ +#ifndef POLYGON2D_HPP +#define POLYGON2D_HPP + +#include "domain.hpp" + +#include +#include +#include +#include +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +namespace pygalmesh { + +class Polygon2D { + public: + explicit Polygon2D(const std::vector> & _points): + points(vector_to_cgal_points(_points)) + { + } + + virtual ~Polygon2D() = default; + + std::vector + vector_to_cgal_points(const std::vector> & _points) const + { + std::vector points2(_points.size()); + for (size_t i = 0; i < _points.size(); i++) { + assert(_points[i].size() == 2); + points2[i] = K::Point_2(_points[i][0], _points[i][1]); + } + return points2; + } + + bool + is_inside(const std::array & point) + { + K::Point_2 pt(point[0], point[1]); + switch(CGAL::bounded_side_2(this->points.begin(), this->points.end(), pt, K())) { + case CGAL::ON_BOUNDED_SIDE: + return true; + case CGAL::ON_BOUNDARY: + return true; + case CGAL::ON_UNBOUNDED_SIDE: + return false; + default: + return false; + } + return false; + } + + public: + const std::vector points; +}; + + +class Extrude: public pygalmesh::DomainBase { + public: + Extrude( + const std::shared_ptr & poly, + const std::array & direction, + const double alpha = 0.0, + const double max_edge_size_at_feature_edges = 0.0 + ): + poly_(poly), + direction_(direction), + alpha_(alpha), + max_edge_size_at_feature_edges_(max_edge_size_at_feature_edges) + { + } + + virtual ~Extrude() = default; + + virtual + double + eval(const std::array & x) const + { + if (x[2] < 0.0 || x[2] > direction_[2]) { + return 1.0; + } + + const double beta = x[2] / direction_[2]; + + std::array x2 = { + x[0] - beta * direction_[0], + x[1] - beta * direction_[1] + }; + + if (alpha_ != 0.0) { + std::array x3; + // turn by -beta*alpha + const double sinAlpha = sin(beta*alpha_); + const double cosAlpha = cos(beta*alpha_); + x3[0] = cosAlpha * x2[0] + sinAlpha * x2[1]; + x3[1] = -sinAlpha * x2[0] + cosAlpha * x2[1]; + x2 = x3; + } + + return poly_->is_inside(x2) ? -1.0 : 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + double max = 0.0; + for (const auto & pt: poly_->points) { + // bottom polygon + const double nrm0 = pt.x()*pt.x() + pt.y()*pt.y(); + if (nrm0 > max) { + max = nrm0; + } + + // TODO rotation + + // top polygon + const double x = pt.x() + direction_[0]; + const double y = pt.y() + direction_[1]; + const double z = direction_[2]; + const double nrm1 = x*x + y*y + z*z; + if (nrm1 > max) { + max = nrm1; + } + } + return max; + } + + virtual + std::vector>> + get_features() const + { + std::vector>> features = {}; + + size_t n; + + // bottom polygon + n = poly_->points.size(); + for (size_t i=0; i < n-1; i++) { + features.push_back({ + {poly_->points[i].x(), poly_->points[i].y(), 0.0}, + {poly_->points[i+1].x(), poly_->points[i+1].y(), 0.0} + }); + } + features.push_back({ + {poly_->points[n-1].x(), poly_->points[n-1].y(), 0.0}, + {poly_->points[0].x(), poly_->points[0].y(), 0.0} + }); + + // top polygon, R*x + d + n = poly_->points.size(); + const double sinAlpha = sin(alpha_); + const double cosAlpha = cos(alpha_); + for (size_t i=0; i < n-1; i++) { + features.push_back({ + { + cosAlpha * poly_->points[i].x() - sinAlpha * poly_->points[i].y() + direction_[0], + sinAlpha * poly_->points[i].x() + cosAlpha * poly_->points[i].y() + direction_[1], + direction_[2] + }, + { + cosAlpha * poly_->points[i+1].x() - sinAlpha * poly_->points[i+1].y() + direction_[0], + sinAlpha * poly_->points[i+1].x() + cosAlpha * poly_->points[i+1].y() + direction_[1], + direction_[2] + } + }); + } + features.push_back({ + { + cosAlpha * poly_->points[n-1].x() - sinAlpha * poly_->points[n-1].y() + direction_[0], + sinAlpha * poly_->points[n-1].x() + cosAlpha * poly_->points[n-1].y() + direction_[1], + direction_[2] + }, + { + cosAlpha * poly_->points[0].x() - sinAlpha * poly_->points[0].y() + direction_[0], + sinAlpha * poly_->points[0].x() + cosAlpha * poly_->points[0].y() + direction_[1], + direction_[2] + } + }); + + // features connecting the top and bottom + if (alpha_ == 0) { + for (const auto & pt: poly_->points) { + std::vector> line = { + {pt.x(), pt.y(), 0.0}, + {pt.x() + direction_[0], pt.y() + direction_[1], direction_[2]} + }; + features.push_back(line); + } + } else { + // Alright, we need to chop the lines on which the polygon corners are + // sitting into pieces. How long? About max_edge_size_at_feature_edges. For the starting point + // (x0, y0, z0) height h and angle alpha, the lines are given by + // + // f(beta) = ( + // cos(alpha*beta) x0 - sin(alpha*beta) y0, + // sin(alpha*beta) x0 + cos(alpha*beta) y0, + // z0 + beta * h + // ) + // + // with beta in [0, 1]. The length from beta0 till beta1 is then + // + // l = sqrt(alpha^2 (x0^2 + y0^2) + h^2) * (beta1 - beta0). + // + const double height = direction_[2]; + for (const auto & pt: poly_->points) { + const double l = sqrt(alpha_*alpha_ * (pt.x()*pt.x() + pt.y()*pt.y()) + height*height); + assert(max_edge_size_at_feature_edges_ > 0.0); + const size_t n = int(l / max_edge_size_at_feature_edges_ - 0.5) + 1; + std::vector> line = { + {pt.x(), pt.y(), 0.0}, + }; + for (size_t i=0; i < n; i++) { + const double beta = double(i+1) / n; + const double sinAB = sin(alpha_*beta); + const double cosAB = cos(alpha_*beta); + line.push_back({ + cosAB * pt.x() - sinAB * pt.y(), + sinAB * pt.x() + cosAB * pt.y(), + beta * height + }); + } + features.push_back(line); + } + } + + return features; + }; + + private: + const std::shared_ptr poly_; + const std::array direction_; + const double alpha_; + const double max_edge_size_at_feature_edges_; +}; + + +class ring_extrude: public pygalmesh::DomainBase { + public: + ring_extrude( + const std::shared_ptr & poly, + const double max_edge_size_at_feature_edges + ): + poly_(poly), + max_edge_size_at_feature_edges_(max_edge_size_at_feature_edges) + { + assert(max_edge_size_at_feature_edges > 0.0); + } + + virtual ~ring_extrude() = default; + + virtual + double + eval(const std::array & x) const + { + const double r = sqrt(x[0]*x[0] + x[1]*x[1]); + const double z = x[2]; + + return poly_->is_inside({r, z}) ? -1.0 : 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + double max = 0.0; + for (const auto & pt: poly_->points) { + const double nrm1 = pt.x()*pt.x() + pt.y()*pt.y(); + if (nrm1 > max) { + max = nrm1; + } + } + return max; + } + + virtual + std::vector>> + get_features() const + { + std::vector>> features = {}; + + for (const auto & pt: poly_->points) { + const double r = pt.x(); + const double circ = 2 * 3.14159265359 * r; + const size_t n = int(circ / max_edge_size_at_feature_edges_ - 0.5) + 1; + std::vector> line; + for (size_t i=0; i < n; i++) { + const double alpha = (2 * 3.14159265359 * i) / n; + line.push_back({ + r * cos(alpha), + r * sin(alpha), + pt.y() + }); + } + line.push_back(line.front()); + features.push_back(line); + } + return features; + } + + private: + const std::shared_ptr poly_; + const double max_edge_size_at_feature_edges_; +}; + +} // namespace pygalmesh + +#endif // POLYGON2D_HPP diff --git a/src/primitives.hpp b/src/primitives.hpp new file mode 100644 index 0000000..02ff35f --- /dev/null +++ b/src/primitives.hpp @@ -0,0 +1,484 @@ +// Note: +// One could also implement the primitives as Python classes and have much more readable +// code. Unfortunately, this approach would be a lot slower. The reason is that CGAL +// calls the eval() method with individual points, and this many CPP-to-Python calls are +// very expensive. It would be a lot better if CGAL called the method with batches of X +// at once, which would also enable users to employ some optimization, but this isn't +// the case yet. It's not clear if the mesh building algorithm allows for such +// optimziation at all. +// The corresponding issue hasn't gained much traction +// . +// +#ifndef PRIMITIVES_HPP +#define PRIMITIVES_HPP + +#include "domain.hpp" + +#include +#include + +namespace pygalmesh { + +class Ball: public pygalmesh::DomainBase +{ + public: + Ball( + const std::array & x0, + const double radius + ): + x0_(x0), + radius_(radius) + { + assert(x0_.size() == 3); + } + + virtual ~Ball() = default; + + virtual + double + eval(const std::array & x) const + { + const double xx0 = x[0] - x0_[0]; + const double yy0 = x[1] - x0_[1]; + const double zz0 = x[2] - x0_[2]; + return xx0*xx0 + yy0*yy0 + zz0*zz0 - radius_*radius_; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + const double x0_nrm = sqrt(x0_[0]*x0_[0] + x0_[1]*x0_[1] + x0_[2]*x0_[2]); + return (x0_nrm + radius_) * (x0_nrm + radius_); + } + + private: + const std::array x0_; + const double radius_; +}; + + +class Cuboid: public pygalmesh::DomainBase +{ + public: + Cuboid( + const std::array & x0, + const std::array & x1 + ): + x0_(x0), + x1_(x1) + { + } + + virtual ~Cuboid() = default; + + virtual + double + eval(const std::array & x) const + { + // TODO differentiable expression? + return std::max(std::max( + (x[0] - x0_[0]) * (x[0] - x1_[0]), + (x[1] - x0_[1]) * (x[1] - x1_[1]) + ), + (x[2] - x0_[2]) * (x[2] - x1_[2]) + ); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + const double x0_nrm2 = x0_[0]*x0_[0] + x0_[1]*x0_[1] + x0_[2]*x0_[2]; + const double x1_nrm2 = x1_[0]*x1_[0] + x1_[1]*x1_[1] + x1_[2]*x1_[2]; + return std::max({x0_nrm2, x1_nrm2}); + } + + virtual + std::vector>> + get_features() const + { + std::vector> corners = { + {x0_[0], x0_[1], x0_[2]}, + {x1_[0], x0_[1], x0_[2]}, + {x0_[0], x1_[1], x0_[2]}, + {x0_[0], x0_[1], x1_[2]}, + {x1_[0], x1_[1], x0_[2]}, + {x1_[0], x0_[1], x1_[2]}, + {x0_[0], x1_[1], x1_[2]}, + {x1_[0], x1_[1], x1_[2]} + }; + return { + {corners[0], corners[1]}, + {corners[0], corners[2]}, + {corners[0], corners[3]}, + {corners[1], corners[4]}, + {corners[1], corners[5]}, + {corners[2], corners[4]}, + {corners[2], corners[6]}, + {corners[3], corners[5]}, + {corners[3], corners[6]}, + {corners[4], corners[7]}, + {corners[5], corners[7]}, + {corners[6], corners[7]} + }; + }; + + private: + const std::array x0_; + const std::array x1_; +}; + + +class Ellipsoid: public pygalmesh::DomainBase +{ + public: + Ellipsoid( + const std::array & x0, + const double a0, + const double a1, + const double a2 + ): + x0_(x0), + a0_2_(a0*a0), + a1_2_(a1*a1), + a2_2_(a2*a1) + { + } + + virtual ~Ellipsoid() = default; + + virtual + double + eval(const std::array & x) const + { + const double xx0 = x[0] - x0_[0]; + const double yy0 = x[1] - x0_[1]; + const double zz0 = x[2] - x0_[2]; + return xx0*xx0/a0_2_ + yy0*yy0/a1_2_ + zz0*zz0/a2_2_ - 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return std::max({a0_2_, a1_2_, a2_2_}); + } + + private: + const std::array x0_; + const double a0_2_; + const double a1_2_; + const double a2_2_; +}; + + +class Cylinder: public pygalmesh::DomainBase +{ + public: + Cylinder( + const double z0, + const double z1, + const double radius, + const double feature_edge_length + ): + z0_(z0), + z1_(z1), + radius_(radius), + feature_edge_length_(feature_edge_length) + { + assert(z1_ > z0_); + } + + virtual ~Cylinder() = default; + + virtual + double + eval(const std::array & x) const + { + return (z0_ < x[2] && x[2] < z1_) ? + x[0]*x[0] + x[1]*x[1] - radius_*radius_ : + 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + const double zmax = std::max({abs(z0_), abs(z1_)}); + return zmax*zmax + radius_*radius_; + } + + virtual + std::vector>> + get_features() const + { + const double pi = 3.1415926535897932384; + const size_t n = 2 * pi * radius_ / feature_edge_length_; + std::vector> circ0(n+1); + std::vector> circ1(n+1); + for (size_t i=0; i < n; i++) { + const double c = radius_ * cos((2*pi * i) / n); + const double s = radius_ * sin((2*pi * i) / n); + circ0[i] = {c, s, z0_}; + circ1[i] = {c, s, z1_}; + } + // close the circles + circ0[n] = circ0[0]; + circ1[n] = circ1[0]; + return {circ0, circ1}; + }; + + private: + const double z0_; + const double z1_; + const double radius_; + const double feature_edge_length_; +}; + + +class Cone: public pygalmesh::DomainBase +{ + public: + Cone( + const double radius, + const double height, + const double feature_edge_length + ): + radius_(radius), + height_(height), + feature_edge_length_(feature_edge_length) + { + assert(radius_ > 0.0); + assert(height_ > 0.0); + } + + virtual ~Cone() = default; + + virtual + double + eval(const std::array & x) const + { + const double rad = radius_ * (1.0 - x[2] / height_); + + return (0.0 < x[2] && x[2] < height_) ? + x[0]*x[0] + x[1]*x[1] - rad*rad : + 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + const double max = std::max({radius_, height_}); + return max*max; + } + + virtual + std::vector>> + get_features() const + { + const double pi = 3.1415926535897932384; + const size_t n = 2 * pi * radius_ / feature_edge_length_; + std::vector> circ0(n+1); + for (size_t i=0; i < n; i++) { + const double c = radius_ * cos((2*pi * i) / n); + const double s = radius_ * sin((2*pi * i) / n); + circ0[i] = {c, s, 0.0}; + } + circ0[n] = circ0[0]; + return {circ0}; + }; + + private: + const double radius_; + const double height_; + const double feature_edge_length_; +}; + + +class Tetrahedron: public pygalmesh::DomainBase +{ + public: + Tetrahedron( + const std::array & x0, + const std::array & x1, + const std::array & x2, + const std::array & x3 + ): + x0_(Eigen::Vector3d(x0.data())), + x1_(Eigen::Vector3d(x1.data())), + x2_(Eigen::Vector3d(x2.data())), + x3_(Eigen::Vector3d(x3.data())), + A_(constructA(x0, x1, x2, x3)) + { + } + + Eigen::Matrix4d constructA( + const std::array & x0, + const std::array & x1, + const std::array & x2, + const std::array & x3 + ) { + Eigen::Matrix4d A; + A << x0[0], x1[0], x2[0], x3[0], + x0[1], x1[1], x2[1], x3[1], + x0[2], x1[2], x2[2], x3[2], + 1.0, 1.0, 1.0, 1.0; + return A; + } + + virtual ~Tetrahedron() = default; + + // bool isOnSameSide( + // const Eigen::Vector3d & v0, + // const Eigen::Vector3d & v1, + // const Eigen::Vector3d & v2, + // const Eigen::Vector3d & v3, + // const Eigen::Vector3d & p + // ) const + // { + // const auto normal = (v1 - v0).cross(v2 - v0); + // const double dot_v3 = normal.dot(v3 - v0); + // const double dot_p = normal.dot(p - v0); + // return ( + // (dot_v3 > 0 && dot_p > 0) || (dot_v3 < 0 && dot_p < 0) + // ); + // } + + virtual + double + eval(const std::array & x) const + { + Eigen::Vector4d b; + b << x[0], x[1], x[2], 1.0; + Eigen::Vector4d bary = A_.partialPivLu().solve(b); + return -bary.minCoeff(); + + // Eigen::Vector3d pvec(x.data()); + // const bool a = + // isOnSameSide(x0_, x1_, x2_, x3_, pvec) && + // isOnSameSide(x1_, x2_, x3_, x0_, pvec) && + // isOnSameSide(x2_, x3_, x0_, x1_, pvec) && + // isOnSameSide(x3_, x0_, x1_, x2_, pvec); + // return a ? -1.0 : 1.0; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return std::max({ + x0_.dot(x0_), + x1_.dot(x1_), + x2_.dot(x2_), + x3_.dot(x3_) + }); + } + + virtual + std::vector>> + get_features() const + { + std::vector> pts = { + {x0_[0], x0_[1], x0_[2]}, + {x1_[0], x1_[1], x1_[2]}, + {x2_[0], x2_[1], x2_[2]}, + {x3_[0], x3_[1], x3_[2]} + }; + return { + {pts[0], pts[1]}, + {pts[0], pts[2]}, + {pts[0], pts[3]}, + {pts[1], pts[2]}, + {pts[1], pts[3]}, + {pts[2], pts[3]} + }; + }; + + private: + const Eigen::Vector3d x0_; + const Eigen::Vector3d x1_; + const Eigen::Vector3d x2_; + const Eigen::Vector3d x3_; + const Eigen::Matrix4d A_; +}; + + +class Torus: public pygalmesh::DomainBase +{ + public: + Torus( + const double major_radius, + const double minor_radius + ): + major_radius_(major_radius), + minor_radius_(minor_radius) + { + } + + virtual ~Torus() = default; + + virtual + double + eval(const std::array & x) const + { + const double r = sqrt(x[0]*x[0] + x[1]*x[1]); + return ( + (r - major_radius_)*(r - major_radius_) + x[2]*x[2] + - minor_radius_*minor_radius_ + ); + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return (major_radius_ + minor_radius_)*(major_radius_ + minor_radius_); + } + + private: + const double major_radius_; + const double minor_radius_; +}; + + +class HalfSpace: public pygalmesh::DomainBase +{ + public: + HalfSpace( + const std::array & n, + const double alpha, + const double bounding_sphere_squared_radius + ): + n_(n), + alpha_(alpha), + bounding_sphere_squared_radius_(bounding_sphere_squared_radius) + { + } + + virtual ~HalfSpace() = default; + + virtual + double + eval(const std::array & x) const + { + return n_[0]*x[0] + n_[1]*x[1] + n_[2]*x[2] - alpha_; + } + + virtual + double + get_bounding_sphere_squared_radius() const + { + return bounding_sphere_squared_radius_; + } + + private: + const std::array n_; + const double alpha_; + const double bounding_sphere_squared_radius_; +}; + +} // namespace pygalmesh + +#endif // PRIMITIVES_HPP diff --git a/src/pybind11.cpp b/src/pybind11.cpp new file mode 100644 index 0000000..9c8afc1 --- /dev/null +++ b/src/pybind11.cpp @@ -0,0 +1,396 @@ +#include "domain.hpp" +#include "generate.hpp" +#include "generate_2d.hpp" +#include "generate_from_off.hpp" +#include "generate_from_inr.hpp" +#include "remesh_surface.hpp" +#include "generate_periodic.hpp" +#include "generate_surface_mesh.hpp" +#include "polygon2d.hpp" +#include "primitives.hpp" +#include "sizing_field.hpp" + +#include + +#include +#include + +namespace py = pybind11; +using namespace pygalmesh; + + +// https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python +class PyDomainBase: public DomainBase { +public: + using DomainBase::DomainBase; + + double + eval(const std::array & x) const override { + PYBIND11_OVERLOAD_PURE(double, DomainBase, eval, x); + } + + double + get_bounding_sphere_squared_radius() const override { + PYBIND11_OVERLOAD_PURE(double, DomainBase, get_bounding_sphere_squared_radius); + } + + // std::vector>> + // get_features() const override { + // PYBIND11_OVERLOAD( + // std::vector>>, + // DomainBase, + // get_features, + // 0.0 + // ); + // } +}; + + +// https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python +class PySizingFieldBase: public SizingFieldBase { +public: + using SizingFieldBase::SizingFieldBase; + + double + eval(const std::array & x) const override { + PYBIND11_OVERLOAD_PURE(double, SizingFieldBase, eval, x); + } +}; + + +PYBIND11_MODULE(_pygalmesh, m) { + // m.doc() = "documentation string"; + + // Domain base. + // shared_ptr b/c of + // + py::class_>(m, "DomainBase") + .def(py::init<>()) + .def("eval", &DomainBase::eval) + .def("get_bounding_sphere_squared_radius", &DomainBase::get_bounding_sphere_squared_radius) + .def("get_features", &DomainBase::get_features); + + // Sizing field base. + // shared_ptr b/c of + // + py::class_>(m, "SizingFieldBase") + .def(py::init<>()) + .def("eval", &SizingFieldBase::eval); + + // Domain transformations + py::class_>(m, "Translate") + .def(py::init< + const std::shared_ptr &, + const std::array & + >()) + .def("eval", &Translate::eval) + .def("translate_features", &Translate::translate_features) + .def("get_bounding_sphere_squared_radius", &Translate::get_bounding_sphere_squared_radius) + .def("get_features", &Translate::get_features); + + py::class_>(m, "Rotate") + .def(py::init< + const std::shared_ptr &, + const std::array &, + const double + >()) + .def("eval", &Rotate::eval) + .def("rotate", &Rotate::rotate) + .def("rotate_features", &Rotate::rotate_features) + .def("get_bounding_sphere_squared_radius", &Rotate::get_bounding_sphere_squared_radius) + .def("get_features", &Rotate::get_features); + + py::class_>(m, "Scale") + .def(py::init< + std::shared_ptr &, + const double + >()) + .def("eval", &Scale::eval) + .def("scale_features", &Scale::scale_features) + .def("get_bounding_sphere_squared_radius", &Scale::get_bounding_sphere_squared_radius) + .def("get_features", &Scale::get_features); + + py::class_>(m, "Stretch") + .def(py::init< + std::shared_ptr &, + const std::array & + >()) + .def("eval", &Stretch::eval) + .def("stretch_features", &Stretch::stretch_features) + .def("get_bounding_sphere_squared_radius", &Stretch::get_bounding_sphere_squared_radius) + .def("get_features", &Stretch::get_features); + + py::class_>(m, "Intersection") + .def(py::init< + std::vector> & + >()) + .def("eval", &Intersection::eval) + .def("get_bounding_sphere_squared_radius", &Intersection::get_bounding_sphere_squared_radius) + .def("get_features", &Intersection::get_features); + + py::class_>(m, "Union") + .def(py::init< + std::vector> & + >()) + .def("eval", &Union::eval) + .def("get_bounding_sphere_squared_radius", &Union::get_bounding_sphere_squared_radius) + .def("get_features", &Union::get_features); + + py::class_>(m, "Difference") + .def(py::init< + std::shared_ptr &, + std::shared_ptr & + >()) + .def("eval", &Difference::eval) + .def("get_bounding_sphere_squared_radius", &Difference::get_bounding_sphere_squared_radius) + .def("get_features", &Difference::get_features); + + // Primitives + py::class_>(m, "Ball") + .def(py::init< + const std::array &, + const double + >()) + .def("eval", &Ball::eval) + .def("get_bounding_sphere_squared_radius", &Ball::get_bounding_sphere_squared_radius); + + py::class_>(m, "Cuboid") + .def(py::init< + const std::array &, + const std::array & + >()) + .def("eval", &Cuboid::eval) + .def("get_bounding_sphere_squared_radius", &Cuboid::get_bounding_sphere_squared_radius) + .def("get_features", &Cuboid::get_features); + + py::class_>(m, "Ellipsoid") + .def(py::init< + const std::array &, + const double, + const double, + const double + >()) + .def("eval", &Ellipsoid::eval) + .def("get_bounding_sphere_squared_radius", &Ellipsoid::get_bounding_sphere_squared_radius) + .def("get_features", &Ellipsoid::get_features); + + py::class_>(m, "Cylinder") + .def(py::init< + const double, + const double, + const double, + const double + >()) + .def("eval", &Cylinder::eval) + .def("get_bounding_sphere_squared_radius", &Cylinder::get_bounding_sphere_squared_radius) + .def("get_features", &Cylinder::get_features); + + py::class_>(m, "Cone") + .def(py::init< + const double, + const double, + const double + >()) + .def("eval", &Cone::eval) + .def("get_bounding_sphere_squared_radius", &Cone::get_bounding_sphere_squared_radius) + .def("get_features", &Cone::get_features); + + py::class_>(m, "Tetrahedron") + .def(py::init< + const std::array &, + const std::array &, + const std::array &, + const std::array & + >()) + .def("eval", &Tetrahedron::eval) + .def("get_bounding_sphere_squared_radius", &Tetrahedron::get_bounding_sphere_squared_radius) + .def("get_features", &Tetrahedron::get_features); + + py::class_>(m, "Torus") + .def(py::init< + const double, + const double + >()) + .def("eval", &Torus::eval) + .def("get_bounding_sphere_squared_radius", &Torus::get_bounding_sphere_squared_radius) + .def("get_features", &Torus::get_features); + + py::class_>(m, "HalfSpace") + .def(py::init< + const std::array &, + const double, + const double + >()) + .def("eval", &HalfSpace::eval) + .def("get_bounding_sphere_squared_radius", &HalfSpace::get_bounding_sphere_squared_radius); + + // polygon2d + py::class_>(m, "Polygon2D") + .def(py::init< + const std::vector> & + >()) + .def("vector_to_cgal_points", &Polygon2D::vector_to_cgal_points) + .def("is_inside", &Polygon2D::is_inside); + + py::class_>(m, "Extrude") + .def(py::init< + const std::shared_ptr &, + const std::array &, + const double, + const double + >(), + py::arg("poly"), + py::arg("direction"), + py::arg("alpha") = 0.0, + py::arg("max_edge_size_at_feature_edges") = 0.0 + ) + .def("eval", &Extrude::eval) + .def("get_bounding_sphere_squared_radius", &Extrude::get_bounding_sphere_squared_radius) + .def("get_features", &Extrude::get_features); + + py::class_>(m, "RingExtrude") + .def(py::init< + const std::shared_ptr &, + const double + >()) + .def("eval", &ring_extrude::eval) + .def("get_bounding_sphere_squared_radius", &ring_extrude::get_bounding_sphere_squared_radius) + .def("get_features", &ring_extrude::get_features); + + // functions + m.def( + "_generate_2d", &generate_2d, + py::arg("points"), + py::arg("constraints"), + py::arg("max_circumradius_shortest_edge_ratio") = 1.41421356237, + py::arg("max_edge_size") = 0.0, + py::arg("num_lloyd_steps") = 0 + ); + m.def( + "_generate_mesh", &generate_mesh, + py::arg("domain"), + py::arg("outfile"), + py::arg("extra_feature_edges") = std::vector>>(), + py::arg("bounding_sphere_radius") = 0.0, + py::arg("lloyd") = false, + py::arg("odt") = false, + py::arg("perturb") = true, + py::arg("exude") = true, + py::arg("max_edge_size_at_feature_edges_value") = 0.0, + py::arg("max_edge_size_at_feature_edges_field") = nullptr, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball_value") = 0.0, + py::arg("max_radius_surface_delaunay_ball_field") = nullptr, + py::arg("max_facet_distance_value") = 0.0, + py::arg("max_facet_distance_field") = nullptr, + py::arg("max_circumradius_edge_ratio") = 0.0, + py::arg("max_cell_circumradius_value") = 0.0, + py::arg("max_cell_circumradius_field") = nullptr, + py::arg("exude_time_limit") = 0.0, + py::arg("exude_sliver_bound") = 0.0, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.def( + "_generate_periodic_mesh", &generate_periodic_mesh, + py::arg("domain"), + py::arg("outfile"), + py::arg("bounding_cuboid"), + py::arg("lloyd") = false, + py::arg("odt") = false, + py::arg("perturb") = true, + py::arg("exude") = true, + py::arg("max_edge_size_at_feature_edges") = 0.0, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("max_circumradius_edge_ratio") = 0.0, + py::arg("max_cell_circumradius") = 0.0, + py::arg("number_of_copies_in_output") = 1, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.def( + "_generate_surface_mesh", &generate_surface_mesh, + py::arg("domain"), + py::arg("outfile"), + py::arg("bounding_sphere_radius") = 0.0, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.def( + "_generate_from_off", &generate_from_off, + py::arg("infile"), + py::arg("outfile"), + py::arg("lloyd") = false, + py::arg("odt") = false, + py::arg("perturb") = true, + py::arg("exude") = true, + py::arg("max_edge_size_at_feature_edges") = 0.0, // std::numeric_limits::max(), + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("max_circumradius_edge_ratio") = 0.0, + py::arg("max_cell_circumradius") = 0.0, + py::arg("exude_time_limit") = 0.0, + py::arg("exude_sliver_bound") = 0.0, + py::arg("verbose") = true, + py::arg("reorient") = false, + py::arg("seed") = 0 + ); + m.def( + "_generate_from_inr", &generate_from_inr, + py::arg("inr_filename"), + py::arg("outfile"), + py::arg("lloyd") = false, + py::arg("odt") = false, + py::arg("perturb") = true, + py::arg("exude") = true, + py::arg("max_edge_size_at_feature_edges") = 0.0, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("max_circumradius_edge_ratio") = 0.0, + py::arg("max_cell_circumradius") = 0.0, + py::arg("exude_time_limit") = 0.0, + py::arg("exude_sliver_bound") = 0.0, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.def( + "_generate_from_inr_with_subdomain_sizing", &generate_from_inr_with_subdomain_sizing, + py::arg("inr_filename"), + py::arg("outfile"), + py::arg("default_max_cell_circumradius"), + py::arg("max_cell_circumradiuss"), + py::arg("cell_labels"), + py::arg("lloyd") = false, + py::arg("odt") = false, + py::arg("perturb") = true, + py::arg("exude") = true, + py::arg("max_edge_size_at_feature_edges") = 0.0, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("max_circumradius_edge_ratio") = 0.0, + py::arg("exude_time_limit") = 0.0, + py::arg("exude_sliver_bound") = 0.0, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.def( + "_remesh_surface", &remesh_surface, + py::arg("infile"), + py::arg("outfile"), + py::arg("max_edge_size_at_feature_edges") = 0.0, + py::arg("min_facet_angle") = 0.0, + py::arg("max_radius_surface_delaunay_ball") = 0.0, + py::arg("max_facet_distance") = 0.0, + py::arg("verbose") = true, + py::arg("seed") = 0 + ); + m.attr("_CGAL_VERSION_STR") = CGAL_VERSION_STR; +} diff --git a/src/remesh_surface.cpp b/src/remesh_surface.cpp new file mode 100644 index 0000000..48e838b --- /dev/null +++ b/src/remesh_surface.cpp @@ -0,0 +1,87 @@ +#define CGAL_MESH_3_VERBOSE 1 + +#include "remesh_surface.hpp" + +#include +#include +#include +#include +#include +#include + +namespace pygalmesh { +// Domain +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Polyhedral_mesh_domain_with_features_3 Mesh_domain; +// Polyhedron type +typedef CGAL::Mesh_polyhedron_3::type Polyhedron; +// Triangulation +typedef CGAL::Mesh_triangulation_3::type Tr; +typedef CGAL::Mesh_complex_3_in_triangulation_3< + Tr,Mesh_domain::Corner_index,Mesh_domain::Curve_index> C3t3; +// Criteria +typedef CGAL::Mesh_criteria_3 Mesh_criteria; + +// +void +remesh_surface( + const std::string & infile, + const std::string & outfile, + const double max_edge_size_at_feature_edges, + const double min_facet_angle, + const double max_radius_surface_delaunay_ball, + const double max_facet_distance, + const bool verbose, + const int seed + ) +{ + CGAL::get_default_random() = CGAL::Random(seed); + + // Load a polyhedron + Polyhedron poly; + std::ifstream input(infile.c_str()); + input >> poly; + if (!CGAL::is_triangle_mesh(poly)){ + throw "Input geometry is not triangulated."; + } + // Create a vector with only one element: the pointer to the polyhedron. + std::vector poly_ptrs_vector(1, &poly); + // Create a polyhedral domain with only one polyhedron and no "bounding polyhedron" + // so the volumetric part of the domain will be empty. + Mesh_domain domain(poly_ptrs_vector.begin(), poly_ptrs_vector.end()); + + // Get sharp features + domain.detect_features(); //includes detection of borders + // Mesh criteria + Mesh_criteria criteria( + CGAL::parameters::edge_size=max_edge_size_at_feature_edges, + CGAL::parameters::facet_angle=min_facet_angle, + CGAL::parameters::facet_size=max_radius_surface_delaunay_ball, + CGAL::parameters::facet_distance=max_facet_distance + ); + + // Mesh generation + if (!verbose) { + // suppress output + std::cerr.setstate(std::ios_base::failbit); + } + C3t3 c3t3 = CGAL::make_mesh_3( + domain, + criteria, + CGAL::parameters::no_perturb(), + CGAL::parameters::no_exude() + ); + if (!verbose) { + std::cerr.clear(); + } + // Output the facets of the c3t3 to an OFF file. The facets will not be + // oriented. + std::ofstream off_file(outfile.c_str()); + c3t3.output_boundary_to_off(off_file); + if (off_file.fail()) { + throw "Failed to write OFF."; + } + return; +} + +} // namespace pygalmesh diff --git a/src/remesh_surface.hpp b/src/remesh_surface.hpp new file mode 100644 index 0000000..0e6043b --- /dev/null +++ b/src/remesh_surface.hpp @@ -0,0 +1,22 @@ +#ifndef REMESH_SURFACE_HPP +#define REMESH_SURFACE_HPP + +#include +#include + +namespace pygalmesh { + +void remesh_surface( + const std::string & infilen, + const std::string & outfile, + const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits::max(), + const double min_facet_angle = 0.0, + const double max_radius_surface_delaunay_ball = 0.0, + const double max_facet_distance = 0.0, + const bool verbose = true, + const int seed = 0 + ); + +} // namespace pygalmesh + +#endif // REMESH_SURFACE_HPP diff --git a/src/sizing_field.hpp b/src/sizing_field.hpp new file mode 100644 index 0000000..db6ab4b --- /dev/null +++ b/src/sizing_field.hpp @@ -0,0 +1,23 @@ +#ifndef SIZING_FIELD_HPP +#define SIZING_FIELD_HPP + +#include +#include + +namespace pygalmesh { + +class SizingFieldBase +{ + public: + + virtual ~SizingFieldBase() = default; + + virtual + double + eval(const std::array & x) const = 0; + + double val = -1.0; +}; + +} // namespace pygalmesh +#endif // SIZING_FIELD_HPP diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..ad7d826 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,36 @@ +import numpy + + +def _row_dot(a, b): + # http://stackoverflow.com/a/26168677/353337 + return numpy.einsum("ij, ij->i", a, b) + + +def compute_volumes(vertices, tets): + cell_coords = vertices[tets] + + a = cell_coords[:, 1, :] - cell_coords[:, 0, :] + b = cell_coords[:, 2, :] - cell_coords[:, 0, :] + c = cell_coords[:, 3, :] - cell_coords[:, 0, :] + + # omega = + omega = _row_dot(a, numpy.cross(b, c)) + + # https://en.wikipedia.org/wiki/Tetrahedron#Volume + return abs(omega) / 6.0 + + +def compute_triangle_areas(vertices, triangles): + e0 = vertices[triangles[:, 1]] - vertices[triangles[:, 0]] + e1 = vertices[triangles[:, 2]] - vertices[triangles[:, 1]] + + assert e0.shape == e1.shape + if e0.shape[1] == 2: + z_component_of_e0_cross_e1 = numpy.cross(e0, e1) + cross_magnitude = z_component_of_e0_cross_e1 + else: + assert e0.shape[1] == 3 + e0_cross_e1 = numpy.cross(e0, e1) + cross_magnitude = numpy.sqrt(_row_dot(e0_cross_e1, e0_cross_e1)) + + return 0.5 * cross_magnitude diff --git a/tests/meshes/elephant.vtu b/tests/meshes/elephant.vtu new file mode 100644 index 0000000..23c4edd --- /dev/null +++ b/tests/meshes/elephant.vtu @@ -0,0 +1,15 @@ + + + + + + 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 + AgAAAACAAACwLQAAlhgAAOQIAAA=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 + AgAAAACAAACwLQAASgAAACwAAAA=eJztxaEBAAAMAqAV/395wTOEQq5i27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt2/bwD+weUAF4nO3FoQEAAAwCoBX/f3nBF4xQyFVs27Zt27Zt27Zt27Zt27Zt29MfEfscjw== + + + + diff --git a/tests/meshes/sphere.inr b/tests/meshes/sphere.inr new file mode 100644 index 0000000..f43ad43 --- /dev/null +++ b/tests/meshes/sphere.inr @@ -0,0 +1,132 @@ +#INRIMAGE-4#{ +XDIM=10 +YDIM=10 +ZDIM=10 +VDIM=1 +TYPE=unsigned fixed +PIXSIZE=8 bits +SCALE=2**0 +CPU=decm +VX=1 +VY=1 +VZ=1 +#GEOMETRY=CARTESIAN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +##} + \ No newline at end of file diff --git a/tests/test_2d.py b/tests/test_2d.py new file mode 100644 index 0000000..953d4da --- /dev/null +++ b/tests/test_2d.py @@ -0,0 +1,49 @@ +import numpy as np +from helpers import compute_triangle_areas + +import pygalmesh + + +def test_rectangle(): + points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]) + constraints = [[0, 1], [1, 2], [2, 3], [3, 0]] + + mesh = pygalmesh.generate_2d( + points, constraints, max_edge_size=1.0e-1, num_lloyd_steps=10 + ) + + assert mesh.points.shape == (276, 2) + assert mesh.get_cells_type("triangle").shape == (486, 3) + + # # show mesh + # import matplotlib.pyplot as plt + # pts = points[cells] + # for pt in pts: + # plt.plot([pt[0][0], pt[1][0]], [pt[0][1], pt[1][1]], "-k") + # plt.plot([pt[1][0], pt[2][0]], [pt[1][1], pt[2][1]], "-k") + # plt.plot([pt[2][0], pt[0][0]], [pt[2][1], pt[0][1]], "-k") + # # for pt in points: + # # plt.plot(pt[0], pt[1], "or") + # plt.gca().set_aspect("equal") + # plt.show() + + # mesh.points *= 100 + # mesh.write("rect.svg") + + +def test_disk(): + h = 0.1 + n = int(2 * np.pi / h) + alpha = np.linspace(0.0, 2 * np.pi, n + 1, endpoint=False) + points = np.column_stack([np.cos(alpha), np.sin(alpha)]) + + constraints = [[k, k + 1] for k in range(n)] + [[n, 0]] + mesh = pygalmesh.generate_2d( + points, constraints, max_edge_size=h, num_lloyd_steps=0 + ) + areas = compute_triangle_areas(mesh.points, mesh.get_cells_type("triangle")) + assert np.all(areas > 1.0e-5) + + +if __name__ == "__main__": + test_disk() diff --git a/tests/test_from_array.py b/tests/test_from_array.py new file mode 100644 index 0000000..4f9595d --- /dev/null +++ b/tests/test_from_array.py @@ -0,0 +1,72 @@ +import helpers +import numpy as np + +import pygalmesh + + +def test_from_array(): + n = 200 + shape = (n, n, n) + h = (1.0 / shape[0], 1.0 / shape[1], 1.0 / shape[2]) + vol = np.zeros(shape, dtype=np.uint16) + i, j, k = np.arange(shape[0]), np.arange(shape[1]), np.arange(shape[2]) + ii, jj, kk = np.meshgrid(i, j, k) + vol[ii * ii + jj * jj + kk * kk < n ** 2] = 1 + vol[ii * ii + jj * jj + kk * kk < (0.5 * n) ** 2] = 2 + + mesh = pygalmesh.generate_from_array( + vol, + h, + max_cell_circumradius=100 * min(h), + max_facet_distance=min(h), + verbose=False, + ) + + tol = min(h) + ref = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] + assert abs(max(mesh.points[:, 0]) - ref[0]) < (1.0 + ref[0]) * tol + assert abs(min(mesh.points[:, 0]) - ref[1]) < (1.0 + ref[1]) * tol + assert abs(max(mesh.points[:, 1]) - ref[2]) < (1.0 + ref[2]) * tol + assert abs(min(mesh.points[:, 1]) - ref[3]) < (1.0 + ref[3]) * tol + assert abs(max(mesh.points[:, 2]) - ref[4]) < (1.0 + ref[4]) * tol + assert abs(min(mesh.points[:, 2]) - ref[5]) < (1.0 + ref[5]) * tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref = 1.0 / 6.0 * np.pi + # Debian needs 2.0e-2 here. + # + assert abs(vol - ref) < ref * 2.0e-2 + + +def test_from_array_with_subdomain_sizing(): + n = 200 + shape = (n, n, n) + h = (1.0 / shape[0], 1.0 / shape[1], 1.0 / shape[2]) + vol = np.zeros(shape, dtype=np.uint16) + i, j, k = np.arange(shape[0]), np.arange(shape[1]), np.arange(shape[2]) + ii, jj, kk = np.meshgrid(i, j, k) + vol[ii * ii + jj * jj + kk * kk < n ** 2] = 1 + vol[ii * ii + jj * jj + kk * kk < (0.5 * n) ** 2] = 2 + + mesh = pygalmesh.generate_from_array( + vol, + h, + max_cell_circumradius={1: 100 * min(h), 2: 10 * min(h)}, + max_facet_distance=min(h), + verbose=False, + ) + + tol = min(h) + ref = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] + assert abs(max(mesh.points[:, 0]) - ref[0]) < tol + assert abs(min(mesh.points[:, 0]) - ref[1]) < tol + assert abs(max(mesh.points[:, 1]) - ref[2]) < tol + assert abs(min(mesh.points[:, 1]) - ref[3]) < tol + assert abs(max(mesh.points[:, 2]) - ref[4]) < tol + assert abs(min(mesh.points[:, 2]) - ref[5]) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref = 1.0 / 6.0 * np.pi + # Debian needs 2.0e-2 here. + # + assert abs(vol - ref) < ref * 2.0e-2 diff --git a/tests/test_inr.py b/tests/test_inr.py new file mode 100644 index 0000000..333673d --- /dev/null +++ b/tests/test_inr.py @@ -0,0 +1,44 @@ +import pathlib +import tempfile + +import helpers +import meshio + +import pygalmesh + + +def test_inr(): + this_dir = pathlib.Path(__file__).resolve().parent + # mesh = pygalmesh.generate_from_inr( + # this_dir / "meshes" / "skull_2.9.inr", max_cell_circumradius=5.0, verbose=False + # ) + with tempfile.TemporaryDirectory() as tmp: + out_filename = str(pathlib.Path(tmp) / "out.vtk") + pygalmesh._cli.cli( + [ + "from-inr", + str(this_dir / "meshes" / "sphere.inr"), + out_filename, + "--max-cell-circumradius", + "1.0", + "--quiet", + ] + ) + mesh = meshio.read(out_filename) + + vals_refs = [ + (max(mesh.points[:, 0]), 9.00478554e00), + (min(mesh.points[:, 0]), -4.25843196e-03), + (max(mesh.points[:, 1]), 9.00332642e00), + (min(mesh.points[:, 1]), -4.41271299e-03), + (max(mesh.points[:, 2]), 9.00407982e00), + (min(mesh.points[:, 2]), -3.98639357e-03), + ] + for val, ref in vals_refs: + assert abs(val - ref) < 1.0e-3 * (1.0 + abs(ref)), f"{val:.8e} != {ref:.8e}" + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref = 6.95558790e02 + # Debian needs 2.0e-2 here. + # + assert abs(vol - ref) < ref * 2.0e-2, f"{vol:.8e}" diff --git a/tests/test_periodic.py b/tests/test_periodic.py new file mode 100644 index 0000000..c35ffa8 --- /dev/null +++ b/tests/test_periodic.py @@ -0,0 +1,42 @@ +import numpy + +import pygalmesh + + +def test_schwarz(): + class Schwarz(pygalmesh.DomainBase): + def __init__(self): + super().__init__() + + def eval(self, x): + x2 = numpy.cos(x[0] * 2 * numpy.pi) + y2 = numpy.cos(x[1] * 2 * numpy.pi) + z2 = numpy.cos(x[2] * 2 * numpy.pi) + return x2 + y2 + z2 + + mesh = pygalmesh.generate_periodic_mesh( + Schwarz(), + [0, 0, 0, 1, 1, 1], + max_cell_circumradius=0.05, + min_facet_angle=30, + max_radius_surface_delaunay_ball=0.05, + max_facet_distance=0.025, + max_circumradius_edge_ratio=2.0, + number_of_copies_in_output=4, + # odt=True, + # lloyd=True, + verbose=False, + ) + + # The RNG in CGAL makes the following assertions fail sometimes. + # assert len(mesh.cells["triangle"]) == 12784 + # assert len(mesh.cells["tetra"]) == 67120 + + return mesh + + +if __name__ == "__main__": + import meshio + + mesh = test_schwarz() + meshio.write("out.vtk", mesh) diff --git a/tests/test_remesh_surface.py b/tests/test_remesh_surface.py new file mode 100644 index 0000000..0e0832c --- /dev/null +++ b/tests/test_remesh_surface.py @@ -0,0 +1,56 @@ +import pathlib +import tempfile + +import helpers +import meshio + +import pygalmesh + + +def test_remesh_surface(): + this_dir = pathlib.Path(__file__).resolve().parent + # mesh = pygalmesh.remesh_surface( + # this_dir / "meshes" / "lion-head.vtu", + # max_edge_size_at_feature_edges=0.025, + # min_facet_angle=25, + # max_radius_surface_delaunay_ball=0.1, + # max_facet_distance=0.001, + # verbose=False, + # ) + with tempfile.TemporaryDirectory() as tmp: + out_filename = str(pathlib.Path(tmp) / "out.vtk") + pygalmesh._cli.cli( + [ + "remesh-surface", + str(this_dir / "meshes" / "elephant.vtu"), + out_filename, + "--max-edge-size-at-feature-edges", + "0.025", + "--min-facet-angle", + "25", + "--max-radius-surface-delaunay-ball", + "0.1", + "--max-facet-distance", + "0.001", + "--quiet", + ] + ) + mesh = meshio.read(out_filename) + + vals_refs = [ + (max(mesh.points[:, 0]), 3.60217000e-01), + (min(mesh.points[:, 0]), -3.60140000e-01), + (max(mesh.points[:, 1]), 4.98948000e-01), + (min(mesh.points[:, 1]), -4.99336000e-01), + (max(mesh.points[:, 2]), 3.00977000e-01), + (min(mesh.points[:, 2]), -3.01316000e-01), + ] + for val, ref in vals_refs: + assert abs(val - ref) < 1.0e-3 * (1.0 + abs(ref)), f"{val:.8e} != {ref:.8e}" + + triangle_areas = helpers.compute_triangle_areas( + mesh.points, mesh.get_cells_type("triangle") + ) + vol = sum(triangle_areas) + ref = 1.2357989593759846 + assert abs(vol - ref) < ref * 1.0e-3, vol diff --git a/tests/test_surface_mesh.py b/tests/test_surface_mesh.py new file mode 100644 index 0000000..3849a1f --- /dev/null +++ b/tests/test_surface_mesh.py @@ -0,0 +1,28 @@ +import helpers +import numpy + +import pygalmesh + + +def test_sphere(): + radius = 1.0 + s = pygalmesh.Ball([0.0, 0.0, 0.0], radius) + mesh = pygalmesh.generate_surface_mesh( + s, + min_facet_angle=30.0, + max_radius_surface_delaunay_ball=0.1, + max_facet_distance=0.1, + verbose=False, + ) + + tol = 1.0e-2 + assert abs(max(mesh.points[:, 0]) - radius) < (1.0 + radius) * tol + assert abs(min(mesh.points[:, 0]) + radius) < (1.0 + radius) * tol + assert abs(max(mesh.points[:, 1]) - radius) < (1.0 + radius) * tol + assert abs(min(mesh.points[:, 1]) + radius) < (1.0 + radius) * tol + assert abs(max(mesh.points[:, 2]) - radius) < (1.0 + radius) * tol + assert abs(min(mesh.points[:, 2]) + radius) < (1.0 + radius) * tol + + areas = helpers.compute_triangle_areas(mesh.points, mesh.get_cells_type("triangle")) + surface_area = sum(areas) + assert abs(surface_area - 4 * numpy.pi * radius ** 2) < 0.1 diff --git a/tests/test_volume_from_surface.py b/tests/test_volume_from_surface.py new file mode 100644 index 0000000..076d071 --- /dev/null +++ b/tests/test_volume_from_surface.py @@ -0,0 +1,53 @@ +import pathlib +import tempfile + +import helpers +import meshio + +import pygalmesh + + +def test_volume_from_surface(): + this_dir = pathlib.Path(__file__).resolve().parent + # mesh = pygalmesh.generate_volume_mesh_from_surface_mesh( + # this_dir / "meshes" / "elephant.vtu", + # min_facet_angle=25.0, + # max_radius_surface_delaunay_ball=0.15, + # max_facet_distance=0.008, + # max_circumradius_edge_ratio=3.0, + # verbose=False, + # ) + with tempfile.TemporaryDirectory() as tmp: + out_filename = str(pathlib.Path(tmp) / "out.vtk") + pygalmesh._cli.cli( + [ + "volume-from-surface", + str(this_dir / "meshes" / "elephant.vtu"), + out_filename, + "--min-facet-angle", + "0.5", + "--max-radius-surface-delaunay-ball", + "0.15", + "--max-facet-distance", + "0.008", + "--max-circumradius-edge-ratio", + "3.0", + "--quiet", + ] + ) + mesh = meshio.read(out_filename) + + tol = 2.0e-2 + vals_refs = [ + (max(mesh.points[:, 0]), +0.357612477657), + (min(mesh.points[:, 0]), -0.358747130015), + (max(mesh.points[:, 1]), +0.496137874959), + (min(mesh.points[:, 1]), -0.495301051456), + (max(mesh.points[:, 2]), +0.298780230629), + (min(mesh.points[:, 2]), -0.300472866512), + ] + for val, ref in vals_refs: + assert abs(val - ref) < (1.0 + ref) * tol, f"{val:.15e} != {ref:.15e}" + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 0.044164693065) < (1.0 + vol) * tol diff --git a/tests/test_volume_mesh.py b/tests/test_volume_mesh.py new file mode 100644 index 0000000..394e3bb --- /dev/null +++ b/tests/test_volume_mesh.py @@ -0,0 +1,616 @@ +import helpers +import numpy + +import pygalmesh + + +def test_ball(): + s = pygalmesh.Ball([0.0, 0.0, 0.0], 1.0) + mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.2, verbose=False) + + assert abs(max(mesh.points[:, 0]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 0]) + 1.0) < 0.02 + assert abs(max(mesh.points[:, 1]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 1]) + 1.0) < 0.02 + assert abs(max(mesh.points[:, 2]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 2]) + 1.0) < 0.02 + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 4.0 / 3.0 * numpy.pi) < 0.15 + + +def test_balls_union(): + radius = 1.0 + displacement = 0.5 + s0 = pygalmesh.Ball([displacement, 0, 0], radius) + s1 = pygalmesh.Ball([-displacement, 0, 0], radius) + u = pygalmesh.Union([s0, s1]) + + a = numpy.sqrt(radius ** 2 - displacement ** 2) + max_edge_size_at_feature_edges = 0.1 + n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges) + circ = [ + [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)] + for i in range(n) + ] + circ.append(circ[0]) + + mesh = pygalmesh.generate_mesh( + u, + extra_feature_edges=[circ], + max_cell_circumradius=0.15, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + assert abs(max(mesh.points[:, 0]) - (radius + displacement)) < 0.02 + assert abs(min(mesh.points[:, 0]) + (radius + displacement)) < 0.02 + assert abs(max(mesh.points[:, 1]) - radius) < 0.02 + assert abs(min(mesh.points[:, 1]) + radius) < 0.02 + assert abs(max(mesh.points[:, 2]) - radius) < 0.02 + assert abs(min(mesh.points[:, 2]) + radius) < 0.02 + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + h = radius - displacement + ref_vol = 2 * ( + 4.0 / 3.0 * numpy.pi * radius ** 3 - h * numpy.pi / 6.0 * (3 * a ** 2 + h ** 2) + ) + + assert abs(vol - ref_vol) < 0.1 + + +def test_balls_intersection(): + radius = 1.0 + displacement = 0.5 + s0 = pygalmesh.Ball([displacement, 0, 0], radius) + s1 = pygalmesh.Ball([-displacement, 0, 0], radius) + u = pygalmesh.Intersection([s0, s1]) + + a = numpy.sqrt(radius ** 2 - displacement ** 2) + max_edge_size_at_feature_edges = 0.1 + n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges) + circ = [ + [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)] + for i in range(n) + ] + circ.append(circ[0]) + + mesh = pygalmesh.generate_mesh( + u, + extra_feature_edges=[circ], + max_cell_circumradius=0.15, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + assert abs(max(mesh.points[:, 0]) - (radius - displacement)) < 0.02 + assert abs(min(mesh.points[:, 0]) + (radius - displacement)) < 0.02 + assert abs(max(mesh.points[:, 1]) - a) < 0.02 + assert abs(min(mesh.points[:, 1]) + a) < 0.02 + assert abs(max(mesh.points[:, 2]) - a) < 0.02 + assert abs(min(mesh.points[:, 2]) + a) < 0.02 + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + h = radius - displacement + ref_vol = 2 * (h * numpy.pi / 6.0 * (3 * a ** 2 + h ** 2)) + + assert abs(vol - ref_vol) < 0.1 + + +def test_balls_difference(): + radius = 1.0 + displacement = 0.5 + s0 = pygalmesh.Ball([displacement, 0, 0], radius) + s1 = pygalmesh.Ball([-displacement, 0, 0], radius) + u = pygalmesh.Difference(s0, s1) + + a = numpy.sqrt(radius ** 2 - displacement ** 2) + max_edge_size_at_feature_edges = 0.15 + n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges) + circ = [ + [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)] + for i in range(n) + ] + circ.append(circ[0]) + + mesh = pygalmesh.generate_mesh( + u, + extra_feature_edges=[circ], + max_cell_circumradius=0.15, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + min_facet_angle=25, + max_radius_surface_delaunay_ball=0.15, + max_circumradius_edge_ratio=2.0, + verbose=False, + ) + + tol = 0.02 + assert abs(max(mesh.points[:, 0]) - (radius + displacement)) < tol + assert abs(min(mesh.points[:, 0]) - 0.0) < tol + assert abs(max(mesh.points[:, 1]) - radius) < tol + assert abs(min(mesh.points[:, 1]) + radius) < tol + assert abs(max(mesh.points[:, 2]) - radius) < tol + assert abs(min(mesh.points[:, 2]) + radius) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + h = radius - displacement + ref_vol = 4.0 / 3.0 * numpy.pi * radius ** 3 - 2 * h * numpy.pi / 6.0 * ( + 3 * a ** 2 + h ** 2 + ) + + assert abs(vol - ref_vol) < 0.05 + + +def test_cuboids_intersection(): + c0 = pygalmesh.Cuboid([0, 0, -0.5], [3, 3, 0.5]) + c1 = pygalmesh.Cuboid([1, 1, -2], [2, 2, 2]) + u = pygalmesh.Intersection([c0, c1]) + + # In CGAL, feature edges must not intersect, and that's a problem here: The + # intersection edges of the cuboids share eight points with the edges of + # the tall and skinny cuboid. + # eps = 1.0e-2 + # extra_features = [ + # [[1.0, 1.0 + eps, 0.5], [1.0, 2.0 - eps, 0.5]], + # [[1.0 + eps, 2.0, 0.5], [2.0 - eps, 2.0, 0.5]], + # [[2.0, 2.0 - eps, 0.5], [2.0, 1.0 + eps, 0.5]], + # [[2.0 - eps, 1.0, 0.5], [1.0 + eps, 1.0, 0.5]], + # ] + + mesh = pygalmesh.generate_mesh( + u, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + # filter the vertices that belong to cells + verts = mesh.points[numpy.unique(mesh.get_cells_type("tetra"))] + + tol = 1.0e-2 + assert abs(max(verts[:, 0]) - 2.0) < tol + assert abs(min(verts[:, 0]) - 1.0) < tol + assert abs(max(verts[:, 1]) - 2.0) < tol + assert abs(min(verts[:, 1]) - 1.0) < tol + assert abs(max(verts[:, 2]) - 0.5) < 0.05 + assert abs(min(verts[:, 2]) + 0.5) < 0.05 + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 1.0) < 0.05 + + +def test_cuboids_union(): + c0 = pygalmesh.Cuboid([0, 0, -0.5], [3, 3, 0.5]) + c1 = pygalmesh.Cuboid([1, 1, -2], [2, 2, 2]) + u = pygalmesh.Union([c0, c1]) + + mesh = pygalmesh.generate_mesh( + u, + max_cell_circumradius=0.2, + max_edge_size_at_feature_edges=0.2, + verbose=False, + ) + + # filter the vertices that belong to cells + verts = mesh.points[numpy.unique(mesh.get_cells_type("tetra"))] + + tol = 1.0e-2 + assert abs(max(verts[:, 0]) - 3.0) < tol + assert abs(min(verts[:, 0]) - 0.0) < tol + assert abs(max(verts[:, 1]) - 3.0) < tol + assert abs(min(verts[:, 1]) - 0.0) < tol + assert abs(max(verts[:, 2]) - 2.0) < tol + assert abs(min(verts[:, 2]) + 2.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 12.0) < 0.1 + + +def test_cuboid(): + s0 = pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]) + mesh = pygalmesh.generate_mesh( + s0, max_edge_size_at_feature_edges=0.1, verbose=False + ) + + tol = 1.0e-3 + assert abs(max(mesh.points[:, 0]) - 1.0) < tol + assert abs(min(mesh.points[:, 0]) + 0.0) < tol + assert abs(max(mesh.points[:, 1]) - 2.0) < tol + assert abs(min(mesh.points[:, 1]) + 0.0) < tol + assert abs(max(mesh.points[:, 2]) - 3.0) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 6.0) < tol + + +def test_cone(): + base_radius = 1.0 + height = 2.0 + max_edge_size_at_feature_edges = 0.1 + s0 = pygalmesh.Cone(base_radius, height, max_edge_size_at_feature_edges) + mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + tol = 2.0e-1 + assert abs(max(mesh.points[:, 0]) - base_radius) < tol + assert abs(min(mesh.points[:, 0]) + base_radius) < tol + assert abs(max(mesh.points[:, 1]) - base_radius) < tol + assert abs(min(mesh.points[:, 1]) + base_radius) < tol + assert abs(max(mesh.points[:, 2]) - height) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref_vol = numpy.pi * base_radius * base_radius / 3.0 * height + assert abs(vol - ref_vol) < tol + + +def test_cylinder(): + radius = 1.0 + z0 = 0.0 + z1 = 1.0 + edge_length = 0.1 + s0 = pygalmesh.Cylinder(z0, z1, radius, edge_length) + mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=edge_length, + verbose=False, + ) + + tol = 1.0e-1 + assert abs(max(mesh.points[:, 0]) - radius) < tol + assert abs(min(mesh.points[:, 0]) + radius) < tol + assert abs(max(mesh.points[:, 1]) - radius) < tol + assert abs(min(mesh.points[:, 1]) + radius) < tol + assert abs(max(mesh.points[:, 2]) - z1) < tol + assert abs(min(mesh.points[:, 2]) + z0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref_vol = numpy.pi * radius * radius * (z1 - z0) + assert abs(vol - ref_vol) < tol + + +def test_tetrahedron(): + s0 = pygalmesh.Tetrahedron( + [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0] + ) + mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + tol = 1.0e-3 + assert abs(max(mesh.points[:, 0]) - 1.0) < tol + assert abs(min(mesh.points[:, 0]) + 0.0) < tol + assert abs(max(mesh.points[:, 1]) - 1.0) < tol + assert abs(min(mesh.points[:, 1]) + 0.0) < tol + assert abs(max(mesh.points[:, 2]) - 1.0) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 1.0 / 6.0) < tol + + +def test_torus(): + major_radius = 1.0 + minor_radius = 0.5 + s0 = pygalmesh.Torus(major_radius, minor_radius) + mesh = pygalmesh.generate_mesh(s0, max_cell_circumradius=0.1, verbose=False) + + tol = 1.0e-2 + radii_sum = major_radius + minor_radius + assert abs(max(mesh.points[:, 0]) - radii_sum) < tol + assert abs(min(mesh.points[:, 0]) + radii_sum) < tol + assert abs(max(mesh.points[:, 1]) - radii_sum) < tol + assert abs(min(mesh.points[:, 1]) + radii_sum) < tol + assert abs(max(mesh.points[:, 2]) - minor_radius) < tol + assert abs(min(mesh.points[:, 2]) + minor_radius) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref_vol = (numpy.pi * minor_radius * minor_radius) * (2 * numpy.pi * major_radius) + assert abs(vol - ref_vol) < 1.0e-1 + + +def test_custom_function(): + class Hyperboloid(pygalmesh.DomainBase): + def __init__(self, max_edge_size_at_feature_edges): + super().__init__() + self.z0 = -1.0 + self.z1 = 1.0 + self.waist_radius = 0.5 + self.max_edge_size_at_feature_edges = max_edge_size_at_feature_edges + + def eval(self, x): + if self.z0 < x[2] and x[2] < self.z1: + return x[0] ** 2 + x[1] ** 2 - (x[2] ** 2 + self.waist_radius) ** 2 + return 1.0 + + def get_bounding_sphere_squared_radius(self): + z_max = max(abs(self.z0), abs(self.z1)) + r_max = z_max ** 2 + self.waist_radius + return r_max * r_max + z_max * z_max + + def get_features(self): + radius0 = self.z0 ** 2 + self.waist_radius + n0 = int(2 * numpy.pi * radius0 / self.max_edge_size_at_feature_edges) + circ0 = [ + [ + radius0 * numpy.cos((2 * numpy.pi * k) / n0), + radius0 * numpy.sin((2 * numpy.pi * k) / n0), + self.z0, + ] + for k in range(n0) + ] + circ0.append(circ0[0]) + + radius1 = self.z1 ** 2 + self.waist_radius + n1 = int(2 * numpy.pi * radius1 / self.max_edge_size_at_feature_edges) + circ1 = [ + [ + radius1 * numpy.cos((2 * numpy.pi * k) / n1), + radius1 * numpy.sin((2 * numpy.pi * k) / n1), + self.z1, + ] + for k in range(n1) + ] + circ1.append(circ1[0]) + return [circ0, circ1] + + max_edge_size_at_feature_edges = 0.12 + d = Hyperboloid(max_edge_size_at_feature_edges) + + mesh = pygalmesh.generate_mesh( + d, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + # TODO check the reference values + tol = 1.0e-1 + assert abs(max(mesh.points[:, 0]) - 1.4) < tol + assert abs(min(mesh.points[:, 0]) + 1.4) < tol + assert abs(max(mesh.points[:, 1]) - 1.4) < tol + assert abs(min(mesh.points[:, 1]) + 1.4) < tol + assert abs(max(mesh.points[:, 2]) - 1.0) < tol + assert abs(min(mesh.points[:, 2]) + 1.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 2 * numpy.pi * 47.0 / 60.0) < 0.16 + + +def test_scaling(): + alpha = 1.3 + s = pygalmesh.Scale(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), alpha) + mesh = pygalmesh.generate_mesh( + s, + max_cell_circumradius=0.2, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + tol = 1.0e-2 + assert abs(max(mesh.points[:, 0]) - 1 * alpha) < tol + assert abs(min(mesh.points[:, 0]) + 0.0) < tol + assert abs(max(mesh.points[:, 1]) - 2 * alpha) < tol + assert abs(min(mesh.points[:, 1]) + 0.0) < tol + assert abs(max(mesh.points[:, 2]) - 3 * alpha) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 6.0 * alpha ** 3) < tol + + +def test_stretch(): + alpha = 2.0 + s = pygalmesh.Stretch(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [alpha, 0.0, 0.0]) + mesh = pygalmesh.generate_mesh( + s, + max_cell_circumradius=0.2, + max_edge_size_at_feature_edges=0.2, + verbose=False, + ) + + tol = 1.0e-2 + assert abs(max(mesh.points[:, 0]) - alpha) < tol + assert abs(min(mesh.points[:, 0]) + 0.0) < tol + assert abs(max(mesh.points[:, 1]) - 2.0) < tol + assert abs(min(mesh.points[:, 1]) + 0.0) < tol + assert abs(max(mesh.points[:, 2]) - 3.0) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 12.0) < tol + + +def test_rotation(): + s0 = pygalmesh.Rotate( + pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [1.0, 0.0, 0.0], numpy.pi / 12.0 + ) + mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + tol = 1.0e-2 + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 6.0) < tol + + +def test_translation(): + s0 = pygalmesh.Translate(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [1.0, 0.0, 0.0]) + mesh = pygalmesh.generate_mesh( + s0, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + tol = 1.0e-2 + assert abs(max(mesh.points[:, 0]) - 2.0) < tol + assert abs(min(mesh.points[:, 0]) - 1.0) < tol + assert abs(max(mesh.points[:, 1]) - 2.0) < tol + assert abs(min(mesh.points[:, 1]) + 0.0) < tol + assert abs(max(mesh.points[:, 2]) - 3.0) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 6.0) < tol + + +def test_extrude(): + p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]]) + domain = pygalmesh.Extrude(p, [0.0, 0.3, 1.0]) + mesh = pygalmesh.generate_mesh( + domain, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=0.1, + verbose=False, + ) + + tol = 1.0e-3 + assert abs(max(mesh.points[:, 0]) - 0.5) < tol + assert abs(min(mesh.points[:, 0]) + 0.5) < tol + assert abs(max(mesh.points[:, 1]) - 0.8) < tol + assert abs(min(mesh.points[:, 1]) + 0.3) < tol + # Relax tolerance for debian, see + assert abs(max(mesh.points[:, 2]) - 1.0) < 1.1e-3 + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 0.4) < tol + + +def test_extrude_rotate(): + p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]]) + max_edge_size_at_feature_edges = 0.1 + domain = pygalmesh.Extrude( + p, + [0.0, 0.0, 1.0], + 0.5 * 3.14159265359, + max_edge_size_at_feature_edges, + ) + mesh = pygalmesh.generate_mesh( + domain, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + tol = 1.0e-3 + assert abs(max(mesh.points[:, 0]) - 0.583012701892) < tol + assert abs(min(mesh.points[:, 0]) + 0.5) < tol + assert abs(max(mesh.points[:, 1]) - 0.5) < tol + assert abs(min(mesh.points[:, 1]) + 0.583012701892) < tol + assert abs(max(mesh.points[:, 2]) - 1.0) < tol + assert abs(min(mesh.points[:, 2]) + 0.0) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 0.4) < 0.05 + + +def test_ring_extrude(): + p = pygalmesh.Polygon2D([[0.5, -0.3], [1.5, -0.3], [1.0, 0.5]]) + max_edge_size_at_feature_edges = 0.1 + domain = pygalmesh.RingExtrude(p, max_edge_size_at_feature_edges) + mesh = pygalmesh.generate_mesh( + domain, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + verbose=False, + ) + + tol = 1.0e-2 + assert abs(max(mesh.points[:, 0]) - 1.5) < tol + assert abs(min(mesh.points[:, 0]) + 1.5) < tol + assert abs(max(mesh.points[:, 1]) - 1.5) < tol + assert abs(min(mesh.points[:, 1]) + 1.5) < tol + assert abs(max(mesh.points[:, 2]) - 0.5) < tol + assert abs(min(mesh.points[:, 2]) + 0.3) < tol + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 2 * numpy.pi * 0.4) < 0.05 + + +def test_heart(): + class Heart(pygalmesh.DomainBase): + def __init__(self, max_edge_size_at_feature_edges): + super().__init__() + + def eval(self, x): + return ( + (x[0] ** 2 + 9.0 / 4.0 * x[1] ** 2 + x[2] ** 2 - 1) ** 3 + - x[0] ** 2 * x[2] ** 3 + - 9.0 / 80.0 * x[1] ** 2 * x[2] ** 3 + ) + + def get_bounding_sphere_squared_radius(self): + return 10.0 + + max_edge_size_at_feature_edges = 0.1 + d = Heart(max_edge_size_at_feature_edges) + + mesh = pygalmesh.generate_mesh( + d, + max_cell_circumradius=0.1, + max_edge_size_at_feature_edges=max_edge_size_at_feature_edges, + # odt=True, + # lloyd=True, + # verbose=True + ) + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + ref = 3.3180194961823872 + assert abs(vol - ref) < 1.0e-3 * ref + + +def test_halfspace(): + c = pygalmesh.Cuboid([0, 0, 0], [1, 1, 1]) + s = pygalmesh.HalfSpace([1.0, 2.0, 3.0], 1.0, 2.0) + u = pygalmesh.Intersection([c, s]) + + mesh = pygalmesh.generate_mesh( + u, + max_cell_circumradius=0.2, + max_edge_size_at_feature_edges=0.2, + verbose=False, + ) + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 1 / 750) < 1.0e-3 + + +def test_ball_with_sizing_field(): + mesh = pygalmesh.generate_mesh( + pygalmesh.Ball([0.0, 0.0, 0.0], 1.0), + min_facet_angle=30.0, + max_radius_surface_delaunay_ball=0.1, + max_facet_distance=0.025, + max_circumradius_edge_ratio=2.0, + max_cell_circumradius=lambda x: abs(numpy.sqrt(numpy.dot(x, x)) - 0.5) / 5 + + 0.025, + verbose=False, + ) + + assert abs(max(mesh.points[:, 0]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 0]) + 1.0) < 0.02 + assert abs(max(mesh.points[:, 1]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 1]) + 1.0) < 0.02 + assert abs(max(mesh.points[:, 2]) - 1.0) < 0.02 + assert abs(min(mesh.points[:, 2]) + 1.0) < 0.02 + + vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra"))) + assert abs(vol - 4.0 / 3.0 * numpy.pi) < 0.15 + + +if __name__ == "__main__": + test_ball() + # test_ball_with_sizing_field() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ca3b6e4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +envlist = py3 +isolated_build = True + +[testenv] +deps = + pytest + pytest-codeblocks + pytest-cov + pytest-randomly +commands = + pytest {posargs} --codeblocks