pax_global_header00006660000000000000000000000064147014256720014522gustar00rootroot0000000000000052 comment=8ddb83037009d88d6cf67811f90766d7a0159c3c ufl-2024.2.0/000077500000000000000000000000001470142567200125375ustar00rootroot00000000000000ufl-2024.2.0/.github/000077500000000000000000000000001470142567200140775ustar00rootroot00000000000000ufl-2024.2.0/.github/FUNDING.yml000066400000000000000000000002141470142567200157110ustar00rootroot00000000000000# These are supported funding model platforms github: FEniCS # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] ufl-2024.2.0/.github/workflows/000077500000000000000000000000001470142567200161345ustar00rootroot00000000000000ufl-2024.2.0/.github/workflows/build-wheels.yml000066400000000000000000000042711470142567200212470ustar00rootroot00000000000000name: Build wheels # By default this action does not push to test or production PyPI. The wheels # are available as an artifact that can be downloaded and tested locally. on: workflow_dispatch: inputs: ufl_ref: description: "UFL git ref to checkout" default: "main" type: string test_pypi_publish: description: "Publish to Test PyPi (true | false)" default: false type: boolean pypi_publish: description: "Publish to PyPi (true | false)" default: false type: boolean workflow_call: inputs: ufl_ref: description: "UFL git ref to checkout" default: "main" type: string test_pypi_publish: description: "Publish to Test PyPi (true | false)" default: false type: boolean pypi_publish: description: "Publish to PyPi (true | false))" default: false type: boolean jobs: build: name: Build wheels and source distributions runs-on: ubuntu-latest steps: - name: Checkout UFL uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.ufl_ref }} - name: Upgrade pip and setuptools run: python -m pip install setuptools pip build --upgrade - name: Build sdist and wheel run: python -m build . - uses: actions/upload-artifact@v4 with: path: dist/* upload_pypi: name: Upload to PyPI (optional) needs: [build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: artifact path: dist - name: Push to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: ${{ github.event.inputs.pypi_publish == 'true' }} with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} repository-url: https://upload.pypi.org/legacy/ - name: Push to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: ${{ github.event.inputs.test_pypi_publish == 'true' }} with: user: __token__ password: ${{ secrets.PYPI_TEST_TOKEN }} repository-url: https://test.pypi.org/legacy/ ufl-2024.2.0/.github/workflows/fenicsx-tests.yml000066400000000000000000000047071470142567200214660ustar00rootroot00000000000000# This workflow will install Basix, FFCx, DOLFINx and run the DOLFINx and FFCx unit tests. name: FEniCSx integration on: pull_request: branches: - main jobs: ffcx-tests: name: Run FFCx tests runs-on: ubuntu-latest env: CC: gcc-10 CXX: g++-10 PETSC_ARCH: linux-gnu-real-64 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.12 - name: Install test dependencies run: | sudo apt-get install -y graphviz libgraphviz-dev ninja-build pkg-config - name: Install UFL run: | pip3 install --break-system-packages . - name: Install Basix run: | python3 -m pip install git+https://github.com/FEniCS/basix.git - name: Clone FFCx uses: actions/checkout@v4 with: path: ./ffcx repository: FEniCS/ffcx ref: main - name: Install FFCx run: | cd ffcx pip install .[ci] - name: Run FFCx unit tests run: python3 -m pytest -n auto ffcx/test dolfinx-tests: name: Run DOLFINx tests runs-on: ubuntu-latest container: ghcr.io/fenics/test-env:current-openmpi env: PETSC_ARCH: linux-gnu-complex128-32 OMPI_ALLOW_RUN_AS_ROOT: 1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1 steps: - uses: actions/checkout@v4 - name: Install UFL run: | pip3 install --break-system-packages . - name: Install Basix and FFCx run: | python3 -m pip install --break-system-packages git+https://github.com/FEniCS/basix.git python3 -m pip install --break-system-packages git+https://github.com/FEniCS/ffcx.git - name: Clone DOLFINx uses: actions/checkout@v4 with: path: ./dolfinx repository: FEniCS/dolfinx ref: main - name: Install DOLFINx run: | cmake -G Ninja -DCMAKE_BUILD_TYPE=Developer -B build -S dolfinx/cpp/ cmake --build build cmake --install build python3 -m pip install --break-system-packages -r dolfinx/python/build-requirements.txt python3 -m pip -v install --break-system-packages --no-build-isolation --check-build-dependencies --config-settings=cmake.build-type="Developer" dolfinx/python/[ci] - name: Run DOLFINx unit tests run: python3 -m pytest -n auto dolfinx/python/test/unit ufl-2024.2.0/.github/workflows/pythonapp.yml000066400000000000000000000070421470142567200207040ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint # with a single version of Python For more information see: # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: UFL CI on: push: branches: - "**" tags: - "**" pull_request: branches: - main jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] exclude: - os: macos-latest python-version: '3.8' - os: macos-latest python-version: '3.9' include: - os: windows-latest python-version: '3.11' steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Lint with ruff run: | pip install ruff ruff check . ruff format --check . - name: Install UFL run: python -m pip install .[ci] - name: Run unit tests run: | python -m pytest -n auto --cov=ufl/ --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml test/ - name: Upload to Coveralls if: ${{ github.repository == 'FEniCS/ufl' && github.head_ref == '' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' }} env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} run: coveralls - name: Build documentation run: | cd doc/sphinx make html - name: Upload documentation artifact uses: actions/upload-artifact@v4 with: name: doc-${{ matrix.os }}-${{ matrix.python-version }} path: doc/sphinx/build/html/ retention-days: 2 if-no-files-found: error - name: Checkout FEniCS/docs if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.8 }} uses: actions/checkout@v4 with: repository: "FEniCS/docs" path: "docs" ssh-key: "${{ secrets.SSH_GITHUB_DOCS_PRIVATE_KEY }}" - name: Set version name if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.8 }} run: | echo "VERSION_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Copy documentation into repository if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.8 }} run: | cd docs git rm -r --ignore-unmatch ufl/${{ env.VERSION_NAME }} mkdir -p ufl/${{ env.VERSION_NAME }} cp -r ../doc/sphinx/build/html/* ufl/${{ env.VERSION_NAME }} - name: Commit and push documentation to FEniCS/docs if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.8 }} run: | cd docs git config --global user.email "fenics@github.com" git config --global user.name "FEniCS GitHub Actions" git add --all git commit --allow-empty -m "Python FEniCS/ufl@${{ github.sha }}" git push ufl-2024.2.0/.github/workflows/spack.yml000066400000000000000000000033561470142567200177670ustar00rootroot00000000000000name: Spack build on: # Uncomment the below 'push' to trigger on push # push: # branches: # - "**" schedule: # '*' is a special character in YAML, so string must be quoted - cron: "0 2 * * WED" workflow_dispatch: inputs: spack_repo: description: "Spack repository to test" default: "spack/spack" type: string spack_ref: description: "Spack repository branch/tag to test" default: "develop" type: string jobs: build: runs-on: ubuntu-latest container: ubuntu:latest steps: - name: Install Spack requirements run: | apt-get -y update apt-get install -y bzip2 curl file git gzip make patch python3-minimal tar xz-utils apt-get install -y g++ gfortran # compilers - name: Get Spack if: github.event_name != 'workflow_dispatch' uses: actions/checkout@v4 with: path: ./spack repository: spack/spack - name: Get Spack if: github.event_name == 'workflow_dispatch' uses: actions/checkout@v4 with: path: ./spack repository: ${{ github.event.inputs.spack_repo }} ref: ${{ github.event.inputs.spack_ref }} - name: Install UFL development version and run tests run: | . ./spack/share/spack/setup-env.sh spack env create main spack env activate main spack add py-fenics-ufl@main spack install --test=root - name: Install UFL release version and run tests run: | . ./spack/share/spack/setup-env.sh spack env create release spack env activate release spack add py-fenics-ufl spack install --test=root ufl-2024.2.0/.github/workflows/tsfc-tests.yml000066400000000000000000000022751470142567200207640ustar00rootroot00000000000000# This workflow will install tsfc and run its unit tests name: tsfc integration on: pull_request: branches: - main jobs: tsfc-tests: name: Run TSFC tests runs-on: ubuntu-latest env: CC: gcc-10 CXX: g++-10 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: 3.9 - name: Install UFL run: | pip3 install . - name: Clone tsfc uses: actions/checkout@v3 with: path: ./tsfc repository: firedrakeproject/tsfc ref: mscroggs/gdim - name: Install tsfc run: | cd tsfc pip install -r requirements-ext.txt pip install git+https://github.com/coneoproject/COFFEE.git#egg=coffee pip install git+https://github.com/firedrakeproject/fiat.git#egg=fenics-fiat pip install git+https://github.com/FInAT/FInAT.git@mscroggs/gdim#egg=finat pip install git+https://github.com/firedrakeproject/loopy.git#egg=loopy pip install .[ci] pip install pytest - name: Run tsfc unit tests run: python3 -m pytest tsfc/tests ufl-2024.2.0/AUTHORS000066400000000000000000000027101470142567200136070ustar00rootroot00000000000000Authors ======= Authors: | Martin Sandve Alnæs | Anders Logg Contributors: | Kristian B. Ølgaard | Garth N. Wells | Marie E. Rognes | Kent-Andre Mardal | Johan Hake | David Ham | Florian Rathgeber | Andrew T. T. McRae | Lawrence Mitchell | Johannes Ring | Aslak Bergersen | Chris Richardson | Massimiliano Leoni | Jan Blechta | Graham Markall | Lizao Li | Miklos Homolya | Matthias Liertzer | Maximilian Albert | Corrado Maurini | Jack S. Hale | Tuomas Airaksinen | Nacime Bouziani | Reuben W. Hill | Nacime Bouziani | Matthew Scroggs ufl-2024.2.0/CODE_OF_CONDUCT.md000066400000000000000000000072501470142567200153420ustar00rootroot00000000000000Code 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 both within project spaces and in public spaces when an individual is representing the project or its community. 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 fenics-steering-council@googlegroups.com. Alternatively, you may report individually to one of the members of the Steering Council. 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. If you feel that your report has not been followed up satisfactorily, then you may contact our parent organisation NumFOCUS at info@numfocus.org for further redress. Attribution ----------- This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. Adaptations ----------- * Allow reporting to individual Steering Council members * Added the option to contact NumFOCUS for further redress. For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faqufl-2024.2.0/COPYING000066400000000000000000001045131470142567200135760ustar00rootroot00000000000000 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 . ufl-2024.2.0/COPYING.LESSER000066400000000000000000000167271470142567200146030ustar00rootroot00000000000000 GNU LESSER 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. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ufl-2024.2.0/ChangeLog.md000066400000000000000000000371061470142567200147170ustar00rootroot00000000000000# Changelog ## 2021.1.0 - Mixed dimensional domain support ## 2019.1.0 (2019-04-17) - Remove scripts - Remove LaTeX support (not functional) - Add support for complex valued elements; complex mode is chosen by `compute_form_data(form, complex_mode=True)` typically by a form compiler; otherwise UFL language is agnostic to the choice of real/complex domain ## 2018.1.0 (2018-06-14) - Remove python2 support ## 2017.2.0 (2017-12-05) - Add geometric quantity `CellDiameter` defined as a set diameter of the cell, i.e., maximal distance between any two points of the cell; implemented on simplices and quads/hexes - Rename internally used reference quantities `(Cell|Facet)EdgeVectors` to `Reference(Cell|Facet)EdgeVectors` - Add internally used quantites `CellVertices`, `(Cell|Facet)EdgeVectors` which are physical-coordinates-valued; will be useful for further geometry lowering implementations for quads/hexes - Implement geometry lowering of `(Min|Max)(Cell|Facet)EdgeLength` for quads and hexes ## 2017.1.0.post1 (2017-09-12) - Change PyPI package name to fenics-ufl. ## 2017.1.0 (2017-05-09) - Add the `DirectionalSobolevSpace` subclass of `SobolevSpace`. This allows one to use spaces where elements have varying continuity in different spatial directions. - Add `sobolev_space` methods for `HDiv` and `HCurl` finite elements. - Add `sobolev_space` methods for `TensorProductElement` and `EnrichedElement`. The smallest shared Sobolev space will be returned for enriched elements. For the tensor product elements, a `DirectionalSobolevSpace` is returned depending on the order of the spaces associated with the component elements. ## 2016.2.0 (2016-11-30) - Add call operator syntax to `Form` to replace arguments and coefficients. This makes it easier to e.g. express the norm defined by a bilinear form as a functional. Example usage: ```python # Equivalent to replace(a, {u: f, v: f}) M = a(f, f) # Equivalent to replace(a, {f:1}) c = a(coefficients={f:1}) ``` - Add call operator syntax to `Form` to replace arguments and coefficients: ```python a(f, g) == replace(a, {u: f, v: g}) a(coefficients={f:1}) == replace(a, {f:1}) ``` - Add `@` operator to `Form`: `form @ f == action(form, f)` (python 3.5+ only) - Reduce noise in Mesh str such that `print(form)` gets more short and readable - Fix repeated `split(function)` for arbitrary nested elements - EnrichedElement: Remove `+/*` warning In the distant past, `A + B => MixedElement([A, B])`. The change that `A + B => EnrichedElement([A, B])` was made in `d622c74` (22 March 2010). A warning was introduced in `fcbc5ff` (26 March 2010) that the meaning of `+` had changed, and that users wanting a `MixedElement` should use `*` instead. People have, presumably, been seeing this warning for 6 1/2 years by now, so it's probably safe to remove. - Rework `TensorProductElement` implementation, replaces `OuterProductElement` - Rework `TensorProductCell` implementation, replaces `OuterProductCell` - Remove `OuterProductVectorElement` and `OuterProductTensorElement` - Add `FacetElement` and `InteriorElement` - Add `Hellan-Herrmann-Johnson` element - Add support for double covariant and contravariant mappings in mixed elements - Support discontinuous Taylor elements on all simplices - Some more performance improvements - Minor bugfixes - Improve Python 3 support - More permissive in integer types accepted some places - Make ufl pass almost all flake8 tests - Add bitbucket pipelines testing - Improve documentation ## 2016.1.0 (2016-06-23) - Add operator A^(i,j) := as_tensor(A, (i,j)) - Updates to old manual for publishing on fenics-ufl.readthedocs.org - Bugfix for ufl files with utf-8 encoding - Bugfix in conditional derivatives to avoid inf/nan values in generated code. This bugfix may break ffc if uflacs is not used, to get around that the old workaround in ufl can be enabled by setting ufl.algorithms.apply_derivatives.CONDITIONAL_WORKAROUND = True at the top of your program. - Allow sum([expressions]) where expressions are nonscalar by defining expr+0==expr - Allow form=0; form -= other; - Deprecate .cell(), .domain(), .element() in favour of .ufl_cell(), .ufl_domain(), .ufl_element(), in multiple classes, to allow closer integration with dolfin. - Remove deprecated properties cell.{d,x,n,volume,circumradius,facet_area}. - Remove ancient form2ufl script - Add new class Mesh to replace Domain - Add new class FunctionSpace(mesh, element) - Make FiniteElement classes take Cell, not Domain. - Large reworking of symbolic geometry pipeline - Implement symbolic Piola mappings ## 1.6.0 (2015-07-28) - Change approach to attaching __hash__ implementation to accomodate python 3 - Implement new non-recursive traversal based hash computation - Allow derivative(M, ListTensor(), ...) just like list/tuple works - Add traits is_in_reference_frame, is_restriction, is_evaluation, is_differential - Add missing linear operators to ArgumentDependencyExtractor - Add _ufl_is_literal_ type trait - Add _ufl_is_terminal_modifier_ type trait and Expr._ufl_terminal_modifiers_ list - Add new types ReferenceDiv and ReferenceCurl - Outer product element support in degree estimation - Add TraceElement, InteriorElement, FacetElement, BrokenElement - Add OuterProductCell to valid Real elements - Add _cache member to form for use by external frameworks - Add Sobolev space HEin - Add measures dI,dO,dC for interface, overlap, cutcell - Remove Measure constants - Remove cell2D and cell3D - Implement reference_value in apply_restrictions - Rename point integral to vertex integral and kept `*dP` syntax - Replace lambda functions in ufl_type with named functions for nicer stack traces - Minor bugfixes, removal of unused code and cleanups ## 1.5.0 (2015-01-12) - Require Python 2.7 - Python 3 support - Change to py.test - Rewrite parts of expression representation core, providing significant optimizations in speed and memory use, as well as a more elaborate type metadata system for internal use - Use expr.ufl_shape instead of ufl.shape() - Use expr.ufl_indices instead of ufl.indices(), returns tuple of free index ids, not Index objects - Use expr.ufl_index_dimensions instead of ufl.index_dimensions(), returns tuple of dimensions ordered corresponding to expr.ufl_indices, not a dict - Rewrite core algorithms for expression traversal - Add new core algorithms map_expr_dag(), map_integrand_dag(), similar to python map() but applying a callable MultiFunction recursively to each Expr node, without Python recursion - Highly recommend rewriting algorithms based on Transformer using map_expr_dag and MultiFunction, avoiding Python recursion overhead - Rewrite core algorithms apply_derivatives, apply_restrictions - Form signature is now computed without applying derivatives first, introducing smaller overhead on jit cache hits - Use form.signature() to compute form signature - Use form.arguments() instead of extract_arguments(form) - Use form.coefficients() instead of extract_coefficients(form) - Small improvement to str and latex output of expressions - Allow diff(expr, coefficient) without wrapping coefficient in variable - Add keywords to measures: dx(..., degree=3, rule="canonical") - Introduce notation from the Periodic Table of the Finite Elements - Introduce notation for FEEC families of elements: P-, P, Q-, S - Experimental support for high-order geometric domains - Algorithms for symbolic rewriting of geometric quantities (used by uflacs) - Remove the *Constant* classes, using Coefficient with a Real element instead - Add types for MinValue and MaxValue - Disable automatic rewriting a+a->2*a, a*a->a**2, a/a->1, these are costly and the compiler should handle them instead - Fix signature stability w.r.t. metadata dicts - Minor bugfixes, removal of unused code and cleanups ## 1.4.0 (2014-06-02) - New integral type custom_integral (`*dc`) - Add analysis of which coefficients each integral actually uses to optimize assembly - Improved svg rendering of cells and sobolevspaces in ipython notebook - Add sobolev spaces, use notation "element in HCurl" (HCurl, HDiv, H1, H2, L2) - Improved error checking of facet geometry in non-facet integrals - Improved restriction handling, restricting continuous coefficients and constants is now optional - Introduce notation from the Periodic Table of the Finite Elements (draft) - Remove alias "Q" for quadrature element, use "Quadrature" - New derivative type ReferenceGrad - New discontinuous RT element - New geometry types Jacobian, JacobianInverse, JacobianDeterminant - New geometry types FacetJacobian, FacetJacobianInverse, FacetJacobianDeterminant - New geometry types CellFacetJacobian, CellFacetJacobianInverse, CellFacetJacobianDeterminant - New geometry types FacetOrigin, CellOrigin - New geometry types CellCoordinate, FacetCoordinate - New geometry types CellNormal, CellOrientation, QuadratureWeight - Argument (and TestFunction, TrialFunction) now use absolute numbering f.number() instead of relative f.count() - New syntax: integrand*dx(domain) - New syntax: integrand*dx(1, domain=domain) - New syntax: integrand*dx(1, subdomain_data=domain_data) - Using domain instead of cell in many places. - Deprecated notation 'cell.n', 'cell.x' etc. - Recommended new notation: FacetNormal(domain) - Experimental: Argument (and TestFunction, TrialFunction) now can have a specified part index for representing block systems - Experimental: Domains can now be created with a Coefficient providing coordinates: Domain(Coefficient(VectorElement("CG", domain, 2))) - Experimental: New concept Domain: domain = Domain(triangle, geometric_dimension=3, label="MyDomain") - Various general optimizations - Various minor bugfixes - Various docstring improvements ## 1.3.0 (2014-01-07) - Add cell_avg and facet_avg operators, can be applied to a Coefficient or Argument or restrictions thereof - Fix bug in cofactor: now it is transposed the correct way. - Add cell.min_facet_edge_length - Add cell.max_facet_edge_length - Simplify 0^f -> 0 if f is a non-negative scalar value - Add atan2 function - Allow form+0 -> form ## 1.2.0 (2013-03-24) - NB! Using shapes such as (1,) and (1,1) instead of () for 1D tensor quantities I, x, grad(f) - Add cell.facet_diameter - Add new concept Domain - Add new concept Region, which is the union of numbered subdomains - Add integration over regions (which may be overlapping by sharing subdomains) - Add integration over everywhere - Add functions cosh, sinh, tanh, Max, Min - Generalize jump(v,n) for rank(v) > 2 - Fix some minor bugs ## 1.1.0 (2013-01-07) - Add support for pickling of expressions (thanks to Graham Markall) - Add shorthand notation A**2 == inner(A, A), special cased for power 2. - Add support for measure sum notation f*(dx(0) + dx(3)) == f*dx(0) + f*dx(3) - Supporting code for bugfix in PyDOLFIN when comparing test/trial functions - Remove support for tuple form notation as this was ambiguous - Bugfix in quadrature degree estimation, never returning <0 now - Remove use of cmp to accomodate removal from python 3 ## 1.1-alpha-prerelease (2012-11-18) (Not released, snapshot archived with submission of UFL journal paper) - Support adding 0 to forms, allowing sum([a]) - Major memory savings and optimizations. - Some bugfixes. - Add perp operator. - Support nested tuple syntax like MixedElement((U,V),W) - Allow outer(a, b, c, ...) by recursive application from left. - Add simplification f/f -> 1 - Add operators <,>,<=,>= in place of lt,gt,le,ge ## 1.0.0 (2011-12-07) - No changes since rc1. ## 1.0-rc1 (2011-11-22) - Added tests covering snippets from UFL chapter in FEniCS book - Added more unit tests - Added operators diag and diag_vector - Added geometric quantities cell.surface_area and cell.facet_area - Fixed rtruediv bug - Fixed bug with derivatives of elements of type Real with unspecified cell ## 1.0-beta3 (2011-10-26) - Added nabla_grad and nabla_div operators - Added error function erf(x) - Added bessel functions of first and second kind, normal and modified, bessel_J(nu, x), bessel_Y(nu, x), bessel_I(nu, x), bessel_K(nu, x) - Extended derivative() to allow indexed coefficient(s) as differentiation variable - Made `*Constant` use the `Real` space instead of `DG0` - Bugfix in adjoint where test and trial functions were in different spaces - Bugfix in replace where the argument to a grad was replaced with 0 - Bugfix in reconstruction of tensor elements - Some other minor bugfixes ## 1.0-beta2 (2011-08-11) - Support c*form where c depends on a coefficient in a Real space ## 1.0-beta (2011-07-08) - Add script ufl-version - Added syntax for associating an arbitrary domain data object with a measure: dss = ds[boundaries]; M = f*dss(1) + g*dss(2) - Added new operators elem_mult, elem_div, elem_pow and elem_op for elementwise application of scalar operators to tensors of equal shape - Added condition operators And(lhs,rhs) and Or(lhs,rhs) and Not(cond) - Fixed support for symmetries in subelements of a mixed element - Add support for specifying derivatives of coefficients to derivative() ## 0.9.1 (2011-05-16) - Remove set_foo functions in finite element classes - Change license from GPL v3 or later to LGPL v3 or later - Change behavior of preprocess(), form.compute_form_data(), form_data.preprocessed_form - Allowing grad, div, inner, dot, det, inverse on scalars - Simplify Identity(1) -> IntValue(1) automatically - Added Levi-Cevita symbol: e = PermutationSymbol(3); e[i,j,k] - Fix bug with future division behaviour (ufl does not support floor division) - Add subdomain member variables to form class - Allow action on forms of arbitrary rank ## 0.9.0 (2011-02-23) - Allow jump(Sigma, n) for matrix-valued expression Sigma - Bug fix in scalar curl operator - Bug fix in deviatoric operator ## 0.5.4 (2010-09-01) - Bug fixes in PartExtracter - Do not import x for coordinate - Add Circumradius to Cell (Cell.circumradius) - Add CellVolume to Cell (Cell.volume) ## 0.5.3 (2010-07-01) - Rename ElementRestriction --> RestrictedElement - Experimental import of x from tetrahedron - Make lhs/rhs work for resrictions - Redefine operator + for FiniteElements and replace + by * - Rename ElementUnion -> EnrichedElement - Add support for tan() and inverse trigonometric functions ## 0.5.2 (2010-02-15) - Attach form data to preprocessed form, accessible by form.form_data() ## 0.5.1 (2010-02-03) - Fix bug in propagate_restriction ## 0.5.0 (2010-02-01) - Several interface changes in FormData class - Introduce call preprocess(form) to be called at beginning of compilation - Rename BasisFunction --> Argument - Rename Function --> Coefficient ## 0.4.1 (2009-12-04) - Redefine grad().T --> grad() - New meaning of estimate_max_polynomial_degree - New function estimate_total_polynomial_degree - Allow degree = None and cell = None for elements ## 0.4.0 (2009-09-23) - Extensions for ElementRestriction (restrict FiniteElement to Cell) - Bug fix for lhs/rhs with list tensor types - Add new log function set_prefix - Add new log function log(level, message) - Added macro cell integral `*dE` - Added mechanism to add additional integral types - Added LiftingOperator and LiftingFunction - Added ElementRestriction ## 0.3.0 (2009-05-28) - Some critical bugfixes, in particular in differentiation. - Added form operators "system" and "sensitivity_rhs". - diff can take form as argument, applies to all integrands. - Rudimentary precedence handling for better use of parentheses in str(expression). - Added script ufl2py, mainly for debugging purposes. - Crude implementation of estimate_max_polynomial_degree for quadrature degree estimation. - Improved manual. ## 0.2.0 (2009-04-07) - Initial release of UFL. ## 0.1.0 (unreleased) - Unreleased development versions of UFL. ufl-2024.2.0/MANIFEST.in000066400000000000000000000002701470142567200142740ustar00rootroot00000000000000include AUTHORS include COPYING include COPYING.LESSER include ChangeLog.rst recursive-include demo * recursive-include doc * recursive-include test * global-exclude __pycache__ *.pyc ufl-2024.2.0/README.md000066400000000000000000000026171470142567200140240ustar00rootroot00000000000000# UFL - Unified Form Language The Unified Form Language (UFL) is a domain specific language for declaration of finite element discretizations of variational forms. More precisely, it defines a flexible interface for choosing finite element spaces and defining expressions for weak forms in a notation close to mathematical notation. UFL is part of the FEniCS Project. For more information, visit https://www.fenicsproject.org [![UFL CI](https://github.com/FEniCS/ufl/workflows/UFL%20CI/badge.svg)](https://github.com/FEniCS/ufl/workflows/UFL%20CI) [![Coverage Status](https://coveralls.io/repos/github/FEniCS/ufl/badge.svg?branch=master)](https://coveralls.io/github/FEniCS/ufl?branch=master) ## Documentation Documentation can be viewed at https://docs.fenicsproject.org ## License This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . ufl-2024.2.0/demo/000077500000000000000000000000001470142567200134635ustar00rootroot00000000000000ufl-2024.2.0/demo/Constant.py000066400000000000000000000027101470142567200156260ustar00rootroot00000000000000# Copyright (C) 2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnes, 2009 # # Test form for scalar and vector constants. from ufl import ( Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant, dot, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) c = Constant(domain) d = VectorConstant(domain) a = c * dot(grad(v), grad(u)) * dx L = inner(d, grad(v)) * dx ufl-2024.2.0/demo/ConvectionJacobi.py000066400000000000000000000012021470142567200172470ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) w = Coefficient(space) a = dot(dot(u, grad(w)) + dot(w, grad(u)), v) * dx ufl-2024.2.0/demo/ConvectionJacobi2.py000066400000000000000000000011311470142567200173320ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, i, j, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) w = Coefficient(space) a = (u[j] * w[i].dx(j) + w[j] * u[i].dx(j)) * v[i] * dx ufl-2024.2.0/demo/ConvectionVector.py000066400000000000000000000010371470142567200173300ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, dot, dx, grad, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) w = Coefficient(space) a = dot(dot(w, grad(w)), v) * dx ufl-2024.2.0/demo/Elasticity.py000066400000000000000000000012201470142567200161420ustar00rootroot00000000000000# # Author: Anders Logg # Modified by: Martin Sandve Alnes # Date: 2009-01-12 # from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, tetrahedron from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) def epsilon(v): Dv = grad(v) return 0.5 * (Dv + Dv.T) a = inner(epsilon(v), epsilon(u)) * dx ufl-2024.2.0/demo/EnergyNorm.py000066400000000000000000000024071470142567200161250ustar00rootroot00000000000000# Copyright (C) 2005-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # This example demonstrates how to define a functional, here # the energy norm (squared) for a reaction-diffusion problem. from ufl import Coefficient, FunctionSpace, Mesh, dot, dx, grad, tetrahedron from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Coefficient(space) a = (v * v + dot(grad(v), grad(v))) * dx ufl-2024.2.0/demo/Equation.py000066400000000000000000000037261470142567200156320ustar00rootroot00000000000000# Copyright (C) 2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Specification of a system F(v, u) = 0 and extraction of # the bilinear and linear forms a and L for the left- and # right-hand sides: # # F(v, u) = a(v, u) - L(v) = 0 # # The example below demonstrates the specification of the # linear system for a cG(1)/Crank-Nicholson time step for # the heat equation. # # The below formulation is equivalent to writing # # a = v*u*dx + 0.5*k*dot(grad(v), grad(u))*dx # L = v*u0*dx - 0.5*k*dot(grad(v), grad(u0))*dx # # but instead of manually shuffling terms not including # the unknown u to the right-hand side, all terms may # be listed on one line and left- and right-hand sides # extracted by lhs() and rhs(). from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, lhs, rhs, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) k = 0.1 v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) F = v * (u - u0) * dx + k * dot(grad(v), grad(0.5 * (u0 + u))) * dx a = lhs(F) L = rhs(F) ufl-2024.2.0/demo/ExplicitConvection.py000066400000000000000000000011601470142567200176440ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) w = Coefficient(space) a = dot(dot(w, grad(u)), v) * dx ufl-2024.2.0/demo/FunctionOperators.py000066400000000000000000000026411470142567200175240ustar00rootroot00000000000000# Copyright (C) 2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Test form for operators on Coefficients. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, max_value, sqrt, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) a = ( sqrt(1 / max_value(1 / f, -1 / f)) * sqrt(g) * dot(grad(v), grad(u)) * dx + v * u * sqrt(f * g) * g * dx ) ufl-2024.2.0/demo/H1norm.py000066400000000000000000000007771470142567200152140ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import Coefficient, FunctionSpace, Mesh, dot, dx, grad, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) a = (f * f + dot(grad(f), grad(f))) * dx ufl-2024.2.0/demo/HarmonicMap.py000066400000000000000000000014311470142567200162320ustar00rootroot00000000000000# # Harmonic map demo using separate coefficients x and y. # Author: Martin Alnes # Date: 2009-04-09 # from ufl import Coefficient, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle X = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) Y = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) X_space = FunctionSpace(domain, X) Y_space = FunctionSpace(domain, Y) x = Coefficient(X_space) y = Coefficient(Y_space) L = inner(grad(x), grad(x)) * dx + dot(x, x) * y * dx F = derivative(L, (x, y)) J = derivative(F, (x, y)) forms = [L, F, J] ufl-2024.2.0/demo/HarmonicMap2.py000066400000000000000000000014311470142567200163140ustar00rootroot00000000000000# # Harmonic map demo using one mixed function u to represent x and y. # Author: Martin Alnes # Date: 2009-04-09 # from ufl import Coefficient, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, split, triangle from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle X = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) Y = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) M = MixedElement([X, Y]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, M) u = Coefficient(space) x, y = split(u) L = inner(grad(x), grad(x)) * dx + dot(x, x) * y * dx F = derivative(L, u) J = derivative(F, u) forms = [L, F, J] ufl-2024.2.0/demo/Heat.py000066400000000000000000000031741470142567200147230ustar00rootroot00000000000000# Copyright (C) 2005-2009 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnes, 2009 # # The bilinear form a(v, u1) and linear form L(v) for # one backward Euler step with the heat equation. # from ufl import ( Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) # Test function u1 = TrialFunction(space) # Value at t_n u0 = Coefficient(space) # Value at t_n-1 c = Coefficient(space) # Heat conductivity f = Coefficient(space) # Heat source k = Constant(domain) # Time step a = v * u1 * dx + k * c * dot(grad(v), grad(u1)) * dx L = v * u0 * dx + k * v * f * dx ufl-2024.2.0/demo/HornSchunck.py000066400000000000000000000022211470142567200162570ustar00rootroot00000000000000# # Implemented by imitation of # http://code.google.com/p/debiosee/wiki/DemosOptiocFlowHornSchunck # but not tested so this could contain errors! # from ufl import ( Coefficient, Constant, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # Finite element spaces for scalar and vector fields cell = triangle S = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) S_space = FunctionSpace(domain, S) V_space = FunctionSpace(domain, V) # Optical flow function u = Coefficient(V_space) # Previous image brightness I0 = Coefficient(S_space) # Current image brightness I1 = Coefficient(S_space) # Regularization parameter lamda = Constant(domain) # Coefficiental to minimize M = (dot(u, grad(I1)) + (I1 - I0)) ** 2 * dx + lamda * inner(grad(u), grad(u)) * dx # Derived linear system L = derivative(M, u) a = derivative(L, u) L = -L ufl-2024.2.0/demo/HyperElasticity.py000066400000000000000000000051351470142567200171630ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-12-22 # from ufl import ( Coefficient, Constant, FacetNormal, FunctionSpace, Identity, Mesh, SpatialCoordinate, TestFunction, TrialFunction, derivative, det, diff, dot, ds, dx, exp, grad, inner, inv, tetrahedron, tr, variable, ) from ufl.finiteelement import FiniteElement # Modified by Garth N. Wells, 2009 from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # Cell and its properties cell = tetrahedron domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) d = 3 N = FacetNormal(domain) x = SpatialCoordinate(domain) # Elements u_element = FiniteElement("Lagrange", cell, 2, (3,), identity_pullback, H1) p_element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) A_element = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1) # Spaces u_space = FunctionSpace(domain, u_element) p_space = FunctionSpace(domain, p_element) A_space = FunctionSpace(domain, A_element) # Test and trial functions v = TestFunction(u_space) w = TrialFunction(u_space) # Displacement at current and two previous timesteps u = Coefficient(u_space) up = Coefficient(u_space) upp = Coefficient(u_space) # Time parameters dt = Constant(domain) # Fiber field A = Coefficient(A_space) # External forces T = Coefficient(u_space) p0 = Coefficient(p_space) # Material parameters FIXME rho = Constant(domain) K = Constant(domain) c00 = Constant(domain) c11 = Constant(domain) c22 = Constant(domain) # Deformation gradient Id = Identity(d) F = Id + grad(u) F = variable(F) Finv = inv(F) J = det(F) # Left Cauchy-Green deformation tensor B = F * F.T I1_B = tr(B) I2_B = (I1_B**2 - tr(B * B)) / 2 I3_B = J**2 # Right Cauchy-Green deformation tensor C = F.T * F I1_C = tr(C) I2_C = (I1_C**2 - tr(C * C)) / 2 I3_C = J**2 # Green strain tensor E = (C - Id) / 2 # Mapping of strain in fiber directions Ef = A * E * A.T # Strain energy function W(Q(Ef)) Q = ( c00 * Ef[0, 0] ** 2 + c11 * Ef[1, 1] ** 2 + c22 * Ef[2, 2] ** 2 ) # FIXME: insert some simple law here W = (K / 2) * (exp(Q) - 1) # + p stuff # First Piola-Kirchoff stress tensor P = diff(W, F) # Acceleration term discretized with finite differences k = dt / rho acc = u - 2 * up + upp # Residual equation # FIXME: Can contain errors, not tested! a_F = ( inner(acc, v) * dx + k * inner(P, grad(v)) * dx - k * dot(J * Finv * T, v) * ds(0) - k * dot(J * Finv * p0 * N, v) * ds(1) ) # Jacobi matrix of residual equation a_J = derivative(a_F, u, w) # Export forms forms = [a_F, a_J] ufl-2024.2.0/demo/HyperElasticity1D.py000066400000000000000000000013011470142567200173370ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import Coefficient, Constant, FunctionSpace, Mesh, derivative, dx, exp, interval, variable from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = interval element = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (1,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Coefficient(space) b = Constant(domain) K = Constant(domain) E = u.dx(0) + u.dx(0) ** 2 / 2 E = variable(E) Q = b * E**2 psi = K * (exp(Q) - 1) f = psi * dx F = derivative(f, u) J = derivative(-F, u) forms = [f, F, J] ufl-2024.2.0/demo/L2norm.py000066400000000000000000000007311470142567200152070ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import Coefficient, FunctionSpace, Mesh, dx, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) a = f**2 * dx ufl-2024.2.0/demo/Mass.py000066400000000000000000000023671470142567200147500ustar00rootroot00000000000000# Copyright (C) 2004-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnes, 2009 # # Last changed: 2009-03-02 # # The bilinear form for a mass matrix. from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dx, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) a = v * u * dx ufl-2024.2.0/demo/MassAD.py000066400000000000000000000010731470142567200151460ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-28 # from ufl import Coefficient, FunctionSpace, Mesh, derivative, dx, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Coefficient(space) # L2 norm M = u**2 / 2 * dx # source vector L = derivative(M, u) # mass matrix a = derivative(L, u) ufl-2024.2.0/demo/MixedElasticity.py000066400000000000000000000036241470142567200171430ustar00rootroot00000000000000# Copyright (C) 2008-2010 Marie Rognes # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # First added: 2008-10-03 # Last changed: 2011-07-22 from ufl import ( FunctionSpace, Mesh, TestFunctions, TrialFunctions, as_vector, div, dot, dx, inner, skew, tetrahedron, tr, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import contravariant_piola, identity_pullback from ufl.sobolevspace import H1, L2, HDiv def skw(tau): """Define vectorized skew operator""" sk = 2 * skew(tau) return as_vector((sk[0, 1], sk[0, 2], sk[1, 2])) cell = tetrahedron n = 3 # Finite element exterior calculus syntax r = 1 S = FiniteElement("vector BDM", cell, r, (3, 3), contravariant_piola, HDiv) V = FiniteElement("Discontinuous Lagrange", cell, r - 1, (3,), identity_pullback, L2) Q = FiniteElement("Discontinuous Lagrange", cell, r - 1, (3,), identity_pullback, L2) W = MixedElement([S, V, Q]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, W) (sigma, u, gamma) = TrialFunctions(space) (tau, v, eta) = TestFunctions(space) a = ( inner(sigma, tau) - tr(sigma) * tr(tau) + dot(div(tau), u) - dot(div(sigma), v) + inner(skw(tau), gamma) + inner(skw(sigma), eta) ) * dx ufl-2024.2.0/demo/MixedMixedElement.py000066400000000000000000000017551470142567200174140ustar00rootroot00000000000000# Copyright (C) 2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # A mixed element of mixed elements from ufl import triangle from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) element = MixedElement([[P3, P3], [P3, P3]]) ufl-2024.2.0/demo/MixedPoisson.py000066400000000000000000000033731470142567200164640ustar00rootroot00000000000000# Copyright (C) 2006-2009 Anders Logg and Marie Rognes # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnes, 2009 # # Last changed: 2009-01-12 # # The bilinear form a(v, u) and linear form L(v) for # a mixed formulation of Poisson's equation with BDM # (Brezzi-Douglas-Marini) elements. # from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, triangle, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import contravariant_piola, identity_pullback from ufl.sobolevspace import H1, HDiv cell = triangle BDM1 = FiniteElement("Brezzi-Douglas-Marini", cell, 1, (2,), contravariant_piola, HDiv) DG0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, H1) element = MixedElement([BDM1, DG0]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) dg0_space = FunctionSpace(domain, DG0) (tau, w) = TestFunctions(space) (sigma, u) = TrialFunctions(space) f = Coefficient(dg0_space) a = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx L = w * f * dx ufl-2024.2.0/demo/MixedPoisson2.py000066400000000000000000000016311470142567200165410ustar00rootroot00000000000000# # Author: Marie Rognes # Modified by: Martin Sandve Alnes # Date: 2009-02-12 # from ufl import ( FacetNormal, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, ds, dx, tetrahedron, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import contravariant_piola, identity_pullback from ufl.sobolevspace import H1, HDiv cell = tetrahedron RT = FiniteElement("Raviart-Thomas", cell, 1, (3,), contravariant_piola, HDiv) DG = FiniteElement("DG", cell, 0, (), identity_pullback, H1) MX = MixedElement([RT, DG]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, MX) (u, p) = TrialFunctions(space) (v, q) = TestFunctions(space) n = FacetNormal(domain) a0 = (dot(u, v) + div(u) * q + div(v) * p) * dx a1 = (dot(u, v) + div(u) * q + div(v) * p) * dx - p * dot(v, n) * ds forms = [a0, a1] ufl-2024.2.0/demo/NavierStokes.py000066400000000000000000000026701470142567200164570ustar00rootroot00000000000000# Copyright (C) 2004-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnes # # Last changed: 2009-03-02 # # The bilinear form for the nonlinear term in the # Navier-Stokes equations with fixed convective velocity. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, tetrahedron, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = tetrahedron element = FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) Du = grad(u) a = dot(dot(w, Du), v) * dx ufl-2024.2.0/demo/NeumannProblem.py000066400000000000000000000026521470142567200167640ustar00rootroot00000000000000# Copyright (C) 2006-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # The bilinear form a(v, u) and linear form L(v) for # Poisson's equation with Neumann boundary conditions. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, ds, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) a = inner(grad(v), grad(u)) * dx L = inner(v, f) * dx + inner(v, g) * ds ufl-2024.2.0/demo/NonlinearPoisson.py000066400000000000000000000013031470142567200173320ustar00rootroot00000000000000from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) f = Coefficient(space) a = (1 + u0**2) * dot(grad(v), grad(u)) * dx + 2 * u0 * u * dot(grad(v), grad(u0)) * dx L = v * f * dx - (1 + u0**2) * dot(grad(v), grad(u0)) * dx ufl-2024.2.0/demo/P5tet.py000066400000000000000000000017271470142567200150450ustar00rootroot00000000000000# Copyright (C) 2006-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # A fifth degree Lagrange finite element on a tetrahedron from ufl import tetrahedron from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) ufl-2024.2.0/demo/P5tri.py000066400000000000000000000017161470142567200150450ustar00rootroot00000000000000# Copyright (C) 2006-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # A fifth degree Lagrange finite element on a triangle from ufl import triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1) ufl-2024.2.0/demo/Poisson.py000066400000000000000000000027011470142567200154670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2004-2008 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by Martin Sandve Alnæs, 2009 # # Last changed: 2009-03-02 # # The bilinear form a(v, u) and linear form L(v) for Poisson's equation. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) f = Coefficient(space) a = inner(grad(v), grad(u)) * dx(degree=1) L = v * f * dx(degree=2) ufl-2024.2.0/demo/PoissonDG.py000066400000000000000000000036261470142567200157110ustar00rootroot00000000000000# Copyright (C) 2006-2007 Kristiand Oelgaard and Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # First added: 2006-12-05 # Last changed: 2007-07-15 # # The bilinear form a(v, u) and linear form L(v) for # Poisson's equation in a discontinuous Galerkin (DG) # formulation. from ufl import ( Coefficient, Constant, FacetNormal, FunctionSpace, Mesh, TestFunction, TrialFunction, avg, dot, dS, ds, dx, grad, inner, jump, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 cell = triangle element = FiniteElement("Discontinuous Lagrange", cell, 1, (), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) n = FacetNormal(domain) h = Constant(domain) gN = Coefficient(space) alpha = 4.0 gamma = 8.0 a = ( inner(grad(v), grad(u)) * dx - inner(avg(grad(v)), jump(u, n)) * dS - inner(jump(v, n), avg(grad(u))) * dS + alpha / h("+") * dot(jump(v, n), jump(u, n)) * dS - inner(grad(v), u * n) * ds - inner(v * n, grad(u)) * ds + gamma / h * v * u * ds ) L = v * f * dx + v * gN * ds ufl-2024.2.0/demo/PoissonSystem.py000066400000000000000000000027151470142567200167010ustar00rootroot00000000000000# Copyright (C) 2005-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by: Martin Sandve Alnes, 2009 # # Last changed: 2009-03-02 # # The bilinear form a(v, u) and linear form L(v) for # Poisson's equation in system form (vector-valued). from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle element = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) a = inner(grad(v), grad(u)) * dx L = dot(v, f) * dx ufl-2024.2.0/demo/PowAD.py000066400000000000000000000011611470142567200150060ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, derivative, dx, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) L = w**5 * v * dx a = derivative(L, w) ufl-2024.2.0/demo/ProjectionSystem.py000066400000000000000000000010011470142567200173460ustar00rootroot00000000000000from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) a = u * v * dx L = f * v * dx ufl-2024.2.0/demo/QuadratureElement.py000066400000000000000000000036151470142567200174710ustar00rootroot00000000000000# Copyright (C) 2008 Kristian B. Oelgaard # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # First added: 2008-03-31 # Last changed: 2008-03-31 # # The linearised bilinear form a(u,v) and linear form L(v) for # the nonlinear equation - div (1+u) grad u = f (non-linear Poisson) from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, i, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) QE = FiniteElement("Quadrature", triangle, 2, (), identity_pullback, H1) sig = FiniteElement("Quadrature", triangle, 1, (2,), identity_pullback, H1) qe_space = FunctionSpace(domain, QE) sig_space = FunctionSpace(domain, sig) v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) C = Coefficient(qe_space) sig0 = Coefficient(sig_space) f = Coefficient(space) a = ( v.dx(i) * C * u.dx(i) * dx(metadata={"quadrature_degree": 2}) + v.dx(i) * 2 * u0 * u * u0.dx(i) * dx ) L = v * f * dx - dot(grad(v), sig0) * dx(metadata={"quadrature_degree": 1}) ufl-2024.2.0/demo/Stiffness.py000066400000000000000000000010371470142567200160020ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-03 # from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) a = dot(grad(u), grad(v)) * dx ufl-2024.2.0/demo/StiffnessAD.py000066400000000000000000000015251470142567200162110ustar00rootroot00000000000000# # Author: Martin Sandve Alnes # Date: 2008-10-30 # from ufl import ( Coefficient, FunctionSpace, Mesh, action, adjoint, derivative, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) w = Coefficient(space) # H1 semi-norm f = inner(grad(w), grad(w)) / 2 * dx # grad(w) : grad(v) b = derivative(f, w) # stiffness matrix, grad(u) : grad(v) a = derivative(b, w) # adjoint, grad(v) : grad(u) astar = adjoint(a) # action of adjoint, grad(v) : grad(w) astaraction = action(astar) forms = [f, b, a, astar, astaraction] ufl-2024.2.0/demo/Stokes.py000066400000000000000000000032271470142567200153110ustar00rootroot00000000000000# Copyright (C) 2005-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # Modified by: Martin Sandve Alnes (2009) # Date: 2009-03-02 # # The bilinear form a(v, u) and Linear form L(v) for the Stokes # equations using a mixed formulation (Taylor-Hood elements). from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle P2 = FiniteElement("Lagrange", cell, 2, (2,), identity_pullback, H1) P1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) TH = MixedElement([P2, P1]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, TH) p2_space = FunctionSpace(domain, P2) (v, q) = TestFunctions(space) (u, p) = TrialFunctions(space) f = Coefficient(p2_space) a = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx L = dot(v, f) * dx ufl-2024.2.0/demo/StokesEquation.py000066400000000000000000000034131470142567200170140ustar00rootroot00000000000000# Copyright (C) 2005-2009 Anders Logg and Harish Narayanan # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # The bilinear form a(v, u) and Linear form L(v) for the Stokes # equations using a mixed formulation (Taylor-Hood elements) in # combination with the lhs() and rhs() operators to extract the # bilinear and linear forms from an expression F = 0. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, grad, inner, lhs, rhs, triangle, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 cell = triangle P2 = FiniteElement("Lagrange", cell, 2, (2,), identity_pullback, H1) P1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) TH = MixedElement([P2, P1]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, TH) p2_space = FunctionSpace(domain, P2) (v, q) = TestFunctions(space) (u, p) = TrialFunctions(space) f = Coefficient(p2_space) F = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx - dot(v, f) * dx a = lhs(F) L = rhs(F) ufl-2024.2.0/demo/SubDomain.py000066400000000000000000000024571470142567200157260ustar00rootroot00000000000000# Copyright (C) 2008 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # This example illustrates how to define a form over a # given subdomain of a mesh, in this case a functional. from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, ds, dx, tetrahedron from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) M = f * dx(2) + f * ds(5) ufl-2024.2.0/demo/SubDomains.py000066400000000000000000000027111470142567200161020ustar00rootroot00000000000000# Copyright (C) 2008 Anders Logg and Kristian B. Oelgaard # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # This simple example illustrates how forms can be defined on different sub domains. # It is supported for all three integral types. from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dS, ds, dx, tetrahedron from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) a = ( v * u * dx(0) + 10.0 * v * u * dx(1) + v * u * ds(0) + 2.0 * v * u * ds(1) + v("+") * u("+") * dS(0) + 4.3 * v("+") * u("+") * dS(1) ) ufl-2024.2.0/demo/TensorWeightedPoisson.py000066400000000000000000000027301470142567200203450ustar00rootroot00000000000000# Copyright (C) 2005-2007 Anders Logg # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # The bilinear form a(v, u) and linear form L(v) for # tensor-weighted Poisson's equation. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) p1_space = FunctionSpace(domain, P1) p0_space = FunctionSpace(domain, P0) v = TestFunction(p1_space) u = TrialFunction(p1_space) C = Coefficient(p0_space) a = inner(grad(v), C * grad(u)) * dx ufl-2024.2.0/demo/VectorLaplaceGradCurl.py000066400000000000000000000036601470142567200202120ustar00rootroot00000000000000# Copyright (C) 2007 Marie Rognes # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # The bilinear form a(v, u) and linear form L(v) for the Hodge Laplace # problem using 0- and 1-forms. Intended to demonstrate use of Nedelec # elements. from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, curl, dx, grad, inner, tetrahedron, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import covariant_piola, identity_pullback from ufl.sobolevspace import H1, HCurl def HodgeLaplaceGradCurl(space, fspace): tau, v = TestFunctions(space) sigma, u = TrialFunctions(space) f = Coefficient(fspace) a = ( inner(tau, sigma) - inner(grad(tau), u) + inner(v, grad(sigma)) + inner(curl(v), curl(u)) ) * dx L = inner(v, f) * dx return a, L cell = tetrahedron order = 1 GRAD = FiniteElement("Lagrange", cell, order, (), identity_pullback, H1) CURL = FiniteElement("N1curl", cell, order, (3,), covariant_piola, HCurl) VectorLagrange = FiniteElement("Lagrange", cell, order + 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, MixedElement([GRAD, CURL])) fspace = FunctionSpace(domain, VectorLagrange) a, L = HodgeLaplaceGradCurl(space, fspace) ufl-2024.2.0/demo/_TensorProductElement.py000066400000000000000000000030611470142567200203210ustar00rootroot00000000000000# Copyright (C) 2012 Marie E. Rognes (meg@simula.no) # # This file is part of UFL. # # UFL is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # UFL 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with UFL. If not, see . # # First added: 2012-08-16 # Last changed: 2012-08-16 from ufl import ( FunctionSpace, Mesh, TensorProductElement, TestFunction, TrialFunction, dx, interval, tetrahedron, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 V0 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V1 = FiniteElement("DG", interval, 0, (), identity_pullback, L2) V2 = FiniteElement("DG", tetrahedron, 0, (), identity_pullback, L2) V = TensorProductElement(V0, V1, V2) c0 = FiniteElement("CG", triangle, 1) c1 = FiniteElement("CG", interval, 1) c2 = FiniteElement("CG", tetrahedron, 1) domain = Mesh(TensorProductElement(c0, c1, c2)) space = FunctionSpace(domain, V) u = TrialFunction(space) v = TestFunction(space) dxxx = dx * dx * dx a = u * v * dxxx ufl-2024.2.0/doc/000077500000000000000000000000001470142567200133045ustar00rootroot00000000000000ufl-2024.2.0/doc/man/000077500000000000000000000000001470142567200140575ustar00rootroot00000000000000ufl-2024.2.0/doc/man/man1/000077500000000000000000000000001470142567200147135ustar00rootroot00000000000000ufl-2024.2.0/doc/man/man1/ufl-analyse.1.gz000066400000000000000000000014511470142567200176350ustar00rootroot00000000000000UWTr6}W䩝fڙ6e#S*IQKqǺ(JE"؃s0j6HtW𚱤"}ȘoW\^@+$hcCo.*Xr _@OmX/j.`n{gbpB>9jP1! zCCMv$|$Œzɞ맋M >@kv+ a ?:GS3?WyVbjP`roi2ϘQpeXp2{>,Ah~|zni4׫C[s}3Dc?6àF+FIHN5Q9;am h';0,kTMVT;a)3u{n CluFl|HA˔z+ڑ ̫-=aHnXwwp KhPb;fՑ4G'" #O1m@LdGܝȃumi BJ xU~!-eZhfGKb Wn$!+oizzhȪ fRXe߬i U\TYq*mEDž ߍ53 AB8f`bR 2"p&Fb~Ov=0]US qfnͣTp*[{~iϕ'/R\~Ks/Cʮufl-2024.2.0/doc/man/man1/ufl-convert.1.gz000066400000000000000000000016751470142567200176710ustar00rootroot00000000000000&WTn6}W e/(fUN¶Inj4}ŭDjIʎ~?R$@>93gp=j6.,}M^G8(e+P*p;&0e[x~*(h> dY8\Cl쩼}VtjXf_ikރ[gb% gA7hkxP|ocn_ nx#KPrqZArմ$Cb5۶V1BIK3MKIIN#s!?< Qu ` peƖF@?*zt*)ŗol_/ \glieW''}9LNmE鵑E-y G'ԣSx<驓0aZ h1n*k[s=(EnZ!fJk[ yťI"]LM GޫpܤKUe8h}Ҟk S AΒW̉A Q:gI&vJlaE=J$_kJ( nT}ܭ;G"I"po )KmD[4պKl vGR^0+E^ >=Bgjea@o<IlOtVLqM[ bhlOz,|plh߅fn 8Hp] $ y㭪_T-}sH-wJvWP#RKsG7m;xh]t" Kn q&&NAv޽`_F[R0y37'2I kI}k-#%砖eوL//=ms@o~`_^ufl-2024.2.0/doc/man/man1/ufl-version.1.gz000066400000000000000000000011241470142567200176630ustar00rootroot00000000000000dWSMo@JV[E%lh٥aĿ㦉T27ͼ -\(}r_ -$S|#Oh v|!.!3_sxB~`"t fnPF`mڵà[C TW }:"+(} gtUp$X ԡ5zm q#7xPqEQz pUIAkLyUY;4,ǤZKGTh/`'*TSrb3j3^EF9ݬ͟~ye[#0> $wo8Gojj@n:fãJorcq94 tgR-1\4(}},{Ie٧!L؅)Ϣ`LYCëq!-9>*-)5t ʼntq0ZMM (VF9ĔqV%`w) `.hz+ ;ݶp+݊K(}wYcϕ'o#^TۗhqW+ufl-2024.2.0/doc/man/man1/ufl2py.1.gz000066400000000000000000000012161470142567200166350ustar00rootroot00000000000000?WSn0+=+- *pdAHK+E|Gc]6(_;3;;5|ح67|a,*֐OKy3Zq 4B"8 " TSmV$Tʄ U?"O2٦@@mV`+# Ѵƃo[3ˌ97A%*;i6n6Pu\)EUGh3 0,|dL%+;aZ{ec[+uFC礠׵hF:`^hu-ނnc ZKwp ?HQFT,'pb;0P "Xi硛{Ͽ^xh3 QcۀB';ZG4遺 p@/='z+!Nyou;W<ᄤf Wn$i/>~H6IWI.VbLMC˳mB6޺j3 ̫q!-uQZR&k+H+Sn'ƸԪڤ/i@i70"܅Sc*?WxG旟z,p;čQ5K緅~:/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" apidoc: sphinx-apidoc -f -T -o source/api-doc/ ../../ufl clean: rm -rf $(BUILDDIR)/* html: apidoc $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: apidoc $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: apidoc $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: apidoc $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: apidoc $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: apidoc $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: apidoc $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/UnifiedFormLanguageUFL.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/UnifiedFormLanguageUFL.qhc" applehelp: apidoc $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." epub: apidoc $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: apidoc $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: apidoc $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: apidoc $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: apidoc $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: apidoc $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: apidoc $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: apidoc $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: apidoc $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: apidoc $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: apidoc $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: apidoc $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: apidoc $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: apidoc $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: apidoc $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ufl-2024.2.0/doc/sphinx/README000066400000000000000000000011371470142567200154770ustar00rootroot00000000000000==================== Sphinx documentation ==================== UFL is documented using Sphinx and reStructured text. The documentation is hosted at http://fenics-ufl.readthedocs.org/. The online documentation is automatically updated upon pushes to the UFL master branch. Building the documentation locally ================================== The HTML documentation can be built locally using:: make html In order for changes in the docstring to be propagated to the html pages, ufl must be installed anew. In other words, sphinx reads the docstrings from the installed code, not the source code. ufl-2024.2.0/doc/sphinx/requirements.txt000066400000000000000000000000071470142567200200760ustar00rootroot00000000000000sphinx ufl-2024.2.0/doc/sphinx/source/000077500000000000000000000000001470142567200161155ustar00rootroot00000000000000ufl-2024.2.0/doc/sphinx/source/conf.py000066400000000000000000000224101470142567200174130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Unified Form Language (UFL) documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 11:05:14 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime import importlib.metadata # 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. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # 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.coverage", "sphinx.ext.mathjax", "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Unified Form Language (UFL)" this_year = datetime.date.today().year copyright = "%s, FEniCS Project" % this_year author = "FEniCS Project" version = importlib.metadata.version("fenics-ufl") release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- 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' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "UnifiedFormLanguageUFLdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "UnifiedFormLanguageUFL.tex", "Unified Form Language (UFL) Documentation", "FEniCS Project", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "unifiedformlanguageufl", "Unified Form Language (UFL) Documentation", [author], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "UnifiedFormLanguageUFL", "Unified Form Language (UFL) Documentation", author, "UnifiedFormLanguageUFL", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False ufl-2024.2.0/doc/sphinx/source/index.rst000066400000000000000000000013671470142567200177650ustar00rootroot00000000000000.. title:: UFL ========================== UFL: Unified Form Language ========================== The Unified Form Language (UFL) is a domain specific language for declaration of finite element discretizations of variational forms. More precisely, it defines a flexible interface for choosing finite element spaces and defining expressions for weak forms in a notation close to mathematical notation. UFL is part of the FEniCS Project. For more information, visit http://www.fenicsproject.org Documentation ============= .. toctree:: :titlesonly: :maxdepth: 1 installation manual API reference releases [FIXME: These links don't belong here, should go under API reference somehow.] * :ref:`genindex` * :ref:`modindex` ufl-2024.2.0/doc/sphinx/source/installation.rst000066400000000000000000000021011470142567200213420ustar00rootroot00000000000000.. title:: Installation ============ Installation ============ UFL is normally installed as part of an installation of FEniCS. If you are using UFL as part of the FEniCS software suite, it is recommended that you follow the `installation instructions for FEniCS `__. To install UFL itself, read on below for a list of requirements and installation instructions. Requirements and dependencies ============================= UFL requires Python version 3.5 or later and depends on the following Python packages: * NumPy These packages will be automatically installed as part of the installation of UFL, if not already present on your system. Installation instructions ========================= To install UFL, download the source code from the `UFL GitHub repository `__, and run the following command: .. code-block:: console pip install . To install to a specific location, add the ``--prefix`` flag to the installation command: .. code-block:: console pip install --prefix= . ufl-2024.2.0/doc/sphinx/source/manual.rst000066400000000000000000000003621470142567200201250ustar00rootroot00000000000000.. title:: User manual =========== User manual =========== .. toctree:: :maxdepth: 1 manual/introduction manual/form_language manual/examples manual/internal_representation manual/algorithms manual/command_line_utils ufl-2024.2.0/doc/sphinx/source/manual/000077500000000000000000000000001470142567200173725ustar00rootroot00000000000000ufl-2024.2.0/doc/sphinx/source/manual/algorithms.rst000066400000000000000000000363361470142567200223100ustar00rootroot00000000000000********** Algorithms ********** Algorithms to work with UFL forms and expressions can be found in the submodule ``ufl.algorithms``. You can import all of them with the line :: from ufl.algorithms import * This chapter gives an overview of (most of) the implemented algorithms. The intended audience is primarily developers, but advanced users may find information here useful for debugging. While domain specific languages introduce notation to express particular ideas more easily, which can reduce the probability of bugs in user code, they also add yet another layer of abstraction which can make debugging more difficult when the need arises. Many of the utilities described here can be useful in that regard. Formatting expressions ====================== Expressions can be formatted in various ways for inspection, which is particularly useful for debugging. We use the following as an example form for the formatting sections below:: element = FiniteElement("CG", triangle, 1) v = TestFunction(element) u = TrialFunction(element) c = Coefficient(element) f = Coefficient(element) a = c*u*v*dx + f*v*ds str --- Compact, human readable pretty printing. Useful in interactive Python sessions. Example output of ``str(a)``:: { v_0 * v_1 * w_0 } * dx(>>[everywhere], {}) + { v_0 * w_1 } * ds(>>[everywhere], {}) repr ---- Accurate description of an expression, with the property that ``eval(repr(a)) == a``. Useful to see which representation types occur in an expression, especially if ``str(a)`` is ambiguous. Example output of ``repr(a)``:: Form([Integral(Product(Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0, None), Product(Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 1, None), Coefficient(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0))), 'cell', Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), 'everywhere', {}, None), Integral(Product(Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0, None), Coefficient(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 1)), 'exterior_facet', Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), 'everywhere', {}, None)]) Tree formatting --------------- ASCII tree formatting, useful to inspect the tree structure of an expression in interactive Python sessions. Example output of ``tree_format(a)``:: Form: Integral: integral type: cell subdomain id: everywhere integrand: Product ( Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0, None) Product ( Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 1, None) Coefficient(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0) ) ) Integral: integral type: exterior_facet subdomain id: everywhere integrand: Product ( Argument(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 0, None) Coefficient(FunctionSpace(Mesh(VectorElement('Lagrange', triangle, 1, dim=2), -1), FiniteElement('Lagrange', triangle, 1)), 1) ) Inspecting and manipulating the expression tree =============================================== This subsection is mostly for form compiler developers and technically interested users. Traversing expressions ---------------------- ``iter_expressions`` ^^^^^^^^^^^^^^^^^^^^^ Example usage:: for e in iter_expressions(a): print str(e) outputs:: v_0 * v_1 * w_0 v_0 * w_1 .. ``post_traversal`` ^^^^^^^^^^^^^^^^^^^ .. TODO: traversal.py .. ``pre_traversal`` ^^^^^^^^^^^^^^^^^^ .. TODO: traversal.py .. ``walk`` ^^^^^^^^ .. TODO: traversal.py .. ``traverse_terminals`` ^^^^^^^^^^^^^^^^^^^^^^^ .. TODO: traversal.py .. Extracting information ---------------------- .. TODO: analysis.py Transforming expressions ------------------------ So far we presented algorithms meant to inspect expressions in various ways. Some recurring patterns occur when writing algorithms to modify expressions, either to apply mathematical transformations or to change their representation. Usually, different expression node types need different treatment. To assist in such algorithms, UFL provides the ``Transformer`` class. This implements a variant of the Visitor pattern to enable easy definition of transformation rules for the types you wish to handle. Shown here is maybe the simplest transformer possible:: class Printer(Transformer): def __init__(self): Transformer.__init__(self) def expr(self, o, *operands): print "Visiting", str(o), "with operands:" print ", ".join(map(str,operands)) return o element = FiniteElement("CG", triangle, 1) v = TestFunction(element) u = TrialFunction(element) a = u*v p = Printer() p.visit(a) The call to ``visit`` will traverse ``a`` and call ``Printer.expr`` on all expression nodes in post--order, with the argument ``operands`` holding the return values from visits to the operands of ``o``. The output is:: Visiting v_0 * v_1 with operands: v_0, v_1 :math:`(v^0_h)(v^1_h)` Implementing ``expr`` above provides a default handler for any expression node type. For each subclass of ``Expr`` you can define a handler function to override the default by using the name of the type in underscore notation, e.g. ``vector_constant`` for ``VectorConstant``. The constructor of ``Transformer`` and implementation of ``Transformer.visit`` handles the mapping from type to handler function automatically. Here is a simple example to show how to override default behaviour:: from ufl.classes import * class CoefficientReplacer(Transformer): def __init__(self): Transformer.__init__(self) expr = Transformer.reuse_if_possible terminal = Transformer.always_reuse def coefficient(self, o): return FloatValue(3.14) element = FiniteElement("CG", triangle, 1) v = TestFunction(element) f = Coefficient(element) a = f*v r = CoefficientReplacer() b = r.visit(a) print b which outputs :: 3.14 * v_0 The output of this code is the transformed expression ``b == 3.14*v``. This code also demonstrates how to reuse existing handlers. The handler ``Transformer.reuse_if_possible`` will return the input object if the operands have not changed, and otherwise reconstruct a new instance of the same type but with the new transformed operands. The handler ``Transformer.always_reuse`` always reuses the instance without recursing into its children, usually applied to terminals. To set these defaults with less code, inherit ``ReuseTransformer`` instead of ``Transformer``. This ensures that the parts of the expression tree that are not changed by the transformation algorithms will always reuse the same instances. We have already mentioned the difference between pre--traversal and post--traversal, and some times you need to combine the two. ``Transformer`` makes this easy by checking the number of arguments to your handler functions to see if they take transformed operands as input or not. If a handler function does not take more than a single argument in addition to self, its children are not visited automatically, and the handler function must call ``visit`` on its operands itself. Here is an example of mixing pre- and post-traversal:: class Traverser(ReuseTransformer): def __init__(self): ReuseTransformer.__init__(self) def sum(self, o): operands = o.operands() newoperands = [] for e in operands: newoperands.append( self.visit(e) ) return sum(newoperands) element = FiniteElement("CG", triangle, 1) f = Coefficient(element) g = Coefficient(element) h = Coefficient(element) a = f+g+h r = Traverser() b = r.visit(a) print b This code inherits the ``ReuseTransformer`` as explained above, so the default behaviour is to recurse into children first and then call ``Transformer.reuse_if_possible`` to reuse or reconstruct each expression node. Since ``sum`` only takes ``self`` and the expression node instance ``o`` as arguments, its children are not visited automatically, and ``sum`` explicitly calls ``self.visit`` to do this. Automatic differentiation implementation ======================================== This subsection is mostly for form compiler developers and technically interested users. First of all, we give a brief explanation of the algorithm. Recall that a ``Coefficient`` represents a sum of unknown coefficients multiplied with unknown basis functions in some finite element space. .. math:: w(x) = \sum_k w_k \phi_k(x) Also recall that an ``Argument`` represents any (unknown) basis function in some finite element space. .. math:: v(x) = \phi_k(x), \qquad \phi_k \in V_h . A form :math:`L(v; w)` implemented in UFL is intended for discretization like .. math:: b_i = L(\phi_i; \sum_k w_k \phi_k), \qquad \forall \phi_i \in V_h . The Jacobi matrix :math:`A_{ij}` of this vector can be obtained by differentiation of :math:`b_i` w.r.t. :math:`w_j`, which can be written .. math:: A_{ij} = \frac{d b_i}{d w_j} = a(\phi_i, \phi_j; \sum_k w_k \phi_k), \qquad \forall \phi_i \in V_h, \quad \forall \phi_j \in V_h , for some form `a`. In UFL, the form `a` can be obtained by differentiating `L`. To manage this, we note that as long as the domain :math:`\Omega` is independent of :math:`w_j`, :math:`\int_\Omega` commutes with :math:`\frac{d}{d w_j}`, and we can differentiate the integrand expression instead, e.g., .. math:: L(v; w) = \int_\Omega I_c(v; w) \, dx + \int_{\partial\Omega} I_e(v; w) \, ds, \\ \frac{d}{d w_j} L(v; w) = \int_\Omega \frac{d I_c}{d w_j} \, dx + \int_{\partial\Omega} \frac{d I_e}{d w_j} \, ds. In addition, we need that .. math:: \frac{d w}{d w_j} = \phi_j, \qquad \forall \phi_j \in V_h , which in UFL can be represented as .. math:: w &= \mathtt{Coefficient(element)}, \\ v &= \mathtt{Argument(element)}, \\ \frac{dw}{d w_j} &= v, since :math:`w` represents the sum and :math:`v` represents any and all basis functions in :math:`V_h`. Other operators have well defined derivatives, and by repeatedly applying the chain rule we can differentiate the integrand automatically. .. TODO: More details about AD algorithms for developers. .. Forward mode ------------ .. TODO: forward_ad.py .. Reverse mode ------------ .. TODO: reverse_ad.py .. Mixed derivatives ----------------- .. TODO: ad.py Computational graphs ==================== This section is for form compiler developers and is probably of no interest to end-users. An expression tree can be seen as a directed acyclic graph (DAG). To aid in the implementation of form compilers, UFL includes tools to build a linearized [#]_ computational graph from the abstract expression tree. A graph can be partitioned into subgraphs based on dependencies of subexpressions, such that a quadrature based compiler can easily place subexpressions inside the right sets of loops. .. [#] Linearized as in a linear datastructure, do not confuse this with automatic differentiation. .. TODO: Finish and test this before writing about it :) The vertices of a graph can be reordered to improve the efficiency of the generated code, an operation usually called operation scheduling. The computational graph ----------------------- .. TODO: finish graph.py: Consider the expression .. math:: f = (a + b) * (c + d) where a, b, c, d are arbitrary scalar expressions. The *expression tree* for f looks like this:: a b c d \ / \ / + + \ / * In UFL f is represented like this expression tree. If a, b, c, d are all distinct Coefficient instances, the UFL representation will look like this:: Coefficient Coefficient Coefficient Coefficient \ / \ / Sum Sum \ / --- Product --- If we instead have the expression .. math:: f = (a + b) * (a - b) the tree will in fact look like this, with the functions a and b only represented once:: Coefficient Coefficient | \ / | | Sum Product -- IntValue(-1) | | | | Product | | | | |------- Sum -------| The expression tree is a directed acyclic graph (DAG) where the vertices are Expr instances and each edge represents a direct dependency between two vertices, i.e. that one vertex is among the operands of another. A graph can also be represented in a linearized data structure, consisting of an array of vertices and an array of edges. This representation is convenient for many algorithms. An example to illustrate this graph representation follows:: G = V, E V = [a, b, a+b, c, d, c+d, (a+b)*(c+d)] E = [(6,2), (6,5), (5,3), (5,4), (2,0), (2,1)] In the following, this representation of an expression will be called the *computational graph*. To construct this graph from a UFL expression, simply do :: G = Graph(expression) V, E = G The Graph class can build some useful data structures for use in algorithms:: Vin = G.Vin() # Vin[i] = list of vertex indices j such that there is an edge from V[j] to V[i] Vout = G.Vout() # Vout[i] = list of vertex indices j such that there is an edge from V[i] to V[j] Ein = G.Ein() # Ein[i] = list of edge indices j such that E[j] is an edge to V[i], e.g. E[j][1] == i Eout = G.Eout() # Eout[i] = list of edge indices j such that E[j] is an edge from V[i], e.g. E[j][0] == i The ordering of the vertices in the graph can in principle be arbitrary, but here they are ordered such that .. math:: v_i \prec v_j, \quad \forall j > i, where :math:`a \prec b` means that :math:`a` does not depend on :math:`b` directly or indirectly. Another property of the computational graph built by UFL is that no identical expression is assigned to more than one vertex. This is achieved efficiently by inserting expressions in a dict (a hash map) during graph building. In principle, correct code can be generated for an expression from its computational graph simply by iterating over the vertices and generating code for each one separately. However, we can do better than that. Partitioning the graph ---------------------- To help generate better code efficiently, we can partition vertices by their dependencies, which allows us to, e.g., place expressions outside the quadrature loop if they don't depend (directly or indirectly) on the spatial coordinates. This is done simply by .. TODO :: P = partition(G) ufl-2024.2.0/doc/sphinx/source/manual/examples.rst000066400000000000000000000372631470142567200217550ustar00rootroot00000000000000************* Example forms ************* The following examples illustrate basic usage of the form language for the definition of a collection of standard multilinear forms. We assume that ``dx`` has been declared as an integral over the interior of :math:`\Omega` and that both ``i`` and ``j`` have been declared as a free ``Index``. The examples presented below can all be found in the subdirectory ``demo/`` of the UFL source tree together with numerous other examples. The mass matrix =============== As a first example, consider the bilinear form corresponding to a mass matrix, .. math:: a(v, u) = \int_{\Omega} v \, u \mathop{dx}, which can be implemented in UFL as follows:: element = FiniteElement("Lagrange", triangle, 1) v = TestFunction(element) u = TrialFunction(element) a = v*u*dx This example is implemented in the file ``Mass.py`` in the collection of demonstration forms included with the UFL source distribution. Poisson equation ================ The bilinear and linear forms form for Poisson's equation, .. math:: a(v, u) &= \int_{\Omega} \nabla v \cdot \nabla u \mathop{dx}, \\ L(v; f) &= \int_{\Omega} v \, f \mathop{dx}, can be implemented as follows:: element = FiniteElement("Lagrange", triangle, 1) v = TestFunction(element) u = TrialFunction(element) f = Coefficient(element) a = dot(grad(v), grad(u))*dx L = v*f*dx Alternatively, index notation can be used to express the scalar product like this:: a = Dx(v, i)*Dx(u, i)*dx or like this:: a = v.dx(i)*u.dx(i)*dx This example is implemented in the file ``Poisson.py`` in the collection of demonstration forms included with the UFL source distribution. Vector-valued Poisson ===================== The bilinear and linear forms for a system of (independent) Poisson equations, .. math:: a(v, u) &= \int_{\Omega} \nabla v : \nabla u \mathop{dx}, \\ L(v; f) &= \int_{\Omega} v \cdot f \mathop{dx}, with :math:`v`, :math:`u` and :math:`f` vector-valued can be implemented as follows:: element = VectorElement("Lagrange", triangle, 1) v = TestFunction(element) u = TrialFunction(element) f = Coefficient(element) a = inner(grad(v), grad(u))*dx L = dot(v, f)*dx Alternatively, index notation may be used like this:: a = Dx(v[i], j)*Dx(u[i], j)*dx L = v[i]*f[i]*dx or like this:: a = v[i].dx(j)*u[i].dx(j)*dx L = v[i]*f[i]*dx This example is implemented in the file ``PoissonSystem.py`` in the collection of demonstration forms included with the UFL source distribution. The strain-strain term of linear elasticity =========================================== The strain-strain term of linear elasticity, .. math:: a(v, u) = \int_{\Omega} \epsilon(v) : \epsilon(u) \mathop{dx}, where .. math:: \epsilon(v) = \frac{1}{2}(\nabla v + (\nabla v)^{\top}) can be implemented as follows:: element = VectorElement("Lagrange", tetrahedron, 1) v = TestFunction(element) u = TrialFunction(element) def epsilon(v): Dv = grad(v) return 0.5*(Dv + Dv.T) a = inner(epsilon(v), epsilon(u))*dx Alternatively, index notation can be used to define the form:: a = 0.25*(Dx(v[j], i) + Dx(v[i], j))* \ (Dx(u[j], i) + Dx(u[i], j))*dx or like this:: a = 0.25*(v[j].dx(i) + v[i].dx(j))* \ (u[j].dx(i) + u[i].dx(j))*dx This example is implemented in the file ``Elasticity.py`` in the collection of demonstration forms included with the UFL source distribution. The nonlinear term of Navier--Stokes ==================================== The bilinear form for fixed-point iteration on the nonlinear term of the incompressible Navier--Stokes equations, .. math:: a(v, u; w) = \int_{\Omega} (w \cdot \nabla u) \cdot v \mathop{dx}, with :math:`w` the frozen velocity from a previous iteration, can be implemented as follows:: element = VectorElement("Lagrange", tetrahedron, 1) v = TestFunction(element) u = TrialFunction(element) w = Coefficient(element) a = dot(grad(u)*w, v)*dx alternatively using index notation like this:: a = v[i]*w[j]*Dx(u[i], j)*dx or like this:: a = v[i]*w[j]*u[i].dx(j)*dx This example is implemented in the file ``NavierStokes.py`` in the collection of demonstration forms included with the UFL source distribution. The heat equation ================= Discretizing the heat equation, .. math:: \dot{u} - \nabla \cdot (c \nabla u) = f, in time using the :math:`\mathrm{dG}(0)` method (backward Euler), we obtain the following variational problem for the discrete solution :math:`u_h = u_h(x, t)`: Find :math:`u_h^n = u_h(\cdot, t_n)` with :math:`u_h^{n-1} = u_h(\cdot, t_{n-1})` given such that .. math:: \frac{1}{k_n} \int_{\Omega} v \, (u_h^n - u_h^{n-1}) \mathop{dx} + \int_{\Omega} c \, \nabla v \cdot \nabla u_h^n \mathop{dx} = \int_{\Omega} v \, f^n \mathop{dx} for all test functions :math:`v`, where :math:`k_n = t_n - t_{n-1}` denotes the time step. In the example below, we implement this variational problem with piecewise linear test and trial functions, but other choices are possible (just choose another finite element). Rewriting the variational problem in the standard form :math:`a(v, u_h) = L(v)` for all :math:`v`, we obtain the following pair of bilinear and linear forms: .. math:: a(v, u_h^n; c, k) &= \int_{\Omega} v \, u_h^n \mathop{dx} + k_n \int_{\Omega} c \, \nabla v \cdot \nabla u_h^n \mathop{dx}, \\ L(v; u_h^{n-1}, f, k) &= \int_{\Omega} v \, u_h^{n-1} \mathop{dx} + k_n \int_{\Omega} v \, f^n \mathop{dx}, which can be implemented as follows:: element = FiniteElement("Lagrange", triangle, 1) v = TestFunction(element) # Test function u1 = TrialFunction(element) # Value at t_n u0 = Coefficient(element) # Value at t_n-1 c = Coefficient(element) # Heat conductivity f = Coefficient(element) # Heat source k = Constant("triangle") # Time step a = v*u1*dx + k*c*dot(grad(v), grad(u1))*dx L = v*u0*dx + k*v*f*dx This example is implemented in the file ``Heat.py`` in the collection of demonstration forms included with the UFL source distribution. Mixed formulation of Stokes =========================== To solve Stokes' equations, .. math:: - \Delta u + \nabla p &= f, \\ \nabla \cdot u &= 0, we write the variational problem in standard form :math:`a(v, u) = L(v)` for all :math:`v` to obtain the following pair of bilinear and linear forms: .. math:: a((v, q), (u, p)) &= \int_{\Omega} \nabla v : \nabla u - (\nabla \cdot v) \, p + q \, (\nabla \cdot u) \mathop{dx}, \\ L((v, q); f) &= \int_{\Omega} v \cdot f \mathop{dx}. Using a mixed formulation with Taylor-Hood elements, this can be implemented as follows:: cell = triangle P2 = VectorElement("Lagrange", cell, 2) P1 = FiniteElement("Lagrange", cell, 1) TH = P2 * P1 (v, q) = TestFunctions(TH) (u, p) = TrialFunctions(TH) f = Coefficient(P2) a = (inner(grad(v), grad(u)) - div(v)*p + q*div(u))*dx L = dot(v, f)*dx This example is implemented in the file ``Stokes.py`` in the collection of demonstration forms included with the UFL source distribution. Mixed formulation of Poisson ============================ We next consider the following formulation of Poisson's equation as a pair of first order equations for :math:`\sigma \in H(\mathrm{div})` and :math:`u \in L^2`: .. math:: \sigma + \nabla u &= 0, \\ \nabla \cdot \sigma &= f. We multiply the two equations by a pair of test functions :math:`\tau` and :math:`w` and integrate by parts to obtain the following variational problem: Find :math:`(\sigma, u) \in V = H(\mathrm{div}) \times L^2` such that .. math:: a((\tau, w), (\sigma, u)) = L((\tau, w)) \quad \forall \, (\tau, w) \in V, where .. math:: a((\tau, w), (\sigma, u)) &= \int_{\Omega} \tau \cdot \sigma - \nabla \cdot \tau \, u + w \nabla \cdot \sigma \mathop{dx}, \\ L((\tau, w); f) &= \int_{\Omega} w \cdot f \mathop{dx}. We may implement the corresponding forms in our form language using first order BDM H(div)-conforming elements for :math:`\sigma` and piecewise constant :math:`L^2`-conforming elements for :math:`u` as follows:: cell = triangle BDM1 = FiniteElement("Brezzi-Douglas-Marini", cell, 1) DG0 = FiniteElement("Discontinuous Lagrange", cell, 0) element = BDM1 * DG0 (tau, w) = TestFunctions(element) (sigma, u) = TrialFunctions(element) f = Coefficient(DG0) a = (dot(tau, sigma) - div(tau)*u + w*div(sigma))*dx L = w*f*dx This example is implemented in the file ``MixedPoisson.py`` in the collection of demonstration forms included with the UFL source distribution. Poisson equation with DG elements ================================= We consider again Poisson's equation, but now in an (interior penalty) discontinuous Galerkin formulation: Find :math:`u \in V = L^2` such that .. math:: a(v, u) = L(v) \quad \forall v \in V, where .. math:: a(v, u; h) &= \int_{\Omega} \nabla v \cdot \nabla u \mathop{dx} \\ &+ \sum_S \int_S - \langle \nabla v \rangle \cdot [[ u ]]_n - [[ v ]]_n \cdot \langle \nabla u \rangle + (\alpha/h) [[ v ]]_n \cdot [[ u ]]_n \mathop{dS} \\ &+ \int_{\partial\Omega} - \nabla v \cdot [[ u ]]_n - [[ v ]]_n \cdot \nabla u + (\gamma/h) v u \mathop{ds} \\ L(v; f, g) &= \int_{\Omega} v f \mathop{dx} + \int_{\partial\Omega} v g \mathop{ds}. The corresponding finite element variational problem for discontinuous first order elements may be implemented as follows:: cell = triangle DG1 = FiniteElement("Discontinuous Lagrange", cell, 1) v = TestFunction(DG1) u = TrialFunction(DG1) f = Coefficient(DG1) g = Coefficient(DG1) h = 2.0*Circumradius(cell) alpha = 1 gamma = 1 a = dot(grad(v), grad(u))*dx \ - dot(avg(grad(v)), jump(u))*dS \ - dot(jump(v), avg(grad(u)))*dS \ + alpha/h('+')*dot(jump(v), jump(u))*dS \ - dot(grad(v), jump(u))*ds \ - dot(jump(v), grad(u))*ds \ + gamma/h*v*u*ds L = v*f*dx + v*g*ds .. TODO: set alpha and gamma to proper values This example is implemented in the file ``PoissonDG.py`` in the collection of demonstration forms included with the UFL source distribution. The Quadrature family ===================== .. *FIXME: The code examples in this section have been mostly converted to UFL syntax, but the quadrature elements need some more updating, as well as the text. In UFL, I think we should define the element order and not the number of points for quadrature elements, and let the form compiler choose a quadrature rule. This way the form depends less on the cell in use.* We consider here a nonlinear version of the Poisson's equation to illustrate the main point of the ``Quadrature`` finite element family. The strong equation looks as follows: .. math:: - \nabla \cdot (1+u^2)\nabla u = f. The linearised bilinear and linear forms for this equation, .. math:: a(v, u; u_0) &= \int_{\Omega} (1+u_{0}^2) \nabla v \cdot \nabla u \mathop{dx} + \int_{\Omega} 2u_0 u \nabla v \cdot \nabla u_0 \mathop{dx}, \\ L(v; u_0, f) &= \int_{\Omega} v \, f \mathop{dx} - \int_{\Omega} (1+u_{0}^2) \nabla v \cdot \nabla u_0 \mathop{dx}, can be implemented in a single form file as follows:: element = FiniteElement("Lagrange", triangle, 1) v = TestFunction(element) u = TrialFunction(element) u0 = Coefficient(element) f = Coefficient(element) a = (1+u0**2)*dot(grad(v), grad(u))*dx + 2*u0*u*dot(grad(v), grad(u0))*dx L = v*f*dx - (1+u0**2)*dot(grad(v), grad(u0))*dx Here, :math:`u_0` represents the solution from the previous Newton-Raphson iteration. The above form will be denoted REF1 and serves as our reference implementation for linear elements. A similar form (REF2) using quadratic elements will serve as a reference for quadratic elements. Now, assume that we want to treat the quantities :math:`C = (1 + u_{0}^2)` and :math:`\sigma_0 = (1+u_{0}^2) \nabla u_0` as given functions (to be computed elsewhere). Substituting into the bilinear and linear forms, we obtain .. math:: a(v, u) &= \int_{\Omega} \text{C} \nabla v \cdot \nabla u \mathop{dx} + \int_{\Omega} 2u_0 u \nabla v \cdot \nabla u_0 \mathop{dx}, \\ L(v; \sigma_0, f) &= \int_{\Omega} v \, f \mathop{dx} - \int_{\Omega} \nabla v \cdot \sigma_0 \mathop{dx}. Then, two additional forms are created to compute the tangent C and the gradient of :math:`u_0`. This situation shows up in plasticity and other problems where certain quantities need to be computed elsewhere (in user-defined functions). The three forms using the standard ``FiniteElement`` (linear elements) can then be implemented as :: # NonlinearPoisson.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) DG = FiniteElement("Discontinuous Lagrange", triangle, 0) sig = VectorElement("Discontinuous Lagrange", triangle, 0) v = TestFunction(element) u = TrialFunction(element) u0 = Coefficient(element) C = Coefficient(DG) sig0 = Coefficient(sig) f = Coefficient(element) a = v.dx(i)*C*u.dx(i)*dx + v.dx(i)*2*u0*u*u0.dx(i)*dx L = v*f*dx - dot(grad(v), sig0)*dx and :: # Tangent.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) DG = FiniteElement("Discontinuous Lagrange", triangle, 0) v = TestFunction(DG) u = TrialFunction(DG) u0= Coefficient(element) a = v*u*dx L = v*(1.0 + u0**2)*dx and :: # Gradient.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) DG = VectorElement("Discontinuous Lagrange", triangle, 0) v = TestFunction(DG) u = TrialFunction(DG) u0 = Coefficient(element) a = dot(v, u)*dx L = dot(v, (1.0 + u0**2)*grad(u0))*dx The three forms can be implemented using the ``QuadratureElement`` in a similar fashion in which only the element declaration is different:: # QE1NonlinearPoisson.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) QE = FiniteElement("Quadrature", triangle, 2) sig = VectorElement("Quadrature", triangle, 2) and :: # QE1Tangent.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) QE = FiniteElement("Quadrature", triangle, 2) and :: # QE1Gradient.py from ufl import * element = FiniteElement("Lagrange", triangle, 1) QE = VectorElement("Quadrature", triangle, 2) Note that we use two points when declaring the ``QuadratureElement``. This is because the RHS of ``Tangent.form`` is second order and therefore we need two points for exact integration. Due to consistency issues, when passing functions around between the forms, we also need to use two points when declaring the ``QuadratureElement`` in the other forms. Typical values of the relative residual for each Newton iteration for all three approaches are shown in the table below. It is to be noted that the convergence rate is quadratic as it should be for all three methods. Relative residuals for each approach for linear elements:: Iteration REF1 FE1 QE1 ========= ==== === === 1 6.3e-02 6.3e-02 6.3e-02 2 5.3e-04 5.3e-04 5.3e-04 3 3.7e-08 3.7e-08 3.7e-08 4 2.9e-16 2.9e-16 2.5e-16 However, if quadratic elements are used to interpolate the unknown field :math:`u`, the order of all elements in the above forms is increased by 1. This influences the convergence rate as seen in the table below. Clearly, using the standard ``FiniteElement`` leads to a poor convergence whereas the ``QuadratureElement`` still leads to quadratic convergence. Relative residuals for each approach for quadratic elements:: Iteration REF2 FE2 QE2 ========= ==== === === 1 2.6e-01 3.9e-01 2.6e-01 2 1.1e-02 4.6e-02 1.1e-02 3 1.2e-05 1.1e-02 1.6e-05 4 1.1e-11 7.2e-04 9.1e-09 More examples ============= Feel free to send additional demo form files for your favourite PDE to the UFL mailing list. .. %TODO: Modify rest of FFC example forms to UFL syntax and add here. ufl-2024.2.0/doc/sphinx/source/manual/form_language.rst000066400000000000000000001460041470142567200227370ustar00rootroot00000000000000************* Form language ************* UFL consists of a set of operators and atomic expressions that can be used to express variational forms and functionals. Below we will define all these operators and atomic expressions in detail. UFL is built on top of the Python language, and any Python code is valid in the definition of a form. In particular, comments (lines starting with ``#``) and functions (keyword ``def``, see user-defined_ below) are useful in the definition of a form. However, it is usually a good idea to avoid using advanced Python features in the form definition, to stay close to the mathematical notation. The entire form language can be imported in Python with the line .. code-block:: python from ufl import * This can be useful for experimenting with the language in an interactive Python interpreter. Forms and integrals =================== UFL is designed to express forms in the following generalized format: .. math:: a(\mathbf{v}; \mathbf{w}) = \sum_{k=1}^{n_c} \int_{\Omega_k} I^c_k(\mathbf{v}; \mathbf{w}) dx + \sum_{k=1}^{n_e} \int_{\partial\Omega_k} I^e_k(\mathbf{v}; \mathbf{w}) ds + \sum_{k=1}^{n_i} \int_{\Gamma_k} I^i_k(\mathbf{v}; \mathbf{w}) dS. Here the form :math:`a` depends on the *form arguments* :math:`\mathbf{v} = (v_1, \ldots, v_r)` and the *form coefficients* :math:`\mathbf{w} = (w_1, \ldots, w_n)`, and its expression is a sum of integrals. Each term of a valid form expression must be a scalar-valued expression integrated exactly once. How to define form arguments and integrand expressions is detailed in the rest of this chapter. Integrals are expressed through multiplication with a measure, representing an integral over either * the interior of the domain :math:`\Omega` (``dx``, cell integral); * the boundary :math:`\partial\Omega` of :math:`\Omega` (``ds``, exterior facet integral); * the set of interior facets :math:`\Gamma` (``dS``, interior facet integral). (Note that newer versions of UFL support several other integral types currently not documented here). As a basic example, assume ``v`` is a scalar-valued expression and consider the integral of ``v`` over the interior of :math:`\Omega`. This may be expressed as:: a = v*dx and the integral of ``v`` over :math:`\partial\Omega` is written as:: a = v*ds. Alternatively, measures can be redefined to represent numbered subsets of a domain, such that a form evaluates to different expressions on different parts of the domain. If ``c``, ``e0`` and ``e1`` are scalar-valued expressions, then:: a = c*dx + e0*ds(0) + e1*ds(1) represents .. math:: a = \int_\Omega c\,dx + \int_{\partial\Omega_0} e_0 \, ds + \int_{\partial\Omega_1} e_1 \, ds, where .. math:: \partial\Omega_0 \subset \partial\Omega, \qquad \partial\Omega_1 \subset \partial\Omega. .. note:: The domain :math:`\Omega`, its subdomains and boundaries are not known to UFL. These are defined in a problem solving environment such as DOLFIN, which uses UFL to specify forms. Finite element spaces ===================== Before defining forms which can be integrated, it is necessary to describe the finite element spaces over which the integration takes place. UFL can represent very flexible general hierarchies of mixed finite elements, and has predefined names for most common element families. A finite element space is defined by an element domain, shape functions and nodal variables. In UFL, the element domain is called a ``Cell``. Cells ----- A polygonal cell is defined by a shape name and a geometric dimension, written as:: cell = Cell(shape, gdim) Valid shapes are "interval", "triangle", "tetrahedron", "quadrilateral", and "hexahedron". Some examples:: # Regular triangle cell cell = Cell("triangle") # Triangle cell embedded in 3D space cell = Cell("triangle", 3) Objects for regular cells of all basic shapes are predefined:: # Predefined linear cells cell = interval cell = triangle cell = tetrahedron cell = quadrilateral cell = hexahedron In the rest of this document, a variable name ``cell`` will be used where any cell is a valid argument, to make the examples dimension-independent wherever possible. Using a variable ``cell`` to hold the cell type used in a form is highly recommended, since this makes most form definitions dimension-independent. Element families ---------------- UFL predefines a set of names of known element families. When defining a finite element below, the argument ``family`` is a string and its possible values include * ``"Lagrange"`` or ``"CG"``, representing standard scalar Lagrange finite elements (continuous piecewise polynomial functions); * ``"Discontinuous Lagrange"`` or ``"DG"``, representing scalar discontinuous Lagrange finite elements (discontinuous piecewise polynomial functions); * ``"Crouzeix-Raviart"`` or ``"CR"``, representing scalar Crouzeix--Raviart elements; * ``"Brezzi-Douglas-Marini"`` or ``"BDM"``, representing vector-valued Brezzi--Douglas--Marini H(div) elements; * ``"Brezzi-Douglas-Fortin-Marini`` or ``"BDFM"``, representing vector-valued Brezzi--Douglas--Fortin--Marini H(div) elements; * ``"Raviart-Thomas"`` or ``"RT"``, representing vector-valued Raviart--Thomas H(div) elements. * ``"Nedelec 1st kind H(div)"`` or ``"N1div"``, representing vector-valued Nedelec H(div) elements (of the first kind). * ``"Nedelec 2st kind H(div)"`` or ``"N2div"``, representing vector-valued Nedelec H(div) elements (of the second kind). * ``"Nedelec 1st kind H(curl)"`` or ``"N1curl"``, representing vector-valued Nedelec H(curl) elements (of the first kind). * ``"Nedelec 2st kind H(curl)"`` or ``"N2curl"``, representing vector-valued Nedelec H(curl) elements (of the second kind). * ``"Bubble"``, representing bubble elements, useful for example to build the mini elements. * ``"Quadrature"`` or ``"Q"``, representing artificial "finite elements" with degrees of freedom being function evaluations at quadrature points; * ``"Boundary Quadrature"`` or ``"BQ"``, representing artificial "finite elements" with degrees of freedom being function evaluations at quadrature points on the boundary. Note that new versions of UFL also support notation from the Periodic Table of Finite Elements, currently not documented here. Basic elements -------------- A ``FiniteElement``, sometimes called a basic element, represents a finite element from some family on a given cell with a certain polynomial degree. Valid families and cells are explained above. The notation is :: element = FiniteElement(family, cell, degree) Some examples:: element = FiniteElement("Lagrange", interval, 3) element = FiniteElement("DG", tetrahedron, 0) element = FiniteElement("BDM", triangle, 1) Vector elements --------------- A ``VectorElement`` represents a combination of basic elements such that each component of a vector is represented by the basic element. The size is usually omitted, the default size equals the geometry dimension. The notation is :: element = VectorElement(family, cell, degree[, size]) Some examples:: # A quadratic "P2" vector element on a triangle element = VectorElement("CG", triangle, 2) # A linear 3D vector element on a 1D interval element = VectorElement("CG", interval, 1, size=3) # A six-dimensional piecewise constant element on a tetrahedron element = VectorElement("DG", tetrahedron, 0, size=6) Tensor elements --------------- A ``TensorElement`` represents a combination of basic elements such that each component of a tensor is represented by the basic element. The shape is usually omitted, the default shape is :math: `(d, d)` where :math: `d` is the geometric dimension. The notation is :: element = TensorElement(family, cell, degree[, shape, symmetry]) Any shape tuple consisting of positive integers is valid, and the optional symmetry can either be set to ``True`` which means standard matrix symmetry (like :math:`A_{ij} = A_{ji}`), or a ``dict`` like ``{ (0,1):(1,0), (0,2):(2,0) }`` where the ``dict`` keys are index tuples that are represented by the corresponding ``dict`` value. Examples:: element = TensorElement("CG", cell, 2) element = TensorElement("DG", cell, 0, shape=(6,6)) element = TensorElement("DG", cell, 0, symmetry=True) element = TensorElement("DG", cell, 0, symmetry={(0,0): (1,1)}) Mixed elements -------------- A ``MixedElement`` represents an arbitrary combination of other elements. ``VectorElement`` and ``TensorElement`` are special cases of a ``MixedElement`` where all sub-elements are equal. General notation for an arbitrary number of subelements:: element = MixedElement(element1, element2[, element3, ...]) Shorthand notation for two subelements:: element = element1 * element2 .. note:: The ``*`` operator is left-associative, such that:: element = element1 * element2 * element3 represents ``(e1 * e2) * e3``, i.e. this is a mixed element with two sub-elements ``(e1 * e2)`` and ``e3``. See `Form arguments`_ for details on how defining functions on mixed spaces can differ from defining functions on other finite element spaces. Examples:: # Taylor-Hood element V = VectorElement("Lagrange", cell, 2) P = FiniteElement("Lagrange", cell, 1) TH = V * P # A tensor-vector-scalar element T = TensorElement("Lagrange", cell, 2, symmetry=True) V = VectorElement("Lagrange", cell, 1) P = FiniteElement("DG", cell, 0) ME = MixedElement(T, V, P) EnrichedElement --------------- The data type ``EnrichedElement`` represents the vector sum of two (or more) finite elements. Example: The Mini element can be constructed as :: P1 = VectorElement("Lagrange", "triangle", 1) B = VectorElement("Bubble", "triangle", 3) Q = FiniteElement("Lagrange", "triangle", 1) Mini = (P1 + B) * Q Form arguments ============== Form arguments are divided in two groups, arguments and coefficients. An ``Argument`` represents an arbitrary basis function in a given discrete finite element space, while a ``Coefficient`` represents a function in a discrete finite element space that will be provided by the user at a later stage. The number of ``Argument``\ s that occur in a ``Form`` equals the "arity" of the form. Basis functions --------------- The data type ``Argument`` represents a basis function on a given finite element. An ``Argument`` must be created for a previously declared finite element (simple or mixed):: v = Argument(element) Note that more than one ``Argument`` can be declared for the same ``FiniteElement``. Basis functions are associated with the arguments of a multilinear form in the order of declaration. For a ``MixedElement``, the function ``Arguments`` can be used to construct tuples of ``Argument``\ s, as illustrated here for a mixed Taylor--Hood element:: v, q = Arguments(TH) u, p = Arguments(TH) For a ``Argument`` on a ``MixedElement`` (or ``VectorElement`` or ``TensorElement``), the function ``split`` can be used to extract basis function values on subspaces, as illustrated here for a mixed Taylor--Hood element:: vq = Argument(TH) v, q = split(up) This is equivalent to the previous use of ``Arguments``:: v, q = Arguments(TH) For convenience, ``TestFunction`` and ``TrialFunction`` are special instances of ``Argument`` with the property that a ``TestFunction`` will always be the first argument in a form and ``TrialFunction`` will always be the second argument in a form (order of declaration does not matter). Their usage is otherwise the same as for ``Argument``:: v = TestFunction(element) u = TrialFunction(element) v, q = TestFunctions(TH) u, p = TrialFunctions(TH) Meshes and function spaces -------------------------- Note that newer versions of UFL introduce the concept of a Mesh and a FunctionSpace. These are currently not documented here. Coefficient functions --------------------- The data type ``Coefficient`` represents a function belonging to a given finite element space, that is, a linear combination of basis functions of the finite element space. A ``Coefficient`` must be declared for a previously declared ``FiniteElement``:: f = Coefficient(element) Note that the order in which ``Coefficient``\ s are declared is important, directly reflected in the ordering they have among the arguments to each ``Form`` they are part of. ``Coefficient`` is used to represent user-defined functions, including, e.g., source terms, body forces, variable coefficients and stabilization terms. UFL treats each ``Coefficient`` as a linear combination of unknown basis functions with unknown coefficients, that is, UFL knows nothing about the concrete basis functions of the element and nothing about the value of the function. .. note:: Note that more than one function can be declared for the same ``FiniteElement``. The following example declares two ``Argument``\ s and two ``Coefficient``\ s for the same ``FiniteElement``:: v = Argument(element) u = Argument(element) f = Coefficient(element) g = Coefficient(element) For a ``Coefficient`` on a ``MixedElement`` (or ``VectorElement`` or ``TensorElement``), the function ``split`` can be used to extract function values on subspaces, as illustrated here for a mixed Taylor--Hood element:: up = Coefficient(TH) u, p = split(up) There is a shorthand for this, whose use is similar to ``Arguments``, called ``Coefficients``:: u, p = Coefficients(TH) Spatially constant values can conveniently be represented by ``Constant``, ``VectorConstant``, and ``TensorConstant``:: c0 = Constant(cell) v0 = VectorConstant(cell) t0 = TensorConstant(cell) These three lines are equivalent with first defining DG0 elements and then defining a ``Coefficient`` on each, illustrated here:: DG0 = FiniteElement("Discontinuous Lagrange", cell, 0) DG0v = VectorElement("Discontinuous Lagrange", cell, 0) DG0t = TensorElement("Discontinuous Lagrange", cell, 0) c1 = Coefficient(DG0) v1 = Coefficient(DG0v) t1 = Coefficient(DG0t) Basic Datatypes =============== UFL expressions can depend on some other quantities in addition to the functions and basis functions described above. Literals and geometric quantities --------------------------------- Some atomic quantities are derived from the cell. For example, the (global) spatial coordinates are available as a vector valued expression ``SpatialCoordinate(cell)``:: # Linear form for a load vector with a sin(y) coefficient v = TestFunction(element) x = SpatialCoordinate(cell) L = sin(x[1])*v*dx Another quantity is the (outwards pointing) facet normal ``FacetNormal(cell)``. The normal vector is only defined on the boundary, so it can't be used in a cell integral. Example functional ``M``, an integral of the normal component of a function ``g`` over the boundary:: n = FacetNormal(cell) g = Coefficient(VectorElement("CG", cell, 1)) M = dot(n, g)*ds Python scalars (int, float) can be used anywhere a scalar expression is allowed. Another literal constant type is ``Identity`` which represents an :math:`n\times n` unit matrix of given size :math:`n`, as in this example:: # Geometric dimension d = cell.geometric_dimension() # d x d identiy matrix I = Identity(d) # Kronecker delta delta_ij = I[i,j] Indexing and tensor components ============================== UFL supports index notation, which is often a convenient way to express forms. The basic principle of index notation is that summation is implicit over indices repeated twice in each term of an expression. The following examples illustrate the index notation, assuming that each of the variables ``i`` and ``j`` has been declared as a free ``Index``: * ``v[i]*w[i]``: :math:`\sum_{i=0}^{n-1} v_i w_i = \mathbf{v}\cdot\mathbf{w}` * ``Dx(v, i)*Dx(w, i)``: :math:`\sum_{i=0}^{d-1} \frac{\partial v}{\partial x_i} \frac{\partial w}{\partial x_i} = \nabla v \cdot \nabla w` * ``Dx(v[i], i)``: :math:`\sum_{i=0}^{d-1} \frac{\partial v_i}{\partial x_i} = \nabla \cdot v` * ``Dx(v[i], j)*Dx(w[i], j)``: :math:`\sum_{i=0}^{n-1} \sum_{j=0}^{d-1} \frac{\partial v_i}{\partial x_j} \frac{\partial w_i}{\partial x_j} = \nabla \mathbf{v} : \nabla \mathbf{w}` Here we will try to very briefly summarize the basic concepts of tensor algebra and index notation, just enough to express the operators in UFL. Assuming an Euclidean space in :math:`d` dimensions with :math:`1 \le d \le 3`, and a set of orthonormal basis vectors :math:`\mathbf{i}_i` for :math:`i \in {0, \ldots, d-1 }`, we can define the dot product of any two basis functions as .. math:: \mathbf{i}_{i} \cdot \mathbf{i}_{j} = \delta_{ij}, where :math:`\delta_{ij}` is the Kronecker delta .. math:: \delta_{ij} \equiv \begin{cases} 1, \quad i = j, \\ 0, \quad \text{otherwise}. \end{cases} A rank 1 tensor (vector) quantity :math:`\mathbf{v}` can be represented in terms of unit vectors and its scalar components in that basis. In tensor algebra it is common to assume implicit summation over indices repeated twice in a product: .. math:: \mathbf{v} = v_k \mathbf{i}_k \equiv \sum_k v_k \mathbf{i}_k. Similarly, a rank two tensor (matrix) quantity :math:`\mathbf{A}` can be represented in terms of unit matrices, that is outer products of unit vectors: .. math:: \mathbf{A} = A_{ij} \mathbf{i}_i \mathbf{i}_j \equiv \sum_i \sum_j A_{ij} \mathbf{i}_i \mathbf{i}_j . This generalizes to tensors of arbitrary rank: .. math:: \mathcal{C} &= C_\iota \mathbf{i}_{\iota_0} \otimes \cdots \otimes \mathbf{i}_{\iota_{r-1}} \\ &\equiv \sum_{\iota_0} \cdots \sum_{\iota_{r-1}} C_\iota \mathbf{i}_{\iota_0}\otimes\cdots \otimes \mathbf{i}_{\iota_{r-1}}, where :math:`\mathcal{C}` is a rank :math:`r` tensor and :math:`\iota` is a multi-index of length :math:`r`. When writing equations on paper, a mathematician can easily switch between the :math:`\mathbf{v}` and :math:`v_i` representations without stating it explicitly. This is possible because of flexible notation and conventions. In a programming language, we can't use the boldface notation which associates :math:`\mathbf{v}` and :math:`v` by convention, and we can't always interpret such conventions unambiguously. Therefore, UFL requires that an expression is explicitly mapped from its tensor representation (:math:`\mathbf{v}`, :math:`\mathbf{A}`) to its component representation (:math:`v_i`, :math:`A_{ij}`) and back. This is done using ``Index`` objects, the indexing operator (``v[i]``) and the function ``as_tensor``. More details on these follow. In the following descriptions of UFL operator syntax, i-l and p-s are assumed to be predefined indices, and unless otherwise specified the name v refers to some vector valued expression, and the name A refers to some matrix valued expression. The name C refers to a tensor expression of arbitrary rank. Defining indices ---------------- A set of indices ``i``, ``j``, ``k``, ``l`` and ``p``, ``q``, ``r``, ``s`` are predefined, and these should be enough for many applications. Examples will usually use these objects instead of creating new ones to conserve space. The data type ``Index`` represents an index used for subscripting derivatives or taking components of non-scalar expressions. To create indices you can either make a single one using ``Index()`` or make several at once conveniently using ``indices(n)``:: i = Index() j, k, l = indices(3) Each of these represents an ``index range`` determined by the context; if used to subscript a tensor-valued expression, the range is given by the shape of the expression, and if used to subscript a derivative, the range is given by the dimension :math:`d` of the underlying shape of the finite element space. As we shall see below, indices can be a powerful tool when used to define forms in tensor notation. .. note:: Advanced usage If using UFL inside DOLFIN or another larger programming environment, it is a good idea to define your indices explicitly just before your form uses them, to avoid name collisions. The definition of the predefined indices is simply:: i, j, k, l = indices(4) p, q, r, s = indices(4) .. note:: Advanced usage Note that in the old FFC notation, the definition :: i = Index(0) meant that the value of the index remained constant. This does not mean the same in UFL, and this notation is only meant for internal usage. Fixed indices are simply integers instead:: i = 0 Taking components of tensors ---------------------------- Basic fixed indexing of a vector valued expression v or matrix valued expression A: * ``v[0]``: component access, representing the scalar value of the first component of v * ``A[0,1]``: component access, representing the scalar value of the first row, second column of A Basic indexing: * ``v[i]``: component access, representing the scalar value of some component of v * ``A[i,j]``: component access, representing the scalar value of some component i,j of A More advanced indexing: * ``A[i,0]``: component access, representing the scalar value of some component i of the first column of A * ``A[i,:]``: row access, representing some row i of A, i.e. rank(A[i,:]) == 1 * ``A[:,j]``: column access, representing some column j of A, i.e. rank(A[:,j]) == 1 * ``C[...,0]``: subtensor access, representing the subtensor of A with the last axis fixed, e.g., A[...,0] == A[:,0] * ``C[j,...]``: subtensor access, representing the subtensor of A with the first axis fixed, e.g., A[j,...] == A[j,:] Making tensors from components ------------------------------ If you have expressions for scalar components of a tensor and wish to convert them to a tensor, there are two ways to do it. If you have a single expression with free indices that should map to tensor axes, like mapping :math:`v_k` to :math:`\mathbf{v}` or :math:`A_{ij}` to :math:`\mathbf{A}`, the following examples show how this is done:: vk = Identity(cell.geometric_dimension())[0,k] v = as_tensor(vk, (k,)) Aij = v[i]*u[j] A = as_tensor(Aij, (i,j)) Here ``v`` will represent unit vector :math:`\mathbf{i}_0`, and ``A`` will represent the outer product of ``v`` and ``u``. If you have multiple expressions without indices, you can build tensors from them just as easily, as illustrated here:: v = as_vector([1.0, 2.0, 3.0]) A = as_matrix([[u[0], 0], [0, u[1]]]) B = as_matrix([[a+b for b in range(2)] for a in range(2)]) Here ``v``, ``A`` and ``B`` will represent the expressions .. math:: \mathbf{v} &= \mathbf{i}_0 + 2 \mathbf{i}_1 + 3 \mathbf{i}_2, \\ \mathbf{A} &= \begin{bmatrix} u_0 & 0 \\ 0 & u_1 \end{bmatrix}, \\ \mathbf{B} &= \begin{bmatrix} 0 & 1 \\ 1 & 2 \end{bmatrix}. Note that the function ``as_tensor`` generalizes from vectors to tensors of arbitrary rank, while the alternative functions ``as_vector`` and ``as_matrix`` work the same way but are only for constructing vectors and matrices. They are included for readability and convenience. Implicit summation ------------------ Implicit summation can occur in only a few situations. A product of two terms that shares the same free index is implicitly treated as a sum over that free index: * ``v[i]*v[i]``: :math:`\sum_i v_i v_i` * ``A[i,j]*v[i]*v[j]``: :math:`\sum_j (\sum_i A_{ij} v_i) v_j` A tensor valued expression indexed twice with the same free index is treated as a sum over that free index: * ``A[i,i]``: :math:`\sum_i A_{ii}` * ``C[i,j,j,i]``: :math:`\sum_i \sum_j C_{ijji}` The spatial derivative, in the direction of a free index, of an expression with the same free index, is treated as a sum over that free index: * ``v[i].dx(i)``: :math:`\sum_i \frac{d(v_{i})}{dx_i}` * ``A[i,j].dx(i)``: :math:`\sum_i \frac{d(A_{ij})}{dx_i}` Note that these examples are some times written :math:`v_{i,i}` and :math:`A_{ij,i}` in pen-and-paper index notation. Basic algebraic operators ========================= The basic algebraic operators ``+``, ``-``, ``*``, ``/`` can be used freely on UFL expressions. They do have some requirements on their operands, summarized here: Addition or subtraction, ``a + b`` or ``a - b``: * The operands ``a`` and ``b`` must have the same shape. * The operands ``a`` and ``b`` must have the same set of free indices. Division, ``a / b``: * The operand ``b`` must be a scalar expression. * The operand ``b`` must have no free indices. * The operand ``a`` can be non-scalar with free indices, in which division represents scalar division of all components with the scalar ``b``. Multiplication, ``a * b``: * The only non-scalar operations allowed is scalar-tensor, matrix-vector and matrix-matrix multiplication. * If either of the operands have any free indices, both must be scalar. * If any free indices are repeated, summation is implied. Basic nonlinear functions ========================= Some basic nonlinear functions are also available, their meaning mostly obvious. The following functions are defined and should be imported from `ufl` * ``sign(f)``: the sign of f (+1 or -1). * ``sqrt(f)``: square root, :math:`\sqrt{f}` * ``exp(f)``: exponential of f * ``ln(f)``: natural logarithm of f * ``cos(f)``: cosine of f * ``sin(f)``: sine of f * ``tan(f)``: tangent of f * ``cosh(f)``: hyperbolic cosine of f * ``sinh(f)``: hyperbolic sine of f * ``tanh(f)``: hyperbolic tangent of f * ``acos(f)``: inverse cosine of f * ``asin(f)``: inverse sine of f * ``atan(f)``: inverse tangent of f * ``atan2(f1, f2)``: inverse tangent of (f1/f2) * ``erf(f)``: error function of f, :math:`{2\over\sqrt{\pi}} \int_0^f \exp(-t^2) \mathop{dt}` * ``bessel_J(nu, f)``: Bessel function of the first kind, :math:`J_\nu(f)` * ``bessel_Y(nu, f)``: Bessel function of the second kind, :math:`Y_\nu(f)` * ``bessel_I(nu, f)``: Modified Bessel function of the first kind, :math:`I_\nu(f)` * ``bessel_K(nu, f)``: Modified Bessel function of the second kind, :math:`K_\nu(f)` while the following Python built in functions can be used without an import statement * ``abs(f)``: the absolute value of f. * ``pow(f, g)`` or ``f**g``: f to the power g, :math:`f^g` These functions do not accept non-scalar operands or operands with free indices or ``Argument`` dependencies. Tensor algebra operators ======================== ``transpose`` ------------- The transpose of a matrix A can be written as:: AT = transpose(A) AT = A.T AT = as_matrix(A[i,j], (j,i)) The definition of the transpose is .. math:: \mathtt{AT[i,j]} \leftrightarrow (A^{\top})_{ij} = A_{ji} For transposing higher order tensor expressions, index notation can be used:: AT = as_tensor(A[i,j,k,l], (l,k,j,i)) ``tr`` ------ The trace of a matrix A is the sum of the diagonal entries. This can be written as:: t = tr(A) t = A[i,i] The definition of the trace is .. math:: \mathtt{tr(A)} \leftrightarrow \mathrm{tr} \mathbf{A} = A_{ii} = \sum_{i=0}^{n-1} A_{ii}. ``dot`` ------- The dot product of two tensors a and b can be written:: # General tensors f = dot(a, b) # Vectors a and b f = a[i]*b[i] # Matrices a and b f = as_matrix(a[i,k]*b[k,j], (i,j)) The definition of the dot product of unit vectors is (assuming an orthonormal basis for a Euclidean space): .. math:: \mathbf{i}_i \cdot \mathbf{i}_j = \delta_{ij} where :math:`\delta_{ij}` is the Kronecker delta function. The dot product of higher order tensors follow from this, as illustrated with the following examples. An example with two vectors .. math:: \mathbf{v} \cdot \mathbf{u} = (v_i \mathbf{i}_i) \cdot (u_j \mathbf{i}_j) = v_i u_j (\mathbf{i}_i \cdot \mathbf{i}_j) = v_i u_j \delta_{ij} = v_i u_i An example with a tensor of rank two .. math:: \mathbf{A} \cdot \mathbf{B} &= (A_{ij} \mathbf{i}_i \mathbf{i}_j) \cdot (B_{kl} \mathbf{i}_k \mathbf{i}_l) \\ &= (A_{ij}B_{kl}) \mathbf{i}_i(\mathbf{i}_j \cdot \mathbf{i}_k) \mathbf{i}_l \\ &= (A_{ij}B_{kl}\delta_{jk}) \mathbf{i}_i \mathbf{i}_l \\ &= A_{ik}B_{kl} \mathbf{i}_i \mathbf{i}_l. This is the same as a matrix-matrix multiplication. An example with a vector and a tensor of rank two .. math:: \mathbf{v} \cdot \mathbf{A} &= (v_j \mathbf{i}_j) \cdot (A_{kl} \mathbf{i}_k \mathbf{i}_l) \\ &= (v_j A_{kl}) (\mathbf{i}_j \cdot \mathbf{i}_k) \mathbf{i}_l \\ &= (v_j A_{kl}\delta_{jk}) \mathbf{i}_l \\ &= v_k A_{kl} \mathbf{i}_l This is the same as a vector-matrix multiplication. This generalizes to tensors of arbitrary rank: the dot product applies to the last axis of a and the first axis of b. The tensor rank of the product is rank(a)+rank(b)-2. ``inner`` --------- The inner product is a contraction over all axes of a and b, that is the sum of all component-wise products. The operands must have exactly the same dimensions. For two vectors it is equivalent to the dot product. Complex values are supported by UFL taking the complex conjugate of the second operand. This has no impact if the values are real. If :math:`\mathbf{A}` and :math:`\mathbf{B}` are rank two tensors and :math:`\mathcal{C}` and :math:`\mathcal{D}` are rank 3 tensors their inner products are .. math:: \mathbf{A} : \mathbf{B} &= A_{ij} B^*_{ij} \\ \mathcal{C} : \mathcal{D} &= C_{ijk} D^*_{ijk} Using UFL notation, for real values, the following sets of declarations are equivalent:: # Vectors f = dot(a, b) f = inner(a, b) f = a[i]*b[i] # Matrices f = inner(A, B) f = A[i,j]*B[i,j] # Rank 3 tensors f = inner(C, D) f = C[i,j,k]*D[i,j,k] Note that, in the UFL notation, `dot` and `inner` products are not equivalent for complex values. ``outer`` --------- The outer product of two tensors a and b can be written:: A = outer(a, b) The general definition of the outer product of two tensors :math:`\mathcal{C}` of rank :math:`r` and :math:`\mathcal{D}` of rank :math:`s` is .. math:: \mathcal{C} \otimes \mathcal{D} = C^*_{\iota^a_0 \ldots \iota^a_{r-1}} D_{\iota^b_0 \ldots\iota^b_{s-1}} \mathbf{i}_{\iota^a_0}\otimes\cdots\otimes\mathbf{i}_{\iota^a_{r-2}} \otimes \mathbf{i}_{\iota^b_1} \otimes \cdots \otimes \mathbf{i}_{\iota^b_{s-1}} For consistency with the inner product, the complex conjugate is taken of the first operand. Some examples with vectors and matrices are easier to understand: .. math:: \mathbf{v} \otimes \mathbf{u} = v^*_i u_j \mathbf{i}_i \mathbf{i}_j, \\ \mathbf{v} \otimes \mathbf{B} = v^*_i B_{kl} \mathbf{i}_i \mathbf{i}_k \mathbf{i}_l, \\ \mathbf{A} \otimes \mathbf{B} = A^*_{ij} B_{kl} \mathbf{i}_i \mathbf{i}_j \mathbf{i}_k \mathbf{i}_l . The outer product of vectors is often written simply as .. math:: \mathbf{v} \otimes \mathbf{u} = \mathbf{v} \mathbf{u}, which is what we have done with :math:`\mathbf{i}_i \mathbf{i}_j` above. The rank of the outer product is the sum of the ranks of the operands. ``cross`` --------- The operator ``cross`` accepts as arguments two logically vector-valued expressions and returns a vector which is the cross product (vector product) of the two vectors: .. math:: \mathtt{cross(v, w)} \leftrightarrow \mathbf{v} \times \mathbf{w} = (v_1 w_2 - v_2 w_1, v_2 w_0 - v_0 w_2, v_0 w_1 - v_1 w_0) Note that this operator is only defined for vectors of length three. ``det`` ------- The determinant of a matrix A can be written as :: d = det(A) ``dev`` ------- The deviatoric part of matrix A can be written as :: B = dev(A) The definition is .. math:: {\rm dev} \mathbf{A} = \mathbf{A} - \frac{\mathbf{A}_{ii}}{d} \mathbf{I} where :math:`d` is the rank of matrix A and :math:`\mathbf{I}` is the identity matrix. ``sym`` ------- The symmetric part of A can be written as :: B = sym(A) The definition is .. math:: {\rm sym} \mathbf{A} = \frac{1}{2}(\mathbf{A} + \mathbf{A}^T) ``skew`` -------- The skew symmetric part of A can be written as :: B = skew(A) The definition is .. math:: {\rm skew} \mathbf{A} = \frac{1}{2}(\mathbf{A} - \mathbf{A}^T) ``cofac`` --------- The cofactor of a matrix A can be written as :: B = cofac(A) The definition is .. math:: {\rm cofac} \mathbf{A} = \det (\mathbf{A}) \mathbf{A}^{-1} The implementation of this is currently rather crude, with a hardcoded symbolic expression for the cofactor. Therefore, this is limited to 1x1, 2x2 and 3x3 matrices. ``inv`` ------- The inverse of matrix A can be written as :: Ainv = inv(A) The implementation of this is currently rather crude, with a hardcoded symbolic expression for the inverse. Therefore, this is limited to 1x1, 2x2 and 3x3 matrices. Differential Operators ====================== Three different kinds of derivatives are currently supported: spatial derivatives, derivatives w.r.t. user defined variables, and derivatives of a form or functional w.r.t. a function. Basic spatial derivatives ------------------------- Spatial derivatives hold a special physical meaning in partial differential equations and there are several ways to express those. The basic way is:: # Derivative w.r.t. x_2 f = Dx(v, 2) f = v.dx(2) # Derivative w.r.t. x_i g = Dx(v, i) g = v.dx(i) If ``v`` is a scalar expression, ``f`` here is the scalar derivative of ``v`` with respect to spatial direction :math:`z`. If ``v`` has no free indices, ``g`` is the scalar derivative in spatial direction :math:`x_i`, and ``g`` has the free index ``i``. This can be expressed compactly as :math:`v_{,i}`: .. math:: f = \frac{\partial v}{\partial x_2} = v_{,2}, \\ g = \frac{\partial v}{\partial x_i} = v_{,i}. If the expression to be differentiated w.r.t. :math:`x_i` has ``i`` as a free-index, implicit summation is implied:: # Sum of derivatives w.r.t. x_i for all i g = Dx(v[i], i) g = v[i].dx(i) Here ``g`` will represent the sum of derivatives w.r.t. :math:`x_i` for all ``i``, that is .. math:: g = \sum_i \frac{\partial v}{\partial x_i} = v_{i,i}. .. note:: `v[i].dx(i)` and :math:`v_{i,i}` with compact notation denote implicit summation. Compound spatial derivatives ---------------------------- UFL implements several common differential operators. The notation is simple and their names should be self-explanatory:: Df = grad(f) df = div(f) cf = curl(v) rf = rot(f) The operand ``f`` can have no free indices. Gradient -------- The gradient of a scalar :math:`u` is defined as .. math:: \mathrm{grad}(u) \equiv \nabla u = \sum_{k=0}^{d-1} \frac{\partial u}{\partial x_k} \mathbf{i}_k, which is a vector of all spatial partial derivatives of :math:`u`. The gradient of a vector :math:`\mathbf{v}` is defined as .. math:: \mathrm{grad}(\mathbf{v}) \equiv \nabla \mathbf{v} = \frac{\partial v_i}{\partial x_j} \mathbf{i}_i \mathbf{i}_j, which, written componentwise, reads .. math:: \mathbf{A} = \nabla \mathbf{v}, \qquad A_{ij} = v_{i,j} In general for a tensor :math:`\mathbf{A}` of rank :math:`r` the definition is .. math:: {\rm grad}(\mathbf{A}) \equiv \nabla \mathbf{A} = (\frac{\partial}{\partial x_i}) (A_\iota\mathbf{i}_{\iota_0} \otimes\cdots\otimes \mathbf{i}_{\iota_{r-1}}) \otimes \mathbf{i}_i = \frac{\partial A_\iota}{\partial x_i} \mathbf{i}_{\iota_0} \otimes \cdots \otimes \mathbf{i}_{\iota_{r-1}} \otimes \mathbf{i}_i, where :math:`\iota` is a multi-index of length :math:`r`. In UFL, the following pairs of declarations are equivalent:: Dfi = grad(f)[i] Dfi = f.dx(i) Dvi = grad(v)[i, j] Dvi = v[i].dx(j) DAi = grad(A)[..., i] DAi = A.dx(i) for a scalar expression ``f``, a vector expression ``v``, and a tensor expression ``A`` of arbitrary rank. Divergence ---------- The divergence of any nonscalar (vector or tensor) expression :math:`\mathbf{A}` is defined as the contraction of the partial derivative over the last axis of the expression. The divergence of a vector :math:`\mathbf{v}` is defined as .. math:: \mathrm{div}(\mathbf{v}) \equiv \nabla\cdot\mathbf{v} = \sum_{k=0}^{d-1}\frac{\partial v_i}{\partial x_i} In UFL, the following declarations are equivalent:: dv = div(v) dv = v[i].dx(i) dA = div(A) dA = A[..., i].dx(i) for a vector expression v and a tensor expression A. Curl and rot ------------ The operator ``curl`` or ``rot`` accepts as argument a vector-valued expression and returns its curl .. math:: \mathrm{curl}(\mathbf{v}) = \nabla \times \mathbf{v} = (\frac{\partial v_2}{\partial x_1} - \frac{\partial v_1}{\partial x_2}, \frac{\partial v_0}{\partial x_2} - \frac{\partial v_2}{\partial x_0}, \frac{\partial v_1}{\partial x_0} - \frac{\partial v_0}{\partial x_1}). .. note:: The `curl` or `rot` operator is only defined for vectors of length three. In UFL, the following declarations are equivalent:: omega = curl(v) omega = rot(v) Variable derivatives -------------------- UFL also supports differentiation with respect to user defined variables. A user defined variable can be any expression that is defined as a variable. The notation is illustrated here:: # Define some arbitrary expression u = Coefficient(element) w = sin(u**2) # Annotate expression w as a variable that can be used by "diff" w = variable(w) # This expression is a function of w F = w**2 # The derivative of expression F w.r.t. the variable w dF = diff(F, w) # == 2*w Note that the variable ``w`` still represents the same expression. This can be useful for example to implement material laws in hyperelasticity where the stress tensor is derived from a Helmholtz strain energy function. Currently, UFL does not implement time in any particular way, but differentiation w.r.t. time can be done without this support through the use of a constant variable t:: t = variable(Constant(cell)) f = sin(x[0])**2 * cos(t) dfdt = diff(f, t) Functional derivatives ---------------------- The third and final kind of derivative are derivatives of functionals or forms w.r.t. to a ``Coefficient``. This is described in more detail in the section `AD`_ about form transformations. DG operators ============ UFL provides operators for implementation of discontinuous Galerkin methods. These include the evaluation of the jump and average of a function (or in general an expression) over the interior facets (edges or faces) of a mesh. Restriction: ``v('+')`` and ``v('-')`` ----------------------------------------------------------- When integrating over interior facets (``*dS``), one may restrict expressions to the positive or negative side of the facet:: element = FiniteElement("Discontinuous Lagrange", tetrahedron, 0) v = TestFunction(element) u = TrialFunction(element) f = Coefficient(element) a = f('+')*dot(grad(v)('+'), grad(u)('-'))*dS Restriction may be applied to functions of any finite element space but will only have effect when applied to expressions that are discontinuous across facets. Jump: ``jump(v)`` ----------------- The operator ``jump`` may be used to express the jump of a function across a common facet of two cells. Two versions of the ``jump`` operator are provided. If called with only one argument, then the ``jump`` operator evaluates to the difference between the restrictions of the given expression on the positive and negative sides of the facet: .. math:: \mathtt{jump(v)} \leftrightarrow [[ v ]] = v^+ - v^- If the expression ``v`` is scalar, then ``jump(v)`` will also be scalar, and if ``v`` is vector-valued, then ``jump(v)`` will also be vector-valued. If called with two arguments, ``jump(v, n)`` evaluates to the jump in ``v`` weighted by ``n``. Typically, ``n`` will be chosen to represent the unit outward normal of the facet (as seen from each of the two neighboring cells). If ``v`` is scalar, then ``jump(v, n)`` is given by .. math:: \mathtt{jump(v, n)} \leftrightarrow [[ v ]]_n = v^+ n^+ + v^- n^- If ``v`` is vector-valued, then ``jump(v, n)`` is given by .. math:: \mathtt{jump(v, n)} \leftrightarrow [[ v ]]_n = v^+ \cdot n^+ + v^- \cdot n^- Thus, if the expression ``v`` is scalar, then ``jump(v, n)`` will be vector-valued, and if ``v`` is vector-valued, then ``jump(v, n)`` will be scalar. Average: ``avg(v)`` ------------------- The operator ``avg`` may be used to express the average of an expression across a common facet of two cells: .. math:: \mathtt{avg(v)} \leftrightarrow [[ v ]] = \frac{1}{2} (v^+ + v^-) The expression ``avg(v)`` has the same value shape as the expression ``v``. Conditional Operators ===================== Conditional ----------- UFL has limited support for branching, but for some PDEs it is needed. The expression ``c`` in:: c = conditional(condition, true_value, false_value) evaluates to ``true_value`` at run-time if ``condition`` evaluates to true, or to ``false_value`` otherwise. This corresponds to the C++ syntax ``(condition ? true_value: false_value)``, or the Python syntax ``(true_value if condition else false_value)``. Conditions ---------- * ``eq(a, b)`` must be used in place of the notation ``a == b`` * ``ne(a, b)`` must be used in place of the notation ``a != b`` * ``le(a, b)`` is equivalent to ``a <= b`` * ``ge(a, b)`` is equivalent to ``a >= b`` * ``lt(a, b)`` is equivalent to ``a < b`` * ``gt(a, b)`` is equivalent to ``a > b`` .. note:: Because of details in the way Python behaves, we cannot overload the == operator, hence these named operators. .. _user-defined: User-defined operators ====================== A user may define new operators, using standard Python syntax. As an example, consider the strain-rate operator :math:`\epsilon` of linear elasticity, defined by .. math:: \epsilon(v) = \frac{1}{2} (\nabla v + (\nabla v)^{\top}). This operator can be implemented as a function using the Python ``def`` keyword:: def epsilon(v): return 0.5*(grad(v) + grad(v).T) Alternatively, using the shorthand ``lambda`` notation, the strain operator may be defined as follows:: epsilon = lambda v: 0.5*(grad(v) + grad(v).T) Complex values ============== UFL supports the definition of forms over either the real or the complex field. Indeed, UFL does not explicitly define whether ``Coefficient`` or ``Constant`` are real or complex. This is instead a matter for the form compiler to define. The complex-valued finite element spaces supported by UFL always have a real basis but complex coefficients. This means that ``Constant`` are ``Coefficient`` are complex-valued, but ``Argument`` is real-valued. Complex operators ----------------- * ``conj(f)`` :: complex conjugate of ``f``. * ``imag(f)`` :: imaginary part of ``f``. * ``real(f)`` :: real part of ``f``. Sesquilinearity --------------- ``inner`` and ``outer`` are sesquilinear rather than linear when applied to complex values. Consequently, forms with two arguments are also sesquilinear in this case. UFL adopts the convention that inner products take the complex conjugate of the second operand. This is the usual convention in complex analysis but the reverse of the usual convention in physics. Complex values and conditionals ------------------------------- Since the field of complex numbers does not admit a well order, complex expressions are not permissable as operands to ``lt``, ``gt``, ``le``, or ``ge``. When compiling complex forms, the preprocessing stage of a compiler will attempt to prove that the relevant operands are real and will raise an exception if it is unable to do so. The user may always explicitly use ``real`` (or ``imag``) in order to ensure that the operand is real. Compiling real forms -------------------- When the compiler treats a form as real, the preprocessing stage will discard all instances of ``conj`` and ``real`` in the form. Any instances of ``imag`` or complex literal constants will cause an exception. Form Transformations ==================== When you have defined a ``Form``, you can derive new related forms from it automatically. UFL defines a set of common form transformations described in this section. Replacing arguments of a Form ----------------------------- The function ``replace`` lets you replace terminal objects with other values, using a mapping defined by a Python dictionary. This can be used for example to replace a ``Coefficient`` with a fixed value for optimized run-time evaluation. Example:: f = Coefficient(element) g = Coefficient(element) c = Constant(cell) a = f*g*v*dx b = replace(a, { f: 3.14, g: c }) The replacement values must have the same basic properties as the original values, in particular value shape and free indices. Action of a form on a function ------------------------------ The action of a bilinear form :math:`a` is defined as .. math:: b(v; w) = a(v, w) The action of a linear form :math:`L` is defined as .. math:: f(;w) = L(w) This operation is implemented in UFL simply by replacing the rightmost basis function (trial function for `a`, test function for `L`) in a ``Form``, and is used like this:: L = action(a, w) f = action(L, w) To give a concrete example, these declarations are equivalent:: a = inner(grad(u), grad(v))*dx L = action(a, w) a = inner(grad(u), grad(v))*dx L = inner(grad(w), grad(v))*dx If a is a rank 2 form used to assemble the matrix A, L is a rank 1 form that can be used to assemble the vector :math:`b = Ax` directly. This can be used to define both the form of a matrix and the form of its action without code duplication, and for the action of a Jacobi matrix computed using derivative. If L is a rank 1 form used to assemble the vector b, f is a functional that can be used to assemble the scalar value :math:`f = b \cdot w` directly. This operation is sometimes used in, e.g., error control with L being the residual equation and w being the solution to the dual problem. (However, the discrete vector for the assembled residual equation will typically be available, so doing the dot product using linear algebra would be faster than using this feature.) Energy norm of a bilinear form ------------------------------- The functional representing the energy norm :math:`|v|_A = v^T A v` of a matrix A assembled from a form :math:`a` can be computed with:: f = energy_norm(a, w) which is equivalent to:: f = action(action(a, w), w) Adjoint of a bilinear form --------------------------- The adjoint :math:`a'` of a bilinear form :math:`a` is defined as .. math:: a'(u,v) = a(v,u). This operation is implemented in UFL simply by swapping test and trial functions in a ``Form``, and is used like this:: aprime = adjoint(a) Linear and bilinear parts of a form ----------------------------------- Sometimes it is useful to write an equation on the format .. math:: a(v,u) - L(v) = 0. Before assembly, we need to extract the forms corresponding to the left hand side and right hand side. This corresponds to extracting the bilinear and linear terms of the form respectively, or separating the terms that depend on both a test and a trial function on one side and the terms that depend on only a test function on the other. This is easily done in UFL using ``lhs`` and ``rhs``:: b = u*v*dx - f*v*dx a, L = lhs(b), rhs(b) Note that ``rhs`` multiplies the extracted terms by -1, corresponding to moving them from left to right, so this is equivalent to :: a = u*v*dx L = f*v*dx As a slightly more complicated example, this formulation:: F = v*(u - w)*dx + k*dot(grad(v), grad(0.5*(w + u)))*dx a, L = lhs(F), rhs(F) is equivalent to :: a = v*u*dx + k*dot(grad(v), 0.5*grad(u))*dx L = v*w*dx - k*dot(grad(v), 0.5*grad(w))*dx .. _AD: Automatic functional differentiation ------------------------------------ UFL can compute derivatives of functionals or forms w.r.t. to a ``Coefficient``. This functionality can be used for example to linearize your nonlinear residual equation automatically, or derive a linear system from a functional, or compute sensitivity vectors w.r.t. some coefficient. A functional can be differentiated to obtain a linear form, .. math:: F(v; w) = \frac{d}{dw} f(;w) and a linear form can be differentiated to obtain the bilinear form corresponding to its Jacobi matrix. .. note:: Note that by "linear form" we only mean a form that is linear in its test function, not in the function you differentiate with respect to. .. math:: J(v, u; w) = \frac{d}{dw} F(v; w). The UFL code to express this is (for a simple functional :math:`f(w)=\int_\Omega \frac 1 2 w^2\,dx`) :: f = (w**2)/2 * dx F = derivative(f, w, v) J = derivative(F, w, u) which is equivalent to :: f = (w**2)/2 * dx F = w*v*dx J = u*v*dx Assume in the following examples that :: v = TestFunction(element) u = TrialFunction(element) w = Coefficient(element) The stiffness matrix can be computed from the functional :math:`\int_\Omega \nabla w : \nabla w \, dx`, by :: f = inner(grad(w), grad(w))/2 * dx F = derivative(f, w, v) J = derivative(F, w, u) which is equivalent to :: f = inner(grad(w), grad(w))/2 * dx F = inner(grad(w), grad(v)) * dx J = inner(grad(u), grad(v)) * dx Note that here the basis functions are provided explicitly, which is sometimes necessary, e.g., if part of the form is linearlized manually as in :: g = Coefficient(element) f = inner(grad(w), grad(w))*dx F = derivative(f, w, v) + dot(w-g,v)*dx J = derivative(F, w, u) Derivatives can also be computed w.r.t. functions in mixed spaces. Consider this example, an implementation of the harmonic map equations using automatic differentiation:: X = VectorElement("Lagrange", cell, 1) Y = FiniteElement("Lagrange", cell, 1) x = Coefficient(X) y = Coefficient(Y) L = inner(grad(x), grad(x))*dx + dot(x,x)*y*dx F = derivative(L, (x,y)) J = derivative(F, (x,y)) Here ``L`` is defined as a functional with two coefficient functions ``x`` and ``y`` from separate finite element spaces. However, ``F`` and ``J`` become linear and bilinear forms respectively with basis functions defined on the mixed finite element :: M = X + Y There is a subtle difference between defining ``x`` and ``y`` separately and this alternative implementation (reusing the elements ``X``, ``Y``, ``M``):: u = Coefficient(M) x, y = split(u) L = inner(grad(x), grad(x))*dx + dot(x,x)*y*dx F = derivative(L, u) J = derivative(F, u) The difference is that the forms here have *one* coefficient function ``u`` in the mixed space, and the forms above have *two* coefficient functions ``x`` and ``y``. Combining form transformations ------------------------------ Form transformations can be combined freely. Note that, to do this, derivatives are usually evaluated before applying (e.g.) the action of a form, because ``derivative`` changes the arity of the form:: element = FiniteElement("CG", cell, 1) w = Coefficient(element) f = w**4/4*dx(0) + inner(grad(w), grad(w))*dx(1) F = derivative(f, w) J = derivative(F, w) Ja = action(J, w) Jp = adjoint(J) Jpa = action(Jp, w) g = Coefficient(element) Jnorm = energy_norm(J, g) Form files ========== UFL forms and elements can be collected in a *form file*. Form files are standard Python files which usually import entire UFL namespace. Form compilers will typically execute this file with the global UFL namespace available, and extract forms and elements that are defined after execution. The compilers do not compile all forms and elements that are defined in file, but only those that are "exported". A finite element with the variable name ``element`` is exported by default, as are forms with the names ``M``, ``L``, and ``a``. The default form names are intended for a functional, linear form, and bilinear form respectively. To export multiple forms and elements or use other names, an explicit list with the forms and elements to export can be defined. Simply write :: elements = [V, P, TH] forms = [a, L, F, J, L2, H1] at the end of the file to export the elements and forms held by these variables. ufl-2024.2.0/doc/sphinx/source/manual/internal_representation.rst000066400000000000000000000137261470142567200250730ustar00rootroot00000000000000******************************* Internal representation details ******************************* .. FIXME: This chapter is very much outdated. Most of the concepts are still the same but a lot of the details are different. This chapter explains how UFL forms and expressions are represented in detail. Most operations are mirrored by a representation class, e.g., ``Sum`` and ``Product``, which are subclasses of ``Expr``. You can import all of them from the submodule ``ufl.classes`` by :: from ufl.classes import * Structure of a form =================== Each ``Form`` owns multiple ``Integral`` instances, each associated with a different ``Measure``. An ``Integral`` owns a ``Measure`` and an ``Expr``, which represents the integrand expression. The ``Expr`` is the base class of all expressions. It has two direct subclasses ``Terminal`` and ``Operator``. Subclasses of ``Terminal`` represent atomic quantities which terminate the expression tree, e.g. they have no subexpressions. Subclasses of ``Operator`` represent operations on one or more other expressions, which may usually be ``Expr`` subclasses of arbitrary type. Different ``Operator``\ s may have restrictions on some properties of their arguments. All the types mentioned here are conceptually immutable, i.e. they should never be modified over the course of their entire lifetime. When a modified expression, measure, integral, or form is needed, a new instance must be created, possibly sharing some data with the old one. Since the shared data is also immutable, sharing can cause no problems. General properties of expressions ================================= Any UFL expression has certain properties, defined by functions that every ``Expr`` subclass must implement. In the following, ``u`` represents an arbitrary UFL expression, i.e. an instance of an arbitrary ``Expr`` subclass. ``operands`` ------------ ``u.operands()`` returns a tuple with all the operands of u, which should all be ``Expr`` instances. ``reconstruct`` --------------- ``u.reconstruct(operands)`` returns a new ``Expr`` instance representing the same operation as ``u`` but with other operands. Terminal objects may simply return ``self`` since all ``Expr`` instance are immutable. An important invariant is that ``u.reconstruct(u.operands()) == u``. ``cell`` -------- ``u.cell()`` returns the first ``Cell`` instance found in ``u``. It is currently assumed in UFL that no two different cells are used in a single form. Not all expression define a cell, in which case this returns ``None`` and ``u`` is spatially constant. Note that this property is used in some algorithms. ``shape`` --------- ``u.shape()`` returns a tuple of integers, which is the tensor shape of ``u``. ``free_indices`` ----------------- ``u.free_indices()`` returns a tuple of ``Index`` objects, which are the unassigned, free indices of ``u``. ``index_dimensions`` --------------------- ``u.index_dimensions()`` returns a ``dict`` mapping from each ``Index`` instance in ``u.free_indices()`` to the integer dimension of the value space each index can range over. ``str(u)`` ---------- ``str(u)`` returns a human-readable string representation of ``u``. ``repr(u)`` ----------- ``repr(u)`` returns a Python string representation of ``u``, such that ``eval(repr(u)) == u`` holds in Python. ``hash(u)`` ----------- ``hash(u)`` returns a hash code for ``u``, which is used extensively (indirectly) in algorithms whenever ``u`` is placed in a Python ``dict`` or ``set``. ``u == v`` ---------- ``u == v`` returns true if and only if ``u`` and ``v`` represents the same expression in the exact same way. This is used extensively (indirectly) in algorithms whenever ``u`` is placed in a Python ``dict`` or ``set``. About other relational operators -------------------------------- In general, UFL expressions are not possible to fully evaluate since the cell and the values of form arguments are not available. Implementing relational operators for immediate evaluation is therefore impossible. Overloading relational operators as a part of the form language is not possible either, since it interferes with the correct use of container types in Python like ``dict`` or ``set``. Elements ======== All finite element classes have a common base class ``FiniteElementBase``. The class hierarchy looks like this: .. TODO: Class figure. .. TODO: Describe all FiniteElementBase subclasses here. Terminals ========= All ``Terminal`` subclasses have some non-``Expr`` data attached to them. ``ScalarValue`` has a Python scalar, ``Coefficient`` has a ``FiniteElement``, etc. Therefore, a unified implementation of ``reconstruct`` is not possible, but since all ``Expr`` instances are immutable, ``reconstruct`` for terminals can simply return self. This feature and the immutability property is used extensively in algorithms. Operators ========= All instances of ``Operator`` subclasses are fully specified by their type plus the tuple of ``Expr`` instances that are the operands. Their constructors should take these operands as the positional arguments, and only that. This way, a unified implementation of ``reconstruct`` is possible, by simply calling the constructor with new operands. This feature is used extensively in algorithms. Extending UFL ============= Adding new types to the UFL class hierarchy must be done with care. If you can get away with implementing a new operator as a combination of existing ones, that is the easiest route. The reason is that only some of the properties of an operator is represented by the ``Expr`` subclass. Other properties are part of the various algorithms in UFL. One example is derivatives, which are defined in the differentiation algorithm, and how to render a type to the dot formats. These properties could be merged into the class hierarchy, but other properties like how to map a UFL type to some ``ffc`` or ``dolfin`` type cannot be part of UFL. So before adding a new class, consider that doing so may require changes in multiple algorithms and even other projects. ufl-2024.2.0/doc/sphinx/source/manual/introduction.rst000066400000000000000000000035401470142567200226470ustar00rootroot00000000000000************ Introduction ************ The Unified Form Language (UFL) is a domain specific language for defining discrete variational forms and functionals in a notation close to pen-and-paper formulation. UFL is part of the FEniCS Project and is usually used in combination with other components from this project to compute solutions to partial differential equations. The form compiler FFC uses UFL as its end-user interface, producing implementations of the UFC interface as output. See DOLFIN for more details about using UFL in an integrated problem solving environment. This manual is intended for different audiences. If you are an end-user and all you want to do is to solve your PDEs with the FEniCS framework, you should read :doc:`form_language`, and also :doc:`examples`. These two sections explain how to use all operators available in the language and present a number of examples to illustrate the use of the form language in applications. The remaining chapters contain more technical details intended for developers who need to understand what is happening behind the scenes and modify or extend UFL in the future. :doc:`internal_representation` describes the implementation of the language, in particular how expressions are represented internally by UFL. This can also be useful knowledge to understand error messages and debug errors in your form files. :doc:`algorithms` explains the many algorithms available to work with UFL expressions, mostly intended to aid developers of form compilers. The algorithms include helper functions for easy and efficient iteration over expression trees, formatting tools to present expressions as text or images of different kinds, utilities to analyse properties of expressions or checking their validity, automatic differentiation algorithms, as well as algorithms to work with the computational graphs of expressions. ufl-2024.2.0/doc/sphinx/source/manual/user_manual.rst000066400000000000000000000003171470142567200224400ustar00rootroot00000000000000.. UFL user manual .. _ufl_user_manual: ############### UFL user manual ############### .. toctree:: :maxdepth: 1 introduction form_language examples internal_representation algorithms ufl-2024.2.0/doc/sphinx/source/releases.rst000066400000000000000000000004501470142567200204510ustar00rootroot00000000000000.. title:: Release notes ============= Release notes ============= .. toctree:: :maxdepth: 2 releases/next releases/v2019.1.0 releases/v2018.1.0 releases/v2017.2.0 releases/v2017.1.0.post1 releases/v2017.1.0 releases/v2016.2.0 releases/v2016.1.0 releases/v1.6.0 ufl-2024.2.0/doc/sphinx/source/releases/000077500000000000000000000000001470142567200177205ustar00rootroot00000000000000ufl-2024.2.0/doc/sphinx/source/releases/next.rst000066400000000000000000000010021470142567200214210ustar00rootroot00000000000000=========================== Changes in the next release =========================== Summary of changes ================== .. note:: Developers should use this page to track and list changes during development. At the time of release, this page should be published (and renamed) to list the most important changes in the new release. Detailed changes ================ .. note:: At the time of release, make a verbatim copy of the ChangeLog here (and remove this note). ufl-2024.2.0/doc/sphinx/source/releases/v1.6.0.rst000066400000000000000000000025501470142567200213040ustar00rootroot00000000000000======================== Changes in version 1.6.0 ======================== UFL 1.6.0 was released on 2015-07-28. - Change approach to attaching ``__hash__`` implementation to accomodate Python 3 - Implement new non-recursive traversal based hash computation - Allow ``derivative(M, ListTensor(), ...)`` just like list/tuple works - Add traits ``is_in_reference_frame``, ``is_restriction``, ``is_evaluation``, ``is_differential`` - Add missing linear operators to ``ArgumentDependencyExtractor`` - Add ``_ufl_is_literal_`` type trait - Add ``_ufl_is_terminal_modifier_ type`` trait and ``Expr._ufl_terminal_modifiers_`` list - Add new types ``ReferenceDiv`` and ``ReferenceCurl`` - Outer product element support in degree estimation - Add ``TraceElement``, ``InteriorElement``, ``FacetElement``, ``BrokenElement`` - Add ``OuterProductCell`` to valid ``Real`` elements - Add ``_cache`` member to form for use by external frameworks - Add Sobolev space ``HEin`` - Add measures ``dI``, ``dO``, ``dC`` for interface, overlap, cutcell - Remove ``Measure`` constants - Remove ``cell2D`` and ``cell3D`` - Implement ``reference_value`` in ``apply_restrictions`` - Rename point integral to vertex integral and kept ``*dP`` syntax - Replace lambda functions in ``ufl_type`` with named functions for nicer stack traces - Minor bugfixes, removal of unused code and cleanups ufl-2024.2.0/doc/sphinx/source/releases/v2016.1.0.rst000066400000000000000000000022571470142567200215330ustar00rootroot00000000000000=========================== Changes in version 2016.1.0 =========================== UFL 2016.1.0 was released on 2016-06-23. - Add operator A^(i,j) := as_tensor(A, (i,j)) - Updates to old manual for publishing on fenics-ufl.readthedocs.org - Bugfix for ufl files with utf-8 encoding - Bugfix in conditional derivatives to avoid inf/nan values in generated code. This bugfix may break ffc if uflacs is not used, to get around that the old workaround in ufl can be enabled by setting ufl.algorithms.apply_derivatives.CONDITIONAL_WORKAROUND = True at the top of your program. - Allow sum([expressions]) where expressions are nonscalar by defining expr+0==expr - Allow form=0; form -= other; - Deprecate .cell(), .domain(), .element() in favour of .ufl_cell(), .ufl_domain(), .ufl_element(), in multiple classes, to allow closer integration with dolfin. - Remove deprecated properties cell.{d,x,n,volume,circumradius,facet_area}. - Remove ancient form2ufl script - Add new class Mesh to replace Domain - Add new class FunctionSpace(mesh, element) - Make FiniteElement classes take Cell, not Domain. - Large reworking of symbolic geometry pipeline - Implement symbolic Piola mappings ufl-2024.2.0/doc/sphinx/source/releases/v2016.2.0.rst000066400000000000000000000060071470142567200215310ustar00rootroot00000000000000=========================== Changes in version 2016.2.0 =========================== UFL 2016.2.0 was released on 2016-11-30. Summary of changes ================== - Deprecate ``.cell()``, ``.domain()``, ``.element()`` in favour of ``.ufl_cell()``, ``.ufl_domain()``, ``.ufl_element()``, in multiple classes, to allow closer integration with DOLFIN - Remove deprecated properties ``cell.{d,x,n,volume,circumradius,facet_area}`` - Remove ancient ``form2ufl`` script - Large reworking of symbolic geometry pipeline - Implement symbolic Piola mappings - ``OuterProductCell`` and ``OuterProductElement`` are merged into ``TensorProductCell`` and ``TensorProductElement`` respectively - Better degree estimation for quadrilaterals - Expansion rules for Q, DQ, RTCE, RTCF, NCE and NCF on tensor product cells - Add discontinuous Taylor elements - Add support for the mapping ``double covariant Piola`` in ``uflacs`` - Add support for the mapping ``double contravariant Piola`` in ``uflacs`` - Support for tensor-valued subelements in ``uflacs`` fixed - Replacing ``Discontinuous Lagrange Trace`` with ``HDiv Trace`` and removing ``TraceElement`` - Assigning ``Discontinuous Lagrange Trace`` and ``DGT`` as aliases for ``HDiv Trace`` Detailed changes ================ - Add call operator syntax to Form to replace arguments and coefficients. This makes it easier to e.g. express the norm defined by a bilinear form as a functional. Example usage:: # Equivalent to replace(a, {u: f, v: f}) M = a(f, f) # Equivalent to replace(a, {f:1}) c = a(coefficients={f:1}) - Add call operator syntax to Form to replace arguments and coefficients:: a(f, g) == replace(a, {u: f, v: g}) a(coefficients={f:1}) == replace(a, {f:1}) - Add @ operator to Form: form @ f == action(form, f) (python 3.5+ only) - Reduce noise in Mesh str such that print(form) gets more short and readable - Fix repeated split(function) for arbitrary nested elements - EnrichedElement: Remove +/* warning In the distant past, A + B => MixedElement([A, B]). The change that A + B => EnrichedElement([A, B]) was made in d622c74 (22 March 2010). A warning was introduced in fcbc5ff (26 March 2010) that the meaning of "+" had changed, and that users wanting a MixedElement should use "*" instead. People have, presumably, been seeing this warning for 6 1/2 years by now, so it's probably safe to remove. - Rework TensorProductElement implementation, replaces OuterProductElement - Rework TensorProductCell implementation, replaces OuterProductCell - Remove OuterProductVectorElement and OuterProductTensorElement - Add FacetElement and InteriorElement - Add Hellan-Herrmann-Johnson element - Add support for double covariant and contravariant mappings in mixed elements - Support discontinuous Taylor elements on all simplices - Some more performance improvements - Minor bugfixes - Improve Python 3 support - More permissive in integer types accepted some places - Make ufl pass almost all flake8 tests - Add bitbucket pipelines testing - Improve documentation ufl-2024.2.0/doc/sphinx/source/releases/v2017.1.0.post1.rst000066400000000000000000000003501470142567200225710ustar00rootroot00000000000000================================= Changes in version 2017.1.0.post1 ================================= UFL 2017.1.0.post1 was released on 2017-09-12. Summary of changes ================== - Change PyPI package name to fenics-ufl. ufl-2024.2.0/doc/sphinx/source/releases/v2017.1.0.rst000066400000000000000000000013431470142567200215270ustar00rootroot00000000000000=========================== Changes in version 2017.1.0 =========================== UFL 2017.1.0 was released on 2017-05-09. Summary of changes ================== - Add the ``DirectionalSobolevSpace`` subclass of ``SobolevSpace``. This allows one to use spaces where elements have varying continuity in different spatial directions. - Add ``sobolev_space`` methods for ``HDiv`` and ``HCurl`` finite elements. - Add ``sobolev_space`` methods for ``TensorProductElement`` and ``EnrichedElement``. The smallest shared Sobolev space will be returned for enriched elements. For the tensor product elements, a ``DirectionalSobolevSpace`` is returned depending on the order of the spaces associated with the component elements. ufl-2024.2.0/doc/sphinx/source/releases/v2017.2.0.rst000066400000000000000000000020101470142567200215200ustar00rootroot00000000000000=========================== Changes in version 2017.2.0 =========================== UFL 2017.2.0 was released on 2017-12-05. Summary of changes ================== - Add ``CellDiameter`` expression giving diameter of a cell, i.e., maximal distance between any two points of the cell. Implemented for all simplices and quads/hexes. - Make ``(Min|Max)(Cell|Facet)EdgeLength`` working for quads/hexes Detailed changes ================ - Add geometric quantity ``CellDiameter`` defined as a set diameter of the cell, i.e., maximal distance between any two points of the cell; implemented on simplices and quads/hexes - Rename internally used reference quantities ``(Cell|Facet)EdgeVectors`` to ``Reference(Cell|Facet)EdgeVectors`` - Add internally used quantites ``CellVertices``, ``(Cell|Facet)EdgeVectors`` which are physical-coordinates-valued; will be useful for further geometry lowering implementations for quads/hexes - Implement geometry lowering of ``(Min|Max)(Cell|Facet)EdgeLength`` for quads and hexes ufl-2024.2.0/doc/sphinx/source/releases/v2018.1.0.rst000066400000000000000000000003001470142567200215200ustar00rootroot00000000000000=========================== Changes in version 2018.1.0 =========================== UFL 2018.1.0 was released on 2018-06-14. Summary of changes ================== - Remove python2 support. ufl-2024.2.0/doc/sphinx/source/releases/v2019.1.0.rst000066400000000000000000000007431470142567200215340ustar00rootroot00000000000000=========================== Changes in version 2019.1.0 =========================== Summary of changes ================== - Add support for complex valued elements - Remove LaTeX support (not functional) - Remove scripts Detailed changes ================ - Add support for complex valued elements; complex mode is chosen by ``compute_form_data(form, complex_mode=True)`` typically by a form compiler; otherwise UFL language is agnostic to the choice of real/complex domain ufl-2024.2.0/pyproject.toml000066400000000000000000000037761470142567200154700ustar00rootroot00000000000000[build-system] requires = ["setuptools>=62", "wheel"] build-backend = "setuptools.build_meta" [project] name = "fenics-ufl" version = "2024.2.0" authors = [{ name = "UFL contributors" }] maintainers = [ { email = "fenics-steering-council@googlegroups.com" }, { name = "FEniCS Steering Council" }, ] description = "Unified Form Language" readme = "README.md" license = { file = "COPYING.lesser" } requires-python = ">=3.8.0" dependencies = ["numpy"] [project.urls] homepage = "https://fenicsproject.org" repository = "https://github.com/fenics/ufl.git" documentation = "https://docs.fenicsproject.org" issues = "https://github.com/FEniCS/ufl/issues" funding = "https://numfocus.org/donate" [project.optional-dependencies] lint = ["ruff"] docs = ["sphinx", "sphinx_rtd_theme"] test = ["pytest"] ci = [ "coveralls", "coverage", "pytest-cov", "pytest-xdist", "fenics-ufl[docs]", "fenics-ufl[lint]", "fenics-ufl[test]", ] [tool.setuptools] packages = [ "ufl", "ufl.algorithms", "ufl.core", "ufl.corealg", "ufl.formatting", "ufl.utils", ] [tool.ruff] line-length = 100 indent-width = 4 [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] select = [ # "N", # pep8-naming "E", # pycodestyle "W", # pycodestyle "D", # pydocstyle "F", # pyflakes "I", # isort "RUF", # Ruff-specific rules # "UP", # pyupgrade "ICN", # flake8-import-conventions "NPY", # numpy-specific rules "FLY", # use f-string not static joins "LOG", # https://docs.astral.sh/ruff/rules/#flake8-logging-log # "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc # "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b # "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a ] ignore = ["RUF005", "RUF012"] allowed-confusables = ["𝐚", "𝐀", "∕", "γ", "⨯", "∨"] [tool.ruff.lint.per-file-ignores] "demo/*" = ["D"] "doc/*" = ["D"] "test/*" = ["D"] [tool.ruff.lint.pydocstyle] convention = "google" ufl-2024.2.0/test/000077500000000000000000000000001470142567200135165ustar00rootroot00000000000000ufl-2024.2.0/test/conftest.py000066400000000000000000000034251470142567200157210ustar00rootroot00000000000000import os import pytest import ufl from ufl import as_ufl, dx, inner from ufl.algorithms import compute_form_data class Tester: def assertTrue(self, a): assert a def assertFalse(self, a): assert not a def assertEqual(self, a, b): assert a == b def assertAlmostEqual(self, a, b): assert abs(a - b) < 1e-7 def assertNotEqual(self, a, b): assert a != b def assertIsInstance(self, obj, cls): assert isinstance(obj, cls) def assertNotIsInstance(self, obj, cls): assert not isinstance(obj, cls) def assertRaises(self, e, f): assert pytest.raises(e, f) def assertEqualTotalShape(self, value, expected): self.assertEqual(value.ufl_shape, expected.ufl_shape) self.assertEqual(value.ufl_free_indices, expected.ufl_free_indices) self.assertEqual(value.ufl_index_dimensions, expected.ufl_index_dimensions) def assertSameIndices(self, expr, free_indices): self.assertEqual(expr.ufl_free_indices, tuple(sorted(i.count() for i in free_indices))) def assertEqualAfterPreprocessing(self, a, b): a2 = compute_form_data(a * dx).preprocessed_form b2 = compute_form_data(b * dx).preprocessed_form self.assertEqual(a2, b2) def assertEqualValues(self, A, B): B = as_ufl(B) self.assertEqual(A.ufl_shape, B.ufl_shape) self.assertEqual(inner(A - B, A - B)(None), 0) @pytest.fixture(scope="session") def self(): return Tester() def testspath(): return os.path.abspath(os.path.dirname(__file__)) def filespath(): return os.path.abspath(os.path.join(testspath(), "../demo")) _all_cells = [ufl.interval, ufl.triangle, ufl.tetrahedron] @pytest.fixture(params=_all_cells) def cell(request): return request.param ufl-2024.2.0/test/mockobjects.py000066400000000000000000000022341470142567200163740ustar00rootroot00000000000000from ufl import Measure, Mesh, triangle class MockMesh: def __init__(self, ufl_id): self._ufl_id = ufl_id def ufl_id(self): return self._ufl_id def ufl_domain(self): return Mesh(triangle, ufl_id=self.ufl_id(), cargo=self) def ufl_measure( self, integral_type="dx", subdomain_id="everywhere", metadata=None, subdomain_data=None ): return Measure( integral_type, subdomain_id=subdomain_id, metadata=metadata, domain=self, subdomain_data=subdomain_data, ) class MockMeshFunction: """Mock class for the pydolfin compatibility hack for domain data with [] syntax.""" def __init__(self, ufl_id, mesh): self._mesh = mesh self._ufl_id = ufl_id def ufl_id(self): return self._ufl_id def mesh(self): return self._mesh def ufl_measure(self, integral_type=None, subdomain_id="everywhere", metadata=None): return Measure( integral_type, subdomain_id=subdomain_id, metadata=metadata, domain=self.mesh(), subdomain_data=self, ) ufl-2024.2.0/test/test_algorithms.py000077500000000000000000000112741470142567200173100ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2008-03-12 -- 2009-01-28" # Modified by Anders Logg, 2008 # Modified by Garth N. Wells, 2009 import pytest from ufl import ( Argument, Coefficient, FacetNormal, FunctionSpace, Mesh, TestFunction, TrialFunction, adjoint, div, dot, ds, dx, grad, inner, triangle, ) from ufl.algorithms import ( expand_derivatives, expand_indices, extract_arguments, extract_coefficients, extract_elements, extract_unique_elements, ) from ufl.corealg.traversal import ( post_traversal, pre_traversal, unique_post_traversal, unique_pre_traversal, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # TODO: add more tests, covering all utility algorithms @pytest.fixture(scope="module") def element(): return FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) @pytest.fixture(scope="module") def domain(): return Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) @pytest.fixture(scope="module") def space(element, domain): return FunctionSpace(domain, element) @pytest.fixture(scope="module") def arguments(space): v = TestFunction(space) u = TrialFunction(space) return (v, u) @pytest.fixture(scope="module") def coefficients(space): c = Coefficient(space) f = Coefficient(space) return (c, f) @pytest.fixture def forms(arguments, coefficients, domain): v, u = arguments c, f = coefficients n = FacetNormal(domain) a = u * v * dx L = f * v * dx b = u * v * dx(0) + inner(c * grad(u), grad(v)) * dx(1) + dot(n, grad(u)) * v * ds + f * v * dx return (a, L, b) def test_extract_arguments_vs_fixture(arguments, forms): assert arguments == tuple(extract_arguments(forms[0])) assert tuple(arguments[:1]) == tuple(extract_arguments(forms[1])) def test_extract_coefficients_vs_fixture(coefficients, forms): assert coefficients == tuple(extract_coefficients(forms[2])) def test_extract_elements_and_extract_unique_elements(forms, element, domain): b = forms[2] integrals = b.integrals_by_type("cell") integrals[0].integrand() element1 = element element2 = element space1 = FunctionSpace(domain, element1) space2 = FunctionSpace(domain, element2) v = TestFunction(space1) u = TrialFunction(space2) a = u * v * dx assert extract_elements(a) == (element1, element2) assert extract_unique_elements(a) == (element1,) def test_pre_and_post_traversal(space): v = TestFunction(space) f = Coefficient(space) g = Coefficient(space) p1 = f * v p2 = g * v s = p1 + p2 # NB! These traversal algorithms are intended to guarantee only # parent before child and vice versa, not this particular # ordering: assert list(pre_traversal(s)) == [s, p2, g, v, p1, f, v] assert list(post_traversal(s)) == [g, v, p2, f, v, p1, s] assert list(unique_pre_traversal(s)) == [s, p2, g, v, p1, f] assert list(unique_post_traversal(s)) == [v, f, p1, g, p2, s] def test_expand_indices(domain): element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) def evaluate(form): return form.cell_integral()[0].integrand()( (), {v: 3, u: 5} ) # TODO: How to define values of derivatives? a = div(grad(v)) * u * dx # a1 = evaluate(a) a = expand_derivatives(a) # a2 = evaluate(a) a = expand_indices(a) # a3 = evaluate(a) # TODO: Compare a1, a2, a3 # TODO: Test something more def test_adjoint(domain): cell = triangle V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) s1 = FunctionSpace(domain, V1) s2 = FunctionSpace(domain, V2) u = TrialFunction(s1) v = TestFunction(s2) assert u.number() > v.number() u2 = Argument(s1, 2) v2 = Argument(s2, 3) assert u2.number() < v2.number() a = u * v * dx a_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(a)] assert a_arg_degrees == [2, 1] b = adjoint(a) b_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(b)] assert b_arg_degrees == [1, 2] c = adjoint(a, (u2, v2)) c_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(c)] assert c_arg_degrees == [1, 2] d = adjoint(b) d_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(d)] assert d_arg_degrees == [2, 1] ufl-2024.2.0/test/test_analyse_demos.py000077500000000000000000000012401470142567200177520ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2008-09-28 -- 2008-09-28" import os from glob import glob import pytest from ufl.algorithms import load_ufl_file, validate_form demodir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo")) def get_demo_filenames(): filenames = sorted( set(glob(os.path.join(demodir, "*.py"))) - set(glob(os.path.join(demodir, "_*.py"))) ) return filenames @pytest.mark.parametrize("filename", get_demo_filenames()) def test_demo_files(filename): "Check each form in each file with validate_form." data = load_ufl_file(filename) for form in data.forms: validate_form(form) ufl-2024.2.0/test/test_apply_algebra_lowering.py000077500000000000000000000100741470142567200216440ustar00rootroot00000000000000import pytest from ufl import Coefficient, FunctionSpace, Index, Mesh, as_tensor, interval, sqrt, triangle from ufl.algorithms.renumbering import renumber_indices from ufl.compound_expressions import cross_expr, determinant_expr, inverse_expr from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def A0(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)), FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1), ) ) @pytest.fixture def A1(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)), FiniteElement("Lagrange", interval, 1, (1, 1), identity_pullback, H1), ) ) @pytest.fixture def A2(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1), ) ) @pytest.fixture def A3(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (3, 3), identity_pullback, H1), ) ) @pytest.fixture def A21(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (2, 1), identity_pullback, H1), ) ) @pytest.fixture def A31(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (3, 1), identity_pullback, H1), ) ) @pytest.fixture def A32(request): return Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (3, 2), identity_pullback, H1), ) ) def test_determinant0(A0): assert determinant_expr(A0) == A0 def test_determinant1(A1): assert determinant_expr(A1) == A1[0, 0] def test_determinant2(A2): assert determinant_expr(A2) == A2[0, 0] * A2[1, 1] - A2[0, 1] * A2[1, 0] def test_determinant3(A3): assert determinant_expr(A3) == ( A3[0, 0] * (A3[1, 1] * A3[2, 2] - A3[1, 2] * A3[2, 1]) + (A3[1, 0] * A3[2, 2] - A3[1, 2] * A3[2, 0]) * (-A3[0, 1]) + A3[0, 2] * (A3[1, 0] * A3[2, 1] - A3[1, 1] * A3[2, 0]) ) def test_pseudo_determinant21(A21): i = Index() assert renumber_indices(determinant_expr(A21)) == renumber_indices(sqrt(A21[i, 0] * A21[i, 0])) def test_pseudo_determinant31(A31): i = Index() assert renumber_indices(determinant_expr(A31)) == renumber_indices( sqrt((A31[i, 0] * A31[i, 0])) ) def test_pseudo_determinant32(A32): i = Index() c = cross_expr(A32[:, 0], A32[:, 1]) assert renumber_indices(determinant_expr(A32)) == renumber_indices(sqrt(c[i] * c[i])) def test_inverse0(A0): expected = 1.0 / A0 # stays scalar assert inverse_expr(A0) == renumber_indices(expected) def test_inverse1(A1): expected = as_tensor(((1.0 / A1[0, 0],),)) # reshaped into 1x1 tensor assert inverse_expr(A1) == renumber_indices(expected) def xtest_inverse2(A2): expected = "TODO" assert inverse_expr(A2) == renumber_indices(expected) def xtest_inverse3(A3): expected = "TODO" assert inverse_expr(A3) == renumber_indices(expected) def xtest_pseudo_inverse21(A21): expected = "TODO" assert renumber_indices(inverse_expr(A21)) == renumber_indices(expected) def xtest_pseudo_inverse31(A31): expected = "TODO" assert renumber_indices(inverse_expr(A31)) == renumber_indices(expected) def xtest_pseudo_inverse32(A32): expected = "TODO" assert renumber_indices(inverse_expr(A32)) == renumber_indices(expected) ufl-2024.2.0/test/test_apply_function_pullbacks.py000077500000000000000000000372571470142567200222420ustar00rootroot00000000000000import numpy as np from ufl import Cell, Coefficient, FunctionSpace, Mesh, as_tensor, as_vector, dx, indices, triangle from ufl.algorithms.renumbering import renumber_indices from ufl.classes import Jacobian, JacobianDeterminant, JacobianInverse, ReferenceValue from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement from ufl.pullback import ( contravariant_piola, covariant_piola, double_contravariant_piola, double_covariant_piola, identity_pullback, l2_piola, ) from ufl.sobolevspace import H1, L2, HCurl, HDiv, HDivDiv, HEin def check_single_function_pullback(g, mappings): expected = mappings[g] actual = g.ufl_element().pullback.apply(ReferenceValue(g)) assert expected.ufl_shape == actual.ufl_shape for idx in np.ndindex(actual.ufl_shape): rexp = renumber_indices(expected[idx]) ract = renumber_indices(actual[idx]) if not rexp == ract: print() print("In check_single_function_pullback:") print("input:") print(repr(g)) print("expected:") print(str(rexp)) print("actual:") print(str(ract)) print("signatures:") print((expected**2 * dx).signature()) print((actual**2 * dx).signature()) print() assert ract == rexp def test_apply_single_function_pullbacks_triangle3d(): cell = Cell("triangle") domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) UL2 = FiniteElement("Discontinuous Lagrange", cell, 1, (), l2_piola, L2) U0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2) U = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1) Vd = FiniteElement("Raviart-Thomas", cell, 1, (2,), contravariant_piola, HDiv) Vc = FiniteElement("N1curl", cell, 1, (2,), covariant_piola, HCurl) T = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1) S = SymmetricElement( { (0, 0): 0, (1, 0): 1, (2, 0): 2, (0, 1): 1, (1, 1): 3, (2, 1): 4, (0, 2): 2, (1, 2): 4, (2, 2): 5, }, [FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for _ in range(6)], ) # (0, 2)-symmetric tensors COV2T = FiniteElement("Regge", cell, 0, (2, 2), double_covariant_piola, HEin) # (2, 0)-symmetric tensors CONTRA2T = FiniteElement("HHJ", cell, 0, (2, 2), double_contravariant_piola, HDivDiv) Uml2 = MixedElement([UL2, UL2]) Um = MixedElement([U, U]) Vm = MixedElement([U, V]) Vdm = MixedElement([V, Vd]) Vcm = MixedElement([Vd, Vc]) Tm = MixedElement([Vc, T]) Sm = MixedElement([T, S]) Vd0 = MixedElement([Vd, U0]) # case from failing ffc demo W = MixedElement([S, T, Vc, Vd, V, U]) ul2 = Coefficient(FunctionSpace(domain, UL2)) u = Coefficient(FunctionSpace(domain, U)) v = Coefficient(FunctionSpace(domain, V)) vd = Coefficient(FunctionSpace(domain, Vd)) vc = Coefficient(FunctionSpace(domain, Vc)) t = Coefficient(FunctionSpace(domain, T)) s = Coefficient(FunctionSpace(domain, S)) cov2t = Coefficient(FunctionSpace(domain, COV2T)) contra2t = Coefficient(FunctionSpace(domain, CONTRA2T)) uml2 = Coefficient(FunctionSpace(domain, Uml2)) um = Coefficient(FunctionSpace(domain, Um)) vm = Coefficient(FunctionSpace(domain, Vm)) vdm = Coefficient(FunctionSpace(domain, Vdm)) vcm = Coefficient(FunctionSpace(domain, Vcm)) tm = Coefficient(FunctionSpace(domain, Tm)) sm = Coefficient(FunctionSpace(domain, Sm)) vd0m = Coefficient(FunctionSpace(domain, Vd0)) # case from failing ffc demo w = Coefficient(FunctionSpace(domain, W)) rul2 = ReferenceValue(ul2) ru = ReferenceValue(u) rv = ReferenceValue(v) rvd = ReferenceValue(vd) rvc = ReferenceValue(vc) rt = ReferenceValue(t) rs = ReferenceValue(s) rcov2t = ReferenceValue(cov2t) rcontra2t = ReferenceValue(contra2t) ruml2 = ReferenceValue(uml2) rum = ReferenceValue(um) rvm = ReferenceValue(vm) rvdm = ReferenceValue(vdm) rvcm = ReferenceValue(vcm) rtm = ReferenceValue(tm) rsm = ReferenceValue(sm) rvd0m = ReferenceValue(vd0m) rw = ReferenceValue(w) assert len(w) == 9 + 9 + 3 + 3 + 3 + 1 assert len(rw) == 6 + 9 + 2 + 2 + 3 + 1 assert len(w) == 28 assert len(rw) == 23 assert len(vd0m) == 4 assert len(rvd0m) == 3 # Geometric quantities we need: J = Jacobian(domain) detJ = JacobianDeterminant(domain) Jinv = JacobianInverse(domain) # o = CellOrientation(domain) i, j, k, l = indices(4) # noqa: E741 # Contravariant H(div) Piola mapping: M_hdiv = (1.0 / detJ) * J # Not applying cell orientation here # Covariant H(curl) Piola mapping: Jinv.T mappings = { # Simple elements should get a simple representation ul2: rul2 / detJ, u: ru, v: rv, vd: as_vector(M_hdiv[i, j] * rvd[j], i), vc: as_vector(Jinv[j, i] * rvc[j], i), t: rt, s: as_tensor([[rs[0], rs[1], rs[2]], [rs[1], rs[3], rs[4]], [rs[2], rs[4], rs[5]]]), cov2t: as_tensor(Jinv[k, i] * rcov2t[k, l] * Jinv[l, j], (i, j)), contra2t: as_tensor((1.0 / detJ) ** 2 * J[i, k] * rcontra2t[k, l] * J[j, l], (i, j)), # Mixed elements become a bit more complicated uml2: as_vector([ruml2[0] / detJ, ruml2[1] / detJ]), um: rum, vm: rvm, vdm: as_vector( [ # V rvdm[0], rvdm[1], rvdm[2], # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rvdm[3], rvdm[4]])[j], (i,))[n] for n in range(3) ), ] ), vcm: as_vector( [ # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rvcm[0], rvcm[1]])[j], (i,))[n] for n in range(3) ), # Vc *( as_tensor(Jinv[i, j] * as_vector([rvcm[2], rvcm[3]])[i], (j,))[n] for n in range(3) ), ] ), tm: as_vector( [ # Vc *( as_tensor(Jinv[i, j] * as_vector([rtm[0], rtm[1]])[i], (j,))[n] for n in range(3) ), # T rtm[2], rtm[3], rtm[4], rtm[5], rtm[6], rtm[7], rtm[8], rtm[9], rtm[10], ] ), sm: as_vector( [ # T rsm[0], rsm[1], rsm[2], rsm[3], rsm[4], rsm[5], rsm[6], rsm[7], rsm[8], # S rsm[9], rsm[10], rsm[11], rsm[10], rsm[12], rsm[13], rsm[11], rsm[13], rsm[14], ] ), # Case from failing ffc demo: vd0m: as_vector( [ M_hdiv[0, j] * as_vector([rvd0m[0], rvd0m[1]])[j], M_hdiv[1, j] * as_vector([rvd0m[0], rvd0m[1]])[j], M_hdiv[2, j] * as_vector([rvd0m[0], rvd0m[1]])[j], rvd0m[2], ] ), # This combines it all: w: as_vector( [ # S rw[0], rw[1], rw[2], rw[1], rw[3], rw[4], rw[2], rw[4], rw[5], # T rw[6], rw[7], rw[8], rw[9], rw[10], rw[11], rw[12], rw[13], rw[14], # Vc *( as_tensor(Jinv[i, j] * as_vector([rw[15], rw[16]])[i], (j,))[n] for n in range(3) ), # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rw[17], rw[18]])[j], (i,))[n] for n in range(3) ), # V rw[19], rw[20], rw[21], # U rw[22], ] ), } # Check functions of various elements outside a mixed context check_single_function_pullback(ul2, mappings) check_single_function_pullback(u, mappings) check_single_function_pullback(v, mappings) check_single_function_pullback(vd, mappings) check_single_function_pullback(vc, mappings) check_single_function_pullback(t, mappings) check_single_function_pullback(s, mappings) check_single_function_pullback(cov2t, mappings) check_single_function_pullback(contra2t, mappings) # Check functions of various elements inside a mixed context check_single_function_pullback(uml2, mappings) check_single_function_pullback(um, mappings) check_single_function_pullback(vm, mappings) check_single_function_pullback(vdm, mappings) check_single_function_pullback(vcm, mappings) check_single_function_pullback(tm, mappings) check_single_function_pullback(sm, mappings) # Check the ridiculous mixed element W combining it all check_single_function_pullback(w, mappings) def test_apply_single_function_pullbacks_triangle(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) Ul2 = FiniteElement("Discontinuous Lagrange", cell, 1, (), l2_piola, L2) U = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) Vd = FiniteElement("Raviart-Thomas", cell, 1, (2,), contravariant_piola, HDiv) Vc = FiniteElement("N1curl", cell, 1, (2,), covariant_piola, HCurl) T = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1) S = SymmetricElement( {(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}, [FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for i in range(3)], ) Uml2 = MixedElement([Ul2, Ul2]) Um = MixedElement([U, U]) Vm = MixedElement([U, V]) Vdm = MixedElement([V, Vd]) Vcm = MixedElement([Vd, Vc]) Tm = MixedElement([Vc, T]) Sm = MixedElement([T, S]) W = MixedElement([S, T, Vc, Vd, V, U]) ul2 = Coefficient(FunctionSpace(domain, Ul2)) u = Coefficient(FunctionSpace(domain, U)) v = Coefficient(FunctionSpace(domain, V)) vd = Coefficient(FunctionSpace(domain, Vd)) vc = Coefficient(FunctionSpace(domain, Vc)) t = Coefficient(FunctionSpace(domain, T)) s = Coefficient(FunctionSpace(domain, S)) uml2 = Coefficient(FunctionSpace(domain, Uml2)) um = Coefficient(FunctionSpace(domain, Um)) vm = Coefficient(FunctionSpace(domain, Vm)) vdm = Coefficient(FunctionSpace(domain, Vdm)) vcm = Coefficient(FunctionSpace(domain, Vcm)) tm = Coefficient(FunctionSpace(domain, Tm)) sm = Coefficient(FunctionSpace(domain, Sm)) w = Coefficient(FunctionSpace(domain, W)) rul2 = ReferenceValue(ul2) ru = ReferenceValue(u) rv = ReferenceValue(v) rvd = ReferenceValue(vd) rvc = ReferenceValue(vc) rt = ReferenceValue(t) rs = ReferenceValue(s) ruml2 = ReferenceValue(uml2) rum = ReferenceValue(um) rvm = ReferenceValue(vm) rvdm = ReferenceValue(vdm) rvcm = ReferenceValue(vcm) rtm = ReferenceValue(tm) rsm = ReferenceValue(sm) rw = ReferenceValue(w) assert len(w) == 4 + 4 + 2 + 2 + 2 + 1 assert len(rw) == 3 + 4 + 2 + 2 + 2 + 1 assert len(w) == 15 assert len(rw) == 14 # Geometric quantities we need: J = Jacobian(domain) detJ = JacobianDeterminant(domain) Jinv = JacobianInverse(domain) i, j, k, l = indices(4) # noqa: E741 # Contravariant H(div) Piola mapping: M_hdiv = (1.0 / detJ) * J # Covariant H(curl) Piola mapping: Jinv.T mappings = { # Simple elements should get a simple representation ul2: rul2 / detJ, u: ru, v: rv, vd: as_vector(M_hdiv[i, j] * rvd[j], i), vc: as_vector(Jinv[j, i] * rvc[j], i), t: rt, s: as_tensor([[rs[0], rs[1]], [rs[1], rs[2]]]), # Mixed elements become a bit more complicated uml2: as_vector([ruml2[0] / detJ, ruml2[1] / detJ]), um: rum, vm: rvm, vdm: as_vector( [ # V rvdm[0], rvdm[1], # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rvdm[2], rvdm[3]])[j], (i,))[n] for n in range(2) ), ] ), vcm: as_vector( [ # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rvcm[0], rvcm[1]])[j], (i,))[n] for n in range(2) ), # Vc *( as_tensor(Jinv[i, j] * as_vector([rvcm[2], rvcm[3]])[i], (j,))[n] for n in range(2) ), ] ), tm: as_vector( [ # Vc *( as_tensor(Jinv[i, j] * as_vector([rtm[0], rtm[1]])[i], (j,))[n] for n in range(2) ), # T rtm[2], rtm[3], rtm[4], rtm[5], ] ), sm: as_vector( [ # T rsm[0], rsm[1], rsm[2], rsm[3], # S rsm[4], rsm[5], rsm[5], rsm[6], ] ), # This combines it all: w: as_vector( [ # S rw[0], rw[1], rw[1], rw[2], # T rw[3], rw[4], rw[5], rw[6], # Vc *(as_tensor(Jinv[i, j] * as_vector([rw[7], rw[8]])[i], (j,))[n] for n in range(2)), # Vd *( as_tensor(M_hdiv[i, j] * as_vector([rw[9], rw[10]])[j], (i,))[n] for n in range(2) ), # V rw[11], rw[12], # U rw[13], ] ), } # Check functions of various elements outside a mixed context check_single_function_pullback(ul2, mappings) check_single_function_pullback(u, mappings) check_single_function_pullback(v, mappings) check_single_function_pullback(vd, mappings) check_single_function_pullback(vc, mappings) check_single_function_pullback(t, mappings) check_single_function_pullback(s, mappings) # Check functions of various elements inside a mixed context check_single_function_pullback(uml2, mappings) check_single_function_pullback(um, mappings) check_single_function_pullback(vm, mappings) check_single_function_pullback(vdm, mappings) check_single_function_pullback(vcm, mappings) check_single_function_pullback(tm, mappings) check_single_function_pullback(sm, mappings) # Check the ridiculous mixed element W combining it all check_single_function_pullback(w, mappings) ufl-2024.2.0/test/test_apply_restrictions.py000077500000000000000000000045501470142567200210730ustar00rootroot00000000000000from pytest import raises from ufl import ( Coefficient, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, as_tensor, grad, i, triangle, ) from ufl.algorithms.apply_restrictions import apply_default_restrictions, apply_restrictions from ufl.algorithms.renumbering import renumber_indices from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 def test_apply_restrictions(): cell = triangle V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2) V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) v0_space = FunctionSpace(domain, V0) v1_space = FunctionSpace(domain, V1) v2_space = FunctionSpace(domain, V2) f0 = Coefficient(v0_space) f = Coefficient(v1_space) g = Coefficient(v2_space) n = FacetNormal(domain) x = SpatialCoordinate(domain) assert raises(BaseException, lambda: apply_restrictions(f0)) assert raises(BaseException, lambda: apply_restrictions(grad(f))) assert raises(BaseException, lambda: apply_restrictions(n)) # Continuous function gets default restriction if none # provided otherwise the user choice is respected assert apply_restrictions(f) == f("+") assert apply_restrictions(f("-")) == f("-") assert apply_restrictions(f("+")) == f("+") # Propagation to terminals assert apply_restrictions((f + f0)("+")) == f("+") + f0("+") # Propagation stops at grad assert apply_restrictions(grad(f)("-")) == grad(f)("-") assert apply_restrictions((grad(f) ** 2)("+")) == grad(f)("+") ** 2 assert apply_restrictions((grad(f) + grad(g))("-")) == (grad(f)("-") + grad(g)("-")) # x is the same from both sides but computed from one of them assert apply_default_restrictions(x) == x("+") # n on a linear mesh is opposite pointing from the other side assert apply_restrictions(n("+")) == n("+") assert renumber_indices(apply_restrictions(n("-"))) == renumber_indices( as_tensor(-1 * n("+")[i], i) ) # This would be nicer, but -f is translated to -1*f which is # translated to as_tensor(-1*f[i], i). assert # apply_restrictions(n('-')) == -n('+') ufl-2024.2.0/test/test_arithmetic.py000077500000000000000000000065741470142567200172770ustar00rootroot00000000000000from ufl import ( Identity, Mesh, SpatialCoordinate, as_matrix, as_ufl, as_vector, elem_div, elem_mult, elem_op, sin, tetrahedron, triangle, ) from ufl.classes import ComplexValue, Division, FloatValue, IntValue from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_scalar_casting(self): f = as_ufl(2.0) r = as_ufl(4) c = as_ufl(1 + 2j) self.assertIsInstance(f, FloatValue) self.assertIsInstance(r, IntValue) self.assertIsInstance(c, ComplexValue) assert float(f) == 2.0 assert int(r) == 4 assert complex(c) == 1.0 + 2.0j def test_ufl_float_division(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) d = SpatialCoordinate(domain)[0] / 10.0 # TODO: Use mock instead of x self.assertIsInstance(d, Division) def test_float_ufl_division(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) d = 3.14 / SpatialCoordinate(domain)[0] # TODO: Use mock instead of x self.assertIsInstance(d, Division) def test_float_division(self): d = as_ufl(20.0) / 10.0 self.assertIsInstance(d, FloatValue) assert float(d) == 2.0 def test_int_division(self): # UFL treats all divisions as true division d = as_ufl(40) / 7 self.assertIsInstance(d, FloatValue) assert float(d) == 40.0 / 7.0 # self.assertAlmostEqual(float(d), 40 / 7.0, 15) def test_float_int_division(self): d = as_ufl(20.0) / 5 self.assertIsInstance(d, FloatValue) assert float(d) == 4.0 def test_floor_division_fails(self): f = as_ufl(2.0) r = as_ufl(4) s = as_ufl(5) self.assertRaises(NotImplementedError, lambda: r // 4) self.assertRaises(NotImplementedError, lambda: r // s) self.assertRaises(NotImplementedError, lambda: f // s) def test_elem_mult(self): self.assertEqual(int(elem_mult(2, 3)), 6) v = as_vector((1, 2, 3)) u = as_vector((4, 5, 6)) self.assertEqual(elem_mult(v, u), as_vector((4, 10, 18))) def test_elem_mult_on_matrices(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) A = as_matrix(((1, 2), (3, 4))) B = as_matrix(((4, 5), (6, 7))) self.assertEqual(elem_mult(A, B), as_matrix(((4, 10), (18, 28)))) x, y = SpatialCoordinate(domain) A = as_matrix(((x, y), (3, 4))) B = as_matrix(((4, 5), (y, x))) self.assertEqual(elem_mult(A, B), as_matrix(((4 * x, 5 * y), (3 * y, 4 * x)))) x, y = SpatialCoordinate(domain) A = as_matrix(((x, y), (3, 4))) B = Identity(2) self.assertEqual(elem_mult(A, B), as_matrix(((x, 0), (0, 4)))) def test_elem_div(self): domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) x, y, z = SpatialCoordinate(domain) A = as_matrix(((x, y, z), (3, 4, 5))) B = as_matrix(((7, 8, 9), (z, x, y))) self.assertEqual(elem_div(A, B), as_matrix(((x / 7, y / 8, z / 9), (3 / z, 4 / x, 5 / y)))) def test_elem_op(self): domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) x, y, z = SpatialCoordinate(domain) A = as_matrix(((x, y, z), (3, 4, 5))) self.assertEqual( elem_op(sin, A), as_matrix(((sin(x), sin(y), sin(z)), (sin(3), sin(4), sin(5)))) ) self.assertEqual(elem_op(sin, A).dx(0).ufl_shape, (2, 3)) ufl-2024.2.0/test/test_automatic_differentiation.py000077500000000000000000000525741470142567200223670ustar00rootroot00000000000000"""Automatic differentiation tests. These tests should cover the behaviour of the automatic differentiation algorithm at a technical level, and are thus implementation specific. Other tests check for mathematical correctness of diff and derivative. """ import pytest from ufl import ( And, Argument, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse, MaxCellEdgeLength, MaxFacetEdgeLength, Mesh, MinCellEdgeLength, MinFacetEdgeLength, Not, Or, PermutationSymbol, SpatialCoordinate, acos, as_matrix, as_tensor, as_ufl, as_vector, asin, atan, bessel_I, bessel_J, bessel_K, bessel_Y, cofac, conditional, cos, cross, derivative, det, dev, diff, dot, eq, erf, exp, ge, grad, gt, indices, inner, interval, inv, le, ln, lt, ne, outer, replace, sin, skew, sqrt, sym, tan, tetrahedron, tr, triangle, variable, ) from ufl.algorithms import expand_derivatives from ufl.conditional import Conditional from ufl.corealg.traversal import unique_post_traversal from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 class ExpressionCollection(object): def __init__(self, cell): self.cell = cell d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) x = SpatialCoordinate(domain) n = FacetNormal(domain) c = CellVolume(domain) R = Circumradius(domain) h = CellDiameter(domain) f = FacetArea(domain) # s = CellSurfaceArea(domain) mince = MinCellEdgeLength(domain) maxce = MaxCellEdgeLength(domain) minfe = MinFacetEdgeLength(domain) maxfe = MaxFacetEdgeLength(domain) J = Jacobian(domain) detJ = JacobianDeterminant(domain) invJ = JacobianInverse(domain) # FIXME: Add all new geometry types here! ident = Identity(d) eps = PermutationSymbol(d) U = FiniteElement("Undefined", cell, None, (), identity_pullback, L2) V = FiniteElement("Undefined", cell, None, (d,), identity_pullback, L2) W = FiniteElement("Undefined", cell, None, (d, d), identity_pullback, L2) u_space = FunctionSpace(domain, U) v_space = FunctionSpace(domain, V) w_space = FunctionSpace(domain, W) u = Coefficient(u_space) v = Coefficient(v_space) w = Coefficient(w_space) du = Argument(u_space, 0) dv = Argument(v_space, 1) dw = Argument(w_space, 2) class ObjectCollection(object): pass self.shared_objects = ObjectCollection() for key, value in list(locals().items()): setattr(self.shared_objects, key, value) self.literals = list(map(as_ufl, [0, 1, 3.14, ident, eps])) self.geometry = [x, n, c, R, h, f, mince, maxce, minfe, maxfe, J, detJ, invJ] self.functions = [u, du, v, dv, w, dw] self.terminals = [] self.terminals += self.literals self.terminals += self.geometry self.terminals += self.functions self.algebra = [ u * 2, v * 2, w * 2, u + 2 * u, v + 2 * v, w + 2 * w, 2 / u, u / 2, v / 2, w / 2, u**3, 3**u, ] self.mathfunctions = [ abs(u), sqrt(u), exp(u), ln(u), cos(u), sin(u), tan(u), acos(u), asin(u), atan(u), erf(u), bessel_I(1, u), bessel_J(1, u), bessel_K(1, u), bessel_Y(1, u), ] self.variables = [ variable(u), variable(v), variable(w), variable(w * u), 3 * variable(w * u), ] if d == 1: w2 = as_matrix(((u**2,),)) if d == 2: w2 = as_matrix(((u**2, u**3), (u**4, u**5))) if d == 3: w2 = as_matrix(((u**2, u**3, u**4), (u**4, u**5, u**6), (u**6, u**7, u**8))) # Indexed, ListTensor, ComponentTensor, IndexSum i, j, k, l = indices(4) # noqa: E741 self.indexing = [ v[0], w[d - 1, 0], v[i], w[i, j], v[:], w[0, :], w[:, 0], v[...], w[0, ...], w[..., 0], v[i] * v[j], w[i, 0] * v[j], w[d - 1, j] * v[i], v[i] * v[i], w[i, 0] * w[0, i], v[i] * w[0, i], v[j] * w[d - 1, j], w[i, i], w[i, j] * w[j, i], as_tensor(v[i] * w[k, 0], (k, i)), as_tensor(v[i] * w[k, 0], (k, i))[:, l], as_tensor(w[i, j] * w[k, l], (k, j, l, i)), as_tensor(w[i, j] * w[k, l], (k, j, l, i))[0, 0, 0, 0], as_vector((u, 2, 3)), as_matrix(((u**2, u**3), (u**4, u**5))), as_vector((u, 2, 3))[i], w2[i, j] * w[i, j], ] self.conditionals = [ conditional(le(u, 1.0), 1, 0), conditional(eq(3.0, u), 1, 0), conditional(ne(sin(u), cos(u)), 1, 0), conditional(lt(sin(u), cos(u)), 1, 0), conditional(ge(sin(u), cos(u)), 1, 0), conditional(gt(sin(u), cos(u)), 1, 0), conditional(And(lt(u, 3), gt(u, 1)), 1, 0), conditional(Or(lt(u, 3), gt(u, 1)), 1, 0), conditional(Not(ge(u, 0.0)), 1, 0), conditional(le(u, 0.0), 1, 2), conditional(Not(ge(u, 0.0)), 1, 2), conditional(And(Not(ge(u, 0.0)), lt(u, 1.0)), 1, 2), conditional(le(u, 0.0), u**3, ln(u)), ] self.restrictions = [u("+"), u("-"), v("+"), v("-"), w("+"), w("-")] if d > 1: i, j = indices(2) self.restrictions += [ v("+")[i] * v("+")[i], v[i]("+") * v[i]("+"), (v[i] * v[i])("+"), (v[i] * v[j])("+") * w[i, j]("+"), ] self.noncompounds = [] self.noncompounds += self.algebra self.noncompounds += self.mathfunctions self.noncompounds += self.variables self.noncompounds += self.indexing self.noncompounds += self.conditionals self.noncompounds += self.restrictions if d == 1: self.tensorproducts = [] else: self.tensorproducts = [ dot(v, v), dot(v, w), dot(w, w), inner(v, v), inner(w, w), outer(v, v), outer(w, v), outer(v, w), outer(w, w), ] if d == 1: self.tensoralgebra = [] else: self.tensoralgebra = [ w.T, sym(w), skew(w), dev(w), det(w), tr(w), cofac(w), inv(w), ] if d != 3: self.crossproducts = [] else: self.crossproducts = [ cross(v, v), cross(v, 2 * v), cross(v, w[0, :]), cross(v, w[:, 1]), cross(w[:, 0], v), ] self.compounds = [] self.compounds += self.tensorproducts self.compounds += self.tensoralgebra self.compounds += self.crossproducts self.all_expressions = [] self.all_expressions += self.terminals self.all_expressions += self.noncompounds self.all_expressions += self.compounds @pytest.fixture(params=(1, 2, 3)) def d_expr(request): d = request.param cell = {1: interval, 2: triangle, 3: tetrahedron}[d] expr = ExpressionCollection(cell) return d, expr def ad_algorithm(expr): # alt = 1 # alt = 4 # alt = 6 alt = 0 if alt == 0: return expand_derivatives(expr) elif alt == 1: return expand_derivatives( expr, apply_expand_compounds_before=True, apply_expand_compounds_after=False, use_alternative_wrapper_algorithm=True, ) elif alt == 2: return expand_derivatives( expr, apply_expand_compounds_before=False, apply_expand_compounds_after=True, use_alternative_wrapper_algorithm=False, ) elif alt == 3: return expand_derivatives( expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, use_alternative_wrapper_algorithm=False, ) elif alt == 4: return expand_derivatives( expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, use_alternative_wrapper_algorithm=True, ) elif alt == 5: return expand_derivatives( expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, use_alternative_wrapper_algorithm=False, ) def _test_no_derivatives_no_change(self, collection): for expr in collection: before = expr after = ad_algorithm(before) # print '\n', str(before), '\n', str(after), '\n' self.assertEqualTotalShape(before, after) assert before == after def _test_no_derivatives_but_still_changed(self, collection): # Planning to fix these: for expr in collection: before = expr after = ad_algorithm(before) # print '\n', str(before), '\n', str(after), '\n' self.assertEqualTotalShape(before, after) # assert before == after # Without expand_compounds self.assertNotEqual(before, after) # With expand_compounds def test_only_terminals_no_change(self, d_expr): d, ex = d_expr _test_no_derivatives_no_change(self, ex.terminals) def test_no_derivatives_no_change(self, d_expr): d, ex = d_expr _test_no_derivatives_no_change(self, ex.noncompounds) def xtest_compounds_no_derivatives_no_change( self, d_expr ): # This test fails with expand_compounds enabled d, ex = d_expr _test_no_derivatives_no_change(self, ex.compounds) def test_zero_derivatives_of_terminals_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_zero_derivatives_of_terminals_produce_the_right_types_and_shapes(self, ex) def _test_zero_derivatives_of_terminals_produce_the_right_types_and_shapes(self, collection): c = Constant(collection.shared_objects.domain) u = Coefficient(collection.shared_objects.u_space) v = Coefficient(collection.shared_objects.v_space) w = Coefficient(collection.shared_objects.w_space) for t in collection.terminals: for var in (u, v, w): before = derivative(t, var) # This will often get preliminary simplified to zero after = ad_algorithm(before) expected = 0 * t # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected before = derivative(c * t, var) # This will usually not get simplified to zero after = ad_algorithm(before) expected = 0 * t # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected def test_zero_diffs_of_terminals_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_zero_diffs_of_terminals_produce_the_right_types_and_shapes(self, ex) def _test_zero_diffs_of_terminals_produce_the_right_types_and_shapes(self, collection): c = Constant(collection.shared_objects.domain) u = Coefficient(collection.shared_objects.u_space) v = Coefficient(collection.shared_objects.v_space) w = Coefficient(collection.shared_objects.w_space) vu = variable(u) vv = variable(v) vw = variable(w) for t in collection.terminals: for var in (vu, vv, vw): before = diff(t, var) # This will often get preliminary simplified to zero after = ad_algorithm(before) expected = 0 * outer(t, var) # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected before = diff(c * t, var) # This will usually not get simplified to zero after = ad_algorithm(before) expected = 0 * outer(t, var) # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected def test_zero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_zero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, ex) def _test_zero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 u = Coefficient(collection.shared_objects.u_space) v = Coefficient(collection.shared_objects.v_space) w = Coefficient(collection.shared_objects.w_space) # for t in chain(collection.noncompounds, collection.compounds): # debug = True for t in collection.noncompounds: for var in (u, v, w): if debug: print("\n", "shapes: ", t.ufl_shape, var.ufl_shape, "\n") if debug: print("\n", "t: ", str(t), "\n") if debug: print("\n", "t ind: ", str(t.ufl_free_indices), "\n") if debug: print("\n", "var: ", str(var), "\n") before = derivative(t, var) if debug: print("\n", "before: ", str(before), "\n") after = ad_algorithm(before) if debug: print("\n", "after: ", str(after), "\n") expected = 0 * t if debug: print("\n", "expected: ", str(expected), "\n") assert after == expected def test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, ex) def _test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 u = Coefficient(collection.shared_objects.u_space) v = Coefficient(collection.shared_objects.v_space) w = Coefficient(collection.shared_objects.w_space) vu = variable(u) vv = variable(v) vw = variable(w) # for t in chain(collection.noncompounds, collection.compounds): for t in collection.noncompounds: for var in (vu, vv, vw): before = diff(t, var) if debug: print("\n", "before: ", str(before), "\n") after = ad_algorithm(before) if debug: print("\n", "after: ", str(after), "\n") expected = 0 * outer(t, var) if debug: print("\n", "expected: ", str(expected), "\n") # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected def test_nonzero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_nonzero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, ex) def _test_nonzero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 u = collection.shared_objects.u v = collection.shared_objects.v w = collection.shared_objects.w # for t in chain(collection.noncompounds, collection.compounds): for t in collection.noncompounds: for var in (u, v, w): # Include d/dx [z ? y: x] but not d/dx [x ? f: z] if isinstance(t, Conditional) and (var in unique_post_traversal(t.ufl_operands[0])): if debug: print(("Depends on %s :: %s" % (str(var), str(t)))) continue if debug: print(("\n", "...: ", t.ufl_shape, var.ufl_shape, "\n")) before = derivative(t, var) if debug: print(("\n", "before: ", str(before), "\n")) after = ad_algorithm(before) if debug: print(("\n", "after: ", str(after), "\n")) expected_shape = 0 * t if debug: print(("\n", "expected_shape: ", str(expected_shape), "\n")) # print '\n', str(expected_shape), '\n', str(after), '\n', str(before), '\n' if var in unique_post_traversal(t): self.assertEqualTotalShape(after, expected_shape) self.assertNotEqual(after, expected_shape) else: assert after == expected_shape def test_nonzero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, d_expr): d, ex = d_expr _test_nonzero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, ex) def _test_nonzero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 u = collection.shared_objects.u v = collection.shared_objects.v w = collection.shared_objects.w vu = variable(u) vv = variable(v) vw = variable(w) # for t in chain(collection.noncompounds, collection.compounds): for t in collection.noncompounds: t = replace(t, {u: vu, v: vv, w: vw}) for var in (vu, vv, vw): # Include d/dx [z ? y: x] but not d/dx [x ? f: z] if isinstance(t, Conditional) and (var in unique_post_traversal(t.ufl_operands[0])): if debug: print(("Depends on %s :: %s" % (str(var), str(t)))) continue before = diff(t, var) if debug: print(("\n", "before: ", str(before), "\n")) after = ad_algorithm(before) if debug: print(("\n", "after: ", str(after), "\n")) expected_shape = 0 * outer(t, var) # expected shape, not necessarily value if debug: print(("\n", "expected_shape: ", str(expected_shape), "\n")) # print '\n', str(expected_shape), '\n', str(after), '\n', str(before), '\n' if var in unique_post_traversal(t): self.assertEqualTotalShape(after, expected_shape) self.assertNotEqual(after, expected_shape) else: assert after == expected_shape def test_grad_coeff(self, d_expr): d, collection = d_expr u = collection.shared_objects.u v = collection.shared_objects.v w = collection.shared_objects.w for f in (u, v, w): before = grad(f) after = ad_algorithm(before) if before.ufl_shape != after.ufl_shape: print(("\n", "shapes:", before.ufl_shape, after.ufl_shape)) print(("\n", str(before), "\n", str(after), "\n")) self.assertEqualTotalShape(before, after) if f is u: # Differing by being wrapped in indexing types assert before == after before = grad(grad(f)) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert before == after # Differing by being wrapped in indexing types before = grad(grad(grad(f))) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert before == after # Differing by being wrapped in indexing types def test_derivative_grad_coeff(self, d_expr): d, collection = d_expr u = collection.shared_objects.u v = collection.shared_objects.v w = collection.shared_objects.w for f in (u, v, w): before = derivative(grad(f), f) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected before = derivative(grad(grad(f)), f) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected before = derivative(grad(grad(grad(f))), f) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected if 0: print() print(("B", f, "::", before)) print(("A", f, "::", after)) def xtest_derivative_grad_coeff_with_variation_components(self, d_expr): d, collection = d_expr v = collection.shared_objects.v w = collection.shared_objects.w dv = collection.shared_objects.dv dw = collection.shared_objects.dw for g, dg in ((v, dv), (w, dw)): # Pick a single component ii = (0,) * (len(g.ufl_shape)) f = g[ii] df = dg[ii] before = derivative(grad(g), f, df) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected before = derivative(grad(grad(g)), f, df) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected before = derivative(grad(grad(grad(g))), f, df) after = ad_algorithm(before) self.assertEqualTotalShape(before, after) # assert after == expected if 0: print() print(("B", f, "::", before)) print(("A", f, "::", after)) ufl-2024.2.0/test/test_cell.py000066400000000000000000000043501470142567200160500ustar00rootroot00000000000000import pytest import ufl def test_interval(): cell = ufl.interval assert cell.num_vertices() == 2 assert cell.num_edges() == 1 assert cell.num_faces() == 0 def test_triangle(): cell = ufl.triangle assert cell.num_vertices() == 3 assert cell.num_edges() == 3 assert cell.num_faces() == 1 def test_quadrilateral(): cell = ufl.quadrilateral assert cell.num_vertices() == 4 assert cell.num_edges() == 4 assert cell.num_faces() == 1 def test_tetrahedron(): cell = ufl.tetrahedron assert cell.num_vertices() == 4 assert cell.num_edges() == 6 assert cell.num_faces() == 4 def test_hexahedron(): cell = ufl.hexahedron assert cell.num_vertices() == 8 assert cell.num_edges() == 12 assert cell.num_faces() == 6 def test_pentatope(): cell = ufl.pentatope assert cell.num_vertices() == 5 assert cell.num_edges() == 10 assert cell.num_faces() == 10 def test_tesseract(): cell = ufl.tesseract assert cell.num_vertices() == 16 assert cell.num_edges() == 32 assert cell.num_faces() == 24 @pytest.mark.parametrize("cell", [ufl.interval]) def test_cells_1d(cell): assert cell.num_facets() == cell.num_vertices() assert cell.num_ridges() == 0 assert cell.num_peaks() == 0 @pytest.mark.parametrize("cell", [ufl.triangle, ufl.quadrilateral]) def test_cells_2d(cell): assert cell.num_facets() == cell.num_edges() assert cell.num_ridges() == cell.num_vertices() assert cell.num_peaks() == 0 @pytest.mark.parametrize("cell", [ufl.tetrahedron, ufl.hexahedron, ufl.prism, ufl.pyramid]) def test_cells_3d(cell): assert cell.num_facets() == cell.num_faces() assert cell.num_ridges() == cell.num_edges() assert cell.num_peaks() == cell.num_vertices() @pytest.mark.parametrize("cell", [ufl.tesseract, ufl.pentatope]) def test_cells_4d(cell): assert cell.num_facets() == cell.num_sub_entities(3) assert cell.num_ridges() == cell.num_faces() assert cell.num_peaks() == cell.num_edges() def test_tensorproductcell(): orig = ufl.TensorProductCell(ufl.interval, ufl.interval) cell = orig.reconstruct() assert cell.sub_cells() == orig.sub_cells() assert cell.topological_dimension() == orig.topological_dimension() ufl-2024.2.0/test/test_change_to_local.py000077500000000000000000000053571470142567200202450ustar00rootroot00000000000000"""Tests of the change to local representaiton algorithms.""" from ufl import Coefficient, FunctionSpace, Mesh, as_tensor, grad, indices, triangle from ufl.algorithms import change_to_reference_grad from ufl.algorithms.renumbering import renumber_indices from ufl.classes import JacobianInverse, ReferenceGrad from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_change_to_reference_grad(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) U = FunctionSpace(domain, FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)) V = FunctionSpace(domain, FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) u = Coefficient(U) v = Coefficient(V) Jinv = JacobianInverse(domain) i, j, k = indices(3) q, r, s = indices(3) (t,) = indices(1) # Single grad change on a scalar function expr = grad(u) actual = change_to_reference_grad(expr) expected = as_tensor(Jinv[k, i] * ReferenceGrad(u)[k], (i,)) assert renumber_indices(actual) == renumber_indices(expected) # Single grad change on a vector valued function expr = grad(v) actual = change_to_reference_grad(expr) expected = as_tensor(Jinv[k, j] * ReferenceGrad(v)[i, k], (i, j)) assert renumber_indices(actual) == renumber_indices(expected) # Multiple grads should work fine for affine domains: expr = grad(grad(u)) actual = change_to_reference_grad(expr) expected = as_tensor(Jinv[s, j] * (Jinv[r, i] * ReferenceGrad(ReferenceGrad(u))[r, s]), (i, j)) assert renumber_indices(actual) == renumber_indices(expected) expr = grad(grad(grad(u))) actual = change_to_reference_grad(expr) expected = as_tensor( Jinv[s, k] * (Jinv[r, j] * (Jinv[q, i] * ReferenceGrad(ReferenceGrad(ReferenceGrad(u)))[q, r, s])), (i, j, k), ) assert renumber_indices(actual) == renumber_indices(expected) # Multiple grads on a vector valued function expr = grad(grad(v)) actual = change_to_reference_grad(expr) expected = as_tensor( Jinv[s, j] * (Jinv[r, i] * ReferenceGrad(ReferenceGrad(v))[t, r, s]), (t, i, j) ) assert renumber_indices(actual) == renumber_indices(expected) expr = grad(grad(grad(v))) actual = change_to_reference_grad(expr) expected = as_tensor( Jinv[s, k] * (Jinv[r, j] * (Jinv[q, i] * ReferenceGrad(ReferenceGrad(ReferenceGrad(v)))[t, q, r, s])), (t, i, j, k), ) assert renumber_indices(actual) == renumber_indices(expected) # print tree_format(expected) # print tree_format(actual) # print tree_format(renumber_indices(actual)) # print tree_format(renumber_indices(expected)) ufl-2024.2.0/test/test_change_to_reference_frame.py000077500000000000000000000143021470142567200222510ustar00rootroot00000000000000"""Tests of the change to reference frame algorithm.""" from ufl import Coefficient, FunctionSpace, Mesh, triangle from ufl.classes import Expr, ReferenceValue from ufl.finiteelement import FiniteElement from ufl.pullback import contravariant_piola, identity_pullback from ufl.sobolevspace import H1, HDiv def change_to_reference_frame(expr): assert isinstance(expr, Expr) return ReferenceValue(expr) def test_change_unmapped_form_arguments_to_reference_frame(): U = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) T = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) u_space = FunctionSpace(domain, U) v_space = FunctionSpace(domain, V) t_space = FunctionSpace(domain, T) expr = Coefficient(u_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) expr = Coefficient(v_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) expr = Coefficient(t_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) def test_change_hdiv_form_arguments_to_reference_frame(): V = FiniteElement("Raviart-Thomas", triangle, 1, (2,), contravariant_piola, HDiv) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) expr = Coefficient(v_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) def test_change_hcurl_form_arguments_to_reference_frame(): V = FiniteElement("Raviart-Thomas", triangle, 1, (2,), contravariant_piola, HDiv) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) expr = Coefficient(v_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) """ # user input grad(f + g)('+') # change to reference frame -> (K*rgrad(rv(M)*rv(f) + rv(M)*rv(g)))('+') # apply derivatives -> (K*(rv(M)*rgrad(rv(f)) + rv(M)*rgrad(rv(g))))('+') # apply restrictions -> K('+')*(rv(M('+'))*rgrad(rv(f('+'))) + rv(M('+'))*rgrad(rv(g('+')))) # user input grad(f + g)('+') # some derivatives applied before processing (grad(f) + grad(g))('+') # ... replace to get fully defined form arguments here ... # expand compounds # * context options: # - keep {types} without rewriting to lower level types # - preserve div and curl if applied directly to terminal # (ffc context may set this to off) # * output invariants: # - no compound operator types left in expression (simplified language) # - div and curl rewritten in terms of grad (optionally unless applied directly to terminal) -> (grad(f) + grad(g))('+') # change to reference frame # * context options: # - keep {types} without rewriting to lower level types (e.g. JacobianInverse) # (ffc context may initially add all code snippets expressions) # - keep {types} in global frame (e.g. Coefficient) # (ffc context may initially add Coefficient and Argument here to refrain from changing) # - skip integral scaling # (ffc context may turn skipping on to preserve current behaviour) # * output invariants: # - ReferenceValue bound directly to terminals where applicable # - grad replaced by mapping expression of rgrad # - div replaced by mapping expression of rdiv # - curl replaced by mapping expression of rcurl -> as_tensor(IndexSum(K[i,j]*rgrad(as_tensor(rv(M)[k,l]*rv(f)[l], (l,)) + as_tensor(rv(M)[r,s]*rv(g)[s], (s,)))[j], j), (i,))('+') # apply derivatives # * context options: # - N/A? # * output invariants: # - grad,div,curl, bound directly to terminals # - rgrad,rdiv,rcurl bound directly to referencevalue objects (rgrad(global_f) invalid) -> (K*(rv(M)*rgrad(rv(f)) + rv(M)*rgrad(rv(g))))('+') # apply restrictions # * context options: # - N/A? # * output invariants: # - *_restricted bound directly to terminals # - all terminals that must be restricted to make sense are restricted -> K('+')*(rv(M('+'))*rgrad(rv(f('+'))) + rv(M('+'))*rgrad(rv(g('+')))) # final modified terminal structure: t = terminal | restricted(terminal) # choice of terminal r = rval(t) | rgrad(r) # in reference frame: value or n-gradient g = t | grad(g) # in global frame: value or n-gradient v = r | g # value in either frame e = v | cell_avg(v) | facet_avg(v) | at_cell_midpoint(v) | at_facet_midpoint(v) # evaluated at point or averaged over cell entity m = e | indexed(e) # scalar component of """ """ New form preprocessing pipeline: Preferably introduce these changes: 1) Create new FormArgument Expression without element or domain 2) Create new FormArgument Constant without domain 3) Drop replace --> but just applying replace first is fine i) group and join integrals by (domain, type, subdomain_id), ii) process integrands: a) apply_coefficient_completion # replace coefficients to ensure proper elements and domains b) lower_compound_operators # expand_compounds c) change_to_reference_frame # change f->rv(f), m->M*rv(m), grad(f)->K*rgrad(rv(f)), grad(grad(f))->K*rgrad(K*rgrad(rv(f))), grad(expr)->K*rgrad(expr) # if grad(expr)->K*rgrad(expr) should be valid, then rgrad must be applicable to quite generic expressions d) apply_derivatives # one possibility is to add an apply_mapped_derivatives AD algorithm which includes mappings e) apply_geometry_lowering f) apply_restrictions # requiring grad(f)('+') instead of grad(f('+')) would simplify a lot... iii) extract final metadata about elements and coefficient ordering """ ufl-2024.2.0/test/test_check_arities.py000077500000000000000000000043311470142567200177300ustar00rootroot00000000000000import pytest from ufl import ( Coefficient, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, TestFunction, TrialFunction, adjoint, cofac, conj, derivative, ds, dx, grad, inner, tetrahedron, ) from ufl.algorithms.check_arities import ArityMismatch from ufl.algorithms.compute_form_data import compute_form_data from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_check_arities(): # Code from bitbucket issue #49 cell = tetrahedron D = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (3,), identity_pullback, H1)) dv = TestFunction(V) du = TrialFunction(V) X = SpatialCoordinate(D) N = FacetNormal(D) u = Coefficient(V) x = X + u F = grad(x) n = cofac(F) * N M = inner(x, n) * ds L = derivative(M, u, dv) a = derivative(L, u, du) compute_form_data(M) compute_form_data(L) compute_form_data(a) def test_complex_arities(): cell = tetrahedron D = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (3,), identity_pullback, H1)) v = TestFunction(V) u = TrialFunction(V) # Valid form. F = inner(u, v) * dx compute_form_data(F, complex_mode=True) # Check that adjoint conjugates correctly compute_form_data(adjoint(F), complex_mode=True) with pytest.raises(ArityMismatch): compute_form_data(inner(v, u) * dx, complex_mode=True) with pytest.raises(ArityMismatch): compute_form_data(inner(conj(v), u) * dx, complex_mode=True) def test_product_arity(): cell = tetrahedron D = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (3,), identity_pullback, H1)) v = TestFunction(V) u = TrialFunction(V) with pytest.raises(ArityMismatch): F = inner(u, u) * dx compute_form_data(F, complex_mode=True) with pytest.raises(ArityMismatch): L = inner(v, v) * dx compute_form_data(L, complex_mode=False) ufl-2024.2.0/test/test_classcoverage.py000077500000000000000000000425131470142567200177600ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2008-09-06 -- 2009-02-10" import ufl from ufl import * # noqa: F403 from ufl import ( And, Argument, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse, MaxFacetEdgeLength, Mesh, MinFacetEdgeLength, Not, Or, PermutationSymbol, SpatialCoordinate, TensorConstant, VectorConstant, acos, action, as_matrix, as_tensor, as_ufl, as_vector, asin, atan, cell_avg, cofac, conditional, cos, cosh, cross, curl, derivative, det, dev, diff, div, dot, dS, ds, dx, eq, exp, facet_avg, ge, grad, gt, i, inner, inv, j, k, l, le, ln, lt, nabla_div, nabla_grad, ne, outer, rot, sin, sinh, skew, sqrt, sym, tan, tanh, tetrahedron, tr, transpose, triangle, variable, ) from ufl.algorithms import * # noqa: F403 from ufl.classes import * # noqa: F403 from ufl.classes import ( Acos, Asin, Atan, CellCoordinate, Cos, Cosh, Exp, Expr, FacetJacobian, FacetJacobianDeterminant, FacetJacobianInverse, FloatValue, IntValue, Ln, Outer, Sin, Sinh, Sqrt, Tan, Tanh, all_ufl_classes, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 has_repr = set() has_dict = set() def _test_object(a, shape, free_indices): # Check if instances of this type has certain memory consuming members if hasattr(a, "_repr"): has_repr.add(a.__class__.__name__) if hasattr(a, "__dict__"): has_dict.add(a.__class__.__name__) # Test reproduction via repr string r = repr(a) e = eval(r, globals()) assert hash(a) == hash(e) # Can't really test str more than that it exists str(a) # Check that some properties are at least available fi = a.ufl_free_indices sh = a.ufl_shape # Compare with provided properties if free_indices is not None: free_indices = [i.count() for i in free_indices] if len(set(fi) ^ set(free_indices)) != 0: print(type(a)) print(a) print(fi) print(free_indices) assert len(set(fi) ^ set(free_indices)) == 0 if shape is not None: if sh != shape: print(("sh:", sh)) print(("shape:", shape)) assert sh == shape def _test_object2(a): # Check if instances of this type has certain memory consuming members if hasattr(a, "_repr"): has_repr.add(a.__class__.__name__) if hasattr(a, "__dict__"): has_dict.add(a.__class__.__name__) # Test reproduction via repr string r = repr(a) e = eval(r, globals()) assert hash(a) == hash(e) # Can't really test str more than that it exists str(a) def _test_form(a): # Test reproduction via repr string r = repr(a) e = eval(r, globals()) assert hash(a) == hash(e) # Can't really test str more than that it exists str(a) def testExports(self): "Verify that ufl.classes exports all Expr subclasses." all_expr_classes = [] for m in list(vars(ufl).values()): if isinstance(m, type(ufl)): for c in list(vars(m).values()): if isinstance(c, type) and issubclass(c, Expr): all_expr_classes.append(c) missing_classes = set(c.__name__ for c in all_expr_classes) - set( c.__name__ for c in all_ufl_classes ) if missing_classes: print("The following subclasses of Expr were not exported from ufl.classes:") print(("\n".join(sorted(missing_classes)))) assert missing_classes == set() def testAll(self): Expr.ufl_enable_profiling() # --- Elements: cell = triangle dim = 2 e0 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) e1 = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) e2 = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1) e3 = MixedElement([e0, e1, e2]) e13D = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (dim,), identity_pullback, H1)) domain3D = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) e0_space = FunctionSpace(domain, e0) e1_space = FunctionSpace(domain, e1) e2_space = FunctionSpace(domain, e2) e3_space = FunctionSpace(domain, e3) e13d_space = FunctionSpace(domain3D, e13D) # --- Terminals: v13D = Argument(e13d_space, 3) f13D = Coefficient(e13d_space) v0 = Argument(e0_space, 4) v1 = Argument(e1_space, 5) v2 = Argument(e2_space, 6) v3 = Argument(e3_space, 7) _test_object(v0, (), ()) _test_object(v1, (dim,), ()) _test_object(v2, (dim, dim), ()) _test_object(v3, (1 + dim + dim**2,), ()) f0 = Coefficient(e0_space) f1 = Coefficient(e1_space) f2 = Coefficient(e2_space) f3 = Coefficient(e3_space) _test_object(f0, (), ()) _test_object(f1, (dim,), ()) _test_object(f2, (dim, dim), ()) _test_object(f3, (1 + dim + dim**2,), ()) c = Constant(domain) _test_object(c, (), ()) c = VectorConstant(domain) _test_object(c, (dim,), ()) c = TensorConstant(domain) _test_object(c, (dim, dim), ()) a = FloatValue(1.23) _test_object(a, (), ()) a = IntValue(123) _test_object(a, (), ()) ident = Identity(1) _test_object(ident, (1, 1), ()) ident = Identity(2) _test_object(ident, (2, 2), ()) ident = Identity(3) _test_object(ident, (3, 3), ()) e = PermutationSymbol(2) _test_object(e, (2, 2), ()) e = PermutationSymbol(3) _test_object(e, (3, 3, 3), ()) x = SpatialCoordinate(domain) _test_object(x, (dim,), ()) xi = CellCoordinate(domain) _test_object(xi, (dim,), ()) # g = CellBarycenter(domain) # _test_object(g, (dim,), ()) # g = FacetBarycenter(domain) # _test_object(g, (dim,), ()) g = Jacobian(domain) _test_object(g, (dim, dim), ()) g = JacobianDeterminant(domain) _test_object(g, (), ()) g = JacobianInverse(domain) _test_object(g, (dim, dim), ()) g = FacetJacobian(domain) _test_object(g, (dim, dim - 1), ()) g = FacetJacobianDeterminant(domain) _test_object(g, (), ()) g = FacetJacobianInverse(domain) _test_object(g, (dim - 1, dim), ()) g = FacetNormal(domain) _test_object(g, (dim,), ()) # g = CellNormal(domain) # _test_object(g, (dim,), ()) g = CellVolume(domain) _test_object(g, (), ()) g = CellDiameter(domain) _test_object(g, (), ()) g = Circumradius(domain) _test_object(g, (), ()) # g = CellSurfaceArea(domain) # _test_object(g, (), ()) g = FacetArea(domain) _test_object(g, (), ()) g = MinFacetEdgeLength(domain) _test_object(g, (), ()) g = MaxFacetEdgeLength(domain) _test_object(g, (), ()) # g = FacetDiameter(domain) # _test_object(g, (), ()) a = variable(v0) _test_object(a, (), ()) a = variable(v1) _test_object(a, (dim,), ()) a = variable(v2) _test_object(a, (dim, dim), ()) a = variable(v3) _test_object(a, (1 + dim + dim**2,), ()) a = variable(f0) _test_object(a, (), ()) a = variable(f1) _test_object(a, (dim,), ()) a = variable(f2) _test_object(a, (dim, dim), ()) a = variable(f3) _test_object(a, (1 + dim + dim**2,), ()) # a = MultiIndex() # --- Non-terminals: # a = Indexed() a = v2[i, j] _test_object(a, (), (i, j)) a = v2[0, k] _test_object(a, (), (k,)) a = v2[l, 1] _test_object(a, (), (l,)) a = f2[i, j] _test_object(a, (), (i, j)) a = f2[0, k] _test_object(a, (), (k,)) a = f2[l, 1] _test_object(a, (), (l,)) ident = Identity(dim) a = inv(ident) _test_object(a, (dim, dim), ()) a = inv(v2) _test_object(a, (dim, dim), ()) a = inv(f2) _test_object(a, (dim, dim), ()) for v in (v0, v1, v2, v3): for f in (f0, f1, f2, f3): a = outer(v, f) _test_object(a, None, None) for v, f in zip((v0, v1, v2, v3), (f0, f1, f2, f3)): a = inner(v, f) _test_object(a, None, None) for v, f in zip((v1, v2, v3), (f1, f2, f3)): a = dot(v, f) sh = v.ufl_shape[:-1] + f.ufl_shape[1:] _test_object(a, sh, None) a = cross(v13D, f13D) _test_object(a, (3,), ()) # a = Sum() a = v0 + f0 + v0 _test_object(a, (), ()) a = v1 + f1 + v1 _test_object(a, (dim,), ()) a = v2 + f2 + v2 _test_object(a, (dim, dim), ()) # a = Product() a = 3 * v0 * (2.0 * v0) * f0 * (v0 * 3.0) _test_object(a, (), ()) # a = Division() a = v0 / 2.0 _test_object(a, (), ()) a = v0 / f0 _test_object(a, (), ()) a = v0 / (f0 + 7) _test_object(a, (), ()) # a = Power() a = f0**3 _test_object(a, (), ()) a = (f0 * 2) ** 1.23 _test_object(a, (), ()) # a = ListTensor() a = as_vector([1.0, 2.0 * f0, f0**2]) _test_object(a, (3,), ()) a = as_matrix([[1.0, 2.0 * f0, f0**2], [1.0, 2.0 * f0, f0**2]]) _test_object(a, (2, 3), ()) a = as_tensor( [[[0.00, 0.01, 0.02], [0.10, 0.11, 0.12]], [[1.00, 1.01, 1.02], [1.10, 1.11, 1.12]]] ) _test_object(a, (2, 2, 3), ()) # a = ComponentTensor() a = as_vector(v1[i] * f1[j], i) _test_object(a, (dim,), (j,)) a = as_matrix(v1[i] * f1[j], (j, i)) _test_object(a, (dim, dim), ()) a = as_tensor(v1[i] * f1[j], (i, j)) _test_object(a, (dim, dim), ()) a = as_tensor(v2[i, j] * f2[j, k], (i, k)) _test_object(a, (dim, dim), ()) a = dev(v2) _test_object(a, (dim, dim), ()) a = dev(f2) _test_object(a, (dim, dim), ()) a = dev(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) a = sym(v2) _test_object(a, (dim, dim), ()) a = sym(f2) _test_object(a, (dim, dim), ()) a = sym(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) a = skew(v2) _test_object(a, (dim, dim), ()) a = skew(f2) _test_object(a, (dim, dim), ()) a = skew(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) a = v2.T _test_object(a, (dim, dim), ()) a = f2.T _test_object(a, (dim, dim), ()) a = transpose(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) a = det(v2) _test_object(a, (), ()) a = det(f2) _test_object(a, (), ()) a = det(f2 * f0 + v2 * 3) _test_object(a, (), ()) a = tr(v2) _test_object(a, (), ()) a = tr(f2) _test_object(a, (), ()) a = tr(f2 * f0 + v2 * 3) _test_object(a, (), ()) a = cofac(v2) _test_object(a, (dim, dim), ()) a = cofac(f2) _test_object(a, (dim, dim), ()) a = cofac(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) cond1 = le(f0, 1.0) cond2 = eq(3.0, f0) cond3 = ne(sin(f0), cos(f0)) cond4 = lt(sin(f0), cos(f0)) cond5 = ge(sin(f0), cos(f0)) cond6 = gt(sin(f0), cos(f0)) cond7 = And(cond1, cond2) cond8 = Or(cond1, cond2) cond9 = Not(cond8) a = conditional(cond1, 1, 2) b = conditional(cond2, f0**3, ln(f0)) _test_object2(cond1) _test_object2(cond2) _test_object2(cond3) _test_object2(cond4) _test_object2(cond5) _test_object2(cond6) _test_object2(cond7) _test_object2(cond8) _test_object2(cond9) _test_object(a, (), ()) _test_object(b, (), ()) a = abs(f0) _test_object(a, (), ()) a = sqrt(f0) _test_object(a, (), ()) a = cos(f0) _test_object(a, (), ()) a = sin(f0) _test_object(a, (), ()) a = tan(f0) _test_object(a, (), ()) a = cosh(f0) _test_object(a, (), ()) a = sinh(f0) _test_object(a, (), ()) a = tanh(f0) _test_object(a, (), ()) a = exp(f0) _test_object(a, (), ()) a = ln(f0) _test_object(a, (), ()) a = asin(f0) _test_object(a, (), ()) a = acos(f0) _test_object(a, (), ()) a = atan(f0) _test_object(a, (), ()) one = as_ufl(1) a = abs(one) _test_object(a, (), ()) a = Sqrt(one) _test_object(a, (), ()) a = Cos(one) _test_object(a, (), ()) a = Sin(one) _test_object(a, (), ()) a = Tan(one) _test_object(a, (), ()) a = Cosh(one) _test_object(a, (), ()) a = Sinh(one) _test_object(a, (), ()) a = Tanh(one) _test_object(a, (), ()) a = Acos(one) _test_object(a, (), ()) a = Asin(one) _test_object(a, (), ()) a = Atan(one) _test_object(a, (), ()) a = Exp(one) _test_object(a, (), ()) a = Ln(one) _test_object(a, (), ()) # TODO: # a = SpatialDerivative() a = f0.dx(0) _test_object(a, (), ()) a = f0.dx(i) _test_object(a, (), (i,)) a = f0.dx(i, j, 1) _test_object(a, (), (i, j)) s0 = variable(f0) s1 = variable(f1) s2 = variable(f2) f = dot(s0 * s1, s2) _test_object(s0, (), ()) _test_object(s1, (dim,), ()) _test_object(s2, (dim, dim), ()) _test_object(f, (dim,), ()) a = diff(f, s0) _test_object(a, (dim,), ()) a = diff(f, s1) _test_object( a, ( dim, dim, ), (), ) a = diff(f, s2) _test_object(a, (dim, dim, dim), ()) a = div(v1) _test_object(a, (), ()) a = div(f1) _test_object(a, (), ()) a = div(v2) _test_object(a, (dim,), ()) a = div(f2) _test_object(a, (dim,), ()) a = div(Outer(f1, f1)) _test_object(a, (dim,), ()) a = grad(v0) _test_object(a, (dim,), ()) a = grad(f0) _test_object(a, (dim,), ()) a = grad(v1) _test_object(a, (dim, dim), ()) a = grad(f1) _test_object(a, (dim, dim), ()) a = grad(f0 * v0) _test_object(a, (dim,), ()) a = grad(f0 * v1) _test_object(a, (dim, dim), ()) a = nabla_div(v1) _test_object(a, (), ()) a = nabla_div(f1) _test_object(a, (), ()) a = nabla_div(v2) _test_object(a, (dim,), ()) a = nabla_div(f2) _test_object(a, (dim,), ()) a = nabla_div(Outer(f1, f1)) _test_object(a, (dim,), ()) a = nabla_grad(v0) _test_object(a, (dim,), ()) a = nabla_grad(f0) _test_object(a, (dim,), ()) a = nabla_grad(v1) _test_object(a, (dim, dim), ()) a = nabla_grad(f1) _test_object(a, (dim, dim), ()) a = nabla_grad(f0 * v0) _test_object(a, (dim,), ()) a = nabla_grad(f0 * v1) _test_object(a, (dim, dim), ()) a = curl(v13D) _test_object(a, (3,), ()) a = curl(f13D) _test_object(a, (3,), ()) a = rot(v1) _test_object(a, (), ()) a = rot(f1) _test_object(a, (), ()) # a = PositiveRestricted(v0) # _test_object(a, (), ()) a = v0("+") _test_object(a, (), ()) a = v0("+") * f0 _test_object(a, (), ()) # a = NegativeRestricted(v0) # _test_object(a, (), ()) a = v0("-") _test_object(a, (), ()) a = v0("-") + f0 _test_object(a, (), ()) a = cell_avg(v0) _test_object(a, (), ()) a = facet_avg(v0) _test_object(a, (), ()) a = cell_avg(v1) _test_object(a, (dim,), ()) a = facet_avg(v1) _test_object(a, (dim,), ()) a = cell_avg(v1)[i] _test_object(a, (), (i,)) a = facet_avg(v1)[i] _test_object(a, (), (i,)) # --- Integrals: a = v0 * dx _test_form(a) a = v0 * dx(0) _test_form(a) a = v0 * dx(1) _test_form(a) a = v0 * ds _test_form(a) a = v0 * ds(0) _test_form(a) a = v0 * ds(1) _test_form(a) a = v0 * dS _test_form(a) a = v0 * dS(0) _test_form(a) a = v0 * dS(1) _test_form(a) a = v0 * dot(v1, f1) * dx _test_form(a) a = v0 * dot(v1, f1) * dx(0) _test_form(a) a = v0 * dot(v1, f1) * dx(1) _test_form(a) a = v0 * dot(v1, f1) * ds _test_form(a) a = v0 * dot(v1, f1) * ds(0) _test_form(a) a = v0 * dot(v1, f1) * ds(1) _test_form(a) a = v0 * dot(v1, f1) * dS _test_form(a) a = v0 * dot(v1, f1) * dS(0) _test_form(a) a = v0 * dot(v1, f1) * dS(1) _test_form(a) # --- Form transformations: a = f0 * v0 * dx + f0 * v0 * dot(f1, v1) * dx # b = lhs(a) # TODO # c = rhs(a) # TODO d = derivative(a, f1, v1) f = action(d) # e = action(b) # --- Check which classes have been created ic, dc = Expr.ufl_disable_profiling() constructed = set() unused = set(Expr._ufl_all_classes_) for cls in Expr._ufl_all_classes_: tc = cls._ufl_typecode_ if ic[tc]: constructed.add(cls) if cls._ufl_is_abstract_: unused.remove(cls) if unused: print() print("The following classes were never instantiated in class coverage test:") print(("\n".join(sorted(map(str, unused))))) print() # --- Check which classes had certain member variables if has_repr: print() print("The following classes contain a _repr member:") print(("\n".join(sorted(map(str, has_repr))))) print() if has_dict: print() print("The following classes contain a __dict__ member:") print(("\n".join(sorted(map(str, has_dict))))) print() # TODO: Add tests for bessel functions: # BesselI # BesselJ # BesselK # BesselY # Erf # TODO: Add tests for: # Label ufl-2024.2.0/test/test_complex.py000077500000000000000000000146131470142567200166060ustar00rootroot00000000000000import cmath import pytest from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, as_tensor, as_ufl, atan, conditional, conj, cos, cosh, dot, dx, exp, ge, grad, gt, imag, inner, le, ln, lt, max_value, min_value, outer, real, sin, sqrt, triangle, ) from ufl.algebra import Conj, Imag, Real from ufl.algorithms import estimate_total_polynomial_degree from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering from ufl.algorithms.comparison_checker import ComplexComparisonError, do_comparison_check from ufl.algorithms.formtransformations import compute_form_adjoint from ufl.algorithms.remove_complex_nodes import remove_complex_nodes from ufl.constantvalue import ComplexValue, Zero from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_conj(self): z1 = ComplexValue(1 + 2j) z2 = ComplexValue(1 - 2j) assert z1 == Conj(z2) assert z2 == Conj(z1) def test_real(self): z0 = Zero() z1 = as_ufl(1.0) z2 = ComplexValue(1j) z3 = ComplexValue(1 + 1j) assert Real(z1) == z1 assert Real(z3) == z1 assert Real(z2) == z0 def test_imag(self): z0 = Zero() z1 = as_ufl(1.0) z2 = as_ufl(1j) z3 = ComplexValue(1 + 1j) assert Imag(z2) == z1 assert Imag(z3) == z1 assert Imag(z1) == z0 def test_compute_form_adjoint(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) a = inner(grad(u), grad(v)) * dx assert compute_form_adjoint(a) == conj(inner(grad(v), grad(u))) * dx def test_complex_algebra(self): z1 = ComplexValue(1j) z2 = ComplexValue(1 + 1j) # Remember that ufl.algebra functions return ComplexValues, but # ufl.mathfunctions return complex Python scalar # Any operations with a ComplexValue and a complex Python scalar # promote to ComplexValue assert z1 * z2 == ComplexValue(-1 + 1j) assert z2 / z1 == ComplexValue(1 - 1j) assert pow(z2, z1) == ComplexValue((1 + 1j) ** 1j) assert sqrt(z2) * as_ufl(1) == ComplexValue(cmath.sqrt(1 + 1j)) assert (sin(z2) + cosh(z2) - atan(z2)) * z1 == ComplexValue( (cmath.sin(1 + 1j) + cmath.cosh(1 + 1j) - cmath.atan(1 + 1j)) * 1j ) assert (abs(z2) - ln(z2)) / exp(z1) == ComplexValue( (abs(1 + 1j) - cmath.log(1 + 1j)) / cmath.exp(1j) ) def test_automatic_simplification(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) assert inner(u, v) == u * conj(v) assert dot(u, v) == u * v assert outer(u, v) == conj(u) * v def test_apply_algebra_lowering_complex(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) gv = grad(v) gu = grad(u) a = dot(gu, gv) b = inner(gv, gu) c = outer(gu, gv) lowered_a = apply_algebra_lowering(a) lowered_b = apply_algebra_lowering(b) lowered_c = apply_algebra_lowering(c) lowered_a_index = lowered_a.index() lowered_b_index = lowered_b.index() lowered_c_indices = lowered_c.indices() assert lowered_a == gu[lowered_a_index] * gv[lowered_a_index] assert lowered_b == gv[lowered_b_index] * conj(gu[lowered_b_index]) assert lowered_c == as_tensor( conj(gu[lowered_c_indices[0]]) * gv[lowered_c_indices[1]], (lowered_c_indices[0],) + (lowered_c_indices[1],), ) def test_remove_complex_nodes(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) f = Coefficient(space) a = conj(v) b = real(u) c = imag(f) d = conj(real(v)) * imag(conj(u)) assert remove_complex_nodes(a) == v assert remove_complex_nodes(b) == u with pytest.raises(BaseException): remove_complex_nodes(c) with pytest.raises(BaseException): remove_complex_nodes(d) def test_comparison_checker(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = TrialFunction(space) v = TestFunction(space) a = conditional(ge(abs(u), imag(v)), u, v) b = conditional(le(sqrt(abs(u)), imag(v)), as_ufl(1), as_ufl(1j)) c = conditional(gt(abs(u), pow(imag(v), 0.5)), sin(u), cos(v)) d = conditional(lt(as_ufl(-1), as_ufl(1)), u, v) e = max_value(as_ufl(0), real(u)) f = min_value(sin(u), cos(v)) g = min_value(sin(pow(u, 3)), cos(abs(v))) assert do_comparison_check(a) == conditional(ge(real(abs(u)), real(imag(v))), u, v) with pytest.raises(ComplexComparisonError): b = do_comparison_check(b) with pytest.raises(ComplexComparisonError): c = do_comparison_check(c) assert do_comparison_check(d) == conditional(lt(real(as_ufl(-1)), real(as_ufl(1))), u, v) assert do_comparison_check(e) == max_value(real(as_ufl(0)), real(real(u))) assert do_comparison_check(f) == min_value(real(sin(u)), real(cos(v))) assert do_comparison_check(g) == min_value(real(sin(pow(u, 3))), real(cos(abs(v)))) def test_complex_degree_handling(self): cell = triangle element = FiniteElement("Lagrange", cell, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) a = conj(v) b = imag(v) c = real(v) assert estimate_total_polynomial_degree(a) == 3 assert estimate_total_polynomial_degree(b) == 3 assert estimate_total_polynomial_degree(c) == 3 ufl-2024.2.0/test/test_conditionals.py000077500000000000000000000107101470142567200176170ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2008-08-20 -- 2012-11-30" import pytest from ufl import Coefficient, FunctionSpace, Mesh, conditional, eq, ge, gt, le, lt, ne, triangle from ufl.classes import EQ, GE, GT, LE, LT, NE from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def f(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) return Coefficient(space) @pytest.fixture def g(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) return Coefficient(space) def test_conditional_does_not_allow_bool_condition(f, g): # The reason for this test is that it protects from the case # conditional(a == b, t, f) in which a == b means comparing representations with pytest.raises(BaseException): conditional(True, 1, 0) def test_eq_oper_produces_bool(f, g): expr1 = f == f expr2 = f == g assert isinstance(expr1, bool) assert isinstance(expr2, bool) assert expr1 assert not expr2 def test_eq_produces_ufl_expr(f, g): expr1 = eq(f, g) expr2 = eq(f, f) expr3 = f == g expr4 = f == f # Correct types: assert isinstance(expr1, EQ) assert isinstance(expr2, EQ) assert isinstance(expr3, bool) assert isinstance(expr4, bool) # Comparing representations correctly: assert bool(expr1 == eq(f, g)) assert bool(expr1 != eq(g, g)) assert bool(expr2 == eq(f, f)) assert bool(expr2 != eq(g, f)) # Bool evaluation yields actual bools: assert isinstance(bool(expr1), bool) assert isinstance(bool(expr2), bool) assert not expr3 assert expr4 # Allow use in boolean python expression context: # NB! This means comparing representations! Required by dict and set. assert not bool(expr1) assert bool(expr2) assert not bool(expr3) assert bool(expr4) def test_ne_produces_ufl_expr(f, g): expr1 = ne(f, g) expr2 = ne(f, f) expr3 = f != g expr4 = f != f # Correct types: assert isinstance(expr1, NE) assert isinstance(expr2, NE) assert isinstance(expr3, bool) assert isinstance(expr4, bool) # Comparing representations correctly: assert bool(expr1 == ne(f, g)) assert bool(expr1 != ne(g, g)) assert bool(expr2 == ne(f, f)) assert bool(expr2 != ne(g, f)) assert not bool(expr2 == expr3) # Bool evaluation yields actual bools: assert isinstance(bool(expr1), bool) assert isinstance(bool(expr2), bool) # Allow use in boolean python expression context: # NB! This means the opposite of ==, i.e. comparing representations! assert bool(expr1) assert not bool(expr2) assert bool(expr1) assert not bool(expr2) def test_lt_produces_ufl_expr(f, g): expr1 = lt(f, g) expr2 = f < g # Correct types (no bools here!): assert isinstance(expr1, LT) assert isinstance(expr2, LT) # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: with pytest.raises(BaseException): bool(expr1) def test_gt_produces_ufl_expr(f, g): expr1 = gt(f, g) expr2 = f > g # Correct types (no bools here!): assert isinstance(expr1, GT) assert isinstance(expr2, GT) # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: with pytest.raises(BaseException): bool(expr1) def test_le_produces_ufl_expr(f, g): expr1 = le(f, g) expr2 = f <= g # Correct types (no bools here!): assert isinstance(expr1, LE) assert isinstance(expr2, LE) # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: with pytest.raises(BaseException): bool(expr1) def test_ge_produces_ufl_expr(f, g): expr1 = ge(f, g) expr2 = f >= g # Correct types (no bools here!): assert isinstance(expr1, GE) assert isinstance(expr2, GE) # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: with pytest.raises(BaseException): bool(expr1) ufl-2024.2.0/test/test_degree_estimation.py000077500000000000000000000116711470142567200206270ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2008-03-12 -- 2009-01-28" from ufl import ( Argument, Coefficient, Coefficients, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, cos, div, dot, grad, i, inner, nabla_div, nabla_grad, sin, tan, triangle, ) from ufl.algorithms import estimate_total_polynomial_degree from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_total_degree_estimation(): V1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V2 = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) VV = FiniteElement("Lagrange", triangle, 3, (2,), identity_pullback, H1) VM = MixedElement([V1, V2]) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v1_space = FunctionSpace(domain, V1) v2_space = FunctionSpace(domain, V2) vv_space = FunctionSpace(domain, VV) vm_space = FunctionSpace(domain, VM) v1 = Argument(v1_space, 2) v2 = Argument(v2_space, 3) f1, f2 = Coefficients(vm_space) vv = Argument(vv_space, 4) vu = Argument(vv_space, 5) x, y = SpatialCoordinate(domain) assert estimate_total_polynomial_degree(x) == 1 assert estimate_total_polynomial_degree(x * y) == 2 assert estimate_total_polynomial_degree(x**3) == 3 assert estimate_total_polynomial_degree(x**3) == 3 assert estimate_total_polynomial_degree((x - 1) ** 4) == 4 assert estimate_total_polynomial_degree(vv[0]) == 3 assert estimate_total_polynomial_degree(v2 * vv[0]) == 5 assert estimate_total_polynomial_degree(vu[0] * vv[0]) == 6 assert estimate_total_polynomial_degree(vu[i] * vv[i]) == 6 assert estimate_total_polynomial_degree(v1) == 1 assert estimate_total_polynomial_degree(v2) == 2 # TODO: This should be 1, but 2 is expected behaviour now # because f1 is part of a mixed element with max degree 2. assert estimate_total_polynomial_degree(f1) == 2 assert estimate_total_polynomial_degree(f2) == 2 assert estimate_total_polynomial_degree(v2 * v1) == 3 # TODO: This should be 2, but 3 is expected behaviour now # because f1 is part of a mixed element with max degree 2. assert estimate_total_polynomial_degree(f1 * v1) == 3 assert estimate_total_polynomial_degree(f2 * v1) == 3 assert estimate_total_polynomial_degree(f2 * v2 * v1) == 5 assert estimate_total_polynomial_degree(f2 + 3) == 2 assert estimate_total_polynomial_degree(f2 * 3) == 2 assert estimate_total_polynomial_degree(f2**3) == 6 assert estimate_total_polynomial_degree(f2 / 3) == 2 assert estimate_total_polynomial_degree(f2 / v2) == 4 assert estimate_total_polynomial_degree(f2 / (x - 1)) == 3 assert estimate_total_polynomial_degree(v1.dx(0)) == 0 assert estimate_total_polynomial_degree(f2.dx(0)) == 1 assert estimate_total_polynomial_degree(f2 * v2.dx(0) * v1.dx(0)) == 2 + 1 assert estimate_total_polynomial_degree(f2) == 2 assert estimate_total_polynomial_degree(f2**2) == 4 assert estimate_total_polynomial_degree(f2**3) == 6 assert estimate_total_polynomial_degree(f2**3 * v1) == 7 assert estimate_total_polynomial_degree(f2**3 * v1 + f1 * v1) == 7 # Math functions of constant values are constant values nx, ny = FacetNormal(domain) e = nx**2 for f in [sin, cos, tan, abs, lambda z: z**7]: assert estimate_total_polynomial_degree(f(e)) == 0 # Based on the arbitrary chosen math function heuristics... heuristic_add = 2 e = x**3 for f in [sin, cos, tan]: assert estimate_total_polynomial_degree(f(e)) == 3 + heuristic_add def test_some_compound_types(): # NB! Although some compound types are supported here, # some derivatives and compounds must be preprocessed # prior to degree estimation. In generic code, this algorithm # should only be applied after preprocessing. etpd = estimate_total_polynomial_degree P2 = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) V2 = FiniteElement("Lagrange", triangle, 2, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) u = Coefficient(FunctionSpace(domain, P2)) v = Coefficient(FunctionSpace(domain, V2)) assert etpd(u.dx(0)) == 2 - 1 assert etpd(grad(u)) == 2 - 1 assert etpd(nabla_grad(u)) == 2 - 1 assert etpd(div(u)) == 2 - 1 assert etpd(v.dx(0)) == 2 - 1 assert etpd(grad(v)) == 2 - 1 assert etpd(nabla_grad(v)) == 2 - 1 assert etpd(div(v)) == 2 - 1 assert etpd(nabla_div(v)) == 2 - 1 assert etpd(dot(v, v)) == 2 + 2 assert etpd(inner(v, v)) == 2 + 2 assert etpd(dot(grad(u), grad(u))) == 2 - 1 + 2 - 1 assert etpd(inner(grad(u), grad(u))) == 2 - 1 + 2 - 1 assert etpd(dot(grad(v), grad(v))) == 2 - 1 + 2 - 1 assert etpd(inner(grad(v), grad(v))) == 2 - 1 + 2 - 1 ufl-2024.2.0/test/test_derivative.py000077500000000000000000000556241470142567200173100ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-17 -- 2009-02-17" from itertools import chain from ufl import ( CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FunctionSpace, Identity, Index, Jacobian, JacobianInverse, Mesh, SpatialCoordinate, TestFunction, TrialFunction, acos, as_matrix, as_tensor, as_vector, asin, atan, conditional, cos, derivative, diff, dot, dx, exp, i, indices, inner, interval, j, k, ln, lt, nabla_grad, outer, quadrilateral, replace, sign, sin, split, sqrt, tan, tetrahedron, triangle, variable, zero, ) from ufl.algorithms import compute_form_data, expand_indices, strip_variables from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering from ufl.algorithms.apply_derivatives import apply_derivatives from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering from ufl.classes import Indexed, MultiIndex, ReferenceGrad from ufl.constantvalue import as_ufl from ufl.domain import extract_unique_domain from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 def assertEqualBySampling(actual, expected): ad = compute_form_data(actual * dx) a = ad.preprocessed_form.integrals_by_type("cell")[0].integrand() bd = compute_form_data(expected * dx) b = bd.preprocessed_form.integrals_by_type("cell")[0].integrand() assert [ad.function_replace_map[ac] for ac in ad.reduced_coefficients] == [ bd.function_replace_map[bc] for bc in bd.reduced_coefficients ] n = ad.num_coefficients def make_value(c): if isinstance(c, Coefficient): z = 0.3 m = c.count() else: z = 0.7 m = c.number() if c.ufl_shape == (): return z * (0.1 + 0.9 * m / n) elif len(c.ufl_shape) == 1: return tuple((z * (j + 0.1 + 0.9 * m / n) for j in range(c.ufl_shape[0]))) else: raise NotImplementedError("Tensor valued expressions not supported here.") amapping = dict( (c, make_value(c)) for c in chain(ad.original_form.coefficients(), ad.original_form.arguments()) ) bmapping = dict( (c, make_value(c)) for c in chain(bd.original_form.coefficients(), bd.original_form.arguments()) ) adomain = extract_unique_domain(actual) bdomain = extract_unique_domain(expected) acell = adomain.ufl_cell() bcell = bdomain.ufl_cell() assert acell == bcell if adomain.geometric_dimension() == 1: x = (0.3,) elif adomain.geometric_dimension() == 2: x = (0.3, 0.4) elif adomain.geometric_dimension() == 3: x = (0.3, 0.4, 0.5) av = a(x, amapping) bv = b(x, bmapping) if not av == bv: print("Tried to sample expressions to compare but failed:") print() print((str(a))) print(av) print() print((str(b))) print(bv) print() assert av == bv def _test(self, f, df): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) xv = (0.3, 0.7) uv = 7.0 vv = 13.0 wv = 11.0 x = xv mapping = {v: vv, u: uv, w: wv} dfv1 = derivative(f(w), w, v) dfv2 = df(w, v) dfv1 = dfv1(x, mapping) dfv2 = dfv2(x, mapping) assert dfv1 == dfv2 dfv1 = derivative(f(7 * w), w, v) dfv2 = 7 * df(7 * w, v) dfv1 = dfv1(x, mapping) dfv2 = dfv2(x, mapping) assert dfv1 == dfv2 # --- Literals def testScalarLiteral(self): def f(w): return as_ufl(1) def df(w, v): return zero() _test(self, f, df) def testIdentityLiteral(self): def f(w): return Identity(2)[i, i] def df(w, v): return zero() _test(self, f, df) # --- Form arguments def testCoefficient(self): def f(w): return w def df(w, v): return v _test(self, f, df) def testArgument(self): def f(w): return TestFunction( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1), ) ) def df(w, v): return zero() _test(self, f, df) # --- Geometry def testSpatialCoordinate(self): def f(w): return SpatialCoordinate( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) )[0] def df(w, v): return zero() _test(self, f, df) def testFacetNormal(self): def f(w): return FacetNormal( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) )[0] def df(w, v): return zero() _test(self, f, df) def testFacetArea(self): def f(w): return FacetArea(Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1))) def df(w, v): return zero() _test(self, f, df) def testCellDiameter(self): def f(w): return CellDiameter( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) ) def df(w, v): return zero() _test(self, f, df) def testCircumradius(self): def f(w): return Circumradius( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) ) def df(w, v): return zero() _test(self, f, df) def testCellVolume(self): def f(w): return CellVolume(Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1))) def df(w, v): return zero() _test(self, f, df) # --- Basic operators def testSum(self): def f(w): return w + 1 def df(w, v): return v _test(self, f, df) def testProduct(self): def f(w): return 3 * w def df(w, v): return 3 * v _test(self, f, df) def testPower(self): def f(w): return w**3 def df(w, v): return 3 * w**2 * v _test(self, f, df) def testDivision(self): def f(w): return w / 3.0 def df(w, v): return v / 3.0 _test(self, f, df) def testDivision2(self): def f(w): return 3.0 / w def df(w, v): return -3.0 * v / w**2 _test(self, f, df) def testExp(self): def f(w): return exp(w) def df(w, v): return v * exp(w) _test(self, f, df) def testLn(self): def f(w): return ln(w) def df(w, v): return v / w _test(self, f, df) def testCos(self): def f(w): return cos(w) def df(w, v): return -v * sin(w) _test(self, f, df) def testSin(self): def f(w): return sin(w) def df(w, v): return v * cos(w) _test(self, f, df) def testTan(self): def f(w): return tan(w) def df(w, v): return v * 2.0 / (cos(2.0 * w) + 1.0) _test(self, f, df) def testAcos(self): def f(w): return acos(w / 1000) def df(w, v): return -(v / 1000) / sqrt(1.0 - (w / 1000) ** 2) _test(self, f, df) def testAsin(self): def f(w): return asin(w / 1000) def df(w, v): return (v / 1000) / sqrt(1.0 - (w / 1000) ** 2) _test(self, f, df) def testAtan(self): def f(w): return atan(w) def df(w, v): return v / (1.0 + w**2) _test(self, f, df) # FIXME: Add the new erf and bessel_* # --- Abs and conditionals def testAbs(self): def f(w): return abs(w) def df(w, v): return sign(w) * v _test(self, f, df) def testConditional(self): # This will fail without bugfix in derivative def cond(w): return lt(w, 1.0) def f(w): return conditional(cond(w), 2 * w, 3 * w) def df(w, v): return conditional(cond(w), 1, 0) * 2 * v + conditional(cond(w), 0, 1) * 3 * v _test(self, f, df) # --- Tensor algebra basics def testIndexSum(self): def f(w): # 3*w + 4*w**2 + 5*w**3 a = as_vector((w, w**2, w**3)) b = as_vector((3, 4, 5)) (i,) = indices(1) return a[i] * b[i] def df(w, v): return 3 * v + 4 * 2 * w * v + 5 * 3 * w**2 * v _test(self, f, df) def testListTensor(self): v = variable(as_ufl(42)) f = as_tensor( ( ((0, 0), (0, 0)), ((v, 2 * v), (0, 0)), ((v**2, 1), (2, v / 2)), ) ) assert f.ufl_shape == (3, 2, 2) g = as_tensor( ( ((0, 0), (0, 0)), ((1, 2), (0, 0)), ((84, 0), (0, 0.5)), ) ) assert g.ufl_shape == (3, 2, 2) dfv = diff(f, v) x = None for a in range(3): for b in range(2): for c in range(2): self.assertEqual(dfv[a, b, c](x), g[a, b, c](x)) # --- Coefficient and argument input configurations def test_single_scalar_coefficient_derivative(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) u = Coefficient(space) v = TestFunction(space) a = 3 * u**2 b = derivative(a, u, v) self.assertEqualAfterPreprocessing(b, 3 * (u * (2 * v))) def test_single_vector_coefficient_derivative(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) u = Coefficient(space) v = TestFunction(space) a = 3 * dot(u, u) actual = derivative(a, u, v) expected = 3 * (2 * (u[i] * v[i])) assertEqualBySampling(actual, expected) def test_multiple_coefficient_derivative(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) W = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) M = MixedElement([V, W]) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) w_space = FunctionSpace(domain, W) m_space = FunctionSpace(domain, M) uv = Coefficient(v_space) uw = Coefficient(w_space) v = TestFunction(m_space) vv, vw = split(v) a = sin(uv) * dot(uw, uw) actual = derivative(a, (uv, uw), split(v)) expected = cos(uv) * vv * (uw[i] * uw[i]) + (uw[j] * vw[j]) * 2 * sin(uv) assertEqualBySampling(actual, expected) actual = derivative(a, (uv, uw), v) expected = cos(uv) * vv * (uw[i] * uw[i]) + (uw[j] * vw[j]) * 2 * sin(uv) assertEqualBySampling(actual, expected) def test_indexed_coefficient_derivative(self): cell = triangle ident = Identity(2) V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) W = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) w_space = FunctionSpace(domain, W) u = Coefficient(w_space) v = TestFunction(v_space) w = dot(u, nabla_grad(u)) # a = dot(w, w) a = (u[i] * u[k].dx(i)) * w[k] actual = derivative(a, u[0], v) dw = v * u[k].dx(0) + u[i] * ident[0, k] * v.dx(i) expected = 2 * w[k] * dw assertEqualBySampling(actual, expected) def test_multiple_indexed_coefficient_derivative(self): cell = tetrahedron V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V2 = MixedElement([V, V]) W = FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) v2_space = FunctionSpace(domain, V2) w_space = FunctionSpace(domain, W) u = Coefficient(w_space) w = Coefficient(w_space) v = TestFunction(v2_space) vu, vw = split(v) actual = derivative(cos(u[i] * w[i]), (u[2], w[1]), (vu, vw)) expected = -sin(u[i] * w[i]) * (vu * w[2] + u[1] * vw) assertEqualBySampling(actual, expected) def test_segregated_derivative_of_convection(self): cell = tetrahedron V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) W = FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) w_space = FunctionSpace(domain, W) u = Coefficient(w_space) v = Coefficient(w_space) du = TrialFunction(v_space) dv = TestFunction(v_space) L = dot(dot(u, nabla_grad(u)), v) Lv = {} Lvu = {} for a in range(3): Lv[a] = derivative(L, v[a], dv) for b in range(3): Lvu[a, b] = derivative(Lv[a], u[b], du) for a in range(3): for b in range(3): form = Lvu[a, b] * dx fd = compute_form_data(form) pf = fd.preprocessed_form expand_indices(pf) k = Index() for a in range(3): for b in range(3): actual = Lvu[a, b] expected = du * u[a].dx(b) * dv + u[k] * du.dx(k) * dv assertEqualBySampling(actual, expected) # --- User provided derivatives of coefficients def test_coefficient_derivatives(self): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) dv = TestFunction(space) f = Coefficient(space, count=0) g = Coefficient(space, count=1) df = Coefficient(space, count=2) dg = Coefficient(space, count=3) u = Coefficient(space, count=4) cd = {f: df, g: dg} integrand = inner(f, g) expected = (df * dv) * g + f * (dg * dv) F = integrand * dx J = derivative(F, u, dv, cd) fd = compute_form_data(J) actual = fd.preprocessed_form.integrals()[0].integrand() assert (actual * dx).signature() == (expected * dx).signature() self.assertEqual(replace(actual, fd.function_replace_map), expected) def test_vector_coefficient_scalar_derivatives(self): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) VV = FiniteElement("vector Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) vv_space = FunctionSpace(domain, VV) dv = TestFunction(v_space) df = Coefficient(vv_space, count=0) g = Coefficient(vv_space, count=1) f = Coefficient(vv_space, count=2) u = Coefficient(v_space, count=3) cd = {f: df} integrand = inner(f, g) i0, i1, i2, i3, i4 = [Index(count=c) for c in range(5)] expected = as_tensor(df[i1] * dv, (i1,))[i0] * g[i0] F = integrand * dx J = derivative(F, u, dv, cd) fd = compute_form_data(J) actual = fd.preprocessed_form.integrals()[0].integrand() assert (actual * dx).signature() == (expected * dx).signature() def test_vector_coefficient_derivatives(self): V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) VV = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) vv_space = FunctionSpace(domain, VV) dv = TestFunction(v_space) df = Coefficient(vv_space, count=0) g = Coefficient(v_space, count=1) f = Coefficient(v_space, count=2) u = Coefficient(v_space, count=3) cd = {f: df} integrand = inner(f, g) i0, i1, i2, i3, i4 = [Index(count=c) for c in range(5)] expected = as_tensor(df[i2, i1] * dv[i1], (i2,))[i0] * g[i0] F = integrand * dx J = derivative(F, u, dv, cd) fd = compute_form_data(J) actual = fd.preprocessed_form.integrals()[0].integrand() assert (actual * dx).signature() == (expected * dx).signature() # self.assertEqual(replace(actual, fd.function_replace_map), expected) def test_vector_coefficient_derivatives_of_product(self): V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) VV = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) vv_space = FunctionSpace(domain, VV) dv = TestFunction(v_space) df = Coefficient(vv_space, count=0) g = Coefficient(v_space, count=1) dg = Coefficient(vv_space, count=2) f = Coefficient(v_space, count=3) u = Coefficient(v_space, count=4) cd = {f: df, g: dg} integrand = f[i] * g[i] i0, i1, i2, i3, i4 = [Index(count=c) for c in range(5)] expected = ( as_tensor(df[i2, i1] * dv[i1], (i2,))[i0] * g[i0] + f[i0] * as_tensor(dg[i4, i3] * dv[i3], (i4,))[i0] ) F = integrand * dx J = derivative(F, u, dv, cd) fd = compute_form_data(J) actual = fd.preprocessed_form.integrals()[0].integrand() # Tricky case! These are equal in representation except # that the outermost sum/indexsum are swapped. # Sampling the expressions instead of comparing representations. x = (0, 0) funcs = {dv: (13, 14), f: (1, 2), g: (3, 4), df: ((5, 6), (7, 8)), dg: ((9, 10), (11, 12))} self.assertEqual(replace(actual, fd.function_replace_map)(x, funcs), expected(x, funcs)) # TODO: Add tests covering more cases, in particular mixed stuff # --- Some actual forms def testHyperElasticity(self): cell = interval element = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (1,), identity_pullback, H1)) space = FunctionSpace(domain, element) w = Coefficient(space) v = TestFunction(space) u = TrialFunction(space) b = Constant(domain) K = Constant(domain) dw = w.dx(0) dv = v.dx(0) du = v.dx(0) E = dw + dw**2 / 2 E = variable(E) Q = b * E**2 psi = K * (exp(Q) - 1) f = psi * dx F = derivative(f, w, v) J = derivative(F, w, u) form_data_f = compute_form_data(f) form_data_F = compute_form_data(F) form_data_J = compute_form_data(J) f = form_data_f.preprocessed_form F = form_data_F.preprocessed_form J = form_data_J.preprocessed_form f_expression = strip_variables(f.integrals_by_type("cell")[0].integrand()) F_expression = strip_variables(F.integrals_by_type("cell")[0].integrand()) J_expression = strip_variables(J.integrals_by_type("cell")[0].integrand()) # classes = set(c.__class__ for c in post_traversal(f_expression)) Kv = 0.2 bv = 0.3 dw = 0.5 dv = 0.7 du = 0.11 E = dw + dw**2 / 2.0 Q = bv * E**2 expQ = float(exp(Q)) psi = Kv * (expQ - 1) fv = psi Fv = 2 * Kv * bv * E * (1 + dw) * expQ * dv Jv = 2 * Kv * bv * expQ * dv * du * (E + (1 + dw) ** 2 * (2 * bv * E**2 + 1)) def Nv(x, derivatives): assert derivatives == (0,) return dv def Nu(x, derivatives): assert derivatives == (0,) return du def Nw(x, derivatives): assert derivatives == (0,) return dw mapping = {K: Kv, b: bv, w: Nw} fv2 = f_expression((0,), mapping) self.assertAlmostEqual(fv, fv2) (v,) = form_data_F.original_form.arguments() mapping = {K: Kv, b: bv, v: Nv, w: Nw} Fv2 = F_expression((0,), mapping) self.assertAlmostEqual(Fv, Fv2) v, u = form_data_J.original_form.arguments() mapping = {K: Kv, b: bv, v: Nv, u: Nu, w: Nw} Jv2 = J_expression((0,), mapping) self.assertAlmostEqual(Jv, Jv2) def test_mass_derived_from_functional(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) f = (w**2 / 2) * dx L = w * v * dx # a = u*v*dx F = derivative(f, w, v) derivative(L, w, u) derivative(F, w, u) # TODO: assert something # --- Interaction with replace def test_derivative_replace_works_together(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) M = cos(f) * sin(g) F = derivative(M, f, v) J = derivative(F, f, u) JR = replace(J, {f: g}) F2 = -sin(f) * v * sin(g) J2 = -cos(f) * u * v * sin(g) JR2 = -cos(g) * u * v * sin(g) assertEqualBySampling(F, F2) assertEqualBySampling(J, J2) assertEqualBySampling(JR, JR2) def test_index_simplification_handles_repeated_indices(self): mesh = Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2,), identity_pullback, H1)) V = FunctionSpace(mesh, FiniteElement("DQ", quadrilateral, 0, (2, 2), identity_pullback, L2)) K = JacobianInverse(mesh) G = outer(Identity(2), Identity(2)) i, j, k, L, m, n = indices(6) A = as_tensor(K[m, i] * K[n, j] * G[i, j, k, L], (m, n, k, L)) i, j = indices(2) # Can't use A[i, i, j, j] because UFL automagically index-sums # repeated indices in the __getitem__ call. Adiag = Indexed(A, MultiIndex((i, i, j, j))) A = as_tensor(Adiag, (i, j)) v = TestFunction(V) f = inner(A, v) * dx fd = compute_form_data(f, do_apply_geometry_lowering=True) (integral,) = fd.preprocessed_form.integrals() assert integral.integrand().ufl_free_indices == () def test_index_simplification_reference_grad(self): mesh = Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2,), identity_pullback, H1)) (i,) = indices(1) A = as_tensor(Indexed(Jacobian(mesh), MultiIndex((i, i))), (i,)) expr = apply_derivatives(apply_geometry_lowering(apply_algebra_lowering(A[0]))) assert expr == ReferenceGrad(SpatialCoordinate(mesh))[0, 0] assert expr.ufl_free_indices == () assert expr.ufl_shape == () # --- Scratch space def test_foobar(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) du = TrialFunction(space) U = Coefficient(space) def planarGrad(u): return as_matrix([[u[0].dx(0), 0, u[0].dx(1)], [0, 0, 0], [u[1].dx(0), 0, u[1].dx(1)]]) def epsilon(u): return 0.5 * (planarGrad(u) + planarGrad(u).T) def NS_a(u, v): return inner(epsilon(u), epsilon(v)) L = NS_a(U, v) * dx _ = derivative(L, U, du) # TODO: assert something ufl-2024.2.0/test/test_diff.py000077500000000000000000000073541470142567200160530ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-17 -- 2014-10-14" import pytest from ufl import ( Coefficient, FunctionSpace, Mesh, SpatialCoordinate, as_vector, atan, cos, diff, exp, indices, ln, sin, tan, triangle, variable, ) from ufl.algorithms import expand_derivatives from ufl.constantvalue import as_ufl from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def get_variables(): xv = None vv = 5.0 return (xv, variable(vv)) @pytest.fixture def v(): xv, vv = get_variables() return vv def _test(f, df): x, v = get_variables() dfv1 = diff(f(v), v) dfv2 = df(v) dfv1 = dfv1(x) dfv2 = dfv2(x) assert round(dfv1 - dfv2, 7) == 0 dfv1 = diff(f(7 * v), v) dfv2 = 7 * df(7 * v) dfv1 = dfv1(x) dfv2 = dfv2(x) assert round(dfv1 - dfv2, 7) == 0 def testVariable(v): def f(v): return v def df(v): return as_ufl(1) _test(f, df) def testSum(v): def f(v): return v + 1 def df(v): return as_ufl(1) _test(f, df) def testProduct(v): def f(v): return 3 * v def df(v): return as_ufl(3) _test(f, df) def testPower(v): def f(v): return v**3 def df(v): return 3 * v**2 _test(f, df) def testDivision(v): def f(v): return v / 3.0 def df(v): return as_ufl(1.0 / 3.0) _test(f, df) def testDivision2(v): def f(v): return 3.0 / v def df(v): return -3.0 / v**2 _test(f, df) def testExp(v): def f(v): return exp(v) def df(v): return exp(v) _test(f, df) def testLn(v): def f(v): return ln(v) def df(v): return 1.0 / v _test(f, df) def testSin(v): def f(v): return sin(v) def df(v): return cos(v) _test(f, df) def testCos(v): def f(v): return cos(v) def df(v): return -sin(v) _test(f, df) def testTan(v): def f(v): return tan(v) def df(v): return 2.0 / (cos(2.0 * v) + 1.0) _test(f, df) # TODO: Check the following tests. They run into strange math domain errors. # def testAsin(v): # def f(v): return asin(v) # def df(v): return 1/sqrt(1.0 - v**2) # _test(f, df) # def testAcos(v): # def f(v): return acos(v) # def df(v): return -1/sqrt(1.0 - v**2) # _test(f, df) def testAtan(v): def f(v): return atan(v) def df(v): return 1 / (1.0 + v**2) _test(f, df) def testIndexSum(v): def f(v): # 3*v + 4*v**2 + 5*v**3 a = as_vector((v, v**2, v**3)) b = as_vector((3, 4, 5)) (i,) = indices(1) return a[i] * b[i] def df(v): return 3 + 4 * 2 * v + 5 * 3 * v**2 _test(f, df) def testCoefficient(): coord_elem = FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1) mesh = Mesh(coord_elem) V = FunctionSpace(mesh, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) v = Coefficient(V) assert round(expand_derivatives(diff(v, v)) - 1.0, 7) == 0 def testDiffX(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) f = x[0] ** 2 * x[1] ** 2 (i,) = indices(1) df1 = diff(f, x) df2 = as_vector(f.dx(i), i) xv = (2, 3) df10 = df1[0](xv) df11 = df1[1](xv) df20 = df2[0](xv) df21 = df2[1](xv) assert round(df10 - df20, 7) == 0 assert round(df11 - df21, 7) == 0 assert round(df10 - 2 * 2 * 9, 7) == 0 assert round(df11 - 2 * 4 * 3, 7) == 0 # TODO: More tests involving wrapper types and indices ufl-2024.2.0/test/test_domains.py000077500000000000000000000321411470142567200165650ustar00rootroot00000000000000"""Tests of domain language and attaching domains to forms.""" import pytest from mockobjects import MockMesh import ufl # noqa: F401 from ufl import ( Cell, Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, dS, ds, dx, hexahedron, interval, quadrilateral, tetrahedron, triangle, ) from ufl.algorithms import compute_form_data from ufl.domain import extract_domains from ufl.finiteelement import FiniteElement from ufl.pullback import ( IdentityPullback, # noqa: F401 identity_pullback, ) from ufl.sobolevspace import H1 all_cells = (interval, triangle, tetrahedron, quadrilateral, hexahedron) def test_construct_domains_from_cells(): for cell in all_cells: d = cell.topological_dimension() Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) def test_construct_domains_with_names(): for cell in all_cells: d = cell.topological_dimension() e = FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1) D2 = Mesh(e, ufl_id=2) D3 = Mesh(e, ufl_id=3) D3b = Mesh(e, ufl_id=3) assert D2 != D3 assert D3 == D3b def test_domains_sort_by_name(): # This ordering is rather arbitrary, but at least this shows sorting is # working domains1 = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ), ufl_id=hash(cell.cellname()), ) for cell in all_cells ] domains2 = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ), ufl_id=hash(cell.cellname()), ) for cell in sorted(all_cells) ] sdomains = sorted(domains1, key=lambda D: (D.topological_dimension(), D.ufl_cell(), D.ufl_id())) assert sdomains != domains1 assert sdomains == domains2 def test_topdomain_creation(): D = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) assert D.geometric_dimension() == 1 D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert D.geometric_dimension() == 2 D = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) assert D.geometric_dimension() == 3 def test_cell_legacy_case(): # Passing cell like old code does D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) f = Coefficient(FunctionSpace(D, V)) assert f.ufl_domains() == (D,) M = f * dx assert M.ufl_domains() == (D,) def test_simple_domain_case(): # Creating domain from just cell with label like new dolfin will do D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=3) V = FunctionSpace(D, FiniteElement("Lagrange", D.ufl_cell(), 1, (), identity_pullback, "H1")) f = Coefficient(V) assert f.ufl_domains() == (D,) M = f * dx assert M.ufl_domains() == (D,) def test_creating_domains_with_coordinate_fields(): # FIXME: Rewrite for new approach # Definition of higher order domain, element, coefficient, form # Mesh with P2 representation of coordinates cell = triangle P2 = FiniteElement("Lagrange", cell, 2, (2,), identity_pullback, H1) domain = Mesh(P2) # Piecewise linear function space over quadratic mesh element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = FunctionSpace(domain, element) f = Coefficient(V) M = f * dx assert f.ufl_domains() == (domain,) assert M.ufl_domains() == (domain,) # Test the gymnastics that dolfin will have to go through domain2 = Mesh(P2, ufl_id=domain.ufl_id()) V2 = FunctionSpace(domain2, eval(repr(V.ufl_element()))) f2 = Coefficient(V2, count=f.count()) assert f == f2 assert domain == domain2 assert V == V2 def test_join_domains(): from ufl.domain import join_domains mesh7 = MockMesh(7) mesh8 = MockMesh(8) triangle = Cell("triangle") xa = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) xb = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) # Equal domains are joined assert 1 == len( join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7), Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7), ] ) ) assert 1 == len( join_domains( [ Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh7, ), Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh7, ), ] ) ) assert 1 == len(join_domains([Mesh(xa, ufl_id=3), Mesh(xa, ufl_id=3)])) # Different domains are not joined assert 2 == len( join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), ] ) ) assert 2 == len( join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7), Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=8), ] ) ) assert 2 == len( join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7), Mesh( FiniteElement("Lagrange", quadrilateral, 1, (2,), identity_pullback, H1), ufl_id=8, ), ] ) ) assert 2 == len(join_domains([Mesh(xa, ufl_id=7), Mesh(xa, ufl_id=8)])) assert 2 == len(join_domains([Mesh(xa), Mesh(xb)])) # Incompatible coordinates require labeling xc = Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ) ) xd = Coefficient( FunctionSpace( Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ) ) with pytest.raises(BaseException): join_domains([Mesh(xc), Mesh(xd)]) # Incompatible data is checked if and only if the domains are the same assert 2 == len( join_domains( [ Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh7, ), Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=8, cargo=mesh8, ), ] ) ) assert 2 == len( join_domains( [ Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh7, ), Mesh( FiniteElement("Lagrange", quadrilateral, 1, (2,), identity_pullback, H1), ufl_id=8, cargo=mesh8, ), ] ) ) # Geometric dimensions must match with pytest.raises(BaseException): join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)), Mesh(FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1)), ] ) with pytest.raises(BaseException): join_domains( [ Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh7, ), Mesh( FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1), ufl_id=8, cargo=mesh8, ), ] ) # Cargo and mesh ids must match with pytest.raises(BaseException): Mesh( FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7, cargo=mesh8, ) # Nones are removed assert 2 == len( join_domains( [ None, Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=3), None, Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=3), None, Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=4), ] ) ) assert 2 == len( join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=7), None, Mesh( FiniteElement("Lagrange", quadrilateral, 1, (2,), identity_pullback, H1), ufl_id=8, ), ] ) ) assert None not in join_domains( [ Mesh(FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1), ufl_id=7), None, Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1), ufl_id=8), ] ) def test_everywhere_integrals_with_backwards_compatibility(): D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) V = FunctionSpace(D, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) f = Coefficient(V) a = f * dx (ida,) = compute_form_data(a).integral_data # Check some integral data assert ida.integral_type == "cell" assert len(ida.subdomain_id) == 1 assert ida.subdomain_id[0] == "otherwise" assert ida.metadata == {} # Integrands are not equal because of renumbering itg1 = ida.integrals[0].integrand() itg2 = a.integrals()[0].integrand() assert type(itg1) is type(itg2) assert itg1.ufl_element() == itg2.ufl_element() def test_merge_sort_integral_data(): D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) V = FunctionSpace(D, FiniteElement("CG", triangle, 1, (), identity_pullback, H1)) u = Coefficient(V) c = Constant(D) a = c * dS((2, 4)) + u * dx + u * ds + 2 * u * dx(3) + 2 * c * dS + 2 * u * dx((1, 4)) form_data = compute_form_data(a, do_append_everywhere_integrals=False).integral_data assert len(form_data) == 5 # Check some integral data assert form_data[0].integral_type == "cell" assert len(form_data[0].subdomain_id) == 1 assert form_data[0].subdomain_id[0] == "otherwise" assert form_data[0].metadata == {} assert form_data[1].integral_type == "cell" assert len(form_data[1].subdomain_id) == 3 assert form_data[1].subdomain_id[0] == 1 assert form_data[1].subdomain_id[1] == 3 assert form_data[1].subdomain_id[2] == 4 assert form_data[1].metadata == {} assert form_data[2].integral_type == "exterior_facet" assert len(form_data[2].subdomain_id) == 1 assert form_data[2].subdomain_id[0] == "otherwise" assert form_data[2].metadata == {} assert form_data[3].integral_type == "interior_facet" assert len(form_data[3].subdomain_id) == 1 assert form_data[3].subdomain_id[0] == "otherwise" assert form_data[3].metadata == {} assert form_data[4].integral_type == "interior_facet" assert len(form_data[4].subdomain_id) == 2 assert form_data[4].subdomain_id[0] == 2 assert form_data[4].subdomain_id[1] == 4 assert form_data[4].metadata == {} def test_extract_domains(): "Test that the domains are extracted properly from a mixed-domain expression" # Create domains of different topological dimensions gdim = 2 cell_type_0 = triangle cell_type_1 = interval dom_0 = Mesh(FiniteElement("Lagrange", cell_type_0, 1, (gdim,), identity_pullback, H1)) dom_1 = Mesh(FiniteElement("Lagrange", cell_type_1, 1, (gdim,), identity_pullback, H1)) # Define some finite element spaces k = 1 ele_type = "Lagrange" ele_0 = FiniteElement(ele_type, cell_type_0, k, (), identity_pullback, H1) ele_1 = FiniteElement(ele_type, cell_type_1, k, (), identity_pullback, H1) V_0 = FunctionSpace(dom_0, ele_0) V_1 = FunctionSpace(dom_1, ele_1) # Create test and trial functions u = TrialFunction(V_0) v = TestFunction(V_1) # Create a mixed-domain expression expr = u * v domains = extract_domains(expr) assert domains[0] == dom_1 assert domains[1] == dom_0 ufl-2024.2.0/test/test_duals.py000066400000000000000000000241031470142567200162370ustar00rootroot00000000000000__authors__ = "India Marsden" __date__ = "2020-12-28" import pytest from ufl import ( Action, Adjoint, Argument, Coargument, Coefficient, Cofunction, FormSum, FunctionSpace, Matrix, Mesh, MixedFunctionSpace, TestFunction, TrialFunction, action, adjoint, derivative, dx, inner, interval, tetrahedron, triangle, ) from ufl.algorithms.ad import expand_derivatives from ufl.constantvalue import Zero from ufl.duals import is_dual, is_primal from ufl.finiteelement import FiniteElement from ufl.form import ZeroBaseForm from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_mixed_functionspace(self): # Domains domain_3d = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) # Finite elements f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) f_3d = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) # Function spaces V_3d = FunctionSpace(domain_3d, f_3d) V_2d = FunctionSpace(domain_2d, f_2d) V_1d = FunctionSpace(domain_1d, f_1d) # MixedFunctionSpace = V_3d x V_2d x V_1d V = MixedFunctionSpace(V_3d, V_2d, V_1d) # Check sub spaces assert is_primal(V_3d) assert is_primal(V_2d) assert is_primal(V_1d) assert is_primal(V) # Get dual of V_3 V_dual = V_3d.dual() # Test dual functions on MixedFunctionSpace = V_dual x V_2d x V_1d V = MixedFunctionSpace(V_dual, V_2d, V_1d) V_mixed_dual = MixedFunctionSpace(V_dual, V_2d.dual(), V_1d.dual()) assert is_dual(V_dual) assert not is_dual(V) assert is_dual(V_mixed_dual) def test_dual_coefficients(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) V_dual = V.dual() v = Coefficient(V, count=1) u = Coefficient(V_dual, count=1) w = Cofunction(V_dual) assert is_primal(v) assert not is_dual(v) assert is_dual(u) assert not is_primal(u) assert is_dual(w) assert not is_primal(w) with pytest.raises(ValueError): Cofunction(V) def test_dual_arguments(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) V_dual = V.dual() v = Argument(V, 1) u = Argument(V_dual, 2) w = Coargument(V_dual, 3) assert is_primal(v) assert not is_dual(v) assert is_dual(u) assert not is_primal(u) assert is_dual(w) assert not is_primal(w) with pytest.raises(ValueError): Coargument(V, 4) def test_addition(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) V_dual = V.dual() fvector_2d = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) W = FunctionSpace(domain_2d, fvector_2d) u = TrialFunction(V) v = TestFunction(V) # linear 1-form L = v * dx a = Cofunction(V_dual) res = L + a assert isinstance(res, FormSum) assert res L = u * v * dx a = Matrix(V, V) res = L + a assert isinstance(res, FormSum) assert res # Check BaseForm._add__ simplification res += ZeroBaseForm((v, u)) assert res == a + L # Check Form._add__ simplification L += ZeroBaseForm((v,)) assert L == u * v * dx # Check BaseForm._add__ simplification res = ZeroBaseForm((v, u)) res += a assert res == a # Check __neg__ res = L res -= ZeroBaseForm((v,)) assert res == L # Simplification with respect to ufl.Zero a_W = Matrix(W, W) res = a_W + Zero(W.value_shape) assert res == a_W def test_scalar_mult(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) V_dual = V.dual() # linear 1-form a = Cofunction(V_dual) res = 2 * a assert isinstance(res, FormSum) assert res a = Matrix(V, V) res = 2 * a assert isinstance(res, FormSum) assert res def test_adjoint(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) a = Matrix(V, V) adj = adjoint(a) res = 2 * adj assert isinstance(res, FormSum) assert res res = adjoint(2 * a) assert isinstance(res, FormSum) assert isinstance(res.components()[0], Adjoint) # Adjoint(Adjoint(.)) = Id assert adjoint(adj) == a def test_action(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) U = FunctionSpace(domain_1d, f_1d) a = Matrix(V, U) b = Matrix(V, U.dual()) u = Coefficient(U) u_a = Argument(U, 0) v = Coefficient(V) ustar = Cofunction(U.dual()) u_form = u_a * dx res = action(a, u) assert res assert len(res.arguments()) < len(a.arguments()) assert isinstance(res, Action) repeat = action(res, v) assert repeat assert len(repeat.arguments()) < len(res.arguments()) res = action(2 * a, u) assert isinstance(res, FormSum) assert isinstance(res.components()[0], Action) res = action(b, u_form) assert res assert len(res.arguments()) < len(b.arguments()) with pytest.raises(TypeError): res = action(a, v) with pytest.raises(TypeError): res = action(a, ustar) b2 = Matrix(V, U.dual()) ustar2 = Cofunction(U.dual()) # Check Action left-distributivity with FormSum res = action(b, ustar + ustar2) assert res == Action(b, ustar) + Action(b, ustar2) # Check Action right-distributivity with FormSum res = action(b + b2, ustar) assert res == Action(b, ustar) + Action(b2, ustar) a2 = Matrix(V, U) u2 = Coefficient(U) u3 = Coefficient(U) # Check Action left-distributivity with Sum # Add 3 Coefficients to check composition of Sum works fine since u # + u2 + u3 => Sum(u, Sum(u2, u3)) res = action(a, u + u2 + u3) assert res == Action(a, u3) + Action(a, u) + Action(a, u2) # Check Action right-distributivity with Sum res = action(a + a2, u) assert res == Action(a, u) + Action(a2, u) def test_differentiation(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) U = FunctionSpace(domain_1d, f_1d) u = Coefficient(U) v = Argument(U, 0) vstar = Argument(U.dual(), 0) # -- Cofunction -- # w = Cofunction(U.dual()) dwdu = expand_derivatives(derivative(w, u)) assert isinstance(dwdu, ZeroBaseForm) assert dwdu.arguments() == ( Argument(w.ufl_function_space().dual(), 0), Argument(u.ufl_function_space(), 1), ) # Check compatibility with int/float assert dwdu == 0 dwdw = expand_derivatives(derivative(w, w, vstar)) assert dwdw == vstar dudw = expand_derivatives(derivative(u, w)) # du/dw is a ufl.Zero and not a ZeroBaseForm # as we are not differentiating a BaseForm assert isinstance(dudw, Zero) assert dudw == 0 # -- Coargument -- # dvstardu = expand_derivatives(derivative(vstar, u)) assert isinstance(dvstardu, ZeroBaseForm) assert dvstardu.arguments() == vstar.arguments() + (Argument(u.ufl_function_space(), 1),) # Check compatibility with int/float assert dvstardu == 0 # -- Matrix -- # M = Matrix(V, U) dMdu = expand_derivatives(derivative(M, u)) assert isinstance(dMdu, ZeroBaseForm) assert dMdu.arguments() == M.arguments() + (Argument(u.ufl_function_space(), 2),) # Check compatibility with int/float assert dMdu == 0 # -- Action -- # Ac = Action(w, u) dAcdu = derivative(Ac, u) assert dAcdu == ( action(adjoint(derivative(w, u), derivatives_expanded=True), u, derivatives_expanded=True) + action(w, derivative(u, u), derivatives_expanded=True) ) dAcdu = expand_derivatives(dAcdu) # Since dw/du = 0 assert dAcdu == Action(w, v) # -- Form sum -- # uhat = Argument(U, 1) what = Argument(U, 2) Fs = M + inner(u * uhat, v) * dx dFsdu = expand_derivatives(derivative(Fs, u)) # Distribute differentiation over FormSum components assert dFsdu == inner(what * uhat, v) * dx def test_zero_base_form_mult(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) v = Argument(V, 0) Z = ZeroBaseForm((v, v)) u = Coefficient(V) Zu = Z * u assert Zu == action(Z, u) assert action(Zu, u) == ZeroBaseForm(()) def test_base_form_call(): domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) V = FunctionSpace(domain_2d, f_2d) # Check duality pairing f = Coefficient(V) c = Cofunction(V.dual()) assert c(f) == action(c, f) ufl-2024.2.0/test/test_equals.py000077500000000000000000000075771470142567200164440ustar00rootroot00000000000000"""Test of expression comparison.""" from ufl import Coefficient, Cofunction, FunctionSpace, Mesh, triangle from ufl.exprcontainers import ExprList from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_comparison_of_coefficients(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) U = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) Ub = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) u_space = FunctionSpace(domain, U) ub_space = FunctionSpace(domain, Ub) v1 = Coefficient(v_space, count=1) v1b = Coefficient(v_space, count=1) v2 = Coefficient(v_space, count=2) u1 = Coefficient(u_space, count=1) u2 = Coefficient(u_space, count=2) u2b = Coefficient(ub_space, count=2) # Identical objects assert v1 == v1 assert u2 == u2 # Equal but distinct objects assert v1 == v1b assert u2 == u2b # Different objects assert not v1 == v2 assert not u1 == u2 assert not v1 == u1 assert not v2 == u2 def test_comparison_of_cofunctions(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) U = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) Ub = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) u_space = FunctionSpace(domain, U) ub_space = FunctionSpace(domain, Ub) v1 = Cofunction(v_space.dual(), count=1) v1b = Cofunction(v_space.dual(), count=1) v2 = Cofunction(v_space.dual(), count=2) u1 = Cofunction(u_space.dual(), count=1) u2 = Cofunction(u_space.dual(), count=2) u2b = Cofunction(ub_space.dual(), count=2) # Identical objects assert v1 == v1 assert u2 == u2 # Equal but distinct objects assert v1 == v1b assert u2 == u2b # Different objects assert not v1 == v2 assert not u1 == u2 assert not v1 == u1 assert not v2 == u2 # Objects in ExprList as happens when taking derivatives. assert ExprList(v1, v1) == ExprList(v1, v1b) assert not ExprList(v1, v2) == ExprList(v1, v1) def test_comparison_of_products(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) v = Coefficient(v_space) u = Coefficient(v_space) a = (v * 2) * u b = (2 * v) * u c = 2 * (v * u) assert a == b assert not a == c assert not b == c def test_comparison_of_sums(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) v = Coefficient(v_space) u = Coefficient(v_space) a = (v + 2) + u b = (2 + v) + u c = 2 + (v + u) assert a == b assert not a == c assert not b == c def test_comparison_of_deeply_nested_expression(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v_space = FunctionSpace(domain, V) v = Coefficient(v_space, count=1) u = Coefficient(v_space, count=1) w = Coefficient(v_space, count=2) def build_expr(a): for i in range(100): if i % 3 == 0: a = a + i elif i % 3 == 1: a = a * i elif i % 3 == 2: a = a**i return a a = build_expr(u) b = build_expr(v) c = build_expr(w) assert a == b assert not a == c assert not b == c ufl-2024.2.0/test/test_evaluate.py000077500000000000000000000233451470142567200167470ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-13 -- 2009-02-13" import math from ufl import ( Argument, Coefficient, FunctionSpace, Identity, Mesh, SpatialCoordinate, as_matrix, as_vector, cos, cross, det, dev, dot, exp, i, indices, inner, j, ln, outer, sin, skew, sqrt, sym, tan, tetrahedron, tr, triangle, ) from ufl.constantvalue import as_ufl from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def testScalars(): s = as_ufl(123) e = s((5, 7)) v = 123 assert e == v def testZero(): s = as_ufl(0) e = s((5, 7)) v = 0 assert e == v def testIdentity(): cell = triangle ident = Identity(cell.topological_dimension()) s = 123 * ident[0, 0] e = s((5, 7)) v = 123 assert e == v s = 123 * ident[1, 0] e = s((5, 7)) v = 0 assert e == v def testCoords(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) s = x[0] + x[1] e = s((5, 7)) v = 5 + 7 assert e == v def testFunction1(): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) s = 3 * f e = s((5, 7), {f: 123}) v = 3 * 123 assert e == v def testFunction2(): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) def g(x): return x[0] s = 3 * f e = s((5, 7), {f: g}) v = 3 * 5 assert e == v def testArgument2(): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Argument(space, 2) def g(x): return x[0] s = 3 * f e = s((5, 7), {f: g}) v = 3 * 5 assert e == v def testAlgebra(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) s = 3 * (x[0] + x[1]) - 7 + x[0] ** (x[1] / 2) e = s((5, 7)) v = 3 * (5.0 + 7.0) - 7 + 5.0 ** (7.0 / 2) assert e == v def testIndexSum(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) (i,) = indices(1) s = x[i] * x[i] e = s((5, 7)) v = 5**2 + 7**2 assert e == v def testIndexSum2(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) ident = Identity(cell.topological_dimension()) i, j = indices(2) s = (x[i] * x[j]) * ident[i, j] e = s((5, 7)) # v = sum_i sum_j x_i x_j delta_ij = x_0 x_0 + x_1 x_1 v = 5**2 + 7**2 assert e == v def testMathFunctions(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain)[0] s = sin(x) e = s((5, 7)) v = math.sin(5) assert e == v s = cos(x) e = s((5, 7)) v = math.cos(5) assert e == v s = tan(x) e = s((5, 7)) v = math.tan(5) assert e == v s = ln(x) e = s((5, 7)) v = math.log(5) assert e == v s = exp(x) e = s((5, 7)) v = math.exp(5) assert e == v s = sqrt(x) e = s((5, 7)) v = math.sqrt(5) assert e == v def testListTensor(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x, y = SpatialCoordinate(domain) m = as_matrix([[x, y], [-y, -x]]) s = m[0, 0] + m[1, 0] + m[0, 1] + m[1, 1] e = s((5, 7)) v = 0 assert e == v s = m[0, 0] * m[1, 0] * m[0, 1] * m[1, 1] e = s((5, 7)) v = 5**2 * 7**2 assert e == v def testComponentTensor1(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) m = as_vector(x[i], i) s = m[0] * m[1] e = s((5, 7)) v = 5 * 7 assert e == v def testComponentTensor2(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xx = outer(x, x) m = as_matrix(xx[i, j], (i, j)) s = m[0, 0] + m[1, 0] + m[0, 1] + m[1, 1] e = s((5, 7)) v = 5 * 5 + 5 * 7 + 5 * 7 + 7 * 7 assert e == v def testComponentTensor3(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xx = outer(x, x) m = as_matrix(xx[i, j], (i, j)) s = m[0, 0] * m[1, 0] * m[0, 1] * m[1, 1] e = s((5, 7)) v = 5 * 5 * 5 * 7 * 5 * 7 * 7 * 7 assert e == v def testCoefficient(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) f = Coefficient(space) e = f**2 def eval_f(x): return x[0] * x[1] ** 2 assert e((3, 7), {f: eval_f}) == (3 * 7**2) ** 2 def testCoefficientDerivative(): V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) f = Coefficient(space) e = f.dx(0) ** 2 + f.dx(1) ** 2 def eval_f(x, derivatives): if not derivatives: return eval_f.c * x[0] * x[1] ** 2 # assume only first order derivative (d,) = derivatives if d == 0: return eval_f.c * x[1] ** 2 if d == 1: return eval_f.c * x[0] * 2 * x[1] # shows how to attach data to eval_f eval_f.c = 5 assert e((3, 7), {f: eval_f}) == (5 * 7**2) ** 2 + (5 * 3 * 2 * 7) ** 2 def test_dot(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) s = dot(x, 2 * x) e = s((5, 7)) v = 2 * (5 * 5 + 7 * 7) assert e == v def test_inner(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xx = as_matrix(((2 * x[0], 3 * x[0]), (2 * x[1], 3 * x[1]))) s = inner(xx, 2 * xx) e = s((5, 7)) v = 2 * ((5 * 2) ** 2 + (5 * 3) ** 2 + (7 * 2) ** 2 + (7 * 3) ** 2) assert e == v def test_outer(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xx = outer(outer(x, x), as_vector((2, 3))) s = inner(xx, 2 * xx) e = s((5, 7)) v = 2 * (5**2 + 7**2) ** 2 * (2**2 + 3**2) assert e == v def test_cross(): domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (3, 5, 7) # Test cross product of triplets of orthogonal # vectors, where |a x b| = |a| |b| ts = [ [as_vector((x[0], 0, 0)), as_vector((0, x[1], 0)), as_vector((0, 0, x[2]))], [as_vector((x[0], x[1], 0)), as_vector((x[1], -x[0], 0)), as_vector((0, 0, x[2]))], [as_vector((0, x[0], x[1])), as_vector((0, x[1], -x[0])), as_vector((x[2], 0, 0))], [as_vector((x[0], 0, x[1])), as_vector((x[1], 0, -x[0])), as_vector((0, x[2], 0))], ] for t in ts: for a in range(3): for b in range(3): cab = cross(t[a], t[b]) dab = dot(cab, cab) eab = dab(xv) tna = dot(t[a], t[a])(xv) tnb = dot(t[b], t[b])(xv) vab = tna * tnb if a != b else 0 assert eab == vab def xtest_dev(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s1 = dev(2 * xx) s2 = 2 * (xx - xx.T) # FIXME e = inner(s1, s1)(xv) v = inner(s2, s2)(xv) assert e == v def test_skew(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s1 = skew(2 * xx) s2 = xx - xx.T e = inner(s1, s1)(xv) v = inner(s2, s2)(xv) assert e == v def test_sym(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s1 = sym(2 * xx) s2 = xx + xx.T e = inner(s1, s1)(xv) v = inner(s2, s2)(xv) assert e == v def test_tr(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s = tr(2 * xx) e = s(xv) v = 2 * sum(xv[i] ** 2 for i in (0, 1)) assert e == v def test_det2D(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) xv = (5, 7) a, b = 6.5, -4 xx = as_matrix(((x[0], x[1]), (a, b))) s = det(2 * xx) e = s(xv) v = 2**2 * (5 * b - 7 * a) assert e == v def xtest_det3D(): # FIXME x = SpatialCoordinate(tetrahedron) xv = (5, 7, 9) a, b, c = 6.5, -4, 3 d, e, f = 2, 3, 4 xx = as_matrix(((x[0], x[1], x[2]), (a, b, c), (d, e, f))) s = det(2 * xx) e = s(xv) v = 2**3 * (xv[0] * (b * f - e * c) - xv[1] * (a * f - c * d) + xv[2] * (a * e - b * d)) assert e == v def test_cofac(): pass # TODO def test_inv(): pass # TODO ufl-2024.2.0/test/test_expand_indices.py000077500000000000000000000235621470142567200201170ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2009-03-19 -- 2012-03-20" # Modified by Anders Logg, 2008 # Modified by Garth N. Wells, 2009 import math import pytest from ufl import ( Coefficient, FunctionSpace, Identity, Mesh, as_tensor, cos, det, div, dot, dx, exp, grad, i, inner, j, k, l, ln, nabla_div, nabla_grad, outer, sin, triangle, ) from ufl.algorithms import compute_form_data, expand_derivatives, expand_indices from ufl.algorithms.renumbering import renumber_indices from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # TODO: Test expand_indices2 throuroughly for correctness, then efficiency: # expand_indices, expand_indices2 = expand_indices2, expand_indices class Fixture: def __init__(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) velement = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) telement = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) vspace = FunctionSpace(domain, velement) tspace = FunctionSpace(domain, telement) self.sf = Coefficient(space) self.sf2 = Coefficient(space) self.vf = Coefficient(vspace) self.tf = Coefficient(tspace) # Note: the derivatives of these functions make no sense, but # their unique constant values are used for validation. def SF(x, derivatives=()): # first order derivatives if derivatives == (0,): return 0.30 elif derivatives == (1,): return 0.31 # second order derivatives elif derivatives == (0, 0): return 0 elif derivatives in ((1, 0), (0, 1)): return 0 elif derivatives == (1, 1): return 0 # function value assert derivatives == () return 3 def SF2(x, derivatives=()): # first order derivatives if derivatives == (0,): return 0.30 elif derivatives == (1,): return 0.31 # second order derivatives elif derivatives == (0, 0): return 3.300 elif derivatives in ((1, 0), (0, 1)): return 3.310 elif derivatives == (1, 1): return 3.311 # function value assert derivatives == () return 3 def VF(x, derivatives=()): # first order derivatives if derivatives == (0,): return (0.50, 0.70) elif derivatives == (1,): return (0.51, 0.71) # second order derivatives elif derivatives == (0, 0): return (0.20, 0.21) elif derivatives in ((1, 0), (0, 1)): return (0.30, 0.31) elif derivatives == (1, 1): return (0.40, 0.41) # function value assert derivatives == () return (5, 7) def TF(x, derivatives=()): # first order derivatives if derivatives == (0,): return ((1.10, 1.30), (1.70, 1.90)) elif derivatives == (1,): return ((1.11, 1.31), (1.71, 1.91)) # second order derivatives elif derivatives == (0, 0): return ((10.00, 10.01), (10.10, 10.11)) elif derivatives in ((1, 0), (0, 1)): return ((12.00, 12.01), (12.10, 12.11)) elif derivatives == (1, 1): return ((11.00, 11.01), (11.10, 11.11)) # function value assert derivatives == () return ((11, 13), (17, 19)) self.x = (1.23, 3.14) self.mapping = {self.sf: SF, self.sf2: SF2, self.vf: VF, self.tf: TF} def compare(self, f, value): debug = 0 if debug: print(("f", f)) g = expand_derivatives(f) if debug: print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 g = expand_indices(g) if debug: print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 g = renumber_indices(g) if debug: print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 @pytest.fixture(scope="module") def fixt(): # Workaround for quick pytest transition return Fixture() def test_basic_expand_indices(self, fixt): sf = fixt.sf vf = fixt.vf tf = fixt.tf compare = fixt.compare # Simple expressions with no indices or derivatives to expand compare(sf, 3) compare(sf + 1, 4) compare(sf - 2.5, 0.5) compare(sf / 2, 1.5) compare(sf / 0.5, 6) compare(sf**2, 9) compare(sf**0.5, 3**0.5) compare(sf**3, 27) compare(0.5**sf, 0.5**3) compare(sf * (sf / 6), 1.5) compare(sin(sf), math.sin(3)) compare(cos(sf), math.cos(3)) compare(exp(sf), math.exp(3)) compare(ln(sf), math.log(3)) # Simple indexing compare(vf[0], 5) compare(vf[0] + 1, 6) compare(vf[0] - 2.5, 2.5) compare(vf[0] / 2, 2.5) compare(vf[0] / 0.5, 10) compare(vf[0] ** 2, 25) compare(vf[0] ** 0.5, 5**0.5) compare(vf[0] ** 3, 125) compare(0.5 ** vf[0], 0.5**5) compare(vf[0] * (vf[0] / 6), 5 * (5.0 / 6)) compare(sin(vf[0]), math.sin(5)) compare(cos(vf[0]), math.cos(5)) compare(exp(vf[0]), math.exp(5)) compare(ln(vf[0]), math.log(5)) # Double indexing compare(tf[1, 1], 19) compare(tf[1, 1] + 1, 20) compare(tf[1, 1] - 2.5, 16.5) compare(tf[1, 1] / 2, 9.5) compare(tf[1, 1] / 0.5, 38) compare(tf[1, 1] ** 2, 19**2) compare(tf[1, 1] ** 0.5, 19**0.5) compare(tf[1, 1] ** 3, 19**3) compare(0.5 ** tf[1, 1], 0.5**19) compare(tf[1, 1] * (tf[1, 1] / 6), 19 * (19.0 / 6)) compare(sin(tf[1, 1]), math.sin(19)) compare(cos(tf[1, 1]), math.cos(19)) compare(exp(tf[1, 1]), math.exp(19)) compare(ln(tf[1, 1]), math.log(19)) def test_expand_indices_index_sum(self, fixt): vf = fixt.vf tf = fixt.tf compare = fixt.compare # Basic index sums compare(vf[i] * vf[i], 5 * 5 + 7 * 7) compare(vf[j] * tf[i, j] * vf[i], 5 * 5 * 11 + 5 * 7 * 13 + 5 * 7 * 17 + 7 * 7 * 19) compare(vf[j] * tf.T[j, i] * vf[i], 5 * 5 * 11 + 5 * 7 * 13 + 5 * 7 * 17 + 7 * 7 * 19) compare(tf[i, i], 11 + 19) compare( tf[i, j] * (tf[j, i] + outer(vf, vf)[i, j]), (5 * 5 + 11) * 11 + (7 * 5 + 17) * 13 + (7 * 5 + 13) * 17 + (7 * 7 + 19) * 19, ) compare(as_tensor(as_tensor(tf[i, j], (i, j))[k, l], (l, k))[i, i], 11 + 19) def test_expand_indices_derivatives(self, fixt): sf = fixt.sf vf = fixt.vf compare = fixt.compare # Basic derivatives compare(sf.dx(0), 0.3) compare(sf.dx(1), 0.31) compare(sf.dx(i) * vf[i], 0.30 * 5 + 0.31 * 7) compare(vf[j].dx(i) * vf[i].dx(j), 0.50 * 0.50 + 0.51 * 0.70 + 0.70 * 0.51 + 0.71 * 0.71) def test_expand_indices_hyperelasticity(self, fixt): vf = fixt.vf compare = fixt.compare # Deformation gradient ident = Identity(2) u = vf F = ident + grad(u) # F = (1 + vf[0].dx(0), vf[0].dx(1), vf[1].dx(0), 1 + vf[1].dx(1)) # F = (1 + 0.50, 0.51, 0.70, 1 + 0.71) F00 = 1 + 0.50 F01 = 0.51 F10 = 0.70 F11 = 1 + 0.71 compare(F[0, 0], F00) compare(F[0, 1], F01) compare(F[1, 0], F10) compare(F[1, 1], F11) J = det(F) compare(J, (1 + 0.50) * (1 + 0.71) - 0.70 * 0.51) # Strain tensors C = F.T * F # Cij = sum_k Fki Fkj C00 = F00 * F00 + F10 * F10 C01 = F00 * F01 + F10 * F11 C10 = F01 * F00 + F11 * F10 C11 = F01 * F01 + F11 * F11 compare(C[0, 0], C00) compare(C[0, 1], C01) compare(C[1, 0], C10) compare(C[1, 1], C11) E = (C - ident) / 2 E00 = (C00 - 1) / 2 E01 = (C01) / 2 E10 = (C10) / 2 E11 = (C11 - 1) / 2 compare(E[0, 0], E00) compare(E[0, 1], E01) compare(E[1, 0], E10) compare(E[1, 1], E11) # Strain energy Q = inner(E, E) Qvalue = E00**2 + E01**2 + E10**2 + E11**2 compare(Q, Qvalue) K = 0.5 psi = (K / 2) * exp(Q) compare(psi, 0.25 * math.exp(Qvalue)) def test_expand_indices_div_grad(self, fixt): sf = fixt.sf sf2 = fixt.sf2 vf = fixt.vf tf = fixt.tf compare = fixt.compare a = div(grad(sf)) compare(a, 0) a = div(grad(sf2)) compare(a, 3.300 + 3.311) if 0: Dvf = grad(vf) Lvf = div(Dvf) Lvf2 = dot(Lvf, Lvf) pp = compute_form_data(Lvf2 * dx).preprocessed_form.integrals()[0].integrand() print(("vf", vf.ufl_shape, str(vf))) print(("Dvf", Dvf.ufl_shape, str(Dvf))) print(("Lvf", Lvf.ufl_shape, str(Lvf))) print(("Lvf2", Lvf2.ufl_shape, str(Lvf2))) print(("pp", pp.ufl_shape, str(pp))) a = div(grad(vf)) compare(dot(a, a), (0.20 + 0.40) ** 2 + (0.21 + 0.41) ** 2) a = div(grad(tf)) compare( inner(a, a), (10.00 + 11.00) ** 2 + (10.01 + 11.01) ** 2 + (10.10 + 11.10) ** 2 + (10.11 + 11.11) ** 2, ) def test_expand_indices_nabla_div_grad(self, fixt): sf = fixt.sf sf2 = fixt.sf2 vf = fixt.vf tf = fixt.tf compare = fixt.compare a = nabla_div(nabla_grad(sf)) compare(a, 0) a = nabla_div(nabla_grad(sf2)) compare(a, 3.300 + 3.311) a = nabla_div(nabla_grad(vf)) compare(dot(a, a), (0.20 + 0.40) ** 2 + (0.21 + 0.41) ** 2) a = nabla_div(nabla_grad(tf)) compare( inner(a, a), (10.00 + 11.00) ** 2 + (10.01 + 11.01) ** 2 + (10.10 + 11.10) ** 2 + (10.11 + 11.11) ** 2, ) ufl-2024.2.0/test/test_external_operator.py000066400000000000000000000373751470142567200207030ustar00rootroot00000000000000"""Test ExternalOperator object.""" __authors__ = "Nacime Bouziani" __date__ = "2019-03-26" import pytest from ufl import ( Action, Argument, Coargument, Coefficient, Constant, Form, FunctionSpace, Matrix, Mesh, TestFunction, TrialFunction, action, adjoint, cos, derivative, dx, inner, replace, sin, triangle, ) from ufl.algorithms import expand_derivatives from ufl.algorithms.apply_derivatives import apply_derivatives from ufl.core.external_operator import ExternalOperator from ufl.finiteelement import FiniteElement from ufl.form import BaseForm from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def domain_2d(): return Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) @pytest.fixture def V1(domain_2d): f1 = FiniteElement("CG", triangle, 1, (), identity_pullback, H1) return FunctionSpace(domain_2d, f1) @pytest.fixture def V2(domain_2d): f1 = FiniteElement("CG", triangle, 2, (), identity_pullback, H1) return FunctionSpace(domain_2d, f1) @pytest.fixture def V3(domain_2d): f1 = FiniteElement("CG", triangle, 3, (), identity_pullback, H1) return FunctionSpace(domain_2d, f1) def test_properties(V1): u = Coefficient(V1, count=0) r = Coefficient(V1, count=1) e = ExternalOperator(u, r, function_space=V1) assert e.ufl_function_space() == V1 assert e.ufl_operands[0] == u assert e.ufl_operands[1] == r assert e.derivatives == (0, 0) assert e.ufl_shape == () e2 = ExternalOperator(u, r, function_space=V1, derivatives=(3, 4)) assert e2.derivatives == (3, 4) assert e2.ufl_shape == () # Test __str__ s = Coefficient(V1, count=2) t = Coefficient(V1, count=3) v0 = Argument(V1, 0) v1 = Argument(V1, 1) e = ExternalOperator(u, function_space=V1) assert str(e) == "e(w_0; v_0)" e = ExternalOperator(u, function_space=V1, derivatives=(1,)) assert str(e) == "∂e(w_0; v_0)/∂o1" e = ExternalOperator( u, r, 2 * s, t, function_space=V1, derivatives=(1, 0, 1, 2), argument_slots=(v0, v1) ) assert str(e) == "∂e(w_0, w_1, 2 * w_2, w_3; v_1, v_0)/∂o1∂o3∂o4∂o4" def test_form(V1, V2): u = Coefficient(V1) m = Coefficient(V1) u_hat = TrialFunction(V1) v = TestFunction(V1) # F = N * v * dx N = ExternalOperator(u, m, function_space=V2) F = N * v * dx actual = derivative(F, u, u_hat) (vstar,) = N.arguments() Nhat = TrialFunction(N.ufl_function_space()) dNdu = N._ufl_expr_reconstruct_(u, m, derivatives=(1, 0), argument_slots=(vstar, u_hat)) dFdN = Nhat * v * dx expected = Action(dFdN, dNdu) assert apply_derivatives(actual) == expected # F = N * u * v * dx N = ExternalOperator(u, m, function_space=V1) F = N * u * v * dx actual = derivative(F, u, u_hat) (vstar,) = N.arguments() Nhat = TrialFunction(N.ufl_function_space()) dNdu = N._ufl_expr_reconstruct_(u, m, derivatives=(1, 0), argument_slots=(vstar, u_hat)) dFdu_partial = N * u_hat * v * dx dFdN = Nhat * u * v * dx expected = dFdu_partial + Action(dFdN, dNdu) assert apply_derivatives(actual) == expected def test_differentiation_procedure_action(V1, V2): s = Coefficient(V1) u = Coefficient(V2) m = Coefficient(V2) # External operators N1 = ExternalOperator(u, m, function_space=V1) N2 = ExternalOperator(cos(s), function_space=V1) # Check arguments and argument slots assert len(N1.arguments()) == 1 assert len(N2.arguments()) == 1 assert N1.arguments() == N1.argument_slots() assert N2.arguments() == N2.argument_slots() # Check coefficients assert N1.coefficients() == (u, m) assert N2.coefficients() == (s,) # Get v* (vstar_N1,) = N1.arguments() (vstar_N2,) = N2.arguments() assert vstar_N1.ufl_function_space().dual() == V1 assert vstar_N2.ufl_function_space().dual() == V1 u_hat = Argument(V1, 1) s_hat = Argument(V2, 1) w = Coefficient(V1) r = Coefficient(V2) # Bilinear forms a1 = inner(N1, m) * dx Ja1 = derivative(a1, u, u_hat) Ja1 = expand_derivatives(Ja1) a2 = inner(N2, m) * dx Ja2 = derivative(a2, s, s_hat) Ja2 = expand_derivatives(Ja2) # Get external operators assert isinstance(Ja1, Action) dN1du = Ja1.right() dN1du_action = Action(dN1du, w) assert isinstance(Ja2, Action) dN2du = Ja2.right() dN2du_action = Action(dN2du, r) # Check shape assert dN1du.ufl_shape == () assert dN2du.ufl_shape == () # Get v*s vstar_dN1du, _ = dN1du.arguments() vstar_dN2du, _ = dN2du.arguments() assert vstar_dN1du.ufl_function_space().dual() == V1 # shape: (2,) assert vstar_dN2du.ufl_function_space().dual() == V1 # shape: (2,) # Check derivatives assert dN1du.derivatives == (1, 0) assert dN2du.derivatives == (1,) # Check arguments assert dN1du.arguments() == (vstar_dN1du, u_hat) assert dN1du_action.arguments() == (vstar_dN1du,) assert dN2du.arguments() == (vstar_dN2du, s_hat) assert dN2du_action.arguments() == (vstar_dN2du,) # Check argument slots assert dN1du.argument_slots() == (vstar_dN1du, u_hat) assert dN2du.argument_slots() == (vstar_dN2du, -sin(s) * s_hat) def test_extractions(domain_2d, V1): from ufl.algorithms.analysis import ( extract_arguments, extract_arguments_and_coefficients, extract_base_form_operators, extract_coefficients, extract_constants, ) u = Coefficient(V1) c = Constant(domain_2d) e = ExternalOperator(u, c, function_space=V1) (vstar_e,) = e.arguments() assert extract_coefficients(e) == [u] assert extract_arguments(e) == [vstar_e] assert extract_arguments_and_coefficients(e) == ([vstar_e], [u]) assert extract_constants(e) == [c] assert extract_base_form_operators(e) == [e] F = e * dx assert extract_coefficients(F) == [u] assert extract_arguments(e) == [vstar_e] assert extract_arguments_and_coefficients(e) == ([vstar_e], [u]) assert extract_constants(F) == [c] assert F.base_form_operators() == (e,) u_hat = Argument(V1, 1) e = ExternalOperator(u, function_space=V1, derivatives=(1,), argument_slots=(vstar_e, u_hat)) assert extract_coefficients(e) == [u] assert extract_arguments(e) == [vstar_e, u_hat] assert extract_arguments_and_coefficients(e) == ([vstar_e, u_hat], [u]) assert extract_base_form_operators(e) == [e] F = e * dx assert extract_coefficients(F) == [u] assert extract_arguments(e) == [vstar_e, u_hat] assert extract_arguments_and_coefficients(e) == ([vstar_e, u_hat], [u]) assert F.base_form_operators() == (e,) w = Coefficient(V1) e2 = ExternalOperator(w, e, function_space=V1) (vstar_e2,) = e2.arguments() assert extract_coefficients(e2) == [u, w] assert extract_arguments(e2) == [vstar_e2, u_hat] assert extract_arguments_and_coefficients(e2) == ([vstar_e2, u_hat], [u, w]) assert extract_base_form_operators(e2) == [e, e2] F = e2 * dx assert extract_coefficients(e2) == [u, w] assert extract_arguments(e2) == [vstar_e2, u_hat] assert extract_arguments_and_coefficients(e2) == ([vstar_e2, u_hat], [u, w]) assert F.base_form_operators() == (e, e2) def get_external_operators(form_base): if isinstance(form_base, ExternalOperator): return (form_base,) elif isinstance(form_base, BaseForm): return form_base.base_form_operators() else: raise ValueError("Expecting BaseForm argument!") def test_adjoint_action_jacobian(V1, V2, V3): u = Coefficient(V1) m = Coefficient(V2) # N(u, m; v*) N = ExternalOperator(u, m, function_space=V3) # Arguments for the Gateaux-derivative def u_hat(number): return Argument(V1, number) # V1: degree 1 # dFdu.arguments()[-1] def m_hat(number): return Argument(V2, number) # V2: degree 2 # dFdm.arguments()[-1] def vstar_N(number): return Argument(V3.dual(), number) # V3: degree 3 # Coefficients for the action w = Coefficient(V1) # for u p = Coefficient(V2) # for m v2 = TestFunction(V2) v3 = TestFunction(V3) form_base_expressions = (N * dx, N * v2 * dx, N * v3 * dx) # , N) for F in form_base_expressions: # Get test function v_F = F.arguments() if isinstance(F, Form) else () # If we have a 0-form with an ExternalOperator: e.g. F = N * dx # => F.arguments() = (), because of form composition. # But we still need to make arguments with number 1 (i.e. n_arg = 1) # since at the external operator level, argument numbering is based on # the external operator arguments and not on the outer form arguments. n_arg = len(v_F) if len(v_F) else 1 assert n_arg < 2 # Differentiate dFdu = expand_derivatives(derivative(F, u, u_hat(n_arg))) dFdm = expand_derivatives(derivative(F, m, m_hat(n_arg))) assert dFdu.arguments() == v_F + (u_hat(n_arg),) assert dFdm.arguments() == v_F + (m_hat(n_arg),) assert isinstance(dFdu, Action) # dNdu(u, m; u_hat, v*) dNdu = dFdu.right() # dNdm(u, m; m_hat, v*) dNdm = dFdm.right() assert dNdu.derivatives == (1, 0) assert dNdm.derivatives == (0, 1) assert dNdu.arguments() == (vstar_N(0), u_hat(n_arg)) assert dNdm.arguments() == (vstar_N(0), m_hat(n_arg)) assert dNdu.argument_slots() == dNdu.arguments() assert dNdm.argument_slots() == dNdm.arguments() # Action action_dFdu = action(dFdu, w) action_dFdm = action(dFdm, p) assert action_dFdu.arguments() == v_F + () assert action_dFdm.arguments() == v_F + () # If we have 2 arguments if len(v_F): # Adjoint dFdu_adj = adjoint(dFdu) dFdm_adj = adjoint(dFdm) V = v_F[0].ufl_function_space() assert dFdu_adj.arguments() == (TestFunction(V1), TrialFunction(V)) assert dFdm_adj.arguments() == (TestFunction(V2), TrialFunction(V)) # Action of the adjoint q = Coefficient(V) action_dFdu_adj = action(dFdu_adj, q) action_dFdm_adj = action(dFdm_adj, q) assert action_dFdu_adj.arguments() == (TestFunction(V1),) assert action_dFdm_adj.arguments() == (TestFunction(V2),) def test_multiple_external_operators(V1, V2): u = Coefficient(V1) m = Coefficient(V1) w = Coefficient(V2) v = TestFunction(V1) v_hat = TrialFunction(V1) w_hat = TrialFunction(V2) # N1(u, m; v*) N1 = ExternalOperator(u, m, function_space=V1) # N2(w; v*) N2 = ExternalOperator(w, function_space=V2) # N3(u; v*) N3 = ExternalOperator(u, function_space=V1) # N4(N1, u; v*) N4 = ExternalOperator(N1, u, function_space=V1) # N5(N4(N1, u); v*) N5 = ExternalOperator(N4, u, function_space=V1) # --- F = < N1(u, m; v*), v > + + --- # F = (inner(N1, v) + inner(N2, v) + inner(N3, v)) * dx # dFdu = Action(dFdN1, dN1du) + Action(dFdN3, dN3du) dFdu = expand_derivatives(derivative(F, u)) dFdN1 = inner(v_hat, v) * dx dFdN2 = inner(w_hat, v) * dx dFdN3 = inner(v_hat, v) * dx dN1du = N1._ufl_expr_reconstruct_( u, m, derivatives=(1, 0), argument_slots=N1.arguments() + (v_hat,) ) dN3du = N3._ufl_expr_reconstruct_(u, derivatives=(1,), argument_slots=N3.arguments() + (v_hat,)) assert dFdu == Action(dFdN1, dN1du) + Action(dFdN3, dN3du) # dFdm = Action(dFdN1, dN1dm) dFdm = expand_derivatives(derivative(F, m)) dN1dm = N1._ufl_expr_reconstruct_( u, m, derivatives=(0, 1), argument_slots=N1.arguments() + (v_hat,) ) assert dFdm == Action(dFdN1, dN1dm) # dFdw = Action(dFdN2, dN2dw) dFdw = expand_derivatives(derivative(F, w)) dN2dw = N2._ufl_expr_reconstruct_(w, derivatives=(1,), argument_slots=N2.arguments() + (w_hat,)) assert dFdw == Action(dFdN2, dN2dw) # --- F = < N4(N1(u, m), u; v*), v > --- # F = inner(N4, v) * dx # dFdu = ∂F/∂u + Action(∂F/∂N1, dN1/du) + Action(∂F/∂N4, dN4/du) # = Action(∂F/∂N4, dN4/du), since ∂F/∂u = 0 and ∂F/∂N1 = 0 # # In addition, we have: # dN4/du = ∂N4/∂u + Action(∂N4/∂N1, dN1/du) # # Using the fact that Action is distributive, we have: # # dFdu = Action(∂F/∂N4, ∂N4/∂u) + # Action(∂F/∂N4, Action(∂N4/∂N1, dN1/du)) dFdu = expand_derivatives(derivative(F, u)) dFdN4_partial = inner(v_hat, v) * dx dN4dN1_partial = N4._ufl_expr_reconstruct_( N1, u, derivatives=(1, 0), argument_slots=N4.arguments() + (v_hat,) ) dN4du_partial = N4._ufl_expr_reconstruct_( N1, u, derivatives=(0, 1), argument_slots=N4.arguments() + (v_hat,) ) assert dFdu == Action(dFdN4_partial, Action(dN4dN1_partial, dN1du)) + Action( dFdN4_partial, dN4du_partial ) # dFdm = Action(∂F/∂N4, Action(∂N4/∂N1, dN1/dm)) dFdm = expand_derivatives(derivative(F, m)) assert dFdm == Action(dFdN4_partial, Action(dN4dN1_partial, dN1dm)) # --- F = < N1(u, m; v*), v > + + + < # N4(N1(u, m), u; v*), v > --- # F = (inner(N1, v) + inner(N2, v) + inner(N3, v) + inner(N4, v)) * dx dFdu = expand_derivatives(derivative(F, u)) assert dFdu == Action(dFdN1, dN1du) + Action(dFdN3, dN3du) + Action( dFdN4_partial, Action(dN4dN1_partial, dN1du) ) + Action(dFdN4_partial, dN4du_partial) dFdm = expand_derivatives(derivative(F, m)) assert dFdm == Action(dFdN1, dN1dm) + Action(dFdN4_partial, Action(dN4dN1_partial, dN1dm)) dFdw = expand_derivatives(derivative(F, w)) assert dFdw == Action(dFdN2, dN2dw) # --- F = < N5(N4(N1(u, m), u), u; v*), v > + < N1(u, m; v*), v > + # < u * N5(N4(N1(u, m), u), u; v*), v >--- # F = (inner(N5, v) + inner(N1, v) + inner(u * N5, v)) * dx # dFdu = ∂F/∂u + Action(∂F/∂N1, dN1/du) + Action(∂F/∂N4, dN4/du) + Action(∂F/∂N5, dN5/du) # # where: # - ∂F/∂u = inner(w * N5, v) * dx # - ∂F/∂N1 = inner(w, v) * dx # - ∂F/∂N5 = inner(w, v) * dx + inner(u * w, v) * dx # - ∂F/∂N4 = 0 # - dN5/du = ∂N5/∂u + Action(∂N5/∂N4, dN4/du) # = ∂N5/∂u + Action(∂N5/∂N4, ∂N4/∂u) + Action(∂N5/∂N4, Action(∂N4/∂N1, dN1/du)) # with w = TrialFunction(V1) w = TrialFunction(V1) dFdu_partial = inner(w * N5, v) * dx dFdN1_partial = inner(w, v) * dx dFdN5_partial = (inner(w, v) + inner(u * w, v)) * dx dN5dN4_partial = N5._ufl_expr_reconstruct_( N4, u, derivatives=(1, 0), argument_slots=N4.arguments() + (w,) ) dN5du_partial = N5._ufl_expr_reconstruct_( N4, u, derivatives=(0, 1), argument_slots=N4.arguments() + (w,) ) dN5du = ( Action(dN5dN4_partial, Action(dN4dN1_partial, dN1du)) + Action(dN5dN4_partial, dN4du_partial) + dN5du_partial ) dFdu = expand_derivatives(derivative(F, u)) assert dFdu == dFdu_partial + Action(dFdN1_partial, dN1du) + Action(dFdN5_partial, dN5du) def test_replace(V1): u = Coefficient(V1, count=0) N = ExternalOperator(u, function_space=V1) # dN(u; uhat, v*) dN = expand_derivatives(derivative(N, u)) vstar, uhat = dN.arguments() assert isinstance(vstar, Coargument) # Replace v* by a Form v = TestFunction(V1) F = inner(u, v) * dx G = replace(dN, {vstar: F}) dN_replaced = dN._ufl_expr_reconstruct_(u, argument_slots=(F, uhat)) assert G == dN_replaced # Replace v* by an Action M = Matrix(V1, V1) A = Action(M, u) G = replace(dN, {vstar: A}) dN_replaced = dN._ufl_expr_reconstruct_(u, argument_slots=(A, uhat)) assert G == dN_replaced ufl-2024.2.0/test/test_extract_blocks.py000066400000000000000000000066061470142567200201460ustar00rootroot00000000000000import pytest import ufl import ufl.algorithms from ufl.finiteelement import FiniteElement, MixedElement def epsilon(u): return ufl.sym(ufl.grad(u)) def sigma(u, p): return epsilon(u) - p * ufl.Identity(u.ufl_shape[0]) @pytest.mark.parametrize("rank", [0, 1, 2]) def test_extract_blocks(rank): """Test extractions of blocks from mixed function space.""" cell = ufl.triangle domain = ufl.Mesh(FiniteElement("Lagrange", cell, 1, (2,), ufl.identity_pullback, ufl.H1)) fe_scalar = FiniteElement("Lagrange", cell, 1, (), ufl.identity_pullback, ufl.H1) fe_vector = FiniteElement("Lagrange", cell, 1, (2,), ufl.identity_pullback, ufl.H1) me = MixedElement([fe_vector, fe_scalar]) # # Function spaces W = ufl.FunctionSpace(domain, me) V = ufl.FunctionSpace(domain, fe_vector) Q = ufl.FunctionSpace(domain, fe_scalar) if rank == 0: wh = ufl.Coefficient(W) uh, ph = ufl.split(wh) # Test that functionals return the identity J = ufl.inner(sigma(uh, ph), sigma(uh, ph)) * ufl.dx J0 = ufl.extract_blocks(J, 0) assert len(J0) == 1 assert J == J0[0] elif rank == 1: def rhs(uh, ph, v, q): F_0 = ufl.inner(sigma(uh, ph), epsilon(v)) * ufl.dx(domain=domain) F_1 = ufl.div(uh) * q * ufl.dx return F_0, F_1 wh = ufl.Coefficient(W) uh, ph = ufl.split(wh) v, q = ufl.TestFunctions(W) F = sum(rhs(uh, ph, v, q)) v_ = ufl.TestFunction(V) q_ = ufl.TestFunction(Q) F_sub = rhs(uh, ph, ufl.as_vector([vi for vi in v_]), q_) F_0_ext = ufl.extract_blocks(F, 0) assert F_sub[0].signature() == F_0_ext.signature() F_1_ext = ufl.extract_blocks(F, 1) assert F_sub[1].signature() == F_1_ext.signature() elif rank == 2: def lhs(u, p, v, q): J_00 = ufl.inner(u, v) * ufl.dx(domain=domain) J_01 = ufl.div(v) * p * ufl.dx J_10 = q * ufl.div(u) * ufl.dx J_11 = ufl.inner(ufl.grad(p), ufl.grad(q)) * ufl.dx return J_00, J_01, J_10, J_11 v_ = ufl.TestFunction(V) q_ = ufl.TestFunction(Q) u_ = ufl.TrialFunction(V) p_ = ufl.TrialFunction(Q) J_sub = lhs(ufl.as_vector([ui for ui in u_]), p_, ufl.as_vector([vi for vi in v_]), q_) v, q = ufl.TestFunctions(W) uh, ph = ufl.TrialFunctions(W) J = sum(lhs(uh, ph, v, q)) for i in range(2): for j in range(2): J_ij_ext = ufl.extract_blocks(J, i, j) assert J_sub[2 * i + j].signature() == J_ij_ext.signature() def test_postive_restricted_extract_none(): cell = ufl.triangle d = cell.topological_dimension() domain = ufl.Mesh(FiniteElement("Lagrange", cell, 1, (d,), ufl.identity_pullback, ufl.H1)) el_u = FiniteElement("Lagrange", cell, 2, (d,), ufl.identity_pullback, ufl.H1) el_p = FiniteElement("Lagrange", cell, 1, (), ufl.identity_pullback, ufl.H1) V = ufl.FunctionSpace(domain, el_u) Q = ufl.FunctionSpace(domain, el_p) W = ufl.MixedFunctionSpace(V, Q) u, p = ufl.TrialFunctions(W) v, q = ufl.TestFunctions(W) a = ( ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx + ufl.div(u) * q * ufl.dx + ufl.div(v) * p * ufl.dx ) a += ufl.inner(u("+"), v("+")) * ufl.dS a_blocks = ufl.extract_blocks(a) assert a_blocks[1][1] is None ufl-2024.2.0/test/test_ffcforms.py000077500000000000000000000322661470142567200167500ustar00rootroot00000000000000"""Test FFC forms. Unit tests including all demo forms from FFC 0.5.0. The forms are modified (with comments) to work with the UFL notation which differs from the FFC notation in some places. """ __author__ = "Anders Logg (logg@simula.no) et al." __date__ = "2008-04-09 -- 2008-09-26" __copyright__ = "Copyright (C) 2008 Anders Logg et al." __license__ = "GNU GPL version 3 or any later version" # Examples copied from the FFC demo directory, examples contributed # by Johan Jansson, Kristian Oelgaard, Marie Rognes, and Garth Wells. from ufl import ( Coefficient, Constant, Dx, FacetNormal, FunctionSpace, Mesh, TestFunction, TestFunctions, TrialFunction, TrialFunctions, VectorConstant, avg, curl, div, dot, dS, ds, dx, grad, i, inner, j, jump, lhs, rhs, sqrt, tetrahedron, triangle, ) from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback from ufl.sobolevspace import H1, L2, HCurl, HDiv def testConstant(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) c = Constant(domain) d = VectorConstant(domain) _ = c * dot(grad(v), grad(u)) * dx # FFC notation: L = dot(d, grad(v))*dx _ = inner(d, grad(v)) * dx def testElasticity(): element = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) def eps(v): # FFC notation: return grad(v) + transp(grad(v)) return grad(v) + (grad(v)).T # FFC notation: a = 0.25*dot(eps(v), eps(u))*dx _ = 0.25 * inner(eps(v), eps(u)) * dx def testEnergyNorm(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Coefficient(space) _ = (v * v + dot(grad(v), grad(v))) * dx def testEquation(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) k = 0.1 v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) F = v * (u - u0) * dx + k * dot(grad(v), grad(0.5 * (u0 + u))) * dx _ = lhs(F) _ = rhs(F) def testFunctionOperators(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) # FFC notation: a = sqrt(1/modulus(1/f))*sqrt(g)*dot(grad(v), grad(u))*dx # + v*u*sqrt(f*g)*g*dx _ = sqrt(1 / abs(1 / f)) * sqrt(g) * dot(grad(v), grad(u)) * dx + v * u * sqrt(f * g) * g * dx def testHeat(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u1 = TrialFunction(space) u0 = Coefficient(space) c = Coefficient(space) f = Coefficient(space) k = Constant(domain) _ = v * u1 * dx + k * c * dot(grad(v), grad(u1)) * dx _ = v * u0 * dx + k * v * f * dx def testMass(): element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) _ = v * u * dx def testMixedMixedElement(): P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) MixedElement([[P3, P3], [P3, P3]]) def testMixedPoisson(): q = 1 BDM = FiniteElement("Brezzi-Douglas-Marini", triangle, q, (2,), contravariant_piola, HDiv) DG = FiniteElement("Discontinuous Lagrange", triangle, q - 1, (), identity_pullback, L2) mixed_element = MixedElement([BDM, DG]) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, mixed_element) (tau, w) = TestFunctions(space) (sigma, u) = TrialFunctions(space) f = Coefficient(FunctionSpace(domain, DG)) _ = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx _ = w * f * dx def testNavierStokes(): element = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) # FFC notation: a = v[i]*w[j]*D(u[i], j)*dx _ = v[i] * w[j] * Dx(u[i], j) * dx def testNeumannProblem(): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) # FFC notation: a = dot(grad(v), grad(u))*dx _ = inner(grad(v), grad(u)) * dx # FFC notation: L = dot(v, f)*dx + dot(v, g)*ds _ = inner(v, f) * dx + inner(v, g) * ds def testOptimization(): element = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) _ = dot(grad(v), grad(u)) * dx _ = v * f * dx def testP5tet(): FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) def testP5tri(): FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1) def testPoissonDG(): element = FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) n = FacetNormal(domain) # FFC notation: h = MeshSize(domain), not supported by UFL h = Constant(domain) gN = Coefficient(space) alpha = 4.0 gamma = 8.0 # FFC notation # a = dot(grad(v), grad(u))*dx \ # - dot(avg(grad(v)), jump(u, n))*dS \ # - dot(jump(v, n), avg(grad(u)))*dS \ # + alpha/h('+')*dot(jump(v, n), jump(u, n))*dS \ # - dot(grad(v), mult(u,n))*ds \ # - dot(mult(v,n), grad(u))*ds \ # + gamma/h*v*u*ds a = inner(grad(v), grad(u)) * dx a -= inner(avg(grad(v)), jump(u, n)) * dS a -= inner(jump(v, n), avg(grad(u))) * dS a += alpha / h("+") * dot(jump(v, n), jump(u, n)) * dS a -= inner(grad(v), u * n) * ds a -= inner(u * n, grad(u)) * ds a += gamma / h * v * u * ds _ = v * f * dx + v * gN * ds def testPoisson(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) # Note: inner() also works _ = dot(grad(v), grad(u)) * dx _ = v * f * dx def testPoissonSystem(): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) # FFC notation: a = dot(grad(v), grad(u))*dx _ = inner(grad(v), grad(u)) * dx # FFC notation: L = dot(v, f)*dx _ = inner(v, f) * dx def testProjection(): # Projections are not supported by UFL and have been broken # in FFC for a while. For DOLFIN, the current (global) L^2 # projection can be extended to handle also local projections. P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, P1) _ = TestFunction(space) _ = Coefficient(space) # pi0 = Projection(P0) # pi1 = Projection(P1) # pi2 = Projection(P2) # # a = v*(pi0(f) + pi1(f) + pi2(f))*dx def testQuadratureElement(): element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) # FFC notation: # QE = QuadratureElement(triangle, 3) # sig = VectorQuadratureElement(triangle, 3) QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2) sig = FiniteElement("Quadrature", triangle, 3, (2,), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) C = Coefficient(FunctionSpace(domain, QE)) sig0 = Coefficient(FunctionSpace(domain, sig)) f = Coefficient(space) _ = v.dx(i) * C * u.dx(i) * dx + v.dx(i) * 2 * u0 * u * u0.dx(i) * dx _ = v * f * dx - dot(grad(v), sig0) * dx def testStokes(): # UFLException: Shape mismatch in sum. P2 = FiniteElement("Lagrange", triangle, 2, (2,), identity_pullback, H1) P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) TH = MixedElement([P2, P1]) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) th_space = FunctionSpace(domain, TH) p2_space = FunctionSpace(domain, P2) (v, q) = TestFunctions(th_space) (u, p) = TrialFunctions(th_space) f = Coefficient(p2_space) # FFC notation: # a = (dot(grad(v), grad(u)) - div(v)*p + q*div(u))*dx _ = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx _ = dot(v, f) * dx def testSubDomain(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) _ = f * dx(2) + f * ds(5) def testSubDomains(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) a = v * u * dx(0) + 10.0 * v * u * dx(1) + v * u * ds(0) + 2.0 * v * u * ds(1) a += v("+") * u("+") * dS(0) + 4.3 * v("+") * u("+") * dS(1) def testTensorWeightedPoisson(): # FFC notation: # P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (), identity_pullback, L2) # # v = TestFunction(P1) # u = TrialFunction(P1) # f = Coefficient(P1) # # c00 = Coefficient(P0) # c01 = Coefficient(P0) # c10 = Coefficient(P0) # c11 = Coefficient(P0) # # C = [[c00, c01], [c10, c11]] # # a = dot(grad(v), mult(C, grad(u)))*dx P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) p1_space = FunctionSpace(domain, P1) p0_space = FunctionSpace(domain, P0) v = TestFunction(p1_space) u = TrialFunction(p1_space) C = Coefficient(p0_space) _ = inner(grad(v), C * grad(u)) * dx def testVectorLaplaceGradCurl(): def HodgeLaplaceGradCurl(space, fspace): (tau, v) = TestFunctions(space) (sigma, u) = TrialFunctions(space) f = Coefficient(fspace) # FFC notation: a = (dot(tau, sigma) - dot(grad(tau), u) + dot(v, # grad(sigma)) + dot(curl(v), curl(u)))*dx a = ( inner(tau, sigma) - inner(grad(tau), u) + inner(v, grad(sigma)) + inner(curl(v), curl(u)) ) * dx # FFC notation: L = dot(v, f)*dx L = inner(v, f) * dx return [a, L] shape = tetrahedron order = 1 GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1) # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1) CURL = FiniteElement("N1curl", shape, order, (3,), covariant_piola, HCurl) VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", shape, 1, (3,), identity_pullback, H1)) [a, L] = HodgeLaplaceGradCurl( FunctionSpace(domain, MixedElement([GRAD, CURL])), FunctionSpace(domain, VectorLagrange) ) ufl-2024.2.0/test/test_form.py000077500000000000000000000127141470142567200161020ustar00rootroot00000000000000import pytest from ufl import ( Coefficient, Cofunction, Form, FormSum, FunctionSpace, Mesh, SpatialCoordinate, TestFunction, TrialFunction, dot, ds, dx, grad, inner, nabla_grad, triangle, ) from ufl.finiteelement import FiniteElement from ufl.form import BaseForm from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def element(): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) return element @pytest.fixture def domain(): cell = triangle return Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) @pytest.fixture def mass(domain): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) return u * v * dx @pytest.fixture def stiffness(domain): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) return inner(grad(u), grad(v)) * dx @pytest.fixture def convection(domain): cell = triangle element = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) return dot(dot(w, nabla_grad(u)), v) * dx @pytest.fixture def load(domain): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) space = FunctionSpace(domain, element) f = Coefficient(space) v = TestFunction(space) return f * v * dx @pytest.fixture def boundary_load(domain): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) space = FunctionSpace(domain, element) f = Coefficient(space) v = TestFunction(space) return f * v * ds def test_form_arguments(mass, stiffness, convection, load): v, u = mass.arguments() (f,) = load.coefficients() assert v.number() == 0 assert u.number() == 1 assert stiffness.arguments() == (v, u) assert load.arguments() == (v,) assert (v * dx).arguments() == (v,) assert (v * dx + v * ds).arguments() == (v,) assert (v * dx + f * v * ds).arguments() == (v,) assert (u * v * dx(1) + v * u * dx(2)).arguments() == (v, u) assert ((f * v) * u * dx + (u * 3) * (v / 2) * dx(2)).arguments() == (v, u) def test_form_coefficients(element, domain): space = FunctionSpace(domain, element) v = TestFunction(space) f = Coefficient(space) g = Coefficient(space) assert (g * dx).coefficients() == (g,) assert (g * dx + g * ds).coefficients() == (g,) assert (g * dx + f * ds).coefficients() == (f, g) assert (g * dx(1) + f * dx(2)).coefficients() == (f, g) assert (g * v * dx + f * v * dx(2)).coefficients() == (f, g) def test_form_domains(): cell = triangle element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) V = FunctionSpace(domain, element) v = TestFunction(V) f = Coefficient(V) x = SpatialCoordinate(domain)[0] assert (x * dx).ufl_domains() == (domain,) assert (v * dx).ufl_domains() == (domain,) assert (f * dx).ufl_domains() == (domain,) assert (x * v * f * dx).ufl_domains() == (domain,) assert (1 * dx(domain)).ufl_domains() == (domain,) def test_form_empty(mass): assert not mass.empty() assert Form([]).empty() def test_form_integrals(mass, boundary_load): assert isinstance(mass.integrals(), tuple) assert len(mass.integrals()) == 1 assert mass.integrals()[0].integral_type() == "cell" assert mass.integrals_by_type("cell") == mass.integrals() assert mass.integrals_by_type("exterior_facet") == () assert isinstance(boundary_load.integrals_by_type("cell"), tuple) assert len(boundary_load.integrals_by_type("cell")) == 0 assert len(boundary_load.integrals_by_type("exterior_facet")) == 1 def test_form_call(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) V = FunctionSpace(domain, element) v = TestFunction(V) u = TrialFunction(V) f = Coefficient(V) g = Coefficient(V) a = g * inner(grad(v), grad(u)) * dx M = a(f, f, coefficients={g: 1}) assert M == grad(f) ** 2 * dx import sys if sys.version_info.major >= 3 and sys.version_info.minor >= 5: a = u * v * dx M = eval("(a @ f) @ g") assert M == g * f * dx def test_formsum(mass): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) V = FunctionSpace(domain, element) v = Cofunction(V.dual()) assert v + mass assert mass + v assert isinstance((mass + v), FormSum) assert len((mass + v + v).components()) == 3 # Variational forms are summed appropriately assert len((mass + v + mass).components()) == 2 assert v - mass assert mass - v assert isinstance((mass + v), FormSum) assert -v assert isinstance(-v, BaseForm) assert (-v).weights()[0] == -1 assert 2 * v assert isinstance(2 * v, BaseForm) assert (2 * v).weights()[0] == 2 ufl-2024.2.0/test/test_illegal.py000077500000000000000000000034361470142567200165510ustar00rootroot00000000000000import pytest from ufl import Argument, Coefficient, FunctionSpace, Mesh, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # TODO: Add more illegal expressions to check! @pytest.fixture def selement(): return FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) @pytest.fixture def velement(): return FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) @pytest.fixture def domain(): return Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) @pytest.fixture def sspace(domain, selement): return FunctionSpace(domain, selement) @pytest.fixture def vspace(domain, velement): return FunctionSpace(domain, velement) @pytest.fixture def a(sspace): return Argument(sspace, 2) @pytest.fixture def b(sspace): return Argument(sspace, 3) @pytest.fixture def v(vspace): return Argument(vspace, 4) @pytest.fixture def u(vspace): return Argument(vspace, 5) @pytest.fixture def f(sspace): return Coefficient(sspace) @pytest.fixture def g(sspace): return Coefficient(sspace) @pytest.fixture def vf(vspace): return Coefficient(vspace) @pytest.fixture def vg(vspace): return Coefficient(vspace) def test_mul_v_u(v, u): with pytest.raises(BaseException): v * u def test_mul_vf_u(vf, u): with pytest.raises(BaseException): vf * u def test_mul_vf_vg(vf, vg): with pytest.raises(BaseException): vf * vg def test_add_a_v(a, v): with pytest.raises(BaseException): a + v def test_add_vf_b(vf, b): with pytest.raises(BaseException): vf + b def test_add_vectorexpr_b(vg, v, u, vf, b): tmp = vg + v + u + vf with pytest.raises(BaseException): tmp + b ufl-2024.2.0/test/test_indexing.py000077500000000000000000000033721470142567200167440ustar00rootroot00000000000000import pytest from ufl import Index, Mesh, SpatialCoordinate, outer, triangle from ufl.classes import FixedIndex, Indexed, MultiIndex, Outer, Zero from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def domain(): return Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) @pytest.fixture def x1(domain): x = SpatialCoordinate(domain) return x @pytest.fixture def x2(domain): x = SpatialCoordinate(domain) return outer(x, x) @pytest.fixture def x3(domain): x = SpatialCoordinate(domain) return outer(outer(x, x), x) def test_annotated_literals(): z = Zero(()) assert z.ufl_shape == () assert z.ufl_free_indices == () assert z.ufl_index_dimensions == () z = Zero((3,)) assert z.ufl_shape == (3,) assert z.ufl_free_indices == () assert z.ufl_index_dimensions == () i = Index(count=2) j = Index(count=4) z = Zero((), (j, i), {i: 3, j: 5}) assert z.ufl_shape == () assert z.ufl_free_indices == (2, 4) assert z.ufl_index_dimensions == (3, 5) def test_fixed_indexing_of_expression(x1, x2, x3): x0 = x1[0] x00 = x2[0, 0] x000 = x3[0, 0, 0] assert isinstance(x0, Indexed) assert isinstance(x00, Indexed) assert isinstance(x000, Indexed) assert isinstance(x0.ufl_operands[0], SpatialCoordinate) assert isinstance(x00.ufl_operands[0], Outer) assert isinstance(x000.ufl_operands[0], Outer) assert isinstance(x0.ufl_operands[1], MultiIndex) assert isinstance(x00.ufl_operands[1], MultiIndex) assert isinstance(x000.ufl_operands[1], MultiIndex) mi = x000.ufl_operands[1] assert len(mi) == 3 assert mi.indices() == (FixedIndex(0),) * 3 ufl-2024.2.0/test/test_indices.py000077500000000000000000000241071470142567200165540ustar00rootroot00000000000000import pytest import ufl.algorithms import ufl.classes from ufl import ( Argument, Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, as_matrix, as_tensor, as_vector, cos, dx, exp, i, indices, interval, j, k, l, outer, sin, triangle, ) from ufl.classes import IndexSum from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 # TODO: add more expressions to test as many possible combinations of # index notation as feasible... def test_vector_indices(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Argument(space, 2) f = Coefficient(space) u[i] * f[i] * dx u[j] * f[j] * dx def test_tensor_indices(self): element = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Argument(space, 2) f = Coefficient(space) u[i, j] * f[i, j] * dx u[j, i] * f[i, j] * dx u[j, i] * f[j, i] * dx with pytest.raises(BaseException): (u[i, i] + f[j, i]) * dx def test_indexed_sum1(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Argument(space, 2) f = Coefficient(space) a = u[i] + f[i] with pytest.raises(BaseException): a * dx def test_indexed_sum2(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Argument(space, 2) u = Argument(space, 3) f = Coefficient(space) a = u[j] + f[j] + v[j] + 2 * v[j] + exp(u[i] * u[i]) / 2 * f[j] with pytest.raises(BaseException): a * dx def test_indexed_sum3(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) u = Argument(space, 2) f = Coefficient(space) with pytest.raises(BaseException): u[i] + f[j] def test_indexed_function1(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Argument(space, 2) u = Argument(space, 3) f = Coefficient(space) aarg = (u[i] + f[i]) * v[i] exp(aarg) * dx def test_indexed_function2(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Argument(space, 2) u = Argument(space, 3) f = Coefficient(space) bfun = cos(f[0]) left = u[i] + f[i] right = v[i] * bfun assert len(left.ufl_free_indices) == 1 assert left.ufl_free_indices[0] == i.count() assert len(right.ufl_free_indices) == 1 assert right.ufl_free_indices[0] == i.count() left * right * dx def test_indexed_function3(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) Argument(space, 2) u = Argument(space, 3) f = Coefficient(space) with pytest.raises(BaseException): sin(u[i] + f[i]) * dx def test_vector_from_indices(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) # legal vv = as_vector(u[i], i) uu = as_vector(v[j], j) w = v + u ww = vv + uu assert len(vv.ufl_shape) == 1 assert len(uu.ufl_shape) == 1 assert len(w.ufl_shape) == 1 assert len(ww.ufl_shape) == 1 def test_matrix_from_indices(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) A = as_matrix(u[i] * v[j], (i, j)) B = as_matrix(v[k] * v[k] * u[i] * v[j], (j, i)) C = A + A C = B + B D = A + B assert len(A.ufl_shape) == 2 assert len(B.ufl_shape) == 2 assert len(C.ufl_shape) == 2 assert len(D.ufl_shape) == 2 def test_vector_from_list(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) # create vector from list vv = as_vector([u[0], v[0]]) ww = vv + vv assert len(vv.ufl_shape) == 1 assert len(ww.ufl_shape) == 1 def test_matrix_from_list(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) # create matrix from list A = as_matrix([[u[0], u[1]], [v[0], v[1]]]) # create matrix from indices B = as_matrix((v[k] * v[k]) * u[i] * v[j], (j, i)) # Test addition C = A + A C = B + B D = A + B assert len(A.ufl_shape) == 2 assert len(B.ufl_shape) == 2 assert len(C.ufl_shape) == 2 assert len(D.ufl_shape) == 2 def test_tensor(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) # define the components of a fourth order tensor Cijkl = u[i] * v[j] * f[k] * g[l] assert len(Cijkl.ufl_shape) == 0 assert set(Cijkl.ufl_free_indices) == {i.count(), j.count(), k.count(), l.count()} # make it a tensor C = as_tensor(Cijkl, (i, j, k, l)) assert len(C.ufl_shape) == 4 self.assertSameIndices(C, ()) # get sub-matrix A = C[:, :, 0, 0] assert len(A.ufl_shape) == 2 self.assertSameIndices(A, ()) A = C[:, :, i, j] assert len(A.ufl_shape) == 2 assert set(A.ufl_free_indices) == {i.count(), j.count()} # legal? vv = as_vector([u[i], v[i]]) f[i] * vv # this is well defined: ww = sum_i # illegal with pytest.raises(BaseException): as_vector([u[i], v[j]]) # illegal with pytest.raises(BaseException): as_matrix([[u[0], u[1]], [v[0]]]) def test_indexed(self): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) Coefficient(space) i, j, k, l = indices(4) # noqa: E741 a = v[i] self.assertSameIndices(a, (i,)) a = outer(v, u)[i, j] self.assertSameIndices(a, (i, j)) a = outer(v, u)[i, i] self.assertSameIndices(a, ()) self.assertIsInstance(a, IndexSum) def test_spatial_derivative(self): cell = triangle element = FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) i, j, k, l = indices(4) # noqa: E741 d = 2 a = v[i].dx(i) self.assertSameIndices(a, ()) self.assertIsInstance(a, IndexSum) assert a.ufl_shape == () a = v[i].dx(j) self.assertSameIndices(a, (i, j)) self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == () a = (v[i] * u[j]).dx(i, j) self.assertSameIndices(a, ()) self.assertIsInstance(a, IndexSum) assert a.ufl_shape == () a = v.dx(i, j) # self.assertSameIndices(a, (i,j)) assert set(a.ufl_free_indices) == {j.count(), i.count()} self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == (d,) a = v[i].dx(0) self.assertSameIndices(a, (i,)) self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == () a = (v[i] * u[j]).dx(0, 1) assert set(a.ufl_free_indices) == {i.count(), j.count()} self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == () a = v.dx(i)[i] self.assertSameIndices(a, ()) self.assertIsInstance(a, IndexSum) assert a.ufl_shape == () def test_renumbering(self): """Test that kernels with common integral data, but different index numbering, are correctly renumbered.""" cell = interval mesh = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) V = FunctionSpace(mesh, FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) v = TestFunction(V) u = TrialFunction(V) i = indices(1) a0 = u[i].dx(0) * v[i].dx(0) * ufl.dx((1)) a1 = ( u[i].dx(0) * v[i].dx(0) * ufl.dx( ( 2, 3, ) ) ) form_data = ufl.algorithms.compute_form_data( a0 + a1, do_apply_function_pullbacks=True, do_apply_integral_scaling=True, do_apply_geometry_lowering=True, preserve_geometry_types=(ufl.classes.Jacobian,), do_apply_restrictions=True, do_append_everywhere_integrals=False, ) assert len(form_data.integral_data) == 1 ufl-2024.2.0/test/test_interpolate.py000066400000000000000000000120241470142567200174540ustar00rootroot00000000000000"""Test Interpolate object.""" __authors__ = "Nacime Bouziani" __date__ = "2021-11-19" import pytest from ufl import ( Action, Adjoint, Argument, Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, action, adjoint, derivative, dx, grad, inner, replace, triangle, ) from ufl.algorithms.ad import expand_derivatives from ufl.algorithms.analysis import ( extract_arguments, extract_arguments_and_coefficients, extract_base_form_operators, extract_coefficients, ) from ufl.algorithms.expand_indices import expand_indices from ufl.core.interpolate import Interpolate from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture def domain_2d(): return Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) @pytest.fixture def V1(domain_2d): f1 = FiniteElement("CG", triangle, 1, (), identity_pullback, H1) return FunctionSpace(domain_2d, f1) @pytest.fixture def V2(domain_2d): f1 = FiniteElement("CG", triangle, 2, (), identity_pullback, H1) return FunctionSpace(domain_2d, f1) def test_symbolic(V1, V2): # Set dual of V2 V2_dual = V2.dual() u = Coefficient(V1) vstar = Argument(V2_dual, 0) Iu = Interpolate(u, vstar) assert Iu == Interpolate(u, V2) assert Iu.ufl_function_space() == V2 assert Iu.argument_slots() == (vstar, u) assert Iu.arguments() == (vstar,) assert Iu.ufl_operands == (u,) def test_action_adjoint(V1, V2): # Set dual of V2 V2_dual = V2.dual() vstar = Argument(V2_dual, 0) u = Coefficient(V1) Iu = Interpolate(u, vstar) v1 = TrialFunction(V1) Iv = Interpolate(v1, vstar) assert Iv.argument_slots() == (vstar, v1) assert Iv.arguments() == (vstar, v1) # -- Action -- # v = TestFunction(V1) v2 = TrialFunction(V2) F = v2 * v * dx assert action(Iv, u) == Action(Iv, u) assert action(F, Iv) == Action(F, Iv) assert action(F, Iu) == Iu * v * dx # -- Adjoint -- # adjoint(Iv) == Adjoint(Iv) def test_differentiation(V1, V2): u = Coefficient(V1) v = TestFunction(V1) # Define Interpolate Iu = Interpolate(u, V2) # -- Differentiate: Interpolate(u, V2) -- # uhat = TrialFunction(V1) dIu = expand_derivatives(derivative(Iu, u, uhat)) # dInterpolate(u, v*)/du[uhat] <==> Interpolate(uhat, v*) assert dIu == Interpolate(uhat, V2) # -- Differentiate: Interpolate(u**2, V2) -- # g = u**2 Ig = Interpolate(g, V2) dIg = expand_derivatives(derivative(Ig, u, uhat)) assert dIg == Interpolate(2 * uhat * u, V2) # -- Differentiate: I(u, V2) * v * dx -- # F = Iu * v * dx Ihat = TrialFunction(Iu.ufl_function_space()) dFdu = expand_derivatives(derivative(F, u, uhat)) # Compute dFdu = ∂F/∂u + Action(dFdIu, dIu/du) # = Action(dFdIu, Iu(uhat, v*)) dFdIu = expand_derivatives(derivative(F, Iu, Ihat)) assert dFdIu == Ihat * v * dx assert dFdu == Action(dFdIu, dIu) # -- Differentiate: u * I(u, V2) * v * dx -- # F = u * Iu * v * dx dFdu = expand_derivatives(derivative(F, u, uhat)) # Compute dFdu = ∂F/∂u + Action(dFdIu, dIu/du) # = ∂F/∂u + Action(dFdIu, Iu(uhat, v*)) dFdu_partial = uhat * Iu * v * dx dFdIu = Ihat * u * v * dx assert dFdu == dFdu_partial + Action(dFdIu, dIu) # -- Differentiate (wrt Iu): + - f = Coefficient(V1) F = inner(Iu, v) * dx + inner(grad(Iu), grad(v)) * dx - inner(f, v) * dx dFdIu = expand_derivatives(derivative(F, Iu, Ihat)) # BaseFormOperators are treated as coefficients when a form is differentiated wrt them. # -> dFdIu <=> dFdw w = Coefficient(V2) F = replace(F, {Iu: w}) dFdw = expand_derivatives(derivative(F, w, Ihat)) # Need to expand indices to be able to match equal (different MultiIndex used for both). assert expand_indices(dFdIu) == expand_indices(dFdw) def test_extract_base_form_operators(V1, V2): u = Coefficient(V1) uhat = TrialFunction(V1) vstar = Argument(V2.dual(), 0) # -- Interpolate(u, V2) -- # Iu = Interpolate(u, V2) assert extract_arguments(Iu) == [vstar] assert extract_arguments_and_coefficients(Iu) == ([vstar], [u]) F = Iu * dx # Form composition: Iu * dx <=> Action(v * dx, Iu(u; v*)) assert extract_arguments(F) == [] assert extract_arguments_and_coefficients(F) == ([], [u]) for e in [Iu, F]: assert extract_coefficients(e) == [u] assert extract_base_form_operators(e) == [Iu] # -- Interpolate(u, V2) -- # Iv = Interpolate(uhat, V2) assert extract_arguments(Iv) == [vstar, uhat] assert extract_arguments_and_coefficients(Iv) == ([vstar, uhat], []) assert extract_coefficients(Iv) == [] assert extract_base_form_operators(Iv) == [Iv] # -- Action(v * v2 * dx, Iv) -- # v2 = TrialFunction(V2) v = TestFunction(V1) F = Action(v * v2 * dx, Iv) assert extract_arguments(F) == [v, uhat] ufl-2024.2.0/test/test_lhs_rhs.py000077500000000000000000000045421470142567200166010ustar00rootroot00000000000000__authors__ = "Marie E. Rognes" # First added: 2011-11-09 # Last changed: 2011-11-09 from ufl import ( Argument, Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, action, derivative, dS, ds, dx, exp, interval, system, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_lhs_rhs_simple(): V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = TestFunction(space) u = TrialFunction(space) w = Argument(space, 2) # This was 0, not sure why f = Coefficient(space) F0 = f * u * v * w * dx a, L = system(F0) assert len(a.integrals()) == 0 assert len(L.integrals()) == 0 F1 = derivative(F0, f) a, L = system(F1) assert len(a.integrals()) == 0 assert len(L.integrals()) == 0 F2 = action(F0, f) a, L = system(F2) assert len(a.integrals()) == 1 assert len(L.integrals()) == 0 F3 = action(F2, f) a, L = system(F3) assert len(L.integrals()) == 1 def test_lhs_rhs_derivatives(): V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) F0 = exp(f) * u * v * dx + v * dx + f * v * ds + exp(f)("+") * v * dS a, L = system(F0) assert len(a.integrals()) == 1 assert len(L.integrals()) == 3 derivative(F0, f) a, L = system(F0) def test_lhs_rhs_slightly_obscure(): V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) space = FunctionSpace(domain, V) u = TrialFunction(space) w = Argument(space, 2) f = Constant(domain) # FIXME: # ufl.algorithsm.formtransformations.compute_form_with_arity # is not perfect, e.g. try # F = f*u*w*dx + f*w*dx F = f * u * w * dx a, L = system(F) assert len(a.integrals()) == 1 assert len(L.integrals()) == 0 F = f * w * dx a, L = system(F) assert len(L.integrals()) == 1 ufl-2024.2.0/test/test_literals.py000077500000000000000000000077571470142567200167710ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2011-04-14" import numpy as np from ufl import PermutationSymbol, as_matrix, as_vector, indices, product from ufl.classes import Indexed from ufl.constantvalue import ComplexValue, FloatValue, IntValue, Zero, as_ufl def test_zero(self): z1 = Zero(()) assert z1 == z1 assert int(z1) == 0 assert float(z1) == 0.0 assert complex(z1) == 0.0 + 0.0j self.assertNotEqual(z1, 1.0) self.assertFalse(z1) # If zero() == 0 is to be allowed, it must not have the same hash or # it will collide with 0 as key in dicts... self.assertNotEqual(hash(z1), hash(0.0)) self.assertNotEqual(hash(z1), hash(0)) def test_float(self): f1 = as_ufl(1) f2 = as_ufl(1.0) f3 = FloatValue(1) f4 = FloatValue(1.0) f5 = 3 - FloatValue(1) - 1 f6 = 3 * FloatValue(2) / 6 f7 = as_ufl(np.ones((1,), dtype="d")[0]) assert f1 == f1 self.assertNotEqual(f1, f2) # IntValue vs FloatValue, == compares representations! assert f2 == f3 assert f2 == f4 assert f2 == f5 assert f2 == f6 assert f2 == f7 def test_int(self): f1 = as_ufl(1) f2 = as_ufl(1.0) f3 = IntValue(1) f4 = IntValue(1.0) f5 = 3 - IntValue(1) - 1 f6 = 3 * IntValue(2) / 6 f7 = as_ufl(np.ones((1,), dtype="int")[0]) assert f1 == f1 self.assertNotEqual(f1, f2) # IntValue vs FloatValue, == compares representations! assert f1 == f3 assert f1 == f4 assert f1 == f5 assert f2 == f6 # Division produces a FloatValue assert f1 == f7 def test_complex(self): f1 = as_ufl(1 + 1j) f2 = as_ufl(1) f3 = as_ufl(1j) f4 = ComplexValue(1 + 1j) f5 = ComplexValue(1.0 + 1.0j) f6 = as_ufl(1.0) f7 = as_ufl(1.0j) f8 = as_ufl(np.array([1 + 1j], dtype="complex")[0]) assert f1 == f1 assert f1 == f4 assert f1 == f5 # ComplexValue uses floats assert f1 == f2 + f3 # Type promotion of IntValue to ComplexValue with arithmetic assert f4 == f2 + f3 assert f5 == f2 + f3 assert f4 == f5 assert f6 + f7 == f2 + f3 assert f4 == f8 def test_scalar_sums(self): n = 10 s = [as_ufl(i) for i in range(n)] for i in range(n): self.assertNotEqual(s[i], i + 1) for i in range(n): assert s[i] == i for i in range(n): assert 0 + s[i] == i for i in range(n): assert s[i] + 0 == i for i in range(n): assert 0 + s[i] + 0 == i for i in range(n): assert 1 + s[i] - 1 == i assert s[1] + s[1] == 2 assert s[1] + s[2] == 3 assert s[1] + s[2] + s[3] == s[6] assert s[5] - s[2] == 3 assert 1 * s[5] == 5 assert 2 * s[5] == 10 assert s[6] / 3 == 2 def test_identity(self): pass # FIXME def test_permutation_symbol_3(self): e = PermutationSymbol(3) assert e.ufl_shape == (3, 3, 3) assert eval(repr(e)) == e for i in range(3): for j in range(3): for k in range(3): value = (j - i) * (k - i) * (k - j) / 2 self.assertEqual(e[i, j, k], value) i, j, k = indices(3) self.assertIsInstance(e[i, j, k], Indexed) x = (0, 0, 0) self.assertEqual((e[i, j, k] * e[i, j, k])(x), 6) def test_permutation_symbol_n(self): for n in range(2, 5): # tested with upper limit 7, but evaluation is a bit slow then e = PermutationSymbol(n) assert e.ufl_shape == (n,) * n assert eval(repr(e)) == e ii = indices(n) x = (0,) * n nfac = product(m for m in range(1, n + 1)) assert (e[ii] * e[ii])(x) == nfac def test_unit_dyads(self): from ufl.tensors import unit_matrices, unit_vectors ei, ej = unit_vectors(2) self.assertEqual(as_vector((1, 0)), ei) self.assertEqual(as_vector((0, 1)), ej) eii, eij, eji, ejj = unit_matrices(2) self.assertEqual(as_matrix(((1, 0), (0, 0))), eii) self.assertEqual(as_matrix(((0, 1), (0, 0))), eij) self.assertEqual(as_matrix(((0, 0), (1, 0))), eji) self.assertEqual(as_matrix(((0, 0), (0, 1))), ejj) ufl-2024.2.0/test/test_measures.py000077500000000000000000000150031470142567200167550ustar00rootroot00000000000000"""Tests of the various ways Measure objects can be created and used.""" from mockobjects import MockMesh, MockMeshFunction from ufl import Cell, Coefficient, FunctionSpace, Measure, Mesh, as_ufl, dC, dI, dO, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_construct_forms_from_default_measures(): # Create defaults: dx = Measure("dx") # dE = Measure("dE") ds = Measure("ds") dS = Measure("dS") dP = Measure("dP") # dV = Measure("dV") dc = Measure("dc") # dC = Measure("dC") # dO = Measure("dO") # dI = Measure("dI") ds_b = Measure("ds_b") ds_t = Measure("ds_t") ds_v = Measure("ds_v") dS_h = Measure("dS_h") dS_v = Measure("dS_v") # Check that names are mapped properly assert dx.integral_type() == "cell" # assert dE.integral_type() == "macro_cell" assert ds.integral_type() == "exterior_facet" assert dS.integral_type() == "interior_facet" assert dP.integral_type() == "vertex" # TODO: Change dP to dV: # assert dP.integral_type() == "point" # assert dV.integral_type() == "vertex" assert dc.integral_type() == "custom" assert dC.integral_type() == "cutcell" assert dO.integral_type() == "overlap" assert dI.integral_type() == "interface" # TODO: Remove firedrake hacks: assert ds_b.integral_type() == "exterior_facet_bottom" assert ds_t.integral_type() == "exterior_facet_top" assert ds_v.integral_type() == "exterior_facet_vert" assert dS_h.integral_type() == "interior_facet_horiz" assert dS_v.integral_type() == "interior_facet_vert" # Check that defaults are set properly assert dx.ufl_domain() is None assert dx.metadata() == {} # Check that we can create a basic form with default measure one = as_ufl(1) one * dx(Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1))) def test_foo(): # Define a manifold domain, allows checking gdim/tdim mixup errors gdim = 3 tdim = 2 cell = Cell("triangle") mymesh = MockMesh(9) mydomain = Mesh( FiniteElement("Lagrange", cell, 1, (gdim,), identity_pullback, H1), ufl_id=9, cargo=mymesh ) assert cell.topological_dimension() == tdim assert cell.cellname() == "triangle" assert mydomain.topological_dimension() == tdim assert mydomain.geometric_dimension() == gdim assert mydomain.ufl_cell() == cell assert mydomain.ufl_id() == 9 assert mydomain.ufl_cargo() == mymesh # Define a coefficient for use in tests below V = FunctionSpace(mydomain, FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)) f = Coefficient(V) # Test definition of a custom measure with explicit parameters metadata = {"opt": True} mydx = Measure("dx", domain=mydomain, subdomain_id=3, metadata=metadata) assert mydx.ufl_domain().ufl_id() == mydomain.ufl_id() assert mydx.metadata() == metadata M = f * mydx # Compatibility: dx = Measure("dx") # domain=None, # subdomain_id="everywhere", # metadata=None) assert dx.ufl_domain() is None assert dx.subdomain_id() == "everywhere" # Set subdomain_id to "everywhere", still no domain set dxe = dx() assert dxe.ufl_domain() is None assert dxe.subdomain_id() == "everywhere" # Set subdomain_id to 5, still no domain set dx5 = dx(5) assert dx5.ufl_domain() is None assert dx5.subdomain_id() == 5 # Check that original dx is untouched assert dx.ufl_domain() is None assert dx.subdomain_id() == "everywhere" # Set subdomain_id to (2,3), still no domain set dx23 = dx((2, 3)) assert dx23.ufl_domain() is None assert dx23.subdomain_id(), 2 == 3 # Map metadata to metadata, ffc interprets as before dxm = dx(metadata={"dummy": 123}) # assert dxm.metadata() == {"dummy":123} assert dxm.metadata() == {"dummy": 123} # Deprecated, TODO: Remove assert dxm.ufl_domain() is None assert dxm.subdomain_id() == "everywhere" # dxm = dx(metadata={"dummy":123}) # assert dxm.metadata() == {"dummy":123} dxm = dx(metadata={"dummy": 123}) assert dxm.metadata() == {"dummy": 123} assert dxm.ufl_domain() is None assert dxm.subdomain_id() == "everywhere" # Mock some dolfin data structures dx = Measure("dx") ds = Measure("ds") dS = Measure("dS") mesh = MockMesh(8) cell_domains = MockMeshFunction(1, mesh) exterior_facet_domains = MockMeshFunction(2, mesh) interior_facet_domains = MockMeshFunction(3, mesh) dxd = dx(subdomain_data=cell_domains) dsd = ds(subdomain_data=exterior_facet_domains) dSd = dS(subdomain_data=interior_facet_domains) # Current behaviour: no domain created, measure domain data is a single # object not a full dict assert dxd.ufl_domain() is None assert dsd.ufl_domain() is None assert dSd.ufl_domain() is None assert dxd.subdomain_data() is cell_domains assert dsd.subdomain_data() is exterior_facet_domains assert dSd.subdomain_data() is interior_facet_domains # Create some forms with these measures (used in checks below): Mx = f * dxd Ms = f**2 * dsd MS = f("+") * dSd M = f * dxd + f**2 * dsd + f("+") * dSd # Test extracting domain data from a form for each measure: (domain,) = Mx.ufl_domains() assert domain.ufl_id() == mydomain.ufl_id() assert domain.ufl_cargo() == mymesh assert len(Mx.subdomain_data()[mydomain]["cell"]) == 1 assert Mx.subdomain_data()[mydomain]["cell"][0] == cell_domains (domain,) = Ms.ufl_domains() assert domain.ufl_cargo() == mymesh assert len(Ms.subdomain_data()[mydomain]["exterior_facet"]) == 1 assert Ms.subdomain_data()[mydomain]["exterior_facet"][0] == exterior_facet_domains (domain,) = MS.ufl_domains() assert domain.ufl_cargo() == mymesh assert len(MS.subdomain_data()[mydomain]["interior_facet"]) == 1 assert MS.subdomain_data()[mydomain]["interior_facet"][0] == interior_facet_domains # Test joining of these domains in a single form (domain,) = M.ufl_domains() assert domain.ufl_cargo() == mymesh assert len(M.subdomain_data()[mydomain]["cell"]) == 1 assert M.subdomain_data()[mydomain]["cell"][0] == cell_domains assert len(M.subdomain_data()[mydomain]["exterior_facet"]) == 1 assert M.subdomain_data()[mydomain]["exterior_facet"][0] == exterior_facet_domains assert len(M.subdomain_data()[mydomain]["interior_facet"]) == 1 assert M.subdomain_data()[mydomain]["interior_facet"][0] == interior_facet_domains ufl-2024.2.0/test/test_mixed_function_space.py000066400000000000000000000061321470142567200213170ustar00rootroot00000000000000__authors__ = "Cecile Daversin Catty" __date__ = "2019-03-26 -- 2019-03-26" from ufl import ( FunctionSpace, Measure, Mesh, MixedFunctionSpace, TestFunctions, TrialFunctions, interval, tetrahedron, triangle, ) from ufl.algorithms.formsplitter import extract_blocks from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_mixed_functionspace(self): # Domains domain_3d = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) # Finite elements f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1) f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) f_3d = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) # Function spaces V_3d = FunctionSpace(domain_3d, f_3d) V_2d = FunctionSpace(domain_2d, f_2d) V_1d = FunctionSpace(domain_1d, f_1d) # MixedFunctionSpace = V_3d x V_2d x V_1d V = MixedFunctionSpace(V_3d, V_2d, V_1d) # Check sub spaces assert V.num_sub_spaces() == 3 assert V.ufl_sub_space(0) == V_3d assert V.ufl_sub_space(1) == V_2d assert V.ufl_sub_space(2) == V_1d # Arguments from MixedFunctionSpace (u_3d, u_2d, u_1d) = TrialFunctions(V) (v_3d, v_2d, v_1d) = TestFunctions(V) # Measures dx3 = Measure("dx", domain=domain_3d) dx2 = Measure("dx", domain=domain_2d) dx1 = Measure("dx", domain=domain_1d) # Mixed variational form # LHS a_11 = u_1d * v_1d * dx1 a_22 = u_2d * v_2d * dx2 a_33 = u_3d * v_3d * dx3 a_21 = u_2d * v_1d * dx1 a_12 = u_1d * v_2d * dx1 a_32 = u_3d * v_2d * dx2 a_23 = u_2d * v_3d * dx2 a_31 = u_3d * v_1d * dx1 a_13 = u_1d * v_3d * dx1 a = a_11 + a_22 + a_33 + a_21 + a_12 + a_32 + a_23 + a_31 + a_13 # RHS f_1 = v_1d * dx1 f_2 = v_2d * dx2 f_3 = v_3d * dx3 f = f_1 + f_2 + f_3 # Check extract_block algorithm # LHS assert extract_blocks(a, 0, 0) == a_33 assert extract_blocks(a, 0, 1) == a_23 assert extract_blocks(a, 0, 2) == a_13 assert extract_blocks(a, 1, 0) == a_32 assert extract_blocks(a, 1, 1) == a_22 assert extract_blocks(a, 1, 2) == a_12 assert extract_blocks(a, 2, 0) == a_31 assert extract_blocks(a, 2, 1) == a_21 assert extract_blocks(a, 2, 2) == a_11 # RHS assert extract_blocks(f, 0) == f_3 assert extract_blocks(f, 1) == f_2 assert extract_blocks(f, 2) == f_1 # Test dual space method V_dual = V.dual() assert V_dual.num_sub_spaces() == 3 assert V_dual.ufl_sub_space(0) == V_3d.dual() assert V_dual.ufl_sub_space(1) == V_2d.dual() assert V_dual.ufl_sub_space(2) == V_1d.dual() V_dual = V.dual(0, 2) assert V_dual.num_sub_spaces() == 3 assert V_dual.ufl_sub_space(0) == V_3d.dual() assert V_dual.ufl_sub_space(1) == V_2d assert V_dual.ufl_sub_space(2) == V_1d.dual() ufl-2024.2.0/test/test_new_ad.py000077500000000000000000000157171470142567200164020ustar00rootroot00000000000000from ufl import ( CellVolume, Coefficient, Constant, FacetNormal, FunctionSpace, Identity, Mesh, SpatialCoordinate, TestFunction, VectorConstant, as_ufl, cos, derivative, diff, exp, grad, ln, sin, tan, triangle, variable, zero, ) from ufl.algorithms.apply_derivatives import ( GenericDerivativeRuleset, GradRuleset, apply_derivatives, ) from ufl.algorithms.renumbering import renumber_indices from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 # Note: the old tests in test_automatic_differentiation.py are a bit messy # but still cover many things that are not in here yet. # FIXME: Write UNIT tests for all terminal derivatives! # FIXME: Write UNIT tests for operator derivatives! def test_apply_derivatives_doesnt_change_expression_without_derivatives(): cell = triangle d = 2 V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2) V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) v0_space = FunctionSpace(domain, V0) v1_space = FunctionSpace(domain, V1) # Literals z = zero((3, 2)) one = as_ufl(1) two = as_ufl(2.0) ident = Identity(d) literals = [z, one, two, ident] # Geometry x = SpatialCoordinate(domain) n = FacetNormal(domain) volume = CellVolume(domain) geometry = [x, n, volume] # Arguments v0 = TestFunction(v0_space) v1 = TestFunction(v1_space) arguments = [v0, v1] # Coefficients f0 = Coefficient(v0_space) f1 = Coefficient(v1_space) coefficients = [f0, f1] # Expressions e0 = f0 + f1 e1 = v0 * (f1 / 3 - f0**2) e2 = exp(sin(cos(tan(ln(x[0]))))) expressions = [e0, e1, e2] # Check that all are unchanged for expr in literals + geometry + arguments + coefficients + expressions: # Note the use of "is" here instead of ==, this property # is important for efficiency and memory usage assert apply_derivatives(expr) is expr def test_literal_derivatives_are_zero(): cell = triangle d = 2 # Literals one = as_ufl(1) two = as_ufl(2.0) ident = Identity(d) literals = [one, two, ident] # Generic ruleset handles literals directly: for lit in literals: for sh in [(), (d,), (d, d + 1)]: assert GenericDerivativeRuleset(sh)(lit) == zero(lit.ufl_shape + sh) # Variables v0 = variable(one) v1 = variable(zero((d,))) v2 = variable(ident) variables = [v0, v1, v2] # Test literals via apply_derivatives and variable ruleset: for lit in literals: for v in variables: assert apply_derivatives(diff(lit, v)) == zero(lit.ufl_shape + v.ufl_shape) V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2) V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) v0_space = FunctionSpace(domain, V0) v1_space = FunctionSpace(domain, V1) u0 = Coefficient(v0_space) u1 = Coefficient(v1_space) v0 = TestFunction(v0_space) v1 = TestFunction(v1_space) args = [(u0, v0), (u1, v1)] # Test literals via apply_derivatives and variable ruleset: for lit in literals: for u, v in args: assert apply_derivatives(derivative(lit, u, v)) == zero(lit.ufl_shape + v.ufl_shape) # Test grad ruleset directly since grad(literal) is invalid: assert GradRuleset(d)(one) == zero((d,)) assert GradRuleset(d)(one) == zero((d,)) def test_grad_ruleset(): cell = triangle d = 2 V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2) V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) W0 = FiniteElement("Discontinuous Lagrange", cell, 0, (2,), identity_pullback, L2) W1 = FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1) W2 = FiniteElement("Lagrange", cell, 2, (d,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) v0_space = FunctionSpace(domain, V0) v1_space = FunctionSpace(domain, V1) v2_space = FunctionSpace(domain, V2) w0_space = FunctionSpace(domain, W0) w1_space = FunctionSpace(domain, W1) w2_space = FunctionSpace(domain, W2) # Literals one = as_ufl(1) two = as_ufl(2.0) ident = Identity(d) # Geometry x = SpatialCoordinate(domain) n = FacetNormal(domain) volume = CellVolume(domain) # Arguments u0 = TestFunction(v0_space) u1 = TestFunction(v1_space) arguments = [u0, u1] # Coefficients r = Constant(domain) vr = VectorConstant(domain) f0 = Coefficient(v0_space) f1 = Coefficient(v1_space) f2 = Coefficient(v2_space) vf0 = Coefficient(w0_space) vf1 = Coefficient(w1_space) vf2 = Coefficient(w2_space) rules = GradRuleset(d) # Literals assert rules(one) == zero((d,)) assert rules(two) == zero((d,)) assert rules(ident) == zero((d, d, d)) # Assumed piecewise constant geometry for g in [n, volume]: assert rules(g) == zero(g.ufl_shape + (d,)) # Non-constant geometry assert rules(x) == ident # Arguments for u in arguments: assert rules(u) == grad(u) # Piecewise constant coefficients (Constant) assert rules(r) == zero((d,)) assert rules(vr) == zero((d, d)) assert rules(grad(r)) == zero((d, d)) assert rules(grad(vr)) == zero((d, d, d)) # Piecewise constant coefficients (DG0) assert rules(f0) == zero((d,)) assert rules(vf0) == zero((d, d)) assert rules(grad(f0)) == zero((d, d)) assert rules(grad(vf0)) == zero((d, d, d)) # Piecewise linear coefficients assert rules(f1) == grad(f1) assert rules(vf1) == grad(vf1) # assert rules(grad(f1)) == zero((d,d)) # TODO: Use degree to make this work # assert rules(grad(vf1)) == zero((d,d,d)) # Piecewise quadratic coefficients assert rules(grad(f2)) == grad(grad(f2)) assert rules(grad(vf2)) == grad(grad(vf2)) # Indexed coefficients assert renumber_indices(apply_derivatives(grad(vf2[0]))) == renumber_indices(grad(vf2)[0, :]) assert renumber_indices(apply_derivatives(grad(vf2[1])[0])) == renumber_indices(grad(vf2)[1, 0]) # Grad of gradually more complex expressions assert apply_derivatives(grad(2 * f0)) == zero((d,)) assert renumber_indices(apply_derivatives(grad(2 * f1))) == renumber_indices(2 * grad(f1)) assert renumber_indices(apply_derivatives(grad(sin(f1)))) == renumber_indices( cos(f1) * grad(f1) ) assert renumber_indices(apply_derivatives(grad(cos(f1)))) == renumber_indices( -sin(f1) * grad(f1) ) def test_variable_ruleset(): pass def test_gateaux_ruleset(): pass ufl-2024.2.0/test/test_pickle.py000077500000000000000000000431041470142567200164030ustar00rootroot00000000000000"""Pickle all the unit test forms from FFC 0.5.0""" __author__ = "Anders Logg (logg@simula.no) et al." __date__ = "2008-04-09 -- 2008-09-26" __copyright__ = "Copyright (C) 2008 Anders Logg et al." __license__ = "GNU GPL version 3 or any later version" # Examples copied from the FFC demo directory, examples contributed # by Johan Jansson, Kristian Oelgaard, Marie Rognes, and Garth Wells. import pickle from ufl import ( Coefficient, Constant, Dx, FacetNormal, FunctionSpace, Identity, Mesh, TestFunction, TestFunctions, TrialFunction, TrialFunctions, VectorConstant, avg, curl, div, dot, dS, ds, dx, grad, i, inner, j, jump, lhs, rhs, sqrt, tetrahedron, triangle, ) from ufl.algorithms import compute_form_data from ufl.finiteelement import FiniteElement, MixedElement from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback from ufl.sobolevspace import H1, L2, HCurl, HDiv p = pickle.HIGHEST_PROTOCOL def testConstant(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) c = Constant(domain) d = VectorConstant(domain) a = c * dot(grad(v), grad(u)) * dx # FFC notation: L = dot(d, grad(v))*dx L = inner(d, grad(v)) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testElasticity(): element = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) def eps(v): # FFC notation: return grad(v) + transp(grad(v)) return grad(v) + (grad(v)).T # FFC notation: a = 0.25*dot(eps(v), eps(u))*dx a = 0.25 * inner(eps(v), eps(u)) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testEnergyNorm(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = Coefficient(space) a = (v * v + dot(grad(v), grad(v))) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testEquation(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) k = 0.1 v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) F = v * (u - u0) * dx + k * dot(grad(v), grad(0.5 * (u0 + u))) * dx a = lhs(F) L = rhs(F) a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testFunctionOperators(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) # FFC notation: a = sqrt(1/modulus(1/f))*sqrt(g)*dot(grad(v), grad(u))*dx # + v*u*sqrt(f*g)*g*dx a = sqrt(1 / abs(1 / f)) * sqrt(g) * dot(grad(v), grad(u)) * dx + v * u * sqrt(f * g) * g * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testHeat(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u1 = TrialFunction(space) u0 = Coefficient(space) c = Coefficient(space) f = Coefficient(space) k = Constant(domain) a = v * u1 * dx + k * c * dot(grad(v), grad(u1)) * dx L = v * u0 * dx + k * v * f * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testMass(): element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) a = v * u * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testMixedMixedElement(): P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) element = MixedElement([[P3, P3], [P3, P3]]) element_pickle = pickle.dumps(element, p) element_restore = pickle.loads(element_pickle) assert element == element_restore def testMixedPoisson(): q = 1 BDM = FiniteElement("Brezzi-Douglas-Marini", triangle, q, (2,), contravariant_piola, HDiv) DG = FiniteElement("Discontinuous Lagrange", triangle, q - 1, (), identity_pullback, L2) mixed_element = MixedElement([BDM, DG]) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) mixed_space = FunctionSpace(domain, mixed_element) dg_space = FunctionSpace(domain, DG) (tau, w) = TestFunctions(mixed_space) (sigma, u) = TrialFunctions(mixed_space) f = Coefficient(dg_space) a = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx L = w * f * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testNavierStokes(): element = FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) w = Coefficient(space) # FFC notation: a = v[i]*w[j]*D(u[i], j)*dx a = v[i] * w[j] * Dx(u[i], j) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testNeumannProblem(): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) g = Coefficient(space) # FFC notation: a = dot(grad(v), grad(u))*dx a = inner(grad(v), grad(u)) * dx # FFC notation: L = dot(v, f)*dx + dot(v, g)*ds L = inner(v, f) * dx + inner(v, g) * ds a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testOptimization(): element = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) a = dot(grad(v), grad(u)) * dx L = v * f * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testP5tet(): element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) element_pickle = pickle.dumps(element, p) element_restore = pickle.loads(element_pickle) assert element == element_restore def testP5tri(): element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1) element_pickle = pickle.dumps(element, p) pickle.loads(element_pickle) def testPoissonDG(): element = FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) n = FacetNormal(domain) # FFC notation: h = MeshSize(domain), not supported by UFL h = Constant(domain) gN = Coefficient(space) alpha = 4.0 gamma = 8.0 # FFC notation # a = dot(grad(v), grad(u))*dx \ # - dot(avg(grad(v)), jump(u, n))*dS \ # - dot(jump(v, n), avg(grad(u)))*dS \ # + alpha/h('+')*dot(jump(v, n), jump(u, n))*dS \ # - dot(grad(v), mult(u,n))*ds \ # - dot(mult(v,n), grad(u))*ds \ # + gamma/h*v*u*ds a = ( inner(grad(v), grad(u)) * dx - inner(avg(grad(v)), jump(u, n)) * dS - inner(jump(v, n), avg(grad(u))) * dS + alpha / h("+") * dot(jump(v, n), jump(u, n)) * dS - inner(grad(v), u * n) * ds - inner(u * n, grad(u)) * ds + gamma / h * v * u * ds ) L = v * f * dx + v * gN * ds a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testPoisson(): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) # Note: inner() also works a = dot(grad(v), grad(u)) * dx L = v * f * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testPoissonSystem(): element = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) f = Coefficient(space) # FFC notation: a = dot(grad(v), grad(u))*dx a = inner(grad(v), grad(u)) * dx # FFC notation: L = dot(v, f)*dx L = inner(v, f) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testQuadratureElement(): element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) # FFC notation: # QE = QuadratureElement(triangle, 3) # sig = VectorQuadratureElement(triangle, 3) QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2) sig = FiniteElement("Quadrature", triangle, 3, (2,), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) qe_space = FunctionSpace(domain, QE) sig_space = FunctionSpace(domain, sig) v = TestFunction(space) u = TrialFunction(space) u0 = Coefficient(space) C = Coefficient(qe_space) sig0 = Coefficient(sig_space) f = Coefficient(space) a = v.dx(i) * C * u.dx(i) * dx + v.dx(i) * 2 * u0 * u * u0.dx(i) * dx L = v * f * dx - dot(grad(v), sig0) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testStokes(): # UFLException: Shape mismatch in sum. P2 = FiniteElement("Lagrange", triangle, 2, (2,), identity_pullback, H1) P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) TH = MixedElement([P2, P1]) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) th_space = FunctionSpace(domain, TH) p2_space = FunctionSpace(domain, P2) (v, q) = TestFunctions(th_space) (u, r) = TrialFunctions(th_space) f = Coefficient(p2_space) # FFC notation: # a = (dot(grad(v), grad(u)) - div(v)*p + q*div(u))*dx a = (inner(grad(v), grad(u)) - div(v) * r + q * div(u)) * dx L = dot(v, f) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testSubDomain(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) M = f * dx(2) + f * ds(5) M_pickle = pickle.dumps(M, p) M_restore = pickle.loads(M_pickle) assert M.signature() == M_restore.signature() def testSubDomains(): element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) a = ( v * u * dx(0) + 10.0 * v * u * dx(1) + v * u * ds(0) + 2.0 * v * u * ds(1) + v("+") * u("+") * dS(0) + 4.3 * v("+") * u("+") * dS(1) ) a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testTensorWeightedPoisson(): # FFC notation: # P1 = FiniteElement("Lagrange", triangle, 1) # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0) # # v = TestFunction(P1) # u = TrialFunction(P1) # f = Coefficient(P1) # # c00 = Coefficient(P0) # c01 = Coefficient(P0) # c10 = Coefficient(P0) # c11 = Coefficient(P0) # # C = [[c00, c01], [c10, c11]] # # a = dot(grad(v), mult(C, grad(u)))*dx P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) p1_space = FunctionSpace(domain, P1) p0_space = FunctionSpace(domain, P0) v = TestFunction(p1_space) u = TrialFunction(p1_space) C = Coefficient(p0_space) a = inner(grad(v), C * grad(u)) * dx a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) assert a.signature() == a_restore.signature() def testVectorLaplaceGradCurl(): def HodgeLaplaceGradCurl(space, fspace): (tau, v) = TestFunctions(space) (sigma, u) = TrialFunctions(space) f = Coefficient(fspace) # FFC notation: a = (dot(tau, sigma) - dot(grad(tau), u) + dot(v, # grad(sigma)) + dot(curl(v), curl(u)))*dx a = ( inner(tau, sigma) - inner(grad(tau), u) + inner(v, grad(sigma)) + inner(curl(v), curl(u)) ) * dx # FFC notation: L = dot(v, f)*dx L = inner(v, f) * dx return [a, L] shape = tetrahedron order = 1 GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1) # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1) CURL = FiniteElement("N1curl", shape, order, (3,), covariant_piola, HCurl) VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", shape, 1, (3,), identity_pullback, H1)) [a, L] = HodgeLaplaceGradCurl( FunctionSpace(domain, MixedElement([GRAD, CURL])), FunctionSpace(domain, VectorLagrange) ) a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) assert a.signature() == a_restore.signature() assert L.signature() == L_restore.signature() def testIdentity(): i = Identity(2) i_pickle = pickle.dumps(i, p) i_restore = pickle.loads(i_pickle) assert i == i_restore def testFormData(): element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) a = v * u * dx form_data = compute_form_data(a) form_data_pickle = pickle.dumps(form_data, p) form_data_restore = pickle.loads(form_data_pickle) assert str(form_data) == str(form_data_restore) ufl-2024.2.0/test/test_piecewise_checks.py000077500000000000000000000240061470142567200204310ustar00rootroot00000000000000"""Test the is_cellwise_constant function on all relevant terminal types.""" import pytest from ufl import ( Cell, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FunctionSpace, Jacobian, JacobianDeterminant, JacobianInverse, MaxFacetEdgeLength, Mesh, MinFacetEdgeLength, SpatialCoordinate, TestFunction, hexahedron, interval, quadrilateral, tetrahedron, triangle, ) from ufl.checks import is_cellwise_constant from ufl.classes import ( CellCoordinate, FacetJacobian, FacetJacobianDeterminant, FacetJacobianInverse, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2, HInf def get_domains(): all_cells = [ # vertex, interval, triangle, quadrilateral, tetrahedron, hexahedron, ] return [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ) ) for cell in all_cells ] def get_nonlinear(): domains_with_quadratic_coordinates = [] for D in get_domains(): V = FiniteElement( "Lagrange", D.ufl_cell(), 2, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) domains_with_quadratic_coordinates.append(E) return domains_with_quadratic_coordinates @pytest.fixture(params=list(range(5))) def nonlinear_domains(request): return get_nonlinear()[request.param] @pytest.fixture(params=list(range(10))) def domains_not_linear(request): all_domains_not_linear = get_domains() + get_nonlinear() return all_domains_not_linear[request.param] @pytest.fixture(params=list(range(15))) def domains(request): domains = get_domains() domains_with_linear_coordinates = [] for D in domains: V = FiniteElement( "Lagrange", D.ufl_cell(), 1, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) domains_with_linear_coordinates.append(E) all_domains = domains + domains_with_linear_coordinates + get_nonlinear() return all_domains[request.param] @pytest.fixture(params=list(range(6))) def affine_domains(request): affine_cells = [ interval, triangle, tetrahedron, ] affine_domains = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ) ) for cell in affine_cells ] affine_domains_with_linear_coordinates = [] for D in affine_domains: V = FiniteElement( "Lagrange", D.ufl_cell(), 1, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) affine_domains_with_linear_coordinates.append(E) all_affine_domains = affine_domains + affine_domains_with_linear_coordinates return all_affine_domains[request.param] @pytest.fixture(params=list(range(8))) def affine_facet_domains(request): affine_facet_cells = [ interval, triangle, quadrilateral, tetrahedron, ] affine_facet_domains = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ) ) for cell in affine_facet_cells ] affine_facet_domains_with_linear_coordinates = [] for D in affine_facet_domains: V = FiniteElement( "Lagrange", D.ufl_cell(), 1, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) affine_facet_domains_with_linear_coordinates.append(E) all_affine_facet_domains = affine_facet_domains + affine_facet_domains_with_linear_coordinates return all_affine_facet_domains[request.param] @pytest.fixture(params=list(range(4))) def nonaffine_domains(request): nonaffine_cells = [ quadrilateral, hexahedron, ] nonaffine_domains = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ) ) for cell in nonaffine_cells ] nonaffine_domains_with_linear_coordinates = [] for D in nonaffine_domains: V = FiniteElement( "Lagrange", D.ufl_cell(), 1, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) nonaffine_domains_with_linear_coordinates.append(E) all_nonaffine_domains = nonaffine_domains + nonaffine_domains_with_linear_coordinates return all_nonaffine_domains[request.param] @pytest.fixture(params=list(range(2))) def nonaffine_facet_domains(request): nonaffine_facet_cells = [ hexahedron, ] nonaffine_facet_domains = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ) ) for cell in nonaffine_facet_cells ] nonaffine_facet_domains_with_linear_coordinates = [] for D in nonaffine_facet_domains: V = FiniteElement( "Lagrange", D.ufl_cell(), 1, (D.ufl_cell().topological_dimension(),), identity_pullback, H1, ) E = Mesh(V) nonaffine_facet_domains_with_linear_coordinates.append(E) all_nonaffine_facet_domains = ( nonaffine_facet_domains + nonaffine_facet_domains_with_linear_coordinates ) return all_nonaffine_facet_domains[request.param] def test_always_cellwise_constant_geometric_quantities(domains): "Test geometric quantities that are always constant over a cell." e = CellVolume(domains) assert is_cellwise_constant(e) e = CellDiameter(domains) assert is_cellwise_constant(e) e = Circumradius(domains) assert is_cellwise_constant(e) e = FacetArea(domains) assert is_cellwise_constant(e) e = MinFacetEdgeLength(domains) assert is_cellwise_constant(e) e = MaxFacetEdgeLength(domains) assert is_cellwise_constant(e) def test_coordinates_never_cellwise_constant(domains): e = SpatialCoordinate(domains) assert not is_cellwise_constant(e) e = CellCoordinate(domains) assert not is_cellwise_constant(e) def test_coordinates_never_cellwise_constant_vertex(): # The only exception here: domains = Mesh(FiniteElement("Lagrange", Cell("vertex"), 1, (3,), identity_pullback, H1)) assert domains.ufl_cell().cellname() == "vertex" e = SpatialCoordinate(domains) assert is_cellwise_constant(e) e = CellCoordinate(domains) assert is_cellwise_constant(e) def mappings_are_cellwise_constant(domain, test): e = Jacobian(domain) assert is_cellwise_constant(e) == test e = JacobianDeterminant(domain) assert is_cellwise_constant(e) == test e = JacobianInverse(domain) assert is_cellwise_constant(e) == test if domain.topological_dimension() != 1: e = FacetJacobian(domain) assert is_cellwise_constant(e) == test e = FacetJacobianDeterminant(domain) assert is_cellwise_constant(e) == test e = FacetJacobianInverse(domain) assert is_cellwise_constant(e) == test def test_mappings_are_cellwise_constant_on_linear_affine_cells(affine_domains): mappings_are_cellwise_constant(affine_domains, True) def test_mappings_are_cellwise_not_constant_on_nonaffine_cells(nonaffine_domains): mappings_are_cellwise_constant(nonaffine_domains, False) def test_mappings_are_cellwise_not_constant_on_nonlinear_cells(nonlinear_domains): mappings_are_cellwise_constant(nonlinear_domains, False) def facetnormal_cellwise_constant(domain, test): e = FacetNormal(domain) assert is_cellwise_constant(e) == test def test_facetnormal_cellwise_constant_affine(affine_facet_domains): facetnormal_cellwise_constant(affine_facet_domains, True) def test_facetnormal_not_cellwise_constant_nonaffine(nonaffine_facet_domains): facetnormal_cellwise_constant(nonaffine_facet_domains, False) def test_facetnormal_not_cellwise_constant_nonlinear(nonlinear_domains): facetnormal_cellwise_constant(nonlinear_domains, False) def test_coefficient_sometimes_cellwise_constant(domains_not_linear): e = Constant(domains_not_linear) assert is_cellwise_constant(e) V = FiniteElement( "Discontinuous Lagrange", domains_not_linear.ufl_cell(), 0, (), identity_pullback, L2 ) d = domains_not_linear.ufl_cell().topological_dimension() domain = Mesh( FiniteElement("Lagrange", domains_not_linear.ufl_cell(), 1, (d,), identity_pullback, H1) ) space = FunctionSpace(domain, V) e = Coefficient(space) assert is_cellwise_constant(e) V = FiniteElement("Real", domains_not_linear.ufl_cell(), 0, (), identity_pullback, HInf) space = FunctionSpace(domain, V) e = Coefficient(space) assert is_cellwise_constant(e) # This should be true, but that has to wait for a fix of issue #13 # e = TestFunction(V) # assert is_cellwise_constant(e) # V = FiniteElement("R", domains_not_linear.ufl_cell(), 0) # e = TestFunction(V) # assert is_cellwise_constant(e) def test_coefficient_mostly_not_cellwise_constant(domains_not_linear): V = FiniteElement( "Discontinuous Lagrange", domains_not_linear.ufl_cell(), 1, (), identity_pullback, L2 ) d = domains_not_linear.ufl_cell().topological_dimension() domain = Mesh( FiniteElement("Lagrange", domains_not_linear.ufl_cell(), 1, (d,), identity_pullback, H1) ) space = FunctionSpace(domain, V) e = Coefficient(space) assert not is_cellwise_constant(e) e = TestFunction(space) assert not is_cellwise_constant(e) ufl-2024.2.0/test/test_reference_shapes.py000077500000000000000000000037571470142567200204470ustar00rootroot00000000000000from ufl import Cell, Mesh from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement from ufl.functionspace import FunctionSpace from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback from ufl.sobolevspace import H1, HCurl, HDiv def test_reference_shapes(): # show_elements() cell = Cell("triangle") domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) V = FiniteElement("N1curl", cell, 1, (2,), covariant_piola, HCurl) Vspace = FunctionSpace(domain, V) assert Vspace.value_shape == (3,) assert V.reference_value_shape == (2,) U = FiniteElement("Raviart-Thomas", cell, 1, (2,), contravariant_piola, HDiv) Uspace = FunctionSpace(domain, U) assert Uspace.value_shape == (3,) assert U.reference_value_shape == (2,) W = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) Wspace = FunctionSpace(domain, W) assert Wspace.value_shape == () assert W.reference_value_shape == () Q = FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1) Qspace = FunctionSpace(domain, Q) assert Qspace.value_shape == (3,) assert Q.reference_value_shape == (3,) T = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1) Tspace = FunctionSpace(domain, T) assert Tspace.value_shape == (3, 3) assert T.reference_value_shape == (3, 3) S = SymmetricElement( { (0, 0): 0, (1, 0): 1, (2, 0): 2, (0, 1): 1, (1, 1): 3, (2, 1): 4, (0, 2): 2, (1, 2): 4, (2, 2): 5, }, [FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for _ in range(6)], ) Sspace = FunctionSpace(domain, S) assert Sspace.value_shape == (3, 3) assert S.reference_value_shape == (6,) M = MixedElement([V, U, W]) Mspace = FunctionSpace(domain, M) assert Mspace.value_shape == (7,) assert M.reference_value_shape == (5,) ufl-2024.2.0/test/test_scratch.py000077500000000000000000000376771470142567200166050ustar00rootroot00000000000000"""Test scratch. This is a template file you can copy when making a new test case. Begin by copying this file to a filename matching test_*.py. The tests in the file will then automatically be run by ./test.py. Next look at the TODO markers below for places to edit. """ import warnings from ufl import ( Coefficient, FunctionSpace, Identity, Mesh, TestFunction, as_matrix, as_tensor, as_vector, dx, grad, indices, inner, outer, triangle, ) from ufl.classes import FixedIndex, FormArgument, Grad, Indexed, ListTensor, Zero from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 from ufl.tensors import as_scalar, unit_indexed_tensor, unwrap_list_tensor class MockForwardAD: def __init__(self): self._w = () self._v = () class Obj: def __init__(self): self._data = {} self._cd = Obj() def grad(self, g): # If we hit this type, it has already been propagated # to a coefficient (or grad of a coefficient), # FIXME: Assert this! # so we need to take the gradient of the variation or return zero. # Complications occur when dealing with derivatives w.r.t. single components... # Figure out how many gradients are around the inner terminal ngrads = 0 o = g while isinstance(o, Grad): (o,) = o.ufl_operands ngrads += 1 if not isinstance(o, FormArgument): raise ValueError("Expecting gradient of a FormArgument, not %s" % repr(o)) def apply_grads(f): if not isinstance(f, FormArgument): print(("," * 60)) print(f) print(o) print(g) print(("," * 60)) raise ValueError("What?") for i in range(ngrads): f = Grad(f) return f # Find o among all w without any indexing, which makes this easy for w, v in zip(self._w, self._v): if o == w and isinstance(v, FormArgument): # Case: d/dt [w + t v] return (g, apply_grads(v)) # If o is not among coefficient derivatives, return do/dw=0 gprimesum = Zero(g.ufl_shape) def analyse_variation_argument(v): # Analyse variation argument if isinstance(v, FormArgument): # Case: d/dt [w[...] + t v] vval, vcomp = v, () elif isinstance(v, Indexed): # Case: d/dt [w + t v[...]] # Case: d/dt [w[...] + t v[...]] vval, vcomp = v.ufl_operands vcomp = tuple(vcomp) else: raise ValueError("Expecting argument or component of argument.") if not all(isinstance(k, FixedIndex) for k in vcomp): raise ValueError("Expecting only fixed indices in variation.") return vval, vcomp def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): # Apply gradients directly to argument vval, # and get the right indexed scalar component(s) kk = indices(ngrads) Dvkk = apply_grads(vval)[vcomp + kk] # Place scalar component(s) Dvkk into the right tensor positions if wshape: Ejj, jj = unit_indexed_tensor(wshape, wcomp) else: Ejj, jj = 1, () gprimeterm = as_tensor(Ejj * Dvkk, jj + kk) return gprimeterm # Accumulate contributions from variations in different components for w, v in zip(self._w, self._v): # Analyse differentiation variable coefficient if isinstance(w, FormArgument): if not w == o: continue wshape = w.ufl_shape if isinstance(v, FormArgument): # Case: d/dt [w + t v] return (g, apply_grads(v)) elif isinstance(v, ListTensor): # Case: d/dt [w + t <...,v,...>] for wcomp, vsub in unwrap_list_tensor(v): if not isinstance(vsub, Zero): vval, vcomp = analyse_variation_argument(vsub) gprimesum = gprimesum + compute_gprimeterm( ngrads, vval, vcomp, wshape, wcomp ) else: if wshape != (): raise ValueError("Expecting scalar coefficient in this branch.") # Case: d/dt [w + t v[...]] wval, wcomp = w, () vval, vcomp = analyse_variation_argument(v) gprimesum = gprimesum + compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp) elif isinstance( w, Indexed ): # This path is tested in unit tests, but not actually used? # Case: d/dt [w[...] + t v[...]] # Case: d/dt [w[...] + t v] wval, wcomp = w.ufl_operands if not wval == o: continue assert isinstance(wval, FormArgument) if not all(isinstance(k, FixedIndex) for k in wcomp): raise ValueError("Expecting only fixed indices in differentiation variable.") wshape = wval.ufl_shape vval, vcomp = analyse_variation_argument(v) gprimesum = gprimesum + compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp) else: raise ValueError("Expecting coefficient or component of coefficient.") # FIXME: Handle other coefficient derivatives: oprimes = self._cd._data.get(o) if 0: oprimes = self._cd._data.get(o) if oprimes is None: if self._cd._data: # TODO: Make it possible to silence this message in particular? # It may be good to have for debugging... warnings.warn("Assuming d{%s}/d{%s} = 0." % (o, self._w)) else: # Make sure we have a tuple to match the self._v tuple if not isinstance(oprimes, tuple): oprimes = (oprimes,) if len(oprimes) != len(self._v): raise ValueError( "Got a tuple of arguments, " "expecting a matching tuple of coefficient derivatives." ) # Compute dg/dw_j = dg/dw_h : v. # Since we may actually have a tuple of oprimes and vs in a # 'mixed' space, sum over them all to get the complete inner # product. Using indices to define a non-compound inner product. for oprime, v in zip(oprimes, self._v): raise ValueError("FIXME: Figure out how to do this with ngrads") so, oi = as_scalar(oprime) rv = len(v.ufl_shape) oi1 = oi[:-rv] oi2 = oi[-rv:] prod = so * v[oi2] if oi1: gprimesum += as_tensor(prod, oi1) else: gprimesum += prod return (g, gprimesum) def test_unit_tensor(self): E2_1, ii = unit_indexed_tensor((2,), (1,)) E3_1, ii = unit_indexed_tensor((3,), (1,)) E22_10, ii = unit_indexed_tensor((2, 2), (1, 0)) # TODO: Evaluate and assert values def test_unwrap_list_tensor(self): lt = as_tensor((1, 2)) expected = [ ((0,), 1), ((1,), 2), ] comp = unwrap_list_tensor(lt) assert comp == expected lt = as_tensor(((1, 2), (3, 4))) expected = [ ((0, 0), 1), ((0, 1), 2), ((1, 0), 3), ((1, 1), 4), ] comp = unwrap_list_tensor(lt) assert comp == expected lt = as_tensor((((1, 2), (3, 4)), ((11, 12), (13, 14)))) expected = [ ((0, 0, 0), 1), ((0, 0, 1), 2), ((0, 1, 0), 3), ((0, 1, 1), 4), ((1, 0, 0), 11), ((1, 0, 1), 12), ((1, 1, 0), 13), ((1, 1, 1), 14), ] comp = unwrap_list_tensor(lt) assert comp == expected def test__forward_coefficient_ad__grad_of_scalar_coefficient(self): U = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, U) u = Coefficient(space) du = TestFunction(space) mad = MockForwardAD() mad._w = (u,) mad._v = (du,) # Simple grad(coefficient) -> grad(variation) f = grad(u) df = grad(du) g, dg = mad.grad(f) assert g == f assert dg == df # Simple grad(grad(coefficient)) -> grad(grad(variation)) f = grad(grad(u)) df = grad(grad(du)) g, dg = mad.grad(f) assert g == f assert dg == df def test__forward_coefficient_ad__grad_of_vector_coefficient(self): V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = Coefficient(space) dv = TestFunction(space) mad = MockForwardAD() mad._w = (v,) mad._v = (dv,) # Simple grad(coefficient) -> grad(variation) f = grad(v) df = grad(dv) g, dg = mad.grad(f) assert g == f assert dg == df # Simple grad(grad(coefficient)) -> grad(grad(variation)) f = grad(grad(v)) df = grad(grad(dv)) g, dg = mad.grad(f) assert g == f assert dg == df def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_variation(self): V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = Coefficient(space) dv = TestFunction(space) mad = MockForwardAD() # Component of variation: # grad(grad(c))[0,...] -> grad(grad(dc))[1,...] mad._w = (v[0],) mad._v = (dv[1],) f = grad(v) df = grad(as_vector((dv[1], 0))) # Mathematically this would be the natural result j, k = indices(2) df = as_tensor( Identity(2)[0, j] * grad(dv)[1, k], (j, k) ) # Actual representation should have grad right next to dv g, dg = mad.grad(f) if 0: print(("\nf ", f)) print(("df ", df)) print(("g ", g)) print(("dg ", dg)) assert f.ufl_shape == df.ufl_shape assert g.ufl_shape == f.ufl_shape assert dg.ufl_shape == df.ufl_shape assert g == f self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering # Multiple components of variation: # grad(grad(c))[0,1,:,:] -> grad(grad(dc))[1,0,:,:] mad._w = (v[0], v[1]) mad._v = (dv[1], dv[0]) f = grad(v) # Mathematically this would be the natural result: df = grad(as_vector((dv[1], dv[0]))) # Actual representation should have grad right next to dv: j0, k0 = indices(2) j1, k1 = indices(2) # Using j0,k0 for both terms gives different signature df = as_tensor(Identity(2)[0, j0] * grad(dv)[1, k0], (j0, k0)) + as_tensor( Identity(2)[1, j1] * grad(dv)[0, k1], (j1, k1) ) g, dg = mad.grad(f) print(("\nf ", f)) print(("df ", df)) print(("g ", g)) print(("dg ", dg)) assert f.ufl_shape == df.ufl_shape assert g.ufl_shape == f.ufl_shape assert dg.ufl_shape == df.ufl_shape assert g == f self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_variation_in_list( self, ): V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) v = Coefficient(space) dv = TestFunction(space) mad = MockForwardAD() # Component of variation: # grad(grad(c))[0,...] -> grad(grad(dc))[1,...] mad._w = (v,) mad._v = (as_vector((dv[1], 0)),) f = grad(v) df = grad(as_vector((dv[1], 0))) # Mathematically this would be the natural result j, k = indices(2) df = as_tensor( Identity(2)[0, j] * grad(dv)[1, k], (j, k) ) # Actual representation should have grad right next to dv g, dg = mad.grad(f) if 0: print(("\nf ", f)) print(("df ", df)) print(("g ", g)) print(("dg ", dg)) assert f.ufl_shape == df.ufl_shape assert g.ufl_shape == f.ufl_shape assert dg.ufl_shape == df.ufl_shape assert g == f self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering # Multiple components of variation: # grad(grad(c))[0,1,:,:] -> grad(grad(dc))[1,0,:,:] mad._w = (v,) mad._v = (as_vector((dv[1], dv[0])),) f = grad(v) # Mathematically this would be the natural result: df = grad(as_vector((dv[1], dv[0]))) # Actual representation should have grad right next to dv: j0, k0 = indices(2) j1, k1 = indices(2) # Using j0,k0 for both terms gives different signature df = as_tensor(Identity(2)[0, j0] * grad(dv)[1, k0], (j0, k0)) + as_tensor( Identity(2)[1, j1] * grad(dv)[0, k1], (j1, k1) ) g, dg = mad.grad(f) print(("\nf ", f)) print(("df ", df)) print(("g ", g)) print(("dg ", dg)) assert f.ufl_shape == df.ufl_shape assert g.ufl_shape == f.ufl_shape assert dg.ufl_shape == df.ufl_shape assert g == f self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering def test__forward_coefficient_ad__grad_of_tensor_coefficient(self): W = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, W) w = Coefficient(space) dw = TestFunction(space) mad = MockForwardAD() mad._w = (w,) mad._v = (dw,) # Simple grad(coefficient) -> grad(variation) f = grad(w) df = grad(dw) g, dg = mad.grad(f) assert g == f assert dg == df # Simple grad(grad(coefficient)) -> grad(grad(variation)) f = grad(grad(w)) df = grad(grad(dw)) g, dg = mad.grad(f) assert g == f assert dg == df def test__forward_coefficient_ad__grad_of_tensor_coefficient__with_component_variation(self): W = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, W) w = Coefficient(space) dw = TestFunction(space) mad = MockForwardAD() # Component of variation: # grad(grad(c))[0,...] -> grad(grad(dc))[1,...] wc = (1, 0) dwc = (0, 1) mad._w = (w[wc],) mad._v = (dw[dwc],) f = grad(w) df = grad(as_matrix(((0, 0), (dw[dwc], 0)))) # Mathematically this is it. i, j, k = indices(3) E = outer(Identity(2)[wc[0], i], Identity(2)[wc[1], j]) Ddw = grad(dw)[dwc + (k,)] df = as_tensor(E * Ddw, (i, j, k)) # Actual representation should have grad next to dv g, dg = mad.grad(f) if 0: print(("\nf ", f)) print(("df ", df)) print(("g ", g)) print(("dg ", dg)) assert f.ufl_shape == df.ufl_shape assert g.ufl_shape == f.ufl_shape assert dg.ufl_shape == df.ufl_shape assert g == f self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering ufl-2024.2.0/test/test_signature.py000077500000000000000000000514111470142567200171350ustar00rootroot00000000000000"""Test the computation of form signatures.""" from ufl import ( Argument, CellDiameter, CellVolume, Circumradius, Coefficient, FacetArea, FacetNormal, FunctionSpace, Identity, Mesh, SpatialCoordinate, TestFunction, as_vector, diff, dot, ds, dx, hexahedron, indices, inner, interval, quadrilateral, tetrahedron, triangle, variable, ) from ufl.algorithms.signature import compute_multiindex_hashdata, compute_terminal_hashdata from ufl.classes import FixedIndex, MultiIndex from ufl.finiteelement import FiniteElement, SymmetricElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1, L2 # TODO: Test compute_terminal_hashdata # TODO: Check that form argument counts only affect the sig by their relative ordering # TODO: Check that all other relevant terminal propeties affect the terminal_hashdata # TODO: Test that operator types affect the sig # TODO: Test that we do not get collisions for some large sets of generated forms # TODO: How do we know that we have tested the signature reliably enough? def domain_numbering(*cells): renumbering = {} for i, cell in enumerate(cells): d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i) renumbering[domain] = i return renumbering def compute_unique_terminal_hashdatas(hashdatas): count = 0 data = set() hashes = set() reprs = set() for d in hashdatas: # Each d is the result of a compute_terminal_hashdatas call, # which is a dict where the keys are non-canonical terminals # and the values are the canonical hashdata. # We want to count unique hashdata values, # ignoring the original terminals. assert isinstance(d, dict) # Sorting values by hash should be stable at least in a single test run: t = tuple(sorted(list(d.values()), key=lambda x: hash(x))) # print t # Add the hashdata values tuple to sets based on itself, its hash, # and its repr (not sure why I included repr anymore?) hashes.add(hash(t)) # This will fail if t is not hashable, which it should be! data.add(t) reprs.add(repr(t)) count += 1 return count, len(data), len(reprs), len(hashes) def test_terminal_hashdata_depends_on_literals(self): reprs = set() hashes = set() def forms(): i, j = indices(2) for d, cell in [(2, triangle), (3, tetrahedron)]: domain = Mesh( FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=d - 2 ) x = SpatialCoordinate(domain) ident = Identity(d) for fv in (1.1, 2.2): for iv in (5, 7): expr = (ident[0, j] * (fv * x[j])) ** iv reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, {domain: 0}) c, d, r, h = compute_unique_terminal_hashdatas(forms()) assert c == 8 assert d == c assert r == c assert h == c assert len(reprs) == c assert len(hashes) == c def test_terminal_hashdata_depends_on_geometry(self): reprs = set() hashes = set() def forms(): i, j = indices(2) cells = (triangle, tetrahedron) for i, cell in enumerate(cells): d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i) x = SpatialCoordinate(domain) n = FacetNormal(domain) h = CellDiameter(domain) r = Circumradius(domain) a = FacetArea(domain) # s = CellSurfaceArea(domain) v = CellVolume(domain) ident = Identity(d) ws = (x, n) qs = (h, r, a, v) # , s) for w in ws: for q in qs: expr = ident[0, j] * (q * w[j]) reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, domain_numbering(*cells)) c, d, r, h = compute_unique_terminal_hashdatas(forms()) assert c == 2 * 4 * 2 # len(ws)*len(qs)*len(cells) assert d == c assert r == c assert h == c assert len(reprs) == c assert len(hashes) == c def test_terminal_hashdata_depends_on_form_argument_properties(self): reprs = set() hashes = set() nelm = 5 nreps = 2 # Data cells = (triangle, tetrahedron) degrees = (1, 2) families = (("Lagrange", H1), ("Lagrange", H1), ("Discontinuous Lagrange", L2)) def forms(): for rep in range(nreps): for i, cell in enumerate(cells): d = cell.topological_dimension() domain = Mesh( FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i ) for degree in degrees: for family, sobolev in families: V = FiniteElement(family, cell, degree, (), identity_pullback, sobolev) W = FiniteElement(family, cell, degree, (d,), identity_pullback, sobolev) W2 = FiniteElement( family, cell, degree, (d + 1,), identity_pullback, sobolev ) T = FiniteElement(family, cell, degree, (d, d), identity_pullback, sobolev) if d == 2: S = SymmetricElement( {(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}, [ FiniteElement( family, cell, degree, (), identity_pullback, sobolev ) for _ in range(3) ], ) else: assert d == 3 S = SymmetricElement( { (0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 1, (1, 1): 3, (1, 2): 4, (2, 0): 2, (2, 1): 4, (2, 2): 5, }, [ FiniteElement( family, cell, degree, (), identity_pullback, sobolev ) for _ in range(6) ], ) elements = [V, W, W2, T, S] assert len(elements) == nelm for H in elements[:nelm]: space = FunctionSpace(domain, H) # Keep number and count fixed, we're not testing that here a = Argument(space, number=1) c = Coefficient(space, count=1) renumbering = domain_numbering(*cells) renumbering[c] = 0 for f in (a, c): expr = inner(f, f) reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, renumbering) c, d, r, h = compute_unique_terminal_hashdatas(forms()) c1 = nreps * len(cells) * len(degrees) * len(families) * nelm * 2 assert c == c1 c0 = len(cells) * len(degrees) * (len(families) - 1) * nelm * 2 assert d == c0 assert r == c0 assert h == c0 assert len(reprs) == c0 assert len(hashes) == c0 def test_terminal_hashdata_does_not_depend_on_coefficient_count_values_only_ordering(self): reprs = set() hashes = set() counts = list(range(-3, 4)) cells = (interval, triangle, hexahedron) assert len(counts) == 7 nreps = 1 def forms(): for rep in range(nreps): for i, cell in enumerate(cells): d = cell.topological_dimension() domain = Mesh( FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i ) for k in counts: V = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) space = FunctionSpace(domain, V) f = Coefficient(space, count=k) g = Coefficient(space, count=k + 2) expr = inner(f, g) renumbering = domain_numbering(*cells) renumbering[f] = 0 renumbering[g] = 1 reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, renumbering) c, d, r, h = compute_unique_terminal_hashdatas(forms()) c0 = len(cells) # Number of actually unique cases from a code generation perspective c1 = len(counts) * c0 # Number of unique cases from a symbolic representation perspective assert len(reprs) == c1 assert len(hashes) == c1 assert c == nreps * c1 # number of inner loop executions in forms() above assert d == c0 assert r == c0 assert h == c0 def test_terminal_hashdata_does_depend_on_argument_number_values(self): # TODO: Include part numbers as well reprs = set() hashes = set() counts = list(range(4)) cells = (interval, triangle, hexahedron) nreps = 2 def forms(): for rep in range(nreps): for i, cell in enumerate(cells): d = cell.topological_dimension() domain = Mesh( FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i ) for k in counts: V = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) space = FunctionSpace(domain, V) f = Argument(space, k) g = Argument(space, k + 2) expr = inner(f, g) reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, domain_numbering(*cells)) c, d, r, h = compute_unique_terminal_hashdatas(forms()) c0 = len(cells) * len( counts ) # Number of actually unique cases from a code generation perspective c1 = 1 * c0 # Number of unique cases from a symbolic representation perspective assert len(reprs) == c1 assert len(hashes) == c1 self.assertEqual(c, nreps * c1) # number of inner loop executions in forms() above assert d == c0 assert r == c0 assert h == c0 def test_domain_signature_data_does_not_depend_on_domain_label_value(self): cells = [triangle, tetrahedron, hexahedron] s0s = set() s1s = set() s2s = set() for i, cell in enumerate(cells): d = cell.topological_dimension() domain = FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1) d0 = Mesh(domain) d1 = Mesh(domain, ufl_id=1) d2 = Mesh(domain, ufl_id=2) s0 = d0._ufl_signature_data_({d0: 0}) s1 = d1._ufl_signature_data_({d1: 0}) s2 = d2._ufl_signature_data_({d2: 0}) assert s0 == s1 assert s0 == s2 s0s.add(s0) s1s.add(s1) s2s.add(s2) assert len(s0s) == len(cells) assert len(s1s) == len(cells) assert len(s2s) == len(cells) def test_terminal_hashdata_does_not_depend_on_domain_label_value(self): reprs = set() hashes = set() ufl_ids = [1, 2] cells = [triangle, quadrilateral] domains = [ Mesh( FiniteElement( "Lagrange", cell, 1, (cell.topological_dimension(),), identity_pullback, H1 ), ufl_id=ufl_id, ) for cell in cells for ufl_id in ufl_ids ] nreps = 2 num_exprs = 2 def forms(): for rep in range(nreps): for domain in domains: V = FunctionSpace( domain, FiniteElement("Lagrange", domain.ufl_cell(), 2, (), identity_pullback, H1), ) f = Coefficient(V, count=0) v = TestFunction(V) x = SpatialCoordinate(domain) n = FacetNormal(domain) exprs = [inner(x, n), inner(f, v)] assert num_exprs == len(exprs) # Assumed in checks below # This numbering needs to be recreated to count 'domain' and 'f' as 0 each time: renumbering = {f: 0, domain: 0} for expr in exprs: reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_terminal_hashdata(expr, renumbering) c, d, r, h = compute_unique_terminal_hashdatas(forms()) c0 = num_exprs * len( cells ) # Number of actually unique cases from a code generation perspective c1 = num_exprs * len( domains ) # Number of unique cases from a symbolic representation perspective assert len(reprs) == c1 assert len(hashes) == c1 self.assertEqual(c, nreps * c1) # number of inner loop executions in forms() above assert d == c0 assert r == c0 assert h == c0 def compute_unique_multiindex_hashdatas(hashdatas): count = 0 data = set() hashes = set() reprs = set() for d in hashdatas: data.add(tuple(d)) hashes.add(hash(tuple(d))) reprs.add(repr(d)) count += 1 return count, len(data), len(reprs), len(hashes) def test_multiindex_hashdata_depends_on_fixed_index_values(self): reprs = set() hashes = set() def hashdatas(): for i in range(3): for ii in ((i,), (i, 0), (1, i)): jj = tuple(FixedIndex(j) for j in ii) expr = MultiIndex(jj) reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_multiindex_hashdata(expr, {}) c, d, r, h = compute_unique_multiindex_hashdatas(hashdatas()) assert c == 9 assert d == 9 - 1 # (1,0 is repeated, therefore -1) assert len(reprs) == 9 - 1 assert len(hashes) == 9 - 1 def test_multiindex_hashdata_does_not_depend_on_counts(self): reprs = set() hashes = set() def hashdatas(): ijs = [] iind = indices(3) jind = indices(3) for i in iind: ijs.append((i,)) for j in jind: ijs.append((i, j)) ijs.append((j, i)) for ij in ijs: expr = MultiIndex(ij) reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_multiindex_hashdata(expr, {}) c, d, r, h = compute_unique_multiindex_hashdatas(hashdatas()) assert c == 3 + 9 + 9 assert d == 1 + 1 assert len(reprs) == 3 + 9 + 9 assert len(hashes) == 3 + 9 + 9 def test_multiindex_hashdata_depends_on_the_order_indices_are_observed(self): reprs = set() hashes = set() nrep = 3 def hashdatas(): for rep in range(nrep): # Resetting index_numbering for each repetition, resulting # in hashdata staying the same for each repetition but repr # and hashes changing because new indices are created each # repetition. index_numbering = {} i, j, k, l = indices(4) # noqa: E741 for expr in ( MultiIndex((i,)), MultiIndex((i,)), # r MultiIndex((i, j)), MultiIndex((j, i)), MultiIndex((i, j)), # r MultiIndex((i, j, k)), MultiIndex((k, j, i)), MultiIndex((j, i)), ): # r reprs.add(repr(expr)) hashes.add(hash(expr)) yield compute_multiindex_hashdata(expr, index_numbering) c, d, r, h = compute_unique_multiindex_hashdatas(hashdatas()) assert c == nrep * 8 assert d == 5 assert len(reprs) == nrep * 5 assert len(hashes) == nrep * 5 def check_unique_signatures(forms): count = 0 sigs = set() sigs2 = set() hashes = set() reprs = set() for a in forms: sig = a.signature() sig2 = a.signature() sigs.add(sig) sigs2.add(sig2) assert sig hashes.add(hash(a)) reprs.add(repr(a)) count += 1 assert len(sigs) == count assert len(sigs2) == count assert len(reprs) == count assert len(hashes) == count def test_signature_is_affected_by_element_properties(self): def forms(): for family, sobolev in (("Lagrange", H1), ("Discontinuous Lagrange", L2)): for cell in (triangle, tetrahedron, quadrilateral): d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) for degree in (1, 2): V = FiniteElement(family, cell, degree, (), identity_pullback, sobolev) space = FunctionSpace(domain, V) u = Coefficient(space) v = TestFunction(space) x = SpatialCoordinate(domain) w = as_vector([v] * x.ufl_shape[0]) f = dot(w, u * x) a = f * dx yield a check_unique_signatures(forms()) def test_signature_is_affected_by_domains(self): def forms(): for cell in (triangle, tetrahedron): d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) for di in (1, 2): for dj in (1, 2): for dk in (1, 2): V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) space = FunctionSpace(domain, V) u = Coefficient(space) a = u * dx(di) + 2 * u * dx(dj) + 3 * u * ds(dk) yield a check_unique_signatures(forms()) def test_signature_of_forms_with_diff(self): def forms(): for i, cell in enumerate([triangle, tetrahedron]): d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i) for k in (1, 2, 3): d = cell.topological_dimension() V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) W = FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1) v_space = FunctionSpace(domain, V) w_space = FunctionSpace(domain, W) u = Coefficient(v_space) w = Coefficient(w_space) vu = variable(u) vw = variable(w) f = vu * dot(vw, vu**k * vw) g = diff(f, vu) h = dot(diff(f, vw), FacetNormal(domain)) a = f * dx(1) + g * dx(2) + h * ds(0) yield a check_unique_signatures(forms()) def test_signature_of_form_depend_on_coefficient_numbering_across_integrals(self): cell = triangle V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, V) f = Coefficient(space) g = Coefficient(space) M1 = f * dx(0) + g * dx(1) M2 = g * dx(0) + f * dx(1) M3 = g * dx(0) + g * dx(1) self.assertTrue(M1.signature() != M2.signature()) self.assertTrue(M1.signature() != M3.signature()) self.assertTrue(M2.signature() != M3.signature()) def test_signature_of_forms_change_with_operators(self): def forms(): for cell in (triangle, tetrahedron): d = cell.topological_dimension() V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) space = FunctionSpace(domain, V) u = Coefficient(space) v = Coefficient(space) fs = [ (u * v) + (u / v), (u + v) + (u / v), (u + v) * (u / v), (u * v) * (u * v), (u + v) * (u * v), # H1 same # (u*v)*(u+v), # H1 same (u * v) + (u + v), ] for f in fs: a = f * dx yield a check_unique_signatures(forms()) ufl-2024.2.0/test/test_simplify.py000077500000000000000000000106351470142567200167730ustar00rootroot00000000000000import math from ufl import ( Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant, acos, as_tensor, as_ufl, asin, atan, cos, cosh, dx, exp, i, j, ln, max_value, min_value, outer, sin, sinh, tan, tanh, triangle, ) from ufl.algorithms import compute_form_data from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def xtest_zero_times_argument(self): # FIXME: Allow zero forms element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) v = TestFunction(space) u = TrialFunction(space) L = 0 * v * dx a = 0 * (u * v) * dx b = (0 * u) * v * dx assert len(compute_form_data(L).arguments) == 1 assert len(compute_form_data(a).arguments) == 2 assert len(compute_form_data(b).arguments) == 2 def test_divisions(self): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) # Test simplification of division by 1 a = f b = f / 1 assert a == b # Test simplification of division by 1.0 a = f b = f / 1.0 assert a == b # Test simplification of division by of zero by something a = 0 / f b = 0 * f assert a == b # Test simplification of division by self (this simplification has been disabled) # a = f/f # b = 1 # assert a == b def test_products(self): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) g = Coefficient(space) # Test simplification of literal multiplication assert f * 0 == as_ufl(0) assert 0 * f == as_ufl(0) assert 1 * f == f assert f * 1 == f assert as_ufl(2) * as_ufl(3) == as_ufl(6) assert as_ufl(2.0) * as_ufl(3.0) == as_ufl(6.0) # Test reordering of operands assert f * g == g * f # Test simplification of self-multiplication (this simplification has been disabled) # assert f*f == f**2 def test_sums(self): element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) space = FunctionSpace(domain, element) f = Coefficient(space) g = Coefficient(space) # Test reordering of operands assert f + g == g + f # Test adding zero assert f + 0 == f assert 0 + f == f # Test collapsing of basic sum (this simplification has been disabled) # assert f + f == 2 * f # Test reordering of operands and collapsing sum a = f + g + f # not collapsed, but ordered b = g + f + f # not collapsed, but ordered c = (g + f) + f # not collapsed, but ordered d = f + (f + g) # not collapsed, but ordered assert a == b assert a == c assert a == d # Test reordering of operands and collapsing sum a = f + f + g # collapsed b = g + (f + f) # collapsed assert a == b def test_mathfunctions(self): for a in (0.1, 0.3, 0.9): assert math.sin(a) == sin(a) assert math.cos(a) == cos(a) assert math.tan(a) == tan(a) assert math.sinh(a) == sinh(a) assert math.cosh(a) == cosh(a) assert math.tanh(a) == tanh(a) assert math.asin(a) == asin(a) assert math.acos(a) == acos(a) assert math.atan(a) == atan(a) assert math.exp(a) == exp(a) assert math.log(a) == ln(a) # TODO: Implement automatic simplification of conditionals? assert a == float(max_value(a, a - 1)) # TODO: Implement automatic simplification of conditionals? assert a == float(min_value(a, a + 1)) def test_indexing(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) u = VectorConstant(domain) v = VectorConstant(domain) A = outer(u, v) A2 = as_tensor(A[i, j], (i, j)) assert A2 == A Bij = u[i] * v[j] Bij2 = as_tensor(Bij, (i, j))[i, j] as_tensor(Bij, (i, j)) assert Bij2 == Bij ufl-2024.2.0/test/test_sobolevspace.py000077500000000000000000000122601470142567200176200ustar00rootroot00000000000000__authors__ = "David Ham" __date__ = "2014-03-04" from math import inf from ufl import H1, H2, L2, HCurl, HDiv, HInf, triangle from ufl.finiteelement import FiniteElement from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback from ufl.sobolevspace import ( DirectionalSobolevSpace, SobolevSpace, # noqa: F401 ) # Construct directional Sobolev spaces, with varying smoothness in # spatial coordinates H0dx0dy = DirectionalSobolevSpace((0, 0)) H1dx1dy = DirectionalSobolevSpace((1, 1)) H2dx2dy = DirectionalSobolevSpace((2, 2)) Hinfdxinfdy = DirectionalSobolevSpace((inf, inf)) H1dx = DirectionalSobolevSpace((1, 0)) H1dy = DirectionalSobolevSpace((0, 1)) H000 = DirectionalSobolevSpace((0, 0, 0)) H1dz = DirectionalSobolevSpace((0, 0, 1)) H1dh = DirectionalSobolevSpace((1, 1, 0)) H2dhH1dz = DirectionalSobolevSpace((2, 2, 1)) # TODO: Add construction of all elements with periodic table notation here. def test_inclusion(): assert H2 < H1 # Inclusion assert not H2 > H1 # Not included assert HDiv <= HDiv # Reflexivity assert H2 < L2 # Transitivity assert H1 > H2 assert L2 > H1 def test_directional_space_relations(): assert H0dx0dy == L2 assert H1dx1dy == H1 assert H2dx2dy == H2 assert H1dx1dy <= HDiv assert H1dx1dy <= HCurl assert H2dx2dy <= H1dx1dy assert H2dhH1dz < H1 assert Hinfdxinfdy <= HInf assert Hinfdxinfdy < H2dx2dy assert H1dz > H2dhH1dz assert H1dh < L2 assert H1dz < L2 assert L2 > H1dx assert L2 > H1dy assert not H1dh <= HDiv assert not H1dh <= HCurl def test_repr(): assert eval(repr(H2)) == H2 def xtest_contains_mixed(): pass # FIXME: How to handle this? def test_contains_l2(): l2_elements = [ FiniteElement("Discontinuous Lagrange", triangle, 0, (), identity_pullback, L2), FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2), FiniteElement("Discontinuous Lagrange", triangle, 2, (), identity_pullback, L2), ] for l2_element in l2_elements: assert l2_element in L2 assert l2_element in H0dx0dy assert l2_element not in H1 assert l2_element not in H1dx1dy assert l2_element not in HCurl assert l2_element not in HDiv assert l2_element not in H2 assert l2_element not in H2dx2dy def test_contains_h1(): h1_elements = [ # Standard Lagrange elements: FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1), FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1), # Some special elements: FiniteElement("MTW", triangle, 3, (2,), contravariant_piola, H1), FiniteElement("Hermite", triangle, 3, (), "custom", H1), ] for h1_element in h1_elements: assert h1_element in H1 assert h1_element in H1dx1dy assert h1_element in HDiv assert h1_element in HCurl assert h1_element in L2 assert h1_element in H0dx0dy assert h1_element not in H2 assert h1_element not in H2dx2dy def test_contains_h2(): h2_elements = [ FiniteElement("ARG", triangle, 5, (), "custom", H2), FiniteElement("MOR", triangle, 2, (), "custom", H2), ] for h2_element in h2_elements: assert h2_element in H2 assert h2_element in H2dx2dy assert h2_element in H1 assert h2_element in H1dx1dy assert h2_element in HDiv assert h2_element in HCurl assert h2_element in L2 assert h2_element in H0dx0dy def test_contains_hinf(): hinf_elements = [FiniteElement("Real", triangle, 0, (), identity_pullback, HInf)] for hinf_element in hinf_elements: assert hinf_element in HInf assert hinf_element in H2 assert hinf_element in H2dx2dy assert hinf_element in H1 assert hinf_element in H1dx1dy assert hinf_element in HDiv assert hinf_element in HCurl assert hinf_element in L2 assert hinf_element in H0dx0dy def test_contains_hdiv(): hdiv_elements = [ FiniteElement("Raviart-Thomas", triangle, 1, (2,), contravariant_piola, HDiv), FiniteElement("BDM", triangle, 1, (2,), contravariant_piola, HDiv), FiniteElement("BDFM", triangle, 2, (2,), contravariant_piola, HDiv), ] for hdiv_element in hdiv_elements: assert hdiv_element in HDiv assert hdiv_element in L2 assert hdiv_element in H0dx0dy assert hdiv_element not in H1 assert hdiv_element not in H1dx1dy assert hdiv_element not in HCurl assert hdiv_element not in H2 assert hdiv_element not in H2dx2dy def test_contains_hcurl(): hcurl_elements = [ FiniteElement("N1curl", triangle, 1, (2,), covariant_piola, HCurl), FiniteElement("N2curl", triangle, 1, (2,), covariant_piola, HCurl), ] for hcurl_element in hcurl_elements: assert hcurl_element in HCurl assert hcurl_element in L2 assert hcurl_element in H0dx0dy assert hcurl_element not in H1 assert hcurl_element not in H1dx1dy assert hcurl_element not in HDiv assert hcurl_element not in H2 assert hcurl_element not in H2dx2dy ufl-2024.2.0/test/test_split.py000077500000000000000000000064461470142567200162770ustar00rootroot00000000000000__authors__ = "Martin Sandve Alnæs" __date__ = "2009-03-14 -- 2009-03-14" from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, as_vector, product, split, triangle from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_split(self): cell = triangle d = cell.topological_dimension() domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) f = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) v = FiniteElement( "Lagrange", cell, 1, (d,), identity_pullback, H1, sub_elements=[f for _ in range(d)] ) w = FiniteElement( "Lagrange", cell, 1, (d + 1,), identity_pullback, H1, sub_elements=[f for _ in range(d + 1)] ) t = FiniteElement( "Lagrange", cell, 1, (d, d), identity_pullback, H1, sub_elements=[f for _ in range(d**2)] ) s = SymmetricElement({(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}, [f for _ in range(3)]) m = MixedElement([f, v, w, t, s, s]) f_space = FunctionSpace(domain, f) v_space = FunctionSpace(domain, v) w_space = FunctionSpace(domain, w) t_space = FunctionSpace(domain, t) s_space = FunctionSpace(domain, s) m_space = FunctionSpace(domain, m) # Check that shapes of all these functions are correct: assert () == Coefficient(f_space).ufl_shape assert (d,) == Coefficient(v_space).ufl_shape assert (d + 1,) == Coefficient(w_space).ufl_shape assert (d, d) == Coefficient(t_space).ufl_shape assert (d, d) == Coefficient(s_space).ufl_shape # sum of value sizes, not accounting for symmetries: assert (3 * d * d + 2 * d + 2,) == Coefficient(m_space).ufl_shape # Shapes of subelements are reproduced: g = Coefficient(m_space) (s,) = g.ufl_shape for g2 in split(g): s -= product(g2.ufl_shape) assert s == 0 # Mixed elements of non-scalar subelements are flattened v2 = MixedElement([v, v]) m2 = MixedElement([t, t]) v2_space = FunctionSpace(domain, v2) m2_space = FunctionSpace(domain, m2) # assert d == 2 # assert (2,2) == Coefficient(v2_space).ufl_shape assert (d + d,) == Coefficient(v2_space).ufl_shape assert (2 * d * d,) == Coefficient(m2_space).ufl_shape # Split simple element t = TestFunction(f_space) assert split(t) == (t,) # Split twice on nested mixed elements gets # the innermost scalar subcomponents t = TestFunction(FunctionSpace(domain, MixedElement([f, v]))) assert split(t) == (t[0], as_vector((t[1], t[2]))) assert split(split(t)[1]) == (t[1], t[2]) t = TestFunction(FunctionSpace(domain, MixedElement([f, [f, v]]))) assert split(t) == (t[0], as_vector((t[1], t[2], t[3]))) assert split(split(t)[1]) == (t[1], as_vector((t[2], t[3]))) t = TestFunction(FunctionSpace(domain, MixedElement([[v, f], [f, v]]))) assert split(t) == (as_vector((t[0], t[1], t[2])), as_vector((t[3], t[4], t[5]))) assert split(split(t)[0]) == (as_vector((t[0], t[1])), t[2]) assert split(split(t)[1]) == (t[3], as_vector((t[4], t[5]))) assert split(split(split(t)[0])[0]) == (t[0], t[1]) assert split(split(split(t)[0])[1]) == (t[2],) assert split(split(split(t)[1])[0]) == (t[3],) assert split(split(split(t)[1])[1]) == (t[4], t[5]) ufl-2024.2.0/test/test_str.py000077500000000000000000000101301470142567200157350ustar00rootroot00000000000000from ufl import ( CellDiameter, CellVolume, Circumradius, FacetArea, FacetNormal, FunctionSpace, Index, Mesh, SpatialCoordinate, TestFunction, TrialFunction, as_matrix, as_ufl, as_vector, quadrilateral, tetrahedron, triangle, ) from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 def test_str_int_value(self): assert str(as_ufl(3)) == "3" def test_str_float_value(self): assert str(as_ufl(3.14)) == "3.14" def test_str_zero(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x = SpatialCoordinate(domain) assert str(as_ufl(0)) == "0" assert str(0 * x) == "0 (shape (2,))" assert str(0 * x * x[Index(42)]) == "0 (shape (2,), index labels (42,))" def test_str_index(self): assert str(Index(3)) == "i_3" assert str(Index(42)) == "i_{42}" def test_str_coordinate(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(SpatialCoordinate(domain)) == "x" assert str(SpatialCoordinate(domain)[0]) == "x[0]" def test_str_normal(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(FacetNormal(domain)) == "n" assert str(FacetNormal(domain)[0]) == "n[0]" def test_str_circumradius(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(Circumradius(domain)) == "circumradius" def test_str_diameter(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(CellDiameter(domain)) == "diameter" def test_str_facetarea(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(FacetArea(domain)) == "facetarea" def test_str_volume(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert str(CellVolume(domain)) == "volume" def test_str_scalar_argument(self): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) v = TestFunction( FunctionSpace(domain, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) ) u = TrialFunction( FunctionSpace(domain, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) ) assert str(v) == "v_0" assert str(u) == "v_1" # def test_str_vector_argument(self): # FIXME # def test_str_scalar_coefficient(self): # FIXME # def test_str_vector_coefficient(self): # FIXME def test_str_list_vector(): domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) x, y, z = SpatialCoordinate(domain) v = as_vector((x, y, z)) assert str(v) == ("[%s, %s, %s]" % (x, y, z)) def test_str_list_vector_with_zero(): domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) x, y, z = SpatialCoordinate(domain) v = as_vector((x, 0, 0)) assert str(v) == ("[%s, 0, 0]" % (x,)) def test_str_list_matrix(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x, y = SpatialCoordinate(domain) v = as_matrix(((2 * x, 3 * y), (4 * x, 5 * y))) a = str(2 * x) b = str(3 * y) c = str(4 * x) d = str(5 * y) assert str(v) == ("[\n [%s, %s],\n [%s, %s]\n]" % (a, b, c, d)) def test_str_list_matrix_with_zero(): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) x, y = SpatialCoordinate(domain) v = as_matrix(((2 * x, 3 * y), (0, 0))) a = str(2 * x) b = str(3 * y) c = str(as_vector((0, 0))) assert str(v) == ("[\n [%s, %s],\n%s\n]" % (a, b, c)) # FIXME: Add more tests for tensors collapsing # partly or completely into Zero! def test_str_element(): elem = FiniteElement("Q", quadrilateral, 1, (), identity_pullback, H1) assert ( repr(elem) == 'ufl.finiteelement.FiniteElement("Q", quadrilateral, 1, (), IdentityPullback(), H1)' ) assert str(elem) == "" ufl-2024.2.0/test/test_strip_forms.py000066400000000000000000000065671470142567200175140ustar00rootroot00000000000000import gc import sys from ufl import ( Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle, ) from ufl.algorithms import replace_terminal_data, strip_terminal_data from ufl.core.ufl_id import attach_ufl_id from ufl.core.ufl_type import UFLObject from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 MIN_REF_COUNT = 2 """The minimum value returned by sys.getrefcount.""" @attach_ufl_id class AugmentedMesh(Mesh, UFLObject): def __init__(self, *args, data): super().__init__(*args) self.data = data class AugmentedFunctionSpace(FunctionSpace, UFLObject): def __init__(self, *args, data): super().__init__(*args) self.data = data class AugmentedCoefficient(Coefficient): def __init__(self, *args, data): super().__init__(*args) self.data = data class AugmentedConstant(Constant): def __init__(self, *args, data): super().__init__(*args) self.data = data def test_strip_form_arguments_strips_data_refs(): mesh_data = object() fs_data = object() coeff_data = object() const_data = object() # Sanity check assert sys.getrefcount(mesh_data) == MIN_REF_COUNT assert sys.getrefcount(fs_data) == MIN_REF_COUNT assert sys.getrefcount(coeff_data) == MIN_REF_COUNT assert sys.getrefcount(const_data) == MIN_REF_COUNT cell = triangle domain = AugmentedMesh( FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1), data=mesh_data ) element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = AugmentedFunctionSpace(domain, element, data=fs_data) v = TestFunction(V) u = TrialFunction(V) f = AugmentedCoefficient(V, data=coeff_data) k = AugmentedConstant(V, data=const_data) form = k * f * inner(grad(v), grad(u)) * dx # Remove extraneous references del cell, domain, element, V, v, u, f, k assert sys.getrefcount(mesh_data) == MIN_REF_COUNT + 1 assert sys.getrefcount(fs_data) == MIN_REF_COUNT + 1 assert sys.getrefcount(coeff_data) == MIN_REF_COUNT + 1 assert sys.getrefcount(const_data) == MIN_REF_COUNT + 1 stripped_form, mapping = strip_terminal_data(form) del form, mapping gc.collect() # This is needed to update the refcounts assert sys.getrefcount(mesh_data) == MIN_REF_COUNT assert sys.getrefcount(fs_data) == MIN_REF_COUNT assert sys.getrefcount(coeff_data) == MIN_REF_COUNT assert sys.getrefcount(const_data) == MIN_REF_COUNT def test_strip_form_arguments_does_not_change_form(): mesh_data = object() fs_data = object() coeff_data = object() const_data = object() cell = triangle domain = AugmentedMesh( FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1), data=mesh_data ) element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) V = AugmentedFunctionSpace(domain, element, data=fs_data) v = TestFunction(V) u = TrialFunction(V) f = AugmentedCoefficient(V, data=coeff_data) k = AugmentedConstant(V, data=const_data) form = k * f * inner(grad(v), grad(u)) * dx stripped_form, mapping = strip_terminal_data(form) assert stripped_form.signature() == form.signature() assert replace_terminal_data(stripped_form, mapping) == form ufl-2024.2.0/test/test_tensoralgebra.py000077500000000000000000000120031470142567200177560ustar00rootroot00000000000000"""Test tensor algebra operators.""" import pytest from ufl import ( FacetNormal, Mesh, as_matrix, as_tensor, as_vector, cofac, cross, det, dev, diag, diag_vector, dot, inner, inv, outer, perp, skew, sym, tr, transpose, triangle, zero, ) from ufl.algorithms.remove_complex_nodes import remove_complex_nodes from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 @pytest.fixture(scope="module") def A(): return as_matrix([[2, 3], [4, 5]]) @pytest.fixture(scope="module") def B(): return as_matrix([[6, 7], [8, 9]]) @pytest.fixture(scope="module") def u(): return as_vector([10, 20]) @pytest.fixture(scope="module") def v(): return as_vector([30, 40]) def test_repeated_as_tensor(self, A, B, u, v): A2 = as_tensor(A) B2 = as_matrix(B) u2 = as_tensor(u) v2 = as_vector(v) assert A2 == A assert B2 == B assert u2 == u assert v2 == v def test_outer(self, A, B, u, v): C = outer(u, v) D = as_matrix([[10 * 30, 10 * 40], [20 * 30, 20 * 40]]) self.assertEqualValues(C, D) C = outer(A, v) A, v = A, v dims = (0, 1) D = as_tensor([[[A[i, j] * v[k] for k in dims] for j in dims] for i in dims]) self.assertEqualValues(C, D) # TODO: Test other ranks def test_inner(self, A, B, u, v): C = inner(A, B) D = 2 * 6 + 3 * 7 + 4 * 8 + 5 * 9 self.assertEqualValues(C, D) C = inner(u, v) D = 10 * 30 + 20 * 40 self.assertEqualValues(C, D) def test_pow2_inner(self, A, u): domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) f = FacetNormal(domain)[0] f2 = f * f assert f2 == remove_complex_nodes(inner(f, f)) u2 = u**2 assert u2 == remove_complex_nodes(inner(u, u)) A2 = A**2 assert A2 == remove_complex_nodes(inner(A, A)) # Only tensor**2 notation is supported: self.assertRaises(BaseException, lambda: A**3) def test_dot(self, A, B, u, v): C = dot(u, v) D = 10 * 30 + 20 * 40 self.assertEqualValues(C, D) C = dot(A, B) dims = (0, 1) D = as_matrix([[sum(A[i, k] * B[k, j] for k in dims) for j in dims] for i in dims]) self.assertEqualValues(C, D) def test_cross(self): u = as_vector([3, 3, 3]) v = as_vector([2, 2, 2]) C = cross(u, v) D = zero(3) self.assertEqualValues(C, D) u = as_vector([3, 3, 0]) v = as_vector([-2, 2, 0]) C = cross(u, v) z = det(as_matrix([[3, 3], [-2, 2]])) D = as_vector([0, 0, z]) self.assertEqualValues(C, D) def test_perp(self): # Test perp is generally doing the correct thing u = as_vector([3, 1]) v = perp(u) w = as_vector([-1, 3]) self.assertEqualValues(v, w) # Test that a perp does the correct thing to Zero u = zero(2) v = perp(u) self.assertEqualValues(u, v) # Test that perp throws an error if the wrong thing is provided u = as_vector([3, 1, -1]) # 3D vector instead of 2D with pytest.raises(ValueError): v = perp(u) u = as_matrix([[1, 3], [0, 4]]) # Matrix instead of vector with pytest.raises(ValueError): v = perp(u) def xtest_dev(self, A): C = dev(A) D = 0 * C # FIXME: Add expected value here self.assertEqualValues(C, D) def test_skew(self, A): C = skew(A) A, dims = A, (0, 1) D = 0.5 * as_matrix([[A[i, j] - A[j, i] for j in dims] for i in dims]) self.assertEqualValues(C, D) def test_sym(self, A): C = sym(A) A, dims = A, (0, 1) D = 0.5 * as_matrix([[A[i, j] + A[j, i] for j in dims] for i in dims]) self.assertEqualValues(C, D) def test_transpose(self, A): C = transpose(A) dims = (0, 1) D = as_matrix([[A[j, i] for j in dims] for i in dims]) self.assertEqualValues(C, D) def test_diag(self, A, u): dims = (0, 1) C = diag(A) D = as_matrix([[(0 if i != j else A[i, i]) for j in dims] for i in dims]) self.assertEqualValues(C, D) C = diag(u) D = as_matrix([[(0 if i != j else u[i]) for j in dims] for i in dims]) self.assertEqualValues(C, D) def test_diag_vector(self, A): dims = (0, 1) C = diag_vector(A) D = as_vector([A[i, i] for i in dims]) self.assertEqualValues(C, D) def test_tr(self, A): C = tr(A) A, dims = A, (0, 1) D = sum(A[i, i] for i in dims) self.assertEqualValues(C, D) def test_det(self, A): dims = (0, 1) C = det(A) D = sum((-A[i, 0] * A[0, i] if i != 0 else A[i - 1, -1] * A[i, 0]) for i in dims) self.assertEqualValues(C, D) def test_cofac(self, A): C = cofac(A) D = as_matrix([[(-A[i, j] if i != j else A[i, j]) for j in (-1, 0)] for i in (-1, 0)]) self.assertEqualValues(C, D) def xtest_inv(self, A): # FIXME: Test fails probably due to integer division C = inv(A) detA = sum((-A[i, 0] * A[0, i] if i != 0 else A[i - 1, -1] * A[i, 0]) for i in (0, 1)) D = as_matrix([[(-A[i, j] if i != j else A[i, j]) for j in (-1, 0)] for i in (-1, 0)]) / detA self.assertEqualValues(C, D) ufl-2024.2.0/test/test_utilities.py000077500000000000000000000113301470142567200171430ustar00rootroot00000000000000"""Test internal utility functions.""" from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index def test_shape_to_strides(): assert () == shape_to_strides(()) assert (1,) == shape_to_strides((3,)) assert (2, 1) == shape_to_strides((3, 2)) assert (4, 1) == shape_to_strides((3, 4)) assert (12, 4, 1) == shape_to_strides((6, 3, 4)) def test_flatten_multiindex_to_multiindex(): sh = (2, 3, 5) strides = shape_to_strides(sh) for i in range(sh[2]): for j in range(sh[1]): for k in range(sh[0]): index = (k, j, i) c = flatten_multiindex(index, strides) index2 = unflatten_index(c, strides) assert index == index2 def test_indexing_to_component(): assert 0 == flatten_multiindex((), shape_to_strides(())) assert 0 == flatten_multiindex((0,), shape_to_strides((2,))) assert 1 == flatten_multiindex((1,), shape_to_strides((2,))) assert 3 == flatten_multiindex((1, 1), shape_to_strides((2, 2))) for i in range(5): for j in range(3): for k in range(2): assert 15 * k + 5 * j + i == flatten_multiindex( (k, j, i), shape_to_strides((2, 3, 5)) ) def test_component_numbering(): from ufl.permutation import build_component_numbering sh = (2, 2) sm = {(1, 0): (0, 1)} v, s = build_component_numbering(sh, sm) assert v == {(0, 1): 1, (1, 0): 1, (0, 0): 0, (1, 1): 2} assert s == [(0, 0), (0, 1), (1, 1)] sh = (3, 3) sm = {(1, 0): (0, 1), (2, 0): (0, 2), (2, 1): (1, 2)} v, s = build_component_numbering(sh, sm) assert v == { (0, 1): 1, (1, 2): 4, (0, 0): 0, (2, 1): 4, (1, 1): 3, (2, 0): 2, (2, 2): 5, (1, 0): 1, (0, 2): 2, } assert s == [(0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 2)] def test_index_flattening(): from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index # Scalar shape s = () st = shape_to_strides(s) assert st == () c = () q = flatten_multiindex(c, st) c2 = unflatten_index(q, st) assert q == 0 assert c2 == () # Vector shape s = (2,) st = shape_to_strides(s) assert st == (1,) for i in range(s[0]): c = (i,) q = flatten_multiindex(c, st) c2 = unflatten_index(q, st) assert c == c2 # Tensor shape s = (2, 3) st = shape_to_strides(s) assert st == (3, 1) for i in range(s[0]): for j in range(s[1]): c = (i, j) q = flatten_multiindex(c, st) c2 = unflatten_index(q, st) assert c == c2 # Rank 3 tensor shape s = (2, 3, 4) st = shape_to_strides(s) assert st == (12, 4, 1) for i in range(s[0]): for j in range(s[1]): for k in range(s[2]): c = (i, j, k) q = flatten_multiindex(c, st) c2 = unflatten_index(q, st) assert c == c2 # Taylor-Hood example: # pressure element is index 3: c = (3,) # get flat index: i = flatten_multiindex(c, shape_to_strides((4,))) # remove offset: i -= 3 # map back to scalar component: c2 = unflatten_index(i, shape_to_strides(())) assert () == c2 # vector element y-component is index 1: c = (1,) # get flat index: i = flatten_multiindex(c, shape_to_strides((4,))) # remove offset: i -= 0 # map back to vector component: c2 = unflatten_index(i, shape_to_strides((3,))) assert (1,) == c2 # Try a tensor/vector element: mixed_shape = (6,) ts = (2, 2) vs = (2,) offset = 4 # product(ts) # vector element y-component is index offset+1: c = (offset + 1,) # get flat index: i = flatten_multiindex(c, shape_to_strides(mixed_shape)) # remove offset: i -= offset # map back to vector component: c2 = unflatten_index(i, shape_to_strides(vs)) assert (1,) == c2 for k in range(4): # tensor element (1,1)-component is index 3: c = (k,) # get flat index: i = flatten_multiindex(c, shape_to_strides(mixed_shape)) # remove offset: i -= 0 # map back to tensor component: c2 = unflatten_index(i, shape_to_strides(ts)) assert (k // 2, k % 2) == c2 def test_stackdict(): from ufl.utils.stacks import StackDict d = StackDict(a=1) assert d["a"] == 1 d.push("a", 2) assert d["a"] == 2 d.push("a", 3) d.push("b", 9) assert d["a"] == 3 assert d["b"] == 9 d.pop() assert d["a"] == 3 assert "b" not in d d.pop() assert d["a"] == 2 d.pop() assert d["a"] == 1 ufl-2024.2.0/ufl/000077500000000000000000000000001470142567200133255ustar00rootroot00000000000000ufl-2024.2.0/ufl/__init__.py000066400000000000000000000247041470142567200154450ustar00rootroot00000000000000"""UFL: The Unified Form Language. The Unified Form Language is an embedded domain specific language for definition of variational forms intended for finite element discretization. More precisely, it defines a fixed interface for choosing finite element spaces and defining expressions for weak forms in a notation close to the mathematical one. This Python module contains the language as well as algorithms to work with it. * To import the language, type:: import ufl * To import the underlying classes an UFL expression tree is built from, type :: import ufl.classes * Various algorithms for working with UFL expression trees can be accessed by :: import ufl.algorithms Classes and algorithms are considered implementation details and should not be used in form definitions. For more details on the language, see http://www.fenicsproject.org and http://arxiv.org/abs/1211.4047 The development version can be found in the repository at https://github.com/FEniCS/ufl A very brief overview of the language contents follows: * Cells:: -AbstractCell -Cell -TensorProductCell -vertex -interval -triangle -tetrahedron -quadrilateral -hexahedron -prism -pyramid -pentatope -tesseract * Domains:: -AbstractDomain -Mesh -MeshView * Sobolev spaces:: -L2 -H1 -H2 -HInf -HDiv -HCurl -HEin -HDivDiv * Pull backs:: -identity_pullback -contravariant_piola -covariant_piola -l2_piola -double_contravariant_piola -double_covariant_piola * Function spaces:: -FunctionSpace -MixedFunctionSpace * Arguments:: -Argument -TestFunction -TrialFunction -Arguments -TestFunctions -TrialFunctions * Coefficients:: -Coefficient -Constant -VectorConstant -TensorConstant * Splitting form arguments in mixed spaces:: -split * Literal constants:: -Identity -PermutationSymbol * Geometric quantities:: -SpatialCoordinate -FacetNormal -CellNormal -CellVolume -CellDiameter -Circumradius -MinCellEdgeLength -MaxCellEdgeLength -FacetArea -MinFacetEdgeLength -MaxFacetEdgeLength -Jacobian -JacobianDeterminant -JacobianInverse * Indices:: -Index -indices -i, j, k, l -p, q, r, s * Scalar to tensor expression conversion:: -as_tensor -as_vector -as_matrix * Unit vectors and matrices:: -unit_vector -unit_vectors -unit_matrix -unit_matrices * Tensor algebra operators:: -outer, inner, dot, cross, perp -det, inv, cofac -transpose, tr, diag, diag_vector -dev, skew, sym * Elementwise tensor operators:: -elem_mult -elem_div -elem_pow -elem_op * Differential operators:: -variable (-diff,) -grad, nabla_grad -div, nabla_div -curl, rot -Dx, Dn * Nonlinear functions:: -max_value, min_value -abs, sign -sqrt -exp, ln, erf -cos, sin, tan -acos, asin, atan, atan2 -cosh, sinh, tanh -bessel_J, bessel_Y, bessel_I, bessel_K * Complex operations:: - conj, real, imag conjugate is an alias for conj * Discontinuous Galerkin operators:: -v("+"), v("-") -jump -avg -cell_avg, facet_avg * Conditional operators:: - eq, ne, le, ge, lt, gt - <, >, <=, >= - And, Or, Not - conditional * Integral measures:: -dx, ds, dS, dP -dc, dC, dO, dI, dX -ds_b, ds_t, ds_tb, ds_v, dS_h, dS_v * Form transformations:: -rhs, lhs -system -functional -replace -adjoint -action (-energy_norm,) -sensitivity_rhs -derivative """ # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Kristian B. Oelgaard, 2009, 2011 # Modified by Anders Logg, 2009. # Modified by Johannes Ring, 2014. # Modified by Andrew T. T. McRae, 2014 # Modified by Lawrence Mitchell, 2014 # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 # Modified by Nacime Bouziani, 2019 import importlib.metadata __version__ = importlib.metadata.version("fenics-ufl") from math import e, pi from ufl.action import Action from ufl.adjoint import Adjoint from ufl.argument import ( Argument, Arguments, Coargument, TestFunction, TestFunctions, TrialFunction, TrialFunctions, ) from ufl.cell import AbstractCell, Cell, TensorProductCell, as_cell from ufl.coefficient import Coefficient, Coefficients, Cofunction from ufl.constant import Constant, TensorConstant, VectorConstant from ufl.constantvalue import Identity, PermutationSymbol, as_ufl, zero from ufl.core.external_operator import ExternalOperator from ufl.core.interpolate import Interpolate, interpolate from ufl.core.multiindex import Index, indices from ufl.domain import AbstractDomain, Mesh, MeshView from ufl.finiteelement import AbstractFiniteElement from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm from ufl.formoperators import ( action, adjoint, derivative, energy_norm, extract_blocks, functional, lhs, replace, rhs, sensitivity_rhs, system, ) from ufl.functionspace import FunctionSpace, MixedFunctionSpace from ufl.geometry import ( CellDiameter, CellNormal, CellVolume, Circumradius, FacetArea, FacetNormal, Jacobian, JacobianDeterminant, JacobianInverse, MaxCellEdgeLength, MaxFacetEdgeLength, MinCellEdgeLength, MinFacetEdgeLength, SpatialCoordinate, ) from ufl.integral import Integral from ufl.matrix import Matrix from ufl.measure import Measure, custom_integral_types, integral_types, register_integral_type from ufl.objects import ( dC, dc, dI, dO, dP, dS, ds, ds_b, dS_h, ds_t, ds_tb, dS_v, ds_v, dX, dx, facet, hexahedron, i, interval, j, k, l, p, pentatope, prism, pyramid, q, quadrilateral, r, s, tesseract, tetrahedron, triangle, vertex, ) from ufl.operators import ( And, Dn, Dx, Not, Or, acos, asin, atan, atan2, avg, bessel_I, bessel_J, bessel_K, bessel_Y, cell_avg, cofac, conditional, conj, cos, cosh, cross, curl, det, dev, diag, diag_vector, diff, div, dot, elem_div, elem_mult, elem_op, elem_pow, eq, erf, exp, exterior_derivative, facet_avg, ge, grad, gt, imag, inner, inv, jump, le, ln, lt, max_value, min_value, nabla_div, nabla_grad, ne, outer, perp, rank, real, rot, shape, sign, sin, sinh, skew, sqrt, sym, tan, tanh, tr, transpose, variable, ) from ufl.pullback import ( AbstractPullback, MixedPullback, SymmetricPullback, contravariant_piola, covariant_piola, double_contravariant_piola, double_covariant_piola, identity_pullback, l2_piola, ) from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HEin, HInf from ufl.split_functions import split from ufl.tensors import ( as_matrix, as_tensor, as_vector, unit_matrices, unit_matrix, unit_vector, unit_vectors, ) from ufl.utils.sequences import product __all__ = [ "product", "as_cell", "AbstractCell", "Cell", "TensorProductCell", "AbstractDomain", "Mesh", "MeshView", "L2", "H1", "H2", "HCurl", "HDiv", "HInf", "HEin", "HDivDiv", "identity_pullback", "l2_piola", "contravariant_piola", "covariant_piola", "double_contravariant_piola", "double_covariant_piola", "l2_piola", "MixedPullback", "SymmetricPullback", "AbstractPullback", "SpatialCoordinate", "CellVolume", "CellDiameter", "Circumradius", "MinCellEdgeLength", "MaxCellEdgeLength", "FacetArea", "MinFacetEdgeLength", "MaxFacetEdgeLength", "FacetNormal", "CellNormal", "Jacobian", "JacobianDeterminant", "JacobianInverse", "AbstractFiniteElement", "FunctionSpace", "MixedFunctionSpace", "Argument", "Coargument", "TestFunction", "TrialFunction", "Arguments", "TestFunctions", "TrialFunctions", "Coefficient", "Cofunction", "Coefficients", "Matrix", "Adjoint", "Action", "Interpolate", "interpolate", "ExternalOperator", "Constant", "VectorConstant", "TensorConstant", "split", "PermutationSymbol", "Identity", "zero", "as_ufl", "Index", "indices", "as_tensor", "as_vector", "as_matrix", "unit_vector", "unit_vectors", "unit_matrix", "unit_matrices", "rank", "shape", "conj", "real", "imag", "outer", "inner", "dot", "cross", "perp", "det", "inv", "cofac", "transpose", "tr", "diag", "diag_vector", "dev", "skew", "sym", "sqrt", "exp", "ln", "erf", "cos", "sin", "tan", "acos", "asin", "atan", "atan2", "cosh", "sinh", "tanh", "bessel_J", "bessel_Y", "bessel_I", "bessel_K", "eq", "ne", "le", "ge", "lt", "gt", "And", "Or", "Not", "conditional", "sign", "max_value", "min_value", "variable", "diff", "Dx", "grad", "div", "curl", "rot", "nabla_grad", "nabla_div", "Dn", "exterior_derivative", "jump", "avg", "cell_avg", "facet_avg", "elem_mult", "elem_div", "elem_pow", "elem_op", "Form", "BaseForm", "FormSum", "ZeroBaseForm", "Integral", "Measure", "register_integral_type", "integral_types", "custom_integral_types", "replace", "derivative", "action", "energy_norm", "rhs", "lhs", "extract_blocks", "system", "functional", "adjoint", "sensitivity_rhs", "dx", "ds", "dS", "dP", "dc", "dC", "dO", "dI", "dX", "ds_b", "ds_t", "ds_tb", "ds_v", "dS_h", "dS_v", "vertex", "interval", "triangle", "tetrahedron", "prism", "pyramid", "pentatope", "tesseract", "quadrilateral", "hexahedron", "facet", "i", "j", "k", "l", "p", "q", "r", "s", "e", "pi", ] ufl-2024.2.0/ufl/action.py000066400000000000000000000200601470142567200151520ustar00rootroot00000000000000"""This module defines the Action class.""" # Copyright (C) 2021 India Marsden # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2021-2022. from itertools import chain from ufl.algebra import Sum from ufl.argument import Argument, Coargument from ufl.coefficient import BaseCoefficient, Coefficient, Cofunction from ufl.constantvalue import Zero from ufl.core.base_form_operator import BaseFormOperator from ufl.core.ufl_type import ufl_type from ufl.differentiation import CoefficientDerivative from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm from ufl.matrix import Matrix # --- The Action class represents the action of a numerical object that needs # to be computed at assembly time --- @ufl_type() class Action(BaseForm): """UFL base form type: respresents the action of an object on another. For example: res = Ax A would be the first argument, left and x would be the second argument, right. Action objects will result when the action of an assembled object (e.g. a Matrix) is taken. This delays the evaluation of the action until assembly occurs. """ __slots__ = ( "_left", "_right", "ufl_operands", "_repr", "_arguments", "_coefficients", "_domains", "_hash", ) def __new__(cls, *args, **kw): """Create a new Action.""" left, right = args # Check trivial case if left == 0 or right == 0: if isinstance(left, Zero): # There is no point in checking the action arguments # if `left` is a `ufl.Zero` as those objects don't have arguments. # We can also not reliably determine the `ZeroBaseForm` arguments. return ZeroBaseForm(()) # Still need to work out the ZeroBaseForm arguments. new_arguments, _ = _get_action_form_arguments(left, right) return ZeroBaseForm(new_arguments) # Coarguments (resp. Argument) from V* to V* (resp. from V to V) are identity matrices, # i.e. we have: V* x V -> R (resp. V x V* -> R). if isinstance(left, (Coargument, Argument)): return right if isinstance(right, (Coargument, Argument)): return left if isinstance(left, (FormSum, Sum)): # Action distributes over sums on the LHS return FormSum(*[(Action(component, right), 1) for component in left.ufl_operands]) if isinstance(right, (FormSum, Sum)): # Action also distributes over sums on the RHS return FormSum(*[(Action(left, component), 1) for component in right.ufl_operands]) return super(Action, cls).__new__(cls) def __init__(self, left, right): """Initialise.""" BaseForm.__init__(self) self._left = left self._right = right self.ufl_operands = (self._left, self._right) self._domains = None # Check compatibility of function spaces _check_function_spaces(left, right) self._repr = "Action(%s, %s)" % (repr(self._left), repr(self._right)) self._hash = None def ufl_function_spaces(self): """Get the tuple of function spaces of the underlying form.""" if isinstance(self._right, Form): return self._left.ufl_function_spaces()[:-1] + self._right.ufl_function_spaces()[1:] elif isinstance(self._right, Coefficient): return self._left.ufl_function_spaces()[:-1] def left(self): """Get left.""" return self._left def right(self): """Get right.""" return self._right def _analyze_form_arguments(self): """Compute the Arguments of this Action. The highest number Argument of the left operand and the lowest number Argument of the right operand are consumed by the action. """ self._arguments, self._coefficients = _get_action_form_arguments(self._left, self._right) def _analyze_domains(self): """Analyze which domains can be found in Action.""" from ufl.domain import join_domains # Collect domains self._domains = join_domains( chain.from_iterable(e.ufl_domains() for e in self.ufl_operands) ) def ufl_domains(self): """Return all domains found in the base form.""" if self._domains is None: self._analyze_domains() return self._domains def equals(self, other): """Check if two Actions are equal.""" if type(other) is not Action: return False if self is other: return True # Make sure we are returning a boolean as left and right equalities can be `ufl.Equation`s # if the underlying objects are `ufl.BaseForm`. return bool(self._left == other._left) and bool(self._right == other._right) def __str__(self): """Format as a string.""" return f"Action({self._left}, {self._right})" def __repr__(self): """Representation.""" return self._repr def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(("Action", hash(self._right), hash(self._left))) return self._hash def _check_function_spaces(left, right): """Check if the function spaces of left and right match.""" if isinstance(right, CoefficientDerivative): # Action differentiation pushes differentiation through # right as a consequence of Leibniz formula. right, *_ = right.ufl_operands # `left` can also be a Coefficient in V (= V**), e.g. # `action(Coefficient(V), Cofunction(V.dual()))`. left_arg = left.arguments()[-1] if not isinstance(left, Coefficient) else left if isinstance(right, (Form, Action, Matrix, ZeroBaseForm)): if left_arg.ufl_function_space().dual() != right.arguments()[0].ufl_function_space(): raise TypeError("Incompatible function spaces in Action") elif isinstance(right, (Coefficient, Cofunction, Argument, BaseFormOperator)): if left_arg.ufl_function_space() != right.ufl_function_space(): raise TypeError("Incompatible function spaces in Action") # `Zero` doesn't contain any information about the function space. # -> Not a problem since Action will get simplified with a # `ZeroBaseForm` which won't take into account the arguments on # the right because of argument contraction. # This occurs for: # `derivative(Action(A, B), u)` with B is an `Expr` such that dB/du == 0 # -> `derivative(B, u)` becomes `Zero` when expanding derivatives since B is an Expr. elif not isinstance(right, Zero): raise TypeError("Incompatible argument in Action: %s" % type(right)) def _get_action_form_arguments(left, right): """Perform argument contraction to work out the arguments of Action.""" coefficients = () # `left` can also be a Coefficient in V (= V**), e.g. # `action(Coefficient(V), Cofunction(V.dual()))`. left_args = left.arguments()[:-1] if not isinstance(left, Coefficient) else () if isinstance(right, BaseForm): arguments = left_args + right.arguments()[1:] coefficients += right.coefficients() elif isinstance(right, CoefficientDerivative): # Action differentiation pushes differentiation through # right as a consequence of Leibniz formula. from ufl.algorithms.analysis import extract_arguments_and_coefficients right_args, right_coeffs = extract_arguments_and_coefficients(right) arguments = left_args + tuple(right_args) coefficients += tuple(right_coeffs) elif isinstance(right, (BaseCoefficient, Zero)): arguments = left_args # When right is ufl.Zero, Action gets simplified so updating # coefficients here doesn't matter coefficients += (right,) elif isinstance(right, Argument): arguments = left_args + (right,) else: raise TypeError if isinstance(left, BaseForm): coefficients += left.coefficients() return arguments, coefficients ufl-2024.2.0/ufl/adjoint.py000066400000000000000000000105021470142567200153250ustar00rootroot00000000000000"""This module defines the Adjoint class.""" # Copyright (C) 2021 India Marsden # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2021-2022. from itertools import chain from ufl.argument import Coargument from ufl.core.ufl_type import ufl_type from ufl.form import BaseForm, FormSum, ZeroBaseForm # --- The Adjoint class represents the adjoint of a numerical object that # needs to be computed at assembly time --- @ufl_type() class Adjoint(BaseForm): """UFL base form type: represents the adjoint of an object. Adjoint objects will result when the adjoint of an assembled object (e.g. a Matrix) is taken. This delays the evaluation of the adjoint until assembly occurs. """ __slots__ = ( "_form", "_repr", "_arguments", "_coefficients", "_domains", "ufl_operands", "_hash", ) def __new__(cls, *args, **kw): """Create a new Adjoint.""" form = args[0] # Check trivial case: This is not a ufl.Zero but a ZeroBaseForm! if form == 0: # Swap the arguments return ZeroBaseForm(form.arguments()[::-1]) if isinstance(form, Adjoint): return form._form elif isinstance(form, FormSum): # Adjoint distributes over sums return FormSum(*[(Adjoint(component), 1) for component in form.components()]) elif isinstance(form, Coargument): # The adjoint of a coargument `c: V* -> V*` is the identity # matrix mapping from V to V (i.e. V x V* -> R). # Equivalently, the adjoint of `c` is its first argument, # which is a ufl.Argument defined on the primal space of # `c`. primal_arg, _ = form.arguments() # Returning the primal argument avoids explicit argument # reconstruction, making it a robust strategy for handling # subclasses of `ufl.Coargument`. return primal_arg return super(Adjoint, cls).__new__(cls) def __init__(self, form): """Initialise.""" BaseForm.__init__(self) if len(form.arguments()) != 2: raise ValueError("Can only take Adjoint of a 2-form.") self._form = form self.ufl_operands = (self._form,) self._domains = None self._hash = None self._repr = "Adjoint(%s)" % repr(self._form) def ufl_function_spaces(self): """Get the tuple of function spaces of the underlying form.""" return self._form.ufl_function_spaces() def form(self): """Return the form.""" return self._form def _analyze_form_arguments(self): """The arguments of adjoint are the reverse of the form arguments.""" reversed_args = self._form.arguments()[::-1] # Canonical numbering for arguments that is consistent with other BaseForm objects. self._arguments = tuple( type(arg)(arg.ufl_function_space(), number=i) for i, arg in enumerate(reversed_args) ) self._coefficients = self._form.coefficients() def _analyze_domains(self): """Analyze which domains can be found in Adjoint.""" from ufl.domain import join_domains # Collect unique domains self._domains = join_domains( chain.from_iterable(e.ufl_domains() for e in self.ufl_operands) ) def ufl_domains(self): """Return all domains found in the base form.""" if self._domains is None: self._analyze_domains() return self._domains def equals(self, other): """Check if two Adjoints are equal.""" if type(other) is not Adjoint: return False if self is other: return True # Make sure we are returning a boolean as the equality can # result in a `ufl.Equation` if the underlying objects are # `ufl.BaseForm`. return bool(self._form == other._form) def __str__(self): """Format as a string.""" return f"Adjoint({self._form})" def __repr__(self): """Representation.""" return self._repr def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(("Adjoint", hash(self._form))) return self._hash ufl-2024.2.0/ufl/algebra.py000066400000000000000000000336331470142567200153040ustar00rootroot00000000000000"""Basic algebra operations.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 from ufl.checks import is_true_ufl_scalar, is_ufl_scalar from ufl.constantvalue import ComplexValue, IntValue, ScalarValue, Zero, as_ufl, zero from ufl.core.expr import ufl_err_str from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.index_combination_utils import merge_unique_indices from ufl.precedence import parstr from ufl.sorting import sorted_expr # --- Algebraic operators --- @ufl_type( num_ops=2, inherit_shape_from_operand=0, inherit_indices_from_operand=0, binop="__add__", rbinop="__radd__", ) class Sum(Operator): """Sum.""" __slots__ = () def __new__(cls, a, b): """Create a new Sum.""" # Make sure everything is an Expr a = as_ufl(a) b = as_ufl(b) # Assert consistent tensor properties sh = a.ufl_shape fi = a.ufl_free_indices fid = a.ufl_index_dimensions if b.ufl_shape != sh: raise ValueError("Can't add expressions with different shapes.") if b.ufl_free_indices != fi: raise ValueError("Can't add expressions with different free indices.") if b.ufl_index_dimensions != fid: raise ValueError("Can't add expressions with different index dimensions.") # Skip adding zero if isinstance(a, Zero): return b elif isinstance(b, Zero): return a # Handle scalars specially and sort operands sa = isinstance(a, ScalarValue) sb = isinstance(b, ScalarValue) if sa and sb: # Apply constant propagation return as_ufl(a._value + b._value) elif sa: # Place scalar first # operands = (a, b) pass # a, b = a, b elif sb: # Place scalar first # operands = (b, a) a, b = b, a # elif a == b: # # Replace a+b with 2*foo # return 2*a else: # Otherwise sort operands in a canonical order # operands = (b, a) a, b = sorted_expr((a, b)) # construct and initialize a new Sum object self = Operator.__new__(cls) self._init(a, b) return self def _init(self, a, b): """Initialise.""" self.ufl_operands = (a, b) def __init__(self, a, b): """Initialise.""" Operator.__init__(self) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" return sum(o.evaluate(x, mapping, component, index_values) for o in self.ufl_operands) def __str__(self): """Format as a string.""" return " + ".join([parstr(o, self) for o in self.ufl_operands]) @ufl_type(num_ops=2, binop="__mul__", rbinop="__rmul__") class Product(Operator): """The product of two or more UFL objects.""" __slots__ = ("ufl_free_indices", "ufl_index_dimensions") def __new__(cls, a, b): """Create a new product.""" # Conversion a = as_ufl(a) b = as_ufl(b) # Type checking # Make sure everything is scalar if a.ufl_shape or b.ufl_shape: raise ValueError( "Product can only represent products of scalars, " f"got\n {ufl_err_str(a)}\nand\n {ufl_err_str(b)}" ) # Simplification if isinstance(a, Zero) or isinstance(b, Zero): # Got any zeros? Return zero. fi, fid = merge_unique_indices( a.ufl_free_indices, a.ufl_index_dimensions, b.ufl_free_indices, b.ufl_index_dimensions, ) return Zero((), fi, fid) sa = isinstance(a, ScalarValue) sb = isinstance(b, ScalarValue) if sa and sb: # const * const = const # FIXME: Handle free indices like with zero? I think # IntValue may be index annotated now? return as_ufl(a._value * b._value) elif sa: # 1 * b = b if a._value == 1: return b # a, b = a, b elif sb: # a * 1 = a if b._value == 1: return a a, b = b, a # elif a == b: # a * a = a**2 # TODO: Why? Maybe just remove this? # if not a.ufl_free_indices: # return a**2 else: # a * b = b * a # Sort operands in a semi-canonical order # (NB! This is fragile! Small changes here can have large effects.) a, b = sorted_expr((a, b)) # Construction self = Operator.__new__(cls) self._init(a, b) return self def _init(self, a, b): """Constructor, called by __new__ with already checked arguments.""" self.ufl_operands = (a, b) # Extract indices fi, fid = merge_unique_indices( a.ufl_free_indices, a.ufl_index_dimensions, b.ufl_free_indices, b.ufl_index_dimensions ) self.ufl_free_indices = fi self.ufl_index_dimensions = fid def __init__(self, a, b): """Initialise.""" Operator.__init__(self) ufl_shape = () def evaluate(self, x, mapping, component, index_values): """Evaluate.""" ops = self.ufl_operands sh = self.ufl_shape if sh: if sh != ops[-1].ufl_shape: raise ValueError( "Expecting nonscalar product operand to be the last by convention." ) tmp = ops[-1].evaluate(x, mapping, component, index_values) ops = ops[:-1] else: tmp = 1 for o in ops: tmp *= o.evaluate(x, mapping, (), index_values) return tmp def __str__(self): """Format as a string.""" a, b = self.ufl_operands return " * ".join((parstr(a, self), parstr(b, self))) @ufl_type(num_ops=2, inherit_indices_from_operand=0, binop="__div__", rbinop="__rdiv__") class Division(Operator): """Division.""" __slots__ = () def __new__(cls, a, b): """Create a new Division.""" # Conversion a = as_ufl(a) b = as_ufl(b) # Type checking # TODO: Enabled workaround for nonscalar division in __div__, # so maybe we can keep this assertion. Some algorithms may # need updating. if not is_ufl_scalar(a): raise ValueError("Expecting scalar nominator in Division.") if not is_true_ufl_scalar(b): raise ValueError("Division by non-scalar is undefined.") if isinstance(b, Zero): raise ValueError("Division by zero!") # Simplification # Simplification a/b -> a if isinstance(a, Zero) or (isinstance(b, ScalarValue) and b._value == 1): return a # Simplification "literal a / literal b" -> "literal value of # a/b". Avoiding integer division by casting to float if isinstance(a, ScalarValue) and isinstance(b, ScalarValue): try: return as_ufl(float(a._value) / float(b._value)) except TypeError: return as_ufl(complex(a._value) / complex(b._value)) # Simplification "a / a" -> "1" # if not a.ufl_free_indices and not a.ufl_shape and a == b: # return as_ufl(1) # Construction self = Operator.__new__(cls) self._init(a, b) return self def _init(self, a, b): """Initialise.""" self.ufl_operands = (a, b) def __init__(self, a, b): """Initialise.""" Operator.__init__(self) ufl_shape = () # self.ufl_operands[0].ufl_shape def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a, b = self.ufl_operands a = a.evaluate(x, mapping, component, index_values) b = b.evaluate(x, mapping, component, index_values) # Avoiding integer division by casting to float try: e = float(a) / float(b) except TypeError: e = complex(a) / complex(b) return e def __str__(self): """Format as a string.""" return f"{parstr(self.ufl_operands[0], self)} / {parstr(self.ufl_operands[1], self)}" @ufl_type(num_ops=2, inherit_indices_from_operand=0, binop="__pow__", rbinop="__rpow__") class Power(Operator): """Power.""" __slots__ = () def __new__(cls, a, b): """Create new Power.""" # Conversion a = as_ufl(a) b = as_ufl(b) # Type checking if not is_true_ufl_scalar(a): raise ValueError(f"Cannot take the power of a non-scalar expression {ufl_err_str(a)}.") if not is_true_ufl_scalar(b): raise ValueError(f"Cannot raise an expression to a non-scalar power {ufl_err_str(b)}.") # Simplification if isinstance(a, ScalarValue) and isinstance(b, ScalarValue): return as_ufl(a._value**b._value) if isinstance(b, Zero): return IntValue(1) if isinstance(a, Zero) and isinstance(b, ScalarValue): if isinstance(b, ComplexValue): raise ValueError("Cannot raise zero to a complex power.") bf = float(b) if bf < 0: raise ValueError("Division by zero, cannot raise 0 to a negative power.") else: return zero() if isinstance(b, ScalarValue) and b._value == 1: return a # Construction self = Operator.__new__(cls) self._init(a, b) return self def _init(self, a, b): """Initialise.""" self.ufl_operands = (a, b) def __init__(self, a, b): """Initialise.""" Operator.__init__(self) ufl_shape = () def evaluate(self, x, mapping, component, index_values): """Evalute.""" a, b = self.ufl_operands a = a.evaluate(x, mapping, component, index_values) b = b.evaluate(x, mapping, component, index_values) return a**b def __str__(self): """Format as a string.""" a, b = self.ufl_operands return f"{parstr(a, self)} ** {parstr(b, self)}" @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0, unop="__abs__") class Abs(Operator): """Absolute value.""" __slots__ = () def __new__(cls, a): """Create a new Abs.""" a = as_ufl(a) # Simplification if isinstance(a, (Zero, Abs)): return a if isinstance(a, Conj): return Abs(a.ufl_operands[0]) if isinstance(a, ScalarValue): return as_ufl(abs(a._value)) return Operator.__new__(cls) def __init__(self, a): """Initialise.""" Operator.__init__(self, (a,)) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return abs(a) def __str__(self): """Format as a string.""" (a,) = self.ufl_operands return f"|{parstr(a, self)}|" @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Conj(Operator): """Complex conjugate.""" __slots__ = () def __new__(cls, a): """Creatr a new Conj.""" a = as_ufl(a) # Simplification if isinstance(a, (Abs, Real, Imag, Zero)): return a if isinstance(a, Conj): return a.ufl_operands[0] if isinstance(a, ScalarValue): return as_ufl(a._value.conjugate()) return Operator.__new__(cls) def __init__(self, a): """Initialise.""" Operator.__init__(self, (a,)) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return a.conjugate() def __str__(self): """Format as a string.""" (a,) = self.ufl_operands return f"conj({parstr(a, self)})" @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Real(Operator): """Real part.""" __slots__ = () def __new__(cls, a): """Create a new Real.""" a = as_ufl(a) # Simplification if isinstance(a, Conj): a = a.ufl_operands[0] if isinstance(a, Zero): return a if isinstance(a, ScalarValue): return as_ufl(a.real()) if isinstance(a, Real): a = a.ufl_operands[0] return Operator.__new__(cls) def __init__(self, a): """Initialise.""" Operator.__init__(self, (a,)) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return a.real def __str__(self): """Format as a string.""" (a,) = self.ufl_operands return f"Re[{parstr(a, self)}]" @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Imag(Operator): """Imaginary part.""" __slots__ = () def __new__(cls, a): """Create a new Imag.""" a = as_ufl(a) # Simplification if isinstance(a, Zero): return a if isinstance(a, (Real, Imag, Abs)): return Zero(a.ufl_shape, a.ufl_free_indices, a.ufl_index_dimensions) if isinstance(a, ScalarValue): return as_ufl(a.imag()) return Operator.__new__(cls) def __init__(self, a): """Initialise.""" Operator.__init__(self, (a,)) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return a.imag def __str__(self): """Format as a string.""" (a,) = self.ufl_operands return f"Im[{parstr(a, self)}]" ufl-2024.2.0/ufl/algorithms/000077500000000000000000000000001470142567200154765ustar00rootroot00000000000000ufl-2024.2.0/ufl/algorithms/__init__.py000066400000000000000000000055731470142567200176210ustar00rootroot00000000000000"""This module collects algorithms and utility functions operating on UFL objects.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # FIXME: Clean up this to become a more official set of supported # algorithms. Currently contains too much stuff that's not # recommended to use. The __all__ list below is a start based # on grepping of other FEniCS code for ufl.algorithm imports. __all__ = [ "estimate_total_polynomial_degree", "sort_elements", "compute_form_data", "preprocess_form", "apply_transformer", "ReuseTransformer", "load_ufl_file", "Transformer", "MultiFunction", "extract_unique_elements", "extract_type", "extract_elements", "extract_sub_elements", "expand_indices", "replace", "expand_derivatives", "extract_coefficients", "extract_base_form_operators", "strip_variables", "strip_terminal_data", "replace_terminal_data", "post_traversal", "change_to_reference_grad", "validate_form", "FormSplitter", "extract_arguments", "compute_form_adjoint", "compute_form_action", "compute_energy_norm", "compute_form_lhs", "compute_form_rhs", "compute_form_functional", "compute_form_signature", "compute_form_arities", "tree_format", "read_ufl_file", "load_forms", ] from ufl.algorithms.ad import expand_derivatives from ufl.algorithms.analysis import ( extract_arguments, extract_base_form_operators, extract_coefficients, extract_elements, extract_sub_elements, extract_type, extract_unique_elements, sort_elements, ) from ufl.algorithms.change_to_reference import change_to_reference_grad from ufl.algorithms.checks import validate_form from ufl.algorithms.compute_form_data import compute_form_data, preprocess_form from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree from ufl.algorithms.expand_indices import expand_indices from ufl.algorithms.formfiles import load_forms, load_ufl_file, read_ufl_file from ufl.algorithms.formsplitter import FormSplitter from ufl.algorithms.formtransformations import ( compute_energy_norm, compute_form_action, compute_form_adjoint, compute_form_arities, compute_form_functional, compute_form_lhs, compute_form_rhs, ) from ufl.algorithms.replace import replace from ufl.algorithms.signature import compute_form_signature from ufl.algorithms.strip_terminal_data import replace_terminal_data, strip_terminal_data from ufl.algorithms.transformer import ( ReuseTransformer, Transformer, apply_transformer, strip_variables, ) from ufl.corealg.multifunction import MultiFunction from ufl.corealg.traversal import post_traversal from ufl.utils.formatting import tree_format ufl-2024.2.0/ufl/algorithms/ad.py000066400000000000000000000020601470142567200164320ustar00rootroot00000000000000"""Front-end for AD routines.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009. import warnings from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering from ufl.algorithms.apply_derivatives import apply_derivatives def expand_derivatives(form, **kwargs): """Expand all derivatives of expr. In the returned expression g which is mathematically equivalent to expr, there are no VariableDerivative or CoefficientDerivative objects left, and Grad objects have been propagated to Terminal nodes. """ # For a deprecation period (I see that dolfin-adjoint passes some # args here) if kwargs: warnings("Deprecation: expand_derivatives no longer takes any keyword arguments") # Lower abstractions for tensor-algebra types into index notation form = apply_algebra_lowering(form) # Apply differentiation form = apply_derivatives(form) return form ufl-2024.2.0/ufl/algorithms/analysis.py000066400000000000000000000227221470142567200177000ustar00rootroot00000000000000"""Utility algorithms for inspection of and information extraction from UFL objects.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010. # Modified by Johan Hake, 2010. from itertools import chain from ufl.algorithms.traversal import iter_expressions from ufl.argument import BaseArgument, Coargument from ufl.coefficient import BaseCoefficient from ufl.constant import Constant from ufl.core.base_form_operator import BaseFormOperator from ufl.core.terminal import Terminal from ufl.corealg.traversal import traverse_unique_terminals, unique_pre_traversal from ufl.form import BaseForm, Form from ufl.utils.sorting import sorted_by_count, topological_sorting # TODO: Some of these can possibly be optimised by implementing # inlined stack based traversal algorithms def _sorted_by_number_and_part(seq): """Sort items by number and part.""" return sorted(seq, key=lambda x: (x.number(), x.part())) def unique_tuple(objects): """Return tuple of unique objects, preserving initial ordering.""" unique_objects = [] handled = set() for obj in objects: if obj not in handled: handled.add(obj) unique_objects.append(obj) return tuple(unique_objects) # --- Utilities to extract information from an expression --- def extract_type(a, ufl_types): """Build a set of all objects found in a whose class is in ufl_types. Args: a: A BaseForm, Integral or Expr ufl_types: A list of UFL types Returns: All objects found in a whose class is in ufl_type """ if not isinstance(ufl_types, (list, tuple)): ufl_types = (ufl_types,) if all(t is not BaseFormOperator for t in ufl_types): remove_base_form_ops = True ufl_types += (BaseFormOperator,) else: remove_base_form_ops = False # BaseForms that aren't forms or base form operators # only contain arguments & coefficients if isinstance(a, BaseForm) and not isinstance(a, (Form, BaseFormOperator)): objects = set() arg_types = tuple(t for t in ufl_types if issubclass(t, BaseArgument)) if arg_types: objects.update([e for e in a.arguments() if isinstance(e, arg_types)]) coeff_types = tuple(t for t in ufl_types if issubclass(t, BaseCoefficient)) if coeff_types: objects.update([e for e in a.coefficients() if isinstance(e, coeff_types)]) return objects if all(issubclass(t, Terminal) for t in ufl_types): # Optimization objects = set( o for e in iter_expressions(a) for o in traverse_unique_terminals(e) if any(isinstance(o, t) for t in ufl_types) ) else: objects = set( o for e in iter_expressions(a) for o in unique_pre_traversal(e) if any(isinstance(o, t) for t in ufl_types) ) # Need to extract objects contained in base form operators whose # type is in ufl_types base_form_ops = set(e for e in objects if isinstance(e, BaseFormOperator)) ufl_types_no_args = tuple(t for t in ufl_types if not issubclass(t, BaseArgument)) base_form_objects = () for o in base_form_ops: # This accounts for having BaseFormOperator in Forms: if N is a BaseFormOperator # `N(u; v*) * v * dx` <=> `action(v1 * v * dx, N(...; v*))` # where `v`, `v1` are `Argument`s and `v*` a `Coargument`. for ai in tuple(arg for arg in o.argument_slots(isinstance(a, Form))): # Extracting BaseArguments of an object of which a # Coargument is an argument, then we just return the dual # argument of the Coargument and not its primal argument. if isinstance(ai, Coargument): new_types = tuple(Coargument if t is BaseArgument else t for t in ufl_types) base_form_objects += tuple(extract_type(ai, new_types)) else: base_form_objects += tuple(extract_type(ai, ufl_types)) # Look for BaseArguments in BaseFormOperator's argument slots # only since that's where they are by definition. Don't look # into operands, which is convenient for external operator # composition, e.g. N1(N2; v*) where N2 is seen as an operator # and not a form. slots = o.ufl_operands for ai in slots: base_form_objects += tuple(extract_type(ai, ufl_types_no_args)) objects.update(base_form_objects) # `Remove BaseFormOperator` objects if there were initially not in `ufl_types` if remove_base_form_ops: objects -= base_form_ops return objects def has_type(a, ufl_type): """Return if an object of class ufl_type or a subclass can be found in a. Args: a: A BaseForm, Integral or Expr ufl_type: A UFL type Returns: Whether an object of class ufl_type can be found in a """ if issubclass(ufl_type, Terminal): # Optimization traversal = traverse_unique_terminals else: traversal = unique_pre_traversal return any(isinstance(o, ufl_type) for e in iter_expressions(a) for o in traversal(e)) def has_exact_type(a, ufl_type): """Return if an object of class ufl_type can be found in a. Args: a: A BaseForm, Integral or Expr ufl_type: A UFL type Returns: Whether an object of class ufl_type can be found in a """ tc = ufl_type._ufl_typecode_ if issubclass(ufl_type, Terminal): # Optimization traversal = traverse_unique_terminals else: traversal = unique_pre_traversal return any(o._ufl_typecode_ == tc for e in iter_expressions(a) for o in traversal(e)) def extract_arguments(a): """Build a sorted list of all arguments in a. Args: a: A BaseForm, Integral or Expr """ return _sorted_by_number_and_part(extract_type(a, BaseArgument)) def extract_coefficients(a): """Build a sorted list of all coefficients in a. Args: a: A BaseForm, Integral or Expr """ return sorted_by_count(extract_type(a, BaseCoefficient)) def extract_constants(a): """Build a sorted list of all constants in a. Args: a: A BaseForm, Integral or Expr """ return sorted_by_count(extract_type(a, Constant)) def extract_base_form_operators(a): """Build a sorted list of all base form operators in a. Args: a: A BaseForm, Integral or Expr """ return sorted_by_count(extract_type(a, BaseFormOperator)) def extract_arguments_and_coefficients(a): """Build two sorted lists of all arguments and coefficients in a. This function is faster than extract_arguments + extract_coefficients for large forms, and has more validation built in. Args: a: A BaseForm, Integral or Expr """ # Extract lists of all BaseArgument and BaseCoefficient instances base_coeff_and_args = extract_type(a, (BaseArgument, BaseCoefficient)) arguments = [f for f in base_coeff_and_args if isinstance(f, BaseArgument)] coefficients = [f for f in base_coeff_and_args if isinstance(f, BaseCoefficient)] # Build number,part: instance mappings, should be one to one bfnp = dict((f, (f.number(), f.part())) for f in arguments) if len(bfnp) != len(set(bfnp.values())): raise ValueError( "Found different Arguments with same number and part.\n" "Did you combine test or trial functions from different spaces?\n" "The Arguments found are:\n" + "\n".join(f" {a}" for a in arguments) ) # Build count: instance mappings, should be one to one fcounts = dict((f, f.count()) for f in coefficients) if len(fcounts) != len(set(fcounts.values())): raise ValueError( "Found different coefficients with same counts.\n" "The arguments found are:\n" + "\n".join(f" {c}" for c in coefficients) ) # Passed checks, so we can safely sort the instances by count arguments = _sorted_by_number_and_part(arguments) coefficients = sorted_by_count(coefficients) return arguments, coefficients def extract_elements(form): """Build sorted tuple of all elements used in form.""" args = chain(*extract_arguments_and_coefficients(form)) return tuple(f.ufl_element() for f in args) def extract_unique_elements(form): """Build sorted tuple of all unique elements used in form.""" return unique_tuple(extract_elements(form)) def extract_sub_elements(elements): """Build sorted tuple of all sub elements (including parent element).""" sub_elements = tuple(chain(*[e.sub_elements for e in elements])) if not sub_elements: return tuple(elements) return tuple(elements) + extract_sub_elements(sub_elements) def sort_elements(elements): """Sort elements. A sort is performed so that any sub elements appear before the corresponding mixed elements. This is useful when sub elements need to be defined before the corresponding mixed elements. The ordering is based on sorting a directed acyclic graph. """ # Set nodes nodes = list(elements) # Set edges edges = dict((node, []) for node in nodes) for element in elements: for sub_element in element.sub_elements: edges[element].append(sub_element) # Sort graph sorted_elements = topological_sorting(nodes, edges) # Reverse list of elements sorted_elements.reverse() return sorted_elements ufl-2024.2.0/ufl/algorithms/apply_algebra_lowering.py000066400000000000000000000105331470142567200225620ustar00rootroot00000000000000"""Algorithm for expanding compound expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010 from ufl.algorithms.map_integrands import map_integrand_dags from ufl.classes import Conj, Grad, Product from ufl.compound_expressions import cofactor_expr, determinant_expr, deviatoric_expr, inverse_expr from ufl.core.multiindex import Index, indices from ufl.corealg.multifunction import MultiFunction from ufl.tensors import as_matrix, as_tensor, as_vector class LowerCompoundAlgebra(MultiFunction): """Expands high level compound operators to equivalent representations using basic operators.""" def __init__(self): """Initialize.""" MultiFunction.__init__(self) ufl_type = MultiFunction.reuse_if_untouched # ------------ Compound tensor operators def trace(self, o, A): """Lower a trace.""" i = Index() return A[i, i] def transposed(self, o, A): """Lower a transposed.""" i, j = indices(2) return as_tensor(A[i, j], (j, i)) def deviatoric(self, o, A): """Lower a deviatoric.""" return deviatoric_expr(A) def skew(self, o, A): """Lower a skew.""" i, j = indices(2) return as_matrix((A[i, j] - A[j, i]) / 2, (i, j)) def sym(self, o, A): """Lower a sym.""" i, j = indices(2) return as_matrix((A[i, j] + A[j, i]) / 2, (i, j)) def cross(self, o, a, b): """Lower a cross.""" def c(i, j): return Product(a[i], b[j]) - Product(a[j], b[i]) return as_vector((c(1, 2), c(2, 0), c(0, 1))) def perp(self, o, a): """Lower a perp.""" return as_vector([-a[1], a[0]]) def dot(self, o, a, b): """Lower a dot.""" ai = indices(len(a.ufl_shape) - 1) bi = indices(len(b.ufl_shape) - 1) k = (Index(),) # Creates a single IndexSum over a Product s = a[ai + k] * b[k + bi] return as_tensor(s, ai + bi) def inner(self, o, a, b): """Lower an inner.""" ash = a.ufl_shape bsh = b.ufl_shape if ash != bsh: raise ValueError("Nonmatching shapes.") ii = indices(len(ash)) # Creates multiple IndexSums over a Product s = a[ii] * Conj(b[ii]) return s def outer(self, o, a, b): """Lower an outer.""" ii = indices(len(a.ufl_shape)) jj = indices(len(b.ufl_shape)) # Create a Product with no shared indices s = Conj(a[ii]) * b[jj] return as_tensor(s, ii + jj) def determinant(self, o, A): """Lower a determinant.""" return determinant_expr(A) def cofactor(self, o, A): """Lower a cofactor.""" return cofactor_expr(A) def inverse(self, o, A): """Lower an inverse.""" return inverse_expr(A) # ------------ Compound differential operators def div(self, o, a): """Lower a div.""" i = Index() return a[..., i].dx(i) def nabla_div(self, o, a): """Lower a nabla_div.""" i = Index() return a[i, ...].dx(i) def nabla_grad(self, o, a): """Lower a nabla_grad.""" sh = a.ufl_shape if sh == (): return Grad(a) else: j = Index() ii = tuple(indices(len(sh))) return as_tensor(a[ii].dx(j), (j,) + ii) def curl(self, o, a): """Lower a curl.""" # o = curl a = "[a.dx(1), -a.dx(0)]" if a.ufl_shape == () # o = curl a = "cross(nabla, (a0, a1, 0))[2]" if a.ufl_shape == (2,) # o = curl a = "cross(nabla, a)" if a.ufl_shape == (3,) def c(i, j): """A component of curl.""" return a[j].dx(i) - a[i].dx(j) sh = a.ufl_shape if sh == (): return as_vector((a.dx(1), -a.dx(0))) if sh == (2,): return c(0, 1) if sh == (3,): return as_vector((c(1, 2), c(2, 0), c(0, 1))) raise ValueError(f"Invalid shape {sh} of curl argument.") def apply_algebra_lowering(expr): """Expands high level compound operators to equivalent representations using basic operators.""" return map_integrand_dags(LowerCompoundAlgebra(), expr) ufl-2024.2.0/ufl/algorithms/apply_derivatives.py000066400000000000000000001764561470142567200216250ustar00rootroot00000000000000"""Apply derivatives algorithm which computes the derivatives of a form of expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import warnings from collections import defaultdict from math import pi from ufl.action import Action from ufl.algorithms.analysis import extract_arguments from ufl.algorithms.map_integrands import map_integrand_dags from ufl.algorithms.replace_derivative_nodes import replace_derivative_nodes from ufl.argument import BaseArgument from ufl.checks import is_cellwise_constant from ufl.classes import ( Coefficient, ComponentTensor, Conj, ConstantValue, ExprList, ExprMapping, FloatValue, FormArgument, Grad, Identity, Imag, Indexed, IndexSum, Jacobian, JacobianDeterminant, JacobianInverse, ListTensor, Product, Real, ReferenceGrad, ReferenceValue, SpatialCoordinate, Sum, Variable, Zero, ) from ufl.constantvalue import is_true_ufl_scalar, is_ufl_scalar from ufl.core.base_form_operator import BaseFormOperator from ufl.core.expr import ufl_err_str from ufl.core.multiindex import FixedIndex, MultiIndex, indices from ufl.core.terminal import Terminal from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction from ufl.differentiation import ( BaseFormCoordinateDerivative, BaseFormOperatorDerivative, CoordinateDerivative, ) from ufl.domain import extract_unique_domain from ufl.form import Form, ZeroBaseForm from ufl.operators import ( bessel_I, bessel_J, bessel_K, bessel_Y, cell_avg, conditional, cos, cosh, exp, facet_avg, ln, sign, sin, sinh, sqrt, ) from ufl.pullback import CustomPullback, PhysicalPullback from ufl.tensors import as_scalar, as_scalars, as_tensor, unit_indexed_tensor, unwrap_list_tensor # TODO: Add more rulesets? # - DivRuleset # - CurlRuleset # - ReferenceGradRuleset # - ReferenceDivRuleset class GenericDerivativeRuleset(MultiFunction): """A generic derivative.""" def __init__(self, var_shape): """Initialise.""" MultiFunction.__init__(self) self._var_shape = var_shape # --- Error checking for missing handlers and unexpected types def expr(self, o): """Raise error.""" raise ValueError( f"Missing differentiation handler for type {o._ufl_class_.__name__}. " "Have you added a new type?" ) def unexpected(self, o): """Raise error about unexpected type.""" raise ValueError(f"Unexpected type {o._ufl_class_.__name__} in AD rules.") def override(self, o): """Raise error about overriding.""" raise ValueError( f"Type {o._ufl_class_.__name__} must be overridden in specialized AD rule set." ) def derivative(self, o): """Raise error.""" raise ValueError( f"Unhandled derivative type {o._ufl_class_.__name__}, " "nested differentiation has failed." ) # --- Some types just don't have any derivative, this is just to # --- make algorithm structure generic def non_differentiable_terminal(self, o): """Return the non-differentiated object. Labels and indices are not differentiable: it's convenient to return the non-differentiated object. """ return o label = non_differentiable_terminal multi_index = non_differentiable_terminal # --- Helper functions for creating zeros with the right shapes def independent_terminal(self, o): """A zero with correct shape for terminals independent of diff. variable.""" return Zero(o.ufl_shape + self._var_shape) def independent_operator(self, o): """A zero with correct shape and indices for operators independent of diff. variable.""" return Zero(o.ufl_shape + self._var_shape, o.ufl_free_indices, o.ufl_index_dimensions) # --- All derivatives need to define grad and averaging grad = override cell_avg = override facet_avg = override # --- Default rules for terminals # Literals are by definition independent of any differentiation variable constant_value = independent_terminal # Constants are independent of any differentiation constant = independent_terminal # Rules for form arguments must be specified in specialized rule set form_argument = override # Rules for geometric quantities must be specified in specialized rule set geometric_quantity = override # These types are currently assumed independent, but for non-affine domains # this no longer holds and we want to implement rules for them. # facet_normal = independent_terminal # spatial_coordinate = independent_terminal # cell_coordinate = independent_terminal # Measures of cell entities, assuming independent although # this will not be true for all of these for non-affine domains # cell_volume = independent_terminal # circumradius = independent_terminal # facet_area = independent_terminal # cell_surface_area = independent_terminal # min_cell_edge_length = independent_terminal # max_cell_edge_length = independent_terminal # min_facet_edge_length = independent_terminal # max_facet_edge_length = independent_terminal # Other stuff # cell_orientation = independent_terminal # quadrature_weigth = independent_terminal # These types are currently not expected to show up in AD pass. # To make some of these available to the end-user, they need to be # implemented here. # facet_coordinate = unexpected # cell_origin = unexpected # facet_origin = unexpected # cell_facet_origin = unexpected # jacobian = unexpected # jacobian_determinant = unexpected # jacobian_inverse = unexpected # facet_jacobian = unexpected # facet_jacobian_determinant = unexpected # facet_jacobian_inverse = unexpected # cell_facet_jacobian = unexpected # cell_facet_jacobian_determinant = unexpected # cell_facet_jacobian_inverse = unexpected # cell_vertices = unexpected # cell_edge_vectors = unexpected # facet_edge_vectors = unexpected # reference_cell_edge_vectors = unexpected # reference_facet_edge_vectors = unexpected # cell_normal = unexpected # TODO: Expecting rename # cell_normals = unexpected # facet_tangents = unexpected # cell_tangents = unexpected # cell_midpoint = unexpected # facet_midpoint = unexpected # --- Default rules for operators def variable(self, o, df, unused_l): """Differentiate a variable.""" return df # --- Indexing and component handling def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in nesting rules """Differentiate an indexed.""" # Propagate zeros if isinstance(Ap, Zero): return self.independent_operator(o) # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] to simplify # resulting expression if isinstance(Ap, ComponentTensor): B, jj = Ap.ufl_operands if isinstance(B, Indexed): C, kk = B.ufl_operands kk = list(kk) if all(j in kk for j in jj): rep = dict(zip(jj, ii)) Cind = [rep.get(k, k) for k in kk] expr = Indexed(C, MultiIndex(tuple(Cind))) assert expr.ufl_free_indices == o.ufl_free_indices assert expr.ufl_shape == o.ufl_shape return expr # Otherwise a more generic approach r = len(Ap.ufl_shape) - len(ii) if r: kk = indices(r) op = Indexed(Ap, MultiIndex(ii.indices() + kk)) op = as_tensor(op, kk) else: op = Indexed(Ap, ii) return op def list_tensor(self, o, *dops): """Differentiate a list_tensor.""" return ListTensor(*dops) def component_tensor(self, o, Ap, ii): """Differentiate a component_tensor.""" if isinstance(Ap, Zero): op = self.independent_operator(o) else: Ap, jj = as_scalar(Ap) op = as_tensor(Ap, ii.indices() + jj) return op # --- Algebra operators def index_sum(self, o, Ap, i): """Differentiate an index_sum.""" return IndexSum(Ap, i) def sum(self, o, da, db): """Differentiate a sum.""" return da + db def product(self, o, da, db): """Differentiate a product.""" # Even though arguments to o are scalar, da and db may be # tensor valued a, b = o.ufl_operands (da, db), ii = as_scalars(da, db) pa = Product(da, b) pb = Product(a, db) s = Sum(pa, pb) if ii: s = as_tensor(s, ii) return s def division(self, o, fp, gp): """Differentiate a division.""" f, g = o.ufl_operands if not is_ufl_scalar(f): raise ValueError("Not expecting nonscalar nominator") if not is_true_ufl_scalar(g): raise ValueError("Not expecting nonscalar denominator") # do_df = 1/g # do_dg = -h/g # op = do_df*fp + do_df*gp # op = (fp - o*gp) / g # Get o and gp as scalars, multiply, then wrap as a tensor # again so, oi = as_scalar(o) sgp, gi = as_scalar(gp) o_gp = so * sgp if oi or gi: o_gp = as_tensor(o_gp, oi + gi) op = (fp - o_gp) / g return op def power(self, o, fp, gp): """Differentiate a power.""" f, g = o.ufl_operands if not is_true_ufl_scalar(f): raise ValueError("Expecting scalar expression f in f**g.") if not is_true_ufl_scalar(g): raise ValueError("Expecting scalar expression g in f**g.") # Derivation of the general case: o = f(x)**g(x) # do/df = g * f**(g-1) = g / f * o # do/dg = ln(f) * f**g = ln(f) * o # do/df * df + do/dg * dg = o * (g / f * df + ln(f) * dg) if isinstance(gp, Zero): # This probably produces better results for the common # case of f**constant op = fp * g * f ** (g - 1) else: # Note: This produces expressions like (1/w)*w**5 instead of w**4 # op = o * (fp * g / f + gp * ln(f)) # This reuses o op = f ** (g - 1) * ( g * fp + f * ln(f) * gp ) # This gives better accuracy in dolfin integration test # Example: d/dx[x**(x**3)]: # f = x # g = x**3 # df = 1 # dg = 3*x**2 # op1 = o * (fp * g / f + gp * ln(f)) # = x**(x**3) * (x**3/x + 3*x**2*ln(x)) # op2 = f**(g-1) * (g*fp + f*ln(f)*gp) # = x**(x**3-1) * (x**3 + x*3*x**2*ln(x)) return op def abs(self, o, df): """Differentiate an abs.""" (f,) = o.ufl_operands # return conditional(eq(f, 0), 0, Product(sign(f), df)) abs is # not complex differentiable, so we workaround the case of a # real F in complex mode by defensively casting to real inside # the sign. return sign(Real(f)) * df # --- Complex algebra def conj(self, o, df): """Differentiate a conj.""" return Conj(df) def real(self, o, df): """Differentiate a real.""" return Real(df) def imag(self, o, df): """Differentiate a imag.""" return Imag(df) # --- Mathfunctions def math_function(self, o, df): """Differentiate a math_function.""" # FIXME: Introduce a UserOperator type instead of this hack # and define user derivative() function properly if hasattr(o, "derivative"): (f,) = o.ufl_operands return df * o.derivative() raise ValueError("Unknown math function.") def sqrt(self, o, fp): """Differentiate a sqrt.""" return fp / (2 * o) def exp(self, o, fp): """Differentiate an exp.""" return fp * o def ln(self, o, fp): """Differentiate a ln.""" (f,) = o.ufl_operands if isinstance(f, Zero): raise ZeroDivisionError() return fp / f def cos(self, o, fp): """Differentiate a cos.""" (f,) = o.ufl_operands return fp * -sin(f) def sin(self, o, fp): """Differentiate a sin.""" (f,) = o.ufl_operands return fp * cos(f) def tan(self, o, fp): """Differentiate a tan.""" (f,) = o.ufl_operands return 2.0 * fp / (cos(2.0 * f) + 1.0) def cosh(self, o, fp): """Differentiate a cosh.""" (f,) = o.ufl_operands return fp * sinh(f) def sinh(self, o, fp): """Differentiate a sinh.""" (f,) = o.ufl_operands return fp * cosh(f) def tanh(self, o, fp): """Differentiate a tanh.""" (f,) = o.ufl_operands def sech(y): return (2.0 * cosh(y)) / (cosh(2.0 * y) + 1.0) return fp * sech(f) ** 2 def acos(self, o, fp): """Differentiate an acos.""" (f,) = o.ufl_operands return -fp / sqrt(1.0 - f**2) def asin(self, o, fp): """Differentiate an asin.""" (f,) = o.ufl_operands return fp / sqrt(1.0 - f**2) def atan(self, o, fp): """Differentiate an atan.""" (f,) = o.ufl_operands return fp / (1.0 + f**2) def atan2(self, o, fp, gp): """Differentiate an atan2.""" f, g = o.ufl_operands return (g * fp - f * gp) / (f**2 + g**2) def erf(self, o, fp): """Differentiate an erf.""" (f,) = o.ufl_operands return fp * (2.0 / sqrt(pi) * exp(-(f**2))) # --- Bessel functions def bessel_j(self, o, nup, fp): """Differentiate a bessel_j.""" nu, f = o.ufl_operands if not (nup is None or isinstance(nup, Zero)): raise NotImplementedError( "Differentiation of bessel function w.r.t. nu is not supported." ) if isinstance(nu, Zero): op = -bessel_J(1, f) else: op = 0.5 * (bessel_J(nu - 1, f) - bessel_J(nu + 1, f)) return op * fp def bessel_y(self, o, nup, fp): """Differentiate a bessel_y.""" nu, f = o.ufl_operands if not (nup is None or isinstance(nup, Zero)): raise NotImplementedError( "Differentiation of bessel function w.r.t. nu is not supported." ) if isinstance(nu, Zero): op = -bessel_Y(1, f) else: op = 0.5 * (bessel_Y(nu - 1, f) - bessel_Y(nu + 1, f)) return op * fp def bessel_i(self, o, nup, fp): """Differentiate a bessel_i.""" nu, f = o.ufl_operands if not (nup is None or isinstance(nup, Zero)): raise NotImplementedError( "Differentiation of bessel function w.r.t. nu is not supported." ) if isinstance(nu, Zero): op = bessel_I(1, f) else: op = 0.5 * (bessel_I(nu - 1, f) + bessel_I(nu + 1, f)) return op * fp def bessel_k(self, o, nup, fp): """Differentiate a bessel_k.""" nu, f = o.ufl_operands if not (nup is None or isinstance(nup, Zero)): raise NotImplementedError( "Differentiation of bessel function w.r.t. nu is not supported." ) if isinstance(nu, Zero): op = -bessel_K(1, f) else: op = -0.5 * (bessel_K(nu - 1, f) + bessel_K(nu + 1, f)) return op * fp # --- Restrictions def restricted(self, o, fp): """Differentiate a restricted.""" # Restriction and differentiation commutes if isinstance(fp, ConstantValue): return fp # TODO: Add simplification to Restricted instead? else: return fp(o._side) # (f+-)' == (f')+- # --- Conditionals def binary_condition(self, o, dl, dr): """Differentiate a binary_condition.""" # Should not be used anywhere... return None def not_condition(self, o, c): """Differentiate a not_condition.""" # Should not be used anywhere... return None def conditional(self, o, unused_dc, dt, df): """Differentiate a conditional.""" if isinstance(dt, Zero) and isinstance(df, Zero): # Assuming dt and df have the same indices here, which # should be the case return dt else: # Not placing t[1],f[1] outside, allowing arguments inside # conditionals. This will make legacy ffc fail, but # should work with uflacs. c = o.ufl_operands[0] return conditional(c, dt, df) def max_value(self, o, df, dg): """Differentiate a max_value.""" # d/dx max(f, g) = # f > g: df/dx # f < g: dg/dx # Placing df,dg outside here to avoid getting arguments inside # conditionals f, g = o.ufl_operands dc = conditional(f > g, 1, 0) return dc * df + (1.0 - dc) * dg def min_value(self, o, df, dg): """Differentiate a min_value.""" # d/dx min(f, g) = # f < g: df/dx # else: dg/dx # Placing df,dg outside here to avoid getting arguments # inside conditionals f, g = o.ufl_operands dc = conditional(f < g, 1, 0) return dc * df + (1.0 - dc) * dg class GradRuleset(GenericDerivativeRuleset): """Take the grad derivative.""" def __init__(self, geometric_dimension): """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=(geometric_dimension,)) self._Id = Identity(geometric_dimension) # --- Specialized rules for geometric quantities def geometric_quantity(self, o): """Differentiate a geometric_quantity. Default for geometric quantities is do/dx = 0 if piecewise constant, otherwise transform derivatives to reference derivatives. Override for specific types if other behaviour is needed. """ if is_cellwise_constant(o): return self.independent_terminal(o) else: domain = extract_unique_domain(o) K = JacobianInverse(domain) Do = grad_to_reference_grad(o, K) return Do def jacobian_inverse(self, o): """Differentiate a jacobian_inverse.""" # grad(K) == K_ji rgrad(K)_rj if is_cellwise_constant(o): return self.independent_terminal(o) if not o._ufl_is_terminal_: raise ValueError("ReferenceValue can only wrap a terminal") Do = grad_to_reference_grad(o, o) return Do # TODO: Add more explicit geometry type handlers here, with # non-affine domains several should be non-zero. def spatial_coordinate(self, o): """Differentiate a spatial_coordinate. dx/dx = I. """ return self._Id def cell_coordinate(self, o): """Differentiate a cell_coordinate. dX/dx = inv(dx/dX) = inv(J) = K. """ # FIXME: Is this true for manifolds? What about orientation? return JacobianInverse(extract_unique_domain(o)) # --- Specialized rules for form arguments def base_form_operator(self, o): """Differentiate a base_form_operator.""" # Push the grad through the operator is not legal in most cases: # -> Not enouth regularity for chain rule to hold! # By the time we evaluate `grad(o)`, the operator `o` will have # been assembled and substituted by its output. return Grad(o) def coefficient(self, o): """Differentiate a coefficient.""" if is_cellwise_constant(o): return self.independent_terminal(o) return Grad(o) def argument(self, o): """Differentiate an argument.""" # TODO: Enable this after fixing issue#13, unless we move # simplificat ion to a separate stage? # if is_cellwise_constant(o): # # Collapse gradient of cellwise constant function to zero # # TODO: Missing this type # return AnnotatedZero(o.ufl_shape + self._var_shape, arguments=(o,)) return Grad(o) # --- Rules for values or derivatives in reference frame def reference_value(self, o): """Differentiate a reference_value.""" # grad(o) == grad(rv(f)) -> K_ji*rgrad(rv(f))_rj f = o.ufl_operands[0] if isinstance(f.ufl_element().pullback, PhysicalPullback): # TODO: Do we need to be more careful for immersed things? return ReferenceGrad(o) if not f._ufl_is_terminal_: raise ValueError("ReferenceValue can only wrap a terminal") domain = extract_unique_domain(f) K = JacobianInverse(domain) Do = grad_to_reference_grad(o, K) return Do def reference_grad(self, o): """Differentiate a reference_grad.""" # grad(o) == grad(rgrad(rv(f))) -> K_ji*rgrad(rgrad(rv(f)))_rj f = o.ufl_operands[0] valid_operand = f._ufl_is_in_reference_frame_ or isinstance( f, (JacobianInverse, SpatialCoordinate, Jacobian, JacobianDeterminant) ) if not valid_operand: raise ValueError("ReferenceGrad can only wrap a reference frame type!") domain = extract_unique_domain(f) K = JacobianInverse(domain) Do = grad_to_reference_grad(o, K) return Do # --- Nesting of gradients def grad(self, o): """Differentiate a grad. Represent grad(grad(f)) as Grad(Grad(f)). """ # Check that o is a "differential terminal" if not isinstance(o.ufl_operands[0], (Grad, Terminal)): raise ValueError("Expecting only grads applied to a terminal.") return Grad(o) def _grad(self, o): """Differentiate a _grad.""" pass # TODO: Not sure how to detect that gradient of f is cellwise constant. # Can we trust element degrees? # if is_cellwise_constant(o): # return self.terminal(o) # TODO: Maybe we can ask "f.has_derivatives_of_order(n)" to check # if we should make a zero here? # 1) n = count number of Grads, get f # 2) if not f.has_derivatives(n): return zero(...) cell_avg = GenericDerivativeRuleset.independent_operator facet_avg = GenericDerivativeRuleset.independent_operator def grad_to_reference_grad(o, K): """Relates grad(o) to reference_grad(o) using the Jacobian inverse. Args: o: Operand K: Jacobian inverse Returns: grad(o) written in terms of reference_grad(o) and K """ r = indices(len(o.ufl_shape)) i, j = indices(2) # grad(o) == K_ji rgrad(o)_rj Do = as_tensor(K[j, i] * ReferenceGrad(o)[r + (j,)], r + (i,)) return Do class ReferenceGradRuleset(GenericDerivativeRuleset): """Apply the reference grad derivative.""" def __init__(self, topological_dimension): """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=(topological_dimension,)) self._Id = Identity(topological_dimension) # --- Specialized rules for geometric quantities def geometric_quantity(self, o): """Differentiate a geometric_quantity. dg/dX = 0 if piecewise constant, otherwise ReferenceGrad(g). """ if is_cellwise_constant(o): return self.independent_terminal(o) else: # TODO: Which types does this involve? I don't think the # form compilers will handle this. return ReferenceGrad(o) def spatial_coordinate(self, o): """Differentiate a spatial_coordinate. dx/dX = J. """ # Don't convert back to J, otherwise we get in a loop return ReferenceGrad(o) def cell_coordinate(self, o): """Differentiate a cell_coordinate. dX/dX = I. """ return self._Id # TODO: Add more geometry types here, with non-affine domains # several should be non-zero. # --- Specialized rules for form arguments def reference_value(self, o): """Differentiate a reference_value.""" if not o.ufl_operands[0]._ufl_is_terminal_: raise ValueError("ReferenceValue can only wrap a terminal") return ReferenceGrad(o) def coefficient(self, o): """Differentiate a coefficient.""" raise ValueError("Coefficient should be wrapped in ReferenceValue by now") def argument(self, o): """Differentiate an argument.""" raise ValueError("Argument should be wrapped in ReferenceValue by now") # --- Nesting of gradients def grad(self, o): """Differentiate a grad.""" raise ValueError( f"Grad should have been transformed by this point, but got {type(o).__name__}." ) def reference_grad(self, o): """Differentiate a reference_grad. Represent ref_grad(ref_grad(f)) as RefGrad(RefGrad(f)). """ # Check that o is a "differential terminal" if not isinstance(o.ufl_operands[0], (ReferenceGrad, ReferenceValue, Terminal)): raise ValueError("Expecting only grads applied to a terminal.") return ReferenceGrad(o) cell_avg = GenericDerivativeRuleset.independent_operator facet_avg = GenericDerivativeRuleset.independent_operator class VariableRuleset(GenericDerivativeRuleset): """Differentiate with respect to a variable.""" def __init__(self, var): """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=var.ufl_shape) if var.ufl_free_indices: raise ValueError("Differentiation variable cannot have free indices.") self._variable = var self._Id = self._make_identity(self._var_shape) def _make_identity(self, sh): """Differentiate a _make_identity. Creates a higher order identity tensor to represent dv/dv. """ res = None if sh == (): # Scalar dv/dv is scalar return FloatValue(1.0) elif len(sh) == 1: # Vector v makes dv/dv the identity matrix return Identity(sh[0]) else: # TODO: Add a type for this higher order identity? # II[i0,i1,i2,j0,j1,j2] = 1 if all((i0==j0, i1==j1, i2==j2)) else 0 # Tensor v makes dv/dv some kind of higher rank identity tensor ind1 = () ind2 = () for d in sh: i, j = indices(2) dij = Identity(d)[i, j] if res is None: res = dij else: res *= dij ind1 += (i,) ind2 += (j,) fp = as_tensor(res, ind1 + ind2) return fp # Explicitly defining dg/dw == 0 geometric_quantity = GenericDerivativeRuleset.independent_terminal # Explicitly defining da/dw == 0 argument = GenericDerivativeRuleset.independent_terminal def coefficient(self, o): """Differentiate a coefficient. df/dv = Id if v is f else 0. Note that if v = variable(f), df/dv is still 0, but if v == f, i.e. isinstance(v, Coefficient) == True, then df/dv == df/df = Id. """ v = self._variable if isinstance(v, Coefficient) and o == v: # dv/dv = identity of rank 2*rank(v) return self._Id else: # df/v = 0 return self.independent_terminal(o) def variable(self, o, df, a): """Differentiate a variable.""" v = self._variable if isinstance(v, Variable) and v.label() == a: # dv/dv = identity of rank 2*rank(v) return self._Id else: # df/v = df return df def grad(self, o): """Differentiate a grad. Variable derivative of a gradient of a terminal must be 0. """ # Check that o is a "differential terminal" if not isinstance(o.ufl_operands[0], (Grad, Terminal)): raise ValueError("Expecting only grads applied to a terminal.") return self.independent_terminal(o) # --- Rules for values or derivatives in reference frame def reference_value(self, o): """Differentiate a reference_value.""" # d/dv(o) == d/dv(rv(f)) = 0 if v is not f, or rv(dv/df) v = self._variable if isinstance(v, Coefficient) and o.ufl_operands[0] == v: if not v.ufl_element().pullback.is_identity: # FIXME: This is a bit tricky, instead of Identity it is # actually inverse(transform), or we should rather not # convert to reference frame in the first place raise ValueError( "Missing implementation: To handle derivatives of rv(f) w.r.t. f for " "mapped elements, rewriting to reference frame should not happen first..." ) # dv/dv = identity of rank 2*rank(v) return self._Id else: # df/v = 0 return self.independent_terminal(o) def reference_grad(self, o): """Differentiate a reference_grad. Variable derivative of a gradient of a terminal must be 0. """ if not isinstance(o.ufl_operands[0], (ReferenceGrad, ReferenceValue)): raise ValueError("Unexpected argument to reference_grad.") return self.independent_terminal(o) cell_avg = GenericDerivativeRuleset.independent_operator facet_avg = GenericDerivativeRuleset.independent_operator class GateauxDerivativeRuleset(GenericDerivativeRuleset): """Apply AFD (Automatic Functional Differentiation) to expression. Implements rules for the Gateaux derivative D_w[v](...) defined as D_w[v](e) = d/dtau e(w+tau v)|tau=0. """ def __init__(self, coefficients, arguments, coefficient_derivatives, pending_operations): """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=()) # Type checking if not isinstance(coefficients, ExprList): raise ValueError("Expecting a ExprList of coefficients.") if not isinstance(arguments, ExprList): raise ValueError("Expecting a ExprList of arguments.") if not isinstance(coefficient_derivatives, ExprMapping): raise ValueError("Expecting a coefficient-coefficient ExprMapping.") # The coefficient(s) to differentiate w.r.t. and the # argument(s) s.t. D_w[v](e) = d/dtau e(w+tau v)|tau=0 self._w = coefficients.ufl_operands self._v = arguments.ufl_operands self._w2v = {w: v for w, v in zip(self._w, self._v)} # Build more convenient dict {f: df/dw} for each coefficient f # where df/dw is nonzero cd = coefficient_derivatives.ufl_operands self._cd = {cd[2 * i]: cd[2 * i + 1] for i in range(len(cd) // 2)} # Record the operations delayed to the derivative expansion phase: # Example: dN(u)/du where `N` is an ExternalOperator and `u` a Coefficient self.pending_operations = pending_operations # Explicitly defining dg/dw == 0 geometric_quantity = GenericDerivativeRuleset.independent_terminal def cell_avg(self, o, fp): """Differentiate a cell_avg.""" # Cell average of a single function and differentiation # commutes, D_f[v](cell_avg(f)) = cell_avg(v) return cell_avg(fp) def facet_avg(self, o, fp): """Differentiate a facet_avg.""" # Facet average of a single function and differentiation # commutes, D_f[v](facet_avg(f)) = facet_avg(v) return facet_avg(fp) # Explicitly defining da/dw == 0 argument = GenericDerivativeRuleset.independent_terminal def coefficient(self, o): """Differentiate a coefficient.""" # Define dw/dw := d/ds [w + s v] = v # Return corresponding argument if we can find o among w do = self._w2v.get(o) if do is not None: return do # Look for o among coefficient derivatives dos = self._cd.get(o) if dos is None: # If o is not among coefficient derivatives, return # do/dw=0 do = Zero(o.ufl_shape) return do else: # Compute do/dw_j = do/dw_h : v. # Since we may actually have a tuple of oprimes and vs in a # 'mixed' space, sum over them all to get the complete inner # product. Using indices to define a non-compound inner product. # Example: # (f:g) -> (dfdu:v):g + f:(dgdu:v) # shape(dfdu) == shape(f) + shape(v) # shape(f) == shape(g) == shape(dfdu : v) # Make sure we have a tuple to match the self._v tuple if not isinstance(dos, tuple): dos = (dos,) if len(dos) != len(self._v): raise ValueError( "Got a tuple of arguments, expecting a " "matching tuple of coefficient derivatives." ) dosum = Zero(o.ufl_shape) for do, v in zip(dos, self._v): so, oi = as_scalar(do) rv = len(oi) - len(v.ufl_shape) oi1 = oi[:rv] oi2 = oi[rv:] prod = so * v[oi2] if oi1: dosum += as_tensor(prod, oi1) else: dosum += prod return dosum def reference_value(self, o): """Differentiate a reference_value.""" raise NotImplementedError( "Currently no support for ReferenceValue in CoefficientDerivative." ) # TODO: This is implementable for regular derivative(M(f),f,v) # but too messy if customized coefficient derivative # relations are given by the user. We would only need # this to allow the user to write # derivative(...ReferenceValue...,...). # f, = o.ufl_operands # if not f._ufl_is_terminal_: # raise ValueError("ReferenceValue can only wrap terminals directly.") # FIXME: check all cases like in coefficient # if f is w: # # FIXME: requires that v is an Argument with the same element mapping! # return ReferenceValue(v) # else: # return self.independent_terminal(o) def reference_grad(self, o): """Differentiate a reference_grad.""" raise NotImplementedError( "Currently no support for ReferenceGrad in CoefficientDerivative." ) # TODO: This is implementable for regular derivative(M(f),f,v) # but too messy if customized coefficient derivative # relations are given by the user. We would only need # this to allow the user to write # derivative(...ReferenceValue...,...). def grad(self, g): """Differentiate a grad.""" # If we hit this type, it has already been propagated to a # coefficient (or grad of a coefficient) or a base form operator, # FIXME: Assert # this! so we need to take the gradient of the variation or # return zero. Complications occur when dealing with # derivatives w.r.t. single components... # Figure out how many gradients are around the inner terminal ngrads = 0 o = g while isinstance(o, Grad): (o,) = o.ufl_operands ngrads += 1 # `grad(N)` where N is a BaseFormOperator is treated as if `N` was a Coefficient. if not isinstance(o, (FormArgument, BaseFormOperator)): raise ValueError(f"Expecting gradient of a FormArgument, not {ufl_err_str(o)}.") def apply_grads(f): for i in range(ngrads): f = Grad(f) return f # Find o among all w without any indexing, which makes this # easy for w, v in zip(self._w, self._v): if o == w and isinstance(v, FormArgument): # Case: d/dt [w + t v] return apply_grads(v) # If o is not among coefficient derivatives, return do/dw=0 gprimesum = Zero(g.ufl_shape) def analyse_variation_argument(v): # Analyse variation argument if isinstance(v, FormArgument): # Case: d/dt [w[...] + t v] vval, vcomp = v, () elif isinstance(v, Indexed): # Case: d/dt [w + t v[...]] # Case: d/dt [w[...] + t v[...]] vval, vcomp = v.ufl_operands vcomp = tuple(vcomp) else: raise ValueError("Expecting argument or component of argument.") if not all(isinstance(k, FixedIndex) for k in vcomp): raise ValueError("Expecting only fixed indices in variation.") return vval, vcomp def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): # Apply gradients directly to argument vval, and get the # right indexed scalar component(s) kk = indices(ngrads) Dvkk = apply_grads(vval)[vcomp + kk] # Place scalar component(s) Dvkk into the right tensor # positions if wshape: Ejj, jj = unit_indexed_tensor(wshape, wcomp) else: Ejj, jj = 1, () gprimeterm = as_tensor(Ejj * Dvkk, jj + kk) return gprimeterm # Accumulate contributions from variations in different # components for w, v in zip(self._w, self._v): # -- Analyse differentiation variable coefficient -- # # Can differentiate a Form wrt a BaseFormOperator if isinstance(w, (FormArgument, BaseFormOperator)): if not w == o: continue wshape = w.ufl_shape if isinstance(v, FormArgument): # Case: d/dt [w + t v] return apply_grads(v) elif isinstance(v, ListTensor): # Case: d/dt [w + t <...,v,...>] for wcomp, vsub in unwrap_list_tensor(v): if not isinstance(vsub, Zero): vval, vcomp = analyse_variation_argument(vsub) gprimesum = gprimesum + compute_gprimeterm( ngrads, vval, vcomp, wshape, wcomp ) elif isinstance(v, Zero): pass else: if wshape != (): raise ValueError("Expecting scalar coefficient in this branch.") # Case: d/dt [w + t v[...]] wval, wcomp = w, () vval, vcomp = analyse_variation_argument(v) gprimesum = gprimesum + compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp) elif isinstance( w, Indexed ): # This path is tested in unit tests, but not actually used? # Case: d/dt [w[...] + t v[...]] # Case: d/dt [w[...] + t v] wval, wcomp = w.ufl_operands if not wval == o: continue assert isinstance(wval, FormArgument) if not all(isinstance(k, FixedIndex) for k in wcomp): raise ValueError("Expecting only fixed indices in differentiation variable.") wshape = wval.ufl_shape vval, vcomp = analyse_variation_argument(v) gprimesum = gprimesum + compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp) else: raise ValueError("Expecting coefficient or component of coefficient.") # FIXME: Handle other coefficient derivatives: oprimes = # self._cd.get(o) if 0: oprimes = self._cd.get(o) if oprimes is None: if self._cd: # TODO: Make it possible to silence this message # in particular? It may be good to have for # debugging... warnings.warn(f"Assuming d{{{0}}}/d{{{self._w}}} = 0.") else: # Make sure we have a tuple to match the self._v tuple if not isinstance(oprimes, tuple): oprimes = (oprimes,) if len(oprimes) != len(self._v): raise ValueError( "Got a tuple of arguments, expecting a" " matching tuple of coefficient derivatives." ) # Compute dg/dw_j = dg/dw_h : v. # Since we may actually have a tuple of oprimes and vs # in a 'mixed' space, sum over them all to get the # complete inner product. Using indices to define a # non-compound inner product. for oprime, v in zip(oprimes, self._v): raise NotImplementedError("FIXME: Figure out how to do this with ngrads") so, oi = as_scalar(oprime) rv = len(v.ufl_shape) oi1 = oi[:-rv] oi2 = oi[-rv:] prod = so * v[oi2] if oi1: gprimesum += as_tensor(prod, oi1) else: gprimesum += prod return gprimesum def coordinate_derivative(self, o): """Differentiate a coordinate_derivative.""" o = o.ufl_operands return CoordinateDerivative(map_expr_dag(self, o[0]), o[1], o[2], o[3]) def base_form_operator(self, o, *dfs): """Differentiate a base_form_operator. If d_coeff = 0 => BaseFormOperator's derivative is taken wrt a variable => we call the appropriate handler. Otherwise => differentiation done wrt the BaseFormOperator (dF/dN[Nhat]) => we treat o as a Coefficient. """ d_coeff = self.coefficient(o) # It also handles the non-scalar case if d_coeff == 0: self.pending_operations += (o,) return d_coeff # -- Handlers for BaseForm objects -- # def cofunction(self, o): """Differentiate a cofunction.""" # Same rule than for Coefficient except that we use a Coargument. # The coargument is already attached to the class (self._v) # which `self.coefficient` relies on. dc = self.coefficient(o) if dc == 0: # Convert ufl.Zero into ZeroBaseForm return ZeroBaseForm(o.arguments() + self._v) return dc def coargument(self, o): """Differentiate a coargument.""" # Same rule than for Argument (da/dw == 0). dc = self.argument(o) if dc == 0: # Convert ufl.Zero into ZeroBaseForm return ZeroBaseForm(o.arguments() + self._v) return dc def matrix(self, M): """Differentiate a matrix.""" # Matrix rule: D_w[v](M) = v if M == w else 0 # We can't differentiate wrt a matrix so always return zero in # the appropriate space return ZeroBaseForm(M.arguments() + self._v) class BaseFormOperatorDerivativeRuleset(GateauxDerivativeRuleset): """Apply AFD (Automatic Functional Differentiation) to BaseFormOperator. Implements rules for the Gateaux derivative D_w[v](...) defined as D_w[v](B) = d/dtau B(w+tau v)|tau=0 where B is a ufl.BaseFormOperator. """ def __init__(self, coefficients, arguments, coefficient_derivatives, pending_operations): """Initialise.""" GateauxDerivativeRuleset.__init__( self, coefficients, arguments, coefficient_derivatives, pending_operations ) def pending_operations_recording(base_form_operator_handler): """Decorate a function to record pending operations.""" def wrapper(self, base_form_op, *dfs): """Decorate.""" # Get the outer `BaseFormOperator` expression, i.e. the # operator that is being differentiated. expression = self.pending_operations.expression # If the base form operator we observe is different from the # outer `BaseFormOperator`: # -> Record that `BaseFormOperator` so that # `d(expression)/d(base_form_op)` can then be computed # later. # Else: # -> Compute the Gateaux derivative of `base_form_ops` by # calling the appropriate handler. if expression != base_form_op: self.pending_operations += (base_form_op,) return self.coefficient(base_form_op) return base_form_operator_handler(self, base_form_op, *dfs) return wrapper @pending_operations_recording def interpolate(self, i_op, dw): """Differentiate an interpolate.""" # Interpolate rule: D_w[v](i_op(w, v*)) = i_op(v, v*), by linearity of Interpolate! if not dw: # i_op doesn't depend on w: # -> It also covers the Hessian case since Interpolate is linear, # e.g. D_w[v](D_w[v](i_op(w, v*))) = D_w[v](i_op(v, v*)) = 0 (since w not found). return ZeroBaseForm(i_op.arguments() + self._v) return i_op._ufl_expr_reconstruct_(expr=dw) @pending_operations_recording def external_operator(self, N, *dfs): """Differentiate an external_operator.""" result = () for i, df in enumerate(dfs): derivatives = tuple(dj + int(i == j) for j, dj in enumerate(N.derivatives)) if len(extract_arguments(df)) != 0: # Handle the symbolic differentiation of external operators. # This bit returns: # # `\sum_{i} dNdOi(..., Oi, ...; DOi(u)[v], ..., v*)` # # where we differentate wrt u, Oi is the i-th operand, # N(..., Oi, ...; ..., v*) an ExternalOperator and v the # direction (Argument). dNdOi(..., Oi, ...; DOi(u)[v]) # is an ExternalOperator representing the # Gateaux-derivative of N. For example: # -> From N(u) = u**2, we get `dNdu(u; uhat, v*) = 2 * u * uhat`. new_args = N.argument_slots() + (df,) extop = N._ufl_expr_reconstruct_( *N.ufl_operands, derivatives=derivatives, argument_slots=new_args ) elif df == 0: extop = Zero(N.ufl_shape) else: raise NotImplementedError( "Frechet derivative of external operators need to be provided!" ) result += (extop,) return sum(result) class DerivativeRuleDispatcher(MultiFunction): """Dispatch a derivative rule.""" def __init__(self): """Initialise.""" MultiFunction.__init__(self) # caches for reuse in the dispatched transformers self.vcaches = defaultdict(dict) self.rcaches = defaultdict(dict) # Record the operations delayed to the derivative expansion phase: # Example: dN(u)/du where `N` is a BaseFormOperator and `u` a Coefficient self.pending_operations = () def terminal(self, o): """Apply to a terminal.""" return o def derivative(self, o): """Apply to a derivative.""" raise NotImplementedError(f"Missing derivative handler for {type(o).__name__}.") ufl_type = MultiFunction.reuse_if_untouched def grad(self, o, f): """Apply to a grad.""" rules = GradRuleset(o.ufl_shape[-1]) key = (GradRuleset, o.ufl_shape[-1]) return map_expr_dag(rules, f, vcache=self.vcaches[key], rcache=self.rcaches[key]) def reference_grad(self, o, f): """Apply to a reference_grad.""" rules = ReferenceGradRuleset(o.ufl_shape[-1]) # FIXME: Look over this and test better. key = (ReferenceGradRuleset, o.ufl_shape[-1]) return map_expr_dag(rules, f, vcache=self.vcaches[key], rcache=self.rcaches[key]) def variable_derivative(self, o, f, dummy_v): """Apply to a variable_derivative.""" op = o.ufl_operands[1] rules = VariableRuleset(op) key = (VariableRuleset, op) return map_expr_dag(rules, f, vcache=self.vcaches[key], rcache=self.rcaches[key]) def coefficient_derivative(self, o, f, dummy_w, dummy_v, dummy_cd): """Apply to a coefficient_derivative.""" dummy, w, v, cd = o.ufl_operands pending_operations = BaseFormOperatorDerivativeRecorder( f, w, arguments=v, coefficient_derivatives=cd ) rules = GateauxDerivativeRuleset(w, v, cd, pending_operations) key = (GateauxDerivativeRuleset, w, v, cd) # We need to go through the dag first to record the pending # operations mapped_expr = map_expr_dag(rules, f, vcache=self.vcaches[key], rcache=self.rcaches[key]) # Need to account for pending operations that have been stored # in other integrands self.pending_operations += pending_operations return mapped_expr def base_form_operator_derivative(self, o, f, dummy_w, dummy_v, dummy_cd): """Apply to a base_form_operator_derivative.""" dummy, w, v, cd = o.ufl_operands pending_operations = BaseFormOperatorDerivativeRecorder( f, w, arguments=v, coefficient_derivatives=cd ) rules = BaseFormOperatorDerivativeRuleset(w, v, cd, pending_operations=pending_operations) key = (BaseFormOperatorDerivativeRuleset, w, v, cd) if isinstance(f, ZeroBaseForm): (arg,) = v.ufl_operands arguments = f.arguments() # derivative(F, u, du) with `du` a Coefficient # is equivalent to taking the action of the derivative. # In that case, we don't add arguments to `ZeroBaseForm`. if isinstance(arg, BaseArgument): arguments += (arg,) return ZeroBaseForm(arguments) # We need to go through the dag first to record the pending operations mapped_expr = map_expr_dag(rules, f, vcache=self.vcaches[key], rcache=self.rcaches[key]) mapped_f = rules.coefficient(f) if mapped_f != 0: # If dN/dN needs to return an Argument in N space # with N a BaseFormOperator. return mapped_f # Need to account for pending operations that have been stored in other integrands self.pending_operations += pending_operations return mapped_expr def coordinate_derivative(self, o, f, dummy_w, dummy_v, dummy_cd): """Apply to a coordinate_derivative.""" o_ = o.ufl_operands key = (CoordinateDerivative, o_[0]) return CoordinateDerivative( map_expr_dag(self, o_[0], vcache=self.vcaches[key], rcache=self.rcaches[key]), o_[1], o_[2], o_[3], ) def base_form_coordinate_derivative(self, o, f, dummy_w, dummy_v, dummy_cd): """Apply to a base_form_coordinate_derivative.""" o_ = o.ufl_operands key = (BaseFormCoordinateDerivative, o_[0]) return BaseFormCoordinateDerivative( map_expr_dag(self, o_[0], vcache=self.vcaches[key], rcache=self.rcaches[key]), o_[1], o_[2], o_[3], ) def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in generic rules """Apply to an indexed.""" # Reuse if untouched if Ap is o.ufl_operands[0]: return o # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] to simplify # resulting expression if isinstance(Ap, ComponentTensor): B, jj = Ap.ufl_operands if isinstance(B, Indexed): C, kk = B.ufl_operands kk = list(kk) if all(j in kk for j in jj): rep = dict(zip(jj, ii)) Cind = [rep.get(k, k) for k in kk] expr = Indexed(C, MultiIndex(tuple(Cind))) assert expr.ufl_free_indices == o.ufl_free_indices assert expr.ufl_shape == o.ufl_shape return expr # Otherwise a more generic approach r = len(Ap.ufl_shape) - len(ii) if r: kk = indices(r) op = Indexed(Ap, MultiIndex(ii.indices() + kk)) op = as_tensor(op, kk) else: op = Indexed(Ap, ii) return op class BaseFormOperatorDerivativeRecorder: """A derivative recorded for a base form operator.""" def __init__(self, expression, var, **kwargs): """Initialise.""" base_form_ops = kwargs.pop("base_form_ops", ()) if kwargs.keys() != {"arguments", "coefficient_derivatives"}: raise ValueError( "Only `arguments` and `coefficient_derivatives` are " "allowed as derivative arguments." ) self.expression = expression self.var = var self.der_kwargs = kwargs self.base_form_ops = base_form_ops def __len__(self): """Get the length.""" return len(self.base_form_ops) def __bool__(self): """Convert to a bool.""" return bool(self.base_form_ops) def __add__(self, other): """Add.""" if isinstance(other, (list, tuple)): base_form_ops = self.base_form_ops + other elif isinstance(other, BaseFormOperatorDerivativeRecorder): if self.der_kwargs != other.der_kwargs: raise ValueError( f"Derivative arguments must match when summing {type(self).__name__} objects." ) base_form_ops = self.base_form_ops + other.base_form_ops else: raise NotImplementedError( f"Sum of {type(self)} and {type(other)} objects is not supported." ) return BaseFormOperatorDerivativeRecorder( self.expression, self.var, base_form_ops=base_form_ops, **self.der_kwargs ) def __radd__(self, other): """Add.""" # Recording order doesn't matter as collected # `BaseFormOperator`s are sorted later on. return self.__add__(other) def __iadd__(self, other): """Add.""" if isinstance(other, (list, tuple)): self.base_form_ops += other elif isinstance(other, BaseFormOperatorDerivativeRecorder): self.base_form_ops += other.base_form_ops else: raise NotImplementedError return self def apply_derivatives(expression): """Apply derivatives to an expression. Args: expression: A Form, an Expr or a BaseFormOperator to be differentiated Returns: A differentiated expression """ # Notation: Let `var` be the thing we are differentating with respect to. rules = DerivativeRuleDispatcher() # If we hit a base form operator (bfo), then if `var` is: # - a BaseFormOperator → Return `d(expression)/dw` where `w` is # the coefficient produced by the bfo `var`. # - else → Record the bfo on the MultiFunction object and returns # - 0. # Example: # → If derivative(F(u, N(u); v), u) was taken the following line would compute `∂F/∂u`. dexpression_dvar = map_integrand_dags(rules, expression) # Get the recorded delayed operations pending_operations = rules.pending_operations if not pending_operations: return dexpression_dvar # Don't take into account empty Forms if not (isinstance(dexpression_dvar, Form) and len(dexpression_dvar.integrals()) == 0): dexpression_dvar = (dexpression_dvar,) else: dexpression_dvar = () # Retrieve the base form operators, var, and the argument and # coefficient_derivatives for `derivative` var = pending_operations.var base_form_ops = pending_operations.base_form_ops der_kwargs = pending_operations.der_kwargs for N in sorted(set(base_form_ops), key=lambda x: x.count()): # -- Replace dexpr/dvar by dexpr/dN -- # # We don't use `apply_derivatives` since the differentiation is # done via `\partial` and not `d`. dexpr_dN = map_integrand_dags( rules, replace_derivative_nodes(expression, {var.ufl_operands[0]: N}) ) # -- Add the BaseFormOperatorDerivative node -- # (var_arg,) = der_kwargs["arguments"].ufl_operands cd = der_kwargs["coefficient_derivatives"] # Not always the case since `derivative`'s syntax enables one to # use a Coefficient as the Gateaux direction if isinstance(var_arg, BaseArgument): # Construct the argument number based on the # BaseFormOperator arguments instead of naively using # `var_arg`. This is critical when BaseFormOperators are # used inside 0-forms. # # Example: F = 0.5 * u** 2 * dx + 0.5 * N(u; v*)** 2 * dx # -> dFdu[vhat] = + Action(, dNdu(u; v1, v*)) # with `vhat` a 0-numbered argument, and where `v1` and # `vhat` have the same function space but a different # number. Here, applying `vhat` (`var_arg`) naively would # result in `dNdu(u; vhat, v*)`, i.e. the 2-forms `dNdu` # would have two 0-numbered arguments. Instead we increment # the argument number of `vhat` to form `v1`. var_arg = type(var_arg)( var_arg.ufl_function_space(), number=len(N.arguments()), part=var_arg.part() ) dN_dvar = apply_derivatives(BaseFormOperatorDerivative(N, var, ExprList(var_arg), cd)) # -- Sum the Action: dF/du = ∂F/∂u + \sum_{i=1,...} Action(∂F/∂Ni, dNi/du) -- # if not (isinstance(dexpr_dN, Form) and len(dexpr_dN.integrals()) == 0): # In this case: Action <=> ufl.action since `dN_var` has 2 arguments. # We use Action to handle the trivial case `dN_dvar` = 0. dexpression_dvar += (Action(dexpr_dN, dN_dvar),) return sum(dexpression_dvar) class CoordinateDerivativeRuleset(GenericDerivativeRuleset): """Apply AFD (Automatic Functional Differentiation) to expression. Implements rules for the Gateaux derivative D_w[v](...) defined as D_w[v](e) = d/dtau e(w+tau v)|tau=0 where 'e' is a ufl form after pullback and w is a SpatialCoordinate. """ def __init__(self, coefficients, arguments, coefficient_derivatives): """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=()) # Type checking if not isinstance(coefficients, ExprList): raise ValueError("Expecting a ExprList of coefficients.") if not isinstance(arguments, ExprList): raise ValueError("Expecting a ExprList of arguments.") if not isinstance(coefficient_derivatives, ExprMapping): raise ValueError("Expecting a coefficient-coefficient ExprMapping.") # The coefficient(s) to differentiate w.r.t. and the # argument(s) s.t. D_w[v](e) = d/dtau e(w+tau v)|tau=0 self._w = coefficients.ufl_operands self._v = arguments.ufl_operands self._w2v = {w: v for w, v in zip(self._w, self._v)} # Build more convenient dict {f: df/dw} for each coefficient f # where df/dw is nonzero cd = coefficient_derivatives.ufl_operands self._cd = {cd[2 * i]: cd[2 * i + 1] for i in range(len(cd) // 2)} # Explicitly defining dg/dw == 0 geometric_quantity = GenericDerivativeRuleset.independent_terminal # Explicitly defining da/dw == 0 argument = GenericDerivativeRuleset.independent_terminal def coefficient(self, o): """Differentiate a coefficient.""" raise NotImplementedError( "CoordinateDerivative of coefficient in physical space is not implemented." ) def grad(self, o): """Differentiate a grad.""" raise NotImplementedError("CoordinateDerivative grad in physical space is not implemented.") def spatial_coordinate(self, o): """Differentiate a spatial_coordinate.""" do = self._w2v.get(o) # d x /d x => Argument(x.function_space()) if do is not None: return do else: raise NotImplementedError( "CoordinateDerivative found a SpatialCoordinate that is different " "from the one being differentiated." ) def reference_value(self, o): """Differentiate a reference_value.""" do = self._cd.get(o) if do is not None: return do else: return self.independent_terminal(o) def reference_grad(self, g): """Differentiate a reference_grad.""" # d (grad_X(...(x)) / dx => grad_X(...(Argument(x.function_space())) o = g ngrads = 0 while isinstance(o, ReferenceGrad): (o,) = o.ufl_operands ngrads += 1 if not (isinstance(o, SpatialCoordinate) or isinstance(o.ufl_operands[0], FormArgument)): raise ValueError(f"Expecting gradient of a FormArgument, not {ufl_err_str(o)}") def apply_grads(f): for i in range(ngrads): f = ReferenceGrad(f) return f # Find o among all w without any indexing, which makes this # easy for w, v in zip(self._w, self._v): if ( o == w and isinstance(v, ReferenceValue) and isinstance(v.ufl_operands[0], FormArgument) ): # Case: d/dt [w + t v] return apply_grads(v) return self.independent_terminal(o) def jacobian(self, o): """Differentiate a jacobian.""" # d (grad_X(x))/d x => grad_X(Argument(x.function_space()) for w, v in zip(self._w, self._v): if extract_unique_domain(o) == extract_unique_domain(w) and isinstance( v.ufl_operands[0], FormArgument ): return ReferenceGrad(v) return self.independent_terminal(o) class CoordinateDerivativeRuleDispatcher(MultiFunction): """Dispatcher.""" def __init__(self): """Initialise.""" MultiFunction.__init__(self) self.vcache = defaultdict(dict) self.rcache = defaultdict(dict) def terminal(self, o): """Apply to a terminal.""" return o def derivative(self, o): """Apply to a derivative.""" raise NotImplementedError(f"Missing derivative handler for {type(o).__name__}.") expr = MultiFunction.reuse_if_untouched def grad(self, o): """Apply to a grad.""" return o def reference_grad(self, o): """Apply to a reference_grad.""" return o def coefficient_derivative(self, o): """Apply to a coefficient_derivative.""" return o def coordinate_derivative(self, o, f, w, v, cd): """Apply to a coordinate_derivative.""" from ufl.algorithms import extract_unique_elements for space in extract_unique_elements(o): if isinstance(space.pullback, CustomPullback): raise NotImplementedError( "CoordinateDerivative is not supported for elements with custom pull back." ) _, w, v, cd = o.ufl_operands rules = CoordinateDerivativeRuleset(w, v, cd) key = (CoordinateDerivativeRuleset, w, v, cd) return map_expr_dag(rules, f, vcache=self.vcache[key], rcache=self.rcache[key]) def apply_coordinate_derivatives(expression): """Apply coordinate derivatives to an expression.""" rules = CoordinateDerivativeRuleDispatcher() return map_integrand_dags(rules, expression) ufl-2024.2.0/ufl/algorithms/apply_function_pullbacks.py000066400000000000000000000034641470142567200231510ustar00rootroot00000000000000"""Algorithm for replacing gradients in an expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.algorithms.map_integrands import map_integrand_dags from ufl.classes import ReferenceValue from ufl.corealg.multifunction import MultiFunction, memoized_handler class FunctionPullbackApplier(MultiFunction): """A pull back applier.""" def __init__(self): """Initalise.""" MultiFunction.__init__(self) expr = MultiFunction.reuse_if_untouched def terminal(self, t): """Apply to a terminal.""" return t @memoized_handler def form_argument(self, o): """Apply to a form_argument.""" # Represent 0-derivatives of form arguments on reference # element r = ReferenceValue(o) space = o.ufl_function_space() element = o.ufl_element() if r.ufl_shape != element.reference_value_shape: raise ValueError( "Expecting reference space expression with shape " f"'{element.reference_value_shape}', got '{r.ufl_shape}'" ) f = element.pullback.apply(r) if f.ufl_shape != space.value_shape: raise ValueError( f"Expecting pulled back expression with shape '{space.value_shape}', " f"got '{f.ufl_shape}'" ) assert f.ufl_shape == o.ufl_shape return f def apply_function_pullbacks(expr): """Change representation of coefficients and arguments in an expression. Applies Piola mappings where applicable and represents all form arguments in reference value. Args: expr: An Expression """ return map_integrand_dags(FunctionPullbackApplier(), expr) ufl-2024.2.0/ufl/algorithms/apply_geometry_lowering.py000066400000000000000000000422261470142567200230240ustar00rootroot00000000000000"""Algorithm for lowering abstractions of geometric types. This means replacing high-level types with expressions of mostly the Jacobian and reference cell data. """ # Copyright (C) 2013-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import warnings from functools import reduce from itertools import combinations from ufl.classes import ( CellCoordinate, CellEdgeVectors, CellFacetJacobian, CellOrientation, CellOrigin, CellVertices, CellVolume, Expr, FacetEdgeVectors, FacetJacobian, FacetJacobianDeterminant, FloatValue, Form, Integral, Jacobian, JacobianDeterminant, JacobianInverse, MaxCellEdgeLength, ReferenceCellVolume, ReferenceFacetVolume, ReferenceGrad, ReferenceNormal, SpatialCoordinate, ) from ufl.compound_expressions import cross_expr, determinant_expr, inverse_expr from ufl.core.multiindex import Index, indices from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction, memoized_handler from ufl.domain import extract_unique_domain from ufl.measure import custom_integral_types, point_integral_types from ufl.operators import conj, max_value, min_value, real, sqrt from ufl.tensors import as_tensor, as_vector class GeometryLoweringApplier(MultiFunction): """Geometry lowering.""" def __init__(self, preserve_types=()): """Initialise.""" MultiFunction.__init__(self) # Store preserve_types as boolean lookup table self._preserve_types = [False] * Expr._ufl_num_typecodes_ for cls in preserve_types: self._preserve_types[cls._ufl_typecode_] = True expr = MultiFunction.reuse_if_untouched def terminal(self, t): """Apply to terminal.""" return t @memoized_handler def jacobian(self, o): """Apply to jacobian.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if not domain.ufl_coordinate_element().pullback.is_identity: raise ValueError("Piola mapped coordinates are not implemented.") # Note: No longer supporting domain.coordinates(), always # preserving SpatialCoordinate object. However if Jacobians # are not preserved, using # ReferenceGrad(SpatialCoordinate(domain)) to represent them. x = self.spatial_coordinate(SpatialCoordinate(domain)) return ReferenceGrad(x) @memoized_handler def _future_jacobian(self, o): """Apply to _future_jacobian.""" # If we're not using Coefficient to represent the spatial # coordinate, we can just as well just return o here too # unless we add representation of basis functions and dofs to # the ufl layer (which is nice to avoid). return o @memoized_handler def jacobian_inverse(self, o): """Apply to jacobian_inverse.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) # TODO: This could in principle use # preserve_types[JacobianDeterminant] with minor refactoring: K = inverse_expr(J) return K @memoized_handler def jacobian_determinant(self, o): """Apply to jacobian_determinant.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) detJ = determinant_expr(J) # TODO: Is "signing" the determinant for manifolds the # cleanest approach? The alternative is to have a # specific type for the unsigned pseudo-determinant. if domain.topological_dimension() < domain.geometric_dimension(): co = CellOrientation(domain) detJ = co * detJ return detJ @memoized_handler def facet_jacobian(self, o): """Apply to facet_jacobian.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) RFJ = CellFacetJacobian(domain) i, j, k = indices(3) return as_tensor(J[i, k] * RFJ[k, j], (i, j)) @memoized_handler def facet_jacobian_inverse(self, o): """Apply to facet_jacobian_inverse.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) FJ = self.facet_jacobian(FacetJacobian(domain)) # This could in principle use # preserve_types[JacobianDeterminant] with minor refactoring: return inverse_expr(FJ) @memoized_handler def facet_jacobian_determinant(self, o): """Apply to facet_jacobian_determinant.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) FJ = self.facet_jacobian(FacetJacobian(domain)) detFJ = determinant_expr(FJ) # TODO: Should we "sign" the facet jacobian determinant for # manifolds? It's currently used unsigned in # apply_integral_scaling. # if domain.topological_dimension() < domain.geometric_dimension(): # co = CellOrientation(domain) # detFJ = co*detFJ return detFJ @memoized_handler def spatial_coordinate(self, o): """Apply to spatial_coordinate. Fall through to coordinate field of domain if it exists. """ if self._preserve_types[o._ufl_typecode_]: return o if not extract_unique_domain(o).ufl_coordinate_element().pullback.is_identity: raise ValueError("Piola mapped coordinates are not implemented.") # No longer supporting domain.coordinates(), always preserving # SpatialCoordinate object. return o @memoized_handler def cell_coordinate(self, o): """Apply to cell_coordinate. Compute from physical coordinates if they are known, using the appropriate mappings. """ if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) K = self.jacobian_inverse(JacobianInverse(domain)) x = self.spatial_coordinate(SpatialCoordinate(domain)) x0 = CellOrigin(domain) i, j = indices(2) X = as_tensor(K[i, j] * (x[j] - x0[j]), (i,)) return X @memoized_handler def facet_cell_coordinate(self, o): """Apply to facet_cell_coordinate.""" if self._preserve_types[o._ufl_typecode_]: return o raise ValueError( "Missing computation of facet reference coordinates " "from physical coordinates via mappings." ) @memoized_handler def cell_volume(self, o): """Apply to cell_volume.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if not domain.is_piecewise_linear_simplex_domain(): # Don't lower for non-affine cells, instead leave it to # form compiler warnings.warn("Only know how to compute the cell volume of an affine cell.") return o r = self.jacobian_determinant(JacobianDeterminant(domain)) r0 = ReferenceCellVolume(domain) return abs(r * r0) @memoized_handler def facet_area(self, o): """Apply to facet_area.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) tdim = domain.topological_dimension() if not domain.is_piecewise_linear_simplex_domain(): # Don't lower for non-affine cells, instead leave it to # form compiler warnings.warn("Only know how to compute the facet area of an affine cell.") return o # Area of "facet" of interval (i.e. "area" of a vertex) is defined as 1.0 if tdim == 1: return FloatValue(1.0) r = self.facet_jacobian_determinant(FacetJacobianDeterminant(domain)) r0 = ReferenceFacetVolume(domain) return abs(r * r0) @memoized_handler def circumradius(self, o): """Apply to circumradius.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if not domain.is_piecewise_linear_simplex_domain(): raise ValueError("Circumradius only makes sense for affine simplex cells") cellname = domain.ufl_cell().cellname() cellvolume = self.cell_volume(CellVolume(domain)) if cellname == "interval": # Optimization for square interval; no square root needed return 0.5 * cellvolume # Compute lengths of cell edges edges = CellEdgeVectors(domain) num_edges = edges.ufl_shape[0] j = Index() elen = [real(sqrt(real(edges[e, j] * conj(edges[e, j])))) for e in range(num_edges)] if cellname == "triangle": return (elen[0] * elen[1] * elen[2]) / (4.0 * cellvolume) elif cellname == "tetrahedron": # la, lb, lc = lengths of the sides of an intermediate triangle # NOTE: Is here some hidden numbering assumption? la = elen[3] * elen[2] lb = elen[4] * elen[1] lc = elen[5] * elen[0] # p = perimeter p = la + lb + lc # s = semiperimeter s = p / 2 # area of intermediate triangle with Herons formula triangle_area = sqrt(s * (s - la) * (s - lb) * (s - lc)) return triangle_area / (6.0 * cellvolume) @memoized_handler def max_cell_edge_length(self, o): """Apply to max_cell_edge_length.""" return self._reduce_cell_edge_length(o, max_value) @memoized_handler def min_cell_edge_length(self, o): """Apply to min_cell_edge_length.""" return self._reduce_cell_edge_length(o, min_value) def _reduce_cell_edge_length(self, o, reduction_op): """Apply to _reduce_cell_edge_length.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler warnings.warn("Only know how to compute cell edge lengths of P1 or Q1 cell.") return o elif domain.ufl_cell().cellname() == "interval": # Interval optimization, square root not needed return self.cell_volume(CellVolume(domain)) else: # Other P1 or Q1 cells edges = CellEdgeVectors(domain) num_edges = edges.ufl_shape[0] j = Index() elen2 = [real(edges[e, j] * conj(edges[e, j])) for e in range(num_edges)] return real(sqrt(reduce(reduction_op, elen2))) @memoized_handler def cell_diameter(self, o): """Apply to cell_diameter.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler warnings.warn("Only know how to compute cell diameter of P1 or Q1 cell.") return o elif domain.is_piecewise_linear_simplex_domain(): # Simplices return self.max_cell_edge_length(MaxCellEdgeLength(domain)) else: # Q1 cells, maximal distance between any two vertices verts = CellVertices(domain) verts = [verts[v, ...] for v in range(verts.ufl_shape[0])] j = Index() elen2 = (real((v0 - v1)[j] * conj((v0 - v1)[j])) for v0, v1 in combinations(verts, 2)) return real(sqrt(reduce(max_value, elen2))) @memoized_handler def max_facet_edge_length(self, o): """Apply to max_facet_edge_length.""" return self._reduce_facet_edge_length(o, max_value) @memoized_handler def min_facet_edge_length(self, o): """Apply to min_facet_edge_length.""" return self._reduce_facet_edge_length(o, min_value) def _reduce_facet_edge_length(self, o, reduction_op): """Apply to _reduce_facet_edge_length.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) if domain.ufl_cell().topological_dimension() < 3: raise ValueError("Facet edge lengths only make sense for topological dimension >= 3.") elif domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler warnings.warn("Only know how to compute facet edge lengths of P1 or Q1 cell.") return o else: # P1 tetrahedron or Q1 hexahedron edges = FacetEdgeVectors(domain) num_edges = edges.ufl_shape[0] j = Index() elen2 = [real(edges[e, j] * conj(edges[e, j])) for e in range(num_edges)] return real(sqrt(reduce(reduction_op, elen2))) @memoized_handler def cell_normal(self, o): """Apply to cell_normal.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) gdim = domain.geometric_dimension() tdim = domain.topological_dimension() if tdim == gdim - 1: # n-manifold embedded in n-1 space i = Index() J = self.jacobian(Jacobian(domain)) if tdim == 2: # Surface in 3D t0 = as_vector(J[i, 0], i) t1 = as_vector(J[i, 1], i) cell_normal = cross_expr(t0, t1) elif tdim == 1: # Line in 2D (cell normal is 'up' for a line pointing # to the 'right') cell_normal = as_vector((-J[1, 0], J[0, 0])) else: raise ValueError(f"Cell normal not implemented for tdim {tdim}, gdim {gdim}") # Return normalized vector, sign corrected by cell # orientation co = CellOrientation(domain) return co * cell_normal / sqrt(cell_normal[i] * cell_normal[i]) else: raise ValueError(f"Cell normal undefined for tdim {tdim}, gdim {gdim}") @memoized_handler def facet_normal(self, o): """Apply to facet_normal.""" if self._preserve_types[o._ufl_typecode_]: return o domain = extract_unique_domain(o) tdim = domain.topological_dimension() if tdim == 1: # Special-case 1D (possibly immersed), for which we say # that n is just in the direction of J. J = self.jacobian(Jacobian(domain)) # dx/dX ndir = J[:, 0] gdim = domain.geometric_dimension() if gdim == 1: nlen = abs(ndir[0]) else: i = Index() nlen = sqrt(ndir[i] * ndir[i]) rn = ReferenceNormal(domain) # +/- 1.0 here n = rn[0] * ndir / nlen r = n else: # Recall that the covariant Piola transform u -> J^(-T)*u # preserves tangential components. The normal vector is # characterised by having zero tangential component in # reference and physical space. Jinv = self.jacobian_inverse(JacobianInverse(domain)) i, j = indices(2) rn = ReferenceNormal(domain) # compute signed, unnormalised normal; note transpose ndir = as_vector(Jinv[j, i] * rn[j], i) # normalise i = Index() n = ndir / sqrt(ndir[i] * ndir[i]) r = n if r.ufl_shape != o.ufl_shape: raise ValueError( f"Inconsistent dimensions (in={o.ufl_shape[0]}, out={r.ufl_shape[0]})." ) return r def apply_geometry_lowering(form, preserve_types=()): """Change GeometricQuantity objects in expression to the lowest level GeometricQuantity objects. Assumes the expression is preprocessed or at least that derivatives have been expanded. Args: form: An Expr or Form. preserve_types: Preserved types """ if isinstance(form, Form): newintegrals = [ apply_geometry_lowering(integral, preserve_types) for integral in form.integrals() ] return Form(newintegrals) elif isinstance(form, Integral): integral = form if integral.integral_type() in (custom_integral_types + point_integral_types): automatic_preserve_types = [SpatialCoordinate, Jacobian] else: automatic_preserve_types = [CellCoordinate] preserve_types = set(preserve_types) | set(automatic_preserve_types) mf = GeometryLoweringApplier(preserve_types) newintegrand = map_expr_dag(mf, integral.integrand()) return integral.reconstruct(integrand=newintegrand) elif isinstance(form, Expr): expr = form mf = GeometryLoweringApplier(preserve_types) return map_expr_dag(mf, expr) else: raise ValueError(f"Invalid type {form.__class__.__name__}") ufl-2024.2.0/ufl/algorithms/apply_integral_scaling.py000066400000000000000000000113521470142567200225640ustar00rootroot00000000000000"""Algorithm for replacing gradients in an expression.""" # Copyright (C) 2013-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree from ufl.classes import ( FacetJacobianDeterminant, Form, Integral, JacobianDeterminant, QuadratureWeight, ) from ufl.differentiation import CoordinateDerivative from ufl.measure import custom_integral_types, point_integral_types def compute_integrand_scaling_factor(integral): """Change integrand geometry to the right representations.""" domain = integral.ufl_domain() integral_type = integral.integral_type() # co = CellOrientation(domain) weight = QuadratureWeight(domain) tdim = domain.topological_dimension() # gdim = domain.geometric_dimension() # Polynomial degree of integrand scaling degree = 0 if integral_type == "cell": if tdim > 0: detJ = JacobianDeterminant(domain) degree = estimate_total_polynomial_degree(apply_geometry_lowering(detJ)) # Despite the abs, |detJ| is polynomial except for # self-intersecting cells, where we have other problems. scale = abs(detJ) * weight else: # No need to scale 'integral' over a vertex scale = 1 elif integral_type.startswith("exterior_facet"): if tdim > 1: # Scaling integral by facet jacobian determinant and # quadrature weight detFJ = FacetJacobianDeterminant(domain) degree = estimate_total_polynomial_degree(apply_geometry_lowering(detFJ)) scale = detFJ * weight else: # No need to scale 'integral' over a vertex scale = 1 elif integral_type.startswith("interior_facet"): if tdim > 1: # Scaling integral by facet jacobian determinant from one # side and quadrature weight detFJ = FacetJacobianDeterminant(domain) degree = estimate_total_polynomial_degree(apply_geometry_lowering(detFJ)) scale = detFJ("+") * weight else: # No need to scale 'integral' over a vertex scale = 1 elif integral_type in custom_integral_types: # Scaling with custom weight, which includes eventual volume # scaling scale = weight elif integral_type in point_integral_types: # No need to scale 'integral' over a point scale = 1 else: raise ValueError(f"Unknown integral type {integral_type}, don't know how to scale.") return scale, degree def apply_integral_scaling(form): """Multiply integrands by a factor to scale the integral to reference frame.""" # TODO: Consider adding an in_reference_frame property to Integral # and checking it here and setting it in the returned form if isinstance(form, Form): newintegrals = [apply_integral_scaling(integral) for integral in form.integrals()] return Form(newintegrals) elif isinstance(form, Integral): integral = form integrand = integral.integrand() # Compute and apply integration scaling factor since we want to compute # coordinate derivatives at the end, the scaling factor has to be moved # inside those scale, degree = compute_integrand_scaling_factor(integral) md = {} md.update(integral.metadata()) new_degree = degree cur_degree = md.get("estimated_polynomial_degree") if cur_degree is not None: if isinstance(cur_degree, tuple) and isinstance(degree, tuple): new_degree = tuple(d[0] + d[1] for d in zip(cur_degree, degree)) elif isinstance(cur_degree, tuple): new_degree = tuple(d + degree for d in cur_degree) elif isinstance(degree, tuple): new_degree = tuple(cur_degree + d for d in degree) else: new_degree = cur_degree + degree md["estimated_polynomial_degree"] = new_degree def scale_coordinate_derivative(o, scale): """Scale the coordinate derivative.""" o_ = o.ufl_operands if isinstance(o, CoordinateDerivative): return CoordinateDerivative( scale_coordinate_derivative(o_[0], scale), o_[1], o_[2], o_[3] ) else: return scale * o newintegrand = scale_coordinate_derivative(integrand, scale) return integral.reconstruct(integrand=newintegrand, metadata=md) else: raise ValueError(f"Invalid type {form.__class__.__name__}") ufl-2024.2.0/ufl/algorithms/apply_restrictions.py000066400000000000000000000217071470142567200220140ustar00rootroot00000000000000"""Apply restrictions. This module contains the apply_restrictions algorithm which propagates restrictions in a form towards the terminals. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.algorithms.map_integrands import map_integrand_dags from ufl.classes import Restricted from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction from ufl.domain import extract_unique_domain from ufl.measure import integral_type_to_measure_name from ufl.sobolevspace import H1 class RestrictionPropagator(MultiFunction): """Restriction propagator.""" def __init__(self, side=None): """Initialise.""" MultiFunction.__init__(self) self.current_restriction = side self.default_restriction = "+" # Caches for propagating the restriction with map_expr_dag self.vcaches = {"+": {}, "-": {}} self.rcaches = {"+": {}, "-": {}} if self.current_restriction is None: self._rp = {"+": RestrictionPropagator("+"), "-": RestrictionPropagator("-")} def restricted(self, o): """When hitting a restricted quantity, visit child with a separate restriction algorithm.""" # Assure that we have only two levels here, inside or outside # the Restricted node if self.current_restriction is not None: raise ValueError("Cannot restrict an expression twice.") # Configure a propagator for this side and apply to subtree side = o.side() return map_expr_dag( self._rp[side], o.ufl_operands[0], vcache=self.vcaches[side], rcache=self.rcaches[side] ) # --- Reusable rules def _ignore_restriction(self, o): """Ignore current restriction. Quantity is independent of side also from a computational point of view. """ return o def _require_restriction(self, o): """Restrict a discontinuous quantity to current side, require a side to be set.""" if self.current_restriction is None: raise ValueError(f"Discontinuous type {o._ufl_class_.__name__} must be restricted.") return o(self.current_restriction) def _default_restricted(self, o): """Restrict a continuous quantity to default side if no current restriction is set.""" r = self.current_restriction if r is None: r = self.default_restriction return o(r) def _opposite(self, o): """Restrict a quantity to default side. If the current restriction is different swap the sign, require a side to be set. """ if self.current_restriction is None: raise ValueError(f"Discontinuous type {o._ufl_class_.__name__} must be restricted.") elif self.current_restriction == self.default_restriction: return o(self.default_restriction) else: return -o(self.default_restriction) def _missing_rule(self, o): """Raise an error.""" raise ValueError(f"Missing rule for {o._ufl_class_.__name__}") # --- Rules for operators # Default: Operators should reconstruct only if subtrees are not touched operator = MultiFunction.reuse_if_untouched # Assuming apply_derivatives has been called, # propagating Grad inside the Restricted nodes. # Considering all grads to be discontinuous, may # want something else for facet functions in future. grad = _require_restriction def variable(self, o, op, label): """Strip variable.""" return op def reference_value(self, o): """Reference value of something follows same restriction rule as the underlying object.""" (f,) = o.ufl_operands assert f._ufl_is_terminal_ g = self(f) if isinstance(g, Restricted): side = g.side() return o(side) else: return o # --- Rules for terminals # Require handlers to be specified for all terminals terminal = _missing_rule multi_index = _ignore_restriction label = _ignore_restriction # Default: Literals should ignore restriction constant_value = _ignore_restriction constant = _ignore_restriction # Even arguments with continuous elements such as Lagrange must be # restricted to associate with the right part of the element # matrix argument = _require_restriction # Defaults for geometric quantities geometric_cell_quantity = _require_restriction geometric_facet_quantity = _require_restriction # Only a few geometric quantities are independent on the restriction: facet_coordinate = _ignore_restriction quadrature_weight = _ignore_restriction # Assuming homogeoneous mesh reference_cell_volume = _ignore_restriction reference_facet_volume = _ignore_restriction def coefficient(self, o): """Restrict a coefficient. Allow coefficients to be unrestricted (apply default if so) if the values are fully continuous across the facet. """ if o.ufl_element() in H1: # If the coefficient _value_ is _fully_ continuous # It must still be computed from one of the sides, we just don't care which return self._default_restricted(o) else: return self._require_restriction(o) def facet_normal(self, o): """Restrict a facet_normal.""" D = extract_unique_domain(o) e = D.ufl_coordinate_element() gd = D.geometric_dimension() td = D.topological_dimension() if e.embedded_superdegree <= 1 and e in H1 and gd == td: # For meshes with a continuous linear non-manifold # coordinate field, the facet normal from side - points in # the opposite direction of the one from side +. We must # still require a side to be chosen by the user but # rewrite n- -> n+. This is an optimization, possibly # premature, however it's more difficult to do at a later # stage. return self._opposite(o) else: # For other meshes, we require a side to be # chosen by the user and respect that return self._require_restriction(o) def apply_restrictions(expression): """Propagate restriction nodes to wrap differential terminals directly.""" integral_types = [ k for k in integral_type_to_measure_name.keys() if k.startswith("interior_facet") ] rules = RestrictionPropagator() return map_integrand_dags(rules, expression, only_integral_type=integral_types) class DefaultRestrictionApplier(MultiFunction): """Default restriction applier.""" def __init__(self, side=None): """Initialise.""" MultiFunction.__init__(self) self.current_restriction = side self.default_restriction = "+" if self.current_restriction is None: self._rp = {"+": DefaultRestrictionApplier("+"), "-": DefaultRestrictionApplier("-")} def terminal(self, o): """Apply to terminal.""" # Most terminals are unchanged return o # Default: Operators should reconstruct only if subtrees are not touched operator = MultiFunction.reuse_if_untouched def restricted(self, o): """Apply to restricted.""" # Don't restrict twice return o def derivative(self, o): """Apply to derivative.""" # I don't think it's safe to just apply default restriction # to the argument of any derivative, i.e. grad(cg1_function) # is not continuous across cells even if cg1_function is. return o def _default_restricted(self, o): """Restrict a continuous quantity to default side if no current restriction is set.""" r = self.current_restriction if r is None: r = self.default_restriction return o(r) # These are the same from either side but to compute them # cell (or facet) data from one side must be selected: spatial_coordinate = _default_restricted # Depends on cell only to get to the facet: facet_jacobian = _default_restricted facet_jacobian_determinant = _default_restricted facet_jacobian_inverse = _default_restricted # facet_tangents = _default_restricted # facet_midpoint = _default_restricted facet_area = _default_restricted # facet_diameter = _default_restricted min_facet_edge_length = _default_restricted max_facet_edge_length = _default_restricted facet_origin = _default_restricted # FIXME: Is this valid for quads? def apply_default_restrictions(expression): """Some terminals can be restricted from either side. This applies a default restriction to such terminals if unrestricted. """ integral_types = [ k for k in integral_type_to_measure_name.keys() if k.startswith("interior_facet") ] rules = DefaultRestrictionApplier() return map_integrand_dags(rules, expression, only_integral_type=integral_types) ufl-2024.2.0/ufl/algorithms/balancing.py000066400000000000000000000044151470142567200177720ustar00rootroot00000000000000"""Balancing.""" # -*- coding: utf-8 -*- # Copyright (C) 2011-2017 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.classes import ( CellAvg, FacetAvg, Grad, Indexed, NegativeRestricted, PositiveRestricted, ReferenceGrad, ReferenceValue, ) from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction modifier_precedence = [ ReferenceValue, ReferenceGrad, Grad, CellAvg, FacetAvg, PositiveRestricted, NegativeRestricted, Indexed, ] modifier_precedence = {m._ufl_handler_name_: i for i, m in enumerate(modifier_precedence)} def balance_modified_terminal(expr): """Balance modified terminal.""" # NB! Assuming e.g. grad(cell_avg(expr)) does not occur, # i.e. it is simplified to 0 immediately. if expr._ufl_is_terminal_: return expr assert expr._ufl_is_terminal_modifier_ orig = expr # Build list of modifier layers layers = [expr] while not expr._ufl_is_terminal_: assert expr._ufl_is_terminal_modifier_ expr = expr.ufl_operands[0] layers.append(expr) assert layers[-1] is expr assert expr._ufl_is_terminal_ # Apply modifiers in order layers = sorted(layers[:-1], key=lambda e: modifier_precedence[e._ufl_handler_name_]) for op in layers: ops = (expr,) + op.ufl_operands[1:] expr = op._ufl_expr_reconstruct_(*ops) # Preserve id if nothing has changed return orig if expr == orig else expr class BalanceModifiers(MultiFunction): """Balance modifiers.""" def expr(self, expr, *ops): """Apply to expr.""" return expr._ufl_expr_reconstruct_(*ops) def terminal(self, expr): """Apply to terminal.""" return expr def _modifier(self, expr, *ops): """Apply to _modifier.""" return balance_modified_terminal(expr) reference_value = _modifier reference_grad = _modifier grad = _modifier cell_avg = _modifier facet_avg = _modifier positive_restricted = _modifier negative_restricted = _modifier def balance_modifiers(expr): """Balance modifiers.""" mf = BalanceModifiers() return map_expr_dag(mf, expr) ufl-2024.2.0/ufl/algorithms/change_to_reference.py000066400000000000000000000203201470142567200220120ustar00rootroot00000000000000"""Algorithm for replacing gradients in an expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.algorithms.apply_function_pullbacks import apply_function_pullbacks from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering from ufl.checks import is_cellwise_constant from ufl.classes import Grad, JacobianInverse, ReferenceGrad, ReferenceValue, Restricted from ufl.core.multiindex import indices from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction from ufl.domain import extract_unique_domain from ufl.tensors import as_tensor """ # Some notes: # Below, let v_i mean physical coordinate of vertex i and V_i mean the reference cell coordinate of the same vertex. # Add a type for CellVertices? Note that vertices must be computed in linear cell cases! triangle_vertices[i,j] = component j of vertex i, following ufc numbering conventions # DONE Add a type for CellEdgeLengths? Note that these are only easy to define in the linear cell case! triangle_edge_lengths = [v1v2, v0v2, v0v1] # shape (3,) tetrahedron_edge_lengths = [v0v1, v0v2, v0v3, v1v2, v1v3, v2v3] # shape (6,) # DONE Here's how to compute edge lengths from the Jacobian: J =[ [dx0/dX0, dx0/dX1], [dx1/dX0, dx1/dX1] ] # First compute the edge vector, which is constant for each edge: the vector from one vertex to the other reference_edge_vector_0 = V2 - V1 # Example! Add a type ReferenceEdgeVectors? # Then apply J to it and take the length of the resulting vector, this is generic for affine cells edge_length_i = || dot(J, reference_edge_vector_i) || e2 = || J[:,0] . < 1, 0> || = || J[:,0] || = || dx/dX0 || = edge length of edge 2 (v0-v1) e1 = || J[:,1] . < 0, 1> || = || J[:,1] || = || dx/dX1 || = edge length of edge 1 (v0-v2) e0 = || J[:,:] . <-1, 1> || = || < J[0,1]-J[0,0], J[1,1]-J[1,0] > || = || dx/dX <-1,1> || = edge length of edge 0 (v1-v2) trev = triangle_reference_edge_vector evec0 = J00 * trev[edge][0] + J01 * trev[edge][1] = J*trev[edge] evec1 = J10 * trev[edge][0] + J11 * trev[edge][1] elen[edge] = sqrt(evec0*evec0 + evec1*evec1) = sqrt((J*trev[edge])**2) trev = triangle_reference_edge_vector evec0 = J00 * trev[edge][0] + J01 * trev[edge][1] = J*trev evec1 = J10 * trev[edge][0] + J11 * trev[edge][1] evec2 = J20 * trev[edge][0] + J21 * trev[edge][1] # Manifold: triangle in 3D elen[edge] = sqrt(evec0*evec0 + evec1*evec1 + evec2*evec2) = sqrt((J*trev[edge])**2) trev = tetrahedron_reference_edge_vector evec0 = sum(J[0,k] * trev[edge][k] for k in range(3)) evec1 = sum(J[1,k] * trev[edge][k] for k in range(3)) evec2 = sum(J[2,k] * trev[edge][k] for k in range(3)) elen[edge] = sqrt(evec0*evec0 + evec1*evec1 + evec2*evec2) = sqrt((J*trev[edge])**2) # DONE Here's how to compute min/max facet edge length: triangle: r = facetarea tetrahedron: min(elen[edge] for edge in range(6)) or min( min(elen[0], min(elen[1], elen[2])), min(elen[3], min(elen[4], elen[5])) ) or min1 = min_value(elen[0], min_value(elen[1], elen[2])) min2 = min_value(elen[3], min_value(elen[4], elen[5])) r = min_value(min1, min2) (want proper Min/Max types for this!) # DONE Here's how to compute circumradius for an interval: circumradius_interval = cellvolume / 2 # DONE Here's how to compute circumradius for a triangle: e0 = elen[0] e1 = elen[1] e2 = elen[2] circumradius_triangle = (e0*e1*e2) / (4*cellvolume) # DONE Here's how to compute circumradius for a tetrahedron: # v1v2 = edge length between vertex 1 and 2 # la,lb,lc = lengths of the sides of an intermediate triangle la = v1v2 * v0v3 lb = v0v2 * v1v3 lc = v0v1 * v2v3 # p = perimeter p = (la + lb + lc) # s = semiperimeter s = p / 2 # area of intermediate triangle with Herons formula tmp_area = sqrt(s * (s - la) * (s - lb) * (s - lc)) circumradius_tetrahedron = tmp_area / (6*cellvolume) """ class ChangeToReferenceGrad(MultiFunction): """Change to reference grad.""" def __init__(self): """Initalise.""" MultiFunction.__init__(self) expr = MultiFunction.reuse_if_untouched def terminal(self, o): """Apply to terminal.""" return o def grad(self, o): """Apply to grad.""" # Peel off the Grads and count them, and get restriction if # it's between the grad and the terminal ngrads = 0 restricted = "" rv = False while not o._ufl_is_terminal_: if isinstance(o, Grad): (o,) = o.ufl_operands ngrads += 1 elif isinstance(o, Restricted): restricted = o.side() (o,) = o.ufl_operands elif isinstance(o, ReferenceValue): rv = True (o,) = o.ufl_operands else: raise ValueError(f"Invalid type {o._ufl_class_.__name__}") f = o if rv: f = ReferenceValue(f) # Get domain and create Jacobian inverse object domain = extract_unique_domain(o) Jinv = JacobianInverse(domain) if is_cellwise_constant(Jinv): # Optimise slightly by turning Grad(Grad(...)) into # J^(-T)J^(-T)RefGrad(RefGrad(...)) # rather than J^(-T)RefGrad(J^(-T)RefGrad(...)) # Create some new indices ii = indices(len(f.ufl_shape)) # Indices to get to the scalar component of f jj = indices( ngrads ) # Indices to sum over the local gradient axes with the inverse Jacobian kk = indices(ngrads) # Indices for the leftover inverse Jacobian axes # Preserve restricted property if restricted: Jinv = Jinv(restricted) f = f(restricted) # Apply the same number of ReferenceGrad without mappings lgrad = f for i in range(ngrads): lgrad = ReferenceGrad(lgrad) # Apply mappings with scalar indexing operations (assumes # ReferenceGrad(Jinv) is zero) jinv_lgrad_f = lgrad[ii + jj] for j, k in zip(jj, kk): jinv_lgrad_f = Jinv[j, k] * jinv_lgrad_f # Wrap back in tensor shape, derivative axes at the end jinv_lgrad_f = as_tensor(jinv_lgrad_f, ii + kk) else: # J^(-T)RefGrad(J^(-T)RefGrad(...)) # Preserve restricted property if restricted: Jinv = Jinv(restricted) f = f(restricted) jinv_lgrad_f = f for foo in range(ngrads): ii = indices( len(jinv_lgrad_f.ufl_shape) ) # Indices to get to the scalar component of f j, k = indices(2) lgrad = ReferenceGrad(jinv_lgrad_f) jinv_lgrad_f = Jinv[j, k] * lgrad[ii + (j,)] # Wrap back in tensor shape, derivative axes at the end jinv_lgrad_f = as_tensor(jinv_lgrad_f, ii + (k,)) return jinv_lgrad_f def reference_grad(self, o): """Apply to reference_grad.""" raise ValueError("Not expecting reference grad while applying change to reference grad.") def coefficient_derivative(self, o): """Apply to coefficient_derivative.""" raise ValueError( "Coefficient derivatives should be expanded before applying change to reference grad." ) def change_to_reference_grad(e): """Change Grad objects in expression to products of JacobianInverse and ReferenceGrad. Assumes the expression is preprocessed or at least that derivatives have been expanded. Args: e: An Expr or Form. """ mf = ChangeToReferenceGrad() return map_expr_dag(mf, e) def change_integrand_geometry_representation(integrand, scale, integral_type): """Change integrand geometry to the right representations.""" integrand = apply_function_pullbacks(integrand) integrand = change_to_reference_grad(integrand) integrand = integrand * scale if integral_type == "quadrature": physical_coordinates_known = True else: physical_coordinates_known = False integrand = apply_geometry_lowering(integrand, physical_coordinates_known) return integrand ufl-2024.2.0/ufl/algorithms/check_arities.py000066400000000000000000000165131470142567200206530ustar00rootroot00000000000000"""Check arities.""" from itertools import chain from typing import Tuple from ufl.classes import Argument, Zero from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction from ufl.corealg.traversal import traverse_unique_terminals class ArityMismatch(BaseException): """Arity mismatch exception.""" pass def _afmt(atuple: Tuple[Argument, bool]) -> str: """Return a string representation of an arity tuple.""" arg, conj = atuple return f"conj({arg})" if conj else str(arg) class ArityChecker(MultiFunction): """Arity checker.""" def __init__(self, arguments): """Initialise.""" MultiFunction.__init__(self) self.arguments = arguments self._et = () def terminal(self, o): """Apply to terminal.""" return self._et def argument(self, o): """Apply to argument.""" return ((o, False),) def nonlinear_operator(self, o): """Apply to nonlinear_operator.""" # Cutoff traversal by not having *ops in argument list of this # handler. Traverse only the terminals under here the fastest # way we know of: for t in traverse_unique_terminals(o): if t._ufl_typecode_ == Argument._ufl_typecode_: raise ArityMismatch( f"Applying nonlinear operator {o._ufl_class_.__name__} to " f"expression depending on form argument {t}." ) return self._et expr = nonlinear_operator def sum(self, o, a, b): """Apply to sum.""" if a != b: raise ArityMismatch( f"Adding expressions with non-matching form arguments {_afmt(a)} vs {_afmt(b)}." ) return a def division(self, o, a, b): """Apply to division.""" if b: raise ArityMismatch(f"Cannot divide by form argument {b}.") return a def product(self, o, a, b): """Apply to product.""" if a and b: # Check that we don't have test*test, trial*trial, even # for different parts in a block system anumbers = set(x[0].number() for x in a) for x in b: if x[0].number() in anumbers: raise ArityMismatch( "Multiplying expressions with overlapping form argument number " f"{x[0].number()}, argument is {_afmt(x)}." ) # Combine argument lists c = tuple(sorted(set(a + b), key=lambda x: (x[0].number(), x[0].part()))) # Check that we don't have any arguments shared between a # and b if len(c) != len(a) + len(b) or len(c) != len({x[0] for x in c}): raise ArityMismatch( "Multiplying expressions with overlapping form arguments " f"{_afmt(a)} vs {_afmt(b)}." ) # It's fine for argument parts to overlap return c elif a: return a else: return b # inner, outer and dot all behave as product but for conjugates def inner(self, o, a, b): """Apply to inner.""" return self.product(o, a, self.conj(None, b)) dot = inner def outer(self, o, a, b): """Apply to outer.""" return self.product(o, self.conj(None, a), b) def linear_operator(self, o, a): """Apply to linear_operator.""" return a # Positive and negative restrictions behave as linear operators positive_restricted = linear_operator negative_restricted = linear_operator # Cell and facet average are linear operators cell_avg = linear_operator facet_avg = linear_operator # Grad is a linear operator grad = linear_operator reference_grad = linear_operator reference_value = linear_operator # Conj, is a sesquilinear operator def conj(self, o, a): """Apply to conj.""" return tuple((a_[0], not a_[1]) for a_ in a) # Does it make sense to have a Variable(Argument)? I see no # problem. def variable(self, o, f, a): """Apply to variable.""" return f # Conditional is linear on each side of the condition def conditional(self, o, c, a, b): """Apply to conditional.""" if c: raise ArityMismatch(f"Condition cannot depend on form arguments ({_afmt(a)}).") if a and isinstance(o.ufl_operands[2], Zero): # Allow conditional(c, arg, 0) return a elif b and isinstance(o.ufl_operands[1], Zero): # Allow conditional(c, 0, arg) return b elif a == b: # Allow conditional(c, test, test) return a else: # Do not allow e.g. conditional(c, test, trial), # conditional(c, test, nonzeroconstant) raise ArityMismatch( "Conditional subexpressions with non-matching form arguments " f"{_afmt(a)} vs {_afmt(b)}." ) def linear_indexed_type(self, o, a, i): """Apply to linear_indexed_type.""" return a # All of these indexed thingies behave as a linear_indexed_type indexed = linear_indexed_type index_sum = linear_indexed_type component_tensor = linear_indexed_type def list_tensor(self, o, *ops): """Apply to list_tensor.""" args = set(chain(*ops)) if args: # Check that each list tensor component has the same # argument numbers (ignoring parts) numbers = set(tuple(sorted(set(arg[0].number() for arg in op))) for op in ops) if () in numbers: # Allow e.g. but not numbers.remove(()) if len(numbers) > 1: raise ArityMismatch( "Listtensor components must depend on the same argument numbers, " f"found {numbers}." ) # Allow different parts with the same number return tuple(sorted(args, key=lambda x: (x[0].number(), x[0].part()))) else: # No argument dependencies return self._et def check_integrand_arity(expr, arguments, complex_mode=False): """Check the arity of an integrand.""" arguments = tuple(sorted(set(arguments), key=lambda x: (x.number(), x.part()))) rules = ArityChecker(arguments) arg_tuples = map_expr_dag(rules, expr, compress=False) args = tuple(a[0] for a in arg_tuples) if args != arguments: raise ArityMismatch(f"Integrand arguments {args} differ from form arguments {arguments}.") if complex_mode: # Check that the test function is conjugated and that any # trial function is not conjugated. Further arguments are # treated as trial funtions (i.e. no conjugation) but this # might not be correct. for arg, conj in arg_tuples: if arg.number() == 0 and not conj: raise ArityMismatch("Failure to conjugate test function in complex Form") elif arg.number() > 0 and conj: raise ArityMismatch(f"Argument {arg} is spuriously conjugated in complex Form") def check_form_arity(form, arguments, complex_mode=False): """Check the arity of a form.""" for itg in form.integrals(): check_integrand_arity(itg.integrand(), arguments, complex_mode) ufl-2024.2.0/ufl/algorithms/check_restrictions.py000066400000000000000000000037621470142567200217450ustar00rootroot00000000000000"""Algorithms related to restrictions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction class RestrictionChecker(MultiFunction): """Restiction checker.""" def __init__(self, require_restriction): """Initialise.""" MultiFunction.__init__(self) self.current_restriction = None self.require_restriction = require_restriction def expr(self, o): """Apply to expr.""" pass def restricted(self, o): """Apply to restricted.""" if self.current_restriction is not None: raise ValueError("Not expecting twice restricted expression.") self.current_restriction = o._side (e,) = o.ufl_operands self.visit(e) self.current_restriction = None def facet_normal(self, o): """Apply to facet_normal.""" if self.require_restriction: if self.current_restriction is None: raise ValueError("Facet normal must be restricted in interior facet integrals.") else: if self.current_restriction is not None: raise ValueError("Restrictions are only allowed for interior facet integrals.") def form_argument(self, o): """Apply to form_argument.""" if self.require_restriction: if self.current_restriction is None: raise ValueError("Form argument must be restricted in interior facet integrals.") else: if self.current_restriction is not None: raise ValueError("Restrictions are only allowed for interior facet integrals.") def check_restrictions(expression, require_restriction): """Check that types that must be restricted are restricted in expression.""" rules = RestrictionChecker(require_restriction) return map_expr_dag(rules, expression) ufl-2024.2.0/ufl/algorithms/checks.py000066400000000000000000000076631470142567200173240ustar00rootroot00000000000000"""Functions to check the validity of forms.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # Modified by Mehdi Nikbakht, 2010. from ufl.algorithms.check_restrictions import check_restrictions # UFL algorithms from ufl.algorithms.traversal import iter_expressions from ufl.argument import Argument from ufl.coefficient import Coefficient from ufl.constantvalue import is_true_ufl_scalar # UFL classes from ufl.core.expr import ufl_err_str from ufl.corealg.traversal import traverse_unique_terminals from ufl.domain import extract_unique_domain from ufl.form import Form def validate_form( form, ): # TODO: Can we make this return a list of errors instead of raising exception? """Performs all implemented validations on a form. Raises exception if something fails.""" errors = [] if not isinstance(form, Form): raise ValueError(f"Validation failed, not a Form:\n{ufl_err_str(form)}") # FIXME: There's a bunch of other checks we should do here. # FIXME: Add back check for multilinearity # Check that form is multilinear # if not is_multilinear(form): # errors.append("Form is not multilinear in arguments.") # FIXME DOMAIN: Add check for consistency between domains somehow domains = set( extract_unique_domain(t) for e in iter_expressions(form) for t in traverse_unique_terminals(e) ) - {None} if not domains: errors.append("Missing domain definition in form.") # Check that cell is the same everywhere cells = set(dom.ufl_cell() for dom in domains) - {None} if not cells: errors.append("Missing cell definition in form.") elif len(cells) > 1: errors.append(f"Multiple cell definitions in form: {cells}") # Check that no Coefficient or Argument instance have the same # count unless they are the same coefficients = {} arguments = {} for e in iter_expressions(form): for f in traverse_unique_terminals(e): if isinstance(f, Coefficient): c = f.count() if c in coefficients: g = coefficients[c] if f is not g: errors.append(f"Found different Coefficients with same count: {f} and {g}.") else: coefficients[c] = f elif isinstance(f, Argument): n = f.number() p = f.part() if (n, p) in arguments: g = arguments[(n, p)] if f is not g: if n == 0: msg = "TestFunctions" elif n == 1: msg = "TrialFunctions" else: msg = "Arguments with same number and part" msg = f"Found different {msg}: {f!r} and {g!r}." errors.append(msg) else: arguments[(n, p)] = f # Check that all integrands are scalar for expression in iter_expressions(form): if not is_true_ufl_scalar(expression): errors.append("Found non-scalar integrand expression: {ufl_err_str(expression)}\n") # Check that restrictions are permissible for integral in form.integrals(): # Only allow restrictions on interior facet integrals and # surface measures if integral.integral_type().startswith("interior_facet"): check_restrictions(integral.integrand(), True) else: check_restrictions(integral.integrand(), False) # Raise exception with all error messages # TODO: Return errors list instead, need to collect messages from # all validations above first. if errors: raise ValueError("Found errors in validation of form:\n" + "\n\n".join(errors)) ufl-2024.2.0/ufl/algorithms/comparison_checker.py000066400000000000000000000106621470142567200217130ustar00rootroot00000000000000"""Algorithm to check for 'comparison' nodes in a form when the user is in 'complex mode'.""" from ufl.algebra import Real from ufl.algorithms.map_integrands import map_integrand_dags from ufl.argument import Argument from ufl.constantvalue import RealValue, Zero from ufl.corealg.multifunction import MultiFunction from ufl.geometry import GeometricQuantity class CheckComparisons(MultiFunction): """Raises an error if comparisons are done with complex quantities. If quantities are real, adds the Real operator to the compared quantities. Terminals that are real are RealValue, Zero, and Argument (even in complex FEM, the basis functions are real) Operations that produce reals are Abs, Real, Imag. Terminals default to complex, and Sqrt, Pow (defensively) imply complex. Otherwise, operators preserve the type of their operands. """ def __init__(self): """Initialise.""" MultiFunction.__init__(self) self.nodetype = {} def expr(self, o, *ops): """Defaults expressions to complex unless they only act on real quantities. Overridden for specific operators. Rebuilds objects if necessary. """ types = {self.nodetype[op] for op in ops} if types: t = "complex" if "complex" in types else "real" else: t = "complex" o = self.reuse_if_untouched(o, *ops) self.nodetype[o] = t return o def compare(self, o, *ops): """Compare.""" types = {self.nodetype[op] for op in ops} if "complex" in types: raise ComplexComparisonError("Ordering undefined for complex values.") else: o = o._ufl_expr_reconstruct_(*map(Real, ops)) self.nodetype[o] = "bool" return o gt = compare lt = compare ge = compare le = compare sign = compare def max_value(self, o, *ops): """Apply to max_value.""" types = {self.nodetype[op] for op in ops} if "complex" in types: raise ComplexComparisonError("You can't compare complex numbers with max.") else: o = o._ufl_expr_reconstruct_(*map(Real, ops)) self.nodetype[o] = "bool" return o def min_value(self, o, *ops): """Apply to min_value.""" types = {self.nodetype[op] for op in ops} if "complex" in types: raise ComplexComparisonError("You can't compare complex numbers with min.") else: o = o._ufl_expr_reconstruct_(*map(Real, ops)) self.nodetype[o] = "bool" return o def real(self, o, *ops): """Apply to real.""" o = self.reuse_if_untouched(o, *ops) self.nodetype[o] = "real" return o def imag(self, o, *ops): """Apply to imag.""" o = self.reuse_if_untouched(o, *ops) self.nodetype[o] = "real" return o def sqrt(self, o, *ops): """Apply to sqrt.""" o = self.reuse_if_untouched(o, *ops) self.nodetype[o] = "complex" return o def power(self, o, base, exponent): """Apply to power.""" o = self.reuse_if_untouched(o, base, exponent) try: # Attempt to diagnose circumstances in which the result must be real. exponent = float(exponent) if self.nodetype[base] == "real" and int(exponent) == exponent: self.nodetype[o] = "real" return o except TypeError: pass self.nodetype[o] = "complex" return o def abs(self, o, *ops): """Apply to abs.""" o = self.reuse_if_untouched(o, *ops) self.nodetype[o] = "real" return o def terminal(self, term, *ops): """Apply to terminal.""" # default terminals to complex, except the ones we *know* are real if isinstance(term, (RealValue, Zero, Argument, GeometricQuantity)): self.nodetype[term] = "real" else: self.nodetype[term] = "complex" return term def indexed(self, o, expr, multiindex): """Apply to indexed.""" o = self.reuse_if_untouched(o, expr, multiindex) self.nodetype[o] = self.nodetype[expr] return o def do_comparison_check(form): """Raises an error if invalid comparison nodes exist.""" return map_integrand_dags(CheckComparisons(), form) class ComplexComparisonError(BaseException): """Complex compariseon exception.""" ufl-2024.2.0/ufl/algorithms/compute_form_data.py000066400000000000000000000414751470142567200215530ustar00rootroot00000000000000"""This module provides the compute_form_data function. Form compilers will typically call compute_form_dataprior to code generation to preprocess/simplify a raw input form given by a user. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from itertools import chain from ufl.algorithms.analysis import extract_coefficients, extract_sub_elements, unique_tuple from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering from ufl.algorithms.apply_derivatives import apply_coordinate_derivatives, apply_derivatives # These are the main symbolic processing steps: from ufl.algorithms.apply_function_pullbacks import apply_function_pullbacks from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering from ufl.algorithms.apply_integral_scaling import apply_integral_scaling from ufl.algorithms.apply_restrictions import apply_default_restrictions, apply_restrictions from ufl.algorithms.check_arities import check_form_arity from ufl.algorithms.comparison_checker import do_comparison_check # See TODOs at the call sites of these below: from ufl.algorithms.domain_analysis import ( build_integral_data, group_form_integrals, reconstruct_form_from_integral_data, ) from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree from ufl.algorithms.formdata import FormData from ufl.algorithms.formtransformations import compute_form_arities from ufl.algorithms.remove_complex_nodes import remove_complex_nodes from ufl.classes import Coefficient, Form, FunctionSpace, GeometricFacetQuantity from ufl.corealg.traversal import traverse_unique_terminals from ufl.domain import extract_unique_domain from ufl.utils.sequences import max_degree def _auto_select_degree(elements): """Automatically select degree for all elements of the form. This is be used in cases where the degree has not been specified by the user. This feature is used by DOLFIN to allow the specification of Expressions with undefined degrees. """ # Use max degree of all elements, at least 1 (to work with # Lagrange elements) return max_degree({e.embedded_superdegree for e in elements} - {None} | {1}) def _compute_element_mapping(form): """Compute element mapping for element replacement.""" # The element mapping is a slightly messy concept with two use # cases: # - Expression with missing cell or element TODO: Implement proper # Expression handling in UFL and get rid of this # - Constant with missing cell TODO: Fix anything that needs to be # worked around to drop this requirement # Extract all elements and include subelements of mixed elements elements = [obj.ufl_element() for obj in chain(form.arguments(), form.coefficients())] elements = extract_sub_elements(elements) # Try to find a common degree for elements common_degree = _auto_select_degree(elements) # Compute element map element_mapping = {} for element in elements: # Flag for whether element needs to be reconstructed reconstruct = False # Set cell cell = element.cell if cell is None: domains = form.ufl_domains() if not all(domains[0].ufl_cell() == d.ufl_cell() for d in domains): raise ValueError( "Cannot replace unknown element cell without unique common cell in form." ) cell = domains[0].ufl_cell() reconstruct = True # Set degree degree = element.embedded_superdegree if degree is None: degree = common_degree reconstruct = True # Reconstruct element and add to map if reconstruct: element_mapping[element] = element.reconstruct(cell=cell, degree=degree) else: element_mapping[element] = element return element_mapping def _compute_max_subdomain_ids(integral_data): """Compute the maximum subdomain ids.""" max_subdomain_ids = {} for itg_data in integral_data: it = itg_data.integral_type for integral in itg_data.integrals: # Convert string for default integral to -1 sids = (-1 if isinstance(si, str) else si for si in integral.subdomain_id()) newmax = max(sids) + 1 prevmax = max_subdomain_ids.get(it, 0) max_subdomain_ids[it] = max(prevmax, newmax) return max_subdomain_ids def _compute_form_data_elements(self, arguments, coefficients, domains): """Compute form data elements.""" self.argument_elements = tuple(f.ufl_element() for f in arguments) self.coefficient_elements = tuple(f.ufl_element() for f in coefficients) self.coordinate_elements = tuple(domain.ufl_coordinate_element() for domain in domains) # TODO: Include coordinate elements from argument and coefficient # domains as well? Can they differ? # Note: Removed self.elements and self.sub_elements to make sure # code that depends on the selection of argument + # coefficient elements blow up, as opposed to silently # almost working, with the introduction of the coordinate # elements here. all_elements = self.argument_elements + self.coefficient_elements + self.coordinate_elements all_sub_elements = extract_sub_elements(all_elements) self.unique_elements = unique_tuple(all_elements) self.unique_sub_elements = unique_tuple(all_sub_elements) def _check_elements(form_data): """Check elements.""" for element in chain(form_data.unique_elements, form_data.unique_sub_elements): if element.cell is None: raise ValueError(f"Found element with undefined cell: {element}") def _check_facet_geometry(integral_data): """Check facet geometry.""" for itg_data in integral_data: for itg in itg_data.integrals: it = itg_data.integral_type # Facet geometry is only valid in facet integrals. # Allowing custom integrals to pass as well, although # that's not really strict enough. if not ("facet" in it or "custom" in it or "interface" in it): # Not a facet integral for expr in traverse_unique_terminals(itg.integrand()): cls = expr._ufl_class_ if issubclass(cls, GeometricFacetQuantity): raise ValueError(f"Integral of type {it} cannot contain a {cls.__name__}.") def _check_form_arity(preprocessed_form): """Check that we don't have a mixed linear/bilinear form or anything like that.""" # FIXME: This is slooow and should be moved to form compiler # and/or replaced with something faster if 1 != len(compute_form_arities(preprocessed_form)): raise ValueError("All terms in form must have same rank.") def _build_coefficient_replace_map(coefficients, element_mapping=None): """Create new Coefficient objects with count starting at 0. Returns: mapping from old to new objects, and lists of the new objects """ if element_mapping is None: element_mapping = {} new_coefficients = [] replace_map = {} for i, f in enumerate(coefficients): old_e = f.ufl_element() new_e = element_mapping.get(old_e, old_e) # XXX: This is a hack to ensure that if the original # coefficient had a domain, the new one does too. # This should be overhauled with requirement that Expressions # always have a domain. domain = extract_unique_domain(f) if domain is not None: new_e = FunctionSpace(domain, new_e) new_f = Coefficient(new_e, count=i) new_coefficients.append(new_f) replace_map[f] = new_f return new_coefficients, replace_map def attach_estimated_degrees(form): """Attach estimated polynomial degree to a form's integrals. Args: form: The Form` to inspect. Returns: A new Form with estimate degrees attached. """ integrals = form.integrals() new_integrals = [] for integral in integrals: md = {} md.update(integral.metadata()) degree = estimate_total_polynomial_degree(integral.integrand()) md["estimated_polynomial_degree"] = degree new_integrals.append(integral.reconstruct(metadata=md)) return Form(new_integrals) def preprocess_form(form, complex_mode): """Preprocess a form.""" # Note: Default behaviour here will process form the way that is # currently expected by vanilla FFC # Check that the form does not try to compare complex quantities: # if the quantites being compared are 'provably' real, wrap them # with Real, otherwise throw an error. if complex_mode: form = do_comparison_check(form) # Lower abstractions for tensor-algebra types into index notation, # reducing the number of operators later algorithms and form # compilers need to handle form = apply_algebra_lowering(form) # After lowering to index notation, remove any complex nodes that # have been introduced but are not wanted when working in real mode, # allowing for purely real forms to be written if not complex_mode: form = remove_complex_nodes(form) # Apply differentiation before function pullbacks, because for # example coefficient derivatives are more complicated to derive # after coefficients are rewritten, and in particular for # user-defined coefficient relations it just gets too messy form = apply_derivatives(form) return form def compute_form_data( form, do_apply_function_pullbacks=False, do_apply_integral_scaling=False, do_apply_geometry_lowering=False, preserve_geometry_types=(), do_apply_default_restrictions=True, do_apply_restrictions=True, do_estimate_degrees=True, do_append_everywhere_integrals=True, complex_mode=False, ): """Compute form data. The default arguments configured to behave the way old FFC expects. """ # TODO: Move this to the constructor instead self = FormData() # --- Store untouched form for reference. # The user of FormData may get original arguments, # original coefficients, and form signature from this object. # But be aware that the set of original coefficients are not # the same as the ones used in the final UFC form. # See 'reduced_coefficients' below. self.original_form = form # --- Pass form integrands through some symbolic manipulation form = preprocess_form(form, complex_mode) # --- Group form integrals # TODO: Refactor this, it's rather opaque what this does # TODO: Is self.original_form.ufl_domains() right here? # It will matter when we start including 'num_domains' in ufc form. form = group_form_integrals( form, self.original_form.ufl_domains(), do_append_everywhere_integrals=do_append_everywhere_integrals, ) # Estimate polynomial degree of integrands now, before applying # any pullbacks and geometric lowering. Otherwise quad degrees # blow up horrifically. if do_estimate_degrees: form = attach_estimated_degrees(form) if do_apply_function_pullbacks: # Rewrite coefficients and arguments in terms of their # reference cell values with Piola transforms and symmetry # transforms injected where needed. # Decision: Not supporting grad(dolfin.Expression) without a # Domain. Current dolfin works if Expression has a # cell but this should be changed to a mesh. form = apply_function_pullbacks(form) # Scale integrals to reference cell frames if do_apply_integral_scaling: form = apply_integral_scaling(form) # Apply default restriction to fully continuous terminals if do_apply_default_restrictions: form = apply_default_restrictions(form) # Lower abstractions for geometric quantities into a smaller set # of quantities, allowing the form compiler to deal with a smaller # set of types and treating geometric quantities like any other # expressions w.r.t. loop-invariant code motion etc. if do_apply_geometry_lowering: form = apply_geometry_lowering(form, preserve_geometry_types) # Apply differentiation again, because the algorithms above can # generate new derivatives or rewrite expressions inside # derivatives if do_apply_function_pullbacks or do_apply_geometry_lowering: form = apply_derivatives(form) # Neverending story: apply_derivatives introduces new Jinvs, # which needs more geometry lowering if do_apply_geometry_lowering: form = apply_geometry_lowering(form, preserve_geometry_types) # Lower derivatives that may have appeared form = apply_derivatives(form) form = apply_coordinate_derivatives(form) # Propagate restrictions to terminals if do_apply_restrictions: form = apply_restrictions(form) # If in real mode, remove any complex nodes introduced during form processing. if not complex_mode: form = remove_complex_nodes(form) # --- Group integrals into IntegralData objects # Most of the heavy lifting is done above in group_form_integrals. self.integral_data = build_integral_data(form.integrals()) # --- Create replacements for arguments and coefficients # Figure out which form coefficients each integral should enable for itg_data in self.integral_data: itg_coeffs = set() # Get all coefficients in integrand for itg in itg_data.integrals: itg_coeffs.update(extract_coefficients(itg.integrand())) # Store with IntegralData object itg_data.integral_coefficients = itg_coeffs # Figure out which coefficients from the original form are # actually used in any integral (Differentiation may reduce the # set of coefficients w.r.t. the original form) reduced_coefficients_set = set() for itg_data in self.integral_data: reduced_coefficients_set.update(itg_data.integral_coefficients) self.reduced_coefficients = sorted(reduced_coefficients_set, key=lambda c: c.count()) self.num_coefficients = len(self.reduced_coefficients) self.original_coefficient_positions = [ i for i, c in enumerate(self.original_form.coefficients()) if c in self.reduced_coefficients ] # Store back into integral data which form coefficients are used # by each integral for itg_data in self.integral_data: itg_data.enabled_coefficients = [ bool(coeff in itg_data.integral_coefficients) for coeff in self.reduced_coefficients ] # --- Collect some trivial data # Get rank of form from argument list (assuming not a mixed arity form) self.rank = len(self.original_form.arguments()) # Extract common geometric dimension (topological is not common!) self.geometric_dimension = self.original_form.integrals()[0].ufl_domain().geometric_dimension() # --- Build mapping from old incomplete element objects to new # well defined elements. This is to support the Expression # construct in dolfin which subclasses Coefficient but doesn't # provide an element, and the Constant construct that doesn't # provide the domain that a Coefficient is supposed to have. A # future design iteration in UFL/UFC/FFC/DOLFIN may allow removal # of this mapping with the introduction of UFL types for # Expression-like functions that can be evaluated in quadrature # points. self.element_replace_map = _compute_element_mapping(self.original_form) # Mappings from elements and coefficients that reside in form to # objects with canonical numbering as well as completed cells and # elements renumbered_coefficients, function_replace_map = _build_coefficient_replace_map( self.reduced_coefficients, self.element_replace_map ) self.function_replace_map = function_replace_map # --- Store various lists of elements and sub elements (adds # members to self) _compute_form_data_elements( self, self.original_form.arguments(), renumbered_coefficients, self.original_form.ufl_domains(), ) # --- Store number of domains for integral types # TODO: Group this by domain first. For now keep a backwards # compatible data structure. self.max_subdomain_ids = _compute_max_subdomain_ids(self.integral_data) # --- Checks _check_elements(self) _check_facet_geometry(self.integral_data) # TODO: This is a very expensive check... Replace with something # faster! preprocessed_form = reconstruct_form_from_integral_data(self.integral_data) # TODO: Test how fast this is check_form_arity(preprocessed_form, self.original_form.arguments(), complex_mode) # TODO: This member is used by unit tests, change the tests to # remove this! self.preprocessed_form = preprocessed_form return self ufl-2024.2.0/ufl/algorithms/coordinate_derivative_helpers.py000066400000000000000000000063131470142567200241460ustar00rootroot00000000000000"""Tools to strip away and reattach coordinate derivatives. This is used in compute_form_data. """ # Copyright (C) 2018 Florian Wechsung # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.classes import Integral from ufl.corealg.map_dag import map_expr_dags from ufl.corealg.multifunction import MultiFunction from ufl.differentiation import CoordinateDerivative class CoordinateDerivativeIsOutermostChecker(MultiFunction): """Traverses the tree to make sure that CoordinateDerivatives are only on the outside. The visitor returns False as long as no CoordinateDerivative has been seen. """ def multi_index(self, o): """Apply to multi_index.""" return False def terminal(self, o): """Apply to terminal.""" return False def expr(self, o, *operands): """Apply to expr. If we have already seen a CoordinateDerivative, then no other expressions apart from more CoordinateDerivatives are allowed to wrap around it. """ if any(operands): raise ValueError("CoordinateDerivative(s) must be outermost") return False def coordinate_derivative(self, o, expr, *_): """Apply to coordinate derivative.""" return True def strip_coordinate_derivatives(integrals): """Strip coordinate derivatives.""" if isinstance(integrals, list): if len(integrals) == 0: return integrals, None stripped_integrals_and_cds = [] for integral in integrals: (si, cd) = strip_coordinate_derivatives(integral) stripped_integrals_and_cds.append((si, cd)) return stripped_integrals_and_cds elif isinstance(integrals, Integral): integral = integrals integrand = integral.integrand() checker = CoordinateDerivativeIsOutermostChecker() map_expr_dags(checker, [integrand]) coordinate_derivatives = [] def take_top_coordinate_derivatives(o): """Get all coordinate derivatives and store them. So we can apply them later again. """ o_ = o.ufl_operands if isinstance(o, CoordinateDerivative): coordinate_derivatives.append((o_[1], o_[2], o_[3])) return take_top_coordinate_derivatives(o_[0]) else: return o newintegrand = take_top_coordinate_derivatives(integrand) return (integral.reconstruct(integrand=newintegrand), coordinate_derivatives) else: raise ValueError(f"Invalid type {integrals.__class__.__name__}") def attach_coordinate_derivatives(integral, coordinate_derivatives): """Attach coordinate derivatives.""" if coordinate_derivatives is None: return integral if isinstance(integral, Integral): integrand = integral.integrand() # apply the stored coordinate derivatives back onto the integrand for tup in reversed(coordinate_derivatives): integrand = CoordinateDerivative(integrand, tup[0], tup[1], tup[2]) return integral.reconstruct(integrand=integrand) else: raise ValueError(f"Invalid type {integral.__class__.__name__}") ufl-2024.2.0/ufl/algorithms/domain_analysis.py000066400000000000000000000355301470142567200212300ustar00rootroot00000000000000"""Algorithms for building canonical data structure for integrals over subdomains.""" # Copyright (C) 2009-2016 Anders Logg and Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import numbers import typing from collections import defaultdict import ufl from ufl.algorithms.coordinate_derivative_helpers import ( attach_coordinate_derivatives, strip_coordinate_derivatives, ) from ufl.algorithms.renumbering import renumber_indices from ufl.form import Form from ufl.integral import Integral from ufl.protocols import id_or_none from ufl.sorting import cmp_expr, sorted_expr from ufl.utils.sorting import canonicalize_metadata, sorted_by_key class IntegralData(object): """Utility class. This class has members (domain, integral_type, subdomain_id, integrals, metadata), where metadata is an empty dictionary that may be used for associating metadata with each object. """ __slots__ = ( "domain", "integral_type", "subdomain_id", "integrals", "metadata", "integral_coefficients", "enabled_coefficients", ) def __init__(self, domain, integral_type, subdomain_id, integrals, metadata): """Initialise.""" if 1 != len(set(itg.ufl_domain() for itg in integrals)): raise ValueError("Multiple domains mismatch in integral data.") if not all(integral_type == itg.integral_type() for itg in integrals): raise ValueError("Integral type mismatch in integral data.") if not all(subdomain_id == itg.subdomain_id() for itg in integrals): raise ValueError("Subdomain id mismatch in integral data.") self.domain = domain self.integral_type = integral_type self.subdomain_id = subdomain_id self.integrals = integrals # This is populated in preprocess using data not available at # this stage: self.integral_coefficients = None self.enabled_coefficients = None # TODO: I think we can get rid of this with some refactoring # in ffc: self.metadata = metadata def __lt__(self, other): """Check if self is less than other.""" # To preserve behaviour of extract_integral_data: return (self.integral_type, self.subdomain_id, self.integrals, self.metadata) < ( other.integral_type, other.subdomain_id, other.integrals, other.metadata, ) def __eq__(self, other): """Check for equality.""" # Currently only used for tests: return ( self.integral_type == other.integral_type and self.subdomain_id == other.subdomain_id and self.integrals == other.integrals and self.metadata == other.metadata ) def __str__(self): """Format as a string.""" s = f"IntegralData over domain({self.integral_type}, {self.subdomain_id})" s += " with integrals:\n" s += "\n\n".join(map(str, self.integrals)) s += "\nand metadata:\n{metadata}" return s class ExprTupleKey(object): """Tuple comparison helper.""" __slots__ = ("x",) def __init__(self, x): """Initialise.""" self.x = x def __lt__(self, other): """Check if self is less than other.""" # Comparing expression first c = cmp_expr(self.x[0], other.x[0]) if c < 0: return True elif c > 0: return False else: # Comparing form compiler data mds = canonicalize_metadata(self.x[1]) mdo = canonicalize_metadata(other.x[1]) return mds < mdo def group_integrals_by_domain_and_type(integrals, domains): """Group integrals by domain and type. Args: integrals: list of Integral objects domains: list of AbstractDomain objects from the parent Form Returns: Dictionary mapping (domain, integral_type) to list(Integral) """ integrals_by_domain_and_type = defaultdict(list) for itg in integrals: if itg.ufl_domain() is None: raise ValueError("Integral has no domain.") key = (itg.ufl_domain(), itg.integral_type()) # Append integral to list of integrals with shared key integrals_by_domain_and_type[key].append(itg) return integrals_by_domain_and_type def integral_subdomain_ids(integral): """Get a tuple of integer subdomains or a valid string subdomain from integral.""" did = integral.subdomain_id() if isinstance(did, numbers.Integral): return (did,) elif isinstance(did, tuple): if not all(isinstance(d, numbers.Integral) for d in did): raise ValueError("Expecting only integer subdomains in tuple.") return did elif did in ("everywhere", "otherwise"): # TODO: Define list of valid strings somewhere more central return did else: raise ValueError(f"Invalid domain id {did}.") def rearrange_integrals_by_single_subdomains( integrals: typing.List[Integral], do_append_everywhere_integrals: bool ) -> typing.Dict[int, typing.List[Integral]]: """Rearrange integrals over multiple subdomains to single subdomain integrals. Args: integrals: List of integrals do_append_everywhere_integrals: Boolean indicating if integrals defined on the whole domain should just be restricted to the set of input subdomain ids. Returns: The integrals reconstructed with single subdomain_id """ # Split integrals into lists of everywhere and subdomain integrals everywhere_integrals = [] subdomain_integrals = [] for itg in integrals: dids = integral_subdomain_ids(itg) if dids == "otherwise": raise ValueError("'otherwise' integrals should never occur before preprocessing.") elif dids == "everywhere": everywhere_integrals.append(itg) else: subdomain_integrals.append((dids, itg)) # Fill single_subdomain_integrals with lists of integrals from # subdomain_integrals, but split and restricted to single # subdomain ids single_subdomain_integrals = defaultdict(list) for dids, itg in subdomain_integrals: # Region or single subdomain id for did in dids: # Restrict integral to this subdomain! single_subdomain_integrals[did].append(itg.reconstruct(subdomain_id=did)) # Add everywhere integrals to each single subdomain id integral # list otherwise_integrals = [] for ev_itg in everywhere_integrals: # Restrict everywhere integral to 'otherwise' otherwise_integrals.append(ev_itg.reconstruct(subdomain_id="otherwise")) # Restrict everywhere integral to each subdomain # and append to each integral list if do_append_everywhere_integrals: for subdomain_id in sorted(single_subdomain_integrals.keys()): single_subdomain_integrals[subdomain_id].append( ev_itg.reconstruct(subdomain_id=subdomain_id) ) if otherwise_integrals: single_subdomain_integrals["otherwise"] = otherwise_integrals return single_subdomain_integrals def accumulate_integrands_with_same_metadata(integrals): """Accumulate integrands with the same metedata. Args: integrals: a list of integrals Returns: A list of the form [(integrand0, metadata0), (integrand1, metadata1), ...] where integrand0 < integrand1 by the canonical ufl expression ordering criteria. """ # Group integrals by compiler data hash by_cdid = {} for itg in integrals: cd = itg.metadata() cdid = hash(canonicalize_metadata(cd)) if cdid not in by_cdid: by_cdid[cdid] = ([], cd) by_cdid[cdid][0].append(itg) # Accumulate integrands separately for each compiler data object # id for cdid in by_cdid: integrals, cd = by_cdid[cdid] # Ensure canonical sorting of more than two integrands integrands = sorted_expr((itg.integrand() for itg in integrals)) integrands_sum = sum(integrands[1:], integrands[0]) by_cdid[cdid] = (integrands_sum, cd) # Sort integrands canonically by integrand first then compiler # data return sorted(by_cdid.values(), key=ExprTupleKey) def build_integral_data(integrals): """Build integral data given a list of integrals. The integrals you pass in here must have been rearranged and gathered (removing the "everywhere" subdomain_id). To do this, you should call group_form_integrals. Args: integrals: An iterable of Integral objects. Returns: A tuple of IntegralData objects. """ itgs = defaultdict(list) # --- Merge integral data that has the same integrals, for integral in integrals: integral_type = integral.integral_type() ufl_domain = integral.ufl_domain() subdomain_ids = integral.subdomain_id() if "everywhere" in subdomain_ids: raise ValueError( "'everywhere' not a valid subdomain id. " "Did you forget to call group_form_integrals?" ) # Group for integral data (One integral data object for all # integrals with same domain, itype, (but possibly different metadata). itgs[(ufl_domain, integral_type, subdomain_ids)].append(integral) # Build list with canonical ordering, iteration over dicts # is not deterministic across python versions def keyfunc(item): (d, itype, sid), integrals = item sid_int = tuple(-1 if i == "otherwise" else i for i in sid) return (d._ufl_sort_key_(), itype, (type(sid).__name__,), sid_int) integral_datas = [] for (d, itype, sid), integrals in sorted(itgs.items(), key=keyfunc): integral_datas.append(IntegralData(d, itype, sid, integrals, {})) return integral_datas def group_form_integrals(form, domains, do_append_everywhere_integrals=True): """Group integrals by domain and type, performing canonical simplification. Args: form: the Form to group the integrals of. domains: an iterable of Domains. do_append_everywhere_integrals: Boolean indicating if integrals defined on the whole domain should just be restricted to the set of input subdomain ids. Returns: A new Form with gathered integrands. """ # Group integrals by domain and type integrals_by_domain_and_type = group_integrals_by_domain_and_type(form.integrals(), domains) integrals = [] for domain in domains: for integral_type in ufl.measure.integral_types(): # Get integrals with this domain and type ddt_integrals = integrals_by_domain_and_type.get((domain, integral_type)) if ddt_integrals is None: continue # Group integrals by subdomain id, after splitting e.g. # f*dx((1,2)) + g*dx((2,3)) -> f*dx(1) + (f+g)*dx(2) + g*dx(3) # (note: before this call, 'everywhere' is a valid subdomain_id, # and after this call, 'otherwise' is a valid subdomain_id) single_subdomain_integrals = rearrange_integrals_by_single_subdomains( ddt_integrals, do_append_everywhere_integrals ) for subdomain_id, ss_integrals in sorted_by_key(single_subdomain_integrals): # strip the coordinate derivatives from all integrals # this yields a list of the form [(coordinate derivative, integral), ...] stripped_integrals_and_coordderivs = strip_coordinate_derivatives(ss_integrals) # now group the integrals by the coordinate derivative def calc_hash(cd): return sum( sum(tuple_elem._ufl_compute_hash_() for tuple_elem in tuple_) for tuple_ in cd ) coordderiv_integrals_dict = {} for integral, coordderiv in stripped_integrals_and_coordderivs: coordderivhash = calc_hash(coordderiv) if coordderivhash in coordderiv_integrals_dict: coordderiv_integrals_dict[coordderivhash][1].append(integral) else: coordderiv_integrals_dict[coordderivhash] = (coordderiv, [integral]) # cd_integrals_dict is now a dict of the form # { hash: (CoordinateDerivative, [integral, integral, ...]), ... } # we can now put the integrals back together and then afterwards # apply the CoordinateDerivative again for cdhash, samecd_integrals in sorted_by_key(coordderiv_integrals_dict): # Accumulate integrands of integrals that share the # same compiler data integrands_and_cds = accumulate_integrands_with_same_metadata( samecd_integrals[1] ) for integrand, metadata in integrands_and_cds: integral = Integral( integrand, integral_type, domain, subdomain_id, metadata, None ) integral = attach_coordinate_derivatives(integral, samecd_integrals[0]) integrals.append(integral) # Group integrals by common integrand # u.dx(0)*dx(1) + u.dx(0)*dx(2) -> u.dx(0)*dx((1,2)) # to avoid duplicate kernels generated after geometry lowering unique_integrals = defaultdict(tuple) metadata_table = defaultdict(dict) for integral in integrals: integral_type = integral.integral_type() ufl_domain = integral.ufl_domain() metadata = integral.metadata() meta_hash = hash(canonicalize_metadata(metadata)) subdomain_id = integral.subdomain_id() subdomain_data = id_or_none(integral.subdomain_data()) integrand = renumber_indices(integral.integrand()) unique_integrals[(integral_type, ufl_domain, meta_hash, integrand, subdomain_data)] += ( subdomain_id, ) metadata_table[(integral_type, ufl_domain, meta_hash, integrand, subdomain_data)] = metadata grouped_integrals = [] for integral_data, subdomain_ids in unique_integrals.items(): (integral_type, ufl_domain, metadata, integrand, subdomain_data) = integral_data integral = Integral( integrand, integral_type, ufl_domain, subdomain_ids, metadata_table[integral_data], subdomain_data, ) grouped_integrals.append(integral) return Form(grouped_integrals) def reconstruct_form_from_integral_data(integral_data): """Reconstruct a form from integral data.""" integrals = [] for ida in integral_data: integrals.extend(ida.integrals) return Form(integrals) ufl-2024.2.0/ufl/algorithms/estimate_degrees.py000066400000000000000000000274671470142567200214010ustar00rootroot00000000000000"""Algorithms for estimating polynomial degrees of expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010 # Modified by Jan Blechta, 2012 import warnings from ufl.checks import is_cellwise_constant from ufl.constantvalue import IntValue from ufl.corealg.map_dag import map_expr_dags from ufl.corealg.multifunction import MultiFunction from ufl.domain import extract_unique_domain from ufl.form import Form from ufl.integral import Integral class SumDegreeEstimator(MultiFunction): """Sum degree estimator. This algorithm is exact for a few operators and heuristic for many. """ def __init__(self, default_degree, element_replace_map): """Initialise.""" MultiFunction.__init__(self) self.default_degree = default_degree self.element_replace_map = element_replace_map def constant_value(self, v): """Apply to constant_value. Constant values are constant. """ return 0 def constant(self, v): """Apply to constant.""" return 0 def geometric_quantity(self, v): """Apply to geometric_quantity. Some geometric quantities are cellwise constant. Others are nonpolynomial and thus hard to estimate. """ if is_cellwise_constant(v): return 0 else: # As a heuristic, just returning domain degree to bump up degree somewhat return extract_unique_domain(v).ufl_coordinate_element().embedded_superdegree def spatial_coordinate(self, v): """Apply to spatial_coordinate. A coordinate provides additional degrees depending on coordinate field of domain. """ return extract_unique_domain(v).ufl_coordinate_element().embedded_superdegree def cell_coordinate(self, v): """Apply to cell_coordinate. A coordinate provides one additional degree. """ return 1 def argument(self, v): """Apply to argument. A form argument provides a degree depending on the element, or the default degree if the element has no degree. """ return ( v.ufl_element().embedded_superdegree ) # FIXME: Use component to improve accuracy for mixed elements def coefficient(self, v): """Apply to coefficient. A form argument provides a degree depending on the element, or the default degree if the element has no degree. """ e = v.ufl_element() e = self.element_replace_map.get(e, e) d = e.embedded_superdegree # FIXME: Use component to improve accuracy for mixed elements if d is None: d = self.default_degree return d def _reduce_degree(self, v, f): """Reduce the estimated degree by one. This is used when derivatives are taken. It does not reduce the degree when TensorProduct elements or quadrilateral elements are involved. """ if isinstance(f, int) and extract_unique_domain(v).ufl_cell().cellname() not in [ "quadrilateral", "hexahedron", ]: return max(f - 1, 0) else: return f def _add_degrees(self, v, *ops): """Apply to _add_degrees.""" if any(isinstance(o, tuple) for o in ops): # we can add a slight hack here to handle things # like adding 0 to (3, 3) [by expanding # 0 to (0, 0) when making tempops] tempops = [foo if isinstance(foo, tuple) else (foo, foo) for foo in ops] return tuple(map(sum, zip(*tempops))) else: return sum(ops) def _max_degrees(self, v, *ops): """Apply to _max_degrees.""" if any(isinstance(o, tuple) for o in ops): tempops = [foo if isinstance(foo, tuple) else (foo, foo) for foo in ops] return tuple(map(max, zip(*tempops))) else: return max(ops + (0,)) def _not_handled(self, v, *args): """Apply to _not_handled.""" raise ValueError(f"Missing degree handler for type {v._ufl_class_.__name__}") def expr(self, v, *ops): """Apply to expr. For most operators we take the max degree of its operands. """ warnings.warn(f"Missing degree estimation handler for type {v._ufl_class_.__name__}") return self._add_degrees(v, *ops) # Utility types with no degree concept def multi_index(self, v): """Apply to multi_index.""" return None def label(self, v): """Apply to label.""" return None # Fall-through, indexing and similar types def reference_value(self, rv, f): """Apply to reference_value.""" return f def variable(self, v, e, a): """Apply to variable.""" return e def transposed(self, v, A): """Apply to transposed.""" return A def index_sum(self, v, A, ii): """Apply to index_sum.""" return A def indexed(self, v, A, ii): """Apply to indexed.""" return A def component_tensor(self, v, A, ii): """Apply to component_tensor.""" return A list_tensor = _max_degrees def positive_restricted(self, v, a): """Apply to positive_restricted.""" return a def negative_restricted(self, v, a): """Apply to negative_restricted.""" return a def conj(self, v, a): """Apply to conj.""" return a def real(self, v, a): """Apply to real.""" return a def imag(self, v, a): """Apply to imag.""" return a # A sum takes the max degree of its operands: sum = _max_degrees # TODO: Need a new algorithm which considers direction of # derivatives of form arguments A spatial derivative reduces the # degree with one grad = _reduce_degree reference_grad = _reduce_degree # Handling these types although they should not occur... please # apply preprocessing before using this algorithm: nabla_grad = _reduce_degree div = _reduce_degree reference_div = _reduce_degree nabla_div = _reduce_degree curl = _reduce_degree reference_curl = _reduce_degree def cell_avg(self, v, a): """Apply to cell_avg. Cell average of a function is always cellwise constant. """ return 0 def facet_avg(self, v, a): """Apply to facet_avg. Facet average of a function is always cellwise constant. """ return 0 # A product accumulates the degrees of its operands: product = _add_degrees # Handling these types although they should not occur... please # apply preprocessing before using this algorithm: inner = _add_degrees dot = _add_degrees outer = _add_degrees cross = _add_degrees # Explicitly not handling these types, please apply preprocessing # before using this algorithm: derivative = _not_handled # base type compound_derivative = _not_handled # base type compound_tensor_operator = _not_handled # base class variable_derivative = _not_handled trace = _not_handled determinant = _not_handled cofactor = _not_handled inverse = _not_handled deviatoric = _not_handled skew = _not_handled sym = _not_handled def abs(self, v, a): """Apply to abs. This is a heuristic, correct if there is no. """ if a == 0: return a else: return a def division(self, v, *ops): """Apply to division. Using the sum here is a heuristic. Consider e.g. (x+1)/(x-1). """ return self._add_degrees(v, *ops) def power(self, v, a, b): """Apply to power. If b is a positive integer: degree(a**b) == degree(a)*b otherwise use the heuristic: degree(a**b) == degree(a) + 2. """ f, g = v.ufl_operands if isinstance(g, IntValue): gi = g.value() if gi >= 0: if isinstance(a, int): return a * gi else: return tuple(foo * gi for foo in a) # Something to a non-(positive integer) power, e.g. float, # negative integer, Coefficient, etc. return self._add_degrees(v, a, 2) def atan2(self, v, a, b): """Apply to atan2. Using the heuristic: degree(atan2(const,const)) == 0 degree(atan2(a,b)) == max(degree(a),degree(b))+2 which can be wildly inaccurate but at least gives a somewhat high integration degree. """ if a or b: return self._add_degrees(v, self._max_degrees(v, a, b), 2) else: return self._max_degrees(v, a, b) def math_function(self, v, a): """Apply to math_function. Using the heuristic: degree(sin(const)) == 0 degree(sin(a)) == degree(a)+2 which can be wildly inaccurate but at least gives a somewhat high integration degree. """ if a: return self._add_degrees(v, a, 2) else: return a def bessel_function(self, v, nu, x): """Apply to bessel_function. Using the heuristic degree(bessel_*(const)) == 0 degree(bessel_*(x)) == degree(x)+2 which can be wildly inaccurate but at least gives a somewhat high integration degree. """ if x: return self._add_degrees(v, x, 2) else: return x def condition(self, v, *args): """Apply to condition.""" return None def conditional(self, v, c, t, f): """Apply to conditional. Degree of condition does not influence degree of values which conditional takes. So heuristicaly taking max of true degree and false degree. This will be exact in cells where condition takes single value. For improving accuracy of quadrature near condition transition surface quadrature order must be adjusted manually. """ return self._max_degrees(v, t, f) def min_value(self, v, a, r): """Apply to min_value. Same as conditional. """ return self._max_degrees(v, a, r) max_value = min_value def coordinate_derivative(self, v, integrand_degree, b, direction_degree, d): """Apply to coordinate_derivative. We use the heuristic that a shape derivative in direction V introduces terms V and grad(V) into the integrand. Hence we add the degree of the deformation to the estimate. """ return self._add_degrees(v, integrand_degree, direction_degree) def expr_list(self, v, *o): """Apply to expr_list.""" return self._max_degrees(v, *o) def expr_mapping(self, v, *o): """Apply to expr_mapping.""" return self._max_degrees(v, *o) def estimate_total_polynomial_degree(e, default_degree=1, element_replace_map={}): """Estimate total polynomial degree of integrand. NB: Although some compound types are supported here, some derivatives and compounds must be preprocessed prior to degree estimation. In generic code, this algorithm should only be applied after preprocessing. For coefficients defined on an element with unspecified degree (None), the degree is set to the given default degree. """ de = SumDegreeEstimator(default_degree, element_replace_map) if isinstance(e, Form): if not e.integrals(): raise ValueError("Form has no integrals.") degrees = map_expr_dags(de, [it.integrand() for it in e.integrals()]) elif isinstance(e, Integral): degrees = map_expr_dags(de, [e.integrand()]) else: degrees = map_expr_dags(de, [e]) degree = max(degrees) if degrees else default_degree return degree ufl-2024.2.0/ufl/algorithms/expand_indices.py000066400000000000000000000165431470142567200210360ustar00rootroot00000000000000"""This module defines expression transformation utilities. These utilities are for expanding free indices in expressions to explicit fixed indices only. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009. from ufl.algorithms.transformer import ReuseTransformer, apply_transformer from ufl.classes import Terminal from ufl.constantvalue import Zero from ufl.core.multiindex import FixedIndex, Index, MultiIndex from ufl.differentiation import Grad from ufl.utils.stacks import Stack, StackDict class IndexExpander(ReuseTransformer): """Index expander.""" def __init__(self): """Initialise.""" ReuseTransformer.__init__(self) self._components = Stack() self._index2value = StackDict() def component(self): """Return current component tuple.""" if self._components: return self._components.peek() return () def terminal(self, x): """Apply to terminal.""" if x.ufl_shape: c = self.component() if len(x.ufl_shape) != len(c): raise ValueError("Component size mismatch.") return x[c] return x def form_argument(self, x): """Apply to form_argument.""" sh = x.ufl_shape if sh == (): return x else: space = x.ufl_function_space() r = len(sh) # Get component c = self.component() if r != len(c): raise ValueError("Component size mismatch.") # Map it through an eventual symmetry mapping if len(space.components) > 1: c = min(i for i, j in space.components.items() if j == space.components[c]) if r != len(c): raise ValueError("Component size mismatch after symmetry mapping.") return x[c] def zero(self, x): """Apply to zero.""" if len(x.ufl_shape) != len(self.component()): raise ValueError("Component size mismatch.") s = set(x.ufl_free_indices) - set(i.count() for i in self._index2value.keys()) if s: raise ValueError(f"Free index set mismatch, these indices have no value assigned: {s}.") # There is no index/shape info in this zero because that is asserted above return Zero() def scalar_value(self, x): """Apply to scalar_value.""" if len(x.ufl_shape) != len(self.component()): self.print_visit_stack() if len(x.ufl_shape) != len(self.component()): raise ValueError("Component size mismatch.") s = set(x.ufl_free_indices) - set(i.count() for i in self._index2value.keys()) if s: raise ValueError(f"Free index set mismatch, these indices have no value assigned: {s}.") return x._ufl_class_(x.value()) def conditional(self, x): """Apply to conditional.""" c, t, f = x.ufl_operands # Not accepting nonscalars in condition if c.ufl_shape != (): raise ValueError("Not expecting tensor in condition.") # Conditional may be indexed, push empty component self._components.push(()) c = self.visit(c) self._components.pop() # Keep possibly non-scalar components for values t = self.visit(t) f = self.visit(f) return self.reuse_if_possible(x, c, t, f) def division(self, x): """Apply to division.""" a, b = x.ufl_operands # Not accepting nonscalars in division anymore if a.ufl_shape != (): raise ValueError("Not expecting tensor in division.") if self.component() != (): raise ValueError("Not expecting component in division.") if b.ufl_shape != (): raise ValueError("Not expecting division by tensor.") a = self.visit(a) # self._components.push(()) b = self.visit(b) # self._components.pop() return self.reuse_if_possible(x, a, b) def index_sum(self, x): """Apply to index_sum.""" ops = [] summand, multiindex = x.ufl_operands (index,) = multiindex # TODO: For the list tensor purging algorithm, do something like: # if index not in self._to_expand: # return self.expr(x, *[self.visit(o) for o in x.ufl_operands]) for value in range(x.dimension()): self._index2value.push(index, value) ops.append(self.visit(summand)) self._index2value.pop() return sum(ops) def _multi_index_values(self, x): """Apply to _multi_index_values.""" comp = [] for i in x._indices: if isinstance(i, FixedIndex): comp.append(i._value) elif isinstance(i, Index): comp.append(self._index2value[i]) return tuple(comp) def multi_index(self, x): """Apply to multi_index.""" comp = self._multi_index_values(x) return MultiIndex(tuple(FixedIndex(i) for i in comp)) def indexed(self, x): """Apply to indexed.""" A, ii = x.ufl_operands # Push new component built from index value map self._components.push(self._multi_index_values(ii)) # Hide index values (doing this is not correct behaviour) # for i in ii: # if isinstance(i, Index): # self._index2value.push(i, None) result = self.visit(A) # Un-hide index values # for i in ii: # if isinstance(i, Index): # self._index2value.pop() # Reset component self._components.pop() return result def component_tensor(self, x): """Apply to component_tensor.""" # This function evaluates the tensor expression # with indices equal to the current component tuple expression, indices = x.ufl_operands if expression.ufl_shape != (): raise ValueError("Expecting scalar base expression.") # Update index map with component tuple values comp = self.component() if len(indices) != len(comp): raise ValueError("Index/component mismatch.") for i, v in zip(indices.indices(), comp): self._index2value.push(i, v) self._components.push(()) # Evaluate with these indices result = self.visit(expression) # Revert index map for _ in comp: self._index2value.pop() self._components.pop() return result def list_tensor(self, x): """Apply to list_tensor.""" # Pick the right subtensor and subcomponent c = self.component() c0, c1 = c[0], c[1:] op = x.ufl_operands[c0] # Evaluate subtensor with this subcomponent self._components.push(c1) r = self.visit(op) self._components.pop() return r def grad(self, x): """Apply to grad.""" (f,) = x.ufl_operands if not isinstance(f, (Terminal, Grad)): raise ValueError("Expecting expand_derivatives to have been applied.") # No need to visit child as long as it is on the form [Grad]([Grad](terminal)) return x[self.component()] def expand_indices(e): """Expand indices.""" return apply_transformer(e, IndexExpander()) ufl-2024.2.0/ufl/algorithms/formdata.py000066400000000000000000000025701470142567200176510ustar00rootroot00000000000000"""FormData class easy for collecting of various data about a form.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008. from ufl.utils.formatting import estr, lstr, tstr class FormData(object): """Class collecting various information extracted from a Form by calling preprocess.""" def __init__(self): """Create empty form data for given form.""" def __str__(self): """Return formatted summary of form data.""" types = sorted(self.max_subdomain_ids.keys()) geometry = (("Geometric dimension", self.geometric_dimension),) subdomains = tuple( (f"Number of {integral_type} subdomains", self.max_subdomain_ids[integral_type]) for integral_type in types ) functions = ( # Arguments ("Rank", self.rank), ("Arguments", lstr(self.original_form.arguments())), # Coefficients ("Number of coefficients", self.num_coefficients), ("Coefficients", lstr(self.reduced_coefficients)), # Elements ("Unique elements", estr(self.unique_elements)), ("Unique sub elements", estr(self.unique_sub_elements)), ) return tstr(geometry + subdomains + functions) ufl-2024.2.0/ufl/algorithms/formfiles.py000066400000000000000000000151521470142567200200420ustar00rootroot00000000000000"""A collection of utility algorithms for handling UFL files.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # Modified by Marie E. Rognes, 2011. import io import os import re from ufl.argument import Argument from ufl.coefficient import Coefficient from ufl.constant import Constant from ufl.core.expr import Expr from ufl.finiteelement import AbstractFiniteElement from ufl.form import Form from ufl.utils.sorting import sorted_by_key class FileData(object): """File data.""" def __init__(self): """Initialise.""" self.elements = [] self.coefficients = [] self.expressions = [] self.forms = [] self.object_names = {} self.object_by_name = {} self.reserved_objects = {} def __bool__(self): """Convert to a bool.""" return bool( self.elements or self.coefficients or self.forms or self.expressions or self.object_names or self.object_by_name or self.reserved_objects ) __nonzero__ = __bool__ def read_lines_decoded(fn): """Read decoded lines of a UFL file.""" r = re.compile(b".*coding: *([^ ]+)") def match(line): """Match.""" return r.match(line, re.ASCII) # First read lines as bytes with io.open(fn, "rb") as f: lines = f.readlines() # Check for coding: in the first two lines for i in range(min(2, len(lines))): m = match(lines[i]) if m: (encoding,) = m.groups() # Drop encoding line lines = lines[:i] + lines[i + 1 :] break else: # Default to utf-8 (works for ascii files # as well, default for python files in py3) encoding = "utf-8" # Decode all lines lines = [line.decode(encoding=encoding) for line in lines] return lines def read_ufl_file(filename): """Read a UFL file.""" if not os.path.exists(filename): raise ValueError(f"File '{filename}' doesn't exist.") lines = read_lines_decoded(filename) code = "".join(lines) return code def execute_ufl_code(uflcode): """Execute code.""" namespace = {} exec(uflcode, namespace) return namespace def interpret_ufl_namespace(namespace): """Take a namespace dict from an executed ufl file and convert it to a FileData object.""" # Object to hold all returned data ufd = FileData() # Extract object names for Form, Coefficient and AbstractFiniteElement objects # The use of id(obj) as key in object_names is necessary # because we need to distinguish between instances, # and not just between objects with different values. for name, value in sorted_by_key(namespace): # Store objects by reserved name OR instance id reserved_names = ("unknown",) # Currently only one reserved name if name in reserved_names: # Store objects with reserved names ufd.reserved_objects[name] = value # FIXME: Remove after FFC is updated to use reserved_objects: ufd.object_names[name] = value ufd.object_by_name[name] = value elif isinstance( value, (AbstractFiniteElement, Coefficient, Constant, Argument, Form, Expr) ): # Store instance <-> name mappings for important objects # without a reserved name ufd.object_names[id(value)] = name ufd.object_by_name[name] = value # Get list of exported forms forms = namespace.get("forms") if forms is None: # Get forms from object_by_name, which has already mapped # tuple->Form where needed def get_form(name): form = ufd.object_by_name.get(name) return form if isinstance(form, Form) else None a_form = get_form("a") L_form = get_form("L") M_form = get_form("M") forms = [a_form, L_form, M_form] # Add forms F and J if not "a" and "L" are used if a_form is None or L_form is None: F_form = get_form("F") J_form = get_form("J") forms += [F_form, J_form] # Remove Nones forms = [f for f in forms if isinstance(f, Form)] ufd.forms = forms # Validate types if not isinstance(ufd.forms, (list, tuple)): raise ValueError(f"Expecting 'forms' to be a list or tuple, not '{type(ufd.forms)}'.") if not all(isinstance(a, Form) for a in ufd.forms): raise ValueError("Expecting 'forms' to be a list of Form instances.") # Get list of exported elements elements = namespace.get("elements") if elements is None: elements = [ufd.object_by_name.get(name) for name in ("element",)] elements = [e for e in elements if e is not None] ufd.elements = elements # Validate types if not isinstance(ufd.elements, (list, tuple)): raise ValueError( f"Expecting 'elements' to be a list or tuple, not '{type(ufd.elements)}''." ) if not all(isinstance(e, AbstractFiniteElement) for e in ufd.elements): raise ValueError("Expecting 'elements' to be a list of AbstractFiniteElement instances.") # Get list of exported coefficients functions = [] ufd.coefficients = namespace.get("coefficients", functions) # Validate types if not isinstance(ufd.coefficients, (list, tuple)): raise ValueError( f"Expecting 'coefficients' to be a list or tuple, not '{type(ufd.coefficients)}'." ) if not all(isinstance(e, Coefficient) for e in ufd.coefficients): raise ValueError("Expecting 'coefficients' to be a list of Coefficient instances.") # Get list of exported expressions ufd.expressions = namespace.get("expressions", []) # Validate types if not isinstance(ufd.expressions, (list, tuple)): raise ValueError( f"Expecting 'expressions' to be a list or tuple, not '{type(ufd.expressions)}'." ) if not all(isinstance(e[0], Expr) for e in ufd.expressions): raise ValueError("Expecting 'expressions' to be a list of Expr instances.") # Return file data return ufd def load_ufl_file(filename): """Load a UFL file with elements, coefficients, expressions and forms.""" # Read code from file and execute it uflcode = read_ufl_file(filename) namespace = execute_ufl_code(uflcode) return interpret_ufl_namespace(namespace) def load_forms(filename): """Return a list of all forms in a file.""" ufd = load_ufl_file(filename) return ufd.forms ufl-2024.2.0/ufl/algorithms/formsplitter.py000066400000000000000000000141641470142567200206100ustar00rootroot00000000000000"""Extract part of a form in a mixed FunctionSpace.""" # Copyright (C) 2016-2024 Chris Richardson and Lawrence Mitchell # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Cecile Daversin-Catty, 2018 from typing import Optional from ufl.algorithms.map_integrands import map_expr_dag, map_integrand_dags from ufl.argument import Argument from ufl.classes import FixedIndex, ListTensor from ufl.constantvalue import Zero from ufl.corealg.multifunction import MultiFunction from ufl.functionspace import FunctionSpace from ufl.tensors import as_vector class FormSplitter(MultiFunction): """Form splitter.""" def split(self, form, ix, iy=0): """Split.""" # Remember which block to extract self.idx = [ix, iy] return map_integrand_dags(self, form) def argument(self, obj): """Apply to argument.""" if obj.part() is not None: # Mixed element built from MixedFunctionSpace, # whose sub-function spaces are indexed by obj.part() if obj.part() == self.idx[obj.number()]: return obj else: return Zero(obj.ufl_shape) else: # Mixed element built from MixedElement, # whose sub-elements need their function space to be created Q = obj.ufl_function_space() dom = Q.ufl_domain() sub_elements = obj.ufl_element().sub_elements # If not a mixed element, do nothing if len(sub_elements) == 0: return obj args = [] for i, sub_elem in enumerate(sub_elements): Q_i = FunctionSpace(dom, sub_elem) a = Argument(Q_i, obj.number(), part=obj.part()) indices = [()] for m in a.ufl_shape: indices = [(k + (j,)) for k in indices for j in range(m)] if i == self.idx[obj.number()]: args += [a[j] for j in indices] else: args += [Zero() for j in indices] return as_vector(args) def indexed(self, o, child, multiindex): """Extract indexed entry if multindices are fixed. This avoids tensors like (v_0, 0)[1] to be created. """ indices = multiindex.indices() if isinstance(child, ListTensor) and all(isinstance(i, FixedIndex) for i in indices): if len(indices) == 1: return child.ufl_operands[indices[0]._value] else: return ListTensor(*(child.ufl_operands[i._value] for i in multiindex.indices())) return self.expr(o, child, multiindex) def multi_index(self, obj): """Apply to multi_index.""" return obj def restricted(self, o): """Apply to a restricted function.""" # If we hit a restriction first apply form splitter to argument, then check for zero op_split = map_expr_dag(self, o.ufl_operands[0]) if isinstance(op_split, Zero): return op_split else: return op_split(o._side) expr = MultiFunction.reuse_if_untouched def extract_blocks(form, i: Optional[int] = None, j: Optional[None] = None): """Extract blocks of a form. If arity is 0, returns the form. If arity is 1, return the ith block. If ``i`` is ``None``, return all blocks. If arity is 2, return the ``(i,j)`` entry. If ``j`` is ``None``, return the ith row. If neither `i` nor `j` are set, return all blocks (as a scalar, vector or tensor). Args: form: A form i: Index of the block to extract. If set to ``None``, ``j`` must be None. j: Index of the block to extract. """ if i is None and j is not None: raise RuntimeError(f"Cannot extract block with {j=} and {i=}.") fs = FormSplitter() arguments = form.arguments() numbers = tuple(sorted(set(a.number() for a in arguments))) arity = len(numbers) assert arity <= 2 if arity == 0: return (form,) # If mixed element, each argument has no sub-elements parts = tuple(sorted(set(part for a in arguments if (part := a.part()) is not None))) if parts == (): if i is None and j is None: num_sub_elements = arguments[0].ufl_element().num_sub_elements forms = [] for pi in range(num_sub_elements): form_i = [] for pj in range(num_sub_elements): f = fs.split(form, pi, pj) if f.empty(): form_i.append(None) else: form_i.append(f) forms.append(tuple(form_i)) return tuple(forms) else: return fs.split(form, i, j) # If mixed function space, each argument has sub-elements forms = [] num_parts = len(parts) for pi in range(num_parts): form_i = [] if arity > 1: for pj in range(num_parts): f = fs.split(form, pi, pj) if f.empty(): form_i.append(None) else: if (num_args := len(f.arguments())) != 2: raise RuntimeError(f"Expected 2 arguments, got {num_args}") form_i.append(f) forms.append(tuple(form_i)) else: f = fs.split(form, pi) if f.empty(): forms.append(None) else: forms.append(f) try: forms_tuple = tuple(forms) except TypeError: # Only one form returned forms_tuple = (forms,) if i is not None: if (num_rows := len(forms_tuple)) <= i: raise RuntimeError(f"Cannot extract block {i} from form with {num_rows} blocks.") if arity > 1 and j is not None: if (num_cols := len(forms_tuple[i])) <= j: raise RuntimeError( f"Cannot extract block {i},{j} from form with {num_rows}x{num_cols} blocks." ) return forms_tuple[i][j] else: return forms_tuple[i] else: return forms_tuple ufl-2024.2.0/ufl/algorithms/formtransformations.py000066400000000000000000000404151470142567200221710ustar00rootroot00000000000000"""Utilities for transforming complete Forms into new related Forms.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # Modified by Garth N. Wells, 2010. # Modified by Marie E. Rognes, 2010. import warnings from logging import debug from ufl.algebra import Conj # Other algorithms: from ufl.algorithms.map_integrands import map_integrands from ufl.algorithms.replace import replace from ufl.algorithms.transformer import Transformer from ufl.argument import Argument from ufl.coefficient import Coefficient from ufl.constantvalue import Zero # All classes: from ufl.core.expr import ufl_err_str # FIXME: Don't use this below, it makes partextracter more expensive than necessary def _expr_has_terminal_types(expr, ufl_types): """Check if an expression has terminal types.""" input = [expr] while input: e = input.pop() ops = e.ufl_operands if ops: input.extend(ops) elif isinstance(e, ufl_types): return True return False def zero_expr(e): """Create a zero expression.""" return Zero(e.ufl_shape, e.ufl_free_indices, e.ufl_index_dimensions) class PartExtracter(Transformer): """PartExtracter extracts those parts of a form that contain the given argument(s).""" def __init__(self, arguments): """Initialise.""" Transformer.__init__(self) self._want = set(arguments) def expr(self, x): """Apply to expr. The default is a nonlinear operator not accepting any Arguments among its children. """ if _expr_has_terminal_types(x, Argument): raise ValueError(f"Found Argument in {ufl_err_str(x)}, this is an invalid expression.") return (x, set()) # Terminals that are not Variables or Arguments behave as default # expr-s. terminal = expr def variable(self, x): """Return relevant parts of this variable.""" # Extract parts/provides from this variable's expression expression, label = x.ufl_operands part, provides = self.visit(expression) # If the extracted part is zero or we provide more than we # want, return zero if isinstance(part, Zero) or (provides - self._want): return (zero_expr(x), set()) # Reuse varible if possible (or reconstruct from part) x = self.reuse_if_possible(x, part, label) return (x, provides) def argument(self, x): """Return itself unless itself provides too much.""" # An argument provides itself provides = {x} # If we provide more than we want, return zero if provides - self._want: return (zero_expr(x), set()) return (x, provides) def sum(self, x): """Return the terms that might eventually yield the correct parts(!). The logic required for sums is a bit elaborate: A sum may contain terms providing different arguments. We should return (a sum of) a suitable subset of these terms. Those should all provide the same arguments. For each term in a sum, there are 2 simple possibilities: 1a) The relevant part of the term is zero -> skip. 1b) The term provides more arguments than we want -> skip 2) If all terms fall into the above category, we can just return zero. Any remaining terms may provide exactly the arguments we want, or fewer. This is where things start getting interesting. 3) Bottom-line: if there are terms with providing different arguments -- provide terms that contain the most arguments. If there are terms providing different sets of same size -> throw error (e.g. Argument(-1) + Argument(-2)). """ parts_that_provide = {} # 1. Skip terms that provide too much original_terms = x.ufl_operands assert len(original_terms) == 2 for term in original_terms: # Visit this term in the sum part, term_provides = self.visit(term) # If this part is zero or it provides more than we want, # skip it if isinstance(part, Zero) or (term_provides - self._want): continue # Add the contributions from this part to temporary list term_provides = frozenset(term_provides) if term_provides in parts_that_provide: parts_that_provide[term_provides] += [part] else: parts_that_provide[term_provides] = [part] # 2. If there are no remaining terms, return zero if not parts_that_provide: return (zero_expr(x), set()) # 3. Return the terms that provide the biggest set most_provided = frozenset() for provideds, parts in parts_that_provide.items(): # TODO: Just sort instead? # Throw error if size of sets are equal (and not zero) if len(provideds) == len(most_provided) and len(most_provided): raise ValueError("Don't know what to do with sums with different Arguments.") if provideds > most_provided: most_provided = provideds terms = parts_that_provide[most_provided] if len(terms) == 2: x = self.reuse_if_possible(x, *terms) else: (x,) = terms return (x, most_provided) def product(self, x, *ops): """Apply to product. Note: Product is a visit-children-first handler. ops are the visited factors. """ provides = set() factors = [] for factor, factor_provides in ops: # If any factor is zero, return if isinstance(factor, Zero): return (zero_expr(x), set()) # Add factor to factors and extend provides factors.append(factor) provides = provides | factor_provides # If we provide more than we want, return zero if provides - self._want: return (zero_expr(x), provides) # Reuse product if possible (or reconstruct from factors) x = self.reuse_if_possible(x, *factors) return (x, provides) # inner, outer and dot all behave as product inner = product outer = product dot = product def division(self, x): """Return parts_of_numerator/denominator.""" # Get numerator and denominator numerator, denominator = x.ufl_operands # Check for Arguments in the denominator if _expr_has_terminal_types(denominator, Argument): raise ValueError( f"Found Argument in denominator of {ufl_err_str(x)}, this is an invalid expression." ) # Visit numerator numerator_parts, provides = self.visit(numerator) # If numerator is zero, return zero. (No need to check whether # it provides too much, already checked by visit.) if isinstance(numerator_parts, Zero): return (zero_expr(x), set()) # Reuse x if possible, otherwise reconstruct from (parts of) # numerator and denominator x = self.reuse_if_possible(x, numerator_parts, denominator) return (x, provides) def linear_operator(self, x, arg): """Apply to linear_operator. A linear operator with a single operand accepting arity > 0, providing whatever Argument its operand does. """ # linear_operator is a visit-children-first handler. Handled # arguments are in arg. part, provides = arg # Discard if part is zero. (No need to check whether we # provide too much, already checked by children.) if isinstance(part, Zero): return (zero_expr(x), set()) x = self.reuse_if_possible(x, part) return (x, provides) # Positive and negative restrictions behave as linear operators positive_restricted = linear_operator negative_restricted = linear_operator # Cell and facet average are linear operators cell_avg = linear_operator facet_avg = linear_operator # Grad is a linear operator grad = linear_operator # Conj, Real, Imag are linear operators conj = linear_operator real = linear_operator imag = linear_operator def linear_indexed_type(self, x): """Return parts of expression belonging to this indexed expression.""" expression, index = x.ufl_operands part, provides = self.visit(expression) # Return zero if extracted part is zero. (The expression # should already have checked if it provides too much.) if isinstance(part, Zero): return (zero_expr(x), set()) # Reuse x if possible (or reconstruct by indexing part) x = self.reuse_if_possible(x, part, index) return (x, provides) # All of these indexed thingies behave as a linear_indexed_type indexed = linear_indexed_type index_sum = linear_indexed_type component_tensor = linear_indexed_type def list_tensor(self, x, *ops): """Apply to list_tensor.""" # list_tensor is a visit-children-first handler. ops contains # the visited operands with their provides. (It follows that # none of the visited operands provide more than wanted.) # Extract the most arguments provided by any of the components most_provides = ops[0][1] for component, provides in ops: if provides - most_provides: most_provides = provides # Check that all components either provide the same arguments # or vanish. (This check is here b/c it is not obvious what to # return if the components provide different arguments, at # least with the current transformer design.) for component, provides in ops: if provides != most_provides and not isinstance(component, Zero): raise ValueError( "PartExtracter does not know how to handle list_tensors with " "non-zero components providing fewer arguments" ) # Return components components = [op[0] for op in ops] x = self.reuse_if_possible(x, *components) return (x, most_provides) def compute_form_with_arity(form, arity, arguments=None): """Compute parts of form of given arity.""" # Extract all arguments in form if arguments is None: arguments = form.arguments() parts = [arg.part() for arg in arguments] if set(parts) - {None}: raise ValueError("compute_form_with_arity cannot handle parts.") if len(arguments) < arity: warnings.warn(f"Form has no parts with arity {arity}.") return 0 * form # Assuming that the form is not a sum of terms # that depend on different arguments, e.g. (u+v)*dx # would result in just v*dx. But that doesn't make # any sense anyway. sub_arguments = set(arguments[:arity]) pe = PartExtracter(sub_arguments) def _transform(e): e, provides = pe.visit(e) if provides == sub_arguments: return e return Zero() return map_integrands(_transform, form) def compute_form_arities(form): """Return set of arities of terms present in form.""" # Extract all arguments present in form arguments = form.arguments() parts = [arg.part() for arg in arguments] if set(parts) - {None}: raise ValueError("compute_form_arities cannot handle parts.") arities = set() for arity in range(len(arguments) + 1): # Compute parts with arity "arity" parts = compute_form_with_arity(form, arity, arguments) # Register arity if "parts" does not vanish if parts and parts.integrals(): arities.add(arity) return arities def compute_form_lhs(form): """Compute the left hand side of a form. Example: a = u*v*dx + f*v*dx a = lhs(a) -> u*v*dx """ return compute_form_with_arity(form, 2) def compute_form_rhs(form): """Compute the right hand side of a form. Example: a = u*v*dx + f*v*dx L = rhs(a) -> -f*v*dx """ return -compute_form_with_arity(form, 1) def compute_form_functional(form): """Compute the functional part of a form, that is the terms independent of Arguments. (Used for testing, not sure if it's useful for anything?) """ return compute_form_with_arity(form, 0) def compute_form_action(form, coefficient): """Compute the action of a form on a Coefficient. This works simply by replacing the last Argument with a Coefficient on the same function space (element). The form returned will thus have one Argument less and one additional Coefficient at the end if no Coefficient has been provided. """ # TODO: Check whatever makes sense for coefficient # Extract all arguments arguments = form.arguments() parts = [arg.part() for arg in arguments] if set(parts) - {None}: raise ValueError("compute_form_action cannot handle parts.") # Pick last argument (will be replaced) u = arguments[-1] fs = u.ufl_function_space() if coefficient is None: coefficient = Coefficient(fs) elif coefficient.ufl_function_space() != fs: debug("Computing action of form on a coefficient in a different function space.") return replace(form, {u: coefficient}) def compute_energy_norm(form, coefficient): """Compute the a-norm of a Coefficient given a form a. This works simply by replacing the two Arguments with a Coefficient on the same function space (element). The Form returned will thus be a functional with no Arguments, and one additional Coefficient at the end if no coefficient has been provided. """ from ufl.formoperators import action # Delayed import to avoid circularity arguments = form.arguments() parts = [arg.part() for arg in arguments] if set(parts) - {None}: raise ValueError("compute_energy_norm cannot handle parts.") if len(arguments) != 2: raise ValueError("Expecting bilinear form.") v, u = arguments U = u.ufl_function_space() V = v.ufl_function_space() if U != V: raise ValueError( f"Expecting equal finite elements for test and trial functions, got '{U}' and '{V}'." ) if coefficient is None: coefficient = Coefficient(V) else: if coefficient.ufl_function_space() != U: raise ValueError( "Trying to compute action of form on a " "coefficient in an incompatible element space." ) return action(action(form, coefficient), coefficient) def compute_form_adjoint(form, reordered_arguments=None): """Compute the adjoint of a bilinear form. This works simply by swapping the number and part of the two arguments, but keeping their elements and places in the integrand expressions. """ arguments = form.arguments() parts = [arg.part() for arg in arguments] if set(parts) - {None}: raise ValueError("compute_form_adjoint cannot handle parts.") if len(arguments) != 2: raise ValueError("Expecting bilinear form.") v, u = arguments if v.number() >= u.number(): raise ValueError("Mistaken assumption in code!") if reordered_arguments is None: reordered_u = Argument(u.ufl_function_space(), number=v.number(), part=v.part()) reordered_v = Argument(v.ufl_function_space(), number=u.number(), part=u.part()) else: reordered_u, reordered_v = reordered_arguments if reordered_u.number() >= reordered_v.number(): raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_u.part() != v.part(): raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_v.part() != u.part(): raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_u.ufl_function_space() != u.ufl_function_space(): raise ValueError("Element mismatch between new and old arguments (trial functions).") if reordered_v.ufl_function_space() != v.ufl_function_space(): raise ValueError("Element mismatch between new and old arguments (test functions).") return map_integrands(Conj, replace(form, {v: reordered_v, u: reordered_u})) ufl-2024.2.0/ufl/algorithms/map_integrands.py000066400000000000000000000067731470142567200210600ustar00rootroot00000000000000"""Basic algorithms for applying functions to sub-expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # NOTE: Placing this under algorithms/ because I want corealg/ to stay clean # as part of a careful refactoring process, and this file depends on ufl.form # which drags in a lot of stuff. from ufl.action import Action from ufl.adjoint import Adjoint from ufl.constantvalue import Zero from ufl.core.expr import Expr from ufl.corealg.map_dag import map_expr_dag from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm from ufl.integral import Integral def map_integrands(function, form, only_integral_type=None): """Map integrands. Apply transform(expression) to each integrand expression in form, or to form if it is an Expr. """ if isinstance(form, Form): mapped_integrals = [ map_integrands(function, itg, only_integral_type) for itg in form.integrals() ] nonzero_integrals = [ itg for itg in mapped_integrals if not isinstance(itg.integrand(), Zero) ] return Form(nonzero_integrals) elif isinstance(form, Integral): itg = form if (only_integral_type is None) or (itg.integral_type() in only_integral_type): return itg.reconstruct(function(itg.integrand())) else: return itg elif isinstance(form, FormSum): mapped_components = [ map_integrands(function, component, only_integral_type) for component in form.components() ] nonzero_components = [ (component, w) for component, w in zip(mapped_components, form.weights()) # Catch ufl.Zero and ZeroBaseForm if component != 0 ] # Simplify case with one nonzero component and the corresponding weight is 1 if len(nonzero_components) == 1 and nonzero_components[0][1] == 1: return nonzero_components[0][0] if all(not isinstance(component, BaseForm) for component, _ in nonzero_components): # Simplification of `BaseForm` objects may turn `FormSum` into a sum of `Expr` objects # that are not `BaseForm`, i.e. into a `Sum` object. # Example: `Action(Adjoint(c*), u)` with `c*` a `Coargument` and u a `Coefficient`. return sum([component for component, _ in nonzero_components]) return FormSum(*nonzero_components) elif isinstance(form, Adjoint): # Zeros are caught inside `Adjoint.__new__` return Adjoint(map_integrands(function, form._form, only_integral_type)) elif isinstance(form, Action): left = map_integrands(function, form._left, only_integral_type) right = map_integrands(function, form._right, only_integral_type) # Zeros are caught inside `Action.__new__` return Action(left, right) elif isinstance(form, ZeroBaseForm): arguments = tuple( map_integrands(function, arg, only_integral_type) for arg in form._arguments ) return ZeroBaseForm(arguments) elif isinstance(form, (Expr, BaseForm)): integrand = form return function(integrand) else: raise ValueError("Expecting Form, Integral or Expr.") def map_integrand_dags(function, form, only_integral_type=None, compress=True): """Map integrand dags.""" return map_integrands( lambda expr: map_expr_dag(function, expr, compress), form, only_integral_type ) ufl-2024.2.0/ufl/algorithms/remove_complex_nodes.py000066400000000000000000000022501470142567200222630ustar00rootroot00000000000000"""Remove conj, real, and imag nodes from a form.""" from ufl.algorithms.map_integrands import map_integrand_dags from ufl.constantvalue import ComplexValue from ufl.corealg.multifunction import MultiFunction class ComplexNodeRemoval(MultiFunction): """Replaces complex operator nodes with their children.""" expr = MultiFunction.reuse_if_untouched def conj(self, o, a): """Apply to conj.""" return a def real(self, o, a): """Apply to real.""" return a def imag(self, o, a): """Apply to imag.""" raise ValueError("Unexpected imag in real expression.") def terminal(self, t, *ops): """Apply to terminal.""" if isinstance(t, ComplexValue): raise ValueError("Unexpected complex value in real expression.") else: return t def remove_complex_nodes(expr): """Replaces complex operator nodes with their children. This is called during compute_form_data if the compiler wishes to compile real-valued forms. In essence this strips all trace of complex support from the preprocessed form. """ return map_integrand_dags(ComplexNodeRemoval(), expr) ufl-2024.2.0/ufl/algorithms/renumbering.py000066400000000000000000000036201470142567200203660ustar00rootroot00000000000000"""Algorithms for renumbering of counted objects, currently variables and indices.""" # Copyright (C) 2008-2024 Martin Sandve Alnæs, Anders Logg, Jørgen S. Dokken and Lawrence Mitchell # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from collections import defaultdict from itertools import count as _count from ufl.algorithms.map_integrands import map_integrand_dags from ufl.core.multiindex import Index from ufl.corealg.multifunction import MultiFunction class IndexRelabeller(MultiFunction): """Renumber indices to have a consistent index numbering starting from 0.""" def __init__(self): """Initialize index relabeller with a zero count.""" super().__init__() count = _count() self.index_cache = defaultdict(lambda: Index(next(count))) expr = MultiFunction.reuse_if_untouched def multi_index(self, o): """Apply to multi-indices.""" return type(o)( tuple(self.index_cache[i] if isinstance(i, Index) else i for i in o.indices()) ) def zero(self, o): """Apply to zero.""" fi = o.ufl_free_indices fid = o.ufl_index_dimensions new_indices = [self.index_cache[Index(i)].count() for i in fi] if fi == () and fid == (): return o new_fi, new_fid = zip(*sorted(zip(new_indices, fid), key=lambda x: x[0])) return type(o)(o.ufl_shape, tuple(new_fi), tuple(new_fid)) def renumber_indices(form): """Renumber indices to have a consistent index numbering starting from 0. This is useful to avoid multiple kernels for the same integrand, but with different subdomain ids. Args: form: A UFL form, integral or expression. Returns: A new form, integral or expression with renumbered indices. """ reindexer = IndexRelabeller() return map_integrand_dags(reindexer, form) ufl-2024.2.0/ufl/algorithms/replace.py000066400000000000000000000065151470142567200174720ustar00rootroot00000000000000"""Algorithm for replacing terminals in an expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010 from ufl.algorithms.analysis import has_exact_type from ufl.algorithms.map_integrands import map_integrand_dags from ufl.classes import BaseForm, CoefficientDerivative from ufl.constantvalue import as_ufl from ufl.core.external_operator import ExternalOperator from ufl.core.interpolate import Interpolate from ufl.corealg.multifunction import MultiFunction class Replacer(MultiFunction): """Replacer.""" def __init__(self, mapping): """Initialize.""" super().__init__() self.mapping = mapping # One can replace Coarguments by 1-Forms def get_shape(x): """Get the shape of an object.""" if isinstance(x, BaseForm): return x.arguments()[0].ufl_shape return x.ufl_shape if not all(get_shape(k) == get_shape(v) for k, v in mapping.items()): raise ValueError( "Replacement expressions must have the same shape as what they replace." ) def ufl_type(self, o, *args): """Replace a ufl_type.""" try: return self.mapping[o] except KeyError: return self.reuse_if_untouched(o, *args) def external_operator(self, o): """Replace an external_operator.""" o = self.mapping.get(o) or o if isinstance(o, ExternalOperator): new_ops = tuple(replace(op, self.mapping) for op in o.ufl_operands) new_args = tuple(replace(arg, self.mapping) for arg in o.argument_slots()) return o._ufl_expr_reconstruct_(*new_ops, argument_slots=new_args) return o def interpolate(self, o): """Replace an interpolate.""" o = self.mapping.get(o) or o if isinstance(o, Interpolate): new_args = tuple(replace(arg, self.mapping) for arg in o.argument_slots()) return o._ufl_expr_reconstruct_(*reversed(new_args)) return o def coefficient_derivative(self, o): """Replace a coefficient derivative.""" raise ValueError("Derivatives should be applied before executing replace.") def replace(e, mapping): """Replace subexpressions in expression. Params: e: An Expr or Form mapping: A dict with from:to replacements to perform Returns: The expression with replacements performed """ mapping2 = dict((k, as_ufl(v)) for (k, v) in mapping.items()) # Workaround for problem with delayed derivative evaluation # The problem is that J = derivative(f(g, h), g) does not evaluate immediately # So if we subsequently do replace(J, {g: h}) we end up with an expression: # derivative(f(h, h), h) # rather than what were were probably thinking of: # replace(derivative(f(g, h), g), {g: h}) # # To fix this would require one to expand derivatives early (which # is not attractive), or make replace lazy too. if has_exact_type(e, CoefficientDerivative): # Hack to avoid circular dependencies from ufl.algorithms.ad import expand_derivatives e = expand_derivatives(e) return map_integrand_dags(Replacer(mapping2), e) ufl-2024.2.0/ufl/algorithms/replace_derivative_nodes.py000066400000000000000000000052511470142567200231000ustar00rootroot00000000000000"""Algorithm for replacing derivative nodes in a BaseForm or Expr.""" import ufl from ufl.algorithms.analysis import extract_arguments from ufl.algorithms.map_integrands import map_integrand_dags from ufl.constantvalue import as_ufl from ufl.corealg.multifunction import MultiFunction from ufl.tensors import ListTensor class DerivativeNodeReplacer(MultiFunction): """Replace derivative nodes with new derivative nodes.""" def __init__(self, mapping, **derivative_kwargs): """Initialise.""" super().__init__() self.mapping = mapping self.der_kwargs = derivative_kwargs expr = MultiFunction.reuse_if_untouched def coefficient_derivative(self, cd, o, coefficients, arguments, coefficient_derivatives): """Apply to coefficient_derivative.""" der_kwargs = self.der_kwargs new_coefficients = tuple( self.mapping[c] if c in self.mapping.keys() else c for c in coefficients.ufl_operands ) # Ensure type compatibility for arguments! if "argument" not in der_kwargs.keys(): # Argument's number/part can be retrieved from the former coefficient derivative. arguments = arguments.ufl_operands new_arguments = () for c, a in zip(new_coefficients, arguments): if isinstance(a, ListTensor): (a,) = extract_arguments(a) new_arguments += (type(a)(c.ufl_function_space(), a.number(), a.part()),) der_kwargs.update({"argument": new_arguments}) return ufl.derivative(o, new_coefficients, **der_kwargs) def replace_derivative_nodes(expr, mapping, **derivative_kwargs): """Replaces derivative nodes. Replaces the variable with respect to which the derivative is taken. This is called during apply_derivatives to treat delayed derivatives. Example: Let u be a Coefficient, N an ExternalOperator independent of u (i.e. N's operands don't depend on u), and let uhat and Nhat be Arguments. F = u ** 2 * N * dx dFdu = derivative(F, u, uhat) dFdN = replace_derivative_nodes(dFdu, {u: N}, argument=Nhat) Then, by subsequently expanding the derivatives we have: dFdu -> 2 * u * uhat * N * dx dFdN -> u ** 2 * Nhat * dx Args: expr: An Expr or BaseForm. mapping: A dict with from:to replacements to perform. derivative_kwargs: A dict containing the keyword arguments for derivative (i.e. `argument` and `coefficient_derivatives`). """ mapping2 = dict((k, as_ufl(v)) for (k, v) in mapping.items()) return map_integrand_dags(DerivativeNodeReplacer(mapping2, **derivative_kwargs), expr) ufl-2024.2.0/ufl/algorithms/signature.py000066400000000000000000000131121470142567200200470ustar00rootroot00000000000000"""Signature computation for forms.""" # Copyright (C) 2012-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import hashlib from ufl.algorithms.domain_analysis import canonicalize_metadata from ufl.classes import ( Argument, Coefficient, Constant, ConstantValue, ExprList, ExprMapping, GeometricQuantity, Index, Label, MultiIndex, ) from ufl.corealg.traversal import traverse_unique_terminals, unique_post_traversal def compute_multiindex_hashdata(expr, index_numbering): """Compute multiindex hashdata.""" data = [] for i in expr: if isinstance(i, Index): j = index_numbering.get(i) if j is None: # Use negative ints for Index j = -(len(index_numbering) + 1) index_numbering[i] = j data.append(j) else: # Use nonnegative ints for FixedIndex data.append(int(i)) return tuple(data) def compute_terminal_hashdata(expressions, renumbering): """Compute terminal hashdata.""" if not isinstance(expressions, list): expressions = [expressions] assert renumbering is not None # Extract a unique numbering of free indices, as well as form # arguments, and just take repr of the rest of the terminals while # we're iterating over them terminal_hashdata = {} index_numbering = {} for expression in expressions: for expr in traverse_unique_terminals(expression): if isinstance(expr, MultiIndex): # Indices need a canonical numbering for a stable # signature, thus this algorithm data = compute_multiindex_hashdata(expr, index_numbering) elif isinstance(expr, ConstantValue): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, Coefficient): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, Constant): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, Argument): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, GeometricQuantity): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, Label): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, ExprList): # Not really a terminal but can have 0 operands... data = "[]" elif isinstance(expr, ExprMapping): # Not really a terminal but can have 0 operands... data = "{}" else: raise ValueError(f"Unknown terminal type {type(expr)}") terminal_hashdata[expr] = data return terminal_hashdata def compute_expression_hashdata(expression, terminal_hashdata) -> bytes: """Compute expression hashdata.""" cache = {} for expr in unique_post_traversal(expression): # Uniquely traverse tree and hash each node # E.g. (a + b*c) is hashed as hash([+, hash(a), hash([*, hash(b), hash(c)])]) # Traversal uses post pattern, so children hashes are cached if expr._ufl_is_terminal_: data = [terminal_hashdata[expr]] else: data = [expr._ufl_typecode_] for op in expr.ufl_operands: data += [cache[op]] cache[expr] = hashlib.sha512(str(data).encode("utf-8")).digest() return cache[expression] def compute_expression_signature(expr, renumbering): # FIXME: Fix callers """Compute expression signature.""" # FIXME: Rewrite in terms of compute_form_signature? # Build hashdata for all terminals first terminal_hashdata = compute_terminal_hashdata([expr], renumbering) # Build hashdata for full expression expression_hashdata = compute_expression_hashdata(expr, terminal_hashdata) # Pass it through a seriously overkill hashing algorithm # (should we use sha1 instead?) return expression_hashdata.hex() def compute_form_signature(form, renumbering): # FIXME: Fix callers """Compute form signature.""" # Extract integrands integrals = form.integrals() integrands = [integral.integrand() for integral in integrals] # Build hashdata for all terminals first, with on-the-fly # replacement of functions and index labels. terminal_hashdata = compute_terminal_hashdata(integrands, renumbering) # Build hashdata for each integral hashdata = [] for integral in integrals: # Compute hash data for expression, this is the expensive part integrand_hashdata = compute_expression_hashdata(integral.integrand(), terminal_hashdata) domain_hashdata = integral.ufl_domain()._ufl_signature_data_(renumbering) # Collect all data about integral that should be reflected in # signature, including compiler data but not domain data, # because compiler data affects the way the integral is # compiled while domain data is only carried for convenience # in the problem solving environment. integral_hashdata = ( integrand_hashdata, domain_hashdata, integral.integral_type(), integral.subdomain_id(), canonicalize_metadata(integral.metadata()), ) hashdata.append(integral_hashdata) # Pass it through a seriously overkill hashing algorithm # (should we use sha1 instead?) data = str(hashdata).encode("utf-8") return hashlib.sha512(data).hexdigest() ufl-2024.2.0/ufl/algorithms/strip_terminal_data.py000066400000000000000000000116051470142567200221000ustar00rootroot00000000000000"""Algorithm for replacing form arguments with 'stripped' versions. In the stripped version, any data-carrying objects have been extracted to a mapping. """ from ufl.algorithms.replace import replace from ufl.classes import ( Argument, Coefficient, Constant, Form, FunctionSpace, Integral, Mesh, MeshView, MixedFunctionSpace, TensorProductFunctionSpace, ) from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction class TerminalStripper(MultiFunction): """Terminal stripper.""" def __init__(self): """Initialise.""" super().__init__() self.mapping = {} def argument(self, o): """Apply to argument.""" o_new = Argument(strip_function_space(o.ufl_function_space()), o.number(), o.part()) return self.mapping.setdefault(o, o_new) def coefficient(self, o): """Apply to coefficient.""" o_new = Coefficient(strip_function_space(o.ufl_function_space()), o.count()) return self.mapping.setdefault(o, o_new) def constant(self, o): """Apply to constant.""" o_new = Constant(strip_domain(o.ufl_domain()), o.ufl_shape, o.count()) return self.mapping.setdefault(o, o_new) expr = MultiFunction.reuse_if_untouched def strip_terminal_data(o): """Return a new form where all terminals have been replaced by UFL-only equivalents. This function is useful for forms containing augmented UFL objects that hold references to large data structures. These objects are be extracted into the mapping allowing the form to be cached without leaking memory. Args: o: The object to be stripped. This must either be a Form or Integral Returns: A 2-tuple containing an equivalent UFL-only object and a mapping allowing the original form to be reconstructed using replace_terminal_data """ # We need to keep track of two maps because integrals store references to the # domain and ``replace`` expects only a mapping containing ``Expr`` objects. if isinstance(o, Form): integrals = [] expr_map = {} domain_map = {} for integral in o.integrals(): itg, (emap, dmap) = strip_terminal_data(integral) integrals.append(itg) expr_map.update(emap) domain_map.update(dmap) return Form(integrals), (expr_map, domain_map) elif isinstance(o, Integral): handler = TerminalStripper() integrand = map_expr_dag(handler, o.integrand()) domain = strip_domain(o.ufl_domain()) # invert the mapping so it can be passed straight into replace_terminal_data expr_map = {v: k for k, v in handler.mapping.items()} domain_map = {domain: o.ufl_domain()} return o.reconstruct(integrand, domain=domain), (expr_map, domain_map) else: raise ValueError("Only Form or Integral inputs expected") def replace_terminal_data(o, mapping): """Return a new form where the terminals have been replaced using the provided mapping. Args: o: The object to have its terminals replaced. This must either be a Form or Integral. mapping: A mapping suitable for reconstructing the form such as the one returned by strip_terminal_data. Returns: The new form. """ if isinstance(o, Form): return Form([replace_terminal_data(itg, mapping) for itg in o.integrals()]) elif isinstance(o, Integral): expr_map, domain_map = mapping integrand = replace(o.integrand(), expr_map) return o.reconstruct(integrand, domain=domain_map[o.ufl_domain()]) else: raise ValueError("Only Form or Integral inputs expected") def strip_function_space(function_space): """Return a new function space with all non-UFL information removed.""" if isinstance(function_space, FunctionSpace): return FunctionSpace( strip_domain(function_space.ufl_domain()), function_space.ufl_element() ) elif isinstance(function_space, TensorProductFunctionSpace): subspaces = [strip_function_space(sub) for sub in function_space.ufl_sub_spaces()] return TensorProductFunctionSpace(*subspaces) elif isinstance(function_space, MixedFunctionSpace): subspaces = [strip_function_space(sub) for sub in function_space.ufl_sub_spaces()] return MixedFunctionSpace(*subspaces) else: raise NotImplementedError(f"{type(function_space)} cannot be stripped") def strip_domain(domain): """Return a new domain with all non-UFL information removed.""" if isinstance(domain, Mesh): return Mesh(domain.ufl_coordinate_element(), domain.ufl_id()) elif isinstance(domain, MeshView): return MeshView( strip_domain(domain.ufl_mesh()), domain.topological_dimension(), domain.ufl_id() ) else: raise NotImplementedError(f"{type(domain)} cannot be stripped") ufl-2024.2.0/ufl/algorithms/transformer.py000066400000000000000000000174121470142567200204170ustar00rootroot00000000000000"""Transformer. This module defines the Transformer base class and some basic specializations to further base other algorithms upon, as well as some utilities for easier application of such algorithms. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010 import inspect from ufl.algorithms.map_integrands import map_integrands from ufl.classes import Variable, all_ufl_classes from ufl.core.ufl_type import UFLType def is_post_handler(function): """Check if function is a handler that expects transformed children as input.""" insp = inspect.getfullargspec(function) num_args = len(insp[0]) + int(insp[1] is not None) visit_children_first = num_args > 2 return visit_children_first class Transformer(object): """Transformer. Base class for a visitor-like algorithm design pattern used to transform expression trees from one representation to another. """ _handlers_cache = {} def __init__(self, variable_cache=None): """Initialise.""" if variable_cache is None: variable_cache = {} self._variable_cache = variable_cache # Analyse class properties and cache handler data the # first time this is run for a particular class cache_data = Transformer._handlers_cache.get(type(self)) if not cache_data: cache_data = [None] * len(all_ufl_classes) # For all UFL classes for classobject in all_ufl_classes: # Iterate over the inheritance chain # (NB! This assumes that all UFL classes inherits a single # Expr subclass and that this is the first superclass!) for c in classobject.mro(): # Register classobject with handler for the first # encountered superclass try: handler_name = c._ufl_handler_name_ except AttributeError as attribute_error: if type(classobject) is not UFLType: raise attribute_error # Default handler name for UFL types handler_name = UFLType._ufl_handler_name_ function = getattr(self, handler_name, None) if function: cache_data[classobject._ufl_typecode_] = ( handler_name, is_post_handler(function), ) break Transformer._handlers_cache[type(self)] = cache_data # Build handler list for this particular class (get functions # bound to self) self._handlers = [(getattr(self, name), post) for (name, post) in cache_data] # Keep a stack of objects visit is called on, to ease # backtracking self._visit_stack = [] def print_visit_stack(self): """Print visit stack.""" print("/" * 80) print("Visit stack in Transformer:") def sstr(s): """Format.""" ss = str(type(s)) + " ; " n = 160 - len(ss) return ss + str(s)[:n] print("\n".join(sstr(s) for s in self._visit_stack)) print("\\" * 80) def visit(self, o): """Visit.""" # Update stack self._visit_stack.append(o) # Get handler for the UFL class of o (type(o) may be an # external subclass of the actual UFL class) h, visit_children_first = self._handlers[o._ufl_typecode_] # Is this a handler that expects transformed children as # input? if visit_children_first: # Yes, visit all children first and then call h. r = h(o, *[self.visit(op) for op in o.ufl_operands]) else: # No, this is a handler that handles its own children # (arguments self and o, where self is already bound) r = h(o) # Update stack and return self._visit_stack.pop() return r def undefined(self, o): """Trigger error.""" raise ValueError(f"No handler defined for {o._ufl_class_.__name__}.") def reuse(self, o): """Reuse Expr (ignore children).""" return o def reuse_if_untouched(self, o, *ops): """Reuse object if operands are the same objects. Use in your own subclass by setting e.g. `expr = MultiFunction.reuse_if_untouched` as a default rule. """ if all(a is b for a, b in zip(o.ufl_operands, ops)): return o else: return o._ufl_expr_reconstruct_(*ops) # It's just so slow to compare all operands, avoiding it now reuse_if_possible = reuse_if_untouched def always_reconstruct(self, o, *operands): """Reconstruct expr.""" return o._ufl_expr_reconstruct_(*operands) # Set default behaviour for any UFLType ufl_type = undefined # Set default behaviour for any Terminal terminal = reuse def reuse_variable(self, o): """Reuse variable.""" # Check variable cache to reuse previously transformed # variable if possible e, l = o.ufl_operands # noqa: E741 v = self._variable_cache.get(l) if v is not None: return v # Visit the expression our variable represents e2 = self.visit(e) # If the expression is the same, reuse Variable object if e == e2: v = o else: # Recreate Variable (with same label) v = Variable(e2, l) # Cache variable self._variable_cache[l] = v return v def reconstruct_variable(self, o): """Reconstruct variable.""" # Check variable cache to reuse previously transformed # variable if possible e, l = o.ufl_operands # noqa: E741 v = self._variable_cache.get(l) if v is not None: return v # Visit the expression our variable represents e2 = self.visit(e) # Always reconstruct Variable (with same label) v = Variable(e2, l) self._variable_cache[l] = v return v class ReuseTransformer(Transformer): """Reuse transformer.""" def __init__(self, variable_cache=None): """Initialise.""" Transformer.__init__(self, variable_cache) # Set default behaviour for any Expr expr = Transformer.reuse_if_untouched # Set default behaviour for any Terminal terminal = Transformer.reuse # Set default behaviour for Variable variable = Transformer.reuse_variable class CopyTransformer(Transformer): """Copy transformer.""" def __init__(self, variable_cache=None): """Initialise.""" Transformer.__init__(self, variable_cache) # Set default behaviour for any Expr expr = Transformer.always_reconstruct # Set default behaviour for any Terminal terminal = Transformer.reuse # Set default behaviour for Variable variable = Transformer.reconstruct_variable class VariableStripper(ReuseTransformer): """Variable stripper.""" def __init__(self): """Initialise.""" ReuseTransformer.__init__(self) def variable(self, o): """Visit a variable.""" return self.visit(o.ufl_operands[0]) def apply_transformer(e, transformer, integral_type=None): """Apply transforms. Apply transformer.visit(expression) to each integrand expression in form, or to form if it is an Expr. """ return map_integrands(lambda expr: transformer.visit(expr), e, integral_type) def strip_variables(e): """Replace all Variable instances with the expression they represent.""" return apply_transformer(e, VariableStripper()) ufl-2024.2.0/ufl/algorithms/traversal.py000066400000000000000000000024671470142567200200640ustar00rootroot00000000000000"""This module contains algorithms for traversing expression trees in different ways.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 from ufl.action import Action from ufl.adjoint import Adjoint from ufl.core.expr import Expr from ufl.form import BaseForm, Form, FormSum from ufl.integral import Integral def iter_expressions(a): """Handle Form, Integral and any Expr the same way when inspecting expressions. Returns an iterable over Expr instances: - a is an Expr: (a,) - a is an Integral: the integrand expression of a - a is a Form: all integrand expressions of all integrals - a is a FormSum: the components of a - a is an Action: the left and right component of a - a is an Adjoint: the underlying form of a """ if isinstance(a, Form): return (itg.integrand() for itg in a.integrals()) elif isinstance(a, Integral): return (a.integrand(),) elif isinstance(a, (FormSum, Adjoint, Action)): return tuple(e for op in a.ufl_operands for e in iter_expressions(op)) elif isinstance(a, (Expr, BaseForm)): return (a,) raise ValueError(f"Not an UFL type: {type(a)}") ufl-2024.2.0/ufl/argument.py000066400000000000000000000241151470142567200155240ustar00rootroot00000000000000"""Argument. This module defines the class Argument and a number of related classes (functions), including TestFunction and TrialFunction. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # Modified by Massimiliano Leoni, 2016. # Modified by Cecile Daversin-Catty, 2018. # Modified by Ignacia Fierro-Piccardo 2023. import numbers from ufl.core.terminal import FormArgument from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual, is_primal from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace, MixedFunctionSpace from ufl.split_functions import split # Export list for ufl.classes (TODO: not actually classes: drop? these are in ufl.*) __all_classes__ = ["TestFunction", "TrialFunction", "TestFunctions", "TrialFunctions"] # --- Class representing an argument (basis function) in a form --- class BaseArgument(object): """UFL value: Representation of an argument to a form.""" __slots__ = () _ufl_is_abstract_ = True def __getnewargs__(self): """Get new args.""" return (self._ufl_function_space, self._number, self._part) def __init__(self, function_space, number, part=None): """Initialise.""" if not isinstance(function_space, AbstractFunctionSpace): raise ValueError("Expecting a FunctionSpace.") self._ufl_function_space = function_space self._ufl_shape = function_space.value_shape if not isinstance(number, numbers.Integral): raise ValueError(f"Expecting an int for number, not {number}") if part is not None and not isinstance(part, numbers.Integral): raise ValueError(f"Expecting None or an int for part, not {part}") self._number = number self._part = part self._repr = f"BaseArgument({self._ufl_function_space}, {self._number}, {self._part})" @property def ufl_shape(self): """Return the associated UFL shape.""" return self._ufl_shape def ufl_function_space(self): """Get the function space of this Argument.""" return self._ufl_function_space def ufl_domain(self): """Return the UFL domain.""" return self._ufl_function_space.ufl_domain() def ufl_element(self): """Return The UFL element.""" return self._ufl_function_space.ufl_element() def number(self): """Return the Argument number.""" return self._number def part(self): """Return the part.""" return self._part def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # TODO: Should in principle do like with Coefficient, # but that may currently simplify away some arguments # we want to keep, or? See issue#13. # When we can annotate zero with arguments, we can change this. return False def ufl_domains(self): """Return UFL domains.""" return self._ufl_function_space.ufl_domains() def _ufl_signature_data_(self, renumbering): """Signature data. Signature data for form arguments depend on the global numbering of the form arguments and domains. """ fsdata = self._ufl_function_space._ufl_signature_data_(renumbering) return ("Argument", self._number, self._part, fsdata) def __str__(self): """Format as a string.""" number = str(self._number) if len(number) == 1: s = "v_%s" % number else: s = "v_{%s}" % number if self._part is not None: part = str(self._part) if len(part) == 1: s = "%s^%s" % (s, part) else: s = "%s^{%s}" % (s, part) return s def __repr__(self): """Representation.""" return self._repr def __eq__(self, other): """Check equality. Deliberately comparing exact type and not using isinstance here, meaning eventual subclasses must reimplement this function to work correctly, and instances of this class will compare not equal to instances of eventual subclasses. The overloading allows subclasses to distinguish between test and trial functions with a different non-ufl payload, such as dolfin FunctionSpace with different mesh. This is necessary because arguments with the same element and argument number are always equal from a pure ufl point of view, e.g. TestFunction(V1) == TestFunction(V2) if V1 and V2 are the same ufl element but different dolfin function spaces. """ return ( type(self) is type(other) and self._number == other._number and self._part == other._part and self._ufl_function_space == other._ufl_function_space ) @ufl_type() class Argument(FormArgument, BaseArgument): """UFL value: Representation of an argument to a form.""" __slots__ = ( "_ufl_function_space", "_ufl_shape", "_number", "_part", "_repr", ) _primal = True _dual = False __getnewargs__ = BaseArgument.__getnewargs__ __str__ = BaseArgument.__str__ _ufl_signature_data_ = BaseArgument._ufl_signature_data_ __eq__ = BaseArgument.__eq__ def __new__(cls, *args, **kw): """Create new Argument.""" if args[0] and is_dual(args[0]): return Coargument(*args, **kw) return super().__new__(cls) def __init__(self, function_space, number, part=None): """Initialise.""" FormArgument.__init__(self) BaseArgument.__init__(self, function_space, number, part) self._repr = "Argument(%s, %s, %s)" % ( repr(self._ufl_function_space), repr(self._number), repr(self._part), ) def ufl_domains(self): """Return UFL domains.""" return BaseArgument.ufl_domains(self) def __repr__(self): """Representation.""" return self._repr @ufl_type() class Coargument(BaseForm, BaseArgument): """UFL value: Representation of an argument to a form in a dual space.""" __slots__ = ( "_ufl_function_space", "_ufl_shape", "_arguments", "_coefficients", "ufl_operands", "_number", "_part", "_repr", "_hash", ) _primal = False _dual = True def __new__(cls, *args, **kw): """Create a new Coargument.""" if args[0] and is_primal(args[0]): raise ValueError( "ufl.Coargument takes in a dual space! If you want to define an argument " "in the primal space you should use ufl.Argument." ) return super().__new__(cls) def __init__(self, function_space, number, part=None): """Initialise.""" BaseArgument.__init__(self, function_space, number, part) BaseForm.__init__(self) self.ufl_operands = () self._hash = None self._repr = "Coargument(%s, %s, %s)" % ( repr(self._ufl_function_space), repr(self._number), repr(self._part), ) def arguments(self, outer_form=None): """Return all Argument objects found in form.""" if self._arguments is None: self._analyze_form_arguments(outer_form=outer_form) return self._arguments def _analyze_form_arguments(self, outer_form=None): """Analyze which Argument and Coefficient objects can be found in the form.""" # Define canonical numbering of arguments and coefficients self._coefficients = () # Coarguments map from V* to V*, i.e. V* -> V*, or equivalently V* x V -> R. # So they have one argument in the primal space and one in the dual space. # However, when they are composed with linear forms with dual # arguments, such as BaseFormOperators, the primal argument is # discarded when analysing the argument as Coarguments. if not outer_form: self._arguments = (Argument(self.ufl_function_space().dual(), 0), self) else: self._arguments = (self,) def ufl_domain(self): """Return the UFL domain.""" return BaseArgument.ufl_domain(self) def equals(self, other): """Check equality.""" if type(other) is not Coargument: return False if self is other: return True return ( self._ufl_function_space == other._ufl_function_space and self._number == other._number and self._part == other._part ) def __hash__(self): """Hash.""" return hash(("Coargument", hash(self._ufl_function_space), self._number, self._part)) # --- Helper functions for pretty syntax --- def TestFunction(function_space, part=None): """UFL value: Create a test function argument to a form.""" return Argument(function_space, 0, part) def TrialFunction(function_space, part=None): """UFL value: Create a trial function argument to a form.""" return Argument(function_space, 1, part) # --- Helper functions for creating subfunctions on mixed elements --- def Arguments(function_space, number): """Create an Argument in a mixed space. Returns a tuple with the function components corresponding to the subelements. """ if isinstance(function_space, MixedFunctionSpace): return [ Argument(function_space.ufl_sub_space(i), number, i) for i in range(function_space.num_sub_spaces()) ] else: return split(Argument(function_space, number)) def TestFunctions(function_space): """Create a TestFunction in a mixed space. Returns a tuple with the function components corresponding to the subelements. """ return Arguments(function_space, 0) def TrialFunctions(function_space): """Create a TrialFunction in a mixed space. Returns a tuple with the function components corresponding to the subelements. """ return Arguments(function_space, 1) ufl-2024.2.0/ufl/averaging.py000066400000000000000000000041131470142567200156410ustar00rootroot00000000000000"""Averaging operations.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.constantvalue import ConstantValue from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type @ufl_type( inherit_shape_from_operand=0, inherit_indices_from_operand=0, num_ops=1, is_evaluation=True ) class CellAvg(Operator): """Cell average.""" __slots__ = () def __new__(cls, f): """Create a new CellAvg.""" if isinstance(f, ConstantValue): return f return super(CellAvg, cls).__new__(cls) def __init__(self, f): """Initialise.""" Operator.__init__(self, (f,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape def evaluate(self, x, mapping, component, index_values): """Performs an approximate symbolic evaluation, since we don't have a cell.""" return self.ufl_operands[0].evaluate(x, mapping, component, index_values) def __str__(self): """Format as a string.""" return f"cell_avg({self.ufl_operands[0]})" @ufl_type( inherit_shape_from_operand=0, inherit_indices_from_operand=0, num_ops=1, is_evaluation=True ) class FacetAvg(Operator): """Facet average.""" __slots__ = () def __new__(cls, f): """Create a new FacetAvg.""" if isinstance(f, ConstantValue): return f return super(FacetAvg, cls).__new__(cls) def __init__(self, f): """Initialise.""" Operator.__init__(self, (f,)) @property def ufl_shape(self): """Return the UFL shape.""" return self.ufl_operands[0].ufl_shape def evaluate(self, x, mapping, component, index_values): """Performs an approximate symbolic evaluation, since we dont have a cell.""" return self.ufl_operands[0].evaluate(x, mapping, component, index_values) def __str__(self): """Format as a string.""" return f"facet_avg({self.ufl_operands[0]})" ufl-2024.2.0/ufl/cell.py000066400000000000000000000374241470142567200146300ustar00rootroot00000000000000"""Types for representing a cell.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from __future__ import annotations import functools import numbers import typing import weakref from abc import abstractmethod from ufl.core.ufl_type import UFLObject __all_classes__ = ["AbstractCell", "Cell", "TensorProductCell"] class AbstractCell(UFLObject): """A base class for all cells.""" @abstractmethod def topological_dimension(self) -> int: """Return the dimension of the topology of this cell.""" @abstractmethod def is_simplex(self) -> bool: """Return True if this is a simplex cell.""" @abstractmethod def has_simplex_facets(self) -> bool: """Return True if all the facets of this cell are simplex cells.""" @abstractmethod def _lt(self, other) -> bool: """Less than operator. Define an arbitrarily chosen but fixed sort order for all instances of this type with the same dimensions. """ @abstractmethod def num_sub_entities(self, dim: int) -> int: """Get the number of sub-entities of the given dimension.""" @abstractmethod def sub_entities(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the sub-entities of the given dimension.""" @abstractmethod def sub_entity_types(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the unique sub-entity types of the given dimension.""" @abstractmethod def cellname(self) -> str: """Return the cellname of the cell.""" @abstractmethod def reconstruct(self, **kwargs: typing.Any) -> Cell: """Reconstruct this cell, overwriting properties by those in kwargs.""" def __lt__(self, other: AbstractCell) -> bool: """Define an arbitrarily chosen but fixed sort order for all cells.""" if type(self) is type(other): s = self.topological_dimension() o = other.topological_dimension() if s != o: return s < o return self._lt(other) else: if type(self).__name__ == type(other).__name__: raise ValueError("Cannot order cell types with the same name") return type(self).__name__ < type(other).__name__ def num_vertices(self) -> int: """Get the number of vertices.""" return self.num_sub_entities(0) def num_edges(self) -> int: """Get the number of edges.""" return self.num_sub_entities(1) def num_faces(self) -> int: """Get the number of faces.""" return self.num_sub_entities(2) def num_facets(self) -> int: """Get the number of facets. Facets are entities of dimension tdim-1. """ tdim = self.topological_dimension() return self.num_sub_entities(tdim - 1) def num_ridges(self) -> int: """Get the number of ridges. Ridges are entities of dimension tdim-2. """ tdim = self.topological_dimension() return self.num_sub_entities(tdim - 2) def num_peaks(self) -> int: """Get the number of peaks. Peaks are entities of dimension tdim-3. """ tdim = self.topological_dimension() return self.num_sub_entities(tdim - 3) def vertices(self) -> typing.Tuple[AbstractCell, ...]: """Get the vertices.""" return self.sub_entities(0) def edges(self) -> typing.Tuple[AbstractCell, ...]: """Get the edges.""" return self.sub_entities(1) def faces(self) -> typing.Tuple[AbstractCell, ...]: """Get the faces.""" return self.sub_entities(2) def facets(self) -> typing.Tuple[AbstractCell, ...]: """Get the facets. Facets are entities of dimension tdim-1. """ tdim = self.topological_dimension() return self.sub_entities(tdim - 1) def ridges(self) -> typing.Tuple[AbstractCell, ...]: """Get the ridges. Ridges are entities of dimension tdim-2. """ tdim = self.topological_dimension() return self.sub_entities(tdim - 2) def peaks(self) -> typing.Tuple[AbstractCell, ...]: """Get the peaks. Peaks are entities of dimension tdim-3. """ tdim = self.topological_dimension() return self.sub_entities(tdim - 3) def vertex_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique vertices types.""" return self.sub_entity_types(0) def edge_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique edge types.""" return self.sub_entity_types(1) def face_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique face types.""" return self.sub_entity_types(2) def facet_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique facet types. Facets are entities of dimension tdim-1. """ tdim = self.topological_dimension() return self.sub_entity_types(tdim - 1) def ridge_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique ridge types. Ridges are entities of dimension tdim-2. """ tdim = self.topological_dimension() return self.sub_entity_types(tdim - 2) def peak_types(self) -> typing.Tuple[AbstractCell, ...]: """Get the unique peak types. Peaks are entities of dimension tdim-3. """ tdim = self.topological_dimension() return self.sub_entity_types(tdim - 3) _sub_entity_celltypes = { "vertex": [("vertex",)], "interval": [tuple("vertex" for i in range(2)), ("interval",)], "triangle": [ tuple("vertex" for i in range(3)), tuple("interval" for i in range(3)), ("triangle",), ], "quadrilateral": [ tuple("vertex" for i in range(4)), tuple("interval" for i in range(4)), ("quadrilateral",), ], "tetrahedron": [ tuple("vertex" for i in range(4)), tuple("interval" for i in range(6)), tuple("triangle" for i in range(4)), ("tetrahedron",), ], "hexahedron": [ tuple("vertex" for i in range(8)), tuple("interval" for i in range(12)), tuple("quadrilateral" for i in range(6)), ("hexahedron",), ], "prism": [ tuple("vertex" for i in range(6)), tuple("interval" for i in range(9)), ("triangle", "quadrilateral", "quadrilateral", "quadrilateral", "triangle"), ("prism",), ], "pyramid": [ tuple("vertex" for i in range(5)), tuple("interval" for i in range(8)), ("quadrilateral", "triangle", "triangle", "triangle", "triangle"), ("pyramid",), ], "pentatope": [ tuple("vertex" for i in range(5)), tuple("interval" for i in range(10)), tuple("triangle" for i in range(10)), tuple("tetrahedron" for i in range(5)), ("pentatope",), ], "tesseract": [ tuple("vertex" for i in range(16)), tuple("interval" for i in range(32)), tuple("quadrilateral" for i in range(24)), tuple("hexahedron" for i in range(8)), ("tesseract",), ], } class Cell(AbstractCell): """Representation of a named finite element cell with known structure.""" __slots__ = ( "_cellname", "_tdim", "_num_cell_entities", "_sub_entity_types", "_sub_entities", "_sub_entity_types", ) def __init__(self, cellname: str): """Initialise. Args: cellname: Name of the cell """ if cellname not in _sub_entity_celltypes: raise ValueError(f"Unsupported cell type: {cellname}") self._sub_entity_celltypes = _sub_entity_celltypes[cellname] self._cellname = cellname self._tdim = len(self._sub_entity_celltypes) - 1 self._num_cell_entities = [len(i) for i in self._sub_entity_celltypes] self._sub_entities = [ tuple(Cell(t) for t in se_types) for se_types in self._sub_entity_celltypes[:-1] ] self._sub_entity_types = [tuple(set(i)) for i in self._sub_entities] self._sub_entities.append((weakref.proxy(self),)) self._sub_entity_types.append((weakref.proxy(self),)) if not isinstance(self._tdim, numbers.Integral): raise ValueError("Expecting integer topological_dimension.") def topological_dimension(self) -> int: """Return the dimension of the topology of this cell.""" return self._tdim def is_simplex(self) -> bool: """Return True if this is a simplex cell.""" return self._cellname in ["vertex", "interval", "triangle", "tetrahedron"] def has_simplex_facets(self) -> bool: """Return True if all the facets of this cell are simplex cells.""" return self._cellname in ["interval", "triangle", "quadrilateral", "tetrahedron"] def num_sub_entities(self, dim: int) -> int: """Get the number of sub-entities of the given dimension.""" if dim < 0: return 0 try: return self._num_cell_entities[dim] except IndexError: return 0 def sub_entities(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the sub-entities of the given dimension.""" if dim < 0: return () try: return self._sub_entities[dim] except IndexError: return () def sub_entity_types(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the unique sub-entity types of the given dimension.""" if dim < 0: return () try: return self._sub_entity_types[dim] except IndexError: return () def _lt(self, other) -> bool: return self._cellname < other._cellname def cellname(self) -> str: """Return the cellname of the cell.""" return self._cellname def __str__(self) -> str: """Format as a string.""" return self._cellname def __repr__(self) -> str: """Representation.""" return self._cellname def _ufl_hash_data_(self) -> typing.Hashable: """UFL hash data.""" return (self._cellname,) def reconstruct(self, **kwargs: typing.Any) -> Cell: """Reconstruct this cell, overwriting properties by those in kwargs.""" for key, value in kwargs.items(): raise TypeError(f"reconstruct() got unexpected keyword argument '{key}'") return Cell(self._cellname) class TensorProductCell(AbstractCell): """Tensor product cell.""" __slots__ = ("_cells", "_tdim") def __init__(self, *cells: Cell): """Initialise. Args: cells: Cells to take the tensor product of """ self._cells = tuple(as_cell(cell) for cell in cells) self._tdim = sum([cell.topological_dimension() for cell in self._cells]) if not isinstance(self._tdim, numbers.Integral): raise ValueError("Expecting integer topological_dimension.") def sub_cells(self) -> typing.List[AbstractCell]: """Return list of cell factors.""" return self._cells def topological_dimension(self) -> int: """Return the dimension of the topology of this cell.""" return self._tdim def is_simplex(self) -> bool: """Return True if this is a simplex cell.""" if len(self._cells) == 1: return self._cells[0].is_simplex() return False def has_simplex_facets(self) -> bool: """Return True if all the facets of this cell are simplex cells.""" if len(self._cells) == 1: return self._cells[0].has_simplex_facets() if self._tdim == 1: return True return False def num_sub_entities(self, dim: int) -> int: """Get the number of sub-entities of the given dimension.""" if dim < 0 or dim > self._tdim: return 0 if dim == 0: return functools.reduce(lambda x, y: x * y, [c.num_vertices() for c in self._cells]) if dim == self._tdim - 1: # Note: This is not the number of facets that the cell has, # but I'm leaving it here for now to not change past # behaviour return sum(c.num_facets() for c in self._cells if c.topological_dimension() > 0) if dim == self._tdim: return 1 raise NotImplementedError(f"TensorProductCell.num_sub_entities({dim}) is not implemented.") def sub_entities(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the sub-entities of the given dimension.""" if dim < 0 or dim > self._tdim: return [] if dim == 0: return [Cell("vertex") for i in range(self.num_sub_entities(0))] if dim == self._tdim: return [self] raise NotImplementedError(f"TensorProductCell.sub_entities({dim}) is not implemented.") def sub_entity_types(self, dim: int) -> typing.Tuple[AbstractCell, ...]: """Get the unique sub-entity types of the given dimension.""" if dim < 0 or dim > self._tdim: return [] if dim == 0: return [Cell("vertex")] if dim == self._tdim: return [self] raise NotImplementedError(f"TensorProductCell.sub_entities({dim}) is not implemented.") def _lt(self, other) -> bool: return self._ufl_hash_data_() < other._ufl_hash_data_() def cellname(self) -> str: """Return the cellname of the cell.""" return " * ".join([cell.cellname() for cell in self._cells]) def __str__(self) -> str: """Format as a string.""" return "TensorProductCell(" + ", ".join(f"{c!r}" for c in self._cells) + ")" def __repr__(self) -> str: """Representation.""" return str(self) def _ufl_hash_data_(self) -> typing.Hashable: """UFL hash data.""" return tuple(c._ufl_hash_data_() for c in self._cells) def reconstruct(self, **kwargs: typing.Any) -> Cell: """Reconstruct this cell, overwriting properties by those in kwargs.""" for key, value in kwargs.items(): raise TypeError(f"reconstruct() got unexpected keyword argument '{key}'") return TensorProductCell(*self._cells) def simplex(topological_dimension: int): """Return a simplex cell of the given dimension.""" if topological_dimension == 0: return Cell("vertex") if topological_dimension == 1: return Cell("interval") if topological_dimension == 2: return Cell("triangle") if topological_dimension == 3: return Cell("tetrahedron") if topological_dimension == 4: return Cell("pentatope") raise ValueError(f"Unsupported topological dimension for simplex: {topological_dimension}") def hypercube(topological_dimension: int): """Return a hypercube cell of the given dimension.""" if topological_dimension == 0: return Cell("vertex") if topological_dimension == 1: return Cell("interval") if topological_dimension == 2: return Cell("quadrilateral") if topological_dimension == 3: return Cell("hexahedron") if topological_dimension == 4: return Cell("tesseract") raise ValueError(f"Unsupported topological dimension for hypercube: {topological_dimension}") def as_cell(cell: typing.Union[AbstractCell, str, typing.Tuple[AbstractCell, ...]]) -> AbstractCell: """Convert any valid object to a Cell or return cell if it is already a Cell. Allows an already valid cell, a known cellname string, or a tuple of cells for a product cell. """ if isinstance(cell, AbstractCell): return cell elif isinstance(cell, str): return Cell(cell) elif isinstance(cell, tuple): return TensorProductCell(cell) else: raise ValueError(f"Invalid cell {cell}.") ufl-2024.2.0/ufl/checks.py000066400000000000000000000041761470142567200151470ustar00rootroot00000000000000"""Utility functions for checking properties of expressions.""" # Copyright (C) 2013-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009 from ufl.core.expr import Expr from ufl.core.terminal import FormArgument from ufl.corealg.traversal import traverse_unique_terminals from ufl.geometry import GeometricQuantity from ufl.sobolevspace import H1 def is_python_scalar(expression): """Return True iff expression is of a Python scalar type.""" return isinstance(expression, (int, float, complex)) def is_ufl_scalar(expression): """Return True iff expression is scalar-valued, but possibly containing free indices.""" return isinstance(expression, Expr) and not expression.ufl_shape def is_true_ufl_scalar(expression): """Return True iff expression is scalar-valued, with no free indices.""" return isinstance(expression, Expr) and not ( expression.ufl_shape or expression.ufl_free_indices ) def is_cellwise_constant(expr): """Return whether expression is constant over a single cell.""" # TODO: Implement more accurately considering e.g. derivatives? return all(e.is_cellwise_constant() for e in traverse_unique_terminals(expr)) def is_scalar_constant_expression(expr): """Check if an expression is a globally constant scalar expression.""" if is_python_scalar(expr): return True if expr.ufl_shape: return False # TODO: This does not consider gradients of coefficients, so false # negatives are possible. for e in traverse_unique_terminals(expr): # Return False if any single terminal is not constant if isinstance(e, FormArgument): # Accept only globally constant Arguments and Coefficients if e.ufl_element().embedded_superdegree > 0 or e.ufl_element() not in H1: return False elif isinstance(e, GeometricQuantity): # Reject all geometric quantities, they all vary over # cells return False # All terminals passed constant check return True ufl-2024.2.0/ufl/classes.py000066400000000000000000000072171470142567200153430ustar00rootroot00000000000000"""Classes. This file is useful for external code like tests and form compilers, since it enables the syntax "from ufl.classes import CellFacetooBar" for getting implementation details not exposed through the default ufl namespace. It also contains functionality used by algorithms for dealing with groups of classes, and for mapping types to different handler functions. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009. # Modified by Kristian B. Oelgaard, 2011 # Modified by Andrew T. T. McRae, 2014 # This will be populated part by part below __all__ = [] # Import all submodules, triggering execution of the ufl_type class # decorator for each Expr class. import ufl.algebra import ufl.argument import ufl.averaging import ufl.cell import ufl.coefficient import ufl.conditional import ufl.constantvalue import ufl.core.expr import ufl.core.multiindex import ufl.core.operator import ufl.core.terminal import ufl.differentiation import ufl.domain import ufl.equation import ufl.exprcontainers import ufl.finiteelement import ufl.form import ufl.functionspace import ufl.geometry import ufl.indexed import ufl.indexsum import ufl.integral import ufl.mathfunctions import ufl.measure import ufl.pullback import ufl.referencevalue import ufl.restriction import ufl.sobolevspace import ufl.tensoralgebra import ufl.tensors import ufl.variable from ufl import exproperators as __exproperators # Collect all classes in sets automatically classified by some properties all_ufl_classes = set(ufl.core.expr.Expr._ufl_all_classes_) abstract_classes = set(c for c in all_ufl_classes if c._ufl_is_abstract_) ufl_classes = set(c for c in all_ufl_classes if not c._ufl_is_abstract_) terminal_classes = set(c for c in all_ufl_classes if c._ufl_is_terminal_) nonterminal_classes = set(c for c in all_ufl_classes if not c._ufl_is_terminal_) __all__ += [ "all_ufl_classes", "abstract_classes", "ufl_classes", "terminal_classes", "nonterminal_classes", "__exproperators", ] def populate_namespace_with_expr_classes(namespace): """Export all Expr subclasses into the namespace under their natural name.""" names = [] classes = ufl.core.expr.Expr._ufl_all_classes_ for cls in classes: class_name = cls.__name__ namespace[class_name] = cls names.append(class_name) return names __all__ += populate_namespace_with_expr_classes(locals()) # Semi-automated imports of non-expr classes: def populate_namespace_with_module_classes(mod, loc): """Export the classes that submodules list in __all_classes__.""" names = mod.__all_classes__ for name in names: loc[name] = getattr(mod, name) return names __all__ += populate_namespace_with_module_classes(ufl.cell, locals()) __all__ += populate_namespace_with_module_classes(ufl.finiteelement, locals()) __all__ += populate_namespace_with_module_classes(ufl.domain, locals()) __all__ += populate_namespace_with_module_classes(ufl.functionspace, locals()) __all__ += populate_namespace_with_module_classes(ufl.core.multiindex, locals()) __all__ += populate_namespace_with_module_classes(ufl.argument, locals()) __all__ += populate_namespace_with_module_classes(ufl.measure, locals()) __all__ += populate_namespace_with_module_classes(ufl.integral, locals()) __all__ += populate_namespace_with_module_classes(ufl.form, locals()) __all__ += populate_namespace_with_module_classes(ufl.equation, locals()) __all__ += populate_namespace_with_module_classes(ufl.pullback, locals()) __all__ += populate_namespace_with_module_classes(ufl.sobolevspace, locals()) ufl-2024.2.0/ufl/coefficient.py000066400000000000000000000161401470142567200161570ustar00rootroot00000000000000"""This module defines the Coefficient class and a number of related classes, including Constant.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009. # Modified by Massimiliano Leoni, 2016. # Modified by Cecile Daversin-Catty, 2018. # Modified by Ignacia Fierro-Piccardo 2023. from ufl.argument import Argument from ufl.core.terminal import FormArgument from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual, is_primal from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace, MixedFunctionSpace from ufl.split_functions import split from ufl.utils.counted import Counted # --- The Coefficient class represents a coefficient in a form --- class BaseCoefficient(Counted): """UFL form argument type: Parent Representation of a form coefficient.""" # Slots are disabled here because they cause trouble in PyDOLFIN # multiple inheritance pattern: # __slots__ = ("_count", "_ufl_function_space", "_repr", "_ufl_shape") _ufl_noslots_ = True __slots__ = () _ufl_is_abstract_ = True def __getnewargs__(self): """Get new args.""" return (self._ufl_function_space, self._count) def __init__(self, function_space, count=None): """Initalise.""" Counted.__init__(self, count, Coefficient) if not isinstance(function_space, AbstractFunctionSpace): raise ValueError("Expecting a FunctionSpace.") self._ufl_function_space = function_space self._ufl_shape = function_space.value_shape self._repr = "BaseCoefficient(%s, %s)" % (repr(self._ufl_function_space), repr(self._count)) @property def ufl_shape(self): """Return the associated UFL shape.""" return self._ufl_shape def ufl_function_space(self): """Get the function space of this coefficient.""" return self._ufl_function_space def ufl_domain(self): """Shortcut to get the domain of the function space of this coefficient.""" return self._ufl_function_space.ufl_domain() def ufl_element(self): """Shortcut to get the finite element of the function space of this coefficient.""" return self._ufl_function_space.ufl_element() def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" return self.ufl_element().is_cellwise_constant() def ufl_domains(self): """Return tuple of domains related to this terminal object.""" return self._ufl_function_space.ufl_domains() def _ufl_signature_data_(self, renumbering): """Signature data. Signature data for form arguments depend on the global numbering of the form arguments and domains. """ count = renumbering[self] fsdata = self._ufl_function_space._ufl_signature_data_(renumbering) return ("Coefficient", count, fsdata) def __str__(self): """Format as a string.""" return f"w_{self._count}" def __repr__(self): """Representation.""" return self._repr def __eq__(self, other): """Check equality.""" if not isinstance(other, BaseCoefficient): return False if self is other: return True return self._count == other._count and self._ufl_function_space == other._ufl_function_space @ufl_type() class Cofunction(BaseCoefficient, BaseForm): """UFL form argument type: Representation of a form coefficient from a dual space.""" __slots__ = ( "_count", "_counted_class", "_arguments", "_coefficients", "_ufl_function_space", "ufl_operands", "_repr", "_ufl_shape", "_hash", ) _primal = False _dual = True _ufl_is_terminal_ = True __eq__ = BaseForm.__eq__ def __new__(cls, *args, **kw): """Create a new Cofunction.""" if args[0] and is_primal(args[0]): raise ValueError( "ufl.Cofunction takes in a dual space. If you want to define a coefficient " "in the primal space you should use ufl.Coefficient." ) return super().__new__(cls) def __init__(self, function_space, count=None): """Initialise.""" BaseCoefficient.__init__(self, function_space, count) BaseForm.__init__(self) self.ufl_operands = () self._hash = None self._repr = "Cofunction(%s, %s)" % (repr(self._ufl_function_space), repr(self._count)) def equals(self, other): """Check equality.""" if type(other) is not Cofunction: return False if self is other: return True return self._count == other._count and self._ufl_function_space == other._ufl_function_space def __hash__(self): """Hash.""" return hash(("Cofunction", hash(self._ufl_function_space), self._count)) def _analyze_form_arguments(self): """Analyze which Argument and Coefficient objects can be found in the form.""" # Define canonical numbering of arguments and coefficients # Cofunctions have one argument in primal space as they map from V to R. self._arguments = (Argument(self._ufl_function_space.dual(), 0),) self._coefficients = (self,) @ufl_type() class Coefficient(FormArgument, BaseCoefficient): """UFL form argument type: Representation of a form coefficient.""" _ufl_noslots_ = True _primal = True _dual = False __getnewargs__ = BaseCoefficient.__getnewargs__ __str__ = BaseCoefficient.__str__ _ufl_signature_data_ = BaseCoefficient._ufl_signature_data_ def __new__(cls, *args, **kw): """Create a new Coefficient.""" if args[0] and is_dual(args[0]): return Cofunction(*args, **kw) return super().__new__(cls) def __init__(self, function_space, count=None): """Initialise.""" FormArgument.__init__(self) BaseCoefficient.__init__(self, function_space, count) self._repr = "Coefficient(%s, %s)" % (repr(self._ufl_function_space), repr(self._count)) def ufl_domains(self): """Get the UFL domains.""" return BaseCoefficient.ufl_domains(self) def __eq__(self, other): """Check equality.""" if not isinstance(other, Coefficient): return False if self is other: return True return self._count == other._count and self._ufl_function_space == other._ufl_function_space def __repr__(self): """Representation.""" return self._repr # --- Helper functions for subfunctions on mixed elements --- def Coefficients(function_space): """Create a Coefficient in a mixed space. Returns a tuple with the function components corresponding to the subelements. """ if isinstance(function_space, MixedFunctionSpace): return [ Coefficient(fs) if is_primal(fs) else Cofunction(fs) for fs in function_space.num_sub_spaces() ] else: return split(Coefficient(function_space)) ufl-2024.2.0/ufl/compound_expressions.py000066400000000000000000000413101470142567200201640ustar00rootroot00000000000000"""Support for compound expressions as equivalent representations using basic operators.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010 from ufl.constantvalue import Zero, zero from ufl.core.multiindex import Index, indices from ufl.operators import sqrt from ufl.tensors import as_matrix, as_tensor, as_vector # Note: To avoid typing errors, the expressions for cofactor and # deviatoric parts below were created with the script # tensoralgebrastrings.py under sandbox/scripts/ # Note: Avoiding or delaying application of these horrible expressions # would be a major improvement to UFL and the form compiler toolchain. # It could easily be a moderate to major undertaking to get rid of # though. def cross_expr(a, b): """Symbolic cross product.""" assert len(a) == 3 assert len(b) == 3 def c(i, j): return a[i] * b[j] - a[j] * b[i] return as_vector((c(1, 2), c(2, 0), c(0, 1))) def generic_pseudo_determinant_expr(A): """Compute the pseudo-determinant of A: sqrt(det(A.T*A)).""" i, j, k = indices(3) ATA = as_tensor(A[k, i] * A[k, j], (i, j)) return sqrt(determinant_expr(ATA)) def pseudo_determinant_expr(A): """Compute the pseudo-determinant of A.""" m, n = A.ufl_shape if n == 1: # Special case 1xm for simpler expression i = Index() return sqrt(A[i, 0] * A[i, 0]) elif n == 2 and m == 3: # Special case 2x3 for simpler expression c = cross_expr(A[:, 0], A[:, 1]) i = Index() return sqrt(c[i] * c[i]) else: # Generic formulation based on A.T*A return generic_pseudo_determinant_expr(A) def generic_pseudo_inverse_expr(A): """Compute the Penrose-Moore pseudo-inverse of A: (A.T*A)^-1 * A.T.""" i, j, k = indices(3) ATA = as_tensor(A[k, i] * A[k, j], (i, j)) ATAinv = inverse_expr(ATA) q, r, s = indices(3) return as_tensor(ATAinv[r, q] * A[s, q], (r, s)) def pseudo_inverse_expr(A): """Compute the Penrose-Moore pseudo-inverse of A: (A.T*A)^-1 * A.T.""" m, n = A.ufl_shape if n == 1: # Simpler special case for 1d i, j, k = indices(3) return as_tensor(A[i, j], (j, i)) / (A[k, 0] * A[k, 0]) else: # Generic formulation return generic_pseudo_inverse_expr(A) def determinant_expr(A): """Compute the (pseudo-)determinant of A.""" sh = A.ufl_shape if isinstance(A, Zero): return zero() elif sh == (): return A elif sh[0] == sh[1]: if sh[0] == 1: return A[0, 0] elif sh[0] == 2: return determinant_expr_2x2(A) elif sh[0] == 3: return determinant_expr_3x3(A) else: return determinant_expr_nxn(A) else: return pseudo_determinant_expr(A) # TODO: Implement generally for all dimensions? raise ValueError(f"determinant_expr not implemented for shape {sh}.") def _det_2x2(B, i, j, k, l): # noqa: E741 """Determinant of a 2 by 2 matrix.""" return B[i, k] * B[j, l] - B[i, l] * B[j, k] def determinant_expr_2x2(B): """Determinant of a 2 by 2 matrix.""" return _det_2x2(B, 0, 1, 0, 1) def determinant_expr_3x3(A): """Determinant of a 3 by 3 matrix.""" return codeterminant_expr_nxn(A, [0, 1, 2], [0, 1, 2]) def determinant_expr_nxn(A): """Determinant of a n by n matrix.""" nrow, ncol = A.ufl_shape assert nrow == ncol return codeterminant_expr_nxn(A, list(range(nrow)), list(range(ncol))) def codeterminant_expr_nxn(A, rows, cols): """Determinant of a n by n matrix.""" if len(rows) == 2: return _det_2x2(A, rows[0], rows[1], cols[0], cols[1]) codet = 0.0 r = rows[0] subrows = rows[1:] for i, c in enumerate(cols): subcols = cols[:i] + cols[i + 1 :] codet += (-1) ** i * A[r, c] * codeterminant_expr_nxn(A, subrows, subcols) return codet def inverse_expr(A): """Compute the inverse of A.""" sh = A.ufl_shape if sh == (): return 1.0 / A elif sh[0] == sh[1]: if sh[0] == 1: return as_tensor(((1.0 / A[0, 0],),)) else: return adj_expr(A) / determinant_expr(A) else: return pseudo_inverse_expr(A) def adj_expr(A): """Adjoint of a matrix.""" sh = A.ufl_shape if sh[0] != sh[1]: raise ValueError("Expecting square matrix.") if sh[0] == 2: return adj_expr_2x2(A) elif sh[0] == 3: return adj_expr_3x3(A) elif sh[0] == 4: return adj_expr_4x4(A) raise ValueError(f"adj_expr not implemented for dimension {sh[0]}.") def adj_expr_2x2(A): """Adjoint of a 2 by 2 matrix.""" return as_matrix([[A[1, 1], -A[0, 1]], [-A[1, 0], A[0, 0]]]) def adj_expr_3x3(A): """Adjoint of a 3 by 3 matrix.""" return as_matrix( [ [ A[2, 2] * A[1, 1] - A[1, 2] * A[2, 1], -A[0, 1] * A[2, 2] + A[0, 2] * A[2, 1], A[0, 1] * A[1, 2] - A[0, 2] * A[1, 1], ], [ -A[2, 2] * A[1, 0] + A[1, 2] * A[2, 0], -A[0, 2] * A[2, 0] + A[2, 2] * A[0, 0], A[0, 2] * A[1, 0] - A[1, 2] * A[0, 0], ], [ A[1, 0] * A[2, 1] - A[2, 0] * A[1, 1], A[0, 1] * A[2, 0] - A[0, 0] * A[2, 1], A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0], ], ] ) def adj_expr_4x4(A): """Adjoint of a 4 by 4 matrix.""" return as_matrix( [ [ -A[3, 3] * A[2, 1] * A[1, 2] + A[1, 2] * A[3, 1] * A[2, 3] + A[1, 1] * A[3, 3] * A[2, 2] - A[3, 1] * A[2, 2] * A[1, 3] + A[2, 1] * A[1, 3] * A[3, 2] - A[1, 1] * A[3, 2] * A[2, 3], -A[3, 1] * A[0, 2] * A[2, 3] + A[0, 1] * A[3, 2] * A[2, 3] - A[0, 3] * A[2, 1] * A[3, 2] + A[3, 3] * A[2, 1] * A[0, 2] - A[3, 3] * A[0, 1] * A[2, 2] + A[0, 3] * A[3, 1] * A[2, 2], A[3, 1] * A[1, 3] * A[0, 2] + A[1, 1] * A[0, 3] * A[3, 2] - A[0, 3] * A[1, 2] * A[3, 1] - A[0, 1] * A[1, 3] * A[3, 2] + A[3, 3] * A[1, 2] * A[0, 1] - A[1, 1] * A[3, 3] * A[0, 2], A[1, 1] * A[0, 2] * A[2, 3] - A[2, 1] * A[1, 3] * A[0, 2] + A[0, 3] * A[2, 1] * A[1, 2] - A[1, 2] * A[0, 1] * A[2, 3] - A[1, 1] * A[0, 3] * A[2, 2] + A[0, 1] * A[2, 2] * A[1, 3], ], [ A[3, 3] * A[1, 2] * A[2, 0] - A[3, 0] * A[1, 2] * A[2, 3] + A[1, 0] * A[3, 2] * A[2, 3] - A[3, 3] * A[1, 0] * A[2, 2] - A[1, 3] * A[3, 2] * A[2, 0] + A[3, 0] * A[2, 2] * A[1, 3], A[0, 3] * A[3, 2] * A[2, 0] - A[0, 3] * A[3, 0] * A[2, 2] + A[3, 3] * A[0, 0] * A[2, 2] + A[3, 0] * A[0, 2] * A[2, 3] - A[0, 0] * A[3, 2] * A[2, 3] - A[3, 3] * A[0, 2] * A[2, 0], -A[3, 3] * A[0, 0] * A[1, 2] + A[0, 0] * A[1, 3] * A[3, 2] - A[3, 0] * A[1, 3] * A[0, 2] + A[3, 3] * A[1, 0] * A[0, 2] + A[0, 3] * A[3, 0] * A[1, 2] - A[0, 3] * A[1, 0] * A[3, 2], A[0, 3] * A[1, 0] * A[2, 2] + A[1, 3] * A[0, 2] * A[2, 0] - A[0, 0] * A[2, 2] * A[1, 3] - A[0, 3] * A[1, 2] * A[2, 0] + A[0, 0] * A[1, 2] * A[2, 3] - A[1, 0] * A[0, 2] * A[2, 3], ], [ A[3, 1] * A[1, 3] * A[2, 0] + A[3, 3] * A[2, 1] * A[1, 0] + A[1, 1] * A[3, 0] * A[2, 3] - A[1, 0] * A[3, 1] * A[2, 3] - A[3, 0] * A[2, 1] * A[1, 3] - A[1, 1] * A[3, 3] * A[2, 0], A[3, 3] * A[0, 1] * A[2, 0] - A[3, 3] * A[0, 0] * A[2, 1] - A[0, 3] * A[3, 1] * A[2, 0] - A[3, 0] * A[0, 1] * A[2, 3] + A[0, 0] * A[3, 1] * A[2, 3] + A[0, 3] * A[3, 0] * A[2, 1], -A[0, 0] * A[3, 1] * A[1, 3] + A[0, 3] * A[1, 0] * A[3, 1] - A[3, 3] * A[1, 0] * A[0, 1] + A[1, 1] * A[3, 3] * A[0, 0] - A[1, 1] * A[0, 3] * A[3, 0] + A[3, 0] * A[0, 1] * A[1, 3], A[0, 0] * A[2, 1] * A[1, 3] + A[1, 0] * A[0, 1] * A[2, 3] - A[0, 3] * A[2, 1] * A[1, 0] + A[1, 1] * A[0, 3] * A[2, 0] - A[1, 1] * A[0, 0] * A[2, 3] - A[0, 1] * A[1, 3] * A[2, 0], ], [ -A[1, 2] * A[3, 1] * A[2, 0] - A[2, 1] * A[1, 0] * A[3, 2] + A[3, 0] * A[2, 1] * A[1, 2] - A[1, 1] * A[3, 0] * A[2, 2] + A[1, 0] * A[3, 1] * A[2, 2] + A[1, 1] * A[3, 2] * A[2, 0], -A[3, 0] * A[2, 1] * A[0, 2] - A[0, 1] * A[3, 2] * A[2, 0] + A[3, 1] * A[0, 2] * A[2, 0] - A[0, 0] * A[3, 1] * A[2, 2] + A[3, 0] * A[0, 1] * A[2, 2] + A[0, 0] * A[2, 1] * A[3, 2], A[0, 0] * A[1, 2] * A[3, 1] - A[1, 0] * A[3, 1] * A[0, 2] + A[1, 1] * A[3, 0] * A[0, 2] + A[1, 0] * A[0, 1] * A[3, 2] - A[3, 0] * A[1, 2] * A[0, 1] - A[1, 1] * A[0, 0] * A[3, 2], -A[1, 1] * A[0, 2] * A[2, 0] + A[2, 1] * A[1, 0] * A[0, 2] + A[1, 2] * A[0, 1] * A[2, 0] + A[1, 1] * A[0, 0] * A[2, 2] - A[1, 0] * A[0, 1] * A[2, 2] - A[0, 0] * A[2, 1] * A[1, 2], ], ] ) def cofactor_expr(A): """Cofactor of a matrix.""" sh = A.ufl_shape if sh[0] != sh[1]: raise ValueError("Expecting square matrix.") if sh[0] == 2: return cofactor_expr_2x2(A) elif sh[0] == 3: return cofactor_expr_3x3(A) elif sh[0] == 4: return cofactor_expr_4x4(A) raise ValueError(f"cofactor_expr not implemented for dimension {sh[0]}.") def cofactor_expr_2x2(A): """Cofactor of a 2 by 2 matrix.""" return as_matrix([[A[1, 1], -A[1, 0]], [-A[0, 1], A[0, 0]]]) def cofactor_expr_3x3(A): """Cofactor of a 3 by 3 matrix.""" return as_matrix( [ [ A[1, 1] * A[2, 2] - A[2, 1] * A[1, 2], A[2, 0] * A[1, 2] - A[1, 0] * A[2, 2], -A[2, 0] * A[1, 1] + A[1, 0] * A[2, 1], ], [ A[2, 1] * A[0, 2] - A[0, 1] * A[2, 2], A[0, 0] * A[2, 2] - A[2, 0] * A[0, 2], -A[0, 0] * A[2, 1] + A[2, 0] * A[0, 1], ], [ A[0, 1] * A[1, 2] - A[1, 1] * A[0, 2], A[1, 0] * A[0, 2] - A[0, 0] * A[1, 2], -A[1, 0] * A[0, 1] + A[0, 0] * A[1, 1], ], ] ) def cofactor_expr_4x4(A): """Cofactor of a 4 by 4 matrix.""" return as_matrix( [ [ -A[3, 1] * A[2, 2] * A[1, 3] - A[3, 2] * A[2, 3] * A[1, 1] + A[1, 3] * A[3, 2] * A[2, 1] + A[3, 1] * A[2, 3] * A[1, 2] + A[2, 2] * A[1, 1] * A[3, 3] - A[3, 3] * A[2, 1] * A[1, 2], -A[1, 0] * A[2, 2] * A[3, 3] + A[2, 0] * A[3, 3] * A[1, 2] + A[2, 2] * A[1, 3] * A[3, 0] - A[2, 3] * A[3, 0] * A[1, 2] + A[1, 0] * A[3, 2] * A[2, 3] - A[1, 3] * A[3, 2] * A[2, 0], A[1, 0] * A[3, 3] * A[2, 1] + A[2, 3] * A[1, 1] * A[3, 0] - A[2, 0] * A[1, 1] * A[3, 3] - A[1, 3] * A[3, 0] * A[2, 1] - A[1, 0] * A[3, 1] * A[2, 3] + A[3, 1] * A[1, 3] * A[2, 0], A[3, 0] * A[2, 1] * A[1, 2] + A[1, 0] * A[3, 1] * A[2, 2] + A[3, 2] * A[2, 0] * A[1, 1] - A[2, 2] * A[1, 1] * A[3, 0] - A[3, 1] * A[2, 0] * A[1, 2] - A[1, 0] * A[3, 2] * A[2, 1], ], [ A[3, 1] * A[2, 2] * A[0, 3] + A[0, 2] * A[3, 3] * A[2, 1] + A[0, 1] * A[3, 2] * A[2, 3] - A[3, 1] * A[0, 2] * A[2, 3] - A[0, 1] * A[2, 2] * A[3, 3] - A[3, 2] * A[0, 3] * A[2, 1], -A[2, 2] * A[0, 3] * A[3, 0] - A[0, 2] * A[2, 0] * A[3, 3] - A[3, 2] * A[2, 3] * A[0, 0] + A[2, 2] * A[3, 3] * A[0, 0] + A[0, 2] * A[2, 3] * A[3, 0] + A[3, 2] * A[2, 0] * A[0, 3], A[3, 1] * A[2, 3] * A[0, 0] - A[0, 1] * A[2, 3] * A[3, 0] - A[3, 1] * A[2, 0] * A[0, 3] - A[3, 3] * A[0, 0] * A[2, 1] + A[0, 3] * A[3, 0] * A[2, 1] + A[0, 1] * A[2, 0] * A[3, 3], A[3, 2] * A[0, 0] * A[2, 1] - A[0, 2] * A[3, 0] * A[2, 1] + A[0, 1] * A[2, 2] * A[3, 0] + A[3, 1] * A[0, 2] * A[2, 0] - A[0, 1] * A[3, 2] * A[2, 0] - A[3, 1] * A[2, 2] * A[0, 0], ], [ A[3, 1] * A[1, 3] * A[0, 2] - A[0, 2] * A[1, 1] * A[3, 3] - A[3, 1] * A[0, 3] * A[1, 2] + A[3, 2] * A[1, 1] * A[0, 3] + A[0, 1] * A[3, 3] * A[1, 2] - A[0, 1] * A[1, 3] * A[3, 2], A[1, 3] * A[3, 2] * A[0, 0] - A[1, 0] * A[3, 2] * A[0, 3] - A[1, 3] * A[0, 2] * A[3, 0] + A[0, 3] * A[3, 0] * A[1, 2] + A[1, 0] * A[0, 2] * A[3, 3] - A[3, 3] * A[0, 0] * A[1, 2], -A[1, 0] * A[0, 1] * A[3, 3] + A[0, 1] * A[1, 3] * A[3, 0] - A[3, 1] * A[1, 3] * A[0, 0] - A[1, 1] * A[0, 3] * A[3, 0] + A[1, 0] * A[3, 1] * A[0, 3] + A[1, 1] * A[3, 3] * A[0, 0], A[0, 2] * A[1, 1] * A[3, 0] - A[3, 2] * A[1, 1] * A[0, 0] - A[0, 1] * A[3, 0] * A[1, 2] - A[1, 0] * A[3, 1] * A[0, 2] + A[3, 1] * A[0, 0] * A[1, 2] + A[1, 0] * A[0, 1] * A[3, 2], ], [ A[0, 3] * A[2, 1] * A[1, 2] + A[0, 2] * A[2, 3] * A[1, 1] + A[0, 1] * A[2, 2] * A[1, 3] - A[2, 2] * A[1, 1] * A[0, 3] - A[1, 3] * A[0, 2] * A[2, 1] - A[0, 1] * A[2, 3] * A[1, 2], A[1, 0] * A[2, 2] * A[0, 3] + A[1, 3] * A[0, 2] * A[2, 0] - A[1, 0] * A[0, 2] * A[2, 3] - A[2, 0] * A[0, 3] * A[1, 2] - A[2, 2] * A[1, 3] * A[0, 0] + A[2, 3] * A[0, 0] * A[1, 2], -A[0, 1] * A[1, 3] * A[2, 0] + A[2, 0] * A[1, 1] * A[0, 3] + A[1, 3] * A[0, 0] * A[2, 1] - A[1, 0] * A[0, 3] * A[2, 1] + A[1, 0] * A[0, 1] * A[2, 3] - A[2, 3] * A[1, 1] * A[0, 0], A[1, 0] * A[0, 2] * A[2, 1] - A[0, 2] * A[2, 0] * A[1, 1] + A[0, 1] * A[2, 0] * A[1, 2] + A[2, 2] * A[1, 1] * A[0, 0] - A[1, 0] * A[0, 1] * A[2, 2] - A[0, 0] * A[2, 1] * A[1, 2], ], ] ) def deviatoric_expr(A): """Deviatoric of a matrix.""" sh = A.ufl_shape if sh[0] != sh[1]: raise ValueError("Expecting square matrix.") if sh[0] == 2: return deviatoric_expr_2x2(A) elif sh[0] == 3: return deviatoric_expr_3x3(A) raise ValueError(f"deviatoric_expr not implemented for dimension {sh[0]}.") def deviatoric_expr_2x2(A): """Deviatoric of a 2 by 2 matrix.""" return as_matrix( [ [-1.0 / 2 * A[1, 1] + 1.0 / 2 * A[0, 0], A[0, 1]], [A[1, 0], 1.0 / 2 * A[1, 1] - 1.0 / 2 * A[0, 0]], ] ) def deviatoric_expr_3x3(A): """Deviatoric of a 3 by 3 matrix.""" return as_matrix( [ [-1.0 / 3 * A[1, 1] - 1.0 / 3 * A[2, 2] + 2.0 / 3 * A[0, 0], A[0, 1], A[0, 2]], [A[1, 0], 2.0 / 3 * A[1, 1] - 1.0 / 3 * A[2, 2] - 1.0 / 3 * A[0, 0], A[1, 2]], [A[2, 0], A[2, 1], -1.0 / 3 * A[1, 1] + 2.0 / 3 * A[2, 2] - 1.0 / 3 * A[0, 0]], ] ) ufl-2024.2.0/ufl/conditional.py000066400000000000000000000265101470142567200162060ustar00rootroot00000000000000"""This module defines classes for conditional expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import warnings from ufl.checks import is_true_ufl_scalar from ufl.constantvalue import as_ufl from ufl.core.expr import ufl_err_str from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.exprequals import expr_equals from ufl.precedence import parstr # --- Condition classes --- # TODO: Would be nice with some kind of type system to show that this # is a boolean type not a float type @ufl_type(is_abstract=True, is_scalar=True) class Condition(Operator): """Condition.""" __slots__ = () def __init__(self, operands): """Initialise.""" Operator.__init__(self, operands) def __bool__(self): """Convert to a bool.""" # Showing explicit error here to protect against misuse raise ValueError("UFL conditions cannot be evaluated as bool in a Python context.") __nonzero__ = __bool__ @ufl_type(is_abstract=True, num_ops=2) class BinaryCondition(Condition): """Binary condition.""" __slots__ = ("_name",) def __init__(self, name, left, right): """Initialise.""" left = as_ufl(left) right = as_ufl(right) Condition.__init__(self, (left, right)) self._name = name if name in ("!=", "=="): # Since equals and not-equals are used for comparing # representations, we have to allow any shape here. The # scalar properties must be checked when used in # conditional instead! pass elif name in ("&&", "||"): # Binary operators acting on boolean expressions allow # only conditions for arg in (left, right): if not isinstance(arg, Condition): raise ValueError(f"Expecting a Condition, not {ufl_err_str(arg)}.") else: # Binary operators acting on non-boolean expressions allow # only scalars if left.ufl_shape != () or right.ufl_shape != (): raise ValueError("Expecting scalar arguments.") if left.ufl_free_indices != () or right.ufl_free_indices != (): raise ValueError("Expecting scalar arguments.") def __str__(self): """Format as a string.""" return "%s %s %s" % ( parstr(self.ufl_operands[0], self), self._name, parstr(self.ufl_operands[1], self), ) # Not associating with __eq__, the concept of equality with == is # reserved for object equivalence for use in set and dict. @ufl_type() class EQ(BinaryCondition): """Equality condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "==", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a == b) def __bool__(self): """Convert to a bool.""" return expr_equals(self.ufl_operands[0], self.ufl_operands[1]) __nonzero__ = __bool__ # Not associating with __ne__, the concept of equality with == is # reserved for object equivalence for use in set and dict. @ufl_type() class NE(BinaryCondition): """Not equal condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "!=", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a != b) def __bool__(self): """Convert to a bool.""" return not expr_equals(self.ufl_operands[0], self.ufl_operands[1]) __nonzero__ = __bool__ @ufl_type(binop="__le__") class LE(BinaryCondition): """Less than or equal condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "<=", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a <= b) @ufl_type(binop="__ge__") class GE(BinaryCondition): """Greater than or equal to condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, ">=", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a >= b) @ufl_type(binop="__lt__") class LT(BinaryCondition): """Less than condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "<", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a < b) @ufl_type(binop="__gt__") class GT(BinaryCondition): """Greater than condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, ">", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a > b) @ufl_type() class AndCondition(BinaryCondition): """And condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "&&", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a and b) @ufl_type() class OrCondition(BinaryCondition): """Or condition.""" __slots__ = () def __init__(self, left, right): """Initialise.""" BinaryCondition.__init__(self, "||", left, right) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) return bool(a or b) @ufl_type(num_ops=1) class NotCondition(Condition): """Not condition.""" __slots__ = () def __init__(self, condition): """Initialise.""" Condition.__init__(self, (condition,)) if not isinstance(condition, Condition): raise ValueError("Expecting a condition.") def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return bool(not a) def __str__(self): """Format as a string.""" return "!(%s)" % (str(self.ufl_operands[0]),) @ufl_type(num_ops=3, inherit_shape_from_operand=1, inherit_indices_from_operand=1) class Conditional(Operator): """Conditional expression. In C++ these take the format `(condition ? true_value : false_value)`. """ __slots__ = () def __init__(self, condition, true_value, false_value): """Initialise.""" if not isinstance(condition, Condition): raise ValueError("Expecting condition as first argument.") true_value = as_ufl(true_value) false_value = as_ufl(false_value) tsh = true_value.ufl_shape fsh = false_value.ufl_shape if tsh != fsh: raise ValueError("Shape mismatch between conditional branches.") tfi = true_value.ufl_free_indices ffi = false_value.ufl_free_indices if tfi != ffi: raise ValueError("Free index mismatch between conditional branches.") if isinstance(condition, (EQ, NE)): if not all( ( condition.ufl_operands[0].ufl_shape == (), condition.ufl_operands[0].ufl_free_indices == (), condition.ufl_operands[1].ufl_shape == (), condition.ufl_operands[1].ufl_free_indices == (), ) ): raise ValueError("Non-scalar == or != is not allowed.") Operator.__init__(self, (condition, true_value, false_value)) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" c = self.ufl_operands[0].evaluate(x, mapping, component, index_values) if c: a = self.ufl_operands[1] else: a = self.ufl_operands[2] return a.evaluate(x, mapping, component, index_values) def __str__(self): """Format as a string.""" return "%s ? %s : %s" % tuple(parstr(o, self) for o in self.ufl_operands) # --- Specific functions higher level than a conditional --- @ufl_type(is_scalar=True, num_ops=1) class MinValue(Operator): """Take the minimum of two values.""" __slots__ = () def __init__(self, left, right): """Initialise.""" Operator.__init__(self, (left, right)) if not (is_true_ufl_scalar(left) and is_true_ufl_scalar(right)): raise ValueError("Expecting scalar arguments.") def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a, b = self.ufl_operands a = a.evaluate(x, mapping, component, index_values) b = b.evaluate(x, mapping, component, index_values) try: res = min(a, b) except ValueError: warnings.warn("Value error in evaluation of min() of %s and %s." % self.ufl_operands) raise return res def __str__(self): """Format as a string.""" return "min_value(%s, %s)" % self.ufl_operands @ufl_type(is_scalar=True, num_ops=1) class MaxValue(Operator): """Take the maximum of two values.""" __slots__ = () def __init__(self, left, right): """Initialise.""" Operator.__init__(self, (left, right)) if not (is_true_ufl_scalar(left) and is_true_ufl_scalar(right)): raise ValueError("Expecting scalar arguments.") def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a, b = self.ufl_operands a = a.evaluate(x, mapping, component, index_values) b = b.evaluate(x, mapping, component, index_values) try: res = max(a, b) except ValueError: warnings.warn("Value error in evaluation of max() of %s and %s." % self.ufl_operands) raise return res def __str__(self): """Format as a string.""" return "max_value(%s, %s)" % self.ufl_operands ufl-2024.2.0/ufl/constant.py000066400000000000000000000050521470142567200155320ustar00rootroot00000000000000"""Support fpr non-literal values which are constant with respect to a domain.""" # Copyright (C) 2019 Michal Habera # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.domain import as_domain from ufl.utils.counted import Counted @ufl_type() class Constant(Terminal, Counted): """Constant.""" _ufl_noslots_ = True def __init__(self, domain, shape=(), count=None): """Initalise.""" Terminal.__init__(self) Counted.__init__(self, count, Constant) self._ufl_domain = as_domain(domain) self._ufl_shape = shape # Repr string is build in such way, that reconstruction # with eval() is possible self._repr = "Constant({}, {}, {})".format( repr(self._ufl_domain), repr(self._ufl_shape), repr(self._count) ) @property def ufl_shape(self): """Get the UFL shape.""" return self._ufl_shape def ufl_domain(self): """Get the UFL domain.""" return self._ufl_domain def ufl_domains(self): """Get the UFL domains.""" return (self.ufl_domain(),) def is_cellwise_constant(self): """Return True if the function is cellwise constant.""" return True def __str__(self): """Format as a string.""" return f"c_{self._count}" def __repr__(self): """Representation.""" return self._repr def __eq__(self, other): """Check equality.""" if not isinstance(other, Constant): return False if self is other: return True return ( self._count == other._count and self._ufl_domain == other._ufl_domain and self._ufl_shape == self._ufl_shape ) def _ufl_signature_data_(self, renumbering): """Signature data for constant depends on renumbering.""" return "Constant({}, {}, {})".format( self._ufl_domain._ufl_signature_data_(renumbering), repr(self._ufl_shape), repr(renumbering[self]), ) def VectorConstant(domain, count=None): """Vector constant.""" domain = as_domain(domain) return Constant(domain, shape=(domain.geometric_dimension(),), count=count) def TensorConstant(domain, count=None): """Tensor constant.""" domain = as_domain(domain) return Constant( domain, shape=(domain.geometric_dimension(), domain.geometric_dimension()), count=count ) ufl-2024.2.0/ufl/constantvalue.py000066400000000000000000000351401470142567200165700ustar00rootroot00000000000000"""This module defines classes representing constant values.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2011. # Modified by Massimiliano Leoni, 2016. import numbers from math import atan2 import ufl # --- Helper functions imported here for compatibility--- from ufl.checks import is_python_scalar, is_true_ufl_scalar, is_ufl_scalar # noqa: F401 from ufl.core.expr import Expr from ufl.core.multiindex import FixedIndex, Index from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type # Precision for float formatting precision = None def format_float(x): """Format float value based on global UFL precision.""" if precision: return "{:.{prec}}".format(float(x), prec=precision) else: return "{}".format(float(x)) # --- Base classes for constant types --- @ufl_type(is_abstract=True) class ConstantValue(Terminal): """Constant value.""" __slots__ = () def __init__(self): """Initialise.""" Terminal.__init__(self) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" return True def ufl_domains(self): """Return tuple of domains related to this terminal object.""" return () # TODO: Add geometric dimension/domain and Argument dependencies to Zero? @ufl_type(is_literal=True) class Zero(ConstantValue): """Representation of a zero valued expression. Class for representing zero tensors of different shapes. """ __slots__ = ("ufl_shape", "ufl_free_indices", "ufl_index_dimensions") _cache = {} def __getnewargs__(self): """Get new args.""" return (self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) def __new__(cls, shape=(), free_indices=(), index_dimensions=None): """Create new Zero.""" if free_indices: self = ConstantValue.__new__(cls) else: self = Zero._cache.get(shape) if self is not None: return self self = ConstantValue.__new__(cls) Zero._cache[shape] = self self._init(shape, free_indices, index_dimensions) return self def __init__(self, shape=(), free_indices=(), index_dimensions=None): """Initialise.""" pass def _init(self, shape=(), free_indices=(), index_dimensions=None): """Initialise.""" ConstantValue.__init__(self) if not all(isinstance(i, int) for i in shape): raise ValueError("Expecting tuple of int.") if not isinstance(free_indices, tuple): raise ValueError(f"Expecting tuple for free_indices, not {free_indices}.") self.ufl_shape = shape if not free_indices: self.ufl_free_indices = () self.ufl_index_dimensions = () elif all(isinstance(i, Index) for i in free_indices): # Handle old input format if not isinstance(index_dimensions, dict) and all( isinstance(i, Index) for i in index_dimensions.keys() ): raise ValueError(f"Expecting tuple of index dimensions, not {index_dimensions}") self.ufl_free_indices = tuple(sorted(i.count() for i in free_indices)) self.ufl_index_dimensions = tuple( d for i, d in sorted(index_dimensions.items(), key=lambda x: x[0].count()) ) else: # Handle new input format if not all(isinstance(i, int) for i in free_indices): raise ValueError(f"Expecting tuple of integer free index ids, not {free_indices}") if not isinstance(index_dimensions, tuple) and all( isinstance(i, int) for i in index_dimensions ): raise ValueError( f"Expecting tuple of integer index dimensions, not {index_dimensions}" ) # Assuming sorted now to avoid this cost, enable for debugging: # if sorted(free_indices) != list(free_indices): # raise ValueError("Expecting sorted input. Remove this check later for efficiency.") self.ufl_free_indices = free_indices self.ufl_index_dimensions = index_dimensions def evaluate(self, x, mapping, component, index_values): """Evaluate.""" return 0.0 def __str__(self): """Format as a string.""" if self.ufl_shape == () and self.ufl_free_indices == (): return "0" if self.ufl_free_indices == (): return "0 (shape %s)" % (self.ufl_shape,) if self.ufl_shape == (): return "0 (index labels %s)" % (self.ufl_free_indices,) return "0 (shape %s, index labels %s)" % (self.ufl_shape, self.ufl_free_indices) def __repr__(self): """Representation.""" r = "Zero(%s, %s, %s)" % ( repr(self.ufl_shape), repr(self.ufl_free_indices), repr(self.ufl_index_dimensions), ) return r def __eq__(self, other): """Check equalty.""" if isinstance(other, Zero): if self is other: return True return ( self.ufl_shape == other.ufl_shape and self.ufl_free_indices == other.ufl_free_indices and self.ufl_index_dimensions == other.ufl_index_dimensions ) elif isinstance(other, (int, float)): return other == 0 else: return False def __neg__(self): """Negate.""" return self def __abs__(self): """Absolute value.""" return self def __bool__(self): """Convert to a bool.""" return False __nonzero__ = __bool__ def __float__(self): """Convert to a float.""" return 0.0 def __int__(self): """Convert to an int.""" return 0 def __complex__(self): """Convert to a complex number.""" return 0 + 0j def zero(*shape): """UFL literal constant: Return a zero tensor with the given shape.""" if len(shape) == 1 and isinstance(shape[0], tuple): return Zero(shape[0]) else: return Zero(shape) # --- Scalar value types --- @ufl_type(is_abstract=True, is_scalar=True) class ScalarValue(ConstantValue): """A constant scalar value.""" __slots__ = ("_value",) def __init__(self, value): """Initialise.""" ConstantValue.__init__(self) self._value = value def value(self): """Get the value.""" return self._value def evaluate(self, x, mapping, component, index_values): """Evaluate.""" return self._value def __eq__(self, other): """Check equalty. This is implemented to allow comparison with python scalars. Note that this will make IntValue(1) != FloatValue(1.0), but ufl-python comparisons like IntValue(1) == 1.0 FloatValue(1.0) == 1 can still succeed. These will however not have the same hash value and therefore not collide in a dict. """ if isinstance(other, self._ufl_class_): return self._value == other._value elif isinstance(other, (int, float)): # FIXME: Disallow this, require explicit 'expr == # IntValue(3)' instead to avoid ambiguities! return other == self._value else: return False def __str__(self): """Format as a string.""" return str(self._value) def __float__(self): """Convert to a float.""" return float(self._value) def __int__(self): """Convert to an int.""" return int(self._value) def __complex__(self): """Convert to a complex number.""" return complex(self._value) def __neg__(self): """Negate.""" return type(self)(-self._value) def __abs__(self): """Absolute value.""" return type(self)(abs(self._value)) def real(self): """Real part.""" return self._value.real def imag(self): """Imaginary part.""" return self._value.imag @ufl_type(wraps_type=complex, is_literal=True) class ComplexValue(ScalarValue): """Representation of a constant, complex scalar.""" __slots__ = () def __getnewargs__(self): """Get new args.""" return (self._value,) def __new__(cls, value): """Create a new ComplexValue.""" if value.imag == 0: if value.real == 0: return Zero() else: return FloatValue(value.real) else: return ConstantValue.__new__(cls) def __init__(self, value): """Initialise.""" ScalarValue.__init__(self, complex(value)) def modulus(self): """Get the modulus.""" return abs(self.value()) def argument(self): """Get the argument.""" return atan2(self.value().imag, self.value().real) def __repr__(self): """Representation.""" r = "%s(%s)" % (type(self).__name__, repr(self._value)) return r def __float__(self): """Convert to a float.""" raise TypeError("ComplexValues cannot be cast to float") def __int__(self): """Convert to an int.""" raise TypeError("ComplexValues cannot be cast to int") @ufl_type(is_abstract=True, is_scalar=True) class RealValue(ScalarValue): """Abstract class used to differentiate real values from complex ones.""" __slots__ = () @ufl_type(wraps_type=float, is_literal=True) class FloatValue(RealValue): """Representation of a constant scalar floating point value.""" __slots__ = () def __getnewargs__(self): """Get new args.""" return (self._value,) def __new__(cls, value): """Create a new FloatValue.""" if value == 0.0: # Always represent zero with Zero return Zero() return ConstantValue.__new__(cls) def __init__(self, value): """Initialise.""" super(FloatValue, self).__init__(float(value)) def __repr__(self): """Representation.""" r = "%s(%s)" % (type(self).__name__, format_float(self._value)) return r @ufl_type(wraps_type=int, is_literal=True) class IntValue(RealValue): """Representation of a constant scalar integer value.""" __slots__ = () _cache = {} def __getnewargs__(self): """Get new args.""" return (self._value,) def __new__(cls, value): """Create a new IntValue.""" if value == 0: # Always represent zero with Zero return Zero() elif abs(value) < 100: # Small numbers are cached to reduce memory usage # (fly-weight pattern) self = IntValue._cache.get(value) if self is not None: return self self = RealValue.__new__(cls) IntValue._cache[value] = self else: self = RealValue.__new__(cls) self._init(value) return self def _init(self, value): """Initialise.""" super(IntValue, self).__init__(int(value)) def __init__(self, value): """Initialise.""" pass def __repr__(self): """Representation.""" r = "%s(%s)" % (type(self).__name__, repr(self._value)) return r # --- Identity matrix --- @ufl_type() class Identity(ConstantValue): """Representation of an identity matrix.""" __slots__ = ("_dim", "ufl_shape") def __init__(self, dim): """Initialise.""" ConstantValue.__init__(self) self._dim = dim self.ufl_shape = (dim, dim) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a, b = component return 1 if a == b else 0 def __getitem__(self, key): """Get an item.""" if len(key) != 2: raise ValueError("Size mismatch for Identity.") if all(isinstance(k, (int, FixedIndex)) for k in key): return IntValue(1) if (int(key[0]) == int(key[1])) else Zero() return Expr.__getitem__(self, key) def __str__(self): """Format as a string.""" return "I" def __repr__(self): """Representation.""" r = "Identity(%d)" % self._dim return r def __eq__(self, other): """Check equalty.""" return isinstance(other, Identity) and self._dim == other._dim # --- Permutation symbol --- @ufl_type() class PermutationSymbol(ConstantValue): """Representation of a permutation symbol. This is also known as the Levi-Civita symbol, antisymmetric symbol, or alternating symbol. """ __slots__ = ("ufl_shape", "_dim") def __init__(self, dim): """Initialise.""" ConstantValue.__init__(self) self._dim = dim self.ufl_shape = (dim,) * dim def evaluate(self, x, mapping, component, index_values): """Evaluate.""" return self.__eps(component) def __getitem__(self, key): """Get an item.""" if len(key) != self._dim: raise ValueError("Size mismatch for PermutationSymbol.") if all(isinstance(k, (int, FixedIndex)) for k in key): return self.__eps(key) return Expr.__getitem__(self, key) def __str__(self): """Format as a string.""" return "eps" def __repr__(self): """Representation.""" r = "PermutationSymbol(%d)" % self._dim return r def __eq__(self, other): """Check equalty.""" return isinstance(other, PermutationSymbol) and self._dim == other._dim def __eps(self, x): """Get eps. This function body is taken from http://www.mathkb.com/Uwe/Forum.aspx/math/29865/N-integer-Levi-Civita """ result = IntValue(1) for i, x1 in enumerate(x): for j in range(i + 1, len(x)): x2 = x[j] if x1 > x2: result = -result elif x1 == x2: return Zero() return result def as_ufl(expression): """Converts expression to an Expr if possible.""" if isinstance(expression, (Expr, ufl.BaseForm)): return expression elif isinstance(expression, numbers.Integral): return IntValue(expression) elif isinstance(expression, numbers.Real): return FloatValue(expression) elif isinstance(expression, numbers.Complex): return ComplexValue(expression) else: raise ValueError( f"Invalid type conversion: {expression} can not be converted to any UFL type." ) ufl-2024.2.0/ufl/core/000077500000000000000000000000001470142567200142555ustar00rootroot00000000000000ufl-2024.2.0/ufl/core/__init__.py000066400000000000000000000000201470142567200163560ustar00rootroot00000000000000"""UFL core.""" ufl-2024.2.0/ufl/core/base_form_operator.py000066400000000000000000000170631470142567200205060ustar00rootroot00000000000000"""Base form operator. This module defines the BaseFormOperator class, which is the base class for objects that can be seen as forms and as operators such as ExternalOperator or Interpolate. """ # Copyright (C) 2019 Nacime Bouziani # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2021-2022 from collections import OrderedDict from ufl.argument import Argument, Coargument from ufl.constantvalue import as_ufl from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace from ufl.utils.counted import Counted @ufl_type(num_ops="varying", is_differential=True) class BaseFormOperator(Operator, BaseForm, Counted): """Base form operator.""" # Slots are disabled here because they cause trouble in PyDOLFIN # multiple inheritance pattern: _ufl_noslots_ = True def __init__(self, *operands, function_space, derivatives=None, argument_slots=()): """Initialise. Args: operands: operands on which acts the operator. function_space: the FunctionSpace or MixedFunctionSpace on which to build this Function. derivatives: tuple specifying the derivative multiindex. argument_slots: tuple composed containing expressions with ufl.Argument or ufl.Coefficient objects. """ BaseForm.__init__(self) ufl_operands = tuple(map(as_ufl, operands)) argument_slots = tuple(map(as_ufl, argument_slots)) Operator.__init__(self, ufl_operands) Counted.__init__(self, counted_class=BaseFormOperator) # -- Function space -- # if not isinstance(function_space, AbstractFunctionSpace): raise ValueError("Expecting a FunctionSpace or FiniteElement.") # -- Derivatives -- # # Some BaseFormOperator does have derivatives (e.g. ExternalOperator) # while other don't since they are fully determined by their # argument slots (e.g. Interpolate) self.derivatives = derivatives # -- Argument slots -- # if len(argument_slots) == 0: # Make v* v_star = Argument(function_space.dual(), 0) argument_slots = (v_star,) self._argument_slots = argument_slots # Internal variables for caching coefficient data self._coefficients = None # BaseFormOperators don't have free indices. ufl_free_indices = () ufl_index_dimensions = () def argument_slots(self, outer_form=False): """Returns a tuple of expressions containing argument and coefficient based expressions. We get an argument uhat when we take the Gateaux derivative in the direction uhat: d/du N(u; v*) = dNdu(u; uhat, v*) where uhat is a ufl.Argument and v* a ufl.Coargument Applying the action replace the last argument by coefficient: action(dNdu(u; uhat, v*), w) = dNdu(u; w, v*) where du is a ufl.Coefficient. """ from ufl.algorithms.analysis import extract_arguments if not outer_form: return self._argument_slots # Takes into account argument contraction when a base form operator is in an outer form: # For example: # F = N(u; v*) * v * dx can be seen as Action(v1 * v * dx, N(u; v*)) # => F.arguments() should return (v,)! return tuple(a for a in self._argument_slots[1:] if len(extract_arguments(a)) != 0) def coefficients(self): """Return all BaseCoefficient objects found in base form operator.""" if self._coefficients is None: self._analyze_form_arguments() return self._coefficients def _analyze_form_arguments(self): """Analyze which Argument and Coefficient objects can be found in the base form.""" from ufl.algorithms.analysis import extract_arguments, extract_coefficients, extract_type dual_arg, *arguments = self.argument_slots() # When coarguments are treated as BaseForms, they have two # arguments (one primal and one dual) as they map from V* to V* # => V* x V -> R. However, when they are treated as mere # "arguments", the primal space argument is discarded and we # only have the dual space argument (Coargument). This is the # exact same situation than BaseFormOperator's arguments which # are different depending on whether the BaseFormOperator is # used in an outer form or not. arguments = tuple(extract_type(dual_arg, Coargument)) + tuple( a for arg in arguments for a in extract_arguments(arg) ) coefficients = tuple(c for op in self.ufl_operands for c in extract_coefficients(op)) # Define canonical numbering of arguments and coefficients # 1) Need concept of order since we may have arguments with the same number # because of form composition (`argument_slots(outer_form=True)`): # Example: Let u \in V1 and N \in V2 and F = N(u; v*) * dx, then # `derivative(F, u)` will contain dNdu(u; uhat, v*) with v* = Argument(0, V2) # and uhat = Argument(0, V1) (since F.arguments() = ()) # 2) Having sorted arguments also makes BaseFormOperator compatible with other # BaseForm objects for which the highest-numbered argument always comes last. self._arguments = tuple(sorted(OrderedDict.fromkeys(arguments), key=lambda x: x.number())) self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count())) def count(self): """Return the count associated to the base form operator.""" return self._count @property def ufl_shape(self): """Return the UFL shape of the coefficient.produced by the operator.""" arg, *_ = self.argument_slots() if isinstance(arg, BaseForm): arg, *_ = arg.arguments() return arg._ufl_shape def ufl_function_space(self): """Return the function space associated to the operator. I.e. return the dual of the base form operator's Coargument. """ arg, *_ = self.argument_slots() if isinstance(arg, BaseForm): arg, *_ = arg.arguments() return arg._ufl_function_space return arg._ufl_function_space.dual() def _ufl_expr_reconstruct_( self, *operands, function_space=None, derivatives=None, argument_slots=None ): """Return a new object of the same type with new operands.""" return type(self)( *operands, function_space=function_space or self.ufl_function_space(), derivatives=derivatives or self.derivatives, argument_slots=argument_slots or self.argument_slots(), ) def __repr__(self): """Default repr string construction for base form operators.""" r = f"{type(self).__name__}(" r += ", ".join(repr(op) for op in self.ufl_operands) r += "; {self.ufl_function_space()!r}; " r += ", ".join(repr(arg) for arg in self.argument_slots()) r += f"; derivatives={self.derivatives!r})" return r def __hash__(self): """Hash code for use in dicts.""" hashdata = ( type(self), tuple(hash(op) for op in self.ufl_operands), tuple(hash(arg) for arg in self._argument_slots), self.derivatives, hash(self.ufl_function_space()), ) return hash(hashdata) def __eq__(self, other): """Check for equality.""" raise NotImplementedError() ufl-2024.2.0/ufl/core/compute_expr_hash.py000066400000000000000000000021351470142567200203450ustar00rootroot00000000000000"""Non-recursive traversal-based hash computation algorithm. Fast iteration over nodes in an ``Expr`` DAG to compute memoized hashes for all unique nodes. """ # Copyright (C) 2015 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 def compute_expr_hash(expr): """Compute hashes of *expr* and all its nodes efficiently, without using Python recursion.""" if expr._hash is not None: return expr._hash # Postorder traversal, can't use unique_post_traversal, since that # uses a set which requires that this hash is computed. lifo = [(expr, list(expr.ufl_operands))] while lifo: expr, deps = lifo[-1] for i, dep in enumerate(deps): if dep is not None and dep._hash is None: lifo.append((dep, list(dep.ufl_operands))) deps[i] = None break else: if expr._hash is None: expr._hash = expr._ufl_compute_hash_() lifo.pop() return expr._hash ufl-2024.2.0/ufl/core/expr.py000066400000000000000000000342011470142567200156050ustar00rootroot00000000000000"""This module defines the ``Expr`` class, the superclass for all expression tree node types in UFL. NB: A note about other operators not implemented here: More operators (special functions) on ``Expr`` instances are defined in ``exproperators.py``, as well as the transpose ``A.T`` and spatial derivative ``a.dx(i)``. This is to avoid circular dependencies between ``Expr`` and its subclasses. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 # Modified by Massimiliano Leoni, 2016 import warnings from ufl.core.ufl_type import UFLType, update_ufl_type_attributes class Expr(object, metaclass=UFLType): """Base class for all UFL expression types. *Instance properties* Every ``Expr`` instance will have certain properties. The most important ones are ``ufl_operands``, ``ufl_shape``, ``ufl_free_indices``, and ``ufl_index_dimensions`` properties. Expressions are immutable and hashable. *Type traits* The ``Expr`` API defines a number of type traits that each subclass needs to provide. Most of these are specified indirectly via the arguments to the ``ufl_type`` class decorator, allowing UFL to do some consistency checks and automate most of the traits for most types. Type traits are accessed via a class or instance object of the form ``obj._ufl_traitname_``. See the source code for description of each type trait. *Operators* Some Python special functions are implemented in this class, some are implemented in subclasses, and some are attached to this class in the ``ufl_type`` class decorator. *Defining subclasses* To define a new expression class, inherit from either ``Terminal`` or ``Operator``, and apply the ``ufl_type`` class decorator with suitable arguments. See the docstring of ``ufl_type`` for details on its arguments. Looking at existing classes similar to the one you wish to add is a good idea. Looking through the comments in the ``Expr`` class and ``ufl_type`` to understand all the properties that may need to be specified is also a good idea. Note that many algorithms in UFL and form compilers will need handlers implemented for each new type::. .. code-block:: python @ufl_type() class MyOperator(Operator): pass *Type collections* All ``Expr`` subclasses are collected by ``ufl_type`` in global variables available via ``Expr``. *Profiling* Object creation statistics can be collected by doing .. code-block:: python Expr.ufl_enable_profiling() # ... run some code initstats, delstats = Expr.ufl_disable_profiling() Giving a list of creation and deletion counts for each typecode. """ # --- Each Expr subclass must define __slots__ or _ufl_noslots_ at # --- the top --- # This is to freeze member variables for objects of this class and # save memory by skipping the per-instance dict. __slots__ = ("_hash", "__weakref__") # _ufl_noslots_ = True # --- Basic object behaviour --- def __getnewargs__(self): """Get newargs tuple. The tuple returned here is passed to as args to cls.__new__(cls, *args). This implementation passes the operands, which is () for terminals. May be necessary to override if __new__ is implemented in a subclass. """ return self.ufl_operands def __init__(self): """Initialise.""" self._hash = None # This shows the principal behaviour of the hash function attached # in ufl_type: # def __hash__(self): # if self._hash is None: # self._hash = self._ufl_compute_hash_() # return self._hash # --- Type traits are added to subclasses by the ufl_type class # --- decorator --- # Note: Some of these are modified after the Expr class definition # because Expr is not defined yet at this point. Note: Boolean # type traits that categorize types are mostly set to None for # Expr but should be True or False for any non-abstract type. # A reference to the UFL class itself. This makes it possible to # do type(f)._ufl_class_ and be sure you get the actual UFL class # instead of a subclass from another library. _ufl_class_ = None # The handler name. This is the name of the handler function you # implement for this type in a multifunction. _ufl_handler_name_ = "expr" # Number of operands, "varying" for some types, or None if not # applicable for abstract types. _ufl_num_ops_ = None # Type trait: If the type is a literal. _ufl_is_literal_ = None # Type trait: If the type is classified as a 'terminal modifier', # for form compiler use. _ufl_is_terminal_modifier_ = None # Type trait: If the type is a shaping operator. Shaping # operations include indexing, slicing, transposing, i.e. not # introducing computation of a new value. _ufl_is_shaping_ = False # Type trait: If the type is in reference frame. _ufl_is_in_reference_frame_ = None # Type trait: If the type is a restriction to a geometric entity. _ufl_is_restriction_ = None # Type trait: If the type is evaluation in a particular way. _ufl_is_evaluation_ = None # Type trait: If the type is a differential operator. _ufl_is_differential_ = None # Type trait: If the type is purely scalar, having no shape or # indices. _ufl_is_scalar_ = None # Type trait: If the type never has free indices. _ufl_is_index_free_ = False # --- All subclasses must define these object attributes --- # Each subclass of Expr is checked to have these properties in # ufl_type _ufl_required_properties_ = ( # A tuple of operands, all of them Expr instances. "ufl_operands", # A tuple of ints, the value shape of the expression. "ufl_shape", # A tuple of free index counts. "ufl_free_indices", # A tuple providing the int dimension for each free index. "ufl_index_dimensions", ) # Each subclass of Expr is checked to have these methods in # ufl_type # FIXME: Add more and enable all _ufl_required_methods_ = ( # To compute the hash on demand, this method is called. "_ufl_compute_hash_", # The data returned from this method is used to compute the # signature of a form "_ufl_signature_data_", # The == operator must be implemented to compare for identical # representation, used by set() and dict(). The __hash__ # operator is added by ufl_type. "__eq__", # To reconstruct an object of the same type with operands or # properties changed. "_ufl_expr_reconstruct_", # Implemented in Operator and Terminal so this should never fail "ufl_domains", # "ufl_cell", # "ufl_domain", # "__str__", # "__repr__", ) # --- Global variables for collecting all types --- # A global dict mapping language_operator_name to the type it # produces _ufl_language_operators_ = {} # List of all terminal modifier types _ufl_terminal_modifiers_ = [] # --- Mechanism for profiling object creation and deletion --- # Backup of default init and del _ufl_regular__init__ = __init__ def _ufl_profiling__init__(self): """Replacement constructor with object counting.""" Expr._ufl_regular__init__(self) Expr._ufl_obj_init_counts_[self._ufl_typecode_] += 1 def _ufl_profiling__del__(self): """Replacement destructor with object counting.""" Expr._ufl_obj_del_counts_[self._ufl_typecode_] -= 1 @staticmethod def ufl_enable_profiling(): """Turn on the object counting mechanism and reset counts to zero.""" Expr.__init__ = Expr._ufl_profiling__init__ setattr(Expr, "__del__", Expr._ufl_profiling__del__) for i in range(len(Expr._ufl_obj_init_counts_)): Expr._ufl_obj_init_counts_[i] = 0 Expr._ufl_obj_del_counts_[i] = 0 @staticmethod def ufl_disable_profiling(): """Turn off the object counting mechanism. Return object init and del counts.""" Expr.__init__ = Expr._ufl_regular__init__ delattr(Expr, "__del__") return (Expr._ufl_obj_init_counts_, Expr._ufl_obj_del_counts_) # === Abstract functions that must be implemented by subclasses === # --- Functions for reconstructing expression --- def _ufl_expr_reconstruct_(self, *operands): """Return a new object of the same type with new operands.""" raise NotImplementedError(self.__class__._ufl_expr_reconstruct_) # --- Functions for geometric properties of expression --- def ufl_domains(self): """Return all domains this expression is defined on.""" warnings.warn( "Expr.ufl_domains() is deprecated, please use extract_domains(expr) instead.", DeprecationWarning, ) from ufl.domain import extract_domains return extract_domains(self) def ufl_domain(self): """Return the single unique domain this expression is defined on, or throw an error.""" warnings.warn( "Expr.ufl_domain() is deprecated, please use extract_unique_domain(expr) instead.", DeprecationWarning, ) from ufl.domain import extract_unique_domain return extract_unique_domain(self) # --- Functions for float evaluation --- def evaluate(self, x, mapping, component, index_values): """Evaluate expression at given coordinate with given values for terminals.""" raise ValueError(f"Symbolic evaluation of {self._ufl_class_.__name__} not available.") def _ufl_evaluate_scalar_(self): if self.ufl_shape or self.ufl_free_indices: raise TypeError("Cannot evaluate a nonscalar expression to a scalar value.") return self(()) # No known x def __float__(self): """Try to evaluate as scalar and cast to float.""" try: v = float(self._ufl_evaluate_scalar_()) except Exception: v = NotImplemented return v def __complex__(self): """Try to evaluate as scalar and cast to complex.""" try: v = complex(self._ufl_evaluate_scalar_()) except TypeError: v = NotImplemented return v def __bool__(self): """By default, all Expr are nonzero/False.""" return True def __nonzero__(self): """By default, all Expr are nonzero/False.""" return self.__bool__() @staticmethod def _ufl_coerce_(value): """Convert any value to a UFL type.""" # Quick skip for most types if isinstance(value, Expr): return value # Conversion from non-ufl types # (the _ufl_from_*_ functions are attached to Expr by ufl_type) ufl_from_type = "_ufl_from_{0}_".format(value.__class__.__name__) return getattr(Expr, ufl_from_type)(value) # if hasattr(Expr, ufl_from_type): # return getattr(Expr, ufl_from_type)(value) # Fail gracefully if no valid type conversion found # raise TypeError("Cannot convert a {0.__class__.__name__} to UFL type.".format(value)) # --- Special functions for string representations --- # All subclasses must implement _ufl_signature_data_ def _ufl_signature_data_(self, renumbering): """Return data that uniquely identifies form compiler relevant aspects of this object.""" raise NotImplementedError(self.__class__._ufl_signature_data_) # All subclasses must implement __repr__ def __repr__(self): """Return string representation this object can be reconstructed from.""" raise NotImplementedError(self.__class__.__repr__) # All subclasses must implement __str__ def __str__(self): """Return pretty print string representation of this object.""" raise NotImplementedError(self.__class__.__str__) def _ufl_err_str_(self): """Return a short string to represent this Expr in an error message.""" return f"<{self._ufl_class_.__name__} id={id(self)}>" # --- Special functions used for processing expressions --- def __eq__(self, other): """Checks whether the two expressions are represented the exact same way. This does not check if the expressions are mathematically equal or equivalent! Used by sets and dicts. """ raise NotImplementedError(self.__class__.__eq__) def __len__(self): """Length of expression. Used for iteration over vector expressions.""" s = self.ufl_shape if len(s) == 1: return s[0] raise NotImplementedError("Cannot take length of non-vector expression.") def __iter__(self): """Iteration over vector expressions.""" for i in range(len(self)): yield self[i] def __floordiv__(self, other): """UFL does not support integer division.""" raise NotImplementedError(self.__class__.__floordiv__) def __pos__(self): """Unary + is a no-op.""" return self def __round__(self, n=None): """Round to nearest integer or to nearest nth decimal.""" try: val = float(self._ufl_evaluate_scalar_()) val = round(val, n) except TypeError: val = complex(self._ufl_evaluate_scalar_()) val = round(val.real, n) + round(val.imag, n) * 1j except TypeError: val = NotImplemented return val # Initializing traits here because Expr is not defined in the class # declaration Expr._ufl_class_ = Expr # Update Expr with metaclass properties (e.g. typecode or handler name) # Explicitly done here instead of using `@ufl_type` to avoid circular imports. update_ufl_type_attributes(Expr) def ufl_err_str(expr): """Return a UFL error string.""" if hasattr(expr, "_ufl_err_str_"): return expr._ufl_err_str_() else: return repr(expr) ufl-2024.2.0/ufl/core/external_operator.py000066400000000000000000000114711470142567200203700ustar00rootroot00000000000000"""External operator. This module defines the ``ExternalOperator`` class, which symbolically represents operators that are not straightforwardly expressible in UFL. Subclasses of ``ExternalOperator`` must define how this operator should be evaluated as well as its derivatives from a given set of operands. """ # Copyright (C) 2019 Nacime Bouziani # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2023 from ufl.core.base_form_operator import BaseFormOperator from ufl.core.ufl_type import ufl_type @ufl_type(num_ops="varying", is_differential=True) class ExternalOperator(BaseFormOperator): """External operator.""" # Slots are disabled here because they cause trouble in PyDOLFIN # multiple inheritance pattern: _ufl_noslots_ = True def __init__(self, *operands, function_space, derivatives=None, argument_slots=()): """Initialise. Args: operands: operands on which acts the ExternalOperator. function_space: the FunctionSpace, or MixedFunctionSpace on which to build this Function. derivatives: tuple specifying the derivative multiindex. argument_slots: tuple composed containing expressions with ufl.Argument or ufl.Coefficient objects. argument_slots: TODO """ # -- Derivatives -- # if derivatives is not None: if not isinstance(derivatives, tuple): raise TypeError(f"Expecting a tuple for derivatives and not {derivatives}.") if not len(derivatives) == len(operands): raise ValueError(f"Expecting a size of {len(operands)} for {derivatives}.") if not all(isinstance(d, int) for d in derivatives) or any(d < 0 for d in derivatives): raise ValueError( "Expecting a derivative multi-index with nonnegative indices, " f"not {derivatives}." ) else: derivatives = (0,) * len(operands) BaseFormOperator.__init__( self, *operands, function_space=function_space, derivatives=derivatives, argument_slots=argument_slots, ) def ufl_element(self): """Shortcut to get the finite element of the function space of the external operator.""" # Useful when applying split on an ExternalOperator return self.arguments()[0].ufl_element() def grad(self): """Returns the symbolic grad of the external operator.""" # By default, differential rules produce `grad(assembled_o)` # `where assembled_o` is the `Coefficient` resulting from # assembling the external operator since the external operator # may not be smooth enough for chain rule to hold. Symbolic # gradient (`grad(ExternalOperator)`) depends on the operator # considered and its implementation may be needed in some cases # (e.g. convolution operator). raise NotImplementedError( "Symbolic gradient not defined for the external operator considered." ) def assemble(self, *args, **kwargs): """Assemble the external operator.""" raise NotImplementedError( f"Symbolic evaluation of {self._ufl_class_.__name__} not available." ) def _ufl_expr_reconstruct_( self, *operands, function_space=None, derivatives=None, argument_slots=None, add_kwargs={} ): """Return a new object of the same type with new operands.""" return type(self)( *operands, function_space=function_space or self.ufl_function_space(), derivatives=derivatives or self.derivatives, argument_slots=argument_slots or self.argument_slots(), **add_kwargs, ) def __str__(self): """Default str string for ExternalOperator operators.""" d = "\N{PARTIAL DIFFERENTIAL}" derivatives = self.derivatives d_ops = "".join(d + "o" + str(i + 1) for i, di in enumerate(derivatives) for j in range(di)) e = "e(" e += ", ".join(str(op) for op in self.ufl_operands) e += "; " e += ", ".join(str(arg) for arg in reversed(self.argument_slots())) e += ")" return d + e + "/" + d_ops if sum(derivatives) > 0 else e def __eq__(self, other): """Check for equality.""" if self is other: return True return ( type(self) is type(other) and all(a == b for a, b in zip(self.ufl_operands, other.ufl_operands)) and all(a == b for a, b in zip(self._argument_slots, other._argument_slots)) and self.derivatives == other.derivatives and self.ufl_function_space() == other.ufl_function_space() ) ufl-2024.2.0/ufl/core/interpolate.py000066400000000000000000000072771470142567200171720ustar00rootroot00000000000000"""This module defines the Interpolate class.""" # Copyright (C) 2021 Nacime Bouziani # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2021-2022 from ufl.action import Action from ufl.argument import Argument, Coargument from ufl.coefficient import Cofunction from ufl.constantvalue import as_ufl from ufl.core.base_form_operator import BaseFormOperator from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual from ufl.form import BaseForm, Form from ufl.functionspace import AbstractFunctionSpace @ufl_type(num_ops="varying", is_differential=True) class Interpolate(BaseFormOperator): """Symbolic representation of the interpolation operator.""" # Slots are disabled here because they cause trouble in PyDOLFIN # multiple inheritance pattern: _ufl_noslots_ = True def __init__(self, expr, v): """Initialise. Args: expr: a UFL expression to interpolate. v: the FunctionSpace to interpolate into or the Coargument defined on the dual of the FunctionSpace to interpolate into. """ # This check could be more rigorous. dual_args = (Coargument, Cofunction, Form, Action, BaseFormOperator) if isinstance(v, AbstractFunctionSpace): if is_dual(v): raise ValueError("Expecting a primal function space.") v = Argument(v.dual(), 0) elif not isinstance(v, dual_args): raise ValueError( "Expecting the second argument to be FunctionSpace, FiniteElement or dual." ) expr = as_ufl(expr) if isinstance(expr, dual_args): raise ValueError("Expecting the first argument to be primal.") # Reversed order convention argument_slots = (v, expr) # Get the primal space (V** = V) if isinstance(v, BaseForm): arg, *_ = v.arguments() function_space = arg.ufl_function_space() else: function_space = v.ufl_function_space().dual() # Set the operand as `expr` for DAG traversal purpose. operand = expr BaseFormOperator.__init__( self, operand, function_space=function_space, argument_slots=argument_slots ) def _ufl_expr_reconstruct_(self, expr, v=None, **add_kwargs): """Return a new object of the same type with new operands.""" v = v or self.argument_slots()[0] return type(self)(expr, v, **add_kwargs) def __repr__(self): """Default repr string construction for Interpolate.""" r = "Interpolate(" r += ", ".join(repr(arg) for arg in reversed(self.argument_slots())) r += f"; {self.ufl_function_space()!r})" return r def __str__(self): """Default str string construction for Interpolate.""" s = "Interpolate(" s += ", ".join(str(arg) for arg in reversed(self.argument_slots())) s += f"; {self.ufl_function_space()})" return s def __eq__(self, other): """Check for equality.""" if self is other: return True return ( type(self) is type(other) and all(a == b for a, b in zip(self._argument_slots, other._argument_slots)) and self.ufl_function_space() == other.ufl_function_space() ) # Helper function def interpolate(expr, v): """Create symbolic representation of the interpolation operator. Args: expr: a UFL expression to interpolate. v: the FunctionSpace to interpolate into or the Coargument defined on the dual of the FunctionSpace to interpolate into. """ return Interpolate(expr, v) ufl-2024.2.0/ufl/core/multiindex.py000066400000000000000000000161301470142567200170120ustar00rootroot00000000000000"""This module defines the single index types and some internal index utilities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016. from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.utils.counted import Counted # Export list for ufl.classes __all_classes__ = ["IndexBase", "FixedIndex", "Index"] class IndexBase(object): """Base class for all indices.""" __slots__ = () def __init__(self): """Initialise.""" class FixedIndex(IndexBase): """UFL value: An index with a specific value assigned.""" __slots__ = ("_value", "_hash") _cache = {} def __getnewargs__(self): """Get new args.""" return (self._value,) def __new__(cls, value): """Create new FixedIndex.""" self = FixedIndex._cache.get(value) if self is None: if not isinstance(value, int): raise ValueError("Expecting integer value for fixed index.") self = IndexBase.__new__(cls) self._init(value) FixedIndex._cache[value] = self return self def _init(self, value): """Initialise.""" IndexBase.__init__(self) self._value = value self._hash = hash(("FixedIndex", self._value)) def __init__(self, value): """Initialise.""" def __hash__(self): """Hash.""" return self._hash def __eq__(self, other): """Check equality.""" return isinstance(other, FixedIndex) and (self._value == other._value) def __int__(self): """Convert to int.""" return self._value def __str__(self): """Represent with a string.""" return f"{self._value}" def __repr__(self): """Return representation.""" return f"FixedIndex({self._value})" class Index(IndexBase, Counted): """UFL value: An index with no value assigned. Used to represent free indices in Einstein indexing notation. """ __slots__ = ("_count", "_counted_class") def __init__(self, count=None): """Initialise.""" IndexBase.__init__(self) Counted.__init__(self, count, Index) def __hash__(self): """Hash.""" return hash(("Index", self._count)) def __eq__(self, other): """Check equality.""" return isinstance(other, Index) and (self._count == other._count) def __str__(self): """Represent as a string.""" c = f"{self._count}" if len(c) > 1: c = f"{{{c}}}" return f"i_{c}" def __repr__(self): """Return representation.""" return f"Index({self._count})" @ufl_type() class MultiIndex(Terminal): """Represents a sequence of indices, either fixed or free.""" __slots__ = ("_indices",) _cache = {} def __getnewargs__(self): """Get new args.""" return (self._indices,) def __new__(cls, indices): """Create new MultiIndex.""" if not isinstance(indices, tuple): raise ValueError("Expecting a tuple of indices.") if all(isinstance(ind, FixedIndex) for ind in indices): # Cache multiindices consisting of purely fixed indices # (aka flyweight pattern) key = tuple(ind._value for ind in indices) self = MultiIndex._cache.get(key) if self is not None: return self self = Terminal.__new__(cls) MultiIndex._cache[key] = self else: # Create a new object if we have any free indices (too # many combinations to cache) if not all(isinstance(ind, IndexBase) for ind in indices): raise ValueError("Expecting only Index and FixedIndex objects.") self = Terminal.__new__(cls) # Initialize here instead of in __init__ to avoid overwriting # self._indices from cached objects self._init(indices) return self def __init__(self, indices): """Initialise.""" def _init(self, indices): """Initialise.""" Terminal.__init__(self) self._indices = indices def indices(self): """Return tuple of indices.""" return self._indices def _ufl_compute_hash_(self): """Compute UFL hash.""" return hash(("MultiIndex",) + tuple(hash(ind) for ind in self._indices)) def __eq__(self, other): """Check equality.""" return isinstance(other, MultiIndex) and self._indices == other._indices def evaluate(self, x, mapping, component, index_values): """Evaluate index.""" # Build component from index values component = [] for i in self._indices: if isinstance(i, FixedIndex): component.append(i._value) elif isinstance(i, Index): component.append(index_values[i]) return tuple(component) @property def ufl_shape(self): """Get the UFL shape. This should not be used. """ raise ValueError("Multiindex has no shape (it is not a tensor expression).") @property def ufl_free_indices(self): """Get the UFL free indices. This should not be used. """ raise ValueError("Multiindex has no free indices (it is not a tensor expression).") @property def ufl_index_dimensions(self): """Get the UFL index dimensions. This should not be used. """ raise ValueError("Multiindex has no free indices (it is not a tensor expression).") def is_cellwise_constant(self): """Check if cellwise constant. Always True. """ return True def ufl_domains(self): """Return tuple of domains related to this terminal object.""" return () # --- Adding multiindices --- def __add__(self, other): """Add.""" if isinstance(other, tuple): return MultiIndex(self._indices + other) elif isinstance(other, MultiIndex): return MultiIndex(self._indices + other._indices) return NotImplemented def __radd__(self, other): """Add.""" if isinstance(other, tuple): return MultiIndex(other + self._indices) elif isinstance(other, MultiIndex): return MultiIndex(other._indices + self._indices) return NotImplemented # --- String formatting --- def __str__(self): """Format as a string.""" return ", ".join(str(i) for i in self._indices) def __repr__(self): """Return representation.""" return f"MultiIndex({self._indices!r})" # --- Iteration protocol --- def __len__(self): """Get length.""" return len(self._indices) def __getitem__(self, i): """Get an item.""" return self._indices[i] def __iter__(self): """Return iteratable.""" return iter(self._indices) def indices(n): """Return a tuple of n new Index objects.""" return tuple(Index() for i in range(n)) ufl-2024.2.0/ufl/core/operator.py000066400000000000000000000031271470142567200164650ustar00rootroot00000000000000"""Operator.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 # Modified by Massimiliano Leoni, 2016 from ufl.core.expr import Expr from ufl.core.ufl_type import ufl_type @ufl_type(is_abstract=True, is_terminal=False) class Operator(Expr): """Base class for all operators, i.e. non-terminal expression types.""" __slots__ = ("ufl_operands",) def __init__(self, operands=None): """Initialise.""" Expr.__init__(self) # If operands is None, the type sets this itself. This is to # get around some tricky too-fancy __new__/__init__ design in # algebra.py, for now. It would be nicer to make the classes # in algebra.py pass operands here. if operands is not None: self.ufl_operands = operands def _ufl_expr_reconstruct_(self, *operands): """Return a new object of the same type with new operands.""" return self._ufl_class_(*operands) def _ufl_signature_data_(self): """Get UFL signature data.""" return self._ufl_typecode_ def _ufl_compute_hash_(self): """Compute a hash code for this expression. Used by sets and dicts.""" return hash((self._ufl_typecode_,) + tuple(hash(o) for o in self.ufl_operands)) def __repr__(self): """Default repr string construction for operators.""" # This should work for most cases return f"{self._ufl_class_.__name__}({', '.join(repr(op) for op in self.ufl_operands)})" ufl-2024.2.0/ufl/core/terminal.py000066400000000000000000000060211470142567200164410ustar00rootroot00000000000000"""This module defines the Terminal class. Terminal the superclass for all types that are terminal nodes in an expression tree. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 # Modified by Massimiliano Leoni, 2016 import warnings from ufl.core.expr import Expr from ufl.core.ufl_type import ufl_type @ufl_type(is_abstract=True, is_terminal=True) class Terminal(Expr): """Base class for terminal objects. A terminal node in the UFL expression tree. """ __slots__ = () def __init__(self): """Initialise the terminal.""" Expr.__init__(self) ufl_operands = () ufl_free_indices = () ufl_index_dimensions = () def ufl_domains(self): """Return tuple of domains related to this terminal object.""" raise NotImplementedError("Missing implementation of domains().") def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get *self* from *mapping* and return the component asked for.""" f = mapping.get(self) # No mapping, trying to evaluate self as a constant if f is None: try: try: f = float(self) except TypeError: f = complex(self) if derivatives: f = 0.0 return f except Exception: pass # If it has an ufl_evaluate function, call it if hasattr(self, "ufl_evaluate"): return self.ufl_evaluate(x, component, derivatives) # Take component if any warnings.warn( f"Couldn't map '{self}' to a float, returning ufl object without evaluation." ) f = self if component: f = f[component] return f # Found a callable in the mapping if callable(f): if derivatives: f = f(x, derivatives) else: f = f(x) else: if derivatives: return 0.0 # Take component if any (expecting nested tuple) for c in component: f = f[c] return f def _ufl_signature_data_(self, renumbering): """Default signature data for of terminals just return the repr string.""" return repr(self) def _ufl_compute_hash_(self): """Default hash of terminals just hash the repr string.""" return hash(repr(self)) def __eq__(self, other): """Default comparison of terminals just compare repr strings.""" return repr(self) == repr(other) # --- Subgroups of terminals --- @ufl_type(is_abstract=True) class FormArgument(Terminal): """An abstract class for a form argument (a thing in a primal finite element space).""" __slots__ = () def __init__(self): """Initialise the form argument.""" Terminal.__init__(self) ufl-2024.2.0/ufl/core/ufl_id.py000066400000000000000000000030341470142567200160710ustar00rootroot00000000000000"""Utilites for types with a globally counted unique id attached to each object.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 def attach_ufl_id(cls): """Equip class with ``.ufl_id()`` and handle bookkeeping. Usage: 1. Apply to class:: @attach_ufl_id class MyClass(object): 2. If ``__slots__`` is defined, include ``_ufl_id`` attribute:: __slots__ = ("_ufl_id",) 3. Add keyword argument to constructor:: def __init__(self, *args, ufl_id=None): 4. Call ``self._init_ufl_id`` with ``ufl_id`` and assign to ``._ufl_id`` attribute:: self._ufl_id = self._init_ufl_id(ufl_id) Result: ``MyClass().ufl_id()`` returns unique value for each constructed object. """ def _get_ufl_id(self): """Return the ufl_id of this object.""" return self._ufl_id def _init_ufl_id(cls): """Initialize new ufl_id for the object under construction.""" # Bind cls with closure here def init_ufl_id(self, ufl_id): if ufl_id is None: ufl_id = cls._ufl_global_id cls._ufl_global_id = max(ufl_id, cls._ufl_global_id) + 1 return ufl_id return init_ufl_id # Modify class: cls._ufl_global_id = 0 cls.ufl_id = _get_ufl_id cls._init_ufl_id = _init_ufl_id(cls) return cls ufl-2024.2.0/ufl/core/ufl_type.py000066400000000000000000000373411470142567200164660ustar00rootroot00000000000000"""UFL type.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 # Modified by Matthew Scroggs, 2023 from __future__ import annotations import typing from abc import ABC, abstractmethod import ufl.core as core from ufl.core.compute_expr_hash import compute_expr_hash from ufl.utils.formatting import camel2underscore class UFLObject(ABC): """A UFL Object.""" @abstractmethod def _ufl_hash_data_(self) -> typing.Hashable: """Return hashable data that uniquely defines this object.""" @abstractmethod def __str__(self) -> str: """Return a human-readable string representation of the object.""" @abstractmethod def __repr__(self) -> str: """Return a string representation of the object.""" def __hash__(self) -> int: """Hash the object.""" return hash(self._ufl_hash_data_()) def __eq__(self, other): """Check if two objects are equal.""" return type(self) is type(other) and self._ufl_hash_data_() == other._ufl_hash_data_() def __ne__(self, other): """Check inequality.""" return not self.__eq__(other) def get_base_attr(cls, name): """Return first non-``None`` attribute of given name among base classes.""" for base in cls.mro(): if hasattr(base, name): attr = getattr(base, name) if attr is not None: return attr return None def set_trait(cls, basename, value, inherit=False): """Assign a trait to class with namespacing ``_ufl_basename_`` applied. If trait value is ``None``, optionally inherit it from the closest base class that has it. """ name = "_ufl_" + basename + "_" if value is None and inherit: value = get_base_attr(cls, name) setattr(cls, name, value) def determine_num_ops(cls, num_ops, unop, binop, rbinop): """Determine number of operands for this type.""" # Try to determine num_ops from other traits or baseclass, or # require num_ops to be set for non-abstract classes if it cannot # be determined automatically if num_ops is not None: return num_ops elif cls._ufl_is_terminal_: return 0 elif unop: return 1 elif binop or rbinop: return 2 else: # Determine from base class return get_base_attr(cls, "_ufl_num_ops_") def check_is_terminal_consistency(cls): """Check for consistency in ``is_terminal`` trait among superclasses.""" if cls._ufl_is_terminal_ is None: msg = ( f"Class {cls.__name__} has not specified the is_terminal trait." " Did you forget to inherit from Terminal or Operator?" ) raise TypeError(msg) base_is_terminal = get_base_attr(cls, "_ufl_is_terminal_") if base_is_terminal is not None and cls._ufl_is_terminal_ != base_is_terminal: msg = ( f"Conflicting given and automatic 'is_terminal' trait for class {cls.__name__}." " Check if you meant to inherit from Terminal or Operator." ) raise TypeError(msg) def check_abstract_trait_consistency(cls): """Check that the first base classes up to ``Expr`` are other UFL types.""" for base in cls.mro(): if base is core.expr.Expr: break if not issubclass(base, core.expr.Expr) and base._ufl_is_abstract_: msg = ( "Base class {0.__name__} of class {1.__name__} " "is not an abstract subclass of {2.__name__}." ) raise TypeError(msg.format(base, cls, core.expr.Expr)) def check_has_slots(cls): """Check if type has __slots__ unless it is marked as exception with _ufl_noslots_.""" if "_ufl_noslots_" in cls.__dict__: return if "__slots__" not in cls.__dict__: msg = ( "Class {0.__name__} is missing the __slots__ " "attribute and is not marked with _ufl_noslots_." ) raise TypeError(msg.format(cls)) # Check base classes for __slots__ as well, skipping object which is the last one for base in cls.mro()[1:-1]: if "__slots__" not in base.__dict__: msg = "Class {0.__name__} is has a base class {1.__name__} with __slots__ missing." raise TypeError(msg.format(cls, base)) def check_type_traits_consistency(cls): """Execute a variety of consistency checks on the ufl type traits.""" # Check for consistency in global type collection sizes Expr = core.expr.Expr assert Expr._ufl_num_typecodes_ == len(Expr._ufl_all_handler_names_) assert Expr._ufl_num_typecodes_ == len(Expr._ufl_all_classes_) assert Expr._ufl_num_typecodes_ == len(Expr._ufl_obj_init_counts_) assert Expr._ufl_num_typecodes_ == len(Expr._ufl_obj_del_counts_) # Check that non-abstract types always specify num_ops if not cls._ufl_is_abstract_: if cls._ufl_num_ops_ is None: msg = "Class {0.__name__} has not specified num_ops." raise TypeError(msg.format(cls)) # Check for non-abstract types that num_ops has the right type if not cls._ufl_is_abstract_: if not (isinstance(cls._ufl_num_ops_, int) or cls._ufl_num_ops_ == "varying"): msg = 'Class {0.__name__} has invalid num_ops value {1} (integer or "varying").' raise TypeError(msg.format(cls, cls._ufl_num_ops_)) # Check that num_ops is not set to nonzero for a terminal if cls._ufl_is_terminal_ and cls._ufl_num_ops_ != 0: msg = "Class {0.__name__} has num_ops > 0 but is terminal." raise TypeError(msg.format(cls)) # Check that a non-scalar type doesn't have a scalar base class. if not cls._ufl_is_scalar_: if get_base_attr(cls, "_ufl_is_scalar_"): msg = "Non-scalar class {0.__name__} is has a scalar base class." raise TypeError(msg.format(cls)) def check_implements_required_methods(cls): """Check if type implements the required methods.""" if not cls._ufl_is_abstract_: for attr in core.expr.Expr._ufl_required_methods_: if not hasattr(cls, attr): msg = "Class {0.__name__} has no {1} method." raise TypeError(msg.format(cls, attr)) elif not callable(getattr(cls, attr)): msg = "Required method {1} of class {0.__name__} is not callable." raise TypeError(msg.format(cls, attr)) def check_implements_required_properties(cls): """Check if type implements the required properties.""" if not cls._ufl_is_abstract_: for attr in core.expr.Expr._ufl_required_properties_: if not hasattr(cls, attr): msg = "Class {0.__name__} has no {1} property." raise TypeError(msg.format(cls, attr)) elif callable(getattr(cls, attr)): msg = "Required property {1} of class {0.__name__} is a callable method." raise TypeError(msg.format(cls, attr)) def attach_implementations_of_indexing_interface( cls, inherit_shape_from_operand, inherit_indices_from_operand ): """Attach implementations of indexing interface.""" # Scalar or index-free? Then we can simplify the implementation of # tensor properties by attaching them here. if cls._ufl_is_scalar_: cls.ufl_shape = () if cls._ufl_is_scalar_ or cls._ufl_is_index_free_: cls.ufl_free_indices = () cls.ufl_index_dimensions = () # Automate direct inheriting of shape and indices from one of the # operands. This simplifies refactoring because a lot of types do # this. if inherit_shape_from_operand is not None: def _inherited_ufl_shape(self): return self.ufl_operands[inherit_shape_from_operand].ufl_shape cls.ufl_shape = property(_inherited_ufl_shape) if inherit_indices_from_operand is not None: def _inherited_ufl_free_indices(self): return self.ufl_operands[inherit_indices_from_operand].ufl_free_indices def _inherited_ufl_index_dimensions(self): return self.ufl_operands[inherit_indices_from_operand].ufl_index_dimensions cls.ufl_free_indices = property(_inherited_ufl_free_indices) cls.ufl_index_dimensions = property(_inherited_ufl_index_dimensions) def update_global_expr_attributes(cls): """Update global attributres. Update global ``Expr`` attributes, mainly by adding *cls* to global collections of ufl types. """ if cls._ufl_is_terminal_modifier_: core.expr.Expr._ufl_terminal_modifiers_.append(cls) # Add to collection of language operators. This collection is # used later to populate the official language namespace. # TODO: I don't think this functionality is fully completed, check # it out later. if not cls._ufl_is_abstract_ and hasattr(cls, "_ufl_function_"): cls._ufl_function_.__func__.__doc__ = cls.__doc__ core.expr.Expr._ufl_language_operators_[cls._ufl_handler_name_] = cls._ufl_function_ def update_ufl_type_attributes(cls): """Update UFL type attributes.""" # Determine integer typecode by incrementally counting all types cls._ufl_typecode_ = UFLType._ufl_num_typecodes_ UFLType._ufl_num_typecodes_ += 1 UFLType._ufl_all_classes_.append(cls) # Determine handler name by a mapping from "TypeName" to "type_name" cls._ufl_handler_name_ = camel2underscore(cls.__name__) UFLType._ufl_all_handler_names_.add(cls._ufl_handler_name_) # Append space for counting object creation and destriction of # this this type. UFLType._ufl_obj_init_counts_.append(0) UFLType._ufl_obj_del_counts_.append(0) def ufl_type( is_abstract=False, is_terminal=None, is_scalar=False, is_index_free=False, is_shaping=False, is_literal=False, is_terminal_modifier=False, is_in_reference_frame=False, is_restriction=False, is_evaluation=False, is_differential=None, use_default_hash=True, num_ops=None, inherit_shape_from_operand=None, inherit_indices_from_operand=None, wraps_type=None, unop=None, binop=None, rbinop=None, ): """Decorator to apply to every subclass in the UFL ``Expr`` and ``BaseForm`` hierarchy. This decorator contains a number of checks that are intended to enforce uniform behaviour across UFL types. The rationale behind the checks and the meaning of the optional arguments should be sufficiently documented in the source code below. """ def _ufl_type_decorator_(cls): """UFL type decorator.""" # Update attributes for UFLType instances (BaseForm and Expr objects) update_ufl_type_attributes(cls) if not issubclass(cls, core.expr.Expr): # Don't need anything else for non Expr subclasses return cls # is_scalar implies is_index_freeg if is_scalar: _is_index_free = True else: _is_index_free = is_index_free # Store type traits cls._ufl_class_ = cls set_trait(cls, "is_abstract", is_abstract, inherit=False) set_trait(cls, "is_terminal", is_terminal, inherit=True) set_trait(cls, "is_literal", is_literal, inherit=True) set_trait(cls, "is_terminal_modifier", is_terminal_modifier, inherit=True) set_trait(cls, "is_shaping", is_shaping, inherit=True) set_trait(cls, "is_in_reference_frame", is_in_reference_frame, inherit=True) set_trait(cls, "is_restriction", is_restriction, inherit=True) set_trait(cls, "is_evaluation", is_evaluation, inherit=True) set_trait(cls, "is_differential", is_differential, inherit=True) set_trait(cls, "is_scalar", is_scalar, inherit=True) set_trait(cls, "is_index_free", _is_index_free, inherit=True) # Number of operands can often be determined automatically _num_ops = determine_num_ops(cls, num_ops, unop, binop, rbinop) set_trait(cls, "num_ops", _num_ops) # Attach builtin type wrappers to Expr """# These are currently handled in the as_ufl implementation in constantvalue.py if wraps_type is not None: if not isinstance(wraps_type, type): msg = "Expecting a type, not a {0.__name__} for the wraps_type argument in definition of {1.__name__}." raise TypeError(msg.format(type(wraps_type), cls)) def _ufl_from_type_(value): return cls(value) from_type_name = "_ufl_from_{0}_".format(wraps_type.__name__) setattr(Expr, from_type_name, staticmethod(_ufl_from_type_)) """ # Attach special function to Expr. # Avoids the circular dependency problem of making # Expr.__foo__ return a Foo that is a subclass of Expr. """# These are currently attached in exproperators.py if unop: def _ufl_expr_unop_(self): return cls(self) setattr(Expr, unop, _ufl_expr_unop_) if binop: def _ufl_expr_binop_(self, other): try: other = Expr._ufl_coerce_(other) except: return NotImplemented return cls(self, other) setattr(Expr, binop, _ufl_expr_binop_) if rbinop: def _ufl_expr_rbinop_(self, other): try: other = Expr._ufl_coerce_(other) except: return NotImplemented return cls(other, self) setattr(Expr, rbinop, _ufl_expr_rbinop_) """ # Make sure every non-abstract class has its own __hash__ and # __eq__. Python 3 will set __hash__ to None if cls has # __eq__, but we've implemented it in a separate function and # want to inherit/use that for all types. Allow overriding by # setting use_default_hash=False. if use_default_hash: cls.__hash__ = compute_expr_hash # NB! This function conditionally adds some methods to the # class! This approach significantly reduces the amount of # small functions to implement across all the types but of # course it's a bit more opaque. attach_implementations_of_indexing_interface( cls, inherit_shape_from_operand, inherit_indices_from_operand ) # Update Expr update_global_expr_attributes(cls) # Apply a range of consistency checks to detect bugs in type # implementations that Python doesn't check for us, including # some checks that a static language compiler would do for us check_abstract_trait_consistency(cls) check_has_slots(cls) check_is_terminal_consistency(cls) check_implements_required_methods(cls) check_implements_required_properties(cls) check_type_traits_consistency(cls) return cls return _ufl_type_decorator_ class UFLType(type): """Base class for all UFL types. Equip UFL types with some ufl specific properties. """ # A global counter of the number of typecodes assigned. _ufl_num_typecodes_ = 0 # Set the handler name for UFLType _ufl_handler_name_ = "ufl_type" # A global array of all Expr and BaseForm subclasses, indexed by typecode _ufl_all_classes_ = [] # A global set of all handler names added _ufl_all_handler_names_ = set() # A global array of the number of initialized objects for each # typecode _ufl_obj_init_counts_ = [] # A global array of the number of deleted objects for each # typecode _ufl_obj_del_counts_ = [] # Type trait: If the type is abstract. An abstract class cannot # be instantiated and does not need all properties specified. _ufl_is_abstract_ = True # Type trait: If the type is terminal. _ufl_is_terminal_ = None ufl-2024.2.0/ufl/corealg/000077500000000000000000000000001470142567200147415ustar00rootroot00000000000000ufl-2024.2.0/ufl/corealg/__init__.py000066400000000000000000000000271470142567200170510ustar00rootroot00000000000000"""Core algorithms.""" ufl-2024.2.0/ufl/corealg/map_dag.py000066400000000000000000000111361470142567200167050ustar00rootroot00000000000000"""Basic algorithms for applying functions to subexpressions.""" # Copyright (C) 2014-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 from ufl.core.expr import Expr from ufl.corealg.multifunction import MultiFunction from ufl.corealg.traversal import cutoff_unique_post_traversal, unique_post_traversal def map_expr_dag(function, expression, compress=True, vcache=None, rcache=None): """Apply a function to each subexpression node in an expression DAG. If the same function is called multiple times in a transformation (as for example in apply_derivatives), then to reuse caches across the call, use the arguments vcache and rcache. Args: function: The function expression: An expression compress: If True (default), the output object from the function is cached in a dict and reused such that the resulting expression DAG does not contain duplicate objects vcache: Optional dict for caching results of intermediate transformations rcache: Optional dict for caching results for compression Returns: The result of the final function call """ (result,) = map_expr_dags( function, [expression], compress=compress, vcache=vcache, rcache=rcache ) return result def map_expr_dags(function, expressions, compress=True, vcache=None, rcache=None): """Apply a function to each sub-expression node in an expression DAG. If *compress* is ``True`` (default) the output object from the function is cached in a ``dict`` and reused such that the resulting expression DAG does not contain duplicate objects. If the same function is called multiple times in a transformation (as for example in apply_derivatives), then to reuse caches across the call, use the arguments vcache and rcache. Args: function: The function expressions: An expression compress: If True (default), the output object from the function is cached in a dict and reused such that the resulting expression DAG does not contain duplicate objects vcache: Optional dict for caching results of intermediate transformations rcache: Optional dict for caching results for compression Returns: a list with the result of the final function call for each expression """ # Temporary data structures # expr -> r = function(expr,...), cache of intermediate results vcache = {} if vcache is None else vcache # r -> r, cache of result objects for memory reuse rcache = {} if rcache is None else rcache # Build mapping typecode:bool, for which types to skip the subtree of if isinstance(function, MultiFunction): cutoff_types = function._is_cutoff_type handlers = function._handlers # Optimization else: # Regular function: no skipping supported cutoff_types = [False] * Expr._ufl_num_typecodes_ handlers = [function] * Expr._ufl_num_typecodes_ # Create visited set here to share between traversal calls visited = set() # Pick faster traversal algorithm if we have no cutoffs if any(cutoff_types): def traversal(expression): return cutoff_unique_post_traversal(expression, cutoff_types, visited) else: def traversal(expression): return unique_post_traversal(expression, visited) for expression in expressions: # Iterate over all subexpression nodes, child before parent for v in traversal(expression): # Skip transformations on cache hit if v in vcache: continue # Cache miss: Get transformed operands, then apply transformation if cutoff_types[v._ufl_typecode_]: r = handlers[v._ufl_typecode_](v) else: r = handlers[v._ufl_typecode_](v, *[vcache[u] for u in v.ufl_operands]) # Optionally check if r is in rcache, a memory optimization # to be able to keep representation of result compact if compress: r2 = rcache.get(r) if r2 is None: # Cache miss: store in rcache rcache[r] = r else: # Cache hit: Use previously computed object r2, # allowing r to be garbage collected as soon as possible r = r2 # Store result in cache vcache[v] = r return [vcache[expression] for expression in expressions] ufl-2024.2.0/ufl/corealg/multifunction.py000066400000000000000000000103721470142567200202160ustar00rootroot00000000000000"""Base class for multifunctions with UFL ``Expr`` type dispatch.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 import inspect from ufl.core.expr import Expr from ufl.core.ufl_type import UFLType def get_num_args(function): """Return the number of arguments accepted by *function*.""" sig = inspect.signature(function) return len(sig.parameters) + 1 def memoized_handler(handler): """Function decorator to memoize ``MultiFunction`` handlers.""" def _memoized_handler(self, o): c = getattr(self, "_memoized_handler_cache") r = c.get(o) if r is None: r = handler(self, o) c[o] = r return r return _memoized_handler class MultiFunction(object): """Base class for collections of non-recursive expression node handlers. Subclass this (remember to call the ``__init__`` method of this class), and implement handler functions for each ``Expr`` type, using the lower case handler name of the type (``exprtype._ufl_handler_name_``). This class is optimized for efficient type based dispatch in the ``__call__`` operator via typecode based lookup of the handler function bound to the algorithm object. Of course Python's function call overhead still applies. """ _handlers_cache = {} def __init__(self): """Initialise.""" # Analyse class properties and cache handler data the # first time this is run for a particular class # (cached for each algorithm for performance) algorithm_class = type(self) cache_data = MultiFunction._handlers_cache.get(algorithm_class) if not cache_data: handler_names = [None] * len(Expr._ufl_all_classes_) # Iterate over the inheritance chain for each Expr # subclass (NB! This assumes that all UFL classes inherits # from a single Expr subclass and that the first # superclass is always from the UFL Expr hierarchy!) for classobject in Expr._ufl_all_classes_: for c in classobject.mro(): # Register classobject with handler for the first # encountered superclass try: handler_name = c._ufl_handler_name_ except AttributeError as attribute_error: if type(classobject) is not UFLType: raise attribute_error # Default handler name for UFL types handler_name = UFLType._ufl_handler_name_ if hasattr(self, handler_name): handler_names[classobject._ufl_typecode_] = handler_name break is_cutoff_type = [get_num_args(getattr(self, name)) == 2 for name in handler_names] cache_data = (handler_names, is_cutoff_type) MultiFunction._handlers_cache[algorithm_class] = cache_data # Build handler list for this particular class (get functions # bound to self, these cannot be cached) handler_names, is_cutoff_type = cache_data self._handlers = [getattr(self, name) for name in handler_names] self._is_cutoff_type = is_cutoff_type # Create cache for memoized_handler self._memoized_handler_cache = {} def __call__(self, o, *args): """Delegate to handler function based on typecode of first argument.""" return self._handlers[o._ufl_typecode_](o, *args) def undefined(self, o, *args): """Trigger error for types with missing handlers.""" raise ValueError(f"No handler defined for {o._ufl_class_.__name__}.") def reuse_if_untouched(self, o, *ops): """Reuse object if operands are the same objects. Use in your own subclass by setting e.g. :: expr = MultiFunction.reuse_if_untouched as a default rule. """ if all(a is b for a, b in zip(o.ufl_operands, ops)): return o else: return o._ufl_expr_reconstruct_(*ops) # Set default behaviour for any UFLType as undefined ufl_type = undefined ufl-2024.2.0/ufl/corealg/traversal.py000066400000000000000000000076631470142567200173320ustar00rootroot00000000000000"""Various expression traversal utilities. The algorithms here are non-recursive, which is faster than recursion by a factor of 10 or so because of the function call overhead. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 def pre_traversal(expr): """Yield ``o`` for each tree node ``o`` in *expr*, parent before child.""" lifo = [expr] while lifo: expr = lifo.pop() yield expr for op in expr.ufl_operands: lifo.append(op) def post_traversal(expr): """Yield ``o`` for each node ``o`` in *expr*, child before parent.""" lifo = [(expr, list(reversed(expr.ufl_operands)))] while lifo: expr, deps = lifo[-1] for i, dep in enumerate(deps): if dep is not None: lifo.append((dep, list(reversed(dep.ufl_operands)))) deps[i] = None break else: yield expr lifo.pop() def cutoff_post_traversal(expr, cutofftypes): """Cut-off post-tranversal. Yield ``o`` for each node ``o`` in *expr*, child before parent, but skipping subtrees of the cutofftypes. """ lifo = [(expr, list(reversed(expr.ufl_operands)))] while lifo: expr, deps = lifo[-1] if cutofftypes[expr._ufl_typecode_]: yield expr lifo.pop() else: for i, dep in enumerate(deps): if dep is not None: lifo.append((dep, list(reversed(dep.ufl_operands)))) deps[i] = None break else: yield expr lifo.pop() def unique_pre_traversal(expr, visited=None): """Yield ``o`` for each tree node ``o`` in *expr*, parent before child. This version only visits each node once. """ if visited is None: visited = set() lifo = [expr] visited.add(expr) while lifo: expr = lifo.pop() yield expr for op in expr.ufl_operands: if op not in visited: lifo.append(op) visited.add(op) def unique_post_traversal(expr, visited=None): """Yield ``o`` for each node ``o`` in *expr*, child before parent. Never visit a node twice. """ lifo = [(expr, list(expr.ufl_operands))] if visited is None: visited = set() visited.add(expr) while lifo: expr, deps = lifo[-1] for i, dep in enumerate(deps): if dep is not None and dep not in visited: lifo.append((dep, list(dep.ufl_operands))) deps[i] = None break else: yield expr visited.add(expr) lifo.pop() def cutoff_unique_post_traversal(expr, cutofftypes, visited=None): """Yield ``o`` for each node ``o`` in *expr*, child before parent. Never visit a node twice. """ lifo = [(expr, list(reversed(expr.ufl_operands)))] if visited is None: visited = set() while lifo: expr, deps = lifo[-1] if cutofftypes[expr._ufl_typecode_]: yield expr visited.add(expr) lifo.pop() else: for i, dep in enumerate(deps): if dep is not None and dep not in visited: lifo.append((dep, list(reversed(dep.ufl_operands)))) deps[i] = None break else: yield expr visited.add(expr) lifo.pop() def traverse_terminals(expr): """Traverse terminals.""" for op in pre_traversal(expr): if op._ufl_is_terminal_: yield op def traverse_unique_terminals(expr, visited=None): """Traverse unique terminals.""" for op in unique_pre_traversal(expr, visited=visited): if op._ufl_is_terminal_: yield op ufl-2024.2.0/ufl/differentiation.py000066400000000000000000000463621470142567200170640ustar00rootroot00000000000000"""Differential operators.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.argument import Argument, Coargument from ufl.checks import is_cellwise_constant from ufl.coefficient import Coefficient from ufl.constantvalue import Zero from ufl.core.base_form_operator import BaseFormOperator from ufl.core.expr import Expr from ufl.core.operator import Operator from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.domain import extract_unique_domain, find_geometric_dimension from ufl.exprcontainers import ExprList, ExprMapping from ufl.form import BaseForm from ufl.precedence import parstr from ufl.variable import Variable # --- Basic differentiation objects --- @ufl_type(is_abstract=True, is_differential=True) class Derivative(Operator): """Base class for all derivative types.""" __slots__ = () def __init__(self, operands): """Initalise.""" Operator.__init__(self, operands) @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class CoefficientDerivative(Derivative): """Derivative of form integrand w.r.t. the degrees of freedom in a discrete Coefficient.""" __slots__ = () def __new__(cls, integrand, coefficients, arguments, coefficient_derivatives): """Create a new CoefficientDerivative.""" if not isinstance(coefficients, ExprList): raise ValueError("Expecting ExprList instance with Coefficients.") if not isinstance(arguments, ExprList): raise ValueError("Expecting ExprList instance with Arguments.") if not isinstance(coefficient_derivatives, ExprMapping): raise ValueError("Expecting ExprMapping for coefficient derivatives.") if isinstance(integrand, Zero): return integrand return Derivative.__new__(cls) def __init__(self, integrand, coefficients, arguments, coefficient_derivatives): """Initalise.""" if not isinstance(coefficient_derivatives, ExprMapping): coefficient_derivatives = ExprMapping(coefficient_derivatives) Derivative.__init__(self, (integrand, coefficients, arguments, coefficient_derivatives)) def __str__(self): """Format as a string.""" return "d/dfj { %s }, with fh=%s, dfh/dfj = %s, and coefficient derivatives %s" % ( self.ufl_operands[0], self.ufl_operands[1], self.ufl_operands[2], self.ufl_operands[3], ) @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class CoordinateDerivative(CoefficientDerivative): """Derivative of the integrand of a form w.r.t. the SpatialCoordinates.""" __slots__ = () def __str__(self): """Format as a string.""" return "d/dfj { %s }, with fh=%s, dfh/dfj = %s, and coordinate derivatives %s" % ( self.ufl_operands[0], self.ufl_operands[1], self.ufl_operands[2], self.ufl_operands[3], ) @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class BaseFormDerivative(CoefficientDerivative, BaseForm): """Derivative of a base form w.r.t the degrees of freedom in a discrete Coefficient.""" _ufl_noslots_ = True def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): """Initalise.""" CoefficientDerivative.__init__( self, base_form, coefficients, arguments, coefficient_derivatives ) BaseForm.__init__(self) def _analyze_form_arguments(self): """Collect the arguments of the corresponding BaseForm.""" from ufl.algorithms.analysis import extract_coefficients, extract_type base_form, _, arguments, _ = self.ufl_operands def arg_type(x): if isinstance(x, BaseForm): return Coargument return Argument # Each derivative arguments can either be a: # - `ufl.BaseForm`: if it contains a `ufl.Coargument` # - or a `ufl.Expr`: if it contains a `ufl.Argument` # When a `Coargument` is encountered, it is treated as an # argument (i.e. as V* -> V* and not V* x V -> R) and should # result in one single argument (in the dual space). base_form_args = base_form.arguments() + tuple( arg for a in arguments.ufl_operands for arg in extract_type(a, arg_type(a)) ) # BaseFormDerivative's arguments don't necessarily contain BaseArgument objects only # -> e.g. `derivative(u ** 2, u, u)` with `u` a Coefficient. base_form_coeffs = base_form.coefficients() + tuple( arg for a in arguments.ufl_operands for arg in extract_coefficients(a) ) # Reconstruct arguments for correct numbering self._arguments = tuple( type(arg)(arg.ufl_function_space(), arg.number(), arg.part()) for arg in base_form_args ) self._coefficients = base_form_coeffs @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class BaseFormCoordinateDerivative(BaseFormDerivative, CoordinateDerivative): """Derivative of a base form w.r.t. the SpatialCoordinates.""" _ufl_noslots_ = True def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): """Initalise.""" BaseFormDerivative.__init__( self, base_form, coefficients, arguments, coefficient_derivatives ) @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class BaseFormOperatorDerivative(BaseFormDerivative, BaseFormOperator): """Derivative of a base form operator w.r.t the degrees of freedom in a discrete Coefficient.""" _ufl_noslots_ = True # BaseFormOperatorDerivative is only needed because of a different # differentiation procedure for BaseformOperator objects. def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): """Initalise.""" BaseFormDerivative.__init__( self, base_form, coefficients, arguments, coefficient_derivatives ) self._argument_slots = base_form._argument_slots # Enforce Operator reconstruction as Operator is a parent class of # both: BaseFormDerivative and BaseFormOperator. # Therefore the latter overwrites Operator reconstruction and we would have: # -> BaseFormOperatorDerivative._ufl_expr_reconstruct_ = # BaseFormOperator._ufl_expr_reconstruct_ _ufl_expr_reconstruct_ = Operator._ufl_expr_reconstruct_ # Set __repr__ __repr__ = Operator.__repr__ def argument_slots(self, outer_form=False): """Return a tuple of expressions containing argument and coefficient based expressions.""" from ufl.algorithms.analysis import extract_arguments base_form, _, arguments, _ = self.ufl_operands argument_slots = base_form.argument_slots(outer_form) + tuple( arg for a in arguments for arg in extract_arguments(a) ) return argument_slots @ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class BaseFormOperatorCoordinateDerivative(BaseFormOperatorDerivative, CoordinateDerivative): """Derivative of a base form operator w.r.t. the SpatialCoordinates.""" _ufl_noslots_ = True def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): """Initalise.""" BaseFormOperatorDerivative.__init__( self, base_form, coefficients, arguments, coefficient_derivatives ) @ufl_type(num_ops=2) class VariableDerivative(Derivative): """Variable Derivative.""" __slots__ = ( "ufl_shape", "ufl_free_indices", "ufl_index_dimensions", ) def __new__(cls, f, v): """Create a new VariableDerivative.""" # Checks if not isinstance(f, Expr): raise ValueError("Expecting an Expr in VariableDerivative.") if not isinstance(v, (Variable, Coefficient)): raise ValueError("Expecting a Variable in VariableDerivative.") if v.ufl_free_indices: raise ValueError("Differentiation variable cannot have free indices.") # Simplification # Return zero if expression is trivially independent of variable if f._ufl_is_terminal_ and f != v: return Zero(f.ufl_shape + v.ufl_shape, f.ufl_free_indices, f.ufl_index_dimensions) # Construction return Derivative.__new__(cls) def __init__(self, f, v): """Initalise.""" Derivative.__init__(self, (f, v)) self.ufl_free_indices = f.ufl_free_indices self.ufl_index_dimensions = f.ufl_index_dimensions self.ufl_shape = f.ufl_shape + v.ufl_shape def __str__(self): """Format as a string.""" if isinstance(self.ufl_operands[0], Terminal): return "d%s/d[%s]" % (self.ufl_operands[0], self.ufl_operands[1]) return "d/d[%s] %s" % (self.ufl_operands[1], parstr(self.ufl_operands[0], self)) # --- Compound differentiation objects --- @ufl_type(is_abstract=True) class CompoundDerivative(Derivative): """Base class for all compound derivative types.""" __slots__ = () def __init__(self, operands): """Initalise.""" Derivative.__init__(self, operands) @ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) class Grad(CompoundDerivative): """Grad.""" __slots__ = ("_dim",) def __new__(cls, f): """Create a new Grad.""" # Return zero if expression is trivially constant if is_cellwise_constant(f): dim = find_geometric_dimension(f) return Zero(f.ufl_shape + (dim,), f.ufl_free_indices, f.ufl_index_dimensions) return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) self._dim = find_geometric_dimension(f) def _ufl_expr_reconstruct_(self, op): """Return a new object of the same type with new operands.""" if is_cellwise_constant(op): if op.ufl_shape != self.ufl_operands[0].ufl_shape: raise ValueError("Operand shape mismatch in Grad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in Grad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) return self._ufl_class_(op) def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get child from mapping and return the component asked for.""" component, i = component[:-1], component[-1] derivatives = derivatives + (i,) result = self.ufl_operands[0].evaluate( x, mapping, component, index_values, derivatives=derivatives ) return result @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape + (self._dim,) def __str__(self): """Format as a string.""" return "grad(%s)" % self.ufl_operands[0] @ufl_type( num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True ) class ReferenceGrad(CompoundDerivative): """Reference grad.""" __slots__ = ("_dim",) def __new__(cls, f): """Create a new ReferenceGrad.""" # Return zero if expression is trivially constant if is_cellwise_constant(f): dim = extract_unique_domain(f).topological_dimension() return Zero(f.ufl_shape + (dim,), f.ufl_free_indices, f.ufl_index_dimensions) return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) self._dim = extract_unique_domain(f).topological_dimension() def _ufl_expr_reconstruct_(self, op): """Return a new object of the same type with new operands.""" if is_cellwise_constant(op): if op.ufl_shape != self.ufl_operands[0].ufl_shape: raise ValueError("Operand shape mismatch in ReferenceGrad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in ReferenceGrad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) return self._ufl_class_(op) def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get child from mapping and return the component asked for.""" component, i = component[:-1], component[-1] derivatives = derivatives + (i,) result = self.ufl_operands[0].evaluate( x, mapping, component, index_values, derivatives=derivatives ) return result @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape + (self._dim,) def __str__(self): """Format as a string.""" return "reference_grad(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) class Div(CompoundDerivative): """Div.""" __slots__ = () def __new__(cls, f): """Create a new Div.""" if f.ufl_free_indices: raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): return Zero(f.ufl_shape[:-1]) # No free indices asserted above return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape[:-1] def __str__(self): """Format as a string.""" return "div(%s)" % self.ufl_operands[0] @ufl_type( num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True ) class ReferenceDiv(CompoundDerivative): """Reference divergence.""" __slots__ = () def __new__(cls, f): """Create a new ReferenceDiv.""" if f.ufl_free_indices: raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): return Zero(f.ufl_shape[:-1]) # No free indices asserted above return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape[:-1] def __str__(self): """Format as a string.""" return "reference_div(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_indices_from_operand=0) class NablaGrad(CompoundDerivative): """Nabla grad.""" __slots__ = ("_dim",) def __new__(cls, f): """Create a new NablaGrad.""" # Return zero if expression is trivially constant if is_cellwise_constant(f): dim = find_geometric_dimension(f) return Zero((dim,) + f.ufl_shape, f.ufl_free_indices, f.ufl_index_dimensions) return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) self._dim = find_geometric_dimension(f) def _ufl_expr_reconstruct_(self, op): """Return a new object of the same type with new operands.""" if is_cellwise_constant(op): if op.ufl_shape != self.ufl_operands[0].ufl_shape: raise ValueError("Operand shape mismatch in NablaGrad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in NablaGrad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) return self._ufl_class_(op) @property def ufl_shape(self): """Get the UFL shape.""" return (self._dim,) + self.ufl_operands[0].ufl_shape def __str__(self): """Format as a string.""" return "nabla_grad(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_indices_from_operand=0) class NablaDiv(CompoundDerivative): """Nabla div.""" __slots__ = () def __new__(cls, f): """Create a new NablaDiv.""" if f.ufl_free_indices: raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): return Zero(f.ufl_shape[1:]) # No free indices asserted above return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape[1:] def __str__(self): """Format as a string.""" return "nabla_div(%s)" % self.ufl_operands[0] _curl_shapes = {(): (2,), (2,): (), (3,): (3,)} @ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) class Curl(CompoundDerivative): """Compound derivative.""" __slots__ = ("ufl_shape",) def __new__(cls, f): """Create a new CompoundDerivative.""" # Validate input sh = f.ufl_shape if f.ufl_shape not in ((), (2,), (3,)): raise ValueError("Expecting a scalar, 2D vector or 3D vector.") if f.ufl_free_indices: raise ValueError("Free indices in the curl argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): sh = {(): (2,), (2,): (), (3,): (3,)}[sh] return Zero(sh) # No free indices asserted above return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) self.ufl_shape = _curl_shapes[f.ufl_shape] def __str__(self): """Format as a string.""" return "curl(%s)" % self.ufl_operands[0] @ufl_type( num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True ) class ReferenceCurl(CompoundDerivative): """Reference curl.""" __slots__ = ("ufl_shape",) def __new__(cls, f): """Create a new ReferenceCurl.""" # Validate input sh = f.ufl_shape if f.ufl_shape not in ((), (2,), (3,)): raise ValueError("Expecting a scalar, 2D vector or 3D vector.") if f.ufl_free_indices: raise ValueError("Free indices in the curl argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): sh = {(): (2,), (2,): (), (3,): (3,)}[sh] return Zero(sh) # No free indices asserted above return CompoundDerivative.__new__(cls) def __init__(self, f): """Initalise.""" CompoundDerivative.__init__(self, (f,)) self.ufl_shape = _curl_shapes[f.ufl_shape] def __str__(self): """Format as a string.""" return "reference_curl(%s)" % self.ufl_operands[0] ufl-2024.2.0/ufl/domain.py000066400000000000000000000210551470142567200151510ustar00rootroot00000000000000"""Types for representing a geometric domain.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import numbers from ufl.cell import AbstractCell from ufl.core.ufl_id import attach_ufl_id from ufl.core.ufl_type import UFLObject from ufl.corealg.traversal import traverse_unique_terminals from ufl.sobolevspace import H1 # Export list for ufl.classes __all_classes__ = ["AbstractDomain", "Mesh", "MeshView"] class AbstractDomain(object): """Symbolic representation of a geometric domain. Domain has only a geometric and a topological dimension. """ def __init__(self, topological_dimension, geometric_dimension): """Initialise.""" # Validate dimensions if not isinstance(geometric_dimension, numbers.Integral): raise ValueError( f"Expecting integer geometric dimension, not {geometric_dimension.__class__}" ) if not isinstance(topological_dimension, numbers.Integral): raise ValueError( f"Expecting integer topological dimension, not {topological_dimension.__class__}" ) if topological_dimension > geometric_dimension: raise ValueError("Topological dimension cannot be larger than geometric dimension.") # Store validated dimensions self._topological_dimension = topological_dimension self._geometric_dimension = geometric_dimension def geometric_dimension(self): """Return the dimension of the space this domain is embedded in.""" return self._geometric_dimension def topological_dimension(self): """Return the dimension of the topology of this domain.""" return self._topological_dimension # TODO: Would it be useful to have a domain representing R^d? E.g. for # Expression. # class EuclideanSpace(AbstractDomain): # def __init__(self, geometric_dimension): # AbstractDomain.__init__(self, geometric_dimension, geometric_dimension) @attach_ufl_id class Mesh(AbstractDomain, UFLObject): """Symbolic representation of a mesh.""" def __init__(self, coordinate_element, ufl_id=None, cargo=None): """Initialise.""" self._ufl_id = self._init_ufl_id(ufl_id) # Store reference to object that will not be used by UFL self._ufl_cargo = cargo if cargo is not None and cargo.ufl_id() != self._ufl_id: raise ValueError("Expecting cargo object (e.g. dolfin.Mesh) to have the same ufl_id.") # No longer accepting coordinates provided as a Coefficient from ufl.coefficient import Coefficient if isinstance(coordinate_element, (Coefficient, AbstractCell)): raise ValueError("Expecting a coordinate element in the ufl.Mesh construct.") # Store coordinate element self._ufl_coordinate_element = coordinate_element # Derive dimensions from element (gdim,) = coordinate_element.reference_value_shape tdim = coordinate_element.cell.topological_dimension() AbstractDomain.__init__(self, tdim, gdim) def ufl_cargo(self): """Return carried object that will not be used by UFL.""" return self._ufl_cargo def ufl_coordinate_element(self): """Get the coordinate element.""" return self._ufl_coordinate_element def ufl_cell(self): """Get the cell.""" return self._ufl_coordinate_element.cell def is_piecewise_linear_simplex_domain(self): """Check if the domain is a piecewise linear simplex.""" ce = self._ufl_coordinate_element return ce.embedded_superdegree <= 1 and ce in H1 and self.ufl_cell().is_simplex() def __repr__(self): """Representation.""" r = "Mesh(%s, %s)" % (repr(self._ufl_coordinate_element), repr(self._ufl_id)) return r def __str__(self): """Format as a string.""" return "" % (self._ufl_id,) def _ufl_hash_data_(self): """UFL hash data.""" return (self._ufl_id, self._ufl_coordinate_element) def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return ("Mesh", renumbering[self], self._ufl_coordinate_element) # NB! Dropped __lt__ here, don't want users to write 'mesh1 < # mesh2'. def _ufl_sort_key_(self): """UFL sort key.""" typespecific = (self._ufl_id, self._ufl_coordinate_element) return (self.geometric_dimension(), self.topological_dimension(), "Mesh", typespecific) @attach_ufl_id class MeshView(AbstractDomain, UFLObject): """Symbolic representation of a mesh.""" def __init__(self, mesh, topological_dimension, ufl_id=None): """Initialise.""" self._ufl_id = self._init_ufl_id(ufl_id) # Store mesh self._ufl_mesh = mesh # Derive dimensions from element coordinate_element = mesh.ufl_coordinate_element() (gdim,) = coordinate_element.value_shape tdim = coordinate_element.cell.topological_dimension() AbstractDomain.__init__(self, tdim, gdim) def ufl_mesh(self): """Get the mesh.""" return self._ufl_mesh def ufl_cell(self): """Get the cell.""" return self._ufl_mesh.ufl_cell() def is_piecewise_linear_simplex_domain(self): """Check if the domain is a piecewise linear simplex.""" return self._ufl_mesh.is_piecewise_linear_simplex_domain() def __repr__(self): """Representation.""" tdim = self.topological_dimension() r = "MeshView(%s, %s, %s)" % (repr(self._ufl_mesh), repr(tdim), repr(self._ufl_id)) return r def __str__(self): """Format as a string.""" return "" % ( self._ufl_id, self.topological_dimension(), self._ufl_mesh, ) def _ufl_hash_data_(self): """UFL hash data.""" return (self._ufl_id,) + self._ufl_mesh._ufl_hash_data_() def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return ("MeshView", renumbering[self], self._ufl_mesh._ufl_signature_data_(renumbering)) # NB! Dropped __lt__ here, don't want users to write 'mesh1 < # mesh2'. def _ufl_sort_key_(self): """UFL sort key.""" typespecific = (self._ufl_id, self._ufl_mesh) return (self.geometric_dimension(), self.topological_dimension(), "MeshView", typespecific) def as_domain(domain): """Convert any valid object to an AbstractDomain type.""" if isinstance(domain, AbstractDomain): # Modern UFL files and dolfin behaviour return domain try: return extract_unique_domain(domain) except AttributeError: return domain.ufl_domain() def sort_domains(domains): """Sort domains in a canonical ordering.""" return tuple(sorted(domains, key=lambda domain: domain._ufl_sort_key_())) def join_domains(domains): """Take a list of domains and return a tuple with only unique domain objects. Checks that domains with the same id are compatible. """ # Use hashing to join domains, ignore None domains = set(domains) - set((None,)) if not domains: return () # Check geometric dimension compatibility gdims = set() for domain in domains: gdims.add(domain.geometric_dimension()) if len(gdims) != 1: raise ValueError("Found domains with different geometric dimensions.") return domains # TODO: Move these to an analysis module? def extract_domains(expr): """Return all domains expression is defined on.""" domainlist = [] for t in traverse_unique_terminals(expr): domainlist.extend(t.ufl_domains()) return sorted( join_domains(domainlist), key=lambda D: (D.topological_dimension(), D.ufl_cell(), D.ufl_id()), ) def extract_unique_domain(expr): """Return the single unique domain expression is defined on or throw an error.""" domains = extract_domains(expr) if len(domains) == 1: return domains[0] elif domains: raise ValueError("Found multiple domains, cannot return just one.") else: return None def find_geometric_dimension(expr): """Find the geometric dimension of an expression.""" gdims = set() for t in traverse_unique_terminals(expr): domain = extract_unique_domain(t) if domain is not None: gdims.add(domain.geometric_dimension()) if len(gdims) != 1: raise ValueError("Cannot determine geometric dimension from expression.") (gdim,) = gdims return gdim ufl-2024.2.0/ufl/duals.py000066400000000000000000000014341470142567200150110ustar00rootroot00000000000000"""Predicates for recognising duals.""" # Copyright (C) 2021 India Marsden # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # def is_primal(object): """Determine if the object belongs to a primal space. This is not simply the negation of is_dual, because a mixed function space containing both primal and dual components is neither primal nor dual. """ return hasattr(object, "_primal") and object._primal def is_dual(object): """Determine if the object belongs to a dual space. This is not simply the negation of is_primal, because a mixed function space containing both primal and dual components is neither primal nor dual. """ return hasattr(object, "_dual") and object._dual ufl-2024.2.0/ufl/equation.py000066400000000000000000000035461470142567200155340ustar00rootroot00000000000000"""The Equation class, used to express equations like a == L.""" # Copyright (C) 2012-2016 Anders Logg and Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # Export list for ufl.classes __all_classes__ = ["Equation"] class Equation(object): """Equation. This class is used to represent equations expressed by the "==" operator. Examples include a == L and F == 0 where a, L and F are Form objects. """ def __init__(self, lhs, rhs): """Create equation lhs == rhs.""" self.lhs = lhs self.rhs = rhs def __bool__(self): """Evaluate bool(lhs_form == rhs_form). This will not trigger when setting 'equation = a == L', but when e.g. running 'if equation:'. """ # NB!: pep8 will say you should use isinstance here, but we do # actually want to compare the exact types in this case. # Not equal if types are not identical (i.e. not accepting # subclasses) if type(self.lhs) != type(self.rhs): # noqa: E721 return False # Try to delegate to equals function if hasattr(self.lhs, "equals"): return self.lhs.equals(self.rhs) elif hasattr(self.rhs, "equals"): return self.rhs.equals(self.lhs) else: raise ValueError("Either lhs or rhs of Equation must implement self.equals(other).") __nonzero__ = __bool__ def __eq__(self, other): """Compare two equations by comparing lhs and rhs.""" return isinstance(other, Equation) and self.lhs == other.lhs and self.rhs == other.rhs def __hash__(self): """Hash.""" return hash((hash(self.lhs), hash(self.rhs))) def __repr__(self): """Representation.""" return f"Equation({self.lhs!r}, {self.rhs!r})" ufl-2024.2.0/ufl/exprcontainers.py000066400000000000000000000075401470142567200167510ustar00rootroot00000000000000"""This module defines special types for representing mapping of expressions to expressions.""" # Copyright (C) 2014 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.argument import Coargument from ufl.coefficient import Cofunction from ufl.core.expr import Expr from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type # --- Non-tensor types --- @ufl_type(num_ops="varying") class ExprList(Operator): """List of Expr objects. For internal use, never to be created by end users.""" __slots__ = () def __init__(self, *operands): """Initialise.""" Operator.__init__(self, operands) # Enable Cofunction/Coargument for BaseForm differentiation if not all(isinstance(i, (Expr, Cofunction, Coargument)) for i in operands): raise ValueError("Expecting Expr, Cofunction or Coargument in ExprList.") def __getitem__(self, i): """Get an item.""" return self.ufl_operands[i] def __len__(self): """Get the length.""" return len(self.ufl_operands) def __iter__(self): """Return iterable.""" return iter(self.ufl_operands) def __str__(self): """Format as a string.""" return "ExprList(*(%s,))" % ", ".join(str(i) for i in self.ufl_operands) def __repr__(self): """Representation.""" r = "ExprList(*%s)" % repr(self.ufl_operands) return r @property def ufl_shape(self): """Get the UFL shape.""" raise ValueError("A non-tensor type has no ufl_shape.") @property def ufl_free_indices(self): """Get the UFL free indices.""" raise ValueError("A non-tensor type has no ufl_free_indices.") def free_indices(self): """Get the free indices.""" raise ValueError("A non-tensor type has no free_indices.") @property def ufl_index_dimensions(self): """Get the UFL index dimensions.""" raise ValueError("A non-tensor type has no ufl_index_dimensions.") def index_dimensions(self): """Get the index dimensions.""" raise ValueError("A non-tensor type has no index_dimensions.") @ufl_type(num_ops="varying") class ExprMapping(Operator): """Mapping of Expr objects. For internal use, never to be created by end users.""" __slots__ = () def __init__(self, *operands): """Initialise.""" Operator.__init__(self, operands) if not all(isinstance(e, Expr) for e in operands): raise ValueError("Expecting Expr in ExprMapping.") def ufl_domains(self): """Get the UFL domains.""" # Because this type can act like a terminal if it has no # operands, we need to override some recursive operations if self.ufl_operands: return Operator.ufl_domains() else: return [] def __str__(self): """Format as a string.""" return "ExprMapping(*%s)" % repr(self.ufl_operands) def __repr__(self): """Representation.""" r = "ExprMapping(*%s)" % repr(self.ufl_operands) return r @property def ufl_shape(self): """Get the UFL shape.""" raise ValueError("A non-tensor type has no ufl_shape.") @property def ufl_free_indices(self): """Get the UFL free indices.""" raise ValueError("A non-tensor type has no ufl_free_indices.") def free_indices(self): """Get the free indices.""" raise ValueError("A non-tensor type has no free_indices.") @property def ufl_index_dimensions(self): """Get the UFL index dimensions.""" raise ValueError("A non-tensor type has no ufl_index_dimensions.") def index_dimensions(self): """Get the index dimensions.""" raise ValueError("A non-tensor type has no index_dimensions.") ufl-2024.2.0/ufl/exprequals.py000066400000000000000000000032251470142567200160720ustar00rootroot00000000000000"""Expr equals.""" from collections import defaultdict hash_total = defaultdict(int) hash_collisions = defaultdict(int) hash_equals = defaultdict(int) hash_notequals = defaultdict(int) def expr_equals(self, other): """Checks whether the two expressions are represented the exact same way. This does not check if the expressions are mathematically equal or equivalent! Used by sets and dicts. """ # Fast cutoffs for common cases, type difference or hash # difference will cutoff more or less all nonequal types if type(self) is not type(other) or hash(self) != hash(other): return False # Large objects are costly to compare with themselves if self is other: return True # Modelled after pre_traversal to avoid recursion: left = [(self, other)] while left: s, o = left.pop() if s._ufl_is_terminal_: # Compare terminals if not s == o: return False else: # Delve into subtrees so = s.ufl_operands oo = o.ufl_operands if len(so) != len(oo): return False for s, o in zip(so, oo): # Fast cutoff for common case if s._ufl_typecode_ != o._ufl_typecode_: return False # Skip subtree if objects are the same if s is o: continue # Append subtree for further inspection left.append((s, o)) # Equal if we get out of the above loop! # Eagerly DAGify to reduce the size of the tree. self.ufl_operands = other.ufl_operands return True ufl-2024.2.0/ufl/exproperators.py000066400000000000000000000264721470142567200166270ustar00rootroot00000000000000"""Expr operators. This module attaches special functions to Expr. This way we avoid circular dependencies between e.g. Sum and its superclass Expr. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016. import numbers from ufl.algebra import Abs, Division, Power, Product, Sum from ufl.conditional import GE, GT, LE, LT from ufl.constantvalue import Zero, as_ufl from ufl.core.expr import Expr from ufl.core.multiindex import Index, MultiIndex, indices from ufl.differentiation import Grad from ufl.exprequals import expr_equals from ufl.index_combination_utils import create_slice_indices, merge_overlapping_indices from ufl.indexed import Indexed from ufl.indexsum import IndexSum from ufl.restriction import NegativeRestricted, PositiveRestricted from ufl.tensoralgebra import Inner, Transposed from ufl.tensors import ComponentTensor, as_tensor from ufl.utils.stacks import StackDict # --- Boolean operators --- def _le(left, right): """A boolean expresion (left <= right) for use with conditional.""" return LE(left, right) def _ge(left, right): """A boolean expresion (left >= right) for use with conditional.""" return GE(left, right) def _lt(left, right): """A boolean expresion (left < right) for use with conditional.""" return LT(left, right) def _gt(left, right): """A boolean expresion (left > right) for use with conditional.""" return GT(left, right) # '==' needs to implement comparison of expression representations for # use in hashmaps (dict and set), but the others can be overloaded in # the language. It is possible that we can overload eq as well, but # we'll need to fix some issues first and also check for a possible # significant performance hit with compilation of complex # forms. Replacing a==b with equiv(a,b) all over the code could be one # way to reduce such a performance hit, but we cannot do anything # about dict and set calling __eq__... Expr.__eq__ = expr_equals # != is used at least by tests, possibly in code as well, and must # mean the opposite of ==, i.e. when evaluated as bool it must mean # 'not equal representation'. def _ne(self, other): return not self.__eq__(other) Expr.__ne__ = _ne Expr.__lt__ = _lt Expr.__gt__ = _gt Expr.__le__ = _le Expr.__ge__ = _ge # Python operators 'and'/'or' cannot be overloaded, and bitwise # operators &/| don't have the right precedence levels # Expr.__and__ = _and # Expr.__or__ = _or def _as_tensor(self, indices): """A^indices := as_tensor(A, indices).""" if not isinstance(indices, tuple): raise ValueError( "Expecting a tuple of Index objects to A^indices := as_tensor(A, indices)." ) if not all(isinstance(i, Index) for i in indices): raise ValueError( "Expecting a tuple of Index objects to A^indices := as_tensor(A, indices)." ) return as_tensor(self, indices) Expr.__xor__ = _as_tensor # --- Helper functions for product handling --- def _mult(a, b): """Multiply.""" # Discover repeated indices, which results in index sums afi = a.ufl_free_indices bfi = b.ufl_free_indices afid = a.ufl_index_dimensions bfid = b.ufl_index_dimensions fi, fid, ri, rid = merge_overlapping_indices(afi, afid, bfi, bfid) # Pick out valid non-scalar products here (dot products): # - matrix-matrix (A*B, M*grad(u)) => A . B # - matrix-vector (A*v) => A . v s1, s2 = a.ufl_shape, b.ufl_shape r1, r2 = len(s1), len(s2) if r1 == 0 and r2 == 0: # Create scalar product p = Product(a, b) ti = () elif r1 == 0 or r2 == 0: # Scalar - tensor product if r2 == 0: a, b = b, a # Check for zero, simplifying early if possible if isinstance(a, Zero) or isinstance(b, Zero): shape = s1 or s2 return Zero(shape, fi, fid) # Repeated indices are allowed, like in: # v[i]*M[i,:] # Apply product to scalar components ti = indices(len(b.ufl_shape)) p = Product(a, b[ti]) elif r1 == 2 and r2 in (1, 2): # Matrix-matrix or matrix-vector if ri: raise ValueError("Not expecting repeated indices in non-scalar product.") # Check for zero, simplifying early if possible if isinstance(a, Zero) or isinstance(b, Zero): shape = s1[:-1] + s2[1:] return Zero(shape, fi, fid) # Return dot product in index notation ai = indices(len(a.ufl_shape) - 1) bi = indices(len(b.ufl_shape) - 1) k = indices(1) p = a[ai + k] * b[k + bi] ti = ai + bi else: raise ValueError(f"Invalid ranks {r1} and {r2} in product.") # TODO: I think applying as_tensor after index sums results in # cleaner expression graphs. # Wrap as tensor again if ti: p = as_tensor(p, ti) # If any repeated indices were found, apply implicit summation # over those for i in ri: mi = MultiIndex((Index(count=i),)) p = IndexSum(p, mi) return p # --- Extend Expr with algebraic operators --- _valid_types = (Expr, numbers.Real, numbers.Integral, numbers.Complex) def _mul(self, o): """Multiply.""" if not isinstance(o, _valid_types): return NotImplemented o = as_ufl(o) return _mult(self, o) Expr.__mul__ = _mul def _rmul(self, o): """Multiply.""" if not isinstance(o, _valid_types): return NotImplemented o = as_ufl(o) return _mult(o, self) Expr.__rmul__ = _rmul def _add(self, o): """Add.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(self, o) Expr.__add__ = _add def _radd(self, o): """Add.""" if not isinstance(o, _valid_types): return NotImplemented if isinstance(o, numbers.Number) and o == 0: # Allow adding scalar int 0 as a no-op, even for shaped self, # needed for sum([a,b]) return self return Sum(o, self) Expr.__radd__ = _radd def _sub(self, o): """Subtract.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(self, -o) Expr.__sub__ = _sub def _rsub(self, o): """Subtract.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(o, -self) Expr.__rsub__ = _rsub def _div(self, o): """Divide.""" if not isinstance(o, _valid_types): return NotImplemented sh = self.ufl_shape if sh: ii = indices(len(sh)) d = Division(self[ii], o) return as_tensor(d, ii) return Division(self, o) Expr.__div__ = _div Expr.__truediv__ = _div def _rdiv(self, o): """Divide.""" if not isinstance(o, _valid_types): return NotImplemented return Division(o, self) Expr.__rdiv__ = _rdiv Expr.__rtruediv__ = _rdiv def _pow(self, o): """Raise to a power.""" if not isinstance(o, _valid_types): return NotImplemented if o == 2 and self.ufl_shape: return Inner(self, self) return Power(self, o) Expr.__pow__ = _pow def _rpow(self, o): """Raise to a power.""" if not isinstance(o, _valid_types): return NotImplemented return Power(o, self) Expr.__rpow__ = _rpow # TODO: Add Negated class for this? Might simplify reductions in Add. def _neg(self): """Negate.""" return -1 * self Expr.__neg__ = _neg def _abs(self): """Absolute value.""" return Abs(self) Expr.__abs__ = _abs # --- Extend Expr with restiction operators a("+"), a("-") --- def _restrict(self, side): """Restrict.""" if side == "+": return PositiveRestricted(self) if side == "-": return NegativeRestricted(self) raise ValueError(f"Invalid side '{side}' in restriction operator.") def _eval(self, coord, mapping=None, component=()): """Evaluate. Evaluate expression at this particular coordinate, with provided values for other terminals in mapping. """ # Evaluate derivatives first from ufl.algorithms import expand_derivatives f = expand_derivatives(self) # Evaluate recursively if mapping is None: mapping = {} index_values = StackDict() return f.evaluate(coord, mapping, component, index_values) def _call(self, arg, mapping=None, component=()): """Take the restriction or evaluate depending on argument.""" if arg in ("+", "-"): if mapping is not None: raise ValueError("Not expecting a mapping when taking restriction.") return _restrict(self, arg) else: return _eval(self, arg, mapping, component) Expr.__call__ = _call # --- Extend Expr with the transpose operation A.T --- def _transpose(self): """Transpose a rank-2 tensor expression. For more general transpose operations of higher order tensor expressions, use indexing and Tensor. """ return Transposed(self) Expr.T = property(_transpose) # --- Extend Expr with indexing operator a[i] --- def _getitem(self, component): """Get an item.""" # Treat component consistently as tuple below if not isinstance(component, tuple): component = (component,) shape = self.ufl_shape # Analyse slices (:) and Ellipsis (...) all_indices, slice_indices, repeated_indices = create_slice_indices( component, shape, self.ufl_free_indices ) # Check that we have the right number of indices for a tensor with # this shape if len(shape) != len(all_indices): raise ValueError( f"Invalid number of indices {len(all_indices)} for expression of rank {len(shape)}." ) # Special case for simplifying foo[...] => foo, foo[:] => foo or # similar if len(slice_indices) == len(all_indices): return self # Special case for simplifying as_tensor(ai,(i,))[i] => ai if isinstance(self, ComponentTensor): if all_indices == self.indices().indices(): return self.ufl_operands[0] # Apply all indices to index self, yielding a scalar valued # expression mi = MultiIndex(all_indices) a = Indexed(self, mi) # TODO: I think applying as_tensor after index sums results in # cleaner expression graphs. # If the Ellipsis or any slices were found, wrap as tensor valued # with the slice indices created at the top here if slice_indices: a = as_tensor(a, slice_indices) # If any repeated indices were found, apply implicit summation # over those for i in repeated_indices: mi = MultiIndex((i,)) a = IndexSum(a, mi) # Check for zero (last so we can get indices etc from a, could # possibly be done faster by checking early instead) if isinstance(self, Zero): shape = a.ufl_shape fi = a.ufl_free_indices fid = a.ufl_index_dimensions a = Zero(shape, fi, fid) return a Expr.__getitem__ = _getitem # --- Extend Expr with spatial differentiation operator a.dx(i) --- def _dx(self, *ii): """Return the partial derivative with respect to spatial variable number *ii*.""" d = self # Unwrap ii to allow .dx(i,j) and .dx((i,j)) if len(ii) == 1 and isinstance(ii[0], tuple): ii = ii[0] # Apply all derivatives for i in ii: d = Grad(d) # Take all components, applying repeated index sums in the [] operation return d.__getitem__((Ellipsis,) + ii) Expr.dx = _dx ufl-2024.2.0/ufl/finiteelement.py000066400000000000000000000317341470142567200165370ustar00rootroot00000000000000"""This module defines the UFL finite element classes.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Kristian B. Oelgaard # Modified by Marie E. Rognes 2010, 2012 # Modified by Massimiliano Leoni, 2016 # Modified by Matthew Scroggs, 2023 from __future__ import annotations import abc as _abc import typing as _typing from ufl.cell import Cell as _Cell from ufl.pullback import AbstractPullback as _AbstractPullback from ufl.pullback import IdentityPullback as _IdentityPullback from ufl.pullback import MixedPullback as _MixedPullback from ufl.pullback import SymmetricPullback as _SymmetricPullback from ufl.sobolevspace import SobolevSpace as _SobolevSpace from ufl.utils.sequences import product __all_classes__ = ["AbstractFiniteElement", "FiniteElement", "MixedElement", "SymmetricElement"] class AbstractFiniteElement(_abc.ABC): """Base class for all finite elements. To make your element library compatible with UFL, you should make a subclass of AbstractFiniteElement and provide implementions of all the abstract methods and properties. All methods and properties that are not marked as abstract are implemented here and should not need to be overwritten in your subclass. An example of how the methods in your subclass could be implemented can be found in Basix; see https://github.com/FEniCS/basix/blob/main/python/basix/ufl.py """ @_abc.abstractmethod def __repr__(self) -> str: """Format as string for evaluation as Python object.""" @_abc.abstractmethod def __str__(self) -> str: """Format as string for nice printing.""" @_abc.abstractmethod def __hash__(self) -> int: """Return a hash.""" @_abc.abstractmethod def __eq__(self, other: AbstractFiniteElement) -> bool: """Check if this element is equal to another element.""" @_abc.abstractproperty def sobolev_space(self) -> _SobolevSpace: """Return the underlying Sobolev space.""" @_abc.abstractproperty def pullback(self) -> _AbstractPullback: """Return the pullback for this element.""" @_abc.abstractproperty def embedded_superdegree(self) -> _typing.Union[int, None]: """Degree of the minimum degree Lagrange space that spans this element. This returns the degree of the lowest degree Lagrange space such that the polynomial space of the Lagrange space is a superspace of this element's polynomial space. If this element contains basis functions that are not in any Lagrange space, this function should return None. Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1 Lagrange space includes the degree 2 polynomial xy. """ @_abc.abstractproperty def embedded_subdegree(self) -> int: """Degree of the maximum degree Lagrange space that is spanned by this element. This returns the degree of the highest degree Lagrange space such that the polynomial space of the Lagrange space is a subspace of this element's polynomial space. If this element's polynomial space does not include the constant function, this function should return -1. Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1 Lagrange space includes the degree 2 polynomial xy. """ @_abc.abstractproperty def cell(self) -> _Cell: """Return the cell of the finite element.""" @_abc.abstractproperty def reference_value_shape(self) -> _typing.Tuple[int, ...]: """Return the shape of the value space on the reference cell.""" @_abc.abstractproperty def sub_elements(self) -> _typing.List: """Return list of sub-elements. This function does not recurse: ie it does not extract the sub-elements of sub-elements. """ def __ne__(self, other: AbstractFiniteElement) -> bool: """Check if this element is different to another element.""" return not self.__eq__(other) def is_cellwise_constant(self) -> bool: """Check whether this element is spatially constant over each cell.""" return self.embedded_superdegree == 0 def _ufl_hash_data_(self) -> str: """Return UFL hash data.""" return repr(self) def _ufl_signature_data_(self) -> str: """Return UFL signature data.""" return repr(self) @property def reference_value_size(self) -> int: """Return the integer product of the reference value shape.""" return product(self.reference_value_shape) @property def num_sub_elements(self) -> int: """Return number of sub-elements. This function does not recurse: ie it does not count the sub-elements of sub-elements. """ return len(self.sub_elements) class FiniteElement(AbstractFiniteElement): """A directly defined finite element.""" __slots__ = ( "_repr", "_str", "_family", "_cell", "_degree", "_reference_value_shape", "_pullback", "_sobolev_space", "_sub_elements", "_subdegree", ) def __init__( self, family: str, cell: _Cell, degree: int, reference_value_shape: _typing.Tuple[int, ...], pullback: _AbstractPullback, sobolev_space: _SobolevSpace, sub_elements=[], _repr: _typing.Optional[str] = None, _str: _typing.Optional[str] = None, subdegree: _typing.Optional[int] = None, ): """Initialise a finite element. This class should only be used for testing Args: family: The family name of the element cell: The cell on which the element is defined degree: The polynomial degree of the element reference_value_shape: The reference value shape of the element pullback: The pullback to use sobolev_space: The Sobolev space containing this element sub_elements: Sub elements of this element _repr: A string representation of this elements _str: A string for printing subdegree: The embedded subdegree of this element """ if subdegree is None: self._subdegree = degree else: self._subdegree = subdegree if _repr is None: if len(sub_elements) > 0: self._repr = ( f'ufl.finiteelement.FiniteElement("{family}", {cell}, {degree}, ' f"{reference_value_shape}, {pullback}, {sobolev_space}, {sub_elements!r})" ) else: self._repr = ( f'ufl.finiteelement.FiniteElement("{family}", {cell}, {degree}, ' f"{reference_value_shape}, {pullback}, {sobolev_space})" ) else: self._repr = _repr if _str is None: self._str = f"<{family}{degree} on a {cell}>" else: self._str = _str self._family = family self._cell = cell self._degree = degree self._reference_value_shape = reference_value_shape self._pullback = pullback self._sobolev_space = sobolev_space self._sub_elements = sub_elements def __repr__(self) -> str: """Format as string for evaluation as Python object.""" return self._repr def __str__(self) -> str: """Format as string for nice printing.""" return self._str def __hash__(self) -> int: """Return a hash.""" return hash(f"{self!r}") def __eq__(self, other) -> bool: """Check if this element is equal to another element.""" return type(self) is type(other) and repr(self) == repr(other) @property def sobolev_space(self) -> _SobolevSpace: """Return the underlying Sobolev space.""" return self._sobolev_space @property def pullback(self) -> _AbstractPullback: """Return the pullback for this element.""" return self._pullback @property def embedded_superdegree(self) -> _typing.Union[int, None]: """Degree of the minimum degree Lagrange space that spans this element. This returns the degree of the lowest degree Lagrange space such that the polynomial space of the Lagrange space is a superspace of this element's polynomial space. If this element contains basis functions that are not in any Lagrange space, this function should return None. Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1 Lagrange space includes the degree 2 polynomial xy. """ return self._degree @property def embedded_subdegree(self) -> int: """Degree of the maximum degree Lagrange space that is spanned by this element. This returns the degree of the highest degree Lagrange space such that the polynomial space of the Lagrange space is a subspace of this element's polynomial space. If this element's polynomial space does not include the constant function, this function should return -1. Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1 Lagrange space includes the degree 2 polynomial xy. """ return self._subdegree @property def cell(self) -> _Cell: """Return the cell of the finite element.""" return self._cell @property def reference_value_shape(self) -> _typing.Tuple[int, ...]: """Return the shape of the value space on the reference cell.""" return self._reference_value_shape @property def sub_elements(self) -> _typing.List: """Return list of sub-elements. This function does not recurse: ie it does not extract the sub-elements of sub-elements. """ return self._sub_elements class SymmetricElement(FiniteElement): """A symmetric finite element.""" def __init__( self, symmetry: _typing.Dict[_typing.Tuple[int, ...], int], sub_elements: _typing.List[AbstractFiniteElement], ): """Initialise a symmetric element. This class should only be used for testing Args: symmetry: Map from physical components to reference components sub_elements: Sub-elements of this element """ self._sub_elements = sub_elements pullback = _SymmetricPullback(self, symmetry) reference_value_shape = (sum(e.reference_value_size for e in sub_elements),) degree = max(e.embedded_superdegree for e in sub_elements) cell = sub_elements[0].cell for e in sub_elements: if e.cell != cell: raise ValueError("All sub-elements must be defined on the same cell") sobolev_space = max(e.sobolev_space for e in sub_elements) super().__init__( "Symmetric element", cell, degree, reference_value_shape, pullback, sobolev_space, sub_elements=sub_elements, _repr=(f"ufl.finiteelement.SymmetricElement({symmetry!r}, {sub_elements!r})"), _str=f"", ) class MixedElement(FiniteElement): """A mixed element.""" def __init__(self, sub_elements): """Initialise a mixed element. This class should only be used for testing Args: sub_elements: Sub-elements of this element """ sub_elements = [MixedElement(e) if isinstance(e, list) else e for e in sub_elements] cell = sub_elements[0].cell for e in sub_elements: assert e.cell == cell degree = max(e.embedded_superdegree for e in sub_elements) reference_value_shape = (sum(e.reference_value_size for e in sub_elements),) if all(isinstance(e.pullback, _IdentityPullback) for e in sub_elements): pullback = _IdentityPullback() else: pullback = _MixedPullback(self) sobolev_space = max(e.sobolev_space for e in sub_elements) super().__init__( "Mixed element", cell, degree, reference_value_shape, pullback, sobolev_space, sub_elements=sub_elements, _repr=f"ufl.finiteelement.MixedElement({sub_elements!r})", _str=f"", ) ufl-2024.2.0/ufl/form.py000066400000000000000000000771731470142567200146610ustar00rootroot00000000000000"""The Form class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2011. # Modified by Massimiliano Leoni, 2016. # Modified by Cecile Daversin-Catty, 2018. # Modified by Nacime Bouziani, 2020. # Modified by Jørgen S. Dokken 2023. import numbers import warnings from collections import defaultdict from itertools import chain from ufl.checks import is_scalar_constant_expression from ufl.constant import Constant from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str from ufl.core.ufl_type import UFLType, ufl_type from ufl.domain import extract_unique_domain, sort_domains from ufl.equation import Equation from ufl.integral import Integral from ufl.utils.counted import Counted from ufl.utils.sorting import sorted_by_count # Export list for ufl.classes __all_classes__ = ["Form", "BaseForm", "ZeroBaseForm"] # --- The Form class, representing a complete variational form or functional --- def _sorted_integrals(integrals): """Sort integrals for a stable signature computation. Sort integrals by domain id, integral type, subdomain id for a more stable signature computation. """ # Group integrals in multilevel dict by keys # [domain][integral_type][subdomain_id] integrals_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) for integral in integrals: d = integral.ufl_domain() if d is None: raise ValueError( "Each integral in a form must have a uniquely defined integration domain." ) it = integral.integral_type() si = integral.subdomain_id() integrals_dict[d][it][si] += [integral] all_integrals = [] def keyfunc(item): if isinstance(item, numbers.Integral): sid_int = item else: # As subdomain ids can be either int or tuples, we need to compare them sid_int = tuple(-1 if i == "otherwise" else i for i in item) return (type(item).__name__, sid_int) # Order integrals canonically to increase signature stability for d in sort_domains(integrals_dict): for it in sorted(integrals_dict[d]): # str is sortable for si in sorted(integrals_dict[d][it], key=keyfunc): unsorted_integrals = integrals_dict[d][it][si] # TODO: At this point we could order integrals by # metadata and integrand, or even add the # integrands with the same metadata. This is done # in accumulate_integrands_with_same_metadata in # algorithms/domain_analysis.py and would further # increase the signature stability. all_integrals.extend(unsorted_integrals) # integrals_dict[d][it][si] = unsorted_integrals return tuple(all_integrals) # integrals_dict @ufl_type() class BaseForm(object, metaclass=UFLType): """Description of an object containing arguments.""" # Slots is kept empty to enable multiple inheritance with other # classes __slots__ = () _ufl_is_abstract_ = True _ufl_required_methods_ = ("_analyze_form_arguments", "_analyze_domains", "ufl_domains") def __init__(self): """Initialise.""" # Internal variables for caching form argument/coefficient data self._arguments = None self._coefficients = None # --- Accessor interface --- def arguments(self): """Return all ``Argument`` objects found in form.""" if self._arguments is None: self._analyze_form_arguments() return self._arguments def coefficients(self): """Return all ``Coefficient`` objects found in form.""" if self._coefficients is None: self._analyze_form_arguments() return self._coefficients def ufl_domain(self): """Return the single geometric integration domain occuring in the base form. Fails if multiple domains are found. """ try: (domain,) = set(self.ufl_domains()) except ValueError: raise ValueError("%s must have exactly one domain." % type(self).__name__) # Return the one and only domain return domain # --- Operator implementations --- def __eq__(self, other): """Delayed evaluation of the == operator. Just 'lhs_form == rhs_form' gives an Equation, while 'bool(lhs_form == rhs_form)' delegates to lhs_form.equals(rhs_form). """ return Equation(self, other) def __radd__(self, other): """Add.""" # Ordering of form additions make no difference return self.__add__(other) def __add__(self, other): """Add.""" if isinstance(other, numbers.Number) and other == 0: # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b]) return self elif isinstance(other, Zero): # Allow adding ufl Zero as a no-op, needed for sum([a,b]) return self elif isinstance(other, ZeroBaseForm): # Simplify addition with ZeroBaseForm return self # For `ZeroBaseForm(...) + B` with B a BaseForm. # We could overwrite ZeroBaseForm.__add__ but that implies # duplicating cases with `0` and `ufl.Zero`. elif isinstance(self, ZeroBaseForm): # Simplify addition with ZeroBaseForm return other elif isinstance(other, BaseForm): # Add integrals from both forms return FormSum((self, 1), (other, 1)) else: # Let python protocols do their job if we don't handle it return NotImplemented def __sub__(self, other): """Subtract other form from this one.""" return self + (-other) def __rsub__(self, other): """Subtract this form from other.""" return other + (-self) def __neg__(self): """Negate all integrals in form. This enables the handy "-form" syntax for e.g. the linearized system (J, -F) from a nonlinear form F. """ if isinstance(self, ZeroBaseForm): # `-` doesn't change anything for ZeroBaseForm. # This also facilitates simplifying FormSum containing ZeroBaseForm objects. return self return FormSum((self, -1)) def __rmul__(self, scalar): """Multiply all integrals in form with constant scalar value.""" # This enables the handy "0*form" or "dt*form" syntax if is_scalar_constant_expression(scalar): return FormSum((self, scalar)) return NotImplemented def __mul__(self, coefficient): """Take the action of this form on the given coefficient.""" if isinstance(coefficient, Expr): from ufl.formoperators import action return action(self, coefficient) return NotImplemented def __ne__(self, other): """Immediately evaluate the != operator (as opposed to the == operator).""" return not self.equals(other) def __call__(self, x): """Take the action of this form on ``x``.""" from ufl.formoperators import action return action(self, x) def _ufl_compute_hash_(self): """Compute the hash.""" # Ensure compatibility with MultiFunction # `hash(self)` will call the `__hash__` method of the subclass. return hash(self) def _ufl_expr_reconstruct_(self, *operands): """Return a new object of the same type with new operands.""" return type(self)(*operands) __matmul__ = __mul__ @ufl_type() class Form(BaseForm): """Description of a weak form consisting of a sum of integrals over subdomains.""" __slots__ = ( # --- List of Integral objects (a Form is a sum of these # Integrals, everything else is derived) "_integrals", # --- Internal variables for caching various data "_integration_domains", "_domain_numbering", "_subdomain_data", "_arguments", "_base_form_operators", "_coefficients", "_coefficient_numbering", "_constants", "_constant_numbering", "_terminal_numbering", "_hash", "_signature", # --- Dict that external frameworks can place framework-specific # data in to be carried with the form # Never use this internally in ufl! "_cache", ) def __init__(self, integrals): """Initialise.""" BaseForm.__init__(self) # Basic input checking (further compatibilty analysis happens # later) if not all(isinstance(itg, Integral) for itg in integrals): raise ValueError("Expecting list of integrals.") # Store integrals sorted canonically to increase signature # stability self._integrals = _sorted_integrals(integrals) # Internal variables for caching domain data self._integration_domains = None self._domain_numbering = None # Internal variables for caching subdomain data self._subdomain_data = None # Internal variables for caching form argument data self._coefficients = None self._coefficient_numbering = None self._constant_numbering = None self._terminal_numbering = None # Internal variables for caching base form operator data self._base_form_operators = None from ufl.algorithms.analysis import extract_constants self._constants = extract_constants(self) # Internal variables for caching of hash and signature after # first request self._hash = None self._signature = None # Never use this internally in ufl! self._cache = {} # --- Accessor interface --- def integrals(self): """Return a sequence of all integrals in form.""" return self._integrals def integrals_by_type(self, integral_type): """Return a sequence of all integrals with a particular domain type.""" return tuple( integral for integral in self.integrals() if integral.integral_type() == integral_type ) def integrals_by_domain(self, domain): """Return a sequence of all integrals with a particular integration domain.""" return tuple(integral for integral in self.integrals() if integral.ufl_domain() == domain) def empty(self): """Returns whether the form has no integrals.""" return self.integrals() == () def ufl_domains(self): """Return the geometric integration domains occuring in the form. NB! This does not include domains of coefficients defined on other meshes. The return type is a tuple even if only a single domain exists. """ if self._integration_domains is None: self._analyze_domains() return self._integration_domains def ufl_cell(self): """Return the single cell this form is defined on. Fails if multiple cells are found. """ return self.ufl_domain().ufl_cell() def geometric_dimension(self): """Return the geometric dimension shared by all domains and functions in this form.""" gdims = tuple(set(domain.geometric_dimension() for domain in self.ufl_domains())) if len(gdims) != 1: raise ValueError( "Expecting all domains and functions in a form " f"to share geometric dimension, got {tuple(sorted(gdims))}" ) return gdims[0] def domain_numbering(self): """Return a contiguous numbering of domains in a mapping ``{domain:number}``.""" if self._domain_numbering is None: self._analyze_domains() return self._domain_numbering def subdomain_data(self): """Returns a mapping on the form ``{domain:{integral_type: subdomain_data}}``.""" if self._subdomain_data is None: self._analyze_subdomain_data() return self._subdomain_data def coefficients(self): """Return all ``Coefficient`` objects found in form.""" if self._coefficients is None: self._analyze_form_arguments() return self._coefficients def base_form_operators(self): """Return all ``BaseFormOperator`` objects found in form.""" if self._base_form_operators is None: self._analyze_base_form_operators() return self._base_form_operators def coefficient_numbering(self): """Return a contiguous numbering of coefficients in a mapping ``{coefficient:number}``.""" # cyclic import from ufl.coefficient import Coefficient if self._coefficient_numbering is None: self._coefficient_numbering = { expr: num for expr, num in self.terminal_numbering().items() if isinstance(expr, Coefficient) } return self._coefficient_numbering def constants(self): """Get constants.""" return self._constants def constant_numbering(self): """Return a contiguous numbering of constants in a mapping ``{constant:number}``.""" if self._constant_numbering is None: self._constant_numbering = { expr: num for expr, num in self.terminal_numbering().items() if isinstance(expr, Constant) } return self._constant_numbering def terminal_numbering(self): """Return a contiguous numbering for all counted objects in the form. The returned object is mapping from terminal to its number (an integer). The numbering is computed per type so :class:`Coefficient`s, :class:`Constant`s, etc will each be numbered from zero. """ # cyclic import from ufl.algorithms.analysis import extract_type if self._terminal_numbering is None: exprs_by_type = defaultdict(set) for counted_expr in extract_type(self, Counted): exprs_by_type[counted_expr._counted_class].add(counted_expr) numbering = {} for exprs in exprs_by_type.values(): for i, expr in enumerate(sorted_by_count(exprs)): numbering[expr] = i self._terminal_numbering = numbering return self._terminal_numbering def signature(self): """Signature for use with jit cache (independent of incidental numbering of indices etc).""" if self._signature is None: self._compute_signature() return self._signature # --- Operator implementations --- def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(tuple(hash(itg) for itg in self.integrals())) return self._hash def __ne__(self, other): """Immediate evaluation of the != operator (as opposed to the == operator).""" return not self.equals(other) def equals(self, other): """Evaluate ``bool(lhs_form == rhs_form)``.""" if type(other) is not Form: return False if len(self._integrals) != len(other._integrals): return False if hash(self) != hash(other): return False return all(a == b for a, b in zip(self._integrals, other._integrals)) def __radd__(self, other): """Add.""" # Ordering of form additions make no difference return self.__add__(other) def __add__(self, other): """Add.""" if isinstance(other, Form): # Add integrals from both forms return Form(list(chain(self.integrals(), other.integrals()))) if isinstance(other, ZeroBaseForm): # Simplify addition with ZeroBaseForm return self elif isinstance(other, BaseForm): # Create form sum if form is of other type return FormSum((self, 1), (other, 1)) elif isinstance(other, (int, float)) and other == 0: # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b]) return self elif isinstance(other, Zero) and not (other.ufl_shape or other.ufl_free_indices): # Allow adding ufl Zero as a no-op, needed for sum([a,b]) return self else: # Let python protocols do their job if we don't handle it return NotImplemented def __sub__(self, other): """Subtract other form from this one.""" return self + (-other) def __rsub__(self, other): """Subtract this form from other.""" return other + (-self) def __neg__(self): """Negate all integrals in form. This enables the handy "-form" syntax for e.g. the linearized system (J, -F) from a nonlinear form F. """ return Form([-itg for itg in self.integrals()]) def __rmul__(self, scalar): """Multiply all integrals in form with constant scalar value.""" # This enables the handy "0*form" or "dt*form" syntax if is_scalar_constant_expression(scalar): return Form([scalar * itg for itg in self.integrals()]) return NotImplemented def __mul__(self, coefficient): """UFL form operator: Take the action of this form on the given coefficient.""" if isinstance(coefficient, Expr): from ufl.formoperators import action return action(self, coefficient) return NotImplemented def __call__(self, *args, **kwargs): """UFL form operator: Evaluate form by replacing arguments and coefficients. Replaces form.arguments() with given positional arguments in same number and ordering. Number of positional arguments must be 0 or equal to the number of Arguments in the form. The optional keyword argument coefficients can be set to a dict to replace Coefficients with expressions of matching shapes. Example: V = FiniteElement("CG", triangle, 1) v = TestFunction(V) u = TrialFunction(V) f = Coefficient(V) g = Coefficient(V) a = g*inner(grad(u), grad(v))*dx M = a(f, f, coefficients={ g: 1 }) Is equivalent to M == grad(f)**2*dx. """ repdict = {} if args: arguments = self.arguments() if len(arguments) != len(args): raise ValueError(f"Need {len(arguments)} arguments to form(), got {len(args)}.") repdict.update(zip(arguments, args)) coefficients = kwargs.pop("coefficients", None) if kwargs: raise ValueError(f"Unknown kwargs {list(kwargs)}") if coefficients is not None: coeffs = self.coefficients() for f in coefficients: if f in coeffs: repdict[f] = coefficients[f] else: warnings.warn("Coefficient %s is not in form." % ufl_err_str(f)) if repdict: from ufl.formoperators import replace return replace(self, repdict) else: return self __matmul__ = __mul__ # --- String conversion functions, for UI purposes only --- def __str__(self): """Compute shorter string representation of form. This can be huge for complicated forms.""" # Warning used for making sure we don't use this in the general pipeline: # warning("Calling str on form is potentially expensive and # should be avoided except during debugging.") Not caching this # because it can be huge s = "\n + ".join(str(itg) for itg in self.integrals()) return s or "" def __repr__(self): """Compute repr string of form. This can be huge for complicated forms.""" # Warning used for making sure we don't use this in the general pipeline: # warning("Calling repr on form is potentially expensive and # should be avoided except during debugging.") Not caching this # because it can be huge itgs = ", ".join(repr(itg) for itg in self.integrals()) r = "Form([" + itgs + "])" return r # --- Analysis functions, precomputation and caching of various quantities def _analyze_domains(self): """Analyze domains.""" from ufl.domain import join_domains, sort_domains # Collect unique integration domains integration_domains = join_domains([itg.ufl_domain() for itg in self._integrals]) # Make canonically ordered list of the domains self._integration_domains = sort_domains(integration_domains) # TODO: Not including domains from coefficients and arguments # here, may need that later self._domain_numbering = dict((d, i) for i, d in enumerate(self._integration_domains)) def _analyze_subdomain_data(self): """Analyze subdomain data.""" integration_domains = self.ufl_domains() integrals = self.integrals() # Make clear data structures to collect subdomain data in subdomain_data = {} for domain in integration_domains: subdomain_data[domain] = {} for integral in integrals: # Get integral properties domain = integral.ufl_domain() it = integral.integral_type() sd = integral.subdomain_data() # Collect subdomain data if subdomain_data[domain].get(it) is None: subdomain_data[domain][it] = [sd] else: subdomain_data[domain][it].append(sd) self._subdomain_data = subdomain_data def _analyze_form_arguments(self): """Analyze which Argument and Coefficient objects can be found in the form.""" from ufl.algorithms.analysis import extract_arguments_and_coefficients arguments, coefficients = extract_arguments_and_coefficients(self) # Define canonical numbering of arguments and coefficients self._arguments = tuple(sorted(set(arguments), key=lambda x: x.number())) self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count())) def _analyze_base_form_operators(self): """Analyze which BaseFormOperator objects can be found in the form.""" from ufl.algorithms.analysis import extract_base_form_operators base_form_ops = extract_base_form_operators(self) self._base_form_operators = tuple(sorted(base_form_ops, key=lambda x: x.count())) def _compute_renumbering(self): """Compute renumbering.""" # Include integration domains and coefficients in renumbering dn = self.domain_numbering() tn = self.terminal_numbering() renumbering = {} renumbering.update(dn) renumbering.update(tn) # Add domains of coefficients, these may include domains not # among integration domains k = len(dn) for c in self.coefficients(): d = extract_unique_domain(c) if d is not None and d not in renumbering: renumbering[d] = k k += 1 # Add domains of arguments, these may include domains not # among integration domains for a in self._arguments: d = a.ufl_function_space().ufl_domain() if d is not None and d not in renumbering: renumbering[d] = k k += 1 # Add domains of constants, these may include domains not # among integration domains for c in self._constants: d = extract_unique_domain(c) if d is not None and d not in renumbering: renumbering[d] = k k += 1 return renumbering def _compute_signature(self): """Compute signature.""" from ufl.algorithms.signature import compute_form_signature self._signature = compute_form_signature(self, self._compute_renumbering()) def as_form(form): """Convert to form if not a form, otherwise return form.""" if not isinstance(form, BaseForm) and form != 0: raise ValueError(f"Unable to convert object to a UFL form: {ufl_err_str(form)}") return form @ufl_type() class FormSum(BaseForm): """Form sum. Description of a weighted sum of variational forms and form-like objects components is the list of Forms to be summed arg_weights is a list of tuples of component index and weight """ __slots__ = ( "_arguments", "_coefficients", "_weights", "_components", "ufl_operands", "_domains", "_domain_numbering", "_hash", ) _ufl_required_methods_ = "_analyze_form_arguments" def __new__(cls, *args, **kwargs): """Create a new FormSum.""" # All the components are `ZeroBaseForm` if all(component == 0 for component, _ in args): # Assume that the arguments of all the components have # consistent with each other and select the first one to # define the arguments of `ZeroBaseForm`. # This might not always be true but `ZeroBaseForm`'s # arguments are not checked anywhere because we can't # reliably always infer them. ((arg, _), *_) = args arguments = arg.arguments() return ZeroBaseForm(arguments) return super(FormSum, cls).__new__(cls) def __init__(self, *components): """Initialise.""" BaseForm.__init__(self) # Remove `ZeroBaseForm` components filtered_components = [(component, w) for component, w in components if component != 0] weights = [] full_components = [] for component, w in filtered_components: if isinstance(component, FormSum): full_components.extend(component.components()) weights.extend([w * wc for wc in component.weights()]) else: full_components.append(component) weights.append(w) self._arguments = None self._coefficients = None self._domains = None self._domain_numbering = None self._hash = None self._weights = weights self._components = full_components self._sum_variational_components() self.ufl_operands = self._components def components(self): """Get components.""" return self._components def weights(self): """Get weights.""" return self._weights def _sum_variational_components(self): """Sum variational components.""" var_forms = None other_components = [] new_weights = [] for i, component in enumerate(self._components): if isinstance(component, Form): if var_forms: var_forms = var_forms + (self._weights[i] * component) else: var_forms = self._weights[i] * component else: other_components.append(component) new_weights.append(self._weights[i]) if var_forms: other_components.insert(0, var_forms) new_weights.insert(0, 1) self._components = other_components self._weights = new_weights def _analyze_form_arguments(self): """Return all ``Argument`` objects found in form.""" arguments = [] coefficients = [] for component in self._components: arguments.extend(component.arguments()) coefficients.extend(component.coefficients()) # Define canonical numbering of arguments and coefficients self._arguments = tuple(sorted(set(arguments), key=lambda x: x.number())) self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count())) def _analyze_domains(self): """Analyze which domains can be found in FormSum.""" from ufl.domain import join_domains # Collect unique domains self._domains = join_domains( chain.from_iterable(e.ufl_domains() for e in self.ufl_operands) ) def ufl_domains(self): """Return all domains found in the base form.""" if self._domains is None: self._analyze_domains() return self._domains def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(tuple(hash(component) for component in self.components())) return self._hash def equals(self, other): """Evaluate ``bool(lhs_form == rhs_form)``.""" if type(other) is not FormSum: return False if self is other: return True return len(self.components()) == len(other.components()) and all( a == b for a, b in zip(self.components(), other.components()) ) def __str__(self): """Compute shorter string representation of form. This can be huge for complicated forms.""" # Warning used for making sure we don't use this in the general pipeline: # warning("Calling str on form is potentially expensive and # should be avoided except during debugging.") # Not caching this because it can be huge s = "\n + ".join(str(component) for component in self.components()) return s or "" def __repr__(self): """Compute repr string of form. This can be huge for complicated forms.""" # Warning used for making sure we don't use this in the general pipeline: # warning("Calling repr on form is potentially expensive and # should be avoided except during debugging.") # Not caching this because it can be huge itgs = ", ".join(repr(component) for component in self.components()) r = "FormSum([" + itgs + "])" return r @ufl_type() class ZeroBaseForm(BaseForm): """Description of a zero base form. ZeroBaseForm is idempotent with respect to assembly and is mostly used for sake of simplifying base-form expressions. """ __slots__ = ( "_arguments", "_coefficients", "ufl_operands", "_domains", "_hash", # Pyadjoint compatibility "form", ) def __init__(self, arguments): """Initialise.""" BaseForm.__init__(self) self._arguments = arguments self.ufl_operands = arguments self._hash = None self.form = None def _analyze_form_arguments(self): """Analyze form arguments.""" # `self._arguments` is already set in `BaseForm.__init__` self._coefficients = () def _analyze_domains(self): """Analyze which domains can be found in ZeroBaseForm.""" from ufl.domain import join_domains # Collect unique domains self._domains = join_domains( chain.from_iterable(e.ufl_domains() for e in self.ufl_operands) ) def ufl_domains(self): """Return all domains found in the base form.""" if self._domains is None: self._analyze_domains() return self._domains def __ne__(self, other): """Overwrite BaseForm.__neq__ which relies on `equals`.""" return not self == other def __eq__(self, other): """Check equality.""" if type(other) is ZeroBaseForm: if self is other: return True return self._arguments == other._arguments elif isinstance(other, (int, float)): return other == 0 else: return False def __str__(self): """Format as a string.""" return "ZeroBaseForm(%s)" % (", ".join(str(arg) for arg in self._arguments)) def __repr__(self): """Representation.""" return "ZeroBaseForm(%s)" % (", ".join(repr(arg) for arg in self._arguments)) def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(("ZeroBaseForm", hash(self._arguments))) return self._hash ufl-2024.2.0/ufl/formatting/000077500000000000000000000000001470142567200154775ustar00rootroot00000000000000ufl-2024.2.0/ufl/formatting/__init__.py000066400000000000000000000000221470142567200176020ustar00rootroot00000000000000"""Formatting.""" ufl-2024.2.0/ufl/formatting/ufl2unicode.py000066400000000000000000000536121470142567200202770ustar00rootroot00000000000000"""UFL to unicode.""" import numbers import ufl from ufl.algorithms import compute_form_data from ufl.core.multiindex import FixedIndex, Index from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction from ufl.form import Form try: import colorama has_colorama = True except ImportError: has_colorama = False class PrecedenceRules(MultiFunction): """An enum-like class for C operator precedence levels.""" def __init__(self): """Initialise.""" MultiFunction.__init__(self) def highest(self, o): """Return the highest precendence.""" return 0 terminal = highest list_tensor = highest component_tensor = highest def restricted(self, o): """Return precedence of a restriced.""" return 5 cell_avg = restricted facet_avg = restricted def call(self, o): """Return precedence of a call.""" return 10 indexed = call min_value = call max_value = call math_function = call bessel_function = call def power(self, o): """Return precedence of a power.""" return 12 def mathop(self, o): """Return precedence of a mathop.""" return 15 derivative = mathop trace = mathop deviatoric = mathop cofactor = mathop skew = mathop sym = mathop def not_condition(self, o): """Return precedence of a not_condition.""" return 20 def product(self, o): """Return precedence of a product.""" return 30 division = product # mod = product dot = product inner = product outer = product cross = product def add(self, o): """Return precedence of an add.""" return 40 # sub = add index_sum = add def lt(self, o): """Return precedence of a lt.""" return 50 le = lt gt = lt ge = lt def eq(self, o): """Return precedence of an eq.""" return 60 ne = eq def and_condition(self, o): """Return precedence of an and_condition.""" return 70 def or_condition(self, o): """Return precedence of an or_condition.""" return 71 def conditional(self, o): """Return precedence of a conditional.""" return 72 def lowest(self, o): """Return precedence of a lowest.""" return 80 operator = lowest _precrules = PrecedenceRules() def precedence(expr): """Get the precedence of an expr.""" return _precrules(expr) class UC: """An enum-like class for unicode characters.""" # Letters in this alphabet have contiguous code point numbers bold_math_a = "𝐚" bold_math_A = "𝐀" thin_space = "\u2009" superscript_plus = "⁺" superscript_minus = "⁻" superscript_equals = "⁼" superscript_left_paren = "⁽" superscript_right_paren = "⁾" superscript_digits = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"] subscript_plus = "₊" subscript_minus = "₋" subscript_equals = "₌" subscript_left_paren = "₍" subscript_right_paren = "₎" subscript_digits = ["₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"] sqrt = "√" transpose = "ᵀ" integral = "∫" integral_double = "∬" integral_triple = "∭" integral_contour = "∮" integral_surface = "∯" integral_volume = "∰" sum = "∑" division_slash = "∕" partial = "∂" epsilon = "ε" omega = "ω" Omega = "Ω" gamma = "γ" Gamma = "Γ" nabla = "∇" for_all = "∀" dot = "⋅" cross_product = "⨯" circled_times = "⊗" nary_product = "∏" ne = "≠" lt = "<" le = "≤" gt = ">" ge = "≥" logical_and = "∧" logical_or = "∨" logical_not = "¬" element_of = "∈" not_element_of = "∉" left_white_square_bracket = "⟦" right_white_squared_bracket = "⟧" left_angled_bracket = "⟨" right_angled_bracket = "⟩" left_double_angled_bracket = "⟪" right_double_angled_bracket = "⟫" combining_right_arrow_above = "\u20d7" combining_overline = "\u0305" def bolden_letter(c): """Bolden a letter.""" if ord("A") <= ord(c) <= ord("Z"): c = chr(ord(c) - ord("A") + ord(UC.bold_math_A)) elif ord("a") <= ord(c) <= ord("z"): c = chr(ord(c) - ord("a") + ord(UC.bold_math_a)) return c def superscript_digit(digit): """Make a digit superscript.""" return UC.superscript_digits[ord(digit) - ord("0")] def subscript_digit(digit): """Make a digit subscript.""" return UC.subscript_digits[ord(digit) - ord("0")] def bolden_string(s): """Bolden a string.""" return "".join(bolden_letter(c) for c in s) def overline_string(f): """Overline a string.""" return "".join(f"{c}{UC.combining_overline}" for c in f) def subscript_number(number): """Make a number subscript.""" assert isinstance(number, int) prefix = UC.subscript_minus if number < 0 else "" number = str(number) return prefix + "".join(subscript_digit(c) for c in str(number)) def superscript_number(number): """Make a number superscript.""" assert isinstance(number, int) prefix = UC.superscript_minus if number < 0 else "" number = str(number) return prefix + "".join(superscript_digit(c) for c in str(number)) def opfont(opname): """Use the font for operators.""" return bolden_string(opname) def measure_font(dx): """Use the font for measures.""" return bolden_string(dx) integral_by_dim = {3: UC.integral_triple, 2: UC.integral_double, 1: UC.integral, 0: UC.integral} integral_type_to_codim = { "cell": 0, "exterior_facet": 1, "interior_facet": 1, "vertex": "tdim", "point": "tdim", "custom": 0, "overlap": 0, "interface": 1, "cutcell": 0, } integral_symbols = { "cell": UC.integral_volume, "exterior_facet": UC.integral_surface, "interior_facet": UC.integral_surface, "vertex": UC.integral, "point": UC.integral, "custom": UC.integral, "overlap": UC.integral, "interface": UC.integral, "cutcell": UC.integral, } integral_postfixes = { "cell": "", "exterior_facet": "ext", "interior_facet": "int", "vertex": "vertex", "point": "point", "custom": "custom", "overlap": "overlap", "interface": "interface", "cutcell": "cutcell", } def get_integral_symbol(integral_type, domain, subdomain_id): """Get the symbol for an integral.""" tdim = domain.topological_dimension() codim = integral_type_to_codim[integral_type] itgdim = tdim - codim # ipost = integral_postfixes[integral_type] istr = integral_by_dim[itgdim] # TODO: Render domain description subdomain_strs = [] for subdomain in subdomain_id: if isinstance(subdomain, numbers.Integral): subdomain_strs.append(subscript_number(int(subdomain))) elif subdomain == "everywhere": pass elif subdomain == "otherwise": subdomain_strs.append("[rest of domain]") istr += ",".join(subdomain_strs) dxstr = ufl.measure.integral_type_to_measure_name[integral_type] dxstr = measure_font(dxstr) return istr, dxstr def par(s): """Wrap in parentheses.""" return f"({s})" def is_int(s): """Check if a value is an integer.""" try: int(s) return True except ValueError: return False def format_index(ii): """Format an index.""" if isinstance(ii, FixedIndex): s = f"{ii._value}" elif isinstance(ii, Index): s = "i{subscript_number(ii._count)}" else: raise ValueError(f"Invalid index type {type(ii)}.") return s def ufl2unicode(expression): """Generate Unicode string for a UFL expression or form.""" if isinstance(expression, Form): form_data = compute_form_data(expression) preprocessed_form = form_data.preprocessed_form return form2unicode(preprocessed_form, form_data) else: return expression2unicode(ufl.as_ufl(expression)) def expression2unicode(expression, argument_names=None, coefficient_names=None): """Generate Unicode string for a UFL expression.""" rules = Expression2UnicodeHandler(argument_names, coefficient_names) return map_expr_dag(rules, expression) def form2unicode(form, formdata): """Generate Unicode string for a UFL form.""" argument_names = None coefficient_names = None # Define form as sum of integrals lines = [] integrals = form.integrals() for itg in integrals: integrand_string = expression2unicode(itg.integrand(), argument_names, coefficient_names) istr, dxstr = get_integral_symbol(itg.integral_type(), itg.ufl_domain(), itg.subdomain_id()) line = f"{istr} {integrand_string} {dxstr}" lines.append(line) return "\n + ".join(lines) def binop(expr, a, b, op, sep=" "): """Format a binary operation.""" eprec = precedence(expr) op0, op1 = expr.ufl_operands aprec = precedence(op0) bprec = precedence(op1) # Assuming left-to-right evaluation, therefore >= and > here: if aprec >= eprec: a = par(a) if bprec > eprec: b = par(b) return sep.join((a, op, b)) def mathop(expr, arg, opname): """Format a math operation.""" eprec = precedence(expr) aprec = precedence(expr.ufl_operands[0]) op = opfont(opname) if aprec > eprec: arg = par(arg) sep = "" else: sep = UC.thin_space return f"{op}{sep}{arg}" class Expression2UnicodeHandler(MultiFunction): """Convert expressions to unicode.""" def __init__(self, argument_names=None, coefficient_names=None, colorama_bold=False): """Initialise.""" MultiFunction.__init__(self) self.argument_names = argument_names self.coefficient_names = coefficient_names self.colorama_bold = colorama_bold and has_colorama # --- Terminal objects --- def scalar_value(self, o): """Format a scalar_value.""" if o.ufl_shape and self.colorama_bold: return f"{colorama.Style.BRIGHT}{o._value}{colorama.Style.RESET_ALL}" return f"{o._value}" def zero(self, o): """Format a zero.""" if o.ufl_shape and self.colorama_bold: if len(o.ufl_shape) == 1: return f"0{UC.combining_right_arrow_above}" return f"{colorama.Style.BRIGHT}0{colorama.Style.RESET_ALL}" return "0" def identity(self, o): """Format a identity.""" if self.colorama_bold: return f"{colorama.Style.BRIGHT}I{colorama.Style.RESET_ALL}" return "I" def permutation_symbol(self, o): """Format a permutation_symbol.""" if self.colorama_bold: return f"{colorama.Style.BRIGHT}{UC.epsilon}{colorama.Style.RESET_ALL}" return UC.epsilon def facet_normal(self, o): """Format a facet_normal.""" return f"n{UC.combining_right_arrow_above}" def spatial_coordinate(self, o): """Format a spatial_coordinate.""" return f"x{UC.combining_right_arrow_above}" def argument(self, o): """Format an argument.""" # Using ^ for argument numbering and _ for indexing since # indexing is more common than exponentiation if self.argument_names is None: i = o.number() bfn = "v" if i == 0 else "u" if not o.ufl_shape: return bfn elif len(o.ufl_shape) == 1: return f"{bfn}{UC.combining_right_arrow_above}" elif self.colorama_bold: return f"{colorama.Style.BRIGHT}{bfn}{colorama.Style.RESET_ALL}" else: return bfn return self.argument_names[(o.number(), o.part())] def coefficient(self, o): """Format a coefficient.""" # Using ^ for coefficient numbering and _ for indexing since # indexing is more common than exponentiation if self.coefficient_names is None: i = o.count() var = "w" if len(o.ufl_shape) == 1: var += UC.combining_right_arrow_above elif len(o.ufl_shape) > 1 and self.colorama_bold: var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}" return f"{var}{subscript_number(i)}" return self.coefficient_names[o.count()] def cofunction(self, o): """Format a cofunction.""" if self.coefficient_names is None: i = o.count() var = "cofunction" if len(o.ufl_shape) == 1: var += UC.combining_right_arrow_above elif len(o.ufl_shape) > 1 and self.colorama_bold: var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}" return f"{var}{subscript_number(i)}" return self.coefficient_names[o.count()] def base_form_operator(self, o): """Format a base_form_operator.""" return "BaseFormOperator" def action(self, o, a, b): """Format an Action.""" return f"Action({a}, {b})" def constant(self, o): """Format a constant.""" i = o.count() var = "c" if len(o.ufl_shape) == 1: var += UC.combining_right_arrow_above elif len(o.ufl_shape) > 1 and self.colorama_bold: var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}" return f"{var}{superscript_number(i)}" def multi_index(self, o): """Format a multi_index.""" return ",".join(format_index(i) for i in o) def label(self, o): """Format a label.""" return f"l{subscript_number(o.count())}" # --- Non-terminal objects --- def variable(self, o, f, a): """Format a variable.""" return f"var({f},{a})" def index_sum(self, o, f, i): """Format a index_sum.""" if 1: # prec(o.ufl_operands[0]) >? prec(o): f = par(f) return f"{UC.sum}[{i}]{f}" def sum(self, o, a, b): """Format a sum.""" return binop(o, a, b, "+") def product(self, o, a, b): """Format a product.""" return binop(o, a, b, " ", sep="") def division(self, o, a, b): """Format a division.""" if is_int(b): b = subscript_number(int(b)) if is_int(a): # Return as a fraction # NOTE: Maybe consider using fractional slash # with normal numbers if terminals can handle it a = superscript_number(int(a)) else: a = par(a) return f"{a} {UC.division_slash} {b}" return binop(o, a, b, UC.division_slash) def abs(self, o, a): """Format an ans.""" return f"|{a}|" def transposed(self, o, a): """Format a transposed.""" a = par(a) return f"{a}{UC.transpose}" def indexed(self, o, A, ii): """Format an indexed.""" op0, op1 = o.ufl_operands Aprec = precedence(op0) oprec = precedence(o) if Aprec > oprec: A = par(A) return f"{A}[{ii}]" def variable_derivative(self, o, f, v): """Format a variable_derivative.""" f = par(f) v = par(v) nom = f"{UC.partial}{f}" denom = f"{UC.partial}{v}" return par(f"{nom}{UC.division_slash}{denom}") def coefficient_derivative(self, o, f, w, v, cd): """Format a coefficient_derivative.""" f = par(f) w = par(w) nom = f"{UC.partial}{f}" denom = f"{UC.partial}{w}" return par(f"{nom}{UC.division_slash}{denom}[{v}]") def grad(self, o, f): """Format a grad.""" return mathop(o, f, "grad") def div(self, o, f): """Format a div.""" return mathop(o, f, "div") def nabla_grad(self, o, f): """Format a nabla_grad.""" oprec = precedence(o) fprec = precedence(o.ufl_operands[0]) if fprec > oprec: f = par(f) return f"{UC.nabla}{UC.thin_space}{f}" def nabla_div(self, o, f): """Format a nabla_div.""" oprec = precedence(o) fprec = precedence(o.ufl_operands[0]) if fprec > oprec: f = par(f) return f"{UC.nabla}{UC.thin_space}{UC.dot}{UC.thin_space}{f}" def curl(self, o, f): """Format a curl.""" oprec = precedence(o) fprec = precedence(o.ufl_operands[0]) if fprec > oprec: f = par(f) return f"{UC.nabla}{UC.thin_space}{UC.cross_product}{UC.thin_space}{f}" def math_function(self, o, f): """Format a math_function.""" op = opfont(o._name) return f"{op}{par(f)}" def sqrt(self, o, f): """Format a sqrt.""" return f"{UC.sqrt}{par(f)}" def exp(self, o, f): """Format a exp.""" op = opfont("exp") return f"{op}{par(f)}" def atan2(self, o, f1, f2): """Format a atan2.""" f1 = par(f1) f2 = par(f2) op = opfont("arctan2") return f"{op}({f1}, {f2})" def bessel_j(self, o, nu, f): """Format a bessel_j.""" op = opfont("J") nu = subscript_number(int(nu)) return f"{op}{nu}{par(f)}" def bessel_y(self, o, nu, f): """Format a bessel_y.""" op = opfont("Y") nu = subscript_number(int(nu)) return f"{op}{nu}{par(f)}" def bessel_i(self, o, nu, f): """Format a bessel_i.""" op = opfont("I") nu = subscript_number(int(nu)) return f"{op}{nu}{par(f)}" def bessel_K(self, o, nu, f): """Format a bessel_K.""" op = opfont("K") nu = subscript_number(int(nu)) return f"{op}{nu}{par(f)}" def power(self, o, a, b): """Format a power.""" if is_int(b): b = superscript_number(int(b)) return binop(o, a, b, "", sep="") return binop(o, a, b, "^", sep="") def outer(self, o, a, b): """Format an outer.""" return binop(o, a, b, UC.circled_times) def inner(self, o, a, b): """Format an inner.""" return f"{UC.left_angled_bracket}{a}, {b}{UC.right_angled_bracket}" def dot(self, o, a, b): """Format a dot.""" return binop(o, a, b, UC.dot) def cross(self, o, a, b): """Format a cross.""" return binop(o, a, b, UC.cross_product) def determinant(self, o, A): """Format a determinant.""" return f"|{A}|" def inverse(self, o, A): """Format an inverse.""" A = par(A) return f"{A}{superscript_number(-1)}" def trace(self, o, A): """Format a trace.""" return mathop(o, A, "tr") def deviatoric(self, o, A): """Format a deviatoric.""" return mathop(o, A, "dev") def cofactor(self, o, A): """Format a cofactor.""" return mathop(o, A, "cofac") def skew(self, o, A): """Format a skew.""" return mathop(o, A, "skew") def sym(self, o, A): """Format a sym.""" return mathop(o, A, "sym") def conj(self, o, a): """Format a conj.""" # Overbar is already taken for average, and there is no superscript asterix in unicode. return mathop(o, a, "conj") def real(self, o, a): """Format a real.""" return mathop(o, a, "Re") def imag(self, o, a): """Format a imag.""" return mathop(o, a, "Im") def list_tensor(self, o, *ops): """Format a list_tensor.""" return f"[{', '.join(ops)}]" def component_tensor(self, o, A, ii): """Format a component_tensor.""" return f"[{A} {UC.for_all} {ii}]" def positive_restricted(self, o, f): """Format a positive_restriced.""" return f"{par(f)}{UC.superscript_plus}" def negative_restricted(self, o, f): """Format a negative_restriced.""" return f"{par(f)}{UC.superscript_minus}" def cell_avg(self, o, f): """Format a cell_avg.""" f = overline_string(f) return f def facet_avg(self, o, f): """Format a facet_avg.""" f = overline_string(f) return f def eq(self, o, a, b): """Format an eq.""" return binop(o, a, b, "=") def ne(self, o, a, b): """Format a ne.""" return binop(o, a, b, UC.ne) def le(self, o, a, b): """Format a le.""" return binop(o, a, b, UC.le) def ge(self, o, a, b): """Format a ge.""" return binop(o, a, b, UC.ge) def lt(self, o, a, b): """Format a lt.""" return binop(o, a, b, UC.lt) def gt(self, o, a, b): """Format a gt.""" return binop(o, a, b, UC.gt) def and_condition(self, o, a, b): """Format an and_condition.""" return binop(o, a, b, UC.logical_and) def or_condition(self, o, a, b): """Format an or_condition.""" return binop(o, a, b, UC.logical_or) def not_condition(self, o, a): """Format a not_condition.""" a = par(a) return f"{UC.logical_not}{a}" def conditional(self, o, c, t, f): """Format a conditional.""" c = par(c) t = par(t) f = par(t) If = opfont("if") Else = opfont("else") return f"{t} {If} {c} {Else} {f}" def min_value(self, o, a, b): """Format an min_value.""" op = opfont("min") return f"{op}({a}, {b})" def max_value(self, o, a, b): """Format an max_value.""" op = opfont("max") return f"{op}({a}, {b})" def expr_list(self, o, *ops): """Format an expr_list.""" items = ", ".join(ops) return f"{UC.left_white_square_bracket} {items} {UC.right_white_squared_bracket}" def expr_mapping(self, o, *ops): """Format an expr_mapping.""" items = ", ".join(ops) return f"{UC.left_double_angled_bracket} {items} {UC.left_double_angled_bracket}" def expr(self, o): """Format an expr.""" raise ValueError(f"Missing handler for type {type(o)}") ufl-2024.2.0/ufl/formoperators.py000066400000000000000000000422131470142567200166030ustar00rootroot00000000000000"""Various high level ways to transform a complete Form into a new Form.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009 # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 from ufl.action import Action from ufl.adjoint import Adjoint from ufl.algorithms import ( compute_energy_norm, compute_form_action, compute_form_adjoint, compute_form_functional, compute_form_lhs, compute_form_rhs, expand_derivatives, extract_arguments, formsplitter, replace, # noqa: F401 ) from ufl.argument import Argument from ufl.coefficient import Coefficient, Cofunction from ufl.constantvalue import as_ufl, is_true_ufl_scalar from ufl.core.base_form_operator import BaseFormOperator from ufl.core.expr import Expr, ufl_err_str from ufl.core.multiindex import FixedIndex, MultiIndex from ufl.differentiation import ( BaseFormCoordinateDerivative, BaseFormDerivative, BaseFormOperatorCoordinateDerivative, BaseFormOperatorDerivative, CoefficientDerivative, CoordinateDerivative, ) from ufl.exprcontainers import ExprList, ExprMapping from ufl.finiteelement import MixedElement from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm, as_form from ufl.functionspace import FunctionSpace from ufl.geometry import SpatialCoordinate from ufl.indexed import Indexed from ufl.sorting import sorted_expr from ufl.split_functions import split from ufl.tensors import ListTensor, as_tensor from ufl.variable import Variable def extract_blocks(form, i=None, j=None): """Extract blocks. Given a linear or bilinear form on a mixed space, extract the block corresponding to the indices ix, iy. Example: a = inner(grad(u), grad(v))*dx + div(u)*q*dx + div(v)*p*dx extract_blocks(a, 0, 0) -> inner(grad(u), grad(v))*dx extract_blocks(a) -> [inner(grad(u), grad(v))*dx, div(v)*p*dx, div(u)*q*dx, 0] """ return formsplitter.extract_blocks(form, i, j) def lhs(form): """Get the left hand side. Given a combined bilinear and linear form, extract the left hand side (bilinear form part). Example: a = u*v*dx + f*v*dx a = lhs(a) -> u*v*dx """ form = as_form(form) form = expand_derivatives(form) return compute_form_lhs(form) def rhs(form): """Get the right hand side. Given a combined bilinear and linear form, extract the right hand side (negated linear form part). Example: a = u*v*dx + f*v*dx L = rhs(a) -> -f*v*dx """ form = as_form(form) form = expand_derivatives(form) return compute_form_rhs(form) def system(form): """Split a form into the left hand side and right hand side. See ``lhs`` and ``rhs``. """ return lhs(form), rhs(form) def functional(form): # TODO: Does this make sense for anything other than testing? """Extract the functional part of form.""" form = as_form(form) form = expand_derivatives(form) return compute_form_functional(form) def action(form, coefficient=None, derivatives_expanded=None): """Get the action. Given a bilinear form, return a linear form with an additional coefficient, representing the action of the form on the coefficient. This can be used for matrix-free methods. For formbase objects,coefficient can be any object of the correct type, and this function returns an Action object. When `action` is being called multiple times on the same form, expanding derivatives become expensive -> `derivatives_expanded` enables to use caching mechanisms to avoid that. """ form = as_form(form) is_coefficient_valid = not isinstance(coefficient, BaseForm) or ( isinstance(coefficient, BaseFormOperator) and len(coefficient.arguments()) == 1 ) # Can't expand derivatives on objects that are not Form or Expr (e.g. Matrix) if isinstance(form, (Form, BaseFormOperator)) and is_coefficient_valid: if not derivatives_expanded: # For external operators differentiation may turn a Form into a FormSum form = expand_derivatives(form) if isinstance(form, Form): return compute_form_action(form, coefficient) return Action(form, coefficient) def energy_norm(form, coefficient=None): """Get the energy norm. Given a bilinear form *a* and a coefficient *f*, return the functional :math:`a(f,f)`. """ form = as_form(form) form = expand_derivatives(form) return compute_energy_norm(form, coefficient) def adjoint(form, reordered_arguments=None, derivatives_expanded=None): """Get the adjoint. Given a combined bilinear form, compute the adjoint form by changing the ordering (count) of the test and trial functions, and taking the complex conjugate of the result. By default, new ``Argument`` objects will be created with opposite ordering. However, if the adjoint form is to be added to other forms later, their arguments must match. In that case, the user must provide a tuple *reordered_arguments*=(u2,v2). If the form is a baseform instance instead of a Form object, we return an Adjoint object instructing the adjoint to be computed at a later point. When `adjoint` is being called multiple times on the same form, expanding derivatives become expensive -> `derivatives_expanded` enables to use caching mechanisms to avoid that. """ form = as_form(form) if isinstance(form, BaseForm): # Allow BaseForm objects that are not BaseForm such as Adjoint since there are cases # where we need to expand derivatives: e.g. to get the number of arguments # => For example: Adjoint(Action(2-form, derivative(u,u))) if not derivatives_expanded: # For external operators differentiation may turn a Form into a FormSum form = expand_derivatives(form) if isinstance(form, Form): return compute_form_adjoint(form, reordered_arguments) return Adjoint(form) def zero_lists(shape): """Createa list of zeros of the given shape.""" if len(shape) == 0: raise ValueError("Invalid shape.") elif len(shape) == 1: return [0] * shape[0] else: return [zero_lists(shape[1:]) for i in range(shape[0])] def set_list_item(li, i, v): """Set an item in a nested list.""" # Get to the innermost list if len(i) > 1: for j in i[:-1]: li = li[j] # Set item in innermost list li[i[-1]] = v def _handle_derivative_arguments(form, coefficient, argument): """Handle derivative arguments.""" # Wrap single coefficient in tuple for uniform treatment below if isinstance(coefficient, (list, tuple, ListTensor)): coefficients = tuple(coefficient) else: coefficients = (coefficient,) if argument is None: # Try to create argument if not provided if not all( isinstance(c, (Coefficient, Cofunction, BaseFormOperator)) for c in coefficients ): raise ValueError( "Can only create arguments automatically for non-indexed coefficients." ) # Get existing arguments from form and position the new one # with the next argument number if isinstance(form, Form): form_arguments = form.arguments() else: # To handle derivative(expression), which is at least used # in tests. Remove? form_arguments = extract_arguments(form) numbers = sorted(set(arg.number() for arg in form_arguments)) number = max(numbers + [-1]) + 1 # Don't know what to do with parts, let the user sort it out # in that case parts = set(arg.part() for arg in form_arguments) if len(parts - {None}) != 0: raise ValueError("Not expecting parts here, provide your own arguments.") part = None # Create argument and split it if in a mixed space function_spaces = [c.ufl_function_space() for c in coefficients] if len(function_spaces) == 1: arguments = (Argument(function_spaces[0], number, part),) else: # Create in mixed space over assumed (for now) same domain domains = [fs.ufl_domain() for fs in function_spaces] elements = [fs.ufl_element() for fs in function_spaces] assert all(fs.ufl_domain() == domains[0] for fs in function_spaces) elm = MixedElement(elements) fs = FunctionSpace(domains[0], elm) arguments = split(Argument(fs, number, part)) else: # Wrap single argument in tuple for uniform treatment below if isinstance(argument, (list, tuple)): arguments = tuple(argument) else: n = len(coefficients) if n == 1: arguments = (argument,) else: if argument.ufl_shape == (n,): arguments = tuple(argument[i] for i in range(n)) else: arguments = split(argument) # Build mapping from coefficient to argument m = {} for c, a in zip(coefficients, arguments): if c.ufl_shape != a.ufl_shape: raise ValueError("Coefficient and argument shapes do not match!") if isinstance(c, (Coefficient, Cofunction, BaseFormOperator, SpatialCoordinate)): m[c] = a else: if not isinstance(c, Indexed): raise ValueError(f"Invalid coefficient type for {ufl_err_str(c)}") f, i = c.ufl_operands if not isinstance(f, Coefficient): raise ValueError(f"Expecting an indexed coefficient, not {ufl_err_str(f)}") if not (isinstance(i, MultiIndex) and all(isinstance(j, FixedIndex) for j in i)): raise ValueError(f"Expecting one or more fixed indices, not {ufl_err_str(i)}") i = tuple(int(j) for j in i) if f not in m: m[f] = {} m[f][i] = a # Merge coefficient derivatives (arguments) based on indices for c, p in m.items(): if isinstance(p, dict): a = zero_lists(c.ufl_shape) for i, g in p.items(): set_list_item(a, i, g) m[c] = as_tensor(a) # Wrap and return generic tuples items = sorted(m.items(), key=lambda x: x[0].count()) coefficients = ExprList(*[item[0] for item in items]) arguments = ExprList(*[item[1] for item in items]) return coefficients, arguments def derivative(form, coefficient, argument=None, coefficient_derivatives=None): """Compute the Gateaux derivative of *form* w.r.t. *coefficient* in direction of *argument*. If the argument is omitted, a new ``Argument`` is created in the same space as the coefficient, with argument number one higher than the highest one in the form. The resulting form has one additional ``Argument`` in the same finite element space as the coefficient. A tuple of ``Coefficient`` s may be provided in place of a single ``Coefficient``, in which case the new ``Argument`` argument is based on a ``MixedElement`` created from this tuple. An indexed ``Coefficient`` from a mixed space may be provided, in which case the argument should be in the corresponding subspace of the coefficient space. If provided, *coefficient_derivatives* should be a mapping from ``Coefficient`` instances to their derivatives w.r.t. *coefficient*. """ if isinstance(form, FormSum): # Distribute derivative over FormSum components return FormSum( *[ (derivative(component, coefficient, argument, coefficient_derivatives), 1) for component in form.components() ] ) elif isinstance(form, Adjoint): # Is `derivative(Adjoint(A), ...)` with A a 2-form even legal ? # -> If yes, what's the right thing to do here ? raise NotImplementedError("Adjoint derivative is not supported.") elif isinstance(form, Action): # Push derivative through Action slots left, right = form.ufl_operands # Eagerly simplify spatial derivatives when Action results in a scalar. if not len(form.arguments()) and isinstance(coefficient, SpatialCoordinate): return ZeroBaseForm(()) if len(left.arguments()) == 1: dleft = derivative(left, coefficient, argument, coefficient_derivatives) dright = derivative(right, coefficient, argument, coefficient_derivatives) # Leibniz formula return action( adjoint(dleft, derivatives_expanded=True), right, derivatives_expanded=True ) + action(left, dright, derivatives_expanded=True) else: raise NotImplementedError( "Action derivative not supported when the left argument is not a 1-form." ) coefficients, arguments = _handle_derivative_arguments(form, coefficient, argument) if coefficient_derivatives is None: coefficient_derivatives = ExprMapping() else: cd = [] for k in sorted_expr(coefficient_derivatives.keys()): cd += [as_ufl(k), as_ufl(coefficient_derivatives[k])] coefficient_derivatives = ExprMapping(*cd) # Got a form? Apply derivatives to the integrands in turn. if isinstance(form, Form): integrals = [] for itg in form.integrals(): if isinstance(coefficient, SpatialCoordinate): fd = CoordinateDerivative( itg.integrand(), coefficients, arguments, coefficient_derivatives ) elif isinstance(coefficient, BaseForm) and not isinstance( coefficient, BaseFormOperator ): # Make the `ZeroBaseForm` arguments arguments = form.arguments() + coefficient.arguments() return ZeroBaseForm(arguments) else: fd = CoefficientDerivative( itg.integrand(), coefficients, arguments, coefficient_derivatives ) integrals.append(itg.reconstruct(fd)) return Form(integrals) elif isinstance(form, BaseFormOperator): if not isinstance(coefficient, SpatialCoordinate): return BaseFormOperatorDerivative( form, coefficients, arguments, coefficient_derivatives ) else: return BaseFormOperatorCoordinateDerivative( form, coefficients, arguments, coefficient_derivatives ) elif isinstance(form, BaseForm): if not isinstance(coefficient, SpatialCoordinate): return BaseFormDerivative(form, coefficients, arguments, coefficient_derivatives) else: return BaseFormCoordinateDerivative( form, coefficients, arguments, coefficient_derivatives ) elif isinstance(form, Expr): # What we got was in fact an integrand if not isinstance(coefficient, SpatialCoordinate): return CoefficientDerivative(form, coefficients, arguments, coefficient_derivatives) else: return CoordinateDerivative(form, coefficients, arguments, coefficient_derivatives) raise ValueError(f"Invalid argument type {type(form)}.") def sensitivity_rhs(a, u, L, v): r"""Compute the right hand side for a sensitivity calculation system. The derivation behind this computation is as follows. Assume *a*, *L* to be bilinear and linear forms corresponding to the assembled linear system .. math:: Ax = b. Where *x* is the vector of the discrete function corresponding to *u*. Let *v* be some scalar variable this equation depends on. Then we can write .. math:: 0 = \\frac{d}{dv}(Ax-b) = \\frac{dA}{dv} x + A \\frac{dx}{dv} - \\frac{db}{dv}, A \\frac{dx}{dv} = \\frac{db}{dv} - \\frac{dA}{dv} x, and solve this system for :math:`\\frac{dx}{dv}`, using the same bilinear form *a* and matrix *A* from the original system. Assume the forms are written :: v = variable(v_expression) L = IL(v) * dx a = Ia(v) * dx where ``IL`` and ``Ia`` are integrand expressions. Define a ``Coefficient u`` representing the solution to the equations. Then we can compute :math:`\\frac{db}{dv}` and :math:`\\frac{dA}{dv}` from the forms :: da = diff(a, v) dL = diff(L, v) and the action of ``da`` on ``u`` by :: dau = action(da, u) In total, we can build the right hand side of the system to compute :math:`\\frac{du}{dv}` with the single line :: dL = diff(L, v) - action(diff(a, v), u) or, using this function, :: dL = sensitivity_rhs(a, u, L, v) """ if not ( isinstance(a, Form) and isinstance(u, Coefficient) and isinstance(L, Form) and isinstance(v, Variable) ): raise ValueError( "Expecting (a, u, L, v), (bilinear form, function, linear form and scalar variable)." ) if not is_true_ufl_scalar(v): raise ValueError("Expecting scalar variable.") from ufl.operators import diff return diff(L, v) - action(diff(a, v), u) ufl-2024.2.0/ufl/functionspace.py000066400000000000000000000257301470142567200165470ustar00rootroot00000000000000"""Types for representing function spaces.""" # Copyright (C) 2015-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 import typing import numpy as np from ufl.core.ufl_type import UFLObject from ufl.domain import join_domains from ufl.duals import is_dual, is_primal from ufl.utils.sequences import product # Export list for ufl.classes __all_classes__ = [ "AbstractFunctionSpace", "FunctionSpace", "DualSpace", "MixedFunctionSpace", "TensorProductFunctionSpace", ] class AbstractFunctionSpace(object): """Abstract function space.""" def ufl_sub_spaces(self): """Return ufl sub spaces.""" raise NotImplementedError( f"Missing implementation of ufl_sub_spaces in {self.__class__.__name__}." ) class BaseFunctionSpace(AbstractFunctionSpace, UFLObject): """Base function space.""" def __init__(self, domain, element, label=""): """Initialise.""" if domain is None: # DOLFIN hack # TODO: Is anything expected from element.cell in this case? pass else: try: domain_cell = domain.ufl_cell() except AttributeError: raise ValueError( "Expected non-abstract domain for initalization of function space." ) else: if element.cell != domain_cell: raise ValueError("Non-matching cell of finite element and domain.") AbstractFunctionSpace.__init__(self) self._label = label self._ufl_domain = domain self._ufl_element = element @property def components(self) -> typing.Dict[typing.Tuple[int, ...], int]: """Get the numbering of the components of the element of this space. Returns: A map from the components of the values on a physical cell (eg (0, 1)) to flat component numbers on the reference cell (eg 1) """ from ufl.pullback import SymmetricPullback if isinstance(self._ufl_element.pullback, SymmetricPullback): return self._ufl_element.pullback._symmetry if len(self._ufl_element.sub_elements) == 0: return {(): 0} components = {} offset = 0 c_offset = 0 for s in self.ufl_sub_spaces(): for i, j in enumerate(np.ndindex(s.value_shape)): components[(offset + i,)] = c_offset + s.components[j] c_offset += max(s.components.values()) + 1 offset += s.value_size return components def label(self): """Return label of boundary domains to differentiate restricted and unrestricted.""" return self._label def ufl_sub_spaces(self): """Return ufl sub spaces.""" return () def ufl_domain(self): """Return ufl domain.""" return self._ufl_domain def ufl_element(self): """Return ufl element.""" return self._ufl_element def ufl_domains(self): """Return ufl domains.""" domain = self.ufl_domain() if domain is None: return () else: return (domain,) def _ufl_hash_data_(self, name=None): """UFL hash data.""" name = name or "BaseFunctionSpace" domain = self.ufl_domain() element = self.ufl_element() if domain is None: ddata = None else: ddata = domain._ufl_hash_data_() if element is None: edata = None else: edata = element._ufl_hash_data_() return (name, ddata, edata, self.label()) def _ufl_signature_data_(self, renumbering, name=None): """UFL signature data.""" name = name or "BaseFunctionSpace" domain = self.ufl_domain() element = self.ufl_element() if domain is None: ddata = None else: ddata = domain._ufl_signature_data_(renumbering) if element is None: edata = None else: edata = element._ufl_signature_data_() return (name, ddata, edata, self.label()) def __repr__(self): """Representation.""" return f"BaseFunctionSpace({self._ufl_domain!r}, {self._ufl_element!r})" @property def value_shape(self) -> typing.Tuple[int, ...]: """Return the shape of the value space on a physical domain.""" return self._ufl_element.pullback.physical_value_shape(self._ufl_element, self._ufl_domain) @property def value_size(self) -> int: """Return the integer product of the value shape on a physical domain.""" return product(self.value_shape) class FunctionSpace(BaseFunctionSpace, UFLObject): """Representation of a Function space.""" _primal = True _dual = False def dual(self): """Get the dual of the space.""" return DualSpace(self._ufl_domain, self._ufl_element, label=self.label()) def _ufl_hash_data_(self): """UFL hash data.""" return BaseFunctionSpace._ufl_hash_data_(self, "FunctionSpace") def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return BaseFunctionSpace._ufl_signature_data_(self, renumbering, "FunctionSpace") def __repr__(self): """Representation.""" return f"FunctionSpace({self._ufl_domain!r}, {self._ufl_element!r})" def __str__(self): """String.""" return f"FunctionSpace({self._ufl_domain}, {self._ufl_element})" class DualSpace(BaseFunctionSpace, UFLObject): """Representation of a Dual space.""" _primal = False _dual = True def __init__(self, domain, element, label=""): """Initialise.""" BaseFunctionSpace.__init__(self, domain, element, label) def dual(self): """Get the dual of the space.""" return FunctionSpace(self._ufl_domain, self._ufl_element, label=self.label()) def _ufl_hash_data_(self): """UFL hash data.""" return BaseFunctionSpace._ufl_hash_data_(self, "DualSpace") def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return BaseFunctionSpace._ufl_signature_data_(self, renumbering, "DualSpace") def __repr__(self): """Representation.""" return f"DualSpace({self._ufl_domain!r}, {self._ufl_element!r})" def __str__(self): """String.""" return f"DualSpace({self._ufl_domain}, {self._ufl_element})" class TensorProductFunctionSpace(AbstractFunctionSpace, UFLObject): """Tensor product function space.""" def __init__(self, *function_spaces): """Initialise.""" AbstractFunctionSpace.__init__(self) self._ufl_function_spaces = function_spaces def ufl_sub_spaces(self): """Return ufl sub spaces.""" return self._ufl_function_spaces def _ufl_hash_data_(self): """UFL hash data.""" return ("TensorProductFunctionSpace",) + tuple( V._ufl_hash_data_() for V in self.ufl_sub_spaces() ) def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return ("TensorProductFunctionSpace",) + tuple( V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces() ) def __repr__(self): """Representation.""" return f"TensorProductFunctionSpace(*{self._ufl_function_spaces!r})" def __str__(self): """String.""" return self.__repr__() class MixedFunctionSpace(AbstractFunctionSpace, UFLObject): """Mixed function space.""" def __init__(self, *args): """Initialise.""" AbstractFunctionSpace.__init__(self) self._ufl_function_spaces = args self._ufl_elements = list() for fs in args: if isinstance(fs, BaseFunctionSpace): self._ufl_elements.append(fs.ufl_element()) else: raise ValueError("Expecting BaseFunctionSpace objects") # A mixed FS is only primal/dual if all the subspaces are primal/dual" self._primal = all([is_primal(subspace) for subspace in self._ufl_function_spaces]) self._dual = all([is_dual(subspace) for subspace in self._ufl_function_spaces]) def ufl_sub_spaces(self): """Return ufl sub spaces.""" return self._ufl_function_spaces def ufl_sub_space(self, i): """Return i-th ufl sub space.""" return self._ufl_function_spaces[i] def dual(self, *args): """Return the dual to this function space. If no additional arguments are passed then a MixedFunctionSpace is returned whose components are the duals of the originals. If additional arguments are passed, these must be integers. In this case, the MixedFunctionSpace which is returned will have dual components in the positions corresponding to the arguments passed, and the original components in the other positions. """ if args: spaces = [ space.dual() if i in args else space for i, space in enumerate(self._ufl_function_spaces) ] return MixedFunctionSpace(*spaces) else: return MixedFunctionSpace(*[space.dual() for space in self._ufl_function_spaces]) def ufl_elements(self): """Return ufl elements.""" return self._ufl_elements def ufl_element(self): """Return ufl element.""" if len(self._ufl_elements) == 1: return self._ufl_elements[0] else: raise ValueError( "Found multiple elements. Cannot return only one. " "Consider building a FunctionSpace from a MixedElement " "in case of homogeneous dimension." ) def ufl_domains(self): """Return ufl domains.""" domainlist = [] for s in self._ufl_function_spaces: domainlist.extend(s.ufl_domains()) return join_domains(domainlist) def ufl_domain(self): """Return ufl domain.""" domains = self.ufl_domains() if len(domains) == 1: return domains[0] elif domains: raise ValueError("Found multiple domains, cannot return just one.") else: return None def num_sub_spaces(self): """Return number of subspaces.""" return len(self._ufl_function_spaces) def _ufl_hash_data_(self): """UFL hash data.""" return ("MixedFunctionSpace",) + tuple(V._ufl_hash_data_() for V in self.ufl_sub_spaces()) def _ufl_signature_data_(self, renumbering): """UFL signature data.""" return ("MixedFunctionSpace",) + tuple( V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces() ) def __repr__(self): """Representation.""" return f"MixedFunctionSpace(*{self._ufl_function_spaces!r})" def __str__(self): """String.""" return self.__repr__() ufl-2024.2.0/ufl/geometry.py000066400000000000000000000601431470142567200155360ustar00rootroot00000000000000"""Types for representing symbolic expressions for geometric quantities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.domain import as_domain, extract_unique_domain from ufl.sobolevspace import H1 """ Possible coordinate bootstrapping: Xf = Xf[q] FacetCoordinate = quadrature point on facet (ds,dS) X = X[q] CellCoordinate = quadrature point on cell (dx) x = x[q] SpatialCoordinate = quadrature point from input array (dc) Jacobians of mappings between coordinates: Jcf = dX/dXf = grad_Xf X(Xf) CellFacetJacobian Jxc = dx/dX = grad_X x(X) Jacobian Jxf = dx/dXf = grad_Xf x(Xf) = Jxc Jcf = dx/dX dX/dXf = grad_X x(X) grad_Xf X(Xf) FacetJacobian = Jacobian * CellFacetJacobian Possible computation of X from Xf: X = Jcf Xf + X0f CellCoordinate = CellFacetJacobian * FacetCoordinate + CellFacetOrigin Possible computation of x from X: x = f(X) SpatialCoordinate = sum_k xdofs_k * xphi_k(X) x = Jxc X + x0 SpatialCoordinate = Jacobian * CellCoordinate + CellOrigin Possible computation of x from Xf: x = x(X(Xf)) x = Jxf Xf + x0f SpatialCoordinate = FacetJacobian * FacetCoordinate + FacetOrigin Inverse relations: X = K * (x - x0) CellCoordinate = JacobianInverse * (SpatialCoordinate - CellOrigin) Xf = FK * (x - x0f) FacetCoordinate = FacetJacobianInverse * (SpatialCoordinate - FacetOrigin) Xf = CFK * (X - X0f) FacetCoordinate = CellFacetJacobianInverse * (CellCoordinate - CellFacetOrigin) """ # --- Expression node types @ufl_type(is_abstract=True) class GeometricQuantity(Terminal): """Geometric quantity.""" __slots__ = ("_domain",) def __init__(self, domain): """Initialise.""" Terminal.__init__(self) self._domain = as_domain(domain) def ufl_domains(self): """Get the UFL domains.""" return (self._domain,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # NB! Geometric quantities are piecewise constant by # default. Override if needed. return True # NB! Geometric quantities are scalar by default. Override if # needed. ufl_shape = () def _ufl_signature_data_(self, renumbering): """Signature data of geometric quantities depend on the domain numbering.""" return (self._ufl_class_.__name__,) + self._domain._ufl_signature_data_(renumbering) def __str__(self): """Format as a string.""" return self._ufl_class_.name def __repr__(self): """Representation.""" r = "%s(%s)" % (self._ufl_class_.__name__, repr(self._domain)) return r def _ufl_compute_hash_(self): """UFL compute hash.""" return hash((type(self).__name__,) + self._domain._ufl_hash_data_()) def __eq__(self, other): """Check equality.""" return isinstance(other, self._ufl_class_) and other._domain == self._domain @ufl_type(is_abstract=True) class GeometricCellQuantity(GeometricQuantity): """Geometric cell quantity.""" __slots__ = () @ufl_type(is_abstract=True) class GeometricFacetQuantity(GeometricQuantity): """Geometric facet quantity.""" __slots__ = () # --- Coordinate represented in different coordinate systems @ufl_type() class SpatialCoordinate(GeometricCellQuantity): """The coordinate in a domain. In the context of expression integration, represents the domain coordinate of each quadrature point. In the context of expression evaluation in a point, represents the value of that point. """ __slots__ = () name = "x" @property def ufl_shape(self): """Return the number of coordinates defined (i.e. the geometric dimension of the domain).""" g = self._domain.geometric_dimension() return (g,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only case this is true is if the domain is a vertex cell. t = self._domain.topological_dimension() return t == 0 def evaluate(self, x, mapping, component, index_values): """Return the value of the coordinate.""" if component == (): if isinstance(x, (tuple, list)): return float(x[0]) else: return float(x) else: return float(x[component[0]]) def count(self): """Count.""" # FIXME: Hack to make SpatialCoordinate behave like a coefficient. # When calling `derivative`, the count is used to sort over. return -1 @ufl_type() class CellCoordinate(GeometricCellQuantity): """The coordinate in a reference cell. In the context of expression integration, represents the reference cell coordinate of each quadrature point. In the context of expression evaluation in a point in a cell, represents that point in the reference coordinate system of the cell. """ __slots__ = () name = "X" @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only case this is true is if the domain is a vertex cell. t = self._domain.topological_dimension() return t == 0 @ufl_type() class FacetCoordinate(GeometricFacetQuantity): """The coordinate in a reference cell of a facet. In the context of expression integration over a facet, represents the reference facet coordinate of each quadrature point. In the context of expression evaluation in a point on a facet, represents that point in the reference coordinate system of the facet. """ __slots__ = () name = "Xf" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError("FacetCoordinate is only defined for topological dimensions >= 2.") @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t - 1,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only case this is true is if the domain is an interval cell # (with a vertex facet). t = self._domain.topological_dimension() return t <= 1 # --- Origin of coordinate systems in larger coordinate systems @ufl_type() class CellOrigin(GeometricCellQuantity): """The spatial coordinate corresponding to origin of a reference cell.""" __slots__ = () name = "x0" @property def ufl_shape(self): """Get the UFL shape.""" g = self._domain.geometric_dimension() return (g,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" return True @ufl_type() class FacetOrigin(GeometricFacetQuantity): """The spatial coordinate corresponding to origin of a reference facet.""" __slots__ = () name = "x0f" @property def ufl_shape(self): """Get the UFL shape.""" g = self._domain.geometric_dimension() return (g,) @ufl_type() class CellFacetOrigin(GeometricFacetQuantity): """The reference cell coordinate corresponding to origin of a reference facet.""" __slots__ = () name = "X0f" @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t,) # --- Jacobians of mappings between coordinate systems @ufl_type() class Jacobian(GeometricCellQuantity): r"""The Jacobian of the mapping from reference cell to spatial coordinates. .. math:: J_{ij} = \\frac{dx_i}{dX_j} """ __slots__ = () name = "J" @property def ufl_shape(self): """Return the number of coordinates defined (i.e. the geometric dimension of the domain).""" g = self._domain.geometric_dimension() t = self._domain.topological_dimension() return (g, t) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class FacetJacobian(GeometricFacetQuantity): """The Jacobian of the mapping from reference facet to spatial coordinates. FJ_ij = dx_i/dXf_j The FacetJacobian is the product of the Jacobian and CellFacetJacobian: FJ = dx/dXf = dx/dX dX/dXf = J * CFJ """ __slots__ = () name = "FJ" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError("FacetJacobian is only defined for topological dimensions >= 2.") @property def ufl_shape(self): """Get the UFL shape.""" g = self._domain.geometric_dimension() t = self._domain.topological_dimension() return (g, t - 1) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex # cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class CellFacetJacobian(GeometricFacetQuantity): # dX/dXf """The Jacobian of the mapping from reference facet to reference cell coordinates. CFJ_ij = dX_i/dXf_j """ __slots__ = () name = "CFJ" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError("CellFacetJacobian is only defined for topological dimensions >= 2.") @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t, t - 1) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always a constant mapping between two reference # coordinate systems. return True @ufl_type() class ReferenceCellEdgeVectors(GeometricCellQuantity): """The vectors between reference cell vertices for each edge in cell.""" __slots__ = () name = "RCEV" def __init__(self, domain): """Initialise.""" GeometricCellQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError("CellEdgeVectors is only defined for topological dimensions >= 2.") @property def ufl_shape(self): """Get the UFL shape.""" cell = extract_unique_domain(self).ufl_cell() ne = cell.num_edges() t = cell.topological_dimension() return (ne, t) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always constant for a given cell type return True @ufl_type() class ReferenceFacetEdgeVectors(GeometricFacetQuantity): """The vectors between reference cell vertices for each edge in current facet.""" __slots__ = () name = "RFEV" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 3: raise ValueError("FacetEdgeVectors is only defined for topological dimensions >= 3.") @property def ufl_shape(self): """Get the UFL shape.""" cell = extract_unique_domain(self).ufl_cell() facet_types = cell.facet_types() # Raise exception for cells with more than one facet type e.g. prisms if len(facet_types) > 1: raise Exception(f"Cell type {cell} not supported.") nfe = facet_types[0].num_edges() t = cell.topological_dimension() return (nfe, t) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always constant for a given cell type return True @ufl_type() class CellVertices(GeometricCellQuantity): """Physical cell vertices.""" __slots__ = () name = "CV" def __init__(self, domain): """Initialise.""" GeometricCellQuantity.__init__(self, domain) @property def ufl_shape(self): """Get the UFL shape.""" domain = extract_unique_domain(self) cell = domain.ufl_cell() nv = cell.num_vertices() g = domain.geometric_dimension() return (nv, g) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always constant for a given cell type return True @ufl_type() class CellEdgeVectors(GeometricCellQuantity): """The vectors between physical cell vertices for each edge in cell.""" __slots__ = () name = "CEV" def __init__(self, domain): """Initialise.""" GeometricCellQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError("CellEdgeVectors is only defined for topological dimensions >= 2.") @property def ufl_shape(self): """Get the UFL shape.""" domain = extract_unique_domain(self) cell = domain.ufl_cell() ne = cell.num_edges() g = domain.geometric_dimension() return (ne, g) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always constant for a given cell type return True @ufl_type() class FacetEdgeVectors(GeometricFacetQuantity): """The vectors between physical cell vertices for each edge in current facet.""" __slots__ = () name = "FEV" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 3: raise ValueError("FacetEdgeVectors is only defined for topological dimensions >= 3.") @property def ufl_shape(self): """Get the UFL shape.""" domain = extract_unique_domain(self) cell = domain.ufl_cell() facet_types = cell.facet_types() # Raise exception for cells with more than one facet type e.g. prisms if len(facet_types) > 1: raise Exception(f"Cell type {cell} not supported.") nfe = facet_types[0].num_edges() g = domain.geometric_dimension() return (nfe, g) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # This is always constant for a given cell type return True # --- Determinants (signed or pseudo) of geometry mapping Jacobians @ufl_type() class JacobianDeterminant(GeometricCellQuantity): """The determinant of the Jacobian. Represents the signed determinant of a square Jacobian or the pseudo-determinant of a non-square Jacobian. """ __slots__ = () name = "detJ" def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex # cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class FacetJacobianDeterminant(GeometricFacetQuantity): """The pseudo-determinant of the FacetJacobian.""" __slots__ = () name = "detFJ" def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class CellFacetJacobianDeterminant(GeometricFacetQuantity): """The pseudo-determinant of the CellFacetJacobian.""" __slots__ = () name = "detCFJ" def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex # cells return self._domain.is_piecewise_linear_simplex_domain() # --- Inverses (signed or pseudo) of geometry mapping Jacobians @ufl_type() class JacobianInverse(GeometricCellQuantity): """The inverse of the Jacobian. Represents the inverse of a square Jacobian or the pseudo-inverse of a non-square Jacobian. """ __slots__ = () name = "K" @property def ufl_shape(self): """Return the number of coordinates defined (i.e. the geometric dimension of the domain).""" g = self._domain.geometric_dimension() t = self._domain.topological_dimension() return (t, g) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex # cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class FacetJacobianInverse(GeometricFacetQuantity): """The pseudo-inverse of the FacetJacobian.""" __slots__ = () name = "FK" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError( "FacetJacobianInverse is only defined for topological dimensions >= 2." ) @property def ufl_shape(self): """Get the UFL shape.""" g = self._domain.geometric_dimension() t = self._domain.topological_dimension() return (t - 1, g) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex # cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class CellFacetJacobianInverse(GeometricFacetQuantity): """The pseudo-inverse of the CellFacetJacobian.""" __slots__ = () name = "CFK" def __init__(self, domain): """Initialise.""" GeometricFacetQuantity.__init__(self, domain) t = self._domain.topological_dimension() if t < 2: raise ValueError( "CellFacetJacobianInverse is only defined for topological dimensions >= 2." ) @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t - 1, t) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex cells return self._domain.is_piecewise_linear_simplex_domain() # --- Types representing normal or tangent vectors @ufl_type() class FacetNormal(GeometricFacetQuantity): """The outwards pointing normal vector of the current facet.""" __slots__ = () name = "n" @property def ufl_shape(self): """Return the number of coordinates defined (i.e. the geometric dimension of the domain).""" g = self._domain.geometric_dimension() return (g,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # For product cells, this is only true for some but not all # facets. Seems like too much work to fix right now. Only # true for a piecewise linear coordinate field with simplex # _facets_. ce = self._domain.ufl_coordinate_element() is_piecewise_linear = ce.embedded_superdegree <= 1 and ce in H1 return is_piecewise_linear and self._domain.ufl_cell().has_simplex_facets() @ufl_type() class CellNormal(GeometricCellQuantity): """The upwards pointing normal vector of the current manifold cell.""" __slots__ = () name = "cell_normal" @property def ufl_shape(self): """Return the number of coordinates defined (i.e. the geometric dimension of the domain).""" g = self._domain.geometric_dimension() # t = self._domain.topological_dimension() # return (g-t,g) # TODO: Should it be CellNormals? For interval in 3D we have two! return (g,) def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # Only true for a piecewise linear coordinate field in simplex cells return self._domain.is_piecewise_linear_simplex_domain() @ufl_type() class ReferenceNormal(GeometricFacetQuantity): """The outwards pointing normal vector of the current facet on the reference cell.""" __slots__ = () name = "reference_normal" @property def ufl_shape(self): """Get the UFL shape.""" t = self._domain.topological_dimension() return (t,) # --- Types representing measures of the cell and entities of the cell, # typically used for stabilisation terms # TODO: Clean up this set of types? Document! @ufl_type() class ReferenceCellVolume(GeometricCellQuantity): """The volume of the reference cell.""" __slots__ = () name = "reference_cell_volume" @ufl_type() class ReferenceFacetVolume(GeometricFacetQuantity): """The volume of the reference cell of the current facet.""" __slots__ = () name = "reference_facet_volume" @ufl_type() class CellVolume(GeometricCellQuantity): """The volume of the cell.""" __slots__ = () name = "volume" @ufl_type() class Circumradius(GeometricCellQuantity): """The circumradius of the cell.""" __slots__ = () name = "circumradius" @ufl_type() class CellDiameter(GeometricCellQuantity): """The diameter of the cell, i.e., maximal distance of two points in the cell.""" __slots__ = () name = "diameter" @ufl_type() class FacetArea(GeometricFacetQuantity): # FIXME: Should this be allowed for interval domain? """The area of the facet.""" __slots__ = () name = "facetarea" @ufl_type() class MinCellEdgeLength(GeometricCellQuantity): """The minimum edge length of the cell.""" __slots__ = () name = "mincelledgelength" @ufl_type() class MaxCellEdgeLength(GeometricCellQuantity): """The maximum edge length of the cell.""" __slots__ = () name = "maxcelledgelength" @ufl_type() class MinFacetEdgeLength(GeometricFacetQuantity): """The minimum edge length of the facet.""" __slots__ = () name = "minfacetedgelength" @ufl_type() class MaxFacetEdgeLength(GeometricFacetQuantity): """The maximum edge length of the facet.""" __slots__ = () name = "maxfacetedgelength" # --- Types representing other stuff @ufl_type() class CellOrientation(GeometricCellQuantity): """The orientation (+1/-1) of the current cell. For non-manifold cells (tdim == gdim), this equals the sign of the Jacobian determinant, i.e. +1 if the physical cell is oriented the same way as the reference cell and -1 otherwise. For manifold cells of tdim==gdim-1 this is input data belonging to the mesh, used to distinguish between the sides of the manifold. """ __slots__ = () name = "cell_orientation" @ufl_type() class FacetOrientation(GeometricFacetQuantity): """The orientation (+1/-1) of the current facet relative to the reference cell.""" __slots__ = () name = "facet_orientation" # This doesn't quite fit anywhere. Make a special set of symbolic # terminal types instead? @ufl_type() class QuadratureWeight(GeometricQuantity): """The current quadrature weight. Only used inside a quadrature context. """ __slots__ = () name = "weight" def is_cellwise_constant(self): """Return whether this expression is spatially constant over each cell.""" # The weight usually varies with the quadrature points return False ufl-2024.2.0/ufl/index_combination_utils.py000066400000000000000000000153121470142567200206120ustar00rootroot00000000000000"""Utilities for analysing and manipulating free index tuples.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.multiindex import FixedIndex, Index, indices # FIXME: Some of these might be merged into one function, some might # be optimized def unique_sorted_indices(indices): """Get unique sorted indices. Given a list of (id, dim) tuples already sorted by id, return a unique list with duplicates removed. Also checks that the dimensions of duplicates are matching. """ newindices = [] prev = (None, None) for i in indices: if i[0] != prev[0]: newindices.append(i) prev = i else: if i[1] != prev[1]: raise ValueError("Nonmatching dimensions for free indices with same id!") return tuple(newindices) def merge_unique_indices(afi, afid, bfi, bfid): """Merge two pairs of (index ids, index dimensions) sequences into one pair without duplicates. The id tuples afi, bfi are assumed already sorted by id. Given a list of (id, dim) tuples already sorted by id, return a unique list with duplicates removed. Also checks that the dimensions of duplicates are matching. """ na = len(afi) nb = len(bfi) if na == 0: return bfi, bfid elif nb == 0: return afi, afid ak = 0 bk = 0 fi = [] fid = [] while True: if afi[ak] < bfi[bk]: fi.append(afi[ak]) fid.append(afid[ak]) ak += 1 elif afi[ak] > bfi[bk]: fi.append(bfi[bk]) fid.append(bfid[bk]) bk += 1 else: fi.append(afi[ak]) fid.append(afid[ak]) ak += 1 bk += 1 if ak == na: if bk != nb: fi.extend(bfi[bk:]) fid.extend(bfid[bk:]) break elif bk == nb: fi.extend(afi[ak:]) fid.extend(afid[ak:]) break return tuple(fi), tuple(fid) def remove_indices(fi, fid, rfi): """Remove indices.""" if not rfi: return fi, fid rfip = sorted((r, p) for p, r in enumerate(rfi)) nrfi = len(rfi) nfi = len(fi) shape = [None] * nrfi k = 0 pos = 0 newfiid = [] while pos < nfi: rk = rfip[k][0] # Keep while fi[pos] < rk: newfiid.append((fi[pos], fid[pos])) pos += 1 # Skip removed = 0 while pos < nfi and fi[pos] == rk: shape[rfip[k][1]] = fid[pos] pos += 1 removed += 1 # Expecting to find each index from rfi in fi if not removed: raise ValueError(f"Index to be removed ({rk}) not part of indices ({fi}).") # Next to remove k += 1 if k == nrfi: # No more to remove, keep the rest if pos < nfi: newfiid.extend(zip(fi[pos:], fid[pos:])) break assert None not in shape # Unpack into two tuples fi, fid = zip(*newfiid) if newfiid else ((), ()) return fi, fid, tuple(shape) def create_slice_indices(component, shape, fi): """Create slice indices.""" all_indices = [] slice_indices = [] repeated_indices = [] free_indices = [] for ind in component: if isinstance(ind, Index): all_indices.append(ind) if ind.count() in fi or ind in free_indices: repeated_indices.append(ind) free_indices.append(ind) elif isinstance(ind, FixedIndex): if int(ind) >= shape[len(all_indices)]: raise ValueError("Index out of bounds.") all_indices.append(ind) elif isinstance(ind, int): if int(ind) >= shape[len(all_indices)]: raise ValueError("Index out of bounds.") all_indices.append(FixedIndex(ind)) elif isinstance(ind, slice): if ind != slice(None): raise ValueError("Only full slices (:) allowed.") i = Index() slice_indices.append(i) all_indices.append(i) elif ind == Ellipsis: er = len(shape) - len(component) + 1 ii = indices(er) slice_indices.extend(ii) all_indices.extend(ii) else: raise ValueError(f"Not expecting {ind}.") if len(all_indices) != len(shape): raise ValueError("Component and shape length don't match.") return tuple(all_indices), tuple(slice_indices), tuple(repeated_indices) # Outer etc. def merge_nonoverlapping_indices(a, b): """Merge non-overlapping free indices into one representation. Example: C[i,j,r,s] = outer(A[i,s], B[j,r]) A, B -> (i,j,r,s), (idim,jdim,rdim,sdim) """ # Extract input properties ai = a.ufl_free_indices bi = b.ufl_free_indices aid = a.ufl_index_dimensions bid = b.ufl_index_dimensions # Merge lists to return s = sorted(zip(ai + bi, aid + bid)) if s: free_indices, index_dimensions = zip(*s) # Consistency checks if len(set(free_indices)) != len(free_indices): raise ValueError("Not expecting repeated indices.") else: free_indices, index_dimensions = (), () return free_indices, index_dimensions # Product def merge_overlapping_indices(afi, afid, bfi, bfid): """Merge overlapping free indices into one free and one repeated representation. Example: C[j,r] := A[i,j,k] * B[i,r,k] A, B -> (j,r), (jdim,rdim), (i,k), (idim,kdim) """ # Extract input properties an = len(afi) bn = len(bfi) # Lists to return free_indices = [] index_dimensions = [] repeated_indices = [] repeated_index_dimensions = [] # Find repeated indices, brute force version for i0 in range(an): for i1 in range(bn): if afi[i0] == bfi[i1]: repeated_indices.append(afi[i0]) repeated_index_dimensions.append(afid[i0]) break # Collect only non-repeated indices, brute force version for i, d in sorted(zip(afi + bfi, afid + bfid)): if i not in repeated_indices: free_indices.append(i) index_dimensions.append(d) # Consistency checks if len(set(free_indices)) != len(free_indices): raise ValueError("Not expecting repeated indices left.") if len(free_indices) + 2 * len(repeated_indices) != an + bn: raise ValueError("Expecting only twice repeated indices.") return ( tuple(free_indices), tuple(index_dimensions), tuple(repeated_indices), tuple(repeated_index_dimensions), ) ufl-2024.2.0/ufl/indexed.py000066400000000000000000000100421470142567200153140ustar00rootroot00000000000000"""This module defines the Indexed class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str from ufl.core.multiindex import FixedIndex, Index, MultiIndex from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.index_combination_utils import unique_sorted_indices from ufl.precedence import parstr @ufl_type(is_shaping=True, num_ops=2, is_terminal_modifier=True) class Indexed(Operator): """Indexed expression.""" __slots__ = ( "ufl_free_indices", "ufl_index_dimensions", ) def __new__(cls, expression, multiindex): """Create a new Indexed.""" if isinstance(expression, Zero): # Zero-simplify indexed Zero objects shape = expression.ufl_shape efi = expression.ufl_free_indices efid = expression.ufl_index_dimensions fi = list(zip(efi, efid)) for pos, ind in enumerate(multiindex._indices): if isinstance(ind, Index): fi.append((ind.count(), shape[pos])) fi = unique_sorted_indices(sorted(fi)) if fi: fi, fid = zip(*fi) else: fi, fid = (), () return Zero(shape=(), free_indices=fi, index_dimensions=fid) elif expression.ufl_shape == () and multiindex == (): return expression else: return Operator.__new__(cls) def __init__(self, expression, multiindex): """Initialise.""" # Store operands Operator.__init__(self, (expression, multiindex)) # Error checking if not isinstance(expression, Expr): raise ValueError(f"Expecting Expr instance, not {ufl_err_str(expression)}.") if not isinstance(multiindex, MultiIndex): raise ValueError(f"Expecting MultiIndex instance, not {ufl_err_str(multiindex)}.") shape = expression.ufl_shape # Error checking if len(shape) != len(multiindex): raise ValueError( f"Invalid number of indices ({len(multiindex)}) for tensor " f"expression of rank {len(expression.ufl_shape)}:\n {ufl_err_str(expression)}" ) if any( int(di) >= int(si) or int(di) < 0 for si, di in zip(shape, multiindex) if isinstance(di, FixedIndex) ): raise ValueError("Fixed index out of range!") # Build tuples of free index ids and dimensions efi = expression.ufl_free_indices efid = expression.ufl_index_dimensions fi = list(zip(efi, efid)) for pos, ind in enumerate(multiindex._indices): if isinstance(ind, Index): fi.append((ind.count(), shape[pos])) fi = unique_sorted_indices(sorted(fi)) if fi: fi, fid = zip(*fi) else: fi, fid = (), () # Cache free index and dimensions self.ufl_free_indices = fi self.ufl_index_dimensions = fid ufl_shape = () def evaluate(self, x, mapping, component, index_values, derivatives=()): """Evaluate.""" A, ii = self.ufl_operands component = ii.evaluate(x, mapping, None, index_values) if derivatives: return A.evaluate(x, mapping, component, index_values, derivatives) else: return A.evaluate(x, mapping, component, index_values) def __str__(self): """Format as a string.""" return "%s[%s]" % (parstr(self.ufl_operands[0], self), self.ufl_operands[1]) def __getitem__(self, key): """Get an item.""" if key == (): # So that one doesn't have to special case indexing of # expressions without shape. return self raise ValueError( f"Attempting to index with {ufl_err_str(key)}, " f"but object is already indexed: {ufl_err_str(self)}" ) ufl-2024.2.0/ufl/indexsum.py000066400000000000000000000052211470142567200155330ustar00rootroot00000000000000"""This module defines the IndexSum class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str from ufl.core.multiindex import MultiIndex from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.precedence import parstr # --- Sum over an index --- @ufl_type(num_ops=2) class IndexSum(Operator): """Index sum.""" __slots__ = ("_dimension", "ufl_free_indices", "ufl_index_dimensions") def __new__(cls, summand, index): """Create a new IndexSum.""" # Error checks if not isinstance(summand, Expr): raise ValueError(f"Expecting Expr instance, got {ufl_err_str(summand)}") if not isinstance(index, MultiIndex): raise ValueError(f"Expecting MultiIndex instance, got {ufl_err_str(index)}") if len(index) != 1: raise ValueError(f"Expecting a single Index but got {len(index)}.") # Simplification to zero if isinstance(summand, Zero): sh = summand.ufl_shape (j,) = index fi = summand.ufl_free_indices fid = summand.ufl_index_dimensions pos = fi.index(j.count()) fi = fi[:pos] + fi[pos + 1 :] fid = fid[:pos] + fid[pos + 1 :] return Zero(sh, fi, fid) return Operator.__new__(cls) def __init__(self, summand, index): """Initialise.""" (j,) = index fi = summand.ufl_free_indices fid = summand.ufl_index_dimensions pos = fi.index(j.count()) self._dimension = fid[pos] self.ufl_free_indices = fi[:pos] + fi[pos + 1 :] self.ufl_index_dimensions = fid[:pos] + fid[pos + 1 :] Operator.__init__(self, (summand, index)) def index(self): """Get index.""" return self.ufl_operands[1][0] def dimension(self): """Get dimension.""" return self._dimension @property def ufl_shape(self): """Get UFL shape.""" return self.ufl_operands[0].ufl_shape def evaluate(self, x, mapping, component, index_values): """Evaluate.""" (i,) = self.ufl_operands[1] tmp = 0 for k in range(self._dimension): index_values.push(i, k) tmp += self.ufl_operands[0].evaluate(x, mapping, component, index_values) index_values.pop() return tmp def __str__(self): """Format as a string.""" return "sum_{%s} %s " % (str(self.ufl_operands[1]), parstr(self.ufl_operands[0], self)) ufl-2024.2.0/ufl/integral.py000066400000000000000000000117611470142567200155120ustar00rootroot00000000000000"""The Integral class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008-2009 # Modified by Massimiliano Leoni, 2016. import ufl from ufl.checks import is_python_scalar, is_scalar_constant_expression from ufl.core.expr import Expr from ufl.protocols import id_or_none # Export list for ufl.classes __all_classes__ = ["Integral"] class Integral(object): """An integral over a single domain.""" __slots__ = ( "_integrand", "_integral_type", "_ufl_domain", "_subdomain_id", "_metadata", "_subdomain_data", ) def __init__(self, integrand, integral_type, domain, subdomain_id, metadata, subdomain_data): """Initialise.""" if not isinstance(integrand, Expr): raise ValueError("Expecting integrand to be an Expr instance.") self._integrand = integrand self._integral_type = integral_type self._ufl_domain = domain self._subdomain_id = subdomain_id self._metadata = metadata self._subdomain_data = subdomain_data def reconstruct( self, integrand=None, integral_type=None, domain=None, subdomain_id=None, metadata=None, subdomain_data=None, ): """Construct a new Integral object with some properties replaced with new values. Example: b = a.reconstruct(expand_compounds(a.integrand())) c = a.reconstruct(metadata={'quadrature_degree':2}) """ if integrand is None: integrand = self.integrand() if integral_type is None: integral_type = self.integral_type() if domain is None: domain = self.ufl_domain() if subdomain_id is None: subdomain_id = self.subdomain_id() if metadata is None: metadata = self.metadata() if subdomain_data is None: subdomain_data = self._subdomain_data return Integral(integrand, integral_type, domain, subdomain_id, metadata, subdomain_data) def integrand(self): """Return the integrand expression, which is an ``Expr`` instance.""" return self._integrand def integral_type(self): """Return the domain type of this integral.""" return self._integral_type def ufl_domain(self): """Return the integration domain of this integral.""" return self._ufl_domain def subdomain_id(self): """Return the subdomain id of this integral.""" return self._subdomain_id def metadata(self): """Return the compiler metadata this integral has been annotated with.""" return self._metadata def subdomain_data(self): """Return the domain data of this integral.""" return self._subdomain_data def __neg__(self): """Negate.""" return self.reconstruct(-self._integrand) def __mul__(self, scalar): """Multiply.""" if not is_python_scalar(scalar): raise ValueError("Cannot multiply an integral with non-constant values.") return self.reconstruct(scalar * self._integrand) def __rmul__(self, scalar): """Multiply.""" if not is_scalar_constant_expression(scalar): raise ValueError( "An integral can only be multiplied by a globally constant scalar expression." ) return self.reconstruct(scalar * self._integrand) def __str__(self): """Format as a string.""" fmt = "{ %s } * %s(%s[%s], %s)" mname = ufl.measure.integral_type_to_measure_name[self._integral_type] s = fmt % (self._integrand, mname, self._ufl_domain, self._subdomain_id, self._metadata) return s def __repr__(self): """Representation.""" return ( f"Integral({self._integrand!r}, {self._integral_type!r}, {self._ufl_domain!r}, " f"{self._subdomain_id!r}, {self._metadata!r}, {self._subdomain_data!r})" ) def __eq__(self, other): """Check equality.""" return ( isinstance(other, Integral) and self._integral_type == other._integral_type and self._ufl_domain == other._ufl_domain and self._subdomain_id == other._subdomain_id and self._integrand == other._integrand and self._metadata == other._metadata and id_or_none(self._subdomain_data) == id_or_none(other._subdomain_data) ) def __hash__(self): """Hash.""" # Assuming few collisions by ignoring hash(self._metadata) (a # dict is not hashable but we assume it is immutable in # practice) hashdata = ( hash(self._integrand), self._integral_type, hash(self._ufl_domain), self._subdomain_id, id_or_none(self._subdomain_data), ) return hash(hashdata) ufl-2024.2.0/ufl/mathfunctions.py000066400000000000000000000333731470142567200165720ustar00rootroot00000000000000"""This module provides basic mathematical functions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 # Modified by Kristian B. Oelgaard, 2011 import cmath import math import numbers import warnings from ufl.constantvalue import ( ComplexValue, ConstantValue, FloatValue, IntValue, RealValue, Zero, as_ufl, is_true_ufl_scalar, ) from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type """ TODO: Include additional functions available in (need derivatives as well): Exponential and logarithmic functions: log10 Compute common logarithm (function) TODO: Any other useful special functions? About bessel functions: http://en.wikipedia.org/wiki/Bessel_function Portable implementations of bessel functions: http://www.boost.org/doc/libs/1_47_0/libs/math/doc/sf_and_dist/html/math_toolkit/main_overview/tr1.html Implementation in C++ std::tr1:: or boost::math::tr1:: - BesselK: cyl_bessel_k(nu, x) - BesselI: cyl_bessel_i(nu, x) - BesselJ: cyl_bessel_j(nu, x) - BesselY: cyl_neumann(nu, x) """ # --- Function representations --- @ufl_type(is_abstract=True, is_scalar=True, num_ops=1) class MathFunction(Operator): """Base class for all unary scalar math functions.""" # Freeze member variables for objects in this class __slots__ = ("_name",) def __init__(self, name, argument): """Initialise.""" Operator.__init__(self, (argument,)) if not is_true_ufl_scalar(argument): raise ValueError("Expecting scalar argument.") self._name = name def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) try: if isinstance(a, numbers.Real): res = getattr(math, self._name)(a) else: res = getattr(cmath, self._name)(a) except ValueError: warnings.warn( "Value error in evaluation of function %s with argument %s." % (self._name, a) ) raise return res def __str__(self): """Format as a string.""" return "%s(%s)" % (self._name, self.ufl_operands[0]) @ufl_type() class Sqrt(MathFunction): """Square root.""" __slots__ = () def __new__(cls, argument): """Create a new Sqrt.""" if isinstance(argument, (RealValue, Zero, numbers.Real)): if float(argument) < 0: return ComplexValue(cmath.sqrt(complex(argument))) else: return FloatValue(math.sqrt(float(argument))) if isinstance(argument, (ComplexValue, complex)): return ComplexValue(cmath.sqrt(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "sqrt", argument) @ufl_type() class Exp(MathFunction): """Exponentiation..""" __slots__ = () def __new__(cls, argument): """Create a new Exp.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.exp(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.exp(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "exp", argument) @ufl_type() class Ln(MathFunction): """Natural logarithm.""" __slots__ = () def __new__(cls, argument): """Create a new Ln.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.log(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.log(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "ln", argument) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) try: return math.log(a) except TypeError: return cmath.log(a) @ufl_type() class Cos(MathFunction): """Cosine.""" __slots__ = () def __new__(cls, argument): """Create a new Cos.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.cos(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.cos(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "cos", argument) @ufl_type() class Sin(MathFunction): """Sine.""" __slots__ = () def __new__(cls, argument): """Create a new Sin.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.sin(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.sin(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "sin", argument) @ufl_type() class Tan(MathFunction): """Tangent.""" __slots__ = () def __new__(cls, argument): """Create a new Tan.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.tan(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.tan(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "tan", argument) @ufl_type() class Cosh(MathFunction): """Hyperbolic cosine.""" __slots__ = () def __new__(cls, argument): """Create a new Cosh.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.cosh(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.cosh(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "cosh", argument) @ufl_type() class Sinh(MathFunction): """Hyperbolic sine.""" __slots__ = () def __new__(cls, argument): """Create a new Sinh.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.sinh(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.sinh(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "sinh", argument) @ufl_type() class Tanh(MathFunction): """Hyperbolic tangent.""" __slots__ = () def __new__(cls, argument): """Create a new Tanh.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.tanh(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.tanh(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "tanh", argument) @ufl_type() class Acos(MathFunction): """Inverse cosine.""" __slots__ = () def __new__(cls, argument): """Create a new Acos.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.acos(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.acos(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "acos", argument) @ufl_type() class Asin(MathFunction): """Inverse sine.""" __slots__ = () def __new__(cls, argument): """Create a new Asin.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.asin(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.asin(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "asin", argument) @ufl_type() class Atan(MathFunction): """Inverse tangent.""" __slots__ = () def __new__(cls, argument): """Create a new Atan.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.atan(float(argument))) if isinstance(argument, (ComplexValue)): return ComplexValue(cmath.atan(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "atan", argument) @ufl_type(is_scalar=True, num_ops=2) class Atan2(Operator): """Inverse tangent with two inputs.""" __slots__ = () def __new__(cls, arg1, arg2): """Create a new Atan2.""" if isinstance(arg1, (RealValue, Zero)) and isinstance(arg2, (RealValue, Zero)): return FloatValue(math.atan2(float(arg1), float(arg2))) if isinstance(arg1, (ComplexValue)) or isinstance(arg2, (ComplexValue)): raise TypeError("Atan2 does not support complex numbers.") return Operator.__new__(cls) def __init__(self, arg1, arg2): """Initialise.""" Operator.__init__(self, (arg1, arg2)) if isinstance(arg1, (ComplexValue, complex)) or isinstance(arg2, (ComplexValue, complex)): raise TypeError("Atan2 does not support complex numbers.") if not is_true_ufl_scalar(arg1): raise ValueError("Expecting scalar argument 1.") if not is_true_ufl_scalar(arg2): raise ValueError("Expecting scalar argument 2.") def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) b = self.ufl_operands[1].evaluate(x, mapping, component, index_values) try: res = math.atan2(a, b) except TypeError: raise ValueError("Atan2 does not support complex numbers.") except ValueError: warnings.warn( "Value error in evaluation of function atan2 with arguments %s, %s." % (a, b) ) raise return res def __str__(self): """Format as a string.""" return "atan2(%s,%s)" % (self.ufl_operands[0], self.ufl_operands[1]) @ufl_type() class Erf(MathFunction): """Erf function.""" __slots__ = () def __new__(cls, argument): """Create a new Erf.""" if isinstance(argument, (RealValue, Zero)): return FloatValue(math.erf(float(argument))) if isinstance(argument, (ConstantValue)): return ComplexValue(math.erf(complex(argument))) return MathFunction.__new__(cls) def __init__(self, argument): """Initialise.""" MathFunction.__init__(self, "erf", argument) def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return math.erf(a) @ufl_type(is_abstract=True, is_scalar=True, num_ops=2) class BesselFunction(Operator): """Base class for all bessel functions.""" __slots__ = "_name" def __init__(self, name, nu, argument): """Initialise.""" if not is_true_ufl_scalar(nu): raise ValueError("Expecting scalar nu.") if not is_true_ufl_scalar(argument): raise ValueError("Expecting scalar argument.") # Use integer representation if suitable fnu = float(nu) inu = int(nu) if fnu == inu: nu = as_ufl(inu) else: nu = as_ufl(fnu) Operator.__init__(self, (nu, argument)) self._name = name def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[1].evaluate(x, mapping, component, index_values) try: import scipy.special except ImportError: raise ValueError( "You must have scipy installed to evaluate bessel functions in python." ) name = self._name[-1] if isinstance(self.ufl_operands[0], IntValue): nu = int(self.ufl_operands[0]) functype = "n" if name != "i" else "v" else: nu = self.ufl_operands[0].evaluate(x, mapping, component, index_values) functype = "v" func = getattr(scipy.special, name + functype) return func(nu, a) def __str__(self): """Format as a string.""" return "%s(%s, %s)" % (self._name, self.ufl_operands[0], self.ufl_operands[1]) @ufl_type() class BesselJ(BesselFunction): """Bessel J function.""" __slots__ = () def __init__(self, nu, argument): """Initialise.""" BesselFunction.__init__(self, "cyl_bessel_j", nu, argument) @ufl_type() class BesselY(BesselFunction): """Bessel Y function.""" __slots__ = () def __init__(self, nu, argument): """Initialise.""" BesselFunction.__init__(self, "cyl_bessel_y", nu, argument) @ufl_type() class BesselI(BesselFunction): """Bessel I function.""" __slots__ = () def __init__(self, nu, argument): """Initialise.""" BesselFunction.__init__(self, "cyl_bessel_i", nu, argument) @ufl_type() class BesselK(BesselFunction): """Bessel K function.""" __slots__ = () def __init__(self, nu, argument): """Initialise.""" BesselFunction.__init__(self, "cyl_bessel_k", nu, argument) ufl-2024.2.0/ufl/matrix.py000066400000000000000000000061641470142567200152120ustar00rootroot00000000000000"""This module defines the Matrix class.""" # Copyright (C) 2021 India Marsden # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Nacime Bouziani, 2021-2022. from ufl.argument import Argument from ufl.core.ufl_type import ufl_type from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace from ufl.utils.counted import Counted # --- The Matrix class represents a matrix, an assembled two form --- @ufl_type() class Matrix(BaseForm, Counted): """An assemble linear operator between two function spaces.""" __slots__ = ( "_count", "_counted_class", "_ufl_function_spaces", "ufl_operands", "_repr", "_hash", "_ufl_shape", "_arguments", "_coefficients", "_domains", ) def __getnewargs__(self): """Get new args.""" return (self._ufl_function_spaces[0], self._ufl_function_spaces[1], self._count) def __init__(self, row_space, column_space, count=None): """Initialise.""" BaseForm.__init__(self) Counted.__init__(self, count, Matrix) if not isinstance(row_space, AbstractFunctionSpace): raise ValueError("Expecting a FunctionSpace as the row space.") if not isinstance(column_space, AbstractFunctionSpace): raise ValueError("Expecting a FunctionSpace as the column space.") self._ufl_function_spaces = (row_space, column_space) self.ufl_operands = () self._domains = None self._hash = None self._repr = ( f"Matrix({self._ufl_function_spaces[0]!r} " f"{self._ufl_function_spaces[1]!r}, {self._count!r})" ) def ufl_function_spaces(self): """Get the tuple of function spaces of this coefficient.""" return self._ufl_function_spaces def _analyze_form_arguments(self): """Define arguments of a matrix when considered as a form.""" self._arguments = ( Argument(self._ufl_function_spaces[0], 0), Argument(self._ufl_function_spaces[1], 1), ) self._coefficients = () def _analyze_domains(self): """Analyze which domains can be found in a Matrix.""" from ufl.domain import join_domains # Collect unique domains self._domains = join_domains([fs.ufl_domain() for fs in self._ufl_function_spaces]) def __str__(self): """Format as a string.""" count = str(self._count) if len(count) == 1: return f"A_{count}" else: return f"A_{{{count}}}" def __repr__(self): """Representation.""" return self._repr def __hash__(self): """Hash.""" if self._hash is None: self._hash = hash(self._repr) return self._hash def equals(self, other): """Check equality.""" if type(other) is not Matrix: return False if self is other: return True return ( self._count == other._count and self._ufl_function_spaces == other._ufl_function_spaces ) ufl-2024.2.0/ufl/measure.py000066400000000000000000000446561470142567200153570ustar00rootroot00000000000000"""The Measure class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg 2008-2016 # Modified by Massimiliano Leoni, 2016. import numbers from itertools import chain from ufl.checks import is_true_ufl_scalar from ufl.constantvalue import as_ufl from ufl.core.expr import Expr from ufl.domain import AbstractDomain, as_domain, extract_domains from ufl.protocols import id_or_none # Export list for ufl.classes __all_classes__ = ["Measure", "MeasureSum", "MeasureProduct"] # TODO: Design a class IntegralType(name, shortname, codim, num_cells, ...)? # TODO: Improve descriptions below: # Enumeration of valid domain types _integral_types = [ # === Integration over full topological dimension: ("cell", "dx"), # Over cells of a mesh # === Integration over topological dimension - 1: ("exterior_facet", "ds"), # Over one-sided exterior facets of a mesh ("interior_facet", "dS"), # Over two-sided facets between pairs of adjacent cells of a mesh # === Integration over topological dimension 0 ("vertex", "dP"), # Over vertices of a mesh # === Integration over custom domains ("custom", "dc"), # Over custom user-defined domains (run-time quadrature points) ("cutcell", "dC"), # Over a cell with some part cut away (run-time quadrature points) ( "interface", "dI", ), # Over a facet fragment overlapping with two or more cells (run-time quadrature points) ( "overlap", "dO", ), # Over a cell fragment overlapping with two or more cells (run-time quadrature points) # === Firedrake specifics: ("exterior_facet_bottom", "ds_b"), # Over bottom facets on extruded mesh ("exterior_facet_top", "ds_t"), # Over top facets on extruded mesh ("exterior_facet_vert", "ds_v"), # Over side facets of an extruded mesh ("interior_facet_horiz", "dS_h"), # Over horizontal facets of an extruded mesh ("interior_facet_vert", "dS_v"), # Over vertical facets of an extruded mesh ] integral_type_to_measure_name = {i: s for i, s in _integral_types} measure_name_to_integral_type = {s: i for i, s in _integral_types} custom_integral_types = ("custom", "cutcell", "interface", "overlap") point_integral_types = ("vertex",) # "point") facet_integral_types = ("exterior_facet", "interior_facet") def register_integral_type(integral_type, measure_name): """Register an integral type.""" global integral_type_to_measure_name, measure_name_to_integral_type if measure_name != integral_type_to_measure_name.get(integral_type, measure_name): raise ValueError("Integral type already added with different measure name!") if integral_type != measure_name_to_integral_type.get(measure_name, integral_type): raise ValueError("Measure name already used for another domain type!") integral_type_to_measure_name[integral_type] = measure_name measure_name_to_integral_type[measure_name] = integral_type def as_integral_type(integral_type): """Map short name to long name and require a valid one.""" integral_type = integral_type.replace(" ", "_") integral_type = measure_name_to_integral_type.get(integral_type, integral_type) if integral_type not in integral_type_to_measure_name: raise ValueError("Invalid integral_type.") return integral_type def integral_types(): """Return a tuple of all domain type strings.""" return tuple(sorted(integral_type_to_measure_name.keys())) def measure_names(): """Return a tuple of all measure name strings.""" return tuple(sorted(measure_name_to_integral_type.keys())) class Measure(object): """Representation of an integration measure. The Measure object holds information about integration properties to be transferred to a Form on multiplication with a scalar expression. """ __slots__ = ("_integral_type", "_domain", "_subdomain_id", "_metadata", "_subdomain_data") def __init__( self, integral_type, # "dx" etc domain=None, subdomain_id="everywhere", metadata=None, subdomain_data=None, ): """Initialise. Args: integral_type: one of "cell", etc, or short form "dx", etc domain: an AbstractDomain object (most often a Mesh) subdomain_id: either string "everywhere", a single subdomain id int, or tuple of ints metadata: dict, with additional compiler-specific parameters affecting how code is generated, including parameters for optimization or debugging of generated code subdomain_data: object representing data to interpret subdomain_id with """ # Map short name to long name and require a valid one self._integral_type = as_integral_type(integral_type) # Check that we either have a proper AbstractDomain or none if domain is not None: domain = as_domain(domain) if not isinstance(domain, AbstractDomain): raise ValueError("Invalid domain.") self._domain = domain # Store subdomain data self._subdomain_data = subdomain_data # FIXME: Cannot require this (yet) because we currently have # no way to implement ufl_id for dolfin SubDomain # if not (self._subdomain_data is None or hasattr(self._subdomain_data, "ufl_id")): # raise ValueError("Invalid domain data, missing ufl_id() implementation.") # Accept "everywhere", single subdomain, or multiple # subdomains if isinstance(subdomain_id, tuple): for did in subdomain_id: if not isinstance(did, numbers.Integral): raise ValueError(f"Invalid subdomain_id {did}.") else: if not (subdomain_id in ("everywhere",) or isinstance(subdomain_id, numbers.Integral)): raise ValueError(f"Invalid subdomain_id {subdomain_id}.") self._subdomain_id = subdomain_id # Validate compiler options are None or dict if metadata is not None and not isinstance(metadata, dict): raise ValueError("Invalid metadata.") self._metadata = metadata or {} def integral_type(self): """Return the domain type. Valid domain types are "cell", "exterior_facet", "interior_facet", etc. """ return self._integral_type def ufl_domain(self): """Return the domain associated with this measure. This may be None or a Domain object. """ return self._domain def subdomain_id(self): """Return the domain id of this measure (integer).""" return self._subdomain_id def metadata(self): """Return the integral metadata. This data is not interpreted by UFL. It is passed to the form compiler which can ignore it or use it to compile each integral of a form in a different way. """ return self._metadata def reconstruct( self, integral_type=None, subdomain_id=None, domain=None, metadata=None, subdomain_data=None ): """Construct a new Measure object with some properties replaced with new values. Example: b = dm.reconstruct(subdomain_id=2) c = dm.reconstruct(metadata={ "quadrature_degree": 3 }) Used by the call operator, so this is equivalent: b = dm(2) c = dm(0, { "quadrature_degree": 3 }) """ if subdomain_id is None: subdomain_id = self.subdomain_id() if domain is None: domain = self.ufl_domain() if metadata is None: metadata = self.metadata() if subdomain_data is None: subdomain_data = self.subdomain_data() return Measure( self.integral_type(), domain=domain, subdomain_id=subdomain_id, metadata=metadata, subdomain_data=subdomain_data, ) def subdomain_data(self): """Return the integral subdomain_data. This data is not interpreted by UFL. Its intension is to give a context in which the domain id is interpreted. """ return self._subdomain_data # Note: Must keep the order of the first two arguments here # (subdomain_id, metadata) for backwards compatibility, because # some tutorials write e.g. dx(0, {...}) to set metadata. def __call__( self, subdomain_id=None, metadata=None, domain=None, subdomain_data=None, degree=None, scheme=None, ): """Reconfigure measure with new domain specification or metadata.""" # Let syntax dx() mean integral over everywhere all_args = (subdomain_id, metadata, domain, subdomain_data, degree, scheme) if all(arg is None for arg in all_args): return self.reconstruct(subdomain_id="everywhere") # Let syntax dx(domain) or dx(domain, metadata) mean integral # over entire domain. To do this we need to hijack the first # argument: if subdomain_id is not None and ( isinstance(subdomain_id, AbstractDomain) or hasattr(subdomain_id, "ufl_domain") ): if domain is not None: raise ValueError( "Ambiguous: setting domain both as keyword argument and first argument." ) subdomain_id, domain = "everywhere", subdomain_id # If degree or scheme is set, inject into metadata. This is a # quick fix to enable the dx(..., degree=3) notation. # TODO: Make degree and scheme properties of integrals instead of adding to metadata. if (degree, scheme) != (None, None): metadata = {} if metadata is None else metadata.copy() if degree is not None: metadata["quadrature_degree"] = degree if scheme is not None: metadata["quadrature_rule"] = scheme # If we get any keywords, use them to reconstruct Measure. # Note that if only one argument is given, it is the # subdomain_id, e.g. dx(3) == dx(subdomain_id=3) return self.reconstruct( subdomain_id=subdomain_id, domain=domain, metadata=metadata, subdomain_data=subdomain_data, ) def __str__(self): """Format as a string.""" name = integral_type_to_measure_name[self._integral_type] args = [] if self._subdomain_id is not None: args.append("subdomain_id=%s" % (self._subdomain_id,)) if self._domain is not None: args.append("domain=%s" % (self._domain,)) if self._metadata: # Stored as {} if None args.append("metadata=%s" % (self._metadata,)) if self._subdomain_data is not None: args.append("subdomain_data=%s" % (self._subdomain_data,)) return "%s(%s)" % (name, ", ".join(args)) def __repr__(self): """Return a repr string for this Measure.""" args = [] args.append(repr(self._integral_type)) if self._subdomain_id is not None: args.append("subdomain_id=%s" % repr(self._subdomain_id)) if self._domain is not None: args.append("domain=%s" % repr(self._domain)) if self._metadata: # Stored as {} if None args.append("metadata=%s" % repr(self._metadata)) if self._subdomain_data is not None: args.append("subdomain_data=%s" % repr(self._subdomain_data)) r = "%s(%s)" % (type(self).__name__, ", ".join(args)) return r def __hash__(self): """Return a hash value for this Measure.""" metadata_hashdata = tuple(sorted((k, id(v)) for k, v in list(self._metadata.items()))) hashdata = ( self._integral_type, self._subdomain_id, hash(self._domain), metadata_hashdata, id_or_none(self._subdomain_data), ) return hash(hashdata) def __eq__(self, other): """Checks if two Measures are equal.""" sorted_metadata = sorted((k, id(v)) for k, v in list(self._metadata.items())) sorted_other_metadata = sorted((k, id(v)) for k, v in list(other._metadata.items())) return ( isinstance(other, Measure) and self._integral_type == other._integral_type and self._subdomain_id == other._subdomain_id and self._domain == other._domain and id_or_none(self._subdomain_data) == id_or_none(other._subdomain_data) and sorted_metadata == sorted_other_metadata ) def __add__(self, other): """Add two measures (self+other). Creates an intermediate object used for the notation expr * (dx(1) + dx(2)) := expr * dx(1) + expr * dx(2) """ if isinstance(other, Measure): # Let dx(1) + dx(2) equal dx((1,2)) return MeasureSum(self, other) else: # Can only add Measures return NotImplemented def __mul__(self, other): """Multiply two measures (self*other). Creates an intermediate object used for the notation expr * (dm1 * dm2) := expr * dm1 * dm2 This is work in progress and not functional. """ if isinstance(other, Measure): # Tensor product measure support return MeasureProduct(self, other) else: # Can't multiply Measure from the right with non-Measure type return NotImplemented def __rmul__(self, integrand): """Multiply a scalar expression with measure to construct a form with a single integral. This is to implement the notation form = integrand * self Integration properties are taken from this Measure object. """ # Avoid circular imports from ufl.form import Form from ufl.integral import Integral # Allow python literals: 1*dx and 1.0*dx if isinstance(integrand, (int, float)): integrand = as_ufl(integrand) # Let other types implement multiplication with Measure if # they want to (to support the dolfin-adjoint TimeMeasure) if not isinstance(integrand, Expr): return NotImplemented # Allow only scalar integrands if not is_true_ufl_scalar(integrand): raise ValueError( "Can only integrate scalar expressions. The integrand is a " f"tensor expression with value shape {integrand.ufl_shape} and " f"free indices with labels {integrand.ufl_free_indices}." ) # If we have a tuple of domain ids build the integrals one by # one and construct as a Form in one go. subdomain_id = self.subdomain_id() if isinstance(subdomain_id, tuple): return Form( list( chain( *( (integrand * self.reconstruct(subdomain_id=d)).integrals() for d in subdomain_id ) ) ) ) # Check that we have an integer subdomain or a string # ("everywhere" or "otherwise", any more?) if not isinstance( subdomain_id, ( str, numbers.Integral, ), ): raise ValueError("Expecting integer or string domain id.") # If we don't have an integration domain, try to find one in # integrand domain = self.ufl_domain() if domain is None: domains = extract_domains(integrand) if len(domains) == 1: (domain,) = domains elif len(domains) == 0: raise ValueError("This integral is missing an integration domain.") else: raise ValueError( "Multiple domains found, making the choice of integration domain ambiguous." ) # Otherwise create and return a one-integral form integral = Integral( integrand=integrand, integral_type=self.integral_type(), domain=domain, subdomain_id=subdomain_id, metadata=self.metadata(), subdomain_data=self.subdomain_data(), ) return Form([integral]) class MeasureSum(object): """Represents a sum of measures. This is a notational intermediate object to translate the notation f*(ds(1)+ds(3)) into f*ds(1) + f*ds(3) """ __slots__ = ("_measures",) def __init__(self, *measures): """Initialise.""" self._measures = measures def __rmul__(self, other): """Multiply.""" integrals = [other * m for m in self._measures] return sum(integrals) def __add__(self, other): """Add.""" if isinstance(other, Measure): return MeasureSum(*(self._measures + (other,))) elif isinstance(other, MeasureSum): return MeasureSum(*(self._measures + other._measures)) return NotImplemented def __str__(self): """Format as a string.""" return "{\n " + "\n + ".join(map(str, self._measures)) + "\n}" class MeasureProduct(object): """Represents a product of measures. This is a notational intermediate object to handle the notation f*(dm1*dm2) This is work in progress and not functional. It needs support in other parts of ufl and the rest of the code generation chain. """ __slots__ = ("_measures",) def __init__(self, *measures): """Create MeasureProduct from given list of measures.""" self._measures = measures if len(self._measures) < 2: raise ValueError("Expecting at least two measures.") def __mul__(self, other): """Flatten multiplication of product measures. This is to ensure that (dm1*dm2)*dm3 is stored as a simple list (dm1,dm2,dm3) in a single MeasureProduct. """ if isinstance(other, Measure): measures = self.sub_measures() + [other] return MeasureProduct(*measures) else: return NotImplemented def __rmul__(self, integrand): """Multiply.""" # TODO: Implement MeasureProduct.__rmul__ to construct integral and form somehow. raise NotImplementedError() def sub_measures(self): """Return submeasures.""" return self._measures ufl-2024.2.0/ufl/objects.py000066400000000000000000000022511470142567200153300ustar00rootroot00000000000000"""Utility objects for pretty syntax in user code.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 # Modified by Kristian Oelgaard, 2009 from ufl.cell import Cell from ufl.core.multiindex import indices from ufl.measure import Measure, integral_type_to_measure_name # Default indices i, j, k, l = indices(4) # noqa: E741 p, q, r, s = indices(4) for integral_type, measure_name in integral_type_to_measure_name.items(): globals()[measure_name] = Measure(integral_type) # TODO: Firedrake hack, remove later ds_tb = ds_b + ds_t # noqa: F821 # Default measure dX including both uncut and cut cells dX = dx + dC # noqa: F821 # Create objects for builtin known cell types vertex = Cell("vertex") interval = Cell("interval") triangle = Cell("triangle") tetrahedron = Cell("tetrahedron") prism = Cell("prism") pyramid = Cell("pyramid") quadrilateral = Cell("quadrilateral") hexahedron = Cell("hexahedron") tesseract = Cell("tesseract") pentatope = Cell("pentatope") # Facet is just a dummy declaration for RestrictedElement facet = "facet" ufl-2024.2.0/ufl/operators.py000066400000000000000000000425021470142567200157200ustar00rootroot00000000000000"""Operators. This module extends the form language with free function operators, which are either already available as member functions on UFL objects or defined as compound operators involving basic operations on the UFL objects. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Kristian B. Oelgaard, 2011 # Modified by Massimiliano Leoni, 2016. import operator import warnings from ufl import sobolevspace from ufl.algebra import Conj, Imag, Real from ufl.averaging import CellAvg, FacetAvg from ufl.checks import is_cellwise_constant from ufl.coefficient import Coefficient from ufl.conditional import ( EQ, NE, AndCondition, Conditional, MaxValue, MinValue, NotCondition, OrCondition, ) from ufl.constantvalue import ComplexValue, RealValue, Zero, as_ufl from ufl.differentiation import Curl, Div, Grad, NablaDiv, NablaGrad, VariableDerivative from ufl.domain import extract_domains from ufl.form import Form from ufl.geometry import FacetNormal, SpatialCoordinate from ufl.indexed import Indexed from ufl.mathfunctions import ( Acos, Asin, Atan, Atan2, BesselI, BesselJ, BesselK, BesselY, Cos, Cosh, Erf, Exp, Ln, Sin, Sinh, Sqrt, Tan, Tanh, ) from ufl.tensoralgebra import ( Cofactor, Cross, Determinant, Deviatoric, Dot, Inner, Inverse, Outer, Perp, Skew, Sym, Trace, Transposed, ) from ufl.tensors import ListTensor, as_matrix, as_tensor, as_vector from ufl.variable import Variable # --- Basic operators --- def rank(f): """The rank of f.""" f = as_ufl(f) return len(f.ufl_shape) def shape(f): """The shape of f.""" f = as_ufl(f) return f.ufl_shape # --- Complex operators --- def conj(f): """The complex conjugate of f.""" f = as_ufl(f) return Conj(f) # Alias because both conj and conjugate are in numpy and we wish to be # consistent. conjugate = conj def real(f): """The real part of f.""" f = as_ufl(f) return Real(f) def imag(f): """The imaginary part of f.""" f = as_ufl(f) return Imag(f) # --- Elementwise tensor operators --- def elem_op_items(op_ind, indices, *args): """Elem op items.""" sh = args[0].ufl_shape indices = tuple(indices) n = sh[len(indices)] def extind(ii): return indices + (ii,) if len(sh) == len(indices) + 1: return [op_ind(extind(i), *args) for i in range(n)] else: return [elem_op_items(op_ind, extind(i), *args) for i in range(n)] def elem_op(op, *args): """Apply element-wise operations. Take the element-wise application of operator op on scalar values from one or more tensor arguments. """ args = [as_ufl(arg) for arg in args] sh = args[0].ufl_shape if not all(sh == x.ufl_shape for x in args): raise ValueError("Cannot take element-wise operation with different shapes.") if sh == (): return op(*args) def op_ind(ind, *args): return op(*[x[ind] for x in args]) return as_tensor(elem_op_items(op_ind, (), *args)) def elem_mult(A, B): """Take the elementwise multiplication of tensors A and B with the same shape.""" return elem_op(operator.mul, A, B) def elem_div(A, B): """Take the elementwise division of tensors A and B with the same shape.""" return elem_op(operator.truediv, A, B) def elem_pow(A, B): """Take the elementwise power of tensors A and B with the same shape.""" return elem_op(operator.pow, A, B) # --- Tensor operators --- def transpose(A): """Take the transposed of tensor A.""" A = as_ufl(A) if A.ufl_shape == (): return A return Transposed(A) def outer(*operands): """Take the outer product of two or more operands. The complex conjugate of the first argument is taken. """ n = len(operands) if n == 1: return operands[0] elif n == 2: a, b = operands else: a = outer(*operands[:-1]) b = operands[-1] a = as_ufl(a) b = as_ufl(b) if a.ufl_shape == () and b.ufl_shape == (): return Conj(a) * b return Outer(a, b) def inner(a, b): """Take the inner product of a and b. The complex conjugate of the second argument is taken. """ a = as_ufl(a) b = as_ufl(b) if a.ufl_shape == () and b.ufl_shape == (): return a * Conj(b) return Inner(a, b) def dot(a, b): """Take the dot product of a and b. This won't take the complex conjugate of the second argument. """ a = as_ufl(a) b = as_ufl(b) if a.ufl_shape == () and b.ufl_shape == (): return a * b return Dot(a, b) def perp(v): """Take the perp of v. I.e. :math:`(-v_1, +v_0)`. """ v = as_ufl(v) if v.ufl_shape != (2,): raise ValueError("Expecting a 2D vector expression.") return Perp(v) def cross(a, b): """Take the cross product of a and b.""" a = as_ufl(a) b = as_ufl(b) return Cross(a, b) def det(A): """Take the determinant of A.""" A = as_ufl(A) if A.ufl_shape == (): return A return Determinant(A) def inv(A): """Take the inverse of A.""" A = as_ufl(A) if A.ufl_shape == (): return 1 / A return Inverse(A) def cofac(A): """Take the cofactor of A.""" A = as_ufl(A) return Cofactor(A) def tr(A): """Take the trace of A.""" A = as_ufl(A) return Trace(A) def diag(A): """Diagonal ranl-2 tensor. Take the diagonal part of rank 2 tensor A or make a diagonal rank 2 tensor from a rank 1 tensor. Always returns a rank 2 tensor. See also diag_vector. """ # TODO: Make a compound type or two for this operator # Get and check dimensions r = len(A.ufl_shape) if r == 1: (n,) = A.ufl_shape elif r == 2: m, n = A.ufl_shape if m != n: raise ValueError("Can only take diagonal of square tensors.") else: raise ValueError("Expecting rank 1 or 2 tensor.") # Build matrix row by row rows = [] for i in range(n): row = [0] * n row[i] = A[i] if r == 1 else A[i, i] rows.append(row) return as_matrix(rows) def diag_vector(A): """Take the diagonal part of rank 2 tensor A and return as a vector. See also diag. """ # TODO: Make a compound type for this operator # Get and check dimensions if len(A.ufl_shape) != 2: raise ValueError("Expecting rank 2 tensor.") m, n = A.ufl_shape if m != n: raise ValueError("Can only take diagonal of square tensors.") # Return diagonal vector return as_vector([A[i, i] for i in range(n)]) def dev(A): """Take the deviatoric part of A.""" A = as_ufl(A) return Deviatoric(A) def skew(A): """Take the skew symmetric part of A.""" A = as_ufl(A) return Skew(A) def sym(A): """Take the symmetric part of A.""" A = as_ufl(A) return Sym(A) # --- Differential operators def Dx(f, *i): """Take the partial derivative of f with respect to spatial variable number i. Equivalent to f.dx(*i). """ f = as_ufl(f) return f.dx(*i) def Dn(f): """Take the directional derivative of f in the facet normal direction. The facet normal is Dn(f) := dot(grad(f), n). """ f = as_ufl(f) if is_cellwise_constant(f): return Zero(f.ufl_shape, f.ufl_free_indices, f.ufl_index_dimensions) return dot(grad(f), FacetNormal(f.ufl_domain())) def diff(f, v): """Take the derivative of f with respect to the variable v. If f is a form, diff is applied to each integrand. """ # Apply to integrands if isinstance(f, Form): from ufl.algorithms.map_integrands import map_integrands return map_integrands(lambda e: diff(e, v), f) # Apply to expression f = as_ufl(f) if isinstance(v, SpatialCoordinate): return grad(f) elif isinstance(v, (Variable, Coefficient)): return VariableDerivative(f, v) else: raise ValueError("Expecting a Variable or SpatialCoordinate in diff.") def grad(f): """Take the gradient of f. This operator follows the grad convention where grad(s)[i] = s.dx(i) grad(v)[i,j] = v[i].dx(j) grad(T)[:,i] = T[:].dx(i) for scalar expressions s, vector expressions v, and arbitrary rank tensor expressions T. See also: :py:func:`nabla_grad` """ f = as_ufl(f) return Grad(f) def div(f): """Take the divergence of f. This operator follows the div convention where div(v) = v[i].dx(i) div(T)[:] = T[:,i].dx(i) for vector expressions v, and arbitrary rank tensor expressions T. See also: :py:func:`nabla_div` """ f = as_ufl(f) return Div(f) def nabla_grad(f): """Take the gradient of f. This operator follows the grad convention where nabla_grad(s)[i] = s.dx(i) nabla_grad(v)[i,j] = v[j].dx(i) nabla_grad(T)[i,:] = T[:].dx(i) for scalar expressions s, vector expressions v, and arbitrary rank tensor expressions T. See also: :py:func:`grad` """ f = as_ufl(f) return NablaGrad(f) def nabla_div(f): """Take the divergence of f. This operator follows the div convention where nabla_div(v) = v[i].dx(i) nabla_div(T)[:] = T[i,:].dx(i) for vector expressions v, and arbitrary rank tensor expressions T. See also: :py:func:`div` """ f = as_ufl(f) return NablaDiv(f) def curl(f): """Take the curl of f.""" f = as_ufl(f) return Curl(f) rot = curl # --- DG operators --- def jump(v, n=None): """Take the jump of v across a facet.""" v = as_ufl(v) is_constant = len(extract_domains(v)) > 0 if is_constant: if n is None: return v("+") - v("-") r = len(v.ufl_shape) if r == 0: return v("+") * n("+") + v("-") * n("-") else: return dot(v("+"), n("+")) + dot(v("-"), n("-")) else: warnings.warn( "Returning zero from jump of expression without a domain. " "This may be erroneous if a dolfin.Expression is involved." ) # FIXME: Is this right? If v has no domain, it doesn't depend # on anything spatially variable or any form arguments, and # thus the jump is zero. In other words, I'm assuming that "v # has no geometric domains" is equivalent with "v is a spatial # constant". Update: This is NOT true for # jump(Expression("x[0]")) from dolfin. return Zero(v.ufl_shape, v.ufl_free_indices, v.ufl_index_dimensions) def avg(v): """Take the average of v across a facet.""" v = as_ufl(v) return 0.5 * (v("+") + v("-")) def cell_avg(f): """Take the average of v over a cell.""" return CellAvg(f) def facet_avg(f): """Take the average of v over a facet.""" return FacetAvg(f) # --- Other operators --- def variable(e): """Define a variable representing the given expression. See also diff(). """ e = as_ufl(e) return Variable(e) # --- Conditional expressions --- def conditional(condition, true_value, false_value): """A conditional expression. This takes the value of true_value when condition evaluates to true and false_value otherwise. """ return Conditional(condition, true_value, false_value) def eq(left, right): """A boolean expression (left == right) for use with conditional.""" return EQ(left, right) def ne(left, right): """A boolean expression (left != right) for use with conditional.""" return NE(left, right) def le(left, right): """A boolean expression (left <= right) for use with conditional.""" return as_ufl(left) <= as_ufl(right) def ge(left, right): """A boolean expression (left >= right) for use with conditional.""" return as_ufl(left) >= as_ufl(right) def lt(left, right): """A boolean expression (left < right) for use with conditional.""" return as_ufl(left) < as_ufl(right) def gt(left, right): """A boolean expression (left > right) for use with conditional.""" return as_ufl(left) > as_ufl(right) def And(left, right): """A boolean expression (left and right) for use with conditional.""" return AndCondition(left, right) def Or(left, right): """A boolean expression (left or right) for use with conditional.""" return OrCondition(left, right) def Not(condition): """A boolean expression (not condition) for use with conditional.""" return NotCondition(condition) def sign(x): """Return the sign of x. This returns +1 if x is positive, -1 if x is negative, and 0 if x is 0. """ # TODO: Add a Sign type for this? return conditional(eq(x, 0), 0, conditional(lt(x, 0), -1, +1)) def max_value(x, y): """Take the maximum of x and y.""" x = as_ufl(x) y = as_ufl(y) return MaxValue(x, y) def min_value(x, y): """Take the minimum of x and y.""" x = as_ufl(x) y = as_ufl(y) return MinValue(x, y) # --- Math functions --- def _mathfunction(f, cls): """A mat function.""" f = as_ufl(f) r = cls(f) if isinstance(r, (RealValue, Zero, int, float)): return float(r) if isinstance(r, (ComplexValue, complex)): return complex(r) return r def sqrt(f): """Take the square root of f.""" return _mathfunction(f, Sqrt) def exp(f): """Take the exponential of f.""" return _mathfunction(f, Exp) def ln(f): """Take the natural logarithm of f.""" return _mathfunction(f, Ln) def cos(f): """Take the cosine of f.""" return _mathfunction(f, Cos) def sin(f): """Take the sine of f.""" return _mathfunction(f, Sin) def tan(f): """Take the tangent of f.""" return _mathfunction(f, Tan) def cosh(f): """Take the hyperbolic cosine of f.""" return _mathfunction(f, Cosh) def sinh(f): """Take the hyperbolic sine of f.""" return _mathfunction(f, Sinh) def tanh(f): """Take the hyperbolic tangent of f.""" return _mathfunction(f, Tanh) def acos(f): """Take the inverse cosine of f.""" return _mathfunction(f, Acos) def asin(f): """Take the inverse sine of f.""" return _mathfunction(f, Asin) def atan(f): """Take the inverse tangent of f.""" return _mathfunction(f, Atan) def atan2(f1, f2): """Take the inverse tangent with two the arguments f1 and f2.""" f1 = as_ufl(f1) f2 = as_ufl(f2) if isinstance(f1, (ComplexValue, complex)) or isinstance(f2, (ComplexValue, complex)): raise TypeError("atan2 is incompatible with complex numbers.") r = Atan2(f1, f2) if isinstance(r, (RealValue, Zero, int, float)): return float(r) if isinstance(r, (ComplexValue, complex)): return complex(r) return r def erf(f): """Take the error function of f.""" return _mathfunction(f, Erf) def bessel_J(nu, f): """Cylindrical Bessel function of the first kind.""" nu = as_ufl(nu) f = as_ufl(f) return BesselJ(nu, f) def bessel_Y(nu, f): """Cylindrical Bessel function of the second kind.""" nu = as_ufl(nu) f = as_ufl(f) return BesselY(nu, f) def bessel_I(nu, f): """Regular modified cylindrical Bessel function.""" nu = as_ufl(nu) f = as_ufl(f) return BesselI(nu, f) def bessel_K(nu, f): """Irregular modified cylindrical Bessel function.""" nu = as_ufl(nu) f = as_ufl(f) return BesselK(nu, f) # --- Special function for exterior_derivative def exterior_derivative(f): """Take the exterior derivative of f. The exterior derivative uses the element Sobolev space to determine whether id, grad, curl or div should be used. Note that this uses the grad and div operators, as opposed to nabla_grad and nabla_div. """ # Extract the element from the input f if isinstance(f, Indexed): expression, indices = f.ufl_operands if len(indices) > 1: raise NotImplementedError index = int(indices[0]) element = expression.ufl_element() while index != 0: for e in element.sub_elements: if e.value_size > index: element = e break index -= e.value_size elif isinstance(f, ListTensor): f0 = f.ufl_operands[0] f0expr, f0indices = f0.ufl_operands # FIXME: Assumption on type of f0!!! if len(f0indices) > 1: raise NotImplementedError index = int(f0indices[0]) element = f0expr.ufl_element() while index != 0: for e in element.sub_elements: if e.value_size > index: element = e break index -= e.value_size else: try: element = f.ufl_element() except Exception: raise ValueError(f"Unable to determine element from {f}") domain = f.ufl_domain() gdim = domain.geometric_dimension() space = element.sobolev_space if space == sobolevspace.L2: return f elif space == sobolevspace.H1: if gdim == 1: return grad(f)[0] # Special-case 1D vectors as scalars return grad(f) elif space == sobolevspace.HCurl: return curl(f) elif space == sobolevspace.HDiv: return div(f) else: raise ValueError(f"Unable to determine exterior_derivative for element '{element!r}'") ufl-2024.2.0/ufl/permutation.py000066400000000000000000000030211470142567200162420ustar00rootroot00000000000000"""This module provides utility functions for computing permutations and generating index lists.""" # Copyright (C) 2008-2016 Anders Logg and Kent-Andre Mardal # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Martin Alnæs 2009-2016 def compute_indices(shape): """Compute all index combinations for given shape.""" if len(shape) == 0: return ((),) sub_indices = compute_indices(shape[1:]) indices = [] for i in range(shape[0]): for sub_index in sub_indices: indices.append((i,) + sub_index) return tuple(indices) def build_component_numbering(shape, symmetry): """Build a numbering of components within the given value shape. This takes into consideration a symmetry mapping which leaves the mapping noncontiguous. Returns a dict { component -> numbering } and an ordered list of components [ numbering -> component ]. The dict contains all components while the list only contains the ones not mapped by the symmetry mapping. """ vi2si, si2vi = {}, [] indices = compute_indices(shape) # Number components not in symmetry mapping for c in indices: if c not in symmetry: vi2si[c] = len(si2vi) si2vi.append(c) # Copy numbering to mapped components for c in indices: if c in symmetry: vi2si[c] = vi2si[symmetry[c]] # Validate for k, c in enumerate(si2vi): assert vi2si[c] == k return vi2si, si2vi ufl-2024.2.0/ufl/precedence.py000066400000000000000000000067001470142567200157770ustar00rootroot00000000000000"""Precedence handling.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import warnings # FIXME: This code is crap... def parstr(child, parent, pre="(", post=")", format=str): """Parstr.""" # Execute when needed instead of on import, which leads to all # kinds of circular trouble. Fixing this could be an optimization # of str(expr) though. if not hasattr(parent, "_precedence"): assign_precedences(build_precedence_list()) # We want child to be evaluated fully first, and if the parent has # higher precedence we later wrap in (). s = format(child) # Operators where operands are always parenthesized because # precedence is not defined below if parent._precedence == 0: return pre + s + post # If parent operator binds stronger than child, must parenthesize # child # FIXME: Is this correct for all possible positions of () in a + b + c? # FIXME: Left-right rule if parent._precedence > child._precedence: # parent = indexed, child = terminal return pre + s + post # Nothing needed return s def build_precedence_list(): """Build precedence list.""" from ufl.classes import ( Abs, BesselFunction, Division, Indexed, IndexSum, MathFunction, Operator, Power, Product, Sum, Terminal, ) # TODO: Fill in other types... # Power <= Transposed precedence_list = [] # Default operator behaviour: should always add parentheses precedence_list.append((Operator,)) precedence_list.append((Sum,)) # sum_i a + b = (sum_i a) + b != sum_i (a + b), sum_i binds # stronger than +, but weaker than product precedence_list.append((IndexSum,)) precedence_list.append( ( Product, Division, ) ) # NB! Depends on language! precedence_list.append((Power, MathFunction, BesselFunction, Abs)) precedence_list.append((Indexed,)) # Default terminal behaviour: should never add parentheses precedence_list.append((Terminal,)) return precedence_list def build_precedence_mapping(precedence_list): """Given a precedence list, build a dict with class->int mappings. Utility function used by some external code. """ from ufl.classes import Expr, abstract_classes, all_ufl_classes pm = {} missing = set() # Assign integer values for each precedence level k = 0 for p in precedence_list: for c in p: pm[c] = k k += 1 # Check for missing classes, fill in subclasses for c in all_ufl_classes: if c not in abstract_classes and c not in pm: b = c.__bases__[0] while b is not Expr: if b in pm: pm[c] = pm[b] break b = b.__bases__[0] if c not in pm: missing.add(c) return pm, missing def assign_precedences(precedence_list): """Given a precedence list, assign ints to class._precedence.""" pm, missing = build_precedence_mapping(precedence_list) for c, p in sorted(pm.items(), key=lambda x: x[0].__name__): c._precedence = p if missing: warnings.warn( "Missing precedence levels for classes:\n" + "\n".join(f" {c}" for c in sorted(missing)) ) ufl-2024.2.0/ufl/protocols.py000066400000000000000000000011541470142567200157240ustar00rootroot00000000000000"""Protocols.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later def id_or_none(obj): """Returns None if the object is None, obj.ufl_id() if available, or id(obj) if not. This allows external libraries to implement an alternative to id(obj) in the ufl_id() function, such that ufl can identify objects as the same without knowing about their types. """ if obj is None: return None elif hasattr(obj, "ufl_id"): return obj.ufl_id() else: return id(obj) ufl-2024.2.0/ufl/pullback.py000066400000000000000000000454711470142567200155070ustar00rootroot00000000000000"""Pull back and push forward maps.""" # Copyright (C) 2023 Matthew Scroggs, David Ham, Garth Wells # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from __future__ import annotations import typing from abc import ABC, abstractmethod, abstractproperty from typing import TYPE_CHECKING import numpy as np from ufl.core.expr import Expr from ufl.core.multiindex import indices from ufl.domain import extract_unique_domain from ufl.functionspace import FunctionSpace from ufl.tensors import as_tensor if TYPE_CHECKING: from ufl.finiteelement import AbstractFiniteElement as _AbstractFiniteElement __all_classes__ = [ "NonStandardPullbackException", "AbstractPullback", "IdentityPullback", "ContravariantPiola", "CovariantPiola", "L2Piola", "DoubleContravariantPiola", "DoubleCovariantPiola", "MixedPullback", "SymmetricPullback", "PhysicalPullback", "CustomPullback", "UndefinedPullback", ] class NonStandardPullbackException(BaseException): """Exception to raise if a map is non-standard.""" pass class AbstractPullback(ABC): """An abstract pull back.""" @abstractmethod def __repr__(self) -> str: """Return a representation of the object.""" @abstractmethod def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ @abstractproperty def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" def apply(self, expr: Expr) -> Expr: """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ raise NonStandardPullbackException() class IdentityPullback(AbstractPullback): """The identity pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "IdentityPullback()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return True def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ return expr def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ return element.reference_value_shape class ContravariantPiola(AbstractPullback): """The contravariant Piola pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "ContravariantPiola()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return False def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ from ufl.classes import Jacobian, JacobianDeterminant domain = extract_unique_domain(expr) J = Jacobian(domain) detJ = JacobianDeterminant(J) transform = (1.0 / detJ) * J # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) *k, i, j = indices(len(expr.ufl_shape) + 1) kj = (*k, j) return as_tensor(transform[i, j] * expr[kj], (*k, i)) def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ gdim = domain.geometric_dimension() return (gdim,) + element.reference_value_shape[1:] class CovariantPiola(AbstractPullback): """The covariant Piola pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "CovariantPiola()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return False def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ from ufl.classes import JacobianInverse domain = extract_unique_domain(expr) K = JacobianInverse(domain) # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) *k, i, j = indices(len(expr.ufl_shape) + 1) kj = (*k, j) return as_tensor(K[j, i] * expr[kj], (*k, i)) def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ gdim = domain.geometric_dimension() return (gdim,) + element.reference_value_shape[1:] class L2Piola(AbstractPullback): """The L2 Piola pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "L2Piola()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return False def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ from ufl.classes import JacobianDeterminant domain = extract_unique_domain(expr) detJ = JacobianDeterminant(domain) return expr / detJ def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ return element.reference_value_shape class DoubleContravariantPiola(AbstractPullback): """The double contravariant Piola pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "DoubleContravariantPiola()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return False def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ from ufl.classes import Jacobian, JacobianDeterminant domain = extract_unique_domain(expr) J = Jacobian(domain) detJ = JacobianDeterminant(J) # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) *k, i, j, m, n = indices(len(expr.ufl_shape) + 2) kmn = (*k, m, n) return as_tensor((1.0 / detJ) ** 2 * J[i, m] * expr[kmn] * J[j, n], (*k, i, j)) def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ gdim = domain.geometric_dimension() return (gdim, gdim) class DoubleCovariantPiola(AbstractPullback): """The double covariant Piola pull back.""" def __repr__(self) -> str: """Return a representation of the object.""" return "DoubleCovariantPiola()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return False def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ from ufl.classes import JacobianInverse domain = extract_unique_domain(expr) K = JacobianInverse(domain) # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) *k, i, j, m, n = indices(len(expr.ufl_shape) + 2) kmn = (*k, m, n) return as_tensor(K[m, i] * expr[kmn] * K[n, j], (*k, i, j)) def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ gdim = domain.geometric_dimension() return (gdim, gdim) class MixedPullback(AbstractPullback): """Pull back for a mixed element.""" def __init__(self, element: _AbstractFiniteElement): """Initalise. Args: element: The mixed element """ self._element = element def __repr__(self) -> str: """Return a representation of the object.""" return f"MixedPullback({self._element!r})" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return all(e.pullback.is_identity for e in self._element.sub_elements) def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ domain = extract_unique_domain(expr) space = FunctionSpace(domain, self._element) rflat = [expr[idx] for idx in np.ndindex(expr.ufl_shape)] g_components = [] offset = 0 # For each unique piece in reference space, apply the appropriate pullback for subelem in self._element.sub_elements: rsub = as_tensor( np.asarray(rflat[offset : offset + subelem.reference_value_size]).reshape( subelem.reference_value_shape ) ) rmapped = subelem.pullback.apply(rsub) # Flatten into the pulled back expression for the whole thing g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)]) offset += subelem.reference_value_size # And reshape appropriately f = as_tensor(np.asarray(g_components).reshape(space.value_shape)) if f.ufl_shape != space.value_shape: raise ValueError( "Expecting pulled back expression with shape " f"'{space.value_shape}', got '{f.ufl_shape}'" ) return f def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ assert element == self._element dim = sum(FunctionSpace(domain, e).value_size for e in self._element.sub_elements) return (dim,) class SymmetricPullback(AbstractPullback): """Pull back for an element with symmetry.""" def __init__( self, element: _AbstractFiniteElement, symmetry: typing.Dict[typing.tuple[int, ...], int] ): """Initalise. Args: element: The element symmetry: A dictionary mapping from the component in physical space to the local component """ self._element = element self._symmetry = symmetry self._sub_element_value_shape = element.sub_elements[0].reference_value_shape for e in element.sub_elements: if e.reference_value_shape != self._sub_element_value_shape: raise ValueError("Sub-elements must all have the same value shape.") self._block_shape = tuple(i + 1 for i in max(symmetry.keys())) def __repr__(self) -> str: """Return a representation of the object.""" return f"SymmetricPullback({self._element!r}, {self._symmetry!r})" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return all(e.pullback.is_identity for e in self._element.sub_elements) def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ domain = extract_unique_domain(expr) space = FunctionSpace(domain, self._element) rflat = [expr[idx] for idx in np.ndindex(expr.ufl_shape)] g_components = [] offsets = [0] for subelem in self._element.sub_elements: offsets.append(offsets[-1] + subelem.reference_value_size) # For each unique piece in reference space, apply the appropriate pullback for component in np.ndindex(self._block_shape): i = self._symmetry[component] subelem = self._element.sub_elements[i] rsub = as_tensor( np.asarray(rflat[offsets[i] : offsets[i + 1]]).reshape( subelem.reference_value_shape ) ) rmapped = subelem.pullback.apply(rsub) # Flatten into the pulled back expression for the whole thing g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)]) # And reshape appropriately f = as_tensor(np.asarray(g_components).reshape(space.value_shape)) if f.ufl_shape != space.value_shape: raise ValueError( f"Expecting pulled back expression with shape " f"'{space.value_shape}', got '{f.ufl_shape}'" ) return f def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ assert element == self._element return tuple(i + 1 for i in max(self._symmetry.keys())) class PhysicalPullback(AbstractPullback): """Physical pull back. This should probably be removed. """ def __repr__(self) -> str: """Return a representation of the object.""" return "PhysicalPullback()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return True def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ return expr def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ raise NotImplementedError() class CustomPullback(AbstractPullback): """Custom pull back. This should probably be removed. """ def __repr__(self) -> str: """Return a representation of the object.""" return "CustomPullback()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return True def apply(self, expr): """Apply the pull back. Args: expr: A function on a physical cell Returns: The function pulled back to the reference cell """ return expr def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ if element.reference_value_shape == (): return () raise NotImplementedError() class UndefinedPullback(AbstractPullback): """Undefined pull back. This should probably be removed. """ def __repr__(self) -> str: """Return a representation of the object.""" return "UndefinedPullback()" @property def is_identity(self) -> bool: """Is this pull back the identity (or the identity applied to mutliple components).""" return True def physical_value_shape(self, element, domain) -> typing.Tuple[int, ...]: """Get the physical value shape when this pull back is applied to an element on a domain. Args: element: The element that the pull back is applied to domain: The domain Returns: The value shape when the pull back is applied to the given element """ raise NotImplementedError() identity_pullback = IdentityPullback() covariant_piola = CovariantPiola() contravariant_piola = ContravariantPiola() l2_piola = L2Piola() double_covariant_piola = DoubleCovariantPiola() double_contravariant_piola = DoubleContravariantPiola() physical_pullback = PhysicalPullback() custom_pullback = CustomPullback() undefined_pullback = UndefinedPullback() ufl-2024.2.0/ufl/referencevalue.py000066400000000000000000000023131470142567200166710ustar00rootroot00000000000000"""Representation of the reference value of a function.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.operator import Operator from ufl.core.terminal import FormArgument from ufl.core.ufl_type import ufl_type @ufl_type(num_ops=1, is_index_free=True, is_terminal_modifier=True, is_in_reference_frame=True) class ReferenceValue(Operator): """Representation of the reference cell value of a form argument.""" __slots__ = () def __init__(self, f): """Initialise.""" if not isinstance(f, FormArgument): raise ValueError("Can only take reference value of form arguments.") Operator.__init__(self, (f,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_element().reference_value_shape def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get child from mapping and return the component asked for.""" raise NotImplementedError() def __str__(self): """Format as a string.""" return f"reference_value({self.ufl_operands[0]})" ufl-2024.2.0/ufl/restriction.py000066400000000000000000000025441470142567200162510ustar00rootroot00000000000000"""Restriction operations.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.precedence import parstr # --- Restriction operators --- @ufl_type( is_abstract=True, num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0, is_restriction=True, ) class Restricted(Operator): """Restriction.""" __slots__ = () # TODO: Add __new__ operator here, e.g. restricted(literal) == literal def __init__(self, f): """Initialise.""" Operator.__init__(self, (f,)) def side(self): """Get the side.""" return self._side def evaluate(self, x, mapping, component, index_values): """Evaluate.""" return self.ufl_operands[0].evaluate(x, mapping, component, index_values) def __str__(self): """Format as a string.""" return f"{parstr(self.ufl_operands[0], self)}({self._side})" @ufl_type(is_terminal_modifier=True) class PositiveRestricted(Restricted): """Positive restriction.""" __slots__ = () _side = "+" @ufl_type(is_terminal_modifier=True) class NegativeRestricted(Restricted): """Negative restriction.""" __slots__ = () _side = "-" ufl-2024.2.0/ufl/sobolevspace.py000066400000000000000000000137411470142567200163720ustar00rootroot00000000000000"""Sobolev spaces. This module defines a symbolic heirarchy of Sobolev spaces to enable symbolic reasoning about the spaces in which finite elements lie. """ # Copyright (C) 2014 Imperial College London and others # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Written by David Ham 2014 # # Modified by Martin Alnaes 2014 # Modified by Lizao Li 2015 # Modified by Thomas Gibson 2017 from functools import total_ordering from math import inf, isinf __all_classes__ = ["SobolevSpace", "DirectionalSobolevSpace"] @total_ordering class SobolevSpace(object): """Symbolic representation of a Sobolev space. This implements a subset of the methods of a Python set so that finite elements and other Sobolev spaces can be tested for inclusion. """ def __init__(self, name, parents=None): """Instantiate a SobolevSpace object. Args: name: The name of this space, parents: A set of Sobolev spaces of which this space is a subspace. """ self.name = name p = frozenset(parents or []) # Ensure that the inclusion operations are transitive. self.parents = p.union(*[p_.parents for p_ in p]) self._order = { "L2": 0, "H1": 1, "H2": 2, "HInf": inf, # Order for the elements below is taken from # its parent Sobolev space "HDiv": 0, "HCurl": 0, "HEin": 0, "HDivDiv": 0, "DirectionalH": 0, }[self.name] def __str__(self): """Format as a string.""" return self.name def __repr__(self): """Representation.""" return f"SobolevSpace({self.name!r}, {list(self.parents)!r})" def __eq__(self, other): """Check equality.""" return isinstance(other, SobolevSpace) and self.name == other.name def __ne__(self, other): """Not equal.""" return not self == other def __hash__(self): """Hash.""" return hash(("SobolevSpace", self.name)) def __getitem__(self, spatial_index): """Returns the Sobolev space associated with a particular spatial coordinate.""" return self def __contains__(self, other): """Implement `fe in s` where `fe` is a FiniteElement and `s` is a SobolevSpace.""" if isinstance(other, SobolevSpace): raise TypeError( "Unable to test for inclusion of a SobolevSpace in another SobolevSpace. " "Did you mean to use <= instead?" ) return other.sobolev_space == self or self in other.sobolev_space.parents def __lt__(self, other): """In common with intrinsic Python sets, < indicates "is a proper subset of".""" return other in self.parents @total_ordering class DirectionalSobolevSpace(SobolevSpace): """Directional Sobolev space. Symbolic representation of a Sobolev space with varying smoothness in different spatial directions. """ def __init__(self, orders): """Instantiate a DirectionalSobolevSpace object. Args: orders: an iterable of orders of weak derivatives, where the position denotes in what spatial variable the smoothness requirement is enforced. """ assert all( isinstance(x, int) or isinf(x) for x in orders ), "Order must be an integer or infinity." name = "DirectionalH" parents = [L2] super(DirectionalSobolevSpace, self).__init__(name, parents) self._orders = tuple(orders) self._spatial_indices = range(len(self._orders)) def __getitem__(self, spatial_index): """Returns the Sobolev space associated with a particular spatial coordinate.""" if spatial_index not in range(len(self._orders)): raise IndexError("Spatial index out of range.") spaces = {0: L2, 1: H1, 2: H2, inf: HInf} return spaces[self._orders[spatial_index]] def __contains__(self, other): """Check if one space is contained in another. Implement `fe in s` where `fe` is a FiniteElement and `s` is a DirectionalSobolevSpace. """ if isinstance(other, SobolevSpace): raise TypeError( "Unable to test for inclusion of a SobolevSpace in another SobolevSpace. " "Did you mean to use <= instead?" ) return other.sobolev_space == self or all( self[i] in other.sobolev_space.parents for i in self._spatial_indices ) def __eq__(self, other): """Check equality.""" if isinstance(other, DirectionalSobolevSpace): return self._orders == other._orders return all(self[i] == other for i in self._spatial_indices) def __lt__(self, other): """In common with intrinsic Python sets, < indicates "is a proper subset of.""" if isinstance(other, DirectionalSobolevSpace): if self._spatial_indices != other._spatial_indices: return False return any(self._orders[i] > other._orders[i] for i in self._spatial_indices) if other in [HDiv, HCurl]: return all(self._orders[i] >= 1 for i in self._spatial_indices) elif other.name in ["HDivDiv", "HEin"]: # Don't know how these spaces compare return NotImplementedError(f"Don't know how to compare with {other.name}") else: return any(self._orders[i] > other._order for i in self._spatial_indices) def __str__(self): """Format as a string.""" return f"{self.name}({', '.join(map(str, self._orders))})" L2 = SobolevSpace("L2") HDiv = SobolevSpace("HDiv", [L2]) HCurl = SobolevSpace("HCurl", [L2]) H1 = SobolevSpace("H1", [HDiv, HCurl, L2]) H2 = SobolevSpace("H2", [H1, HDiv, HCurl, L2]) HInf = SobolevSpace("HInf", [H2, H1, HDiv, HCurl, L2]) HEin = SobolevSpace("HEin", [L2]) HDivDiv = SobolevSpace("HDivDiv", [L2]) ufl-2024.2.0/ufl/sorting.py000066400000000000000000000127101470142567200153650ustar00rootroot00000000000000"""Sorting. This module contains a sorting rule for expr objects that is more robust w.r.t. argument numbering than using repr. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2009-2010. # Modified by Johan Hake, 2010. from functools import cmp_to_key from ufl.argument import Argument from ufl.coefficient import Coefficient from ufl.core.multiindex import FixedIndex, MultiIndex from ufl.variable import Label def _cmp_multi_index(a, b): """Cmp multi index.""" # Careful not to depend on Index.count() here! # This is placed first because it is most frequent. # Make decision based on the first index pair possible for i, j in zip(a._indices, b._indices): fix1 = isinstance(i, FixedIndex) fix2 = isinstance(j, FixedIndex) if fix1 and fix2: # Both are FixedIndex, sort by value x, y = i._value, j._value if x < y: return -1 elif x > y: return 1 else: # Same value, no decision continue elif fix1: # Sort fixed before free return -1 elif fix2: # Sort fixed before free return 1 else: # Both are Index, no decision, do not depend on count! pass # Failed to make a decision, return 0 by default # (this does not mean equality, it could be e.g. # [i,0] vs [j,0] because the counts of i,j cannot be used) return 0 def _cmp_label(a, b): """Cmp label.""" # Don't compare counts! Causes circular problems when renumbering to get a canonical form. # Therefore, even though a and b are not equal in general (__eq__ won't be True), # but for this sorting they are considered equal and we return 0. return 0 def _cmp_coefficient(a, b): """Cmp coefficient.""" # It's ok to compare relative counts for Coefficients, # since their ordering is a property of the form x, y = a._count, b._count if x < y: return -1 elif x > y: return 1 else: return 0 def _cmp_argument(a, b): """Cmp argument.""" # It's ok to compare relative number and part for Arguments, # since their ordering is a property of the form x = (a._number, a._part) y = (b._number, b._part) if x < y: return -1 elif x > y: return 1 else: return 0 def _cmp_terminal_by_repr(a, b): """Cmp terminal by repr.""" # The cost of repr on a terminal is fairly small, and bounded x = repr(a) y = repr(b) return -1 if x < y else (0 if x == y else 1) # Hack up a MultiFunction-like type dispatch for terminal comparisons _terminal_cmps = {} _terminal_cmps[MultiIndex._ufl_typecode_] = _cmp_multi_index _terminal_cmps[Argument._ufl_typecode_] = _cmp_argument _terminal_cmps[Coefficient._ufl_typecode_] = _cmp_coefficient _terminal_cmps[Label._ufl_typecode_] = _cmp_label def cmp_expr(a, b): """Replacement for cmp(a, b), removed in Python 3, for Expr objects.""" # Modelled after pre_traversal to avoid recursion: left = [(a, b)] while left: a, b = left.pop() # First sort quickly by type code x, y = a._ufl_typecode_, b._ufl_typecode_ if x != y: return -1 if x < y else 1 # Now we know that the type is the same, check further based # on type specific properties. if a._ufl_is_terminal_: if x in _terminal_cmps: c = _terminal_cmps[x](a, b) else: c = _cmp_terminal_by_repr(a, b) if c: return c else: # If the hash is the same, assume equal for the purpose of # sorting. This introduces a minor chance of # nondeterministic behaviour, just as with MultiIndex. # Although collected statistics for complicated forms # suggest that the hash function is pretty good so there # shouldn't be collisions. # if hash(a) == hash(b): # FIXME: Test this for performance improvement. # return 0 # Delve into subtrees aops = a.ufl_operands bops = b.ufl_operands # Sort by children in natural order for r, s in zip(aops, bops): # Skip subtree if objects are the same if r is s: continue # Append subtree for further inspection left.append((r, s)) # All children compare as equal, a and b must be # equal. Except for... A few types, notably ExprList and # ExprMapping, can have a different number of operands. # Sort by the length if it's different. Doing this after # sorting by children because these types are rare so we # try to avoid the cost of this check for most nodes. x, y = len(aops), len(bops) if x != y: return -1 if x < y else 1 # Equal if we get out of the above loop! return 0 def sorted_expr(sequence): """Return a canonically sorted list of Expr objects in sequence.""" return sorted(sequence, key=cmp_to_key(cmp_expr)) def sorted_expr_sum(seq): """Sorted expr sum.""" seq2 = sorted(seq, key=cmp_to_key(cmp_expr)) s = seq2[0] for e in seq2[1:]: s = s + e return s ufl-2024.2.0/ufl/split_functions.py000066400000000000000000000101741470142567200171250ustar00rootroot00000000000000"""Algorithm for splitting a Coefficient or Argument into subfunctions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Anders Logg, 2008 from ufl.domain import extract_unique_domain from ufl.functionspace import FunctionSpace from ufl.indexed import Indexed from ufl.permutation import compute_indices from ufl.tensors import ListTensor, as_matrix, as_vector from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides from ufl.utils.sequences import product def split(v): """Split a coefficient or argument. If v is a Coefficient or Argument in a mixed space, returns a tuple with the function components corresponding to the subelements. """ domain = extract_unique_domain(v) # Default range is all of v begin = 0 end = None if isinstance(v, Indexed): # Special case: split previous output of split again # Consistent with simple element, just return function in a tuple return (v,) elif isinstance(v, ListTensor): # Special case: split previous output of split again ops = v.ufl_operands if all(isinstance(comp, Indexed) for comp in ops): args = [comp.ufl_operands[0] for comp in ops] if all(args[0] == args[i] for i in range(1, len(args))): # Get innermost terminal here and its element v = args[0] # Get relevant range of v components (begin,) = ops[0].ufl_operands[1] (end,) = ops[-1].ufl_operands[1] begin = int(begin) end = int(end) + 1 else: raise ValueError(f"Don't know how to split {v}.") else: raise ValueError(f"Don't know how to split {v}.") # Special case: simple element, just return function in a tuple element = v.ufl_element() if element.num_sub_elements == 0: assert end is None return (v,) if len(v.ufl_shape) != 1: raise ValueError( "Don't know how to split tensor valued mixed functions without flattened index space." ) # Compute value size and set default range end value_size = v.ufl_function_space().value_size if end is None: end = value_size else: # Recursively dive into mixedelement in to subelement # corresponding to beginning of range j = begin while True: for e in element.sub_elements: if j < FunctionSpace(domain, e).value_size: element = e break j -= FunctionSpace(domain, e).value_size # Then break when we find the subelement that covers the whole range if FunctionSpace(domain, element).value_size == (end - begin): break # Build expressions representing the subfunction of v for each subelement offset = begin sub_functions = [] for i, e in enumerate(element.sub_elements): # Get shape, size, indices, and v components # corresponding to subelement value shape = FunctionSpace(domain, e).value_shape strides = shape_to_strides(shape) rank = len(shape) sub_size = product(shape) subindices = [flatten_multiindex(c, strides) for c in compute_indices(shape)] components = [v[k + offset] for k in subindices] # Shape components into same shape as subelement if rank == 0: (subv,) = components elif rank <= 1: subv = as_vector(components) elif rank == 2: subv = as_matrix( [components[i * shape[1] : (i + 1) * shape[1]] for i in range(shape[0])] ) else: raise ValueError( f"Don't know how to split functions with sub functions of rank {rank}." ) offset += sub_size sub_functions.append(subv) if end != offset: raise ValueError( "Function splitting failed to extract components for whole intended range." ) return tuple(sub_functions) ufl-2024.2.0/ufl/tensoralgebra.py000066400000000000000000000405341470142567200165350ustar00rootroot00000000000000"""Compound tensor algebra operations.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.algebra import Conj, Operator from ufl.constantvalue import Zero from ufl.core.expr import ufl_err_str from ufl.core.ufl_type import ufl_type from ufl.index_combination_utils import merge_nonoverlapping_indices from ufl.precedence import parstr from ufl.sorting import sorted_expr # Algebraic operations on tensors: # FloatValues: # dot(a,b) = a*b # inner(a,b) = a*b # outer(a,b) = a*b # Vectors: # dot(u,v) = u_i v_i # inner(u,v) = u_i v_i # outer(u,v) = A | A_ij = u_i v_j # Matrices: # dot(A,B) = C | C_ij = A_{ik} B_{kj} # inner(A,B) = A_{ij} B_{ij} # outer(A,B) = C | C_ijkl = A_ij B_kl # Combined: # dot(A,u) = v | v_i = A_{ik} u_k # inner(A,u) = v | v_i = A_{ik} u_k # outer(A,u) = C | C_ijk = B_ij u_k # dot(u,B) = v | v_i = u_k B_{ki} # inner(u,B) = v | v_i = u_k B_{ki} # outer(u,B) = C | C_ijk = u_i B_jk # # Argument requirements: # dot(x,y): last index of x has same dimension as first index of y # inner(x,y): shape of x equals the shape of y # --- Classes representing compound tensor algebra operations --- @ufl_type(is_abstract=True) class CompoundTensorOperator(Operator): """Compount tensor operator.""" __slots__ = () def __init__(self, operands): """Initialise.""" Operator.__init__(self, operands) # TODO: Use this and make Sum handle scalars only? # This would simplify some algorithms. The only # problem is we can't use + in many algorithms because # this type should be expanded by expand_compounds. # class TensorSum(CompoundTensorOperator): # "Sum of nonscalar expressions." # pass # TODO: Use this similarly to TensorSum? # This would simplify some algorithms. The only # problem is we can't use / in many algorithms because # this type should be expanded by expand_compounds. # class TensorDivision(CompoundTensorOperator): # "Division of nonscalar expression with a scalar expression." # pass # TODO: Use this similarly to TensorSum? # This would simplify some algorithms. The only # problem is we can't use * in many algorithms because # this type should be expanded by expand_compounds. # class MatrixProduct(CompoundTensorOperator): # "Product of a matrix with a matrix or vector." # pass # TODO: Use this similarly to TensorSum? # This would simplify some algorithms. The only # problem is we can't use abs in many algorithms because # this type should be expanded by expand_compounds. # class TensorAbs(CompoundTensorOperator): # "Absolute value of nonscalar expression." # pass @ufl_type(is_shaping=True, num_ops=1, inherit_indices_from_operand=0) class Transposed(CompoundTensorOperator): """Transposed tensor.""" __slots__ = () def __new__(cls, A): """Create new Transposed.""" if isinstance(A, Zero): a, b = A.ufl_shape return Zero((b, a), A.ufl_free_indices, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) if len(A.ufl_shape) != 2: raise ValueError("Transposed is only defined for rank 2 tensors.") @property def ufl_shape(self): """Get the UFL shape.""" s = self.ufl_operands[0].ufl_shape return (s[1], s[0]) def __str__(self): """Format as a string.""" return "%s^T" % parstr(self.ufl_operands[0], self) @ufl_type(num_ops=2) class Outer(CompoundTensorOperator): """Outer.""" __slots__ = ("ufl_free_indices", "ufl_index_dimensions") def __new__(cls, a, b): """Create new Outer.""" ash, bsh = a.ufl_shape, b.ufl_shape if isinstance(a, Zero) or isinstance(b, Zero): fi, fid = merge_nonoverlapping_indices(a, b) return Zero(ash + bsh, fi, fid) if ash == () or bsh == (): return Conj(a) * b return CompoundTensorOperator.__new__(cls) def __init__(self, a, b): """Initialise.""" CompoundTensorOperator.__init__(self, (a, b)) fi, fid = merge_nonoverlapping_indices(a, b) self.ufl_free_indices = fi self.ufl_index_dimensions = fid @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape + self.ufl_operands[1].ufl_shape def __str__(self): """Format as a string.""" return "%s (X) %s" % ( parstr(self.ufl_operands[0], self), parstr(self.ufl_operands[1], self), ) @ufl_type(num_ops=2) class Inner(CompoundTensorOperator): """Inner.""" __slots__ = ("ufl_free_indices", "ufl_index_dimensions") def __new__(cls, a, b): """Create new Inner.""" # Checks ash, bsh = a.ufl_shape, b.ufl_shape if ash != bsh: raise ValueError(f"Shapes do not match: {ufl_err_str(a)} and {ufl_err_str(b)}") # Simplification if isinstance(a, Zero) or isinstance(b, Zero): fi, fid = merge_nonoverlapping_indices(a, b) return Zero((), fi, fid) elif ash == (): return a * Conj(b) # sort operands for unique representation, # must be independent of various counts etc. # as explained in cmp_expr if (a, b) != tuple(sorted_expr((a, b))): return Conj(Inner(b, a)) return CompoundTensorOperator.__new__(cls) def __init__(self, a, b): """Initialise.""" CompoundTensorOperator.__init__(self, (a, b)) fi, fid = merge_nonoverlapping_indices(a, b) self.ufl_free_indices = fi self.ufl_index_dimensions = fid ufl_shape = () def __str__(self): """Format as a string.""" return "%s : %s" % (parstr(self.ufl_operands[0], self), parstr(self.ufl_operands[1], self)) @ufl_type(num_ops=2) class Dot(CompoundTensorOperator): """Dot.""" __slots__ = ("ufl_free_indices", "ufl_index_dimensions") def __new__(cls, a, b): """Create new Dot.""" ash = a.ufl_shape bsh = b.ufl_shape ar, br = len(ash), len(bsh) scalar = ar == 0 and br == 0 # Checks if not ((ar >= 1 and br >= 1) or scalar): raise ValueError( "Dot product requires non-scalar arguments, " f"got arguments with ranks {ar} and {br}." ) if not (scalar or ash[-1] == bsh[0]): raise ValueError("Dimension mismatch in dot product.") # Simplification if isinstance(a, Zero) or isinstance(b, Zero): shape = ash[:-1] + bsh[1:] fi, fid = merge_nonoverlapping_indices(a, b) return Zero(shape, fi, fid) elif scalar: # TODO: Move this to def dot()? return a * b return CompoundTensorOperator.__new__(cls) def __init__(self, a, b): """Initialise.""" CompoundTensorOperator.__init__(self, (a, b)) fi, fid = merge_nonoverlapping_indices(a, b) self.ufl_free_indices = fi self.ufl_index_dimensions = fid @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape[:-1] + self.ufl_operands[1].ufl_shape[1:] def __str__(self): """Format as a string.""" return "%s . %s" % (parstr(self.ufl_operands[0], self), parstr(self.ufl_operands[1], self)) @ufl_type(is_index_free=True, num_ops=1) class Perp(CompoundTensorOperator): """Perp.""" __slots__ = () def __new__(cls, A): """Create new Perp.""" sh = A.ufl_shape # Checks if not len(sh) == 1: raise ValueError(f"Perp requires arguments of rank 1, got {ufl_err_str(A)}") if not sh[0] == 2: raise ValueError(f"Perp can only work on 2D vectors, got {ufl_err_str(A)}") # Simplification if isinstance(A, Zero): return Zero(sh, A.ufl_free_indices, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) ufl_shape = (2,) def __str__(self): """Format as a string.""" return "perp(%s)" % self.ufl_operands[0] @ufl_type(num_ops=2) class Cross(CompoundTensorOperator): """Cross.""" __slots__ = ("ufl_free_indices", "ufl_index_dimensions") def __new__(cls, a, b): """Create new Cross.""" ash = a.ufl_shape bsh = b.ufl_shape # Checks if not (len(ash) == 1 and ash == bsh): raise ValueError( f"Cross product requires arguments of rank 1, got {ufl_err_str(a)} " f"and {ufl_err_str(b)}." ) # Simplification if isinstance(a, Zero) or isinstance(b, Zero): fi, fid = merge_nonoverlapping_indices(a, b) return Zero(ash, fi, fid) return CompoundTensorOperator.__new__(cls) def __init__(self, a, b): """Initialise.""" CompoundTensorOperator.__init__(self, (a, b)) fi, fid = merge_nonoverlapping_indices(a, b) self.ufl_free_indices = fi self.ufl_index_dimensions = fid ufl_shape = (3,) def __str__(self): """Format as a string.""" return "%s x %s" % (parstr(self.ufl_operands[0], self), parstr(self.ufl_operands[1], self)) @ufl_type(num_ops=1, inherit_indices_from_operand=0) class Trace(CompoundTensorOperator): """Trace.""" __slots__ = () def __new__(cls, A): """Create new Trace.""" # Checks if len(A.ufl_shape) != 2: raise ValueError("Trace of tensor with rank != 2 is undefined.") # Simplification if isinstance(A, Zero): return Zero((), A.ufl_free_indices, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) ufl_shape = () def __str__(self): """Format as a string.""" return "tr(%s)" % self.ufl_operands[0] @ufl_type(is_scalar=True, num_ops=1) class Determinant(CompoundTensorOperator): """Determinant.""" __slots__ = () def __new__(cls, A): """Create new Determinant.""" sh = A.ufl_shape r = len(sh) Afi = A.ufl_free_indices # Checks if r not in (0, 2): raise ValueError("Determinant of tensor with rank != 2 is undefined.") if r == 2 and sh[0] != sh[1]: raise ValueError("Cannot take determinant of rectangular rank 2 tensor.") if Afi: raise ValueError("Not expecting free indices in determinant.") # Simplification if isinstance(A, Zero): return Zero((), Afi, A.ufl_index_dimensions) if r == 0: return A return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) def __str__(self): """Format as a string.""" return "det(%s)" % self.ufl_operands[0] # TODO: Drop Inverse and represent it as product of Determinant and # Cofactor? @ufl_type(is_index_free=True, num_ops=1) class Inverse(CompoundTensorOperator): """Inverse.""" __slots__ = () def __new__(cls, A): """Create new Inverse.""" sh = A.ufl_shape r = len(sh) # Checks if A.ufl_free_indices: raise ValueError("Not expecting free indices in Inverse.") if isinstance(A, Zero): raise ValueError("Division by zero!") # Simplification if r == 0: return 1 / A # More checks if r != 2: raise ValueError("Inverse of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: raise ValueError(f"Cannot take inverse of rectangular matrix with dimensions {sh}.") return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape def __str__(self): """Format as a string.""" return "%s^-1" % parstr(self.ufl_operands[0], self) @ufl_type(is_index_free=True, num_ops=1) class Cofactor(CompoundTensorOperator): """Cofactor.""" __slots__ = () def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) # Checks sh = A.ufl_shape if len(sh) != 2: raise ValueError("Cofactor of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: raise ValueError(f"Cannot take cofactor of rectangular matrix with dimensions {sh}.") if A.ufl_free_indices: raise ValueError("Not expecting free indices in Cofactor.") if isinstance(A, Zero): raise ValueError("Cannot take cofactor of zero matrix.") @property def ufl_shape(self): """Get the UFL shape.""" return self.ufl_operands[0].ufl_shape def __str__(self): """Format as a string.""" return "cofactor(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Deviatoric(CompoundTensorOperator): """Deviatoric.""" __slots__ = () def __new__(cls, A): """Create new Deviatoric.""" sh = A.ufl_shape # Checks if len(sh) != 2: raise ValueError("Deviatoric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: raise ValueError( f"Cannot take deviatoric part of rectangular matrix with dimensions {sh}." ) if A.ufl_free_indices: raise ValueError("Not expecting free indices in Deviatoric.") # Simplification if isinstance(A, Zero): return Zero(A.ufl_shape, A.ufl_free_indices, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) def __str__(self): """Format as a string.""" return "dev(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Skew(CompoundTensorOperator): """Skew.""" __slots__ = () def __new__(cls, A): """Create new Skew.""" sh = A.ufl_shape Afi = A.ufl_free_indices # Checks if len(sh) != 2: raise ValueError("Skew symmetric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: raise ValueError(f"Cannot take skew part of rectangular matrix with dimensions {sh}.") if Afi: raise ValueError("Not expecting free indices in Skew.") # Simplification if isinstance(A, Zero): return Zero(A.ufl_shape, Afi, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) def __str__(self): """Format as a string.""" return "skew(%s)" % self.ufl_operands[0] @ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class Sym(CompoundTensorOperator): """Sym.""" __slots__ = () def __new__(cls, A): """Create new Sym.""" sh = A.ufl_shape Afi = A.ufl_free_indices # Checks if len(sh) != 2: raise ValueError("Symmetric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: raise ValueError( f"Cannot take symmetric part of rectangular matrix with dimensions {sh}." ) if Afi: raise ValueError("Not expecting free indices in Sym.") # Simplification if isinstance(A, Zero): return Zero(A.ufl_shape, Afi, A.ufl_index_dimensions) return CompoundTensorOperator.__new__(cls) def __init__(self, A): """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) def __str__(self): """Format as a string.""" return f"sym({self.ufl_operands[0]})" ufl-2024.2.0/ufl/tensors.py000066400000000000000000000352611470142567200154030ustar00rootroot00000000000000"""Classes used to group scalar expressions into expressions with rank > 0.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016. from ufl.constantvalue import Zero, as_ufl from ufl.core.expr import Expr from ufl.core.multiindex import FixedIndex, Index, MultiIndex, indices from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.index_combination_utils import remove_indices from ufl.indexed import Indexed # --- Classes representing tensors of UFL expressions --- @ufl_type(is_shaping=True, num_ops="varying", inherit_indices_from_operand=0) class ListTensor(Operator): """Wraps a list of expressions into a tensor valued expression of one higher rank.""" __slots__ = () def __new__(cls, *expressions): """Create a new ListTensor.""" # All lists and tuples should already be unwrapped in # as_tensor if any(not isinstance(e, Expr) for e in expressions): raise ValueError("Expecting only UFL expressions in ListTensor constructor.") # Get properties of the first expression e0 = expressions[0] sh = e0.ufl_shape fi = e0.ufl_free_indices fid = e0.ufl_index_dimensions # Obviously, each subexpression must have the same shape if any(sh != e.ufl_shape for e in expressions[1:]): raise ValueError( "Cannot create a tensor by joining subexpressions with different shapes." ) if any(fi != e.ufl_free_indices for e in expressions[1:]): raise ValueError( "Cannot create a tensor where the components have different free indices." ) if any(fid != e.ufl_index_dimensions for e in expressions[1:]): raise ValueError( "Cannot create a tensor where the components have different free index dimensions." ) # Simplify to Zero if possible if all(isinstance(e, Zero) for e in expressions): shape = (len(expressions),) + sh return Zero(shape, fi, fid) return Operator.__new__(cls) def __init__(self, *expressions): """Initialise.""" Operator.__init__(self, expressions) # Checks indexset = set(self.ufl_operands[0].ufl_free_indices) if not all(not (indexset ^ set(e.ufl_free_indices)) for e in self.ufl_operands): raise ValueError( "Can't combine subtensor expressions with different sets of free indices." ) @property def ufl_shape(self): """Get the UFL shape.""" return (len(self.ufl_operands),) + self.ufl_operands[0].ufl_shape def evaluate(self, x, mapping, component, index_values, derivatives=()): """Evaluate.""" if len(component) != len(self.ufl_shape): raise ValueError( "Can only evaluate scalars, expecting a component " "tuple of length {len(self.ufl_shape)}, not {component}." ) a = self.ufl_operands[component[0]] component = component[1:] if derivatives: return a.evaluate(x, mapping, component, index_values, derivatives) else: return a.evaluate(x, mapping, component, index_values) def __getitem__(self, key): """Get an item.""" origkey = key if isinstance(key, MultiIndex): key = key.indices() if not isinstance(key, tuple): key = (key,) k = key[0] if isinstance(k, (int, FixedIndex)): sub = self.ufl_operands[int(k)] return sub if len(key) == 1 else sub[key[1:]] return Expr.__getitem__(self, origkey) def __str__(self): """Format as a string.""" def substring(expressions, indent): ind = " " * indent if any(isinstance(e, ListTensor) for e in expressions): substrings = [] for e in expressions: if isinstance(e, ListTensor): substrings.append(substring(e.ufl_operands, indent + 2)) else: substrings.append(str(e)) s = (",\n" + ind).join(substrings) return "%s[\n%s%s\n%s]" % (ind, ind, s, ind) else: s = ", ".join(str(e) for e in expressions) return "%s[%s]" % (ind, s) return substring(self.ufl_operands, 0) @ufl_type(is_shaping=True, num_ops="varying") class ComponentTensor(Operator): """Maps the free indices of a scalar valued expression to tensor axes.""" __slots__ = ("ufl_shape", "ufl_free_indices", "ufl_index_dimensions") def __new__(cls, expression, indices): """Create a new ComponentTensor.""" # Simplify if isinstance(expression, Zero): fi, fid, sh = remove_indices( expression.ufl_free_indices, expression.ufl_index_dimensions, [ind.count() for ind in indices], ) return Zero(sh, fi, fid) # Construct return Operator.__new__(cls) def __init__(self, expression, indices): """Initialise.""" if not isinstance(expression, Expr): raise ValueError("Expecting ufl expression.") if expression.ufl_shape != (): raise ValueError("Expecting scalar valued expression.") if not isinstance(indices, MultiIndex): raise ValueError("Expecting a MultiIndex.") if not all(isinstance(i, Index) for i in indices): raise ValueError(f"Expecting sequence of Index objects, not {indices._ufl_err_str_()}.") Operator.__init__(self, (expression, indices)) fi, fid, sh = remove_indices( expression.ufl_free_indices, expression.ufl_index_dimensions, [ind.count() for ind in indices], ) self.ufl_free_indices = fi self.ufl_index_dimensions = fid self.ufl_shape = sh def _ufl_expr_reconstruct_(self, expressions, indices): """Reconstruct.""" # Special case for simplification as_tensor(A[ii], ii) -> A if isinstance(expressions, Indexed): A, ii = expressions.ufl_operands if indices == ii: return A return Operator._ufl_expr_reconstruct_(self, expressions, indices) def indices(self): """Get indices.""" return self.ufl_operands[1] def evaluate(self, x, mapping, component, index_values): """Evaluate.""" indices = self.ufl_operands[1] a = self.ufl_operands[0] if len(indices) != len(component): raise ValueError("Expecting a component matching the indices tuple.") # Map component to indices for i, c in zip(indices, component): index_values.push(i, c) a = a.evaluate(x, mapping, (), index_values) for _ in component: index_values.pop() return a def __str__(self): """Format as a string.""" return "{ A | A_{%s} = %s }" % (self.ufl_operands[1], self.ufl_operands[0]) # --- User-level functions to wrap expressions in the correct way --- def numpy2nestedlists(arr): """Convert Numpy array to a nested list.""" from numpy import ndarray if not isinstance(arr, ndarray): return arr return [numpy2nestedlists(arr[k]) for k in range(arr.shape[0])] def _as_list_tensor(expressions): """Convert to a list tensor.""" if isinstance(expressions, (list, tuple)): expressions = [_as_list_tensor(e) for e in expressions] return ListTensor(*expressions) else: return as_ufl(expressions) def from_numpy_to_lists(expressions): """Convert Numpy array to lists.""" try: import numpy as np if isinstance(expressions, np.ndarray): if expressions.shape == (): # Unwrap scalar ndarray return expressions.item() else: expressions = numpy2nestedlists(expressions) except Exception: pass return expressions def as_tensor(expressions, indices=None): """Make a tensor valued expression. This works in two different ways, by using indices or lists. 1) Returns :math:`A` such that :math:`A` [*indices*] = *expressions*. If *indices* are provided, *expressions* must be a scalar valued expression with all the provided indices among its free indices. This operator will then map each of these indices to a tensor axis, thereby making a tensor valued expression from a scalar valued expression with free indices. 2) Returns :math:`A` such that :math:`A[k,...]` = *expressions*[k]. If no indices are provided, *expressions* must be a list or tuple of expressions. The expressions can also consist of recursively nested lists to build higher rank tensors. """ if indices is None: # Allow as_tensor(as_tensor(A)) and as_vector(as_vector(v)) in user code if isinstance(expressions, Expr): return expressions # Support numpy array, but avoid importing numpy if not needed if not isinstance(expressions, (list, tuple)): expressions = from_numpy_to_lists(expressions) # Sanity check if not isinstance(expressions, (list, tuple, Expr)): raise ValueError("Expecting nested list or tuple.") # Recursive conversion from nested lists to nested ListTensor # objects return _as_list_tensor(expressions) else: # Make sure we have a tuple of indices if isinstance(indices, list): indices = tuple(indices) elif not isinstance(indices, tuple): indices = (indices,) # Special case for as_tensor(expr, ii) with ii = () if indices == (): return expressions indices = MultiIndex(indices) # Special case for simplification as_tensor(A[ii], ii) -> A if isinstance(expressions, Indexed): A, ii = expressions.ufl_operands if indices.indices() == ii.indices(): return A # Make a tensor from given scalar expression with free indices return ComponentTensor(expressions, indices) def as_matrix(expressions, indices=None): """As *as_tensor()*, but limited to rank 2 tensors.""" if indices is None: # Allow as_matrix(as_matrix(A)) in user code if isinstance(expressions, Expr): if len(expressions.ufl_shape) != 2: raise ValueError("Expecting rank 2 tensor.") return expressions # To avoid importing numpy unneeded, it's quite slow... if not isinstance(expressions, (list, tuple)): expressions = from_numpy_to_lists(expressions) # Check for expected list structure if not isinstance(expressions, (list, tuple)): raise ValueError("Expecting nested list or tuple of Exprs.") if not isinstance(expressions[0], (list, tuple)): raise ValueError("Expecting nested list or tuple of Exprs.") else: if len(indices) != 2: raise ValueError("Expecting exactly two indices.") return as_tensor(expressions, indices) def as_vector(expressions, index=None): """As ``as_tensor()``, but limited to rank 1 tensors.""" if index is None: # Allow as_vector(as_vector(v)) in user code if isinstance(expressions, Expr): if len(expressions.ufl_shape) != 1: raise ValueError("Expecting rank 1 tensor.") return expressions # To avoid importing numpy unneeded, it's quite slow... if not isinstance(expressions, (list, tuple)): expressions = from_numpy_to_lists(expressions) # Check for expected list structure if not isinstance(expressions, (list, tuple)): raise ValueError("Expecting nested list or tuple of Exprs.") else: if not isinstance(index, Index): raise ValueError("Expecting a single Index object.") index = (index,) return as_tensor(expressions, index) def as_scalar(expression): """As scalar. Given a scalar or tensor valued expression A, returns either of the tuples:: (a,b) = (A, ()) (a,b) = (A[indices], indices) such that a is always a scalar valued expression. """ ii = indices(len(expression.ufl_shape)) if ii: expression = expression[ii] return expression, ii def as_scalars(*expressions): """As scalars. Given multiple scalar or tensor valued expressions A, returns either of the tuples:: (a,b) = (A, ()) (a,b) = ([A[0][indices], ..., A[-1][indices]], indices) such that a is always a list of scalar valued expressions. """ ii = indices(len(expressions[0].ufl_shape)) if ii: expressions = [expression[ii] for expression in expressions] return expressions, ii def unit_list(i, n): """Create a list of zeros where the ith entry is 1.""" return [(1 if i == j else 0) for j in range(n)] def unit_list2(i, j, n): """Creage a two dimensional list of zeros where the (i,j)th entry is 1.""" return [[(1 if (i == i0 and j == j0) else 0) for j0 in range(n)] for i0 in range(n)] def unit_vector(i, d): """A constant unit vector in direction *i* with dimension *d*.""" return as_vector(unit_list(i, d)) def unit_vectors(d): """A tuple of constant unit vectors in all directions with dimension *d*.""" return tuple(unit_vector(i, d) for i in range(d)) def unit_matrix(i, j, d): """A constant unit matrix in direction *i*,*j* with dimension *d*.""" return as_matrix(unit_list2(i, j, d)) def unit_matrices(d): """A tuple of constant unit matrices in all directions with dimension *d*.""" return tuple(unit_matrix(i, j, d) for i in range(d) for j in range(d)) def unit_indexed_tensor(shape, component): """Unit indexed tensor.""" from ufl.constantvalue import Identity from ufl.operators import outer # a bit of circular dependency issue here r = len(shape) if r == 0: return 0, () jj = indices(r) es = [] for i in range(r): s = shape[i] c = component[i] j = jj[i] e = Identity(s)[c, j] es.append(e) E = es[0] for e in es[1:]: E = outer(E, e) return E, jj def unwrap_list_tensor(lt): """Unwrap a list tensor.""" components = [] sh = lt.ufl_shape subs = lt.ufl_operands if len(sh) == 1: for s in range(sh[0]): components.append(((s,), subs[s])) else: for s, sub in enumerate(subs): for c, v in unwrap_list_tensor(sub): components.append(((s,) + c, v)) return components ufl-2024.2.0/ufl/utils/000077500000000000000000000000001470142567200144655ustar00rootroot00000000000000ufl-2024.2.0/ufl/utils/__init__.py000066400000000000000000000000211470142567200165670ustar00rootroot00000000000000"""Utilities.""" ufl-2024.2.0/ufl/utils/counted.py000066400000000000000000000024071470142567200165030ustar00rootroot00000000000000"""Mixin class for types with a global unique counter attached to each object.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import itertools class Counted: """Mixin class for globally counted objects.""" # Mixin classes do not work well with __slots__ so _count must be # added to the __slots__ of the inheriting class __slots__ = () _counter = None def __init__(self, count=None, counted_class=None): """Initialize the Counted instance. Args: count: The object count, if ``None`` defaults to the next value according to the global counter (per type). counted_class: Class to attach the global counter too. If ``None`` then ``type(self)`` will be used. """ # create a new counter for each subclass counted_class = counted_class or type(self) if counted_class._counter is None: counted_class._counter = itertools.count() self._count = count if count is not None else next(counted_class._counter) self._counted_class = counted_class def count(self): """Get count.""" return self._count ufl-2024.2.0/ufl/utils/formatting.py000066400000000000000000000077001470142567200172150ustar00rootroot00000000000000"""Various string formatting utilities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later def camel2underscore(name): """Convert a CamelCaps string to underscore_syntax.""" letters = [] lastlower = False for i in name: thislower = i.islower() or i.isdigit() if not thislower: # Don't insert _ between multiple upper case letters if lastlower: letters.append("_") i = i.lower() lastlower = thislower letters.append(i) return "".join(letters) def lstr(a): """Pretty-print list or tuple, invoking str() on items instead of repr() like str() does.""" if isinstance(a, list): return "[" + ", ".join(lstr(item) for item in a) + "]" elif isinstance(a, tuple): return "(" + ", ".join(lstr(item) for item in a) + ")" return str(a) def tstr(t, colsize=80): """Pretty-print list of tuples of key-value pairs.""" if not t: return "" # Compute maximum key length keylen = max(len(str(item[0])) for item in t) # Key-length cannot be larger than colsize if keylen > colsize: return str(t) # Pretty-print table s = "" for key, value in t: key = str(key) if isinstance(value, str): value = "'%s'" % value else: value = str(value) s += key + ":" + " " * (keylen - len(key) + 1) space = "" while len(value) > 0: end = min(len(value), colsize - keylen) s += space + value[:end] + "\n" value = value[end:] space = " " * (keylen + 2) return s def istr(o): """Format object as string, inserting ? for None.""" if o is None: return "?" else: return str(o) def estr(elements): """Format list of elements for printing.""" return ", ".join(f"{e}" for e in elements) def _indent_string(n): """Return indentation string.""" return " " * n def _tree_format_expression(expression, indentation, parentheses): """Tree format expression.""" ind = _indent_string(indentation) if expression._ufl_is_terminal_: s = "%s%s" % (ind, repr(expression)) else: sops = [ _tree_format_expression(o, indentation + 1, parentheses) for o in expression.ufl_operands ] s = "%s%s\n" % (ind, expression._ufl_class_.__name__) if parentheses and len(sops) > 1: s += "%s(\n" % (ind,) s += "\n".join(sops) if parentheses and len(sops) > 1: s += "\n%s)" % (ind,) return s def tree_format(expression, indentation=0, parentheses=True): """Tree format.""" from ufl.core.expr import Expr from ufl.form import Form from ufl.integral import Integral s = "" if isinstance(expression, Form): form = expression integrals = form.integrals() integral_types = sorted(set(itg.integral_type() for itg in integrals)) itgs = [] for integral_type in integral_types: itgs += list(form.integrals_by_type(integral_type)) ind = _indent_string(indentation) s += ind + "Form:\n" s += "\n".join(tree_format(itg, indentation + 1, parentheses) for itg in itgs) elif isinstance(expression, Integral): ind = _indent_string(indentation) s += ind + "Integral:\n" ind = _indent_string(indentation + 1) s += ind + "integral type: %s\n" % expression.integral_type() s += ind + "subdomain id: %s\n" % expression.subdomain_id() s += ind + "integrand:\n" s += tree_format(expression._integrand, indentation + 2, parentheses) elif isinstance(expression, Expr): s += _tree_format_expression(expression, indentation, parentheses) else: raise ValueError(f"Invalid object type {type(expression)}") return s ufl-2024.2.0/ufl/utils/indexflattening.py000066400000000000000000000016341470142567200202260ustar00rootroot00000000000000"""Collection of utilities for mapping between multiindices and a flattened index space.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later def shape_to_strides(sh): """Return a tuple of strides given a shape tuple.""" n = len(sh) if not n: return () strides = [None] * n strides[n - 1] = 1 for i in range(n - 1, 0, -1): strides[i - 1] = strides[i] * sh[i] return tuple(strides) def flatten_multiindex(ii, strides): """Return the flat index corresponding to the given multiindex.""" i = 0 for c, s in zip(ii, strides): i += c * s return i def unflatten_index(i, strides): """Return the multiindex corresponding to the given flat index.""" ii = [] for s in strides: ii.append(i // s) i %= s return tuple(ii) ufl-2024.2.0/ufl/utils/sequences.py000066400000000000000000000014651470142567200170400ustar00rootroot00000000000000"""Various sequence manipulation utilities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from functools import reduce import numpy as np def product(sequence): """Return the product of all elements in a sequence.""" p = 1 for f in sequence: p *= f return p def max_degree(degrees): """Maximum degree for mixture of scalar and tuple degrees.""" # np.maximum broadcasts scalar degrees to tuple degrees if # necessary. reduce applies np.maximum pairwise. degree = reduce(np.maximum, map(np.asarray, degrees)) if degree.ndim: degree = tuple(map(int, degree)) # tuple degree else: degree = int(degree) # scalar degree return degree ufl-2024.2.0/ufl/utils/sorting.py000066400000000000000000000056031470142567200165300ustar00rootroot00000000000000"""Utilites for sorting.""" # Copyright (C) 2008-2016 Johan Hake # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later import warnings def topological_sorting(nodes, edges): """Return a topologically sorted list of the nodes. Implemented algorithm from Wikipedia (http://en.wikipedia.org/wiki/Topological_sorting). No error for cyclic edges... """ L = [] S = nodes[:] for node in nodes: for es in edges.values(): if node in es and node in S: S.remove(node) break while S: node = S.pop(0) L.append(node) node_edges = edges[node] while node_edges: m = node_edges.pop(0) found = False for es in edges.values(): found = m in es if found: break if not found: S.insert(0, m) return L def sorted_by_count(seq): """Sort a sequence by the item.count().""" return sorted(seq, key=lambda x: x.count()) def sorted_by_key(mapping): """Sort dict items by key, allowing different key types.""" # Python3 doesn't allow comparing builtins of different type, # therefore the typename trick here def _key(x): return (type(x[0]).__name__, x[0]) return sorted(mapping.items(), key=_key) def canonicalize_metadata(metadata): """Assuming metadata to be a dict with string keys and builtin python types as values. Transform dict to a tuple of (key, value) item tuples ordered by key, with dict, list and tuple values converted the same way recursively. Lists and tuples are converted to tuples. Other values are converted using str(). This is such that the end result can be hashed and sorted using regular <, because python 3 doesn't allow e.g. (3 < "auto") which occurs regularly in metadata. """ if metadata is None: return () if isinstance(metadata, dict): keys = sorted(metadata.keys()) assert all(isinstance(key, str) for key in keys) values = [metadata[key] for key in keys] elif isinstance(metadata, (tuple, list)): values = metadata newvalues = [] for value in values: if isinstance(value, (dict, list, tuple)): value = canonicalize_metadata(value) elif isinstance(value, (int, float, str)) or value is None: value = str(value) elif hasattr(value, "ufl_signature"): value = value.ufl_signature else: warnings.warn( f"Applying str() to a metadata value of type {type(value).__name__}, " "don't know if this is safe." ) value = str(value) newvalues.append(value) if isinstance(metadata, dict): return tuple(zip(keys, newvalues)) else: return tuple(newvalues) ufl-2024.2.0/ufl/utils/stacks.py000066400000000000000000000023631470142567200163330ustar00rootroot00000000000000"""Various utility data structures.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later class Stack(list): """A stack datastructure.""" def __init__(self, *args): """Initialise.""" list.__init__(self, *args) def push(self, v): """Push.""" list.append(self, v) def peek(self): """Peek.""" return self[-1] class StackDict(dict): """A dictionary type. A dict that can be changed incrementally with 'd.push(k,v)' and have changes rolled back with 'k,v = d.pop()'. """ def __init__(self, *args, **kwargs): """Initialise.""" dict.__init__(self, *args, **kwargs) self._l = [] def push(self, k, v): """Store previous state for this key.""" self._l.append((k, self.get(k, None))) if v is None: if k in self: del self[k] else: self[k] = v def pop(self): """Restore previous state for this key.""" k, v = self._l.pop() if v is None: if k in self: del self[k] else: self[k] = v return k, v ufl-2024.2.0/ufl/variable.py000066400000000000000000000072531470142567200154730ustar00rootroot00000000000000"""Define the Variable and Label classes. These are used to label expressions as variables for differentiation. """ # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.constantvalue import as_ufl from ufl.core.expr import Expr from ufl.core.operator import Operator from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.utils.counted import Counted @ufl_type() class Label(Terminal, Counted): """Label.""" __slots__ = ("_count", "_counted_class") def __init__(self, count=None): """Initialise.""" Terminal.__init__(self) Counted.__init__(self, count, Label) def __str__(self): """Format as a string.""" return "Label(%d)" % self._count def __repr__(self): """Representation.""" r = "Label(%d)" % self._count return r @property def ufl_shape(self): """Get the UFL shape.""" raise ValueError("Label has no shape (it is not a tensor expression).") @property def ufl_free_indices(self): """Get the UFL free indices.""" raise ValueError("Label has no free indices (it is not a tensor expression).") @property def ufl_index_dimensions(self): """Get the UFL index dimensions.""" raise ValueError("Label has no free indices (it is not a tensor expression).") def is_cellwise_constant(self): """Return true if the object is constant on each cell.""" return True def ufl_domains(self): """Return tuple of domains related to this terminal object.""" return () def _ufl_signature_data_(self, renumbering): """UFL signature data.""" if self not in renumbering: return ("Label", self._count) return ("Label", renumbering[self]) @ufl_type(is_shaping=True, is_index_free=True, num_ops=1, inherit_shape_from_operand=0) class Variable(Operator): """A Variable is a representative for another expression. It will be used by the end-user mainly for defining a quantity to differentiate w.r.t. using diff. Example:: e = <...> e = variable(e) f = exp(e**2) df = diff(f, e) """ __slots__ = () def __init__(self, expression, label=None): """Initalise.""" # Conversion expression = as_ufl(expression) if label is None: label = Label() # Checks if not isinstance(expression, Expr): raise ValueError("Expecting Expr.") if not isinstance(label, Label): raise ValueError("Expecting a Label.") if expression.ufl_free_indices: raise ValueError("Variable cannot wrap an expression with free indices.") Operator.__init__(self, (expression, label)) def ufl_domains(self): """Get the UFL domains.""" return self.ufl_operands[0].ufl_domains() def evaluate(self, x, mapping, component, index_values): """Evaluate.""" a = self.ufl_operands[0].evaluate(x, mapping, component, index_values) return a def expression(self): """Get expression.""" return self.ufl_operands[0] def label(self): """Get label.""" return self.ufl_operands[1] def __eq__(self, other): """Check equality.""" return ( isinstance(other, Variable) and self.ufl_operands[1] == other.ufl_operands[1] and self.ufl_operands[0] == other.ufl_operands[0] ) def __str__(self): """Format as a string.""" return "var%d(%s)" % (self.ufl_operands[1].count(), self.ufl_operands[0])