pax_global_header00006660000000000000000000000064141717645770014535gustar00rootroot0000000000000052 comment=494b0039915f3f511da4506c5c725a0d9112f736 meshplex-0.17.0/000077500000000000000000000000001417176457700134475ustar00rootroot00000000000000meshplex-0.17.0/.codecov.yml000066400000000000000000000000141417176457700156650ustar00rootroot00000000000000comment: no meshplex-0.17.0/.flake8000066400000000000000000000001611417176457700146200ustar00rootroot00000000000000[flake8] ignore = E203, E266, E501, W503, C901 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 meshplex-0.17.0/.gitattributes000066400000000000000000000001241417176457700163370ustar00rootroot00000000000000*.vtk filter=lfs diff=lfs merge=lfs -text *.vtu filter=lfs diff=lfs merge=lfs -text meshplex-0.17.0/.github/000077500000000000000000000000001417176457700150075ustar00rootroot00000000000000meshplex-0.17.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001417176457700171725ustar00rootroot00000000000000meshplex-0.17.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011541417176457700216650ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: Needs triage assignees: "" --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** A minimal code example that reproduces the problem would be a big help if you can provide it. **Diagnose** I may ask you to cut and paste the output of the following command. ``` pip freeze | grep meshplex ``` **Did I help?** If I was able to resolve your problem, consider [sponsoring](https://github.com/sponsors/nschloe) my work on meshplex, or [buy me a coffee](https://ko-fi.com/nschloe) to say thanks. meshplex-0.17.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000013221417176457700227150ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "[REQUEST]" labels: Needs triage assignees: '' --- Consider posting in https://github.com/nschloe/meshplex/discussions for feedback before raising a feature request. **How would you improve meshplex?** Give as much detail as you can. Example code of how you would like it to work would help. **What problem does it solved for you?** What problem do you have that this feature would solve? I may be able to suggest an existing way of solving it. **Did I help** If I was able to resolve your problem, consider [sponsoring](https://github.com/sponsors/nschloe) my work on meshplex, or [buy me a coffee](https://ko-fi.com/nschloe) to say thanks. meshplex-0.17.0/.github/workflows/000077500000000000000000000000001417176457700170445ustar00rootroot00000000000000meshplex-0.17.0/.github/workflows/ci.yml000066400000000000000000000022061417176457700201620ustar00rootroot00000000000000name: ci on: push: branches: - main pull_request: branches: - main jobs: doc: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v2 with: python-version: "3.x" - uses: actions/checkout@v2 - run: | pip install sphinx sphinx-build -M html docs/ build/ 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 build: runs-on: ubuntu-latest strategy: matrix: # vtk not available for 3.10 yet python-version: ["3.7", "3.8", "3.9"] steps: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - uses: actions/checkout@v2 with: lfs: true - name: Test with tox run: | pip install tox tox -- --cov meshplex --cov-report xml --cov-report term - uses: codecov/codecov-action@v1 if: ${{ matrix.python-version == '3.9' }} meshplex-0.17.0/.gitignore000066400000000000000000000001401417176457700154320ustar00rootroot00000000000000.cache/ build/ docs/_build/ docs/build/ README.rst *.egg-info/ dist/ *.png .pytest_cache/ .tox/ meshplex-0.17.0/.pre-commit-config.yaml000066400000000000000000000004541417176457700177330ustar00rootroot00000000000000repos: - repo: https://github.com/PyCQA/isort rev: 5.9.3 hooks: - id: isort - repo: https://github.com/psf/black rev: 21.10b0 hooks: - id: black language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake8 meshplex-0.17.0/.readthedocs.yml000066400000000000000000000001231417176457700165310ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py python: install: - path: . meshplex-0.17.0/CHANGELOG.md000066400000000000000000000013301417176457700152550ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.16.3] - 2021-07-13 ### Changed - Fixed computation of `genus` and `euler_characteristic` ## [0.16.0] - 2021-04-15 ### Changed - `mesh.cells` is now a function; e.g., `mesh.cells["points"]` is now `mesh.cells("points")` - `mesh.idx_hierarchy` is deprecated in favor of `mesh.idx[-1]` (the new `idx` list contains more index magic) ## [0.14.0] - 2020-11-05 ### Changed - `node_coords` is now `points` - `mesh_tri`: fixed inconsistent state after setting the points meshplex-0.17.0/CODE_OF_CONDUCT.md000066400000000000000000000064431417176457700162550ustar00rootroot00000000000000# meshplex Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq/ meshplex-0.17.0/CONTRIBUTING.md000066400000000000000000000014361417176457700157040ustar00rootroot00000000000000# meshplex contributing guidelines The meshplex community appreciates your contributions via issues and pull requests. Note that the [code of conduct](CODE_OF_CONDUCT.md) applies to all interactions with the meshplex project, including issues and pull requests. When submitting pull requests, please follow the style guidelines of the project, ensure that your code is tested and documented, and write good commit messages, e.g., following [these guidelines](https://chris.beams.io/posts/git-commit/). By submitting a pull request, you are licensing your code under the project [license](LICENSE) and affirming that you either own copyright (automatic for most individuals) or are authorized to distribute under the project license (e.g., in case your employer retains copyright on your work). meshplex-0.17.0/LICENSE000066400000000000000000001045161417176457700144630ustar00rootroot00000000000000 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 . meshplex-0.17.0/README.md000066400000000000000000000117551417176457700147370ustar00rootroot00000000000000

meshplex

Fast tools for simplex meshes.

[![PyPi Version](https://img.shields.io/pypi/v/meshplex.svg?style=flat-square)](https://pypi.org/project/meshplex/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/meshplex.svg?style=flat-square)](https://pypi.org/project/meshplex/) [![GitHub stars](https://img.shields.io/github/stars/nschloe/meshplex.svg?style=flat-square&logo=github&label=Stars&logoColor=white)](https://github.com/nschloe/meshplex) [![PyPi downloads](https://img.shields.io/pypi/dm/meshplex.svg?style=flat-square)](https://pypistats.org/packages/meshplex) [![Discord](https://img.shields.io/static/v1?logo=discord&label=chat&message=on%20discord&color=7289da&style=flat-square)](https://discord.gg/hnTJ5MRX2Y) [![Documentation Status](https://readthedocs.org/projects/meshplex/badge?style=flat-square&version=latest)](https://readthedocs.org/projects/meshplex/?badge=latest) [![gh-actions](https://img.shields.io/github/workflow/status/nschloe/meshplex/ci?style=flat-square)](https://github.com/nschloe/meshplex/actions?query=workflow%3Aci) [![codecov](https://img.shields.io/codecov/c/github/nschloe/meshplex.svg?style=flat-square)](https://codecov.io/gh/nschloe/meshplex) [![LGTM](https://img.shields.io/lgtm/grade/python/github/nschloe/meshplex.svg?style=flat-square)](https://lgtm.com/projects/g/nschloe/meshplex) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) Compute all sorts of interesting points, areas, and volumes in simplex (triangle, tetrahedral, n-simplex) meshes of any dimension, with a focus on efficiency. Useful in many contexts, e.g., finite-element and finite-volume computations. meshplex is used in [optimesh](https://github.com/nschloe/optimesh) and [pyfvm](https://github.com/nschloe/pyfvm). ### Quickstart meshplex can compute the following data: ```python import meshplex # create a simple Mesh instance points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]] cells = [[0, 1, 2]] mesh = meshplex.Mesh(points, cells) # or read it from a file # mesh = meshplex.read("pacman.vtk") # triangle volumes, heights print(mesh.cell_volumes) print(mesh.signed_cell_volumes) print(mesh.cell_heights) # circumcenters, centroids, incenters print(mesh.cell_circumcenters) print(mesh.cell_centroids) print(mesh.cell_incenters) # circumradius, inradius, cell quality print(mesh.cell_circumradius) print(mesh.cell_inradius) print(mesh.q_radius_ratio) # d * inradius / circumradius (min 0, max 1) # control volumes, centroids print(mesh.control_volumes) print(mesh.control_volume_centroids) # covolume/edge length ratios print(mesh.ce_ratios) # count Delaunay violations print(mesh.num_delaunay_violations) # removes some cells mesh.remove_cells([0]) ``` For triangular meshes (`MeshTri`), meshplex also has some mesh manipulation routines: ```python mesh.show() # show the mesh mesh.angles # compute angles mesh.flip_until_delaunay() # flips edges until the mesh is Delaunay ``` For a documentation of all classes and functions, see [readthedocs](https://meshplex.readthedocs.io/). (For mesh creation, check out [this list](https://github.com/nschloe/awesome-scientific-computing#meshing)). ### Plotting #### Triangles ```python import meshplex mesh = meshplex.read("pacman.vtk") mesh.show( # show_coedges=True, # control_volume_centroid_color=None, # mesh_color="k", # nondelaunay_edge_color=None, # boundary_edge_color=None, # comesh_color=(0.8, 0.8, 0.8), show_axes=False, ) ``` #### Tetrahedra ```python import numpy as np import meshplex # Generate tetrahedron points = ( np.array( [ [1.0, 0.0, -1.0 / np.sqrt(8)], [-0.5, +np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], [-0.5, -np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], [0.0, 0.0, np.sqrt(2.0) - 1.0 / np.sqrt(8)], ] ) / np.sqrt(3.0) ) cells = [[0, 1, 2, 3]] # Create mesh object mesh = meshplex.MeshTetra(points, cells) # Plot cell 0 with control volume boundaries mesh.show_cell( 0, # barycenter_rgba=(1, 0, 0, 1.0), # circumcenter_rgba=(0.1, 0.1, 0.1, 1.0), # circumsphere_rgba=(0, 1, 0, 1.0), # incenter_rgba=(1, 0, 1, 1.0), # insphere_rgba=(1, 0, 1, 1.0), # face_circumcenter_rgba=(0, 0, 1, 1.0), control_volume_boundaries_rgba=(1.0, 0.0, 0.0, 1.0), line_width=3.0, ) ``` ### Installation meshplex is [available from the Python Package Index](https://pypi.org/project/meshplex/), so simply type ``` pip install meshplex ``` to install. ### License This software is published under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html). meshplex-0.17.0/docs/000077500000000000000000000000001417176457700143775ustar00rootroot00000000000000meshplex-0.17.0/docs/_static/000077500000000000000000000000001417176457700160255ustar00rootroot00000000000000meshplex-0.17.0/docs/_static/meshplex-32x32.ico000066400000000000000000000102761417176457700211330ustar00rootroot00000000000000  ( @ OɘǘLcYS"f}5YS9x!KYSQ,YS)!YSYSHYSQxjjYS'w++$''ֵ33 !((S((fZƙ<$$('ֹ##++('ױnl>=YZDU*(լ0 Z++$''ֵidP㙙n((S(&e('ֹ##F -++)'ְ 99LL9/5(&ג&&(((֧((: 0B((3)'ք('ב((RHK~ ''ֵ)'w''on%((s))E''V((Սۛ.1##(&֠*&=('׫,%B)'ת++$''ֵ33 H! FFqXX('ֹ##1;:vvp]YV `>|?|`D d\f~>s|~p9^?meshplex-0.17.0/docs/_static/meshplex-logo.svg000066400000000000000000000055511417176457700213370ustar00rootroot00000000000000meshplex-0.17.0/docs/conf.py000066400000000000000000000043541417176457700157040ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # from meshplex import __version__ as release # import sys # sys.path.insert(0, os.path.abspath('.')) # Sphinx 1.* compat (for readthedocs) master_doc = "index" # -- Project information ----------------------------------------------------- project = "meshplex" copyright = "2017-2022, Nico Schlömer" author = "Nico Schlömer" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", # "sphinx.ext.doctest", # "sphinx.ext.todo", # "sphinx.ext.coverage", # "sphinx.ext.pngmath", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_favicon = "_static/meshplex-32x32.ico" html_theme_options = { "logo": "meshplex-logo.svg", "github_user": "nschloe", "github_repo": "meshplex", "github_banner": True, "github_button": False, } meshplex-0.17.0/docs/index.rst000066400000000000000000000013021417176457700162340ustar00rootroot00000000000000.. meshplex documentation master file, created by sphinx-quickstart on Mon Apr 29 10:54:58 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. meshplex --- Simplex meshes for Python ====================================== meshplex computes all sorts of interesting points, areas, and volumes in triangular and tetrahedral meshes, with a focus on efficiency. Useful in many contexts, e.g., finite-element and finite-volume computations. For a quickstart, checkout `meshplex's GitHubPage `_. Overview of classes and functions --------------------------------- .. automodule:: meshplex :members: meshplex-0.17.0/justfile000066400000000000000000000007671417176457700152310ustar00rootroot00000000000000version := `python3 -c "from src.meshplex.__about__ import __version__; print(__version__)"` default: @echo "\"just publish\"?" publish: @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi gh release create "v{{version}}" flit publish clean: @find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf @rm -rf src/*.egg-info/ build/ dist/ .tox/ format: isort . black . blacken-docs README.md lint: black --check . flake8 . doc: sphinx-build -M html docs/ build/ meshplex-0.17.0/logo/000077500000000000000000000000001417176457700144075ustar00rootroot00000000000000meshplex-0.17.0/logo/logo.py000066400000000000000000000047541417176457700157330ustar00rootroot00000000000000import matplotlib.pyplot as plt import numpy as np import meshplex def _main(): points = np.array([[0.0, 0.0], [1.0, 0.0], [0.3, 0.8]]) # points = np.array([[0.0, 0.0], [1.0, 0.0], [0.5, np.sqrt(3) / 2]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.Mesh(points, cells) lw = 5.0 col = "0.6" ax = plt.gca() # circumcircle circle1 = plt.Circle( mesh.cell_circumcenters[0], mesh.cell_circumradius[0], color=col, fill=False, linewidth=lw, ) ax.add_patch(circle1) # perpendicular bisectors for i, j in [[0, 1], [1, 2], [2, 0]]: m1 = (points[i] + points[j]) / 2 v1 = m1 - mesh.cell_circumcenters[0] e1 = ( mesh.cell_circumcenters[0] + v1 / np.linalg.norm(v1) * mesh.cell_circumradius[0] ) plt.plot( [mesh.cell_circumcenters[0, 0], e1[0]], [mesh.cell_circumcenters[0, 1], e1[1]], col, linewidth=lw, ) # heights for i, j, k in [[0, 1, 2], [1, 2, 0], [2, 0, 1]]: p = points - points[i] v1 = p[j] / np.linalg.norm(p[j]) m1 = points[i] + np.dot(p[k], v1) * v1 plt.plot([points[k, 0], m1[0]], [points[k, 1], m1[1]], linewidth=lw, color=col) # # incircle # circle2 = plt.Circle( # mesh.cell_incenters[0], mesh.inradius[0], color=col, fill=False, linewidth=lw # ) # ax.add_patch(circle2) # # angle bisectors # for i, j, k in [[0, 1, 2], [1, 2, 0], [2, 0, 1]]: # p = points - points[i] # v1 = p[j] / np.linalg.norm(p[j]) # v2 = p[k] / np.linalg.norm(p[k]) # alpha = np.arccos(np.dot(v1, v2)) # c = np.cos(alpha / 2) # s = np.sin(alpha / 2) # beta = np.linalg.norm(mesh.cell_incenters[0] - points[i]) + mesh.inradius[0] # m1 = points[i] + np.dot([[c, -s], [s, c]], v1) * beta # plt.plot( # [points[i, 0], m1[0]], # [points[i, 1], m1[1]], # col, # linewidth=lw, # color=col, # ) # triangle plt.plot( [points[0, 0], points[1, 0], points[2, 0], points[0, 0]], [points[0, 1], points[1, 1], points[2, 1], points[0, 1]], color="#d62728", linewidth=lw, ) ax.set_xlim(-0.1, 1.1) ax.set_ylim(-0.4, 0.9) ax.set_aspect("equal") plt.axis("off") plt.savefig("logo.svg", bbox_inches="tight", transparent=True) # plt.show() if __name__ == "__main__": _main() meshplex-0.17.0/pyproject.toml000066400000000000000000000026341417176457700163700ustar00rootroot00000000000000[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "meshplex" authors = [{name = "Nico Schlömer", email = "nico.schloemer@gmail.com"}] description = "Fast tools for simplex meshes" readme = "README.md" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", "Programming Language :: Python", "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", ] dynamic = ["version"] requires-python = ">=3.7" dependencies = [ "meshio >=4, <6", "numpy >= 1.20.0", "npx >= 0.0.7", ] [project.urls] Code = "https://github.com/nschloe/meshplex" Issues = "https://github.com/nschloe/meshplex/issues" Funding = "https://github.com/sponsors/nschloe" [project.optional-dependencies] all = [ "matplotlib", "scipy", "vtk" ] plot = [ "matplotlib", "vtk" ] [tool.pytest.ini_options] filterwarnings = [ 'ignore:Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.:UserWarning' ] [tool.isort] profile = "black" meshplex-0.17.0/src/000077500000000000000000000000001417176457700142365ustar00rootroot00000000000000meshplex-0.17.0/src/meshplex/000077500000000000000000000000001417176457700160635ustar00rootroot00000000000000meshplex-0.17.0/src/meshplex/__about__.py000066400000000000000000000000271417176457700203420ustar00rootroot00000000000000__version__ = "0.17.0" meshplex-0.17.0/src/meshplex/__init__.py000066400000000000000000000005151417176457700201750ustar00rootroot00000000000000from .__about__ import __version__ from ._exceptions import MeshplexError from ._mesh import Mesh from ._mesh_tetra import MeshTetra from ._mesh_tri import MeshTri from ._reader import from_meshio, read __all__ = [ "Mesh", "MeshTri", "MeshTetra", "MeshplexError", "read", "from_meshio", "__version__", ] meshplex-0.17.0/src/meshplex/_exceptions.py000066400000000000000000000000511417176457700207510ustar00rootroot00000000000000class MeshplexError(Exception): pass meshplex-0.17.0/src/meshplex/_helpers.py000066400000000000000000000024051417176457700202370ustar00rootroot00000000000000import numpy as np def grp_start_len(a): """Given a sorted 1D input array `a`, e.g., [0 0, 1, 2, 3, 4, 4, 4], this routine returns the indices where the blocks of equal integers start and how long the blocks are. """ # https://stackoverflow.com/a/50394587/353337 m = np.concatenate([[True], a[:-1] != a[1:], [True]]) idx = np.flatnonzero(m) return idx[:-1], np.diff(idx) def _dot(a, n): """Dot product, preserve the leading n dimensions.""" # einsum is faster if the tail survives, e.g., ijk,ijk->jk. # # TODO reorganize the data? assert n <= len(a.shape) # Would use -1 as second argument, but b = a.reshape(*a.shape[:n], np.prod(a.shape[n:]).astype(int)) return np.einsum("...i,...i->...", b, b) def _multiply(a, b, n): """Multiply the along the first n dimensions of a and b. For example, a.shape == (5,6,3), b.shape == (5, 6), n = 2, will return an array c of a.shape with c[i,j,k] = a[i,j,k] * b[i,j]. """ aa = a.reshape(np.prod(a.shape[:n]), *a.shape[n:]) bb = b.reshape(np.prod(b.shape[:n]), *b.shape[n:]) cc = (aa.T * bb).T c = cc.reshape(*a.shape) return c meshplex-0.17.0/src/meshplex/_mesh.py000066400000000000000000001505131417176457700175350ustar00rootroot00000000000000from __future__ import annotations import math import pathlib import warnings import meshio import npx import numpy as np from numpy.typing import ArrayLike, NDArray from ._exceptions import MeshplexError from ._helpers import _dot, _multiply, grp_start_len __all__ = ["Mesh"] class Mesh: def __init__(self, points, cells, sort_cells: bool = False): points = np.asarray(points) cells = np.asarray(cells) if sort_cells: # Sort cells, first every row, then the rows themselves. This helps in many # downstream applications, e.g., when constructing linear systems with the # cells/edges. (When converting to CSR format, the I/J entries must be # sorted.) Don't use cells.sort(axis=1) to avoid # ``` # ValueError: sort array is read-only # ``` cells = np.sort(cells, axis=1) cells = cells[cells[:, 0].argsort()] # assert len(points.shape) <= 2, f"Illegal point coordinates shape {points.shape}" assert len(cells.shape) == 2, f"Illegal cells shape {cells.shape}" self.n = cells.shape[1] # Assert that all vertices are used. # If there are vertices which do not appear in the cells list, this # ``` # uvertices, uidx = np.unique(cells, return_inverse=True) # cells = uidx.reshape(cells.shape) # points = points[uvertices] # ``` # helps. # is_used = np.zeros(len(points), dtype=bool) # is_used[cells] = True # assert np.all(is_used), "There are {} dangling points in the mesh".format( # np.sum(~is_used) # ) self._points = np.asarray(points) # prevent accidental override of parts of the array self._points.setflags(write=False) # Initialize the idx hierarchy. The first entry, idx[0], is the cells->points # relationship, shape [3, numcells] for triangles and [4, numcells] for # tetrahedra. idx[1] is the (half-)facet->points to relationship, shape [2, 3, # numcells] for triangles and [3, 4, numcells] for tetrahedra, for example. The # indexing is chosen such the point idx[0][k] is opposite of the facet idx[1][:, # k]. This indexing keeps going until idx[-1] is of shape [2, 3, ..., numcells]. self.idx = [np.asarray(cells).T] for _ in range(1, self.n - 1): m = len(self.idx[-1]) r = np.arange(m) k = np.array([np.roll(r, -i) for i in range(1, m)]) self.idx.append(self.idx[-1][k]) self._is_point_used = None self._is_boundary_facet = None self._is_boundary_facet_local = None self.facets = None self._boundary_facets = None self._interior_facets = None self._is_interior_point = None self._is_boundary_point = None self._is_boundary_cell = None self._cells_facets = None self.subdomains = {} self._reset_point_data() def _reset_point_data(self): """Reset all data that changes when point coordinates changes.""" self._half_edge_coords = None self._ei_dot_ei = None self._cell_centroids = None self._volumes = None self._integral_x = None self._signed_cell_volumes = None self._circumcenters = None self._cell_circumradii = None self._cell_heights = None self._ce_ratios = None self._cell_partitions = None self._control_volumes = None self._signed_circumcenter_distances = None self._circumcenter_facet_distances = None self._cv_centroids = None self._cvc_cell_mask = None self._cv_cell_mask = None def __repr__(self): name = { 2: "line", 3: "triangle", 4: "tetra", }[self.cells("points").shape[1]] num_points = len(self.points) num_cells = len(self.cells("points")) string = f"" return string # prevent overriding points without adapting the other mesh data @property def points(self) -> np.ndarray: return self._points @points.setter def points(self, new_points: ArrayLike): new_points = np.asarray(new_points) assert new_points.shape == self._points.shape self._points = new_points # reset all computed values self._reset_point_data() def set_points(self, new_points: ArrayLike, idx=slice(None)): self.points.setflags(write=True) self.points[idx] = new_points self.points.setflags(write=False) self._reset_point_data() def cells(self, which) -> NDArray[np.int_]: if which == "points": return self.idx[0].T elif which == "facets": assert self._cells_facets is not None return self._cells_facets assert which == "edges" assert self.n == 3 assert self._cells_facets is not None return self._cells_facets @property def half_edge_coords(self) -> NDArray[np.float_]: if self._half_edge_coords is None: self._compute_cell_values() assert self._half_edge_coords is not None return self._half_edge_coords @property def ei_dot_ei(self) -> NDArray[np.int_]: if self._ei_dot_ei is None: self._compute_cell_values() assert self._ei_dot_ei is not None return self._ei_dot_ei @property def cell_heights(self) -> NDArray[np.float_]: if self._cell_heights is None: self._compute_cell_values() assert self._cell_heights is not None return self._cell_heights @property def edge_lengths(self) -> NDArray[np.float_]: if self._volumes is None: self._compute_cell_values() assert self._volumes is not None return self._volumes[0] @property def facet_areas(self) -> NDArray[np.float_]: if self.n == 2: assert self.facets is not None return np.ones(len(self.facets["points"])) if self._volumes is None: self._compute_cell_values() assert self._volumes is not None return self._volumes[-2] @property def cell_volumes(self) -> NDArray[np.float_]: if self._volumes is None: self._compute_cell_values() assert self._volumes is not None return self._volumes[-1] @property def cell_circumcenters(self) -> NDArray[np.float_]: """Get the center of the circumsphere of each cell.""" if self._circumcenters is None: self._compute_cell_values() assert self._circumcenters is not None return self._circumcenters[-1] @property def cell_circumradius(self) -> NDArray[np.float_]: """Get the circumradii of all cells""" if self._cell_circumradii is None: self._compute_cell_values() assert self._cell_circumradii is not None return self._cell_circumradii @property def cell_partitions(self) -> NDArray[np.float_]: """Each simplex can be subdivided into parts that a closest to each corner. This method gives those parts, like ce_ratios associated with each edge. """ if self._cell_partitions is None: self._compute_cell_values() assert self._cell_partitions is not None return self._cell_partitions @property def circumcenter_facet_distances(self) -> NDArray[np.float_]: if self._circumcenter_facet_distances is None: self._compute_cell_values() assert self._circumcenter_facet_distances is not None return self._circumcenter_facet_distances def get_control_volume_centroids(self, cell_mask=None): """The centroid of any volume V is given by .. math:: c = \\int_V x / \\int_V 1. The denominator is the control volume. The numerator can be computed by making use of the fact that the control volume around any vertex is composed of right triangles, two for each adjacent cell. Optionally disregard the contributions from particular cells. This is useful, for example, for temporarily disregarding flat cells on the boundary when performing Lloyd mesh optimization. """ if self._cv_centroids is None or np.any(cell_mask != self._cvc_cell_mask): if self._integral_x is None: self._compute_cell_values() if cell_mask is None: idx = Ellipsis else: cell_mask = np.asarray(cell_mask) assert cell_mask.dtype == bool assert cell_mask.shape == (self.idx[-1].shape[-1],) # Use ":" for the first n-1 dimensions, then cell_mask idx = tuple((self.n - 1) * [slice(None)] + [~cell_mask]) # TODO this can be improved by first summing up all components per cell integral_p = npx.sum_at( self._integral_x[idx], self.idx[-1][idx], len(self.points) ) # Divide by the control volume cv = self.get_control_volumes(cell_mask) self._cv_centroids = (integral_p.T / cv).T self._cvc_cell_mask = cell_mask return self._cv_centroids @property def control_volume_centroids(self): return self.get_control_volume_centroids() @property def ce_ratios(self) -> NDArray[np.float_]: """The covolume-edgelength ratios.""" # There are many ways for computing the ratio of the covolume and the edge # length. For triangles, for example, there is # # ce_ratios = - / cell_volume / 4, # # for tetrahedra, # # zeta = ( # + ei_dot_ej[0, 2] * ei_dot_ej[3, 5] * ei_dot_ej[5, 4] # + ei_dot_ej[0, 1] * ei_dot_ej[3, 5] * ei_dot_ej[3, 4] # + ei_dot_ej[1, 2] * ei_dot_ej[3, 4] * ei_dot_ej[4, 5] # + self.ei_dot_ej[0] * self.ei_dot_ej[1] * self.ei_dot_ej[2] # ). # # Since we have detailed cell partitions at hand, though, the easiest and # fastest is via those. if self._ce_ratios is None: self._ce_ratios = ( self.cell_partitions[0] / self.ei_dot_ei * 2 * (self.n - 1) ) return self._ce_ratios @property def signed_circumcenter_distances(self): if self._signed_circumcenter_distances is None: if self._cells_facets is None: self.create_facets() self._signed_circumcenter_distances = npx.sum_at( self.circumcenter_facet_distances.T, self.cells("facets"), self.facets["points"].shape[0], )[self.is_interior_facet] return self._signed_circumcenter_distances def _compute_cell_values(self, mask=None): """Computes the volumes of all edges, facets, cells etc. in the mesh. It starts off by computing the (squared) edge lengths, then complements the edge with one vertex to form face. It computes an orthogonal basis of the face (with modified Gram-Schmidt), and from that gets the height of all faces. From this, the area of the face is computed. Then, it complements again to form the 3-simplex, again forms an orthogonal basis with Gram-Schmidt, and so on. """ if mask is None: mask = slice(None) e = self.points[self.idx[-1][..., mask]] e0 = e[0] diff = e[1] - e[0] orthogonal_basis = np.array([diff]) volumes2 = [_dot(diff, self.n - 1)] circumcenters = [0.5 * (e[0] + e[1])] vv = _dot(diff, self.n - 1) circumradii2 = 0.25 * vv sqrt_vv = np.sqrt(vv) lmbda = 0.5 * sqrt_vv sumx = np.array(e + circumcenters[-1]) partitions = 0.5 * np.array([sqrt_vv, sqrt_vv]) norms2 = np.array(volumes2) for kk, idx in enumerate(self.idx[:-1][::-1]): # Use the orthogonal bases of all sides to get a vector `v` orthogonal to # the side, pointing towards the additional point `p0`. p0 = self.points[idx][:, mask] v = p0 - e0 # modified gram-schmidt for w, w_dot_w in zip(orthogonal_basis, norms2): w_dot_v = np.einsum("...k,...k->...", w, v) # Compute / , but don't set the output value where w==0. # The value remains uninitialized and gets canceled out in the next # iteration when multiplied by w. alpha = np.divide(w_dot_v, w_dot_w, where=w_dot_w > 0.0, out=w_dot_v) v -= _multiply(w, alpha, self.n - 1 - kk) vv = np.einsum("...k,...k->...", v, v) # Form the orthogonal basis for the next iteration by choosing one side # `k0`. # shows that it doesn't make a difference which point-facet combination we # choose. k0 = 0 e0 = e0[k0] orthogonal_basis = np.row_stack([orthogonal_basis[:, k0], [v[k0]]]) norms2 = np.row_stack([norms2[:, k0], [vv[k0]]]) # The squared volume is the squared volume of the face times the squared # height divided by (n+1) ** 2. volumes2.append(volumes2[-1][0] * vv[k0] / (kk + 2) ** 2) # get the distance to the circumcenter; used in cell partitions and # circumcenter/-radius computation c = circumcenters[-1] p0c2 = _dot(p0 - c, self.n - 1 - kk) # Be a bit careful here. sigma and lmbda can be negative. Also make sure # that the values aren't nan when they should be inf (for degenerate # simplices, i.e., vv == 0). a = 0.5 * (p0c2 - circumradii2) sqrt_vv = np.sqrt(vv) with warnings.catch_warnings(): # silence division-by-0 warnings # Happens for degenerate cells (sqrt(vv) == 0), and this case is # supported by meshplex. The values lmbda and sigma will just be +-inf. warnings.simplefilter("ignore", category=RuntimeWarning) lmbda = a / sqrt_vv sigma_k0 = a[k0] / vv[k0] # circumcenter, squared circumradius # lmbda2_k0 = sigma_k0 * a[k0] circumradii2 = lmbda2_k0 + circumradii2[k0] with warnings.catch_warnings(): # Similar as above: The multiplicattion `v * sigma` correctly produces # nans for degenerate cells. warnings.simplefilter("ignore", category=RuntimeWarning) circumcenters.append( c[k0] + _multiply(v[k0], sigma_k0, self.n - 2 - kk) ) sumx += circumcenters[-1] # cell partitions partitions *= lmbda / (kk + 2) # The integral of x, # # \\int_V x, # # over all atomic wedges, i.e., areas cornered by a point, an edge midpoint, and # the subsequent circumcenters. # The integral of any linear function over a triangle is the average of the # values of the function in each of the three corners, times the area of the # triangle. integral_x = _multiply(sumx, partitions / self.n, self.n) if np.all(mask == slice(None)): # set new values self._ei_dot_ei = volumes2[0] self._half_edge_coords = diff self._volumes = [np.sqrt(v2) for v2 in volumes2] self._circumcenter_facet_distances = lmbda self._cell_heights = sqrt_vv self._cell_circumradii = np.sqrt(circumradii2) self._circumcenters = circumcenters self._cell_partitions = partitions self._integral_x = integral_x else: # update existing values assert self._ei_dot_ei is not None self._ei_dot_ei[:, mask] = volumes2[0] assert self._half_edge_coords is not None self._half_edge_coords[:, mask] = diff assert self._volumes is not None for k in range(len(self._volumes)): self._volumes[k][..., mask] = np.sqrt(volumes2[k]) assert self._circumcenter_facet_distances is not None self._circumcenter_facet_distances[..., mask] = lmbda assert self._cell_heights is not None self._cell_heights[..., mask] = sqrt_vv assert self._cell_circumradii is not None self._cell_circumradii[mask] = np.sqrt(circumradii2) assert self._circumcenters is not None for k in range(len(self._circumcenters)): self._circumcenters[k][..., mask, :] = circumcenters[k] assert self._cell_partitions is not None self._cell_partitions[..., mask] = partitions assert self._integral_x is not None self._integral_x[..., mask, :] = integral_x @property def signed_cell_volumes(self): """Signed volumes of an n-simplex in nD.""" if self._signed_cell_volumes is None: self._signed_cell_volumes = self.compute_signed_cell_volumes() return self._signed_cell_volumes def compute_signed_cell_volumes(self, idx=slice(None)): """Signed volume of a simplex in nD. Note that signing only makes sense for n-simplices in R^n. """ n = self.points.shape[1] assert ( self.n == self.points.shape[1] + 1 ), f"Signed areas only make sense for n-simplices in in nD. Got {n}D points." if self.n == 3: # On , we have a number of # alternatives computing the oriented area, but it's fastest with the # half-edges. x = self.half_edge_coords assert x is not None return (x[0, idx, 1] * x[2, idx, 0] - x[0, idx, 0] * x[2, idx, 1]) / 2 # https://en.wikipedia.org/wiki/Simplex#Volume cp = self.points[self.cells("points")] # append 1s cp1 = np.concatenate([cp, np.ones(cp.shape[:-1] + (1,))], axis=-1) # There appears to be no canonical convention when it comes to the sign # . With the below choice, the # area of 1D simplices [a, b] with a < b are positive, and the common ordering # of tetrahedra (as in VTK, for example) is positive. sign = -1 if n % 2 == 1 else 1 return sign * np.linalg.det(cp1) / math.factorial(n) def compute_cell_centroids(self, idx=slice(None)): return np.sum(self.points[self.cells("points")[idx]], axis=1) / self.n @property def cell_centroids(self) -> NDArray[np.float_]: """The centroids (barycenters, midpoints of the circumcircles) of all simplices.""" if self._cell_centroids is None: self._cell_centroids = self.compute_cell_centroids() return self._cell_centroids cell_barycenters = cell_centroids @property def cell_incenters(self) -> NDArray[np.float_]: """Get the midpoints of the inspheres.""" # https://en.wikipedia.org/wiki/Incenter#Barycentric_coordinates # https://math.stackexchange.com/a/2864770/36678 abc = self.facet_areas / np.sum(self.facet_areas, axis=0) return np.einsum("ij,jik->jk", abc, self.points[self.cells("points")]) @property def cell_inradius(self) -> NDArray[np.float_]: """Get the inradii of all cells""" # See . # https://en.wikipedia.org/wiki/Tetrahedron#Inradius return (self.n - 1) * self.cell_volumes / np.sum(self.facet_areas, axis=0) @property def is_point_used(self) -> NDArray[np.bool_]: # Check which vertices are used. # If there are vertices which do not appear in the cells list, this # ``` # uvertices, uidx = np.unique(cells, return_inverse=True) # cells = uidx.reshape(cells.shape) # points = points[uvertices] # ``` # helps. if self._is_point_used is None: self._is_point_used = np.zeros(len(self.points), dtype=bool) self._is_point_used[self.cells("points")] = True return self._is_point_used def write( self, filename: str, point_data=None, cell_data=None, field_data=None, ): if self.points.shape[1] == 2: n = len(self.points) a = np.ascontiguousarray(np.column_stack([self.points, np.zeros(n)])) else: a = self.points if self.cells("points").shape[1] == 3: cell_type = "triangle" else: assert ( self.cells("points").shape[1] == 4 ), "Only triangles/tetrahedra supported" cell_type = "tetra" meshio.Mesh( a, {cell_type: self.cells("points")}, point_data=point_data, cell_data=cell_data, field_data=field_data, ).write(filename) def get_vertex_mask(self, subdomain=None): if subdomain is None: # https://stackoverflow.com/a/42392791/353337 return slice(None) if subdomain not in self.subdomains: self._mark_vertices(subdomain) return self.subdomains[subdomain]["vertices"] def get_edge_mask(self, subdomain=None): """Get faces which are fully in subdomain.""" if subdomain is None: # https://stackoverflow.com/a/42392791/353337 return slice(None) if subdomain not in self.subdomains: self._mark_vertices(subdomain) # A face is inside if all its edges are in. # An edge is inside if all its points are in. is_in = self.subdomains[subdomain]["vertices"][self.idx[-1]] # Take `all()` over the first index is_inside = np.all(is_in, axis=tuple(range(1))) if subdomain.is_boundary_only: # Filter for boundary is_inside = is_inside & self.is_boundary_facet return is_inside def get_face_mask(self, subdomain): """Get faces which are fully in subdomain.""" if subdomain is None: # https://stackoverflow.com/a/42392791/353337 return slice(None) if subdomain not in self.subdomains: self._mark_vertices(subdomain) # A face is inside if all its edges are in. # An edge is inside if all its points are in. is_in = self.subdomains[subdomain]["vertices"][self.idx[-1]] # Take `all()` over all axes except the last two (face_ids, cell_ids). n = len(is_in.shape) is_inside = np.all(is_in, axis=tuple(range(n - 2))) if subdomain.is_boundary_only: # Filter for boundary is_inside = is_inside & self.is_boundary_facet_local return is_inside def get_cell_mask(self, subdomain=None): if subdomain is None: # https://stackoverflow.com/a/42392791/353337 return slice(None) if subdomain.is_boundary_only: # There are no boundary cells return np.array([]) if subdomain not in self.subdomains: self._mark_vertices(subdomain) is_in = self.subdomains[subdomain]["vertices"][self.idx[-1]] # Take `all()` over all axes except the last one (cell_ids). n = len(is_in.shape) return np.all(is_in, axis=tuple(range(n - 1))) def _mark_vertices(self, subdomain): """Mark faces/edges which are fully in subdomain.""" if subdomain is None: is_inside = np.ones(len(self.points), dtype=bool) else: is_inside = subdomain.is_inside(self.points.T).T if subdomain.is_boundary_only: # if the boundary hasn't been computed yet, this can take a little # moment is_inside &= self.is_boundary_point self.subdomains[subdomain] = {"vertices": is_inside} def create_facets(self): """Set up facet->point and facet->cell relations.""" if self.n == 2: # Too bad that the need a specializaiton here. Could be avoided if the # idx hierarchy would be of shape (1,2,...,n), not (2,...,n), but not sure # if that's worth the change. idx = self.idx[0].flatten() else: idx = self.idx[1] idx = idx.reshape(idx.shape[0], -1) # Sort the columns to make it possible for `unique()` to identify individual # facets. idx = np.sort(idx, axis=0).T a_unique, inv, cts = npx.unique_rows( idx, return_inverse=True, return_counts=True ) if np.any(cts > 2): num_weird_edges = np.sum(cts > 2) msg = ( f"Found {num_weird_edges} facets with more than two neighboring cells. " "Something is not right." ) # check if cells are identical, list them _, inv, cts = npx.unique_rows( np.sort(self.cells("points")), return_inverse=True, return_counts=True ) if np.any(cts > 1): msg += " The following cells are equal:\n" for multiple_idx in np.where(cts > 1)[0]: msg += str(np.where(inv == multiple_idx)[0]) raise MeshplexError(msg) self._is_boundary_facet_local = (cts[inv] == 1).reshape(self.idx[0].shape) self._is_boundary_facet = cts == 1 self.facets = {"points": a_unique} # cell->facets relationship self._cells_facets = inv.reshape(self.n, -1).T if self.n == 3: self.edges = self.facets self._facets_cells = None self._facets_cells_idx = None elif self.n == 4: self.faces = self.facets @property def is_boundary_facet_local(self) -> NDArray[np.bool_]: if self._is_boundary_facet_local is None: self.create_facets() assert self._is_boundary_facet_local is not None return self._is_boundary_facet_local @property def is_boundary_facet(self) -> NDArray[np.bool_]: if self._is_boundary_facet is None: self.create_facets() assert self._is_boundary_facet is not None return self._is_boundary_facet @property def is_interior_facet(self) -> NDArray[np.bool_]: return ~self.is_boundary_facet @property def is_boundary_cell(self): if self._is_boundary_cell is None: assert self.is_boundary_facet_local is not None self._is_boundary_cell = np.any(self.is_boundary_facet_local, axis=0) return self._is_boundary_cell @property def boundary_facets(self): if self._boundary_facets is None: self._boundary_facets = np.where(self.is_boundary_facet)[0] return self._boundary_facets @property def interior_facets(self): if self._interior_facets is None: self._interior_facets = np.where(~self.is_boundary_facet)[0] return self._interior_facets @property def is_boundary_point(self): if self._is_boundary_point is None: self._is_boundary_point = np.zeros(len(self.points), dtype=bool) # it's a little weird that we have to special-case n==2 here i = 0 if self.n == 2 else 1 self._is_boundary_point[ self.idx[i][..., self.is_boundary_facet_local] ] = True return self._is_boundary_point @property def is_interior_point(self): if self._is_interior_point is None: self._is_interior_point = self.is_point_used & ~self.is_boundary_point return self._is_interior_point @property def facets_cells(self): if self._facets_cells is None: self._compute_facets_cells() return self._facets_cells def _compute_facets_cells(self): """This creates edge->cells relations. While it's not necessary for many applications, it sometimes does come in handy, for example for mesh manipulation. """ if self.facets is None: self.create_facets() # num_edges = len(self.edges["points"]) # count = np.bincount(self.cells("edges").flat, minlength=num_edges) # edges_flat = self.cells("edges").flat idx_sort = np.argsort(edges_flat) sorted_edges = edges_flat[idx_sort] idx_start, count = grp_start_len(sorted_edges) # count is redundant with is_boundary/interior_edge assert np.all((count == 1) == self.is_boundary_facet) assert np.all((count == 2) == self.is_interior_facet) idx_start_count_1 = idx_start[self.is_boundary_facet] idx_start_count_2 = idx_start[self.is_interior_facet] res1 = idx_sort[idx_start_count_1] res2 = idx_sort[np.array([idx_start_count_2, idx_start_count_2 + 1])] edge_id_boundary = sorted_edges[idx_start_count_1] edge_id_interior = sorted_edges[idx_start_count_2] # It'd be nicer if we could organize the data differently, e.g., as a structured # array or as a dict. Those possibilities are slower, unfortunately, for some # operations in remove_cells() (and perhaps elsewhere). # self._facets_cells = { # rows: # 0: edge id # 1: cell id # 2: local edge id (0, 1, or 2) "boundary": np.array([edge_id_boundary, res1 // 3, res1 % 3]), # rows: # 0: edge id # 1: cell id 0 # 2: cell id 1 # 3: local edge id 0 (0, 1, or 2) # 4: local edge id 1 (0, 1, or 2) "interior": np.array([edge_id_interior, *(res2 // 3), *(res2 % 3)]), } self._facets_cells_idx = None @property def facets_cells_idx(self): if self._facets_cells_idx is None: if self._facets_cells is None: self._compute_facets_cells() assert self.is_boundary_facet is not None # For each edge, store the index into the respective edge array. num_edges = len(self.facets["points"]) self._facets_cells_idx = np.empty(num_edges, dtype=int) num_b = np.sum(self.is_boundary_facet) num_i = np.sum(self.is_interior_facet) self._facets_cells_idx[self.facets_cells["boundary"][0]] = np.arange(num_b) self._facets_cells_idx[self.facets_cells["interior"][0]] = np.arange(num_i) return self._facets_cells_idx def remove_dangling_points(self): """Remove all points which aren't part of an array""" is_part_of_cell = np.zeros(self.points.shape[0], dtype=bool) is_part_of_cell[self.cells("points").flat] = True new_point_idx = np.cumsum(is_part_of_cell) - 1 self._points = self._points[is_part_of_cell] for k in range(len(self.idx)): self.idx[k] = new_point_idx[self.idx[k]] if self._control_volumes is not None: self._control_volumes = self._control_volumes[is_part_of_cell] if self._cv_centroids is not None: self._cv_centroids = self._cv_centroids[is_part_of_cell] if self.facets is not None: self.facets["points"] = new_point_idx[self.facets["points"]] if self._is_interior_point is not None: self._is_interior_point = self._is_interior_point[is_part_of_cell] if self._is_boundary_point is not None: self._is_boundary_point = self._is_boundary_point[is_part_of_cell] if self._is_point_used is not None: self._is_point_used = self._is_point_used[is_part_of_cell] return np.sum(~is_part_of_cell) @property def q_radius_ratio(self): """Ratio of incircle and circumcircle ratios times (n-1). ("Normalized shape ratio".) Is 1 for the equilateral simplex, and is often used a quality measure for the cell. """ # There are other sensible possibilities of defining cell quality, e.g.: # * inradius to longest edge # * shortest to longest edge # * minimum dihedral angle # * ... # See # . if self.n == 3: # q = 2 * r_in / r_out # = (-a+b+c) * (+a-b+c) * (+a+b-c) / (a*b*c), # # where r_in is the incircle radius and r_out the circumcircle radius # and a, b, c are the edge lengths. a, b, c = self.edge_lengths return (-a + b + c) * (a - b + c) * (a + b - c) / (a * b * c) return (self.n - 1) * self.cell_inradius / self.cell_circumradius def remove_cells(self, remove_array: ArrayLike): """Remove cells and take care of all the dependent data structures. The input argument `remove_array` can be a boolean array or a list of indices. """ # Although this method doesn't compute anything new, the reorganization of the # data structure is fairly expensive. This is mostly due to the fact that mask # copies like `a[mask]` take long if `a` is large, even if `mask` is True almost # everywhere. # Keep an eye on for possible # workarounds. remove_array = np.asarray(remove_array) if len(remove_array) == 0: return 0 if remove_array.dtype == int: keep = np.ones(len(self.cells("points")), dtype=bool) keep[remove_array] = False else: assert remove_array.dtype == bool keep = ~remove_array assert len(keep) == len(self.cells("points")), "Wrong length of index array." if np.all(keep): return 0 # handle facet; this is a bit messy if self._cells_facets is not None: # updating the boundary data is a lot easier with facets_cells if self._facets_cells is None: self._compute_facets_cells() # Set facet to is_boundary_facet_local=True if it is adjacent to a removed # cell. facet_ids = self.cells("facets")[~keep].flatten() # only consider interior facets facet_ids = facet_ids[self.is_interior_facet[facet_ids]] idx = self.facets_cells_idx[facet_ids] cell_id = self.facets_cells["interior"][1:3, idx].T local_facet_id = self.facets_cells["interior"][3:5, idx].T self._is_boundary_facet_local[local_facet_id, cell_id] = True # now remove the entries corresponding to the removed cells self._is_boundary_facet_local = self._is_boundary_facet_local[:, keep] if self._is_boundary_cell is not None: self._is_boundary_cell[cell_id] = True self._is_boundary_cell = self._is_boundary_cell[keep] # update facets_cells keep_b_ec = keep[self.facets_cells["boundary"][1]] keep_i_ec0, keep_i_ec1 = keep[self.facets_cells["interior"][1:3]] # move ec from interior to boundary if exactly one of the two adjacent cells # was removed keep_i_0 = keep_i_ec0 & ~keep_i_ec1 keep_i_1 = keep_i_ec1 & ~keep_i_ec0 self._facets_cells["boundary"] = np.array( [ # facet id np.concatenate( [ self._facets_cells["boundary"][0, keep_b_ec], self._facets_cells["interior"][0, keep_i_0], self._facets_cells["interior"][0, keep_i_1], ] ), # cell id np.concatenate( [ self._facets_cells["boundary"][1, keep_b_ec], self._facets_cells["interior"][1, keep_i_0], self._facets_cells["interior"][2, keep_i_1], ] ), # local facet id np.concatenate( [ self._facets_cells["boundary"][2, keep_b_ec], self._facets_cells["interior"][3, keep_i_0], self._facets_cells["interior"][4, keep_i_1], ] ), ] ) keep_i = keep_i_ec0 & keep_i_ec1 # this memory copy isn't too fast self._facets_cells["interior"] = self._facets_cells["interior"][:, keep_i] num_facets_old = len(self.facets["points"]) adjacent_facets, counts = np.unique( self.cells("facets")[~keep].flat, return_counts=True ) # remove facet entirely either if 2 adjacent cells are removed or if it is a # boundary facet and 1 adjacent cells are removed is_facet_removed = (counts == 2) | ( (counts == 1) & self._is_boundary_facet[adjacent_facets] ) # set the new boundary facet self._is_boundary_facet[adjacent_facets[~is_facet_removed]] = True # Now actually remove the facets. This includes a reindexing. assert self._is_boundary_facet is not None keep_facets = np.ones(len(self._is_boundary_facet), dtype=bool) keep_facets[adjacent_facets[is_facet_removed]] = False # make sure there is only facets["points"], not facets["cells"] etc. assert self.facets is not None assert len(self.facets) == 1 self.facets["points"] = self.facets["points"][keep_facets] self._is_boundary_facet = self._is_boundary_facet[keep_facets] # update facet and cell indices self._cells_facets = self.cells("facets")[keep] new_index_facets = np.arange(num_facets_old) - np.cumsum(~keep_facets) self._cells_facets = new_index_facets[self.cells("facets")] num_cells_old = len(self.cells("points")) new_index_cells = np.arange(num_cells_old) - np.cumsum(~keep) # this takes fairly long ec = self._facets_cells ec["boundary"][0] = new_index_facets[ec["boundary"][0]] ec["boundary"][1] = new_index_cells[ec["boundary"][1]] ec["interior"][0] = new_index_facets[ec["interior"][0]] ec["interior"][1:3] = new_index_cells[ec["interior"][1:3]] # simply set those to None; their reset is cheap self._facets_cells_idx = None self._boundary_facets = None self._interior_facets = None for k in range(len(self.idx)): self.idx[k] = self.idx[k][..., keep] if self._volumes is not None: for k in range(len(self._volumes)): self._volumes[k] = self._volumes[k][..., keep] if self._ce_ratios is not None: self._ce_ratios = self._ce_ratios[:, keep] if self._half_edge_coords is not None: self._half_edge_coords = self._half_edge_coords[:, keep] if self._ei_dot_ei is not None: self._ei_dot_ei = self._ei_dot_ei[:, keep] if self._cell_centroids is not None: self._cell_centroids = self._cell_centroids[keep] if self._circumcenters is not None: for k in range(len(self._circumcenters)): self._circumcenters[k] = self._circumcenters[k][..., keep, :] if self._cell_partitions is not None: self._cell_partitions = self._cell_partitions[..., keep] if self._signed_cell_volumes is not None: self._signed_cell_volumes = self._signed_cell_volumes[keep] if self._integral_x is not None: self._integral_x = self._integral_x[..., keep, :] if self._circumcenter_facet_distances is not None: self._circumcenter_facet_distances = self._circumcenter_facet_distances[ ..., keep ] # TODO These could also be updated, but let's implement it when needed self._signed_circumcenter_distances = None self._control_volumes = None self._cv_cell_mask = None self._cv_centroids = None self._cvc_cell_mask = None self._is_point_used = None self._is_interior_point = None self._is_boundary_point = None return np.sum(~keep) def remove_boundary_cells(self, criterion): """Helper method for removing cells along the boundary. The input criterion is a callback that must return an array of length `sum(mesh.is_boundary_cell)`. This helps, for example, in the following scenario. When points are moving around, flip_until_delaunay() makes sure the mesh remains a Delaunay mesh. This does not work on boundaries where very flat cells can still occur or cells may even 'invert'. (The interior point moves outside.) In this case, the boundary cell can be removed, and the newly outward node is made a boundary node.""" num_removed = 0 while True: num_boundary_cells = np.sum(self.is_boundary_cell) crit = criterion(self.is_boundary_cell) if ~np.any(crit): break if not isinstance(crit, np.ndarray) or crit.shape != (num_boundary_cells,): raise ValueError( "criterion() callback must return a Boolean NumPy array " f"of shape {(num_boundary_cells,)}, got {crit.shape}." ) idx = self.is_boundary_cell.copy() idx[idx] = crit n = self.remove_cells(idx) num_removed += n if n == 0: break return num_removed def remove_duplicate_cells(self): sorted_cells = np.sort(self.cells("points")) _, inv, cts = npx.unique_rows( sorted_cells, return_inverse=True, return_counts=True ) remove = np.zeros(len(self.cells("points")), dtype=bool) for k in np.where(cts > 1)[0]: rem = inv == k # don't remove first occurrence first_idx = np.where(rem)[0][0] rem[first_idx] = False remove |= rem return self.remove_cells(remove) def get_control_volumes(self, cell_mask: ArrayLike | None = None) -> np.ndarray: """The control volumes around each vertex. Optionally disregard the contributions from particular cells. This is useful, for example, for temporarily disregarding flat cells on the boundary when performing Lloyd mesh optimization. """ if cell_mask is not None: cell_mask = np.asarray(cell_mask) if self._cv_centroids is None or np.any(cell_mask != self._cvc_cell_mask): # Sum up the contributions according to how self.idx is constructed. # roll = np.array([np.roll(np.arange(kk + 3), -i) for i in range(1, kk + 3)]) # vols = npx.sum_at(vols, roll, kk + 3) # v = self.cell_partitions[..., idx] if cell_mask is None: idx = slice(None) else: idx = ~cell_mask # TODO this can be improved by first summing up all components per cell self._control_volumes = npx.sum_at( self.cell_partitions[..., idx], self.idx[-1][..., idx], len(self.points), ) self._cv_cell_mask = cell_mask assert self._control_volumes is not None return self._control_volumes control_volumes = property(get_control_volumes) @property def is_delaunay(self): return self.num_delaunay_violations == 0 @property def num_delaunay_violations(self): """Number of interior facets where the Delaunay condition is violated.""" # Delaunay violations are present exactly on the interior facets where the # signed circumcenter distance is negative. Count those. return np.sum(self.signed_circumcenter_distances < 0.0) @property def idx_hierarchy(self): warnings.warn( "idx_hierarchy is deprecated, use idx[-1] instead", DeprecationWarning ) return self.idx[-1] def show(self, *args, fullscreen=False, **kwargs): """Show the mesh (see plot()).""" import matplotlib.pyplot as plt self.plot(*args, **kwargs) if fullscreen: mng = plt.get_current_fig_manager() # mng.frame.Maximize(True) mng.window.showMaximized() plt.show() plt.close() def save(self, filename, *args, **kwargs): """Save the mesh to a file, either as a PNG/SVG or a mesh file""" if pathlib.Path(filename).suffix in [".png", ".svg"]: import matplotlib.pyplot as plt self.plot(*args, **kwargs) plt.savefig(filename, transparent=True, bbox_inches="tight") plt.close() else: self.write(filename) def plot(self, *args, **kwargs): if self.n == 2: self._plot_line(*args, **kwargs) else: assert self.n == 3 self._plot_tri(*args, **kwargs) def _plot_line(self): import matplotlib.pyplot as plt if len(self.points.shape) == 1: x = self.points y = np.zeros(self.points.shape[0]) else: assert len(self.points.shape) == 2 and self.points.shape[1] == 2 x, y = self.points.T plt.plot(x, y, "-o") def _plot_tri( self, show_coedges=True, control_volume_centroid_color=None, mesh_color="k", nondelaunay_edge_color=None, boundary_edge_color=None, comesh_color=(0.8, 0.8, 0.8), show_axes=True, cell_quality_coloring=None, show_point_numbers=False, show_edge_numbers=False, show_cell_numbers=False, cell_mask=None, mark_points=None, mark_edges=None, mark_cells=None, ): """Show the mesh using matplotlib.""" # Importing matplotlib takes a while, so don't do that at the header. import matplotlib.pyplot as plt from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Polygon fig = plt.figure() ax = fig.gca() plt.axis("equal") if not show_axes: ax.set_axis_off() xmin = np.amin(self.points[:, 0]) xmax = np.amax(self.points[:, 0]) ymin = np.amin(self.points[:, 1]) ymax = np.amax(self.points[:, 1]) width = xmax - xmin xmin -= 0.1 * width xmax += 0.1 * width height = ymax - ymin ymin -= 0.1 * height ymax += 0.1 * height ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) # for k, x in enumerate(self.points): # if self.is_boundary_point[k]: # plt.plot(x[0], x[1], "g.") # else: # plt.plot(x[0], x[1], "r.") if show_point_numbers: for i, x in enumerate(self.points): plt.text( x[0], x[1], str(i), bbox={"facecolor": "w", "alpha": 0.7}, horizontalalignment="center", verticalalignment="center", ) if show_edge_numbers: if self.edges is None: self.create_facets() for i, point_ids in enumerate(self.edges["points"]): midpoint = np.sum(self.points[point_ids], axis=0) / 2 plt.text( midpoint[0], midpoint[1], str(i), bbox={"facecolor": "b", "alpha": 0.7}, color="w", horizontalalignment="center", verticalalignment="center", ) if show_cell_numbers: for i, x in enumerate(self.cell_centroids): plt.text( x[0], x[1], str(i), bbox={"facecolor": "r", "alpha": 0.5}, horizontalalignment="center", verticalalignment="center", ) # coloring if cell_quality_coloring: cmap, cmin, cmax, show_colorbar = cell_quality_coloring plt.tripcolor( self.points[:, 0], self.points[:, 1], self.cells("points"), self.q_radius_ratio, shading="flat", cmap=cmap, vmin=cmin, vmax=cmax, ) if show_colorbar: plt.colorbar() if mark_points is not None: idx = mark_points plt.plot(self.points[idx, 0], self.points[idx, 1], "x", color="r") if mark_cells is not None: if np.asarray(mark_cells).dtype == bool: mark_cells = np.where(mark_cells)[0] patches = [ Polygon(self.points[self.cells("points")[idx]]) for idx in mark_cells ] p = PatchCollection(patches, facecolor="C1") ax.add_collection(p) if self.edges is None: self.create_facets() # Get edges, cut off z-component. e = self.points[self.edges["points"]][:, :, :2] if nondelaunay_edge_color is None: line_segments0 = LineCollection(e, color=mesh_color) ax.add_collection(line_segments0) else: # Plot regular edges, mark those with negative ce-ratio red. is_pos = np.zeros(len(self.edges["points"]), dtype=bool) is_pos[self.interior_facets[self.signed_circumcenter_distances >= 0]] = True # Mark Delaunay-conforming boundary edges is_pos_boundary = self.ce_ratios[self.is_boundary_facet_local] >= 0 is_pos[self.boundary_facets[is_pos_boundary]] = True line_segments0 = LineCollection(e[is_pos], color=mesh_color) ax.add_collection(line_segments0) # line_segments1 = LineCollection(e[~is_pos], color=nondelaunay_edge_color) ax.add_collection(line_segments1) if mark_edges is not None: e = self.points[self.edges["points"][mark_edges]][..., :2] ax.add_collection(LineCollection(e, color="r")) if show_coedges: # Connect all cell circumcenters with the edge midpoints cc = self.cell_circumcenters edge_midpoints = 0.5 * ( self.points[self.edges["points"][:, 0]] + self.points[self.edges["points"][:, 1]] ) # Plot connection of the circumcenter to the midpoint of all three # axes. a = np.stack( [cc[:, :2], edge_midpoints[self.cells("edges")[:, 0], :2]], axis=1 ) b = np.stack( [cc[:, :2], edge_midpoints[self.cells("edges")[:, 1], :2]], axis=1 ) c = np.stack( [cc[:, :2], edge_midpoints[self.cells("edges")[:, 2], :2]], axis=1 ) line_segments = LineCollection( np.concatenate([a, b, c]), color=comesh_color ) ax.add_collection(line_segments) if boundary_edge_color: e = self.points[self.edges["points"][self.is_boundary_facet]][:, :, :2] line_segments1 = LineCollection(e, color=boundary_edge_color) ax.add_collection(line_segments1) if control_volume_centroid_color is not None: centroids = self.get_control_volume_centroids(cell_mask=cell_mask) ax.plot( centroids[:, 0], centroids[:, 1], linestyle="", marker=".", color=control_volume_centroid_color, ) for k, centroid in enumerate(centroids): plt.text( centroid[0], centroid[1], str(k), bbox=dict(facecolor=control_volume_centroid_color, alpha=0.7), horizontalalignment="center", verticalalignment="center", ) return fig meshplex-0.17.0/src/meshplex/_mesh_tetra.py000066400000000000000000000310571417176457700207350ustar00rootroot00000000000000import numpy as np from ._mesh import Mesh __all__ = ["MeshTetra"] class MeshTetra(Mesh): """Class for handling tetrahedral meshes.""" def __init__(self, points, cells, sort_cells=False): super().__init__(points, cells, sort_cells=sort_cells) assert self.n == 4 self.faces = None def _create_face_edge_relationships(self): a = np.vstack( [ self.faces["points"][:, [1, 2]], self.faces["points"][:, [2, 0]], self.faces["points"][:, [0, 1]], ] ) # Find the unique edges b = np.ascontiguousarray(a).view( np.dtype((np.void, a.dtype.itemsize * a.shape[1])) ) _, idx, inv = np.unique(b, return_index=True, return_inverse=True) edge_points = a[idx] self.edges = {"points": edge_points} # face->edge relationship num_faces = len(self.faces["points"]) face_edges = inv.reshape([3, num_faces]).T self.faces["edges"] = face_edges @property def q_min_sin_dihedral_angles(self): """Get the smallest of the sines of the 6 angles between the faces of each tetrahedron, times a scaling factor that makes sure the value is 1 for the equilateral tetrahedron. """ # https://math.stackexchange.com/a/49340/36678 fa = self.facet_areas el2 = self.ei_dot_ei a = el2[0][0] b = el2[1][0] c = el2[2][0] d = el2[0][2] e = el2[1][1] f = el2[0][1] cos_alpha = [] H2 = (4 * a * d - ((b + e) - (c + f)) ** 2) / 16 J2 = (4 * b * e - ((a + d) - (c + f)) ** 2) / 16 K2 = (4 * c * f - ((a + d) - (b + e)) ** 2) / 16 # Angle between face 0 and face 1. # The faces share (face 0, edge 0), (face 1, edge 2). cos_alpha += [(fa[0] ** 2 + fa[1] ** 2 - H2) / (2 * fa[0] * fa[1])] # Angle between face 0 and face 2. # The faces share (face 0, edge 1), (face 2, edge 1). cos_alpha += [(fa[0] ** 2 + fa[2] ** 2 - J2) / (2 * fa[0] * fa[2])] # Angle between face 0 and face 3. # The faces share (face 0, edge 2), (face 3, edge 0). cos_alpha += [(fa[0] ** 2 + fa[3] ** 2 - K2) / (2 * fa[0] * fa[3])] # Angle between face 1 and face 2. # The faces share (face 1, edge 0), (face 2, edge 2). cos_alpha += [(fa[1] ** 2 + fa[2] ** 2 - K2) / (2 * fa[1] * fa[2])] # Angle between face 1 and face 3. # The faces share (face 1, edge 1), (face 3, edge 1). cos_alpha += [(fa[1] ** 2 + fa[3] ** 2 - J2) / (2 * fa[1] * fa[3])] # Angle between face 2 and face 3. # The faces share (face 2, edge 0), (face 3, edge 2). cos_alpha += [(fa[2] ** 2 + fa[3] ** 2 - H2) / (2 * fa[2] * fa[3])] cos_alpha = np.array(cos_alpha).T sin_alpha = np.sqrt(1 - cos_alpha ** 2) m = np.min(sin_alpha, axis=1) / (np.sqrt(2) * 2 / 3) return m @property def q_vol_rms_edgelength3(self): """For each cell, return the ratio of the volume and the cube of the root-mean-square edge length. (This is cell quality measure used by Stellar .) """ el2 = self.ei_dot_ei rms = np.sqrt( (el2[0][0] + el2[1][0] + el2[2][0] + el2[0][2] + el2[1][1] + el2[0][1]) / 6 ) alpha = np.sqrt(2) / 12 # normalization factor return self.cell_volumes / rms ** 3 / alpha def show(self): from matplotlib import pyplot as plt self.plot() plt.show() plt.close() def plot(self): from matplotlib import pyplot as plt from mpl_toolkits.mplot3d import Axes3D ax = plt.axes(projection=Axes3D.name) # "It is not currently possible to manually set the aspect on 3D axes" # plt.axis("equal") for cell_id in range(len(self.cells("points"))): cc = self.cell_circumcenters[cell_id] # face_ccs = self._circumcenters[-2] # draw the face circumcenters ax.plot( face_ccs[..., 0].flatten(), face_ccs[..., 1].flatten(), face_ccs[..., 2].flatten(), "go", ) # draw the connections # tet circumcenter---face circumcenter for face_cc in face_ccs: ax.plot( [cc[0], face_cc[cell_id, 0]], [cc[1], face_cc[cell_id, 1]], [cc[2], face_cc[cell_id, 2]], "b-", ) def show_edge(self, edge_id): from matplotlib import pyplot as plt self.plot_edge(edge_id) plt.show() plt.close() def plot_edge(self, edge_id): """Displays edge with ce_ratio. :param edge_id: Edge ID for which to show the ce_ratio. :type edge_id: int """ # pylint: disable=unused-variable,relative-import from matplotlib import pyplot as plt from mpl_toolkits.mplot3d import Axes3D if self._cells_facets is None: self.create_facets() if "edges" not in self.faces: self._create_face_edge_relationships() ax = plt.axes(projection=Axes3D.name) # "It is not currently possible to manually set the aspect on 3D axes" # plt.axis("equal") # find all faces with this edge adj_face_ids = np.where((self.faces["edges"] == edge_id).any(axis=1))[0] # find all cells with the faces # https://stackoverflow.com/a/38481969/353337 adj_cell_ids = np.where( np.in1d(self.cells("facets"), adj_face_ids) .reshape(self.cells("facets").shape) .any(axis=1) )[0] # plot all those adjacent cells; first collect all edges adj_edge_ids = np.unique( [ adj_edge_id for adj_cell_id in adj_cell_ids for face_id in self.cells("facets")[adj_cell_id] for adj_edge_id in self.faces["edges"][face_id] ] ) col = "k" for adj_edge_id in adj_edge_ids: x = self.points[self.edges["points"][adj_edge_id]] ax.plot(x[:, 0], x[:, 1], x[:, 2], col) # make clear which is edge_id x = self.points[self.edges["points"][edge_id]] ax.plot(x[:, 0], x[:, 1], x[:, 2], color=col, linewidth=3.0) # connect the face circumcenters with the corresponding cell # circumcenters for cell_id in adj_cell_ids: cc = self.cell_circumcenters[cell_id] face_ccs = self._circumcenters[-2] # draw the face circumcenters ax.plot( face_ccs[..., 0].flatten(), face_ccs[..., 1].flatten(), face_ccs[..., 2].flatten(), "go", ) # draw the connections # tet circumcenter---face circumcenter for face_cc in face_ccs: ax.plot( [cc[0], face_cc[cell_id, 0]], [cc[1], face_cc[cell_id, 1]], [cc[2], face_cc[cell_id, 2]], "b-", ) # draw the cell circumcenters cc = self.cell_circumcenters[adj_cell_ids] ax.plot(cc[:, 0], cc[:, 1], cc[:, 2], "ro") def show_cell( self, cell_id, control_volume_boundaries_rgba=None, barycenter_rgba=None, circumcenter_rgba=None, incenter_rgba=None, face_circumcenter_rgba=None, insphere_rgba=None, circumsphere_rgba=None, line_width=1.0, close=False, render=True, ): import vtk def get_line_actor(x0, x1, line_width=1.0): source = vtk.vtkLineSource() source.SetPoint1(x0) source.SetPoint2(x1) # mapper mapper = vtk.vtkPolyDataMapper() mapper.SetInputConnection(source.GetOutputPort()) # actor actor = vtk.vtkActor() actor.SetMapper(mapper) # color actor actor.GetProperty().SetColor(0, 0, 0) actor.GetProperty().SetLineWidth(line_width) return actor def get_sphere_actor(x0, r, rgba): # Generate polygon data for a sphere sphere = vtk.vtkSphereSource() sphere.SetCenter(x0) sphere.SetRadius(r) sphere.SetPhiResolution(100) sphere.SetThetaResolution(100) # Create a mapper for the sphere data sphere_mapper = vtk.vtkPolyDataMapper() # sphere_mapper.SetInput(sphere.GetOutput()) sphere_mapper.SetInputConnection(sphere.GetOutputPort()) # Connect the mapper to an actor sphere_actor = vtk.vtkActor() sphere_actor.SetMapper(sphere_mapper) sphere_actor.GetProperty().SetColor(rgba[:3]) sphere_actor.GetProperty().SetOpacity(rgba[3]) return sphere_actor # Visualize renderer = vtk.vtkRenderer() render_window = vtk.vtkRenderWindow() render_window.AddRenderer(renderer) render_window_interactor = vtk.vtkRenderWindowInteractor() render_window_interactor.SetRenderWindow(render_window) for ij in [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]: x0, x1 = self.points[self.cells("points")[cell_id][ij]] renderer.AddActor(get_line_actor(x0, x1, line_width)) renderer.SetBackground(1.0, 1.0, 1.0) r = 0.02 if circumcenter_rgba is not None: renderer.AddActor( get_sphere_actor(self.cell_circumcenters[cell_id], r, circumcenter_rgba) ) if circumsphere_rgba is not None: renderer.AddActor( get_sphere_actor( self.cell_circumcenters[cell_id], self.cell_circumradius[cell_id], circumsphere_rgba, ) ) if incenter_rgba is not None: renderer.AddActor( get_sphere_actor(self.cell_incenters[cell_id], r, incenter_rgba) ) if insphere_rgba is not None: renderer.AddActor( get_sphere_actor( self.cell_incenters[cell_id], self.cell_inradius[cell_id], insphere_rgba, ) ) if barycenter_rgba is not None: renderer.AddActor( get_sphere_actor(self.cell_barycenters[cell_id], r, barycenter_rgba) ) if face_circumcenter_rgba is not None: face_ccs = self._circumcenters[-2][:, 0] for f in face_ccs: renderer.AddActor(get_sphere_actor(f, r, face_circumcenter_rgba)) if control_volume_boundaries_rgba: cell_cc = self.cell_circumcenters[cell_id] face_ccs = self._circumcenters[-2][:, 0] for face, face_cc in zip(range(4), face_ccs): for edge in range(3): k0, k1 = self.idx[-1][:, edge, face, cell_id] edge_midpoint = 0.5 * (self.points[k0] + self.points[k1]) points = vtk.vtkPoints() points.InsertNextPoint(*edge_midpoint) points.InsertNextPoint(*cell_cc) points.InsertNextPoint(*face_cc) triangle = vtk.vtkTriangle() triangle.GetPointIds().SetId(0, 0) triangle.GetPointIds().SetId(1, 1) triangle.GetPointIds().SetId(2, 2) triangles = vtk.vtkCellArray() triangles.InsertNextCell(triangle) trianglePolyData = vtk.vtkPolyData() trianglePolyData.SetPoints(points) trianglePolyData.SetPolys(triangles) # mapper mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(trianglePolyData) # actor actor = vtk.vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor(*control_volume_boundaries_rgba[:3]) actor.GetProperty().SetOpacity(control_volume_boundaries_rgba[3]) renderer.AddActor(actor) if render: render_window.Render() if close: render_window.Finalize() del render_window, render_window_interactor else: render_window_interactor.Start() meshplex-0.17.0/src/meshplex/_mesh_tri.py000066400000000000000000000567671417176457700204330ustar00rootroot00000000000000import warnings import npx import numpy as np from ._mesh import Mesh __all__ = ["MeshTri"] class MeshTri(Mesh): """Class for handling triangular meshes.""" def __init__(self, points, cells, sort_cells=False): super().__init__(points, cells, sort_cells=sort_cells) assert self.n == 3 # some backwards-compatibility fixes self.create_edges = super().create_facets self.compute_signed_cell_areas = super().compute_signed_cell_volumes self.boundary_edges = super().boundary_facets self.is_boundary_edge = super().is_boundary_facet @property def euler_characteristic(self): # number of vertices - number of edges + number of faces if self._cells_facets is None: self.create_facets() return ( self.points.shape[0] - self.edges["points"].shape[0] + self.cells("points").shape[0] ) @property def genus(self): # : # # chi = 2 - 2 * g - b # # g = 1 - (chi + b) / 2 # # where b is the number of boundary components. if self._is_boundary_facet is None: self.create_facets() if np.any(self.is_boundary_edge): from scipy.sparse import coo_matrix from scipy.sparse.csgraph import connected_components boundary_edges = self.edges["points"][self.is_boundary_edge] n = np.max(boundary_edges) + 1 val = np.ones(len(boundary_edges), dtype=int) M = coo_matrix((val, boundary_edges.T), shape=(n, n)) # Unfortunately, connected_components() counts orphaned nodes as an # individual component. See . _, cc = connected_components(M) num_cc = np.sum(np.bincount(cc) > 1) else: num_cc = 0 chi = self.euler_characteristic if (chi + num_cc) % 2 != 0: raise RuntimeError("Non-integer genus. Is the mesh perhaps not orientable?") return 1 - (chi + num_cc) // 2 @property def angles(self): """All angles in the triangle.""" # The cosines of the angles are the negative dot products of the normalized # edges adjacent to the angle. norms = self.edge_lengths ei_dot_ej = self.ei_dot_ei - np.sum(self.ei_dot_ei, axis=0) / 2 normalized_ei_dot_ej = np.array( [ ei_dot_ej[0] / norms[1] / norms[2], ei_dot_ej[1] / norms[2] / norms[0], ei_dot_ej[2] / norms[0] / norms[1], ] ) return np.arccos(-normalized_ei_dot_ej) # def compute_gradient(self, u): # '''Computes an approximation to the gradient :math:`\\nabla u` of a # given scalar valued function :math:`u`, defined in the points. # This is taken from # # Discrete gradient method in solid mechanics, # Lu, Jia and Qian, Jing and Han, Weimin, # International Journal for Numerical Methods in Engineering, # . # ''' # if self.cell_circumcenters is None: # self.cell_circumcenters = self._circumcenters[-1] # # if 'cells' not in self.edges: # self.edges['cells'] = self.compute_edge_cells() # # # This only works for flat meshes. # assert (abs(self.points[:, 2]) < 1.0e-10).all() # points2d = self.points[:, :2] # cell_circumcenters2d = self.cell_circumcenters[:, :2] # # num_points = len(points2d) # assert len(u) == num_points # # gradient = np.zeros((num_points, 2), dtype=u.dtype) # # # Create an empty 2x2 matrix for the boundary points to hold the # # edge correction ((17) in [1]). # boundary_matrices = {} # for point in self.get_vertices('boundary'): # boundary_matrices[point] = np.zeros((2, 2)) # # for edge_gid, edge in enumerate(self.edges['cells']): # # Compute edge length. # point0 = self.edges['points'][edge_gid][0] # point1 = self.edges['points'][edge_gid][1] # # # Compute coedge length. # if len(self.edges['cells'][edge_gid]) == 1: # # Boundary edge. # edge_midpoint = 0.5 * ( # points2d[point0] + # points2d[point1] # ) # cell0 = self.edges['cells'][edge_gid][0] # coedge_midpoint = 0.5 * ( # cell_circumcenters2d[cell0] + # edge_midpoint # ) # elif len(self.edges['cells'][edge_gid]) == 2: # cell0 = self.edges['cells'][edge_gid][0] # cell1 = self.edges['cells'][edge_gid][1] # # Interior edge. # coedge_midpoint = 0.5 * ( # cell_circumcenters2d[cell0] + # cell_circumcenters2d[cell1] # ) # else: # raise RuntimeError( # 'Edge needs to have either one or two neighbors.' # ) # # # Compute the coefficient r for both contributions # coeffs = self.ce_ratios[edge_gid] / \ # self.control_volumes[self.edges['points'][edge_gid]] # # # Compute R*_{IJ} ((11) in [1]). # r0 = (coedge_midpoint - points2d[point0]) * coeffs[0] # r1 = (coedge_midpoint - points2d[point1]) * coeffs[1] # # diff = u[point1] - u[point0] # # gradient[point0] += r0 * diff # gradient[point1] -= r1 * diff # # # Store the boundary correction matrices. # edge_coords = points2d[point1] - points2d[point0] # if point0 in boundary_matrices: # boundary_matrices[point0] += np.outer(r0, edge_coords) # if point1 in boundary_matrices: # boundary_matrices[point1] += np.outer(r1, -edge_coords) # # # Apply corrections to the gradients on the boundary. # for k, value in boundary_matrices.items(): # gradient[k] = np.linalg.solve(value, gradient[k]) # # return gradient def compute_ncurl(self, vector_field): """Computes the n.dot.curl of a vector field over the mesh. While the vector field is point-based, the curl will be cell-based. The approximation is based on .. math:: n\\cdot curl(F) = \\lim_{A\\to 0} |A|^{-1} \\rangle\\int_{dGamma}, F\\rangle dr; see https://en.wikipedia.org/wiki/Curl_(mathematics). Actually, to approximate the integral, one would only need the projection of the vector field onto the edges at the midpoint of the edges. """ # Compute the projection of A on the edge at each edge midpoint. Take the # average of `vector_field` at the endpoints to get the approximate value at the # edge midpoint. A = 0.5 * np.sum(vector_field[self.idx[-1]], axis=0) # sum of for all three edges sum_edge_dot_A = np.einsum("ijk, ijk->j", self.half_edge_coords, A) # Get normalized vector orthogonal to triangle z = np.cross(self.half_edge_coords[0], self.half_edge_coords[1]) # Now compute # # curl = z / ||z|| * sum_edge_dot_A / |A|. # # Since ||z|| = 2*|A|, one can save a sqrt and do # # curl = z * sum_edge_dot_A * 0.5 / |A|^2. # curl = z * (0.5 * sum_edge_dot_A / self.cell_volumes ** 2)[..., None] return curl def show_vertex(self, *args, **kwargs): """Show the mesh around a vertex (see plot_vertex()).""" import matplotlib.pyplot as plt self.plot_vertex(*args, **kwargs) plt.show() plt.close() def plot_vertex(self, point_id, show_ce_ratio=True): """Plot the vicinity of a point and its covolume/edgelength ratio. :param point_id: Node ID of the point to be shown. :type point_id: int :param show_ce_ratio: If true, shows the ce_ratio of the point, too. :type show_ce_ratio: bool, optional """ # Importing matplotlib takes a while, so don't do that at the header. import matplotlib.pyplot as plt fig = plt.figure() ax = fig.gca() plt.axis("equal") if self.edges is None: self.create_facets() # Find the edges that contain the vertex edge_gids = np.where((self.edges["points"] == point_id).any(axis=1))[0] # ... and plot them for point_ids in self.edges["points"][edge_gids]: x = self.points[point_ids] ax.plot(x[:, 0], x[:, 1], "k") # Highlight ce_ratios. if show_ce_ratio: # Find the cells that contain the vertex cell_ids = np.where((self.cells("points") == point_id).any(axis=1))[0] for cell_id in cell_ids: for edge_gid in self.cells("edges")[cell_id]: if point_id not in self.edges["points"][edge_gid]: continue point_ids = self.edges["points"][edge_gid] edge_midpoint = 0.5 * ( self.points[point_ids[0]] + self.points[point_ids[1]] ) p = np.stack( [self.cell_circumcenters[cell_id], edge_midpoint], axis=1 ) q = np.column_stack( [ self.cell_circumcenters[cell_id], edge_midpoint, self.points[point_id], ] ) ax.fill(q[0], q[1], color="0.5") ax.plot(p[0], p[1], color="0.7") return def flip_until_delaunay(self, tol=0.0, max_steps=100): """Flip edges until the mesh is fully Delaunay (up to `tol`).""" num_flips = 0 assert tol >= 0.0 # If all circumcenter-facet distances are positive, all cells are Delaunay. if np.all(self.circumcenter_facet_distances > -0.5 * tol): return num_flips # Now compute the boundary facet. A little more costly, but we'd have to do that # anyway. If all _interior_ circumcenter-facet distances are positive, all cells # are Delaunay. if np.all( self.circumcenter_facet_distances[~self.is_boundary_facet_local] > -0.5 * tol ): return num_flips step = 0 is_flip_interior_facet = self.signed_circumcenter_distances < -tol while True: # Don't flip the edges which would flip into existing edges. This can # happen, for example, in triangular shell meshes in 3D, and leads to weird # behavior down the line. See # . flip_filter = np.ones(np.sum(is_flip_interior_facet), dtype=bool) # facets_cells_flip = self.facets_cells["interior"][:, is_flip_interior_facet] # facet_gids = facets_cells_flip[0] adj_cells = facets_cells_flip[1:3] lids = facets_cells_flip[3:5] expected_new_edges = np.array( [ self.cells("points")[adj_cells[0], lids[0]], self.cells("points")[adj_cells[1], lids[1]], ] ).T expected_new_edges = np.sort(expected_new_edges, axis=1) # # This isin() call can be quite costly since we're checking against _all_ # existing edges. already_exists = npx.isin_rows(expected_new_edges, self.facets["points"]) flip_filter &= ~already_exists # # Check if some flips would lead to the same flipped edge. _, inv = npx.unique_rows(expected_new_edges, return_inverse=True) is_unique = np.zeros(len(expected_new_edges), dtype=bool) is_unique[inv] = True flip_filter &= is_unique # apply the filter is_flip_interior_facet[is_flip_interior_facet] &= flip_filter step += 1 if not np.any(is_flip_interior_facet): break if step > max_steps: break interior_facets_cells = self.facets_cells["interior"][1:3].T adj_cells = interior_facets_cells[is_flip_interior_facet].T # Check if there are cells for which more than one facet needs to be # flipped. For those, only flip one facet, namely that with the smaller # (more negative) circumcenter_facet_distance. cell_gids, num_flips_per_cell = np.unique(adj_cells, return_counts=True) multiflip_cell_gids = cell_gids[num_flips_per_cell > 1] while np.any(num_flips_per_cell > 1): for cell_gid in multiflip_cell_gids: facet_gids = self.cells("facets")[cell_gid] is_interior_facet = self.is_interior_facet[facet_gids] idx = self.facets_cells_idx[facet_gids[is_interior_facet]] k = np.argmin(self.signed_circumcenter_distances[idx]) is_flip_interior_facet[idx] = False is_flip_interior_facet[idx[k]] = True adj_cells = interior_facets_cells[is_flip_interior_facet].T cell_gids, num_flips_per_cell = np.unique(adj_cells, return_counts=True) multiflip_cell_gids = cell_gids[num_flips_per_cell > 1] # actually perform the flips self.flip_interior_facets(is_flip_interior_facet) num_flips += np.sum(is_flip_interior_facet) is_flip_interior_facet_old = is_flip_interior_facet.copy() # Check which edges need to be flipped next. is_flip_interior_facet = self.signed_circumcenter_distances < -tol # Don't flip edges which have just been flipped. (This can happen due to # round-off errors.) is_flip_interior_facet[is_flip_interior_facet_old] = False is_negative = self.signed_circumcenter_distances < -tol num_nondelaunay_facets = np.sum(is_negative) if num_nondelaunay_facets > 0: dists = self.signed_circumcenter_distances[is_negative] warnings.warn( f"After {step} edge flip steps, " + f"there are {num_nondelaunay_facets} remaining non-Delaunay facets. " f"The signed circumcenter distances are {dists.tolist()}. " + "This can happen due to round-off errors or " + "to prevent non-manifold edges in shell meshes." ) return num_flips def flip_interior_facets(self, is_flip_interior_facet): facets_cells_flip = self.facets_cells["interior"][:, is_flip_interior_facet] facet_gids = facets_cells_flip[0] adj_cells = facets_cells_flip[1:3] lids = facets_cells_flip[3:5] # 3 3 # A A # /|\ / \ # 3/ | \2 3/ \2 # / | \ / 1 \ # 0/ 0 | \1 ==> 0/_______\1 # \ | 1 / \ / # \ | / \ 0 / # 0\ | /1 0\ /1 # \|/ \ / # V V # 2 2 # v = np.array( [ self.cells("points")[adj_cells[0], lids[0]], self.cells("points")[adj_cells[1], lids[1]], self.cells("points")[adj_cells[0], (lids[0] + 1) % 3], self.cells("points")[adj_cells[0], (lids[0] + 2) % 3], ] ) # This must be computed before the points are reset equal_orientation = ( self.cells("points")[adj_cells[0], (lids[0] + 1) % 3] == self.cells("points")[adj_cells[1], (lids[1] + 2) % 3] ) # Set up new cells->points relationships. # Make sure that positive/negative area orientation is preserved. This is # especially important for signed area computations: In a mesh of all positive # areas, you don't want a negative area appear after a facet flip. self.cells("points")[adj_cells[0]] = v[[0, 2, 1]].T self.cells("points")[adj_cells[1]] = v[[0, 1, 3]].T # Set up new facet->points relationships. self.facets["points"][facet_gids] = np.sort(v[[0, 1]], axis=0).T # Set up new cells->facets relationships. previous_facets = self.cells("facets")[adj_cells].copy() # TODO need copy? # Do the neighboring cells have equal orientation (both point sets # clockwise/counterclockwise)? # # facets as in the above ascii art i0 = np.ones(equal_orientation.shape[0], dtype=int) i0[~equal_orientation] = 2 i1 = np.ones(equal_orientation.shape[0], dtype=int) i1[equal_orientation] = 2 e = [ np.choose((lids[0] + 2) % 3, previous_facets[0].T), np.choose((lids[1] + i0) % 3, previous_facets[1].T), np.choose((lids[1] + i1) % 3, previous_facets[1].T), np.choose((lids[0] + 1) % 3, previous_facets[0].T), ] # The order here is tightly coupled to self.cells("points") above self.cells("facets")[adj_cells[0]] = np.column_stack([e[1], facet_gids, e[0]]) self.cells("facets")[adj_cells[1]] = np.column_stack([e[2], e[3], facet_gids]) # update is_boundary_facet_local for k in range(3): self.is_boundary_facet_local[k, adj_cells] = self.is_boundary_facet[ self.cells("facets")[adj_cells, k] ] # Update the facet->cells relationship. We need to update facets_cells info for # all five facets. # First update the flipped facet; it's always interior. idx = self.facets_cells_idx[facet_gids] # cell ids self.facets_cells["interior"][1, idx] = adj_cells[0] self.facets_cells["interior"][2, idx] = adj_cells[1] # local facet ids; see self.cells("facets") self.facets_cells["interior"][3, idx] = 1 self.facets_cells["interior"][4, idx] = 2 # # Now handle the four surrounding facets conf = [ # The data is: # (1) facet id # (2) previous adjacent cell (adj_cells[0] or adj_cells[1]) # (3) new adjacent cell (adj_cells[0] or adj_cells[1]) # (4) local facet index in the new adjacent cell (e[0], 0, 0, 2), (e[1], 1, 0, 0), (e[2], 1, 1, 0), (e[3], 0, 1, 1), ] for facet, prev_adj_idx, new__adj_idx, new_local_facet_index in conf: prev_adj = adj_cells[prev_adj_idx] new__adj = adj_cells[new__adj_idx] idx = self.facets_cells_idx[facet] # boundary... is_boundary = self.is_boundary_facet[facet] idx_bou = idx[is_boundary] prev_adjacent = prev_adj[is_boundary] new__adjacent = new__adj[is_boundary] # The assertion just makes sure we're doing the right thing. It should never # trigger. assert np.all(prev_adjacent == self.facets_cells["boundary"][1, idx_bou]) self.facets_cells["boundary"][1, idx_bou] = new__adjacent self.facets_cells["boundary"][2, idx_bou] = new_local_facet_index # ...or interior? prev_adjacent = prev_adj[~is_boundary] new__adjacent = new__adj[~is_boundary] idx_int = idx[~is_boundary] # Interior facets have two neighboring cells in no particular order. Find # out if the adj_cell if the flipped facet comes first or second. is_first = prev_adjacent == self.facets_cells["interior"][1, idx_int] # The following is just a safety net. We could as well take ~is_first. is_secnd = prev_adjacent == self.facets_cells["interior"][2, idx_int] assert np.all(np.logical_xor(is_first, is_secnd)) # actually set the data idx_first = idx_int[is_first] self.facets_cells["interior"][1, idx_first] = new__adjacent[is_first] self.facets_cells["interior"][3, idx_first] = new_local_facet_index # likewise for when the cell appears in the second column idx_secnd = idx_int[~is_first] self.facets_cells["interior"][2, idx_secnd] = new__adjacent[~is_first] self.facets_cells["interior"][4, idx_secnd] = new_local_facet_index # Schedule the cell ids for data updates update_cell_ids = np.unique(adj_cells.T.flat) # Same for facet ids update_facet_gids = self.cells("facets")[update_cell_ids].flat facet_cell_idx = self.facets_cells_idx[update_facet_gids] update_interior_facet_ids = np.unique( facet_cell_idx[self.is_interior_facet[update_facet_gids]] ) self._update_cell_values(update_cell_ids, update_interior_facet_ids) def _update_cell_values(self, cell_ids, interior_facet_ids): """Updates all sorts of cell information for the given cell IDs.""" # update idx for j in range(1, self.n - 1): m = len(self.idx[j - 1]) r = np.arange(m) k = np.array([np.roll(r, -i) for i in range(1, m)]) self.idx[j][..., cell_ids] = self.idx[j - 1][..., cell_ids][k] # update most of the cell-associated values self._compute_cell_values(cell_ids) if self._signed_cell_volumes is not None: self._signed_cell_volumes[cell_ids] = self.compute_signed_cell_volumes( cell_ids ) if self._is_boundary_cell is not None: self._is_boundary_cell[cell_ids] = np.any( self.is_boundary_facet_local[:, cell_ids], axis=0 ) if self._cell_centroids is not None: self._cell_centroids[cell_ids] = self.compute_cell_centroids(cell_ids) # update the signed circumcenter distances for all interior_facet_ids if self._signed_circumcenter_distances is not None: self._signed_circumcenter_distances[interior_facet_ids] = 0.0 facet_gids = self.interior_facets[interior_facet_ids] adj_cells = self.facets_cells["interior"][1:3, interior_facet_ids].T for i in [0, 1]: is_facet = np.array( [ self.cells("facets")[adj_cells[:, i]][:, k] == facet_gids for k in range(3) ] ) # assert np.all(np.sum(is_facet, axis=0) == 1) for k in range(3): self._signed_circumcenter_distances[ interior_facet_ids[is_facet[k]] ] += self._circumcenter_facet_distances[ k, adj_cells[is_facet[k], i] ] # TODO update those values self._control_volumes = None self._ce_ratios = None self._cv_centroids = None meshplex-0.17.0/src/meshplex/_reader.py000066400000000000000000000021101417176457700200300ustar00rootroot00000000000000import meshio import numpy as np from ._mesh_tetra import MeshTetra from ._mesh_tri import MeshTri __all__ = ["read"] def _sanitize(points, cells): uvertices, uidx = np.unique(cells, return_inverse=True) cells = uidx.reshape(cells.shape) points = points[uvertices] return points, cells def from_meshio(mesh): """Transform from meshio to meshplex format. :param mesh: The meshio mesh object. :type mesh: meshio.Mesh :returns mesh{2,3}d: The mesh data. """ # make sure to include the used nodes only tetra = mesh.get_cells_type("tetra") if len(tetra) > 0: points, cells = _sanitize(mesh.points, tetra) return MeshTetra(points, cells) tri = mesh.get_cells_type("triangle") assert len(tri) > 0 points, cells = _sanitize(mesh.points, tri) return MeshTri(points, cells) def read(filename): """Reads an unstructured mesh into meshplex format. :param filenames: The files to read from. :type filenames: str :returns mesh{2,3}d: The mesh data. """ return from_meshio(meshio.read(filename)) meshplex-0.17.0/tests/000077500000000000000000000000001417176457700146115ustar00rootroot00000000000000meshplex-0.17.0/tests/.gitignore000066400000000000000000000000171417176457700165770ustar00rootroot00000000000000readme_test.py meshplex-0.17.0/tests/__init__.py000066400000000000000000000000771417176457700167260ustar00rootroot00000000000000# dummy init to allow for relative imports in the test/ folder meshplex-0.17.0/tests/helpers.py000066400000000000000000000037521417176457700166340ustar00rootroot00000000000000from math import fsum import numpy as np def is_near_equal(a, b, tol): return np.allclose(a, b, rtol=0.0, atol=tol) def run(mesh, volume, convol_norms, ce_ratio_norms, cellvol_norms, tol=1.0e-12): # Check cell volumes. total_cellvolume = fsum(mesh.cell_volumes) assert abs(volume - total_cellvolume) < tol * volume, total_cellvolume norm2 = np.linalg.norm(mesh.cell_volumes, ord=2) norm_inf = np.linalg.norm(mesh.cell_volumes, ord=np.Inf) assert is_near_equal(cellvol_norms, [norm2, norm_inf], tol), [norm2, norm_inf] # If everything is Delaunay and the boundary elements aren't flat, the volume of the # domain is given by # 1/n * edge_lengths * ce_ratios. # Unfortunately, this isn't always the case. # ``` # total_ce_ratio = \ # fsum(mesh.edge_lengths**2 * mesh.get_ce_ratios_per_edge() / dim) # self.assertAlmostEqual(volume, total_ce_ratio, delta=tol * volume) # ``` # Check ce_ratio norms. alpha2 = fsum((mesh.ce_ratios ** 2).flat) alpha_inf = max(abs(mesh.ce_ratios).flat) assert is_near_equal(ce_ratio_norms, [alpha2, alpha_inf], tol), [alpha2, alpha_inf] # Check the volume by summing over the absolute value of the control volumes. vol = fsum(mesh.control_volumes) assert abs(volume - vol) < tol * volume, vol # Check control volume norms. norm2 = np.linalg.norm(mesh.control_volumes, ord=2) norm_inf = np.linalg.norm(mesh.control_volumes, ord=np.Inf) assert is_near_equal(convol_norms, [norm2, norm_inf], tol), [norm2, norm_inf] def assert_norms(x, ref, tol): ref = np.asarray(ref) val = np.array( [ np.linalg.norm(x.flat, 1), np.linalg.norm(x.flat, 2), np.linalg.norm(x.flat, np.inf), ] ) assert np.all(np.abs(val - ref) < tol * np.abs(ref)), ( "Norms don't coincide.\n" f"Expected: [{ref[0]:.16e}, {ref[1]:.16e}, {ref[2]:.16e}]\n" f"Computed: [{val[0]:.16e}, {val[1]:.16e}, {val[2]:.16e}]\n" ) meshplex-0.17.0/tests/mesh_tri/000077500000000000000000000000001417176457700164235ustar00rootroot00000000000000meshplex-0.17.0/tests/mesh_tri/__init__.py000066400000000000000000000000771417176457700205400ustar00rootroot00000000000000# dummy init to allow for relative imports in the test/ folder meshplex-0.17.0/tests/mesh_tri/helpers.py000066400000000000000000000137651417176457700204530ustar00rootroot00000000000000import numpy as np import meshplex def assert_mesh_consistency(mesh0, tol=1.0e-14): assert np.all(np.logical_xor(mesh0.is_boundary_facet, mesh0.is_interior_facet)) bpts = np.array( [ mesh0.is_boundary_point, mesh0.is_interior_point, ~mesh0.is_point_used, ] ) assert np.all(np.sum(bpts, axis=0) == 1) # consistency check for facets_cells assert np.all(mesh0.is_boundary_facet[mesh0.facets_cells["boundary"][0]]) assert not np.any(mesh0.is_boundary_facet[mesh0.facets_cells["interior"][0]]) for edge_id, cell_id, local_edge_id in mesh0.facets_cells["boundary"].T: assert edge_id == mesh0.cells("facets")[cell_id][local_edge_id] for ( edge_id, cell_id0, cell_id1, local_edge_id0, local_edge_id1, ) in mesh0.facets_cells["interior"].T: assert edge_id == mesh0.cells("facets")[cell_id0][local_edge_id0] assert edge_id == mesh0.cells("facets")[cell_id1][local_edge_id1] # check consistency of facets_cells_idx with facets_cells for edge_id, idx in enumerate(mesh0.facets_cells_idx): if mesh0.is_boundary_facet[edge_id]: assert edge_id == mesh0.facets_cells["boundary"][0, idx] else: assert edge_id == mesh0.facets_cells["interior"][0, idx] # Assert facets_cells integrity for cell_gid, edge_gids in enumerate(mesh0.cells("facets")): for edge_gid in edge_gids: idx = mesh0.facets_cells_idx[edge_gid] if mesh0.is_boundary_facet[edge_gid]: assert cell_gid == mesh0.facets_cells["boundary"][1, idx] else: assert cell_gid in mesh0.facets_cells["interior"][1:3, idx] # make sure the edges are opposite of the points for cell_gid, (point_ids, edge_ids) in enumerate( zip(mesh0.cells("points"), mesh0.cells("facets")) ): for k in range(len(point_ids)): assert set(point_ids) == {*mesh0.edges["points"][edge_ids][k], point_ids[k]} # make sure the is_boundary_point/edge/cell is consistent ref_cells = np.any(mesh0.is_boundary_facet_local, axis=0) assert np.all(mesh0.is_boundary_cell == ref_cells) ref_points = np.zeros(len(mesh0.points), dtype=bool) ref_points[mesh0.idx[1][:, mesh0.is_boundary_facet_local]] = True assert np.all(mesh0.is_boundary_point == ref_points) assert len(mesh0.control_volumes) == len(mesh0.points) assert len(mesh0.control_volume_centroids) == len(mesh0.points) # TODO add more consistency checks # now check the numerical values # create a new mesh from the points and cells and compare mesh1 = meshplex.Mesh(mesh0.points, mesh0.cells("points")) # Can't add those tests since the facet order will be different. # TODO bring back # if mesh0.facets is None: # mesh0.create_facets() # if mesh1.facets is None: # mesh1.create_facets() # assert np.all(mesh0.boundary_facets == mesh1.boundary_facets) # assert np.all(mesh0.interior_facets == mesh1.interior_facets) # assert np.all(mesh0.is_boundary_facet == mesh1.is_boundary_facet) # assert np.all( # np.abs( # mesh0.signed_circumcenter_distances - mesh1.signed_circumcenter_distances # ) # < tol # ) assert np.all(mesh0.is_point_used == mesh1.is_point_used) assert np.all(mesh0.is_boundary_point == mesh1.is_boundary_point) assert np.all(mesh0.is_interior_point == mesh1.is_interior_point) assert np.all(mesh0.is_boundary_facet_local == mesh1.is_boundary_facet_local) assert np.all(mesh0.is_boundary_cell == mesh1.is_boundary_cell) assert np.all(np.abs(mesh0.ei_dot_ei - mesh1.ei_dot_ei) < tol) assert np.all(np.abs(mesh0.cell_volumes - mesh1.cell_volumes) < tol) assert np.all( np.abs(mesh0.circumcenter_facet_distances - mesh1.circumcenter_facet_distances) < tol ) assert np.all(np.abs(mesh0.signed_cell_volumes - mesh1.signed_cell_volumes) < tol) assert np.all(np.abs(mesh0.cell_centroids - mesh1.cell_centroids) < tol) assert np.all(np.abs(mesh0.cell_circumcenters - mesh1.cell_circumcenters) < tol) assert np.all(np.abs(mesh0.control_volumes - mesh1.control_volumes) < tol) assert np.all(np.abs(mesh0.ce_ratios - mesh1.ce_ratios) < tol) ipu = mesh0.is_point_used assert np.all( np.abs( mesh0.control_volume_centroids[ipu] - mesh1.control_volume_centroids[ipu] ) < tol ) def compute_all_entities(mesh): mesh.is_boundary_point mesh.is_interior_point mesh.is_boundary_facet_local mesh.is_boundary_facet mesh.is_boundary_cell mesh.cell_volumes mesh.ce_ratios mesh.signed_cell_volumes mesh.cell_centroids mesh.control_volumes mesh.create_facets() mesh.facets_cells mesh.facets_cells_idx mesh.boundary_facets mesh.interior_facets mesh.cell_circumcenters mesh.signed_circumcenter_distances mesh.control_volume_centroids assert mesh.edges is not None assert mesh.subdomains is not {} assert mesh._is_interior_point is not None assert mesh._is_boundary_point is not None assert mesh._is_boundary_facet_local is not None assert mesh._is_boundary_facet is not None assert mesh._is_boundary_cell is not None assert mesh._facets_cells is not None assert mesh._facets_cells_idx is not None assert mesh._boundary_facets is not None assert mesh._interior_facets is not None assert mesh._is_point_used is not None assert mesh._half_edge_coords is not None assert mesh._ei_dot_ei is not None assert mesh._volumes is not None assert mesh._ce_ratios is not None assert mesh._circumcenters is not None assert mesh._circumcenter_facet_distances is not None assert mesh._signed_circumcenter_distances is not None assert mesh._control_volumes is not None assert mesh._cell_partitions is not None assert mesh._cv_centroids is not None assert mesh._signed_cell_volumes is not None assert mesh._cell_centroids is not None meshplex-0.17.0/tests/mesh_tri/test_curl.py000066400000000000000000000015241417176457700210030ustar00rootroot00000000000000import pathlib import numpy as np import meshplex this_dir = pathlib.Path(__file__).resolve().parent def test_pacman(): mesh = meshplex.read(this_dir / ".." / "meshes" / "pacman.vtu") # mesh = meshplex.MeshTri(mesh.points[:, :2], mesh.cells("points")) # mesh.signed_cell_volumes # exit(1) # Create circular vector field 0.5 * (y, -x, 0) # which has curl (0, 0, 1). A = np.array([[-0.5 * coord[1], 0.5 * coord[0], 0.0] for coord in mesh.points]) # Compute the curl numerically. B = mesh.compute_ncurl(A) # mesh.write( # 'curl.vtu', # point_data={'A': A}, # cell_data={'B': B} # ) tol = 1.0e-14 for b in B: assert abs(b[0] - 0.0) < tol assert abs(b[1] - 0.0) < tol assert abs(b[2] - 1.0) < tol if __name__ == "__main__": test_pacman() meshplex-0.17.0/tests/mesh_tri/test_edge_flip.py000066400000000000000000000264211417176457700217570ustar00rootroot00000000000000import pathlib import meshio import numpy as np import pytest import meshplex from .helpers import assert_mesh_consistency, compute_all_entities this_dir = pathlib.Path(__file__).resolve().parent def test_flip_simple(): # 3 3 # A A # /|\ / \ # 1/ | \4 1/ 1 \4 # / | \ / \ # 0/ 0 3 \2 ==> 0/___3___\2 # \ | 1 / \ / # \ | / \ / # 0\ | /2 0\ 0 /2 # \|/ \ / # V V # 1 1 # points = np.array([[-0.1, 0.0], [0.0, -1.0], [0.1, 0.0], [0.0, 1.1]]) cells = np.array([[0, 1, 3], [1, 2, 3]]) mesh = meshplex.MeshTri(points, cells) mesh.create_facets() assert not mesh.is_delaunay assert mesh.num_delaunay_violations == 1 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [1, 3], [2, 3]] ) assert np.array_equal(mesh.cells("edges"), [[3, 1, 0], [4, 3, 2]]) assert_mesh_consistency(mesh) # mesh.show() num_flips = mesh.flip_until_delaunay() assert num_flips == 1 assert_mesh_consistency(mesh) assert mesh.num_delaunay_violations == 0 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [0, 2], [2, 3]] ) assert np.array_equal(mesh.cells("points"), [[0, 1, 2], [0, 2, 3]]) assert np.array_equal(mesh.cells("edges"), [[2, 3, 0], [4, 1, 3]]) def test_flip_simple_negative_orientation(): # 3 3 # A A # /|\ / \ # 1/ | \4 1/ 1 \4 # / | \ / \ # 0/ 0 3 \2 ==> 0/___3___\2 # \ | 1 / \ / # \ | / \ / # 0\ | /2 0\ 0 /2 # \|/ \ / # V V # 1 1 # points = np.array([[-0.1, 0.0], [0.0, -1.0], [0.1, 0.0], [0.0, 1.1]]) cells = np.array([[0, 3, 1], [1, 3, 2]]) mesh = meshplex.MeshTri(points, cells) mesh.create_facets() assert mesh.num_delaunay_violations == 1 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [1, 3], [2, 3]] ) assert np.array_equal(mesh.cells("edges"), [[3, 0, 1], [4, 2, 3]]) assert_mesh_consistency(mesh) # mesh.show() mesh.flip_until_delaunay() assert_mesh_consistency(mesh) assert mesh.num_delaunay_violations == 0 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [0, 2], [2, 3]] ) assert np.array_equal(mesh.cells("points"), [[0, 3, 2], [0, 2, 1]]) assert np.array_equal(mesh.cells("edges"), [[4, 3, 1], [2, 0, 3]]) def test_flip_simple_opposite_orientation(): # 3 3 # A A # /|\ / \ # 1/ | \4 1/ 1 \4 # / | \ / \ # 0/ 0 3 \2 ==> 0/___3___\2 # \ | 1 / \ / # \ | / \ / # 0\ | /2 0\ 0 /2 # \|/ \ / # V V # 1 1 # points = np.array([[-0.1, 0.0], [0.0, -1.0], [0.1, 0.0], [0.0, 1.1]]) cells = np.array([[0, 1, 3], [1, 3, 2]]) mesh = meshplex.MeshTri(points, cells) mesh.create_facets() assert mesh.num_delaunay_violations == 1 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [1, 3], [2, 3]] ) assert np.array_equal(mesh.cells("edges"), [[3, 1, 0], [4, 2, 3]]) assert_mesh_consistency(mesh) # mesh.show() mesh.flip_until_delaunay() assert_mesh_consistency(mesh) assert mesh.num_delaunay_violations == 0 assert np.array_equal( mesh.edges["points"], [[0, 1], [0, 3], [1, 2], [0, 2], [2, 3]] ) assert np.array_equal(mesh.cells("points"), [[0, 1, 2], [0, 2, 3]]) assert np.array_equal(mesh.cells("edges"), [[2, 3, 0], [4, 1, 3]]) def test_flip_delaunay_near_boundary(): points = np.array([[0.0, +0.0], [0.5, -0.1], [1.0, +0.0], [0.5, +0.1]]) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.MeshTri(points, cells) mesh.create_facets() assert mesh.num_delaunay_violations == 1 assert np.array_equal(mesh.cells("points"), [[0, 1, 2], [0, 2, 3]]) assert np.array_equal(mesh.cells("edges"), [[3, 1, 0], [4, 2, 1]]) mesh.flip_until_delaunay() assert_mesh_consistency(mesh) assert mesh.num_delaunay_violations == 0 assert np.array_equal(mesh.cells("points"), [[1, 2, 3], [1, 3, 0]]) assert np.array_equal(mesh.cells("edges"), [[4, 1, 3], [2, 0, 1]]) def test_flip_same_edge_twice(): points = np.array([[0.0, +0.0], [0.5, -0.1], [1.0, +0.0], [0.5, +0.1]]) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.MeshTri(points, cells) assert mesh.num_delaunay_violations == 1 mesh.flip_until_delaunay() assert mesh.num_delaunay_violations == 0 mesh.show( mark_cells=mesh.is_boundary_cell, show_point_numbers=True, show_edge_numbers=True, show_cell_numbers=True, ) assert_mesh_consistency(mesh) new_points = np.array([[0.0, +0.0], [0.1, -0.5], [0.2, +0.0], [0.1, +0.5]]) mesh.points = new_points assert mesh.num_delaunay_violations == 1 mesh.flip_until_delaunay() assert mesh.num_delaunay_violations == 0 mesh.show() # mesh.plot() def test_flip_two_edges(): alpha = np.array([1.0, 3.0, 5.0, 7.0, 9.0, 11.0]) / 6.0 * np.pi # Make the mesh slightly asymmetric to get the same flips on every architecture; see # . R = [0.95, 1.0, 0.9, 1.0, 1.2, 1.0] points = np.array([[r * np.cos(a), r * np.sin(a), 0.0] for a, r in zip(alpha, R)]) cells = np.array([[1, 3, 5], [0, 1, 5], [1, 2, 3], [3, 4, 5]]) mesh = meshplex.MeshTri(points, cells) assert mesh.num_delaunay_violations == 2 mesh.flip_until_delaunay() assert mesh.num_delaunay_violations == 0 mesh.show(show_point_numbers=True) assert np.array_equal( mesh.cells("points"), [[2, 5, 0], [2, 0, 1], [5, 2, 3], [3, 4, 5]] ) def test_flip_delaunay_near_boundary_preserve_boundary_count(): # This test is to make sure meshplex preserves the boundary point count. points = np.array( [ [+0.0, +0.0], [+0.5, -0.5], [+0.5, +0.5], [+0.0, +0.6], [-0.5, +0.5], [-0.5, -0.5], ] ) cells = np.array([[0, 1, 2], [0, 2, 4], [0, 4, 5], [0, 5, 1], [2, 3, 4]]) mesh = meshplex.MeshTri(points, cells) mesh.create_facets() assert mesh.num_delaunay_violations == 1 is_boundary_point_ref = [False, True, True, True, True, True] assert np.array_equal(mesh.is_boundary_point, is_boundary_point_ref) mesh.flip_until_delaunay() assert np.array_equal(mesh.is_boundary_point, is_boundary_point_ref) def test_flip_orientation(): points = np.array([[0.0, +0.0], [0.5, -0.1], [1.0, +0.0], [0.5, +0.1]]) # preserve positive orientation cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.MeshTri(points, cells) assert np.all(mesh.signed_cell_volumes > 0.0) mesh.flip_until_delaunay() assert np.all(mesh.signed_cell_volumes > 0.0) # also preserve negative orientation cells = np.array([[0, 2, 1], [0, 3, 2]]) mesh = meshplex.MeshTri(points, cells) assert np.all(mesh.signed_cell_volumes < 0.0) mesh.flip_until_delaunay() assert np.all(mesh.signed_cell_volumes < 0.0) def test_flip_infinite(): """In rare cases, it can happen that the ce-ratio of an edge is negative (up to machine precision, -2.13e-15 or something like that), an edge flip is done, and the ce-ratio of the resulting edge is again negative. The flip_until_delaunay() method would continue indefinitely. This test replicates such an edge case.""" a = 3.9375644347017862e02 points = np.array([[205.0, a], [185.0, a], [330.0, 380.0], [60.0, 380.0]]) cells = [[0, 1, 2], [1, 2, 3]] mesh = meshplex.MeshTri(points, cells) num_flips = mesh.flip_until_delaunay(tol=1.0e-13) assert num_flips == 0 def test_flip_interior_to_boundary(): # __________ __________ # |\__ A |\__ A # | \__ /|\ | \__ / \ # | \/ | \ ==> | \/___\ # | __/\ | / | __/\ / # | __/ \|/ | __/ \ / # |/________V |/________V # points = np.array( [[0.0, 0.0], [1.0, 0.0], [1.1, 0.5], [1.0, 1.0], [0.0, 1.0], [0.9, 0.5]] ) cells = np.array([[0, 1, 5], [1, 3, 5], [1, 2, 3], [3, 4, 5], [0, 5, 4]]) mesh = meshplex.MeshTri(points, cells) compute_all_entities(mesh) # mesh.show(mark_cells=mesh.is_boundary_cell) mesh.flip_until_delaunay() assert_mesh_consistency(mesh) # mesh.show(mark_cells=mesh.is_boundary_cell) assert np.all(mesh.is_boundary_cell) def test_flip_delaunay(): rng = np.random.default_rng(123) mesh0 = meshio.read(this_dir / ".." / "meshes" / "pacman.vtu") mesh0.points[:, :2] += 1.0e-1 * rng.random(mesh0.points[:, :2].shape) mesh0 = meshplex.MeshTri(mesh0.points[:, :2], mesh0.get_cells_type("triangle")) compute_all_entities(mesh0) assert np.all(mesh0.signed_cell_volumes > 0) assert mesh0.num_delaunay_violations == 5 mesh0.flip_until_delaunay() assert mesh0.num_delaunay_violations == 0 assert_mesh_consistency(mesh0) # mesh0.show(mark_cells=mesh0.is_boundary_cell) # We don't need to check for exact equality with a replicated mesh. The order of the # edges will be different, for example. Just make sure the mesh is consistent. # mesh1 = meshplex.MeshTri(mesh0.points.copy(), mesh0.cells("points").copy()) # mesh1.create_facets() # assert_mesh_equality(mesh0, mesh1) def test_flip_into_existing_edge(): """For surface meshes, flips can lead to duplicate cells. For context, see . """ points = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.3, 0.3], [0.5, -0.3, 0.3], ] ) cells = np.array( [ [1, 2, 0], [2, 3, 0], [3, 1, 0], ] ) mesh = meshplex.MeshTri(points, cells) with pytest.warns(UserWarning): n = mesh.flip_until_delaunay() # no flips performed assert n == 0 def test_doubled_cell(): # Two congruent cells. One can think of it as a deflated, coarse ball. points = np.array( [ [0.0, 0.0], [1.0, 0.0], [0.5, 0.4], ] ) cells = np.array([[0, 1, 2], [0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) num_flips = mesh.flip_until_delaunay() assert num_flips == 1 ref = [[2, 0, 2], [2, 2, 1]] assert np.all(mesh.cells("points") == ref) def test_negative_after_flip(): points = [[0.0, 0.0], [3.0, 0.0], [1.14960653, 0.03], [1.85039347, 0.03]] cells = [ [0, 3, 2], [0, 1, 3], ] mesh0 = meshplex.MeshTri(points, cells) with pytest.warns(UserWarning): mesh0.flip_until_delaunay() meshplex-0.17.0/tests/mesh_tri/test_genus.py000066400000000000000000000011661417176457700211610ustar00rootroot00000000000000import meshzoo import pytest import meshplex def test_euler_characteristic(): points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]] cells = [[0, 1, 2]] mesh = meshplex.MeshTri(points, cells) assert mesh.euler_characteristic == 1 assert mesh.genus == 0 points, cells = meshzoo.icosa_sphere(5) mesh = meshplex.MeshTri(points, cells) assert mesh.euler_characteristic == 2 assert mesh.genus == 0 points, cells = meshzoo.moebius(num_twists=1, nl=21, nw=6) mesh = meshplex.MeshTri(points, cells) assert mesh.euler_characteristic == 0 with pytest.raises(RuntimeError): mesh.genus meshplex-0.17.0/tests/mesh_tri/test_gradient.py000066400000000000000000000023461417176457700216360ustar00rootroot00000000000000# from helpers import download_mesh # import meshplex # # import numpy as np # import unittest # # # class GradientTest(unittest.TestCase): # # def setUp(self): # return # # def _run_test(self, mesh): # num_nodes = len(mesh.points) # # Create function 2*x + 3*y. # a_x = 7.0 # a_y = 3.0 # a0 = 1.0 # u = a_x * mesh.points[:, 0] + \ # a_y * mesh.points[:, 1] + \ # a0 * np.ones(num_nodes) # # Get the gradient analytically. # sol = np.empty((num_nodes, 2)) # sol[:, 0] = a_x # sol[:, 1] = a_y # # Compute the gradient numerically. # grad_u = mesh.compute_gradient(u) # # tol = 1.0e-13 # for k in range(num_nodes): # self.assertAlmostEqual(grad_u[k][0], sol[k][0], delta=tol) # self.assertAlmostEqual(grad_u[k][1], sol[k][1], delta=tol) # return # # def test_pacman(self): # filename = download_mesh( # 'pacman.vtk', # '2da8ff96537f844a95a83abb48471b6a' # ) # mesh, _, _, _ = meshplex.read(filename) # self._run_test(mesh) # return # # # if __name__ == '__main__': # unittest.main() meshplex-0.17.0/tests/mesh_tri/test_mesh_tri.py000066400000000000000000000537331417176457700216610ustar00rootroot00000000000000import os import pathlib import platform import tempfile import meshio import meshzoo import numpy as np import pytest import meshplex from ..helpers import assert_norms, is_near_equal, run this_dir = pathlib.Path(__file__).resolve().parent def _compute_polygon_area(pts): # shoelace formula return ( np.abs( np.dot(pts[0], np.roll(pts[1], -1)) - np.dot(np.roll(pts[0], -1), pts[1]) ) / 2 ) # The dtype restriction is because of np.bincount. # See and # . cell_dtypes = [] cell_dtypes += [ np.int32, ] if platform.architecture()[0] == "64bit": cell_dtypes += [ np.uint32, # when numpy is fixed, this can go to all arches np.int64, # np.uint64 # depends on the numpy fix ] @pytest.mark.parametrize("cells_dtype", cell_dtypes) def test_unit_triangle(cells_dtype): points = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]) cells = np.array([[0, 1, 2]], dtype=cells_dtype) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-14 # ce_ratios assert is_near_equal(mesh.ce_ratios.T, [0.0, 0.5, 0.5], tol) # control volumes assert is_near_equal(mesh.control_volumes, [0.25, 0.125, 0.125], tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5], tol) # circumcenters assert is_near_equal(mesh.cell_circumcenters, [0.5, 0.5], tol) # centroids assert is_near_equal(mesh.cell_centroids, [1.0 / 3.0, 1.0 / 3.0], tol) assert is_near_equal(mesh.cell_barycenters, [1.0 / 3.0, 1.0 / 3.0], tol) # control volume centroids print(mesh.control_volume_centroids) assert is_near_equal( mesh.control_volume_centroids, [[0.25, 0.25], [2.0 / 3.0, 1.0 / 6.0], [1.0 / 6.0, 2.0 / 3.0]], tol, ) # incenter assert is_near_equal( mesh.cell_incenters, [[(2 - np.sqrt(2)) / 2, (2 - np.sqrt(2)) / 2]], tol ) # circumcenter assert is_near_equal(mesh.cell_circumcenters, [[0.5, 0.5]], tol) assert mesh.num_delaunay_violations == 0 assert mesh.genus == 0 mesh.get_cell_mask() mesh.get_edge_mask() mesh.get_vertex_mask() # dummy subdomain marker test class Subdomain: is_boundary_only = False def is_inside(self, X): return np.ones(X.shape[1:], dtype=bool) cell_mask = mesh.get_cell_mask(Subdomain()) assert np.sum(cell_mask) == 1 # save _, filename = tempfile.mkstemp(suffix=".png") mesh.save(filename) os.remove(filename) _, filename = tempfile.mkstemp(suffix=".vtk") mesh.save(filename) os.remove(filename) def test_regular_tri_additional_points(): points = np.array( [ [0.0, 3.4, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [3.3, 4.4, 0.0], ] ) cells = np.array([[1, 2, 3]]) mesh = meshplex.MeshTri(points, cells) assert np.array_equal(mesh.is_point_used, [False, True, True, True, False]) assert np.array_equal(mesh.is_boundary_point, [False, True, True, True, False]) assert np.array_equal(mesh.is_interior_point, [False, False, False, False, False]) tol = 1.0e-14 assert np.array_equal(mesh.cells("points"), [[1, 2, 3]]) mesh.create_facets() assert np.array_equal(mesh.cells("edges"), [[2, 1, 0]]) assert np.array_equal(mesh.edges["points"], [[1, 2], [1, 3], [2, 3]]) # ce_ratios assert is_near_equal(mesh.ce_ratios.T, [0.0, 0.5, 0.5], tol) # control volumes assert is_near_equal(mesh.control_volumes, [0.0, 0.25, 0.125, 0.125, 0.0], tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5], tol) # circumcenters assert is_near_equal(mesh.cell_circumcenters, [0.5, 0.5, 0.0], tol) # Centroids. # Nans appear here as the some points aren't part of any cell and hence have no # control volume. cvc = mesh.control_volume_centroids assert np.all(np.isnan(cvc[0])) assert np.all(np.isnan(cvc[4])) assert is_near_equal( cvc[1:4], [[0.25, 0.25, 0.0], [2.0 / 3.0, 1.0 / 6.0, 0.0], [1.0 / 6.0, 2.0 / 3.0, 0.0]], tol, ) assert mesh.num_delaunay_violations == 0 def test_regular_tri_order(): points = np.array([[0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) assert all((mesh.cells("points") == [0, 1, 2]).flat) tol = 1.0e-14 # ce_ratios assert is_near_equal(mesh.ce_ratios.T, [0.5, 0.0, 0.5], tol) # control volumes assert is_near_equal(mesh.control_volumes, [0.125, 0.25, 0.125], tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5], tol) # circumcenters assert is_near_equal(mesh.cell_circumcenters, [0.5, 0.5, 0.0], tol) # centroids assert is_near_equal( mesh.control_volume_centroids, [[1.0 / 6.0, 2.0 / 3.0, 0.0], [0.25, 0.25, 0.0], [2.0 / 3.0, 1.0 / 6.0, 0.0]], tol, ) assert mesh.num_delaunay_violations == 0 @pytest.mark.parametrize("a", [1.0, 2.0]) def test_regular_tri2(a): points = ( np.array( [ [-0.5, -0.5 * np.sqrt(3.0), 0], [-0.5, +0.5 * np.sqrt(3.0), 0], [1, 0, 0], ] ) / np.sqrt(3) * a ) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-14 # ce_ratios val = 0.5 / np.sqrt(3.0) assert is_near_equal(mesh.ce_ratios, [val, val, val], tol) # control volumes vol = np.sqrt(3.0) / 4 * a ** 2 assert is_near_equal(mesh.control_volumes, [vol / 3.0, vol / 3.0, vol / 3.0], tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [vol], tol) # circumcenters assert is_near_equal(mesh.cell_circumcenters, [0.0, 0.0, 0.0], tol) # def test_degenerate_small0(): # h = 1.0e-3 # points = np.array([ # [0, 0, 0], # [1, 0, 0], # [0.5, h, 0.0], # ]) # cells = np.array([[0, 1, 2]]) # mesh = meshplex.MeshTri( # points, # cells, # allow_negative_volumes=True # ) # tol = 1.0e-14 # # ce_ratios # alpha = 0.5 * h - 1.0 / (8*h) # beta = 1.0 / (4*h) # assertAlmostEqual(mesh.get_ce_ratios_per_edge()[0], alpha, delta=tol) # self.assertAlmostEqual(mesh.get_ce_ratios_per_edge()[1], beta, delta=tol) # self.assertAlmostEqual(mesh.get_ce_ratios_per_edge()[2], beta, delta=tol) # # control volumes # alpha1 = 0.0625 * (3*h - 1.0/(4*h)) # alpha2 = 0.125 * (h + 1.0 / (4*h)) # assert is_near_equal( # mesh.get_control_volumes(), # [alpha1, alpha1, alpha2], # tol # ) # # cell volumes # self.assertAlmostEqual(mesh.cell_volumes[0], 0.5 * h, delta=tol) # # surface areas # edge_length = np.sqrt(0.5**2 + h**2) # # circumference = 1.0 + 2 * edge_length # alpha = 0.5 * (1.0 + edge_length) # self.assertAlmostEqual(mesh.surface_areas[0], alpha, delta=tol) # self.assertAlmostEqual(mesh.surface_areas[1], alpha, delta=tol) # self.assertAlmostEqual(mesh.surface_areas[2], edge_length, delta=tol) # # centroids # alpha = -41.666666669333345 # beta = 0.58333199998399976 # self.assertAlmostEqual( # mesh.centroids[0][0], # 0.416668000016, # delta=tol # ) # self.assertAlmostEqual(mesh.centroids[0][1], alpha, delta=tol) # self.assertAlmostEqual(mesh.centroids[0][2], 0.0, delta=tol) # self.assertAlmostEqual(mesh.centroids[1][0], beta, delta=tol) # self.assertAlmostEqual(mesh.centroids[1][1], alpha, delta=tol) # self.assertAlmostEqual(mesh.centroids[1][2], 0.0, delta=tol) # self.assertAlmostEqual(mesh.centroids[2][0], 0.5, delta=tol) # self.assertAlmostEqual(mesh.centroids[2][1], -41.666, delta=tol) # self.assertAlmostEqual(mesh.centroids[2][2], 0.0, delta=tol) # self.assertEqual(mesh.num_delaunay_violations, 0) @pytest.mark.parametrize( "h", # TODO [1.0e0, 1.0e-1] [1.0e0], ) def test_degenerate_small0b(h): points = np.array([[0, 0, 0], [1, 0, 0], [0.5, h, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells, sort_cells=True) # test sort_cells, too tol = 1.0e-14 # edge lengths el = np.sqrt(0.5 ** 2 + h ** 2) assert is_near_equal(mesh.edge_lengths.T, [el, el, 1.0], tol) # ce_ratios ce0 = 0.5 / h * (h ** 2 - 0.25) ce12 = 0.25 / h assert is_near_equal(mesh.ce_ratios.T, [ce12, ce12, ce0], tol) # control volumes cv12 = 0.25 * (1.0 ** 2 * ce0 + (0.25 + h ** 2) * ce12) cv0 = 0.5 * (0.25 + h ** 2) * ce12 assert is_near_equal(mesh.control_volumes, [cv12, cv12, cv0], tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5 * h], tol) # circumcenters assert is_near_equal(mesh.cell_circumcenters, [0.5, 0.375, 0.0], tol) assert mesh.num_delaunay_violations == 0 # # TODO parametrize with flat boundary correction # def test_degenerate_small0b_fcc(): # h = 1.0e-3 # points = np.array([[0, 0, 0], [1, 0, 0], [0.5, h, 0.0]]) # cells = np.array([[0, 1, 2]]) # mesh = meshplex.MeshTri(points, cells) # # tol = 1.0e-14 # # # edge lengths # el = np.sqrt(0.5 ** 2 + h ** 2) # assert is_near_equal(mesh.edge_lengths.T, [el, el, 1.0], tol) # # # ce_ratios # ce = h # assert is_near_equal(mesh.ce_ratios.T, [ce, ce, 0.0], tol) # # # control volumes # cv = ce * el # alpha = 0.25 * el * cv # beta = 0.5 * h - 2 * alpha # assert is_near_equal(mesh.control_volumes, [alpha, alpha, beta], tol) # # # cell volumes # assert is_near_equal(mesh.cell_volumes, [0.5 * h], tol) # # # surface areas # g = np.sqrt((0.5 * el) ** 2 + (ce * el) ** 2) # alpha = 0.5 * el + g # beta = el + (1.0 - 2 * g) # assert is_near_equal(mesh.surface_areas, [alpha, alpha, beta], tol) # # # centroids # centroids = mesh.control_volume_centroids # alpha = 1.0 / 6000.0 # gamma = 0.00038888918518558031 # assert is_near_equal(centroids[0], [0.166667, alpha, 0.0], tol) # assert is_near_equal(centroids[1], [0.833333, alpha, 0.0], tol) # assert is_near_equal(centroids[2], [0.5, gamma, 0.0], tol) # assert mesh.num_delaunay_violations == 0 @pytest.mark.parametrize("h, a", [(1.0e-3, 0.3)]) def test_degenerate_small1(h, a): points = np.array([[0, 0, 0], [1, 0, 0], [a, h, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-12 # edge lengths el0 = np.sqrt((1.0 - a) ** 2 + h ** 2) el1 = np.sqrt(a ** 2 + h ** 2) el2 = 1.0 assert is_near_equal(mesh.edge_lengths.T, [[el0, el1, el2]], tol) # ce_ratios ce0 = 0.5 * a / h ce1 = 0.5 * (1 - a) / h ce2 = 0.5 * (h - (1 - a) * a / h) / el2 assert is_near_equal(mesh.ce_ratios[:, 0], [ce0, ce1, ce2], 1.0e-8) # # control volumes # cv1 = ce1 * el1 # alpha1 = 0.25 * el1 * cv1 # cv2 = ce2 * el2 # alpha2 = 0.25 * el2 * cv2 # beta = 0.5 * h - (alpha1 + alpha2) # assert is_near_equal(mesh.control_volumes, [alpha1, alpha2, beta], tol) # assert abs(sum(mesh.control_volumes) - 0.5 * h) < tol # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5 * h], tol) # # surface areas # b1 = np.sqrt((0.5 * el1) ** 2 + cv1 ** 2) # alpha0 = b1 + 0.5 * el1 # b2 = np.sqrt((0.5 * el2) ** 2 + cv2 ** 2) # alpha1 = b2 + 0.5 * el2 # total = 1.0 + el1 + el2 # alpha2 = total - alpha0 - alpha1 # assert is_near_equal(mesh.surface_areas, [alpha0, alpha1, alpha2], tol) assert mesh.num_delaunay_violations == 0 @pytest.mark.parametrize("h", [1.0e-2]) def test_degenerate_small2(h): points = np.array([[0, 0, 0], [1, 0, 0], [0.5, h, 0.0], [0.5, -h, 0.0]]) cells = np.array([[0, 1, 2], [0, 1, 3]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-11 # ce_ratios alpha = h - 1.0 / (4 * h) beta = 1.0 / (4 * h) assert is_near_equal(mesh.signed_circumcenter_distances, [alpha], tol) alpha2 = (h - 1.0 / (4 * h)) / 2 assert is_near_equal( mesh.ce_ratios, [[beta, beta], [beta, beta], [alpha2, alpha2]], tol ) # control volumes alpha1 = 0.125 * (3 * h - 1.0 / (4 * h)) alpha2 = 0.125 * (h + 1.0 / (4 * h)) assert is_near_equal(mesh.control_volumes, [alpha1, alpha1, alpha2, alpha2], tol) # circumcenters assert is_near_equal( mesh.cell_circumcenters, [[0.5, -12.495, 0.0], [0.5, +12.495, 0.0]], tol ) # cell volumes assert is_near_equal(mesh.cell_volumes, [0.5 * h, 0.5 * h], tol) assert mesh.num_delaunay_violations == 1 def test_rectanglesmall(): points = np.array( [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 1.0, 0.0], [0.0, 1.0, 0.0]] ) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-14 assert is_near_equal(mesh.signed_circumcenter_distances, [0.0], tol) assert is_near_equal(mesh.ce_ratios, [[5.0, 0.05], [0.0, 5.0], [0.05, 0.0]], tol) assert is_near_equal(mesh.control_volumes, [2.5, 2.5, 2.5, 2.5], tol) assert is_near_equal(mesh.cell_volumes, [5.0, 5.0], tol) assert mesh.num_delaunay_violations == 0 def test_pacman(): mesh = meshplex.read(this_dir / ".." / "meshes" / "pacman.vtu") run( mesh, 54.312974717523744, [1.9213504740523146, 0.07954185111555329], [403.5307055719196, 0.5512267577002408], [1.3816992621175055, 0.0443755870238773], ) assert mesh.num_delaunay_violations == 0 def test_shell(): points = np.array( [ [+0.0, +0.0, +1.0], [+1.0, +0.0, +0.0], [+0.0, +1.0, +0.0], [-1.0, +0.0, +0.0], [+0.0, -1.0, +0.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 1, 4]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-14 ce_ratios = 0.5 / np.sqrt(3.0) * np.ones((4, 3)) assert is_near_equal(mesh.ce_ratios.T, ce_ratios, tol) cv = np.array([2.0, 1.0, 1.0, 1.0, 1.0]) / np.sqrt(3.0) assert is_near_equal(mesh.control_volumes, cv, tol) cell_vols = np.sqrt(3.0) / 2.0 * np.ones(4) assert is_near_equal(mesh.cell_volumes, cell_vols, tol) assert mesh.num_delaunay_violations == 0 def test_sphere(): points, cells = meshzoo.icosa_sphere(5) mesh = meshplex.Mesh(points, cells) run( mesh, 12.413437988936916, [0.7864027242108207, 0.05524648209283611], [128.70115197256447, 0.3605511489598192], [0.5593675314375034, 0.02963260270642986], ) def test_update_point_coordinates(): mesh = meshio.read(this_dir / ".." / "meshes" / "pacman.vtu") assert np.all(np.abs(mesh.points[:, 2]) < 1.0e-15) mesh1 = meshplex.MeshTri(mesh.points, mesh.get_cells_type("triangle")) np.random.seed(123) X2 = mesh.points + 1.0e-2 * np.random.rand(*mesh.points.shape) mesh2 = meshplex.MeshTri(X2, mesh.get_cells_type("triangle")) mesh1.points = X2 tol = 1.0e-12 assert is_near_equal(mesh1.cell_volumes, mesh2.cell_volumes, tol) def test_inradius(): # 3-4-5 triangle points = np.array([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-15 assert is_near_equal(mesh.cell_inradius, [1.0], tol) # 30-60-90 triangle a = 1.0 points = np.array( [[0.0, 0.0, 0.0], [a / 2, 0.0, 0.0], [0.0, a / 2 * np.sqrt(3.0), 0.0]] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) assert is_near_equal(mesh.cell_inradius, [a / 4 * (np.sqrt(3) - 1)], tol) def test_circumradius(): # 3-4-5 triangle points = np.array([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-15 assert is_near_equal(mesh.cell_circumradius, [2.5], tol) # 30-60-90 triangle a = 1.0 points = np.array( [[0.0, 0.0, 0.0], [a / 2, 0.0, 0.0], [0.0, a / 2 * np.sqrt(3.0), 0.0]] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) assert is_near_equal(mesh.cell_circumradius, [a / 2], tol) def test_quality(): # 3-4-5 triangle points = np.array([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-15 q = mesh.q_radius_ratio assert is_near_equal(q, 2 * mesh.cell_inradius / mesh.cell_circumradius, tol) # 30-60-90 triangle a = 1.0 points = np.array( [[0.0, 0.0, 0.0], [a / 2, 0.0, 0.0], [0.0, a / 2 * np.sqrt(3.0), 0.0]] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) q = mesh.q_radius_ratio assert is_near_equal(q, 2 * mesh.cell_inradius / mesh.cell_circumradius, tol) def test_angles(): # 3-4-5 triangle points = np.array([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) tol = 1.0e-14 assert is_near_equal( mesh.angles, [[np.pi / 2], [np.arcsin(4.0 / 5.0)], [np.arcsin(3.0 / 5.0)]], tol, ) # 30-60-90 triangle a = 1.0 points = np.array( [[0.0, 0.0, 0.0], [a / 2, 0.0, 0.0], [0.0, a / 2 * np.sqrt(3.0), 0.0]] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) ic = mesh.angles / np.pi * 180 assert is_near_equal(ic, [[90], [60], [30]], tol) def test_flat_boundary(): # # 3___________2 # |\_ 2 _/| # | \_ _/ | # | 3 \4/ 1 | # | _/ \_ | # | _/ \_ | # |/ 0 \| # 0-----------1 # x = 0.4 y = 0.5 X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [x, y, 0.0], ] ) cells = np.array([[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]]) mesh = meshplex.MeshTri(X, cells) # Inspect the covolumes in left cell. edge_length = np.sqrt(x ** 2 + y ** 2) ref = np.array([edge_length, edge_length, 1.0]) assert np.all(np.abs(mesh.edge_lengths[:, 3] - ref) < 1.0e-12) # alpha = 0.5 / x * y * np.sqrt(y ** 2 + x ** 2) beta = 0.5 / x * (x ** 2 - y ** 2) ref = [alpha, alpha, beta] covolumes = mesh.ce_ratios[:, 3] * mesh.edge_lengths[:, 3] assert np.all(np.abs(covolumes - ref) < 1.0e-12) # beta = np.sqrt(alpha ** 2 + 0.2 ** 2 + 0.25 ** 2) control_volume_corners = np.array( [ mesh.cell_circumcenters[0][:2], mesh.cell_circumcenters[1][:2], mesh.cell_circumcenters[2][:2], mesh.cell_circumcenters[3][:2], ] ) ref_area = _compute_polygon_area(control_volume_corners.T) assert np.abs(mesh.control_volumes[4] - ref_area) < 1.0e-12 cv = np.zeros(X.shape[0]) for edges, ce_ratios in zip(mesh.idx[1].T, mesh.ce_ratios.T): for i, ce in zip(edges, ce_ratios): ei = mesh.points[i[1]] - mesh.points[i[0]] cv[i] += 0.25 * ce * np.dot(ei, ei) assert np.all(np.abs(cv - mesh.control_volumes) < 1.0e-12 * cv) def test_set_points(): points = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.MeshTri(points, cells) mesh.set_points([0.1, 0.1], [0]) ref = mesh.cell_volumes.copy() mesh2 = meshplex.MeshTri(mesh.points, mesh.cells("points")) assert np.all(np.abs(ref - mesh2.cell_volumes) < 1.0e-10) def test_reference_vals_pacman(): mesh = meshplex.read(this_dir / ".." / "meshes" / "pacman.vtu") mesh = meshplex.MeshTri(mesh.points[:, :2], mesh.cells("points")) assert_norms( mesh.points, [3.0544932927920363e03, 8.8106015937625088e01, 4.2500000000000000e00], 1.0e-15, ) assert_norms( mesh.half_edge_coords, [1.6674048509514694e03, 1.9463913181988705e01, 3.4718650853971766e-01], 1.0e-15, ) assert_norms( mesh.ei_dot_ei, [3.7884391635599366e02, 5.5826366101867908e00, 1.2739502744897091e-01], 1.0e-15, ) assert_norms( mesh.cell_partitions, [5.4312974717523744e01, 5.6678942198355931e-01, 8.5844237283573006e-03], 1.0e-12, ) assert_norms( mesh.cell_centroids, [5.5718766084264298e03, 1.1728553160741399e02, 4.1694621840309081e00], 1.0e-15, ) assert_norms( mesh.edge_lengths, [1.3250455158127431e03, 1.9463913181988705e01, 3.5692440018716975e-01], 1.0e-15, ) assert_norms( mesh.cell_volumes, [5.4312974717523744e01, 1.3816992621175055e00, 4.4375587023877297e-02], 1.0e-15, ) assert_norms( mesh.ce_ratios, [1.3499477445918124e03, 2.0088073714816950e01, 5.5122675770024077e-01], 1.0e-14, ) assert_norms( mesh.control_volumes, [5.4312974717523744e01, 1.9213504740523146e00, 7.9541851115553286e-02], 1.0e-15, ) assert_norms( mesh.control_volume_centroids, [3.0478135855839828e03, 8.7829558499197603e01, 4.1869842124121526e00], 1.0e-15, ) assert_norms( mesh.signed_cell_volumes, [5.4312974717523744e01, 1.3816992621175055e00, 4.4375587023877297e-02], 1.0e-15, ) assert_norms( mesh.cell_circumcenters, [5.5720855984960217e03, 1.1729075391802718e02, 4.1780370020570583e00], 1.0e-15, ) assert_norms( mesh.cell_circumradius, [2.5571964535497142e02, 6.5009888666474742e00, 1.8757161840809547e-01], 1.0e-15, ) assert_norms( mesh.cell_incenters, [5.5715778346847819e03, 1.1727887700257899e02, 4.1655515539293466e00], 1.0e-15, ) assert_norms( mesh.cell_inradius, [1.2685029853822100e02, 3.2249724655140719e00, 9.1724742697552028e-02], 1.0e-15, ) assert_norms( mesh.q_radius_ratio, [1.5359568026022387e03, 3.9044827334140905e01, 9.9999895608618172e-01], 1.0e-15, ) meshplex-0.17.0/tests/mesh_tri/test_plot.py000066400000000000000000000016661417176457700210230ustar00rootroot00000000000000import pathlib import meshplex this_dir = pathlib.Path(__file__).resolve().parent def test_show_mesh(): mesh = meshplex.read(this_dir / ".." / "meshes" / "pacman.vtu") mesh = meshplex.MeshTri(mesh.points[:, :2], mesh.cells("points")) print(mesh) # test __repr__ # mesh.plot(show_axes=False) mesh.show( show_axes=False, cell_quality_coloring=("viridis", 0.0, 1.0, True), show_point_numbers=True, show_edge_numbers=True, show_cell_numbers=True, mark_points=[1], mark_edges=[0], mark_cells=[0, 3, 7], nondelaunay_edge_color="r", boundary_edge_color="b", control_volume_centroid_color="g", ) # mesh.save("pacman.png", show_axes=False) def test_show_vertex(): mesh = meshplex.read(this_dir / ".." / "meshes" / "pacman.vtu") # mesh.plot_vertex(125) mesh.show_vertex(125) if __name__ == "__main__": test_show_mesh() meshplex-0.17.0/tests/mesh_tri/test_remove_points.py000066400000000000000000000012231417176457700227230ustar00rootroot00000000000000import numpy as np import meshplex from .helpers import assert_mesh_consistency, compute_all_entities def test(): points = [ [-2.1, -3.1], [0.0, 0.0], [1.0, 0.0], [-2.1, -3.1], [1.0, 1.0], [0.0, 1.0], ] cells = [[1, 2, 4], [1, 4, 5]] mesh = meshplex.MeshTri(points, cells) compute_all_entities(mesh) mesh.remove_dangling_points() assert len(mesh.points) == 4 assert np.all(mesh.cells("points") == np.array([[0, 1, 2], [0, 2, 3]])) assert np.all( mesh.edges["points"] == np.array([[0, 1], [0, 2], [0, 3], [1, 2], [2, 3]]) ) assert_mesh_consistency(mesh) meshplex-0.17.0/tests/meshes/000077500000000000000000000000001417176457700160755ustar00rootroot00000000000000meshplex-0.17.0/tests/meshes/pacman.vtu000066400000000000000000001350531417176457700201030ustar00rootroot00000000000000 AQAAAACAAABYTQAASDMAAA==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 AgAAAACAAAAgEQAAGh4AAEAEAAA=eJx13Xn051P9B/Dv9zuY7DtD9qzZ94QskSUtsmTJGllCobIlY2dSlC1LNUMiYrKEVGZsJWLUWLKUoRKyL+3SOT/Px5zzeZ7z+/zzOvd+7vu+730tz9frLu97Xxwe+r/fuHfI0K0j79ADQq8OfSX0tdCVRr1D3x4eLLfc8GD5jyT9TOjroR/O/38O/Uvoq6GfGRp8j+fV92LoFSk3X+hDeX6mpEeHPpL8N/Lcvsl/M+lP5P/Lk39o9ffV6vfLw4P91f9nQ08KnWfUYDnPDSf/9Lzvr8n/UZ7bP+nFQ/9bfMOv+VLPn/L/b5P/39Blkv+U/tf/ys+ReugB/s6Q/B8k/cPQ54pfGyeNb/5X/j2pZ7+UP6D0Sv/x483kv1ny+vvwYD0Llr7eHvrP0Lnz3oVDyXPmypeekPr/WPWp59mSq35q17Khf0g+e8JX/CbXz5Z86fH8ofT5zKRfSbmPJX8j+pL/10676P3HU57+X5D8fyV/luTjL/t4V9nd/tVO9qXeOVOePWr3ysl/NM+R199D35P8V0P/GQoX6PtMowbLee6N0m/6Pjrl/0PPkz9q1GA76JH2bAln6GXoS8n/Wsoflfy5Qofy/9z4UflzVTu0a820hx3BoaFRg+3QrvcWbsCL35ce0Is/VTvJayT10Ff6O/uowXbrx4GFH/i9uPbD/ZKj5+mbeubNcwfldQskfVn+f55+JB+esr+5kj+KvaaeR+H18GC7tGe+0HlS7sFq3yllF/jFjvCNPbLDj1e/9Oczyb+U/iV9FftNPRuH/mNksB2jRwbb4/nzhgbrYdfKfRa+Dg+2963kP53046FT8FU74UDy+W14AAfwzfsPrP7+u/g9uvo3e9Lsip3NEv7NVO3SnnfT39Ankv9Q6vlt6J2Fk/wUu9A+7b2q+D1z8VM7tEv7ny75/Cv0N6H0nH5o50j+X6D6dTh/kvRb7HRosDx+kBf9n6f6R8+9T33q+dDwYD+03/uU3yX5X02+eGa2iov8P1xyHF18m7H0cUq1E5/uLHl8gf9MGp7+u3BVHCNuFM/AF3gzY+ERuS2Y/L2T/+nQuasd2jV/4f7eVf660H8k/zHxSuEnO70ydLnQhwuPphWes0txhfhcHAXnv5fnbkx6TNK3hO7En5WdXpl24zd/OkPS3075XyU9zA7y3E+SFqe8nPS6SeMnPq7Dbw4N1kMfjk/5nZPelZ2n3FWhcFU75yu7eCXlPjc0+F5+hX6yL3Et/eGn4Cm7oc+TQ2/Nc7+o9s8xMtiP+40Tksa3Oeq5Ffid6h/+wzm4N3/aCX93KvluHnpM2r9j0rPW//Lpx1xVD306IvXQK/JeMuXJnVz1l3yvK37gwxrDg+3SHuMI4wfx/a+Tz54ern6tUXr0r7QHv/F5tvp/vqTPzvPkTL70acbQdao/+gcH6P0rVd77vL/1Rfvp/xLwMM99hd6H7h56Vuj3U25c0mfS65HB5y8PXVmcnufo38kVF4j/luEHQ79TuHBfKPmyz/krfhhX/Tuz+rlElQPHm4UeEEo/2eOkkcH+/KX6NVvxD9/EDZdUfLd75SuHP6cUn8j3O0OD75EeX/nGd+8K5Q/gDj89peSyU73/d/n/90k/mfTixWfyXyl0xdBj4Uzeu0rqWYVeVb/pA3vYLZSe78wPJp890IPdSx8+U/06ueR5a9ml5w8s+WyX920f+moofwZX+Qv9vL76e0XSayR9WuiyoXvm/w8m/XzeM1+9h528N/nsZay4cGSwnodTj3hJXHdt+ePHS4/Hpzw82C3lVyr5audfq703JD0x9H2lL+wSblwDZ8vPfz3lTg5dr/yNOIUfmbnqU8/MeX6W0GtCR1d5z89Q4x1+DJ+X+X/kJ71Z6EKhR+f/hZO+IWl6CHfYC7yBp3AHv+ctOV0aSl700PPXVvuXLvxlb+IsdrdNxXvs6eMpv23oD0LFOY+Gwh16cEkofWB3cLDHg+Rh3HVxyr076UVCj0k+PuP7T/K+6TiUfPx8LhRf6dmJofTtierf06Hw8Jt5L1yES6sXPpH7dSV/uPBy6oUP5PVk8pcuPSFf8r4r+W+Ffqj4tlLxb7WyM/0VR34j+eKcDYYH+6v/5A1v4MykpNdM+eVKP68tfjw2NFgffu+r3aGLpp658v/cocZp2n1utf/apOnFP0Px96VQfP5I0h8NPSN0q9CtQz8bSv/2rXYuFnpbyuHDjaUXcGA3OB+6d+haKbdk0kuJQ1Lv7KHmTU5P+a+ETgzlf/ld84BP5bkbQ1dO/h1Dg+3Xn5HUYzwELz+c9DahKxT+HV38eSDldqv+zpH8OUOXr/6we/a+QMotGHpgKDyH4/Ds7PxP/9nDnfn/l6HLJ//1lFs1+R+AY6H0SRwoDjm5+E6Oa5c8P5Tntwg1rp431DqA+fQZ8/zoUPpCv28JhRerlt2vXn4cHvLn5M2O6e3FSYtDxakTSz7kwg+wa/ig36elvHGF9mo/fyiughfsBp6JO/iXnyZNzvp9e9LwEk7enDTcFUfBEf2GJ/OkHLthL0tV//RroZQ/oeT1qzz/y1DzQKtWP7T/nNBHkn9h6R8/d2ro1/I+8XnH6/qh/XDyulD69LtQOE+e8P3n1W529Iuyp2kpx57YEfxlH3D41OTzS8azPy49gKfiMXHYVuKF1LNoqHURfu0D5d/eF/r+UHqKz/iC3+xwoZLvhvoZan0Jv8gX3+gB3IW32qG9/O+DKWc+TLzPro0fxI1Ll56KA8UNa8Ej7cn77g5dt3CA/a9Q+rZx6d24pPmlr1a5eVpPk34t9bPz4wpnTi++71B2tmLVo1793AMe40/KLRNq38FppQ/4v0X1Q//XznPrhFpPNb403rSOjv9vDw227/b8z574owsrrhPnncUeQ28LhdffDR0fanz6WsWjKw8N/vYJPSTlTgr9UfkzdgP/jE/gCn/JzpcqvaRn95a+wUX2wS4WS3rxUPOI8Ew8837l8/8epQf8HLuEa+JS8ajxwKahHwxdpvj4t+Ln+im3Qaj9H+S9WLXn2Ipz6LtxmfGYOGOp1POe0KXKbu8pvk5JOfNs5t3I77SS47r+D122+vtm9XvjlNsk1Lz7EqFLhlq35n/F4/wwu+Xf4cCaSa8Vav1/vaTfH2rdabXQ1UOtf+L7EqUX/AC/sGbe+8M89z3vSb75D/GTceAqSa8aah0PbsCL8+FVyq0UunTyl016uVDrV8snvUKofSW/COVH2A0/+7dQdgjPvx0KL+AoXD2n/Dn/Ts/oz3qFl+zG+Nm4AW6sW88bXxhXiOPFA8Z54gJ2Rv/OTfq/Kfef0A0Ll34UCp/uDsUv8Zm4H/6JW8SHF4Xio36vX35bHCtuEcecIW7Sv6R3LJyH+zeFinvMi/2k/reecU/1597QZ0OfCd2rcJO+03P4Yf3kvlDzMexTHM5+Pl12hB8blp2JM8WX8Fc/bqr+js///NolobOlHPmTO32n//TvjaSfChWPbB06R+i3Qo1fzUvsUHHgmIpL0O3Lno4KNW9gfDzv8GA7xBffSvuMa8SPn8j/+v0N+BNq3ET/Ngs9InRy2fe5oeeFijPFl8YB+XtoTKh9ST+r9vGr7Ju9wwnxhP054hL29rkqt0nxBZ4a1xovmdcSN4oXxaPs+uB6nzhUHGHeW7vsJ+L/Lir7fCT0D6F7hT5ZeAs34K767GPjV8WxcAAu0Et2zH7/U3JcP/n2XZj3Nt/KX8A9OCg+WTTl+UvxibgEf78U+sHQh6odO1R7xCHiD+OYI1Num5Sz7w2u0hv4CvfgxM6FPxsVDpmXNU8rHjeuM/41DwGn4S0coi/iYH7h6qSta1xT/d+++CD++UOoOGiD6ie8pL/GUWeEXp3nbhoarNc4wLjAuOJdoTMVjp1T7SW/CanvslDjbfECPYbH5v3g5dcrDrfvVfx1a56DF+JyeAbf4Dk7YHfskJ8S9/JXxhvmZayrwecxJR9yeDFU/PRc8YW9wUv/P8+e8xw8n7f6bT/tAVXPuVXfZaGfTLv3Lz2nHxtVue2GB8s/WvXZ/8ofrlM4bTxg3Gs9j32z9035WfXVfAKcFu9OCuXX+Av+Y+H8vw89C4VP4hnxzfhQccCl9b+459eh5PlmqLiAfPgX/kac+6vSL36Jn+LH4MPB9f8VVY4/EQ+IA4z/Jqdd7OG2UHhLDg+XHfBDvwt9rd7Hz+AH/hgXim/gBHzYNenHQ58IJTf2y56/l/8/VnpnPlZ/ps975X9+B07cVeX4rYuqn8+Gblu4IU7ih/nlLc0rpj5xrzjYfLt5PesVsyZfnOk9xhPGF/zJcaEnhNqnM63e91Qo/OEX4NCuxfePlh+5qZ57OuVODP1j+QvPmfc2P6sd8AveX1bl4Qb7gR/sht3DB3L5Lr9SOMov2qd7Uuo9JdQ4kD4Y371VlFzEMfAV3l5QOLx4/e85cqaPcIl9wKcPhy5S/F68nrcfE5+VF4+yG/Y+OVRcJB7a1vxS8sVF8B6+0Rs4h39nhRpPX1D9t7+bHyDXXYvKZ0+blvz5B/qhHexu1eIfvLTPSnxnnxs8gSP82RfrffBR/84JNW4/P9R4ih+aHAqX4JT64KzvrOgzfKHXjxdfjAf4Z3i4S+hpeY/9CNYnvR/u0Xt6OVu1w//wkX1cGIoP9hPRt8mh5iHwyXPGK09U/+zz/ma1G1/pHzyy34H/4xftdzaOM34bax2j9IqeTUh98Mm6z4SSh/2g8Ej8AZfoP723Xxye4GPjijR5wFX+BB6zd3i6SMmfXj9VerJh6Yt+03N6D5f5L+0QL4mDrLvDWfHuBkkvmLT51xXrPexgC/NW9C7UvKBxgHGBfaXTQvkXfMGvG6ucdWDzVPbxwB/2RR/pg3lafKGf7F45+EMecAiuwS14ZZ5HXGTcAD/EQbuVnKbrbeoRz/If8Fx7tZ88v1PlfB92dZXDR/bL3tnxLqVP9E7cYp3Tes1CSS8c6vs0/GTn+GocbFx8SNJwmN/RLvzmL8Urd5c+ibvtt7N+aT/kd0N/n3rHJw239N8+RPGceW77T17I/9Yd2MGXQ78SKp67p9pnPzf8ZX97lHzJTRzFL4tz8IlfwG/4dmTSR4fap2+e0vyQeSH6Tk70i/6JZ8xHbl/6vUnqgdf8tO/e8Ed7jg0dG4pvx4eSN7vbLvXAF/51i+rvl0KPCp0aav7IfKV4SPwiTsZPfgpf4SEcFH+SH9wkx5tTr/kgcYq4hH0Zh8P9eau/9GhsqPEBXIND8Ed/dq1+2RdiftZ8+vWh9mPSa++x71GcLT4Vr25fcbj45JSi8vFX+RPr//tDjTPNAxpnmY+Cs9o5tuqzzmPd59tJ0yf8xucvJ9+42jym+ST+nXzNk90Rys+JX46jTyVHeq69N5d+iGvZK3thP9Zb2Oenkhb/qsc8p/EIf8SO4bj9QPankeusJV/x44TKF3+sX/qsvexcv40/1iv9xB/j3h2r/fwbnFkgafv2rH8/EOr76b+Q69Bgu3YsucJj8hVvwjM4BmfgC/mYJ6G/9FV/6Kl9SvwAv+B7JrgCZ3yPBRfEd/T2C6kX7vmOCm7BdX6BXLctObJHfqb3U8Gxm0ve25XcyUn84ZwH83zm/Y4I5W/ER/R5OPVYD7buaz3BPPf4igPEmfulHvGh+XpxIvzdsNoPb/gL/oOfY0f0UrvFc58q+fGT9JaewDf6Im61XmPfJX8Ft+E4/LYOKq4xX2s+13kX8vkf/8MRctD+zdln6BT9Sznf5Rwcal+EdVzfW40pfuEDO2T37HGfUOsW+yZtPt983tbJt15pnXJPeJb0z0Ktb7AP9sKe+ZOZSs5bhX4+9LBQ/uLPoT4gY+fsiR2Zvz2mnhenWn+w7uA900L/EWp+kj/zfa51duvrp4eazxNPHS7uTfrZ4qdxID01nykONM9knCHuuafK45d18kND7TO1r8F+httLTncUn44tfps3NY+6U+qx39g+Y/tgzfdPrOfurPfdVdT35fDE+p11O+um7Bh+HJx8fsD/M1X93uu7eesF1gHED8bZ7GA/epl6Dw09LHRaqPHWISXHfUueu6Y8+2ZHxovqU8+RVQ/9hgfw4ZBQfg5O4yf/pd1fDDW+ET9+PuXhE7w9tvpnXCn+4ifgCXwRh/KL6r+r5EH+ym1W5dkVf8y++K9ZQqeG0sNLQukjnDEuYzeT8//tofwm+6Rf7E78A7d/EwoXvBc+aLe44vCSL7lab6dv5HV4qHGx+SH+Y6WSF7tnp8blxuNX8sehvje1T0w8gL9fDz0zFO4b1xjPaB8+ikfwE76ILz6dfPNz7ICe0Ut6RB+mFP/Jg57Tb/5ndjicNPn7/m73UN/hwS16o5/6bT3/PPFX/vd9ue9l5+TPi4/wl9/0ffDOeW6XUOfK0Gt6/mD5Y/uInC9i/MKO2a91X/se+QX7BHzfAM/t+xP/Gw/wm/wP/0n+5mvoKf7uEeqcEP9Pn9+suGjBkjO/bf/A5WVX5kfZF7vi39iX8aP5cDjG79FT+smf8C/8DvsYKfnAma2LTweF/hS/St/oGT9rvALX4Tl9hzvGYaeGsu+rQn0nYf+Z8Sg7OTJp+oxf4i1840fpL73FP36BP6XfcMP5MtY3JofCBzgH99izOF9ca7wjDjB+IBfjuQ2qPP3bM9Q5Sfwnf8qOjXv4t32qHcZh6oczc5QeiA/p5fGlx/Z32NdhXAKv1Gv88kz9v0PpKT6cVfwnD+MV45MT+NnUNxYtf2Y9gV+jz6uF3lL4jn/saL1+T9LzVz2r1/P47vwucZk4TVxNjuTHj9qnZf8hPwjPzGvwh+arzVOPq/LKsTfv8V7+AP7zB/aj7Zn0XqHO6bI+Y73Ge8VJ7I/d8Zv8JTtk5+YV2Lt2tX+CE87ROzpUnDlz/jcOpMfwlj7vPTxY3jks8Eic8VLV5zl+zHhbPHJY6rdOCj/EOfwHveBHrK9a5/iquCuUP4Z79rmZj/xmlePHzyz5f7L6qRx5G8/QM9+NnJL6+SHyJEfxnPUs/eUf+IvPJW1+Av/wzfyReUD2Lr74VtLOW+Mf7es3z7t/1ace+3TFHeINfs+49EvJFyfw89oPT+AIP8lO9g618Rlu+O5jternE9VO+gwn6KnxmP+Ny8YmfXwovKL31kXo/5wld3pgnCV+pYe+o/J9r/OQ7P+0D/GK5Iu7zFN8X3xdduScFDiKL/jLTuCmcz3Z48xVHz0jN/Jin+Ig+mbe9tKkfyCuKD95QcVRU6secZ84kF2Lt8SnX0i+dTb+8WT6DgdCL0k+uzS/4Pw+cdxI1W8fBftgF+ZJ2KX3is8+Wu0XL7MPdgF/4I52iqvMW4iv+Hnxj/298MV374+xq6HB9mon/Idf/IA4jN+Am+ZLzZMaN4gnD6vy5tGsE5lP4w/g5ynFf3w/Wz2pl38eV/19rOoRr/Cr4hb2z28fU3qDT/hjfIS/+vt0yolrnUdHLxeofrBn+5J/mHzjTuMk4yNxIHthP/y6dQL+3T56dv+15Jv38/0qe7yi2qU98NT7vRd+mZ+BY/wT/8xPOZfFvLX5anwX7+M/Pw0v1Q9X+WF6Je49uOQjzhff4yd5nFpygfvwD/6zh5fKLugFe7841LyxdWJ63+sJ5mOMD+A6PITTL5QczWewM/j6yaR3CnV+qO+b6YVzEs0n228Bb24pPdMe9rlN2Rd5Wo+4I7TjX/Ky7uf93qsf5lXoJ79FH7XLfuub6F+1f7XuR57fM1S8zW7Fcycm7ftK3z9fH2rexHyb+RPzNeZ3xVPsib07H1o5eqa8eOHqag9/DLfYi/004lzzIfSanp9d8iGve0Lpt/PnnPf7YD1/Uah9/74Ld27ijknTx5OKv5+qfvkuhR2zX7hmnd66Ob0Xr9MbcSw54Kf2z1L94C/5MXh+ar3fe8lDfKD99tGKC8QD/LZ4A86Sk3ks8hKn2sfp/gL2yn7hAHw/qPLpk3lBfBBnmzedyK9WvEevxNvibPZin8E1ZRfmFdnDhFDjKHbP3sVpcBR+0terSl7i75uT/nmo9Sf+2byl/QLiUO0Vf4l/zy/7wzfjIP7GPMp51Q/4Sd7aq/3sxXqoc6ach0gf+Dn76Oyj8J2tc/bM5+snP2L84NwE56kad/046Z+Gmg8xD3Jj6TM95r/0Gz6zO3In72tD7ee33/+G5Itj+D3+Zct6v/eKM5Yt/sAJdsx+ydV4hnzJX/xDb/GRf8ZP8jMPQo7i+S9W/eaVJhZfzWvaj4FP9JI/x0/zGkdUefjmPFPngrMHfopd2PfgeyLnuIo/zBPorzjGuI49+o7Pd+bqp1dbl37Z97hX1TOh+EBPnKPnfBznYrIH8id34wff5RpHiAfs6xUXWI+k779MvvP/fJftfGD3TrgHwDnV9i+wS/bI/o0v4YB9WPCSnOAjOePLvtXee5P2PYNxIT9iXpL/50f4Y/MK5hOsF9inBQfYs/GBuMu+fPZNXvRWOXbFn0wpfWDf4lvzFd8ofl1R7/U+uGEe6Jaqx/oAfJ1UfFOvOExcIj4Wn7gvwv6zu+Hr8GB96tEO/dMecZXvnMVX5tvNQ/Hv9N18jfNg8Xf54vM9Vf+vk+b/jRecg07/+MNrql/6Iy5lN3BbXM0vWl/kH8Un3u+97I1fYXc3VP3e57sQ7dIe+AGX6Bkc2aP0xXqZdTLnlsM780raRd72UZM73BTnim/tk3+6yl9b7dFPeHhxtZ8eiPvoA7thR+zUd6QXltzph/HK9cUvcd/Eat8S1U7fUz1Uclcf/wLH4L7+abc4Df7dXO3X7vsK7+gH3HsxdJGMQ1YI9T2O7yfuq/Zr9631HuXvhyeh8PC2pPmPSaWf7Oj64sPY6vddSfP/9I6+ic+cNze18F//Hyj9IT98psfmS9UP/+G+c+zhIz92S/HlqeKPeTLzZsaN1p2sI06pcuYDlGfnF5R86JP4QlxB7x8o/Xf+6IrRg5e0g5/J/7dW+sXKv7H4Ru/gt3l194yZn+JnJlW+tPPMJ5Vd0Z9Fiq/4Y/x1TNWzSJWD+9PjuzxH38z/8yPORT+m2qt+5zH4boae+O6PXqvfud3mB5xLb9760LIj+Gtf1/T7I+B1xV3OwXUvyyP1fv2dWu+bHscUbsNrcS/7/HnV+7dQ55S5N8e6B/nwy/TmwdIf+w18l+VeD/Wrd7HQ1asd+nVlyYdc8Eu8LG4wHnCuoXuCxEv0mj7zc/wY/wV/4I77dpxrSB/poXtu3HOw3KjB8srB+zVLfsan7HqZPM+++Tn1qcd8nfkX8+3kQC731vvFS/dVeqPKdx6D8+zcu/dIlbu/3md86L3OQfCc8nBTP+An/wWn3EtofOOeIPcY2idmXGg8SF/ZFb3lb+jd1Gqv9pO/7760S3vEl/TLvYz2TblncNHwzfqO+WK4If42X6394kt4BsfMq+MHPsAF9vJw9efo6he+sCf8sU4Gh9zf5H7RhULdu+b8P2n/O49cvOLeKzjrexR2bd4Vn+CreQXzGO4Vhe97V/vFo/YZuX9Ne8RT2sV+zGeyI/Mb3u8+KfdvuO/PPZtTSw74T1+du+oeQ9/fP1n6bJ+Zc8NXSXnz0PiyetIvlP69r+73ezt0TN0japzhHiLvd54mfHYvjXsv3Xdpvdx+fvn02/ojPaff7JWeG99YF3BfJD679wS/4Yc4zX2PO6TcDKHuYcRW/XhmZDDt/h39m1btp//uYdEu97Q5z9k5zu610m7t0v4lq5/umbI+RZ/c4+o8V+e7ar/vttxX5rySdw8Ntkt7PI8f6nFOfR6ffq+o77CdZzFr3aPn+3b+3Lnxzot3D7D7Ctxf4P5b73NOn/e678BzL5T9uu+AHZPf5qHk6H4Ez7l/z70E7ikQP7sfwH0B3useAf977yeSTy/og/sC+HP1s2f81l/8ct6+++uct++cfXYsvqEv4hz651xx95nhC/mQi/PAnQPuvmNp54SbB3QetXOo3Tvt3GN67b3OFXbOsPqd5+wc59eT75xg5warf9Ok2YF7XpcaGnzOvWrO991kaLC8832XHBosD++cJyxOdd6t82+1kx5/LJQ+O+9X/Kwe+kSv6RU9pl/0yrm73u8+bPpKv+iVdi0RO1wydMuUYwf0n36QG3mRE70gL3qpPvpJP8wPT783WftDp99PnjT5a/c8SdN3ek5+5E+O5Ef+5OgcZecqu6eSXMmHXN6bNH7js+fXqHqcm02v3R9K3s55lu8cUudVuDcZ3o2pfOUur3xyg2PiKO/TLu8Vjzmf2b2izmceV/n8M3/H/4lrjZfEF/wBeVsXEOdP98+hxlPGUe6Z9T7xJj/rHqf/AUxmLt14nF2Y20/PcRjHf79OiuiAUKF0LpRS+QfIyKThH0jN6c7WljubG8ZcumVjTpv/gSuXajMMKXLogBFWDuWi18v2fbp59rw/n+/z+Xyew/t5fh1KLf1lZy7JtxlL8g74LvAs9KPIQvD76K/5bgw5gDyGXM3+a+nk+nrwOuzUI7PAR9g/jMwE34L+FTvfkDWsr0MOg79CVvJdM+tV6PPIenBEKgdZALAY3lcCfgj9ALIRvC+VtKedZ5w3zv5l4N3oKeR27QP3Io3XVuQA+Ab0r+iT2KkCbwfvR+YGP+wM+KxxBK8A/2We8F0xeD77LiOXgz9n3xzfrQA3XvWsGzfj8Zf9xmUR/BQyTzvo74M/36Hv5j4f0N+w/yV6Lfv1SwdyJXgnem06ie8F/42dP8gecOuoiP0X0RuQqzKT7zmckXyX+vXw3tPaRZqHhankuv58gp132NkW/F4Q9uuXt6wbd/PAdfFb4KXIteDm6xT7s0O+1iDN21r0A+kk7rn+lYFvRz+TTuI96Lms14GvRS9BbgTPQ78f9pcjG7BXHXhvOXIH+BX0Ht7bkpnUvae4vDQF3invgZsv5kkbelXgq/3s34fMAZfvNvOdvNeSSt7X+3i/5oBXoF8Idk6iy1f68yN25Dnr8Sn7ipAF4dyeUI8/0OX7UvAZcXTjItH2Bh62DzwOfUK+kxflvb3ppD3tWEfmke+Vl2aRC0jr374qD1g/5rV5q1/sG//7Bbh5Z759Ajc/y0O+PQr+tN/exo48oF37t/nfCL4Abh6uAJfv5flx7nEDKc/P8/1B9hcFv9nH9N8L5CgyP/Rf7+97zEvryfwcQrcfNYGXoU8YT/DiVPIcedV+YB+Q/+1n98B9r/yu//SbcTJ/zVv9Yt/QP9NhrnAecA47D+48pr4n9GX7aCvfyRvWpX2zFf04+76At4M7Zy2GfuRcdpX1NfI2en+YA42n8d2Ebv8wPsbFPmd8jIt9y75n/5L/5CHf1cc9nEPNH/uQ/ce+I79NB/9/RD/i+c4b2JUX5EX/7FfWu33Ic/zOPDKvNstz2K+Uv5DG27nMuDvv3MWOcXHutF61b78/F/q7epdzdPCnfpRPBsHlI/N8DvtjSPk7nlMZ7DwI9ibkWXD9oD3te671/5n9TWHfYKhH/WVe6zd5T76Th+VZ50r51nd0hXs6lzh31IX4zgY/WLf2f+vXvmhd2h/HA/8aX+fXn+y37uyHZ4N/7KP+vrEPNphf2GsL9SavtAa7Q8G+c0kFUt6W77pD3f2fS5TgzmHGx7hYj+ap+TkJ/gcpvzmHud4Z9ruvM8TVfNwZ/DIS/DMX8r4j8OBoiIvziv4zTn5/KZW0Y599mJHErRvrqCHo3lf8JvgMuP8HkI++s25eeZ7v81znfn+P+jvU/uf9fY/zkr8TPPcEeE7IK+u/Ovg/O7xDO/Y57bUGP84Hv/0Dfj3QrA== AQAAAACAAABgMAAAZwkAAA==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 AQAAAACAAABgMAAALAAAAA==eJztxaEBAAAMAqAV/395wTOEQq5i27Zt27Zt27Zt27Zt27Zt27Znfx90Hj0= AQAAAACAAABgMAAA4ykAAA==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 meshplex-0.17.0/tests/meshes/tetrahedron.vtk000066400000000000000000000030531417176457700211430ustar00rootroot00000000000000# vtk DataFile Version 4.2 written by meshio v2.3.8 BINARY DATASET UNSTRUCTURED_GRID POINTS 26 double @@.\.\@Tvf.\Tvf@.\??.\@Tvf.\@Tvf?.\.\Tvf??.\Tvf?.\@ @ H <.\@~ iy.\?Tvf@ .\Tvf.\Tvf@ @Uo3Tvf@Uo3?TvfH @~ iy.\@TvfH ~ iy.\~ iy?$>8%?2χ?qr$>8%?qp?$>8%2χ?qr CELLS 33 165                   CELL_TYPES 33 CELL_DATA 33 FIELD FieldData 0 meshplex-0.17.0/tests/meshes/toy.vtu000066400000000000000000000742571417176457700174670ustar00rootroot00000000000000 AQAAAACAAAAgLgAAlxYAAA==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 AQAAAACAAADQTAAAaA4AAA==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 AQAAAACAAADQTAAAQAAAAA==eJztxbENACAIADAMcfH/gx34gQHapRklbXv417Zt27Zt27Zt27a98mfbtm3btm3btm3btm3btt38WfYHj+BHxw== meshplex-0.17.0/tests/performance.py000066400000000000000000000046031417176457700174670ustar00rootroot00000000000000""" """ import random import numpy as np import perfplot from scipy.spatial import Delaunay import meshplex def setup(n): radius = 1.0 k = np.arange(n) boundary_pts = radius * np.column_stack( [np.cos(2 * np.pi * k / n), np.sin(2 * np.pi * k / n)] ) # Compute the number of interior points such that all triangles can be somewhat # equilateral. edge_length = 2 * np.pi * radius / n domain_area = np.pi - n * (radius ** 2 / 2 * (edge_length - np.sin(edge_length))) cell_area = np.sqrt(3) / 4 * edge_length ** 2 target_num_cells = domain_area / cell_area # Euler: # 2 * num_points - num_boundary_edges - 2 = num_cells # <=> # num_interior_points ~= 0.5 * (num_cells + num_boundary_edges) + 1 - num_boundary_points m = int(0.5 * (target_num_cells + n) + 1 - n) # generate random points in circle; # for seed in range(0, 255): np.random.seed(seed) r = np.random.rand(m) alpha = 2 * np.pi * np.random.rand(m) interior_pts = np.column_stack( [np.sqrt(r) * np.cos(alpha), np.sqrt(r) * np.sin(alpha)] ) pts = np.concatenate([boundary_pts, interior_pts]) tri = Delaunay(pts) # Make sure there are exactly `n` boundary points mesh0 = meshplex.MeshTri(pts, tri.simplices) mesh1 = meshplex.MeshTri(pts, tri.simplices) if np.sum(mesh0.is_boundary_point) == n: break mesh0.create_edges() mesh1.create_edges() num_interior_edges = np.sum(mesh0.is_interior_edge) idx = random.sample(range(num_interior_edges), n // 10) print(num_interior_edges, len(idx), len(idx) / num_interior_edges) # # move interior points a little bit such that we have edges to flip # max_step = np.min(mesh.cell_inradius) / 2 # mesh.points = mesh.points + max_step * np.random.rand(*mesh.points.shape) # print(mesh.num_delaunay_violations) return mesh0, mesh1, idx def flip_old(data): mesh0, mesh1, idx = data mesh0.flip_interior_edges_old(idx) def flip_new(data): mesh0, mesh1, idx = data mesh1.flip_interior_edges(idx) perfplot.show( setup=setup, kernels=[flip_old, flip_new], n_range=[2 ** k for k in range(5, 13)], equality_check=None, # set target time to 0 to avoid more than one repetition target_time_per_measurement=0.0, ) meshplex-0.17.0/tests/test_ce_ratios.py000066400000000000000000000043071417176457700201760ustar00rootroot00000000000000import numpy as np import meshplex def test_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([10.0, 10.0 / 3.0, 5.0 / 3.0]) print(mesh.ce_ratios) assert ref.shape == mesh.ce_ratios.shape assert np.all(np.abs(mesh.ce_ratios - ref) < 1.0e-14 * np.abs(ref)) def test_tri_simple(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], ] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.Mesh(X, cells) ref = np.array([[0.5], [0.0], [0.5]]) print(mesh.ce_ratios) assert ref.shape == mesh.ce_ratios.shape assert np.all(np.abs(mesh.ce_ratios - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) def test_tri(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([[0.5, 0.5], [0.0, 0.5], [0.5, 0.0]]) print(mesh.ce_ratios) assert ref.shape == mesh.ce_ratios.shape assert np.all(np.abs(mesh.ce_ratios - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) def test_tetra(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3], [0, 1, 2, 4]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [ [-1 / 24, -1 / 24], [1 / 8, 1 / 8], [1 / 8, 1 / 8], [0.0, 0.0], ], [ [-1 / 24, -1 / 24], [1 / 8, 1 / 8], [0.0, 0.0], [1 / 8, 1 / 8], ], [ [-1 / 24, -1 / 24], [0.0, 0.0], [1 / 8, 1 / 8], [1 / 8, 1 / 8], ], ], ) print(mesh.ce_ratios) assert ref.shape == mesh.ce_ratios.shape assert np.all(np.abs(mesh.ce_ratios - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) if __name__ == "__main__": test_tri_simple() meshplex-0.17.0/tests/test_cell_partitions.py000066400000000000000000000064321417176457700214220ustar00rootroot00000000000000import numpy as np import meshplex def test_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [0.05, 0.15, 0.3], [0.05, 0.15, 0.3], ] ) assert ref.shape == mesh.cell_partitions.shape assert np.all(np.abs(mesh.cell_partitions - ref) < 1.0e-14 * np.abs(ref)) def test_tri_simple(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], ] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [[0.125], [0.0], [0.125]], [[0.125], [0.0], [0.125]], ] ) print(mesh.cell_partitions) assert ref.shape == mesh.cell_partitions.shape assert np.all(np.abs(mesh.cell_partitions - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) def test_tri(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [ [0.125, 0.125], [0.0, 0.125], [0.125, 0.0], ], [ [0.125, 0.125], [0.0, 0.125], [0.125, 0.0], ], ] ) print(mesh.cell_partitions) assert ref.shape == mesh.cell_partitions.shape assert np.all(np.abs(mesh.cell_partitions - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) def test_tetra(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3], [0, 1, 2, 4]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [ [ [-1 / 72, -1 / 72], [1 / 48, 1 / 48], [1 / 48, 1 / 48], [0.0, 0.0], ], [ [-1 / 72, -1 / 72], [1 / 48, 1 / 48], [0.0, 0.0], [1 / 48, 1 / 48], ], [ [-1 / 72, -1 / 72], [0.0, 0.0], [1 / 48, 1 / 48], [1 / 48, 1 / 48], ], ], [ [ [-1 / 72, -1 / 72], [1 / 48, 1 / 48], [1 / 48, 1 / 48], [0.0, 0.0], ], [ [-1 / 72, -1 / 72], [1 / 48, 1 / 48], [0.0, 0.0], [1 / 48, 1 / 48], ], [ [-1 / 72, -1 / 72], [0.0, 0.0], [1 / 48, 1 / 48], [1 / 48, 1 / 48], ], ], ] ) print(mesh.cell_partitions) assert ref.shape == mesh.cell_partitions.shape assert np.all(np.abs(mesh.cell_partitions - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) if __name__ == "__main__": test_tri_simple() meshplex-0.17.0/tests/test_circumcenters.py000066400000000000000000000042771417176457700211020ustar00rootroot00000000000000import numpy as np import meshplex def test_circumcenters_line(): pts = [0.0, 1.0, 3.0, 4.0] cells = [[0, 1], [1, 2], [2, 3]] mesh = meshplex.Mesh(pts, cells) print(mesh.cell_circumcenters) ref = [0.5, 2.0, 3.5] assert np.all(np.abs(mesh.cell_circumcenters - ref) < np.abs(ref) * 1.0e-13) print(mesh.cell_circumradius) ref = [0.5, 1.0, 0.5] assert np.all(np.abs(mesh.cell_circumradius - ref) < np.abs(ref) * 1.0e-13) def test_circumcenters_tri(): # two triangles in 5D points = [ [0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0], ] cells = [[0, 1, 2], [0, 3, 2]] mesh = meshplex.Mesh(points, cells) ref = [[0.5, 0.5, 0.0, 0.0, 0.0], [0.5, 0.5, 0.0, 0.0, 0.0]] assert np.all( np.abs(mesh.cell_circumcenters - ref) < np.abs(ref) * 1.0e-13 + 1.0e-13 ) print(mesh.cell_circumradius) ref = [np.sqrt(2) / 2, np.sqrt(2) / 2] assert np.all(np.abs(mesh.cell_circumradius - ref) < np.abs(ref) * 1.0e-13) def test_circumcenters_tetra(): points = [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] cells = [[0, 1, 2, 3]] mesh = meshplex.Mesh(points, cells) ref = [[0.5, 0.5, 0.5]] print(mesh.cell_circumcenters) assert np.all(np.abs(mesh.cell_circumcenters - ref) < np.abs(ref) * 1.0e-13) print(mesh.cell_circumradius) ref = [np.sqrt(3) / 2] assert np.all(np.abs(mesh.cell_circumradius - ref) < np.abs(ref) * 1.0e-13) def test_circumcenters_simplex5(): points = [ [0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ] cells = [[0, 1, 2, 3, 4]] mesh = meshplex.Mesh(points, cells) ref = [[0.5, 0.5, 0.5, 0.5]] print(mesh.cell_circumcenters) assert np.all(np.abs(mesh.cell_circumcenters - ref) < np.abs(ref) * 1.0e-13) print(mesh.cell_circumradius) ref = [1.0] assert np.all(np.abs(mesh.cell_circumradius - ref) < np.abs(ref) * 1.0e-13) if __name__ == "__main__": # test_circumcenters_tri() test_circumcenters_tetra() meshplex-0.17.0/tests/test_compute_cell_values.py000066400000000000000000000005051417176457700222540ustar00rootroot00000000000000import pathlib import meshplex this_dir = pathlib.Path(__file__).resolve().parent def test_pacman(): mesh = meshplex.read(this_dir / "meshes" / "pacman.vtu") mesh = meshplex.MeshTri(mesh.points[:, :2], mesh.cells("points")) mesh._compute_cell_values() mesh._compute_cell_values(mask=[0, 12, 53, 54, 55]) meshplex-0.17.0/tests/test_control_volume_centroids.py000066400000000000000000000030071417176457700233430ustar00rootroot00000000000000import numpy as np import meshplex def test_control_volume_centroids_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([0.025, 0.15, 0.475, 0.85]) print(mesh.control_volume_centroids) assert mesh.control_volume_centroids.shape == ref.shape assert np.all( np.abs(ref - mesh.control_volume_centroids) < np.abs(ref) * 1.0e-13 + 1.0e-13 ) def test_control_volume_centroids_tri(): points = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]) cells = np.array([[0, 1, 2]]) mesh = meshplex.Mesh(points, cells) ref = np.array([[0.25, 0.25], [2.0 / 3.0, 1.0 / 6.0], [1.0 / 6.0, 2.0 / 3.0]]) print(mesh.control_volume_centroids) assert mesh.control_volume_centroids.shape == ref.shape assert np.all( np.abs(ref - mesh.control_volume_centroids) < np.abs(ref) * 1.0e-13 + 1.0e-13 ) def test_tetra_simple(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array( [ [0.25, 0.25, 0.25], [17 / 24, 1 / 48, 1 / 48], [1 / 48, 17 / 24, 1 / 48], [1 / 48, 1 / 48, 17 / 24], ] ) print(mesh.control_volume_centroids) assert np.all( np.abs(mesh.control_volume_centroids - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14 ) meshplex-0.17.0/tests/test_control_volumes.py000066400000000000000000000043761417176457700214660ustar00rootroot00000000000000import numpy as np import meshplex def test_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) vol = np.sum(mesh.cell_volumes) assert np.all(np.abs(np.sum(mesh.control_volumes) - vol) < 1.0e-14 * np.abs(vol)) ref = np.array([0.05, 0.2, 0.45, 0.3]) assert np.all(np.abs(mesh.control_volumes - ref) < 1.0e-14 * np.abs(ref)) def test_tri_degen(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [0.5, 1.0e-2], ] ) cells = np.array([[0, 1, 2]]) mesh = meshplex.Mesh(X, cells) vol = np.sum(mesh.cell_volumes) assert np.all(np.abs(np.sum(mesh.control_volumes) - vol) < 1.0e-12 * np.abs(vol)) ref = np.array([-1.560625, -1.560625, 3.12625]) print(mesh.control_volumes) assert np.all(np.abs(mesh.control_volumes - ref) < 1.0e-14 * np.abs(ref)) def test_tri(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([0.25, 0.25, 0.25, 0.25]) assert np.all(np.abs(mesh.control_volumes - ref) < 1.0e-14 * np.abs(ref)) def test_tetra_simple(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([9.0, 1.0, 1.0, 1.0]) / 72 mesh.control_volumes print() print(mesh.control_volumes) print(sum(mesh.control_volumes)) print(ref) print(sum(ref)) assert np.all(np.abs(mesh.control_volumes - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) def test_tetra(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3], [0, 1, 2, 4]]) mesh = meshplex.Mesh(X, cells) ref = np.array([18.0, 2.0, 2.0, 1.0, 1.0]) / 72 print(mesh.control_volumes) print(ref) assert np.all(np.abs(mesh.control_volumes - ref) < 1.0e-14 * np.abs(ref) + 1.0e-14) meshplex-0.17.0/tests/test_create_facets.py000066400000000000000000000041221417176457700210110ustar00rootroot00000000000000import numpy as np import pytest import meshplex @pytest.mark.skip() def test_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) mesh.create_facets() ref = np.array([0, 1, 2, 3]) assert np.all(mesh.facets["points"] == ref) ref = np.array( [ [0, 1], [1, 2], [2, 3], ] ) assert np.all(mesh.cells("facets") == ref) def test_tri(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3]]) mesh = meshplex.Mesh(X, cells) mesh.create_facets() ref = np.array( [ [0, 1], [0, 2], [0, 3], [1, 2], [2, 3], ] ) assert np.all(mesh.facets["points"] == ref) ref = np.array( [ [3, 1, 0], [4, 2, 1], ] ) assert np.all(mesh.cells("facets") == ref) def test_tetra(): X = np.array( [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0], ] ) cells = np.array([[0, 1, 2, 3], [0, 1, 2, 4]]) mesh = meshplex.Mesh(X, cells) mesh.create_facets() ref = np.array( [ [0, 1, 2], [0, 1, 3], [0, 1, 4], [0, 2, 3], [0, 2, 4], [1, 2, 3], [1, 2, 4], ] ) assert np.all(mesh.facets["points"] == ref) ref = np.array( [ [5, 3, 1, 0], [6, 4, 2, 0], ] ) assert np.all(mesh.cells("facets") == ref) def test_duplicate_cells(): X = np.array( [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], ] ) cells = np.array([[0, 1, 2], [0, 2, 3], [0, 2, 1]]) mesh = meshplex.Mesh(X, cells) with pytest.raises(meshplex.MeshplexError): mesh.create_facets() meshplex-0.17.0/tests/test_degenerate.py000066400000000000000000000067211417176457700203330ustar00rootroot00000000000000import numpy as np import meshplex def test_degenerate_cell(tol=1.0e-14): points = [[0.0, 0.0], [0.5, 0.0], [1.0, 0.0]] cells = [[0, 1, 2]] mesh = meshplex.Mesh(points, cells) ref = np.array([0.0]) assert np.all(np.abs(mesh.cell_volumes - ref) < tol * (1.0 + ref)) ref = np.array([[0.0], [0.0], [0.0]]) assert np.all(np.abs(mesh.cell_heights - ref) < tol * (1.0 + ref)) # inf, not nan assert np.all(mesh.cell_circumradius == [np.inf]) assert np.all(mesh.circumcenter_facet_distances == [[np.inf], [-np.inf], [np.inf]]) assert np.all( mesh.cell_partitions == [[[np.inf], [-np.inf], [np.inf]], [[np.inf], [-np.inf], [np.inf]]] ) # those are nan assert np.all(np.isnan(mesh.cell_circumcenters)) def test_repeated_point(tol=1.0e-14): """Special case of the degeneracy: A point is doubled up.""" points = [ [0.0, 0.0], [1.0, 0.0], ] cells = [[0, 1, 1]] mesh = meshplex.Mesh(points, cells) ref = np.array([0.0]) assert np.all(np.abs(mesh.cell_volumes - ref) < tol * (1.0 + ref)) ref = np.array([[1.0], [0.0], [0.0]]) assert np.all(np.abs(mesh.cell_heights - ref) < tol * (1.0 + ref)) ref = np.array([0.5]) assert np.all(np.abs(mesh.cell_circumradius - ref) < tol * (1.0 + ref)) ref = np.array([[0.5, 0.0]]) assert np.all(np.abs(mesh.cell_circumcenters - ref) < tol * (1.0 + ref)) # TODO # print(f"{mesh.circumcenter_facet_distances = }") # assert np.all(mesh.circumcenter_facet_distances == [[0.5], [0.0], [0.0]]) # print(mesh.cell_partitions) # assert np.all( # mesh.cell_partitions # == [[[np.inf], [-np.inf], [np.inf]], [[np.inf], [-np.inf], [np.inf]]] # ) def test_all_same_point(tol=1.0e-14): """Even more extreme: A simplex made of one point.""" points = [[0.0, 0.0]] cells = [[0, 0, 0]] mesh = meshplex.Mesh(points, cells) ref = np.array([0.0]) assert np.all(np.abs(mesh.cell_volumes - ref) < tol * (1.0 + ref)) ref = np.array([[0.0], [0.0], [0.0]]) assert np.all(np.abs(mesh.cell_volumes - ref) < tol * (1.0 + ref)) # TODO for some reason, this test is unstable on gh-actions # ref = np.array([[0.0], [0.0], [0.0]]) # assert np.all(np.abs(mesh.cell_heights - ref) < tol * (1.0 + ref)) # TODO # assert np.all(mesh.cell_circumradius == [0.0]) # assert np.all(mesh.circumcenter_facet_distances == [[0.0], [0.0], [0.0]]) # assert np.all( # mesh.cell_partitions # == [[[np.inf], [-np.inf], [np.inf]], [[np.inf], [-np.inf], [np.inf]]] # ) # ref = np.array([[0.0, 0.0]]) # assert np.all(np.abs(mesh.cell_circumcenters - ref) < tol * (1.0 + ref)) def test_degenerate_flip(): # almost degenerate points = [ [0.0, 0.0], [0.5, -1.0e-5], [1.0, 0.0], [0.5, 0.5], ] cells = [[0, 2, 1], [0, 2, 3]] mesh = meshplex.MeshTri(points, cells) num_flips = mesh.flip_until_delaunay() assert num_flips == 1 ref = np.array([[1, 0, 3], [1, 3, 2]]) assert np.all(mesh.cells("points") == ref) # make sure the same thing happens if the cell is exactly degenerate points = [ [0.0, 0.0], [0.5, 0.0], [1.0, 0.0], [0.5, 0.5], ] cells = [[0, 2, 1], [0, 2, 3]] mesh = meshplex.MeshTri(points, cells) num_flips = mesh.flip_until_delaunay() assert num_flips == 1 ref = np.array([[1, 0, 3], [1, 3, 2]]) assert np.all(mesh.cells("points") == ref) meshplex-0.17.0/tests/test_gh_issues.py000066400000000000000000000022131417176457700202110ustar00rootroot00000000000000import pytest import meshplex def test_gh126(): """https://github.com/nschloe/meshplex/issues/126""" cells = [[1, 2, 0], [2, 3, 0], [3, 1, 0]] points = [ [4.3000283, 8.57424769, 0.010431], [4.2568793, 8.41702649, 0.0], [4.2864766, 8.56681008, 0.0], [4.2897457, 8.58639357, 0.0], ] mesh = meshplex.MeshTri(points, cells) with pytest.warns(UserWarning): mesh.flip_until_delaunay() # this failed in a previous version because there were edges with more than two # cells after the flip mesh.create_facets() def test_gh130(): """https://github.com/nschloe/meshplex/issues/130""" points = [ [0.0, 0.0, 0.0], [0.5, 0.5, 0.0], [1.0, 0.9, 1.0], [1.0, 1.1, 1.0], [1.0, 0.7, 1.0], [1.0, 0.7, 0.0], ] cells = [[2, 0, 1], [2, 3, 0], [4, 0, 3], [4, 3, 5], [2, 5, 3]] mesh = meshplex.MeshTri(points, cells) with pytest.warns(UserWarning): mesh.flip_until_delaunay(max_steps=1) # this failed in a previous version because there were edges with more than two # cells after the flip mesh.create_facets() meshplex-0.17.0/tests/test_heights.py000066400000000000000000000024521417176457700176600ustar00rootroot00000000000000import math import numpy as np import meshplex def test_heights_line(): X = np.array([0.0, 0.1, 0.4, 1.0]) cells = np.array([[0, 1], [1, 2], [2, 3]]) mesh = meshplex.Mesh(X, cells) ref = np.array([0.1, 0.3, 0.6]) print(mesh.cell_heights) assert np.all(np.abs(mesh.cell_heights - ref) < np.abs(ref) * 1.0e-13) def test_heights_tri(): # two triangles in 5D points = [ [0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0], ] cells = [[0, 1, 2], [0, 3, 2]] mesh = meshplex.Mesh(points, cells) ref = np.array( [ [1.0, math.sqrt(0.5), 1.0], [1.0, math.sqrt(0.5), 1.0], ] ).T assert np.all(np.abs(mesh.cell_heights - ref) < np.abs(ref) * 1.0e-13) def test_heights_tetra(): # two triangles in 5D points = [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] cells = [[0, 1, 2, 3]] mesh = meshplex.Mesh(points, cells) ref = np.array( [ [math.sqrt(1 / 3), 1.0, 1.0, 1.0], ] ).T assert np.all(np.abs(mesh.cell_heights - ref) < np.abs(ref) * 1.0e-13) if __name__ == "__main__": # test_heights_tri() test_heights_tetra() meshplex-0.17.0/tests/test_io.py000066400000000000000000000023721417176457700166350ustar00rootroot00000000000000import tempfile import meshzoo import numpy as np import meshplex def test_io_2d(): vertices, cells = meshzoo.rectangle_tri( np.linspace(0.0, 1.0, 3), np.linspace(0.0, 1.0, 3) ) mesh = meshplex.MeshTri(vertices, cells) # mesh = meshplex.read('pacman.vtu') assert mesh.num_delaunay_violations == 0 # mesh.show(show_axes=False, boundary_edge_color="g") # mesh.show_vertex(0) with tempfile.TemporaryDirectory() as tmpdir: mesh.write(tmpdir + "test.vtk") mesh2 = meshplex.read(tmpdir + "test.vtk") assert np.all(mesh.cells("points") == mesh2.cells("points")) # def test_io_3d(self): # vertices, cells = meshzoo.cube( # 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, # 2, 2, 2 # ) # mesh = meshplex.MeshTetra(vertices, cells) # self.assertEqual(mesh.num_delaunay_violations, 0) # # mesh.show_control_volume(0) # # mesh.show_edge(0) # # import matplotlib.pyplot as plt # # plt.show() # mesh.write('test.vtu') # mesh2, _, _, _ = meshplex.read('test.vtu') # for k in range(len(mesh.cells['points'])): # self.assertEqual( # tuple(mesh.cells['points'][k]), # tuple(mesh2.cells['points'][k]) # ) meshplex-0.17.0/tests/test_mesh_line.py000066400000000000000000000011521417176457700201640ustar00rootroot00000000000000import numpy as np import meshplex def _is_near_equal(a, b, tol=1.0e-12): return np.allclose(a, b, rtol=0.0, atol=tol) def test_mesh_line(): pts = [0.0, 1.0, 3.0, 4.0] cells = [[0, 1], [1, 2], [2, 3]] mesh = meshplex.Mesh(pts, cells) assert _is_near_equal(mesh.cell_volumes, [1.0, 2.0, 1.0]) assert _is_near_equal(mesh.control_volumes, [0.5, 1.5, 1.5, 0.5]) assert np.all(mesh.is_boundary_point == [True, False, False, True]) # control volume with some index c = mesh.get_control_volume_centroids([False, False, False]) assert _is_near_equal(c, [0.25, 1.25, 2.75, 3.75]) meshplex-0.17.0/tests/test_mesh_tetra.py000066400000000000000000000322761417176457700203670ustar00rootroot00000000000000import pathlib from math import fsum import numpy as np import pytest import meshplex from .helpers import is_near_equal, run this_dir = pathlib.Path(__file__).resolve().parent @pytest.mark.parametrize("a", [0.5, 1.0, 1.33]) # edge length def test_regular_tet0(a): points = ( a * np.array( [ [1.0, 0, 0], [-0.5, +np.sqrt(3.0) / 2.0, 0], [-0.5, -np.sqrt(3.0) / 2.0, 0], [0.0, 0.0, np.sqrt(2.0)], ] ) / np.sqrt(3.0) ) cells = np.array([[0, 1, 2, 3]]) mesh = meshplex.MeshTetra(points, cells.copy()) # test __repr__ print(mesh) assert np.all(mesh.cells("points") == cells) mesh.show() mesh.show_edge(0) tol = 1.0e-14 z = a / np.sqrt(24.0) assert is_near_equal(mesh.cell_circumcenters, [0.0, 0.0, z], tol) assert is_near_equal(mesh.circumcenter_facet_distances, [z, z, z, z], tol) # covolume/edge length ratios # alpha = a / 12.0 / np.sqrt(2) alpha = a / 2 / np.sqrt(24) / np.sqrt(12) vals = mesh.ce_ratios assert is_near_equal( vals, [ [ [alpha, alpha, alpha], [alpha, alpha, alpha], [alpha, alpha, alpha], [alpha, alpha, alpha], ] ], tol, ) # cell volumes vol = a ** 3 / 6.0 / np.sqrt(2) assert is_near_equal(mesh.cell_volumes, [vol], tol) # control volumes val = vol / 4.0 assert is_near_equal(mesh.control_volumes, [val, val, val, val], tol) # inradius # side_area = np.sqrt(3) / 4 * a ** 2 # vol = a ** 3 / 6.0 / np.sqrt(2) # inradius = 3 * vol / (4 * side_area) inradius = a * np.sqrt(6) / 12 assert is_near_equal(mesh.cell_inradius, [inradius], tol) # circumradius circumradius = a * np.sqrt(6) / 4 assert is_near_equal(mesh.cell_circumradius, [circumradius], tol) # cell quality assert is_near_equal(mesh.q_radius_ratio, [1.0], tol) assert is_near_equal(mesh.cell_barycenters, mesh.cell_centroids, tol) assert is_near_equal(mesh.cell_barycenters, [[0.0, 0.0, a * np.sqrt(6) / 12]], tol) assert is_near_equal(mesh.cell_incenters, [[0.0, 0.0, a * np.sqrt(6) / 12]], tol) assert is_near_equal(mesh.q_min_sin_dihedral_angles, [1.0], tol) assert is_near_equal(mesh.q_vol_rms_edgelength3, [1.0], tol) # @pytest.mark.parametrize( # 'a', # basis edge length # [1.0] # ) # def test_regular_tet1_algebraic(a): # points = np.array([ # [0, 0, 0], # [a, 0, 0], # [0, a, 0], # [0, 0, a] # ]) # cells = np.array([[0, 1, 2, 3]]) # tol = 1.0e-14 # # mesh = meshplex.MeshTetra(points, cells, mode='algebraic') # # assert is_near_equal( # mesh.cell_circumcenters, # [[a/2.0, a/2.0, a/2.0]], # tol # ) # # # covolume/edge length ratios # assert is_near_equal( # mesh.ce_ratios_per_edge, # [a/6.0, a/6.0, a/6.0, 0.0, 0.0, 0.0], # tol # ) # # # cell volumes # assert is_near_equal(mesh.cell_volumes, [a**3/6.0], tol) # # # control volumes # assert is_near_equal( # mesh.control_volumes, # [a**3/12.0, a**3/36.0, a**3/36.0, a**3/36.0], # tol # ) @pytest.mark.parametrize( "a", [ # 0.5, 1.0, # 2.0 ], ) # basis edge length def test_unit_tetrahedron_geometric(a): points = np.array([[0, 0, 0], [a, 0, 0], [0, a, 0], [0, 0, a]]) cells = np.array([[0, 1, 2, 3]]) tol = 1.0e-14 mesh = meshplex.MeshTetra(points, cells) assert all((mesh.cells("points") == cells).flat) assert is_near_equal(mesh.cell_circumcenters, [a / 2.0, a / 2.0, a / 2.0], tol) # covolume/edge length ratios ref = np.array( [ [[-a / 24.0], [a / 8.0], [a / 8.0], [0.0]], [[-a / 24.0], [a / 8.0], [0.0], [a / 8.0]], [[-a / 24.0], [0.0], [a / 8.0], [a / 8.0]], ] ) assert is_near_equal(mesh.ce_ratios, ref, tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [a ** 3 / 6.0], tol) # control volumes assert is_near_equal( mesh.control_volumes, [a ** 3 / 8.0, a ** 3 / 72.0, a ** 3 / 72.0, a ** 3 / 72.0], tol, ) assert is_near_equal( mesh.circumcenter_facet_distances.T, [-0.5 / np.sqrt(3) * a, 0.5 * a, 0.5 * a, 0.5 * a], tol, ) # inradius ref = a * 0.2113248654051872 assert is_near_equal(mesh.cell_inradius, [ref], tol) # circumradius ref = a * np.sqrt(3) / 2 assert is_near_equal(mesh.cell_circumradius, [ref], tol) # cell quality ref = 7.320508075688774e-01 assert is_near_equal(mesh.q_radius_ratio, [ref], tol) assert is_near_equal(mesh.q_min_sin_dihedral_angles, [np.sqrt(3) / 2], tol) assert is_near_equal(mesh.q_vol_rms_edgelength3, [4 * np.sqrt(3) / 9], tol) def test_regular_tet1_geometric_order(): a = 1.0 points = np.array([[0, 0, a], [0, 0, 0], [a, 0, 0], [0, a, 0]]) cells = np.array([[0, 1, 2, 3]]) tol = 1.0e-14 mesh = meshplex.MeshTetra(points, cells) assert all((mesh.cells("points") == [0, 1, 2, 3]).flat) assert is_near_equal(mesh.cell_circumcenters, [a / 2.0, a / 2.0, a / 2.0], tol) # covolume/edge length ratios ref = np.array( [ [[0.0], [-a / 24.0], [a / 8.0], [a / 8.0]], [[a / 8.0], [-a / 24.0], [a / 8.0], [0.0]], [[a / 8.0], [-a / 24.0], [0.0], [a / 8.0]], ] ) assert is_near_equal(mesh.ce_ratios, ref, tol) # cell volumes assert is_near_equal(mesh.cell_volumes, [a ** 3 / 6.0], tol) # control volumes assert is_near_equal( mesh.control_volumes, [a ** 3 / 72.0, a ** 3 / 8.0, a ** 3 / 72.0, a ** 3 / 72.0], tol, ) assert is_near_equal( mesh.circumcenter_facet_distances.T, [0.5 * a, -0.5 / np.sqrt(3) * a, 0.5 * a, 0.5 * a], tol, ) # @pytest.mark.parametrize( # 'h', # [1.0, 1.0e-2] # ) # def test_degenerate_tet0(h): # points = np.array([ # [0, 0, 0], # [1, 0, 0], # [0, 1, 0], # [0.5, 0.5, h], # ]) # cells = np.array([[0, 1, 2, 3]]) # mesh = meshplex.MeshTetra(points, cells, mode='algebraic') # # tol = 1.0e-14 # # z = 0.5 * h - 1.0 / (4*h) # assert is_near_equal( # mesh.cell_circumcenters, # [[0.5, 0.5, z]], # tol # ) # # # covolume/edge length ratios # print(h) # print(mesh.ce_ratios) # assert is_near_equal( # mesh.ce_ratios, # [[ # [0.0, 0.0, 0.0], # [3.0/80.0, 3.0/40.0, 3.0/80.0], # [3.0/40.0, 3.0/80.0, 3.0/80.0], # [0.0, 1.0/16.0, 1.0/16.0], # ]], # tol # ) # # [h / 6.0, h / 6.0, 0.0, -1.0/24/h, 1.0/12/h, 1.0/12/h], # # # control volumes # ref = [ # h / 18.0, # 1.0/72.0 * (3*h - 1.0/(2*h)), # 1.0/72.0 * (3*h - 1.0/(2*h)), # 1.0/36.0 * (h + 1.0/(2*h)) # ] # assert is_near_equal(mesh.control_volumes, ref, tol) # # # cell volumes # assert is_near_equal(mesh.cell_volumes, [h/6.0], tol) # @pytest.mark.parametrize( # 'h', # [1.0e-1] # ) # def test_degenerate_tet1(h): # points = np.array([ # [0, 0, 0], # [1, 0, 0], # [0, 1, 0], # [0.25, 0.25, h], # [0.25, 0.25, -h], # ]) # cells = np.array([ # [0, 1, 2, 3], # [0, 1, 2, 4] # ]) # mesh = meshplex.MeshTetra(points, cells, mode='algebraic') # # total_vol = h / 3.0 # # run( # mesh, # total_vol, # [0.18734818957173291, 77.0/720.0], # [2.420625, 5.0/6.0], # [1.0 / np.sqrt(2.0) / 30., 1.0/60.0] # ) def test_cubesmall(): points = np.array( [ [-0.5, -0.5, -5.0], [-0.5, +0.5, -5.0], [+0.5, -0.5, -5.0], [-0.5, -0.5, +5.0], [+0.5, +0.5, -5.0], [+0.5, +0.5, +5.0], [-0.5, +0.5, +5.0], [+0.5, -0.5, +5.0], ] ) cells = np.array( [[0, 1, 2, 3], [1, 2, 4, 5], [1, 2, 3, 5], [1, 3, 5, 6], [2, 3, 5, 7]] ) mesh = meshplex.MeshTetra(points, cells) tol = 1.0e-14 cv = np.ones(8) * 1.25 cellvols = [5.0 / 3.0, 5.0 / 3.0, 10.0 / 3.0, 5.0 / 3.0, 5.0 / 3.0] assert is_near_equal(mesh.control_volumes, cv, tol) assert is_near_equal(mesh.cell_volumes, cellvols, tol) cv_norms = [np.linalg.norm(cv, ord=2), np.linalg.norm(cv, ord=np.Inf)] cellvol_norms = [ np.linalg.norm(cellvols, ord=2), np.linalg.norm(cellvols, ord=np.Inf), ] run(mesh, 10.0, cv_norms, [28.095851618771825, 1.25], cellvol_norms) def test_arrow3d(): points = np.array( [ [+0.0, +0.0, +0.0], [+2.0, -1.0, +0.0], [+2.0, +0.0, +0.0], [+2.0, +1.0, +0.0], [+0.5, +0.0, -0.9], [+0.5, +0.0, +0.9], ] ) cellsNodes = np.array([[1, 2, 4, 5], [2, 3, 4, 5], [0, 1, 4, 5], [0, 3, 4, 5]]) mesh = meshplex.MeshTetra(points, cellsNodes) run( mesh, 1.2, [0.58276428453480922, 0.459], [0.40826901831985885, 0.2295], [np.sqrt(0.45), 0.45], ) assert mesh.num_delaunay_violations == 2 def test_tetrahedron(): mesh = meshplex.read(this_dir / "meshes" / "tetrahedron.vtk") run( mesh, 64.1500299099584, [16.308991595922095, 7.0264329635751395], [6.898476155562041, 0.34400453539215237], [11.571692332290635, 2.9699087921277054], ) # def test_toy_algebraic(): # filename = download_mesh( # 'toy.vtk', # 'f48abda972822bab224b91a74d695573' # ) # mesh, _, _, _ = meshplex.read(filename) # # # Even if the input data has only a small error, the error in the # # ce_ratios can be magnitudes larger. This is demonstrated here: Take # # the same mesh from two different source files with a difference of # # the order of 1e-16. The ce_ratios differ by up to 1e-7. # if False: # print(mesh.cells.keys()) # pts = mesh.points.copy() # pts += 1.0e-16 * np.random.rand(pts.shape[0], pts.shape[1]) # mesh2 = meshplex.MeshTetra(pts, mesh.cells['points']) # # # diff_coords = mesh.points - mesh2.points # max_diff_coords = max(diff_coords.flatten()) # print('||coords_1 - coords_2||_inf = %e' % max_diff_coords) # diff_ce_ratios = mesh.ce_ratios_per_edge - mesh2.ce_ratios # print( # '||ce_ratios_1 - ce_ratios_2||_inf = %e' # % max(diff_ce_ratios) # ) # from matplotlib import pyplot as plt # plt.figure() # n = len(mesh.ce_ratios_per_edge) # plt.semilogy(range(n), diff_ce_ratios) # plt.show() # exit(1) # # run( # mesh, # volume=9.3875504672601107, # convol_norms=[0.20348466631551548, 0.010271101930468585], # ce_ratio_norms=[396.4116343366758, 3.4508458933423918], # cellvol_norms=[0.091903119589148916, 0.0019959463063558944], # tol=1.0e-6 # ) def test_toy_geometric(): mesh = meshplex.read(this_dir / "meshes" / "toy.vtu") mesh = meshplex.MeshTetra(mesh.points, mesh.cells("points")) run( mesh, volume=1.2558406318969064, convol_norms=[0.06426267704764405, 0.006376495345775972], ce_ratio_norms=[2.3838036244886744, 0.061790574680317234], cellvol_norms=[0.03813510075830597, 0.002568756890638246], tol=1.0e-6, ) cc = mesh.cell_circumcenters cc_norm_2 = fsum(cc.flat) cc_norm_inf = max(cc.flat) assert abs(cc_norm_2 - 250.71142559136365) < 1.0e-11, cc_norm_2 assert abs(cc_norm_inf - 1.6682679741134048) < 1.0e-11, cc_norm_inf def test_show_cell(render=False): pytest.importorskip("vtk") points = ( np.array( [ [1.0, 0.0, -1.0 / np.sqrt(8)], [-0.5, +np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], [-0.5, -np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], [0.0, 0.0, np.sqrt(2.0) - 1.0 / np.sqrt(8)], ] ) / np.sqrt(3.0) ) # points = np.array( # [ # [1.0, 0.0, -1.0 / np.sqrt(8)], # [-0.5, +np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], # [-0.5, -np.sqrt(3.0) / 2.0, -1.0 / np.sqrt(8)], # [0.0, 0.5, 1.0], # ] # ) / np.sqrt(3.0) # points = np.array( # [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] # ) cells = [[0, 1, 2, 3]] mesh = meshplex.MeshTetra(points, cells) mesh.show_cell( 0, barycenter_rgba=(1, 0, 0, 1.0), circumcenter_rgba=(0.1, 0.1, 0.1, 1.0), circumsphere_rgba=(0, 1, 0, 1.0), incenter_rgba=(1, 0, 1, 1.0), insphere_rgba=(1, 0, 1, 1.0), face_circumcenter_rgba=(0, 0, 1, 1.0), control_volume_boundaries_rgba=(1.0, 0.0, 0.0, 1.0), line_width=3.0, close=True, render=render, ) if __name__ == "__main__": test_show_cell(render=True) # test_regular_tet0(0.5) # test_toy_geometric() meshplex-0.17.0/tests/test_remove_cells.py000066400000000000000000000124051417176457700207030ustar00rootroot00000000000000import pathlib import numpy as np import pytest import meshplex from .mesh_tri.helpers import assert_mesh_consistency, compute_all_entities def get_mesh0(): # _____________ # | _/ \_ | # | _/ \_ | # |/_________\| # |\_ _/| # | \_ _/ | # |___ \_/____| # points = [ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.5], [0.5, 0.0], [1.0, 0.5], [0.5, 1.0], ] cells = [ [0, 5, 4], [5, 1, 6], [6, 2, 7], [7, 3, 4], [5, 6, 4], [6, 7, 4], ] mesh = meshplex.Mesh(points, cells) mesh.create_facets() return mesh def get_mesh1(): # _____________ # |\_ _/| # | \_ _/ | # | \_/ | # | _/ \_ | # | _/ \_ | # |/_________\| # points = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]] cells = [[0, 1, 4], [1, 2, 4], [2, 3, 4], [0, 4, 3]] return meshplex.Mesh(points, cells) def get_mesh2(): this_dir = pathlib.Path(__file__).resolve().parent mesh0 = meshplex.read(this_dir / "meshes" / "pacman.vtu") return meshplex.Mesh(mesh0.points[:, :2], mesh0.cells("points")) @pytest.mark.parametrize( "remove_idx,expected_num_cells,expected_num_edges", [ # remove corner cell [[0], 5, 11], # remove corner cell [[0, 4], 4, 10], # remove interior cells [[4, 5], 4, 12], # remove no cells at all [[], 6, 13], ], ) def test_remove_cells(remove_idx, expected_num_cells, expected_num_edges): mesh = get_mesh0() assert len(mesh.cells("points")) == 6 assert len(mesh.edges["points"]) == 13 # remove a corner cell mesh.remove_cells(remove_idx) assert len(mesh.cells("points")) == expected_num_cells assert len(mesh.edges["points"]) == expected_num_edges assert_mesh_consistency(mesh) def test_remove_cells_boundary(): mesh = get_mesh1() assert np.all(mesh.is_boundary_point == [True, True, True, True, False]) assert np.all(mesh.is_boundary_facet_local[0] == [False, False, False, False]) assert np.all(mesh.is_boundary_facet_local[1] == [False, False, False, True]) assert np.all(mesh.is_boundary_facet_local[2] == [True, True, True, False]) assert np.all( mesh.is_boundary_facet == [True, True, False, True, False, True, False, False] ) assert np.all(mesh.is_boundary_cell) assert np.all(mesh.facets_cells_idx == [0, 1, 0, 2, 1, 3, 2, 3]) # cell id: assert np.all(mesh.facets_cells["boundary"][1] == [0, 3, 1, 2]) # local edge: assert np.all(mesh.facets_cells["boundary"][2] == [2, 1, 2, 2]) # cell id: assert np.all( mesh.facets_cells["interior"][1:3].T == [[0, 3], [0, 1], [1, 2], [2, 3]] ) # local edge: assert np.all( mesh.facets_cells["interior"][3:5].T == [[1, 2], [0, 1], [0, 1], [0, 0]] ) # now lets remove some cells mesh.remove_cells([0]) assert_mesh_consistency(mesh) assert np.all(mesh.is_boundary_point) assert np.all(mesh.is_boundary_facet_local[0] == [False, False, False]) assert np.all(mesh.is_boundary_facet_local[1] == [True, False, True]) assert np.all(mesh.is_boundary_facet_local[2] == [True, True, True]) assert np.all( mesh.is_boundary_facet == [True, True, True, True, True, False, False] ) assert np.all(mesh.is_boundary_cell) def test_remove_boundary_cell(): mesh = get_mesh0() mesh.remove_boundary_cells( lambda ibc: np.all(mesh.cell_centroids[ibc] > 0.5, axis=1) ) assert mesh.cells("points").shape[0] == 5 def test_remove_all(): points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]] cells = [[0, 1, 2]] mesh = meshplex.Mesh(points, cells) assert np.all(mesh.is_point_used) mesh.remove_cells([0]) assert_mesh_consistency(mesh) assert not np.any(mesh.is_point_used) @pytest.mark.parametrize( "mesh0, remove_cells", [ (get_mesh0(), [0]), (get_mesh1(), [0, 1]), (get_mesh2(), [0, 3, 57, 59, 60, 61, 100]), ], ) def test_reference(mesh0, remove_cells): # some dummy calls to make sure the respective value are computed before the cell # removal and then updated compute_all_entities(mesh0) # now remove some cells mesh0.remove_cells(remove_cells) assert_mesh_consistency(mesh0) def test_remove_duplicate(): # lines points = [0.0, 0.1, 0.7, 1.0] cells = [[0, 1], [1, 2], [2, 1], [0, 1], [3, 2]] mesh = meshplex.Mesh(points, cells) n = mesh.remove_duplicate_cells() assert n == 2 assert np.all(mesh.cells("points") == [[0, 1], [1, 2], [3, 2]]) # triangle points = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] cells = [[0, 2, 3], [0, 1, 2], [0, 2, 1]] mesh = meshplex.Mesh(points, cells) n = mesh.remove_duplicate_cells() assert n == 1 assert np.all(mesh.cells("points") == [[0, 2, 3], [0, 1, 2]]) # tetrahedra points = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] cells = [[0, 1, 2, 3], [3, 1, 2, 0]] mesh = meshplex.Mesh(points, cells) n = mesh.remove_duplicate_cells() assert n == 1 assert np.all(mesh.cells("points") == [[0, 1, 2, 3]]) if __name__ == "__main__": test_remove_cells_boundary() meshplex-0.17.0/tests/test_signed_area.py000066400000000000000000000031661417176457700204710ustar00rootroot00000000000000import pathlib import meshio import numpy as np import pytest import meshplex this_dir = pathlib.Path(__file__).resolve().parent @pytest.mark.parametrize( "points,cells,ref", [ # line ([[0.0], [0.35]], [[0, 1]], [0.35]), ([[0.0], [0.35]], [[1, 0]], [-0.35]), # triangle ([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [[0, 1, 2]], [0.5]), ([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], [[0, 1, 2]], [-0.5]), ( [[0.0, 0.0], [1.0, 0.0], [1.1, 1.0], [0.0, 1.0]], [[0, 1, 2], [0, 3, 2]], [0.5, -0.55], ), # tetra ( [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[0, 1, 2, 3]], [1 / 6], ), ( [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[0, 1, 3, 2]], [-1 / 6], ), ], ) def test_signed_area(points, cells, ref): mesh = meshplex.Mesh(points, cells) ref = np.array(ref) assert mesh.signed_cell_volumes.shape == ref.shape assert np.all( np.abs(ref - mesh.signed_cell_volumes) < np.abs(ref) * 1.0e-13 + 1.0e-13 ) def test_signed_area_pacman(): mesh = meshio.read(this_dir / "meshes" / "pacman.vtu") assert np.all(np.abs(mesh.points[:, 2]) < 1.0e-15) X = mesh.points[:, :2] mesh = meshplex.Mesh(X, mesh.get_cells_type("triangle")) vols = mesh.signed_cell_volumes # all cells are positively oriented in this mesh assert np.all(mesh.signed_cell_volumes > 0.0) assert np.all(abs(abs(vols) - mesh.cell_volumes) < 1.0e-12 * mesh.cell_volumes) meshplex-0.17.0/tests/test_subdomain.py000066400000000000000000000041211417176457700202010ustar00rootroot00000000000000import pathlib import meshplex this_dir = pathlib.Path(__file__).resolve().parent def test_get_edges(): mesh = meshplex.read(this_dir / "meshes" / "pacman.vtu") mesh.create_facets() edge_mask = mesh.get_edge_mask() edge_points = mesh.edges["points"][edge_mask] assert len(edge_points) == 2372 def test_mark_subdomain2d(): mesh = meshplex.read(this_dir / "meshes" / "pacman.vtu") class Subdomain1: is_boundary_only = True # pylint: disable=no-self-use def is_inside(self, x): return x[0] < 0.0 class Subdomain2: is_boundary_only = False # pylint: disable=no-self-use def is_inside(self, x): return x[0] > 0.0 sd1 = Subdomain1() vertex_mask = mesh.get_vertex_mask(sd1) assert vertex_mask.sum() == 45 face_mask = mesh.get_face_mask(sd1) assert face_mask.sum() == 44 cell_mask = mesh.get_cell_mask(sd1) assert cell_mask.sum() == 0 sd2 = Subdomain2() vertex_mask = mesh.get_vertex_mask(sd2) assert vertex_mask.sum() == 395 face_mask = mesh.get_face_mask(sd2) assert face_mask.sum() == 2148 cell_mask = mesh.get_cell_mask(sd2) assert cell_mask.sum() == 706 def test_mark_subdomain3d(): mesh = meshplex.read(this_dir / "meshes" / "tetrahedron.vtk") class Subdomain1: is_boundary_only = True # pylint: disable=no-self-use def is_inside(self, x): return x[0] < 0.5 class Subdomain2: is_boundary_only = False # pylint: disable=no-self-use def is_inside(self, x): return x[0] > 0.5 sd1 = Subdomain1() vertex_mask = mesh.get_vertex_mask(sd1) assert vertex_mask.sum() == 16 face_mask = mesh.get_face_mask(sd1) assert face_mask.sum() == 20 cell_mask = mesh.get_cell_mask(sd1) assert cell_mask.sum() == 0 sd2 = Subdomain2() vertex_mask = mesh.get_vertex_mask(sd2) assert vertex_mask.sum() == 10 face_mask = mesh.get_face_mask(sd2) assert face_mask.sum() == 25 cell_mask = mesh.get_cell_mask(sd2) assert cell_mask.sum() == 5 meshplex-0.17.0/tests/test_volumes.py000066400000000000000000000027371417176457700177250ustar00rootroot00000000000000import numpy as np import meshplex def test_mesh_line(): pts = [0.0, 1.0, 3.0, 4.0] cells = [[0, 1], [1, 2], [2, 3]] mesh = meshplex.Mesh(pts, cells) print(mesh.cell_volumes) ref = [1.0, 2.0, 1.0] assert np.all(np.abs(mesh.cell_volumes - ref) < np.abs(ref) * 1.0e-13) def test_vol_tri(): # two triangles in 5D points = [ [0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0], ] cells = [[0, 1, 2], [0, 3, 2]] mesh = meshplex.Mesh(points, cells) ref = [0.5, 0.5] print(mesh.cell_volumes) assert np.all(np.abs(mesh.cell_volumes - ref) < np.abs(ref) * 1.0e-13) def test_vol_tetra(): points = [ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] cells = [[0, 1, 2, 3]] mesh = meshplex.Mesh(points, cells) ref = [1 / 6] print(mesh.cell_volumes) assert np.all(np.abs(mesh.cell_volumes - ref) < np.abs(ref) * 1.0e-13) def test_vol_simplex5(): points = [ [0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ] cells = [[0, 1, 2, 3, 4]] mesh = meshplex.Mesh(points, cells) ref = [1 / 24] print(mesh.cell_volumes) assert np.all(np.abs(mesh.cell_volumes - ref) < np.abs(ref) * 1.0e-13) if __name__ == "__main__": # test_vol_tri() test_vol_tetra() meshplex-0.17.0/tox.ini000066400000000000000000000003171417176457700147630ustar00rootroot00000000000000[tox] envlist = py3 isolated_build = True [testenv] deps = meshzoo >= 0.9.0 pytest pytest-cov pytest-codeblocks pytest-randomly extras = all commands = pytest {posargs} --codeblocks