diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..4e7559dcb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index e7f65cb77..7301b1b6c 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -11,11 +11,11 @@ on: default: "main" type: string test_pypi_publish: - description: "Publish to Test PyPi (true | false)" + description: "Publish to Test PyPi" default: false type: boolean pypi_publish: - description: "Publish to PyPi (true | false)" + description: "Publish to PyPi" default: false type: boolean @@ -26,11 +26,11 @@ on: default: "main" type: string test_pypi_publish: - description: "Publish to Test PyPi (true | false)" + description: "Publish to Test PyPi" default: false type: boolean pypi_publish: - description: "Publish to PyPi (true | false))" + description: "Publish to PyPi" default: false type: boolean @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout UFL - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.ufl_ref }} @@ -50,32 +50,48 @@ jobs: - name: Build sdist and wheel run: python -m build . - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: path: dist/* + upload_test_pypi: + name: Upload to test PyPI (optional) + if: ${{ github.event.inputs.test_pypi_publish == 'true' }} + needs: [build] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/fenics-ufl + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: artifact + path: dist + + - name: Push to test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + upload_pypi: name: Upload to PyPI (optional) + if: ${{ github.event.inputs.pypi_publish == 'true' }} needs: [build] runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/fenics-ufl + permissions: + id-token: write + steps: - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: artifact path: dist - name: Push to PyPI - uses: pypa/gh-action-pypi-publish@v1.5.0 - 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@v1.5.0 - if: ${{ github.event.inputs.test_pypi_publish == 'true' }} - with: - user: __token__ - password: ${{ secrets.PYPI_TEST_TOKEN }} - repository_url: https://test.pypi.org/legacy/ + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/fenicsx-tests.yml b/.github/workflows/fenicsx-tests.yml index fa98a9cd1..74d95f115 100644 --- a/.github/workflows/fenicsx-tests.yml +++ b/.github/workflows/fenicsx-tests.yml @@ -11,17 +11,18 @@ 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@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Install test dependencies run: | @@ -29,18 +30,19 @@ jobs: - name: Install UFL run: | - pip3 install . + 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@v2 + uses: actions/checkout@v4 with: path: ./ffcx repository: FEniCS/ffcx ref: main + - name: Install FFCx run: | cd ffcx @@ -51,38 +53,27 @@ jobs: dolfinx-tests: name: Run DOLFINx tests runs-on: ubuntu-latest - container: fenicsproject/test-env:nightly-openmpi + container: ghcr.io/fenics/test-env:current-openmpi env: - CC: clang - CXX: clang++ - - PETSC_ARCH: linux-gnu-complex-32 + PETSC_ARCH: linux-gnu-complex128-32 OMPI_ALLOW_RUN_AS_ROOT: 1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1 - OMPI_MCA_rmaps_base_oversubscribe: 1 - OMPI_MCA_plm: isolated - OMPI_MCA_btl_vader_single_copy_mechanism: none - OMPI_MCA_mpi_yield_when_idle: 1 - OMPI_MCA_hwloc_base_binding_policy: none steps: - - uses: actions/checkout@v2 - - name: Install dependencies (Python) - run: | - python3 -m pip install --upgrade pip + - uses: actions/checkout@v4 - name: Install UFL run: | - pip3 install . + pip3 install --break-system-packages . - name: Install Basix and FFCx run: | - python3 -m pip install git+https://github.com/FEniCS/basix.git - python3 -m pip install git+https://github.com/FEniCS/ffcx.git + 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@v2 + uses: actions/checkout@v4 with: path: ./dolfinx repository: FEniCS/dolfinx @@ -92,6 +83,7 @@ jobs: cmake -G Ninja -DCMAKE_BUILD_TYPE=Developer -B build -S dolfinx/cpp/ cmake --build build cmake --install build - pip3 -v install --global-option build --global-option --debug dolfinx/python/ + 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 diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 4f8017f56..127601b1b 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -9,7 +9,7 @@ on: branches: - "**" tags: - - "v*" + - "**" pull_request: branches: - main @@ -20,40 +20,46 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + 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@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Lint with flake8 - run: | - python -m pip install flake8 - flake8 --statistics . - - name: Check documentation style + - name: Lint with ruff run: | - python -m pip install pydocstyle - python -m pydocstyle . + 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' }} + if: ${{ github.repository == 'FEniCS/ufl' && github.head_ref == '' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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@v2 + uses: actions/upload-artifact@v4 with: name: doc-${{ matrix.os }}-${{ matrix.python-version }} path: doc/sphinx/build/html/ @@ -61,25 +67,25 @@ jobs: 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@v2 + if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.12 }} + 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 }} + if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.12 }} 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 }} + if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.12 }} 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 }} + if: ${{ github.repository == 'FEniCS/ufl' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') ) && runner.os == 'Linux' && matrix.python-version == 3.12 }} run: | cd docs git config --global user.email "fenics@github.com" diff --git a/.github/workflows/spack.yml b/.github/workflows/spack.yml new file mode 100644 index 000000000..7af85df62 --- /dev/null +++ b/.github/workflows/spack.yml @@ -0,0 +1,61 @@ +name: 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 diff --git a/.github/workflows/tsfc-tests.yml b/.github/workflows/tsfc-tests.yml new file mode 100644 index 000000000..e240009f7 --- /dev/null +++ b/.github/workflows/tsfc-tests.yml @@ -0,0 +1,47 @@ +# 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@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install UFL + run: | + pip3 install . + + - name: Clone tsfc + uses: actions/checkout@v4 + with: + path: ./tsfc + repository: firedrakeproject/tsfc + ref: master + - 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#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 diff --git a/AUTHORS b/AUTHORS index 7cd4a89f4..ad4d496c3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,3 +30,6 @@ Contributors: | Tuomas Airaksinen | Reuben W. Hill | Cécile Daversin-Catty + | Nacime Bouziani + | Matthew Scroggs + \ No newline at end of file diff --git a/ChangeLog.rst b/ChangeLog.md similarity index 79% rename from ChangeLog.rst rename to ChangeLog.md index 28559668d..75d1ab629 100644 --- a/ChangeLog.rst +++ b/ChangeLog.md @@ -1,97 +1,91 @@ -Changelog -========= +# Changelog -2021.1.0 -------------- +## 2021.1.0 - Mixed dimensional domain support -2019.1.0 (2019-04-17) ---------------------- +## 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 + 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) ---------------------- +## 2018.1.0 (2018-06-14) - Remove python2 support -2017.2.0 (2017-12-05) ---------------------- +## 2017.2.0 (2017-12-05) -- Add geometric quantity ``CellDiameter`` defined as a set diameter +- 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; + `(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`` +- Implement geometry lowering of `(Min|Max)(Cell|Facet)EdgeLength` for quads and hexes -2017.1.0.post1 (2017-09-12) ---------------------------- +## 2017.1.0.post1 (2017-09-12) - Change PyPI package name to fenics-ufl. -2017.1.0 (2017-05-09) ---------------------- +## 2017.1.0 (2017-05-09) -- Add the ``DirectionalSobolevSpace`` subclass of ``SobolevSpace``. This +- 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 +- 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 +- 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 + `DirectionalSobolevSpace` is returned depending on the order of the spaces associated with the component elements. -2016.2.0 (2016-11-30) ---------------------- +## 2016.2.0 (2016-11-30) -- Add call operator syntax to ``Form`` to replace arguments and +- 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:: - + 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:: - + ``` +- 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)`` + ``` +- 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 +- 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, +- 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 +- 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 @@ -103,8 +97,7 @@ Changelog - Add bitbucket pipelines testing - Improve documentation -2016.1.0 (2016-06-23) ---------------------- +## 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 @@ -127,8 +120,7 @@ Changelog - Large reworking of symbolic geometry pipeline - Implement symbolic Piola mappings -1.6.0 (2015-07-28) ------------------- +## 1.6.0 (2015-07-28) - Change approach to attaching __hash__ implementation to accomodate python 3 - Implement new non-recursive traversal based hash computation @@ -147,13 +139,12 @@ Changelog - Remove Measure constants - Remove cell2D and cell3D - Implement reference_value in apply_restrictions -- Rename point integral to vertex integral and kept ``*dP`` syntax +- 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) ------------------- +## 1.5.0 (2015-01-12) - Require Python 2.7 - Python 3 support @@ -192,10 +183,9 @@ Changelog - Fix signature stability w.r.t. metadata dicts - Minor bugfixes, removal of unused code and cleanups -1.4.0 (2014-06-02) ------------------- +## 1.4.0 (2014-06-02) -- New integral type custom_integral (``*dc``) +- 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) @@ -225,8 +215,7 @@ Changelog - Various minor bugfixes - Various docstring improvements -1.3.0 (2014-01-07) ------------------- +## 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. @@ -236,8 +225,7 @@ Changelog - Add atan2 function - Allow form+0 -> form -1.2.0 (2013-03-24) ------------------- +## 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 @@ -249,8 +237,7 @@ Changelog - Generalize jump(v,n) for rank(v) > 2 - Fix some minor bugs -1.1.0 (2013-01-07) ------------------- +## 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. @@ -260,8 +247,7 @@ Changelog - 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) ---------------------------------- +## 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]) @@ -273,13 +259,11 @@ Changelog - Add simplification f/f -> 1 - Add operators <,>,<=,>= in place of lt,gt,le,ge -1.0.0 (2011-12-07) ------------------- +## 1.0.0 (2011-12-07) - No changes since rc1. -1.0-rc1 (2011-11-22) --------------------- +## 1.0-rc1 (2011-11-22) - Added tests covering snippets from UFL chapter in FEniCS book - Added more unit tests @@ -288,27 +272,24 @@ Changelog - Fixed rtruediv bug - Fixed bug with derivatives of elements of type Real with unspecified cell -1.0-beta3 (2011-10-26) ----------------------- +## 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`` +- 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) ----------------------- +## 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) ---------------------- +## 1.0-beta (2011-07-08) - Add script ufl-version - Added syntax for associating an arbitrary domain data object with a measure: @@ -319,8 +300,7 @@ Changelog - 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) ------------------- +## 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 @@ -332,23 +312,20 @@ Changelog - Add subdomain member variables to form class - Allow action on forms of arbitrary rank -0.9.0 (2011-02-23) ------------------- +## 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) ------------------- +## 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) ------------------- +## 0.5.3 (2010-07-01) - Rename ElementRestriction --> RestrictedElement - Experimental import of x from tetrahedron @@ -357,46 +334,40 @@ Changelog - Rename ElementUnion -> EnrichedElement - Add support for tan() and inverse trigonometric functions -0.5.2 (2010-02-15) ------------------- +## 0.5.2 (2010-02-15) - Attach form data to preprocessed form, accessible by form.form_data() -0.5.1 (2010-02-03) ------------------- +## 0.5.1 (2010-02-03) - Fix bug in propagate_restriction -0.5.0 (2010-02-01) ------------------- +## 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) ------------------- +## 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) ------------------- +## 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 macro cell integral `*dE` - Added mechanism to add additional integral types - Added LiftingOperator and LiftingFunction - Added ElementRestriction -0.3.0 (2009-05-28) ------------------- +## 0.3.0 (2009-05-28) - Some critical bugfixes, in particular in differentiation. - Added form operators "system" and "sensitivity_rhs". @@ -408,12 +379,10 @@ Changelog for quadrature degree estimation. - Improved manual. -0.2.0 (2009-04-07) ------------------- +## 0.2.0 (2009-04-07) - Initial release of UFL. -0.1.0 (unreleased) ------------------- +## 0.1.0 (unreleased) - Unreleased development versions of UFL. diff --git a/README.md b/README.md new file mode 100644 index 000000000..ad6cb8c89 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# 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 . + diff --git a/README.rst b/README.rst deleted file mode 100644 index 15f1446de..000000000 --- a/README.rst +++ /dev/null @@ -1,42 +0,0 @@ -=========================== -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 - -.. image:: https://github.com/FEniCS/ufl/workflows/UFL%20CI/badge.svg - :target: https://github.com/FEniCS/ufl/workflows/UFL%20CI -.. image:: https://coveralls.io/repos/github/FEniCS/ufl/badge.svg?branch=master - :target: https://coveralls.io/github/FEniCS/ufl?branch=master - :alt: Coverage Status -.. image:: https://readthedocs.org/projects/fenics-ufl/badge/?version=latest - :target: https://fenics.readthedocs.io/projects/ufl/en/latest/?badge=latest - :alt: Documentation Status - -Documentation -============= - -Documentation can be viewed at https://fenics-ufl.readthedocs.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 . diff --git a/demo/Constant.py b/demo/Constant.py index aa663087d..09f0d515f 100644 --- a/demo/Constant.py +++ b/demo/Constant.py @@ -18,18 +18,35 @@ # Modified by Martin Sandve Alnes, 2009 # # Test form for scalar and vector constants. -from ufl import (Coefficient, Constant, FiniteElement, TestFunction, - TrialFunction, VectorConstant, dot, dx, grad, inner, triangle) +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) +element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(element) -f = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +f = Coefficient(space) -c = Constant(cell) -d = VectorConstant(cell) +c = Constant(domain) +d = VectorConstant(domain) a = c * dot(grad(v), grad(u)) * dx L = inner(d, grad(v)) * dx diff --git a/demo/ConvectionJacobi.py b/demo/ConvectionJacobi.py index a741d6ae6..f058ca233 100644 --- a/demo/ConvectionJacobi.py +++ b/demo/ConvectionJacobi.py @@ -2,13 +2,27 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, dot, - dx, grad, triangle) +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 = VectorElement("Lagrange", triangle, 1) +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(element) -v = TestFunction(element) -w = Coefficient(element) +u = TrialFunction(space) +v = TestFunction(space) +w = Coefficient(space) a = dot(dot(u, grad(w)) + dot(w, grad(u)), v) * dx diff --git a/demo/ConvectionJacobi2.py b/demo/ConvectionJacobi2.py index 4c0c1d9b5..bbe68b292 100644 --- a/demo/ConvectionJacobi2.py +++ b/demo/ConvectionJacobi2.py @@ -2,13 +2,17 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, dx, - i, j, triangle) +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 = VectorElement("Lagrange", triangle, 1) +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(element) -v = TestFunction(element) -w = Coefficient(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 diff --git a/demo/ConvectionVector.py b/demo/ConvectionVector.py index f9a881baa..59b5e233d 100644 --- a/demo/ConvectionVector.py +++ b/demo/ConvectionVector.py @@ -2,12 +2,16 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, TestFunction, VectorElement, dot, dx, grad, - triangle) +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 = VectorElement("Lagrange", triangle, 1) +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(element) -w = Coefficient(element) +v = TestFunction(space) +w = Coefficient(space) a = dot(dot(w, grad(w)), v) * dx diff --git a/demo/Elasticity.py b/demo/Elasticity.py index ab13ebca1..324b2cefd 100644 --- a/demo/Elasticity.py +++ b/demo/Elasticity.py @@ -3,13 +3,17 @@ # Modified by: Martin Sandve Alnes # Date: 2009-01-12 # -from ufl import (TestFunction, TrialFunction, VectorElement, dx, grad, inner, - tetrahedron) +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 = VectorElement("Lagrange", tetrahedron, 1) +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(element) -u = TrialFunction(element) +v = TestFunction(space) +u = TrialFunction(space) def epsilon(v): diff --git a/demo/EnergyNorm.py b/demo/EnergyNorm.py index 5b1ee3546..de2de347c 100644 --- a/demo/EnergyNorm.py +++ b/demo/EnergyNorm.py @@ -17,9 +17,14 @@ # # This example demonstrates how to define a functional, here # the energy norm (squared) for a reaction-diffusion problem. -from ufl import Coefficient, FiniteElement, dot, dx, grad, tetrahedron +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) +element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = Coefficient(element) +v = Coefficient(space) a = (v * v + dot(grad(v), grad(v))) * dx diff --git a/demo/Equation.py b/demo/Equation.py index 3d64b4d8d..f440f303d 100644 --- a/demo/Equation.py +++ b/demo/Equation.py @@ -34,16 +34,32 @@ # 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, FiniteElement, TestFunction, TrialFunction, dot, - dx, grad, lhs, rhs, triangle) +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) +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(element) -u = TrialFunction(element) -u0 = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +u0 = Coefficient(space) F = v * (u - u0) * dx + k * dot(grad(v), grad(0.5 * (u0 + u))) * dx diff --git a/demo/ExplicitConvection.py b/demo/ExplicitConvection.py index d41177bf5..e5086c1a9 100644 --- a/demo/ExplicitConvection.py +++ b/demo/ExplicitConvection.py @@ -2,13 +2,27 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, dot, - dx, grad, triangle) +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 = VectorElement("Lagrange", triangle, 1) +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(element) -v = TestFunction(element) -w = Coefficient(element) +u = TrialFunction(space) +v = TestFunction(space) +w = Coefficient(space) a = dot(dot(w, grad(u)), v) * dx diff --git a/demo/FEEC.py b/demo/FEEC.py deleted file mode 100644 index 1253cbd86..000000000 --- a/demo/FEEC.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 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 . - -""" -This demo illustrates the FEEC notation - - V = FiniteElement("P Lambda", cell, r, k) - V = FiniteElement("P- Lambda", cell, r, k) - -and their aliases. -""" -from ufl import (FiniteElement, TestFunction, TestFunctions, TrialFunction, - TrialFunctions, dx) -from ufl import exterior_derivative as d -from ufl import inner, interval, tetrahedron, triangle - -cells = [interval, triangle, tetrahedron] -r = 1 - -for cell in cells: - for family in ["P Lambda", "P- Lambda"]: - tdim = cell.topological_dimension() - for k in range(0, tdim + 1): - - # Testing exterior derivative - V = FiniteElement(family, cell, r, form_degree=k) - v = TestFunction(V) - u = TrialFunction(V) - - a = inner(d(u), d(v)) * dx - - # Testing mixed formulation of Hodge Laplace - if k > 0 and k < tdim + 1: - S = FiniteElement(family, cell, r, form_degree=k - 1) - W = S * V - (sigma, u) = TrialFunctions(W) - (tau, v) = TestFunctions(W) - - a = (inner(sigma, tau) - inner(d(tau), u) + - inner(d(sigma), v) + inner(d(u), d(v))) * dx diff --git a/demo/FunctionOperators.py b/demo/FunctionOperators.py index 60b1ae016..4aeb4d621 100644 --- a/demo/FunctionOperators.py +++ b/demo/FunctionOperators.py @@ -16,14 +16,33 @@ # along with UFL. If not, see . # # Test form for operators on Coefficients. -from ufl import (Coefficient, FiniteElement, Max, TestFunction, TrialFunction, - dot, dx, grad, sqrt, triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(element) -f = Coefficient(element) -g = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +f = Coefficient(space) +g = Coefficient(space) -a = sqrt(1 / Max(1 / f, -1 / f)) * sqrt(g) * dot(grad(v), grad(u)) * dx + v * u * sqrt(f * g) * g * dx +a = ( + sqrt(1 / max_value(1 / f, -1 / f)) * sqrt(g) * dot(grad(v), grad(u)) * dx + + v * u * sqrt(f * g) * g * dx +) diff --git a/demo/H1norm.py b/demo/H1norm.py index 0d16d8e8a..769607aff 100644 --- a/demo/H1norm.py +++ b/demo/H1norm.py @@ -2,10 +2,15 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import Coefficient, FiniteElement, dot, dx, grad, triangle +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -f = Coefficient(element) +f = Coefficient(space) a = (f * f + dot(grad(f), grad(f))) * dx diff --git a/demo/HarmonicMap.py b/demo/HarmonicMap.py index a9921253e..1d53906c9 100644 --- a/demo/HarmonicMap.py +++ b/demo/HarmonicMap.py @@ -3,15 +3,20 @@ # Author: Martin Alnes # Date: 2009-04-09 # -from ufl import (Coefficient, FiniteElement, VectorElement, - derivative, dot, dx, grad, inner, triangle) +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 = VectorElement("Lagrange", cell, 1) -Y = FiniteElement("Lagrange", cell, 1) +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) -y = Coefficient(Y) +x = Coefficient(X_space) +y = Coefficient(Y_space) L = inner(grad(x), grad(x)) * dx + dot(x, x) * y * dx diff --git a/demo/HarmonicMap2.py b/demo/HarmonicMap2.py index daf92e028..fc048868f 100644 --- a/demo/HarmonicMap2.py +++ b/demo/HarmonicMap2.py @@ -3,15 +3,19 @@ # Author: Martin Alnes # Date: 2009-04-09 # -from ufl import (Coefficient, FiniteElement, VectorElement, derivative, dot, - dx, grad, inner, split, triangle) +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 = VectorElement("Lagrange", cell, 1) -Y = FiniteElement("Lagrange", cell, 1) -M = X * Y +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(M) +u = Coefficient(space) x, y = split(u) L = inner(grad(x), grad(x)) * dx + dot(x, x) * y * dx diff --git a/demo/Heat.py b/demo/Heat.py index 46ddc7851..2a197b5d1 100644 --- a/demo/Heat.py +++ b/demo/Heat.py @@ -20,18 +20,33 @@ # 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, FiniteElement, TestFunction, - TrialFunction, dot, dx, grad, triangle) +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) +element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -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(cell) # Time step +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 diff --git a/demo/HornSchunck.py b/demo/HornSchunck.py index 13ec5e548..5238ed04e 100644 --- a/demo/HornSchunck.py +++ b/demo/HornSchunck.py @@ -3,29 +3,44 @@ # http://code.google.com/p/debiosee/wiki/DemosOptiocFlowHornSchunck # but not tested so this could contain errors! # -from ufl import (Coefficient, Constant, FiniteElement, VectorElement, - derivative, dot, dx, grad, inner, triangle) +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("CG", cell, 1) -V = VectorElement("CG", cell, 1) +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) +u = Coefficient(V_space) # Previous image brightness -I0 = Coefficient(S) +I0 = Coefficient(S_space) # Current image brightness -I1 = Coefficient(S) +I1 = Coefficient(S_space) # Regularization parameter -lamda = Constant(cell) +lamda = Constant(domain) # Coefficiental to minimize -M = (dot(u, grad(I1)) + (I1 - I0))**2 * dx\ - + lamda * inner(grad(u), grad(u)) * dx +M = (dot(u, grad(I1)) + (I1 - I0)) ** 2 * dx + lamda * inner(grad(u), grad(u)) * dx # Derived linear system L = derivative(M, u) diff --git a/demo/HyperElasticity.py b/demo/HyperElasticity.py index 81bd49a29..e4882d2b6 100644 --- a/demo/HyperElasticity.py +++ b/demo/HyperElasticity.py @@ -3,52 +3,82 @@ # 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 import (Coefficient, Constant, FacetNormal, FiniteElement, Identity, - SpatialCoordinate, TensorElement, TestFunction, TrialFunction, - VectorElement, derivative, det, diff, dot, ds, dx, exp, grad, - inner, inv, tetrahedron, tr, variable) +from ufl.pullback import identity_pullback +from ufl.sobolevspace import H1 # Cell and its properties cell = tetrahedron -d = cell.geometric_dimension() -N = FacetNormal(cell) -x = SpatialCoordinate(cell) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) +d = 3 +N = FacetNormal(domain) +x = SpatialCoordinate(domain) # Elements -u_element = VectorElement("CG", cell, 2) -p_element = FiniteElement("CG", cell, 1) -A_element = TensorElement("CG", cell, 1) +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_element) -w = TrialFunction(u_element) +v = TestFunction(u_space) +w = TrialFunction(u_space) # Displacement at current and two previous timesteps -u = Coefficient(u_element) -up = Coefficient(u_element) -upp = Coefficient(u_element) +u = Coefficient(u_space) +up = Coefficient(u_space) +upp = Coefficient(u_space) # Time parameters -dt = Constant(cell) +dt = Constant(domain) # Fiber field -A = Coefficient(A_element) +A = Coefficient(A_space) # External forces -T = Coefficient(u_element) -p0 = Coefficient(p_element) +T = Coefficient(u_space) +p0 = Coefficient(p_space) # Material parameters FIXME -rho = Constant(cell) -K = Constant(cell) -c00 = Constant(cell) -c11 = Constant(cell) -c22 = Constant(cell) +rho = Constant(domain) +K = Constant(domain) +c00 = Constant(domain) +c11 = Constant(domain) +c22 = Constant(domain) # Deformation gradient -I = Identity(d) -F = I + grad(u) +Id = Identity(d) +F = Id + grad(u) F = variable(F) Finv = inv(F) J = det(F) @@ -66,13 +96,15 @@ I3_C = J**2 # Green strain tensor -E = (C - I) / 2 +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 +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 @@ -80,13 +112,15 @@ # Acceleration term discretized with finite differences k = dt / rho -acc = (u - 2 * up + upp) +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) \ +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) diff --git a/demo/HyperElasticity1D.py b/demo/HyperElasticity1D.py index 0f0e2b376..f2cd1ab52 100644 --- a/demo/HyperElasticity1D.py +++ b/demo/HyperElasticity1D.py @@ -2,16 +2,21 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, Constant, FiniteElement, derivative, dx, exp, - interval, variable) +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("CG", cell, 2) -u = Coefficient(element) -b = Constant(cell) -K = Constant(cell) +element = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (1,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -E = u.dx(0) + u.dx(0)**2 / 2 +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) diff --git a/demo/L2norm.py b/demo/L2norm.py index 3f6c2491f..cd492f647 100644 --- a/demo/L2norm.py +++ b/demo/L2norm.py @@ -2,10 +2,15 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import Coefficient, FiniteElement, dx, triangle +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -f = Coefficient(element) +f = Coefficient(space) a = f**2 * dx diff --git a/demo/Mass.py b/demo/Mass.py index 1b0c24e5e..eb20c5afb 100644 --- a/demo/Mass.py +++ b/demo/Mass.py @@ -20,11 +20,16 @@ # Last changed: 2009-03-02 # # The bilinear form for a mass matrix. -from ufl import FiniteElement, TestFunction, TrialFunction, dx, triangle +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -u = TrialFunction(element) -v = TestFunction(element) +u = TrialFunction(space) +v = TestFunction(space) a = v * u * dx diff --git a/demo/MassAD.py b/demo/MassAD.py index 8cf8165ba..9f90b5eec 100644 --- a/demo/MassAD.py +++ b/demo/MassAD.py @@ -2,11 +2,16 @@ # Author: Martin Sandve Alnes # Date: 2008-10-28 # -from ufl import Coefficient, FiniteElement, derivative, dx, triangle +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -u = Coefficient(element) +u = Coefficient(space) # L2 norm M = u**2 / 2 * dx diff --git a/demo/MixedElasticity.py b/demo/MixedElasticity.py index 742329683..466935be0 100644 --- a/demo/MixedElasticity.py +++ b/demo/MixedElasticity.py @@ -17,8 +17,23 @@ # # First added: 2008-10-03 # Last changed: 2011-07-22 -from ufl import (MixedElement, TestFunctions, TrialFunctions, VectorElement, - as_vector, div, dot, dx, inner, skew, tetrahedron, tr) +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): @@ -32,20 +47,23 @@ def skw(tau): # Finite element exterior calculus syntax r = 1 -S = VectorElement("P Lambda", cell, r, form_degree=n - 1) -V = VectorElement("P Lambda", cell, r - 1, form_degree=n) -Q = VectorElement("P Lambda", cell, r - 1, form_degree=n) +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) -# Alternative syntax: -# S = VectorElement("BDM", cell, r) -# V = VectorElement("Discontinuous Lagrange", cell, r-1) -# Q = VectorElement("Discontinuous Lagrange", cell, r-1) +W = MixedElement([S, V, Q]) -W = MixedElement(S, V, Q) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) +space = FunctionSpace(domain, W) -(sigma, u, gamma) = TrialFunctions(W) -(tau, v, eta) = TestFunctions(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 +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 diff --git a/demo/MixedMixedElement.py b/demo/MixedMixedElement.py index ddffe3c97..0d1b1df20 100644 --- a/demo/MixedMixedElement.py +++ b/demo/MixedMixedElement.py @@ -16,8 +16,11 @@ # along with UFL. If not, see . # # A mixed element of mixed elements -from ufl import FiniteElement, triangle +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) +P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) -element = (P3 * P3) * (P3 * P3) +element = MixedElement([[P3, P3], [P3, P3]]) diff --git a/demo/MixedPoisson.py b/demo/MixedPoisson.py index 4769ab645..6e1c1730f 100644 --- a/demo/MixedPoisson.py +++ b/demo/MixedPoisson.py @@ -23,19 +23,34 @@ # a mixed formulation of Poisson's equation with BDM # (Brezzi-Douglas-Marini) elements. # -from ufl import (Coefficient, FiniteElement, TestFunctions, TrialFunctions, - div, dot, dx, triangle) +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) -DG0 = FiniteElement("Discontinuous Lagrange", cell, 0) +BDM1 = FiniteElement("Brezzi-Douglas-Marini", cell, 1, (2,), contravariant_piola, HDiv) +DG0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, H1) -element = BDM1 * DG0 +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(element) -(sigma, u) = TrialFunctions(element) +(tau, w) = TestFunctions(space) +(sigma, u) = TrialFunctions(space) -f = Coefficient(DG0) +f = Coefficient(dg0_space) a = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx L = w * f * dx diff --git a/demo/MixedPoisson2.py b/demo/MixedPoisson2.py index fa8024118..fc9deb84d 100644 --- a/demo/MixedPoisson2.py +++ b/demo/MixedPoisson2.py @@ -3,18 +3,33 @@ # Modified by: Martin Sandve Alnes # Date: 2009-02-12 # -from ufl import (FacetNormal, FiniteElement, TestFunctions, TrialFunctions, - div, dot, ds, dx, tetrahedron) +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) -DG = FiniteElement("DG", cell, 0) -MX = RT * DG +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(MX) -(v, q) = TestFunctions(MX) +(u, p) = TrialFunctions(space) +(v, q) = TestFunctions(space) -n = FacetNormal(cell) +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 diff --git a/demo/NavierStokes.py b/demo/NavierStokes.py index 77338c050..e1121d9c9 100644 --- a/demo/NavierStokes.py +++ b/demo/NavierStokes.py @@ -21,15 +21,29 @@ # # The bilinear form for the nonlinear term in the # Navier-Stokes equations with fixed convective velocity. -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, dot, - dx, grad, tetrahedron) +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 = VectorElement("Lagrange", cell, 1) +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(element) -u = TrialFunction(element) -w = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +w = Coefficient(space) Du = grad(u) a = dot(dot(w, Du), v) * dx diff --git a/demo/NeumannProblem.py b/demo/NeumannProblem.py index b6b6144a6..95f255933 100644 --- a/demo/NeumannProblem.py +++ b/demo/NeumannProblem.py @@ -17,15 +17,30 @@ # # The bilinear form a(v, u) and linear form L(v) for # Poisson's equation with Neumann boundary conditions. -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, ds, - dx, grad, inner, triangle) +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 = VectorElement("Lagrange", triangle, 1) +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(element) -u = TrialFunction(element) -f = Coefficient(element) -g = Coefficient(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 diff --git a/demo/NonlinearPoisson.py b/demo/NonlinearPoisson.py index 5c56bdac0..56ef5cb9a 100644 --- a/demo/NonlinearPoisson.py +++ b/demo/NonlinearPoisson.py @@ -1,13 +1,26 @@ -from ufl import (Coefficient, FiniteElement, TestFunction, TrialFunction, dot, - dx, grad, triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(element) -u0 = Coefficient(element) -f = Coefficient(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 +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 diff --git a/demo/P5tet.py b/demo/P5tet.py index 44dbb7fb5..5114b6724 100644 --- a/demo/P5tet.py +++ b/demo/P5tet.py @@ -16,6 +16,9 @@ # along with UFL. If not, see . # # A fifth degree Lagrange finite element on a tetrahedron -from ufl import FiniteElement, 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) +element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) diff --git a/demo/P5tri.py b/demo/P5tri.py index eb616e896..ebfd6ba6f 100644 --- a/demo/P5tri.py +++ b/demo/P5tri.py @@ -16,6 +16,9 @@ # along with UFL. If not, see . # # A fifth degree Lagrange finite element on a triangle -from ufl import FiniteElement, 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) +element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1) diff --git a/demo/Poisson.py b/demo/Poisson.py index 341c8f31a..bfae70f8b 100644 --- a/demo/Poisson.py +++ b/demo/Poisson.py @@ -21,14 +21,28 @@ # Last changed: 2009-03-02 # # The bilinear form a(v, u) and linear form L(v) for Poisson's equation. -from ufl import (Coefficient, FiniteElement, TestFunction, TrialFunction, dx, - grad, inner, triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -u = TrialFunction(element) -v = TestFunction(element) -f = Coefficient(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) diff --git a/demo/PoissonDG.py b/demo/PoissonDG.py index 1673ea2fa..bfe01c7a0 100644 --- a/demo/PoissonDG.py +++ b/demo/PoissonDG.py @@ -21,30 +21,53 @@ # 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, FiniteElement, - TestFunction, TrialFunction, avg, dot, dS, ds, dx, grad, - inner, jump, triangle) +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 -element = FiniteElement("Discontinuous Lagrange", triangle, 1) +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(element) -u = TrialFunction(element) -f = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +f = Coefficient(space) -n = FacetNormal(triangle) -h = Constant(triangle) +n = FacetNormal(domain) +h = Constant(domain) -gN = Coefficient(element) +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 \ +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 diff --git a/demo/PoissonSystem.py b/demo/PoissonSystem.py index f6613e2e2..9bcd06ad3 100644 --- a/demo/PoissonSystem.py +++ b/demo/PoissonSystem.py @@ -21,15 +21,30 @@ # # The bilinear form a(v, u) and linear form L(v) for # Poisson's equation in system form (vector-valued). -from ufl import (Coefficient, TestFunction, TrialFunction, VectorElement, dot, - dx, grad, inner, triangle) +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 = VectorElement("Lagrange", cell, 1) +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(element) -u = TrialFunction(element) -f = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +f = Coefficient(space) a = inner(grad(v), grad(u)) * dx L = dot(v, f) * dx diff --git a/demo/PowAD.py b/demo/PowAD.py index 25edd10e1..dde304d99 100644 --- a/demo/PowAD.py +++ b/demo/PowAD.py @@ -2,14 +2,27 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (Coefficient, FiniteElement, TestFunction, TrialFunction, - derivative, dx, triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(element) -w = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +w = Coefficient(space) L = w**5 * v * dx a = derivative(L, w) diff --git a/demo/ProjectionSystem.py b/demo/ProjectionSystem.py index e61c92957..5211abb84 100644 --- a/demo/ProjectionSystem.py +++ b/demo/ProjectionSystem.py @@ -1,10 +1,14 @@ -from ufl import (Coefficient, FiniteElement, TestFunction, TrialFunction, dx, - triangle) +from 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) -v = TestFunction(element) -u = TrialFunction(element) -f = Coefficient(element) +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 diff --git a/demo/QuadratureElement.py b/demo/QuadratureElement.py index 07c104109..5ca50134f 100644 --- a/demo/QuadratureElement.py +++ b/demo/QuadratureElement.py @@ -20,20 +20,41 @@ # # 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, FiniteElement, TestFunction, TrialFunction, - VectorElement, dot, dx, grad, i, triangle) +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) +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, quad_scheme="default") -sig = VectorElement("Quadrature", triangle, 1, quad_scheme="default") +QE = FiniteElement("Quadrature", triangle, 2, (), identity_pullback, H1) +sig = FiniteElement("Quadrature", triangle, 1, (2,), identity_pullback, H1) -v = TestFunction(element) -u = TrialFunction(element) -u0 = Coefficient(element) -C = Coefficient(QE) -sig0 = Coefficient(sig) -f = Coefficient(element) +qe_space = FunctionSpace(domain, QE) +sig_space = FunctionSpace(domain, sig) -a = v.dx(i) * C * u.dx(i) * dx(metadata={"quadrature_degree": 2}) + v.dx(i) * 2 * u0 * u * u0.dx(i) * dx +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}) diff --git a/demo/RestrictedElement.py b/demo/RestrictedElement.py deleted file mode 100644 index f63906991..000000000 --- a/demo/RestrictedElement.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (C) 2009 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 . -# -# Restriction of a finite element. -# The below syntax show how one can restrict a higher order Lagrange element -# to only take into account those DOFs that live on the facets. -from ufl import (FiniteElement, TestFunction, TrialFunction, avg, dS, ds, - triangle) - -# Restricted element -CG_R = FiniteElement("Lagrange", triangle, 4)["facet"] -u_r = TrialFunction(CG_R) -v_r = TestFunction(CG_R) -a = avg(v_r) * avg(u_r) * dS + v_r * u_r * ds diff --git a/demo/Stiffness.py b/demo/Stiffness.py index 19a055999..f96fa6e32 100644 --- a/demo/Stiffness.py +++ b/demo/Stiffness.py @@ -2,12 +2,16 @@ # Author: Martin Sandve Alnes # Date: 2008-10-03 # -from ufl import (FiniteElement, TestFunction, TrialFunction, dot, dx, grad, - triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -u = TrialFunction(element) -v = TestFunction(element) +u = TrialFunction(space) +v = TestFunction(space) a = dot(grad(u), grad(v)) * dx diff --git a/demo/StiffnessAD.py b/demo/StiffnessAD.py index aa9270055..c95b94684 100644 --- a/demo/StiffnessAD.py +++ b/demo/StiffnessAD.py @@ -2,12 +2,27 @@ # Author: Martin Sandve Alnes # Date: 2008-10-30 # -from ufl import (Coefficient, FiniteElement, action, adjoint, derivative, dx, - grad, inner, triangle) +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) +element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -w = Coefficient(element) +w = Coefficient(space) # H1 semi-norm f = inner(grad(w), grad(w)) / 2 * dx diff --git a/demo/Stokes.py b/demo/Stokes.py index 53c7bfce3..4a2c185ac 100644 --- a/demo/Stokes.py +++ b/demo/Stokes.py @@ -20,18 +20,35 @@ # # 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, FiniteElement, TestFunctions, TrialFunctions, - VectorElement, div, dot, dx, grad, inner, triangle) +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 = VectorElement("Lagrange", cell, 2) -P1 = FiniteElement("Lagrange", cell, 1) -TH = P2 * P1 +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(TH) -(u, p) = TrialFunctions(TH) +(v, q) = TestFunctions(space) +(u, p) = TrialFunctions(space) -f = Coefficient(P2) +f = Coefficient(p2_space) a = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx L = dot(v, f) * dx diff --git a/demo/StokesEquation.py b/demo/StokesEquation.py index 944b55e76..667ca729b 100644 --- a/demo/StokesEquation.py +++ b/demo/StokesEquation.py @@ -19,18 +19,37 @@ # 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, FiniteElement, TestFunctions, TrialFunctions, - VectorElement, div, dot, dx, grad, inner, lhs, rhs, triangle) +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 = VectorElement("Lagrange", cell, 2) -P1 = FiniteElement("Lagrange", cell, 1) -TH = P2 * P1 +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(TH) -(u, p) = TrialFunctions(TH) +(v, q) = TestFunctions(space) +(u, p) = TrialFunctions(space) -f = Coefficient(P2) +f = Coefficient(p2_space) F = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx - dot(v, f) * dx a = lhs(F) diff --git a/demo/SubDomain.py b/demo/SubDomain.py index aade63745..39dca0653 100644 --- a/demo/SubDomain.py +++ b/demo/SubDomain.py @@ -17,13 +17,17 @@ # # This example illustrates how to define a form over a # given subdomain of a mesh, in this case a functional. -from ufl import (Coefficient, FiniteElement, TestFunction, TrialFunction, ds, - dx, tetrahedron) +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("CG", tetrahedron, 1) +element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(element) -f = Coefficient(element) +v = TestFunction(space) +u = TrialFunction(space) +f = Coefficient(space) M = f * dx(2) + f * ds(5) diff --git a/demo/SubDomains.py b/demo/SubDomains.py index 27fe3f36a..f4d76da7a 100644 --- a/demo/SubDomains.py +++ b/demo/SubDomains.py @@ -17,13 +17,23 @@ # # This simple example illustrates how forms can be defined on different sub domains. # It is supported for all three integral types. -from ufl import (FiniteElement, TestFunction, TrialFunction, ds, dS, dx, - tetrahedron) +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("CG", tetrahedron, 1) +element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1) +domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3,), identity_pullback, H1)) +space = FunctionSpace(domain, element) -v = TestFunction(element) -u = TrialFunction(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 = ( + 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) +) diff --git a/demo/TensorWeightedPoisson.py b/demo/TensorWeightedPoisson.py index d7eb4386f..a46516dd4 100644 --- a/demo/TensorWeightedPoisson.py +++ b/demo/TensorWeightedPoisson.py @@ -17,14 +17,29 @@ # # The bilinear form a(v, u) and linear form L(v) for # tensor-weighted Poisson's equation. -from ufl import (Coefficient, FiniteElement, TensorElement, TestFunction, - TrialFunction, dx, grad, inner, triangle) +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) -P0 = TensorElement("Discontinuous Lagrange", triangle, 0, shape=(2, 2)) +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) -u = TrialFunction(P1) -C = Coefficient(P0) +v = TestFunction(p1_space) +u = TrialFunction(p1_space) +C = Coefficient(p0_space) a = inner(grad(v), C * grad(u)) * dx diff --git a/demo/VectorLaplaceGradCurl.py b/demo/VectorLaplaceGradCurl.py index fcc94eea4..ca08db9ac 100644 --- a/demo/VectorLaplaceGradCurl.py +++ b/demo/VectorLaplaceGradCurl.py @@ -18,17 +18,31 @@ # 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, FiniteElement, TestFunctions, TrialFunctions, - VectorElement, curl, dx, grad, inner, tetrahedron) - - -def HodgeLaplaceGradCurl(element, felement): - tau, v = TestFunctions(element) - sigma, u = TrialFunctions(element) - f = Coefficient(felement) - - a = (inner(tau, sigma) - inner(grad(tau), u) + - inner(v, grad(sigma)) + inner(curl(v), curl(u))) * dx +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 @@ -37,9 +51,13 @@ def HodgeLaplaceGradCurl(element, felement): cell = tetrahedron order = 1 -GRAD = FiniteElement("Lagrange", cell, order) -CURL = FiniteElement("N1curl", cell, order) +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) -VectorLagrange = VectorElement("Lagrange", cell, order + 1) +domain = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) +space = FunctionSpace(domain, MixedElement([GRAD, CURL])) +fspace = FunctionSpace(domain, VectorLagrange) -a, L = HodgeLaplaceGradCurl(GRAD * CURL, VectorLagrange) +a, L = HodgeLaplaceGradCurl(space, fspace) diff --git a/demo/_TensorProductElement.py b/demo/_TensorProductElement.py index 16d16572f..c32b1fd09 100644 --- a/demo/_TensorProductElement.py +++ b/demo/_TensorProductElement.py @@ -17,17 +17,35 @@ # # First added: 2012-08-16 # Last changed: 2012-08-16 -from ufl import (FiniteElement, TensorProductElement, TestFunction, - TrialFunction, dx, interval, tetrahedron, triangle) +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("CG", triangle, 1) -V1 = FiniteElement("DG", interval, 0) -V2 = FiniteElement("DG", tetrahedron, 0) +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) -u = TrialFunction(V) -v = TestFunction(V) +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 diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 500ba06ed..5ad9345db 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -12,53 +12,51 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os -import shlex -import pkg_resources 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('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# 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', + "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'] +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' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Unified Form Language (UFL)' +project = "Unified Form Language (UFL)" this_year = datetime.date.today().year -copyright = u'%s, FEniCS Project' % this_year -author = u'FEniCS Project' -version = pkg_resources.get_distribution("fenics-ufl").version +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 @@ -70,9 +68,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -80,27 +78,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# 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 +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -110,31 +108,31 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'alabaster' +# 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 = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# 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 +# 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 +# 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, @@ -144,109 +142,111 @@ # 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 = [] +# 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' +# 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 +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# 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' +# 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'} +# 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' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'UnifiedFormLanguageUFLdoc' +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', + # 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', u'Unified Form Language (UFL) Documentation', - u'FEniCS Project', 'manual'), + ( + 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 +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -254,12 +254,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'unifiedformlanguageufl', u'Unified Form Language (UFL) Documentation', - [author], 1) + (master_doc, "unifiedformlanguageufl", "Unified Form Language (UFL) Documentation", [author], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -268,19 +267,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'UnifiedFormLanguageUFL', u'Unified Form Language (UFL) Documentation', - author, 'UnifiedFormLanguageUFL', 'One line description of project.', - 'Miscellaneous'), + ( + 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 = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/doc/sphinx/source/installation.rst b/doc/sphinx/source/installation.rst index bef267c12..72da83577 100644 --- a/doc/sphinx/source/installation.rst +++ b/doc/sphinx/source/installation.rst @@ -30,8 +30,8 @@ Installation instructions ========================= To install UFL, download the source code from the -`UFL Bitbucket repository -`__, +`UFL GitHub repository +`__, and run the following command: .. code-block:: console diff --git a/doc/sphinx/source/manual/form_language.rst b/doc/sphinx/source/manual/form_language.rst index 3d20924de..1c9687893 100644 --- a/doc/sphinx/source/manual/form_language.rst +++ b/doc/sphinx/source/manual/form_language.rst @@ -764,11 +764,10 @@ Basic nonlinear functions Some basic nonlinear functions are also available, their meaning mostly obvious. -* ``abs(f)``: the absolute value of f. +The following functions are defined and should be imported from `ufl` -* ``sign(f)``: the sign of f (+1 or -1). -* ``pow(f, g)`` or ``f**g``: f to the power g, :math:`f^g` +* ``sign(f)``: the sign of f (+1 or -1). * ``sqrt(f)``: square root, :math:`\sqrt{f}` @@ -806,6 +805,14 @@ obvious. * ``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. diff --git a/pyproject.toml b/pyproject.toml index 9bf811c37..555a334d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,84 @@ [build-system] -requires = ["setuptools>=58", "wheel"] - +requires = ["setuptools>=62", "wheel"] build-backend = "setuptools.build_meta" + +[project] +name = "fenics-ufl" +version = "2024.3.0.dev0" +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" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 77beb85f6..000000000 --- a/setup.cfg +++ /dev/null @@ -1,74 +0,0 @@ -# Setuptools does not yet support modern pyproject.toml but will do so in the -# future -[metadata] -name = fenics-ufl -version = 2022.3.0.dev0 -author = FEniCS Project Contributors -email = fenics-dev@googlegroups.com -maintainer = FEniCS Project Steering Council -description = Unified Form Language -url = https://github.com/FEniCS/ufl -project_urls = - Homepage = https://fenicsproject.org - Documentation = https://fenics.readthedocs.io/projects/ufl - Issues = https://github.com/FEniCS/ufl/issues - Funding = https://numfocus.org/donate -long_description = file: README.rst -long_description_content_type = text/x-rst -license=LGPL-3.0-or-later -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Intended Audience :: Science/Research - License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) - Operating System :: POSIX - Operating System :: POSIX :: Linux - Operating System :: MacOS :: MacOS X - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Topic :: Scientific/Engineering :: Mathematics - Topic :: Software Development :: Libraries :: Python Modules - -[options] -packages = find: -include_package_data = True -zip_safe = False -python_requires = >= 3.7 -setup_requires = - setuptools >= 58 - wheel -install_requires = - numpy - -[options.extras_require] -docs = sphinx; sphinx_rtd_theme -lint = flake8; pydocstyle[toml] -test = pytest -ci = - coverage - coveralls - pytest-cov - pytest-xdist - fenics-ufl[docs] - fenics-ufl[lint] - fenics-ufl[test] - -[flake8] -ignore = E501, W504, - E741 # ambiguous variable name -builtins = ufl -exclude = .git,__pycache__,doc/sphinx/source/conf.py,build,dist,test - -[pydocstyle] -# Work on removing these ignores -ignore = D100,D101,D102,D103,D104,D105,D107, - D200,D202, - D203, # this error should be disabled - D204,D205,D208,D209,D210,D212,D213, - D300,D301, - D400,D401,D402,D404,D415,D416 - E741 # Variable names l, O, I, ... diff --git a/setup.py b/setup.py deleted file mode 100755 index daa285d2c..000000000 --- a/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -import setuptools - -try: - import pip - - from packaging import version - if version.parse(pip.__version__) < version.parse("21.3"): - # Issue with older version of pip https://github.com/pypa/pip/issues/7953 - import site - import sys - site.ENABLE_USER_SITE = "--user" in sys.argv[1:] - -except ImportError: - pass - -setuptools.setup() diff --git a/test/conftest.py b/test/conftest.py index 2b2a2fb8e..5c8030c74 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,16 +1,13 @@ -# -*- coding: utf-8 -*- +import os import pytest -import os - import ufl -from ufl import as_ufl, inner, dx +from ufl import as_ufl, dx, inner from ufl.algorithms import compute_form_data class Tester: - def assertTrue(self, a): assert a diff --git a/test/mockobjects.py b/test/mockobjects.py index 6af48f43b..caf8a88a8 100644 --- a/test/mockobjects.py +++ b/test/mockobjects.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - -from ufl import * +from ufl import Measure, Mesh, triangle class MockMesh: - def __init__(self, ufl_id): self._ufl_id = ufl_id @@ -14,8 +11,16 @@ def ufl_id(self): 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) + 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: @@ -33,5 +38,9 @@ def mesh(self): 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) + integral_type, + subdomain_id=subdomain_id, + metadata=metadata, + domain=self.mesh(), + subdomain_data=self, + ) diff --git a/test/test_algorithms.py b/test/test_algorithms.py index b5484def3..f8543249a 100755 --- a/test/test_algorithms.py +++ b/test/test_algorithms.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2008-03-12 -- 2009-01-28" @@ -7,48 +5,82 @@ # Modified by Garth N. Wells, 2009 import pytest -from pprint import * -from ufl import (FiniteElement, TestFunction, TrialFunction, triangle, - div, grad, Argument, dx, adjoint, Coefficient, - FacetNormal, inner, dot, ds) -from ufl.algorithms import (extract_arguments, expand_derivatives, - expand_indices, extract_elements, - extract_unique_elements, extract_coefficients) -from ufl.corealg.traversal import (pre_traversal, post_traversal, - unique_pre_traversal, unique_post_traversal) +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') +@pytest.fixture(scope="module") def element(): - return FiniteElement("CG", triangle, 1) + 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(element): - v = TestFunction(element) - u = TrialFunction(element) + +@pytest.fixture(scope="module") +def arguments(space): + v = TestFunction(space) + u = TrialFunction(space) return (v, u) -@pytest.fixture(scope='module') -def coefficients(element): - c = Coefficient(element) - f = Coefficient(element) +@pytest.fixture(scope="module") +def coefficients(space): + c = Coefficient(space) + f = Coefficient(space) return (c, f) @pytest.fixture -def forms(arguments, coefficients): +def forms(arguments, coefficients, domain): v, u = arguments c, f = coefficients - n = FacetNormal(triangle) + 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 + 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) @@ -61,27 +93,29 @@ def test_extract_coefficients_vs_fixture(coefficients, forms): assert coefficients == tuple(extract_coefficients(forms[2])) -def test_extract_elements_and_extract_unique_elements(forms): +def test_extract_elements_and_extract_unique_elements(forms, element, domain): b = forms[2] integrals = b.integrals_by_type("cell") - integrand = integrals[0].integrand() + integrals[0].integrand() + + element1 = element + element2 = element - element1 = FiniteElement("CG", triangle, 1) - element2 = FiniteElement("CG", triangle, 1) + space1 = FunctionSpace(domain, element1) + space2 = FunctionSpace(domain, element2) - v = TestFunction(element1) - u = TrialFunction(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(): - element = FiniteElement("CG", "triangle", 1) - v = TestFunction(element) - f = Coefficient(element) - g = Coefficient(element) +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 @@ -95,13 +129,16 @@ def test_pre_and_post_traversal(): assert list(unique_post_traversal(s)) == [v, f, p1, g, p2, s] -def test_expand_indices(): - element = FiniteElement("Lagrange", triangle, 2) - v = TestFunction(element) - u = TrialFunction(element) +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? + 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) @@ -113,32 +150,35 @@ def evaluate(form): # TODO: Test something more -def test_adjoint(): +def test_adjoint(domain): cell = triangle - V1 = FiniteElement("CG", cell, 1) - V2 = FiniteElement("CG", cell, 2) + 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(V1) - v = TestFunction(V2) + u = TrialFunction(s1) + v = TestFunction(s2) assert u.number() > v.number() - u2 = Argument(V1, 2) - v2 = Argument(V2, 3) + u2 = Argument(s1, 2) + v2 = Argument(s2, 3) assert u2.number() < v2.number() a = u * v * dx - a_arg_degrees = [arg.ufl_element().degree() for arg in extract_arguments(a)] + 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().degree() for arg in extract_arguments(b)] + 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().degree() for arg in extract_arguments(c)] + 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().degree() for arg in extract_arguments(d)] + d_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(d)] assert d_arg_degrees == [2, 1] diff --git a/test/test_analyse_demos.py b/test/test_analyse_demos.py index 444aa926c..a22980136 100755 --- a/test/test_analyse_demos.py +++ b/test/test_analyse_demos.py @@ -1,22 +1,20 @@ -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2008-09-28 -- 2008-09-28" import os -import pytest -from ufl.algorithms import load_ufl_file, compute_form_data, validate_form 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"))) - ) + set(glob(os.path.join(demodir, "*.py"))) - set(glob(os.path.join(demodir, "_*.py"))) + ) return filenames diff --git a/test/test_apply_algebra_lowering.py b/test/test_apply_algebra_lowering.py index a9a097b15..c0bdceb46 100755 --- a/test/test_apply_algebra_lowering.py +++ b/test/test_apply_algebra_lowering.py @@ -1,44 +1,81 @@ -# -*- coding: utf-8 -*- - import pytest -from ufl import * -from ufl.compound_expressions import * + +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(FiniteElement("CG", interval, 1)) + 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(TensorElement("CG", interval, 1)) + 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(TensorElement("CG", triangle, 1)) + 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(TensorElement("CG", tetrahedron, 1)) + 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(TensorElement("CG", triangle, 1, shape=(2, 1))) + 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(TensorElement("CG", triangle, 1, shape=(3, 1))) + 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(TensorElement("CG", triangle, 1, shape=(3, 2))) + 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): @@ -50,61 +87,65 @@ def test_determinant1(A1): def test_determinant2(A2): - assert determinant_expr(A2) == A2[0, 0]*A2[1, 1] - A2[0, 1]*A2[1, 0] + 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[0, 1]*(A3[1, 2]*A3[2, 0] - A3[1, 0]*A3[2, 2]) - + A3[0, 2]*(A3[1, 0]*A3[2, 1] - A3[1, 1]*A3[2, 0])) + 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])) + 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]))) + 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])) + assert renumber_indices(determinant_expr(A32)) == renumber_indices(sqrt(c[i] * c[i])) def test_inverse0(A0): - expected = 1.0/A0 # stays scalar + 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 + 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 + expected = "TODO" assert inverse_expr(A2) == renumber_indices(expected) def xtest_inverse3(A3): - expected = todo + expected = "TODO" assert inverse_expr(A3) == renumber_indices(expected) def xtest_pseudo_inverse21(A21): - expected = todo + expected = "TODO" assert renumber_indices(inverse_expr(A21)) == renumber_indices(expected) def xtest_pseudo_inverse31(A31): - expected = todo + expected = "TODO" assert renumber_indices(inverse_expr(A31)) == renumber_indices(expected) def xtest_pseudo_inverse32(A32): - expected = todo + expected = "TODO" assert renumber_indices(inverse_expr(A32)) == renumber_indices(expected) diff --git a/test/test_apply_function_pullbacks.py b/test/test_apply_function_pullbacks.py index f0a00dd35..a3c009a77 100755 --- a/test/test_apply_function_pullbacks.py +++ b/test/test_apply_function_pullbacks.py @@ -1,18 +1,25 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- +import numpy as np -import numpy -from ufl import * -from ufl.algorithms.apply_function_pullbacks import apply_single_function_pullbacks +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, JacobianInverse, JacobianDeterminant, ReferenceValue, CellOrientation +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 = apply_single_function_pullbacks(ReferenceValue(g), g.ufl_element()) + actual = g.ufl_element().pullback.apply(ReferenceValue(g)) assert expected.ufl_shape == actual.ufl_shape - for idx in numpy.ndindex(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: @@ -25,61 +32,75 @@ def check_single_function_pullback(g, mappings): print("actual:") print(str(ract)) print("signatures:") - print((expected**2*dx).signature()) - print((actual**2*dx).signature()) + print((expected**2 * dx).signature()) + print((actual**2 * dx).signature()) print() assert ract == rexp def test_apply_single_function_pullbacks_triangle3d(): - triangle3d = Cell("triangle", geometric_dimension=3) - cell = triangle3d - domain = as_domain(cell) - - UL2 = FiniteElement("DG L2", cell, 1) - U0 = FiniteElement("DG", cell, 0) - U = FiniteElement("CG", cell, 1) - V = VectorElement("CG", cell, 1) - Vd = FiniteElement("RT", cell, 1) - Vc = FiniteElement("N1curl", cell, 1) - T = TensorElement("CG", cell, 1) - S = TensorElement("CG", cell, 1, symmetry=True) - COV2T = FiniteElement("Regge", cell, 0) # (0, 2)-symmetric tensors - CONTRA2T = FiniteElement("HHJ", cell, 0) # (2, 0)-symmetric tensors - - Uml2 = UL2*UL2 - Um = U*U - Vm = U*V - Vdm = V*Vd - Vcm = Vd*Vc - Tm = Vc*T - Sm = T*S - - Vd0 = Vd*U0 # case from failing ffc demo - - W = S*T*Vc*Vd*V*U - - ul2 = Coefficient(UL2) - u = Coefficient(U) - v = Coefficient(V) - vd = Coefficient(Vd) - vc = Coefficient(Vc) - t = Coefficient(T) - s = Coefficient(S) - cov2t = Coefficient(COV2T) - contra2t = Coefficient(CONTRA2T) - - uml2 = Coefficient(Uml2) - um = Coefficient(Um) - vm = Coefficient(Vm) - vdm = Coefficient(Vdm) - vcm = Coefficient(Vcm) - tm = Coefficient(Tm) - sm = Coefficient(Sm) - - vd0m = Coefficient(Vd0) # case from failing ffc demo - - w = Coefficient(W) + 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) @@ -115,10 +136,10 @@ def test_apply_single_function_pullbacks_triangle3d(): detJ = JacobianDeterminant(domain) Jinv = JacobianInverse(domain) # o = CellOrientation(domain) - i, j, k, l = indices(4) + i, j, k, l = indices(4) # noqa: E741 # Contravariant H(div) Piola mapping: - M_hdiv = ((1.0/detJ) * J) # Not applying cell orientation here + M_hdiv = (1.0 / detJ) * J # Not applying cell orientation here # Covariant H(curl) Piola mapping: Jinv.T mappings = { @@ -126,85 +147,136 @@ def test_apply_single_function_pullbacks_triangle3d(): 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), + 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]]]), + 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)), + 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], - ]), + 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] - ]), + 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], - ]), + 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 @@ -233,43 +305,46 @@ def test_apply_single_function_pullbacks_triangle3d(): def test_apply_single_function_pullbacks_triangle(): cell = triangle - domain = as_domain(cell) - - Ul2 = FiniteElement("DG L2", cell, 1) - U = FiniteElement("CG", cell, 1) - V = VectorElement("CG", cell, 1) - Vd = FiniteElement("RT", cell, 1) - Vc = FiniteElement("N1curl", cell, 1) - T = TensorElement("CG", cell, 1) - S = TensorElement("CG", cell, 1, symmetry=True) - - Uml2 = Ul2*Ul2 - Um = U*U - Vm = U*V - Vdm = V*Vd - Vcm = Vd*Vc - Tm = Vc*T - Sm = T*S - - W = S*T*Vc*Vd*V*U - - ul2 = Coefficient(Ul2) - u = Coefficient(U) - v = Coefficient(V) - vd = Coefficient(Vd) - vc = Coefficient(Vc) - t = Coefficient(T) - s = Coefficient(S) - - uml2 = Coefficient(Uml2) - um = Coefficient(Um) - vm = Coefficient(Vm) - vdm = Coefficient(Vdm) - vcm = Coefficient(Vcm) - tm = Coefficient(Tm) - sm = Coefficient(Sm) - - w = Coefficient(W) + 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) @@ -298,10 +373,10 @@ def test_apply_single_function_pullbacks_triangle(): J = Jacobian(domain) detJ = JacobianDeterminant(domain) Jinv = JacobianInverse(domain) - i, j, k, l = indices(4) + i, j, k, l = indices(4) # noqa: E741 # Contravariant H(div) Piola mapping: - M_hdiv = (1.0/detJ) * J + M_hdiv = (1.0 / detJ) * J # Covariant H(curl) Piola mapping: Jinv.T mappings = { @@ -309,66 +384,95 @@ def test_apply_single_function_pullbacks_triangle(): 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), + 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], - ]), + 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], - ]), + 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 diff --git a/test/test_apply_restrictions.py b/test/test_apply_restrictions.py index c9775e10d..61b370e00 100755 --- a/test/test_apply_restrictions.py +++ b/test/test_apply_restrictions.py @@ -1,46 +1,66 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - from pytest import raises -from ufl import * -from ufl.algorithms.apply_restrictions import apply_restrictions, apply_default_restrictions + +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("DG", cell, 0) - V1 = FiniteElement("Lagrange", cell, 1) - V2 = FiniteElement("Lagrange", cell, 2) - f0 = Coefficient(V0) - f = Coefficient(V1) - g = Coefficient(V2) - n = FacetNormal(cell) - x = SpatialCoordinate(cell) - - assert raises(UFLException, lambda: apply_restrictions(f0)) - assert raises(UFLException, lambda: apply_restrictions(grad(f))) - assert raises(UFLException, lambda: apply_restrictions(n)) + 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('+') + 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('+') + 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)('-')) + 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('+') + 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('+') + 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('+') diff --git a/test/test_arithmetic.py b/test/test_arithmetic.py index cf26ff85c..554eca967 100755 --- a/test/test_arithmetic.py +++ b/test/test_arithmetic.py @@ -1,10 +1,21 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -import pytest - -from ufl import * -from ufl.classes import Division, FloatValue, IntValue, ComplexValue +from 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): @@ -20,12 +31,14 @@ def test_scalar_casting(self): def test_ufl_float_division(self): - d = SpatialCoordinate(triangle)[0] / 10.0 # TODO: Use mock instead of x + 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): - d = 3.14 / SpatialCoordinate(triangle)[0] # TODO: Use mock instead of x + 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) @@ -67,31 +80,36 @@ def test_elem_mult(self): 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(triangle) + 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)))) + self.assertEqual(elem_mult(A, B), as_matrix(((4 * x, 5 * y), (3 * y, 4 * x)))) - x, y = SpatialCoordinate(triangle) + 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): - x, y, z = SpatialCoordinate(tetrahedron) + 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)))) + 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): - x, y, z = SpatialCoordinate(tetrahedron) + 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), 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)) diff --git a/test/test_automatic_differentiation.py b/test/test_automatic_differentiation.py index c91655655..45549ae6b 100755 --- a/test/test_automatic_differentiation.py +++ b/test/test_automatic_differentiation.py @@ -1,69 +1,138 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- +"""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 itertools import chain - -import ufl - -# This imports everything external code will see from ufl -from ufl import * -import ufl.algorithms -from ufl.corealg.traversal import unique_post_traversal -from ufl.conditional import Conditional +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.geometric_dimension() - x = SpatialCoordinate(cell) - n = FacetNormal(cell) - c = CellVolume(cell) - R = Circumradius(cell) - h = CellDiameter(cell) - f = FacetArea(cell) - # s = CellSurfaceArea(cell) - mince = MinCellEdgeLength(cell) - maxce = MaxCellEdgeLength(cell) - minfe = MinFacetEdgeLength(cell) - maxfe = MaxFacetEdgeLength(cell) - J = Jacobian(cell) - detJ = JacobianDeterminant(cell) - invJ = JacobianInverse(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! - I = Identity(d) + ident = Identity(d) eps = PermutationSymbol(d) - U = FiniteElement("U", cell, None) - V = VectorElement("U", cell, None) - W = TensorElement("U", cell, None) + 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 = Coefficient(U) - v = Coefficient(V) - w = Coefficient(W) - du = Argument(U, 0) - dv = Argument(V, 1) - dw = Argument(W, 2) + 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, I, eps])) + 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] @@ -72,51 +141,84 @@ class ObjectCollection(object): 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), - ]) + 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))) + 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))) + 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) - 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 = ([ + 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), @@ -130,16 +232,16 @@ class ObjectCollection(object): 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('-')] + ] + 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.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 @@ -152,7 +254,7 @@ class ObjectCollection(object): if d == 1: self.tensorproducts = [] else: - self.tensorproducts = ([ + self.tensorproducts = [ dot(v, v), dot(v, w), dot(w, w), @@ -162,26 +264,32 @@ class ObjectCollection(object): 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), - ]) + 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 = ([ + self.crossproducts = [ cross(v, v), - cross(v, 2*v), - cross(v, w[0,:]), + cross(v, 2 * v), + cross(v, w[0, :]), cross(v, w[:, 1]), cross(w[:, 0], v), - ]) + ] self.compounds = [] self.compounds += self.tensorproducts @@ -210,30 +318,40 @@ def ad_algorithm(expr): if alt == 0: return expand_derivatives(expr) elif alt == 1: - return expand_derivatives(expr, + return expand_derivatives( + expr, apply_expand_compounds_before=True, apply_expand_compounds_after=False, - use_alternative_wrapper_algorithm=True) + use_alternative_wrapper_algorithm=True, + ) elif alt == 2: - return expand_derivatives(expr, + return expand_derivatives( + expr, apply_expand_compounds_before=False, apply_expand_compounds_after=True, - use_alternative_wrapper_algorithm=False) + use_alternative_wrapper_algorithm=False, + ) elif alt == 3: - return expand_derivatives(expr, + return expand_derivatives( + expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, - use_alternative_wrapper_algorithm=False) + use_alternative_wrapper_algorithm=False, + ) elif alt == 4: - return expand_derivatives(expr, + return expand_derivatives( + expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, - use_alternative_wrapper_algorithm=True) + use_alternative_wrapper_algorithm=True, + ) elif alt == 5: - return expand_derivatives(expr, + return expand_derivatives( + expr, apply_expand_compounds_before=False, apply_expand_compounds_after=False, - use_alternative_wrapper_algorithm=False) + use_alternative_wrapper_algorithm=False, + ) def _test_no_derivatives_no_change(self, collection): @@ -253,7 +371,7 @@ def _test_no_derivatives_but_still_changed(self, collection): # 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 + self.assertNotEqual(before, after) # With expand_compounds def test_only_terminals_no_change(self, d_expr): @@ -266,7 +384,9 @@ def test_no_derivatives_no_change(self, 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 +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) @@ -277,23 +397,23 @@ def test_zero_derivatives_of_terminals_produce_the_right_types_and_shapes(self, def _test_zero_derivatives_of_terminals_produce_the_right_types_and_shapes(self, collection): - c = Constant(collection.shared_objects.cell) + c = Constant(collection.shared_objects.domain) - u = Coefficient(collection.shared_objects.U) - v = Coefficient(collection.shared_objects.V) - w = Coefficient(collection.shared_objects.W) + 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 + before = derivative(t, var) # This will often get preliminary simplified to zero after = ad_algorithm(before) - expected = 0*t + 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 + before = derivative(c * t, var) # This will usually not get simplified to zero after = ad_algorithm(before) - expected = 0*t + expected = 0 * t # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected @@ -304,26 +424,26 @@ def test_zero_diffs_of_terminals_produce_the_right_types_and_shapes(self, d_expr def _test_zero_diffs_of_terminals_produce_the_right_types_and_shapes(self, collection): - c = Constant(collection.shared_objects.cell) + c = Constant(collection.shared_objects.domain) - u = Coefficient(collection.shared_objects.U) - v = Coefficient(collection.shared_objects.V) - w = Coefficient(collection.shared_objects.W) + 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 + before = diff(t, var) # This will often get preliminary simplified to zero after = ad_algorithm(before) - expected = 0*outer(t, var) + 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 + before = diff(c * t, var) # This will usually not get simplified to zero after = ad_algorithm(before) - expected = 0*outer(t, var) + expected = 0 * outer(t, var) # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected @@ -336,31 +456,31 @@ def test_zero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(sel def _test_zero_derivatives_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 - u = Coefficient(collection.shared_objects.U) - v = Coefficient(collection.shared_objects.V) - w = Coefficient(collection.shared_objects.W) + 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') + print("\n", "shapes: ", t.ufl_shape, var.ufl_shape, "\n") if debug: - print('\n', 't: ', str(t), '\n') + print("\n", "t: ", str(t), "\n") if debug: - print('\n', 't ind: ', str(t.ufl_free_indices), '\n') + print("\n", "t ind: ", str(t.ufl_free_indices), "\n") if debug: - print('\n', 'var: ', str(var), '\n') + print("\n", "var: ", str(var), "\n") before = derivative(t, var) if debug: - print('\n', 'before: ', str(before), '\n') + print("\n", "before: ", str(before), "\n") after = ad_algorithm(before) if debug: - print('\n', 'after: ', str(after), '\n') - expected = 0*t + print("\n", "after: ", str(after), "\n") + expected = 0 * t if debug: - print('\n', 'expected: ', str(expected), '\n') + print("\n", "expected: ", str(expected), "\n") assert after == expected @@ -372,9 +492,9 @@ def test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, d_e def _test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, collection): debug = 0 - u = Coefficient(collection.shared_objects.U) - v = Coefficient(collection.shared_objects.V) - w = Coefficient(collection.shared_objects.W) + 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) @@ -385,13 +505,13 @@ def _test_zero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, co for var in (vu, vv, vw): before = diff(t, var) if debug: - print('\n', 'before: ', str(before), '\n') + print("\n", "before: ", str(before), "\n") after = ad_algorithm(before) if debug: - print('\n', 'after: ', str(after), '\n') - expected = 0*outer(t, var) + print("\n", "after: ", str(after), "\n") + expected = 0 * outer(t, var) if debug: - print('\n', 'expected: ', str(expected), '\n') + print("\n", "expected: ", str(expected), "\n") # print '\n', str(expected), '\n', str(after), '\n', str(before), '\n' assert after == expected @@ -418,16 +538,16 @@ def _test_nonzero_derivatives_of_noncompounds_produce_the_right_types_and_shapes continue if debug: - print(('\n', '...: ', t.ufl_shape, var.ufl_shape, '\n')) + print(("\n", "...: ", t.ufl_shape, var.ufl_shape, "\n")) before = derivative(t, var) if debug: - print(('\n', 'before: ', str(before), '\n')) + print(("\n", "before: ", str(before), "\n")) after = ad_algorithm(before) if debug: - print(('\n', 'after: ', str(after), '\n')) - expected_shape = 0*t + print(("\n", "after: ", str(after), "\n")) + expected_shape = 0 * t if debug: - print(('\n', 'expected_shape: ', str(expected_shape), '\n')) + 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): @@ -454,7 +574,7 @@ def _test_nonzero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, # for t in chain(collection.noncompounds, collection.compounds): for t in collection.noncompounds: - t = replace(t, {u:vu, v:vv, w:vw}) + 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])): @@ -464,13 +584,13 @@ def _test_nonzero_diffs_of_noncompounds_produce_the_right_types_and_shapes(self, before = diff(t, var) if debug: - print(('\n', 'before: ', str(before), '\n')) + 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 + 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", "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): @@ -491,11 +611,11 @@ def test_grad_coeff(self, d_expr): 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')) + 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 + if f is u: # Differing by being wrapped in indexing types assert before == after before = grad(grad(f)) @@ -532,8 +652,8 @@ def test_derivative_grad_coeff(self, d_expr): # assert after == expected if 0: print() - print(('B', f, "::", before)) - print(('A', f, "::", after)) + print(("B", f, "::", before)) + print(("A", f, "::", after)) def xtest_derivative_grad_coeff_with_variation_components(self, d_expr): @@ -545,7 +665,7 @@ def xtest_derivative_grad_coeff_with_variation_components(self, d_expr): dw = collection.shared_objects.dw for g, dg in ((v, dv), (w, dw)): # Pick a single component - ii = (0,)*(len(g.ufl_shape)) + ii = (0,) * (len(g.ufl_shape)) f = g[ii] df = dg[ii] @@ -565,5 +685,5 @@ def xtest_derivative_grad_coeff_with_variation_components(self, d_expr): # assert after == expected if 0: print() - print(('B', f, "::", before)) - print(('A', f, "::", after)) + print(("B", f, "::", before)) + print(("A", f, "::", after)) diff --git a/test/test_book_snippets.py b/test/test_book_snippets.py deleted file mode 100755 index a740014dc..000000000 --- a/test/test_book_snippets.py +++ /dev/null @@ -1,584 +0,0 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -""" -This file contains snippets from the FEniCS book, -and allows us to test that these can still run -with future versions of UFL. Please don't change -these and please do keep UFL compatible with these -snippets as long as possible. -""" - -import pytest - -from ufl import * -from ufl.algorithms import * - - -def test_uflcode_269(self): - # Finite element spaces - cell = tetrahedron - element = VectorElement("Lagrange", cell, 1) - - # Form arguments - phi0 = TestFunction(element) - phi1 = TrialFunction(element) - u = Coefficient(element) - c1 = Constant(cell) - c2 = Constant(cell) - - # Deformation gradient Fij = dXi/dxj - I = Identity(cell.geometric_dimension()) - F = I + grad(u) - - # Right Cauchy-Green strain tensor C with invariants - C = variable(F.T*F) - I_C = tr(C) - II_C = (I_C**2 - tr(C*C))/2 - - # Mooney-Rivlin constitutive law - W = c1*(I_C-3) + c2*(II_C-3) - - # Second Piola-Kirchoff stress tensor - S = 2*diff(W, C) - - # Weak forms - L = inner(F*S, grad(phi0))*dx - a = derivative(L, u, phi1) - - -def test_uflcode_316(self): - shapestring = 'triangle' - cell = Cell(shapestring) - - -def test_uflcode_323(self): - cell = tetrahedron - - -def test_uflcode_356(self): - cell = tetrahedron - - P = FiniteElement("Lagrange", cell, 1) - V = VectorElement("Lagrange", cell, 2) - T = TensorElement("DG", cell, 0, symmetry=True) - - TH = V*P - ME = MixedElement(T, V, P) - - -def test_uflcode_400(self): - V = FiniteElement("CG", triangle, 1) - f = Coefficient(V) - g = Coefficient(V) - h = Coefficient(V) - w = Coefficient(V) - v = TestFunction(V) - u = TrialFunction(V) - # ... - a = w*dot(grad(u), grad(v))*dx - L = f*v*dx + g**2*v*ds(0) + h*v*ds(1) - - -def test_uflcode_469(self): - V = FiniteElement("CG", triangle, 1) - f = Coefficient(V) - g = Coefficient(V) - h = Coefficient(V) - v = TestFunction(V) - # ... - dx02 = dx(0, {"integration_order": 2}) - dx14 = dx(1, {"integration_order": 4}) - dx12 = dx(1, {"integration_order": 2}) - L = f*v*dx02 + g*v*dx14 + h*v*dx12 - - -def test_uflcode_552(self): - element = FiniteElement("CG", triangle, 1) - # ... - phi = Argument(element, 2) - v = TestFunction(element) - u = TrialFunction(element) - - -def test_uflcode_563(self): - cell = triangle - element = FiniteElement("CG", cell, 1) - # ... - w = Coefficient(element) - c = Constant(cell) - v = VectorConstant(cell) - M = TensorConstant(cell) - - -def test_uflcode_574(self): - V0 = FiniteElement("CG", triangle, 1) - V1 = V0 - # ... - V = V0*V1 - u = Coefficient(V) - u0, u1 = split(u) - - -def test_uflcode_582(self): - V0 = FiniteElement("CG", triangle, 1) - V1 = V0 - # ... - V = V0*V1 - u = Coefficient(V) - u0, u1 = split(u) - # ... - v0, v1 = TestFunctions(V) - u0, u1 = TrialFunctions(V) - f0, f1 = Coefficients(V) - - -def test_uflcode_644(self): - V = VectorElement("CG", triangle, 1) - u = Coefficient(V) - v = Coefficient(V) - # ... - A = outer(u, v) - Aij = A[i, j] - - -def test_uflcode_651(self): - V = VectorElement("CG", triangle, 1) - u = Coefficient(V) - v = Coefficient(V) - # ... - Aij = v[j]*u[i] - A = as_tensor(Aij, (i, j)) - - -def test_uflcode_671(self): - i = Index() - j, k, l = indices(3) - - -def test_uflcode_684(self): - V = VectorElement("CG", triangle, 1) - v = Coefficient(V) - # ... - th = pi/2 - A = as_matrix([[cos(th), -sin(th)], - [sin(th), cos(th)]]) - u = A*v - - -def test_uflcode_824(self): - V = VectorElement("CG", triangle, 1) - f = Coefficient(V) - # ... - df = Dx(f, i) - df = f.dx(i) - - -def test_uflcode_886(self): - cell = triangle - # ... - x = SpatialCoordinate(cell) # Original: x = cell.x - g = sin(x[0]) - v = variable(g) - f = exp(v**2) - h = diff(f, v) - # ... - # print v - # print h - - -def test_python_894(self): - # We don't have to keep the string output compatible, so no test here. - pass - #>>> print v - # var0(sin((x)[0])) - #>>> print h - # d/d[var0(sin((x)[0]))] (exp((var0(sin((x)[0]))) ** 2)) - - -def test_uflcode_930(self): - condition = lt(1, 0) - true_value = 1 - false_value = 0 - # ... - f = conditional(condition, true_value, false_value) - - -def test_uflcode_1003(self): - # Not testable, but this is tested below anyway - "a = derivative(L, w, u)" - pass - - -def test_uflcode_1026(self): - element = FiniteElement("CG", triangle, 1) - # ... - v = TestFunction(element) - u = TrialFunction(element) - w = Coefficient(element) - f = 0.5*w**2*dx - F = derivative(f, w, v) - J = derivative(F, w, u) - - -def test_uflcode_1050(self): - Vx = VectorElement("Lagrange", triangle, 1) - Vy = FiniteElement("Lagrange", triangle, 1) - u = Coefficient(Vx*Vy) - x, y = split(u) - f = inner(grad(x), grad(x))*dx + y*dot(x, x)*dx - F = derivative(f, u) - J = derivative(F, u) - - -def test_uflcode_1085(self): - cell = triangle - # ... - V = VectorElement("Lagrange", cell, 1) - T = TensorElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - M = Coefficient(T) - a = M[i, j]*u[k].dx(j)*v[k].dx(i)*dx - astar = adjoint(a) - - -def test_uflcode_1120(self): - cell = triangle - # ... - V = FiniteElement("Lagrange", cell, 1) - v = TestFunction(V) - f = Coefficient(V) - g = Coefficient(V) - L = f**2 / (2*g)*v*dx - L2 = replace(L, {f: g, g: 3}) - L3 = g**2 / 6*v*dx - - -def test_uflcode_1157(self): - cell = triangle - # ... - V = FiniteElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - f = Coefficient(V) - pde = u*v*dx - f*v*dx - a, L = system(pde) - - -def test_uflcode_1190(self): - cell = triangle - element = FiniteElement("Lagrange", cell, 1) - V = element - u = TrialFunction(V) - v = TestFunction(V) - f = Coefficient(V) - c = variable(Coefficient(V)) - pde = c*u*v*dx - c*f*v*dx - a, L = system(pde) - # ... - u = Coefficient(element) - sL = diff(L, c) - action(diff(a, c), u) - - -def test_uflcode_1195(self): - cell = triangle - element = FiniteElement("Lagrange", cell, 1) - V = element - u = TrialFunction(V) - v = TestFunction(V) - f = Coefficient(V) - c = variable(Coefficient(V)) - pde = c*u*v*dx - c*f*v*dx - a, L = system(pde) - u = Coefficient(element) - # ... - sL = sensitivity_rhs(a, u, L, c) - - -def test_uflcode_1365(self): - e = 0 - v = variable(e) - f = sin(v) - g = diff(f, v) - - -def test_python_1426(self): - # Covered by the below test - pass - # from ufl.algorithms import Graph - # G = Graph(expression) - # V, E = G - - -def test_python_1446(self): - cell = triangle - V = FiniteElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - c = Constant(cell) - f = Coefficient(V) - e = c*f**2*u*v - - # The linearized Graph functionality has been removed from UFL: - # from ufl.algorithms import Graph, partition - # G = Graph(e) - # V, E, = G - - # print(("str(e) = %s\n" % str(e))) - # print(("\n".join("V[%d] = %s" % (i, v) for (i, v) in enumerate(V)), "\n")) - # print(("\n".join("E[%d] = %s" % (i, e) for (i, e) in enumerate(E)), "\n")) - - -def test_python_1512(self): - cell = triangle - V = FiniteElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - c = Constant(cell) - f = Coefficient(V) - e = c*f**2*u*v - - # The linearized Graph functionality has been removed from UFL: - # from ufl.algorithms import Graph, partition - # G = Graph(e) - # V, E, = G - # ... - # Vin = G.Vin() - # Vout = G.Vout() - - -def test_python_1557(self): - cell = triangle - V = FiniteElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - c = Constant(cell) - f = Coefficient(V) - e = c*f**2*u*v - - # The linearized Graph functionality has been removed from UFL: - # from ufl.algorithms import Graph, partition - # G = Graph(e) - # V, E, = G - # ... - # partitions, keys = partition(G) - # for deps in sorted(partitions.keys()): - # P = partitions[deps] - # print "The following depends on", tuple(deps) - # for i in sorted(P): - # print "V[%d] = %s" % (i, V[i]) - # ... - # v = V[i] - - -def test_python_1843(self): - def apply_ad(e, ad_routine): - if e._ufl_is_terminal_: - return e - ops = [apply_ad(o, ad_routine) for o in e.ufl_operands] - e = e._ufl_expr_reconstruct_(*ops) - if isinstance(e, Derivative): - e = ad_routine(e) - return e - - -def test_uflcode_1901(self): - cell = triangle - element = FiniteElement("Lagrange", cell, 1) - # ... - v = Argument(element, 2) - w = Coefficient(element) - - -def test_python_1942(self): - def walk(expression, pre_action, post_action): - pre_action(expression) - for o in expression.ufl_operands: - walk(o) - post_action(expression) - - -def test_python_1955(self): - def post_traversal(root): - for o in root.ufl_operands: - yield post_traversal(o) - yield root - - -def test_python_1963(self): - def post_action(e): - # print str(e) - pass - cell = triangle - V = FiniteElement("Lagrange", cell, 1) - u = TrialFunction(V) - v = TestFunction(V) - c = Constant(cell) - f = Coefficient(V) - expression = c*f**2*u*v - # ... - for e in post_traversal(expression): - post_action(e) - - -def test_python_1990(self): - from ufl.classes import IntValue, Sum - expression = as_ufl(3) - - def int_operation(x): - return 7 - # ... - if isinstance(expression, IntValue): - result = int_operation(expression) - elif isinstance(expression, Sum): - result = sum_operation(expression) - # etc. - # ... - self.assertTrue(result == 7) - - -def test_python_2024(self): - class ExampleFunction(MultiFunction): - - def __init__(self): - MultiFunction.__init__(self) - - def terminal(self, expression): - return "Got a Terminal subtype %s." % type(expression) - - def operator(self, expression): - return "Got an Operator subtype %s." % type(expression) - - def argument(self, expression): - return "Got an Argument." - - def sum(self, expression): - return "Got a Sum." - - m = ExampleFunction() - - cell = triangle - element = FiniteElement("Lagrange", cell, 1) - x = SpatialCoordinate(cell) # Original: x = cell.x - if 0: - print((m(Argument(element, 2)))) - print((m(x))) - print((m(x[0] + x[1]))) - print((m(x[0] * x[1]))) - - -def test_python_2066(self): - def apply(e, multifunction): - ops = [apply(o, multifunction) for o in e.ufl_operands] - return multifunction(e, *ops) - - -def test_python_2087(self): - class Replacer(Transformer): - - def __init__(self, mapping): - Transformer.__init__(self) - self.mapping = mapping - - def operator(self, e, *ops): - return e._ufl_expr_reconstruct_(*ops) - - def terminal(self, e): - return self.mapping.get(e, e) - - f = Constant(triangle) - r = Replacer({f: f**2}) - g = r.visit(2*f) - - -def test_python_2189(self): - V = FiniteElement("Lagrange", triangle, 1) - u = TestFunction(V) - v = TrialFunction(V) - f = Coefficient(V) - - # Note no *dx! This is an expression, not a form. - a = dot(grad(f*u), grad(v)) - - ac = expand_compounds(a) - ad = expand_derivatives(ac) - ai = expand_indices(ad) - - af = tree_format(a) - acf = tree_format(ac) - adf = "\n", tree_format(ad) - aif = tree_format(ai) - - if 0: - print(("\na: ", str(a), "\n", tree_format(a))) - print(("\nac:", str(ac), "\n", tree_format(ac))) - print(("\nad:", str(ad), "\n", tree_format(ad))) - print(("\nai:", str(ai), "\n", tree_format(ai))) - - -def test_python_2328(self): - cell = triangle - x = SpatialCoordinate(cell) # Original: x = cell.x - e = x[0] + x[1] - # print e((0.5, 0.7)) # prints 1.2 - # ... - self.assertEqual(e((0.5, 0.7)), 1.2) - - -def test_python_2338(self): - cell = triangle - x = SpatialCoordinate(cell) # Original: x = cell.x - # ... - c = Constant(cell) - e = c*(x[0] + x[1]) - # print e((0.5, 0.7), { c: 10 }) # prints 12.0 - # ... - self.assertEqual(e((0.5, 0.7), {c: 10}), 12.0) - - -def test_python_2349(self): - element = VectorElement("Lagrange", triangle, 1) - c = Constant(triangle) - f = Coefficient(element) - e = c*(f[0] + f[1]) - - def fh(x): - return (x[0], x[1]) - # print e((0.5, 0.7), { c: 10, f: fh }) # prints 12.0 - # ... - self.assertEqual(e((0.5, 0.7), {c: 10, f: fh}), 12.0) - - -def test_python_2364(self): - element = FiniteElement("Lagrange", triangle, 1) - g = Coefficient(element) - e = g**2 + g.dx(0)**2 + g.dx(1)**2 - - def gh(x, der=()): - if der == (): - return x[0]*x[1] - if der == (0,): - return x[1] - if der == (1,): - return x[0] - # print e((2, 3), { g: gh }) # prints 49 - # ... - self.assertEqual(e((2, 3), {g: gh}), 49) - - -def test_python_2462(self): - cell = triangle - element = FiniteElement("Lagrange", cell, 1) - V = element - u = TrialFunction(V) - v = TestFunction(V) - f = Coefficient(V) - c = variable(Coefficient(V)) - pde = c*u*v*dx - c*f*v*dx - a, L = system(pde) - u = Coefficient(element) - myform = a - # ... - # print repr(preprocess(myform).preprocessed_form) - # ... - r = repr(compute_form_data(myform).preprocessed_form) diff --git a/test/test_cell.py b/test/test_cell.py new file mode 100644 index 000000000..dc26052a4 --- /dev/null +++ b/test/test_cell.py @@ -0,0 +1,87 @@ +import 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() diff --git a/test/test_change_to_local.py b/test/test_change_to_local.py index b123c054f..973195bc7 100755 --- a/test/test_change_to_local.py +++ b/test/test_change_to_local.py @@ -1,28 +1,25 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -""" -Tests of the change to local representaiton algorithms. -""" +"""Tests of the change to local representaiton algorithms.""" -import pytest - -from ufl import * -from ufl.classes import ReferenceGrad, JacobianInverse -from ufl.algorithms import tree_format, change_to_reference_grad +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(cell) - U = FunctionSpace(domain, FiniteElement("CG", cell, 1)) - V = FunctionSpace(domain, VectorElement("CG", cell, 1)) + 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) + (t,) = indices(1) # Single grad change on a scalar function expr = grad(u) @@ -39,27 +36,33 @@ def test_change_to_reference_grad(): # 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)) + 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)) + 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)) + 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)) + 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) diff --git a/test/test_change_to_reference_frame.py b/test/test_change_to_reference_frame.py index 264d12463..63b0751b3 100755 --- a/test/test_change_to_reference_frame.py +++ b/test/test_change_to_reference_frame.py @@ -1,105 +1,55 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- """Tests of the change to reference frame algorithm.""" -import pytest - -from ufl import * - -from ufl.classes import Form, Integral, Expr, ReferenceGrad, ReferenceValue - -''' -from ufl.classes import ReferenceGrad, JacobianInverse -from ufl.algorithms import tree_format, change_to_reference_grad - -from ufl.log import error, warning - -from ufl.core.multiindex import Index, indices -from ufl.corealg.multifunction import MultiFunction -from ufl.corealg.map_dag import map_expr_dag - -from ufl.classes import (Expr, FormArgument, GeometricQuantity, - Terminal, ReferenceGrad, Grad, Restricted, ReferenceValue, - Jacobian, JacobianInverse, JacobianDeterminant, - FacetJacobian, FacetJacobianInverse, FacetJacobianDeterminant, - CellFacetJacobian, - ReferenceCellEdgeVectors, ReferenceFacetEdgeVectors, - FacetNormal, CellNormal, ReferenceNormal, - CellVolume, FacetArea, - CellOrientation, FacetOrientation, QuadratureWeight, - SpatialCoordinate, Indexed, MultiIndex, FixedIndex) - -from ufl.constantvalue import as_ufl, Identity -from ufl.tensoralgebra import Transposed -from ufl.tensors import as_tensor, as_vector, as_scalar, ComponentTensor -from ufl.operators import sqrt, max_value, min_value, sign -from ufl.permutation import compute_indices - -from ufl.algorithms.transformer import ReuseTransformer, apply_transformer -from ufl.compound_expressions import determinant_expr, cross_expr, inverse_expr -from ufl.finiteelement import FiniteElement, EnrichedElement, VectorElement, MixedElement, TensorProductElement, TensorElement, FacetElement, InteriorElement, BrokenElement. -''' - - -def change_integral_to_reference_frame(form, context): - if False: # TODO: integral.is_in_reference_frame(): - # TODO: Assume reference frame integral is written purely in - # reference frame or tramsform integrand here as well? - return integrand - else: - # Change integrand expression to reference frame - integrand = change_to_reference_frame(integral.integrand()) - - # Compute and apply integration scaling factor - scale, degree = compute_integrand_scaling_factor(integral.ufl_domain(), - integral.integral_type()) - - return integral.reconstruct(integrand * scale) # TODO: , reference=True) - - -def change_expr_to_reference_frame(expr): - expr = ReferenceValue(expr) - return expr +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): - if isinstance(expr, Form): - return change_form_to_reference_frame(expr) - elif isinstance(expr, Integral): - return change_integral_to_reference_frame(expr) - elif isinstance(expr, Expr): - return change_expr_to_reference_frame(expr) - else: - error("Invalid type.") + assert isinstance(expr, Expr) + return ReferenceValue(expr) def test_change_unmapped_form_arguments_to_reference_frame(): - U = FiniteElement("CG", triangle, 1) - V = VectorElement("CG", triangle, 1) - T = TensorElement("CG", triangle, 1) + 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) - expr = Coefficient(U) + 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) + expr = Coefficient(v_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) - expr = Coefficient(T) + expr = Coefficient(t_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) def test_change_hdiv_form_arguments_to_reference_frame(): - V = FiniteElement("RT", triangle, 1) + 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) + expr = Coefficient(v_space) assert change_to_reference_frame(expr) == ReferenceValue(expr) def test_change_hcurl_form_arguments_to_reference_frame(): - V = FiniteElement("RT", triangle, 1) + V = FiniteElement("Raviart-Thomas", triangle, 1, (2,), contravariant_piola, HDiv) - expr = Coefficient(V) + 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 @@ -171,104 +121,10 @@ def test_change_hcurl_form_arguments_to_reference_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 - ''' - - -def new_analyse_modified_terminal(expr): - assert expr._ufl_is_terminal_ or expr._ufl_is_terminal_modifier_type_ - m = expr - - # The outermost expression may index to get a specific scalar value - if isinstance(m, Indexed): - unindexed, multi_index = m.ufl_operands - indices = tuple(int(i) for i in multi_index) - else: - unindexed = m - indices = () - - # The evaluation mode is one of current point, - # a cell entity midpoint, or averaging over a cell entity - if unindexed._ufl_is_evaluation_type_: # averages and point evaluations - v, = v.ufl_operand - evaluation = unindexed.ufl_handler_name - else: - v = unindexed - evaluation = "current_point" - - # We're either in reference frame or global, checks below ensure we don't mix the two - frame = "reference" if v._ufl_is_reference_type_ else "global" - - # Peel off derivatives (grad^n,div,curl,div(grad),grad(div) etc.) - t = v - derivatives = [] - while t._ufl_is_derivative_type_: - # ensure frame consistency - assert t._ufl_is_reference_type_ == v._ufl_is_reference_type_ - derivatives.append(t._ufl_class_) - t, = t.ufl_operands - core = t - derivatives = tuple(derivatives) - - # This can be an intermediate step to use derivatives instead of ngrads: - num_derivatives = len(derivatives) - - # If we had a reference type before unwrapping terminal, - # there should be a ReferenceValue inside all the derivatives - if v._ufl_is_reference_type_: - assert isinstance(t, ReferenceValue) - t, = t.ufl_operands - - # At the core we may have a restriction - if t._ufl_is_restriction_type_: - restriction = t.side() - t, = t.ufl_operands - else: - restriction = "" - - # And then finally the terminal - assert t._ufl_is_terminal_ - terminal = t - - # This will only be correct for derivatives = grad^n - gdim = terminal.ufl_domain().geometric_dimension() - derivatives_shape = (gdim,)*num_derivatives - - # Get shapes - expr_shape = expr.ufl_shape - unindexed_shape = unindexed.ufl_shape - core_shape = core.ufl_shape - - # Split indices - core_indices = indices[:len(core_shape)] - derivative_indices = indices[len(core_shape):] - - # Apply paranoid dimension checking - assert len(indices) == len(unindexed_shape) - assert all(0 <= i for i in indices) - assert all(i < j for i, j in zip(indices, unindexed_shape)) - assert len(core_indices) == len(core_shape) - assert all(0 <= i for i in core_indices) - assert all(i < j for i, j in zip(core_indices, core_shape)) - assert len(derivative_indices) == len(derivatives_shape) # This will fail for e.g. div(grad(f)) - assert all(0 <= i for i in derivative_indices) - assert all(i < j for i, j in zip(derivative_indices, derivatives_shape)) - - # Return values: - mt = ModifiedTerminal( - # TODO: Use keyword args - expr, - indices, - evaluation, - frame, - num_derivatives, - derivatives, - restriction, - terminal - ) - return mt + """ -''' +""" New form preprocessing pipeline: Preferably introduce these changes: @@ -281,10 +137,15 @@ def new_analyse_modified_terminal(expr): 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 + 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 -''' +""" diff --git a/test/test_check_arities.py b/test/test_check_arities.py index bf2a5b324..e2c32f5f5 100755 --- a/test/test_check_arities.py +++ b/test/test_check_arities.py @@ -1,16 +1,35 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- import pytest -from ufl import * -from ufl.algorithms.compute_form_data import compute_form_data + +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(cell) - V = FunctionSpace(D, VectorElement("P", cell, 2)) + 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) @@ -26,17 +45,15 @@ def test_check_arities(): L = derivative(M, u, dv) a = derivative(L, u, du) - fd = compute_form_data(M) - fd = compute_form_data(L) - fd = compute_form_data(a) - - assert True + compute_form_data(M) + compute_form_data(L) + compute_form_data(a) def test_complex_arities(): cell = tetrahedron - D = Mesh(cell) - V = FunctionSpace(D, VectorElement("P", cell, 2)) + 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) @@ -51,3 +68,19 @@ def test_complex_arities(): 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) diff --git a/test/test_classcoverage.py b/test/test_classcoverage.py index d8355051d..f7d38900f 100755 --- a/test/test_classcoverage.py +++ b/test/test_classcoverage.py @@ -1,16 +1,120 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2008-09-06 -- 2009-02-10" -import pytest - import ufl -from ufl import * -from ufl.constantvalue import as_ufl -from ufl.classes import * -from ufl.algorithms import * +from ufl import * # noqa: F403 +from ufl import ( + And, + Argument, + CellDiameter, + CellVolume, + Circumradius, + Coefficient, + Constant, + EdgeJacobian, + EdgeJacobianDeterminant, + EdgeJacobianInverse, + 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() @@ -18,9 +122,9 @@ def _test_object(a, shape, free_indices): # Check if instances of this type has certain memory consuming members - if hasattr(a, '_repr'): + if hasattr(a, "_repr"): has_repr.add(a.__class__.__name__) - if hasattr(a, '__dict__'): + if hasattr(a, "__dict__"): has_dict.add(a.__class__.__name__) # Test reproduction via repr string @@ -29,7 +133,7 @@ def _test_object(a, shape, free_indices): assert hash(a) == hash(e) # Can't really test str more than that it exists - s = str(a) + str(a) # Check that some properties are at least available fi = a.ufl_free_indices @@ -53,9 +157,9 @@ def _test_object(a, shape, free_indices): def _test_object2(a): # Check if instances of this type has certain memory consuming members - if hasattr(a, '_repr'): + if hasattr(a, "_repr"): has_repr.add(a.__class__.__name__) - if hasattr(a, '__dict__'): + if hasattr(a, "__dict__"): has_dict.add(a.__class__.__name__) # Test reproduction via repr string @@ -64,7 +168,7 @@ def _test_object2(a): assert hash(a) == hash(e) # Can't really test str more than that it exists - s = str(a) + str(a) def _test_form(a): @@ -74,7 +178,7 @@ def _test_form(a): assert hash(a) == hash(e) # Can't really test str more than that it exists - s = str(a) + str(a) def testExports(self): @@ -85,8 +189,9 @@ def testExports(self): 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) + 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)))) @@ -94,52 +199,59 @@ def testExports(self): def testAll(self): - Expr.ufl_enable_profiling() # --- Elements: cell = triangle - dim = cell.geometric_dimension() + dim = 2 - e0 = FiniteElement("CG", cell, 1) - e1 = VectorElement("CG", cell, 1) - e2 = TensorElement("CG", cell, 1) - e3 = MixedElement(e0, e1, e2) + 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 = VectorElement("CG", tetrahedron, 1) + 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, 3) - f13D = Coefficient(e13D) + v13D = Argument(e13d_space, 3) + f13D = Coefficient(e13d_space) - v0 = Argument(e0, 4) - v1 = Argument(e1, 5) - v2 = Argument(e2, 6) - v3 = Argument(e3, 7) + 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, (dim*dim+dim+1,), ()) + _test_object(v3, (1 + dim + dim**2,), ()) - f0 = Coefficient(e0) - f1 = Coefficient(e1) - f2 = Coefficient(e2) - f3 = Coefficient(e3) + 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, (dim*dim+dim+1,), ()) + _test_object(f3, (1 + dim + dim**2,), ()) - c = Constant(cell) + c = Constant(domain) _test_object(c, (), ()) - c = VectorConstant(cell) + c = VectorConstant(domain) _test_object(c, (dim,), ()) - c = TensorConstant(cell) + c = TensorConstant(domain) _test_object(c, (dim, dim), ()) a = FloatValue(1.23) @@ -148,74 +260,69 @@ def testAll(self): a = IntValue(123) _test_object(a, (), ()) - I = Identity(1) - _test_object(I, (1, 1), ()) - I = Identity(2) - _test_object(I, (2, 2), ()) - I = Identity(3) - _test_object(I, (3, 3), ()) + 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(cell) + x = SpatialCoordinate(domain) _test_object(x, (dim,), ()) - xi = CellCoordinate(cell) + xi = CellCoordinate(domain) _test_object(xi, (dim,), ()) - # g = CellBarycenter(cell) - #_test_object(g, (dim,), ()) - # g = FacetBarycenter(cell) - #_test_object(g, (dim,), ()) + # g = CellBarycenter(domain) + # _test_object(g, (dim,), ()) + # g = FacetBarycenter(domain) + # _test_object(g, (dim,), ()) - g = Jacobian(cell) + g = Jacobian(domain) _test_object(g, (dim, dim), ()) - g = JacobianDeterminant(cell) + g = JacobianDeterminant(domain) _test_object(g, (), ()) - g = JacobianInverse(cell) + g = JacobianInverse(domain) _test_object(g, (dim, dim), ()) - g = FacetJacobian(cell) - _test_object(g, (dim, dim-1), ()) - g = FacetJacobianDeterminant(cell) + g = FacetJacobian(domain) + _test_object(g, (dim, dim - 1), ()) + g = FacetJacobianDeterminant(domain) _test_object(g, (), ()) - g = FacetJacobianInverse(cell) - _test_object(g, (dim-1, dim), ()) - - cell3D = tetrahedron # EdgeJacobian is not available in 2D - dim3D = cell3D.geometric_dimension() - assert dim3D == 3 - g = EdgeJacobian(cell3D) - _test_object(g, (dim3D, dim3D-2), ()) - g = EdgeJacobianDeterminant(cell3D) + g = FacetJacobianInverse(domain) + _test_object(g, (dim - 1, dim), ()) + g = EdgeJacobian(domain3D) + _test_object(g, (3, 1), ()) + g = EdgeJacobianDeterminant(domain3D) _test_object(g, (), ()) - g = EdgeJacobianInverse(cell3D) - _test_object(g, (dim3D-2, dim3D), ()) - - g = FacetNormal(cell) - _test_object(g, (dim,), ()) - # g = CellNormal(cell) - #_test_object(g, (dim,), ()) - - g = CellVolume(cell) + g = EdgeJacobianInverse(domain3D) + _test_object(g, (1, 3), ()) + g = FacetNormal(domain3D) + _test_object(g, (3,), ()) + # g = CellNormal(domain) + # _test_object(g, (dim,), ()) + + g = CellVolume(domain) _test_object(g, (), ()) - g = CellDiameter(cell) + g = CellDiameter(domain) _test_object(g, (), ()) - g = Circumradius(cell) + g = Circumradius(domain) _test_object(g, (), ()) - # g = CellSurfaceArea(cell) - #_test_object(g, (), ()) + # g = CellSurfaceArea(domain) + # _test_object(g, (), ()) - g = FacetArea(cell) + g = FacetArea(domain) _test_object(g, (), ()) - g = MinFacetEdgeLength(cell) + g = MinFacetEdgeLength(domain) _test_object(g, (), ()) - g = MaxFacetEdgeLength(cell) + g = MaxFacetEdgeLength(domain) _test_object(g, (), ()) - # g = FacetDiameter(cell) - #_test_object(g, (), ()) + # g = FacetDiameter(domain) + # _test_object(g, (), ()) a = variable(v0) _test_object(a, (), ()) @@ -224,7 +331,7 @@ def testAll(self): a = variable(v2) _test_object(a, (dim, dim), ()) a = variable(v3) - _test_object(a, (dim*dim+dim+1,), ()) + _test_object(a, (1 + dim + dim**2,), ()) a = variable(f0) _test_object(a, (), ()) a = variable(f1) @@ -232,7 +339,7 @@ def testAll(self): a = variable(f2) _test_object(a, (dim, dim), ()) a = variable(f3) - _test_object(a, (dim*dim+dim+1,), ()) + _test_object(a, (1 + dim + dim**2,), ()) # a = MultiIndex() @@ -252,8 +359,8 @@ def testAll(self): a = f2[l, 1] _test_object(a, (), (l,)) - I = Identity(dim) - a = inv(I) + ident = Identity(dim) + a = inv(ident) _test_object(a, (dim, dim), ()) a = inv(v2) _test_object(a, (dim, dim), ()) @@ -285,7 +392,7 @@ def testAll(self): a = v2 + f2 + v2 _test_object(a, (dim, dim), ()) # a = Product() - a = 3*v0*(2.0*v0)*f0*(v0*3.0) + a = 3 * v0 * (2.0 * v0) * f0 * (v0 * 3.0) _test_object(a, (), ()) # a = Division() a = v0 / 2.0 @@ -297,78 +404,76 @@ def testAll(self): # a = Power() a = f0**3 _test_object(a, (), ()) - a = (f0*2)**1.23 + a = (f0 * 2) ** 1.23 _test_object(a, (), ()) # a = ListTensor() - a = as_vector([1.0, 2.0*f0, f0**2]) + 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]]) + 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]]]) + 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) + a = as_vector(v1[i] * f1[j], i) _test_object(a, (dim,), (j,)) - a = as_matrix(v1[i]*f1[j], (j, i)) + a = as_matrix(v1[i] * f1[j], (j, i)) _test_object(a, (dim, dim), ()) - a = as_tensor(v1[i]*f1[j], (i, j)) + 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)) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + a = cofac(f2 * f0 + v2 * 3) _test_object(a, (dim, dim), ()) cond1 = le(f0, 1.0) @@ -463,7 +568,7 @@ def testAll(self): s0 = variable(f0) s1 = variable(f1) s2 = variable(f2) - f = dot(s0*s1, s2) + f = dot(s0 * s1, s2) _test_object(s0, (), ()) _test_object(s1, (dim,), ()) _test_object(s2, (dim, dim), ()) @@ -472,7 +577,14 @@ def testAll(self): a = diff(f, s0) _test_object(a, (dim,), ()) a = diff(f, s1) - _test_object(a, (dim, dim,), ()) + _test_object( + a, + ( + dim, + dim, + ), + (), + ) a = diff(f, s2) _test_object(a, (dim, dim, dim), ()) @@ -495,9 +607,9 @@ def testAll(self): _test_object(a, (dim, dim), ()) a = grad(f1) _test_object(a, (dim, dim), ()) - a = grad(f0*v0) + a = grad(f0 * v0) _test_object(a, (dim,), ()) - a = grad(f0*v1) + a = grad(f0 * v1) _test_object(a, (dim, dim), ()) a = nabla_div(v1) @@ -519,9 +631,9 @@ def testAll(self): _test_object(a, (dim, dim), ()) a = nabla_grad(f1) _test_object(a, (dim, dim), ()) - a = nabla_grad(f0*v0) + a = nabla_grad(f0 * v0) _test_object(a, (dim,), ()) - a = nabla_grad(f0*v1) + a = nabla_grad(f0 * v1) _test_object(a, (dim, dim), ()) a = curl(v13D) @@ -534,17 +646,17 @@ def testAll(self): _test_object(a, (), ()) # a = PositiveRestricted(v0) - #_test_object(a, (), ()) - a = v0('+') + # _test_object(a, (), ()) + a = v0("+") _test_object(a, (), ()) - a = v0('+')*f0 + a = v0("+") * f0 _test_object(a, (), ()) # a = NegativeRestricted(v0) - #_test_object(a, (), ()) - a = v0('-') + # _test_object(a, (), ()) + a = v0("-") _test_object(a, (), ()) - a = v0('-') + f0 + a = v0("-") + f0 _test_object(a, (), ()) a = cell_avg(v0) @@ -562,47 +674,47 @@ def testAll(self): # --- Integrals: - a = v0*dx + a = v0 * dx _test_form(a) - a = v0*dx(0) + a = v0 * dx(0) _test_form(a) - a = v0*dx(1) + a = v0 * dx(1) _test_form(a) - a = v0*ds + a = v0 * ds _test_form(a) - a = v0*ds(0) + a = v0 * ds(0) _test_form(a) - a = v0*ds(1) + a = v0 * ds(1) _test_form(a) - a = v0*dS + a = v0 * dS _test_form(a) - a = v0*dS(0) + a = v0 * dS(0) _test_form(a) - a = v0*dS(1) + a = v0 * dS(1) _test_form(a) - a = v0*dot(v1, f1)*dx + a = v0 * dot(v1, f1) * dx _test_form(a) - a = v0*dot(v1, f1)*dx(0) + a = v0 * dot(v1, f1) * dx(0) _test_form(a) - a = v0*dot(v1, f1)*dx(1) + a = v0 * dot(v1, f1) * dx(1) _test_form(a) - a = v0*dot(v1, f1)*ds + a = v0 * dot(v1, f1) * ds _test_form(a) - a = v0*dot(v1, f1)*ds(0) + a = v0 * dot(v1, f1) * ds(0) _test_form(a) - a = v0*dot(v1, f1)*ds(1) + a = v0 * dot(v1, f1) * ds(1) _test_form(a) - a = v0*dot(v1, f1)*dS + a = v0 * dot(v1, f1) * dS _test_form(a) - a = v0*dot(v1, f1)*dS(0) + a = v0 * dot(v1, f1) * dS(0) _test_form(a) - a = v0*dot(v1, f1)*dS(1) + a = v0 * dot(v1, f1) * dS(1) _test_form(a) # --- Form transformations: - a = f0*v0*dx + f0*v0*dot(f1, v1)*dx + a = f0 * v0 * dx + f0 * v0 * dot(f1, v1) * dx # b = lhs(a) # TODO # c = rhs(a) # TODO d = derivative(a, f1, v1) diff --git a/test/test_complex.py b/test/test_complex.py index 597adfba0..d91d0911c 100755 --- a/test/test_complex.py +++ b/test/test_complex.py @@ -1,24 +1,54 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -import pytest import cmath -import ufl -from ufl.constantvalue import Zero, ComplexValue -from ufl.algebra import Conj, Real, Imag -from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering -from ufl.algorithms.remove_complex_nodes import remove_complex_nodes + +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.comparison_checker import do_comparison_check, ComplexComparisonError +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 import TestFunction, TrialFunction, triangle, FiniteElement, \ - as_ufl, inner, grad, dx, dot, outer, conj, sqrt, sin, cosh, \ - atan, ln, exp, as_tensor, real, imag, conditional, \ - min_value, max_value, gt, lt, cos, ge, le, Coefficient +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) + z1 = ComplexValue(1 + 2j) + z2 = ComplexValue(1 - 2j) assert z1 == Conj(z2) assert z2 == Conj(z1) @@ -28,7 +58,7 @@ def test_real(self): z0 = Zero() z1 = as_ufl(1.0) z2 = ComplexValue(1j) - z3 = ComplexValue(1+1j) + z3 = ComplexValue(1 + 1j) assert Real(z1) == z1 assert Real(z3) == z1 assert Real(z2) == z0 @@ -38,7 +68,7 @@ def test_imag(self): z0 = Zero() z1 = as_ufl(1.0) z2 = as_ufl(1j) - z3 = ComplexValue(1+1j) + z3 = ComplexValue(1 + 1j) assert Imag(z2) == z1 assert Imag(z3) == z1 @@ -47,10 +77,12 @@ def test_imag(self): def test_compute_form_adjoint(self): cell = triangle - element = FiniteElement('Lagrange', cell, 1) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - u = TrialFunction(element) - v = TestFunction(element) + u = TrialFunction(space) + v = TestFunction(space) a = inner(grad(u), grad(v)) * dx @@ -59,24 +91,32 @@ def test_compute_form_adjoint(self): 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)) + 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) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - v = TestFunction(element) - u = TrialFunction(element) + v = TestFunction(space) + u = TrialFunction(space) assert inner(u, v) == u * conj(v) assert dot(u, v) == u * v @@ -85,10 +125,12 @@ def test_automatic_simplification(self): def test_apply_algebra_lowering_complex(self): cell = triangle - element = FiniteElement("Lagrange", cell, 1) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - v = TestFunction(element) - u = TrialFunction(element) + v = TestFunction(space) + u = TrialFunction(space) gv = grad(v) gu = grad(u) @@ -106,36 +148,43 @@ def test_apply_algebra_lowering_complex(self): 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],)) + 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) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - u = TrialFunction(element) - v = TestFunction(element) - f = Coefficient(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)) + d = conj(real(v)) * imag(conj(u)) assert remove_complex_nodes(a) == v assert remove_complex_nodes(b) == u - with pytest.raises(ufl.log.UFLException): + with pytest.raises(BaseException): remove_complex_nodes(c) - with pytest.raises(ufl.log.UFLException): + with pytest.raises(BaseException): remove_complex_nodes(d) def test_comparison_checker(self): cell = triangle - element = FiniteElement("Lagrange", cell, 1) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - u = TrialFunction(element) - v = TestFunction(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)) @@ -158,9 +207,11 @@ def test_comparison_checker(self): def test_complex_degree_handling(self): cell = triangle - element = FiniteElement("Lagrange", cell, 3) + element = FiniteElement("Lagrange", cell, 3, (), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) - v = TestFunction(element) + v = TestFunction(space) a = conj(v) b = imag(v) diff --git a/test/test_conditionals.py b/test/test_conditionals.py index 4d5f4f0a2..8b7980d2d 100755 --- a/test/test_conditionals.py +++ b/test/test_conditionals.py @@ -1,46 +1,38 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2008-08-20 -- 2012-11-30" import pytest -from ufl import * -# from ufl.algorithms import * -from ufl.classes import * +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) - return Coefficient(element) + 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) - return Coefficient(element) + 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(UFLException): + with pytest.raises(BaseException): conditional(True, 1, 0) -def test_eq_produces_ufl_expr(f, g): - expr1 = eq(f, f) - expr2 = eq(f, g) - expr3 = eq(f, g) - assert isinstance(expr1, EQ) - assert isinstance(expr2, EQ) - assert not bool(expr1 == expr2) - assert bool(expr1 != expr2) - assert bool(expr2 == expr3) - - def test_eq_oper_produces_bool(f, g): expr1 = f == f expr2 = f == g @@ -50,15 +42,6 @@ def test_eq_oper_produces_bool(f, g): assert not expr2 -def xtest_eq_produces_ufl_expr(f, g): - expr1 = f == g - expr2 = eq(f, g) - assert isinstance(expr1, EQ) - assert isinstance(expr2, EQ) - assert bool(expr1 == expr2) - assert not bool(expr1 != expr2) - - def test_eq_produces_ufl_expr(f, g): expr1 = eq(f, g) expr2 = eq(f, f) @@ -123,7 +106,7 @@ def test_lt_produces_ufl_expr(f, g): # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: - with pytest.raises(UFLException): + with pytest.raises(BaseException): bool(expr1) @@ -136,7 +119,7 @@ def test_gt_produces_ufl_expr(f, g): # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: - with pytest.raises(UFLException): + with pytest.raises(BaseException): bool(expr1) @@ -149,7 +132,7 @@ def test_le_produces_ufl_expr(f, g): # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: - with pytest.raises(UFLException): + with pytest.raises(BaseException): bool(expr1) @@ -162,5 +145,5 @@ def test_ge_produces_ufl_expr(f, g): # Representations are the same: assert bool(expr1 == expr2) # Protection from misuse in boolean python expression context: - with pytest.raises(UFLException): + with pytest.raises(BaseException): bool(expr1) diff --git a/test/test_degree_estimation.py b/test/test_degree_estimation.py index 3c344a8e3..6101db9ee 100755 --- a/test/test_degree_estimation.py +++ b/test/test_degree_estimation.py @@ -1,36 +1,56 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2008-03-12 -- 2009-01-28" -import pytest -from pprint import * - -from ufl import * -from ufl.algorithms import * +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("CG", triangle, 1) - V2 = FiniteElement("CG", triangle, 2) - VV = VectorElement("CG", triangle, 3) - VM = V1 * V2 - O1 = TensorProductElement(V1, V1) - O2 = TensorProductElement(V2, V1) - v1 = Argument(V1, 2) - v2 = Argument(V2, 3) - f1, f2 = Coefficients(VM) - u1 = Coefficient(O1) - u2 = Coefficient(O2) - vv = Argument(VV, 4) - vu = Argument(VV, 5) - - x, y = SpatialCoordinate(triangle) + 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**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 @@ -57,7 +77,7 @@ def test_total_degree_estimation(): 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) == 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 @@ -68,27 +88,15 @@ def test_total_degree_estimation(): 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 - - # outer product tuple-degree tests - assert estimate_total_polynomial_degree(u1) == (1, 1) - assert estimate_total_polynomial_degree(u2) == (2, 1) - # derivatives should do nothing (don't know in which direction they act) - assert estimate_total_polynomial_degree(grad(u2)) == (2, 1) - assert estimate_total_polynomial_degree(u1 * u1) == (2, 2) - assert estimate_total_polynomial_degree(u2 * u1) == (3, 2) - assert estimate_total_polynomial_degree(u2 * u2) == (4, 2) - assert estimate_total_polynomial_degree(u1 ** 3) == (3, 3) - assert estimate_total_polynomial_degree(u1 ** 3 + u2 * u2) == (4, 3) - assert estimate_total_polynomial_degree(u2 ** 2 * u1) == (5, 3) + 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(triangle) - e = nx ** 2 - for f in [sin, cos, tan, abs, lambda z:z**7]: + 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... @@ -99,7 +107,6 @@ def test_total_degree_estimation(): 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 @@ -107,11 +114,12 @@ def test_some_compound_types(): etpd = estimate_total_polynomial_degree - P2 = FiniteElement("CG", triangle, 2) - V2 = VectorElement("CG", triangle, 2) + 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(P2) - v = Coefficient(V2) + u = Coefficient(FunctionSpace(domain, P2)) + v = Coefficient(FunctionSpace(domain, V2)) assert etpd(u.dx(0)) == 2 - 1 assert etpd(grad(u)) == 2 - 1 diff --git a/test/test_derivative.py b/test/test_derivative.py index c9ce779bf..cce955a12 100755 --- a/test/test_derivative.py +++ b/test/test_derivative.py @@ -1,29 +1,81 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-17 -- 2009-02-17" -import pytest -import math from itertools import chain -from ufl import * -from ufl.classes import Indexed, MultiIndex, ReferenceGrad -from ufl.constantvalue import as_ufl -from ufl.algorithms import expand_indices, strip_variables, post_traversal, compute_form_data +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.algorithms.apply_algebra_lowering import apply_algebra_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) + ad = compute_form_data(actual * dx) a = ad.preprocessed_form.integrals_by_type("cell")[0].integrand() - bd = compute_form_data(expected*dx) + 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]) + 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 @@ -41,17 +93,24 @@ def make_value(c): 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())) - - acell = actual.ufl_domain().ufl_cell() - bcell = expected.ufl_domain().ufl_cell() + 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 acell.geometric_dimension() == 1: + if adomain.geometric_dimension() == 1: x = (0.3,) - elif acell.geometric_dimension() == 2: + elif adomain.geometric_dimension() == 2: x = (0.3, 0.4) - elif acell.geometric_dimension() == 3: + elif adomain.geometric_dimension() == 3: x = (0.3, 0.4, 0.5) av = a(x, amapping) bv = b(x, bmapping) @@ -71,10 +130,13 @@ def make_value(c): def _test(self, f, df): cell = triangle - element = FiniteElement("CG", cell, 1) - v = TestFunction(element) - u = TrialFunction(element) - w = Coefficient(element) + 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 @@ -89,223 +151,296 @@ def _test(self, f, df): dfv2 = dfv2(x, mapping) assert dfv1 == dfv2 - dfv1 = derivative(f(7*w), w, v) - dfv2 = 7*df(7*w, v) + 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 f(w): + return as_ufl(1) + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) def testIdentityLiteral(self): - def f(w): return Identity(2)[i, i] + def f(w): + return Identity(2)[i, i] + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) + # --- Form arguments def testCoefficient(self): - def f(w): return w + def f(w): + return w + + def df(w, v): + return v - def df(w, v): return v _test(self, f, df) def testArgument(self): - def f(w): return TestFunction(FiniteElement("CG", triangle, 1)) + 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() - def df(w, v): return zero() _test(self, f, df) + # --- Geometry def testSpatialCoordinate(self): - def f(w): return SpatialCoordinate(triangle)[0] + def f(w): + return SpatialCoordinate( + Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + )[0] + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) def testFacetNormal(self): - def f(w): return FacetNormal(triangle)[0] + 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 df(w, v): + return zero() -# def testCellSurfaceArea(self): -# def f(w): return CellSurfaceArea(triangle) -# def df(w, v): return zero() -# _test(self, f, df) + _test(self, f, df) def testFacetArea(self): - def f(w): return FacetArea(triangle) + def f(w): + return FacetArea(Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1))) + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) def testCellDiameter(self): - def f(w): return CellDiameter(triangle) + def f(w): + return CellDiameter( + Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + ) + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) def testCircumradius(self): - def f(w): return Circumradius(triangle) + def f(w): + return Circumradius( + Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + ) + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) def testCellVolume(self): - def f(w): return CellVolume(triangle) + def f(w): + return CellVolume(Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1))) + + def df(w, v): + return zero() - def df(w, v): return zero() _test(self, f, df) + # --- Basic operators def testSum(self): - def f(w): return w + 1 + def f(w): + return w + 1 + + def df(w, v): + return v - def df(w, v): return v _test(self, f, df) def testProduct(self): - def f(w): return 3*w + def f(w): + return 3 * w + + def df(w, v): + return 3 * v - def df(w, v): return 3*v _test(self, f, df) def testPower(self): - def f(w): return w**3 + def f(w): + return w**3 + + def df(w, v): + return 3 * w**2 * v - def df(w, v): return 3*w**2*v _test(self, f, df) def testDivision(self): - def f(w): return w / 3.0 + def f(w): + return w / 3.0 + + def df(w, v): + return v / 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 f(w): + return 3.0 / w + + def df(w, v): + return -3.0 * v / w**2 - def df(w, v): return -3.0 * v / w**2 _test(self, f, df) def testExp(self): - def f(w): return exp(w) + def f(w): + return exp(w) + + def df(w, v): + return v * exp(w) - def df(w, v): return v*exp(w) _test(self, f, df) def testLn(self): - def f(w): return ln(w) + def f(w): + return ln(w) + + def df(w, v): + return v / w - def df(w, v): return v / w _test(self, f, df) def testCos(self): - def f(w): return cos(w) + def f(w): + return cos(w) + + def df(w, v): + return -v * sin(w) - def df(w, v): return -v*sin(w) _test(self, f, df) def testSin(self): - def f(w): return sin(w) + def f(w): + return sin(w) + + def df(w, v): + return v * cos(w) - def df(w, v): return v*cos(w) _test(self, f, df) def testTan(self): - def f(w): return tan(w) + def f(w): + return tan(w) + + def df(w, v): + return v * 2.0 / (cos(2.0 * w) + 1.0) - 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 f(w): + return acos(w / 1000) + + def df(w, v): + return -(v / 1000) / sqrt(1.0 - (w / 1000) ** 2) - 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 f(w): + return asin(w / 1000) + + def df(w, v): + return (v / 1000) / sqrt(1.0 - (w / 1000) ** 2) - 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 f(w): + return atan(w) + + def df(w, v): + return v / (1.0 + w**2) - 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): - def cond(w): return lt(1.0, 2.0) - - def f(w): return conditional(cond(w), 2*w, 3*w) - - def df(w, v): return 2*v - _test(self, f, df) - - def cond(w): return lt(2.0, 1.0) + def f(w): + return abs(w) - def f(w): return conditional(cond(w), 2*w, 3*w) + def df(w, v): + return sign(w) * v - def df(w, v): return 3*v _test(self, f, df) def testConditional(self): # This will fail without bugfix in derivative - def cond(w): return lt(w, 1.0) + def cond(w): + return lt(w, 1.0) - def f(w): return conditional(cond(w), 2*w, 3*w) + 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 - 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 @@ -314,94 +449,112 @@ 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] + (i,) = indices(1) + return a[i] * b[i] + + def df(w, v): + return 3 * v + 4 * 2 * w * v + 5 * 3 * w**2 * v - 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)), - )) + 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)), + 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 i in range(3): - for j in range(2): - for k in range(2): - self.assertEqual(dfv[i, j, k](x), g[i, j, k](x)) + 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("CG", cell, 1) - u = Coefficient(V) - v = TestFunction(V) - a = 3*u**2 + 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))) + self.assertEqualAfterPreprocessing(b, 3 * (u * (2 * v))) def test_single_vector_coefficient_derivative(self): cell = triangle - V = VectorElement("CG", cell, 1) - u = Coefficient(V) - v = TestFunction(V) - a = 3*dot(u, u) + 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])) + expected = 3 * (2 * (u[i] * v[i])) assertEqualBySampling(actual, expected) def test_multiple_coefficient_derivative(self): cell = triangle - V = FiniteElement("CG", cell, 1) - W = VectorElement("CG", cell, 1) - M = V*W - uv = Coefficient(V) - uw = Coefficient(W) - v = TestFunction(M) + 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) + 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) + 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) + 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 - I = Identity(cell.geometric_dimension()) - V = FiniteElement("CG", cell, 1) - W = VectorElement("CG", cell, 1) - u = Coefficient(W) - v = TestFunction(V) + 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] + a = (u[i] * u[k].dx(i)) * w[k] actual = derivative(a, u[0], v) - dw = v*u[k].dx(0) + u[i]*I[0, k]*v.dx(i) + dw = v * u[k].dx(0) + u[i] * ident[0, k] * v.dx(i) expected = 2 * w[k] * dw assertEqualBySampling(actual, expected) @@ -409,126 +562,173 @@ def test_indexed_coefficient_derivative(self): def test_multiple_indexed_coefficient_derivative(self): cell = tetrahedron - I = Identity(cell.geometric_dimension()) - V = FiniteElement("CG", cell, 1) - V2 = V*V - W = VectorElement("CG", cell, 1) - u = Coefficient(W) - w = Coefficient(W) - v = TestFunction(V2) + 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) + 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("CG", cell, 1) - W = VectorElement("CG", cell, 1) + 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) - v = Coefficient(W) - du = TrialFunction(V) - dv = TestFunction(V) + 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 i in range(cell.geometric_dimension()): - Lv[i] = derivative(L, v[i], dv) - for j in range(cell.geometric_dimension()): - Lvu[i, j] = derivative(Lv[i], u[j], du) - - for i in range(cell.geometric_dimension()): - for j in range(cell.geometric_dimension()): - form = Lvu[i, j]*dx + 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 - a = expand_indices(pf) - # print (i,j), str(a) + expand_indices(pf) k = Index() - for i in range(cell.geometric_dimension()): - for j in range(cell.geometric_dimension()): - actual = Lvu[i, j] - expected = du*u[i].dx(j)*dv + u[k]*du.dx(k)*dv + 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) + V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) - dv = TestFunction(V) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, V) - f = Coefficient(V, count=0) - g = Coefficient(V, count=1) - df = Coefficient(V, count=2) - dg = Coefficient(V, count=3) - u = Coefficient(V, count=4) + 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) + expected = (df * dv) * g + f * (dg * dv) - F = integrand*dx + 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() + 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 = VectorElement("Lagrange", triangle, 1) - VV = TensorElement("Lagrange", triangle, 1) + 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) + dv = TestFunction(v_space) - df = Coefficient(VV, count=0) - g = Coefficient(V, count=1) - f = Coefficient(V, count=2) - u = Coefficient(V, count=3) + 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] + expected = as_tensor(df[i2, i1] * dv[i1], (i2,))[i0] * g[i0] - F = integrand*dx + 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() + 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 = VectorElement("Lagrange", triangle, 1) - VV = TensorElement("Lagrange", triangle, 1) + V = FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1) + VV = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1) - dv = TestFunction(V) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + v_space = FunctionSpace(domain, V) + vv_space = FunctionSpace(domain, VV) - df = Coefficient(VV, count=0) - g = Coefficient(V, count=1) - dg = Coefficient(VV, count=2) - f = Coefficient(V, count=3) - u = Coefficient(V, count=4) + 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] + 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] + 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 + F = integrand * dx J = derivative(F, u, dv, cd) fd = compute_form_data(J) actual = fd.preprocessed_form.integrals()[0].integrand() @@ -542,17 +742,20 @@ def test_vector_coefficient_derivatives_of_product(self): # TODO: Add tests covering more cases, in particular mixed stuff + # --- Some actual forms def testHyperElasticity(self): cell = interval - element = FiniteElement("CG", cell, 2) - w = Coefficient(element) - v = TestFunction(element) - u = TrialFunction(element) - b = Constant(cell) - K = Constant(cell) + 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) @@ -560,10 +763,10 @@ def testHyperElasticity(self): E = dw + dw**2 / 2 E = variable(E) - Q = b*E**2 - psi = K*(exp(Q)-1) + Q = b * E**2 + psi = K * (exp(Q) - 1) - f = psi*dx + f = psi * dx F = derivative(f, w, v) J = derivative(F, w, u) @@ -581,18 +784,18 @@ def testHyperElasticity(self): # classes = set(c.__class__ for c in post_traversal(f_expression)) - Kv = .2 - bv = .3 - dw = .5 - dv = .7 - du = .11 - E = dw + dw**2 / 2. - Q = bv*E**2 + 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) + 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)) + 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,) @@ -610,7 +813,7 @@ def Nw(x, derivatives): fv2 = f_expression((0,), mapping) self.assertAlmostEqual(fv, fv2) - v, = form_data_F.original_form.arguments() + (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) @@ -623,40 +826,47 @@ def Nw(x, derivatives): def test_mass_derived_from_functional(self): cell = triangle - V = FiniteElement("CG", cell, 1) + V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) - v = TestFunction(V) - u = TrialFunction(V) - w = Coefficient(V) + 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 = (w**2 / 2) * dx + L = w * v * dx + # a = u*v*dx F = derivative(f, w, v) - J1 = derivative(L, w, u) - J2 = derivative(F, w, u) + 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("CG", cell, 1) + V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) - v = TestFunction(V) - u = TrialFunction(V) - f = Coefficient(V) - g = Coefficient(V) + 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) + 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) + 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) @@ -664,55 +874,54 @@ def test_derivative_replace_works_together(self): def test_index_simplification_handles_repeated_indices(self): - mesh = Mesh(VectorElement("P", quadrilateral, 1)) - V = FunctionSpace(mesh, TensorElement("DQ", quadrilateral, 0)) + 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, 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 + f = inner(A, v) * dx fd = compute_form_data(f, do_apply_geometry_lowering=True) - integral, = fd.preprocessed_form.integrals() + (integral,) = fd.preprocessed_form.integrals() assert integral.integrand().ufl_free_indices == () def test_index_simplification_reference_grad(self): - mesh = Mesh(VectorElement("P", quadrilateral, 1)) - i, = indices(1) + 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]))) + 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 = VectorElement("Lagrange", triangle, 1) - v = TestFunction(element) +# --- Scratch space - du = TrialFunction(element) - U = Coefficient(element) +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)]]) + 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) + 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 - a = derivative(L, U, du) + L = NS_a(U, v) * dx + _ = derivative(L, U, du) # TODO: assert something diff --git a/test/test_diff.py b/test/test_diff.py index d0eeeac57..5388eab43 100755 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -1,15 +1,30 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-17 -- 2014-10-14" import pytest -import math -from ufl import * -from ufl.constantvalue import as_ufl +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(): @@ -46,6 +61,7 @@ def f(v): def df(v): return as_ufl(1) + _test(f, df) @@ -55,6 +71,7 @@ def f(v): def df(v): return as_ufl(1) + _test(f, df) @@ -64,15 +81,17 @@ def f(v): def df(v): return as_ufl(3) + _test(f, df) def testPower(v): def f(v): - return v ** 3 + return v**3 def df(v): - return 3 * v ** 2 + return 3 * v**2 + _test(f, df) @@ -82,6 +101,7 @@ def f(v): def df(v): return as_ufl(1.0 / 3.0) + _test(f, df) @@ -90,7 +110,8 @@ def f(v): return 3.0 / v def df(v): - return -3.0 / v ** 2 + return -3.0 / v**2 + _test(f, df) @@ -100,6 +121,7 @@ def f(v): def df(v): return exp(v) + _test(f, df) @@ -109,6 +131,7 @@ def f(v): def df(v): return 1.0 / v + _test(f, df) @@ -118,6 +141,7 @@ def f(v): def df(v): return cos(v) + _test(f, df) @@ -127,6 +151,7 @@ def f(v): def df(v): return -sin(v) + _test(f, df) @@ -136,8 +161,10 @@ def f(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) @@ -155,36 +182,39 @@ def f(v): return atan(v) def df(v): - return 1 / (1.0 + v ** 2) + 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)) + a = as_vector((v, v**2, v**3)) b = as_vector((3, 4, 5)) - i, = indices(1) + (i,) = indices(1) return a[i] * b[i] def df(v): - return 3 + 4 * 2 * v + 5 * 3 * v ** 2 + return 3 + 4 * 2 * v + 5 * 3 * v**2 + _test(f, df) def testCoefficient(): - coord_elem = VectorElement("P", triangle, 1, dim=3) + coord_elem = FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1) mesh = Mesh(coord_elem) - V = FunctionSpace(mesh, FiniteElement("P", triangle, 1)) + V = FunctionSpace(mesh, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) v = Coefficient(V) - assert round(expand_derivatives(diff(v, v))-1.0, 7) == 0 + assert round(expand_derivatives(diff(v, v)) - 1.0, 7) == 0 def testDiffX(): cell = triangle - x = SpatialCoordinate(cell) + domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) + x = SpatialCoordinate(domain) f = x[0] ** 2 * x[1] ** 2 - i, = indices(1) + (i,) = indices(1) df1 = diff(f, x) df2 = as_vector(f.dx(i), i) @@ -198,4 +228,5 @@ def testDiffX(): assert round(df10 - 2 * 2 * 9, 7) == 0 assert round(df11 - 2 * 4 * 3, 7) == 0 + # TODO: More tests involving wrapper types and indices diff --git a/test/test_domains.py b/test/test_domains.py index fe18042ba..cf425cb2a 100755 --- a/test/test_domains.py +++ b/test/test_domains.py @@ -1,53 +1,51 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -""" -Tests of domain language and attaching domains to forms. -""" +"""Tests of domain language and attaching domains to forms.""" import pytest - -from ufl import * -from ufl.domain import as_domain, default_domain +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) - -from mockobjects import MockMesh, MockMeshFunction +all_cells = (interval, triangle, tetrahedron, quadrilateral, hexahedron) def test_construct_domains_from_cells(): for cell in all_cells: - D0 = Mesh(cell) - D1 = default_domain(cell) - D2 = as_domain(cell) - assert D0 is not D1 - assert D0 is not D2 - assert D1 is D2 - if 0: - print() - for D in (D1, D2): - print(('id', id(D))) - print(('str', str(D))) - print(('repr', repr(D))) - print() - assert D0 != D1 - assert D0 != D2 - assert D1 == D2 - - -def test_as_domain_from_cell_is_equal(): - for cell in all_cells: - D1 = as_domain(cell) - D2 = as_domain(cell) - assert D1 == D2 + d = cell.topological_dimension() + Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1)) def test_construct_domains_with_names(): for cell in all_cells: - D2 = Mesh(cell, ufl_id=2) - D3 = Mesh(cell, ufl_id=3) - D3b = Mesh(cell, ufl_id=3) + 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 @@ -55,49 +53,60 @@ def test_construct_domains_with_names(): def test_domains_sort_by_name(): # This ordering is rather arbitrary, but at least this shows sorting is # working - domains1 = [Mesh(cell, ufl_id=hash(cell.cellname())) - for cell in all_cells] - domains2 = [Mesh(cell, ufl_id=hash(cell.cellname())) - for cell in sorted(all_cells)] - sdomains = sorted(domains1, key=lambda D: (D.geometric_dimension(), - D.topological_dimension(), - D.ufl_cell(), - D.ufl_id())) + 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(interval) + D = Mesh(FiniteElement("Lagrange", interval, 1, (1,), identity_pullback, H1)) assert D.geometric_dimension() == 1 - D = Mesh(triangle) + D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) assert D.geometric_dimension() == 2 - D = Mesh(tetrahedron) + 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 = as_domain(triangle) + D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) - V = FiniteElement("CG", triangle, 1) - f = Coefficient(V) - assert f.ufl_domains() == (D, ) + 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, ) + assert M.ufl_domains() == (D,) def test_simple_domain_case(): # Creating domain from just cell with label like new dolfin will do - D = Mesh(triangle, ufl_id=3) + D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1), ufl_id=3) - V = FunctionSpace(D, FiniteElement("CG", D.ufl_cell(), 1)) + V = FunctionSpace(D, FiniteElement("Lagrange", D.ufl_cell(), 1, (), identity_pullback, "H1")) f = Coefficient(V) - assert f.ufl_domains() == (D, ) + assert f.ufl_domains() == (D,) M = f * dx - assert M.ufl_domains() == (D, ) + assert M.ufl_domains() == (D,) def test_creating_domains_with_coordinate_fields(): # FIXME: Rewrite for new approach @@ -105,17 +114,17 @@ def test_creating_domains_with_coordinate_fields(): # FIXME: Rewrite for new ap # Mesh with P2 representation of coordinates cell = triangle - P2 = VectorElement("CG", cell, 2) + P2 = FiniteElement("Lagrange", cell, 2, (2,), identity_pullback, H1) domain = Mesh(P2) # Piecewise linear function space over quadratic mesh - element = FiniteElement("CG", cell, 1) + 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, ) + 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()) @@ -128,346 +137,275 @@ def test_creating_domains_with_coordinate_fields(): # FIXME: Rewrite for new ap def test_join_domains(): from ufl.domain import join_domains + mesh7 = MockMesh(7) mesh8 = MockMesh(8) - triangle3 = Cell("triangle", geometric_dimension=3) - xa = VectorElement("CG", triangle, 1) - xb = VectorElement("CG", triangle, 1) + 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(triangle, ufl_id=7), - Mesh(triangle, ufl_id=7)])) - assert 1 == len(join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7), - Mesh(triangle, ufl_id=7, cargo=mesh7)])) + 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(triangle), Mesh(triangle)])) - assert 2 == len(join_domains([Mesh(triangle, ufl_id=7), - Mesh(triangle, ufl_id=8)])) - assert 2 == len(join_domains([Mesh(triangle, ufl_id=7), - Mesh(quadrilateral, ufl_id=8)])) + 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 cells require labeling - # self.assertRaises(UFLException, lambda: join_domains([Mesh(triangle), Mesh(triangle3)])) # FIXME: Figure out - # self.assertRaises(UFLException, lambda: join_domains([Mesh(triangle), - # Mesh(quadrilateral)])) # FIXME: Figure out - # Incompatible coordinates require labeling - xc = Coefficient(FunctionSpace(Mesh(triangle), VectorElement("CG", triangle, 1))) - xd = Coefficient(FunctionSpace(Mesh(triangle), VectorElement("CG", triangle, 1))) - with pytest.raises(UFLException): + 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(triangle, ufl_id=7, cargo=mesh7), - Mesh(triangle, ufl_id=8, cargo=mesh8)])) - assert 2 == len(join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7), - Mesh(quadrilateral, 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", 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(UFLException): - join_domains([Mesh(triangle), - Mesh(triangle3)]) - with pytest.raises(UFLException): - join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7), - Mesh(triangle3, ufl_id=8, cargo=mesh8)]) + 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(UFLException): - Mesh(triangle, ufl_id=7, cargo=mesh8) + 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(triangle, ufl_id=3), - None, Mesh(triangle, ufl_id=3), - None, Mesh(triangle, ufl_id=4)])) - assert 2 == len(join_domains([Mesh(triangle, ufl_id=7), None, - Mesh(quadrilateral, ufl_id=8)])) - assert None not in join_domains([Mesh(triangle3, ufl_id=7), None, - Mesh(tetrahedron, ufl_id=8)]) + 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(triangle) + D = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) - V = FunctionSpace(D, FiniteElement("CG", triangle, 1)) + V = FunctionSpace(D, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)) f = Coefficient(V) a = f * dx - ida, = compute_form_data(a).integral_data + (ida,) = compute_form_data(a).integral_data # Check some integral data assert ida.integral_type == "cell" - assert ida.subdomain_id == "otherwise" + 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) == type(itg2) + assert type(itg1) is type(itg2) assert itg1.ufl_element() == itg2.ufl_element() -def xtest_mixed_elements_on_overlapping_regions(): # Old sketch, not working - - # Create domain and both disjoint and overlapping regions - cell = tetrahedron - D = Mesh(cell, label='D') - DD = Region(D, (0, 4), "DD") - DL = Region(D, (1, 2), "DL") - DR = Region(D, (2, 3), "DR") - - # Create function spaces on D - V = FiniteElement("CG", D, 1) - VD = FiniteElement("DG", DD, 1) - VC = FiniteElement("R", DD, 0) - VL = VectorElement("DG", DL, 2) - VR = FiniteElement("CG", DR, 3) - - # Create mixed space over all these spaces - M = MixedElement(V, VD, VC, VL, VR) - - # Check that we can get the degree for each value component of the mixed - # space - assert M.degree(0) == 1 - assert M.degree(1) == 1 - assert M.degree(2) == 0 - - assert M.degree(3) == 2 # Vector element - assert M.degree(4) == 2 - assert M.degree(5) == 2 - - assert M.degree(6) == 3 - assert M.degree() == 3 - - # Check that we can get the domain for each value component of the mixed - # space - assert M.ufl_domain(0) == D - assert M.ufl_domain(1) == DD - assert M.ufl_domain(2) == DD - - assert M.ufl_domain(3) == DL # Vector element - assert M.ufl_domain(4) == DL - assert M.ufl_domain(5) == DL - - assert M.ufl_domain(6) == DR - # assert M.ufl_domain() == None # FIXME: What? - - # Create a mixed function and fetch components with names for more - # readable test code below - m = Coefficient(M) - md = m[1] - mc = m[2] - ml = as_vector((m[3], m[4], m[5])) - mr = m[6] - - # These should all work out fine with function and integration domains - # perfectly matching - a = m[0] ** 2 * dx(D) - ad = (md ** 2 + mc ** 2) * dx(DD) - al = ml ** 2 * dx(DL) - ar = mr ** 2 * dx(DR) - - # TODO: Check properties of forms, maybe by computing and inspecting form - # data. - - # These should all work out fine with because integration domains are - # contained in the function domains - ad = m[0] ** 2 * dx(DD) - al = m[0] ** 2 * dx(DL) - ar = m[0] ** 2 * dx("DR") - a0 = m[0] ** 2 * dx(0) - a12 = m[0] ** 2 * dx((1, 2)) - a3 = m[0] ** 2 * dx(3) - - # TODO: Check properties of forms, maybe by computing and inspecting form - # data. - - # These should fail because the functions are undefined on the integration domains - # self.assertRaises(UFLException, lambda: mr**2*dx(DD)) # FIXME: Make this fail - # self.assertRaises(UFLException, lambda: mr**2*dx(DD)) # FIXME: Make this - # fail - - -def xtest_form_domain_model(): # Old sketch, not working - # Create domains with different celltypes - # TODO: Figure out PyDOLFIN integration with Mesh - DA = Mesh(tetrahedron, label='DA') - DB = Mesh(hexahedron, label='DB') - - # Check python protocol behaviour - assert DA != DB - assert {DA, DA} == {DA} - assert {DB, DB} == {DB} - assert {DA, DB} == {DB, DA} - assert sorted((DA, DB, DA, DB)) == sorted((DB, DA, DA, DB)) - - # Check basic properties - assert DA.name() == 'DA' - assert DA.geometric_dimension() == 3 - assert DA.topological_dimension() == 3 - assert DA.ufl_cell() == tetrahedron - - # Check region/domain getters - assert DA.top_domain() == DA - assert DA.subdomain_ids() == None - # assert DA.region_names() == [] - # assert DA.regions() == [] - - # BDA = Boundary(DA) # TODO - # IDAB = Intersection(DA,DB) # TODO - # ODAB = Overlap(DA,DB) # TODO - - # Create overlapping regions of each domain - DAL = Region(DA, (1, 2), "DAL") - DAR = Region(DA, (2, 3), "DAR") - DBL = Region(DB, (0, 1), "DBL") - DBR = Region(DB, (1, 4), "DBR") - - # Check that regions are available through domains - # assert DA.region_names() == ['DAL', 'DAR'] - # assert DA.regions() == [DAL, DAR] - # assert DA["DAR"] == DAR - # assert DA["DAL"] == DAL - - # Create function spaces on DA - VA = FiniteElement("CG", DA, 1) - VAL = FiniteElement("CG", DAL, 1) - VAR = FiniteElement("CG", DAR, 1) - - # Create function spaces on DB - VB = FiniteElement("CG", DB, 1) - VBL = FiniteElement("CG", DBL, 1) - VBR = FiniteElement("CG", DBR, 1) - - # Check that regions are available through elements - assert VA.ufl_domain() == DA - assert VAL.ufl_domain() == DAL - assert VAR.ufl_domain() == DAR - - # Create functions in each space on DA - fa = Coefficient(VA) - fal = Coefficient(VAL) - far = Coefficient(VAR) - - # Create functions in each space on DB - fb = Coefficient(VB) - fbl = Coefficient(VBL) - fbr = Coefficient(VBR) - - # Checks of coefficient domains is covered well in the mixed element test - - # Create measure objects directly based on domain and region objects - dxa = dx(DA) - dxal = dx(DAL) - dxar = dx('DAR') # Get Region by name - - # Create measure proxy objects from strings and ints, requiring - # domains and regions to be part of their integrands - dxb = dx('DB') # Get Mesh by name - dxbl = dx(Region(DB, (1, 4), 'DBL2')) - # Provide a region with different name but same subdomain ids as - # DBL - dxbr = dx((1, 4)) - # Assume unique Mesh and provide subdomain ids explicitly - - # Not checking measure objects in detail, as long as - # they carry information to construct integrals below - # they are good to go. - - # Create integrals on each region with matching spaces and measures - Ma = fa * dxa - Mar = far * dxar - Mal = fal * dxal - Mb = fb * dxb - Mbr = fbr * dxbr - Mbl = fbl * dxbl - - # TODO: Check forms, by getting and inspecting domains somehow. - - # TODO: How do we express the distinction between "everywhere" and - # "nowhere"? subdomain_ids=None vs []? - - # Create forms from integrals over overlapping regions on same top domain - Marl = Mar + Mal - Mbrl = Mbr + Mbl - - # Create forms from integrals over top domain and regions - Mac = Ma + Marl - Mbc = Mb + Mbrl - - # Create forms from separate top domains - Mab = Ma + Mb - - # Create forms from separate top domains with overlapping regions - Mabrl = Mac + Mbc - - # self.assertFalse(True) # Getting here, but it's not bloody likely that - # everything above is actually working. Add assertions! - - -def xtest_subdomain_stuff(): # Old sketch, not working - D = Mesh(triangle) - - D1 = D[1] - D2 = D[2] - D3 = D[3] - - DL = Region(D, (D1, D2), 'DL') - DR = Region(D, (D2, D3), 'DR') - DM = Overlap(DL, DR) - - assert DM == D2 - - VL = VectorElement(DL, "CG", 1) - VR = FiniteElement(DR, "CG", 2) - V = VL * VR - - def sub_elements_on_subdomains(W): - # Get from W: (already there) - subelements = (VL, VR) - # Get from W: - subdomains = (D1, D2, D3) - # Build in W: - dom2elm = {D1: (VL,), - D2: (VL, VR), - D3: (VR,), } - # Build in W: - elm2dom = {VL: (D1, D2), - VR: (D2, D3)} - - # ElementSwitch represents joining of elements restricted to disjunct - # subdomains. - - # An element restricted to a domain union becomes a switch - # of elements restricted to each disjoint subdomain - VL_D1 = VectorElement(D1, "CG", 1) - VL_D2 = VectorElement(D2, "CG", 1) - VLalt = ElementSwitch({D1: VL_D1, - D2: VL_D2}) - # Ditto - VR_D2 = FiniteElement(D2, "CG", 2) - VR_D3 = FiniteElement(D3, "CG", 2) - VRalt = ElementSwitch({D2: VR_D2, - D3: VR_D3}) - # A mixed element of ElementSwitches is mixed only on the overlapping - # domains: - Valt1 = VLalt * VRalt - Valt2 = ElementSwitch({D1: VL_D1, - D2: VL_D2 * VR_D2, - D3: VR_D3}) - - ul, ur = TrialFunctions(V) - vl, vr = TestFunctions(V) - - # Implemented by user: - al = dot(ul, vl) * dx(DL) - ar = ur * vr * dx(DR) - - # Disjunctified by UFL: - alonly = dot(ul, vl) * dx(D1) - # integral_1 knows that only subelement VL is active - am = (dot(ul, vl) + ur * vr) * dx(D2) - # integral_2 knows that both subelements are active - aronly = ur * vr * \ - dx(D3) # integral_3 knows that only subelement VR is active +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 diff --git a/test/test_duals.py b/test/test_duals.py new file mode 100644 index 000000000..909538d49 --- /dev/null +++ b/test/test_duals.py @@ -0,0 +1,352 @@ +__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) diff --git a/test/test_elements.py b/test/test_elements.py deleted file mode 100755 index a83c24f69..000000000 --- a/test/test_elements.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -# Last changed: 2014-02-24 - -import pytest - -from ufl import * - -all_cells = (interval, triangle, tetrahedron, quadrilateral, hexahedron) - -# TODO: cover all valid element definitions - - -def test_scalar_galerkin(): - for cell in all_cells: - for p in range(1, 10): - for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"): - element = FiniteElement(family, cell, p) - assert element.value_shape() == () - assert element == eval(repr(element)) - for p in range(1, 10): - for family in ("TDG", "Discontinuous Taylor"): - element = FiniteElement(family, interval, p) - assert element.value_shape() == () - - -def test_vector_galerkin(): - for cell in all_cells: - dim = cell.geometric_dimension() - # shape = () if dim == 1 else (dim,) - shape = (dim,) - for p in range(1, 10): - for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"): - element = VectorElement(family, cell, p) - assert element.value_shape() == shape - assert element == eval(repr(element)) - for i in range(dim): - c = element.extract_component(i) - assert c[0] == () - - -def test_tensor_galerkin(): - for cell in all_cells: - dim = cell.geometric_dimension() - # shape = () if dim == 1 else (dim,dim) - shape = (dim, dim) - for p in range(1, 10): - for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"): - element = TensorElement(family, cell, p) - assert element.value_shape() == shape - assert element == eval(repr(element)) - for i in range(dim): - for j in range(dim): - c = element.extract_component((i, j)) - assert c[0] == () - - -def test_tensor_symmetry(): - for cell in all_cells: - dim = cell.geometric_dimension() - for p in range(1, 10): - for s in (None, True, {(0, 1): (1, 0)}): - # Symmetry dict is invalid for interval cell - if isinstance(s, dict) and cell == interval: - continue - - for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"): - if isinstance(s, dict): - element = TensorElement( - family, cell, p, shape=(dim, dim), symmetry=s) - else: - element = TensorElement(family, cell, p, symmetry=s) - assert element.value_shape(), (dim == dim) - assert element == eval(repr(element)) - for i in range(dim): - for j in range(dim): - c = element.extract_component((i, j)) - assert c[0] == () - - -def test_mixed_tensor_symmetries(): - from ufl.algorithms import expand_indices, expand_compounds - - S = FiniteElement('CG', triangle, 1) - V = VectorElement('CG', triangle, 1) - T = TensorElement('CG', triangle, 1, symmetry=True) - - # M has dimension 4+1, symmetries are 2->1 - M = T * S - P = Coefficient(M) - M = inner(P, P) * dx - - M2 = expand_indices(expand_compounds(M)) - assert '[1]' in str(M2) - assert '[2]' not in str(M2) - - # M has dimension 2+(1+4), symmetries are 5->4 - M = V * (S * T) - P = Coefficient(M) - M = inner(P, P) * dx - - M2 = expand_indices(expand_compounds(M)) - assert '[4]' in str(M2) - assert '[5]' not in str(M2) - - -def test_bdm(): - for cell in (triangle, tetrahedron): - dim = cell.geometric_dimension() - element = FiniteElement("BDM", cell, 1) - assert element.value_shape() == (dim,) - assert element == eval(repr(element)) - - -def test_vector_bdm(): - for cell in (triangle, tetrahedron): - dim = cell.geometric_dimension() - element = VectorElement("BDM", cell, 1) - assert element.value_shape(), (dim == dim) - assert element == eval(repr(element)) - - -def test_mtw(): - cell = triangle - element = FiniteElement("MTW", cell, 3) - assert element.value_shape() == (cell.geometric_dimension(), ) - assert element == eval(repr(element)) - assert element.mapping() == "contravariant Piola" - - -def test_mixed(): - for cell in (triangle, tetrahedron): - dim = cell.geometric_dimension() - velement = VectorElement("CG", cell, 2) - pelement = FiniteElement("CG", cell, 1) - TH1 = MixedElement(velement, pelement) - TH2 = velement * pelement - assert TH1.value_shape() == (dim + 1,) - assert TH2.value_shape() == (dim + 1,) - assert repr(TH1) == repr(TH2) - assert TH1 == eval(repr(TH2)) - assert TH2 == eval(repr(TH1)) - - -def test_nested_mixed(): - for cell in (triangle, tetrahedron): - dim = cell.geometric_dimension() - velement = VectorElement("CG", cell, 2) - pelement = FiniteElement("CG", cell, 1) - TH1 = MixedElement((velement, pelement), pelement) - TH2 = velement * pelement * pelement - assert TH1.value_shape() == (dim + 2,) - assert TH2.value_shape() == (dim + 2,) - assert repr(TH1) == repr(TH2) - assert TH1 == eval(repr(TH2)) - assert TH2 == eval(repr(TH1)) - - -def test_quadrature_scheme(): - for cell in (triangle, tetrahedron): - for q in (None, 1, 2, 3): - element = FiniteElement("CG", cell, 1, quad_scheme=q) - assert element.quadrature_scheme() == q - assert element == eval(repr(element)) - - -def test_missing_cell(): - # These special cases are here to allow missing - # cell in PyDOLFIN Constant and Expression - for cell in (triangle, None): - element = FiniteElement("Real", cell, 0) - assert element == eval(repr(element)) - element = FiniteElement("Undefined", cell, None) - assert element == eval(repr(element)) - element = VectorElement("Lagrange", cell, 1, dim=2) - assert element == eval(repr(element)) - element = TensorElement("DG", cell, 1, shape=(2, 2)) - assert element == eval(repr(element)) - element = TensorElement("DG L2", cell, 1, shape=(2, 2)) - assert element == eval(repr(element)) - -def test_invalid_degree(): - cell = triangle - for degree in (1, None): - element = FiniteElement("CG", cell, degree) - assert element == eval(repr(element)) - element = VectorElement("CG", cell, degree) - assert element == eval(repr(element)) - - -def test_lobatto(): - cell = interval - for degree in (1, 2, None): - element = FiniteElement("Lob", cell, degree) - assert element == eval(repr(element)) - - element = FiniteElement("Lobatto", cell, degree) - assert element == eval(repr(element)) - - -def test_radau(): - cell = interval - for degree in (0, 1, 2, None): - element = FiniteElement("Rad", cell, degree) - assert element == eval(repr(element)) - - element = FiniteElement("Radau", cell, degree) - assert element == eval(repr(element)) - - -def test_mse(): - for degree in (2, 3, 4, 5): - element = FiniteElement('EGL', interval, degree) - assert element == eval(repr(element)) - - element = FiniteElement('EGL-Edge', interval, degree - 1) - assert element == eval(repr(element)) - - element = FiniteElement('EGL-Edge L2', interval, degree - 1) - assert element == eval(repr(element)) - - for degree in (1, 2, 3, 4, 5): - element = FiniteElement('GLL', interval, degree) - assert element == eval(repr(element)) - - element = FiniteElement('GLL-Edge', interval, degree - 1) - assert element == eval(repr(element)) - - element = FiniteElement('GLL-Edge L2', interval, degree - 1) - assert element == eval(repr(element)) diff --git a/test/test_equals.py b/test/test_equals.py index abc9d0b55..b34a3c4de 100755 --- a/test/test_equals.py +++ b/test/test_equals.py @@ -1,28 +1,62 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- +"""Test of expression comparison.""" -""" -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 -import pytest -# This imports everything external code will see from ufl -from ufl import * +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) -def test_comparison_of_coefficients(): - V = FiniteElement("CG", triangle, 1) - U = FiniteElement("CG", triangle, 2) - Ub = FiniteElement("CG", triangle, 2) - v1 = Coefficient(V, count=1) - v1b = Coefficient(V, count=1) - v2 = Coefficient(V, count=2) - u1 = Coefficient(U, count=1) - u2 = Coefficient(U, count=2) - u2b = Coefficient(Ub, count=2) - - # Itentical objects + 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 @@ -36,11 +70,17 @@ def test_comparison_of_coefficients(): 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("CG", triangle, 1) - v = Coefficient(V) - u = Coefficient(V) + 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) @@ -50,9 +90,11 @@ def test_comparison_of_products(): def test_comparison_of_sums(): - V = FiniteElement("CG", triangle, 1) - v = Coefficient(V) - u = Coefficient(V) + 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) @@ -62,10 +104,12 @@ def test_comparison_of_sums(): def test_comparison_of_deeply_nested_expression(): - V = FiniteElement("CG", triangle, 1) - v = Coefficient(V, count=1) - u = Coefficient(V, count=1) - w = Coefficient(V, count=2) + 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): @@ -74,8 +118,9 @@ def build_expr(a): elif i % 3 == 1: a = a * i elif i % 3 == 2: - a = a ** i + a = a**i return a + a = build_expr(u) b = build_expr(v) c = build_expr(w) diff --git a/test/test_evaluate.py b/test/test_evaluate.py index 30e3a34f1..da01d452b 100755 --- a/test/test_evaluate.py +++ b/test/test_evaluate.py @@ -1,14 +1,42 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2009-02-13 -- 2009-02-13" -import pytest import math -from ufl import * +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(): @@ -27,14 +55,14 @@ def testZero(): def testIdentity(): cell = triangle - I = Identity(cell.geometric_dimension()) + ident = Identity(cell.topological_dimension()) - s = 123 * I[0, 0] + s = 123 * ident[0, 0] e = s((5, 7)) v = 123 assert e == v - s = 123 * I[1, 0] + s = 123 * ident[1, 0] e = s((5, 7)) v = 0 assert e == v @@ -42,7 +70,8 @@ def testIdentity(): def testCoords(): cell = triangle - x = SpatialCoordinate(cell) + 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 @@ -51,8 +80,10 @@ def testCoords(): def testFunction1(): cell = triangle - element = FiniteElement("CG", cell, 1) - f = Coefficient(element) + 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 @@ -61,11 +92,14 @@ def testFunction1(): def testFunction2(): cell = triangle - element = FiniteElement("CG", cell, 1) - f = Coefficient(element) + 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 @@ -74,11 +108,14 @@ def g(x): def testArgument2(): cell = triangle - element = FiniteElement("CG", cell, 1) - f = Argument(element, 2) + 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 @@ -87,37 +124,41 @@ def g(x): def testAlgebra(): cell = triangle - x = SpatialCoordinate(cell) + 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. + 7.) - 7 + 5. ** (7. / 2) + v = 3 * (5.0 + 7.0) - 7 + 5.0 ** (7.0 / 2) assert e == v def testIndexSum(): cell = triangle - x = SpatialCoordinate(cell) - i, = indices(1) + 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 + v = 5**2 + 7**2 assert e == v def testIndexSum2(): cell = triangle - x = SpatialCoordinate(cell) - I = Identity(cell.geometric_dimension()) + 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]) * I[i, j] + 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 + v = 5**2 + 7**2 assert e == v def testMathFunctions(): - x = SpatialCoordinate(triangle)[0] + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + x = SpatialCoordinate(domain)[0] s = sin(x) e = s((5, 7)) @@ -151,7 +192,8 @@ def testMathFunctions(): def testListTensor(): - x, y = SpatialCoordinate(triangle) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + x, y = SpatialCoordinate(domain) m = as_matrix([[x, y], [-y, -x]]) @@ -162,12 +204,13 @@ def testListTensor(): s = m[0, 0] * m[1, 0] * m[0, 1] * m[1, 1] e = s((5, 7)) - v = 5 ** 2 * 7 ** 2 + v = 5**2 * 7**2 assert e == v def testComponentTensor1(): - x = SpatialCoordinate(triangle) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + x = SpatialCoordinate(domain) m = as_vector(x[i], i) s = m[0] * m[1] @@ -177,7 +220,8 @@ def testComponentTensor1(): def testComponentTensor2(): - x = SpatialCoordinate(triangle) + 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)) @@ -189,7 +233,8 @@ def testComponentTensor2(): def testComponentTensor3(): - x = SpatialCoordinate(triangle) + 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)) @@ -201,37 +246,44 @@ def testComponentTensor3(): def testCoefficient(): - V = FiniteElement("CG", triangle, 1) - f = Coefficient(V) - e = f ** 2 + 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 + + assert e((3, 7), {f: eval_f}) == (3 * 7**2) ** 2 def testCoefficientDerivative(): - V = FiniteElement("CG", triangle, 1) - f = Coefficient(V) + 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 + (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 + assert e((3, 7), {f: eval_f}) == (5 * 7**2) ** 2 + (5 * 3 * 2 * 7) ** 2 def test_dot(): - x = SpatialCoordinate(triangle) + 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) @@ -239,7 +291,8 @@ def test_dot(): def test_inner(): - x = SpatialCoordinate(triangle) + 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)) @@ -248,48 +301,43 @@ def test_inner(): def test_outer(): - x = SpatialCoordinate(triangle) + 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) + v = 2 * (5**2 + 7**2) ** 2 * (2**2 + 3**2) assert e == v def test_cross(): - x = SpatialCoordinate(tetrahedron) + 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))], + [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 i in range(3): - for j in range(3): - cij = cross(t[i], t[j]) - dij = dot(cij, cij) - eij = dij(xv) - tni = dot(t[i], t[i])(xv) - tnj = dot(t[j], t[j])(xv) - vij = tni * tnj if i != j else 0 - assert eij == vij + 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(): - x = SpatialCoordinate(triangle) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s1 = dev(2 * xx) @@ -300,29 +348,32 @@ def xtest_dev(): def test_skew(): - x = SpatialCoordinate(triangle) + 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) + s2 = xx - xx.T e = inner(s1, s1)(xv) v = inner(s2, s2)(xv) assert e == v def test_sym(): - x = SpatialCoordinate(triangle) + 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) + s2 = xx + xx.T e = inner(s1, s1)(xv) v = inner(s2, s2)(xv) assert e == v def test_tr(): - x = SpatialCoordinate(triangle) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + x = SpatialCoordinate(domain) xv = (5, 7) xx = outer(x, x) s = tr(2 * xx) @@ -332,13 +383,14 @@ def test_tr(): def test_det2D(): - x = SpatialCoordinate(triangle) + 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) + v = 2**2 * (5 * b - 7 * a) assert e == v @@ -347,14 +399,10 @@ def xtest_det3D(): # FIXME 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))) + 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)) + v = 2**3 * (xv[0] * (b * f - e * c) - xv[1] * (a * f - c * d) + xv[2] * (a * e - b * d)) assert e == v diff --git a/test/test_expand_indices.py b/test/test_expand_indices.py index f0ae687c6..1cd9d8983 100755 --- a/test/test_expand_indices.py +++ b/test/test_expand_indices.py @@ -1,36 +1,62 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" __date__ = "2009-03-19 -- 2012-03-20" # Modified by Anders Logg, 2008 # Modified by Garth N. Wells, 2009 -import pytest import math -from pprint import * -from ufl import * -from ufl.algorithms import * +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.classes import Sum, Product +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) - velement = VectorElement("Lagrange", cell, 1) - telement = TensorElement("Lagrange", cell, 1) - self.sf = Coefficient(element) - self.sf2 = Coefficient(element) - self.vf = Coefficient(velement) - self.tf = Coefficient(telement) + 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. @@ -109,22 +135,22 @@ def TF(x, derivatives=()): def compare(self, f, value): debug = 0 if debug: - print(('f', f)) + print(("f", f)) g = expand_derivatives(f) if debug: - print(('g', g)) + print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 g = expand_indices(g) if debug: - print(('g', g)) + print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 g = renumber_indices(g) if debug: - print(('g', g)) + print(("g", g)) gv = g(self.x, self.mapping) assert abs(gv - value) < 1e-7 @@ -145,13 +171,13 @@ def test_basic_expand_indices(self, fixt): 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, 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(sf * (sf / 6), 1.5) compare(sin(sf), math.sin(3)) compare(cos(sf), math.cos(3)) compare(exp(sf), math.exp(3)) @@ -161,13 +187,13 @@ def test_basic_expand_indices(self, fixt): 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./6)) + 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)) @@ -177,13 +203,13 @@ def test_basic_expand_indices(self, fixt): 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./6)) + 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)) @@ -191,43 +217,42 @@ def test_basic_expand_indices(self, fixt): def test_expand_indices_index_sum(self, fixt): - sf = fixt.sf 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(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( + 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 - tf = fixt.tf 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) + 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): - sf = fixt.sf vf = fixt.vf - tf = fixt.tf compare = fixt.compare # Deformation gradient - I = Identity(2) + ident = Identity(2) u = vf - F = I + grad(u) + 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 @@ -240,25 +265,25 @@ def test_expand_indices_hyperelasticity(self, fixt): compare(F[1, 1], F11) J = det(F) - compare(J, (1 + 0.50)*(1 + 0.71) - 0.70*0.51) + compare(J, (1 + 0.50) * (1 + 0.71) - 0.70 * 0.51) # Strain tensors - C = F.T*F + 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 + 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-I)/2 - E00 = (C00-1)/2 - E01 = (C01)/2 - E10 = (C10)/2 - E11 = (C11-1)/2 + 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) @@ -270,8 +295,8 @@ def test_expand_indices_hyperelasticity(self, fixt): compare(Q, Qvalue) K = 0.5 - psi = (K/2)*exp(Q) - compare(psi, 0.25*math.exp(Qvalue)) + psi = (K / 2) * exp(Q) + compare(psi, 0.25 * math.exp(Qvalue)) def test_expand_indices_div_grad(self, fixt): @@ -291,18 +316,21 @@ def test_expand_indices_div_grad(self, fixt): 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))) + 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) + 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) + 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): @@ -319,30 +347,10 @@ def test_expand_indices_nabla_div_grad(self, fixt): 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) + 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) - - -def xtest_expand_indices_list_tensor_problem(self, fixt): - print() - print(('='*40)) - # TODO: This is the case marked in the expand_indices2 implementation - # as not working. Fix and then try expand_indices2 on other tests! - V = VectorElement("CG", triangle, 1) - w = Coefficient(V) - v = as_vector([w[0], 0]) - a = v[i]*w[i] - # TODO: Compare - print((type(a), str(a))) - A, comp = a.ufl_operands - print((type(A), str(A))) - print((type(comp), str(comp))) - - ei1 = expand_indices(a) - ei2 = expand_indices2(a) - print((str(ei1))) - print((str(ei2))) - print(('='*40)) - print() + compare( + inner(a, a), + (10.00 + 11.00) ** 2 + (10.01 + 11.01) ** 2 + (10.10 + 11.10) ** 2 + (10.11 + 11.11) ** 2, + ) diff --git a/test/test_external_operator.py b/test/test_external_operator.py new file mode 100644 index 000000000..ab996cce1 --- /dev/null +++ b/test/test_external_operator.py @@ -0,0 +1,518 @@ +"""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 diff --git a/test/test_extract_blocks.py b/test/test_extract_blocks.py new file mode 100644 index 000000000..0ce712418 --- /dev/null +++ b/test/test_extract_blocks.py @@ -0,0 +1,103 @@ +import 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 diff --git a/test/test_ffcforms.py b/test/test_ffcforms.py index 16fbcc5ba..9b913947b 100755 --- a/test/test_ffcforms.py +++ b/test/test_ffcforms.py @@ -1,8 +1,9 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -"""Unit tests including all demo forms from FFC 0.5.0. The forms are +"""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.""" +from the FFC notation in some places. +""" __author__ = "Anders Logg (logg@simula.no) et al." __date__ = "2008-04-09 -- 2008-09-26" @@ -12,197 +13,224 @@ # Examples copied from the FFC demo directory, examples contributed # by Johan Jansson, Kristian Oelgaard, Marie Rognes, and Garth Wells. -import pytest -from ufl import * +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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) - c = Constant("triangle") - d = VectorConstant("triangle") + c = Constant(domain) + d = VectorConstant(domain) - a = c * dot(grad(v), grad(u)) * dx + _ = c * dot(grad(v), grad(u)) * dx # FFC notation: L = dot(d, grad(v))*dx - L = inner(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) - element = VectorElement("Lagrange", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(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 + _ = 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) - element = FiniteElement("Lagrange", "tetrahedron", 1) - - v = Coefficient(element) - a = (v * v + dot(grad(v), grad(v))) * dx + v = Coefficient(space) + _ = (v * v + dot(grad(v), grad(v))) * dx def testEquation(): - - element = FiniteElement("Lagrange", "triangle", 1) + 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(element) - u = TrialFunction(element) - u0 = Coefficient(element) + 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) + _ = 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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - g = Coefficient(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 + _ = 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) - element = FiniteElement("Lagrange", "triangle", 1) + v = TestFunction(space) + u1 = TrialFunction(space) + u0 = Coefficient(space) + c = Coefficient(space) + f = Coefficient(space) + k = Constant(domain) - v = TestFunction(element) - u1 = TrialFunction(element) - u0 = Coefficient(element) - c = Coefficient(element) - f = Coefficient(element) - k = Constant("triangle") - - a = v * u1 * dx + k * c * dot(grad(v), grad(u1)) * dx - L = v * u0 * dx + k * v * f * dx + _ = 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) - - v = TestFunction(element) - u = TrialFunction(element) - - a = v * u * dx + 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) - - element = (P3 * P3) * (P3 * P3) + 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) - BDM = FiniteElement("Brezzi-Douglas-Marini", "triangle", q) - DG = FiniteElement("Discontinuous Lagrange", "triangle", q - 1) - - mixed_element = BDM * DG - - (tau, w) = TestFunctions(mixed_element) - (sigma, u) = TrialFunctions(mixed_element) + mixed_element = MixedElement([BDM, DG]) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, mixed_element) - f = Coefficient(DG) - - a = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx - L = w * f * dx + (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) - element = VectorElement("Lagrange", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) - - w = Coefficient(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 + _ = 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) - element = VectorElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - g = Coefficient(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 + _ = 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 + _ = inner(v, f) * dx + inner(v, g) * ds def testOptimization(): - - element = FiniteElement("Lagrange", "triangle", 3) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - - a = dot(grad(v), grad(u)) * dx - L = v * f * dx + 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(): - - element = FiniteElement("Lagrange", tetrahedron, 5) + FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) def testP5tri(): - - element = FiniteElement("Lagrange", triangle, 5) + 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) - element = FiniteElement("Discontinuous Lagrange", triangle, 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) + f = Coefficient(space) + n = FacetNormal(domain) - n = FacetNormal(triangle) - - # FFC notation: h = MeshSize("triangle"), not supported by UFL - h = Constant(triangle) - - gN = Coefficient(element) + # FFC notation: h = MeshSize(domain), not supported by UFL + h = Constant(domain) + gN = Coefficient(space) alpha = 4.0 gamma = 8.0 @@ -215,57 +243,57 @@ def testPoissonDG(): # - 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 + 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 - L = v * f * dx + v * gN * 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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(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 + _ = 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) - element = VectorElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(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 + _ = inner(grad(v), grad(u)) * dx # FFC notation: L = dot(v, f)*dx - L = inner(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. - P0 = FiniteElement("Discontinuous Lagrange", "triangle", 0) - P1 = FiniteElement("Lagrange", "triangle", 1) - P2 = FiniteElement("Lagrange", "triangle", 2) - - v = TestFunction(P1) - f = Coefficient(P1) + 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) @@ -275,74 +303,73 @@ def testProjection(): def testQuadratureElement(): - - element = FiniteElement("Lagrange", "triangle", 2) + element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) # FFC notation: - # QE = QuadratureElement("triangle", 3) - # sig = VectorQuadratureElement("triangle", 3) + # QE = QuadratureElement(triangle, 3) + # sig = VectorQuadratureElement(triangle, 3) - QE = FiniteElement("Quadrature", "triangle", 3) - sig = VectorElement("Quadrature", "triangle", 3) + QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2) + sig = FiniteElement("Quadrature", triangle, 3, (2,), identity_pullback, L2) - v = TestFunction(element) - u = TrialFunction(element) - u0 = Coefficient(element) - C = Coefficient(QE) - sig0 = Coefficient(sig) - f = Coefficient(element) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, 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 + 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(): +def testStokes(): # UFLException: Shape mismatch in sum. - P2 = VectorElement("Lagrange", "triangle", 2) - P1 = FiniteElement("Lagrange", "triangle", 1) - TH = P2 * P1 + P2 = FiniteElement("Lagrange", triangle, 2, (2,), identity_pullback, H1) + P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) + TH = MixedElement([P2, P1]) - (v, q) = TestFunctions(TH) - (u, p) = TrialFunctions(TH) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + th_space = FunctionSpace(domain, TH) + p2_space = FunctionSpace(domain, P2) - f = Coefficient(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 - a = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx - - L = dot(v, f) * dx + _ = (inner(grad(v), grad(u)) - div(v) * p + q * div(u)) * dx + _ = dot(v, f) * dx def testSubDomain(): - - element = FiniteElement("CG", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - - M = f * dx(2) + f * ds(5) + 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) - element = FiniteElement("CG", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) - - 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) + 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) - # P0 = FiniteElement("Discontinuous Lagrange", "triangle", 0) + # P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) + # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (), identity_pullback, L2) # # v = TestFunction(P1) # u = TrialFunction(P1) @@ -357,41 +384,50 @@ def testTensorWeightedPoisson(): # # a = dot(grad(v), mult(C, grad(u)))*dx - P1 = FiniteElement("Lagrange", "triangle", 1) - P0 = TensorElement("Discontinuous Lagrange", "triangle", 0, shape=(2, 2)) + P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) + P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2) - v = TestFunction(P1) - u = TrialFunction(P1) - C = Coefficient(P0) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + p1_space = FunctionSpace(domain, P1) + p0_space = FunctionSpace(domain, P0) - a = inner(grad(v), C * grad(u)) * dx + v = TestFunction(p1_space) + u = TrialFunction(p1_space) + C = Coefficient(p0_space) + _ = inner(grad(v), C * grad(u)) * dx def testVectorLaplaceGradCurl(): - - def HodgeLaplaceGradCurl(element, felement): - (tau, v) = TestFunctions(element) - (sigma, u) = TrialFunctions(element) - f = Coefficient(felement) + 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 + 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" + shape = tetrahedron order = 1 - GRAD = FiniteElement("Lagrange", shape, order) + GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1) # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1) - CURL = FiniteElement("N1curl", shape, order) + CURL = FiniteElement("N1curl", shape, order, (3,), covariant_piola, HCurl) - VectorLagrange = VectorElement("Lagrange", shape, order + 1) + VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3,), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", shape, 1, (3,), identity_pullback, H1)) - [a, L] = HodgeLaplaceGradCurl(GRAD * CURL, VectorLagrange) + [a, L] = HodgeLaplaceGradCurl( + FunctionSpace(domain, MixedElement([GRAD, CURL])), FunctionSpace(domain, VectorLagrange) + ) diff --git a/test/test_form.py b/test/test_form.py index 10141d51a..e83abcfcf 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -1,66 +1,96 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- import pytest -from ufl import * +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) + element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) return element @pytest.fixture -def mass(): +def domain(): cell = triangle - element = FiniteElement("Lagrange", cell, 1) - v = TestFunction(element) - u = TrialFunction(element) + 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(): +def stiffness(domain): cell = triangle - element = FiniteElement("Lagrange", cell, 1) - v = TestFunction(element) - u = TrialFunction(element) + 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(): +def convection(domain): cell = triangle - element = VectorElement("Lagrange", cell, 1) - v = TestFunction(element) - u = TrialFunction(element) - w = Coefficient(element) + 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(): +def load(domain): cell = triangle - element = FiniteElement("Lagrange", cell, 1) - f = Coefficient(element) - v = TestFunction(element) + 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(): +def boundary_load(domain): cell = triangle - element = FiniteElement("Lagrange", cell, 1) - f = Coefficient(element) - v = TestFunction(element) + 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() + (f,) = load.coefficients() assert v.number() == 0 assert u.number() == 1 @@ -74,10 +104,11 @@ def test_form_arguments(mass, stiffness, convection, load): assert ((f * v) * u * dx + (u * 3) * (v / 2) * dx(2)).arguments() == (v, u) -def test_form_coefficients(element): - v = TestFunction(element) - f = Coefficient(element) - g = Coefficient(element) +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,) @@ -88,8 +119,8 @@ def test_form_coefficients(element): def test_form_domains(): cell = triangle - domain = Mesh(cell) - element = FiniteElement("Lagrange", cell, 1) + 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) @@ -120,17 +151,47 @@ def test_form_integrals(mass, boundary_load): def test_form_call(): - V = FiniteElement("CG", triangle, 1) + 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 + 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 + a = u * v * dx M = eval("(a @ f) @ g") - assert M == g*f*dx + 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 diff --git a/test/test_grad.py b/test/test_grad.py deleted file mode 100755 index a8d5eebdb..000000000 --- a/test/test_grad.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -""" -Test use of grad in various situations. -""" - -import pytest - -# This imports everything external code will see from ufl -from ufl import * - -# from ufl.classes import ... -from ufl.algorithms import compute_form_data - - -def xtest_grad_div_curl_properties_in_1D(self): - _test_grad_div_curl_properties(self, interval) - - -def xtest_grad_div_curl_properties_in_2D(self): - _test_grad_div_curl_properties(self, triangle) - - -def xtest_grad_div_curl_properties_in_3D(self): - _test_grad_div_curl_properties(self, tetrahedron) - - -def _test_grad_div_curl_properties(self, cell): - d = cell.geometric_dimension() - - S = FiniteElement("CG", cell, 1) - V = VectorElement("CG", cell, 1) - T = TensorElement("CG", cell, 1) - - cs = Constant(cell) - cv = VectorConstant(cell) - ct = TensorConstant(cell) - - s = Coefficient(S) - v = Coefficient(V) - t = Coefficient(T) - - def eval_s(x, derivatives=()): - return sum(derivatives) - - def eval_v(x, derivatives=()): - return tuple(float(k)+sum(derivatives) for k in range(d)) - - def eval_t(x, derivatives=()): - return tuple(tuple(float(i*j)+sum(derivatives) - for i in range(d)) - for j in range(d)) - - mapping = {cs: eval_s, s: eval_s, - cv: eval_v, v: eval_v, - ct: eval_t, t: eval_t, } - x = tuple(1.0+float(k) for k in range(d)) - - assert s.ufl_shape == () - assert v.ufl_shape == (d,) - assert t.ufl_shape == (d, d) - - assert cs.ufl_shape == () - assert cv.ufl_shape == (d,) - assert ct.ufl_shape == (d, d) - - self.assertEqual(s(x, mapping=mapping), eval_s(x)) - self.assertEqual(v(x, mapping=mapping), eval_v(x)) - self.assertEqual(t(x, mapping=mapping), eval_t(x)) - - assert grad(s).ufl_shape == (d,) - assert grad(v).ufl_shape == (d, d) - assert grad(t).ufl_shape == (d, d, d) - - assert grad(cs).ufl_shape == (d,) - assert grad(cv).ufl_shape == (d, d) - assert grad(ct).ufl_shape == (d, d, d) - - self.assertEqual(grad(s)[0](x, mapping=mapping), eval_s(x, (0,))) - self.assertEqual(grad(v)[d-1, d-1](x, mapping=mapping), - eval_v(x, derivatives=(d-1,))[d-1]) - self.assertEqual(grad(t)[d-1, d-1, d-1](x, mapping=mapping), - eval_t(x, derivatives=(d-1,))[d-1][d-1]) - - assert div(grad(cs)).ufl_shape == () - assert div(grad(cv)).ufl_shape == (d,) - assert div(grad(ct)).ufl_shape == (d, d) - - assert s.dx(0).ufl_shape == () - assert v.dx(0).ufl_shape == (d,) - assert t.dx(0).ufl_shape == (d, d) - - assert s.dx(0 == 0).ufl_shape, () - assert v.dx(0 == 0).ufl_shape, (d,) - assert t.dx(0 == 0).ufl_shape, (d, d) - - i, j = indices(2) - assert s.dx(i).ufl_shape == () - assert v.dx(i).ufl_shape == (d,) - assert t.dx(i).ufl_shape == (d, d) - - assert s.dx(i).ufl_free_indices == (i.count(),) - assert v.dx(i).ufl_free_indices == (i.count(),) - assert t.dx(i).ufl_free_indices == (i.count(),) - - self.assertEqual(s.dx(i, j).ufl_shape, ()) - self.assertEqual(v.dx(i, j).ufl_shape, (d,)) - self.assertEqual(t.dx(i, j).ufl_shape, (d, d)) - - self.assertTrue(s.dx(i, j).ufl_free_indices == tuple(sorted([i.count(), j.count()]))) - self.assertTrue(v.dx(i, j).ufl_free_indices == tuple(sorted([i.count(), j.count()]))) - self.assertTrue(t.dx(i, j).ufl_free_indices == tuple(sorted([i.count(), j.count()]))) - - a0 = s.dx(0)*dx - a1 = s.dx(0)**2*dx - a2 = v.dx(0)**2*dx - a3 = t.dx(0)**2*dx - - a4 = inner(grad(s), grad(s))*dx - a5 = inner(grad(v), grad(v))*dx - a6 = inner(grad(t), grad(t))*dx - - a7 = inner(div(grad(s)), s)*dx - a8 = inner(div(grad(v)), v)*dx - a9 = inner(div(grad(t)), t)*dx - - fd0 = compute_form_data(a0) - fd1 = compute_form_data(a1) - fd2 = compute_form_data(a2) - fd3 = compute_form_data(a3) - - fd4 = compute_form_data(a4) - fd5 = compute_form_data(a5) - fd6 = compute_form_data(a6) - - fd7 = compute_form_data(a7) - fd8 = compute_form_data(a8) - fd9 = compute_form_data(a9) - - # self.assertTrue(False) # Just to show it runs diff --git a/test/test_illegal.py b/test/test_illegal.py index cca7478e4..acc2d33d5 100755 --- a/test/test_illegal.py +++ b/test/test_illegal.py @@ -1,88 +1,104 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - import pytest -from ufl import * -from ufl.algorithms import * +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) + return FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) +@pytest.fixture def velement(): - return VectorElement("Lagrange", "triangle", 1) + 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(): - return Argument(selement(), 2) +def a(sspace): + return Argument(sspace, 2) @pytest.fixture -def b(): - return Argument(selement(), 3) +def b(sspace): + return Argument(sspace, 3) @pytest.fixture -def v(): - return Argument(velement(), 4) +def v(vspace): + return Argument(vspace, 4) @pytest.fixture -def u(): - return Argument(velement(), 5) +def u(vspace): + return Argument(vspace, 5) @pytest.fixture -def f(): - return Coefficient(selement()) +def f(sspace): + return Coefficient(sspace) @pytest.fixture -def g(): - return Coefficient(selement()) +def g(sspace): + return Coefficient(sspace) @pytest.fixture -def vf(): - return Coefficient(velement()) +def vf(vspace): + return Coefficient(vspace) @pytest.fixture -def vg(): - return Coefficient(velement()) +def vg(vspace): + return Coefficient(vspace) def test_mul_v_u(v, u): - with pytest.raises(UFLException): + with pytest.raises(BaseException): v * u def test_mul_vf_u(vf, u): - with pytest.raises(UFLException): + with pytest.raises(BaseException): vf * u def test_mul_vf_vg(vf, vg): - with pytest.raises(UFLException): + with pytest.raises(BaseException): vf * vg def test_add_a_v(a, v): - with pytest.raises(UFLException): + with pytest.raises(BaseException): a + v def test_add_vf_b(vf, b): - with pytest.raises(UFLException): + with pytest.raises(BaseException): vf + b def test_add_vectorexpr_b(vg, v, u, vf, b): tmp = vg + v + u + vf - with pytest.raises(UFLException): + with pytest.raises(BaseException): tmp + b diff --git a/test/test_indexing.py b/test/test_indexing.py index 95d1029c7..8b4d69d29 100755 --- a/test/test_indexing.py +++ b/test/test_indexing.py @@ -1,25 +1,32 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- import pytest -from ufl import * -from ufl.classes import * + +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(): - x = SpatialCoordinate(triangle) +def x1(domain): + x = SpatialCoordinate(domain) return x @pytest.fixture -def x2(): - x = SpatialCoordinate(triangle) +def x2(domain): + x = SpatialCoordinate(domain) return outer(x, x) @pytest.fixture -def x3(): - x = SpatialCoordinate(triangle) +def x3(domain): + x = SpatialCoordinate(domain) return outer(outer(x, x), x) @@ -28,23 +35,16 @@ def test_annotated_literals(): assert z.ufl_shape == () assert z.ufl_free_indices == () assert z.ufl_index_dimensions == () - #assert z.free_indices() == () # Deprecated interface - #assert z.index_dimensions() == {} # Deprecated interface z = Zero((3,)) assert z.ufl_shape == (3,) assert z.ufl_free_indices == () assert z.ufl_index_dimensions == () - #assert z.free_indices() == () # Deprecated interface - #assert z.index_dimensions() == {} # Deprecated interface i = Index(count=2) j = Index(count=4) - # z = Zero((), (2, 4), (3, 5)) z = Zero((), (j, i), {i: 3, j: 5}) assert z.ufl_shape == () - #assert z.free_indices() == (i, j) # Deprecated interface - #assert z.index_dimensions() == {i: 3, j: 5} # Deprecated interface assert z.ufl_free_indices == (2, 4) assert z.ufl_index_dimensions == (3, 5) @@ -66,19 +66,3 @@ def test_fixed_indexing_of_expression(x1, x2, x3): mi = x000.ufl_operands[1] assert len(mi) == 3 assert mi.indices() == (FixedIndex(0),) * 3 - - -def test_indexed(): - pass - - -def test_indexsum(): - pass - - -def test_componenttensor(): - pass - - -def test_tensoralgebra(): - pass diff --git a/test/test_indices.py b/test/test_indices.py index 806fd2d3e..417648095 100755 --- a/test/test_indices.py +++ b/test/test_indices.py @@ -1,121 +1,145 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - import pytest -from ufl import * -# from ufl.indexutils import * -from ufl.algorithms import * +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 xtest_index_utils(self): - ii = indices(3) - assert ii == unique_indices(ii) - assert ii == unique_indices(ii+ii) - - assert () == repeated_indices(ii) - assert ii == repeated_indices(ii+ii) - - assert ii == shared_indices(ii, ii) - assert ii == shared_indices(ii, ii+ii) - assert ii == shared_indices(ii+ii, ii) - assert ii == shared_indices(ii+ii, ii+ii) - - assert ii == single_indices(ii) - assert () == single_indices(ii+ii) +# TODO: add more expressions to test as many possible combinations of +# index notation as feasible... def test_vector_indices(self): - element = VectorElement("CG", "triangle", 1) - u = Argument(element, 2) - f = Coefficient(element) - a = u[i]*f[i]*dx - b = u[j]*f[j]*dx + 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 = TensorElement("CG", "triangle", 1) - u = Argument(element, 2) - f = Coefficient(element) - a = u[i, j]*f[i, j]*dx - b = u[j, i]*f[i, j]*dx - c = u[j, i]*f[j, i]*dx - with pytest.raises(UFLException): - d = (u[i, i]+f[j, i])*dx + 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 = VectorElement("CG", "triangle", 1) - u = Argument(element, 2) - f = Coefficient(element) - a = u[i]+f[i] - with pytest.raises(UFLException): - a*dx + 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 = VectorElement("CG", "triangle", 1) - v = Argument(element, 2) - u = Argument(element, 3) - f = Coefficient(element) - a = u[j]+f[j]+v[j]+2*v[j]+exp(u[i]*u[i])/2*f[j] - with pytest.raises(UFLException): - a*dx + 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 = VectorElement("CG", "triangle", 1) - u = Argument(element, 2) - f = Coefficient(element) - with pytest.raises(UFLException): - a = u[i]+f[j] + 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 = VectorElement("CG", "triangle", 1) - v = Argument(element, 2) - u = Argument(element, 3) - f = Coefficient(element) - aarg = (u[i]+f[i])*v[i] - a = exp(aarg)*dx + 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 = VectorElement("CG", "triangle", 1) - v = Argument(element, 2) - u = Argument(element, 3) - f = Coefficient(element) - bfun = cos(f[0]) - left = u[i] + f[i] + 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() - b = left * right * dx + left * right * dx def test_indexed_function3(self): - element = VectorElement("CG", "triangle", 1) - v = Argument(element, 2) - u = Argument(element, 3) - f = Coefficient(element) - with pytest.raises(UFLException): - c = sin(u[i] + f[i])*dx + 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 = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) + 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 + w = v + u ww = vv + uu assert len(vv.ufl_shape) == 1 assert len(uu.ufl_shape) == 1 @@ -124,15 +148,17 @@ def test_vector_from_indices(self): def test_matrix_from_indices(self): - element = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) - - 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 + 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 @@ -140,9 +166,11 @@ def test_matrix_from_indices(self): def test_vector_from_list(self): - element = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) + 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]]) @@ -152,18 +180,20 @@ def test_vector_from_list(self): def test_matrix_from_list(self): - element = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) + 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]]]) + 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)) + B = as_matrix((v[k] * v[k]) * u[i] * v[j], (j, i)) # Test addition - C = A + A - C = B + B - D = A + B + 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 @@ -171,14 +201,16 @@ def test_matrix_from_list(self): def test_tensor(self): - element = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - g = Coefficient(element) + 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] + 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()} @@ -188,34 +220,34 @@ def test_tensor(self): self.assertSameIndices(C, ()) # get sub-matrix - A = C[:,:, 0, 0] + A = C[:, :, 0, 0] assert len(A.ufl_shape) == 2 self.assertSameIndices(A, ()) - A = C[:,:, i, j] + 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]]) - ww = f[i]*vv # this is well defined: ww = sum_i + f[i] * vv # this is well defined: ww = sum_i # illegal - with pytest.raises(UFLException): - vv = as_vector([u[i], v[j]]) + with pytest.raises(BaseException): + as_vector([u[i], v[j]]) # illegal - with pytest.raises(UFLException): - A = as_matrix([[u[0], u[1]], [v[0],]]) - - # ... + with pytest.raises(BaseException): + as_matrix([[u[0], u[1]], [v[0]]]) def test_indexed(self): - element = VectorElement("CG", "triangle", 1) - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - i, j, k, l = indices(4) + 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,)) @@ -230,11 +262,13 @@ def test_indexed(self): def test_spatial_derivative(self): cell = triangle - element = VectorElement("CG", cell, 1) - v = TestFunction(element) - u = TrialFunction(element) - i, j, k, l = indices(4) - d = cell.geometric_dimension() + 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, ()) @@ -246,7 +280,7 @@ def test_spatial_derivative(self): self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == () - a = (v[i]*u[j]).dx(i, j) + a = (v[i] * u[j]).dx(i, j) self.assertSameIndices(a, ()) self.assertIsInstance(a, IndexSum) assert a.ufl_shape == () @@ -262,7 +296,7 @@ def test_spatial_derivative(self): self.assertNotIsInstance(a, IndexSum) assert a.ufl_shape == () - a = (v[i]*u[j]).dx(0, 1) + 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 == () @@ -274,4 +308,33 @@ def test_spatial_derivative(self): def test_renumbering(self): - pass + """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 diff --git a/test/test_interpolate.py b/test/test_interpolate.py new file mode 100644 index 000000000..877f55a35 --- /dev/null +++ b/test/test_interpolate.py @@ -0,0 +1,182 @@ +"""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] diff --git a/test/test_lhs_rhs.py b/test/test_lhs_rhs.py index d73f956f6..bb99eab3a 100755 --- a/test/test_lhs_rhs.py +++ b/test/test_lhs_rhs.py @@ -1,63 +1,83 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Marie E. Rognes" # First added: 2011-11-09 # Last changed: 2011-11-09 -import pytest -from ufl import * +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("CG", interval, 1) - v = TestFunction(V) - u = TrialFunction(V) - w = Argument(V, 2) # This was 0, not sure why - f = Coefficient(V) + 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) + 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) + 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) + assert len(a.integrals()) == 1 + assert len(L.integrals()) == 0 F3 = action(F2, f) a, L = system(F3) - assert(len(L.integrals()) == 1) + assert len(L.integrals()) == 1 def test_lhs_rhs_derivatives(): - V = FiniteElement("CG", interval, 1) - v = TestFunction(V) - u = TrialFunction(V) - f = Coefficient(V) - - F0 = exp(f) * u * v * dx + v * dx + f * v * ds + exp(f)('+') * v * dS + 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) + assert len(a.integrals()) == 1 + assert len(L.integrals()) == 3 - F1 = derivative(F0, f) + derivative(F0, f) a, L = system(F0) def test_lhs_rhs_slightly_obscure(): - - V = FiniteElement("CG", interval, 1) - u = TrialFunction(V) - w = Argument(V, 2) - f = Constant(interval) + 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 @@ -65,9 +85,9 @@ def test_lhs_rhs_slightly_obscure(): # 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) + assert len(a.integrals()) == 1 + assert len(L.integrals()) == 0 F = f * w * dx a, L = system(F) - assert(len(L.integrals()) == 1) + assert len(L.integrals()) == 1 diff --git a/test/test_literals.py b/test/test_literals.py index ef8217a4f..41ca4460b 100755 --- a/test/test_literals.py +++ b/test/test_literals.py @@ -1,29 +1,16 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "Martin Sandve Alnæs" -__date__ = "2011-04-14 -- 2011-04-14" +__date__ = "2011-04-14" -import pytest +import numpy as np -from ufl import * +from ufl import PermutationSymbol, as_matrix, as_vector, indices, product from ufl.classes import Indexed -from ufl.constantvalue import Zero, FloatValue, IntValue, ComplexValue, as_ufl +from ufl.constantvalue import ComplexValue, FloatValue, IntValue, Zero, as_ufl def test_zero(self): z1 = Zero(()) - z2 = Zero(()) - z3 = as_ufl(0) - z4 = as_ufl(0.0) - z5 = FloatValue(0) - z6 = FloatValue(0.0) - - # self.assertTrue(z1 is z2) - # self.assertTrue(z1 is z3) - # self.assertTrue(z1 is z4) - # self.assertTrue(z1 is z5) - # self.assertTrue(z1 is z6) + assert z1 == z1 assert int(z1) == 0 assert float(z1) == 0.0 @@ -31,7 +18,8 @@ def test_zero(self): 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... + # 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)) @@ -43,6 +31,7 @@ def test_float(self): 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! @@ -50,6 +39,7 @@ def test_float(self): assert f2 == f4 assert f2 == f5 assert f2 == f6 + assert f2 == f7 def test_int(self): @@ -59,6 +49,7 @@ def test_int(self): 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! @@ -66,6 +57,7 @@ def test_int(self): assert f1 == f4 assert f1 == f5 assert f2 == f6 # Division produces a FloatValue + assert f1 == f7 def test_complex(self): @@ -76,6 +68,7 @@ def test_complex(self): 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 @@ -85,6 +78,7 @@ def test_complex(self): assert f5 == f2 + f3 assert f4 == f5 assert f6 + f7 == f2 + f3 + assert f4 == f8 def test_scalar_sums(self): @@ -92,7 +86,7 @@ def test_scalar_sums(self): s = [as_ufl(i) for i in range(n)] for i in range(n): - self.assertNotEqual(s[i], i+1) + self.assertNotEqual(s[i], i + 1) for i in range(n): assert s[i] == i @@ -113,9 +107,9 @@ def test_scalar_sums(self): 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 + assert 1 * s[5] == 5 + assert 2 * s[5] == 10 + assert s[6] / 3 == 2 def test_identity(self): @@ -129,7 +123,7 @@ def test_permutation_symbol_3(self): for i in range(3): for j in range(3): for k in range(3): - value = (j-i)*(k-i)*(k-j)/2 + 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) @@ -140,17 +134,18 @@ def test_permutation_symbol_3(self): 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 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)) + 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_vectors, unit_matrices + 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) diff --git a/test/test_measures.py b/test/test_measures.py index 6462885e1..4d81a4e25 100755 --- a/test/test_measures.py +++ b/test/test_measures.py @@ -1,21 +1,12 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -""" -Tests of the various ways Measure objects can be created and used. -""" - -import pytest - -# This imports everything external code will see from ufl -from ufl import * -from ufl.algorithms import compute_form_data - -# all_cells = (interval, triangle, tetrahedron, -# quadrilateral, hexahedron) +"""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: @@ -68,25 +59,25 @@ def test_construct_forms_from_default_measures(): assert dS_v.integral_type() == "interior_facet_vert" # Check that defaults are set properly - assert dx.ufl_domain() == None + assert dx.ufl_domain() is None assert dx.metadata() == {} # Check that we can create a basic form with default measure one = as_ufl(1) - a = one * dx(Mesh(triangle)) + 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", gdim) + cell = Cell("triangle") mymesh = MockMesh(9) - mydomain = Mesh(cell, ufl_id=9, cargo=mymesh) + mydomain = Mesh( + FiniteElement("Lagrange", cell, 1, (gdim,), identity_pullback, H1), ufl_id=9, cargo=mymesh + ) assert cell.topological_dimension() == tdim - assert cell.geometric_dimension() == gdim assert cell.cellname() == "triangle" assert mydomain.topological_dimension() == tdim assert mydomain.geometric_dimension() == gdim @@ -95,15 +86,12 @@ def test_foo(): assert mydomain.ufl_cargo() == mymesh # Define a coefficient for use in tests below - V = FunctionSpace(mydomain, FiniteElement("CG", cell, 1)) + 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) + 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 @@ -113,34 +101,34 @@ def test_foo(): # domain=None, # subdomain_id="everywhere", # metadata=None) - assert dx.ufl_domain() == 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() == None + 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() == None + assert dx5.ufl_domain() is None assert dx5.subdomain_id() == 5 # Check that original dx is untouched - assert dx.ufl_domain() == None + 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() == None - assert dx23.subdomain_id(), (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() == None + assert dxm.ufl_domain() is None assert dxm.subdomain_id() == "everywhere" # dxm = dx(metadata={"dummy":123}) @@ -148,11 +136,9 @@ def test_foo(): dxm = dx(metadata={"dummy": 123}) assert dxm.metadata() == {"dummy": 123} - assert dxm.ufl_domain() == None + assert dxm.ufl_domain() is None assert dxm.subdomain_id() == "everywhere" - dxi = dx(metadata={"quadrature_degree": 3}) - # Mock some dolfin data structures dx = Measure("dx") ds = Measure("ds") @@ -162,60 +148,47 @@ def test_foo(): exterior_facet_domains = MockMeshFunction(2, mesh) interior_facet_domains = MockMeshFunction(3, mesh) - assert dx[cell_domains] == dx(subdomain_data=cell_domains) - assert dx[cell_domains] != dx - assert dx[cell_domains] != dx[exterior_facet_domains] - - # Test definition of a custom measure with legacy bracket syntax - dxd = dx[cell_domains] - dsd = ds[exterior_facet_domains] - dSd = dS[interior_facet_domains] + 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() == None - assert dsd.ufl_domain() == None - assert dSd.ufl_domain() == None + 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 + 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() + (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 + assert len(Mx.subdomain_data()[mydomain]["cell"]) == 1 + assert Mx.subdomain_data()[mydomain]["cell"][0] == cell_domains - domain, = Ms.ufl_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 + assert len(Ms.subdomain_data()[mydomain]["exterior_facet"]) == 1 + assert Ms.subdomain_data()[mydomain]["exterior_facet"][0] == exterior_facet_domains - domain, = MS.ufl_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) + 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() + (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) + 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 diff --git a/test/test_mixed_function_space.py b/test/test_mixed_function_space.py index 34d445c84..476be6271 100644 --- a/test/test_mixed_function_space.py +++ b/test/test_mixed_function_space.py @@ -1,28 +1,32 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -from ufl import * - __authors__ = "Cecile Daversin Catty" __date__ = "2019-03-26 -- 2019-03-26" -import pytest - -from ufl import * -from ufl.domain import default_domain -from ufl.algorithms.formsplitter import extract_blocks - +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 = default_domain(tetrahedron) - domain_2d = default_domain(triangle) - domain_1d = default_domain(interval) + 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("CG", interval, 1) - f_2d = FiniteElement("CG", triangle, 1) - f_3d = FiniteElement("CG", tetrahedron, 1) + 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) @@ -31,50 +35,63 @@ def test_mixed_functionspace(self): # 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 ) + 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=V_3d) - dx2 = Measure("dx", domain=V_2d) - dx1 = Measure("dx", domain=V_1d) - + 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_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_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 ) + 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 ) + 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() diff --git a/test/test_new_ad.py b/test/test_new_ad.py index 99022fd80..38ef9de44 100755 --- a/test/test_new_ad.py +++ b/test/test_new_ad.py @@ -1,16 +1,36 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -import pytest - -from ufl import * - -from ufl.tensors import as_tensor -from ufl.classes import Grad -from ufl.algorithms import tree_format +from 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.algorithms.apply_derivatives import apply_derivatives, GenericDerivativeRuleset, \ - GradRuleset, VariableRuleset, GateauxDerivativeRuleset - +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. @@ -22,36 +42,40 @@ def test_apply_derivatives_doesnt_change_expression_without_derivatives(): cell = triangle - d = cell.geometric_dimension() - V0 = FiniteElement("DG", cell, 0) - V1 = FiniteElement("Lagrange", cell, 1) + 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) - I = Identity(d) - literals = [z, one, two, I] + ident = Identity(d) + literals = [z, one, two, ident] # Geometry - x = SpatialCoordinate(cell) - n = FacetNormal(cell) - volume = CellVolume(cell) + x = SpatialCoordinate(domain) + n = FacetNormal(domain) + volume = CellVolume(domain) geometry = [x, n, volume] # Arguments - v0 = TestFunction(V0) - v1 = TestFunction(V1) + v0 = TestFunction(v0_space) + v1 = TestFunction(v1_space) arguments = [v0, v1] # Coefficients - f0 = Coefficient(V0) - f1 = Coefficient(V1) + f0 = Coefficient(v0_space) + f1 = Coefficient(v1_space) coefficients = [f0, f1] # Expressions e0 = f0 + f1 - e1 = v0 * (f1/3 - f0**2) + e1 = v0 * (f1 / 3 - f0**2) e2 = exp(sin(cos(tan(ln(x[0]))))) expressions = [e0, e1, e2] @@ -64,43 +88,45 @@ def test_apply_derivatives_doesnt_change_expression_without_derivatives(): def test_literal_derivatives_are_zero(): cell = triangle - d = cell.geometric_dimension() + d = 2 # Literals one = as_ufl(1) two = as_ufl(2.0) - I = Identity(d) - E = PermutationSymbol(d) - literals = [one, two, I] + ident = Identity(d) + literals = [one, two, ident] # Generic ruleset handles literals directly: - for l in literals: - for sh in [(), (d,), (d, d+1)]: - assert GenericDerivativeRuleset(sh)(l) == zero(l.ufl_shape + sh) + 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(I) + v2 = variable(ident) variables = [v0, v1, v2] # Test literals via apply_derivatives and variable ruleset: - for l in literals: + for lit in literals: for v in variables: - assert apply_derivatives(diff(l, v)) == zero(l.ufl_shape + v.ufl_shape) - - V0 = FiniteElement("DG", cell, 0) - V1 = FiniteElement("Lagrange", cell, 1) - u0 = Coefficient(V0) - u1 = Coefficient(V1) - v0 = TestFunction(V0) - v1 = TestFunction(V1) + 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 l in literals: + for lit in literals: for u, v in args: - assert apply_derivatives(derivative(l, u, v)) == zero(l.ufl_shape + v.ufl_shape) + 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,)) @@ -109,68 +135,61 @@ def test_literal_derivatives_are_zero(): def test_grad_ruleset(): cell = triangle - d = cell.geometric_dimension() - - V0 = FiniteElement("DG", cell, 0) - V1 = FiniteElement("Lagrange", cell, 1) - V2 = FiniteElement("Lagrange", cell, 2) - W0 = VectorElement("DG", cell, 0) - W1 = VectorElement("Lagrange", cell, 1) - W2 = VectorElement("Lagrange", cell, 2) + 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) - I = Identity(d) - literals = [one, two, I] + ident = Identity(d) # Geometry - x = SpatialCoordinate(cell) - n = FacetNormal(cell) - volume = CellVolume(cell) - geometry = [x, n, volume] + x = SpatialCoordinate(domain) + n = FacetNormal(domain) + volume = CellVolume(domain) # Arguments - u0 = TestFunction(V0) - u1 = TestFunction(V1) + u0 = TestFunction(v0_space) + u1 = TestFunction(v1_space) arguments = [u0, u1] # Coefficients - r = Constant(cell) - vr = VectorConstant(cell) - f0 = Coefficient(V0) - f1 = Coefficient(V1) - f2 = Coefficient(V2) - vf0 = Coefficient(W0) - vf1 = Coefficient(W1) - vf2 = Coefficient(W2) - coefficients = [f0, f1, vf0, vf1] - - # Expressions - e0 = f0 + f1 - e1 = u0 * (f1/3 - f0**2) - e2 = exp(sin(cos(tan(ln(x[0]))))) - expressions = [e0, e1, e2] - - # Variables - v0 = variable(one) - v1 = variable(f1) - v2 = variable(f0*f1) - variables = [v0, v1, v2] + 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(I) == zero((d, d, 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) == I + assert rules(x) == ident # Arguments for u in arguments: @@ -203,10 +222,14 @@ def test_grad_ruleset(): 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)) + 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(): diff --git a/test/test_pickle.py b/test/test_pickle.py index 145df65b8..e2611c48a 100755 --- a/test/test_pickle.py +++ b/test/test_pickle.py @@ -1,5 +1,3 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- """Pickle all the unit test forms from FFC 0.5.0""" __author__ = "Anders Logg (logg@simula.no) et al." @@ -10,24 +8,57 @@ # Examples copied from the FFC demo directory, examples contributed # by Johan Jansson, Kristian Oelgaard, Marie Rognes, and Garth Wells. -import pytest -from ufl import * +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 -import pickle 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) - element = FiniteElement("Lagrange", "triangle", 1) + v = TestFunction(space) + u = TrialFunction(space) - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - - c = Constant("triangle") - d = VectorConstant("triangle") + c = Constant(domain) + d = VectorConstant(domain) a = c * dot(grad(v), grad(u)) * dx @@ -39,16 +70,17 @@ def testConstant(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = VectorElement("Lagrange", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) + v = TestFunction(space) + u = TrialFunction(space) def eps(v): # FFC notation: return grad(v) + transp(grad(v)) @@ -60,31 +92,33 @@ def eps(v): a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) - assert(a.signature() == a_restore.signature()) + 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) - element = FiniteElement("Lagrange", "tetrahedron", 1) - - v = Coefficient(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()) + assert a.signature() == a_restore.signature() def testEquation(): - - element = FiniteElement("Lagrange", "triangle", 1) + 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(element) - u = TrialFunction(element) - u0 = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) + u0 = Coefficient(space) F = v * (u - u0) * dx + k * dot(grad(v), grad(0.5 * (u0 + u))) * dx @@ -96,40 +130,41 @@ def testEquation(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - g = Coefficient(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 = 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()) + 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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u1 = TrialFunction(element) - u0 = Coefficient(element) - c = Coefficient(element) - f = Coefficient(element) - k = Constant("triangle") + 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 @@ -139,50 +174,52 @@ def testHeat(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = FiniteElement("Lagrange", "tetrahedron", 3) - - v = TestFunction(element) - u = TrialFunction(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()) + assert a.signature() == a_restore.signature() def testMixedMixedElement(): + P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1) - P3 = FiniteElement("Lagrange", "triangle", 3) - - element = (P3 * P3) * (P3 * P3) + element = MixedElement([[P3, P3], [P3, P3]]) element_pickle = pickle.dumps(element, p) element_restore = pickle.loads(element_pickle) - assert(element == element_restore) + assert element == element_restore def testMixedPoisson(): - q = 1 - BDM = FiniteElement("Brezzi-Douglas-Marini", "triangle", q) - DG = FiniteElement("Discontinuous Lagrange", "triangle", 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 = BDM * DG + 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_element) - (sigma, u) = TrialFunctions(mixed_element) + (tau, w) = TestFunctions(mixed_space) + (sigma, u) = TrialFunctions(mixed_space) - f = Coefficient(DG) + f = Coefficient(dg_space) a = (dot(tau, sigma) - div(tau) * u + w * div(sigma)) * dx L = w * f * dx @@ -192,18 +229,19 @@ def testMixedPoisson(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = VectorElement("Lagrange", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) + v = TestFunction(space) + u = TrialFunction(space) - w = Coefficient(element) + 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 @@ -211,17 +249,18 @@ def testNavierStokes(): a_pickle = pickle.dumps(a, p) a_restore = pickle.loads(a_pickle) - assert(a.signature() == a_restore.signature()) + 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) - element = VectorElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) - g = Coefficient(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 @@ -234,17 +273,18 @@ def testNeumannProblem(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = FiniteElement("Lagrange", "triangle", 3) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) + f = Coefficient(space) a = dot(grad(v), grad(u)) * dx L = v * f * dx @@ -254,42 +294,41 @@ def testOptimization(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + assert a.signature() == a_restore.signature() + assert L.signature() == L_restore.signature() def testP5tet(): - - element = FiniteElement("Lagrange", tetrahedron, 5) + element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1) element_pickle = pickle.dumps(element, p) element_restore = pickle.loads(element_pickle) - assert(element == element_restore) + assert element == element_restore def testP5tri(): - - element = FiniteElement("Lagrange", triangle, 5) + element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1) element_pickle = pickle.dumps(element, p) - element_restore = pickle.loads(element_pickle) + 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) - element = FiniteElement("Discontinuous Lagrange", triangle, 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) + f = Coefficient(space) - n = FacetNormal(triangle) + n = FacetNormal(domain) - # FFC notation: h = MeshSize("triangle"), not supported by UFL - h = Constant(triangle) + # FFC notation: h = MeshSize(domain), not supported by UFL + h = Constant(domain) - gN = Coefficient(element) + gN = Coefficient(space) alpha = 4.0 gamma = 8.0 @@ -303,13 +342,15 @@ def testPoissonDG(): # - 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 \ + 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 @@ -318,17 +359,18 @@ def testPoissonDG(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = FiniteElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(element) + v = TestFunction(space) + u = TrialFunction(space) + f = Coefficient(space) # Note: inner() also works a = dot(grad(v), grad(u)) * dx @@ -339,17 +381,18 @@ def testPoisson(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = VectorElement("Lagrange", "triangle", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(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 @@ -362,27 +405,31 @@ def testPoissonSystem(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + assert a.signature() == a_restore.signature() + assert L.signature() == L_restore.signature() def testQuadratureElement(): - - element = FiniteElement("Lagrange", "triangle", 2) + element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1) # FFC notation: - # QE = QuadratureElement("triangle", 3) - # sig = VectorQuadratureElement("triangle", 3) + # QE = QuadratureElement(triangle, 3) + # sig = VectorQuadratureElement(triangle, 3) - QE = FiniteElement("Quadrature", "triangle", 3) - sig = VectorElement("Quadrature", "triangle", 3) + QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2) + sig = FiniteElement("Quadrature", triangle, 3, (2,), identity_pullback, L2) - v = TestFunction(element) - u = TrialFunction(element) - u0 = Coefficient(element) - C = Coefficient(QE) - sig0 = Coefficient(sig) - f = Coefficient(element) + 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 @@ -392,22 +439,25 @@ def testQuadratureElement(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + assert a.signature() == a_restore.signature() + assert L.signature() == L_restore.signature() def testStokes(): - # UFLException: Shape mismatch in sum. - P2 = VectorElement("Lagrange", "triangle", 2) - P1 = FiniteElement("Lagrange", "triangle", 1) - TH = P2 * P1 + P2 = FiniteElement("Lagrange", triangle, 2, (2,), identity_pullback, H1) + P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) + TH = MixedElement([P2, P1]) - (v, q) = TestFunctions(TH) - (u, r) = TrialFunctions(TH) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + th_space = FunctionSpace(domain, TH) + p2_space = FunctionSpace(domain, P2) - f = Coefficient(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 @@ -420,47 +470,52 @@ def testStokes(): L_pickle = pickle.dumps(L, p) L_restore = pickle.loads(L_pickle) - assert(a.signature() == a_restore.signature()) - assert(L.signature() == L_restore.signature()) + 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) - element = FiniteElement("CG", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) - f = Coefficient(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()) + assert M.signature() == M_restore.signature() def testSubDomains(): - - element = FiniteElement("CG", "tetrahedron", 1) - - v = TestFunction(element) - u = TrialFunction(element) - - 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) + 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()) + assert a.signature() == a_restore.signature() def testTensorWeightedPoisson(): - # FFC notation: - # P1 = FiniteElement("Lagrange", "triangle", 1) - # P0 = FiniteElement("Discontinuous Lagrange", "triangle", 0) + # P1 = FiniteElement("Lagrange", triangle, 1) + # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0) # # v = TestFunction(P1) # u = TrialFunction(P1) @@ -475,73 +530,83 @@ def testTensorWeightedPoisson(): # # a = dot(grad(v), mult(C, grad(u)))*dx - P1 = FiniteElement("Lagrange", "triangle", 1) - P0 = TensorElement("Discontinuous Lagrange", "triangle", 0, shape=(2, 2)) + P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1) + P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2) - v = TestFunction(P1) - u = TrialFunction(P1) - C = Coefficient(P0) + 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()) + assert a.signature() == a_restore.signature() def testVectorLaplaceGradCurl(): - - def HodgeLaplaceGradCurl(element, felement): - (tau, v) = TestFunctions(element) - (sigma, u) = TrialFunctions(element) - f = Coefficient(felement) + 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 + 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" + shape = tetrahedron order = 1 - GRAD = FiniteElement("Lagrange", shape, order) + GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1) # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1) - CURL = FiniteElement("N1curl", shape, order) + CURL = FiniteElement("N1curl", shape, order, (3,), covariant_piola, HCurl) - VectorLagrange = VectorElement("Lagrange", shape, order + 1) + VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3,), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", shape, 1, (3,), identity_pullback, H1)) - [a, L] = HodgeLaplaceGradCurl(GRAD * CURL, VectorLagrange) + [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()) + 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) + 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) - element = FiniteElement("Lagrange", "tetrahedron", 3) - - v = TestFunction(element) - u = TrialFunction(element) + v = TestFunction(space) + u = TrialFunction(space) a = v * u * dx @@ -550,4 +615,4 @@ def testFormData(): form_data_pickle = pickle.dumps(form_data, p) form_data_restore = pickle.loads(form_data_pickle) - assert(str(form_data) == str(form_data_restore)) + assert str(form_data) == str(form_data_restore) diff --git a/test/test_piecewise_checks.py b/test/test_piecewise_checks.py index d9632f34c..8ba9b6d29 100755 --- a/test/test_piecewise_checks.py +++ b/test/test_piecewise_checks.py @@ -1,14 +1,44 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -""" -Test the is_cellwise_constant function on all relevant terminal types. -""" +"""Test the is_cellwise_constant function on all relevant terminal types.""" import pytest -from ufl import * -from ufl.classes import * + +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, + EdgeJacobian, + EdgeJacobianDeterminant, + EdgeJacobianInverse, + FacetJacobian, + FacetJacobianDeterminant, + FacetJacobianInverse, +) +from ufl.finiteelement import FiniteElement +from ufl.pullback import identity_pullback +from ufl.sobolevspace import H1, L2, HInf def get_domains(): @@ -20,13 +50,27 @@ def get_domains(): tetrahedron, hexahedron, ] - return [Mesh(cell) for cell in all_cells] + 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 = VectorElement("CG", D.ufl_cell(), 2) + V = FiniteElement( + "Lagrange", + D.ufl_cell(), + 2, + (D.ufl_cell().topological_dimension(),), + identity_pullback, + H1, + ) E = Mesh(V) domains_with_quadratic_coordinates.append(E) @@ -49,7 +93,14 @@ def domains(request): domains = get_domains() domains_with_linear_coordinates = [] for D in domains: - V = VectorElement("CG", D.ufl_cell(), 1) + V = FiniteElement( + "Lagrange", + D.ufl_cell(), + 1, + (D.ufl_cell().topological_dimension(),), + identity_pullback, + H1, + ) E = Mesh(V) domains_with_linear_coordinates.append(E) @@ -64,16 +115,29 @@ def affine_domains(request): triangle, tetrahedron, ] - affine_domains = [Mesh(cell) for cell in affine_cells] + 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 = VectorElement("CG", D.ufl_cell(), 1) + 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 + all_affine_domains = affine_domains + affine_domains_with_linear_coordinates return all_affine_domains[request.param] @@ -85,15 +149,28 @@ def affine_facet_domains(request): quadrilateral, tetrahedron, ] - affine_facet_domains = [Mesh(cell) for cell in affine_facet_cells] + 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 = VectorElement("CG", D.ufl_cell(), 1) + 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 + all_affine_facet_domains = affine_facet_domains + affine_facet_domains_with_linear_coordinates return all_affine_facet_domains[request.param] @@ -104,15 +181,28 @@ def nonaffine_domains(request): quadrilateral, hexahedron, ] - nonaffine_domains = [Mesh(cell) for cell in nonaffine_cells] + 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 = VectorElement("CG", D.ufl_cell(), 1) + 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 + all_nonaffine_domains = nonaffine_domains + nonaffine_domains_with_linear_coordinates return all_nonaffine_domains[request.param] @@ -122,15 +212,30 @@ def nonaffine_facet_domains(request): nonaffine_facet_cells = [ hexahedron, ] - nonaffine_facet_domains = [Mesh(cell) for cell in nonaffine_facet_cells] + 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 = VectorElement("CG", D.ufl_cell(), 1) + 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 + all_nonaffine_facet_domains = ( + nonaffine_facet_domains + nonaffine_facet_domains_with_linear_coordinates + ) return all_nonaffine_facet_domains[request.param] @@ -160,7 +265,7 @@ def test_coordinates_never_cellwise_constant(domains): def test_coordinates_never_cellwise_constant_vertex(): # The only exception here: - domains = Mesh(Cell("vertex", 3)) + 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) @@ -224,11 +329,20 @@ def test_coefficient_sometimes_cellwise_constant(domains_not_linear): e = Constant(domains_not_linear) assert is_cellwise_constant(e) - V = FiniteElement("DG", domains_not_linear.ufl_cell(), 0) - e = Coefficient(V) + 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("R", domains_not_linear.ufl_cell(), 0) - e = Coefficient(V) + + 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 @@ -240,8 +354,15 @@ def test_coefficient_sometimes_cellwise_constant(domains_not_linear): def test_coefficient_mostly_not_cellwise_constant(domains_not_linear): - V = FiniteElement("DG", domains_not_linear.ufl_cell(), 1) - e = Coefficient(V) + 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(V) + e = TestFunction(space) assert not is_cellwise_constant(e) diff --git a/test/test_reference_shapes.py b/test/test_reference_shapes.py index b5813560c..d5aec17a3 100755 --- a/test/test_reference_shapes.py +++ b/test/test_reference_shapes.py @@ -1,40 +1,59 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -import pytest - -from ufl import * +from 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", 3) - - V = FiniteElement("N1curl", cell, 1) - assert V.value_shape() == (3,) - assert V.reference_value_shape() == (2,) - - U = FiniteElement("RT", cell, 1) - assert U.value_shape() == (3,) - assert U.reference_value_shape() == (2,) - - W = FiniteElement("CG", cell, 1) - assert W.value_shape() == () - assert W.reference_value_shape() == () - - Q = VectorElement("CG", cell, 1) - assert Q.value_shape() == (3,) - assert Q.reference_value_shape() == (3,) - - T = TensorElement("CG", cell, 1) - assert T.value_shape() == (3, 3) - assert T.reference_value_shape() == (3, 3) - - S = TensorElement("CG", cell, 1, symmetry=True) - assert S.value_shape() == (3, 3) - assert S.reference_value_shape() == (6,) - - M = MixedElement(V, U, W) - assert M.value_shape() == (7,) - assert M.reference_value_shape() == (5,) + 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,) diff --git a/test/test_scratch.py b/test/test_scratch.py index 2b6673a31..5b429e532 100755 --- a/test/test_scratch.py +++ b/test/test_scratch.py @@ -1,33 +1,45 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -""" +"""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 pytest - -# This imports everything external code will see from ufl -from ufl import * -from ufl.log import error, warning +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 -# TODO: Import only what you need from classes and algorithms: -from ufl.classes import Grad, FormArgument, Zero, Indexed, FixedIndex, ListTensor - class MockForwardAD: - def __init__(self): self._w = () self._v = () class Obj: - def __init__(self): self._data = {} + self._cd = Obj() def grad(self, g): @@ -40,25 +52,25 @@ def grad(self, g): ngrads = 0 o = g while isinstance(o, Grad): - o, = o.ufl_operands + (o,) = o.ufl_operands ngrads += 1 if not isinstance(o, FormArgument): - error("Expecting gradient of a FormArgument, not %s" % repr(o)) + raise ValueError("Expecting gradient of a FormArgument, not %s" % repr(o)) def apply_grads(f): if not isinstance(f, FormArgument): - print((','*60)) + print(("," * 60)) print(f) print(o) print(g) - print((','*60)) - error("What?") + 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): + 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)) @@ -77,27 +89,26 @@ def analyse_variation_argument(v): vval, vcomp = v.ufl_operands vcomp = tuple(vcomp) else: - error("Expecting argument or component of argument.") + raise ValueError("Expecting argument or component of argument.") if not all(isinstance(k, FixedIndex) for k in vcomp): - error("Expecting only fixed indices in variation.") + 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] + 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) + 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): - + for w, v in zip(self._w, self._v): # Analyse differentiation variable coefficient if isinstance(w, FormArgument): if not w == o: @@ -113,18 +124,22 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): 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) + gprimesum = gprimesum + compute_gprimeterm( + ngrads, vval, vcomp, wshape, wcomp + ) else: if wshape != (): - error("Expecting scalar coefficient in this branch.") + 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? + 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 @@ -132,14 +147,14 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): continue assert isinstance(wval, FormArgument) if not all(isinstance(k, FixedIndex) for k in wcomp): - error("Expecting only fixed indices in differentiation variable.") + 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: - error("Expecting coefficient or component of coefficient.") + raise ValueError("Expecting coefficient or component of coefficient.") # FIXME: Handle other coefficient derivatives: oprimes = self._cd._data.get(o) @@ -149,26 +164,28 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): if self._cd._data: # TODO: Make it possible to silence this message in particular? # It may be good to have for debugging... - warning("Assuming d{%s}/d{%s} = 0." % (o, self._w)) + 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): - error("Got a tuple of arguments, " - "expecting a matching tuple of coefficient derivatives.") + 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): - error("FIXME: Figure out how to do this with ngrads") + 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] + prod = so * v[oi2] if oi1: gprimesum += as_tensor(prod, oi1) else: @@ -186,37 +203,44 @@ def test_unit_tensor(self): def test_unwrap_list_tensor(self): lt = as_tensor((1, 2)) - expected = [((0,), 1), - ((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), ] + 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), ] + 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("CG", triangle, 1) - u = Coefficient(U) - du = TestFunction(U) + 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,) @@ -238,9 +262,11 @@ def test__forward_coefficient_ad__grad_of_scalar_coefficient(self): def test__forward_coefficient_ad__grad_of_vector_coefficient(self): - V = VectorElement("CG", triangle, 1) - v = Coefficient(V) - dv = TestFunction(V) + 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,) @@ -262,9 +288,11 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient(self): def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_variation(self): - V = VectorElement("CG", triangle, 1) - v = Coefficient(V) - dv = TestFunction(V) + 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() @@ -275,19 +303,20 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var 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 + 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)) + 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()) + 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: @@ -300,26 +329,30 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var # 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))) + 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)) + 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()) + 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 = VectorElement("CG", triangle, 1) - v = Coefficient(V) - dv = TestFunction(V) +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() @@ -330,24 +363,25 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var 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 + 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)) + 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()) + 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._w = (v,) mad._v = (as_vector((dv[1], dv[0])),) f = grad(v) # Mathematically this would be the natural result: @@ -355,26 +389,28 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var # 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))) + 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)) + 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()) + 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 = TensorElement("CG", triangle, 1) - w = Coefficient(W) - dw = TestFunction(W) + 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,) @@ -396,9 +432,11 @@ def test__forward_coefficient_ad__grad_of_tensor_coefficient(self): def test__forward_coefficient_ad__grad_of_tensor_coefficient__with_component_variation(self): - W = TensorElement("CG", triangle, 1) - w = Coefficient(W) - dw = TestFunction(W) + 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() @@ -413,17 +451,16 @@ def test__forward_coefficient_ad__grad_of_tensor_coefficient__with_component_var 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 + 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)) + 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()) + self.assertEqual((inner(dg, dg) * dx).signature(), (inner(df, df) * dx).signature()) # assert dg == df # Expected to fail because of different index numbering diff --git a/test/test_signature.py b/test/test_signature.py index 840a9565d..aa8c57877 100755 --- a/test/test_signature.py +++ b/test/test_signature.py @@ -1,19 +1,37 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- -""" -Test the computation of form signatures. -""" - -import pytest - -from ufl import * - -from ufl.utils.dicts import EmptyDictType -from ufl.classes import MultiIndex, FixedIndex -from ufl.algorithms.signature import compute_multiindex_hashdata, \ - compute_terminal_hashdata - -from itertools import chain +"""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 @@ -27,25 +45,12 @@ def domain_numbering(*cells): renumbering = {} for i, cell in enumerate(cells): - domain = as_domain(cell) + d = cell.topological_dimension() + domain = Mesh(FiniteElement("Lagrange", cell, 1, (d,), identity_pullback, H1), ufl_id=i) renumbering[domain] = i return renumbering -def test_domain_signatures_of_cell2domains(self): - all_cells = (interval, quadrilateral, hexahedron, triangle, tetrahedron) - for cell in all_cells: - # Equality holds when constructing two domains from a cell: - assert as_domain(cell) == as_domain(cell) - # Hash value holds when constructing two domains from a cell: - assert hash(as_domain(cell)) == hash(as_domain(cell)) - # Signature data holds when constructing two domains from a cell: - D1 = as_domain(cell) - D2 = as_domain(cell) - self.assertEqual(D1._ufl_signature_data_({D1: 0}), - D2._ufl_signature_data_({D2: 0})) - - def compute_unique_terminal_hashdatas(hashdatas): count = 0 data = set() @@ -78,13 +83,15 @@ def test_terminal_hashdata_depends_on_literals(self): def forms(): i, j = indices(2) - for d in (2, 3): - domain = as_domain({2: triangle, 3: tetrahedron}[d]) + 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) - I = Identity(d) + ident = Identity(d) for fv in (1.1, 2.2): for iv in (5, 7): - expr = (I[0, j]*(fv*x[j]))**iv + expr = (ident[0, j] * (fv * x[j])) ** iv reprs.add(repr(expr)) hashes.add(hash(expr)) @@ -106,30 +113,31 @@ def test_terminal_hashdata_depends_on_geometry(self): def forms(): i, j = indices(2) cells = (triangle, tetrahedron) - for cell in cells: - - d = cell.geometric_dimension() - x = SpatialCoordinate(cell) - n = FacetNormal(cell) - h = CellDiameter(cell) - r = Circumradius(cell) - a = FacetArea(cell) - # s = CellSurfaceArea(cell) - v = CellVolume(cell) - I = Identity(d) + 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 = (I[0, j]*(q*w[j])) + 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 c == 2 * 4 * 2 # len(ws)*len(qs)*len(cells) assert d == c assert r == c assert h == c @@ -140,33 +148,68 @@ def forms(): def test_terminal_hashdata_depends_on_form_argument_properties(self): reprs = set() hashes = set() - nelm = 6 + nelm = 5 nreps = 2 # Data cells = (triangle, tetrahedron) degrees = (1, 2) - families = ("CG", "Lagrange", "DG") + families = (("Lagrange", H1), ("Lagrange", H1), ("Discontinuous Lagrange", L2)) def forms(): for rep in range(nreps): - for cell in cells: - d = cell.geometric_dimension() + 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 in families: - V = FiniteElement(family, cell, degree) - W = VectorElement(family, cell, degree) - W2 = VectorElement(family, cell, degree, dim=d+1) - T = TensorElement(family, cell, degree) - S = TensorElement(family, cell, degree, symmetry=True) - S2 = TensorElement(family, cell, degree, shape=(d, d), symmetry={(0, 0): (1, 1)}) - elements = [V, W, W2, T, S, S2] + 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(H, number=1) - c = Coefficient(H, count=1) + a = Argument(space, number=1) + c = Coefficient(space, count=1) renumbering = domain_numbering(*cells) renumbering[c] = 0 for f in (a, c): @@ -177,11 +220,10 @@ def forms(): 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 # Number of cases with repetitions + c1 = nreps * len(cells) * len(degrees) * len(families) * nelm * 2 assert c == c1 - c0 = len(cells) * len(degrees) * (len(families)-1) * nelm * 2 # Number of unique cases, "CG" == "Lagrange" - # c0 = len(cells) * len(degrees) * (len(families)) * nelm * 2 # Number of unique cases, "CG" != "Lagrange" + c0 = len(cells) * len(degrees) * (len(families) - 1) * nelm * 2 assert d == c0 assert r == c0 assert h == c0 @@ -199,11 +241,16 @@ def test_terminal_hashdata_does_not_depend_on_coefficient_count_values_only_orde def forms(): for rep in range(nreps): - for cell in cells: + 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("CG", cell, 2) - f = Coefficient(V, count=k) - g = Coefficient(V, count=k+2) + 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) @@ -235,11 +282,16 @@ def test_terminal_hashdata_does_depend_on_argument_number_values(self): def forms(): for rep in range(nreps): - for cell in cells: + 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("CG", cell, 2) - f = Argument(V, k) - g = Argument(V, k+2) + 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)) @@ -247,7 +299,9 @@ def forms(): 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 + 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 @@ -262,10 +316,12 @@ def test_domain_signature_data_does_not_depend_on_domain_label_value(self): s0s = set() s1s = set() s2s = set() - for cell in cells: - d0 = Mesh(cell) - d1 = Mesh(cell, ufl_id=1) - d2 = Mesh(cell, ufl_id=2) + 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}) @@ -284,14 +340,26 @@ def test_terminal_hashdata_does_not_depend_on_domain_label_value(self): hashes = set() ufl_ids = [1, 2] cells = [triangle, quadrilateral] - domains = [Mesh(cell, ufl_id=ufl_id) for cell in cells for ufl_id in ufl_ids] + 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("CG", domain.ufl_cell(), 2)) + V = FunctionSpace( + domain, + FiniteElement("Lagrange", domain.ufl_cell(), 2, (), identity_pullback, H1), + ) f = Coefficient(V, count=0) v = TestFunction(V) x = SpatialCoordinate(domain) @@ -308,8 +376,12 @@ def forms(): 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 + 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 @@ -346,9 +418,9 @@ def hashdatas(): 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 + 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): @@ -369,11 +441,12 @@ def hashdatas(): 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 + 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): @@ -383,28 +456,31 @@ def test_multiindex_hashdata_depends_on_the_order_indices_are_observed(self): 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. + # 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) - 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 + 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 c == nrep * 8 assert d == 5 - assert len(reprs) == nrep*5 - assert len(hashes) == nrep*5 + assert len(reprs) == nrep * 5 + assert len(hashes) == nrep * 5 def check_unique_signatures(forms): @@ -430,59 +506,75 @@ def check_unique_signatures(forms): def test_signature_is_affected_by_element_properties(self): def forms(): - for family in ("CG", "DG"): + 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) - u = Coefficient(V) - v = TestFunction(V) - x = SpatialCoordinate(cell) - w = as_vector([v]*x.ufl_shape[0]) - f = dot(w, u*x) - a = f*dx + 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("CG", cell, 1) - u = Coefficient(V) - a = u*dx(di) + 2*u*dx(dj) + 3*u*ds(dk) + 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 cell in (triangle, tetrahedron): + 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): - V = FiniteElement("CG", cell, 1) - W = VectorElement("CG", cell, 1) - u = Coefficient(V) - w = Coefficient(W) + 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) + f = vu * dot(vw, vu**k * vw) g = diff(f, vu) - h = dot(diff(f, vw), FacetNormal(cell)) - a = f*dx(1) + g*dx(2) + h*ds(0) + 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("CG", cell, 1) - f = Coefficient(V) - g = Coefficient(V) - M1 = f*dx(0) + g*dx(1) - M2 = g*dx(0) + f*dx(1) - M3 = g*dx(0) + g*dx(1) + 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()) @@ -491,18 +583,23 @@ def test_signature_of_form_depend_on_coefficient_numbering_across_integrals(self def test_signature_of_forms_change_with_operators(self): def forms(): for cell in (triangle, tetrahedron): - V = FiniteElement("CG", cell, 1) - u = Coefficient(V) - v = Coefficient(V) - fs = [(u*v)+(u/v), - (u+v)+(u/v), - (u+v)*(u/v), - (u*v)*(u*v), - (u+v)*(u*v), # (!) same - # (u*v)*(u+v), # (!) same - (u*v)+(u+v), - ] + 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 + a = f * dx yield a + check_unique_signatures(forms()) diff --git a/test/test_simplify.py b/test/test_simplify.py index 48dbfffe9..1bbef4ba5 100755 --- a/test/test_simplify.py +++ b/test/test_simplify.py @@ -1,43 +1,73 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -import pytest -from ufl.classes import Sum, Product import math -from ufl import * + +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("CG", triangle, 1) - v = TestFunction(element) - u = TrialFunction(element) - L = 0*v*dx - a = 0*(u*v)*dx - b = (0*u)*v*dx + 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("CG", triangle, 1) - f = Coefficient(element) - g = Coefficient(element) + 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 + b = f / 1 assert a == b # Test simplification of division by 1.0 a = f - b = f/1.0 + b = f / 1.0 assert a == b # Test simplification of division by of zero by something - a = 0/f - b = 0*f + a = 0 / f + b = 0 * f assert a == b # Test simplification of division by self (this simplification has been disabled) @@ -47,29 +77,33 @@ def test_divisions(self): def test_products(self): - element = FiniteElement("CG", triangle, 1) - f = Coefficient(element) - g = Coefficient(element) + 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) + 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 + 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("CG", triangle, 1) - f = Coefficient(element) - g = Coefficient(element) + 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 @@ -97,33 +131,34 @@ def test_sums(self): def test_mathfunctions(self): - for i in (0.1, 0.3, 0.9): - assert math.sin(i) == sin(i) - assert math.cos(i) == cos(i) - assert math.tan(i) == tan(i) - assert math.sinh(i) == sinh(i) - assert math.cosh(i) == cosh(i) - assert math.tanh(i) == tanh(i) - assert math.asin(i) == asin(i) - assert math.acos(i) == acos(i) - assert math.atan(i) == atan(i) - assert math.exp(i) == exp(i) - assert math.log(i) == ln(i) + 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 i == float(Max(i, i-1)) + assert a == float(max_value(a, a - 1)) # TODO: Implement automatic simplification of conditionals? - assert i == float(Min(i, i+1)) + assert a == float(min_value(a, a + 1)) def test_indexing(self): - u = VectorConstant(triangle) - v = VectorConstant(triangle) + 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] + Bij = u[i] * v[j] Bij2 = as_tensor(Bij, (i, j))[i, j] - Bij3 = as_tensor(Bij, (i, j)) + as_tensor(Bij, (i, j)) assert Bij2 == Bij diff --git a/test/test_sobolevspace.py b/test/test_sobolevspace.py index fb579a2f7..b21760f63 100755 --- a/test/test_sobolevspace.py +++ b/test/test_sobolevspace.py @@ -1,17 +1,15 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - __authors__ = "David Ham" __date__ = "2014-03-04" -import pytest -from ufl import (EnrichedElement, TensorProductElement, - FiniteElement, triangle, interval, - quadrilateral, HDiv, HCurl) -from ufl.sobolevspace import SobolevSpace, DirectionalSobolevSpace -from ufl import H2, H1, HDiv, HCurl, L2, HInf 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 @@ -30,10 +28,10 @@ def test_inclusion(): - assert H2 < H1 # Inclusion - assert not H2 > H1 # Not included + assert H2 < H1 # Inclusion + assert not H2 > H1 # Not included assert HDiv <= HDiv # Reflexivity - assert H2 < L2 # Transitivity + assert H2 < L2 # Transitivity assert H1 > H2 assert L2 > H1 @@ -67,18 +65,9 @@ def xtest_contains_mixed(): def test_contains_l2(): l2_elements = [ - FiniteElement("DG", triangle, 0), - FiniteElement("DG", triangle, 1), - FiniteElement("DG", triangle, 2), - FiniteElement("CR", triangle, 1), - # Tensor product elements: - TensorProductElement(FiniteElement("DG", interval, 1), - FiniteElement("DG", interval, 1)), - TensorProductElement(FiniteElement("DG", interval, 1), - FiniteElement("CG", interval, 2)), - # Enriched element: - EnrichedElement(FiniteElement("DG", triangle, 1), - FiniteElement("B", triangle, 3)) + 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 @@ -94,19 +83,11 @@ def test_contains_l2(): def test_contains_h1(): h1_elements = [ # Standard Lagrange elements: - FiniteElement("CG", triangle, 1), - FiniteElement("CG", triangle, 2), + FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1), + FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1), # Some special elements: - FiniteElement("HER", triangle), - FiniteElement("MTW", triangle), - # Tensor product elements: - TensorProductElement(FiniteElement("CG", interval, 1), - FiniteElement("CG", interval, 1)), - TensorProductElement(FiniteElement("CG", interval, 2), - FiniteElement("CG", interval, 2)), - # Enriched elements: - EnrichedElement(FiniteElement("CG", triangle, 2), - FiniteElement("B", triangle, 3)) + 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 @@ -121,8 +102,8 @@ def test_contains_h1(): def test_contains_h2(): h2_elements = [ - FiniteElement("ARG", triangle, 5), - FiniteElement("MOR", triangle, 2), + FiniteElement("ARG", triangle, 5, (), "custom", H2), + FiniteElement("MOR", triangle, 2, (), "custom", H2), ] for h2_element in h2_elements: assert h2_element in H2 @@ -136,9 +117,7 @@ def test_contains_h2(): def test_contains_hinf(): - hinf_elements = [ - FiniteElement("R", triangle, 0) - ] + 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 @@ -153,16 +132,9 @@ def test_contains_hinf(): def test_contains_hdiv(): hdiv_elements = [ - FiniteElement("RT", triangle, 1), - FiniteElement("BDM", triangle, 1), - FiniteElement("BDFM", triangle, 2), - # HDiv elements: - HDiv(TensorProductElement(FiniteElement("DG", triangle, 1), - FiniteElement("CG", interval, 2))), - HDiv(TensorProductElement(FiniteElement("RT", triangle, 1), - FiniteElement("DG", interval, 1))), - HDiv(TensorProductElement(FiniteElement("N1curl", triangle, 1), - FiniteElement("DG", interval, 1))) + 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 @@ -177,63 +149,8 @@ def test_contains_hdiv(): def test_contains_hcurl(): hcurl_elements = [ - FiniteElement("N1curl", triangle, 1), - FiniteElement("N2curl", triangle, 1), - # HCurl elements: - HCurl(TensorProductElement(FiniteElement("CG", triangle, 1), - FiniteElement("DG", interval, 1))), - HCurl(TensorProductElement(FiniteElement("N1curl", triangle, 1), - FiniteElement("CG", interval, 1))), - HCurl(TensorProductElement(FiniteElement("RT", triangle, 1), - FiniteElement("CG", interval, 1))) - ] - 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 - - -def test_enriched_elements_hdiv(): - A = FiniteElement("CG", interval, 1) - B = FiniteElement("DG", interval, 0) - AxB = TensorProductElement(A, B) - BxA = TensorProductElement(B, A) - C = FiniteElement("RTCF", quadrilateral, 1) - D = FiniteElement("DQ", quadrilateral, 0) - Q1 = TensorProductElement(C, B) - Q2 = TensorProductElement(D, A) - hdiv_elements = [ - EnrichedElement(HDiv(AxB), HDiv(BxA)), - EnrichedElement(HDiv(Q1), HDiv(Q2)) - ] - 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_enriched_elements_hcurl(): - A = FiniteElement("CG", interval, 1) - B = FiniteElement("DG", interval, 0) - AxB = TensorProductElement(A, B) - BxA = TensorProductElement(B, A) - C = FiniteElement("RTCE", quadrilateral, 1) - D = FiniteElement("DQ", quadrilateral, 0) - Q1 = TensorProductElement(C, B) - Q2 = TensorProductElement(D, A) - hcurl_elements = [ - EnrichedElement(HCurl(AxB), HCurl(BxA)), - EnrichedElement(HCurl(Q1), HCurl(Q2)) + 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 @@ -244,31 +161,3 @@ def test_enriched_elements_hcurl(): assert hcurl_element not in HDiv assert hcurl_element not in H2 assert hcurl_element not in H2dx2dy - - -def test_varying_continuity_elements(): - P1DG_t = FiniteElement("DG", triangle, 1) - P1DG_i = FiniteElement("DG", interval, 1) - P1 = FiniteElement("CG", interval, 1) - P2 = FiniteElement("CG", interval, 2) - P3 = FiniteElement("CG", interval, 3) - RT1 = FiniteElement("RT", triangle, 1) - ARG = FiniteElement("ARG", triangle, 5) - - # Tensor product elements - P1DGP2 = TensorProductElement(P1DG_t, P2) - P1P1DG = TensorProductElement(P1, P1DG_i) - P1DGP1 = TensorProductElement(P1DG_i, P1) - RT1DG1 = TensorProductElement(RT1, P1DG_i) - P2P3 = TensorProductElement(P2, P3) - ARGP3 = TensorProductElement(ARG, P3) - - assert P1DGP2 in H1dz and P1DGP2 in L2 - assert P1DGP2 not in H1dh - assert P1DGP1 in H1dy and P1DGP2 in L2 - assert P1P1DG in H1dx and P1P1DG in L2 - assert P1P1DG not in H1dx1dy - assert RT1DG1 in H000 and RT1DG1 in L2 - assert P2P3 in H1dx1dy and P2P3 in H1 - assert ARG in H2dx2dy - assert ARGP3 in H2dhH1dz diff --git a/test/test_split.py b/test/test_split.py index 380def3dc..91eb4b0b6 100755 --- a/test/test_split.py +++ b/test/test_split.py @@ -1,63 +1,76 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- - -from ufl import * - __authors__ = "Martin Sandve Alnæs" __date__ = "2009-03-14 -- 2009-03-14" -import pytest - -from ufl import * +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.geometric_dimension() - f = FiniteElement("CG", cell, 1) - v = VectorElement("CG", cell, 1) - w = VectorElement("CG", cell, 1, dim=d+1) - t = TensorElement("CG", cell, 1) - s = TensorElement("CG", cell, 1, symmetry=True) - r = TensorElement("CG", cell, 1, symmetry={(1, 0): (0, 1)}, shape=(d, d)) - m = MixedElement(f, v, w, t, s, r) + 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).ufl_shape - assert (d,) == Coefficient(v).ufl_shape - assert (d+1,) == Coefficient(w).ufl_shape - assert (d, d) == Coefficient(t).ufl_shape - assert (d, d) == Coefficient(s).ufl_shape - assert (d, d) == Coefficient(r).ufl_shape + 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).ufl_shape + assert (3 * d * d + 2 * d + 2,) == Coefficient(m_space).ufl_shape # Shapes of subelements are reproduced: - g = Coefficient(m) - s, = g.ufl_shape + 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 = 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).ufl_shape - assert (d+d,) == Coefficient(v2).ufl_shape - assert (2*d*d,) == Coefficient(m2).ufl_shape + # 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(f*v) + 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(f*(f*v)) + 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((v*f)*(f*v)) - assert split(t) == (as_vector((t[0], t[1], t[2])), - as_vector((t[3], t[4], t[5]))) + 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]) diff --git a/test/test_str.py b/test/test_str.py index f5021a042..085aee7b2 100755 --- a/test/test_str.py +++ b/test/test_str.py @@ -1,7 +1,25 @@ -# -*- coding: utf-8 -*- - -from ufl import * -from ufl.classes import * +from 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): @@ -13,10 +31,11 @@ def test_str_float_value(self): def test_str_zero(self): - x = SpatialCoordinate(triangle) + 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,))" + 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): @@ -25,38 +44,45 @@ def test_str_index(self): def test_str_coordinate(self): - assert str(SpatialCoordinate(triangle)) == "x" - assert str(SpatialCoordinate(triangle)[0]) == "x[0]" + 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): - assert str(FacetNormal(triangle)) == "n" - assert str(FacetNormal(triangle)[0]) == "n[0]" + 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): - assert str(Circumradius(triangle)) == "circumradius" + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + assert str(Circumradius(domain)) == "circumradius" def test_str_diameter(self): - assert str(CellDiameter(triangle)) == "diameter" - - -# def test_str_cellsurfacearea(self): -# assert str(CellSurfaceArea(triangle)) == "surfacearea" + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + assert str(CellDiameter(domain)) == "diameter" def test_str_facetarea(self): - assert str(FacetArea(triangle)) == "facetarea" + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + assert str(FacetArea(domain)) == "facetarea" def test_str_volume(self): - assert str(CellVolume(triangle)) == "volume" + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + assert str(CellVolume(domain)) == "volume" def test_str_scalar_argument(self): - v = TestFunction(FiniteElement("CG", triangle, 1)) - u = TrialFunction(FiniteElement("CG", triangle, 1)) + 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" @@ -69,37 +95,48 @@ def test_str_scalar_argument(self): def test_str_list_vector(): - x, y, z = SpatialCoordinate(tetrahedron) + 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(): - x, y, z = SpatialCoordinate(tetrahedron) + 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(): - x, y = SpatialCoordinate(triangle) - 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) + 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(): - x, y = SpatialCoordinate(triangle) - v = as_matrix(((2*x, 3*y), - (0, 0))) - a = str(2*x) - b = str(3*y) + 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) == "" diff --git a/test/test_strip_forms.py b/test/test_strip_forms.py index 83eaa7ca7..9b5a0fff9 100644 --- a/test/test_strip_forms.py +++ b/test/test_strip_forms.py @@ -1,28 +1,37 @@ import gc import sys -import pytest - -from ufl import * -from ufl.algorithms import strip_terminal_data, replace_terminal_data +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 attach_operators_from_hash_data - +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_operators_from_hash_data @attach_ufl_id -class AugmentedMesh(Mesh): +class AugmentedMesh(Mesh, UFLObject): def __init__(self, *args, data): super().__init__(*args) self.data = data -@attach_operators_from_hash_data -class AugmentedFunctionSpace(FunctionSpace): +class AugmentedFunctionSpace(FunctionSpace, UFLObject): def __init__(self, *args, data): super().__init__(*args) self.data = data @@ -53,8 +62,10 @@ def test_strip_form_arguments_strips_data_refs(): assert sys.getrefcount(const_data) == MIN_REF_COUNT cell = triangle - domain = AugmentedMesh(cell, data=mesh_data) - element = FiniteElement("Lagrange", cell, 1) + 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) @@ -62,7 +73,7 @@ def test_strip_form_arguments_strips_data_refs(): f = AugmentedCoefficient(V, data=coeff_data) k = AugmentedConstant(V, data=const_data) - form = k*f*inner(grad(v), grad(u))*dx + form = k * f * inner(grad(v), grad(u)) * dx # Remove extraneous references del cell, domain, element, V, v, u, f, k @@ -90,8 +101,10 @@ def test_strip_form_arguments_does_not_change_form(): const_data = object() cell = triangle - domain = AugmentedMesh(cell, data=mesh_data) - element = FiniteElement("Lagrange", cell, 1) + 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) @@ -99,7 +112,7 @@ def test_strip_form_arguments_does_not_change_form(): f = AugmentedCoefficient(V, data=coeff_data) k = AugmentedConstant(V, data=const_data) - form = k*f*inner(grad(v), grad(u))*dx + form = k * f * inner(grad(v), grad(u)) * dx stripped_form, mapping = strip_terminal_data(form) assert stripped_form.signature() == form.signature() diff --git a/test/test_tensoralgebra.py b/test/test_tensoralgebra.py index 70eea5d88..4d80e671d 100755 --- a/test/test_tensoralgebra.py +++ b/test/test_tensoralgebra.py @@ -1,11 +1,35 @@ -# -*- coding: utf-8 -*- -""" -Test tensor algebra operators. -""" +"""Test tensor algebra operators.""" import pytest -from ufl import * + +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") @@ -41,13 +65,13 @@ def test_repeated_as_tensor(self, A, B, u, v): def test_outer(self, A, B, u, v): C = outer(u, v) - D = as_matrix([[10*30, 10*40], [20*30, 20*40]]) + 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]) + 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 @@ -55,17 +79,18 @@ def test_outer(self, A, B, u, v): def test_inner(self, A, B, u, v): C = inner(A, B) - D = 2*6 + 3*7 + 4*8 + 5*9 + D = 2 * 6 + 3 * 7 + 4 * 8 + 5 * 9 self.assertEqualValues(C, D) C = inner(u, v) - D = 10*30 + 20*40 + D = 10 * 30 + 20 * 40 self.assertEqualValues(C, D) def test_pow2_inner(self, A, u): - f = FacetNormal(triangle)[0] - f2 = f*f + 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 @@ -75,18 +100,17 @@ def test_pow2_inner(self, A, u): assert A2 == remove_complex_nodes(inner(A, A)) # Only tensor**2 notation is supported: - self.assertRaises(UFLException, lambda: A**3) + self.assertRaises(BaseException, lambda: A**3) def test_dot(self, A, B, u, v): C = dot(u, v) - D = 10*30 + 20*40 + 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]) + 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) @@ -105,23 +129,44 @@ def test_cross(self): 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 + 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]) + 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]) + D = 0.5 * as_matrix([[A[i, j] + A[j, i] for j in dims] for i in dims]) self.assertEqualValues(C, D) @@ -161,18 +206,19 @@ def test_tr(self, A): 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) + 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)]) + 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 # FIXME: Test fails probably due to integer division + 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) diff --git a/test/test_utilities.py b/test/test_utilities.py index 5cdcc46e4..a06252898 100755 --- a/test/test_utilities.py +++ b/test/test_utilities.py @@ -1,12 +1,6 @@ -#!/usr/bin/env py.test -# -*- coding: utf-8 -*- +"""Test internal utility functions.""" -""" -Test internal utility functions. -""" - -from ufl.utils.indexflattening import (shape_to_strides, flatten_multiindex, - unflatten_index) +from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index def test_shape_to_strides(): @@ -37,11 +31,14 @@ def test_indexing_to_component(): 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))) + 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) @@ -51,15 +48,23 @@ def test_component_numbering(): 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 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 (shape_to_strides, - flatten_multiindex, - unflatten_index) + from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index + # Scalar shape s = () st = shape_to_strides(s) @@ -150,11 +155,12 @@ def test_index_flattening(): i -= 0 # map back to tensor component: c2 = unflatten_index(i, shape_to_strides(ts)) - assert (k//2, k % 2) == c2 + 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) diff --git a/ufl/__init__.py b/ufl/__init__.py index d50f58196..312c43865 100644 --- a/ufl/__init__.py +++ b/ufl/__init__.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- -# flake8: noqa -"""The Unified Form Language is an embedded domain specific language +"""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 @@ -11,19 +11,19 @@ * To import the language, type:: - from ufl import * + import ufl * To import the underlying classes an UFL expression tree is built from, type :: - from ufl.classes import * + import ufl.classes * Various algorithms for working with UFL expression trees can be accessed by :: - from ufl.algorithms import * + import ufl.algorithms Classes and algorithms are considered implementation details and should not be used in form definitions. @@ -38,158 +38,155 @@ The development version can be found in the repository at - https://www.bitbucket.org/fenics-project/ufl + 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 + -AbstractCell + -Cell + -TensorProductCell + -vertex + -interval + -triangle + -tetrahedron + -quadrilateral + -hexahedron + -prism + -pyramid + -pentatope + -tesseract * Domains:: - - AbstractDomain - - Mesh - - MeshView - - TensorProductMesh + -AbstractDomain + -Mesh + -MeshView * Sobolev spaces:: - - L2 - - H1 - - H2 - - HInf - - HDiv - - HCurl - - HEin - - HDivDiv - -* Elements:: - - - FiniteElement - - MixedElement - - VectorElement - - TensorElement - - EnrichedElement - - NodalEnrichedElement - - RestrictedElement - - TensorProductElement - - HDivElement - - HCurlElement - - BrokenElement - - FacetElement - - InteriorElement + -L2 + -H1 + -H2 + -HInf + -HDiv + -HCurl + -HEin + -HDivDiv + -HCurlDiv + + +* Pull backs:: + + -identity_pullback + -contravariant_piola + -covariant_piola + -l2_piola + -double_contravariant_piola + -double_covariant_piola + -covariant_contravariant_piola * Function spaces:: - - FunctionSpace - - MixedFunctionSpace + -FunctionSpace + -MixedFunctionSpace * Arguments:: - - Argument - - TestFunction - - TrialFunction - - Arguments - - TestFunctions - - TrialFunctions + -Argument + -TestFunction + -TrialFunction + -Arguments + -TestFunctions + -TrialFunctions * Coefficients:: - - Coefficient - - Constant - - VectorConstant - - TensorConstant + -Coefficient + -Constant + -VectorConstant + -TensorConstant * Splitting form arguments in mixed spaces:: - - split + -split * Literal constants:: - - Identity - - PermutationSymbol + -Identity + -PermutationSymbol * Geometric quantities:: - - SpatialCoordinate - - FacetNormal - - CellNormal - - CellVolume - - CellDiameter - - Circumradius - - MinCellEdgeLength - - MaxCellEdgeLength - - FacetArea - - MinFacetEdgeLength - - MaxFacetEdgeLength - - Jacobian - - JacobianDeterminant - - JacobianInverse + -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 + -Index + -indices + -i, j, k, l + -p, q, r, s * Scalar to tensor expression conversion:: - - as_tensor - - as_vector - - as_matrix + -as_tensor + -as_vector + -as_matrix * Unit vectors and matrices:: - - unit_vector - - unit_vectors - - unit_matrix - - unit_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 + -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 + -elem_mult + -elem_div + -elem_pow + -elem_op * Differential operators:: - - variable - - diff, - - grad, nabla_grad - - div, nabla_div - - curl, rot - - Dx, Dn + -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, atan_2 - - cosh, sinh, tanh - - bessel_J, bessel_Y, bessel_I, bessel_K + -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:: @@ -198,10 +195,10 @@ * Discontinuous Galerkin operators:: - - v('+'), v('-') - - jump - - avg - - cell_avg, facet_avg + -v("+"), v("-") + -jump + -avg + -cell_avg, facet_avg * Conditional operators:: @@ -212,21 +209,21 @@ * Integral measures:: - - dx, ds, dS, dP, dl - - dc, dC, dO, dI, dX - - ds_b, ds_t, ds_tb, ds_v, dS_h, dS_v + -dx, ds, dS, dP, dl + -dc, dC, dO, dI, dX + -ds_b, ds_t, ds_tb, ds_v, dS_h, dS_v * Form transformations:: - - rhs, lhs - - system - - functional - - replace, replace_integral_domains - - adjoint - - action - - energy_norm, - - sensitivity_rhs - - derivative + -rhs, lhs + -system + -functional + -replace + -adjoint + -action + (-energy_norm,) + -sensitivity_rhs + -derivative """ # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg @@ -242,184 +239,411 @@ # Modified by Lawrence Mitchell, 2014 # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 +# Modified by Nacime Bouziani, 2019 -import pkg_resources - -__version__ = pkg_resources.get_distribution("fenics-ufl").version +import importlib.metadata -# README -# Imports here should be what the user sees when doing "from ufl import *", -# which means we should _not_ import f.ex. "Grad", but "grad". -# This way we expose the language, the operation "grad", but less -# of the implementation, the particular class "Grad". -########## +__version__ = importlib.metadata.version("fenics-ufl") -# Utility functions (product is the counterpart of the built-in -# python function sum, can be useful for users as well?) -from ufl.utils.sequences import product - -# Output control -from ufl.log import get_handler, get_logger, set_handler, set_level, add_logfile, \ - UFLException, DEBUG, INFO, WARNING, ERROR, CRITICAL - -# Types for geometric quantities +from math import e, pi -from ufl.cell import as_cell, AbstractCell, Cell, TensorProductCell -from ufl.domain import as_domain, AbstractDomain, Mesh, MeshView, TensorProductMesh +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, + EdgeJacobian, + EdgeJacobianDeterminant, + EdgeJacobianInverse, + FacetArea, + FacetNormal, + Jacobian, + JacobianDeterminant, + JacobianInverse, + MaxCellEdgeLength, + MaxFacetEdgeLength, + MinCellEdgeLength, + MinFacetEdgeLength, SpatialCoordinate, - FacetNormal, CellNormal, - CellVolume, CellDiameter, Circumradius, MinCellEdgeLength, MaxCellEdgeLength, - FacetArea, MinFacetEdgeLength, MaxFacetEdgeLength, - Jacobian, JacobianDeterminant, JacobianInverse ) - -# Sobolev spaces -from ufl.sobolevspace import L2, H1, H2, HDiv, HCurl, HEin, HDivDiv, HInf - -# Finite elements classes -from ufl.finiteelement import FiniteElementBase, FiniteElement, \ - MixedElement, VectorElement, TensorElement, EnrichedElement, \ - NodalEnrichedElement, RestrictedElement, TensorProductElement, \ - HDivElement, HCurlElement, BrokenElement, \ - FacetElement, InteriorElement, WithMapping - -# Hook to extend predefined element families -from ufl.finiteelement.elementlist import register_element, show_elements # , ufl_elements - -# Function spaces -from ufl.functionspace import FunctionSpace, MixedFunctionSpace - -# Arguments -from ufl.argument import Argument, TestFunction, TrialFunction, \ - Arguments, TestFunctions, TrialFunctions - -# Coefficients -from ufl.coefficient import Coefficient, Coefficients -from ufl.constant import Constant, VectorConstant, TensorConstant - -# Split function -from ufl.split_functions import split - -# Literal constants -from ufl.constantvalue import PermutationSymbol, Identity, zero, as_ufl - -# Indexing of tensor expressions -from ufl.core.multiindex import Index, indices - -# Special functions for expression base classes -# (ensure this is imported, since it attaches operators to Expr) -import ufl.exproperators as __exproperators - -# Containers for expressions with value rank > 0 -from ufl.tensors import as_tensor, as_vector, as_matrix, relabel -from ufl.tensors import unit_vector, unit_vectors, unit_matrix, unit_matrices - -# Operators -from ufl.operators import 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, atan_2, \ - 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, Max, Min, \ - 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 - -# Measure classes -from ufl.measure import Measure, register_integral_type, integral_types, custom_integral_types - -# Form class -from ufl.form import Form, replace_integral_domains - -# Integral classes from ufl.integral import Integral - -# Special functions for Measure class -# (ensure this is imported, since it attaches operators to Measure) -import ufl.measureoperators as __measureoperators - -# Representations of transformed forms -from ufl.formoperators import replace, derivative, action, energy_norm, rhs, lhs,\ -system, functional, adjoint, sensitivity_rhs, extract_blocks #, dirichlet_functional - -# Predefined convenience objects +from ufl.matrix import Matrix +from ufl.measure import Measure, custom_integral_types, integral_types, register_integral_type from ufl.objects import ( - vertex, interval, triangle, tetrahedron, - quadrilateral, hexahedron, prism, pyramid, facet, - i, j, k, l, p, q, r, s, - dx, ds, dS, dP, dl, - dc, dC, dO, dI, dX, - ds_b, ds_t, ds_tb, ds_v, dS_h, dS_v + dC, + dc, + dI, + dl, + 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_contravariant_piola, + covariant_piola, + double_contravariant_piola, + double_covariant_piola, + identity_pullback, + l2_piola, +) +from ufl.sobolevspace import H1, H2, L2, HCurl, HCurlDiv, 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 -# Useful constants -from math import e, pi - - -# Define ufl.* namespace __all__ = [ - 'product', - 'get_handler', 'get_logger', 'set_handler', 'set_level', 'add_logfile', - 'UFLException', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', - 'as_cell', 'AbstractCell', 'Cell', 'TensorProductCell', - 'as_domain', 'AbstractDomain', 'Mesh', 'MeshView', 'TensorProductMesh', - 'L2', 'H1', 'H2', 'HCurl', 'HDiv', 'HInf', 'HEin', 'HDivDiv', - 'SpatialCoordinate', - 'CellVolume', 'CellDiameter', 'Circumradius', - 'MinCellEdgeLength', 'MaxCellEdgeLength', - 'FacetArea', 'MinFacetEdgeLength', 'MaxFacetEdgeLength', - 'FacetNormal', 'CellNormal', - 'Jacobian', 'JacobianDeterminant', 'JacobianInverse', - 'FiniteElementBase', 'FiniteElement', - 'MixedElement', 'VectorElement', 'TensorElement', 'EnrichedElement', - 'NodalEnrichedElement', 'RestrictedElement', 'TensorProductElement', - 'HDivElement', 'HCurlElement', - 'BrokenElement', 'FacetElement', 'InteriorElement', "WithMapping", - 'register_element', 'show_elements', - 'FunctionSpace', 'MixedFunctionSpace', - 'Argument', 'TestFunction', 'TrialFunction', - 'Arguments', 'TestFunctions', 'TrialFunctions', - 'Coefficient', 'Coefficients', - 'Constant', 'VectorConstant', 'TensorConstant', - 'split', - 'PermutationSymbol', 'Identity', 'zero', 'as_ufl', - 'Index', 'indices', - 'as_tensor', 'as_vector', 'as_matrix', 'relabel', - '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', 'atan_2', - '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', 'Max', 'Min', - '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', - 'Integral', 'Measure', 'register_integral_type', 'integral_types', 'custom_integral_types', - 'replace', 'replace_integral_domains', 'derivative', 'action', 'energy_norm', 'rhs', 'lhs', 'extract_blocks', - 'system', 'functional', 'adjoint', 'sensitivity_rhs', - 'dx', 'ds', 'dS', 'dP', 'dl', - 'dc', 'dC', 'dO', 'dI', 'dX', - 'ds_b', 'ds_t', 'ds_tb', 'ds_v', 'dS_h', 'dS_v', - 'vertex', 'interval', 'triangle', 'tetrahedron', - 'prism', 'pyramid', - 'quadrilateral', 'hexahedron', 'facet', - 'i', 'j', 'k', 'l', 'p', 'q', 'r', 's', - 'e', 'pi', + "product", + "as_cell", + "AbstractCell", + "Cell", + "EdgeJacobian", + "EdgeJacobianDeterminant", + "EdgeJacobianInverse", + "TensorProductCell", + "AbstractDomain", + "Mesh", + "MeshView", + "L2", + "H1", + "H2", + "HCurl", + "HDiv", + "HInf", + "HEin", + "HDivDiv", + "HCurlDiv", + "identity_pullback", + "l2_piola", + "contravariant_piola", + "covariant_piola", + "double_contravariant_piola", + "double_covariant_piola", + "covariant_contravariant_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", ] diff --git a/ufl/action.py b/ufl/action.py new file mode 100644 index 000000000..fb6416865 --- /dev/null +++ b/ufl/action.py @@ -0,0 +1,217 @@ +"""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 diff --git a/ufl/adjoint.py b/ufl/adjoint.py new file mode 100644 index 000000000..987372a73 --- /dev/null +++ b/ufl/adjoint.py @@ -0,0 +1,135 @@ +"""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 diff --git a/ufl/algebra.py b/ufl/algebra.py index 3ccc499b8..dea6fd021 100644 --- a/ufl/algebra.py +++ b/ufl/algebra.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"Basic algebra operations." - +"""Basic algebra operations.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -9,26 +7,32 @@ # # Modified by Anders Logg, 2008 -from ufl.log import error -from ufl.core.ufl_type import ufl_type +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.constantvalue import Zero, zero, ScalarValue, IntValue, ComplexValue, as_ufl -from ufl.checks import is_ufl_scalar, is_true_ufl_scalar +from ufl.core.ufl_type import ufl_type from ufl.index_combination_utils import merge_unique_indices -from ufl.sorting import sorted_expr 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__") +@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) @@ -38,11 +42,11 @@ def __new__(cls, a, b): fi = a.ufl_free_indices fid = a.ufl_index_dimensions if b.ufl_shape != sh: - error("Can't add expressions with different shapes.") + raise ValueError("Can't add expressions with different shapes.") if b.ufl_free_indices != fi: - error("Can't add expressions with different free indices.") + raise ValueError("Can't add expressions with different free indices.") if b.ufl_index_dimensions != fid: - error("Can't add expressions with different index dimensions.") + raise ValueError("Can't add expressions with different index dimensions.") # Skip adding zero if isinstance(a, Zero): @@ -78,45 +82,30 @@ def __new__(cls, 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): - return sum(o.evaluate(x, mapping, component, - index_values) for o in self.ufl_operands) + """Evaluate.""" + return sum(o.evaluate(x, mapping, component, index_values) for o in self.ufl_operands) def __str__(self): - ops = [parstr(o, self) for o in self.ufl_operands] - if False: - # Implementation with line splitting: - limit = 70 - delimop = " + \\\n + " - op = " + " - s = ops[0] - n = len(s) - for o in ops[1:]: - m = len(o) - if n + m > limit: - s += delimop - n = m - else: - s += op - n += m - s += o - return s - # Implementation with no line splitting: - return "%s" % " + ".join(ops) - - -@ufl_type(num_ops=2, - binop="__mul__", rbinop="__rmul__") + """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) @@ -124,16 +113,20 @@ def __new__(cls, a, b): # Type checking # Make sure everything is scalar if a.ufl_shape or b.ufl_shape: - error("Product can only represent products of scalars, " - "got\n\t%s\nand\n\t%s" % (ufl_err_str(a), ufl_err_str(b))) + 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) + 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) @@ -163,28 +156,31 @@ def __new__(cls, a, b): return self def _init(self, a, b): - "Constructor, called by __new__ with already checked arguments." + """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) + 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: - error("Expecting nonscalar product operand to be the last by convention.") + 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: @@ -194,17 +190,19 @@ def evaluate(self, x, mapping, component, 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__") +@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) @@ -214,11 +212,11 @@ def __new__(cls, a, b): # so maybe we can keep this assertion. Some algorithms may # need updating. if not is_ufl_scalar(a): - error("Expecting scalar nominator in Division.") + raise ValueError("Expecting scalar nominator in Division.") if not is_true_ufl_scalar(b): - error("Division by non-scalar is undefined.") + raise ValueError("Division by non-scalar is undefined.") if isinstance(b, Zero): - error("Division by zero!") + raise ValueError("Division by zero!") # Simplification # Simplification a/b -> a @@ -241,14 +239,17 @@ def __new__(cls, 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) @@ -260,38 +261,39 @@ def evaluate(self, x, mapping, component, index_values): return e def __str__(self): - return "%s / %s" % (parstr(self.ufl_operands[0], self), - parstr(self.ufl_operands[1], 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__") +@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): - error("Cannot take the power of a non-scalar expression %s." % ufl_err_str(a)) + raise ValueError(f"Cannot take the power of a non-scalar expression {ufl_err_str(a)}.") if not is_true_ufl_scalar(b): - error("Cannot raise an expression to a non-scalar power %s." % ufl_err_str(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) + 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): - error("Cannot raise zero to a complex power.") + raise ValueError("Cannot raise zero to a complex power.") bf = float(b) if bf < 0: - error("Division by zero, cannot raise 0 to a negative power.") + raise ValueError("Division by zero, cannot raise 0 to a negative power.") else: return zero() if isinstance(b, ScalarValue) and b._value == 1: @@ -303,31 +305,36 @@ def __new__(cls, 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 "%s ** %s" % (parstr(a, self), parstr(b, self)) + 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__") +@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 @@ -341,23 +348,28 @@ def __new__(cls, a): 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): - a, = self.ufl_operands - return "|%s|" % (parstr(a, 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) +@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 @@ -371,23 +383,28 @@ def __new__(cls, a): 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): - a, = self.ufl_operands - return "conj(%s)" % (parstr(a, 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) +@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 @@ -403,23 +420,28 @@ def __new__(cls, a): 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): - a, = self.ufl_operands - return "Re[%s]" % (parstr(a, 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) +@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 @@ -433,12 +455,15 @@ def __new__(cls, a): 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): - a, = self.ufl_operands - return "Im[%s]" % (parstr(a, self),) + """Format as a string.""" + (a,) = self.ufl_operands + return f"Im[{parstr(a, self)}]" diff --git a/ufl/algorithms/__init__.py b/ufl/algorithms/__init__.py index 096266657..18dce39fd 100644 --- a/ufl/algorithms/__init__.py +++ b/ufl/algorithms/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# flake8: noqa -"This module collects algorithms and utility functions operating on UFL objects." +"""This module collects algorithms and utility functions operating on UFL objects.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -16,12 +14,11 @@ # 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", - "purge_list_tensors", + "preprocess_form", "apply_transformer", "ReuseTransformer", "load_ufl_file", @@ -31,17 +28,16 @@ "extract_type", "extract_elements", "extract_sub_elements", - "preprocess_expression", "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", - "expand_compounds", "validate_form", "FormSplitter", "extract_arguments", @@ -52,78 +48,48 @@ "compute_form_rhs", "compute_form_functional", "compute_form_signature", + "compute_form_arities", "tree_format", + "read_ufl_file", + "load_forms", ] -# Utilities for traversing over expression trees in different ways -# from ufl.algorithms.traversal import iter_expressions - -# Keeping these imports here for backwards compatibility, doesn't cost -# anything. Prefer importing from ufl.corealg.traversal in future -# code. -# from ufl.corealg.traversal import pre_traversal -from ufl.corealg.traversal import post_traversal -# from ufl.corealg.traversal import traverse_terminals, traverse_unique_terminals - - -# Utilities for extracting information from forms and expressions +from ufl.algorithms.ad import expand_derivatives from ufl.algorithms.analysis import ( - extract_type, extract_arguments, + extract_base_form_operators, extract_coefficients, - # extract_arguments_and_coefficients, extract_elements, - extract_unique_elements, extract_sub_elements, + extract_type, + extract_unique_elements, sort_elements, ) - - -# Preprocessing a form to extract various meta data -# from ufl.algorithms.formdata import FormData -from ufl.algorithms.compute_form_data import compute_form_data - -# Utilities for checking properties of forms -from ufl.algorithms.signature import compute_form_signature - -# Utilities for error checking of forms -from ufl.algorithms.checks import validate_form - -# Utilites for modifying expressions and forms -from ufl.corealg.multifunction import MultiFunction -from ufl.algorithms.transformer import Transformer, ReuseTransformer -# from ufl.algorithms.transformer import is_post_handler -from ufl.algorithms.transformer import apply_transformer -from ufl.algorithms.transformer import strip_variables -from ufl.algorithms.strip_terminal_data import strip_terminal_data -from ufl.algorithms.strip_terminal_data import replace_terminal_data -# from ufl.algorithms.replace import Replacer -from ufl.algorithms.replace import replace from ufl.algorithms.change_to_reference import change_to_reference_grad -from ufl.algorithms.expand_compounds import expand_compounds -# from ufl.algorithms.estimate_degrees import SumDegreeEstimator +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, purge_list_tensors - -# Utilities for transforming complete Forms into other Forms -from ufl.algorithms.formtransformations import compute_form_adjoint -from ufl.algorithms.formtransformations import compute_form_action -from ufl.algorithms.formtransformations import compute_energy_norm -from ufl.algorithms.formtransformations import compute_form_lhs -from ufl.algorithms.formtransformations import compute_form_rhs -from ufl.algorithms.formtransformations import compute_form_functional -from ufl.algorithms.formtransformations import compute_form_arities - +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 - -# Utilities for Automatic Functional Differentiation -from ufl.algorithms.ad import expand_derivatives - -# Utilities for form file handling -from ufl.algorithms.formfiles import read_ufl_file -from ufl.algorithms.formfiles import load_ufl_file -from ufl.algorithms.formfiles import load_forms - -# Utilities for UFL object printing -# from ufl.formatting.printing import integral_info, form_info -from ufl.formatting.printing import tree_format +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 diff --git a/ufl/algorithms/ad.py b/ufl/algorithms/ad.py index 003e27a76..cc1e04224 100644 --- a/ufl/algorithms/ad.py +++ b/ufl/algorithms/ad.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Front-end for AD routines.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -9,7 +8,8 @@ # # Modified by Anders Logg, 2009. -from ufl.log import warning +import warnings + from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering from ufl.algorithms.apply_derivatives import apply_derivatives @@ -25,7 +25,7 @@ def expand_derivatives(form, **kwargs): # For a deprecation period (I see that dolfin-adjoint passes some # args here) if kwargs: - warning("Deprecation: expand_derivatives no longer takes any keyword arguments") + warnings("Deprecation: expand_derivatives no longer takes any keyword arguments") # Lower abstractions for tensor-algebra types into index notation form = apply_algebra_lowering(form) diff --git a/ufl/algorithms/analysis.py b/ufl/algorithms/analysis.py index 4d9265769..1d0464a95 100644 --- a/ufl/algorithms/analysis.py +++ b/ufl/algorithms/analysis.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""Utility algorithms for inspection of and information extraction from UFL objects in various ways.""" +"""Utility algorithms for inspection of and information extraction from UFL objects.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -12,26 +11,27 @@ from itertools import chain -from ufl.log import error -from ufl.utils.sorting import sorted_by_count, topological_sorting - -from ufl.core.terminal import Terminal, FormArgument -from ufl.argument import Argument -from ufl.coefficient import Coefficient -from ufl.constant import Constant from ufl.algorithms.traversal import iter_expressions -from ufl.corealg.traversal import unique_pre_traversal, traverse_unique_terminals - +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." + """Return tuple of unique objects, preserving initial ordering.""" unique_objects = [] handled = set() for obj in objects: @@ -43,31 +43,98 @@ def unique_tuple(objects): # --- Utilities to extract information from an expression --- -def __unused__extract_classes(a): - """Build a set of all unique Expr subclasses used in a. - The argument a can be a Form, Integral or Expr.""" - return set(o._ufl_class_ - for e in iter_expressions(a) - for o in unique_pre_traversal(e)) +def extract_type(a, ufl_types): + """Build a set of all objects found in a whose class is in ufl_types. -def extract_type(a, ufl_type): - """Build a set of all objects of class ufl_type found in a. - The argument a can be a Form, Integral or Expr.""" - if issubclass(ufl_type, Terminal): + 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 - return set(o for e in iter_expressions(a) - for o in traverse_unique_terminals(e) - if isinstance(o, ufl_type)) + 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: - return set(o for e in iter_expressions(a) - for o in unique_pre_traversal(e) - if isinstance(o, ufl_type)) + 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 can be found in a. - The argument a can be a Form, Integral or Expr.""" + """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 @@ -78,7 +145,14 @@ def has_type(a, ufl_type): def has_exact_type(a, ufl_type): """Return if an object of class ufl_type can be found in a. - The argument a can be a Form, Integral or Expr.""" + + 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 @@ -89,50 +163,71 @@ def has_exact_type(a, ufl_type): def extract_arguments(a): - """Build a sorted list of all arguments in a, - which can be a Form, Integral or Expr.""" - return _sorted_by_number_and_part(extract_type(a, Argument)) + """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, - which can be a Form, Integral or Expr.""" - return sorted_by_count(extract_type(a, Coefficient)) + """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""" + """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, which can be a Form, Integral or Expr.""" + """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. + This function is faster than extract_arguments + extract_coefficients + for large forms, and has more validation built in. - # Extract lists of all form argument instances - terminals = extract_type(a, FormArgument) - arguments = [f for f in terminals if isinstance(f, Argument)] - coefficients = [f for f in terminals if isinstance(f, Coefficient)] + 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())): - msg = """\ -Found different Arguments with same number and part. -Did you combine test or trial functions from different spaces? -The Arguments found are:\n%s""" % "\n".join(" %s" % f for f in arguments) - error(msg) + 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())): - msg = """\ -Found different coefficients with same counts. -The arguments found are:\n%s""" % "\n".join(" %s" % f for f in coefficients) - error(msg) + 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) @@ -142,40 +237,40 @@ def extract_arguments_and_coefficients(a): def extract_elements(form): - "Build sorted tuple of all elements used in 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." + """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])) + """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 so that any sub elements appear before the + """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 = sorted(elements) + nodes = list(elements) # Set edges edges = dict((node, []) for node in nodes) for element in elements: - for sub_element in element.sub_elements(): + for sub_element in element.sub_elements: edges[element].append(sub_element) # Sort graph diff --git a/ufl/algorithms/apply_algebra_lowering.py b/ufl/algorithms/apply_algebra_lowering.py index 24fc2fbb6..ea5b3bbb2 100644 --- a/ufl/algorithms/apply_algebra_lowering.py +++ b/ufl/algorithms/apply_algebra_lowering.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"""Algorithm for expanding compound expressions into -equivalent representations using basic operators.""" +"""Algorithm for expanding compound expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # @@ -10,77 +8,63 @@ # # Modified by Anders Logg, 2009-2010 -from ufl.log import error - -from ufl.classes import Product, Grad, Conj -from ufl.core.multiindex import indices, Index, FixedIndex -from ufl.tensors import as_tensor, as_matrix, as_vector - -from ufl.compound_expressions import deviatoric_expr, determinant_expr, cofactor_expr, inverse_expr - -from ufl.corealg.multifunction import MultiFunction 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 (e.g. inner) to equivalent - representations using basic operators (e.g. index notation).""" + """Expands high level compound operators to equivalent representations using basic operators.""" def __init__(self): + """Initialize.""" MultiFunction.__init__(self) - expr = MultiFunction.reuse_if_untouched + 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 _square_matrix_shape(self, A): - sh = A.ufl_shape - if sh[0] != sh[1]: - error("Expecting square matrix.") - if sh[0] is None: - error("Unknown dimension.") - return sh - 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 altenative_dot(self, o, a, b): # TODO: Test this - ash = a.ufl_shape - bsh = b.ufl_shape - ai = indices(len(ash) - 1) - bi = indices(len(bsh) - 1) - # Simplification for tensors where the dot-sum dimension has - # length 1 - if ash[-1] == 1: - k = (FixedIndex(0),) - else: - k = (Index(),) - # Potentially creates a single IndexSum over a Product - s = a[ai + k] * b[k + bi] - return as_tensor(s, ai + bi) + 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(),) @@ -88,37 +72,19 @@ def dot(self, o, a, b): s = a[ai + k] * b[k + bi] return as_tensor(s, ai + bi) - def alternative_inner(self, o, a, b): # TODO: Test this - ash = a.ufl_shape - bsh = b.ufl_shape - if ash != bsh: - error("Nonmatching shapes.") - # Simplification for tensors with one or more dimensions of - # length 1 - ii = [] - zi = FixedIndex(0) - for n in ash: - if n == 1: - ii.append(zi) - else: - ii.append(Index()) - ii = tuple(ii) - # ii = indices(len(a.ufl_shape)) - # Potentially creates multiple IndexSums over a Product - s = a[ii] * Conj(b[ii]) - return s - def inner(self, o, a, b): + """Lower an inner.""" ash = a.ufl_shape bsh = b.ufl_shape if ash != bsh: - error("Nonmatching shapes.") + 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 @@ -126,25 +92,31 @@ def outer(self, o, a, b): 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) @@ -154,11 +126,15 @@ def nabla_grad(self, o, a): 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))) @@ -166,10 +142,9 @@ def c(i, j): return c(0, 1) if sh == (3,): return as_vector((c(1, 2), c(2, 0), c(0, 1))) - error("Invalid shape %s of curl argument." % (sh,)) + raise ValueError(f"Invalid shape {sh} of curl argument.") def apply_algebra_lowering(expr): - """Expands high level compound operators (e.g. inner) to equivalent - representations using basic operators (e.g. index notation).""" + """Expands high level compound operators to equivalent representations using basic operators.""" return map_integrand_dags(LowerCompoundAlgebra(), expr) diff --git a/ufl/algorithms/apply_derivatives.py b/ufl/algorithms/apply_derivatives.py index c4123f3cc..da7b61da1 100644 --- a/ufl/algorithms/apply_derivatives.py +++ b/ufl/algorithms/apply_derivatives.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module contains the apply_derivatives algorithm which computes the derivatives of a form of expression.""" +"""Apply derivatives algorithm which computes the derivatives of a form of expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -7,40 +6,77 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import warnings from collections import defaultdict +from math import pi -from ufl.log import error, warning - +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.core.multiindex import MultiIndex, FixedIndex, indices - -from ufl.tensors import as_tensor, as_scalar, as_scalars, unit_indexed_tensor, unwrap_list_tensor - -from ufl.classes import ConstantValue, Identity, Zero, FloatValue -from ufl.classes import Coefficient, FormArgument, ReferenceValue -from ufl.classes import Grad, ReferenceGrad, Variable -from ufl.classes import Indexed, ListTensor, ComponentTensor -from ufl.classes import ExprList, ExprMapping -from ufl.classes import Product, Sum, IndexSum -from ufl.classes import Conj, Real, Imag -from ufl.classes import JacobianInverse -from ufl.classes import SpatialCoordinate - -from ufl.constantvalue import is_true_ufl_scalar, is_ufl_scalar -from ufl.operators import (conditional, sign, - sqrt, exp, ln, cos, sin, cosh, sinh, - bessel_J, bessel_Y, bessel_I, bessel_K, - cell_avg, facet_avg) - -from math import pi - -from ufl.corealg.multifunction import MultiFunction from ufl.corealg.map_dag import map_expr_dag -from ufl.algorithms.map_integrands import map_integrand_dags +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 -from ufl.checks import is_cellwise_constant -from ufl.differentiation import CoordinateDerivative # TODO: Add more rulesets? # - DivRuleset # - CurlRuleset @@ -48,51 +84,62 @@ # - ReferenceDivRuleset -# Set this to True to enable previously default workaround -# for bug in FFC handling of conditionals, uflacs does not -# have this bug. -CONDITIONAL_WORKAROUND = False - - 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): - error("Missing differentiation handler for type {0}. Have you added a new type?".format(o._ufl_class_.__name__)) + """Raise error.""" + raise ValueError( + f"Missing differentiation handler for type {o._ufl_class_.__name__}. " + "Have you added a new type?" + ) def unexpected(self, o): - error("Unexpected type {0} in AD rules.".format(o._ufl_class_.__name__)) + """Raise error about unexpected type.""" + raise ValueError(f"Unexpected type {o._ufl_class_.__name__} in AD rules.") def override(self, o): - error("Type {0} must be overridden in specialized AD rule set.".format(o._ufl_class_.__name__)) + """Raise error about overriding.""" + raise ValueError( + f"Type {o._ufl_class_.__name__} must be overridden in specialized AD rule set." + ) def derivative(self, o): - error("Unhandled derivative type {0}, nested differentiation has failed.".format(o._ufl_class_.__name__)) - - def fixme(self, o): - error("FIXME: Unimplemented differentiation handler for type {0}.".format(o._ufl_class_.__name__)) + """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): - "Labels and indices are not differentiable. It's convenient to return the non-differentiated object." + """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): - "Return a zero with the right shape for terminals independent of differentiation variable." + """A zero with correct shape for terminals independent of diff. variable.""" return Zero(o.ufl_shape + self._var_shape) def independent_operator(self, o): - "Return a zero with the right shape and indices for operators independent of differentiation variable." + """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 @@ -167,11 +214,13 @@ def independent_operator(self, o): # --- 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) @@ -202,9 +251,11 @@ def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in nesting rules 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: @@ -215,12 +266,15 @@ def component_tensor(self, o, Ap, ii): # --- 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 @@ -233,12 +287,13 @@ def product(self, o, da, db): return s def division(self, o, fp, gp): + """Differentiate a division.""" f, g = o.ufl_operands if not is_ufl_scalar(f): - error("Not expecting nonscalar nominator") + raise ValueError("Not expecting nonscalar nominator") if not is_true_ufl_scalar(g): - error("Not expecting nonscalar denominator") + raise ValueError("Not expecting nonscalar denominator") # do_df = 1/g # do_dg = -h/g @@ -257,12 +312,13 @@ def division(self, o, fp, gp): return op def power(self, o, fp, gp): + """Differentiate a power.""" f, g = o.ufl_operands if not is_true_ufl_scalar(f): - error("Expecting scalar expression f in f**g.") + raise ValueError("Expecting scalar expression f in f**g.") if not is_true_ufl_scalar(g): - error("Expecting scalar expression g in f**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 @@ -272,11 +328,13 @@ def power(self, o, fp, gp): if isinstance(gp, Zero): # This probably produces better results for the common # case of f**constant - op = fp * g * f**(g - 1) + 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 + 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 @@ -291,7 +349,8 @@ def power(self, o, fp, gp): return op def abs(self, o, df): - f, = o.ufl_operands + """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 @@ -301,89 +360,111 @@ def abs(self, o, 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 + if hasattr(o, "derivative"): + (f,) = o.ufl_operands return df * o.derivative() - error("Unknown math function.") + 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): - f, = o.ufl_operands + """Differentiate a ln.""" + (f,) = o.ufl_operands if isinstance(f, Zero): - error("Division by zero.") + raise ZeroDivisionError() return fp / f def cos(self, o, fp): - f, = o.ufl_operands + """Differentiate a cos.""" + (f,) = o.ufl_operands return fp * -sin(f) def sin(self, o, fp): - f, = o.ufl_operands + """Differentiate a sin.""" + (f,) = o.ufl_operands return fp * cos(f) def tan(self, o, fp): - f, = o.ufl_operands + """Differentiate a tan.""" + (f,) = o.ufl_operands return 2.0 * fp / (cos(2.0 * f) + 1.0) def cosh(self, o, fp): - f, = o.ufl_operands + """Differentiate a cosh.""" + (f,) = o.ufl_operands return fp * sinh(f) def sinh(self, o, fp): - f, = o.ufl_operands + """Differentiate a sinh.""" + (f,) = o.ufl_operands return fp * cosh(f) def tanh(self, o, fp): - f, = o.ufl_operands + """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 + + return fp * sech(f) ** 2 def acos(self, o, fp): - f, = o.ufl_operands + """Differentiate an acos.""" + (f,) = o.ufl_operands return -fp / sqrt(1.0 - f**2) def asin(self, o, fp): - f, = o.ufl_operands + """Differentiate an asin.""" + (f,) = o.ufl_operands return fp / sqrt(1.0 - f**2) def atan(self, o, fp): - f, = o.ufl_operands + """Differentiate an atan.""" + (f,) = o.ufl_operands return fp / (1.0 + f**2) - def atan_2(self, o, fp, gp): + 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): - f, = o.ufl_operands - return fp * (2.0 / sqrt(pi) * exp(-f**2)) + """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)): - error("Differentiation of bessel function w.r.t. nu is not supported.") + raise NotImplementedError( + "Differentiation of bessel function w.r.t. nu is not supported." + ) if isinstance(nu, Zero): op = -bessel_J(1, f) @@ -392,9 +473,12 @@ def bessel_j(self, o, nup, fp): 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)): - error("Differentiation of bessel function w.r.t. nu is not supported.") + raise NotImplementedError( + "Differentiation of bessel function w.r.t. nu is not supported." + ) if isinstance(nu, Zero): op = -bessel_Y(1, f) @@ -403,9 +487,12 @@ def bessel_y(self, o, nup, fp): 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)): - error("Differentiation of bessel function w.r.t. nu is not supported.") + raise NotImplementedError( + "Differentiation of bessel function w.r.t. nu is not supported." + ) if isinstance(nu, Zero): op = bessel_I(1, f) @@ -414,9 +501,12 @@ def bessel_i(self, o, nup, fp): 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)): - error("Differentiation of bessel function w.r.t. nu is not supported.") + raise NotImplementedError( + "Differentiation of bessel function w.r.t. nu is not supported." + ) if isinstance(nu, Zero): op = -bessel_K(1, f) @@ -427,6 +517,7 @@ def bessel_k(self, o, nup, 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? @@ -436,26 +527,21 @@ def restricted(self, o, fp): # --- 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): - global CONDITIONAL_WORKAROUND + """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 - elif CONDITIONAL_WORKAROUND: - # Placing t[1],f[1] outside here to avoid getting - # arguments inside conditionals. This will fail when dt - # or df become NaN or Inf in floating point computations! - c = o.ufl_operands[0] - dc = conditional(c, 1, 0) - return dc * dt + (1.0 - dc) * df else: # Not placing t[1],f[1] outside, allowing arguments inside # conditionals. This will make legacy ffc fail, but @@ -464,6 +550,7 @@ def conditional(self, o, unused_dc, dt, df): 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 @@ -474,6 +561,7 @@ def max_value(self, o, df, dg): 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 @@ -485,30 +573,37 @@ def min_value(self, o, df, 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): - """Default for geometric quantities is do/dx = 0 if piecewise constant, + """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.""" + Override for specific types if other behaviour is needed. + """ if is_cellwise_constant(o): return self.independent_terminal(o) else: - domain = o.ufl_domain() + 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_: - error("ReferenceValue can only wrap a terminal") + raise ValueError("ReferenceValue can only wrap a terminal") Do = grad_to_reference_grad(o, o) return Do @@ -516,22 +611,38 @@ def jacobian_inverse(self, o): # non-affine domains several should be non-zero. def spatial_coordinate(self, o): - "dx/dx = I" + """Differentiate a spatial_coordinate. + + dx/dx = I. + """ return self._Id def cell_coordinate(self, o): - "dX/dx = inv(dx/dX) = inv(J) = K" + """Differentiate a cell_coordinate. + + dX/dx = inv(dx/dX) = inv(J) = K. + """ # FIXME: Is this true for manifolds? What about orientation? - return JacobianInverse(o.ufl_domain()) + 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): @@ -543,27 +654,31 @@ def argument(self, 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 f.ufl_element().mapping() == "physical": + 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_: - error("ReferenceValue can only wrap a terminal") - domain = f.ufl_domain() + 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)) + valid_operand = f._ufl_is_in_reference_frame_ or isinstance( + f, (JacobianInverse, SpatialCoordinate, Jacobian, JacobianDeterminant) + ) if not valid_operand: - error("ReferenceGrad can only wrap a reference frame type!") - domain = f.ufl_domain() + 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 @@ -571,15 +686,18 @@ def reference_grad(self, o): # --- Nesting of gradients def grad(self, o): - "Represent grad(grad(f)) as Grad(Grad(f))." + """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)): - error("Expecting only grads applied to a 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? @@ -597,15 +715,11 @@ def _grad(self, o): 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 - ------- - Do: grad(o) written in terms of reference_grad(o) and K - + 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) @@ -615,15 +729,20 @@ def grad_to_reference_grad(o, K): class ReferenceGradRuleset(GenericDerivativeRuleset): + """Apply the reference grad derivative.""" + def __init__(self, topological_dimension): - GenericDerivativeRuleset.__init__(self, - var_shape=(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): - "dg/dX = 0 if piecewise constant, otherwise ReferenceGrad(g)" + """Differentiate a geometric_quantity. + + dg/dX = 0 if piecewise constant, otherwise ReferenceGrad(g). + """ if is_cellwise_constant(o): return self.independent_terminal(o) else: @@ -632,12 +751,18 @@ def geometric_quantity(self, o): return ReferenceGrad(o) def spatial_coordinate(self, o): - "dx/dX = J" + """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): - "dX/dX = I" + """Differentiate a cell_coordinate. + + dX/dX = I. + """ return self._Id # TODO: Add more geometry types here, with non-affine domains @@ -646,27 +771,35 @@ def cell_coordinate(self, o): # --- Specialized rules for form arguments def reference_value(self, o): + """Differentiate a reference_value.""" if not o.ufl_operands[0]._ufl_is_terminal_: - error("ReferenceValue can only wrap a terminal") + raise ValueError("ReferenceValue can only wrap a terminal") return ReferenceGrad(o) def coefficient(self, o): - error("Coefficient should be wrapped in ReferenceValue by now") + """Differentiate a coefficient.""" + raise ValueError("Coefficient should be wrapped in ReferenceValue by now") def argument(self, o): - error("Argument should be wrapped in ReferenceValue by now") + """Differentiate an argument.""" + raise ValueError("Argument should be wrapped in ReferenceValue by now") # --- Nesting of gradients def grad(self, o): - error("Grad should have been transformed by this point, but got {0}.".format(type(o).__name__)) + """Differentiate a grad.""" + raise ValueError( + f"Grad should have been transformed by this point, but got {type(o).__name__}." + ) def reference_grad(self, o): - "Represent ref_grad(ref_grad(f)) as RefGrad(RefGrad(f))." + """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)): - error("Expecting only grads applied to a 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 @@ -674,15 +807,21 @@ def reference_grad(self, o): 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: - error("Differentiation variable cannot have 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): - "Create a higher order identity tensor to represent dv/dv." + """Differentiate a _make_identity. + + Creates a higher order identity tensor to represent dv/dv. + """ res = None if sh == (): # Scalar dv/dv is scalar @@ -714,11 +853,10 @@ def _make_identity(self, sh): # Explicitly defining da/dw == 0 argument = GenericDerivativeRuleset.independent_terminal - # def _argument(self, o): - # return AnnotatedZero(o.ufl_shape + self._var_shape, arguments=(o,)) # TODO: Missing this type - def coefficient(self, o): - """df/dv = Id if v is f else 0. + """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, @@ -732,9 +870,10 @@ def coefficient(self, o): # df/v = 0 return self.independent_terminal(o) - def variable(self, o, df, l): + def variable(self, o, df, a): + """Differentiate a variable.""" v = self._variable - if isinstance(v, Variable) and v.label() == l: + if isinstance(v, Variable) and v.label() == a: # dv/dv = identity of rank 2*rank(v) return self._Id else: @@ -742,24 +881,30 @@ def variable(self, o, df, l): return df def grad(self, o): - "Variable derivative of a gradient of a terminal must be 0." + """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)): - error("Expecting only grads applied to a 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 v.ufl_element().mapping() != "identity": + 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 - error("Missing implementation: To handle derivatives of rv(f) w.r.t. f for" + - " mapped elements, rewriting to reference frame should not happen first...") + 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: @@ -767,10 +912,12 @@ def reference_value(self, o): return self.independent_terminal(o) def reference_grad(self, o): - "Variable derivative of a gradient of a terminal must be 0." - if not isinstance(o.ufl_operands[0], - (ReferenceGrad, ReferenceValue)): - error("Unexpected argument to reference_grad.") + """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 @@ -781,21 +928,20 @@ 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 - + D_w[v](e) = d/dtau e(w+tau v)|tau=0. """ - def __init__(self, coefficients, arguments, coefficient_derivatives): + def __init__(self, coefficients, arguments, coefficient_derivatives, pending_operations): + """Initialise.""" GenericDerivativeRuleset.__init__(self, var_shape=()) # Type checking if not isinstance(coefficients, ExprList): - error("Expecting a ExprList of coefficients.") + raise ValueError("Expecting a ExprList of coefficients.") if not isinstance(arguments, ExprList): - error("Expecting a ExprList of arguments.") + raise ValueError("Expecting a ExprList of arguments.") if not isinstance(coefficient_derivatives, ExprMapping): - error("Expecting a coefficient-coefficient 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 @@ -808,15 +954,21 @@ def __init__(self, coefficients, arguments, coefficient_derivatives): 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) @@ -825,6 +977,7 @@ def facet_avg(self, o, fp): 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 @@ -854,13 +1007,16 @@ def coefficient(self, o): if not isinstance(dos, tuple): dos = (dos,) if len(dos) != len(self._v): - error("Got a tuple of arguments, expecting a matching tuple of coefficient derivatives.") + 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(v.ufl_shape) - oi1 = oi[:-rv] - oi2 = oi[-rv:] + rv = len(oi) - len(v.ufl_shape) + oi1 = oi[:rv] + oi2 = oi[rv:] prod = so * v[oi2] if oi1: dosum += as_tensor(prod, oi1) @@ -869,7 +1025,10 @@ def coefficient(self, o): return dosum def reference_value(self, o): - error("Currently no support for ReferenceValue in CoefficientDerivative.") + """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 @@ -877,7 +1036,7 @@ def reference_value(self, o): # derivative(...ReferenceValue...,...). # f, = o.ufl_operands # if not f._ufl_is_terminal_: - # error("ReferenceValue can only wrap terminals directly.") + # 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! @@ -886,7 +1045,10 @@ def reference_value(self, o): # return self.independent_terminal(o) def reference_grad(self, o): - error("Currently no support for ReferenceGrad in CoefficientDerivative.") + """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 @@ -894,8 +1056,9 @@ def reference_grad(self, o): # 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), # FIXME: Assert + # 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... @@ -904,10 +1067,11 @@ def grad(self, g): ngrads = 0 o = g while isinstance(o, Grad): - o, = o.ufl_operands + (o,) = o.ufl_operands ngrads += 1 - if not isinstance(o, FormArgument): - error("Expecting gradient of a FormArgument, not %s" % ufl_err_str(o)) + # `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): @@ -916,7 +1080,7 @@ def apply_grads(f): # Find o among all w without any indexing, which makes this # easy - for (w, v) in zip(self._w, self._v): + 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) @@ -935,9 +1099,9 @@ def analyse_variation_argument(v): vval, vcomp = v.ufl_operands vcomp = tuple(vcomp) else: - error("Expecting argument or component of argument.") + raise ValueError("Expecting argument or component of argument.") if not all(isinstance(k, FixedIndex) for k in vcomp): - error("Expecting only fixed indices in variation.") + raise ValueError("Expecting only fixed indices in variation.") return vval, vcomp def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): @@ -956,10 +1120,11 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): # Accumulate contributions from variations in different # components - for (w, v) in zip(self._w, self._v): + for w, v in zip(self._w, self._v): + # -- Analyse differentiation variable coefficient -- # - # Analyse differentiation variable coefficient - if isinstance(w, FormArgument): + # Can differentiate a Form wrt a BaseFormOperator + if isinstance(w, (FormArgument, BaseFormOperator)): if not w == o: continue wshape = w.ufl_shape @@ -973,20 +1138,24 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): 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) + gprimesum = gprimesum + compute_gprimeterm( + ngrads, vval, vcomp, wshape, wcomp + ) + elif isinstance(v, Zero): + pass else: if wshape != (): - error("Expecting scalar coefficient in this branch.") + 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) + gprimesum = gprimesum + compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp) - elif isinstance(w, Indexed): # This path is tested in unit tests, but not actually used? + 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 @@ -994,14 +1163,14 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): continue assert isinstance(wval, FormArgument) if not all(isinstance(k, FixedIndex) for k in wcomp): - error("Expecting only fixed indices in differentiation variable.") + 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: - error("Expecting coefficient or component of coefficient.") + raise ValueError("Expecting coefficient or component of coefficient.") # FIXME: Handle other coefficient derivatives: oprimes = # self._cd.get(o) @@ -1013,22 +1182,24 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): # TODO: Make it possible to silence this message # in particular? It may be good to have for # debugging... - warning("Assuming d{%s}/d{%s} = 0." % (o, self._w)) + 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): - error("Got a tuple of arguments, expecting a" - " matching tuple of coefficient derivatives.") + 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): - error("FIXME: Figure out how to do this with ngrads") + 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] @@ -1042,64 +1213,245 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): 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): - error("Missing derivative handler for {0}.".format(type(o).__name__)) + """Apply to a derivative.""" + raise NotImplementedError(f"Missing derivative handler for {type(o).__name__}.") - expr = MultiFunction.reuse_if_untouched + 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]) + 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]) + 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]) + 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 - rules = GateauxDerivativeRuleset(w, v, cd) + pending_operations = BaseFormOperatorDerivativeRecorder( + f, w, arguments=v, coefficient_derivatives=cd + ) + rules = GateauxDerivativeRuleset(w, v, cd, pending_operations) key = (GateauxDerivativeRuleset, w, v, cd) - return map_expr_dag(rules, f, - vcache=self.vcaches[key], - rcache=self.rcaches[key]) + # 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]) + 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 @@ -1131,32 +1483,163 @@ def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in generic rules 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() - return map_integrand_dags(rules, expression) + + # 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 - + 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): - error("Expecting a ExprList of coefficients.") + raise ValueError("Expecting a ExprList of coefficients.") if not isinstance(arguments, ExprList): - error("Expecting a ExprList of arguments.") + raise ValueError("Expecting a ExprList of arguments.") if not isinstance(coefficient_derivatives, ExprMapping): - error("Expecting a coefficient-coefficient 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 @@ -1176,20 +1659,29 @@ def __init__(self, coefficients, arguments, coefficient_derivatives): argument = GenericDerivativeRuleset.independent_terminal def coefficient(self, o): - error("CoordinateDerivative of coefficient in physical space is not implemented.") + """Differentiate a coefficient.""" + raise NotImplementedError( + "CoordinateDerivative of coefficient in physical space is not implemented." + ) def grad(self, o): - error("CoordinateDerivative grad in physical space is not implemented.") + """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: - error("Not implemented: CoordinateDerivative found a SpatialCoordinate that is different from the one being differentiated.") + 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 @@ -1197,14 +1689,15 @@ def reference_value(self, o): 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 + (o,) = o.ufl_operands ngrads += 1 if not (isinstance(o, SpatialCoordinate) or isinstance(o.ufl_operands[0], FormArgument)): - error("Expecting gradient of a FormArgument, not %s" % ufl_err_str(o)) + raise ValueError(f"Expecting gradient of a FormArgument, not {ufl_err_str(o)}") def apply_grads(f): for i in range(ngrads): @@ -1213,50 +1706,68 @@ def apply_grads(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): + 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 o.ufl_domain() == w.ufl_domain() and isinstance(v.ufl_operands[0], FormArgument): + 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): - error("Missing derivative handler for {0}.".format(type(o).__name__)) + """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 - spaces = set(c.family() for c in extract_unique_elements(o)) - unsupported_spaces = {"Argyris", "Bell", "Hermite", "Morley"} - if spaces & unsupported_spaces: - error("CoordinateDerivative is not supported for elements of type %s. " - "This is because their pullback is not implemented in UFL." % unsupported_spaces) + + 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) @@ -1264,5 +1775,6 @@ def coordinate_derivative(self, o, f, w, v, cd): def apply_coordinate_derivatives(expression): + """Apply coordinate derivatives to an expression.""" rules = CoordinateDerivativeRuleDispatcher() return map_integrand_dags(rules, expression) diff --git a/ufl/algorithms/apply_function_pullbacks.py b/ufl/algorithms/apply_function_pullbacks.py index 737fba334..124c2a2dd 100644 --- a/ufl/algorithms/apply_function_pullbacks.py +++ b/ufl/algorithms/apply_function_pullbacks.py @@ -1,200 +1,61 @@ -# -*- coding: utf-8 -*- -"""Algorithm for replacing gradients in an expression with reference gradients and coordinate mappings.""" +"""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 -# -# Modified by Lizao Li , 2016 - -from itertools import chain, accumulate, repeat -from ufl.log import error - -from ufl.core.multiindex import indices -from ufl.corealg.multifunction import MultiFunction, memoized_handler from ufl.algorithms.map_integrands import map_integrand_dags - -from ufl.classes import (ReferenceValue, - Jacobian, JacobianInverse, JacobianDeterminant) - -from ufl.tensors import as_tensor, as_vector -from ufl.utils.sequences import product -import numpy - - -def sub_elements_with_mappings(element): - "Return an ordered list of the largest subelements that have a defined mapping." - if element.mapping() != "undefined": - return [element] - elements = [] - for subelm in element.sub_elements(): - if subelm.mapping() != "undefined": - elements.append(subelm) - else: - elements.extend(sub_elements_with_mappings(subelm)) - return elements - - -def create_nested_lists(shape): - if len(shape) == 0: - return [None] - elif len(shape) == 1: - return [None] * shape[0] - else: - return [create_nested_lists(shape[1:]) for i in range(shape[0])] - - -def reshape_to_nested_list(components, shape): - if len(shape) == 0: - assert len(components) == 1 - return [components[0]] - elif len(shape) == 1: - assert len(components) == shape[0] - return components - else: - n = product(shape[1:]) - return [reshape_to_nested_list(components[n * i:n * (i + 1)], shape[1:]) for i in range(shape[0])] - - -def apply_known_single_pullback(r, element): - """Apply pullback with given mapping. - - :arg r: Expression wrapped in ReferenceValue - :arg element: The element defining the mapping - """ - # Need to pass in r rather than the physical space thing, because - # the latter may be a ListTensor or similar, rather than a - # Coefficient/Argument (in the case of mixed elements, see below - # in apply_single_function_pullbacks), to which we cannot apply ReferenceValue - mapping = element.mapping() - domain = r.ufl_domain() - if mapping == "physical": - return r - elif mapping == "identity": - return r - elif mapping == "contravariant Piola": - J = Jacobian(domain) - detJ = JacobianDeterminant(J) - transform = (1.0 / detJ) * J - # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) - *k, i, j = indices(len(r.ufl_shape) + 1) - kj = (*k, j) - f = as_tensor(transform[i, j] * r[kj], (*k, i)) - return f - elif mapping == "covariant Piola": - K = JacobianInverse(domain) - # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) - *k, i, j = indices(len(r.ufl_shape) + 1) - kj = (*k, j) - f = as_tensor(K[j, i] * r[kj], (*k, i)) - return f - elif mapping == "L2 Piola": - detJ = JacobianDeterminant(domain) - return r / detJ - elif mapping == "double contravariant Piola": - J = Jacobian(domain) - detJ = JacobianDeterminant(J) - transform = (1.0 / detJ) * J - # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) - *k, i, j, m, n = indices(len(r.ufl_shape) + 2) - kmn = (*k, m, n) - f = as_tensor((1.0 / detJ)**2 * J[i, m] * r[kmn] * J[j, n], (*k, i, j)) - return f - elif mapping == "double covariant Piola": - K = JacobianInverse(domain) - # Apply transform "row-wise" to TensorElement(PiolaMapped, ...) - *k, i, j, m, n = indices(len(r.ufl_shape) + 2) - kmn = (*k, m, n) - f = as_tensor(K[m, i] * r[kmn] * K[n, j], (*k, i, j)) - return f - else: - error("Should never be reached!") - - -def apply_single_function_pullbacks(r, element): - """Apply an appropriate pullback to something in physical space - - :arg r: An expression wrapped in ReferenceValue. - :arg element: The element this expression lives in. - :returns: a pulled back expression.""" - mapping = element.mapping() - if r.ufl_shape != element.reference_value_shape(): - error("Expecting reference space expression with shape '%s', got '%s'" % (element.reference_value_shape(), r.ufl_shape)) - if mapping in {"physical", "identity", - "contravariant Piola", "covariant Piola", - "double contravariant Piola", "double covariant Piola", - "L2 Piola"}: - # Base case in recursion through elements. If the element - # advertises a mapping we know how to handle, do that - # directly. - f = apply_known_single_pullback(r, element) - if f.ufl_shape != element.value_shape(): - error("Expecting pulled back expression with shape '%s', got '%s'" % (element.value_shape(), f.ufl_shape)) - return f - elif mapping in {"symmetries", "undefined"}: - # Need to pull back each unique piece of the reference space thing - gsh = element.value_shape() - rsh = r.ufl_shape - if mapping == "symmetries": - subelem = element.sub_elements()[0] - fcm = element.flattened_sub_element_mapping() - offsets = (product(subelem.reference_value_shape()) * i for i in fcm) - elements = repeat(subelem) - else: - elements = sub_elements_with_mappings(element) - # Python >= 3.8 has an initial keyword argument to - # accumulate, but 3.7 does not. - offsets = chain([0], - accumulate(product(e.reference_value_shape()) - for e in elements)) - rflat = as_vector([r[idx] for idx in numpy.ndindex(rsh)]) - g_components = [] - # For each unique piece in reference space, apply the appropriate pullback - for offset, subelem in zip(offsets, elements): - sub_rsh = subelem.reference_value_shape() - rm = product(sub_rsh) - rsub = [rflat[offset + i] for i in range(rm)] - rsub = as_tensor(numpy.asarray(rsub).reshape(sub_rsh)) - rmapped = apply_single_function_pullbacks(rsub, subelem) - # Flatten into the pulled back expression for the whole thing - g_components.extend([rmapped[idx] - for idx in numpy.ndindex(rmapped.ufl_shape)]) - # And reshape appropriately - f = as_tensor(numpy.asarray(g_components).reshape(gsh)) - if f.ufl_shape != element.value_shape(): - error("Expecting pulled back expression with shape '%s', got '%s'" % (element.value_shape(), f.ufl_shape)) - return f - else: - error("Unhandled mapping type '%s'" % mapping) +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 - f = apply_single_function_pullbacks(ReferenceValue(o), o.ufl_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 expression - by applying Piola mappings where applicable and representing all + """Change representation of coefficients and arguments in an expression. + + Applies Piola mappings where applicable and represents all form arguments in reference value. - @param expr: - An Expr. + Args: + expr: An Expression """ return map_integrand_dags(FunctionPullbackApplier(), expr) diff --git a/ufl/algorithms/apply_geometry_lowering.py b/ufl/algorithms/apply_geometry_lowering.py index 6e96a922c..0daac08ad 100644 --- a/ufl/algorithms/apply_geometry_lowering.py +++ b/ufl/algorithms/apply_geometry_lowering.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Algorithm for lowering abstractions of geometric types. This means replacing high-level types with expressions @@ -11,40 +10,52 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import warnings from functools import reduce from itertools import combinations -from ufl.log import error, warning - +from ufl.classes import ( + CellCoordinate, + CellEdgeJacobian, + CellEdgeVectors, + CellFacetJacobian, + CellOrientation, + CellOrigin, + CellVertices, + CellVolume, + EdgeJacobian, + 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.multifunction import MultiFunction, memoized_handler 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.classes import (Expr, Form, Integral, - ReferenceGrad, - Jacobian, JacobianInverse, JacobianDeterminant, - CellOrientation, CellOrigin, CellCoordinate, - FacetJacobian, FacetJacobianDeterminant, - EdgeJacobian, # EdgeJacobianDeterminant, - CellFacetJacobian, CellEdgeJacobian, - MaxCellEdgeLength, - CellEdgeVectors, FacetEdgeVectors, CellVertices, - ReferenceNormal, - ReferenceCellVolume, ReferenceFacetVolume, CellVolume, - SpatialCoordinate, - FloatValue) -# FacetJacobianInverse, -# FacetOrientation, QuadratureWeight, - +from ufl.operators import conj, max_value, min_value, real, sqrt from ufl.tensors import as_tensor, as_vector -from ufl.operators import sqrt, max_value, min_value, conj, real - -from ufl.compound_expressions import determinant_expr, cross_expr, inverse_expr 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_ @@ -54,15 +65,17 @@ def __init__(self, preserve_types=()): 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 = o.ufl_domain() - if domain.ufl_coordinate_element().mapping() != "identity": - error("Piola mapped coordinates are not implemented.") + 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 @@ -72,6 +85,7 @@ def jacobian(self, o): @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 @@ -80,10 +94,11 @@ def _future_jacobian(self, o): @memoized_handler def jacobian_inverse(self, o): + """Apply to jacobian_inverse.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) # TODO: This could in principle use # preserve_types[JacobianDeterminant] with minor refactoring: @@ -92,10 +107,11 @@ def jacobian_inverse(self, o): @memoized_handler def jacobian_determinant(self, o): + """Apply to jacobian_determinant.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) detJ = determinant_expr(J) @@ -109,10 +125,11 @@ def jacobian_determinant(self, o): @memoized_handler def facet_jacobian(self, o): + """Apply to facet_jacobian.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) J = self.jacobian(Jacobian(domain)) RFJ = CellFacetJacobian(domain) i, j, k = indices(3) @@ -120,10 +137,11 @@ def facet_jacobian(self, o): @memoized_handler def facet_jacobian_inverse(self, o): + """Apply to facet_jacobian_inverse.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) FJ = self.facet_jacobian(FacetJacobian(domain)) # This could in principle use # preserve_types[JacobianDeterminant] with minor refactoring: @@ -131,10 +149,11 @@ def facet_jacobian_inverse(self, o): @memoized_handler def facet_jacobian_determinant(self, o): + """Apply to facet_jacobian_determinant.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) FJ = self.facet_jacobian(FacetJacobian(domain)) detFJ = determinant_expr(FJ) @@ -179,22 +198,28 @@ def edge_jacobian_determinant(self, o): @memoized_handler def spatial_coordinate(self, o): - "Fall through to coordinate field of domain if it exists." + """Apply to spatial_coordinate. + + Fall through to coordinate field of domain if it exists. + """ if self._preserve_types[o._ufl_typecode_]: return o - if o.ufl_domain().ufl_coordinate_element().mapping() != "identity": - error("Piola mapped coordinates are not implemented.") + 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): - "Compute from physical coordinates if they are known, using the appropriate mappings." + """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 = o.ufl_domain() + domain = extract_unique_domain(o) K = self.jacobian_inverse(JacobianInverse(domain)) x = self.spatial_coordinate(SpatialCoordinate(domain)) x0 = CellOrigin(domain) @@ -204,22 +229,27 @@ def cell_coordinate(self, o): @memoized_handler def facet_cell_coordinate(self, o): + """Apply to facet_cell_coordinate.""" if self._preserve_types[o._ufl_typecode_]: return o - error("Missing computation of facet reference coordinates " - "from physical coordinates via mappings.") + 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 = o.ufl_domain() + 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 - warning("Only know how to compute the cell volume of an affine cell.") + warnings.warn( + "Only know how to compute the cell volume of an affine cell.") return o r = self.jacobian_determinant(JacobianDeterminant(domain)) @@ -228,15 +258,17 @@ def cell_volume(self, o): @memoized_handler def facet_area(self, o): + """Apply to facet_area.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + 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 - warning("Only know how to compute the facet area of an affine cell.") + 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 @@ -249,13 +281,15 @@ def facet_area(self, o): @memoized_handler def circumradius(self, o): + """Apply to circumradius.""" if self._preserve_types[o._ufl_typecode_]: return o - domain = o.ufl_domain() + domain = extract_unique_domain(o) if not domain.is_piecewise_linear_simplex_domain(): - error("Circumradius only makes sense for affine simplex cells") + raise ValueError( + "Circumradius only makes sense for affine simplex cells") cellname = domain.ufl_cell().cellname() cellvolume = self.cell_volume(CellVolume(domain)) @@ -268,7 +302,8 @@ def circumradius(self, o): 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)] + 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) @@ -280,7 +315,7 @@ def circumradius(self, o): lb = elen[4] * elen[1] lc = elen[5] * elen[0] # p = perimeter - p = (la + lb + lc) + p = la + lb + lc # s = semiperimeter s = p / 2 # area of intermediate triangle with Herons formula @@ -289,21 +324,25 @@ def circumradius(self, o): @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 = o.ufl_domain() + domain = extract_unique_domain(o) - if not domain.ufl_coordinate_element().degree() == 1: + if domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler - warning("Only know how to compute cell edge lengths of P1 or Q1 cell.") + warnings.warn( + "Only know how to compute cell edge lengths of P1 or Q1 cell.") return o elif domain.ufl_cell().cellname() == "interval": @@ -315,19 +354,22 @@ def _reduce_cell_edge_length(self, o, reduction_op): 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)] + 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 = o.ufl_domain() + domain = extract_unique_domain(o) - if not domain.ufl_coordinate_element().degree() in {1, (1, 1)}: + if domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler - warning("Only know how to compute cell diameter of P1 or Q1 cell.") + warnings.warn( + "Only know how to compute cell diameter of P1 or Q1 cell.") return o elif domain.is_piecewise_linear_simplex_domain(): @@ -339,29 +381,35 @@ def cell_diameter(self, o): 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)) + 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 = o.ufl_domain() + domain = extract_unique_domain(o) if domain.ufl_cell().topological_dimension() < 3: - error("Facet edge lengths only make sense for topological dimension >= 3.") + raise ValueError( + "Facet edge lengths only make sense for topological dimension >= 3.") - elif not domain.ufl_coordinate_element().degree() == 1: + elif domain.ufl_coordinate_element().embedded_subdegree > 1: # Don't lower bendy cells, instead leave it to form compiler - warning("Only know how to compute facet edge lengths of P1 or Q1 cell.") + warnings.warn( + "Only know how to compute facet edge lengths of P1 or Q1 cell.") return o else: @@ -369,15 +417,17 @@ def _reduce_facet_edge_length(self, o, reduction_op): 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)] + 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 = o.ufl_domain() + domain = extract_unique_domain(o) gdim = domain.geometric_dimension() tdim = domain.topological_dimension() @@ -395,21 +445,24 @@ def cell_normal(self, o): # to the 'right') cell_normal = as_vector((-J[1, 0], J[0, 0])) else: - error("Cell normal not implemented for tdim %d, gdim %d" % (tdim, gdim)) + 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: - error("What do you want cell normal in gdim={0}, tdim={1} to be?".format(gdim, tdim)) + 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 = o.ufl_domain() + domain = extract_unique_domain(o) tdim = domain.topological_dimension() if tdim == 1: @@ -446,7 +499,9 @@ def facet_normal(self, o): r = n if r.ufl_shape != o.ufl_shape: - error("Inconsistent dimensions (in=%d, out=%d)." % (o.ufl_shape[0], r.ufl_shape[0])) + raise ValueError( + f"Inconsistent dimensions (in={o.ufl_shape[0]}, out={r.ufl_shape[0]})." + ) return r @@ -455,12 +510,14 @@ def apply_geometry_lowering(form, preserve_types=()): Assumes the expression is preprocessed or at least that derivatives have been expanded. - @param form: - An Expr or Form. + 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()] + newintegrals = [ + apply_geometry_lowering(integral, preserve_types) for integral in form.integrals() + ] return Form(newintegrals) elif isinstance(form, Integral): @@ -481,4 +538,4 @@ def apply_geometry_lowering(form, preserve_types=()): return map_expr_dag(mf, expr) else: - error("Invalid type %s" % (form.__class__.__name__,)) + raise ValueError(f"Invalid type {form.__class__.__name__}") diff --git a/ufl/algorithms/apply_integral_scaling.py b/ufl/algorithms/apply_integral_scaling.py index b190f1e0c..1ca906c62 100644 --- a/ufl/algorithms/apply_integral_scaling.py +++ b/ufl/algorithms/apply_integral_scaling.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""Algorithm for replacing gradients in an expression with reference gradients and coordinate mappings.""" +"""Algorithm for replacing gradients in an expression.""" # Copyright (C) 2013-2016 Martin Sandve Alnæs # @@ -7,17 +6,22 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import error -from ufl.classes import JacobianDeterminant, FacetJacobianDeterminant, EdgeJacobianDeterminant, QuadratureWeight, Form, Integral -from ufl.measure import custom_integral_types, point_integral_types -from ufl.differentiation import CoordinateDerivative from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree +from ufl.classes import ( + EdgeJacobianDeterminant, + 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) @@ -55,7 +59,7 @@ def compute_integrand_scaling_factor(integral): # side and quadrature weight detFJ = FacetJacobianDeterminant(domain) degree = estimate_total_polynomial_degree(apply_geometry_lowering(detFJ)) - scale = detFJ('+') * weight + scale = detFJ("+") * weight else: # No need to scale 'integral' over a vertex scale = 1 @@ -80,18 +84,17 @@ def compute_integrand_scaling_factor(integral): scale = 1 else: - error("Unknown integral type {}, don't know how to scale.".format(integral_type)) + 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." + """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()] + newintegrals = [apply_integral_scaling(integral) for integral in form.integrals()] return Form(newintegrals) elif isinstance(form, Integral): @@ -117,13 +120,17 @@ def apply_integral_scaling(form): 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]) + 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: - error("Invalid type %s" % (form.__class__.__name__,)) + raise ValueError(f"Invalid type {form.__class__.__name__}") diff --git a/ufl/algorithms/apply_restrictions.py b/ufl/algorithms/apply_restrictions.py index 16dbd2839..8ad23eff0 100644 --- a/ufl/algorithms/apply_restrictions.py +++ b/ufl/algorithms/apply_restrictions.py @@ -1,5 +1,8 @@ -# -*- coding: utf-8 -*- -"""This module contains the apply_restrictions algorithm which propagates restrictions in a form towards the terminals.""" +"""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 # @@ -7,18 +10,20 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - -from ufl.log import error +from ufl.algorithms.map_integrands import map_integrand_dags from ufl.classes import Restricted -from ufl.corealg.multifunction import MultiFunction from ufl.corealg.map_dag import map_expr_dag -from ufl.algorithms.map_integrands import map_integrand_dags +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 = "+" @@ -26,51 +31,58 @@ def __init__(self, side=None): self.vcaches = {"+": {}, "-": {}} self.rcaches = {"+": {}, "-": {}} if self.current_restriction is None: - self._rp = {"+": RestrictionPropagator("+"), - "-": RestrictionPropagator("-")} + self._rp = {"+": RestrictionPropagator("+"), "-": RestrictionPropagator("-")} def restricted(self, o): - "When hitting a restricted quantity, visit child with a separate restriction algorithm." + """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: - error("Cannot restrict an expression twice.") + 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]) + 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." + """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." + """Restrict a discontinuous quantity to current side, require a side to be set.""" if self.current_restriction is None: - error("Discontinuous type %s must be restricted." % o._ufl_class_.__name__) + 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." + """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." + """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: - error("Discontinuous type %s must be restricted." % o._ufl_class_.__name__) + 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): - error("Missing rule for %s" % o._ufl_class_.__name__) + """Raise an error.""" + raise ValueError(f"Missing rule for {o._ufl_class_.__name__}") # --- Rules for operators @@ -84,12 +96,12 @@ def _missing_rule(self, o): grad = _require_restriction def variable(self, o, op, label): - "Strip variable." + """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 + """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): @@ -128,22 +140,26 @@ def reference_value(self, o): reference_facet_volume = _ignore_restriction def coefficient(self, o): - "Allow coefficients to be unrestricted (apply default if so) if the values are fully continuous across the facet." + """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 - return self._default_restricted(o) # Must still be computed from one of the sides, we just don't care which + # 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): - D = o.ufl_domain() + """Restrict a facet_normal.""" + D = extract_unique_domain(o) e = D.ufl_coordinate_element() - f = e.family() - d = e.degree() gd = D.geometric_dimension() td = D.topological_dimension() - if f == "Lagrange" and d == 1 and gd == td: + 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 @@ -159,24 +175,27 @@ def facet_normal(self, 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")] + """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) + 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("-")} + self._rp = {"+": DefaultRestrictionApplier("+"), "-": DefaultRestrictionApplier("-")} def terminal(self, o): + """Apply to terminal.""" # Most terminals are unchanged return o @@ -184,17 +203,19 @@ def terminal(self, o): 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." + """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 @@ -222,9 +243,10 @@ def _default_restricted(self, o): 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")] + 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) + return map_integrand_dags(rules, expression, only_integral_type=integral_types) diff --git a/ufl/algorithms/balancing.py b/ufl/algorithms/balancing.py index 9e0a6b741..e671d01d0 100644 --- a/ufl/algorithms/balancing.py +++ b/ufl/algorithms/balancing.py @@ -1,3 +1,4 @@ +"""Balancing.""" # -*- coding: utf-8 -*- # Copyright (C) 2011-2017 Martin Sandve Alnæs # @@ -5,23 +6,35 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.classes import (CellAvg, FacetAvg, Grad, Indexed, NegativeRestricted, - PositiveRestricted, ReferenceGrad, ReferenceValue) +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 + ReferenceValue, + ReferenceGrad, + Grad, + CellAvg, + FacetAvg, + PositiveRestricted, + NegativeRestricted, + Indexed, ] -modifier_precedence = { - m._ufl_handler_name_: i - for i, m in enumerate(modifier_precedence) -} +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. @@ -42,10 +55,9 @@ def balance_modified_terminal(expr): assert expr._ufl_is_terminal_ # Apply modifiers in order - layers = sorted( - layers[:-1], key=lambda e: modifier_precedence[e._ufl_handler_name_]) + layers = sorted(layers[:-1], key=lambda e: modifier_precedence[e._ufl_handler_name_]) for op in layers: - ops = (expr, ) + op.ufl_operands[1:] + ops = (expr,) + op.ufl_operands[1:] expr = op._ufl_expr_reconstruct_(*ops) # Preserve id if nothing has changed @@ -53,13 +65,18 @@ def balance_modified_terminal(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 @@ -72,5 +89,6 @@ def _modifier(self, expr, *ops): def balance_modifiers(expr): + """Balance modifiers.""" mf = BalanceModifiers() return map_expr_dag(mf, expr) diff --git a/ufl/algorithms/change_to_reference.py b/ufl/algorithms/change_to_reference.py index bd989f946..17b106ad7 100644 --- a/ufl/algorithms/change_to_reference.py +++ b/ufl/algorithms/change_to_reference.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""Algorithm for replacing gradients in an expression with reference gradients and coordinate mappings.""" +"""Algorithm for replacing gradients in an expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -7,38 +6,29 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import error - -from ufl.core.multiindex import indices -from ufl.corealg.multifunction import MultiFunction -from ufl.corealg.map_dag import map_expr_dag - -from ufl.classes import (FormArgument, GeometricQuantity, - Terminal, ReferenceGrad, Grad, Restricted, ReferenceValue, - Jacobian, JacobianInverse, JacobianDeterminant, - Indexed, MultiIndex, FixedIndex) - -from ufl.constantvalue import as_ufl -from ufl.tensors import as_tensor -from ufl.permutation import compute_indices - -from ufl.finiteelement import MixedElement - 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. +# 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 +# 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! +# 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,) @@ -46,14 +36,16 @@ # 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 +# 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) +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] @@ -115,277 +107,44 @@ """ -# FIXME: This implementation semeed to work last year but lead to performance problems. Look through and test again now. -class NEWChangeToReferenceGrad(MultiFunction): - def __init__(self): - MultiFunction.__init__(self) - self._ngrads = 0 - self._restricted = '' - self._avg = '' - - def expr(self, o, *ops): - return o._ufl_expr_reconstruct_(*ops) - - def terminal(self, o): - return o +class ChangeToReferenceGrad(MultiFunction): + """Change to reference grad.""" - def coefficient_derivative(self, o, *dummy_ops): - error("Coefficient derivatives should be expanded before applying change to reference grad.") - - def reference_grad(self, o, *dummy_ops): - error("Not expecting reference grad while applying change to reference grad.") - - def restricted(self, o, *dummy_ops): - "Store modifier state." - if self._restricted != '': - error("Not expecting nested restrictions.") - self._restricted = o.side() - f, = o.ufl_operands - r = self(f) - self._restricted = '' - return r - - def grad(self, o, *dummy_ops): - "Store modifier state." - self._ngrads += 1 - f, = o.ufl_operands - r = self(f) - self._ngrads -= 1 - return r - - def facet_avg(self, o, *dummy_ops): - if self._avg != '': - error("Not expecting nested averages.") - self._avg = "facet" - f, = o.ufl_operands - r = self(f) - self._avg = "" - return r - - def cell_avg(self, o, *dummy_ops): - if self._avg != '': - error("Not expecting nested averages.") - self._avg = "cell" - f, = o.ufl_operands - r = self(f) - self._avg = "" - return r - - def form_argument(self, t): - return self._mapped(t) - - def geometric_quantity(self, t): - if self._restricted or self._ngrads or self._avg: - return self._mapped(t) - else: - return t - - def _mapped(self, t): - # Check that we have a valid input object - if not isinstance(t, Terminal): - error("Expecting a Terminal.") - - # Get modifiers accumulated by previous handler calls - ngrads = self._ngrads - restricted = self._restricted - avg = self._avg - if avg != "": - error("Averaging not implemented.") # FIXME - - # These are the global (g) and reference (r) values - if isinstance(t, FormArgument): - g = t - r = ReferenceValue(g) - elif isinstance(t, GeometricQuantity): - g = t - r = g - else: - error("Unexpected type {0}.".format(type(t).__name__)) - - # Some geometry mapping objects we may need multiple times below - domain = t.ufl_domain() - J = Jacobian(domain) - detJ = JacobianDeterminant(domain) - K = JacobianInverse(domain) - - # Restrict geometry objects if applicable - if restricted: - J = J(restricted) - detJ = detJ(restricted) - K = K(restricted) - - # Create Hdiv mapping from possibly restricted geometry objects - Mdiv = (1.0 / detJ) * J - - # Get component indices of global and reference terminal objects - gtsh = g.ufl_shape - # rtsh = r.ufl_shape - gtcomponents = compute_indices(gtsh) - # rtcomponents = compute_indices(rtsh) - - # Create core modified terminal, with eventual - # layers of grad applied directly to the terminal, - # then eventual restriction applied last - for i in range(ngrads): - g = Grad(g) - r = ReferenceGrad(r) - if restricted: - g = g(restricted) - r = r(restricted) - - # Get component indices of global and reference objects with - # grads applied - gsh = g.ufl_shape - # rsh = r.ufl_shape - # gcomponents = compute_indices(gsh) - # rcomponents = compute_indices(rsh) - - # Get derivative component indices - dsh = gsh[len(gtsh):] - dcomponents = compute_indices(dsh) - - # Create nested array to hold expressions for global - # components mapped from reference values - def ndarray(shape): - if len(shape) == 0: - return [None] - elif len(shape) == 1: - return [None] * shape[-1] - else: - return [ndarray(shape[1:]) for i in range(shape[0])] - global_components = ndarray(gsh) - - # Compute mapping from reference values for each global component - for gtc in gtcomponents: - - if isinstance(t, FormArgument): - - # Find basic subelement and element-local component - # ec, element, eoffset = t.ufl_element().extract_component2(gtc) # FIXME: Translate this correctly - eoffset = 0 - ec, element = t.ufl_element().extract_reference_component(gtc) - - # Select mapping M from element, pick row emapping = - # M[ec,:], or emapping = [] if no mapping - if isinstance(element, MixedElement): - error("Expecting a basic element here.") - mapping = element.mapping() - if mapping == "contravariant Piola": # S == HDiv: - # Handle HDiv elements with contravariant piola - # mapping contravariant_hdiv_mapping = (1/det J) * - # J * PullbackOf(o) - ec, = ec - emapping = Mdiv[ec, :] - elif mapping == "covariant Piola": # S == HCurl: - # Handle HCurl elements with covariant piola mapping - # covariant_hcurl_mapping = JinvT * PullbackOf(o) - ec, = ec - emapping = K[:, ec] # Column of K is row of K.T - elif mapping == "identity": - emapping = None - else: - error("Unknown mapping {0}".format(mapping)) - - elif isinstance(t, GeometricQuantity): - eoffset = 0 - emapping = None - - else: - error("Unexpected type {0}.".format(type(t).__name__)) - - # Create indices - # if rtsh: - # i = Index() - if len(dsh) != ngrads: - error("Mismatch between derivative shape and ngrads.") - if ngrads: - ii = indices(ngrads) - else: - ii = () - - # Apply mapping row to reference object - if emapping: # Mapped, always nonscalar terminal Not - # using IndexSum for the mapping row dot product to - # keep it simple, because we don't have a slice type - emapped_ops = [emapping[s] * Indexed(r, MultiIndex((FixedIndex(eoffset + s),) + ii)) - for s in range(len(emapping))] - emapped = sum(emapped_ops[1:], emapped_ops[0]) - elif gtc: # Nonscalar terminal, unmapped - emapped = Indexed(r, MultiIndex((FixedIndex(eoffset),) + ii)) - elif ngrads: # Scalar terminal, unmapped, with derivatives - emapped = Indexed(r, MultiIndex(ii)) - else: # Scalar terminal, unmapped, no derivatives - emapped = r - - for di in dcomponents: - # Multiply derivative mapping rows, parameterized by - # free column indices - dmapping = as_ufl(1) - for j in range(ngrads): - dmapping *= K[ii[j], di[j]] # Row of K is column of JinvT - - # Compute mapping from reference values for this - # particular global component - global_value = dmapping * emapped - - # Apply index sums - # if rtsh: - # global_value = IndexSum(global_value, MultiIndex((i,))) - # for j in range(ngrads): # Applied implicitly in the dmapping * emapped above - # global_value = IndexSum(global_value, MultiIndex((ii[j],))) - - # This is the component index into the full object - # with grads applied - gc = gtc + di - - # Insert in nested list - comp = global_components - for i in gc[:-1]: - comp = comp[i] - comp[0 if gc == () else gc[-1]] = global_value - - # Wrap nested list in as_tensor unless we have a scalar - # expression - if gsh: - tensor = as_tensor(global_components) - else: - tensor, = global_components - return tensor - - -class OLDChangeToReferenceGrad(MultiFunction): 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 = '' + restricted = "" rv = False while not o._ufl_is_terminal_: if isinstance(o, Grad): - o, = o.ufl_operands + (o,) = o.ufl_operands ngrads += 1 elif isinstance(o, Restricted): restricted = o.side() - o, = o.ufl_operands + (o,) = o.ufl_operands elif isinstance(o, ReferenceValue): rv = True - o, = o.ufl_operands + (o,) = o.ufl_operands else: - error("Invalid type %s" % o._ufl_class_.__name__) + raise ValueError(f"Invalid type {o._ufl_class_.__name__}") f = o if rv: f = ReferenceValue(f) # Get domain and create Jacobian inverse object - domain = o.ufl_domain() + domain = extract_unique_domain(o) Jinv = JacobianInverse(domain) if is_cellwise_constant(Jinv): @@ -395,7 +154,9 @@ def grad(self, o): # 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 + 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 @@ -427,7 +188,9 @@ def grad(self, o): 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 + 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) @@ -439,28 +202,31 @@ def grad(self, o): return jinv_lgrad_f def reference_grad(self, o): - error("Not expecting reference grad while applying change to reference grad.") + """Apply to reference_grad.""" + raise ValueError("Not expecting reference grad while applying change to reference grad.") def coefficient_derivative(self, o): - error("Coefficient derivatives should be expanded before applying change to reference grad.") + """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. + Assumes the expression is preprocessed or at least that derivatives + have been expanded. - @param e: - An Expr or Form. + Args: + e: An Expr or Form. """ - mf = OLDChangeToReferenceGrad() - # mf = NEWChangeToReferenceGrad() + 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) diff --git a/ufl/algorithms/check_arities.py b/ufl/algorithms/check_arities.py index 065ebf19d..e1d9b366b 100644 --- a/ufl/algorithms/check_arities.py +++ b/ufl/algorithms/check_arities.py @@ -1,72 +1,93 @@ -# -*- coding: utf-8 -*- - +"""Check arities.""" from itertools import chain +from typing import Tuple -from ufl.log import UFLException -from ufl.corealg.traversal import traverse_unique_terminals -from ufl.corealg.multifunction import MultiFunction -from ufl.corealg.map_dag import map_expr_dag 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(UFLException): +class ArityMismatch(BaseException): + """Arity mismatch exception.""" + pass -# String representation of an arity tuple: -def _afmt(atuple): - return tuple("conj({0})".format(arg) if conj else str(arg) - for arg, conj in atuple) +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("Applying nonlinear operator {0} to expression depending on form argument {1}.".format(o._ufl_class_.__name__, t)) + 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("Adding expressions with non-matching form arguments {0} vs {1}.".format(_afmt(a), _afmt(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("Cannot divide by form argument {0}.".format(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 {0}, argument is {1}.".format(x[0].number(), _afmt(x))) + 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 {0} vs {1}.".format(_afmt(a), _afmt(b))) + 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: @@ -76,14 +97,17 @@ def product(self, o, a, 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 @@ -101,17 +125,20 @@ def linear_operator(self, o, a): # 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, l): + 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("Condition cannot depend on form arguments ({0}).".format(_afmt(a))) + 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 @@ -124,9 +151,13 @@ def conditional(self, o, c, a, b): else: # Do not allow e.g. conditional(c, test, trial), # conditional(c, test, nonzeroconstant) - raise ArityMismatch("Conditional subexpressions with non-matching form arguments {0} vs {1}.".format(_afmt(a), _afmt(b))) + 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 @@ -135,6 +166,7 @@ def linear_indexed_type(self, o, a, i): 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 @@ -143,7 +175,10 @@ def list_tensor(self, o, *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, found {0}.".format(numbers)) + 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()))) @@ -153,13 +188,13 @@ def list_tensor(self, o, *ops): def check_integrand_arity(expr, arguments, complex_mode=False): - arguments = tuple(sorted(set(arguments), - key=lambda x: (x.number(), x.part()))) + """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("Integrand arguments {0} differ from form arguments {1}.".format(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 @@ -169,9 +204,10 @@ def check_integrand_arity(expr, arguments, complex_mode=False): 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("Argument {0} is spuriously conjugated in complex Form".format(arg)) + 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) diff --git a/ufl/algorithms/check_restrictions.py b/ufl/algorithms/check_restrictions.py index 43963fed6..061935981 100644 --- a/ufl/algorithms/check_restrictions.py +++ b/ufl/algorithms/check_restrictions.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Algorithms related to restrictions." +"""Algorithms related to restrictions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -7,46 +6,52 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import error -from ufl.corealg.multifunction import MultiFunction 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: - error("Not expecting twice restricted expression.") + raise ValueError("Not expecting twice restricted expression.") self.current_restriction = o._side - e, = o.ufl_operands + (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: - error("Facet normal must be restricted in interior facet integrals.") + raise ValueError("Facet normal must be restricted in interior facet integrals.") else: if self.current_restriction is not None: - error("Restrictions are only allowed for interior facet integrals.") + 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: - error("Form argument must be restricted in interior facet integrals.") + raise ValueError("Form argument must be restricted in interior facet integrals.") else: if self.current_restriction is not None: - error("Restrictions are only allowed for interior facet integrals.") + 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." + """Check that types that must be restricted are restricted in expression.""" rules = RestrictionChecker(require_restriction) return map_expr_dag(rules, expression) diff --git a/ufl/algorithms/checks.py b/ufl/algorithms/checks.py index 9c2c7a8f9..a25ba9ec5 100644 --- a/ufl/algorithms/checks.py +++ b/ufl/algorithms/checks.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Functions to check the validity of forms.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -10,30 +9,29 @@ # Modified by Anders Logg, 2008-2009. # Modified by Mehdi Nikbakht, 2010. -from ufl.log import error +from ufl.algorithms.check_restrictions import check_restrictions -# UFL classes -from ufl.core.expr import ufl_err_str -from ufl.form import Form +# 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 algorithms -from ufl.algorithms.traversal import iter_expressions +# UFL classes +from ufl.core.expr import ufl_err_str from ufl.corealg.traversal import traverse_unique_terminals -from ufl.algorithms.check_restrictions import check_restrictions +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? +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): - msg = "Validation failed, not a Form:\n%s" % ufl_err_str(form) - error(msg) - # errors.append(msg) - # return errors + 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. @@ -43,9 +41,11 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste # errors.append("Form is not multilinear in arguments.") # FIXME DOMAIN: Add check for consistency between domains somehow - domains = set(t.ufl_domain() - for e in iter_expressions(form) - for t in traverse_unique_terminals(e)) - {None} + 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.") @@ -54,7 +54,7 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste if not cells: errors.append("Missing cell definition in form.") elif len(cells) > 1: - errors.append("Multiple cell definitions in form: %s" % str(cells)) + 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 @@ -67,9 +67,7 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste if c in coefficients: g = coefficients[c] if f is not g: - errors.append("Found different Coefficients with " + - "same count: %s and %s." % (repr(f), - repr(g))) + errors.append(f"Found different Coefficients with same count: {f} and {g}.") else: coefficients[c] = f @@ -85,7 +83,7 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste msg = "TrialFunctions" else: msg = "Arguments with same number and part" - msg = "Found different %s: %s and %s." % (msg, repr(f), repr(g)) + msg = f"Found different {msg}: {f!r} and {g!r}." errors.append(msg) else: arguments[(n, p)] = f @@ -93,8 +91,7 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste # 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: %s\n" % - ufl_err_str(expression)) + errors.append("Found non-scalar integrand expression: {ufl_err_str(expression)}\n") # Check that restrictions are permissible for integral in form.integrals(): @@ -109,5 +106,4 @@ def validate_form(form): # TODO: Can we make this return a list of errors inste # TODO: Return errors list instead, need to collect messages from # all validations above first. if errors: - final_msg = 'Found errors in validation of form:\n%s' % '\n\n'.join(errors) - error(final_msg) + raise ValueError("Found errors in validation of form:\n" + "\n\n".join(errors)) diff --git a/ufl/algorithms/comparison_checker.py b/ufl/algorithms/comparison_checker.py index 3b7347190..509287e74 100644 --- a/ufl/algorithms/comparison_checker.py +++ b/ufl/algorithms/comparison_checker.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -"""Algorithm to check for 'comparison' nodes -in a form when the user is in 'complex mode'""" +"""Algorithm to check for 'comparison' nodes in a form when the user is in 'complex mode'.""" -from ufl.corealg.multifunction import MultiFunction -from ufl.algorithms.map_integrands import map_integrand_dags from ufl.algebra import Real -from ufl.constantvalue import RealValue, Zero +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 @@ -23,16 +21,15 @@ class CheckComparisons(MultiFunction): """ 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. + """Defaults expressions to complex unless they only act on real quantities. - Rebuilds objects if necessary. + Overridden for specific operators. Rebuilds objects if necessary. """ - types = {self.nodetype[op] for op in ops} if types: @@ -45,6 +42,7 @@ def expr(self, o, *ops): return o def compare(self, o, *ops): + """Compare.""" types = {self.nodetype[op] for op in ops} if "complex" in types: @@ -61,6 +59,7 @@ def compare(self, o, *ops): sign = compare def max_value(self, o, *ops): + """Apply to max_value.""" types = {self.nodetype[op] for op in ops} if "complex" in types: @@ -71,6 +70,7 @@ def max_value(self, o, *ops): return o def min_value(self, o, *ops): + """Apply to min_value.""" types = {self.nodetype[op] for op in ops} if "complex" in types: @@ -81,57 +81,64 @@ def min_value(self, o, *ops): return o def real(self, o, *ops): + """Apply to real.""" o = self.reuse_if_untouched(o, *ops) - self.nodetype[o] = 'real' + 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' + 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' + 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' + if self.nodetype[base] == "real" and int(exponent) == exponent: + self.nodetype[o] = "real" return o except TypeError: pass - self.nodetype[o] = 'complex' + 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' + 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' + self.nodetype[term] = "real" else: - self.nodetype[term] = 'complex' + 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""" + """Raises an error if invalid comparison nodes exist.""" return map_integrand_dags(CheckComparisons(), form) -class ComplexComparisonError(Exception): - pass +class ComplexComparisonError(BaseException): + """Complex compariseon exception.""" diff --git a/ufl/algorithms/compute_form_data.py b/ufl/algorithms/compute_form_data.py index b9888d241..687e8edc4 100644 --- a/ufl/algorithms/compute_form_data.py +++ b/ufl/algorithms/compute_form_data.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- -"""This module provides the compute_form_data function which form compilers -will typically call prior to code generation to preprocess/simplify a -raw input form given by a user.""" +"""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) @@ -11,47 +11,48 @@ from itertools import chain -from ufl.log import error, info -from ufl.utils.sequences import max_degree - -from ufl.classes import GeometricFacetQuantity, Coefficient, Form, FunctionSpace -from ufl.corealg.traversal import traverse_unique_terminals from ufl.algorithms.analysis import extract_coefficients, extract_sub_elements, unique_tuple -from ufl.algorithms.formdata import FormData -from ufl.algorithms.formtransformations import compute_form_arities -from ufl.algorithms.check_arities import check_form_arity +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_algebra_lowering import apply_algebra_lowering -from ufl.algorithms.apply_derivatives import apply_derivatives, apply_coordinate_derivatives -from ufl.algorithms.apply_integral_scaling import apply_integral_scaling from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering -from ufl.algorithms.apply_restrictions import apply_restrictions, apply_default_restrictions -from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree -from ufl.algorithms.remove_complex_nodes import remove_complex_nodes +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 -from ufl.algorithms.domain_analysis import reconstruct_form_from_integral_data -from ufl.algorithms.domain_analysis import group_form_integrals +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 in cases - where this has not been specified by the user. This feature is - used by DOLFIN to allow the specification of Expressions with + """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.degree() for e in elements} - {None} | {1}) + return max_degree({e.embedded_superdegree for e in elements} - {None} | {1}) def _compute_element_mapping(form): - "Compute element mapping for element replacement" + """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 @@ -60,8 +61,7 @@ def _compute_element_mapping(form): # 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 = [obj.ufl_element() for obj in chain(form.arguments(), form.coefficients())] elements = extract_sub_elements(elements) # Try to find a common degree for elements @@ -70,25 +70,23 @@ def _compute_element_mapping(form): # Compute element map element_mapping = {} for element in elements: - # Flag for whether element needs to be reconstructed reconstruct = False # Set cell - cell = element.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): - error("Cannot replace unknown element cell without unique common cell in form.") + 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() - info("Adjusting missing element cell to %s." % (cell,)) reconstruct = True # Set degree - degree = element.degree() + degree = element.embedded_superdegree if degree is None: - info("Adjusting missing element degree to %d" % (common_degree,)) degree = common_degree reconstruct = True @@ -102,20 +100,21 @@ def _compute_element_mapping(form): 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 - si = itg_data.subdomain_id - if isinstance(si, int): - newmax = si + 1 - else: - newmax = 0 - prevmax = max_subdomain_ids.get(it, 0) - max_subdomain_ids[it] = max(prevmax, newmax) + 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) @@ -137,15 +136,14 @@ def _compute_form_data_elements(self, arguments, coefficients, domains): def _check_elements(form_data): - for element in chain(form_data.unique_elements, - form_data.unique_sub_elements): - if element.family() is None: - error("Found element with undefined familty: %s" % repr(element)) - if element.cell() is None: - error("Found element with undefined cell: %s" % repr(element)) + """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 @@ -157,22 +155,23 @@ def _check_facet_geometry(integral_data): for expr in traverse_unique_terminals(itg.integrand()): cls = expr._ufl_class_ if issubclass(cls, GeometricFacetQuantity): - error("Integral of type %s cannot contain a %s." % (it, cls.__name__)) + 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 + """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)): - error("All terms in form must have same rank.") + 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. Return mapping from old - to new objects, and lists of the new objects.""" + """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 = {} @@ -185,8 +184,9 @@ def _build_coefficient_replace_map(coefficients, element_mapping=None): # coefficient had a domain, the new one does too. # This should be overhauled with requirement that Expressions # always have a domain. - if f.ufl_domain() is not None: - new_e = FunctionSpace(f.ufl_domain(), new_e) + 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 @@ -197,8 +197,11 @@ def _build_coefficient_replace_map(coefficients, element_mapping=None): def attach_estimated_degrees(form): """Attach estimated polynomial degree to a form's integrals. - :arg form: The :class:`~.Form` to inspect. - :returns: A new Form with estimate degrees attached. + Args: + form: The Form` to inspect. + + Returns: + A new Form with estimate degrees attached. """ integrals = form.integrals() @@ -212,32 +215,8 @@ def attach_estimated_degrees(form): return Form(new_integrals) -def compute_form_data(form, - # Default arguments configured to behave the way old FFC expects it: - 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, - ): - - # 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 - +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 @@ -264,12 +243,49 @@ def compute_form_data(form, # 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) + 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 @@ -345,17 +361,18 @@ def compute_form_data(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.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] + 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] + itg_data.enabled_coefficients = [ + bool(coeff in itg_data.integral_coefficients) for coeff in self.reduced_coefficients + ] # --- Collect some trivial data @@ -379,17 +396,19 @@ def compute_form_data(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) + 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()) + _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 @@ -404,7 +423,8 @@ def compute_form_data(form, # faster! preprocessed_form = reconstruct_form_from_integral_data(self.integral_data) - check_form_arity(preprocessed_form, self.original_form.arguments(), complex_mode) # Currently testing how fast this is + # 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! diff --git a/ufl/algorithms/coordinate_derivative_helpers.py b/ufl/algorithms/coordinate_derivative_helpers.py index e1491a439..852b655c2 100644 --- a/ufl/algorithms/coordinate_derivative_helpers.py +++ b/ufl/algorithms/coordinate_derivative_helpers.py @@ -1,6 +1,7 @@ -# -*- coding: utf-8 -*- -"""This module provides the necessary tools to strip away and then reattach the -coordinate derivatives at the right time point in compute_form_data.""" +"""Tools to strip away and reattach coordinate derivatives. + +This is used in compute_form_data. +""" # Copyright (C) 2018 Florian Wechsung # @@ -8,38 +9,44 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import error -from ufl.differentiation import CoordinateDerivative -from ufl.algorithms.multifunction import MultiFunction -from ufl.corealg.map_dag import map_expr_dags 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. """ + """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): - """ If we have already seen a CoordinateDerivative, then no other + """Apply to expr. + + If we have already seen a CoordinateDerivative, then no other expressions apart from more CoordinateDerivatives are allowed to wrap - around it. """ + around it. + """ if any(operands): - error("CoordinateDerivative(s) must be outermost") + 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 @@ -56,9 +63,11 @@ def strip_coordinate_derivatives(integrals): map_expr_dags(checker, [integrand]) coordinate_derivatives = [] - # grab all coordinate derivatives and store them, so that we can apply - # them later again 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])) @@ -70,10 +79,11 @@ def take_top_coordinate_derivatives(o): return (integral.reconstruct(integrand=newintegrand), coordinate_derivatives) else: - error("Invalid type %s" % (integrals.__class__.__name__,)) + 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 @@ -84,4 +94,4 @@ def attach_coordinate_derivatives(integral, coordinate_derivatives): integrand = CoordinateDerivative(integrand, tup[0], tup[1], tup[2]) return integral.reconstruct(integrand=integrand) else: - error("Invalid type %s" % (integral.__class__.__name__,)) + raise ValueError(f"Invalid type {integral.__class__.__name__}") diff --git a/ufl/algorithms/domain_analysis.py b/ufl/algorithms/domain_analysis.py index 0a1cf8f9e..19e0f4e3c 100644 --- a/ufl/algorithms/domain_analysis.py +++ b/ufl/algorithms/domain_analysis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Algorithms for building canonical data structure for integrals over subdomains.""" # Copyright (C) 2009-2016 Anders Logg and Martin Sandve Alnæs @@ -7,39 +6,49 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import numbers +import typing from collections import defaultdict import ufl -from ufl.log import error -from ufl.integral import Integral +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 -from ufl.algorithms.coordinate_derivative_helpers import attach_coordinate_derivatives, strip_coordinate_derivatives -import numbers class IntegralData(object): - """Utility class with the members (domain, integral_type, - subdomain_id, integrals, metadata) - - where metadata is an empty dictionary that may be used for - associating metadata with each 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): + __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)): - error("Multiple domains mismatch in integral data.") + raise ValueError("Multiple domains mismatch in integral data.") if not all(integral_type == itg.integral_type() for itg in integrals): - error("Integral type mismatch in integral data.") + raise ValueError("Integral type mismatch in integral data.") if not all(subdomain_id == itg.subdomain_id() for itg in integrals): - error("Subdomain id mismatch in integral data.") + raise ValueError("Subdomain id mismatch in integral data.") self.domain = domain self.integral_type = integral_type @@ -57,48 +66,45 @@ def __init__(self, domain, integral_type, subdomain_id, integrals, 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)) + 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) + 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): - s = "IntegralData over domain(%s, %s), with integrals:\n%s\nand metadata:\n%s" % ( - self.integral_type, self.subdomain_id, - '\n\n'.join(map(str, self.integrals)), self.metadata) + """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 -def dicts_lt(a, b): - na = 0 if a is None else len(a) - nb = 0 if b is None else len(b) - if na != nb: - return len(a) < len(b) - for ia, ib in zip(sorted_by_key(a), sorted_by_key(b)): - # Assuming keys are sortable (usually str) - if ia[0] != ib[0]: - return (ia[0].__class__.__name__, ia[0]) < (ib[0].__class__.__name__, ib[0]) # Hack to preserve type sorting in py3 - # Assuming values are sortable - if ia[1] != ib[1]: - return (ia[1].__class__.__name__, ia[1]) < (ib[1].__class__.__name__, ib[1]) # Hack to preserve type sorting in py3 - - -# Tuple comparison helper class ExprTupleKey(object): - __slots__ = ('x',) + """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: @@ -113,18 +119,19 @@ def __lt__(self, other): def group_integrals_by_domain_and_type(integrals, domains): - """ - Input: + """Group integrals by domain and type. + + Args: integrals: list of Integral objects domains: list of AbstractDomain objects from the parent Form - Output: - integrals_by_domain_and_type: dict: (domain, integral_type) -> list(Integral) + 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: - error("Integral has no domain.") + raise ValueError("Integral has no domain.") key = (itg.ufl_domain(), itg.integral_type()) # Append integral to list of integrals with shared key @@ -134,29 +141,34 @@ def group_integrals_by_domain_and_type(integrals, domains): def integral_subdomain_ids(integral): - "Get a tuple of integer subdomains or a valid string subdomain from 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): - error("Expecting only integer subdomains in tuple.") + 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: - error("Invalid domain id %s." % did) + raise ValueError(f"Invalid domain id {did}.") -def rearrange_integrals_by_single_subdomains(integrals, do_append_everywhere_integrals): +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. - Input: - integrals: list(Integral) + 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. - Output: - integrals: dict: subdomain_id -> list(Integral) (reconstructed with single subdomain_id) + Returns: + The integrals reconstructed with single subdomain_id """ # Split integrals into lists of everywhere and subdomain integrals everywhere_integrals = [] @@ -164,7 +176,7 @@ def rearrange_integrals_by_single_subdomains(integrals, do_append_everywhere_int for itg in integrals: dids = integral_subdomain_ids(itg) if dids == "otherwise": - error("'otherwise' integrals should never occur before preprocessing.") + raise ValueError("'otherwise' integrals should never occur before preprocessing.") elif dids == "everywhere": everywhere_integrals.append(itg) else: @@ -192,7 +204,8 @@ def rearrange_integrals_by_single_subdomains(integrals, do_append_everywhere_int 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)) + ev_itg.reconstruct(subdomain_id=subdomain_id) + ) if otherwise_integrals: single_subdomain_integrals["otherwise"] = otherwise_integrals @@ -201,15 +214,15 @@ def rearrange_integrals_by_single_subdomains(integrals, do_append_everywhere_int def accumulate_integrands_with_same_metadata(integrals): - """ - Taking input on the form: - integrals = [integral0, integral1, ...] + """Accumulate integrands with the same metedata. - Return result on the form: - integrands_by_id = [(integrand0, metadata0), - (integrand1, metadata1), ...] + Args: + integrals: a list of integrals - where integrand0 < integrand1 by the canonical ufl expression ordering criteria. + 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 = {} @@ -237,31 +250,39 @@ def accumulate_integrands_with_same_metadata(integrals): def build_integral_data(integrals): """Build integral data given a list of integrals. - :arg integrals: An iterable of :class:`~.Integral` objects. - :returns: A tuple of :class:`IntegralData` objects. - The integrals you pass in here must have been rearranged and - gathered (removing the "everywhere" subdomain_id. To do this, you - should call :func:`group_form_integrals`. + 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: - domain = integral.ufl_domain() integral_type = integral.integral_type() - subdomain_id = integral.subdomain_id() - if subdomain_id == "everywhere": - raise ValueError("'everywhere' not a valid subdomain id. Did you forget to call group_form_integrals?") + 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, subdomain_id (but - # possibly different metadata). - itgs[(domain, integral_type, subdomain_id)].append(integral) + # 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 - return (d._ufl_sort_key_(), itype, (type(sid).__name__, sid)) + 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): @@ -272,13 +293,18 @@ def keyfunc(item): def group_form_integrals(form, domains, do_append_everywhere_integrals=True): """Group integrals by domain and type, performing canonical simplification. - :arg form: the :class:`~.Form` to group the integrals of. - :arg domains: an iterable of :class:`~.Domain`s. - :returns: A new :class:`~.Form` with gathered integrands. + 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_by_domain_and_type = group_integrals_by_domain_and_type(form.integrals(), domains) integrals = [] for domain in domains: @@ -292,19 +318,22 @@ def group_form_integrals(form, domains, do_append_everywhere_integrals=True): # 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) + 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) + 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) @@ -319,21 +348,55 @@ def calc_hash(cd): # 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]) + 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 = Integral( + integrand, integral_type, domain, subdomain_id, metadata, None + ) integral = attach_coordinate_derivatives(integral, samecd_integrals[0]) integrals.append(integral) - return Form(integrals) + + # 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) diff --git a/ufl/algorithms/elementtransformations.py b/ufl/algorithms/elementtransformations.py deleted file mode 100644 index 30d95ea6e..000000000 --- a/ufl/algorithms/elementtransformations.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module provides helper functions to - - FFC/DOLFIN adaptive chain, - - UFL algorithms taking care of underspecified DOLFIN expressions.""" - -# Copyright (C) 2012 Marie E. Rognes, 2015 Jan Blechta -# -# This file is part of UFL (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -from ufl.log import error -from ufl.finiteelement import FiniteElement, VectorElement, TensorElement, \ - MixedElement, EnrichedElement, NodalEnrichedElement - -__all__ = ['increase_order', 'tear'] - - -def increase_order(element): - "Return element of same family, but a polynomial degree higher." - return _increase_degree(element, +1) - - -def change_regularity(element, family): - """ - For a given finite element, return the corresponding space - specified by 'family'. - """ - return element.reconstruct(family=family) - - -def tear(element): - "For a finite element, return the corresponding discontinuous element." - return change_regularity(element, "DG") - - -def _increase_degree(element, degree_rise): - if isinstance(element, (FiniteElement, VectorElement, TensorElement)): - # Can't increase degree for reals - if element.family() == "Real": - return element - return element.reconstruct(degree=(element.degree() + degree_rise)) - elif isinstance(element, MixedElement): - return MixedElement([_increase_degree(e, degree_rise) - for e in element.sub_elements()]) - elif isinstance(element, EnrichedElement): - return EnrichedElement([_increase_degree(e, degree_rise) - for e in element.sub_elements()]) - elif isinstance(element, NodalEnrichedElement): - return NodalEnrichedElement([_increase_degree(e, degree_rise) - for e in element.sub_elements()]) - else: - error("Element reconstruction is only done to stay compatible" - " with hacks in DOLFIN. Not expecting a %s" % repr(element)) diff --git a/ufl/algorithms/estimate_degrees.py b/ufl/algorithms/estimate_degrees.py index 115af6b48..da550116c 100644 --- a/ufl/algorithms/estimate_degrees.py +++ b/ufl/algorithms/estimate_degrees.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Algorithms for estimating polynomial degrees of expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg @@ -10,157 +9,188 @@ # Modified by Anders Logg, 2009-2010 # Modified by Jan Blechta, 2012 -from ufl.log import warning, error -from ufl.form import Form -from ufl.integral import Integral -from ufl.algorithms.multifunction import MultiFunction -from ufl.corealg.map_dag import map_expr_dags +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 IrreducibleInt(int): - """Degree type used by quadrilaterals. +class SumDegreeEstimator(MultiFunction): + """Sum degree estimator. - Unlike int, values of this type are not decremeneted by _reduce_degree. + This algorithm is exact for a few operators and heuristic for many. """ - pass - - -class SumDegreeEstimator(MultiFunction): - "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): - "Constant values are constant." + """Apply to constant_value. + + Constant values are constant. + """ return 0 def constant(self, v): + """Apply to constant.""" return 0 def geometric_quantity(self, v): - "Some geometric quantities are cellwise constant. Others are nonpolynomial and thus hard to estimate." + """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 v.ufl_domain().ufl_coordinate_element().degree() + return extract_unique_domain(v).ufl_coordinate_element().embedded_superdegree def spatial_coordinate(self, v): - "A coordinate provides additional degrees depending on coordinate field of domain." - return v.ufl_domain().ufl_coordinate_element().degree() + """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): - "A coordinate provides one additional degree." + """Apply to cell_coordinate. + + A coordinate provides one additional degree. + """ return 1 def argument(self, v): - """A form argument provides a degree depending on the element, - or the default degree if the element has no degree.""" - return v.ufl_element().degree() # FIXME: Use component to improve accuracy for mixed elements + """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): - """A form argument provides a degree depending on the element, - or the default degree if the element has no degree.""" + """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.degree() # FIXME: Use component to improve accuracy for mixed elements + 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): - """Reduces the estimated degree by one; used when derivatives - are taken. Does not reduce the degree when TensorProduct elements - or quadrilateral elements are involved.""" - if isinstance(f, int) and not isinstance(f, IrreducibleInt): + """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: - # if tuple, do not reduce return f def _add_degrees(self, v, *ops): - def add_single(ops): - if any(isinstance(o, IrreducibleInt) for o in ops): - return IrreducibleInt(sum(ops)) - else: - return sum(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(add_single, zip(*tempops))) + return tuple(map(sum, zip(*tempops))) else: - return add_single(ops) + return sum(ops) def _max_degrees(self, v, *ops): - def max_single(ops): - if any(isinstance(o, IrreducibleInt) for o in ops): - return IrreducibleInt(max(ops)) - else: - return max(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_single, zip(*tempops))) + return tuple(map(max, zip(*tempops))) else: - return max_single(ops + (0,)) + return max(ops + (0,)) def _not_handled(self, v, *args): - error("Missing degree handler for type %s" % v._ufl_class_.__name__) + """Apply to _not_handled.""" + raise ValueError(f"Missing degree handler for type {v._ufl_class_.__name__}") def expr(self, v, *ops): - "For most operators we take the max degree of its operands." - warning("Missing degree estimation handler for type %s" % v._ufl_class_.__name__) + """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, l): + 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: @@ -181,11 +211,17 @@ def imag(self, v, a): reference_curl = _reduce_degree def cell_avg(self, v, a): - "Cell average of a function is always cellwise constant." + """Apply to cell_avg. + + Cell average of a function is always cellwise constant. + """ return 0 def facet_avg(self, v, a): - "Facet average of a function is always cellwise constant." + """Apply to facet_avg. + + Facet average of a function is always cellwise constant. + """ return 0 # A product accumulates the degrees of its operands: @@ -212,21 +248,28 @@ def facet_avg(self, v, a): sym = _not_handled def abs(self, v, a): - "This is a heuristic, correct if there is no " + """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): - "Using the sum here is a heuristic. Consider e.g. (x+1)/(x-1)." + """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): - """If b is a positive integer: - degree(a**b) == degree(a)*b - otherwise use the heuristic - degree(a**b) == degree(a) + 2""" + """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): @@ -241,12 +284,14 @@ def power(self, v, a, b): # negative integer, Coefficient, etc. return self._add_degrees(v, a, 2) - def atan_2(self, v, a, b): - """Using the heuristic + 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. + 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) @@ -254,11 +299,13 @@ def atan_2(self, v, a, b): return self._max_degrees(v, a, b) def math_function(self, v, a): - """Using the heuristic + """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. + which can be wildly inaccurate but at least gives a somewhat + high integration degree. """ if a: return self._add_degrees(v, a, 2) @@ -266,11 +313,13 @@ def math_function(self, v, a): return a def bessel_function(self, v, nu, x): - """Using the heuristic + """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. + which can be wildly inaccurate but at least gives a somewhat + high integration degree. """ if x: return self._add_degrees(v, x, 2) @@ -278,54 +327,61 @@ def bessel_function(self, v, nu, x): return x def condition(self, v, *args): + """Apply to condition.""" return None def conditional(self, v, c, t, f): - """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.""" + """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, l, r): - """Same as conditional.""" - return self._max_degrees(v, l, r) + 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): - """ We use the heuristic that a shape derivative in direction V + """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. """ + 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={}): +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, + 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. + 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(): - error("Got form with no 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()]) diff --git a/ufl/algorithms/expand_compounds.py b/ufl/algorithms/expand_compounds.py deleted file mode 100644 index 59e08cf5a..000000000 --- a/ufl/algorithms/expand_compounds.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -"""Algorithm for expanding compound expressions into -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.algorithms.apply_algebra_lowering import apply_algebra_lowering - - -def expand_compounds(e): - return apply_algebra_lowering(e) diff --git a/ufl/algorithms/expand_indices.py b/ufl/algorithms/expand_indices.py index 118a8be23..316998341 100644 --- a/ufl/algorithms/expand_indices.py +++ b/ufl/algorithms/expand_indices.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- -"""This module defines expression transformation utilities, -for expanding free indices in expressions to explicit fixed -indices only.""" +"""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 # @@ -11,88 +11,92 @@ # # Modified by Anders Logg, 2009. -from ufl.log import error -from ufl.utils.stacks import Stack, StackDict -from ufl.classes import Terminal, ListTensor +from ufl.algorithms.transformer import ReuseTransformer, apply_transformer +from ufl.classes import Terminal from ufl.constantvalue import Zero -from ufl.core.multiindex import Index, FixedIndex, MultiIndex +from ufl.core.multiindex import FixedIndex, Index, MultiIndex from ufl.differentiation import Grad -from ufl.algorithms.transformer import ReuseTransformer, apply_transformer -from ufl.corealg.traversal import unique_pre_traversal +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." + """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): - error("Component size mismatch.") + 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: - e = x.ufl_element() + space = x.ufl_function_space() r = len(sh) # Get component c = self.component() if r != len(c): - error("Component size mismatch.") + raise ValueError("Component size mismatch.") # Map it through an eventual symmetry mapping - s = e.symmetry() - c = s.get(c, c) - if r != len(c): - error("Component size mismatch after 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()): - error("Component size mismatch.") + raise ValueError("Component size mismatch.") s = set(x.ufl_free_indices) - set(i.count() for i in self._index2value.keys()) if s: - error("Free index set mismatch, these indices have no value assigned: %s." % str(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()): - error("Component size mismatch.") + raise ValueError("Component size mismatch.") s = set(x.ufl_free_indices) - set(i.count() for i in self._index2value.keys()) if s: - error("Free index set mismatch, these indices have no value assigned: %s." % str(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 != (): - error("Not expecting tensor in condition.") + raise ValueError("Not expecting tensor in condition.") # Conditional may be indexed, push empty component self._components.push(()) @@ -106,16 +110,17 @@ def conditional(self, x): 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 != (): - error("Not expecting tensor in division.") + raise ValueError("Not expecting tensor in division.") if self.component() != (): - error("Not expecting component in division.") + raise ValueError("Not expecting component in division.") if b.ufl_shape != (): - error("Not expecting division by tensor.") + raise ValueError("Not expecting division by tensor.") a = self.visit(a) # self._components.push(()) @@ -125,9 +130,10 @@ def division(self, x): 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 + (index,) = multiindex # TODO: For the list tensor purging algorithm, do something like: # if index not in self._to_expand: @@ -140,6 +146,7 @@ def index_sum(self, x): return sum(ops) def _multi_index_values(self, x): + """Apply to _multi_index_values.""" comp = [] for i in x._indices: if isinstance(i, FixedIndex): @@ -149,10 +156,12 @@ def _multi_index_values(self, x): 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 @@ -175,16 +184,17 @@ def indexed(self, x): 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 != (): - error("Expecting scalar base expression.") + raise ValueError("Expecting scalar base expression.") # Update index map with component tuple values comp = self.component() if len(indices) != len(comp): - error("Index/component mismatch.") + raise ValueError("Index/component mismatch.") for i, v in zip(indices.indices(), comp): self._index2value.push(i, v) self._components.push(()) @@ -199,6 +209,7 @@ def component_tensor(self, x): 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:] @@ -210,21 +221,14 @@ def list_tensor(self, x): return r def grad(self, x): - f, = x.ufl_operands + """Apply to grad.""" + (f,) = x.ufl_operands if not isinstance(f, (Terminal, Grad)): - error("Expecting expand_derivatives to have been applied.") + 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()) - - -def purge_list_tensors(expr): - """Get rid of all ListTensor instances by expanding - expressions to use their components directly. - Will usually increase the size of the expression.""" - if any(isinstance(subexpr, ListTensor) for subexpr in unique_pre_traversal(expr)): - return expand_indices(expr) # TODO: Only expand what's necessary to get rid of list tensors - return expr diff --git a/ufl/algorithms/formdata.py b/ufl/algorithms/formdata.py index 850bc7809..f6bdf4c14 100644 --- a/ufl/algorithms/formdata.py +++ b/ufl/algorithms/formdata.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """FormData class easy for collecting of various data about a form.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -9,26 +8,23 @@ # # Modified by Anders Logg, 2008. -from ufl.utils.formatting import lstr, tstr, estr +from ufl.utils.formatting import estr, lstr, tstr class FormData(object): - """Class collecting various information extracted from a Form by - calling preprocess. - - """ + """Class collecting various information extracted from a Form by calling preprocess.""" def __init__(self): - "Create empty form data for given form." + """Create empty form data for given form.""" def __str__(self): - "Return formatted summary of form data" + """Return formatted summary of form data.""" types = sorted(self.max_subdomain_ids.keys()) - geometry = ( - ("Geometric dimension", self.geometric_dimension), + 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 ) - subdomains = tuple(("Number of %s subdomains" % integral_type, - self.max_subdomain_ids[integral_type]) for integral_type in types) functions = ( # Arguments ("Rank", self.rank), @@ -41,30 +37,3 @@ def __str__(self): ("Unique sub elements", estr(self.unique_sub_elements)), ) return tstr(geometry + subdomains + functions) - - -class ExprData(object): - """ - Class collecting various information extracted from a Expr by - calling preprocess. - """ - - def __init__(self): - "Create empty expr data for given expr." - - def __str__(self): - "Return formatted summary of expr data" - return tstr((("Name", self.name), - ("Cell", self.cell), - ("Topological dimension", self.topological_dimension), - ("Geometric dimension", self.geometric_dimension), - ("Rank", self.rank), - ("Number of coefficients", self.num_coefficients), - ("Arguments", lstr(self.arguments)), - ("Coefficients", lstr(self.coefficients)), - ("Argument names", lstr(self.argument_names)), - ("Coefficient names", lstr(self.coefficient_names)), - ("Unique elements", estr(self.unique_elements)), - ("Unique sub elements", estr(self.unique_sub_elements)), - # FIXME DOMAINS what is "the domain(s)" for an expression? - ("Domains", self.domains), )) diff --git a/ufl/algorithms/formfiles.py b/ufl/algorithms/formfiles.py index e091b1cee..a93383275 100644 --- a/ufl/algorithms/formfiles.py +++ b/ufl/algorithms/formfiles.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """A collection of utility algorithms for handling UFL files.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -13,18 +12,21 @@ import io import os import re -from ufl.log import error, warning -from ufl.utils.sorting import sorted_by_key -from ufl.form import Form -from ufl.finiteelement import FiniteElementBase -from ufl.core.expr import Expr -from ufl.constant import Constant + 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 = [] @@ -34,15 +36,26 @@ def __init__(self): self.reserved_objects = {} def __bool__(self): - 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) + """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 @@ -53,9 +66,9 @@ def match(line): for i in range(min(2, len(lines))): m = match(lines[i]) if m: - encoding, = m.groups() + (encoding,) = m.groups() # Drop encoding line - lines = lines[:i] + lines[i + 1:] + lines = lines[:i] + lines[i + 1 :] break else: # Default to utf-8 (works for ascii files @@ -68,27 +81,27 @@ def match(line): def read_ufl_file(filename): - "Read a UFL file." + """Read a UFL file.""" if not os.path.exists(filename): - error("File '%s' doesn't exist." % 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 + """Execute code.""" namespace = {} exec(uflcode, namespace) return namespace def interpret_ufl_namespace(namespace): - "Takes a namespace dict from an executed ufl file and converts it to a FileData object." + """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 FiniteElementBase objects + # 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. @@ -101,7 +114,9 @@ def interpret_ufl_namespace(namespace): # FIXME: Remove after FFC is updated to use reserved_objects: ufd.object_names[name] = value ufd.object_by_name[name] = value - elif isinstance(value, (FiniteElementBase, Coefficient, Constant, Argument, Form, Expr)): + 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 @@ -115,6 +130,7 @@ def interpret_ufl_namespace(namespace): 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") @@ -130,9 +146,9 @@ def get_form(name): # Validate types if not isinstance(ufd.forms, (list, tuple)): - error("Expecting 'forms' to be a list or tuple, not '%s'." % type(ufd.forms)) + 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): - error("Expecting 'forms' to be a list of Form instances.") + raise ValueError("Expecting 'forms' to be a list of Form instances.") # Get list of exported elements elements = namespace.get("elements") @@ -143,39 +159,41 @@ def get_form(name): # Validate types if not isinstance(ufd.elements, (list, tuple)): - error("Expecting 'elements' to be a list or tuple, not '%s'." % type(ufd.elements)) - if not all(isinstance(e, FiniteElementBase) for e in ufd.elements): - error("Expecting 'elements' to be a list of FiniteElementBase instances.") + 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 - # TODO: Temporarily letting 'coefficients' override 'functions', - # but allow 'functions' for compatibility - functions = namespace.get("functions", []) - if functions: - warning("Deprecation warning: Rename 'functions' to 'coefficients' to export coefficients.") + functions = [] ufd.coefficients = namespace.get("coefficients", functions) # Validate types if not isinstance(ufd.coefficients, (list, tuple)): - error("Expecting 'coefficients' to be a list or tuple, not '%s'." % type(ufd.coefficients)) + 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): - error("Expecting 'coefficients' to be a list of Coefficient instances.") + 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)): - error("Expecting 'expressions' to be a list or tuple, not '%s'." % type(ufd.expressions)) + 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): - error("Expecting 'expressions' to be a list of Expr instances.") + 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." + """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) @@ -183,6 +201,6 @@ def load_ufl_file(filename): def load_forms(filename): - "Return a list of all forms in a file." + """Return a list of all forms in a file.""" ufd = load_ufl_file(filename) return ufd.forms diff --git a/ufl/algorithms/formsplitter.py b/ufl/algorithms/formsplitter.py index 032352b92..e9fefaf68 100644 --- a/ufl/algorithms/formsplitter.py +++ b/ufl/algorithms/formsplitter.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -"Extract part of a form in a mixed FunctionSpace." +"""Extract part of a form in a mixed FunctionSpace.""" -# Copyright (C) 2016 Chris Richardson and Lawrence Mitchell +# Copyright (C) 2016-2024 Chris Richardson and Lawrence Mitchell # # This file is part of UFL (https://www.fenicsproject.org) # @@ -9,48 +8,44 @@ # # Modified by Cecile Daversin-Catty, 2018 -from ufl.corealg.multifunction import MultiFunction -from ufl.algorithms.map_integrands import map_integrand_dags -from ufl.constantvalue import Zero -from ufl.tensors import as_vector +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): - if (obj.part() is not None): + """Apply to argument.""" + if obj.part() is not None: # Mixed element built from MixedFunctionSpace, # whose sub-function spaces are indexed by obj.part() - if len(obj.ufl_shape) == 0: - if (obj.part() == self.idx[obj.number()]): - return obj - else: - return Zero() + if obj.part() == self.idx[obj.number()]: + return obj else: - indices = [()] - for m in obj.ufl_shape: - indices = [(k + (j,)) for k in indices for j in range(m)] - - if (obj.part() == self.idx[obj.number()]): - return as_vector([obj[j] for j in indices]) - else: - return as_vector([Zero() for j in indices]) + 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() + sub_elements = obj.ufl_element().sub_elements # If not a mixed element, do nothing - if (len(sub_elements) == 0): + if len(sub_elements) == 0: return obj args = [] @@ -62,40 +57,101 @@ def argument(self, obj): for m in a.ufl_shape: indices = [(k + (j,)) for k in indices for j in range(m)] - if (i == self.idx[obj.number()]): + 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=None, j=None): +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() - forms = [] - numbers = tuple(sorted(set(a.number() for a in arguments))) arity = len(numbers) - parts = tuple(sorted(set(a.part() for a in arguments))) assert arity <= 2 - if arity == 0: - return (form, ) + 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) - for pi in parts: + # 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 parts: + for pj in range(num_parts): f = fs.split(form, pi, pj) if f.empty(): - forms.append(None) + form_i.append(None) else: - forms.append(f) + 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(): @@ -107,11 +163,16 @@ def extract_blocks(form, i=None, j=None): forms_tuple = tuple(forms) except TypeError: # Only one form returned - forms_tuple = (forms, ) - + 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: - return forms_tuple[i * len(parts) + j] + 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: diff --git a/ufl/algorithms/formtransformations.py b/ufl/algorithms/formtransformations.py index 984986de1..20a6a1743 100644 --- a/ufl/algorithms/formtransformations.py +++ b/ufl/algorithms/formtransformations.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module defines utilities for transforming -complete Forms into new related Forms.""" +"""Utilities for transforming complete Forms into new related Forms.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -12,24 +10,26 @@ # Modified by Garth N. Wells, 2010. # Modified by Marie E. Rognes, 2010. +import warnings +from logging import debug -from ufl.log import error, warning, debug - -# All classes: -from ufl.core.expr import ufl_err_str -from ufl.argument import Argument -from ufl.coefficient import Coefficient -from ufl.constantvalue import Zero from ufl.algebra import Conj # Other algorithms: from ufl.algorithms.map_integrands import map_integrands -from ufl.algorithms.transformer import Transformer 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() @@ -42,24 +42,25 @@ def _expr_has_terminal_types(expr, ufl_types): 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). - """ + """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): - """The default is a nonlinear operator not accepting any - Arguments among its children.""" + """Apply to expr. + + The default is a nonlinear operator not accepting any Arguments among its children. + """ if _expr_has_terminal_types(x, Argument): - error("Found Argument in %s, this is an invalid expression." % ufl_err_str(x)) + 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 @@ -67,8 +68,7 @@ def expr(self, x): terminal = expr def variable(self, x): - "Return relevant parts of this variable." - + """Return relevant parts of this variable.""" # Extract parts/provides from this variable's expression expression, label = x.ufl_operands part, provides = self.visit(expression) @@ -84,8 +84,7 @@ def variable(self, x): return (x, provides) def argument(self, x): - "Return itself unless itself provides too much." - + """Return itself unless itself provides too much.""" # An argument provides itself provides = {x} @@ -96,9 +95,7 @@ def argument(self, x): return (x, provides) def sum(self, x): - """ - Return the terms that might eventually yield the correct - parts(!) + """Return the terms that might eventually yield the correct parts(!). The logic required for sums is a bit elaborate: @@ -120,16 +117,14 @@ def sum(self, x): 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)) + 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) @@ -151,11 +146,10 @@ def sum(self, x): # 3. Return the terms that provide the biggest set most_provided = frozenset() - for (provideds, parts) in parts_that_provide.items(): # TODO: Just sort instead? - + 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): - error("Don't know what to do with sums with different Arguments.") + raise ValueError("Don't know what to do with sums with different Arguments.") if provideds > most_provided: most_provided = provideds @@ -164,19 +158,20 @@ def sum(self, x): if len(terms) == 2: x = self.reuse_if_possible(x, *terms) else: - x, = terms + (x,) = terms return (x, most_provided) def product(self, x, *ops): - """ Note: Product is a visit-children-first handler. ops are - the visited factors.""" + """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()) @@ -200,14 +195,15 @@ def product(self, x, *ops): dot = product def division(self, x): - "Return parts_of_numerator/denominator." - + """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): - error("Found Argument in denominator of %s , this is an invalid expression." % ufl_err_str(x)) + 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) @@ -224,9 +220,11 @@ def division(self, x): return (x, provides) def linear_operator(self, x, arg): - """A linear operator with a single operand accepting arity > 0, - providing whatever Argument its operand does.""" + """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 @@ -257,9 +255,7 @@ def linear_operator(self, x, arg): imag = linear_operator def linear_indexed_type(self, x): - """Return parts of expression belonging to this indexed - expression.""" - + """Return parts of expression belonging to this indexed expression.""" expression, index = x.ufl_operands part, provides = self.visit(expression) @@ -279,23 +275,27 @@ def linear_indexed_type(self, x): 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): + 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)): - error("PartExtracter does not know how to handle list_tensors with non-zero components providing fewer arguments") + 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] @@ -306,17 +306,16 @@ def list_tensor(self, x, *ops): 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}: - error("compute_form_with_arity cannot handle parts.") + raise ValueError("compute_form_with_arity cannot handle parts.") if len(arguments) < arity: - warning("Form has no parts with arity %d." % arity) + warnings.warn(f"Form has no parts with arity {arity}.") return 0 * form # Assuming that the form is not a sum of terms @@ -331,22 +330,21 @@ def _transform(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}: - error("compute_form_arities cannot handle parts.") + 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) @@ -361,10 +359,8 @@ 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) @@ -373,19 +369,17 @@ 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. + """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?)""" + (Used for testing, not sure if it's useful for anything?) + """ return compute_form_with_arity(form, 0) @@ -405,7 +399,7 @@ def compute_form_action(form, coefficient): parts = [arg.part() for arg in arguments] if set(parts) - {None}: - error("compute_form_action cannot handle parts.") + raise ValueError("compute_form_action cannot handle parts.") # Pick last argument (will be replaced) u = arguments[-1] @@ -427,26 +421,32 @@ def compute_energy_norm(form, coefficient): 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}: - error("compute_energy_norm cannot handle parts.") + raise ValueError("compute_energy_norm cannot handle parts.") if len(arguments) != 2: - error("Expecting bilinear form.") + raise ValueError("Expecting bilinear form.") v, u = arguments U = u.ufl_function_space() V = v.ufl_function_space() if U != V: - error("Expecting equal finite elements for test and trial functions, got '%s' and '%s'." % (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: - error("Trying to compute action of form on a " - "coefficient in an incompatible element space.") - return replace(form, {u: coefficient, v: coefficient}) + 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): @@ -459,34 +459,32 @@ def compute_form_adjoint(form, reordered_arguments=None): parts = [arg.part() for arg in arguments] if set(parts) - {None}: - error("compute_form_adjoint cannot handle parts.") + raise ValueError("compute_form_adjoint cannot handle parts.") if len(arguments) != 2: - error("Expecting bilinear form.") + raise ValueError("Expecting bilinear form.") v, u = arguments if v.number() >= u.number(): - error("Mistaken assumption in code!") + 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()) + 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(): - error("Ordering of new arguments is the same as the old arguments!") + raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_u.part() != v.part(): - error("Ordering of new arguments is the same as the old arguments!") + raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_v.part() != u.part(): - error("Ordering of new arguments is the same as the old arguments!") + raise ValueError("Ordering of new arguments is the same as the old arguments!") if reordered_u.ufl_function_space() != u.ufl_function_space(): - error("Element mismatch between new and old arguments (trial functions).") + raise ValueError("Element mismatch between new and old arguments (trial functions).") if reordered_v.ufl_function_space() != v.ufl_function_space(): - error("Element mismatch between new and old arguments (test functions).") + raise ValueError("Element mismatch between new and old arguments (test functions).") return map_integrands(Conj, replace(form, {v: reordered_v, u: reordered_u})) diff --git a/ufl/algorithms/map_integrands.py b/ufl/algorithms/map_integrands.py index 0846218f4..0a6da1817 100644 --- a/ufl/algorithms/map_integrands.py +++ b/ufl/algorithms/map_integrands.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""Basic algorithms for applying functions to subexpressions.""" +"""Basic algorithms for applying functions to sub-expressions.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -11,23 +10,28 @@ # as part of a careful refactoring process, and this file depends on ufl.form # which drags in a lot of stuff. -from ufl.log import error +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 -from ufl.form import Form -from ufl.constantvalue import Zero def map_integrands(function, form, only_integral_type=None): - """Apply transform(expression) to each integrand - expression in form, or to form if it is an Expr. + """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)] + 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 @@ -35,13 +39,50 @@ def map_integrands(function, form, only_integral_type=None): return itg.reconstruct(function(itg.integrand())) else: return itg - elif isinstance(form, Expr): + 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: - error("Expecting Form, Integral or Expr.") + raise ValueError("Expecting Form, Integral or Expr.") def map_integrand_dags(function, form, only_integral_type=None, compress=True): - return map_integrands(lambda expr: map_expr_dag(function, expr, compress), - form, only_integral_type) + """Map integrand dags.""" + return map_integrands( + lambda expr: map_expr_dag(function, expr, compress), form, only_integral_type + ) diff --git a/ufl/algorithms/multifunction.py b/ufl/algorithms/multifunction.py deleted file mode 100644 index b903f4434..000000000 --- a/ufl/algorithms/multifunction.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Moved here to be usable in ufl.* files without depending on -# ufl.algorithms.*... - -from ufl.corealg.multifunction import MultiFunction # noqa: F401 diff --git a/ufl/algorithms/preprocess_expression.py b/ufl/algorithms/preprocess_expression.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/ufl/algorithms/remove_complex_nodes.py b/ufl/algorithms/remove_complex_nodes.py index ed6bc92d6..99b646205 100644 --- a/ufl/algorithms/remove_complex_nodes.py +++ b/ufl/algorithms/remove_complex_nodes.py @@ -1,37 +1,40 @@ -# -*- coding: utf-8 -*- -"""Algorithm for removing conj, real, and imag nodes -from a form for when the user is in 'real mode'""" +"""Remove conj, real, and imag nodes from a form.""" -from ufl.corealg.multifunction import MultiFunction -from ufl.constantvalue import ComplexValue from ufl.algorithms.map_integrands import map_integrand_dags -from ufl.log import error +from ufl.constantvalue import ComplexValue +from ufl.corealg.multifunction import MultiFunction class ComplexNodeRemoval(MultiFunction): - """Replaces complex operator nodes with their children""" + """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): - error("Unexpected imag in real expression.") + """Apply to imag.""" + raise ValueError("Unexpected imag in real expression.") def terminal(self, t, *ops): + """Apply to terminal.""" if isinstance(t, ComplexValue): - error('Unexpected complex value in real expression.') + 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. + """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) diff --git a/ufl/algorithms/renumbering.py b/ufl/algorithms/renumbering.py index 1708a10d7..0e0408c8d 100644 --- a/ufl/algorithms/renumbering.py +++ b/ufl/algorithms/renumbering.py @@ -1,74 +1,57 @@ -# -*- coding: utf-8 -*- -"Algorithms for renumbering of counted objects, currently variables and indices." - -# Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg +"""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 ufl.log import error -from ufl.core.expr import Expr -from ufl.core.multiindex import Index, FixedIndex, MultiIndex -from ufl.variable import Label, Variable -from ufl.algorithms.transformer import ReuseTransformer, apply_transformer -from ufl.classes import Zero +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 VariableRenumberingTransformer(ReuseTransformer): - def __init__(self): - ReuseTransformer.__init__(self) - self.variable_map = {} - def variable(self, o): - e, l = o.ufl_operands # noqa: E741 - v = self.variable_map.get(l) - if v is None: - e = self.visit(e) - l2 = Label(len(self.variable_map)) - v = Variable(e, l2) - self.variable_map[l] = v - return v +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))) -class IndexRenumberingTransformer(VariableRenumberingTransformer): - "This is a poorly designed algorithm. It is used in some tests, please do not use for anything else." + expr = MultiFunction.reuse_if_untouched - def __init__(self): - VariableRenumberingTransformer.__init__(self) - self.index_map = {} + 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 - mapped_fi = tuple(self.index(Index(count=i)) for i in fi) - paired_fid = [(mapped_fi[pos], fid[pos]) for pos, a in enumerate(fi)] - new_fi, new_fid = zip(*tuple(sorted(paired_fid))) - return Zero(o.ufl_shape, new_fi, new_fid) - - def index(self, o): - if isinstance(o, FixedIndex): + new_indices = [self.index_cache[Index(i)].count() for i in fi] + if fi == () and fid == (): return o - else: - c = o._count - i = self.index_map.get(c) - if i is None: - i = Index(count=len(self.index_map)) - self.index_map[c] = i - return i + 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 multi_index(self, o): - new_indices = tuple(self.index(i) for i in o.indices()) - return MultiIndex(new_indices) +def renumber_indices(form): + """Renumber indices to have a consistent index numbering starting from 0. -def renumber_indices(expr): - if isinstance(expr, Expr): - num_free_indices = len(expr.ufl_free_indices) + This is useful to avoid multiple kernels for the same integrand, + but with different subdomain ids. - result = apply_transformer(expr, IndexRenumberingTransformer()) + Args: + form: A UFL form, integral or expression. - if isinstance(expr, Expr): - if num_free_indices != len(result.ufl_free_indices): - error("The number of free indices left in expression should be invariant w.r.t. renumbering.") - return result + Returns: + A new form, integral or expression with renumbered indices. + """ + reindexer = IndexRelabeller() + return map_integrand_dags(reindexer, form) diff --git a/ufl/algorithms/replace.py b/ufl/algorithms/replace.py index 39c4bd9e3..fbe9cecd0 100644 --- a/ufl/algorithms/replace.py +++ b/ufl/algorithms/replace.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Algorithm for replacing terminals in an expression.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg @@ -9,38 +8,73 @@ # # Modified by Anders Logg, 2009-2010 -from ufl.log import error -from ufl.classes import CoefficientDerivative +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 -from ufl.algorithms.map_integrands import map_integrand_dags -from ufl.algorithms.analysis import has_exact_type class Replacer(MultiFunction): + """Replacer.""" + def __init__(self, mapping): + """Initialize.""" super().__init__() self.mapping = mapping - if not all(k.ufl_shape == v.ufl_shape for k, v in mapping.items()): - error("Replacement expressions must have the same shape as what they replace.") - def expr(self, o, *args): + # 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): - error("Derivatives should be applied before executing replace.") + """Replace a coefficient derivative.""" + raise ValueError("Derivatives should be applied before executing replace.") def replace(e, mapping): """Replace subexpressions in expression. - @param e: - An Expr or Form. - @param mapping: - A dict with from:to replacements to perform. + 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()) @@ -56,6 +90,7 @@ def replace(e, mapping): 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) diff --git a/ufl/algorithms/replace_derivative_nodes.py b/ufl/algorithms/replace_derivative_nodes.py new file mode 100644 index 000000000..9d373407e --- /dev/null +++ b/ufl/algorithms/replace_derivative_nodes.py @@ -0,0 +1,69 @@ +"""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) diff --git a/ufl/algorithms/signature.py b/ufl/algorithms/signature.py index 8a778b6c4..807fa5c00 100644 --- a/ufl/algorithms/signature.py +++ b/ufl/algorithms/signature.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """Signature computation for forms.""" - # Copyright (C) 2012-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -8,17 +6,25 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import hashlib -from ufl.classes import (Label, - Index, MultiIndex, - Coefficient, Argument, - GeometricQuantity, ConstantValue, Constant, - ExprList, ExprMapping) -from ufl.log import error -from ufl.corealg.traversal import traverse_unique_terminals, unique_post_traversal + 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): @@ -35,7 +41,7 @@ def compute_multiindex_hashdata(expr, index_numbering): def compute_terminal_hashdata(expressions, renumbering): - + """Compute terminal hashdata.""" if not isinstance(expressions, list): expressions = [expressions] assert renumbering is not None @@ -44,11 +50,9 @@ def compute_terminal_hashdata(expressions, renumbering): # arguments, and just take repr of the rest of the terminals while # we're iterating over them terminal_hashdata = {} - labels = {} 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 @@ -70,12 +74,7 @@ def compute_terminal_hashdata(expressions, renumbering): data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, Label): - # Numbering labels as we visit them # TODO: Include in - # renumbering - data = labels.get(expr) - if data is None: - data = "L%d" % len(labels) - labels[expr] = data + data = expr._ufl_signature_data_(renumbering) elif isinstance(expr, ExprList): # Not really a terminal but can have 0 operands... @@ -86,7 +85,7 @@ def compute_terminal_hashdata(expressions, renumbering): data = "{}" else: - error("Unknown terminal type %s" % type(expr)) + raise ValueError(f"Unknown terminal type {type(expr)}") terminal_hashdata[expr] = data @@ -94,6 +93,7 @@ def compute_terminal_hashdata(expressions, renumbering): def compute_expression_hashdata(expression, terminal_hashdata) -> bytes: + """Compute expression hashdata.""" cache = {} for expr in unique_post_traversal(expression): @@ -112,6 +112,7 @@ def compute_expression_hashdata(expression, terminal_hashdata) -> bytes: 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 @@ -126,6 +127,7 @@ def compute_expression_signature(expr, renumbering): # FIXME: Fix callers def compute_form_signature(form, renumbering): # FIXME: Fix callers + """Compute form signature.""" # Extract integrands integrals = form.integrals() integrands = [integral.integrand() for integral in integrals] @@ -138,8 +140,7 @@ def compute_form_signature(form, renumbering): # FIXME: Fix callers hashdata = [] for integral in integrals: # Compute hash data for expression, this is the expensive part - integrand_hashdata = compute_expression_hashdata(integral.integrand(), - terminal_hashdata) + integrand_hashdata = compute_expression_hashdata(integral.integrand(), terminal_hashdata) domain_hashdata = integral.ufl_domain()._ufl_signature_data_(renumbering) diff --git a/ufl/algorithms/strip_terminal_data.py b/ufl/algorithms/strip_terminal_data.py index 076db443c..a270d3273 100644 --- a/ufl/algorithms/strip_terminal_data.py +++ b/ufl/algorithms/strip_terminal_data.py @@ -1,52 +1,65 @@ -# -*- coding: utf-8 -*- -"""Algorithm for replacing form arguments with 'stripped' versions where any -data-carrying objects have been extracted to a mapping.""" - -from ufl.classes import Form, Integral -from ufl.classes import Argument, Coefficient, Constant -from ufl.classes import FunctionSpace, TensorProductFunctionSpace, MixedFunctionSpace -from ufl.classes import Mesh, MeshView, TensorProductMesh +"""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): - o_new = Argument(strip_function_space(o.ufl_function_space()), - o.number(), o.part()) + """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): - o_new = Coefficient(strip_function_space(o.ufl_function_space()), - o.count()) + """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): - o_new = Constant(strip_domain(o.ufl_domain()), o.ufl_shape, - o.count()) + """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. - - :arg o: The object to be stripped. This must either be a :class:`~.Form` - or :class:`~.Integral`. - :returns: A 2-tuple containing an equivalent UFL-only object and a mapping - allowing the original form to be reconstructed using - :func:`replace_terminal_data`. + """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. @@ -73,14 +86,15 @@ def strip_terminal_data(o): def replace_terminal_data(o, mapping): - """Return a new form where the terminals have been replaced using the - provided mapping. - - :arg o: The object to have its terminals replaced. This must either be a - :class:`~.Form` or :class:`~.Integral`. - :arg mapping: A mapping suitable for reconstructing the form such as the one - returned by :func:`strip_terminal_data`. - :returns: The new form. + """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()]) @@ -93,10 +107,11 @@ def replace_terminal_data(o, mapping): def strip_function_space(function_space): - "Return a new function space with all non-UFL information removed." + """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()) + 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) @@ -108,14 +123,12 @@ def strip_function_space(function_space): def strip_domain(domain): - "Return a new domain with all non-UFL information removed." + """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()) - elif isinstance(domain, TensorProductMesh): - meshes = [strip_domain(mesh) for mesh in domain.ufl_meshes()] - return TensorProductMesh(meshes, domain.ufl_id()) + return MeshView( + strip_domain(domain.ufl_mesh()), domain.topological_dimension(), domain.ufl_id() + ) else: raise NotImplementedError(f"{type(domain)} cannot be stripped") diff --git a/ufl/algorithms/transformer.py b/ufl/algorithms/transformer.py index 7d4f9a87a..6a3328c8a 100644 --- a/ufl/algorithms/transformer.py +++ b/ufl/algorithms/transformer.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- -"""This module defines the Transformer base class and some +"""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.""" - +algorithms. +""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) @@ -16,11 +17,11 @@ from ufl.algorithms.map_integrands import map_integrands from ufl.classes import Variable, all_ufl_classes -from ufl.log import error +from ufl.core.ufl_type import UFLType def is_post_handler(function): - "Is this a handler that expects transformed children as input?" + """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 @@ -28,11 +29,16 @@ def is_post_handler(function): class Transformer(object): - """Base class for a visitor-like algorithm design pattern used to - transform expression trees from one representation to another.""" + """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 @@ -50,29 +56,36 @@ def __init__(self, variable_cache=None): for c in classobject.mro(): # Register classobject with handler for the first # encountered superclass - handler_name = c._ufl_handler_name_ + 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) + 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] + 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] @@ -81,6 +94,7 @@ def sstr(s): print("\\" * 80) def visit(self, o): + """Visit.""" # Update stack self._visit_stack.append(o) @@ -88,10 +102,6 @@ def visit(self, o): # external subclass of the actual UFL class) h, visit_children_first = self._handlers[o._ufl_typecode_] - # if not h: - # # Failed to find a handler! Should never happen, but will happen if a non-Expr object is visited. - # error("Can't handle objects of type %s" % str(type(o))) - # Is this a handler that expects transformed children as # input? if visit_children_first: @@ -107,20 +117,17 @@ def visit(self, o): return r def undefined(self, o): - "Trigger error." - error("No handler defined for %s." % o._ufl_class_.__name__) + """Trigger error.""" + raise ValueError(f"No handler defined for {o._ufl_class_.__name__}.") def reuse(self, o): - "Always reuse Expr (ignore children)" + """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 - + 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)): @@ -132,16 +139,17 @@ def reuse_if_untouched(self, o, *ops): reuse_if_possible = reuse_if_untouched def always_reconstruct(self, o, *operands): - "Always reconstruct expr." + """Reconstruct expr.""" return o._ufl_expr_reconstruct_(*operands) - # Set default behaviour for any Expr - expr = undefined + # 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 @@ -164,6 +172,7 @@ def reuse_variable(self, o): 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 @@ -181,7 +190,10 @@ def reconstruct_variable(self, o): class ReuseTransformer(Transformer): + """Reuse transformer.""" + def __init__(self, variable_cache=None): + """Initialise.""" Transformer.__init__(self, variable_cache) # Set default behaviour for any Expr @@ -195,7 +207,10 @@ def __init__(self, variable_cache=None): class CopyTransformer(Transformer): + """Copy transformer.""" + def __init__(self, variable_cache=None): + """Initialise.""" Transformer.__init__(self, variable_cache) # Set default behaviour for any Expr @@ -209,34 +224,26 @@ def __init__(self, variable_cache=None): 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 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 ufl2ufl(e): - """Convert an UFL expression to a new UFL expression, with no changes. - This is used for testing that objects in the expression behave as expected.""" - return apply_transformer(e, ReuseTransformer()) - + """Apply transforms. -def ufl2uflcopy(e): - """Convert an UFL expression to a new UFL expression. - All nonterminal object instances are replaced with identical - copies, while terminal objects are kept. This is used for - testing that objects in the expression behave as expected.""" - return apply_transformer(e, CopyTransformer()) + 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." + """Replace all Variable instances with the expression they represent.""" return apply_transformer(e, VariableStripper()) diff --git a/ufl/algorithms/traversal.py b/ufl/algorithms/traversal.py index 4623257e7..5f6688086 100644 --- a/ufl/algorithms/traversal.py +++ b/ufl/algorithms/traversal.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module contains algorithms for traversing expression trees in different ways.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg @@ -9,26 +8,30 @@ # # Modified by Anders Logg, 2008 -from ufl.log import error +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 -from ufl.form import Form -# --- Traversal utilities --- - def iter_expressions(a): - """Utility function to handle Form, Integral and any Expr - the same way when inspecting expressions. + """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, Expr): + 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,) - error("Not an UFL type: %s" % str(type(a))) + raise ValueError(f"Not an UFL type: {type(a)}") diff --git a/ufl/argument.py b/ufl/argument.py index 230371a1a..a9326a237 100644 --- a/ufl/argument.py +++ b/ufl/argument.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- -"""This module defines the class Argument and a number of related -classes (functions), including TestFunction and TrialFunction.""" +"""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) @@ -11,15 +12,16 @@ # 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.log import error -from ufl.core.ufl_type import ufl_type + 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.finiteelement import FiniteElementBase -from ufl.domain import default_domain -from ufl.functionspace import AbstractFunctionSpace, FunctionSpace, MixedFunctionSpace # Export list for ufl.classes (TODO: not actually classes: drop? these are in ufl.*) __all_classes__ = ["TestFunction", "TrialFunction", "TestFunctions", "TrialFunctions"] @@ -27,72 +29,61 @@ # --- Class representing an argument (basis function) in a form --- -@ufl_type() -class Argument(FormArgument): + +class BaseArgument(object): """UFL value: Representation of an argument to a form.""" - __slots__ = ( - "_ufl_function_space", - "_ufl_shape", - "_number", - "_part", - "_repr", - ) - def __init__(self, function_space, number, part=None): - FormArgument.__init__(self) + __slots__ = () + _ufl_is_abstract_ = True + + def __getnewargs__(self): + """Get new args.""" + return (self._ufl_function_space, self._number, self._part) - if isinstance(function_space, FiniteElementBase): - # For legacy support for UFL files using cells, we map the cell to - # the default Mesh - element = function_space - domain = default_domain(element.cell()) - function_space = FunctionSpace(domain, element) - elif not isinstance(function_space, AbstractFunctionSpace): - error("Expecting a FunctionSpace or FiniteElement.") + 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.ufl_element().value_shape() + self._ufl_shape = function_space.value_shape if not isinstance(number, numbers.Integral): - error("Expecting an int for number, not %s" % (number,)) + raise ValueError(f"Expecting an int for number, not {number}") if part is not None and not isinstance(part, numbers.Integral): - error("Expecting None or an int for part, not %s" % (part,)) + raise ValueError(f"Expecting None or an int for part, not {part}") self._number = number self._part = part - self._repr = "Argument(%s, %s, %s)" % ( - repr(self._ufl_function_space), repr(self._number), repr(self._part)) + self._repr = f"BaseArgument({self._ufl_function_space}, {self._number}, {self._part})" @property def ufl_shape(self): - "Return the associated UFL shape." + """Return the associated UFL shape.""" return self._ufl_shape def ufl_function_space(self): - "Get the function space of this Argument." + """Get the function space of this Argument.""" return self._ufl_function_space def ufl_domain(self): - "Deprecated, please use .ufl_function_space().ufl_domain() instead." - # TODO: deprecate("Argument.ufl_domain() is deprecated, please - # use .ufl_function_space().ufl_domain() instead.") + """Return the UFL domain.""" return self._ufl_function_space.ufl_domain() def ufl_element(self): - "Deprecated, please use .ufl_function_space().ufl_element() instead." - # TODO: deprecate("Argument.ufl_domain() is deprecated, please - # use .ufl_function_space().ufl_element() instead.") + """Return The UFL element.""" return self._ufl_function_space.ufl_element() def number(self): - "Return the Argument number." + """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." + """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. @@ -100,17 +91,20 @@ def is_cellwise_constant(self): return False def ufl_domains(self): - "Deprecated, please use .ufl_function_space().ufl_domains() instead." - # TODO: deprecate("Argument.ufl_domains() is deprecated, - # please use .ufl_function_space().ufl_domains() instead.") + """Return UFL domains.""" return self._ufl_function_space.ufl_domains() def _ufl_signature_data_(self, renumbering): - "Signature data for form arguments depend on the global numbering of the form arguments and domains." + """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 @@ -125,10 +119,13 @@ def __str__(self): return s def __repr__(self): + """Representation.""" return self._repr def __eq__(self, other): - """Deliberately comparing exact type and not using isinstance here, + """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 @@ -139,10 +136,141 @@ def __eq__(self, other): 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) == type(other) and - self._number == other._number and - self._part == other._part and - self._ufl_function_space == other._ufl_function_space) + 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 --- @@ -159,23 +287,34 @@ def TrialFunction(function_space, part=None): # --- Helper functions for creating subfunctions on mixed elements --- + def Arguments(function_space, number): - """UFL value: Create an Argument in a mixed space, and return a - tuple with the function components corresponding to the subelements.""" + """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())] + 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): - """UFL value: Create a TestFunction in a mixed space, and return a - tuple with the function components corresponding to the subelements.""" + """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): - """UFL value: Create a TrialFunction in a mixed space, and return a - tuple with the function components corresponding to the subelements.""" + """Create a TrialFunction in a mixed space. + + Returns a tuple with the function components corresponding to the + subelements. + """ return Arguments(function_space, 1) diff --git a/ufl/assertions.py b/ufl/assertions.py deleted file mode 100644 index 5d2bc47f1..000000000 --- a/ufl/assertions.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module provides assertion functions used by the UFL implementation.""" - -# 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.log import error - - -# TODO: Move to this file and make other files import from here -from ufl.core.expr import ufl_err_str - - -# TODO: Use these and add more -# --- Standardized error messages --- - -def expecting_instance(v, c): - error("Expecting %s instance, not %s." % (c.__name__, ufl_err_str(v))) - - -def expecting_python_scalar(v): - error("Expecting Python scalar, not %s." % ufl_err_str(v)) - - -def expecting_expr(v): - error("Expecting Expr instance, not %s." % ufl_err_str(v)) - - -def expecting_terminal(v): - error("Expecting Terminal instance, not %s." % ufl_err_str(v)) - - -def expecting_true_ufl_scalar(v): - error("Expecting UFL scalar expression with no free indices, not %s." % ufl_err_str(v)) - - -# --- Standardized assertions --- - -# TODO: Stop using this -def ufl_assert(condition, *message): - "Assert that condition is true and otherwise issue an error with given message." - if not condition: - error(*message) diff --git a/ufl/averaging.py b/ufl/averaging.py index 055695ed3..5c04c8912 100644 --- a/ufl/averaging.py +++ b/ufl/averaging.py @@ -6,61 +6,70 @@ # # 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 -from ufl.constantvalue import ConstantValue -@ufl_type(inherit_shape_from_operand=0, - inherit_indices_from_operand=0, - num_ops=1, - is_evaluation=True) +@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 dont have a cell." - return self.ufl_operands[0].evaluate(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): - return "cell_avg(%s)" % (self.ufl_operands[0],) + """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) +@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." + """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): - return "facet_avg(%s)" % (self.ufl_operands[0],) + """Format as a string.""" + return f"facet_avg({self.ufl_operands[0]})" diff --git a/ufl/cell.py b/ufl/cell.py index 1741bc89c..e734155fa 100644 --- a/ufl/cell.py +++ b/ufl/cell.py @@ -1,323 +1,486 @@ -# -*- coding: utf-8 -*- -"Types for representing a cell." +"""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 -# -# Modified by Anders Logg, 2009. -# Modified by Kristian B. Oelgaard, 2009 -# Modified by Marie E. Rognes 2012 -# Modified by Andrew T. T. McRae, 2014 -# Modified by Massimiliano Leoni, 2016 -import numbers -import functools +from __future__ import annotations -import ufl.cell -from ufl.log import error -from ufl.core.ufl_type import attach_operators_from_hash_data +import functools +import numbers +import typing +import weakref +from abc import abstractmethod +from ufl.core.ufl_type import UFLObject -# Export list for ufl.classes __all_classes__ = ["AbstractCell", "Cell", "TensorProductCell"] -# --- The most abstract cell class, base class for other cell types +class AbstractCell(UFLObject): + """A base class for all cells.""" -class AbstractCell(object): - """Representation of an abstract finite element cell with only the - dimensions known. + @abstractmethod + def topological_dimension(self) -> int: + """Return the dimension of the topology of this cell.""" - """ - __slots__ = ("_topological_dimension", - "_geometric_dimension") - - def __init__(self, topological_dimension, geometric_dimension): - # Validate dimensions - if not isinstance(geometric_dimension, numbers.Integral): - error("Expecting integer geometric_dimension.") - if not isinstance(topological_dimension, numbers.Integral): - error("Expecting integer topological_dimension.") - if topological_dimension > geometric_dimension: - error("Topological dimension cannot be larger than geometric dimension.") - - # Store validated dimensions - self._topological_dimension = topological_dimension - self._geometric_dimension = geometric_dimension - - def topological_dimension(self): - "Return the dimension of the topology of this cell." - return self._topological_dimension - - def geometric_dimension(self): - "Return the dimension of the space this cell is embedded in." - return self._geometric_dimension - - def is_simplex(self): - "Return True if this is a simplex cell." - raise NotImplementedError("Implement this to allow important checks and optimizations.") - - def has_simplex_facets(self): - "Return True if all the facets of this cell are simplex cells." - raise NotImplementedError("Implement this to allow important checks and optimizations.") - - def __lt__(self, other): - "Define an arbitrarily chosen but fixed sort order for all cells." - if not isinstance(other, AbstractCell): - return NotImplemented - # Sort by gdim first, tdim next, then whatever's left - # depending on the subclass - s = (self.geometric_dimension(), self.topological_dimension()) - o = (other.geometric_dimension(), other.topological_dimension()) - if s != o: - return s < o - return self._ufl_hash_data_() < other._ufl_hash_data_() + @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.""" -# --- Basic topological properties of known basic cells + @abstractmethod + def _lt(self, other) -> bool: + """Less than operator. -# Mapping from cell name to number of cell entities of each -# topological dimension -num_cell_entities = {"vertex": (1,), - "interval": (2, 1), - "triangle": (3, 3, 1), - "quadrilateral": (4, 4, 1), - "tetrahedron": (4, 6, 4, 1), - "prism": (6, 9, 5, 1), - "pyramid": (5, 8, 5, 1), - "hexahedron": (8, 12, 6, 1)} + Define an arbitrarily chosen but fixed sort order for all + instances of this type with the same dimensions. + """ -# Mapping from cell name to topological dimension -cellname2dim = dict((k, len(v) - 1) for k, v in num_cell_entities.items()) + @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.""" -# --- Basic cell representation classes + @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.""" -@attach_operators_from_hash_data -class Cell(AbstractCell): - "Representation of a named finite element cell with known structure." - __slots__ = ("_cellname",) + @abstractmethod + def reconstruct(self, **kwargs: typing.Any) -> Cell: + """Reconstruct this cell, overwriting properties by those in kwargs.""" - def __init__(self, cellname, geometric_dimension=None): - "Initialize basic cell description." + 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__ - self._cellname = cellname + def num_vertices(self) -> int: + """Get the number of vertices.""" + return self.num_sub_entities(0) - # The topological dimension is defined by the cell type, so - # the cellname must be among the known ones, so we can find - # the known dimension, unless we have a product cell, in which - # the given dimension is used - topological_dimension = len(num_cell_entities[cellname]) - 1 + def num_edges(self) -> int: + """Get the number of edges.""" + return self.num_sub_entities(1) - # The geometric dimension defaults to equal the topological - # dimension unless overridden for embedded cells - if geometric_dimension is None: - geometric_dimension = topological_dimension + def num_faces(self) -> int: + """Get the number of faces.""" + return self.num_sub_entities(2) - # Initialize and validate dimensions - AbstractCell.__init__(self, topological_dimension, geometric_dimension) + def num_facets(self) -> int: + """Get the number of facets. - # --- Overrides of AbstractCell methods --- + Facets are entities of dimension tdim-1. + """ + tdim = self.topological_dimension() + return self.num_sub_entities(tdim - 1) - def reconstruct(self, geometric_dimension=None): - if geometric_dimension is None: - geometric_dimension = self._geometric_dimension - return Cell(self._cellname, geometric_dimension=geometric_dimension) + def num_ridges(self) -> int: + """Get the number of ridges. - def is_simplex(self): - " Return True if this is a simplex cell." - return self.num_vertices() == self.topological_dimension() + 1 + Ridges are entities of dimension tdim-2. + """ + tdim = self.topological_dimension() + return self.num_sub_entities(tdim - 2) - def has_simplex_facets(self): - "Return True if all the facets of this cell are simplex cells." - return self.is_simplex() or self.cellname() == "quadrilateral" + def num_peaks(self) -> int: + """Get the number of peaks. - # --- Specific cell properties --- + Peaks are entities of dimension tdim-3. + """ + tdim = self.topological_dimension() + return self.num_sub_entities(tdim - 3) - def cellname(self): - "Return the cellname of the cell." - return self._cellname + def vertices(self) -> typing.Tuple[AbstractCell, ...]: + """Get the vertices.""" + return self.sub_entities(0) - def num_vertices(self): - "The number of cell vertices." - return num_cell_entities[self.cellname()][0] + def edges(self) -> typing.Tuple[AbstractCell, ...]: + """Get the edges.""" + return self.sub_entities(1) - def num_edges(self): - "The number of cell edges." - return num_cell_entities[self.cellname()][1] + def faces(self) -> typing.Tuple[AbstractCell, ...]: + """Get the faces.""" + return self.sub_entities(2) - def num_facets(self): - "The number of cell facets." + def facets(self) -> typing.Tuple[AbstractCell, ...]: + """Get the facets. + + Facets are entities of dimension tdim-1. + """ tdim = self.topological_dimension() - return num_cell_entities[self.cellname()][tdim - 1] + return self.sub_entities(tdim - 1) + + def ridges(self) -> typing.Tuple[AbstractCell, ...]: + """Get the ridges. - # --- Facet properties --- + Ridges are entities of dimension tdim-2. + """ + tdim = self.topological_dimension() + return self.sub_entities(tdim - 2) - def facet_types(self): - "A tuple of ufl.Cell representing the facets of self." - # TODO Move outside method? - facet_type_names = {"interval": ("vertex",), - "triangle": ("interval",), - "quadrilateral": ("interval",), - "tetrahedron": ("triangle",), - "hexahedron": ("quadrilateral",), - "prism": ("triangle", "quadrilateral")} - return tuple(ufl.Cell(facet_name, self.geometric_dimension()) - for facet_name in facet_type_names[self.cellname()]) + def peaks(self) -> typing.Tuple[AbstractCell, ...]: + """Get the peaks. - # --- Edge properties --- + Peaks are entities of dimension tdim-3. + """ + tdim = self.topological_dimension() + return self.sub_entities(tdim - 3) - def edge_types(self): - # TODO Move outside method? - "A tuple of ufl.Cell representing the edged of self." - edge_type_names = {"tetrahedron": ("interval",), - "hexahedron": ("interval",), - "prism": ("interval",)} + def vertex_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique vertices types.""" + return self.sub_entity_types(0) - if self.cellname() in ["interval", "triangle", "quadrilateral"]: - error("edge_types cannot be used with dimension < 3") + def edge_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique edge types.""" + return self.sub_entity_types(1) - return tuple(ufl.Cell(edge_name, self.geometric_dimension()) - for edge_name in edge_type_names[self.cellname()]) + def face_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique face types.""" + return self.sub_entity_types(2) - # --- Special functions for proper object behaviour --- + def facet_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique facet types. - def __str__(self): - gdim = self.geometric_dimension() + Facets are entities of dimension tdim-1. + """ tdim = self.topological_dimension() - s = self.cellname() - if gdim > tdim: - s += "%dD" % gdim - return s - - def __repr__(self): - # For standard cells, return name of builtin cell object if - # possible. This reduces the size of the repr strings for - # domains, elements, etc. as well - gdim = self.geometric_dimension() + return self.sub_entity_types(tdim - 1) + + def edge_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique edge types. + + Edges are entities of dimension tdim-2. + """ tdim = self.topological_dimension() - name = self.cellname() - if gdim == tdim and name in cellname2dim: - r = name - else: - r = "Cell(%s, %s)" % (repr(name), repr(gdim)) - return r + assert tdim == 3, "Edges only make sense for cells of dimension 2 or higher." + return self.sub_entity_types(1) - def _ufl_hash_data_(self): - return (self._geometric_dimension, self._topological_dimension, - self._cellname) + 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) -@attach_operators_from_hash_data -class TensorProductCell(AbstractCell): - __slots__ = ("_cells",) + def peak_types(self) -> typing.Tuple[AbstractCell, ...]: + """Get the unique peak types. - def __init__(self, *cells, **kwargs): - keywords = list(kwargs.keys()) - if keywords and keywords != ["geometric_dimension"]: - raise ValueError( - "TensorProductCell got an unexpected keyword argument '%s'" % - keywords[0]) + 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",), + ], +} - self._cells = tuple(as_cell(cell) for cell in cells) - tdim = sum([cell.topological_dimension() for cell in self._cells]) - if kwargs: - gdim = kwargs["geometric_dimension"] - else: - gdim = sum([cell.geometric_dimension() for cell in self._cells]) +class Cell(AbstractCell): + """Representation of a named finite element cell with known structure.""" - AbstractCell.__init__(self, tdim, gdim) + __slots__ = ( + "_cellname", + "_tdim", + "_num_cell_entities", + "_sub_entity_types", + "_sub_entities", + "_sub_entity_types", + ) - def cellname(self): - "Return the cellname of the cell." - return " * ".join([cell._cellname for cell in self._cells]) + def __init__(self, cellname: str): + """Initialise. - def reconstruct(self, geometric_dimension=None): - if geometric_dimension is None: - geometric_dimension = self._geometric_dimension - return TensorProductCell(*(self._cells), geometric_dimension=geometric_dimension) + Args: + cellname: Name of the cell + """ + if cellname not in _sub_entity_celltypes: + raise ValueError(f"Unsupported cell type: {cellname}") - def is_simplex(self): - "Return True if this is a simplex cell." - if len(self._cells) == 1: - return self._cells[0].is_simplex() - return False + self._sub_entity_celltypes = _sub_entity_celltypes[cellname] - def has_simplex_facets(self): - "Return True if all the facets of this cell are simplex cells." - if len(self._cells) == 1: - return self._cells[0].has_simplex_facets() - return False + 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 num_vertices(self): - "The number of cell vertices." - return functools.reduce(lambda x, y: x * y, [c.num_vertices() for c in self._cells]) + def __str__(self) -> str: + """Format as a string.""" + return self._cellname - def num_edges(self): - "The number of cell edges." - error("Not defined for TensorProductCell.") + def __repr__(self) -> str: + """Representation.""" + return self._cellname - def num_facets(self): - "The number of cell facets." - return sum(c.num_facets() for c in self._cells if c.topological_dimension() > 0) + def _ufl_hash_data_(self) -> typing.Hashable: + """UFL hash data.""" + return (self._cellname,) - def sub_cells(self): - "Return list of cell factors." - return 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 Cell(self._cellname) - def __str__(self): - gdim = self.geometric_dimension() - tdim = self.topological_dimension() - reprs = ", ".join(repr(c) for c in self._cells) - if gdim == tdim: - gdimstr = "" - else: - gdimstr = ", geometric_dimension=%d" % gdim - r = "TensorProductCell(%s%s)" % (reprs, gdimstr) - return r - def __repr__(self): - return str(self) +class TensorProductCell(AbstractCell): + """Tensor product cell.""" - def _ufl_hash_data_(self): - return tuple(c._ufl_hash_data_() for c in self._cells) + (self._geometric_dimension,) + __slots__ = ("_cells", "_tdim") + def __init__(self, *cells: Cell): + """Initialise. -# --- Utility conversion functions + Args: + cells: Cells to take the tensor product of + """ + self._cells = tuple(as_cell(cell) for cell in cells) -# Mapping from topological dimension to reference cell name for -# simplices -_simplex_dim2cellname = {0: "vertex", - 1: "interval", - 2: "triangle", - 3: "tetrahedron"} + self._tdim = sum([cell.topological_dimension() for cell in self._cells]) -# Mapping from topological dimension to reference cell name for -# hypercubes -_hypercube_dim2cellname = {0: "vertex", - 1: "interval", - 2: "quadrilateral", - 3: "hexahedron"} + 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 simplex(topological_dimension, geometric_dimension=None): - "Return a simplex cell of given dimension." - return Cell(_simplex_dim2cellname[topological_dimension], - geometric_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.""" + if len(self._cells) == 1: + return self._cells[0].is_simplex() + return False -def hypercube(topological_dimension, geometric_dimension=None): - "Return a hypercube cell of given dimension." - return Cell(_hypercube_dim2cellname[topological_dimension], - geometric_dimension) + 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 as_cell(cell): + 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. @@ -329,4 +492,4 @@ def as_cell(cell): elif isinstance(cell, tuple): return TensorProductCell(cell) else: - error("Invalid cell %s." % cell) + raise ValueError(f"Invalid cell {cell}.") diff --git a/ufl/checks.py b/ufl/checks.py index 0b2ac7337..09ae4453b 100644 --- a/ufl/checks.py +++ b/ufl/checks.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Utility functions for checking properties of expressions.""" # Copyright (C) 2013-2016 Martin Sandve Alnæs @@ -10,54 +9,49 @@ # 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 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 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) + """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." + """Return whether expression is constant over a single cell.""" # TODO: Implement more accurately considering e.g. derivatives? - return all(t.is_cellwise_constant() for t in traverse_unique_terminals(expr)) + return all(e.is_cellwise_constant() for e in traverse_unique_terminals(expr)) -def is_globally_constant(expr): - """Check if an expression is globally constant, which - includes spatially independent constant coefficients that - are not known before assembly time.""" +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. - # from ufl.argument import Argument - # from ufl.coefficient import Coefficient - from ufl.geometry import GeometricQuantity - from ufl.core.terminal import FormArgument for e in traverse_unique_terminals(expr): # Return False if any single terminal is not constant - if e._ufl_is_literal_: - # Accept literals first, they are the most common - # terminals - continue - elif isinstance(e, FormArgument): - # Accept only Real valued Arguments and Coefficients - if e.ufl_element().family() == "Real": - continue - else: + 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 @@ -66,12 +60,3 @@ def is_globally_constant(expr): # All terminals passed constant check return True - - -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 - return is_globally_constant(expr) diff --git a/ufl/classes.py b/ufl/classes.py index 7bef75358..b8f48516c 100644 --- a/ufl/classes.py +++ b/ufl/classes.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- -# flake8: noqa -"""This file is useful for external code like tests and form compilers, +"""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.""" - +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) @@ -19,42 +19,42 @@ # This will be populated part by part below __all__ = [] - # Import all submodules, triggering execution of the ufl_type class # decorator for each Expr class. -# Base classes of Expr type hierarchy -import ufl.core.expr -import ufl.core.terminal -import ufl.core.operator - -# Terminal types -import ufl.constantvalue +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 - -# Operator types -import ufl.averaging import ufl.indexed import ufl.indexsum -import ufl.variable -import ufl.tensors -import ufl.algebra -import ufl.tensoralgebra +import ufl.integral import ufl.mathfunctions -import ufl.differentiation -import ufl.conditional -import ufl.restriction -import ufl.exprcontainers +import ufl.measure +import ufl.pullback import ufl.referencevalue - -# Make sure we import exproperators which attaches special functions -# to Expr +import ufl.restriction +import ufl.sobolevspace +import ufl.tensoralgebra +import ufl.tensors +import ufl.variable from ufl import exproperators as __exproperators -# Make sure to import modules with new Expr subclasses here! - # 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_) @@ -68,6 +68,7 @@ "ufl_classes", "terminal_classes", "nonterminal_classes", + "__exproperators", ] @@ -87,6 +88,7 @@ def populate_namespace_with_expr_classes(namespace): # 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__ @@ -95,32 +97,15 @@ def populate_namespace_with_module_classes(mod, loc): return names -import ufl.cell # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.cell, locals()) - -import ufl.finiteelement # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.finiteelement, locals()) - -import ufl.domain # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.domain, locals()) - -import ufl.functionspace # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.functionspace, locals()) - -import ufl.core.multiindex # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.core.multiindex, locals()) - -import ufl.argument # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.argument, locals()) - -import ufl.measure # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.measure, locals()) - -import ufl.integral # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.integral, locals()) - -import ufl.form # noqa E401 __all__ += populate_namespace_with_module_classes(ufl.form, locals()) - -import ufl.equation # noqa E401 __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()) diff --git a/ufl/coefficient.py b/ufl/coefficient.py index d73677612..bd35f8275 100644 --- a/ufl/coefficient.py +++ b/ufl/coefficient.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module defines the Coefficient class and a number -of related classes, including Constant.""" +"""This module defines the Coefficient class and a number of related classes, including Constant.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -11,108 +9,211 @@ # 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.log import error -from ufl.core.ufl_type import ufl_type +from ufl.argument import Argument from ufl.core.terminal import FormArgument -from ufl.finiteelement import FiniteElementBase -from ufl.domain import default_domain -from ufl.functionspace import AbstractFunctionSpace, FunctionSpace, MixedFunctionSpace +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_init +from ufl.utils.counted import Counted # --- The Coefficient class represents a coefficient in a form --- -@ufl_type() -class Coefficient(FormArgument): - """UFL form argument type: Representation of a form coefficient.""" +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 - _globalcount = 0 + __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): - FormArgument.__init__(self) - counted_init(self, count, Coefficient) + """Initalise.""" + Counted.__init__(self, count, Coefficient) - if isinstance(function_space, FiniteElementBase): - # For legacy support for .ufl files using cells, we map - # the cell to The Default Mesh - element = function_space - domain = default_domain(element.cell()) - function_space = FunctionSpace(domain, element) - elif not isinstance(function_space, AbstractFunctionSpace): - error("Expecting a FunctionSpace or FiniteElement.") + if not isinstance(function_space, AbstractFunctionSpace): + raise ValueError("Expecting a FunctionSpace.") self._ufl_function_space = function_space - self._ufl_shape = function_space.ufl_element().value_shape() + self._ufl_shape = function_space.value_shape - self._repr = "Coefficient(%s, %s)" % ( - repr(self._ufl_function_space), repr(self._count)) - - def count(self): - return self._count + self._repr = "BaseCoefficient(%s, %s)" % (repr(self._ufl_function_space), repr(self._count)) @property def ufl_shape(self): - "Return the associated UFL shape." + """Return the associated UFL shape.""" return self._ufl_shape def ufl_function_space(self): - "Get the function space of this coefficient." + """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." + """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." + """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 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 tuple of domains related to this terminal object.""" return self._ufl_function_space.ufl_domains() def _ufl_signature_data_(self, renumbering): - "Signature data for form arguments depend on the global numbering of the form arguments and domains." + """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): - count = str(self._count) - if len(count) == 1: - return "w_%s" % count - else: - return "w_{%s}" % count + """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) + 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): - """UFL value: Create a Coefficient in a mixed space, and return a - tuple with the function components corresponding to the subelements.""" + """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(function_space.ufl_sub_space(i)) - for i in range(function_space.num_sub_spaces())] + return [ + Coefficient(fs) if is_primal(fs) else Cofunction(fs) + for fs in function_space.num_sub_spaces() + ] else: return split(Coefficient(function_space)) diff --git a/ufl/compound_expressions.py b/ufl/compound_expressions.py index d25a2ea8f..6f92dd00d 100644 --- a/ufl/compound_expressions.py +++ b/ufl/compound_expressions.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"""Functions implementing compound expressions as equivalent representations using basic operators.""" - +"""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) @@ -9,12 +7,10 @@ # # Modified by Anders Logg, 2009-2010 -from ufl.log import error -from ufl.core.multiindex import indices, Index -from ufl.tensors import as_tensor, as_matrix, as_vector -from ufl.operators import sqrt 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 @@ -27,11 +23,13 @@ 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))) @@ -81,7 +79,7 @@ def pseudo_inverse_expr(A): def determinant_expr(A): - "Compute the (pseudo-)determinant of A." + """Compute the (pseudo-)determinant of A.""" sh = A.ufl_shape if isinstance(A, Zero): return zero() @@ -94,45 +92,52 @@ def determinant_expr(A): 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? - error("determinant_expr not implemented for shape %s." % (sh,)) + raise ValueError(f"determinant_expr not implemented for shape {sh}.") -def _det_2x2(B, i, j, k, l): +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 old_determinant_expr_3x3(A): - return (A[0, 0] * _det_2x2(A, 1, 2, 1, 2) + - A[0, 1] * _det_2x2(A, 1, 2, 2, 0) + - A[0, 2] * _det_2x2(A, 1, 2, 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 + 1:] + cols[:i] - codet += A[r, c] * codeterminant_expr_nxn(A, subrows, subcols) + 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." + """Compute the inverse of A.""" sh = A.ufl_shape if sh == (): return 1.0 / A @@ -146,9 +151,10 @@ def inverse_expr(A): def adj_expr(A): + """Adjoint of a matrix.""" sh = A.ufl_shape if sh[0] != sh[1]: - error("Expecting square matrix.") + raise ValueError("Expecting square matrix.") if sh[0] == 2: return adj_expr_2x2(A) @@ -157,47 +163,154 @@ def adj_expr(A): elif sh[0] == 4: return adj_expr_4x4(A) - error("adj_expr not implemented for dimension %s." % sh[0]) + raise ValueError(f"adj_expr not implemented for dimension {sh[0]}.") def adj_expr_2x2(A): - return as_matrix([[A[1, 1], -A[0, 1]], - [-A[1, 0], A[0, 0]]]) + """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): - 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]], - ]) + """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): - 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]], - ]) + """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]: - error("Expecting square matrix.") + raise ValueError("Expecting square matrix.") if sh[0] == 2: return cofactor_expr_2x2(A) @@ -206,62 +319,179 @@ def cofactor_expr(A): elif sh[0] == 4: return cofactor_expr_4x4(A) - error("cofactor_expr not implemented for dimension %s." % sh[0]) + raise ValueError(f"cofactor_expr not implemented for dimension {sh[0]}.") def cofactor_expr_2x2(A): - return as_matrix([[A[1, 1], -A[1, 0]], - [-A[0, 1], A[0, 0]]]) + """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): - 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]], - ]) + """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): - 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]] - ]) + """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]: - error("Expecting square matrix.") + raise ValueError("Expecting square matrix.") if sh[0] == 2: return deviatoric_expr_2x2(A) elif sh[0] == 3: return deviatoric_expr_3x3(A) - error("deviatoric_expr not implemented for dimension %s." % sh[0]) + raise ValueError(f"deviatoric_expr not implemented for dimension {sh[0]}.") def deviatoric_expr_2x2(A): - return as_matrix([[-1. / 2 * A[1, 1] + 1. / 2 * A[0, 0], A[0, 1]], - [A[1, 0], 1. / 2 * A[1, 1] - 1. / 2 * A[0, 0]]]) + """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): - return as_matrix([[-1. / 3 * A[1, 1] - 1. / 3 * A[2, 2] + 2. / 3 * A[0, 0], A[0, 1], A[0, 2]], - [A[1, 0], 2. / 3 * A[1, 1] - 1. / 3 * A[2, 2] - 1. / 3 * A[0, 0], A[1, 2]], - [A[2, 0], A[2, 1], -1. / 3 * A[1, 1] + 2. / 3 * A[2, 2] - 1. / 3 * A[0, 0]]]) + """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]], + ] + ) diff --git a/ufl/conditional.py b/ufl/conditional.py index b02761478..117808c2b 100644 --- a/ufl/conditional.py +++ b/ufl/conditional.py @@ -1,20 +1,19 @@ -# -*- coding: utf-8 -*- """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 -from ufl.log import warning, error +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.ufl_type import ufl_type from ufl.core.operator import Operator -from ufl.constantvalue import as_ufl -from ufl.precedence import parstr +from ufl.core.ufl_type import ufl_type from ufl.exprequals import expr_equals -from ufl.checks import is_true_ufl_scalar +from ufl.precedence import parstr # --- Condition classes --- @@ -24,22 +23,30 @@ @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 - error("UFL conditions cannot be evaluated as bool in a Python context.") + 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): - __slots__ = ('_name',) + """Binary condition.""" + + __slots__ = ("_name",) def __init__(self, name, left, right): + """Initialise.""" left = as_ufl(left) right = as_ufl(right) @@ -47,47 +54,57 @@ def __init__(self, name, left, right): self._name = name - if name in ('!=', '=='): + 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 ('&&', '||'): + elif name in ("&&", "||"): # Binary operators acting on boolean expressions allow # only conditions for arg in (left, right): if not isinstance(arg, Condition): - error("Expecting a Condition, not %s." % ufl_err_str(arg)) + 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 != (): - error("Expecting scalar arguments.") + raise ValueError("Expecting scalar arguments.") if left.ufl_free_indices != () or right.ufl_free_indices != (): - error("Expecting scalar arguments.") + raise ValueError("Expecting scalar arguments.") def __str__(self): - return "%s %s %s" % (parstr(self.ufl_operands[0], self), - self._name, parstr(self.ufl_operands[1], 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__ @@ -95,29 +112,39 @@ def __bool__(self): # 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) @@ -125,12 +152,16 @@ def evaluate(self, x, mapping, component, index_values): @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) @@ -138,12 +169,16 @@ def evaluate(self, x, mapping, component, index_values): @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) @@ -151,12 +186,16 @@ def evaluate(self, x, mapping, component, index_values): @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) @@ -164,12 +203,16 @@ def evaluate(self, x, mapping, component, index_values): @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) @@ -177,12 +220,16 @@ def evaluate(self, x, mapping, component, index_values): @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) @@ -190,51 +237,64 @@ def evaluate(self, x, mapping, component, index_values): @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): - error("Expecting a 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]),) -# --- Conditional expression (condition ? true_value : false_value) --- - -@ufl_type(num_ops=3, inherit_shape_from_operand=1, - inherit_indices_from_operand=1) +@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): - error("Expectiong condition as first argument.") + 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: - error("Shape mismatch between conditional branches.") + raise ValueError("Shape mismatch between conditional branches.") tfi = true_value.ufl_free_indices ffi = false_value.ufl_free_indices if tfi != ffi: - error("Free index mismatch between conditional branches.") + 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 == ())): - error("Non-scalar == or != is not allowed.") + 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] @@ -243,56 +303,66 @@ def evaluate(self, x, mapping, component, index_values): 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): - "UFL operator: Take the minimum of two values." + """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)): - error("Expecting scalar arguments.") + 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: - warning('Value error in evaluation of min() of %s and %s.' % self.ufl_operands) + 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): - "UFL operator: Take the maximum of two values." + """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)): - error("Expecting scalar arguments.") + 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: - warning('Value error in evaluation of max() of %s and %s.' % self.ufl_operands) + 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 diff --git a/ufl/constant.py b/ufl/constant.py index 83a0247fd..e9d658fcc 100644 --- a/ufl/constant.py +++ b/ufl/constant.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module defines classes representing non-literal values -which are constant with respect to a domain.""" +"""Support fpr non-literal values which are constant with respect to a domain.""" # Copyright (C) 2019 Michal Habera # @@ -8,20 +6,22 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.core.ufl_type import ufl_type 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_init +from ufl.utils.counted import Counted @ufl_type() -class Constant(Terminal): +class Constant(Terminal, Counted): + """Constant.""" + _ufl_noslots_ = True - _globalcount = 0 def __init__(self, domain, shape=(), count=None): + """Initalise.""" Terminal.__init__(self) - counted_init(self, count=count, countedclass=Constant) + Counted.__init__(self, count, Constant) self._ufl_domain = as_domain(domain) self._ufl_shape = shape @@ -29,55 +29,64 @@ def __init__(self, domain, shape=(), count=None): # 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)) - - def count(self): - return self._count + 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): - return (self.ufl_domain(), ) + """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): - count = str(self._count) - if len(count) == 1: - return "c_%s" % count - else: - return "c_{%s}" % count + """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) + 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" + """Signature data for constant depends on renumbering.""" return "Constant({}, {}, {})".format( - self._ufl_domain._ufl_signature_data_(renumbering), repr(self._ufl_shape), - repr(renumbering[self])) + 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) + 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) + return Constant( + domain, shape=(domain.geometric_dimension(), domain.geometric_dimension()), count=count + ) diff --git a/ufl/constantvalue.py b/ufl/constantvalue.py index 0f0a311a7..b918a670b 100644 --- a/ufl/constantvalue.py +++ b/ufl/constantvalue.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"This module defines classes representing constant values." +"""This module defines classes representing constant values.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -10,24 +9,24 @@ # Modified by Anders Logg, 2011. # Modified by Massimiliano Leoni, 2016. +import numbers from math import atan2 -from ufl.log import error, UFLValueError +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.multiindex import Index, FixedIndex from ufl.core.ufl_type import ufl_type -# --- Helper functions imported here for compatibility--- -from ufl.checks import is_python_scalar, is_ufl_scalar, is_true_ufl_scalar # noqa: F401 - - # Precision for float formatting precision = None def format_float(x): - "Format float value based on global UFL precision." + """Format float value based on global UFL precision.""" if precision: return "{:.{prec}}".format(float(x), prec=precision) else: @@ -36,37 +35,44 @@ def 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 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 tuple of domains related to this terminal object.""" return () -# --- Class for representing zero tensors of different shapes --- - -# TODO: Add geometric dimension/domain and Argument dependencies to -# Zero? +# TODO: Add geometric dimension/domain and Argument dependencies to Zero? @ufl_type(is_literal=True) class Zero(ConstantValue): - "UFL literal type: Representation of a zero valued expression." + """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: @@ -79,44 +85,54 @@ def __new__(cls, shape=(), free_indices=(), index_dimensions=None): 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): - error("Expecting tuple of int.") + raise ValueError("Expecting tuple of int.") if not isinstance(free_indices, tuple): - error("Expecting tuple for free_indices, not %s" % str(free_indices)) + 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())): - error("Expecting tuple of index dimensions, not %s" % str(index_dimensions)) + 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())) + 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): - error("Expecting tuple of integer free index ids, not %s" % str(free_indices)) - if not (isinstance(index_dimensions, tuple) and - all(isinstance(i, int) for i in index_dimensions)): - error("Expecting tuple of integer index dimensions, not %s" % str(index_dimensions)) + 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): - # error("Expecting sorted input. Remove this check later for efficiency.") + # 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 == (): @@ -126,46 +142,58 @@ def __str__(self): 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)) + 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) + 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." + """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: @@ -174,23 +202,30 @@ def zero(*shape): # --- Scalar value types --- + @ufl_type(is_abstract=True, is_scalar=True) class ScalarValue(ConstantValue): - "A constant scalar value." + """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): - """This is implemented to allow comparison with python scalars. + """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 @@ -209,39 +244,50 @@ def __eq__(self, other): 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): - "UFL literal type: Representation of a constant, complex scalar" + """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() @@ -251,64 +297,79 @@ def __new__(cls, value): 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" + """Abstract class used to differentiate real values from complex ones.""" + __slots__ = () @ufl_type(wraps_type=float, is_literal=True) class FloatValue(RealValue): - "UFL literal type: Representation of a constant scalar floating point value." + """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): - "UFL literal type: Representation of a constant scalar integer value." + """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() @@ -326,91 +387,110 @@ def __new__(cls, 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): - "UFL literal type: Representation of an identity matrix." + """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): - "Evaluates the identity matrix on the given components." + """Evaluate.""" a, b = component return 1 if a == b else 0 def __getitem__(self, key): + """Get an item.""" if len(key) != 2: - error("Size mismatch for Identity.") + 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): - """UFL literal type: Representation of a permutation symbol. + """Representation of a permutation symbol. This is also known as the Levi-Civita symbol, antisymmetric symbol, - or alternating 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): - "Evaluates the permutation symbol." + """Evaluate.""" return self.__eps(component) def __getitem__(self, key): + """Get an item.""" if len(key) != self._dim: - error("Size mismatch for PermutationSymbol.") + 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): - """This function body is taken from - http://www.mathkb.com/Uwe/Forum.aspx/math/29865/N-integer-Levi-Civita + """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): @@ -424,15 +504,16 @@ def __eps(self, x): def as_ufl(expression): - "Converts expression to an Expr if possible." - if isinstance(expression, Expr): + """Converts expression to an Expr if possible.""" + if isinstance(expression, (Expr, ufl.BaseForm)): return expression - elif isinstance(expression, complex): - return ComplexValue(expression) - elif isinstance(expression, float): - return FloatValue(expression) - elif isinstance(expression, int): + 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 UFLValueError("Invalid type conversion: %s can not be converted" - " to any UFL type." % str(expression)) + raise ValueError( + f"Invalid type conversion: {expression} can not be converted to any UFL type." + ) diff --git a/ufl/core/__init__.py b/ufl/core/__init__.py index e69de29bb..06b883b35 100644 --- a/ufl/core/__init__.py +++ b/ufl/core/__init__.py @@ -0,0 +1 @@ +"""UFL core.""" diff --git a/ufl/core/base_form_operator.py b/ufl/core/base_form_operator.py new file mode 100644 index 000000000..aae943f75 --- /dev/null +++ b/ufl/core/base_form_operator.py @@ -0,0 +1,185 @@ +"""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() diff --git a/ufl/core/compute_expr_hash.py b/ufl/core/compute_expr_hash.py index d48669262..e0b6f9cf0 100644 --- a/ufl/core/compute_expr_hash.py +++ b/ufl/core/compute_expr_hash.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Non-recursive traversal-based hash computation algorithm. Fast iteration over nodes in an ``Expr`` DAG to compute diff --git a/ufl/core/expr.py b/ufl/core/expr.py index 66304e2af..cef955f0e 100644 --- a/ufl/core/expr.py +++ b/ufl/core/expr.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- -"""This module defines the ``Expr`` class, the superclass -for all expression tree node types in UFL. +"""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: +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) @@ -19,12 +16,12 @@ # Modified by Anders Logg, 2008 # Modified by Massimiliano Leoni, 2016 -from ufl.log import error +import warnings +from ufl.core.ufl_type import UFLType, update_ufl_type_attributes -# --- The base object for all UFL expression tree nodes --- -class Expr(object): +class Expr(object, metaclass=UFLType): """Base class for all UFL expression types. *Instance properties* @@ -86,14 +83,15 @@ class MyOperator(Operator): # This is to freeze member variables for objects of this class and # save memory by skipping the per-instance dict. - __slots__ = ("_hash", - "__weakref__") + __slots__ = ("_hash", "__weakref__") # _ufl_noslots_ = True # --- Basic object behaviour --- def __getnewargs__(self): - """The tuple returned here is passed to as args to cls.__new__(cls, *args). + """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. @@ -102,6 +100,7 @@ def __getnewargs__(self): return self.ufl_operands def __init__(self): + """Initialise.""" self._hash = None # This shows the principal behaviour of the hash function attached @@ -128,22 +127,10 @@ def __init__(self): # implement for this type in a multifunction. _ufl_handler_name_ = "expr" - # The integer typecode, a contiguous index different for each - # type. This is used for fast lookup into e.g. multifunction - # handler tables. - _ufl_typecode_ = 0 - # 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 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 - # Type trait: If the type is a literal. _ufl_is_literal_ = None @@ -182,13 +169,10 @@ def __init__(self): _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", ) @@ -199,43 +183,25 @@ def __init__(self): _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__", - - # TODO: Add checks for methods/properties of terminals only? - # Required for terminals: - # "is_cellwise_constant", # TODO: Rename to ufl_is_cellwise_constant? ) # --- Global variables for collecting all types --- - # A global counter of the number of typecodes assigned - _ufl_num_typecodes_ = 1 - - # A global set of all handler names added - _ufl_all_handler_names_ = set() - - # A global array of all Expr subclasses, indexed by typecode - _ufl_all_classes_ = [] - # A global dict mapping language_operator_name to the type it # produces _ufl_language_operators_ = {} @@ -245,29 +211,21 @@ def __init__(self): # --- Mechanism for profiling object creation and deletion --- - # A global array of the number of initialized objects for each - # typecode - _ufl_obj_init_counts_ = [0] - - # A global array of the number of deleted objects for each - # typecode - _ufl_obj_del_counts_ = [0] - # Backup of default init and del _ufl_regular__init__ = __init__ def _ufl_profiling__init__(self): - "Replacement constructor with object counting." + """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." + """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." + """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_)): @@ -276,7 +234,7 @@ def ufl_enable_profiling(): @staticmethod def ufl_disable_profiling(): - "Turn off the object counting mechanism. Return object init and del counts." + """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_) @@ -286,26 +244,36 @@ def ufl_disable_profiling(): # --- Functions for reconstructing expression --- def _ufl_expr_reconstruct_(self, *operands): - "Return a new object of the same type with new 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): # TODO: Deprecate this and use extract_domains(expr) - "Return all domains this expression is defined on." + 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): # TODO: Deprecate this and use extract_unique_domain(expr) - "Return the single unique domain this expression is defined on, or throw an error." + 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.""" - error("Symbolic evaluation of %s not available." % self._ufl_class_.__name__) + 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: @@ -313,7 +281,7 @@ def _ufl_evaluate_scalar_(self): return self(()) # No known x def __float__(self): - "Try to evaluate as scalar and cast to float." + """Try to evaluate as scalar and cast to float.""" try: v = float(self._ufl_evaluate_scalar_()) except Exception: @@ -321,7 +289,7 @@ def __float__(self): return v def __complex__(self): - "Try to evaluate as scalar and cast to complex." + """Try to evaluate as scalar and cast to complex.""" try: v = complex(self._ufl_evaluate_scalar_()) except TypeError: @@ -329,16 +297,16 @@ def __complex__(self): return v def __bool__(self): - "By default, all Expr are nonzero/False." + """By default, all Expr are nonzero/False.""" return True def __nonzero__(self): - "By default, all Expr are nonzero/False." + """By default, all Expr are nonzero/False.""" return self.__bool__() @staticmethod def _ufl_coerce_(value): - "Convert any value to a UFL type." + """Convert any value to a UFL type.""" # Quick skip for most types if isinstance(value, Expr): return value @@ -357,53 +325,55 @@ def _ufl_coerce_(value): # 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." + """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." + """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." + """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 "<%s id=%d>" % (self._ufl_class_.__name__, id(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.""" + """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." + """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." + """Iteration over vector expressions.""" for i in range(len(self)): yield self[i] def __floordiv__(self, other): - "UFL does not support integer division." + """UFL does not support integer division.""" raise NotImplementedError(self.__class__.__floordiv__) def __pos__(self): - "Unary + is a no-op." + """Unary + is a no-op.""" return self def __round__(self, n=None): - "Round to nearest integer or to nearest nth decimal." + """Round to nearest integer or to nearest nth decimal.""" try: val = float(self._ufl_evaluate_scalar_()) val = round(val, n) @@ -414,22 +384,18 @@ def __round__(self, n=None): val = NotImplemented return val - # --- Deprecated functions - - def geometric_dimension(self): - "Return the geometric dimension this expression lives in." - from ufl.domain import find_geometric_dimension - return find_geometric_dimension(self) - # Initializing traits here because Expr is not defined in the class # declaration Expr._ufl_class_ = Expr -Expr._ufl_all_handler_names_.add(Expr) -Expr._ufl_all_classes_.append(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: diff --git a/ufl/core/external_operator.py b/ufl/core/external_operator.py new file mode 100644 index 000000000..3a9304854 --- /dev/null +++ b/ufl/core/external_operator.py @@ -0,0 +1,120 @@ +"""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() + ) diff --git a/ufl/core/interpolate.py b/ufl/core/interpolate.py new file mode 100644 index 000000000..3b5e3ba4d --- /dev/null +++ b/ufl/core/interpolate.py @@ -0,0 +1,107 @@ +"""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) diff --git a/ufl/core/multiindex.py b/ufl/core/multiindex.py index 56ac44dd1..2d91e40f3 100644 --- a/ufl/core/multiindex.py +++ b/ufl/core/multiindex.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module defines the single index types and some internal index utilities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg @@ -9,11 +8,9 @@ # # Modified by Massimiliano Leoni, 2016. - -from ufl.log import error -from ufl.utils.counted import counted_init -from ufl.core.ufl_type import ufl_type 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"] @@ -21,101 +18,114 @@ class IndexBase(object): """Base class for all indices.""" + __slots__ = () def __init__(self): - pass + """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): - error("Expecting integer value for fixed index.") + 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): - pass + """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): - return "%d" % self._value + """Represent with a string.""" + return f"{self._value}" def __repr__(self): - r = "FixedIndex(%d)" % self._value - return r + """Return representation.""" + return f"FixedIndex({self._value})" -class Index(IndexBase): +class Index(IndexBase, Counted): """UFL value: An index with no value assigned. - Used to represent free indices in Einstein indexing notation.""" - __slots__ = ("_count",) + Used to represent free indices in Einstein indexing notation. + """ - _globalcount = 0 + __slots__ = ("_count", "_counted_class") def __init__(self, count=None): + """Initialise.""" IndexBase.__init__(self) - counted_init(self, count, Index) - - def count(self): - return self._count + 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): - c = str(self._count) + """Represent as a string.""" + c = f"{self._count}" if len(c) > 1: - c = "{%s}" % c - return "i_%s" % c + c = f"{{{c}}}" + return f"i_{c}" def __repr__(self): - r = "Index(%d)" % self._count - return r + """Return representation.""" + return f"Index({self._count})" @ufl_type() class MultiIndex(Terminal): - "Represents a sequence of indices, either fixed or free." + """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): - error("Expecting a tuple of indices.") + raise ValueError("Expecting a tuple of indices.") if all(isinstance(ind, FixedIndex) for ind in indices): # Cache multiindices consisting of purely fixed indices @@ -130,7 +140,7 @@ def __new__(cls, indices): # 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): - error("Expecting only Index and FixedIndex objects.") + raise ValueError("Expecting only Index and FixedIndex objects.") self = Terminal.__new__(cls) # Initialize here instead of in __init__ to avoid overwriting @@ -139,25 +149,27 @@ def __new__(cls, indices): return self def __init__(self, indices): - pass + """Initialise.""" def _init(self, indices): + """Initialise.""" Terminal.__init__(self) self._indices = indices def indices(self): - "Return tuple of indices." + """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): - return isinstance(other, MultiIndex) and \ - self._indices == other._indices + """Check equality.""" + return isinstance(other, MultiIndex) and self._indices == other._indices def evaluate(self, x, mapping, component, index_values): - "Evaluate index." + """Evaluate index.""" # Build component from index values component = [] for i in self._indices: @@ -169,30 +181,43 @@ def evaluate(self, x, mapping, component, index_values): @property def ufl_shape(self): - "This shall not be used." - error("Multiindex has no shape (it is not a tensor expression).") + """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): - "This shall not be used." - error("Multiindex has no free indices (it is not a tensor expression).") + """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): - "This shall not be used." - error("Multiindex has no free indices (it is not a tensor expression).") + """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): - "Always True." + """Check if cellwise constant. + + Always True. + """ return True def ufl_domains(self): - "Return tuple of domains related to this terminal object." + """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): @@ -200,6 +225,7 @@ def __add__(self, other): return NotImplemented def __radd__(self, other): + """Add.""" if isinstance(other, tuple): return MultiIndex(other + self._indices) elif isinstance(other, MultiIndex): @@ -209,33 +235,27 @@ def __radd__(self, other): # --- String formatting --- def __str__(self): + """Format as a string.""" return ", ".join(str(i) for i in self._indices) def __repr__(self): - r = "MultiIndex(%s)" % repr(self._indices) - return r + """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 as_multi_index(ii, shape=None): - "Return a ``MultiIndex`` version of *ii*." - if isinstance(ii, MultiIndex): - return ii - elif not isinstance(ii, tuple): - ii = (ii,) - return MultiIndex(ii) - - def indices(n): - "UFL value: Return a tuple of :math:`n` new Index objects." + """Return a tuple of n new Index objects.""" return tuple(Index() for i in range(n)) diff --git a/ufl/core/operator.py b/ufl/core/operator.py index 07e8e856f..c2e31b87e 100644 --- a/ufl/core/operator.py +++ b/ufl/core/operator.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- - +"""Operator.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -13,14 +12,14 @@ from ufl.core.ufl_type import ufl_type -# --- Base class for operator objects --- - @ufl_type(is_abstract=True, is_terminal=False) class Operator(Expr): - "Base class for all operators, i.e. non-terminal expression types." + """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 @@ -31,19 +30,18 @@ def __init__(self, operands=None): self.ufl_operands = operands def _ufl_expr_reconstruct_(self, *operands): - "Return a new object of the same type with new 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." + """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." + """Default repr string construction for operators.""" # This should work for most cases - r = "%s(%s)" % (self._ufl_class_.__name__, - ", ".join(repr(op) for op in self.ufl_operands)) - return r + return f"{self._ufl_class_.__name__}({', '.join(repr(op) for op in self.ufl_operands)})" diff --git a/ufl/core/terminal.py b/ufl/core/terminal.py index 490bdbeba..47c6c16d9 100644 --- a/ufl/core/terminal.py +++ b/ufl/core/terminal.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- -"""This module defines the ``Terminal`` class, the superclass -for all types that are terminal nodes in an expression tree.""" +"""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) @@ -11,37 +11,35 @@ # Modified by Anders Logg, 2008 # Modified by Massimiliano Leoni, 2016 -from ufl.log import error, warning +import warnings + from ufl.core.expr import Expr from ufl.core.ufl_type import ufl_type -# --- Base class for terminal objects --- - @ufl_type(is_abstract=True, is_terminal=True) class Terminal(Expr): - "A terminal node in the UFL expression tree." + """Base class for terminal objects. + + A terminal node in the UFL expression tree. + """ + __slots__ = () def __init__(self): + """Initialise the terminal.""" Expr.__init__(self) - def _ufl_expr_reconstruct_(self, *operands): - "Return self." - if operands: - error("Terminal has no operands.") - return self - ufl_operands = () ufl_free_indices = () ufl_index_dimensions = () def ufl_domains(self): - "Return tuple of domains related to this terminal object." + """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." + """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: @@ -56,10 +54,12 @@ def evaluate(self, x, mapping, component, index_values, derivatives=()): except Exception: pass # If it has an ufl_evaluate function, call it - if hasattr(self, 'ufl_evaluate'): + if hasattr(self, "ufl_evaluate"): return self.ufl_evaluate(x, component, derivatives) # Take component if any - warning("Couldn't map '%s' to a float, returning ufl object without evaluation." % str(self)) + warnings.warn( + f"Couldn't map '{self}' to a float, returning ufl object without evaluation." + ) f = self if component: f = f[component] @@ -81,24 +81,27 @@ def evaluate(self, x, mapping, component, index_values, derivatives=()): return f def _ufl_signature_data_(self, renumbering): - "Default signature data for of terminals just return the repr string." + """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." + """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." + """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." + """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) diff --git a/ufl/core/ufl_id.py b/ufl/core/ufl_id.py index 26dd3e675..34cbda859 100644 --- a/ufl/core/ufl_id.py +++ b/ufl/core/ufl_id.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Utilites for types with a globally counted unique id attached to each object." +"""Utilites for types with a globally counted unique id attached to each object.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -40,11 +39,11 @@ def __init__(self, *args, ufl_id=None): """ def _get_ufl_id(self): - "Return the ufl_id of this object." + """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." + """Initialize new ufl_id for the object under construction.""" # Bind cls with closure here def init_ufl_id(self, ufl_id): @@ -52,11 +51,10 @@ def init_ufl_id(self, ufl_id): 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: - if hasattr(cls, "__slots__"): - assert "_ufl_id" in cls.__slots__ cls._ufl_global_id = 0 cls.ufl_id = _get_ufl_id cls._init_ufl_id = _init_ufl_id(cls) diff --git a/ufl/core/ufl_type.py b/ufl/core/ufl_type.py index 9c4468a6f..c409d05fb 100644 --- a/ufl/core/ufl_type.py +++ b/ufl/core/ufl_type.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- - +"""UFL type.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -7,43 +6,48 @@ # SPDX-License-Identifier: LGPL-3.0-or-later # # Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from __future__ import annotations -from ufl.core.expr import Expr +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 -# Make UFL type coercion available under the as_ufl name -# as_ufl = Expr._ufl_coerce_ +class UFLObject(ABC): + """A UFL Object.""" -def attach_operators_from_hash_data(cls): - """Class decorator to attach ``__hash__``, ``__eq__`` and ``__ne__`` implementations. + @abstractmethod + def _ufl_hash_data_(self) -> typing.Hashable: + """Return hashable data that uniquely defines this object.""" - These are implemented in terms of a ``._ufl_hash_data()`` method on the class, - which should return a tuple or hashable and comparable data. - """ - assert hasattr(cls, "_ufl_hash_data_") + @abstractmethod + def __str__(self) -> str: + """Return a human-readable string representation of the object.""" - def __hash__(self): - "__hash__ implementation attached in attach_operators_from_hash_data" + @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_()) - cls.__hash__ = __hash__ def __eq__(self, other): - "__eq__ implementation attached in attach_operators_from_hash_data" - return type(self) == type(other) and self._ufl_hash_data_() == other._ufl_hash_data_() - cls.__eq__ = __eq__ + """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): - "__ne__ implementation attached in attach_operators_from_hash_data" - return type(self) != type(other) or self._ufl_hash_data_() != other._ufl_hash_data_() - cls.__ne__ = __ne__ - - return cls + """Check inequality.""" + return not self.__eq__(other) def get_base_attr(cls, name): - "Return first non-``None`` attribute of given name among base classes." + """Return first non-``None`` attribute of given name among base classes.""" for base in cls.mro(): if hasattr(base, name): attr = getattr(base, name) @@ -55,7 +59,8 @@ def get_base_attr(cls, name): 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. + 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: @@ -64,7 +69,7 @@ def set_trait(cls, basename, value, inherit=False): def determine_num_ops(cls, num_ops, unop, binop, rbinop): - "Determine number of operands for this type." + """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 @@ -82,53 +87,59 @@ def determine_num_ops(cls, num_ops, unop, binop, rbinop): def check_is_terminal_consistency(cls): - "Check for consistency in ``is_terminal`` trait among superclasses." + """Check for consistency in ``is_terminal`` trait among superclasses.""" if cls._ufl_is_terminal_ is None: - msg = ("Class {0.__name__} has not specified the is_terminal trait." + - " Did you forget to inherit from Terminal or Operator?") - raise TypeError(msg.format(cls)) + 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 = ("Conflicting given and automatic 'is_terminal' trait for class {0.__name__}." + - " Check if you meant to inherit from Terminal or Operator.") - raise TypeError(msg.format(cls)) + 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." + """Check that the first base classes up to ``Expr`` are other UFL types.""" for base in cls.mro(): - if base is Expr: + if base is core.expr.Expr: break - if not issubclass(base, 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, Expr)) + 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_``.""" + """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_.") + 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.") + 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." - + """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_) @@ -161,7 +172,7 @@ def check_type_traits_consistency(cls): def check_implements_required_methods(cls): """Check if type implements the required methods.""" if not cls._ufl_is_abstract_: - for attr in Expr._ufl_required_methods_: + 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)) @@ -171,9 +182,9 @@ def check_implements_required_methods(cls): def check_implements_required_properties(cls): - "Check if type implements the required properties." + """Check if type implements the required properties.""" if not cls._ufl_is_abstract_: - for attr in Expr._ufl_required_properties_: + 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)) @@ -182,9 +193,10 @@ def check_implements_required_properties(cls): raise TypeError(msg.format(cls, attr)) -def attach_implementations_of_indexing_interface(cls, - inherit_shape_from_operand, - inherit_indices_from_operand): +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_: @@ -198,27 +210,32 @@ def attach_implementations_of_indexing_interface(cls, # 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 ``Expr`` attributes, mainly by adding *cls* to global collections of ufl types." - Expr._ufl_all_classes_.append(cls) - Expr._ufl_all_handler_names_.add(cls._ufl_handler_name_) + """Update global attributres. + Update global ``Expr`` attributes, mainly by adding *cls* to global + collections of ufl types. + """ if cls._ufl_is_terminal_modifier_: - Expr._ufl_terminal_modifiers_.append(cls) + 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. @@ -226,52 +243,67 @@ def update_global_expr_attributes(cls): # it out later. if not cls._ufl_is_abstract_ and hasattr(cls, "_ufl_function_"): cls._ufl_function_.__func__.__doc__ = cls.__doc__ - Expr._ufl_language_operators_[cls._ufl_handler_name_] = cls._ufl_function_ + 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. - Expr._ufl_obj_init_counts_.append(0) - Expr._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): - """This decorator is to be applied to every subclass in the UFL ``Expr`` 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. + 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): - # Determine integer typecode by oncrementally counting all types - typecode = Expr._ufl_num_typecodes_ - Expr._ufl_num_typecodes_ += 1 - - # Determine handler name by a mapping from "TypeName" to "type_name" - handler_name = camel2underscore(cls.__name__) - - # is_scalar implies is_index_free + """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: @@ -279,17 +311,13 @@ def _ufl_type_decorator_(cls): # Store type traits cls._ufl_class_ = cls - set_trait(cls, "handler_name", handler_name, inherit=False) - set_trait(cls, "typecode", typecode, inherit=False) 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_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_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) @@ -305,7 +333,8 @@ def _ufl_type_decorator_(cls): """# 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__}." + 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): @@ -352,9 +381,9 @@ def _ufl_expr_rbinop_(self, other): # 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) + attach_implementations_of_indexing_interface( + cls, inherit_shape_from_operand, inherit_indices_from_operand + ) # Update Expr update_global_expr_attributes(cls) @@ -372,3 +401,37 @@ def _ufl_expr_rbinop_(self, other): 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 diff --git a/ufl/corealg/__init__.py b/ufl/corealg/__init__.py index e69de29bb..ad28b971b 100644 --- a/ufl/corealg/__init__.py +++ b/ufl/corealg/__init__.py @@ -0,0 +1 @@ +"""Core algorithms.""" diff --git a/ufl/corealg/map_dag.py b/ufl/corealg/map_dag.py index 6b20c4179..9b9196f17 100644 --- a/ufl/corealg/map_dag.py +++ b/ufl/corealg/map_dag.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """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) @@ -10,55 +8,59 @@ # Modified by Massimiliano Leoni, 2016 from ufl.core.expr import Expr -from ufl.corealg.traversal import unique_post_traversal, cutoff_unique_post_traversal 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): +def map_expr_dag(function, expression, compress=True, vcache=None, rcache=None): """Apply a function to each subexpression 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 funtion is called multiple times in a transformation + If the same function is called multiple times in a transformation (as for example in apply_derivatives), then to reuse caches across - the call, provide these two arguments: - - :arg vcache: Optional dict for caching results of intermediate transformations - :arg rcache: Optional dict for caching results for compression. - - Return the result of the final function call. + 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) + (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 subexpression node in an expression DAG. +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 funtion is called multiple times in a transformation + If the same function is called multiple times in a transformation (as for example in apply_derivatives), then to reuse caches across - the call, provide these two arguments: - - :arg vcache: Optional dict for caching results of intermediate transformations - :arg rcache: Optional dict for caching results for compression. - - Return a list with the result of the final function call for each expression. + 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 @@ -79,9 +81,11 @@ def map_expr_dags(function, expressions, # 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) diff --git a/ufl/corealg/multifunction.py b/ufl/corealg/multifunction.py index 16b08a3ba..fc6e45136 100644 --- a/ufl/corealg/multifunction.py +++ b/ufl/corealg/multifunction.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """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) @@ -11,18 +9,18 @@ import inspect -from ufl.log import error 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*." + """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." + """Function decorator to memoize ``MultiFunction`` handlers.""" def _memoized_handler(self, o): c = getattr(self, "_memoized_handler_cache") @@ -31,6 +29,7 @@ def _memoized_handler(self, o): r = handler(self, o) c[o] = r return r + return _memoized_handler @@ -50,6 +49,7 @@ class MultiFunction(object): _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) @@ -66,12 +66,18 @@ def __init__(self): for c in classobject.mro(): # Register classobject with handler for the first # encountered superclass - handler_name = c._ufl_handler_name_ + 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] + 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 @@ -85,12 +91,12 @@ def __init__(self): self._memoized_handler_cache = {} def __call__(self, o, *args): - "Delegate to handler function based on typecode of first argument." + """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." - error("No handler defined for %s." % o._ufl_class_.__name__) + """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. @@ -107,5 +113,5 @@ def reuse_if_untouched(self, o, *ops): else: return o._ufl_expr_reconstruct_(*ops) - # Set default behaviour for any Expr as undefined - expr = undefined + # Set default behaviour for any UFLType as undefined + ufl_type = undefined diff --git a/ufl/corealg/traversal.py b/ufl/corealg/traversal.py index 7b582e918..64c81deaa 100644 --- a/ufl/corealg/traversal.py +++ b/ufl/corealg/traversal.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Various expression traversal utilities. The algorithms here are non-recursive, which is faster than recursion @@ -40,8 +39,11 @@ def post_traversal(expr): def cutoff_post_traversal(expr, cutofftypes): - """Yield ``o`` for each node ``o`` in *expr*, child before parent, but - skipping subtrees of the 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] @@ -81,7 +83,8 @@ def unique_pre_traversal(expr, visited=None): def unique_post_traversal(expr, visited=None): """Yield ``o`` for each node ``o`` in *expr*, child before parent. - Never visit a node twice.""" + Never visit a node twice. + """ lifo = [(expr, list(expr.ufl_operands))] if visited is None: visited = set() @@ -102,7 +105,8 @@ def unique_post_traversal(expr, visited=None): 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.""" + Never visit a node twice. + """ lifo = [(expr, list(reversed(expr.ufl_operands)))] if visited is None: visited = set() @@ -125,12 +129,14 @@ def cutoff_unique_post_traversal(expr, cutofftypes, visited=None): 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 diff --git a/ufl/differentiation.py b/ufl/differentiation.py index 49c7c432b..74e33617d 100644 --- a/ufl/differentiation.py +++ b/ufl/differentiation.py @@ -1,87 +1,200 @@ -# -*- coding: utf-8 -*- -"Differential operators." +"""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 -# -# Modified by Anders Logg, 2009. -from ufl.log import error +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.terminal import Terminal 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.constantvalue import Zero -from ufl.coefficient import Coefficient -from ufl.variable import Variable +from ufl.form import BaseForm from ufl.precedence import parstr -from ufl.domain import find_geometric_dimension -from ufl.checks import is_cellwise_constant - +from ufl.variable import Variable # --- Basic differentiation objects --- -@ufl_type(is_abstract=True, - is_differential=True) + +@ufl_type(is_abstract=True, is_differential=True) class Derivative(Operator): - "Base class for all derivative types." + """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) +@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) class CoefficientDerivative(Derivative): - """Derivative of the integrand of a form w.r.t. the - degrees of freedom in a discrete Coefficient.""" + """Derivative of form integrand w.r.t. the degrees of freedom in a discrete Coefficient.""" + __slots__ = () - def __new__(cls, integrand, coefficients, arguments, - coefficient_derivatives): + def __new__(cls, integrand, coefficients, arguments, coefficient_derivatives): + """Create a new CoefficientDerivative.""" if not isinstance(coefficients, ExprList): - error("Expecting ExprList instance with Coefficients.") + raise ValueError("Expecting ExprList instance with Coefficients.") if not isinstance(arguments, ExprList): - error("Expecting ExprList instance with Arguments.") + raise ValueError("Expecting ExprList instance with Arguments.") if not isinstance(coefficient_derivatives, ExprMapping): - error("Expecting ExprMapping for coefficient derivatives.") + 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): + 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)) + Derivative.__init__(self, (integrand, coefficients, arguments, coefficient_derivatives)) def __str__(self): - 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]) + """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) +@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): - 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]) + """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", @@ -89,143 +202,159 @@ class VariableDerivative(Derivative): ) def __new__(cls, f, v): + """Create a new VariableDerivative.""" # Checks if not isinstance(f, Expr): - error("Expecting an Expr in VariableDerivative.") + raise ValueError("Expecting an Expr in VariableDerivative.") if not isinstance(v, (Variable, Coefficient)): - error("Expecting a Variable in VariableDerivative.") + raise ValueError("Expecting a Variable in VariableDerivative.") if v.ufl_free_indices: - error("Differentiation variable cannot have 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) + 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)) + 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." + """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 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." + """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: - error("Operand shape mismatch in Grad reconstruct.") + raise ValueError("Operand shape mismatch in Grad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: - error("Free index mismatch in Grad reconstruct.") - return Zero(self.ufl_shape, self.ufl_free_indices, - self.ufl_index_dimensions) + 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." + """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) + 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) +@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 = f.ufl_domain().topological_dimension() - return Zero(f.ufl_shape + (dim,), f.ufl_free_indices, - f.ufl_index_dimensions) + 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 = f.ufl_domain().topological_dimension() + 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." + """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: - error("Operand shape mismatch in ReferenceGrad reconstruct.") + raise ValueError("Operand shape mismatch in ReferenceGrad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: - error("Free index mismatch in ReferenceGrad reconstruct.") - return Zero(self.ufl_shape, self.ufl_free_indices, - self.ufl_index_dimensions) + 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." + """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) + 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: - error("Free indices in the divergence argument is not allowed.") + raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): @@ -234,24 +363,31 @@ def __new__(cls, f): 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) +@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: - error("Free indices in the divergence argument is not allowed.") + raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): @@ -260,58 +396,68 @@ def __new__(cls, f): 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 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." + """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: - error("Operand shape mismatch in NablaGrad reconstruct.") + raise ValueError("Operand shape mismatch in NablaGrad reconstruct.") if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: - error("Free index mismatch in NablaGrad reconstruct.") - return Zero(self.ufl_shape, self.ufl_free_indices, - self.ufl_index_dimensions) + 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: - error("Free indices in the divergence argument is not allowed.") + raise ValueError("Free indices in the divergence argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): @@ -320,13 +466,16 @@ def __new__(cls, f): 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] @@ -335,15 +484,18 @@ def __str__(self): @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,)): - error("Expecting a scalar, 2D vector or 3D vector.") + raise ValueError("Expecting a scalar, 2D vector or 3D vector.") if f.ufl_free_indices: - error("Free indices in the curl argument is not allowed.") + raise ValueError("Free indices in the curl argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): @@ -352,26 +504,31 @@ def __new__(cls, f): return CompoundDerivative.__new__(cls) def __init__(self, f): - global _curl_shapes + """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) +@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,)): - error("Expecting a scalar, 2D vector or 3D vector.") + raise ValueError("Expecting a scalar, 2D vector or 3D vector.") if f.ufl_free_indices: - error("Free indices in the curl argument is not allowed.") + raise ValueError("Free indices in the curl argument is not allowed.") # Return zero if expression is trivially constant if is_cellwise_constant(f): @@ -380,9 +537,10 @@ def __new__(cls, f): return CompoundDerivative.__new__(cls) def __init__(self, f): - global _curl_shapes + """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] diff --git a/ufl/domain.py b/ufl/domain.py index cd3d329dd..8f4ae5138 100644 --- a/ufl/domain.py +++ b/ufl/domain.py @@ -1,56 +1,53 @@ -# -*- coding: utf-8 -*- -"Types for representing a geometric domain." +"""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 -# -# Modified by Anders Logg, 2009. -# Modified by Kristian B. Oelgaard, 2009 -# Modified by Marie E. Rognes 2012 -# Modified by Cecile Daversin-Catty, 2018 import numbers -from ufl.core.ufl_type import attach_operators_from_hash_data +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.log import error -from ufl.cell import as_cell, AbstractCell, TensorProductCell -from ufl.finiteelement.tensorproductelement import TensorProductElement - +from ufl.sobolevspace import H1 # Export list for ufl.classes -__all_classes__ = ["AbstractDomain", "Mesh", "MeshView", "TensorProductMesh"] +__all_classes__ = ["AbstractDomain", "Mesh", "MeshView"] class AbstractDomain(object): - """Symbolic representation of a geometric domain with only a geometric - and topological dimension. + """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): - error("Expecting integer geometric dimension, not %s" % (geometric_dimension.__class__,)) + raise ValueError( + f"Expecting integer geometric dimension, not {geometric_dimension.__class__}" + ) if not isinstance(topological_dimension, numbers.Integral): - error("Expecting integer topological dimension, not %s" % (topological_dimension.__class__,)) + raise ValueError( + f"Expecting integer topological dimension, not {topological_dimension.__class__}" + ) if topological_dimension > geometric_dimension: - error("Topological dimension cannot be larger than 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 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 the dimension of the topology of this domain.""" return self._topological_dimension @@ -61,79 +58,81 @@ def topological_dimension(self): # AbstractDomain.__init__(self, geometric_dimension, geometric_dimension) -@attach_operators_from_hash_data @attach_ufl_id -class Mesh(AbstractDomain): +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: - error("Expecting cargo object (e.g. dolfin.Mesh) to have the same 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): - error("Expecting a coordinate element in the ufl.Mesh construct.") - # Accept a cell in place of an element for brevity Mesh(triangle) - if isinstance(coordinate_element, AbstractCell): - from ufl.finiteelement import VectorElement - cell = coordinate_element - coordinate_element = VectorElement("Lagrange", cell, 1, - dim=cell.geometric_dimension()) + 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.value_shape() - tdim = coordinate_element.cell().topological_dimension() + (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 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): - return self._ufl_coordinate_element.cell() + """Get the cell.""" + return self._ufl_coordinate_element.cell def is_piecewise_linear_simplex_domain(self): - return (self._ufl_coordinate_element.degree() == 1) and self.ufl_cell().is_simplex() + """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) + return (self.geometric_dimension(), self.topological_dimension(), "Mesh", typespecific) -@attach_operators_from_hash_data @attach_ufl_id -class MeshView(AbstractDomain): +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 @@ -141,132 +140,50 @@ def __init__(self, mesh, topological_dimension, ufl_id=None): # Derive dimensions from element coordinate_element = mesh.ufl_coordinate_element() - gdim, = coordinate_element.value_shape() - tdim = coordinate_element.cell().topological_dimension() + (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) + 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): - return ("MeshView", renumbering[self], - self._ufl_mesh._ufl_signature_data_(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) - - -@attach_operators_from_hash_data -@attach_ufl_id -class TensorProductMesh(AbstractDomain): - """Symbolic representation of a mesh.""" - - def __init__(self, meshes, ufl_id=None): - self._ufl_id = self._init_ufl_id(ufl_id) - - # TODO: Error checking of meshes - self._ufl_meshes = meshes - - # TODO: Is this what we want to do? - # Build cell from mesh cells - self._ufl_cell = TensorProductCell(*[mesh.ufl_cell() for mesh in meshes]) - - # TODO: Is this what we want to do? - # Build coordinate element from mesh coordinate elements - self._ufl_coordinate_element = TensorProductElement([mesh.ufl_coordinate_element() for mesh in meshes]) - - # Derive dimensions from meshes - gdim = sum(mesh.geometric_dimension() for mesh in meshes) - tdim = sum(mesh.topological_dimension() for mesh in meshes) - - AbstractDomain.__init__(self, tdim, gdim) - - def ufl_coordinate_element(self): - return self._ufl_coordinate_element - - def ufl_cell(self): - return self._ufl_cell - - def ufl_meshes(self): - return self._ufl_meshes - - def is_piecewise_linear_simplex_domain(self): - return False # TODO: Any cases this is True - - def __repr__(self): - r = "TensorProductMesh(%s, %s)" % (repr(self._ufl_meshes), repr(self._ufl_id)) - return r - - def __str__(self): - return "" % ( - self._ufl_id, self._ufl_meshes) - - def _ufl_hash_data_(self): - return (self._ufl_id,) + tuple(mesh._ufl_hash_data_() for mesh in self._ufl_meshes) - - def _ufl_signature_data_(self, renumbering): - return ("TensorProductMesh",) + tuple(mesh._ufl_signature_data_(renumbering) for mesh in self._ufl_meshes) - - # NB! Dropped __lt__ here, don't want users to write 'mesh1 < - # mesh2'. - def _ufl_sort_key_(self): - typespecific = (self._ufl_id, tuple(mesh._ufl_sort_key_() for mesh in self._ufl_meshes)) - return (self.geometric_dimension(), self.topological_dimension(), - "TensorProductMesh", typespecific) - - -# --- Utility conversion functions - -def affine_mesh(cell, ufl_id=None): - "Create a Mesh over a given cell type with an affine geometric parameterization." - from ufl.finiteelement import VectorElement - cell = as_cell(cell) - gdim = cell.geometric_dimension() - degree = 1 - coordinate_element = VectorElement("Lagrange", cell, degree, dim=gdim) - return Mesh(coordinate_element, ufl_id=ufl_id) - - -_default_domains = {} - - -def default_domain(cell): - """Create a singular default Mesh from a cell, always returning the - same Mesh object for the same cell. - - """ - global _default_domains - assert isinstance(cell, AbstractCell) - domain = _default_domains.get(cell) - if domain is None: - # Create one and only one affine Mesh with a negative ufl_id - # to avoid id collision - ufl_id = -(len(_default_domains) + 1) - domain = affine_mesh(cell, ufl_id=ufl_id) - _default_domains[cell] = domain - return domain + return (self.geometric_dimension(), self.topological_dimension(), "MeshView", typespecific) def as_domain(domain): @@ -274,31 +191,22 @@ def as_domain(domain): if isinstance(domain, AbstractDomain): # Modern UFL files and dolfin behaviour return domain - elif hasattr(domain, "ufl_domain"): - # If we get a dolfin.Mesh, it can provide us a corresponding - # ufl.Mesh. This would be unnecessary if dolfin.Mesh could - # subclass ufl.Mesh. + + try: + return extract_unique_domain(domain) + except AttributeError: return domain.ufl_domain() - else: - # Legacy UFL files - # TODO: Make this conversion in the relevant constructors - # closer to the user interface? - # TODO: Make this configurable to be an error from the dolfin side? - cell = as_cell(domain) - return default_domain(cell) def sort_domains(domains): - "Sort domains in a canonical ordering." + """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. + """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,)) @@ -310,69 +218,45 @@ def join_domains(domains): for domain in domains: gdims.add(domain.geometric_dimension()) if len(gdims) != 1: - error("Found domains with different geometric dimensions.") - gdim, = gdims - - # Split into legacy and modern style domains - legacy_domains = [] - modern_domains = [] - for domain in domains: - if isinstance(domain, Mesh) and domain.ufl_id() < 0: - assert domain.ufl_cargo() is None - legacy_domains.append(domain) - else: - modern_domains.append(domain) - - # Handle legacy domains checking - if legacy_domains: - if modern_domains: - error("Found both a new-style domain and a legacy default domain.\n" - "These should not be used interchangeably. To find the legacy\n" - "domain, note that it is automatically created from a cell so\n" - "look for constructors taking a cell.") - return tuple(legacy_domains) + raise ValueError("Found domains with different geometric dimensions.") - # Handle modern domains checking (assuming correct by construction) - return tuple(modern_domains) + return domains # TODO: Move these to an analysis module? + def extract_domains(expr): - "Return all domains expression is defined on." + """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)) + 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." + """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: - error("Found multiple domains, cannot return just one.") + raise ValueError("Found multiple domains, cannot return just one.") else: return None def find_geometric_dimension(expr): - "Find the geometric dimension of an expression." + """Find the geometric dimension of an expression.""" gdims = set() for t in traverse_unique_terminals(expr): - if hasattr(t, "ufl_domain"): - domain = t.ufl_domain() - if domain is not None: - gdims.add(domain.geometric_dimension()) - if hasattr(t, "ufl_element"): - element = t.ufl_element() - if element is not None: - cell = element.cell() - if cell is not None: - gdims.add(cell.geometric_dimension()) + domain = extract_unique_domain(t) + if domain is not None: + gdims.add(domain.geometric_dimension()) if len(gdims) != 1: - error("Cannot determine geometric dimension from expression.") - gdim, = gdims + raise ValueError("Cannot determine geometric dimension from expression.") + (gdim,) = gdims return gdim diff --git a/ufl/duals.py b/ufl/duals.py new file mode 100644 index 000000000..22681440b --- /dev/null +++ b/ufl/duals.py @@ -0,0 +1,27 @@ +"""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 diff --git a/ufl/equation.py b/ufl/equation.py index 3e008a277..956ae80a1 100644 --- a/ufl/equation.py +++ b/ufl/equation.py @@ -1,25 +1,24 @@ -# -*- coding: utf-8 -*- -"The Equation class, used to express equations like a == L." - +"""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 -from ufl.log import error - # Export list for ufl.classes __all_classes__ = ["Equation"] class Equation(object): - """This class is used to represent equations expressed by the "==" + """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.""" + Form objects. + """ def __init__(self, lhs, rhs): - "Create equation lhs == rhs" + """Create equation lhs == rhs.""" self.lhs = lhs self.rhs = rhs @@ -41,18 +40,18 @@ def __bool__(self): elif hasattr(self.rhs, "equals"): return self.rhs.equals(self.lhs) else: - error("Either lhs or rhs of Equation must implement self.equals(other).") + 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 \ - bool(self.lhs == other.lhs) and \ - bool(self.rhs == other.rhs) + """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): - r = "Equation(%s, %s)" % (repr(self.lhs), repr(self.rhs)) - return r + """Representation.""" + return f"Equation({self.lhs!r}, {self.rhs!r})" diff --git a/ufl/exprcontainers.py b/ufl/exprcontainers.py index 119321b01..8da36299c 100644 --- a/ufl/exprcontainers.py +++ b/ufl/exprcontainers.py @@ -1,76 +1,91 @@ -# -*- coding: utf-8 -*- """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.log import error +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." + """List 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(i, Expr) for i in operands): - error("Expecting Expr in ExprList.") + # 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): - error("A non-tensor type has no ufl_shape.") + """Get the UFL shape.""" + raise ValueError("A non-tensor type has no ufl_shape.") @property def ufl_free_indices(self): - error("A non-tensor type has no ufl_free_indices.") + """Get the UFL free indices.""" + raise ValueError("A non-tensor type has no ufl_free_indices.") def free_indices(self): - error("A non-tensor type has no free_indices.") + """Get the free indices.""" + raise ValueError("A non-tensor type has no free_indices.") @property def ufl_index_dimensions(self): - error("A non-tensor type has no ufl_index_dimensions.") + """Get the UFL index dimensions.""" + raise ValueError("A non-tensor type has no ufl_index_dimensions.") def index_dimensions(self): - error("A non-tensor type has no index_dimensions.") + """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." + """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): - error("Expecting Expr in ExprMapping.") + 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: @@ -78,36 +93,34 @@ def ufl_domains(self): else: return [] - # def __getitem__(self, key): - # return self.ufl_operands[key] - - # def __len__(self): - # return len(self.ufl_operands) // 2 - - # def __iter__(self): - # return iter(self.ufl_operands[::2]) - 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): - error("A non-tensor type has no ufl_shape.") + """Get the UFL shape.""" + raise ValueError("A non-tensor type has no ufl_shape.") @property def ufl_free_indices(self): - error("A non-tensor type has no ufl_free_indices.") + """Get the UFL free indices.""" + raise ValueError("A non-tensor type has no ufl_free_indices.") def free_indices(self): - error("A non-tensor type has no free_indices.") + """Get the free indices.""" + raise ValueError("A non-tensor type has no free_indices.") @property def ufl_index_dimensions(self): - error("A non-tensor type has no ufl_index_dimensions.") + """Get the UFL index dimensions.""" + raise ValueError("A non-tensor type has no ufl_index_dimensions.") def index_dimensions(self): - error("A non-tensor type has no index_dimensions.") + """Get the index dimensions.""" + raise ValueError("A non-tensor type has no index_dimensions.") diff --git a/ufl/exprequals.py b/ufl/exprequals.py index 653e39fe4..cf0359d31 100644 --- a/ufl/exprequals.py +++ b/ufl/exprequals.py @@ -1,117 +1,22 @@ -# -*- coding: utf-8 -*- +"""Expr equals.""" from collections import defaultdict -from ufl.core.expr import Expr -from ufl.log import error - hash_total = defaultdict(int) hash_collisions = defaultdict(int) hash_equals = defaultdict(int) hash_notequals = defaultdict(int) -def print_collisions(): - - keys = sorted(hash_total.keys(), key=lambda x: (hash_collisions[x], x)) - - print("Collision statistics ({0} keys):".format(len(keys))) - print("[key: equals; notequals; collisions]") - n = max(len(str(k)) for k in keys) - fmt = ("%%%ds" % n) + ": \t %6d (%3d%%); %6d (%3d%%); %6d (%3d%%) col; tot %d" - for k in keys: - co = hash_collisions[k] - eq = hash_equals[k] - ne = hash_notequals[k] - tot = hash_total[k] - sn, on = k - # Skip those that are all not equal - if sn != on and ne == tot: - continue - print(fmt % (k, eq, int(100.0 * eq / tot), - ne, int(100.0 * ne / tot), - co, int(100.0 * co / tot), - tot)) - - -def measure_collisions(equals_func): - def equals_func_with_collision_measuring(self, other): - # Call equals - equal = equals_func(self, other) - - # Get properties - st = type(self) - ot = type(other) - sn = st.__name__ - on = ot.__name__ - sh = hash(self) - oh = hash(other) - key = (sn, on) - - # If hashes are the same but objects are not equal, we have a - # collision - hash_total[key] += 1 - if sh == oh and not equal: - hash_collisions[key] += 1 - elif sh != oh and equal: - error("Equal objects must always have the same hash! Objects are:\n{0}\n{1}".format(self, other)) - elif sh == oh and equal: - hash_equals[key] += 1 - elif sh != oh and not equal: - hash_notequals[key] += 1 - - return equal - return equals_func_with_collision_measuring - - -# @measure_collisions -def recursive_expr_equals(self, other): # Much faster than the more complex algorithms above! - """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.""" - - # To handle expr == int/float - if not isinstance(other, Expr): - return False - - # Fast cutoff for common case - if self._ufl_typecode_ != other._ufl_typecode_: - return False - - # Compare hashes, will cutoff more or less all nonequal types - if hash(self) != hash(other): - return False - - # Large objects are costly to compare with themselves - if self is other: - return True - - # Terminals - if self._ufl_is_terminal_: - # Compare terminals with custom == to capture subclass - # overloading of __eq__ - return self == other - - # --- Operators, most likely equal, below here is the costly part - # --- if it recurses through a large tree! --- - - # Recurse manually to call expr_equals directly without the class - # EQ overhead! - equal = all(recursive_expr_equals(a, b) for (a, b) in zip(self.ufl_operands, - other.ufl_operands)) - - return equal - - -# @measure_collisions -def nonrecursive_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.""" +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) != type(other) or hash(self) != hash(other): + if type(self) is not type(other) or hash(self) != hash(other): return False # Large objects are costly to compare with themselves @@ -148,7 +53,3 @@ def nonrecursive_expr_equals(self, other): # Eagerly DAGify to reduce the size of the tree. self.ufl_operands = other.ufl_operands return True - - -# expr_equals = recursive_expr_equals -expr_equals = nonrecursive_expr_equals diff --git a/ufl/exproperators.py b/ufl/exproperators.py index 054cead59..ff532e843 100644 --- a/ufl/exproperators.py +++ b/ufl/exproperators.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- -"""This module attaches special functions to Expr. -This way we avoid circular dependencies between e.g. -Sum and its superclass Expr.""" +"""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) @@ -11,47 +12,43 @@ # # Modified by Massimiliano Leoni, 2016. -from itertools import chain import numbers -from ufl.log import error -from ufl.utils.stacks import StackDict -from ufl.core.expr import Expr +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.algebra import Sum, Product, Division, Power, Abs -from ufl.tensoralgebra import Transposed, Inner -from ufl.core.multiindex import MultiIndex, Index, FixedIndex, IndexBase, indices -from ufl.indexed import Indexed -from ufl.indexsum import IndexSum -from ufl.tensors import as_tensor, ComponentTensor -from ufl.restriction import PositiveRestricted, NegativeRestricted +from ufl.core.expr import Expr +from ufl.core.multiindex import Index, MultiIndex, indices from ufl.differentiation import Grad -from ufl.index_combination_utils import create_slice_indices, merge_overlapping_indices - 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 --- -from ufl.conditional import LE, GE, LT, GT - def _le(left, right): - "UFL operator: A boolean expresion (left <= right) for use with conditional." + """A boolean expresion (left <= right) for use with conditional.""" return LE(left, right) def _ge(left, right): - "UFL operator: A boolean expresion (left >= right) for use with conditional." + """A boolean expresion (left >= right) for use with conditional.""" return GE(left, right) def _lt(left, right): - "UFL operator: A boolean expresion (left < right) for use with conditional." + """A boolean expresion (left < right) for use with conditional.""" return LT(left, right) def _gt(left, right): - "UFL operator: A boolean expresion (left > right) for use with conditional." + """A boolean expresion (left > right) for use with conditional.""" return GT(left, right) @@ -86,11 +83,15 @@ def _ne(self, other): def _as_tensor(self, indices): - "UFL operator: A^indices := as_tensor(A, indices)." + """A^indices := as_tensor(A, indices).""" if not isinstance(indices, tuple): - error("Expecting a tuple of Index objects to A^indices := as_tensor(A, indices).") + 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): - error("Expecting a tuple of Index objects to A^indices := as_tensor(A, indices).") + raise ValueError( + "Expecting a tuple of Index objects to A^indices := as_tensor(A, indices)." + ) return as_tensor(self, indices) @@ -99,7 +100,9 @@ def _as_tensor(self, indices): # --- 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 @@ -137,7 +140,7 @@ def _mult(a, b): elif r1 == 2 and r2 in (1, 2): # Matrix-matrix or matrix-vector if ri: - error("Not expecting repeated indices in non-scalar product.") + 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): @@ -153,7 +156,7 @@ def _mult(a, b): ti = ai + bi else: - error("Invalid ranks {0} and {1} in product.".format(r1, r2)) + raise ValueError(f"Invalid ranks {r1} and {r2} in product.") # TODO: I think applying as_tensor after index sums results in # cleaner expression graphs. @@ -176,6 +179,7 @@ def _mult(a, b): def _mul(self, o): + """Multiply.""" if not isinstance(o, _valid_types): return NotImplemented o = as_ufl(o) @@ -186,6 +190,7 @@ def _mul(self, o): def _rmul(self, o): + """Multiply.""" if not isinstance(o, _valid_types): return NotImplemented o = as_ufl(o) @@ -196,6 +201,7 @@ def _rmul(self, o): def _add(self, o): + """Add.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(self, o) @@ -205,6 +211,7 @@ def _add(self, o): def _radd(self, o): + """Add.""" if not isinstance(o, _valid_types): return NotImplemented if isinstance(o, numbers.Number) and o == 0: @@ -218,6 +225,7 @@ def _radd(self, o): def _sub(self, o): + """Subtract.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(self, -o) @@ -227,6 +235,7 @@ def _sub(self, o): def _rsub(self, o): + """Subtract.""" if not isinstance(o, _valid_types): return NotImplemented return Sum(o, -self) @@ -236,6 +245,7 @@ def _rsub(self, o): def _div(self, o): + """Divide.""" if not isinstance(o, _valid_types): return NotImplemented sh = self.ufl_shape @@ -251,6 +261,7 @@ def _div(self, o): def _rdiv(self, o): + """Divide.""" if not isinstance(o, _valid_types): return NotImplemented return Division(o, self) @@ -261,6 +272,7 @@ def _rdiv(self, o): def _pow(self, o): + """Raise to a power.""" if not isinstance(o, _valid_types): return NotImplemented if o == 2 and self.ufl_shape: @@ -272,6 +284,7 @@ def _pow(self, o): def _rpow(self, o): + """Raise to a power.""" if not isinstance(o, _valid_types): return NotImplemented return Power(o, self) @@ -282,6 +295,7 @@ def _rpow(self, o): # TODO: Add Negated class for this? Might simplify reductions in Add. def _neg(self): + """Negate.""" return -1 * self @@ -289,6 +303,7 @@ def _neg(self): def _abs(self): + """Absolute value.""" return Abs(self) @@ -297,20 +312,25 @@ def _abs(self): # --- Extend Expr with restiction operators a("+"), a("-") --- + def _restrict(self, side): + """Restrict.""" if side == "+": return PositiveRestricted(self) if side == "-": return NegativeRestricted(self) - error("Invalid side '%s' in restriction operator." % (side,)) + raise ValueError(f"Invalid side '{side}' in restriction operator.") def _eval(self, coord, mapping=None, component=()): - # Evaluate expression at this particular coordinate, with provided - # values for other terminals in mapping + """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 @@ -321,10 +341,10 @@ def _eval(self, coord, mapping=None, component=()): def _call(self, arg, mapping=None, component=()): - # Taking the restriction or evaluating depending on argument + """Take the restriction or evaluate depending on argument.""" if arg in ("+", "-"): if mapping is not None: - error("Not expecting a mapping when taking restriction.") + raise ValueError("Not expecting a mapping when taking restriction.") return _restrict(self, arg) else: return _eval(self, arg, mapping, component) @@ -335,9 +355,13 @@ def _call(self, arg, mapping=None, component=()): # --- 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.""" + """Transpose a rank-2 tensor expression. + + For more general transpose operations of higher order tensor + expressions, use indexing and Tensor. + """ return Transposed(self) @@ -346,88 +370,9 @@ def _transpose(self): # --- Extend Expr with indexing operator a[i] --- -def analyse_key(ii, rank): - """Takes something the user might input as an index tuple - inside [], which could include complete slices (:) and - ellipsis (...), and returns tuples of actual UFL index objects. - - The return value is a tuple (indices, axis_indices), - each being a tuple of IndexBase instances. - - The return value 'indices' corresponds to all - input objects of these types: - - Index - - FixedIndex - - int => Wrapped in FixedIndex - - The return value 'axis_indices' corresponds to all - input objects of these types: - - Complete slice (:) => Replaced by a single new index - - Ellipsis (...) => Replaced by multiple new indices - """ - # Wrap in tuple - if not isinstance(ii, (tuple, MultiIndex)): - ii = (ii,) - else: - # Flatten nested tuples, happens with f[...,ii] where ii is a - # tuple of indices - jj = [] - for j in ii: - if isinstance(j, (tuple, MultiIndex)): - jj.extend(j) - else: - jj.append(j) - ii = tuple(jj) - - # Convert all indices to Index or FixedIndex objects. If there is - # an ellipsis, split the indices into before and after. - axis_indices = set() - pre = [] - post = [] - indexlist = pre - for i in ii: - if i == Ellipsis: - # Switch from pre to post list when an ellipsis is - # encountered - if indexlist is not pre: - error("Found duplicate ellipsis.") - indexlist = post - else: - # Convert index to a proper type - if isinstance(i, numbers.Integral): - idx = FixedIndex(i) - elif isinstance(i, IndexBase): - idx = i - elif isinstance(i, slice): - if i == slice(None): - idx = Index() - axis_indices.add(idx) - else: - # TODO: Use ListTensor to support partial slices? - error("Partial slices not implemented, only complete slices like [:]") - else: - error("Can't convert this object to index: %s" % (i,)) - - # Store index in pre or post list - indexlist.append(idx) - - # Handle ellipsis as a number of complete slices, that is create a - # number of new axis indices - num_axis = rank - len(pre) - len(post) - if indexlist is post: - ellipsis_indices = indices(num_axis) - axis_indices.update(ellipsis_indices) - else: - ellipsis_indices = () - - # Construct final tuples to return - all_indices = tuple(chain(pre, ellipsis_indices, post)) - axis_indices = tuple(i for i in all_indices if i in axis_indices) - return all_indices, axis_indices - def _getitem(self, component): - + """Get an item.""" # Treat component consistently as tuple below if not isinstance(component, tuple): component = (component,) @@ -435,12 +380,16 @@ def _getitem(self, component): shape = self.ufl_shape # Analyse slices (:) and Ellipsis (...) - all_indices, slice_indices, repeated_indices = create_slice_indices(component, shape, self.ufl_free_indices) + 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): - error("Invalid number of indices {0} for expression of rank {1}.".format(len(all_indices), len(shape))) + 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 @@ -487,8 +436,9 @@ def _getitem(self, component): # --- Extend Expr with spatial differentiation operator a.dx(i) --- + def _dx(self, *ii): - "Return the partial derivative with respect to spatial variable number *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): diff --git a/ufl/finiteelement.py b/ufl/finiteelement.py new file mode 100644 index 000000000..8dbc76ed1 --- /dev/null +++ b/ufl/finiteelement.py @@ -0,0 +1,372 @@ +"""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"", + ) diff --git a/ufl/finiteelement/__init__.py b/ufl/finiteelement/__init__.py deleted file mode 100644 index f7e8b7836..000000000 --- a/ufl/finiteelement/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# flake8: noqa -"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 Andrew T. T. McRae 2014 -# Modified by Lawrence Mitchell 2014 - -from ufl.finiteelement.finiteelementbase import FiniteElementBase -from ufl.finiteelement.finiteelement import FiniteElement -from ufl.finiteelement.mixedelement import MixedElement -from ufl.finiteelement.mixedelement import VectorElement -from ufl.finiteelement.mixedelement import TensorElement -from ufl.finiteelement.enrichedelement import EnrichedElement -from ufl.finiteelement.enrichedelement import NodalEnrichedElement -from ufl.finiteelement.restrictedelement import RestrictedElement -from ufl.finiteelement.tensorproductelement import TensorProductElement -from ufl.finiteelement.hdivcurl import HDivElement, HCurlElement, WithMapping -from ufl.finiteelement.brokenelement import BrokenElement -from ufl.finiteelement.facetelement import FacetElement -from ufl.finiteelement.interiorelement import InteriorElement - -# Export list for ufl.classes -__all_classes__ = [ - "FiniteElementBase", - "FiniteElement", - "MixedElement", - "VectorElement", - "TensorElement", - "EnrichedElement", - "NodalEnrichedElement", - "RestrictedElement", - "TensorProductElement", - "HDivElement", - "HCurlElement", - "BrokenElement", - "FacetElement", - "InteriorElement", - "WithMapping" - ] diff --git a/ufl/finiteelement/brokenelement.py b/ufl/finiteelement/brokenelement.py deleted file mode 100644 index 98ca1812a..000000000 --- a/ufl/finiteelement/brokenelement.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014 Andrew T. T. McRae -# -# 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.finiteelement.finiteelementbase import FiniteElementBase - - -class BrokenElement(FiniteElementBase): - """The discontinuous version of an existing Finite Element space.""" - def __init__(self, element): - self._element = element - - family = "BrokenElement" - cell = element.cell() - degree = element.degree() - quad_scheme = element.quadrature_scheme() - value_shape = element.value_shape() - reference_value_shape = element.reference_value_shape() - FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, reference_value_shape) - - def __repr__(self): - return f"BrokenElement({repr(self._element)})" - - def mapping(self): - return self._element.mapping() - - def reconstruct(self, **kwargs): - return BrokenElement(self._element.reconstruct(**kwargs)) - - def __str__(self): - return f"BrokenElement({repr(self._element)})" - - def shortstr(self): - """Format as string for pretty printing.""" - return f"BrokenElement({repr(self._element)})" diff --git a/ufl/finiteelement/elementlist.py b/ufl/finiteelement/elementlist.py deleted file mode 100644 index e45fc7813..000000000 --- a/ufl/finiteelement/elementlist.py +++ /dev/null @@ -1,482 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module provides an extensive list of predefined finite element -families. Users or, more likely, form compilers, may register new -elements by calling the function register_element.""" - -# 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 Marie E. Rognes , 2010 -# Modified by Lizao Li , 2015, 2016 -# Modified by Massimiliano Leoni, 2016 - -from numpy import asarray - -from ufl.log import warning, error -from ufl.sobolevspace import L2, H1, H2, HDiv, HCurl, HEin, HDivDiv, HInf -from ufl.utils.formatting import istr -from ufl.cell import Cell, TensorProductCell - - -# List of valid elements -ufl_elements = {} - -# Aliases: aliases[name] (...) -> (standard_name, ...) -aliases = {} - - -# Function for registering new elements -def register_element(family, short_name, value_rank, sobolev_space, mapping, - degree_range, cellnames): - "Register new finite element family." - if family in ufl_elements: - error('Finite element \"%s\" has already been registered.' % family) - ufl_elements[family] = (family, short_name, value_rank, sobolev_space, - mapping, degree_range, cellnames) - ufl_elements[short_name] = (family, short_name, value_rank, sobolev_space, - mapping, degree_range, cellnames) - - -def register_element2(family, value_rank, sobolev_space, mapping, - degree_range, cellnames): - "Register new finite element family." - if family in ufl_elements: - error('Finite element \"%s\" has already been registered.' % family) - ufl_elements[family] = (family, family, value_rank, sobolev_space, - mapping, degree_range, cellnames) - - -def register_alias(alias, to): - aliases[alias] = to - - -def show_elements(): - "Shows all registered elements." - print("Showing all registered elements:") - print("================================") - shown = set() - for k in sorted(ufl_elements.keys()): - data = ufl_elements[k] - if data in shown: - continue - shown.add(data) - (family, short_name, value_rank, sobolev_space, mapping, degree_range, cellnames) = data - print("Finite element family: '%s', '%s'" % (family, short_name)) - print("Sobolev space: %s" % (sobolev_space,)) - print("Mapping: %s" % (mapping,)) - print("Degree range: %s" % (degree_range,)) - print("Value rank: %s" % (value_rank,)) - print("Defined on cellnames: %s" % (cellnames,)) - print() - - -# FIXME: Consider cleanup of element names. Use notation from periodic -# table as the main, keep old names as compatibility aliases. - -# NOTE: Any element with polynomial degree 0 will be considered L2, -# independent of the space passed to register_element. - -# NOTE: The mapping of the element basis functions -# from reference to physical representation is -# chosen based on the sobolev space: -# HDiv = contravariant Piola, -# HCurl = covariant Piola, -# H1/L2 = no mapping. - -# TODO: If determining mapping from sobolev_space isn't sufficient in -# the future, add mapping name as another element property. - -# Cell groups -simplices = ("interval", "triangle", "tetrahedron") -cubes = ("interval", "quadrilateral", "hexahedron") -any_cell = (None, - "vertex", "interval", - "triangle", "tetrahedron", "prism", - "pyramid", "quadrilateral", "hexahedron") - -# Elements in the periodic table # TODO: Register these as aliases of -# periodic table element description instead of the other way around -register_element("Lagrange", "CG", 0, H1, "identity", (1, None), - any_cell) # "P" -register_element("Brezzi-Douglas-Marini", "BDM", 1, HDiv, - "contravariant Piola", (1, None), simplices[1:]) # "BDMF" (2d), "N2F" (3d) -register_element("Discontinuous Lagrange", "DG", 0, L2, "identity", (0, None), - any_cell) # "DP" -register_element("Discontinuous Taylor", "TDG", 0, L2, "identity", (0, None), simplices) -register_element("Nedelec 1st kind H(curl)", "N1curl", 1, HCurl, - "covariant Piola", (1, None), simplices[1:]) # "RTE" (2d), "N1E" (3d) -register_element("Nedelec 2nd kind H(curl)", "N2curl", 1, HCurl, - "covariant Piola", (1, None), simplices[1:]) # "BDME" (2d), "N2E" (3d) -register_element("Raviart-Thomas", "RT", 1, HDiv, "contravariant Piola", - (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) - -# Elements not in the periodic table -register_element("Argyris", "ARG", 0, H2, "identity", (5, 5), ("triangle",)) -register_element("Bell", "BELL", 0, H2, "identity", (5, 5), ("triangle",)) -register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, - "contravariant Piola", (1, None), simplices[1:]) -register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), - simplices[1:]) -# TODO: Implement generic Tear operator for elements instead of this: -register_element("Discontinuous Raviart-Thomas", "DRT", 1, L2, - "contravariant Piola", (1, None), simplices[1:]) -register_element("Hermite", "HER", 0, H1, "identity", (3, 3), simplices) -register_element("Kong-Mulder-Veldhuizen", "KMV", 0, H1, "identity", (1, None), - simplices[1:]) -register_element("Mardal-Tai-Winther", "MTW", 1, H1, "contravariant Piola", (3, 3), - ("triangle",)) -register_element("Morley", "MOR", 0, H2, "identity", (2, 2), ("triangle",)) - -# Special elements -register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), - any_cell) -register_element("Bubble", "B", 0, H1, "identity", (2, None), simplices) -register_element("FacetBubble", "FB", 0, H1, "identity", (2, None), simplices) -register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), - any_cell) -register_element("Real", "R", 0, HInf, "identity", (0, 0), - any_cell + ("TensorProductCell",)) -register_element("Undefined", "U", 0, L2, "identity", (0, None), any_cell) -register_element("Radau", "Rad", 0, L2, "identity", (0, None), ("interval",)) -register_element("Regge", "Regge", 2, HEin, "double covariant Piola", - (0, None), simplices[1:]) -register_element("HDiv Trace", "HDivT", 0, L2, "identity", (0, None), any_cell) -register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, - "double contravariant Piola", (0, None), ("triangle",)) -register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDivDiv, - "double contravariant Piola", (2, 2), ("triangle", "tetrahedron")) -register_element("Conforming Arnold-Winther", "AWc", 2, HDivDiv, - "double contravariant Piola", (3, None), ("triangle", "tetrahedron")) -# Spectral elements. -register_element("Gauss-Legendre", "GL", 0, L2, "identity", (0, None), - ("interval",)) -register_element("Gauss-Lobatto-Legendre", "GLL", 0, H1, "identity", (1, None), - ("interval",)) -register_alias("Lobatto", - lambda family, dim, order, degree: ("Gauss-Lobatto-Legendre", order)) -register_alias("Lob", - lambda family, dim, order, degree: ("Gauss-Lobatto-Legendre", order)) - -register_element2("Bernstein", 0, H1, "identity", (1, None), simplices) - - -# Let Nedelec H(div) elements be aliases to BDMs/RTs -register_alias("Nedelec 1st kind H(div)", - lambda family, dim, order, degree: ("Raviart-Thomas", order)) -register_alias("N1div", - lambda family, dim, order, degree: ("Raviart-Thomas", order)) - -register_alias("Nedelec 2nd kind H(div)", - lambda family, dim, order, degree: ("Brezzi-Douglas-Marini", - order)) -register_alias("N2div", - lambda family, dim, order, degree: ("Brezzi-Douglas-Marini", - order)) - -# Let Discontinuous Lagrange Trace element be alias to HDiv Trace -register_alias("Discontinuous Lagrange Trace", - lambda family, dim, order, degree: ("HDiv Trace", order)) -register_alias("DGT", - lambda family, dim, order, degree: ("HDiv Trace", order)) - -# New elements introduced for the periodic table 2014 -register_element2("Q", 0, H1, "identity", (1, None), cubes) -register_element2("DQ", 0, L2, "identity", (0, None), cubes) -register_element2("RTCE", 1, HCurl, "covariant Piola", (1, None), - ("quadrilateral",)) -register_element2("RTCF", 1, HDiv, "contravariant Piola", (1, None), - ("quadrilateral",)) -register_element2("NCE", 1, HCurl, "covariant Piola", (1, None), - ("hexahedron",)) -register_element2("NCF", 1, HDiv, "contravariant Piola", (1, None), - ("hexahedron",)) - -register_element2("S", 0, H1, "identity", (1, None), cubes) -register_element2("DPC", 0, L2, "identity", (0, None), cubes) -register_element2("BDMCE", 1, HCurl, "covariant Piola", (1, None), - ("quadrilateral",)) -register_element2("BDMCF", 1, HDiv, "contravariant Piola", (1, None), - ("quadrilateral",)) -register_element2("AAE", 1, HCurl, "covariant Piola", (1, None), - ("hexahedron",)) -register_element2("AAF", 1, HDiv, "contravariant Piola", (1, None), - ("hexahedron",)) - -# New aliases introduced for the periodic table 2014 -register_alias("P", lambda family, dim, order, degree: ("Lagrange", order)) -register_alias("DP", lambda family, dim, order, - degree: ("Discontinuous Lagrange", order)) -register_alias("RTE", lambda family, dim, order, - degree: ("Nedelec 1st kind H(curl)", order)) -register_alias("RTF", lambda family, dim, order, - degree: ("Raviart-Thomas", order)) -register_alias("N1E", lambda family, dim, order, - degree: ("Nedelec 1st kind H(curl)", order)) -register_alias("N1F", lambda family, dim, order, degree: ("Raviart-Thomas", - order)) - -register_alias("BDME", lambda family, dim, order, - degree: ("Nedelec 2nd kind H(curl)", order)) -register_alias("BDMF", lambda family, dim, order, - degree: ("Brezzi-Douglas-Marini", order)) -register_alias("N2E", lambda family, dim, order, - degree: ("Nedelec 2nd kind H(curl)", order)) -register_alias("N2F", lambda family, dim, order, - degree: ("Brezzi-Douglas-Marini", order)) - -# discontinuous elements using l2 pullbacks -register_element2("DPC L2", 0, L2, "L2 Piola", (1, None), cubes) -register_element2("DQ L2", 0, L2, "L2 Piola", (0, None), cubes) -register_element("Gauss-Legendre L2", "GL L2", 0, L2, "L2 Piola", (0, None), - ("interval",)) -register_element("Discontinuous Lagrange L2", "DG L2", 0, L2, "L2 Piola", (0, None), - any_cell) # "DP" - -register_alias("DP L2", lambda family, dim, order, - degree: ("Discontinuous Lagrange L2", order)) - -register_alias("P- Lambda L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) -register_alias("P Lambda L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) -register_alias("Q- Lambda L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) -register_alias("S Lambda L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) - -register_alias("P- L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) -register_alias("Q- L2", lambda family, dim, order, - degree: feec_element_l2(family, dim, order, degree)) - -# mimetic spectral elements - primal and dual complexs -register_element("Extended-Gauss-Legendre", "EGL", 0, H1, "identity", (2, None), - ("interval",)) -register_element("Extended-Gauss-Legendre Edge", "EGL-Edge", 0, L2, "identity", (1, None), - ("interval",)) -register_element("Extended-Gauss-Legendre Edge L2", "EGL-Edge L2", 0, L2, "L2 Piola", (1, None), - ("interval",)) -register_element("Gauss-Lobatto-Legendre Edge", "GLL-Edge", 0, L2, "identity", (0, None), - ("interval",)) -register_element("Gauss-Lobatto-Legendre Edge L2", "GLL-Edge L2", 0, L2, "L2 Piola", (0, None), - ("interval",)) - -# directly-defined serendipity elements ala Arbogast -# currently the theory is only really worked out for quads. -register_element("Direct Serendipity", "Sdirect", 0, H1, "physical", (1, None), - ("quadrilateral",)) -register_element("Direct Serendipity Full H(div)", "Sdirect H(div)", 1, HDiv, "physical", (1, None), - ("quadrilateral",)) -register_element("Direct Serendipity Reduced H(div)", "Sdirect H(div) red", 1, HDiv, "physical", (1, None), - ("quadrilateral",)) - - -# NOTE- the edge elements for primal mimetic spectral elements are accessed by using variant='mse' in the appropriate places - -def feec_element(family, n, r, k): - """Finite element exterior calculus notation - n = topological dimension of domain - r = polynomial order - k = form_degree""" - - # Note: We always map to edge elements in 2D, don't know how to - # differentiate otherwise? - - # Mapping from (feec name, domain dimension, form degree) to - # (family name, polynomial order) - _feec_elements = { - "P- Lambda": ( - (("P", r), ("DP", r - 1)), - (("P", r), ("RTE", r), ("DP", r - 1)), - (("P", r), ("N1E", r), ("N1F", r), ("DP", r - 1)), - ), - "P Lambda": ( - (("P", r), ("DP", r)), - (("P", r), ("BDME", r), ("DP", r)), - (("P", r), ("N2E", r), ("N2F", r), ("DP", r)), - ), - "Q- Lambda": ( - (("Q", r), ("DQ", r - 1)), - (("Q", r), ("RTCE", r), ("DQ", r - 1)), - (("Q", r), ("NCE", r), ("NCF", r), ("DQ", r - 1)), - ), - "S Lambda": ( - (("S", r), ("DPC", r)), - (("S", r), ("BDMCE", r), ("DPC", r)), - (("S", r), ("AAE", r), ("AAF", r), ("DPC", r)), - ), - } - - # New notation, old verbose notation (including "Lambda") might be - # removed - _feec_elements["P-"] = _feec_elements["P- Lambda"] - _feec_elements["P"] = _feec_elements["P Lambda"] - _feec_elements["Q-"] = _feec_elements["Q- Lambda"] - _feec_elements["S"] = _feec_elements["S Lambda"] - - family, r = _feec_elements[family][n - 1][k] - - return family, r - - -def feec_element_l2(family, n, r, k): - """Finite element exterior calculus notation - n = topological dimension of domain - r = polynomial order - k = form_degree""" - - # Note: We always map to edge elements in 2D, don't know how to - # differentiate otherwise? - - # Mapping from (feec name, domain dimension, form degree) to - # (family name, polynomial order) - _feec_elements = { - "P- Lambda L2": ( - (("P", r), ("DP L2", r - 1)), - (("P", r), ("RTE", r), ("DP L2", r - 1)), - (("P", r), ("N1E", r), ("N1F", r), ("DP L2", r - 1)), - ), - "P Lambda L2": ( - (("P", r), ("DP L2", r)), - (("P", r), ("BDME", r), ("DP L2", r)), - (("P", r), ("N2E", r), ("N2F", r), ("DP L2", r)), - ), - "Q- Lambda L2": ( - (("Q", r), ("DQ L2", r - 1)), - (("Q", r), ("RTCE", r), ("DQ L2", r - 1)), - (("Q", r), ("NCE", r), ("NCF", r), ("DQ L2", r - 1)), - ), - "S Lambda L2": ( - (("S", r), ("DPC L2", r)), - (("S", r), ("BDMCE", r), ("DPC L2", r)), - (("S", r), ("AAE", r), ("AAF", r), ("DPC L2", r)), - ), - } - - # New notation, old verbose notation (including "Lambda") might be - # removed - _feec_elements["P- L2"] = _feec_elements["P- Lambda L2"] - _feec_elements["P L2"] = _feec_elements["P Lambda L2"] - _feec_elements["Q- L2"] = _feec_elements["Q- Lambda L2"] - _feec_elements["S L2"] = _feec_elements["S Lambda L2"] - - family, r = _feec_elements[family][n - 1][k] - - return family, r - - -# General FEEC notation, old verbose (can be removed) -register_alias("P- Lambda", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) -register_alias("P Lambda", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) -register_alias("Q- Lambda", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) -register_alias("S Lambda", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) - -# General FEEC notation, new compact notation -register_alias("P-", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) -register_alias("Q-", lambda family, dim, order, - degree: feec_element(family, dim, order, degree)) - - -def canonical_element_description(family, cell, order, form_degree): - """Given basic element information, return corresponding element information on canonical form. - - Input: family, cell, (polynomial) order, form_degree - Output: family (canonical), short_name (for printing), order, value shape, - reference value shape, sobolev_space. - - This is used by the FiniteElement constructor to ved input - data against the element list and aliases defined in ufl. - """ - - # Get domain dimensions - if cell is not None: - tdim = cell.topological_dimension() - gdim = cell.geometric_dimension() - if isinstance(cell, Cell): - cellname = cell.cellname() - else: - cellname = None - else: - tdim = None - gdim = None - cellname = None - - # Catch general FEEC notation "P" and "S" - if form_degree is not None and family in ("P", "S"): - family, order = feec_element(family, tdim, order, form_degree) - - if form_degree is not None and family in ("P L2", "S L2"): - family, order = feec_element_l2(family, tdim, order, form_degree) - - # Check whether this family is an alias for something else - while family in aliases: - if tdim is None: - error("Need dimension to handle element aliases.") - (family, order) = aliases[family](family, tdim, order, form_degree) - - # Check that the element family exists - if family not in ufl_elements: - error('Unknown finite element "%s".' % family) - - # Check that element data is valid (and also get common family - # name) - (family, short_name, value_rank, sobolev_space, mapping, krange, cellnames) = ufl_elements[family] - - # Accept CG/DG on all kind of cells, but use Q/DQ on "product" cells - if cellname in set(cubes) - set(simplices) or isinstance(cell, TensorProductCell): - if family == "Lagrange": - family = "Q" - elif family == "Discontinuous Lagrange": - if order >= 1: - warning("Discontinuous Lagrange element requested on %s, creating DQ element." % cell.cellname()) - family = "DQ" - elif family == "Discontinuous Lagrange L2": - if order >= 1: - warning("Discontinuous Lagrange L2 element requested on %s, creating DQ L2 element." % cell.cellname()) - family = "DQ L2" - - # Validate cellname if a valid cell is specified - if not (cellname is None or cellname in cellnames): - error('Cellname "%s" invalid for "%s" finite element.' % (cellname, family)) - - # Validate order if specified - if order is not None: - if krange is None: - error('Order "%s" invalid for "%s" finite element, ' - 'should be None.' % (order, family)) - kmin, kmax = krange - if not (kmin is None or (asarray(order) >= kmin).all()): - error('Order "%s" invalid for "%s" finite element.' % - (order, family)) - if not (kmax is None or (asarray(order) <= kmax).all()): - error('Order "%s" invalid for "%s" finite element.' % - (istr(order), family)) - - if value_rank == 2: - # Tensor valued fundamental elements in HEin have this shape - if gdim is None or tdim is None: - error("Cannot infer shape of element without topological and geometric dimensions.") - reference_value_shape = (tdim, tdim) - value_shape = (gdim, gdim) - elif value_rank == 1: - # Vector valued fundamental elements in HDiv and HCurl have a shape - if gdim is None or tdim is None: - error("Cannot infer shape of element without topological and geometric dimensions.") - reference_value_shape = (tdim,) - value_shape = (gdim,) - elif value_rank == 0: - # All other elements are scalar values - reference_value_shape = () - value_shape = () - else: - error("Invalid value rank %d." % value_rank) - - return family, short_name, order, value_shape, reference_value_shape, sobolev_space, mapping diff --git a/ufl/finiteelement/enrichedelement.py b/ufl/finiteelement/enrichedelement.py deleted file mode 100644 index 221fc0648..000000000 --- a/ufl/finiteelement/enrichedelement.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -"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 - -from ufl.log import error -from ufl.finiteelement.finiteelementbase import FiniteElementBase - - -class EnrichedElementBase(FiniteElementBase): - """The vector sum of several finite element spaces: - - .. math:: \\textrm{EnrichedElement}(V, Q) = \\{v + q | v \\in V, q \\in Q\\}. - """ - def __init__(self, *elements): - self._elements = elements - - cell = elements[0].cell() - if not all(e.cell() == cell for e in elements[1:]): - error("Cell mismatch for sub elements of enriched element.") - - if isinstance(elements[0].degree(), int): - degrees = {e.degree() for e in elements} - {None} - degree = max(degrees) if degrees else None - else: - degree = tuple(map(max, zip(*[e.degree() for e in elements]))) - - # We can allow the scheme not to be defined, but all defined - # should be equal - quad_schemes = [e.quadrature_scheme() for e in elements] - quad_schemes = [qs for qs in quad_schemes if qs is not None] - quad_scheme = quad_schemes[0] if quad_schemes else None - if not all(qs == quad_scheme for qs in quad_schemes): - error("Quadrature scheme mismatch.") - - value_shape = elements[0].value_shape() - if not all(e.value_shape() == value_shape for e in elements[1:]): - error("Element value shape mismatch.") - - reference_value_shape = elements[0].reference_value_shape() - if not all(e.reference_value_shape() == reference_value_shape for e in elements[1:]): - error("Element reference value shape mismatch.") - - # mapping = elements[0].mapping() # FIXME: This fails for a mixed subelement here. - # if not all(e.mapping() == mapping for e in elements[1:]): - # error("Element mapping mismatch.") - - # Get name of subclass: EnrichedElement or NodalEnrichedElement - class_name = self.__class__.__name__ - - # Initialize element data - FiniteElementBase.__init__(self, class_name, cell, degree, - quad_scheme, value_shape, - reference_value_shape) - - def mapping(self): - return self._elements[0].mapping() - - def sobolev_space(self): - """Return the underlying Sobolev space.""" - elements = [e for e in self._elements] - if all(e.sobolev_space() == elements[0].sobolev_space() - for e in elements): - return elements[0].sobolev_space() - else: - # Find smallest shared Sobolev space over all sub elements - spaces = [e.sobolev_space() for e in elements] - superspaces = [{s} | set(s.parents) for s in spaces] - intersect = set.intersection(*superspaces) - for s in intersect.copy(): - for parent in s.parents: - intersect.discard(parent) - - sobolev_space, = intersect - return sobolev_space - - def reconstruct(self, **kwargs): - return type(self)(*[e.reconstruct(**kwargs) for e in self._elements]) - - -class EnrichedElement(EnrichedElementBase): - """The vector sum of several finite element spaces: - - .. math:: \\textrm{EnrichedElement}(V, Q) = \\{v + q | v \\in V, q \\in Q\\}. - - Dual basis is a concatenation of subelements dual bases; - primal basis is a concatenation of subelements primal bases; - resulting element is not nodal even when subelements are. - Structured basis may be exploited in form compilers. - """ - def is_cellwise_constant(self): - """Return whether the basis functions of this - element is spatially constant over each cell.""" - return all(e.is_cellwise_constant() for e in self._elements) - - def __repr__(self): - return "EnrichedElement(" + ", ".join(repr(e) for e in self._elements) + ")" - - def __str__(self): - "Format as string for pretty printing." - return "<%s>" % " + ".join(str(e) for e in self._elements) - - def shortstr(self): - "Format as string for pretty printing." - return "<%s>" % " + ".join(e.shortstr() for e in self._elements) - - -class NodalEnrichedElement(EnrichedElementBase): - """The vector sum of several finite element spaces: - - .. math:: \\textrm{EnrichedElement}(V, Q) = \\{v + q | v \\in V, q \\in Q\\}. - - Primal basis is reorthogonalized to dual basis which is - a concatenation of subelements dual bases; resulting - element is nodal. - """ - def is_cellwise_constant(self): - """Return whether the basis functions of this - element is spatially constant over each cell.""" - return False - - def __repr__(self): - return "NodalEnrichedElement(" + ", ".join(repr(e) for e in self._elements) + ")" - - def __str__(self): - "Format as string for pretty printing." - return "" % ", ".join(str(e) for e in self._elements) - - def shortstr(self): - "Format as string for pretty printing." - return "NodalEnriched(%s)" % ", ".join(e.shortstr() for e in self._elements) diff --git a/ufl/finiteelement/facetelement.py b/ufl/finiteelement/facetelement.py deleted file mode 100644 index 40b3db9f2..000000000 --- a/ufl/finiteelement/facetelement.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2017 Miklós Homolya -# -# This file is part of UFL (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -from ufl.finiteelement.restrictedelement import RestrictedElement -from ufl.log import deprecate - - -def FacetElement(element): - """Constructs the restriction of a finite element to the facets of the - cell.""" - deprecate('FacetElement(element) is deprecated, please use element["facet"] instead.') - return RestrictedElement(element, restriction_domain="facet") diff --git a/ufl/finiteelement/finiteelement.py b/ufl/finiteelement/finiteelement.py deleted file mode 100644 index d9fd533a2..000000000 --- a/ufl/finiteelement/finiteelement.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 -*- -"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 Anders Logg 2014 -# Modified by Massimiliano Leoni, 2016 - -from ufl.log import error -from ufl.utils.formatting import istr -from ufl.cell import as_cell - -from ufl.cell import TensorProductCell -from ufl.finiteelement.elementlist import canonical_element_description, simplices -from ufl.finiteelement.finiteelementbase import FiniteElementBase - - -class FiniteElement(FiniteElementBase): - "The basic finite element class for all simple finite elements." - # TODO: Move these to base? - __slots__ = ("_short_name", "_sobolev_space", - "_mapping", "_variant", "_repr") - - def __new__(cls, - family, - cell=None, - degree=None, - form_degree=None, - quad_scheme=None, - variant=None): - """Intercepts construction to expand CG, DG, RTCE and RTCF - spaces on TensorProductCells.""" - if cell is not None: - cell = as_cell(cell) - - if isinstance(cell, TensorProductCell): - # Delay import to avoid circular dependency at module load time - from ufl.finiteelement.tensorproductelement import TensorProductElement - from ufl.finiteelement.enrichedelement import EnrichedElement - from ufl.finiteelement.hdivcurl import HDivElement as HDiv, HCurlElement as HCurl - - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping = \ - canonical_element_description(family, cell, degree, form_degree) - - if family in ["RTCF", "RTCE"]: - cell_h, cell_v = cell.sub_cells() - if cell_h.cellname() != "interval": - error("%s is available on TensorProductCell(interval, interval) only." % family) - if cell_v.cellname() != "interval": - error("%s is available on TensorProductCell(interval, interval) only." % family) - - C_elt = FiniteElement("CG", "interval", degree, variant=variant) - D_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) - - CxD_elt = TensorProductElement(C_elt, D_elt, cell=cell) - DxC_elt = TensorProductElement(D_elt, C_elt, cell=cell) - - if family == "RTCF": - return EnrichedElement(HDiv(CxD_elt), HDiv(DxC_elt)) - if family == "RTCE": - return EnrichedElement(HCurl(CxD_elt), HCurl(DxC_elt)) - - elif family == "NCF": - cell_h, cell_v = cell.sub_cells() - if cell_h.cellname() != "quadrilateral": - error("%s is available on TensorProductCell(quadrilateral, interval) only." % family) - if cell_v.cellname() != "interval": - error("%s is available on TensorProductCell(quadrilateral, interval) only." % family) - - Qc_elt = FiniteElement("RTCF", "quadrilateral", degree, variant=variant) - Qd_elt = FiniteElement("DQ", "quadrilateral", degree - 1, variant=variant) - - Id_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) - Ic_elt = FiniteElement("CG", "interval", degree, variant=variant) - - return EnrichedElement(HDiv(TensorProductElement(Qc_elt, Id_elt, cell=cell)), - HDiv(TensorProductElement(Qd_elt, Ic_elt, cell=cell))) - - elif family == "NCE": - cell_h, cell_v = cell.sub_cells() - if cell_h.cellname() != "quadrilateral": - error("%s is available on TensorProductCell(quadrilateral, interval) only." % family) - if cell_v.cellname() != "interval": - error("%s is available on TensorProductCell(quadrilateral, interval) only." % family) - - Qc_elt = FiniteElement("Q", "quadrilateral", degree, variant=variant) - Qd_elt = FiniteElement("RTCE", "quadrilateral", degree, variant=variant) - - Id_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) - Ic_elt = FiniteElement("CG", "interval", degree, variant=variant) - - return EnrichedElement(HCurl(TensorProductElement(Qc_elt, Id_elt, cell=cell)), - HCurl(TensorProductElement(Qd_elt, Ic_elt, cell=cell))) - - elif family == "Q": - return TensorProductElement(*[FiniteElement("CG", c, degree, variant=variant) - for c in cell.sub_cells()], - cell=cell) - - elif family == "DQ": - def dq_family(cell): - return "DG" if cell.cellname() in simplices else "DQ" - return TensorProductElement(*[FiniteElement(dq_family(c), c, degree, variant=variant) - for c in cell.sub_cells()], - cell=cell) - - elif family == "DQ L2": - def dq_family_l2(cell): - return "DG L2" if cell.cellname() in simplices else "DQ L2" - return TensorProductElement(*[FiniteElement(dq_family_l2(c), c, degree, variant=variant) - for c in cell.sub_cells()], - cell=cell) - - return super(FiniteElement, cls).__new__(cls) - - def __init__(self, - family, - cell=None, - degree=None, - form_degree=None, - quad_scheme=None, - variant=None): - """Create finite element. - - *Arguments* - family (string) - The finite element family - cell - The geometric cell - degree (int) - The polynomial degree (optional) - form_degree (int) - The form degree (FEEC notation, used when field is - viewed as k-form) - quad_scheme - The quadrature scheme (optional) - variant - Hint for the local basis function variant (optional) - """ - # Note: Unfortunately, dolfin sometimes passes None for - # cell. Until this is fixed, allow it: - if cell is not None: - cell = as_cell(cell) - - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping = canonical_element_description(family, cell, degree, form_degree) - - # TODO: Move these to base? Might be better to instead - # simplify base though. - self._sobolev_space = sobolev_space - self._mapping = mapping - self._short_name = short_name - self._variant = variant - - # Finite elements on quadrilaterals and hexahedrons have an IrreducibleInt as degree - if cell is not None: - if cell.cellname() in ["quadrilateral", "hexahedron"]: - from ufl.algorithms.estimate_degrees import IrreducibleInt - degree = IrreducibleInt(degree) - - # Type check variant - if variant is not None and not isinstance(variant, str): - raise ValueError("Illegal variant: must be string or None") - - # Initialize element data - FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, - value_shape, reference_value_shape) - - # Cache repr string - qs = self.quadrature_scheme() - if qs is None: - quad_str = "" - else: - quad_str = ", quad_scheme=%s" % repr(qs) - v = self.variant() - if v is None: - var_str = "" - else: - var_str = ", variant=%s" % repr(v) - self._repr = "FiniteElement(%s, %s, %s%s%s)" % ( - repr(self.family()), repr(self.cell()), repr(self.degree()), quad_str, var_str) - assert '"' not in self._repr - - def __repr__(self): - """Format as string for evaluation as Python object.""" - return self._repr - - def mapping(self): - """Return the mapping type for this element .""" - return self._mapping - - def sobolev_space(self): - """Return the underlying Sobolev space.""" - return self._sobolev_space - - def variant(self): - """Return the variant used to initialise the element.""" - return self._variant - - def reconstruct(self, family=None, cell=None, degree=None, quad_scheme=None, variant=None): - """Construct a new FiniteElement object with some properties - replaced with new values.""" - if family is None: - family = self.family() - if cell is None: - cell = self.cell() - if degree is None: - degree = self.degree() - if quad_scheme is None: - quad_scheme = self.quadrature_scheme() - if variant is None: - variant = self.variant() - return FiniteElement(family, cell, degree, quad_scheme=quad_scheme, variant=variant) - - def __str__(self): - "Format as string for pretty printing." - qs = self.quadrature_scheme() - qs = "" if qs is None else "(%s)" % qs - v = self.variant() - v = "" if v is None else "(%s)" % v - return "<%s%s%s%s on a %s>" % (self._short_name, istr(self.degree()), - qs, v, self.cell()) - - def shortstr(self): - "Format as string for pretty printing." - return "%s%s(%s,%s)" % (self._short_name, istr(self.degree()), - istr(self.quadrature_scheme()), istr(self.variant())) - - def __getnewargs__(self): - """Return the arguments which pickle needs to recreate the object.""" - return (self.family(), - self.cell(), - self.degree(), - None, - self.quadrature_scheme(), - self.variant()) diff --git a/ufl/finiteelement/finiteelementbase.py b/ufl/finiteelement/finiteelementbase.py deleted file mode 100644 index 17f687bb4..000000000 --- a/ufl/finiteelement/finiteelementbase.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -"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 - -from ufl.utils.sequences import product -from ufl.utils.dicts import EmptyDict -from ufl.log import error -from ufl.cell import AbstractCell, as_cell -from abc import ABC, abstractmethod - - -class FiniteElementBase(ABC): - "Base class for all finite elements." - __slots__ = ("_family", "_cell", "_degree", "_quad_scheme", - "_value_shape", "_reference_value_shape", "__weakref__") - - # TODO: Not all these should be in the base class! In particular - # family, degree, and quad_scheme do not belong here. - def __init__(self, family, cell, degree, quad_scheme, value_shape, - reference_value_shape): - """Initialize basic finite element data.""" - if not isinstance(family, str): - error("Invalid family type.") - if not (degree is None or isinstance(degree, (int, tuple))): - error("Invalid degree type.") - if not isinstance(value_shape, tuple): - error("Invalid value_shape type.") - if not isinstance(reference_value_shape, tuple): - error("Invalid reference_value_shape type.") - - if cell is not None: - cell = as_cell(cell) - if not isinstance(cell, AbstractCell): - error("Invalid cell type.") - - self._family = family - self._cell = cell - self._degree = degree - self._value_shape = value_shape - self._reference_value_shape = reference_value_shape - self._quad_scheme = quad_scheme - - @abstractmethod - def __repr__(self): - """Format as string for evaluation as Python object.""" - pass - - @abstractmethod - def sobolev_space(self): - """Return the underlying Sobolev space.""" - pass - - @abstractmethod - def mapping(self): - """Return the mapping type for this element.""" - pass - - def _ufl_hash_data_(self): - return repr(self) - - def _ufl_signature_data_(self): - return repr(self) - - def __hash__(self): - "Compute hash code for insertion in hashmaps." - return hash(self._ufl_hash_data_()) - - def __eq__(self, other): - "Compute element equality for insertion in hashmaps." - return type(self) == type(other) and self._ufl_hash_data_() == other._ufl_hash_data_() - - def __ne__(self, other): - "Compute element inequality for insertion in hashmaps." - return not self.__eq__(other) - - def __lt__(self, other): - "Compare elements by repr, to give a natural stable sorting." - return repr(self) < repr(other) - - def family(self): # FIXME: Undefined for base? - "Return finite element family." - return self._family - - def variant(self): - """Return the variant used to initialise the element.""" - return None - - def degree(self, component=None): - "Return polynomial degree of finite element." - # FIXME: Consider embedded_degree concept for more accurate - # degree, see blueprint - return self._degree - - def quadrature_scheme(self): - "Return quadrature scheme of finite element." - return self._quad_scheme - - def cell(self): - "Return cell of finite element." - return self._cell - - def is_cellwise_constant(self, component=None): - """Return whether the basis functions of this - element is spatially constant over each cell.""" - return self.family() == "Real" or self.degree() == 0 - - def value_shape(self): - "Return the shape of the value space on the global domain." - return self._value_shape - - def reference_value_shape(self): - "Return the shape of the value space on the reference cell." - return self._reference_value_shape - - def value_size(self): - "Return the integer product of the value shape." - return product(self.value_shape()) - - def reference_value_size(self): - "Return the integer product of the reference value shape." - return product(self.reference_value_shape()) - - def symmetry(self): # FIXME: different approach - """Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1` - meaning that component :math:`c_0` is represented by component - :math:`c_1`. - A component is a tuple of one or more ints.""" - return EmptyDict - - def _check_component(self, i): - "Check that component index i is valid" - sh = self.value_shape() - r = len(sh) - if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): - error(("Illegal component index '%s' (value rank %d)" + - "for element (value rank %d).") % (i, len(i), r)) - - def extract_subelement_component(self, i): - """Extract direct subelement index and subelement relative - component index for a given component index.""" - if isinstance(i, int): - i = (i,) - self._check_component(i) - return (None, i) - - def extract_component(self, i): - """Recursively extract component index relative to a (simple) element - and that element for given value component index.""" - if isinstance(i, int): - i = (i,) - self._check_component(i) - return (i, self) - - def _check_reference_component(self, i): - "Check that reference component index i is valid." - sh = self.value_shape() - r = len(sh) - if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): - error(("Illegal component index '%s' (value rank %d)" + - "for element (value rank %d).") % (i, len(i), r)) - - def extract_subelement_reference_component(self, i): - """Extract direct subelement index and subelement relative - reference component index for a given reference component index.""" - if isinstance(i, int): - i = (i,) - self._check_reference_component(i) - return (None, i) - - def extract_reference_component(self, i): - """Recursively extract reference component index relative to a (simple) element - and that element for given reference value component index.""" - if isinstance(i, int): - i = (i,) - self._check_reference_component(i) - return (i, self) - - def num_sub_elements(self): - "Return number of sub-elements." - return 0 - - def sub_elements(self): - "Return list of sub-elements." - return [] - - def __add__(self, other): - "Add two elements, creating an enriched element" - if not isinstance(other, FiniteElementBase): - error("Can't add element and %s." % other.__class__) - from ufl.finiteelement import EnrichedElement - return EnrichedElement(self, other) - - def __mul__(self, other): - "Multiply two elements, creating a mixed element" - if not isinstance(other, FiniteElementBase): - error("Can't multiply element and %s." % other.__class__) - from ufl.finiteelement import MixedElement - return MixedElement(self, other) - - def __getitem__(self, index): - "Restrict finite element to a subdomain, subcomponent or topology (cell)." - if index in ("facet", "interior"): - from ufl.finiteelement import RestrictedElement - return RestrictedElement(self, index) - return NotImplemented diff --git a/ufl/finiteelement/hdivcurl.py b/ufl/finiteelement/hdivcurl.py deleted file mode 100644 index 5e50ded2a..000000000 --- a/ufl/finiteelement/hdivcurl.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2008-2016 Andrew T. T. McRae -# -# 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.finiteelement.finiteelementbase import FiniteElementBase -from ufl.sobolevspace import HDiv, HCurl - - -class HDivElement(FiniteElementBase): - """A div-conforming version of an outer product element, assuming - this makes mathematical sense.""" - __slots__ = ("_element", ) - - def __init__(self, element): - self._element = element - - family = "TensorProductElement" - cell = element.cell() - degree = element.degree() - quad_scheme = element.quadrature_scheme() - value_shape = (element.cell().geometric_dimension(),) - reference_value_shape = (element.cell().topological_dimension(),) - - # Skipping TensorProductElement constructor! Bad code smell, refactor to avoid this non-inheritance somehow. - FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, reference_value_shape) - - def __repr__(self): - return f"HDivElement({repr(self._element)})" - - def mapping(self): - return "contravariant Piola" - - def sobolev_space(self): - """Return the underlying Sobolev space.""" - return HDiv - - def reconstruct(self, **kwargs): - return HDivElement(self._element.reconstruct(**kwargs)) - - def __str__(self): - return f"HDivElement({repr(self._element)})" - - def shortstr(self): - """Format as string for pretty printing.""" - return f"HDivElement({self._element.shortstr()})" - - -class HCurlElement(FiniteElementBase): - """A curl-conforming version of an outer product element, assuming - this makes mathematical sense.""" - __slots__ = ("_element",) - - def __init__(self, element): - self._element = element - - family = "TensorProductElement" - cell = element.cell() - degree = element.degree() - quad_scheme = element.quadrature_scheme() - cell = element.cell() - value_shape = (cell.geometric_dimension(),) - reference_value_shape = (cell.topological_dimension(),) # TODO: Is this right? - # Skipping TensorProductElement constructor! Bad code smell, - # refactor to avoid this non-inheritance somehow. - FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, - value_shape, reference_value_shape) - - def __repr__(self): - return f"HCurlElement({repr(self._element)})" - - def mapping(self): - return "covariant Piola" - - def sobolev_space(self): - "Return the underlying Sobolev space." - return HCurl - - def reconstruct(self, **kwargs): - return HCurlElement(self._element.reconstruct(**kwargs)) - - def __str__(self): - return f"HCurlElement({repr(self._element)})" - - def shortstr(self): - "Format as string for pretty printing." - return f"HCurlElement({self._element.shortstr()})" - - -class WithMapping(FiniteElementBase): - """Specify an alternative mapping for the wrappee. For example, - to use identity mapping instead of Piola map with an element E, - write - remapped = WithMapping(E, "identity") - """ - def __init__(self, wrapee, mapping): - if mapping == "symmetries": - raise ValueError("Can't change mapping to 'symmetries'") - self._mapping = mapping - self.wrapee = wrapee - - def __getattr__(self, attr): - try: - return getattr(self.wrapee, attr) - except AttributeError: - raise AttributeError("'%s' object has no attribute '%s'" % - (type(self).__name__, attr)) - - def __repr__(self): - return f"WithMapping({repr(self.wrapee)}, {self._mapping})" - - def value_shape(self): - gdim = self.cell().geometric_dimension() - mapping = self.mapping() - if mapping in {"covariant Piola", "contravariant Piola"}: - return (gdim,) - elif mapping in {"double covariant Piola", "double contravariant Piola"}: - return (gdim, gdim) - else: - return self.wrapee.value_shape() - - def reference_value_shape(self): - tdim = self.cell().topological_dimension() - mapping = self.mapping() - if mapping in {"covariant Piola", "contravariant Piola"}: - return (tdim,) - elif mapping in {"double covariant Piola", "double contravariant Piola"}: - return (tdim, tdim) - else: - return self.wrapee.reference_value_shape() - - def mapping(self): - return self._mapping - - def reconstruct(self, **kwargs): - mapping = kwargs.pop("mapping", self._mapping) - wrapee = self.wrapee.reconstruct(**kwargs) - return type(self)(wrapee, mapping) - - def __str__(self): - return f"WithMapping({repr(self.wrapee)}, {self._mapping})" - - def shortstr(self): - return f"WithMapping({self.wrapee.shortstr()}, {self._mapping})" diff --git a/ufl/finiteelement/interiorelement.py b/ufl/finiteelement/interiorelement.py deleted file mode 100644 index cd1ee239f..000000000 --- a/ufl/finiteelement/interiorelement.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2017 Miklós Homolya -# -# This file is part of UFL (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -from ufl.finiteelement.restrictedelement import RestrictedElement -from ufl.log import deprecate - - -def InteriorElement(element): - """Constructs the restriction of a finite element to the interior of - the cell.""" - deprecate('InteriorElement(element) is deprecated, please use element["interior"] instead.') - return RestrictedElement(element, restriction_domain="interior") diff --git a/ufl/finiteelement/mixedelement.py b/ufl/finiteelement/mixedelement.py deleted file mode 100644 index e80743fb9..000000000 --- a/ufl/finiteelement/mixedelement.py +++ /dev/null @@ -1,516 +0,0 @@ -# -*- coding: utf-8 -*- -"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 Anders Logg 2014 -# Modified by Massimiliano Leoni, 2016 - -from ufl.log import error -from ufl.permutation import compute_indices -from ufl.utils.sequences import product, max_degree -from ufl.utils.dicts import EmptyDict -from ufl.utils.indexflattening import flatten_multiindex, unflatten_index, shape_to_strides -from ufl.cell import as_cell - -from ufl.finiteelement.finiteelementbase import FiniteElementBase -from ufl.finiteelement.finiteelement import FiniteElement - - -class MixedElement(FiniteElementBase): - """A finite element composed of a nested hierarchy of mixed or simple - elements.""" - __slots__ = ("_sub_elements", "_cells") - - def __init__(self, *elements, **kwargs): - "Create mixed finite element from given list of elements" - - if type(self) is MixedElement: - if kwargs: - error("Not expecting keyword arguments to MixedElement constructor.") - - # Un-nest arguments if we get a single argument with a list of elements - if len(elements) == 1 and isinstance(elements[0], (tuple, list)): - elements = elements[0] - # Interpret nested tuples as sub-mixedelements recursively - elements = [MixedElement(e) if isinstance(e, (tuple, list)) else e - for e in elements] - self._sub_elements = elements - - # Pick the first cell, for now all should be equal - cells = tuple(sorted(set(element.cell() for element in elements) - set([None]))) - self._cells = cells - if cells: - cell = cells[0] - # Require that all elements are defined on the same cell - if not all(c == cell for c in cells[1:]): - error("Sub elements must live on the same cell.") - else: - cell = None - - # Check that all elements use the same quadrature scheme TODO: - # We can allow the scheme not to be defined. - if len(elements) == 0: - quad_scheme = None - else: - quad_scheme = elements[0].quadrature_scheme() - if not all(e.quadrature_scheme() == quad_scheme for e in elements): - error("Quadrature scheme mismatch for sub elements of mixed element.") - - # Compute value sizes in global and reference configurations - value_size_sum = sum(product(s.value_shape()) for s in self._sub_elements) - reference_value_size_sum = sum(product(s.reference_value_shape()) for s in self._sub_elements) - - # Default value shape: Treated simply as all subelement values - # unpacked in a vector. - value_shape = kwargs.get('value_shape', (value_size_sum,)) - - # Default reference value shape: Treated simply as all - # subelement reference values unpacked in a vector. - reference_value_shape = kwargs.get('reference_value_shape', (reference_value_size_sum,)) - - # Validate value_shape (deliberately not for subclasses - # VectorElement and TensorElement) - if type(self) is MixedElement: - # This is not valid for tensor elements with symmetries, - # assume subclasses deal with their own validation - if product(value_shape) != value_size_sum: - error("Provided value_shape doesn't match the " - "total value size of all subelements.") - - # Initialize element data - degrees = {e.degree() for e in self._sub_elements} - {None} - degree = max_degree(degrees) if degrees else None - FiniteElementBase.__init__(self, "Mixed", cell, degree, quad_scheme, - value_shape, reference_value_shape) - - def __repr__(self): - return "MixedElement(" + ", ".join(repr(e) for e in self._sub_elements) + ")" - - def reconstruct_from_elements(self, *elements): - "Reconstruct a mixed element from new subelements." - if all(a == b for (a, b) in zip(elements, self._sub_elements)): - return self - return MixedElement(*elements) - - def symmetry(self): - """Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1` - meaning that component :math:`c_0` is represented by component - :math:`c_1`. - A component is a tuple of one or more ints.""" - # Build symmetry map from symmetries of subelements - sm = {} - # Base index of the current subelement into mixed value - j = 0 - for e in self._sub_elements: - sh = e.value_shape() - st = shape_to_strides(sh) - # Map symmetries of subelement into index space of this - # element - for c0, c1 in e.symmetry().items(): - j0 = flatten_multiindex(c0, st) + j - j1 = flatten_multiindex(c1, st) + j - sm[(j0,)] = (j1,) - # Update base index for next element - j += product(sh) - if j != product(self.value_shape()): - error("Size mismatch in symmetry algorithm.") - return sm or EmptyDict - - def sobolev_space(self): - return max(e.sobolev_space() for e in self._sub_elements) - - def mapping(self): - if all(e.mapping() == "identity" for e in self._sub_elements): - return "identity" - else: - return "undefined" - - def num_sub_elements(self): - "Return number of sub elements." - return len(self._sub_elements) - - def sub_elements(self): - "Return list of sub elements." - return self._sub_elements - - def extract_subelement_component(self, i): - """Extract direct subelement index and subelement relative - component index for a given component index.""" - if isinstance(i, int): - i = (i,) - self._check_component(i) - - # Select between indexing modes - if len(self.value_shape()) == 1: - # Indexing into a long vector of flattened subelement - # shapes - j, = i - - # Find subelement for this index - for sub_element_index, e in enumerate(self._sub_elements): - sh = e.value_shape() - si = product(sh) - if j < si: - break - j -= si - if j < 0: - error("Moved past last value component!") - - # Convert index into a shape tuple - st = shape_to_strides(sh) - component = unflatten_index(j, st) - else: - # Indexing into a multidimensional tensor where subelement - # index is first axis - sub_element_index = i[0] - if sub_element_index >= len(self._sub_elements): - error("Illegal component index (dimension %d)." % sub_element_index) - component = i[1:] - return (sub_element_index, component) - - def extract_component(self, i): - """Recursively extract component index relative to a (simple) element - and that element for given value component index.""" - sub_element_index, component = self.extract_subelement_component(i) - return self._sub_elements[sub_element_index].extract_component(component) - - def extract_subelement_reference_component(self, i): - """Extract direct subelement index and subelement relative - reference_component index for a given reference_component index.""" - if isinstance(i, int): - i = (i,) - self._check_reference_component(i) - - # Select between indexing modes - assert len(self.reference_value_shape()) == 1 - # Indexing into a long vector of flattened subelement shapes - j, = i - - # Find subelement for this index - for sub_element_index, e in enumerate(self._sub_elements): - sh = e.reference_value_shape() - si = product(sh) - if j < si: - break - j -= si - if j < 0: - error("Moved past last value reference_component!") - - # Convert index into a shape tuple - st = shape_to_strides(sh) - reference_component = unflatten_index(j, st) - return (sub_element_index, reference_component) - - def extract_reference_component(self, i): - """Recursively extract reference_component index relative to a (simple) element - and that element for given value reference_component index.""" - sub_element_index, reference_component = self.extract_subelement_reference_component(i) - return self._sub_elements[sub_element_index].extract_reference_component(reference_component) - - def is_cellwise_constant(self, component=None): - """Return whether the basis functions of this - element is spatially constant over each cell.""" - if component is None: - return all(e.is_cellwise_constant() for e in self.sub_elements()) - else: - i, e = self.extract_component(component) - return e.is_cellwise_constant() - - def degree(self, component=None): - "Return polynomial degree of finite element." - if component is None: - return self._degree # from FiniteElementBase, computed as max of subelements in __init__ - else: - i, e = self.extract_component(component) - return e.degree() - - def reconstruct(self, **kwargs): - return MixedElement(*[e.reconstruct(**kwargs) for e in self.sub_elements()]) - - def __str__(self): - "Format as string for pretty printing." - tmp = ", ".join(str(element) for element in self._sub_elements) - return "" - - def shortstr(self): - "Format as string for pretty printing." - tmp = ", ".join(element.shortstr() for element in self._sub_elements) - return "Mixed<" + tmp + ">" - - -class VectorElement(MixedElement): - "A special case of a mixed finite element where all elements are equal." - - __slots__ = ("_repr", "_mapping", "_sub_element") - - def __init__(self, family, cell=None, degree=None, dim=None, - form_degree=None, quad_scheme=None, variant=None): - """ - Create vector element (repeated mixed element) - - *Arguments* - family (string) - The finite element family (or an existing FiniteElement) - cell - The geometric cell, ignored if family is a FiniteElement - degree (int) - The polynomial degree, ignored if family is a FiniteElement - dim (int) - The value dimension of the element (optional) - form_degree (int) - The form degree (FEEC notation, used when field is - viewed as k-form), ignored if family is a FiniteElement - quad_scheme - The quadrature scheme (optional), ignored if family is a FiniteElement - variant - Hint for the local basis function variant (optional) - """ - - if isinstance(family, FiniteElementBase): - sub_element = family - cell = sub_element.cell() - variant = sub_element.variant() - else: - if cell is not None: - cell = as_cell(cell) - # Create sub element - sub_element = FiniteElement(family, cell, degree, - form_degree=form_degree, - quad_scheme=quad_scheme, - variant=variant) - - # Set default size if not specified - if dim is None: - if cell is None: - error("Cannot infer vector dimension without a cell.") - dim = cell.geometric_dimension() - - self._mapping = sub_element.mapping() - # Create list of sub elements for mixed element constructor - sub_elements = [sub_element] * dim - - # Compute value shapes - value_shape = (dim,) + sub_element.value_shape() - reference_value_shape = (dim,) + sub_element.reference_value_shape() - - # Initialize element data - MixedElement.__init__(self, sub_elements, value_shape=value_shape, - reference_value_shape=reference_value_shape) - FiniteElementBase.__init__(self, sub_element.family(), cell, sub_element.degree(), quad_scheme, - value_shape, reference_value_shape) - self._sub_element = sub_element - - if variant is None: - var_str = "" - else: - var_str = ", variant='" + variant + "'" - - # Cache repr string - self._repr = f"VectorElement({repr(sub_element)}, dim={dim}{var_str})" - - def __repr__(self): - return self._repr - - def reconstruct(self, **kwargs): - sub_element = self._sub_element.reconstruct(**kwargs) - return VectorElement(sub_element, dim=len(self.sub_elements())) - - def variant(self): - """Return the variant used to initialise the element.""" - return self._sub_element.variant() - - def mapping(self): - return self._mapping - - def __str__(self): - "Format as string for pretty printing." - return ("" % - (len(self._sub_elements), self._sub_element)) - - def shortstr(self): - "Format as string for pretty printing." - return "Vector<%d x %s>" % (len(self._sub_elements), - self._sub_element.shortstr()) - - -class TensorElement(MixedElement): - """A special case of a mixed finite element where all elements are - equal. - - """ - __slots__ = ("_sub_element", "_shape", "_symmetry", - "_sub_element_mapping", - "_flattened_sub_element_mapping", - "_mapping", "_repr") - - def __init__(self, family, cell=None, degree=None, shape=None, - symmetry=None, quad_scheme=None, variant=None): - """Create tensor element (repeated mixed element with optional symmetries). - - :arg family: The family string, or an existing FiniteElement. - :arg cell: The geometric cell (ignored if family is a FiniteElement). - :arg degree: The polynomial degree (ignored if family is a FiniteElement). - :arg shape: The shape of the element (defaults to a square - tensor given by the geometric dimension of the cell). - :arg symmetry: Optional symmetries. - :arg quad_scheme: Optional quadrature scheme (ignored if - family is a FiniteElement). - :arg variant: Hint for the local basis function variant (optional)""" - - if isinstance(family, FiniteElementBase): - sub_element = family - cell = sub_element.cell() - variant = sub_element.variant() - else: - if cell is not None: - cell = as_cell(cell) - # Create scalar sub element - sub_element = FiniteElement(family, cell, degree, quad_scheme=quad_scheme, - variant=variant) - - # Set default shape if not specified - if shape is None: - if cell is None: - error("Cannot infer tensor shape without a cell.") - dim = cell.geometric_dimension() - shape = (dim, dim) - - if symmetry is None: - symmetry = EmptyDict - elif symmetry is True: - # Construct default symmetry dict for matrix elements - if not (len(shape) == 2 and shape[0] == shape[1]): - error("Cannot set automatic symmetry for non-square tensor.") - symmetry = dict(((i, j), (j, i)) for i in range(shape[0]) - for j in range(shape[1]) if i > j) - else: - if not isinstance(symmetry, dict): - error("Expecting symmetry to be None (unset), True, or dict.") - - # Validate indices in symmetry dict - for i, j in symmetry.items(): - if len(i) != len(j): - error("Non-matching length of symmetry index tuples.") - for k in range(len(i)): - if not (i[k] >= 0 and j[k] >= 0 and i[k] < shape[k] and j[k] < shape[k]): - error("Symmetry dimensions out of bounds.") - - # Compute all index combinations for given shape - indices = compute_indices(shape) - - # Compute mapping from indices to sub element number, - # accounting for symmetry - sub_elements = [] - sub_element_mapping = {} - for index in indices: - if index in symmetry: - continue - sub_element_mapping[index] = len(sub_elements) - sub_elements += [sub_element] - - # Update mapping for symmetry - for index in indices: - if index in symmetry: - sub_element_mapping[index] = sub_element_mapping[symmetry[index]] - flattened_sub_element_mapping = [sub_element_mapping[index] for i, - index in enumerate(indices)] - - # Compute value shape - value_shape = shape - - # Compute reference value shape based on symmetries - if symmetry: - reference_value_shape = (product(shape) - len(symmetry),) - self._mapping = "symmetries" - else: - reference_value_shape = shape - self._mapping = sub_element.mapping() - - value_shape = value_shape + sub_element.value_shape() - reference_value_shape = reference_value_shape + sub_element.reference_value_shape() - # Initialize element data - MixedElement.__init__(self, sub_elements, value_shape=value_shape, - reference_value_shape=reference_value_shape) - self._family = sub_element.family() - self._degree = sub_element.degree() - self._sub_element = sub_element - self._shape = shape - self._symmetry = symmetry - self._sub_element_mapping = sub_element_mapping - self._flattened_sub_element_mapping = flattened_sub_element_mapping - - if variant is None: - var_str = "" - else: - var_str = ", variant='" + variant + "'" - - # Cache repr string - self._repr = (f"TensorElement({repr(sub_element)}, shape={shape}, " - f"symmetry={symmetry}{var_str})") - - def __repr__(self): - return self._repr - - def variant(self): - """Return the variant used to initialise the element.""" - return self._sub_element.variant() - - def mapping(self): - return self._mapping - - def flattened_sub_element_mapping(self): - return self._flattened_sub_element_mapping - - def extract_subelement_component(self, i): - """Extract direct subelement index and subelement relative - component index for a given component index.""" - if isinstance(i, int): - i = (i,) - self._check_component(i) - - i = self.symmetry().get(i, i) - l = len(self._shape) # noqa: E741 - ii = i[:l] - jj = i[l:] - if ii not in self._sub_element_mapping: - error("Illegal component index %s." % (i,)) - k = self._sub_element_mapping[ii] - return (k, jj) - - def symmetry(self): - """Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1` - meaning that component :math:`c_0` is represented by component - :math:`c_1`. - A component is a tuple of one or more ints.""" - return self._symmetry - - def reconstruct(self, **kwargs): - sub_element = self._sub_element.reconstruct(**kwargs) - return TensorElement(sub_element, shape=self._shape, symmetry=self._symmetry) - - def __str__(self): - "Format as string for pretty printing." - if self._symmetry: - tmp = ", ".join("%s -> %s" % (a, b) for (a, b) in self._symmetry.items()) - sym = " with symmetries (%s)" % tmp - else: - sym = "" - return ("" % - (self.value_shape(), self._sub_element, sym)) - - def shortstr(self): - "Format as string for pretty printing." - if self._symmetry: - tmp = ", ".join("%s -> %s" % (a, b) for (a, b) in self._symmetry.items()) - sym = " with symmetries (%s)" % tmp - else: - sym = "" - return "Tensor<%s x %s%s>" % (self.value_shape(), - self._sub_element.shortstr(), sym) diff --git a/ufl/finiteelement/restrictedelement.py b/ufl/finiteelement/restrictedelement.py deleted file mode 100644 index 1d17e8bc4..000000000 --- a/ufl/finiteelement/restrictedelement.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -"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 - -from ufl.finiteelement.finiteelementbase import FiniteElementBase -from ufl.log import error -from ufl.sobolevspace import L2 - -valid_restriction_domains = ("interior", "facet", "face", "edge", "vertex") - - -class RestrictedElement(FiniteElementBase): - "Represents the restriction of a finite element to a type of cell entity." - def __init__(self, element, restriction_domain): - if not isinstance(element, FiniteElementBase): - error("Expecting a finite element instance.") - if restriction_domain not in valid_restriction_domains: - error("Expecting one of the strings %s." % (valid_restriction_domains,)) - - FiniteElementBase.__init__(self, "RestrictedElement", element.cell(), - element.degree(), - element.quadrature_scheme(), - element.value_shape(), - element.reference_value_shape()) - - self._element = element - - self._restriction_domain = restriction_domain - - def __repr__(self): - return f"RestrictedElement({repr(self._element)}, {repr(self._restriction_domain)})" - - def sobolev_space(self): - return L2 - - def is_cellwise_constant(self): - """Return whether the basis functions of this element is spatially - constant over each cell. - - """ - return self._element.is_cellwise_constant() - - def sub_element(self): - "Return the element which is restricted." - return self._element - - def mapping(self): - return self._element.mapping() - - def restriction_domain(self): - "Return the domain onto which the element is restricted." - return self._restriction_domain - - def reconstruct(self, **kwargs): - element = self._element.reconstruct(**kwargs) - return RestrictedElement(element, self._restriction_domain) - - def __str__(self): - "Format as string for pretty printing." - return "<%s>|_{%s}" % (self._element, self._restriction_domain) - - def shortstr(self): - "Format as string for pretty printing." - return "<%s>|_{%s}" % (self._element.shortstr(), - self._restriction_domain) - - def symmetry(self): - """Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1` - meaning that component :math:`c_0` is represented by component - :math:`c_1`. A component is a tuple of one or more ints. - - """ - return self._element.symmetry() - - def num_sub_elements(self): - "Return number of sub elements." - return self._element.num_sub_elements() - - def sub_elements(self): - "Return list of sub elements." - return self._element.sub_elements() - - def num_restricted_sub_elements(self): - # FIXME: Use this where intended, for disambiguation - # w.r.t. different sub_elements meanings. - "Return number of restricted sub elements." - return 1 - - def restricted_sub_elements(self): - # FIXME: Use this where intended, for disambiguation - # w.r.t. different sub_elements meanings. - "Return list of restricted sub elements." - return (self._element,) diff --git a/ufl/finiteelement/tensorproductelement.py b/ufl/finiteelement/tensorproductelement.py deleted file mode 100644 index 54de4aa83..000000000 --- a/ufl/finiteelement/tensorproductelement.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -"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 - -from itertools import chain - -from ufl.log import error -from ufl.cell import TensorProductCell, as_cell -from ufl.sobolevspace import DirectionalSobolevSpace - -from ufl.finiteelement.finiteelementbase import FiniteElementBase - - -class TensorProductElement(FiniteElementBase): - r"""The tensor product of :math:`d` element spaces: - - .. math:: V = V_1 \otimes V_2 \otimes ... \otimes V_d - - Given bases :math:`\{\phi_{j_i}\}` of the spaces :math:`V_i` for :math:`i = 1, ...., d`, - :math:`\{ \phi_{j_1} \otimes \phi_{j_2} \otimes \cdots \otimes \phi_{j_d} - \}` forms a basis for :math:`V`. - """ - __slots__ = ("_sub_elements", "_cell") - - def __init__(self, *elements, **kwargs): - "Create TensorProductElement from a given list of elements." - if not elements: - error("Cannot create TensorProductElement from empty list.") - - keywords = list(kwargs.keys()) - if keywords and keywords != ["cell"]: - raise ValueError("TensorProductElement got an unexpected keyword argument '%s'" % keywords[0]) - cell = kwargs.get("cell") - - family = "TensorProductElement" - - if cell is None: - # Define cell as the product of each elements cell - cell = TensorProductCell(*[e.cell() for e in elements]) - else: - cell = as_cell(cell) - - # Define polynomial degree as a tuple of sub-degrees - degree = tuple(e.degree() for e in elements) - - # No quadrature scheme defined - quad_scheme = None - - # match FIAT implementation - value_shape = tuple(chain(*[e.value_shape() for e in elements])) - reference_value_shape = tuple(chain(*[e.reference_value_shape() for e in elements])) - if len(value_shape) > 1: - error("Product of vector-valued elements not supported") - if len(reference_value_shape) > 1: - error("Product of vector-valued elements not supported") - - FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, - reference_value_shape) - self._sub_elements = elements - self._cell = cell - - def __repr__(self): - return "TensorProductElement(" + ", ".join( - repr(e) for e in self._sub_elements - ) + f", {repr(self._cell)})" - - def mapping(self): - if all(e.mapping() == "identity" for e in self._sub_elements): - return "identity" - elif all(e.mapping() == "L2 Piola" for e in self._sub_elements): - return "L2 Piola" - else: - return "undefined" - - def sobolev_space(self): - "Return the underlying Sobolev space of the TensorProductElement." - elements = self._sub_elements - if all(e.sobolev_space() == elements[0].sobolev_space() - for e in elements): - return elements[0].sobolev_space() - else: - # Generate a DirectionalSobolevSpace which contains - # continuity information parametrized by spatial index - orders = [] - for e in elements: - e_dim = e.cell().geometric_dimension() - e_order = (e.sobolev_space()._order,) * e_dim - orders.extend(e_order) - return DirectionalSobolevSpace(orders) - - def num_sub_elements(self): - "Return number of subelements." - return len(self._sub_elements) - - def sub_elements(self): - "Return subelements (factors)." - return self._sub_elements - - def reconstruct(self, **kwargs): - cell = kwargs.pop("cell", self.cell()) - return TensorProductElement(*[e.reconstruct(**kwargs) for e in self.sub_elements()], cell=cell) - - def __str__(self): - "Pretty-print." - return "TensorProductElement(%s, cell=%s)" \ - % (', '.join([str(e) for e in self._sub_elements]), str(self._cell)) - - def shortstr(self): - "Short pretty-print." - return "TensorProductElement(%s, cell=%s)" \ - % (', '.join([e.shortstr() for e in self._sub_elements]), str(self._cell)) diff --git a/ufl/form.py b/ufl/form.py index 52ea2680e..4fc7c2165 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"The Form class." - +"""The Form class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -10,37 +8,44 @@ # 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. -from itertools import chain +import numbers +import warnings from collections import defaultdict +from itertools import chain -from ufl.log import error, warning -from ufl.domain import sort_domains -from ufl.integral import Integral from ufl.checks import is_scalar_constant_expression -from ufl.equation import Equation -from ufl.core.expr import Expr -from ufl.core.expr import ufl_err_str +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"] +__all_classes__ = ["Form", "BaseForm", "ZeroBaseForm"] # --- The Form class, representing a complete variational form or functional --- def _sorted_integrals(integrals): - """Sort integrals by domain id, integral type, subdomain id - for a more stable signature computation.""" + """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))) + integrals_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) for integral in integrals: d = integral.ufl_domain() if d is None: - error( + raise ValueError( "Each integral in a form must have a uniquely defined integration domain." ) it = integral.integral_type() @@ -49,40 +54,193 @@ def _sorted_integrals(integrals): 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=lambda x: (type(x).__name__, x) - ): # int/str are 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. + # 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 -class Form(object): +@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) + # --- 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", - "_constant_numbering", "_constants", + "_constant_numbering", + "_terminal_numbering", "_hash", "_signature", # --- Dict that external frameworks can place framework-specific @@ -92,10 +250,12 @@ class Form(object): ) 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): - error("Expecting list of integrals.") + raise ValueError("Expecting list of integrals.") # Store integrals sorted canonically to increase signature # stability @@ -109,15 +269,17 @@ def __init__(self, integrals): self._subdomain_data = None # Internal variables for caching form argument data - self._arguments = None 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) - self._constant_numbering = dict( - (c, i) for i, c in enumerate(self._constants)) # Internal variables for caching of hash and signature after # first request @@ -130,27 +292,28 @@ def __init__(self, integrals): # --- Accessor interface --- def integrals(self): - "Return a sequence of all integrals in form." + """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) + """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) + """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." + """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. + 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. """ @@ -159,95 +322,98 @@ def ufl_domains(self): return self._integration_domains def ufl_cell(self): - """Return the single cell this form is defined on, fails if multiple - cells are found. + """Return the single cell this form is defined on. + Fails if multiple cells are found. """ return self.ufl_domain().ufl_cell() - def ufl_domain(self): - """Return the single geometric integration domain occuring in the - form. - - Fails if multiple domains are found. - - NB! This does not include domains of coefficients defined on - other meshes, look at form data for that additional - information. - - """ - # Collect all domains - domains = self.ufl_domains() - # Check that all are equal TODO: don't return more than one if - # all are equal? - if not all(domain == domains[0] for domain in domains): - error( - "Calling Form.ufl_domain() is only valid if all integrals share domain." - ) - - # Return the one and only domain - return domains[0] - 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())) + """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: - error("Expecting all domains and functions in a form " - "to share geometric dimension, got %s." % str( - tuple(sorted(gdims)))) + 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}``.""" + """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}}``.""" + """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 max_subdomain_ids(self): - """Returns a mapping on the form - ``{domain:{integral_type:max_subdomain_id}}``.""" - if self._max_subdomain_ids is None: - self._analyze_subdomain_data() - return self._max_subdomain_ids - - 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." + """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}``.""" + """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._analyze_form_arguments() + 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}``.""" + """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.)" + """Signature for use with jit cache (independent of incidental numbering of indices etc).""" if self._signature is None: self._compute_signature() return self._signature @@ -255,27 +421,18 @@ def signature(self): # --- Operator implementations --- def __hash__(self): - "Hash code for use in dicts (includes incidental numbering of indices etc.)" + """Hash.""" if self._hash is None: self._hash = hash(tuple(hash(itg) for itg in self.integrals())) return self._hash - 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 __ne__(self, other): - "Immediate evaluation of the != operator (as opposed to the == operator)." + """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) != Form: + """Evaluate ``bool(lhs_form == rhs_form)``.""" + if type(other) is not Form: return False if len(self._integrals) != len(other._integrals): return False @@ -284,21 +441,29 @@ def equals(self, other): 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): + 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 @@ -307,37 +472,38 @@ def __add__(self, other): return NotImplemented def __sub__(self, other): - "Subtract other form from this one." + """Subtract other form from this one.""" return self + (-other) def __rsub__(self, other): - "Subtract this form from 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.""" + 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." + """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." + """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. + """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 @@ -347,7 +513,6 @@ def __call__(self, *args, **kwargs): to replace Coefficients with expressions of matching shapes. Example: - ------- V = FiniteElement("CG", triangle, 1) v = TestFunction(V) u = TrialFunction(V) @@ -357,20 +522,18 @@ def __call__(self, *args, **kwargs): 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): - error("Need %d arguments to form(), got %d." % (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") + coefficients = kwargs.pop("coefficients", None) if kwargs: - error("Unknown kwargs %s." % str(list(kwargs))) + raise ValueError(f"Unknown kwargs {list(kwargs)}") if coefficients is not None: coeffs = self.coefficients() @@ -378,31 +541,33 @@ def __call__(self, *args, **kwargs): if f in coeffs: repdict[f] = coefficients[f] else: - warning("Coefficient %s is not in form." % ufl_err_str(f)) + 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 - # "a @ f" notation in python 3.5 __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." + """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 + # 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." + """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 + # 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 @@ -410,21 +575,21 @@ def __repr__(self): # --- 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]) + 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)) + 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() @@ -447,33 +612,36 @@ def _analyze_subdomain_data(self): self._subdomain_data = subdomain_data def _analyze_form_arguments(self): - "Analyze which Argument and Coefficient objects can be found in the form." + """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())) - self._coefficient_numbering = dict( - (c, i) for i, c in enumerate(self._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() - cn = self.coefficient_numbering() - cnstn = self.constant_numbering() + tn = self.terminal_numbering() renumbering = {} renumbering.update(dn) - renumbering.update(cn) - renumbering.update(cnstn) + renumbering.update(tn) # Add domains of coefficients, these may include domains not # among integration domains k = len(dn) - for c in cn: - d = c.ufl_domain() + 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 @@ -489,7 +657,7 @@ def _compute_renumbering(self): # Add domains of constants, these may include domains not # among integration domains for c in self._constants: - d = c.ufl_domain() + d = extract_unique_domain(c) if d is not None and d not in renumbering: renumbering[d] = k k += 1 @@ -497,52 +665,244 @@ def _compute_renumbering(self): 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 sub_forms_by_domain(form): - "Return a list of forms each with an integration domain" - if not isinstance(form, Form): - error("Unable to convert object to a UFL form: %s" % ufl_err_str(form)) - return [Form(form.integrals_by_domain(domain)) for domain in form.ufl_domains()] + 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, Form): - error("Unable to convert object to a UFL form: %s" % ufl_err_str(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 -def replace_integral_domains(form, common_domain): # TODO: Move elsewhere - """Given a form and a domain, assign a common integration domain to - all integrals. +@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, sort_domains + + # Collect unique domains + self._domains = sort_domains( + join_domains(chain.from_iterable(e.ufl_domains() for e in self.ufl_operands)) + ) - Does not modify the input form (``Form`` should always be - immutable). This is to support ill formed forms with no domain - specified, sometimes occurring in pydolfin, e.g. assemble(1*dx, - mesh=mesh). + 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. """ - domains = form.ufl_domains() - if common_domain is not None: - gdim = common_domain.geometric_dimension() - tdim = common_domain.topological_dimension() - if not all((gdim == domain.geometric_dimension() and - tdim == domain.topological_dimension()) - for domain in domains): - error("Common domain does not share dimensions with form domains.") - - reconstruct = False - integrals = [] - for itg in form.integrals(): - domain = itg.ufl_domain() - if domain != common_domain: - itg = itg.reconstruct(domain=common_domain) - reconstruct = True - integrals.append(itg) - if reconstruct: - form = Form(integrals) - return form + + __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, sort_domains + + # Collect unique domains + self._domains = sort_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 diff --git a/ufl/formatting/__init__.py b/ufl/formatting/__init__.py index e69de29bb..b4a49422f 100644 --- a/ufl/formatting/__init__.py +++ b/ufl/formatting/__init__.py @@ -0,0 +1 @@ +"""Formatting.""" diff --git a/ufl/formatting/graph.py b/ufl/formatting/graph.py deleted file mode 100644 index 8c4dfdf6a..000000000 --- a/ufl/formatting/graph.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- -"""Algorithms for working with linearized computational graphs.""" - -# 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 collections import defaultdict -from heapq import heapify, heappop - -from ufl.corealg.traversal import unique_pre_traversal -from ufl.corealg.multifunction import MultiFunction - -# O(n) = O(|V|) = O(|E|), since |E| < c|V| for a fairly small c. - - -# --- Basic utility functions --- - -def lists(n): - return [[] for i in range(n)] - - -def len_items(sequence): - return list(map(len, sequence)) - - -# --- Graph building functions --- - -def build_graph(expr): # O(n) - """Build a linearized graph from an UFL Expr. - - Returns G = (V, E), with V being a list of - graph nodes (Expr objects) in post traversal - ordering and E being a list of edges. Each edge - is represented as a (i, j) tuple where i and j - are vertex indices into V. - """ - V = [] - E = [] - handled = {} - for v in reversed(list(unique_pre_traversal(expr))): - i = handled.get(v) - if i is None: - i = len(V) - handled[v] = i - V.append(v) - for o in v.ufl_operands: - j = handled[o] - e = (i, j) - E.append(e) - G = V, E - return G - - -def extract_incoming_edges(G): # O(n) - "Build lists of incoming edges to each vertex in a linearized graph." - V, E = G - n = len(V) - Ein = lists(n) - for i, e in enumerate(E): - Ein[e[1]].append(i) - return Ein - - -def extract_outgoing_edges(G): # O(n) - "Build list of outgoing edges from each vertex in a linearized graph." - V, E = G - n = len(V) - Eout = lists(n) - for i, e in enumerate(E): - Eout[e[0]].append(i) - return Eout - - -def extract_incoming_vertex_connections(G): # O(n) - """Build lists of vertices in incoming and outgoing - edges to and from each vertex in a linearized graph. - - Returns lists Vin and Vout.""" - V, E = G - n = len(V) - Vin = lists(n) - for a, b in E: - Vin[b].append(a) - return Vin - - -def extract_outgoing_vertex_connections(G): # O(n) - """Build lists of vertices in incoming and outgoing - edges to and from each vertex in a linearized graph. - - Returns lists Vin and Vout.""" - V, E = G - n = len(V) - Vout = lists(n) - for a, b in E: - Vout[a].append(b) - return Vout - - -# --- Graph class --- - -class Graph: - "Graph class which computes connectivity on demand." - - def __init__(self, expression): - self._V, self._E = build_graph(expression) - self._Ein = None - self._Eout = None - self._Vin = None - self._Vout = None - - def V(self): - return self._V - - def E(self): - return self._E - - def Ein(self): - if self._Ein is None: - self._Ein = extract_incoming_edges((self._V, self._E)) - return self._Ein - - def Eout(self): - if self._Eout is None: - self._Eout = extract_outgoing_edges((self._V, self._E)) - return self._Eout - - def Vin(self): - if self._Vin is None: - self._Vin = extract_incoming_vertex_connections((self._V, self._E)) - return self._Vin - - def Vout(self): - if self._Vout is None: - self._Vout = extract_outgoing_vertex_connections((self._V, self._E)) - return self._Vout - - def __iter__(self): - return iter((self._V, self._E)) - - -# --- Scheduling algorithms --- - -class HeapItem(object): - def __init__(self, incoming, outgoing, i): - self.incoming = incoming - self.outgoing = outgoing - self.i = i - - def __lt__(self, other): - a = (self.outgoing[self.i], self.incoming[self.i]) - b = (other.outgoing[other.i], other.incoming[other.i]) - return a < b - - def __le__(self, other): - a = (self.outgoing[self.i], self.incoming[self.i]) - b = (other.outgoing[other.i], other.incoming[other.i]) - return a <= b - - def __eq__(self, other): - a = (self.outgoing[self.i], self.incoming[self.i]) - b = (other.outgoing[other.i], other.incoming[other.i]) - return a == b - - -def depth_first_ordering(G): - V, E = G - Vin = G.Vin() - Vout = G.Vout() - Ein_count = len_items(Vin) - Eout_count = len_items(Vout) - - # Make a list and a heap of the same items - n = len(V) - q = [HeapItem(Ein_count, Eout_count, i) for i in range(n)] - heapify(q) - - ordering = [] - while q: - item = heappop(q) - iv = item.i - ordering.append(iv) - for i in Vin[iv]: - Eout_count[i] -= 1 - # Resort heap, worst case linear time, makes this algorithm - # O(n^2)... TODO: Not good! - heapify(q) - - # TODO: Can later accumulate dependencies as well during dft-like - # algorithm. - return ordering - - -# --- Graph partitoning --- - -class StringDependencyDefiner(MultiFunction): - """Given an expr, returns a frozenset of its dependencies. - - Possible dependency values are: - "c" - depends on runtime information like the cell, local<->global coordinate mappings, facet normals, or coefficients - "x" - depends on local coordinates - "v%d" % i - depends on argument i, for i in [0,rank) - """ - - def __init__(self, argument_deps=None, coefficient_deps=None): - MultiFunction.__init__(self) - if argument_deps is None: - argument_deps = {} - if coefficient_deps is None: - coefficient_deps = {} - self.argument_deps = argument_deps - self.coefficient_deps = coefficient_deps - - def expr(self, o): - return frozenset() - - def argument(self, x): - default = frozenset(("v%d" % x.number(), "x")) # TODO: This is missing the part, but this code is ready for deletion anyway? - return self.argument_deps.get(x, default) - - def coefficient(self, x): - default = frozenset(("c", "x")) - return self.coefficient_deps.get(x, default) - - def geometric_quantity(self, x): - deps = frozenset(("c", "x",)) - return deps - - def facet_normal(self, o): - deps = frozenset(("c",)) - # Enabling coordinate dependency for higher order geometries - # (not handled anywhere else though, so consider this experimental) - # if o.has_higher_degree_cell_geometry(): - # deps = deps | frozenset(("x",)) - return deps - - def spatial_derivative(self, o): # TODO: What about (basis) functions of which derivatives are constant? Should special-case spatial_derivative in partition(). - deps = frozenset(("c",)) - # Enabling coordinate dependency for higher order geometries - # (not handled anywhere else though). - # if o.has_higher_degree_cell_geometry(): - # deps = deps | frozenset(("x",)) - return deps - - -dd = StringDependencyDefiner() - - -def string_set_criteria(v, keys): - # Using sets of ufl objects - key = dd(v) - for k in keys: - key |= k - return frozenset(key) - - -def partition(G, criteria=string_set_criteria): - V, E = G - n = len(V) - - Vout = G.Vout() - - partitions = defaultdict(list) - keys = [None] * n - for iv, v in enumerate(V): - # Get keys from all outgoing edges - edge_keys = [keys[ii] for ii in Vout[iv]] - - # Calculate key for this vertex - key = criteria(v, edge_keys) - - # Store mappings from key to vertex and back - partitions[key].append(iv) - keys[iv] = key - return partitions, keys diff --git a/ufl/formatting/printing.py b/ufl/formatting/printing.py deleted file mode 100644 index f36c783f6..000000000 --- a/ufl/formatting/printing.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -"""A collection of utility algorithms for printing -of UFL objects, mostly intended for debugging purposes.""" - -# 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, 2014 - -from ufl.log import error -from ufl.core.expr import Expr -from ufl.form import Form -from ufl.integral import Integral - - -# --- Utilities for constructing informative strings from UFL objects - -def integral_info(integral): - if not isinstance(integral, Integral): - error("Expecting an Integral.") - s = " Integral:\n" - s += " Type:\n" - s += " %s\n" % integral.integral_type() - s += " Domain:\n" - s += " %s\n" % integral.ufl_domain() - s += " Domain id:\n" - s += " %s\n" % integral.subdomain_id() - s += " Domain data:\n" - s += " %s\n" % integral.subdomain_data() - s += " Compiler metadata:\n" - s += " %s\n" % integral.metadata() - return s - - -def form_info(form): - if not isinstance(form, Form): - error("Expecting a Form.") - - bf = form.arguments() - cf = form.coefficients() - - s = "Form info:\n" - s += " rank: %d\n" % len(bf) - s += " num_coefficients: %d\n" % len(cf) - s += "\n" - - for f in cf: - if f._name: - s += "\n" - s += " Coefficient %d is named '%s'" % (f._count, f._name) - s += "\n" - - integrals = form.integrals() - integral_types = sorted(set(itg.integral_type() for itg in integrals)) - for integral_type in integral_types: - itgs = form.integrals_by_type(integral_type) - s += " num_{0}_integrals: {1}\n".format(integral_type, len(itgs)) - s += "\n" - - for integral_type in integral_types: - itgs = form.integrals_by_type(integral_type) - for itg in itgs: - s += integral_info(itg) - s += "\n" - - return s - - -def _indent_string(n): - return " " * n - - -def _tree_format_expression(expression, indentation, parentheses): - 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): - 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: - error("Invalid object type %s" % type(expression)) - - return s diff --git a/ufl/formatting/ufl2dot.py b/ufl/formatting/ufl2dot.py deleted file mode 100644 index 0b5309028..000000000 --- a/ufl/formatting/ufl2dot.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -"""A collection of utility algorithms for printing -of UFL objects in the DOT graph visualization language, -mostly intended for debugging purposers.""" - -# 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.log import error -from ufl.core.expr import Expr -from ufl.form import Form -from ufl.variable import Variable -from ufl.algorithms.multifunction import MultiFunction - - -class ReprLabeller(MultiFunction): - def __init__(self): - MultiFunction.__init__(self) - - def terminal(self, e): - return repr(e) - - def operator(self, e): - return e._ufl_class_.__name__.split(".")[-1] - - -class CompactLabeller(ReprLabeller): - def __init__(self, function_mapping=None): - ReprLabeller.__init__(self) - self._function_mapping = function_mapping - - # Terminals: - def scalar_value(self, e): - return repr(e._value) - - def zero(self, e): - return "0" - - def identity(self, e): - return "Id" - - def multi_index(self, e): - return str(e) - - def form_argument(self, e): - return self._function_mapping.get(id(e)) or str(e) - - def geometric_quantity(self, e): - return str(e) - - # Operators: - def sum(self, e): - return "+" - - def product(self, e): - return "*" - - def division(self, e): - return "/" - - def power(self, e): - return "**" - - def math_function(self, e): - return e._name - - def index_sum(self, e): - return "∑" - - def indexed(self, e): - return "[]" - - def component_tensor(self, e): # TODO: Understandable short notation for this? - return "][" - - def negative_restricted(self, e): - return "[-]" - - def positive_restricted(self, e): - return "[+]" - - def cell_avg(self, e): # TODO: Understandable short notation for this? - return "_K_" - - def facet_avg(self, e): # TODO: Understandable short notation for this? - return "_F_" - - def conj(self, e): - return "conj" - - def real(self, e): - return "real" - - def imag(self, e): - return "imag" - - def inner(self, e): - return "inner" - - def dot(self, e): - return "dot" - - def outer(self, e): - return "outer" - - def transposed(self, e): - return "transp." - - def determinant(self, e): - return "det" - - def trace(self, e): - return "tr" - - def dev(self, e): - return "dev" - - def skew(self, e): - return "skew" - - def grad(self, e): - return "grad" - - def div(self, e): - return "div" - - def curl(self, e): - return "curl" - - def nabla_grad(self, e): - return "nabla_grad" - - def nabla_div(self, e): - return "nabla_div" - - def diff(self, e): - return "diff" - - -# Make this class like the ones above to use fancy math symbol labels -class2label = {"IndexSum": "∑", - "Sum": "∑", - "Product": "∏", - "Division": "/", - "Inner": ":", - "Dot": "⋅", - "Outer": "⊗", - "Grad": "grad", - "Div": "div", - "NablaGrad": "∇⊗", - "NablaDiv": "∇⋅", - "Curl": "∇×", } - - -class FancyLabeller(CompactLabeller): - pass - - -def build_entities(e, nodes, edges, nodeoffset, prefix="", labeller=None): - # TODO: Maybe this can be cleaner written using the graph - # utilities. - # TODO: To collapse equal nodes with different objects, do not use - # id as key. Make this an option? - - # Cutoff if we have handled e before - if id(e) in nodes: - return - if labeller is None: - labeller = ReprLabeller() - - # Special-case Variable instances - if isinstance(e, Variable): # FIXME: Is this really necessary? - ops = (e._expression,) - label = "variable %d" % e._label._count - else: - ops = e.ufl_operands - label = labeller(e) - - # Create node for parent e - nodename = "%sn%04d" % (prefix, len(nodes) + nodeoffset) - nodes[id(e)] = (nodename, label) - - # Handle all children recursively - n = len(ops) - if n == 2: - oplabels = ["L", "R"] - elif n > 2: - oplabels = ["op%d" % i for i in range(n)] - else: - oplabels = [None] * n - - for i, o in enumerate(ops): - # Handle entire subtree for expression o - build_entities(o, nodes, edges, nodeoffset, prefix, labeller) - - # Add edge between e and child node o - edges.append((id(e), id(o), oplabels[i])) - - -def format_entities(nodes, edges): - entities = [] - for (nodename, label) in nodes.values(): - node = ' %s [label="%s"];' % (nodename, label) - entities.append(node) - for (aid, bid, label) in edges: - anodename = nodes[aid][0] - bnodename = nodes[bid][0] - if label is None: - edge = ' %s -> %s ;' % (anodename, bnodename) - else: - edge = ' %s -> %s [label="%s"] ;' % (anodename, bnodename, label) - entities.append(edge) - return "\n".join(entities) - - -integralgraphformat = """ %(node)s [label="%(label)s"] - form_%(formname)s -> %(node)s ; - %(node)s -> %(root)s ; -%(entities)s""" - -exprgraphformat = """ digraph ufl_expression - { - %s - }""" - - -def ufl2dot(expression, formname="a", nodeoffset=0, begin=True, end=True, - labeling="repr", object_names=None): - if labeling == "repr": - labeller = ReprLabeller() - elif labeling == "compact": - labeller = CompactLabeller(object_names or {}) - print(object_names) - - if isinstance(expression, Form): - form = expression - - subgraphs = [] - k = 0 - for itg in form.integrals(): - prefix = "itg%d_" % k - integralkey = "%s%s" % (itg.integral_type(), itg.subdomain_id()) - - integrallabel = "%s %s" % (itg.integral_type().capitalize().replace("_", " "), "integral") - integrallabel += " %s" % (itg.subdomain_id(),) - - integrand = itg.integrand() - - nodes = {} - edges = [] - - build_entities(integrand, nodes, edges, nodeoffset, prefix, - labeller) - rootnode = nodes[id(integrand)][0] - entitylist = format_entities(nodes, edges) - integralnode = "%s_%s" % (formname, integralkey) - subgraphs.append(integralgraphformat % { - 'node': integralnode, - 'label': integrallabel, - 'formname': formname, - 'root': rootnode, - 'entities': entitylist, }) - nodeoffset += len(nodes) - - s = "" - if begin: - s += 'digraph ufl_form\n{\n node [shape="box"] ;\n' - s += ' form_%s [label="Form %s"] ;' % (formname, formname) - s += "\n".join(subgraphs) - if end: - s += "\n}" - - elif isinstance(expression, Expr): - nodes = {} - edges = [] - - build_entities(expression, nodes, edges, nodeoffset, '', labeller) - entitylist = format_entities(nodes, edges) - s = exprgraphformat % entitylist - - nodeoffset += len(nodes) - - else: - error("Invalid object type %s" % type(expression)) - - return s, nodeoffset diff --git a/ufl/formatting/ufl2unicode.py b/ufl/formatting/ufl2unicode.py index 9757b878e..5d8d5f0c5 100644 --- a/ufl/formatting/ufl2unicode.py +++ b/ufl/formatting/ufl2unicode.py @@ -1,35 +1,48 @@ -# coding: utf-8 +"""UFL to unicode.""" import numbers import ufl -from ufl.log import error -from ufl.corealg.multifunction import MultiFunction +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.core.multiindex import Index, FixedIndex +from ufl.corealg.multifunction import MultiFunction from ufl.form import Form -from ufl.algorithms import compute_form_data + +try: + import colorama + + has_colorama = True +except ImportError: + has_colorama = False class PrecedenceRules(MultiFunction): - "An enum-like class for C operator precedence levels." + """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 @@ -37,10 +50,13 @@ def call(self, o): 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 @@ -49,10 +65,13 @@ def mathop(self, o): 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 @@ -61,31 +80,42 @@ def product(self, o): 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 @@ -93,140 +123,139 @@ def lowest(self, o): def precedence(expr): + """Get the precedence of an expr.""" return _precrules(expr) -try: - import colorama - has_colorama = True -except ImportError: - has_colorama = False - - class UC: - "An enum-like class for unicode characters." + """An enum-like class for unicode characters.""" # Letters in this alphabet have contiguous code point numbers - bold_math_a = u"𝐚" - bold_math_A = u"𝐀" + bold_math_a = "𝐚" + bold_math_A = "𝐀" - thin_space = u"\u2009" + thin_space = "\u2009" - superscript_plus = u'⁺' - superscript_minus = u'⁻' - superscript_equals = u'⁼' - superscript_left_paren = u'⁽' - superscript_right_paren = u'⁾' + superscript_plus = "⁺" + superscript_minus = "⁻" + superscript_equals = "⁼" + superscript_left_paren = "⁽" + superscript_right_paren = "⁾" superscript_digits = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"] - subscript_plus = u'₊' - subscript_minus = u'₋' - subscript_equals = u'₌' - subscript_left_paren = u'₍' - subscript_right_paren = u'₎' + subscript_plus = "₊" + subscript_minus = "₋" + subscript_equals = "₌" + subscript_left_paren = "₍" + subscript_right_paren = "₎" subscript_digits = ["₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"] - sqrt = u'√' - transpose = u'ᵀ' - - integral = u'∫' - integral_double = u'∬' - integral_triple = u'∭' - integral_contour = u'∮' - integral_surface = u'∯' - integral_volume = u'∰' - - sum = u'∑' - division_slash = '∕' - partial = u'∂' - epsilon = u'ε' - omega = u'ω' - Omega = u'Ω' - gamma = u'γ' - Gamma = u'Γ' - nabla = u'∇' - for_all = u'∀' - - dot = u'⋅' - cross_product = u'⨯' - circled_times = u'⊗' - nary_product = u'∏' - - ne = u'≠' - lt = u'<' - le = u'≤' - gt = u'>' - ge = u'≥' - - logical_and = u'∧' - logical_or = u'∨' - logical_not = u'¬' - - element_of = u'∈' - not_element_of = u'∉' - - left_white_square_bracket = u'⟦' - right_white_squared_bracket = u'⟧' - left_angled_bracket = u'⟨' - right_angled_bracket = u'⟩' - left_double_angled_bracket = u'⟪' - right_double_angled_bracket = u'⟫' - - combining_right_arrow_above = '\u20D7' - combining_overline = '\u0305' + 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(u"A") + ord(UC.bold_math_A)) + c = chr(ord(c) - ord("A") + ord(UC.bold_math_A)) elif ord("a") <= ord(c) <= ord("z"): - c = chr(ord(c) - ord(u"a") + ord(UC.bold_math_a)) + 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): - return u"".join(bolden_letter(c) for c in s) + """Bolden a string.""" + return "".join(bolden_letter(c) for c in s) def overline_string(f): - return u"".join("%s%s" % (c, UC.combining_overline) for c in 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 '' + prefix = UC.subscript_minus if number < 0 else "" number = str(number) - return prefix + ''.join(subscript_digit(c) for c in 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 '' + prefix = UC.superscript_minus if number < 0 else "" number = str(number) - return prefix + ''.join(superscript_digit(c) for c in 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, -} +integral_by_dim = {3: UC.integral_triple, 2: UC.integral_double, 1: UC.integral, 0: UC.integral} integral_type_to_codim = { "cell": 0, @@ -266,6 +295,7 @@ def measure_font(dx): 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 @@ -275,14 +305,15 @@ def get_integral_symbol(integral_type, domain, subdomain_id): # TODO: Render domain description - if isinstance(subdomain_id, numbers.Integral): - istr += subscript_number(int(subdomain_id)) - elif subdomain_id == "everywhere": - pass - elif subdomain_id == "otherwise": - istr += "[rest of domain]" - elif isinstance(subdomain_id, tuple): - istr += ",".join([subscript_number(int(i)) for i in subdomain_id]) + 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) @@ -291,15 +322,12 @@ def get_integral_symbol(integral_type, domain, subdomain_id): def par(s): - return "(%s)" % s - - -def prec(expr): - return 0 # FIXME - # return precedence[expr._ufl_class_] + """Wrap in parentheses.""" + return f"({s})" def is_int(s): + """Check if a value is an integer.""" try: int(s) return True @@ -308,17 +336,18 @@ def is_int(s): def format_index(ii): + """Format an index.""" if isinstance(ii, FixedIndex): - s = "%d" % ii._value + s = f"{ii._value}" elif isinstance(ii, Index): - s = "i%s" % subscript_number(ii._count) + s = "i{subscript_number(ii._count)}" else: - error("Invalid index type %s." % type(ii)) + raise ValueError(f"Invalid index type {type(ii)}.") return s def ufl2unicode(expression): - "Generate Unicode string for a UFL expression or form." + """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 @@ -328,12 +357,13 @@ def ufl2unicode(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): - # formname = formdata.name + """Generate Unicode string for a UFL form.""" argument_names = None coefficient_names = None @@ -341,18 +371,18 @@ def form2unicode(form, formdata): lines = [] integrals = form.integrals() for itg in integrals: - integrand_string = expression2unicode( - itg.integrand(), argument_names, coefficient_names) + integrand_string = expression2unicode(itg.integrand(), argument_names, coefficient_names) istr, dxstr = get_integral_symbol(itg.integral_type(), itg.ufl_domain(), itg.subdomain_id()) - line = "%s %s %s" % (istr, integrand_string, dxstr) + line = f"{istr} {integrand_string} {dxstr}" lines.append(line) - return '\n + '.join(lines) + 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) @@ -366,6 +396,7 @@ def binop(expr, a, b, op, sep=" "): def mathop(expr, arg, opname): + """Format a math operation.""" eprec = precedence(expr) aprec = precedence(expr.ufl_operands[0]) op = opfont(opname) @@ -374,11 +405,14 @@ def mathop(expr, arg, opname): sep = "" else: sep = UC.thin_space - return "%s%s%s" % (op, sep, arg) + 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 @@ -387,34 +421,41 @@ def __init__(self, argument_names=None, coefficient_names=None, colorama_bold=Fa # --- Terminal objects --- def scalar_value(self, o): + """Format a scalar_value.""" if o.ufl_shape and self.colorama_bold: - return "%s%s%s" % (colorama.Style.BRIGHT, o._value, colorama.Style.RESET_ALL) - return "%s" % o._value + 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 "0%s" % UC.combining_right_arrow_above - return "%s0%s" % (colorama.Style.BRIGHT, colorama.Style.RESET_ALL) + 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 "%sI%s" % (colorama.Style.BRIGHT, colorama.Style.RESET_ALL) + 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 "%s%s%s" % (colorama.Style.BRIGHT, UC.epsilon, colorama.Style.RESET_ALL) + return f"{colorama.Style.BRIGHT}{UC.epsilon}{colorama.Style.RESET_ALL}" return UC.epsilon def facet_normal(self, o): - return "n%s" % UC.combining_right_arrow_above + """Format a facet_normal.""" + return f"n{UC.combining_right_arrow_above}" def spatial_coordinate(self, o): - return "x%s" % UC.combining_right_arrow_above + """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: @@ -423,14 +464,15 @@ def argument(self, o): if not o.ufl_shape: return bfn elif len(o.ufl_shape) == 1: - return "%s%s" % (bfn, UC.combining_right_arrow_above) + return f"{bfn}{UC.combining_right_arrow_above}" elif self.colorama_bold: - return "%s%s%s" % (colorama.Style.BRIGHT, bfn, colorama.Style.RESET_ALL) + 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: @@ -439,42 +481,70 @@ def coefficient(self, o): if len(o.ufl_shape) == 1: var += UC.combining_right_arrow_above elif len(o.ufl_shape) > 1 and self.colorama_bold: - var = "%s%s%s" % (colorama.Style.BRIGHT, var, colorama.Style.RESET_ALL) - return "%s%s" % (var, subscript_number(i)) + 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 = "%s%s%s" % (colorama.Style.BRIGHT, var, colorama.Style.RESET_ALL) - return "%s%s" % (var, superscript_number(i)) + 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): - return "l%s" % (subscript_number(o.count()),) + """Format a label.""" + return f"l{subscript_number(o.count())}" # --- Non-terminal objects --- - def variable(self, o, f, l): - return "var(%s,%s)" % (f, l) + 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 "%s[%s]%s" % (UC.sum, i, 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): @@ -484,236 +554,277 @@ def division(self, o, a, b): a = superscript_number(int(a)) else: a = par(a) - return "%s %s %s" % (a, UC.division_slash, b) + return f"{a} {UC.division_slash} {b}" return binop(o, a, b, UC.division_slash) def abs(self, o, a): - return "|%s|" % (a,) + """Format an ans.""" + return f"|{a}|" def transposed(self, o, a): + """Format a transposed.""" a = par(a) - return "%s%s" % (a, UC.transpose) + 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 "%s[%s]" % (A, ii) + return f"{A}[{ii}]" def variable_derivative(self, o, f, v): + """Format a variable_derivative.""" f = par(f) v = par(v) - nom = r"%s%s" % (UC.partial, f) - denom = r"%s%s" % (UC.partial, v) - return par(r"%s%s%s" % (nom, UC.division_slash, denom)) + 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 = r"%s%s" % (UC.partial, f) - denom = r"%s%s" % (UC.partial, w) - return par(r"%s%s%s[%s]" % (nom, UC.division_slash, denom, v)) # TODO: Fix this syntax... + 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 "%s%s%s" % (UC.nabla, UC.thin_space, 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 "%s%s%s%s%s" % (UC.nabla, UC.thin_space, UC.dot, UC.thin_space, 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 "%s%s%s%s%s" % (UC.nabla, UC.thin_space, UC.cross_product, UC.thin_space, 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) - f = par(f) - return "%s%s" % (op, f) + return f"{op}{par(f)}" def sqrt(self, o, f): - f = par(f) - return "%s%s" % (UC.sqrt, f) + """Format a sqrt.""" + return f"{UC.sqrt}{par(f)}" def exp(self, o, f): + """Format a exp.""" op = opfont("exp") - f = par(f) - return "%s%s" % (op, f) + return f"{op}{par(f)}" def atan2(self, o, f1, f2): + """Format a atan2.""" f1 = par(f1) f2 = par(f2) op = opfont("arctan2") - return "%s(%s, %s)" % (op, f1, f2) + return f"{op}({f1}, {f2})" def bessel_j(self, o, nu, f): + """Format a bessel_j.""" op = opfont("J") - f = par(f) nu = subscript_number(int(nu)) - return "%s%s%s" % (op, nu, f) + return f"{op}{nu}{par(f)}" def bessel_y(self, o, nu, f): + """Format a bessel_y.""" op = opfont("Y") - f = par(f) nu = subscript_number(int(nu)) - return "%s%s%s" % (op, nu, f) + return f"{op}{nu}{par(f)}" def bessel_i(self, o, nu, f): + """Format a bessel_i.""" op = opfont("I") - f = par(f) nu = subscript_number(int(nu)) - return "%s%s%s" % (op, nu, f) + return f"{op}{nu}{par(f)}" def bessel_K(self, o, nu, f): + """Format a bessel_K.""" op = opfont("K") - f = par(f) nu = subscript_number(int(nu)) - return "%s%s%s" % (op, nu, f) + 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): - return "%s%s, %s%s" % (UC.left_angled_bracket, a, b, UC.right_angled_bracket) + """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): - return "|%s|" % (A,) + """Format a determinant.""" + return f"|{A}|" def inverse(self, o, A): + """Format an inverse.""" A = par(A) - return "%s%s" % (A, superscript_number(-1)) + 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): - l = ", ".join(ops) # noqa: E741 - return "%s%s%s" % ("[", l, "]") + """Format a list_tensor.""" + return f"[{', '.join(ops)}]" def component_tensor(self, o, A, ii): - return "[%s %s %s]" % (A, UC.for_all, ii) + """Format a component_tensor.""" + return f"[{A} {UC.for_all} {ii}]" def positive_restricted(self, o, f): - f = par(f) - return "%s%s" % (f, UC.superscript_plus) + """Format a positive_restriced.""" + return f"{par(f)}{UC.superscript_plus}" def negative_restricted(self, o, f): - f = par(f) - return "%s%s" % (f, UC.superscript_minus) + """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 "%s%s" % (UC.logical_not, 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") - l = " ".join((t, If, c, Else, f)) # noqa: E741 - return l + return f"{t} {If} {c} {Else} {f}" def min_value(self, o, a, b): + """Format an min_value.""" op = opfont("min") - return "%s(%s, %s)" % (op, a, b) + return f"{op}({a}, {b})" def max_value(self, o, a, b): + """Format an max_value.""" op = opfont("max") - return "%s(%s, %s)" % (op, a, b) + return f"{op}({a}, {b})" def expr_list(self, o, *ops): + """Format an expr_list.""" items = ", ".join(ops) - return "%s %s %s" % (UC.left_white_square_bracket, items, - UC.right_white_squared_bracket) + 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 "%s %s %s" % (UC.left_double_angled_bracket, items, - UC.left_double_angled_bracket) + return f"{UC.left_double_angled_bracket} {items} {UC.left_double_angled_bracket}" def expr(self, o): - raise ValueError("Missing handler for type %s" % str(type(o))) + """Format an expr.""" + raise ValueError(f"Missing handler for type {type(o)}") diff --git a/ufl/formoperators.py b/ufl/formoperators.py index 4fbfd7999..6616c7fff 100644 --- a/ufl/formoperators.py +++ b/ufl/formoperators.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"Various high level ways to transform a complete Form into a new Form." - +"""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) @@ -11,59 +9,69 @@ # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 -from ufl.log import error -from ufl.form import Form, as_form +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.split_functions import split +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.variable import Variable from ufl.finiteelement import MixedElement -from ufl.argument import Argument -from ufl.coefficient import Coefficient -from ufl.differentiation import CoefficientDerivative, CoordinateDerivative -from ufl.constantvalue import is_true_ufl_scalar, as_ufl -from ufl.indexed import Indexed -from ufl.core.multiindex import FixedIndex, MultiIndex -from ufl.tensors import as_tensor, ListTensor -from ufl.sorting import sorted_expr +from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm, as_form from ufl.functionspace import FunctionSpace from ufl.geometry import SpatialCoordinate - -# An exception to the rule that ufl.* does not depend on ufl.algorithms.* ... -from ufl.algorithms import compute_form_adjoint, compute_form_action -from ufl.algorithms import compute_energy_norm -from ufl.algorithms import compute_form_lhs, compute_form_rhs, compute_form_functional -from ufl.algorithms import expand_derivatives, extract_arguments - -# Part of the external interface -from ufl.algorithms import replace # noqa +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): - """UFL form operator: + """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 ufl.algorithms.formsplitter.extract_blocks(form, i, j) + return formsplitter.extract_blocks(form, i, j) def lhs(form): - """UFL form operator: + """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) @@ -71,15 +79,14 @@ def lhs(form): def rhs(form): - """UFL form operator: + """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) @@ -87,40 +94,61 @@ def rhs(form): def system(form): - """UFL form operator: Split a form into the left hand side and right hand - side, see ``lhs`` and ``rhs``.""" + """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? - "UFL form operator: Extract the functional part of form." + """Extract the functional part of form.""" form = as_form(form) form = expand_derivatives(form) return compute_form_functional(form) -def action(form, coefficient=None): - """UFL form operator: +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.""" + 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) - form = expand_derivatives(form) - return compute_form_action(form, coefficient) + 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): - """UFL form operator: + """Get the energy norm. + Given a bilinear form *a* and a coefficient *f*, - return the functional :math:`a(f,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): - """UFL form operator: +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. @@ -129,15 +157,30 @@ def adjoint(form, reordered_arguments=None): 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) - form = expand_derivatives(form) - return compute_form_adjoint(form, reordered_arguments) + 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: - error("Invalid shape.") + raise ValueError("Invalid shape.") elif len(shape) == 1: return [0] * shape[0] else: @@ -145,6 +188,7 @@ def zero_lists(shape): 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]: @@ -154,6 +198,7 @@ def set_list_item(li, i, 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) @@ -162,8 +207,12 @@ def _handle_derivative_arguments(form, coefficient, argument): if argument is None: # Try to create argument if not provided - if not all(isinstance(c, Coefficient) for c in coefficients): - error("Can only create arguments automatically for non-indexed coefficients.") + 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 @@ -181,19 +230,19 @@ def _handle_derivative_arguments(form, coefficient, argument): # in that case parts = set(arg.part() for arg in form_arguments) if len(parts - {None}) != 0: - error("Not expecting parts here, provide your own arguments.") + 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] - domains = [fs.ufl_domain() for fs in function_spaces] - elements = [fs.ufl_element() for fs in function_spaces] 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) + elm = MixedElement(elements) fs = FunctionSpace(domains[0], elm) arguments = split(Argument(fs, number, part)) else: @@ -212,19 +261,19 @@ def _handle_derivative_arguments(form, coefficient, argument): # Build mapping from coefficient to argument m = {} - for (c, a) in zip(coefficients, arguments): + for c, a in zip(coefficients, arguments): if c.ufl_shape != a.ufl_shape: - error("Coefficient and argument shapes do not match!") - if isinstance(c, Coefficient) or isinstance(c, SpatialCoordinate): + 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): - error("Invalid coefficient type for %s" % ufl_err_str(c)) + raise ValueError(f"Invalid coefficient type for {ufl_err_str(c)}") f, i = c.ufl_operands if not isinstance(f, Coefficient): - error("Expecting an indexed coefficient, not %s" % ufl_err_str(f)) + 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)): - error("Expecting one or more fixed indices, not %s" % ufl_err_str(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] = {} @@ -246,9 +295,7 @@ def _handle_derivative_arguments(form, coefficient, argument): def derivative(form, coefficient, argument=None, coefficient_derivatives=None): - """UFL form operator: - Compute the Gateaux derivative of *form* w.r.t. *coefficient* in direction - of *argument*. + """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 @@ -268,10 +315,38 @@ def derivative(form, coefficient, argument=None, coefficient_derivatives=None): 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) - + coefficients, arguments = _handle_derivative_arguments(form, coefficient, argument) if coefficient_derivatives is None: coefficient_derivatives = ExprMapping() else: @@ -284,30 +359,53 @@ def derivative(form, coefficient, argument=None, coefficient_derivatives=None): if isinstance(form, Form): integrals = [] for itg in form.integrals(): - if not isinstance(coefficient, SpatialCoordinate): - fd = CoefficientDerivative(itg.integrand(), coefficients, - arguments, coefficient_derivatives) + 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 = CoordinateDerivative(itg.integrand(), coefficients, - arguments, coefficient_derivatives) + 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) + return CoefficientDerivative(form, coefficients, arguments, coefficient_derivatives) else: - return CoordinateDerivative(form, coefficients, - arguments, coefficient_derivatives) + return CoordinateDerivative(form, coefficients, arguments, coefficient_derivatives) - error("Invalid argument type %s." % str(type(form))) + raise ValueError(f"Invalid argument type {type(form)}.") def sensitivity_rhs(a, u, L, v): - """UFL form operator: - Compute the right hand side for a sensitivity calculation system. + 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 @@ -333,8 +431,8 @@ def sensitivity_rhs(a, u, L, v): :: v = variable(v_expression) - L = IL(v)*dx - a = Ia(v)*dx + L = IL(v) * dx + a = Ia(v) * dx where ``IL`` and ``Ia`` are integrand expressions. Define a ``Coefficient u`` representing the solution @@ -361,12 +459,17 @@ def sensitivity_rhs(a, u, L, v): dL = sensitivity_rhs(a, u, L, v) """ - if not (isinstance(a, Form) and - isinstance(u, Coefficient) and - isinstance(L, Form) and - isinstance(v, Variable)): - error("Expecting (a, u, L, v), (bilinear form, function, linear form and scalar variable).") + 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): - error("Expecting scalar variable.") + raise ValueError("Expecting scalar variable.") from ufl.operators import diff + return diff(L, v) - action(diff(a, v), u) diff --git a/ufl/functionspace.py b/ufl/functionspace.py index 2758b2e7d..834734274 100644 --- a/ufl/functionspace.py +++ b/ufl/functionspace.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Types for representing function spaces." +"""Types for representing function spaces.""" # Copyright (C) 2015-2016 Martin Sandve Alnæs # @@ -10,65 +9,112 @@ # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 -from ufl.log import error -from ufl.core.ufl_type import attach_operators_from_hash_data +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): - raise NotImplementedError("Missing implementation of IFunctionSpace.ufl_sub_spaces in %s." % self.__class__.__name__) + """Return ufl sub spaces.""" + raise NotImplementedError( + f"Missing implementation of ufl_sub_spaces in {self.__class__.__name__}." + ) -@attach_operators_from_hash_data -class FunctionSpace(AbstractFunctionSpace): - def __init__(self, domain, element): +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? + # TODO: Is anything expected from element.cell in this case? pass else: try: domain_cell = domain.ufl_cell() except AttributeError: - error("Expected non-abstract domain for initalization of function space.") + raise ValueError( + "Expected non-abstract domain for initalization of function space." + ) else: - if element.cell() != domain_cell: - error("Non-matching cell of finite element and domain.") - + 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 ufl sub spaces.""" return () def ufl_domain(self): - "Return ufl domain." + """Return ufl domain.""" return self._ufl_domain def ufl_element(self): - "Return ufl element." + """Return ufl element.""" return self._ufl_element def ufl_domains(self): - "Return ufl domains." + """Return ufl domains.""" domain = self.ufl_domain() if domain is None: return () else: return (domain,) - def _ufl_hash_data_(self): + 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: @@ -79,9 +125,11 @@ def _ufl_hash_data_(self): edata = None else: edata = element._ufl_hash_data_() - return ("FunctionSpace", ddata, edata) + return (name, ddata, edata, self.label()) - def _ufl_signature_data_(self, renumbering): + 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: @@ -92,91 +140,210 @@ def _ufl_signature_data_(self, renumbering): edata = None else: edata = element._ufl_signature_data_() - return ("FunctionSpace", ddata, edata) + return (name, ddata, edata, self.label()) def __repr__(self): - r = "FunctionSpace(%s, %s)" % (repr(self._ufl_domain), repr(self._ufl_element)) - return r + """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.""" -@attach_operators_from_hash_data -class TensorProductFunctionSpace(AbstractFunctionSpace): 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): - return ("TensorProductFunctionSpace",) + tuple(V._ufl_hash_data_() for V in self.ufl_sub_spaces()) + """UFL hash data.""" + return ("TensorProductFunctionSpace",) + tuple( + V._ufl_hash_data_() for V in self.ufl_sub_spaces() + ) def _ufl_signature_data_(self, renumbering): - return ("TensorProductFunctionSpace",) + tuple(V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces()) + """UFL signature data.""" + return ("TensorProductFunctionSpace",) + tuple( + V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces() + ) def __repr__(self): - r = "TensorProductFunctionSpace(*%s)" % repr(self._ufl_function_spaces) - return r + """Representation.""" + return f"TensorProductFunctionSpace(*{self._ufl_function_spaces!r})" + + def __str__(self): + """String.""" + return self.__repr__() -@attach_operators_from_hash_data -class MixedFunctionSpace(AbstractFunctionSpace): +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, FunctionSpace): + if isinstance(fs, BaseFunctionSpace): self._ufl_elements.append(fs.ufl_element()) else: - error("Expecting FunctionSpace objects") + 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 ufl sub spaces.""" return self._ufl_function_spaces def ufl_sub_space(self, i): - "Return i-th ufl sub space." + """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 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: - error("""Found multiple elements. Cannot return only one. - Consider building a FunctionSpace from a MixedElement - in case of homogeneous dimension.""") + 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." + """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." + """Return ufl domain.""" domains = self.ufl_domains() if len(domains) == 1: return domains[0] elif domains: - error("Found multiple domains, cannot return just one.") + 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): - return ("MixedFunctionSpace",) + tuple(V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces()) + """UFL signature data.""" + return ("MixedFunctionSpace",) + tuple( + V._ufl_signature_data_(renumbering) for V in self.ufl_sub_spaces() + ) def __repr__(self): - r = "MixedFunctionSpace(*%s)" % repr(self._ufl_function_spaces) - return r + """Representation.""" + return f"MixedFunctionSpace(*{self._ufl_function_spaces!r})" + + def __str__(self): + """String.""" + return self.__repr__() diff --git a/ufl/geometry.py b/ufl/geometry.py index 56505100e..642ae965e 100644 --- a/ufl/geometry.py +++ b/ufl/geometry.py @@ -1,24 +1,17 @@ -# -*- coding: utf-8 -*- -"Types for representing symbolic expressions for geometric quantities." +"""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 -# -# Modified by Anders Logg, 2009. -# Modified by Kristian B. Oelgaard, 2009 -# Modified by Marie E. Rognes 2012 -# Modified by Massimiliano Leoni, 2016 -from ufl.log import error -from ufl.core.ufl_type import ufl_type from ufl.core.terminal import Terminal -from ufl.domain import as_domain +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] @@ -114,17 +107,21 @@ @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 (or over each facet for facet quantities)." + """Return whether this expression is spatially constant over each cell.""" # NB! Geometric quantities are piecewise constant by # default. Override if needed. return True @@ -134,30 +131,38 @@ def is_cellwise_constant(self): ufl_shape = () def _ufl_signature_data_(self, renumbering): - "Signature data of geometric quantities depend on the domain numbering." + """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__ = () @@ -168,9 +173,10 @@ class GeometricEdgeQuantity(GeometricQuantity): # --- Coordinate represented in different coordinate systems + @ufl_type() class SpatialCoordinate(GeometricCellQuantity): - """UFL geometry representation: The coordinate in a domain. + """The coordinate in a domain. In the context of expression integration, represents the domain coordinate of each quadrature point. @@ -178,24 +184,24 @@ class SpatialCoordinate(GeometricCellQuantity): 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).""" + """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." + """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." + """Return the value of the coordinate.""" if component == (): if isinstance(x, (tuple, list)): return float(x[0]) @@ -205,6 +211,7 @@ def evaluate(self, x, mapping, component, index_values): 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 @@ -212,7 +219,7 @@ def count(self): @ufl_type() class CellCoordinate(GeometricCellQuantity): - """UFL geometry representation: The coordinate in a reference cell. + """The coordinate in a reference cell. In the context of expression integration, represents the reference cell coordinate of each quadrature point. @@ -220,16 +227,18 @@ class CellCoordinate(GeometricCellQuantity): 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." + """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 @@ -237,7 +246,7 @@ def is_cellwise_constant(self): @ufl_type() class FacetCoordinate(GeometricFacetQuantity): - """UFL geometry representation: The coordinate in a reference cell of a facet. + """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. @@ -245,22 +254,25 @@ class FacetCoordinate(GeometricFacetQuantity): 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: - error("FacetCoordinate is only defined for topological dimensions >= 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." + """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() @@ -277,6 +289,7 @@ class EdgeCoordinate(GeometricEdgeQuantity): In the context of expression evaluation in a point on an edge, represents that point in the reference coordinate system of the edge. """ + __slots__ = () name = "Xe" @@ -292,35 +305,41 @@ def ufl_shape(self): return (t - 2,) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """Return whether this expression is spatially constant over each cell.""" return False # --- Origin of coordinate systems in larger coordinate systems + @ufl_type() class CellOrigin(GeometricCellQuantity): - """UFL geometry representation: The spatial coordinate corresponding to origin of a reference cell.""" + """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): - """UFL geometry representation: The spatial coordinate corresponding to origin of a reference facet.""" + """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,) @@ -328,6 +347,7 @@ def ufl_shape(self): @ufl_type() class EdgeOrigin(GeometricEdgeQuantity): """UFL geometry representation: The spatial coordinate corresponding to origin of a reference edge.""" + __slots__ = () name = "x0e" @@ -339,12 +359,14 @@ def ufl_shape(self): @ufl_type() class CellFacetOrigin(GeometricFacetQuantity): - """UFL geometry representation: The reference cell coordinate corresponding to origin of a reference facet.""" + """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,) @@ -352,6 +374,7 @@ def ufl_shape(self): @ufl_type() class CellEdgeOrigin(GeometricEdgeQuantity): """UFL geometry representation: The reference cell coordinate corresponding to origin of a reference edge.""" + __slots__ = () name = "X0e" @@ -363,57 +386,60 @@ def ufl_shape(self): # --- Jacobians of mappings between coordinate systems + @ufl_type() class Jacobian(GeometricCellQuantity): - """UFL geometry representation: The Jacobian of the mapping from reference cell to spatial coordinates. + 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).""" + """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." + """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): - """UFL geometry representation: The Jacobian of the mapping from reference facet to spatial coordinates. + """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: - error("FacetJacobian is only defined for topological dimensions >= 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." + """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() @@ -430,6 +456,7 @@ class EdgeJacobian(GeometricEdgeQuantity): EJ = dx/dXe = dx/dX dX/dXe = J * CEJ """ + __slots__ = () name = "EJ" @@ -446,7 +473,7 @@ def ufl_shape(self): return (g, t - 2) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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() @@ -454,26 +481,29 @@ def is_cellwise_constant(self): @ufl_type() class CellFacetJacobian(GeometricFacetQuantity): # dX/dXf - """UFL geometry representation: The Jacobian of the mapping from reference facet to reference cell coordinates. + """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: - error("CellFacetJacobian is only defined for topological dimensions >= 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." + """Return whether this expression is spatially constant over each cell.""" # This is always a constant mapping between two reference # coordinate systems. return True @@ -485,6 +515,7 @@ class CellEdgeJacobian(GeometricEdgeQuantity): # dX/dXe CEJ_ij = dX_i/dXe_j """ + __slots__ = () name = "CEJ" @@ -500,7 +531,7 @@ def ufl_shape(self): return (t, t - 2) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """Return whether this expression is spatially constant over each cell.""" # This is always a constant mapping between two reference # coordinate systems. return True @@ -512,6 +543,7 @@ class FacetEdgeJacobian(GeometricEdgeQuantity): # dXf/dXe FEJ_ij = dXf_i/dXe_j """ + __slots__ = () name = "FEJ" @@ -527,7 +559,7 @@ def ufl_shape(self): return (t - 1, t - 2) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """Return whether this expression is spatially constant over each cell.""" # This is always a constant mapping between two reference # coordinate systems. return True @@ -535,44 +567,50 @@ def is_cellwise_constant(self): @ufl_type() class ReferenceCellEdgeVectors(GeometricCellQuantity): - """UFL geometry representation: The vectors between reference cell vertices for each edge in cell.""" + """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: - error("CellEdgeVectors is only defined for topological dimensions >= 2.") + raise ValueError("CellEdgeVectors is only defined for topological dimensions >= 2.") @property def ufl_shape(self): - cell = self.ufl_domain().ufl_cell() + """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." + """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): - """UFL geometry representation: The vectors between reference cell vertices for each edge in current facet.""" + """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: - error("FacetEdgeVectors is only defined for topological dimensions >= 3.") + raise ValueError("FacetEdgeVectors is only defined for topological dimensions >= 3.") @property def ufl_shape(self): - cell = self.ufl_domain().ufl_cell() + """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 @@ -584,73 +622,85 @@ def ufl_shape(self): return (nfe, t) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: Physical cell vertices.""" + """Physical cell vertices.""" + __slots__ = () name = "CV" def __init__(self, domain): + """Initialise.""" GeometricCellQuantity.__init__(self, domain) @property def ufl_shape(self): - cell = self.ufl_domain().ufl_cell() + """Get the UFL shape.""" + domain = extract_unique_domain(self) + cell = domain.ufl_cell() nv = cell.num_vertices() - g = cell.geometric_dimension() + g = domain.geometric_dimension() return (nv, g) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: The vectors between physical cell vertices for each edge in cell.""" + """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: - error("CellEdgeVectors is only defined for topological dimensions >= 2.") + raise ValueError("CellEdgeVectors is only defined for topological dimensions >= 2.") @property def ufl_shape(self): - cell = self.ufl_domain().ufl_cell() + """Get the UFL shape.""" + domain = extract_unique_domain(self) + cell = domain.ufl_cell() ne = cell.num_edges() - g = cell.geometric_dimension() + g = domain.geometric_dimension() return (ne, g) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: The vectors between physical cell vertices for each edge in current facet.""" + """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: - error("FacetEdgeVectors is only defined for topological dimensions >= 3.") + raise ValueError("FacetEdgeVectors is only defined for topological dimensions >= 3.") @property def ufl_shape(self): - cell = self.ufl_domain().ufl_cell() + """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 @@ -658,28 +708,31 @@ def ufl_shape(self): raise Exception(f"Cell type {cell} not supported.") nfe = facet_types[0].num_edges() - g = cell.geometric_dimension() + g = domain.geometric_dimension() return (nfe, g) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: The determinant of the Jacobian. + """The determinant of the Jacobian. - Represents the signed determinant of a square Jacobian or the pseudo-determinant of a non-square 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." + """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() @@ -687,12 +740,13 @@ def is_cellwise_constant(self): @ufl_type() class FacetJacobianDeterminant(GeometricFacetQuantity): - """UFL geometry representation: The pseudo-determinant of the FacetJacobian.""" + """The pseudo-determinant of the FacetJacobian.""" + __slots__ = () name = "detFJ" def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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() @@ -700,23 +754,25 @@ def is_cellwise_constant(self): @ufl_type() class EdgeJacobianDeterminant(GeometricEdgeQuantity): """UFL geometry representation: The pseudo-determinant of the EdgeJacobian.""" + __slots__ = () name = "detEJ" def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: The pseudo-determinant of the CellFacetJacobian.""" + """The pseudo-determinant of the CellFacetJacobian.""" + __slots__ = () name = "detCFJ" def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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() @@ -725,11 +781,12 @@ def is_cellwise_constant(self): @ufl_type() class CellEdgeJacobianDeterminant(GeometricEdgeQuantity): """UFL geometry representation: The pseudo-determinant of the CellEdgeJacobian.""" + __slots__ = () name = "detCEJ" def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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() @@ -737,25 +794,27 @@ def is_cellwise_constant(self): # --- Inverses (signed or pseudo) of geometry mapping Jacobians + @ufl_type() class JacobianInverse(GeometricCellQuantity): - """UFL geometry representation: The inverse of the Jacobian. + """The inverse of the Jacobian. - Represents the inverse of a square Jacobian or the pseudo-inverse of a non-square 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).""" + """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." + """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() @@ -763,24 +822,29 @@ def is_cellwise_constant(self): @ufl_type() class FacetJacobianInverse(GeometricFacetQuantity): - """UFL geometry representation: The pseudo-inverse of the FacetJacobian.""" + """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: - error("FacetJacobianInverse is only defined for topological dimensions >= 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." + """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() @@ -789,6 +853,7 @@ def is_cellwise_constant(self): @ufl_type() class EdgeJacobianInverse(GeometricEdgeQuantity): """UFL geometry representation: The pseudo-inverse of the EdgeJacobian.""" + __slots__ = () name = "EK" @@ -805,7 +870,7 @@ def ufl_shape(self): return (t - 2, g) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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() @@ -813,23 +878,28 @@ def is_cellwise_constant(self): @ufl_type() class CellFacetJacobianInverse(GeometricFacetQuantity): - """UFL geometry representation: The pseudo-inverse of the CellFacetJacobian.""" + """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: - error("CellFacetJacobianInverse is only defined for topological dimensions >= 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." + """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() @@ -837,6 +907,7 @@ def is_cellwise_constant(self): @ufl_type() class CellEdgeJacobianInverse(GeometricEdgeQuantity): """UFL geometry representation: The pseudo-inverse of the EdgeFacetJacobian.""" + __slots__ = () name = "CEK" @@ -852,150 +923,91 @@ def ufl_shape(self): return (t - 2, t) def is_cellwise_constant(self): - "Return whether this expression is spatially constant over each cell." + """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): - """UFL geometry representation: The outwards pointing normal vector of the current facet.""" + """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).""" + """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." + """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_. - is_piecewise_linear = self._domain.ufl_coordinate_element().degree() == 1 + 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): - """UFL geometry representation: The upwards pointing normal vector of the current manifold cell.""" + """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).""" + """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." + """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): - """UFL geometry representation: The outwards pointing normal vector of the current facet on the reference cell""" + """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,) -# TODO: Implement in apply_geometry_lowering and enable -# @ufl_type() -# class FacetTangents(GeometricFacetQuantity): -# """UFL geometry representation: The tangent vectors of the current facet.""" -# __slots__ = () -# name = "t" -# -# def __init__(self, domain): -# GeometricFacetQuantity.__init__(self, domain) -# t = self._domain.topological_dimension() -# if t < 2: -# error("FacetTangents is only defined for topological dimensions >= 2.") -# -# @property -# def ufl_shape(self): -# g = self._domain.geometric_dimension() -# t = self._domain.topological_dimension() -# return (t-1,g) -# -# def is_cellwise_constant(self): # NB! Copied from FacetNormal -# "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_. -# is_piecewise_linear = self._domain.ufl_coordinate_element().degree() == 1 -# return is_piecewise_linear and self._domain.ufl_cell().has_simplex_facets() - -# TODO: Implement in apply_geometry_lowering and enable -# @ufl_type() -# class CellTangents(GeometricCellQuantity): -# """UFL geometry representation: The tangent vectors of the current manifold cell.""" -# __slots__ = () -# name = "cell_tangents" -# -# @property -# def ufl_shape(self): -# g = self._domain.geometric_dimension() -# t = self._domain.topological_dimension() -# return (t,g) - - -# --- Types representing midpoint coordinates - -# TODO: Implement in the rest of fenics -# @ufl_type() -# class CellMidpoint(GeometricCellQuantity): -# """UFL geometry representation: The midpoint coordinate of the current cell.""" -# __slots__ = () -# name = "cell_midpoint" -# -# @property -# def ufl_shape(self): -# g = self._domain.geometric_dimension() -# return (g,) - -# TODO: Implement in the rest of fenics -# @ufl_type() -# class FacetMidpoint(GeometricFacetQuantity): -# """UFL geometry representation: The midpoint coordinate of the current facet.""" -# __slots__ = () -# name = "facet_midpoint" -# -# @property -# def ufl_shape(self): -# g = self._domain.geometric_dimension() -# return (g,) - -# --- Types representing measures of the cell and entities of the cell, typically used for stabilisation terms +# --- 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): - """UFL geometry representation: The volume of the reference cell.""" + """The volume of the reference cell.""" + __slots__ = () name = "reference_cell_volume" @ufl_type() class ReferenceFacetVolume(GeometricFacetQuantity): - """UFL geometry representation: The volume of the reference cell of the current facet.""" + """The volume of the reference cell of the current facet.""" + __slots__ = () name = "reference_facet_volume" @@ -1003,79 +1015,81 @@ class ReferenceFacetVolume(GeometricFacetQuantity): @ufl_type() class ReferenceEdgeVolume(GeometricEdgeQuantity): """UFL geometry representation: The volume of the reference cell of the current edge.""" + __slots__ = () name = "reference_edge_volume" @ufl_type() class CellVolume(GeometricCellQuantity): - """UFL geometry representation: The volume of the cell.""" + """The volume of the cell.""" + __slots__ = () name = "volume" @ufl_type() class Circumradius(GeometricCellQuantity): - """UFL geometry representation: The circumradius of the cell.""" + """The circumradius of the cell.""" + __slots__ = () name = "circumradius" @ufl_type() class CellDiameter(GeometricCellQuantity): - """UFL geometry representation: The diameter of the cell, i.e., - maximal distance of two points in the cell.""" + """The diameter of the cell, i.e., maximal distance of two points in the cell.""" + __slots__ = () name = "diameter" -# @ufl_type() -# class CellSurfaceArea(GeometricCellQuantity): -# """UFL geometry representation: The total surface area of the cell.""" -# __slots__ = () -# name = "surfacearea" - - @ufl_type() class FacetArea(GeometricFacetQuantity): # FIXME: Should this be allowed for interval domain? - """UFL geometry representation: The area of the facet.""" + """The area of the facet.""" + __slots__ = () name = "facetarea" @ufl_type() class MinCellEdgeLength(GeometricCellQuantity): - """UFL geometry representation: The minimum edge length of the cell.""" + """The minimum edge length of the cell.""" + __slots__ = () name = "mincelledgelength" @ufl_type() class MaxCellEdgeLength(GeometricCellQuantity): - """UFL geometry representation: The maximum edge length of the cell.""" + """The maximum edge length of the cell.""" + __slots__ = () name = "maxcelledgelength" @ufl_type() class MinFacetEdgeLength(GeometricFacetQuantity): - """UFL geometry representation: The minimum edge length of the facet.""" + """The minimum edge length of the facet.""" + __slots__ = () name = "minfacetedgelength" @ufl_type() class MaxFacetEdgeLength(GeometricFacetQuantity): - """UFL geometry representation: The maximum edge length of the facet.""" + """The maximum edge length of the facet.""" + __slots__ = () name = "maxfacetedgelength" # --- Types representing other stuff + @ufl_type() class CellOrientation(GeometricCellQuantity): - """UFL geometry representation: The orientation (+1/-1) of the current cell. + """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 @@ -1084,13 +1098,15 @@ class CellOrientation(GeometricCellQuantity): 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): - """UFL geometry representation: The orientation (+1/-1) of the current facet relative to the reference cell.""" + """The orientation (+1/-1) of the current facet relative to the reference cell.""" + __slots__ = () name = "facet_orientation" @@ -1099,14 +1115,15 @@ class FacetOrientation(GeometricFacetQuantity): # terminal types instead? @ufl_type() class QuadratureWeight(GeometricQuantity): - """UFL geometry representation: The current quadrature weight. + """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." + """Return whether this expression is spatially constant over each cell.""" # The weight usually varies with the quadrature points return False diff --git a/ufl/index_combination_utils.py b/ufl/index_combination_utils.py index 166badb98..8bd5087a8 100644 --- a/ufl/index_combination_utils.py +++ b/ufl/index_combination_utils.py @@ -1,22 +1,20 @@ -# -*- coding: utf-8 -*- -"Utilities for analysing and manipulating free index tuples" - +"""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.log import error 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): - """Given a list of (id, dim) tuples already sorted by id, + """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. """ @@ -28,7 +26,7 @@ def unique_sorted_indices(indices): prev = i else: if i[1] != prev[1]: - error("Nonmatching dimensions for free indices with same id!") + raise ValueError("Nonmatching dimensions for free indices with same id!") return tuple(newindices) @@ -40,7 +38,6 @@ def merge_unique_indices(afi, afid, bfi, bfid): return a unique list with duplicates removed. Also checks that the dimensions of duplicates are matching. """ - na = len(afi) nb = len(bfi) @@ -84,8 +81,7 @@ def merge_unique_indices(afi, afid, bfi, bfid): def remove_indices(fi, fid, rfi): - """ - """ + """Remove indices.""" if not rfi: return fi, fid @@ -114,7 +110,7 @@ def remove_indices(fi, fid, rfi): # Expecting to find each index from rfi in fi if not removed: - error("Index to be removed ({0}) not part of indices ({1}).".format(rk, fi)) + raise ValueError(f"Index to be removed ({rk}) not part of indices ({fi}).") # Next to remove k += 1 @@ -133,6 +129,7 @@ def remove_indices(fi, fid, rfi): def create_slice_indices(component, shape, fi): + """Create slice indices.""" all_indices = [] slice_indices = [] repeated_indices = [] @@ -146,15 +143,15 @@ def create_slice_indices(component, shape, fi): free_indices.append(ind) elif isinstance(ind, FixedIndex): if int(ind) >= shape[len(all_indices)]: - error("Index out of bounds.") + raise ValueError("Index out of bounds.") all_indices.append(ind) elif isinstance(ind, int): if int(ind) >= shape[len(all_indices)]: - error("Index out of bounds.") + raise ValueError("Index out of bounds.") all_indices.append(FixedIndex(ind)) elif isinstance(ind, slice): if ind != slice(None): - error("Only full slices (:) allowed.") + raise ValueError("Only full slices (:) allowed.") i = Index() slice_indices.append(i) all_indices.append(i) @@ -164,10 +161,10 @@ def create_slice_indices(component, shape, fi): slice_indices.extend(ii) all_indices.extend(ii) else: - error("Not expecting {0}.".format(ind)) + raise ValueError(f"Not expecting {ind}.") if len(all_indices) != len(shape): - error("Component and shape length don't match.") + raise ValueError("Component and shape length don't match.") return tuple(all_indices), tuple(slice_indices), tuple(repeated_indices) @@ -177,12 +174,9 @@ 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 @@ -195,7 +189,7 @@ def merge_nonoverlapping_indices(a, b): free_indices, index_dimensions = zip(*s) # Consistency checks if len(set(free_indices)) != len(free_indices): - error("Not expecting repeated indices.") + raise ValueError("Not expecting repeated indices.") else: free_indices, index_dimensions = (), () return free_indices, index_dimensions @@ -206,12 +200,9 @@ 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) @@ -238,8 +229,13 @@ def merge_overlapping_indices(afi, afid, bfi, bfid): # Consistency checks if len(set(free_indices)) != len(free_indices): - error("Not expecting repeated indices left.") + raise ValueError("Not expecting repeated indices left.") if len(free_indices) + 2 * len(repeated_indices) != an + bn: - error("Expecting only twice repeated indices.") - - return tuple(free_indices), tuple(index_dimensions), tuple(repeated_indices), tuple(repeated_index_dimensions) + raise ValueError("Expecting only twice repeated indices.") + + return ( + tuple(free_indices), + tuple(index_dimensions), + tuple(repeated_indices), + tuple(repeated_index_dimensions), + ) diff --git a/ufl/indexed.py b/ufl/indexed.py index ed337ee57..9f6525863 100644 --- a/ufl/indexed.py +++ b/ufl/indexed.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module defines the Indexed class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -7,26 +6,26 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import error from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str -from ufl.core.ufl_type import ufl_type +from ufl.core.multiindex import FixedIndex, Index, MultiIndex from ufl.core.operator import Operator -from ufl.core.multiindex import Index, FixedIndex, MultiIndex -from ufl.index_combination_utils import unique_sorted_indices, merge_unique_indices +from ufl.core.ufl_type import ufl_type +from ufl.index_combination_utils import unique_sorted_indices from ufl.precedence import parstr -# --- Indexed expression --- - @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 @@ -48,49 +47,43 @@ def __new__(cls, expression, multiindex): return Operator.__new__(cls) def __init__(self, expression, multiindex): + """Initialise.""" # Store operands Operator.__init__(self, (expression, multiindex)) # Error checking if not isinstance(expression, Expr): - error("Expecting Expr instance, not %s." % ufl_err_str(expression)) + raise ValueError(f"Expecting Expr instance, not {ufl_err_str(expression)}.") if not isinstance(multiindex, MultiIndex): - error("Expecting MultiIndex instance, not %s." % ufl_err_str(multiindex)) + raise ValueError(f"Expecting MultiIndex instance, not {ufl_err_str(multiindex)}.") shape = expression.ufl_shape # Error checking if len(shape) != len(multiindex): - error("Invalid number of indices (%d) for tensor " - "expression of rank %d:\n\t%s\n" - % (len(multiindex), len(expression.ufl_shape), 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)): - error("Fixed index out of range!") + 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 - if 1: - 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 = (), () - + 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: - mfiid = [(ind.count(), shape[pos]) - for pos, ind in enumerate(multiindex._indices) - if isinstance(ind, Index)] - mfi, mfid = zip(*mfiid) if mfiid else ((), ()) - fi, fid = merge_unique_indices(expression.ufl_free_indices, - expression.ufl_index_dimensions, - mfi, mfid) + fi, fid = (), () # Cache free index and dimensions self.ufl_free_indices = fi @@ -99,6 +92,7 @@ def __init__(self, expression, multiindex): 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: @@ -107,12 +101,16 @@ def evaluate(self, x, mapping, component, index_values, derivatives=()): return A.evaluate(x, mapping, component, index_values) def __str__(self): - return "%s[%s]" % (parstr(self.ufl_operands[0], self), - self.ufl_operands[1]) + """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 - error("Attempting to index with %s, but object is already indexed: %s" % (ufl_err_str(key), ufl_err_str(self))) + raise ValueError( + f"Attempting to index with {ufl_err_str(key)}, " + f"but object is already indexed: {ufl_err_str(self)}" + ) diff --git a/ufl/indexsum.py b/ufl/indexsum.py index 0d7f65bde..38b0e1cce 100644 --- a/ufl/indexsum.py +++ b/ufl/indexsum.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module defines the IndexSum class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -7,76 +6,79 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - -from ufl.log import error -from ufl.core.ufl_type import ufl_type +from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str -from ufl.core.operator import Operator 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 -from ufl.constantvalue import Zero - # --- Sum over an index --- + @ufl_type(num_ops=2) class IndexSum(Operator): - __slots__ = ("_dimension", - "ufl_free_indices", - "ufl_index_dimensions",) + """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): - error("Expecting Expr instance, got %s" % ufl_err_str(summand)) + raise ValueError(f"Expecting Expr instance, got {ufl_err_str(summand)}") if not isinstance(index, MultiIndex): - error("Expecting MultiIndex instance, got %s" % ufl_err_str(index)) + raise ValueError(f"Expecting MultiIndex instance, got {ufl_err_str(index)}") if len(index) != 1: - error("Expecting a single Index but got %d." % len(index)) + raise ValueError(f"Expecting a single Index but got {len(index)}.") # Simplification to zero if isinstance(summand, Zero): sh = summand.ufl_shape - j, = index + (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:] + 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): - j, = 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:] + 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): - i, = self.ufl_operands[1] + """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) + tmp += self.ufl_operands[0].evaluate(x, mapping, component, index_values) index_values.pop() return tmp def __str__(self): - return "sum_{%s} %s " % (str(self.ufl_operands[1]), - parstr(self.ufl_operands[0], self)) + """Format as a string.""" + return "sum_{%s} %s " % (str(self.ufl_operands[1]), parstr(self.ufl_operands[0], self)) diff --git a/ufl/integral.py b/ufl/integral.py index 5fc3e6efd..510f4ebd5 100644 --- a/ufl/integral.py +++ b/ufl/integral.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """The Integral class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -11,10 +10,8 @@ # Modified by Massimiliano Leoni, 2016. import ufl -from ufl.log import error -from ufl.core.expr import Expr from ufl.checks import is_python_scalar, is_scalar_constant_expression -from ufl.measure import Measure # noqa +from ufl.core.expr import Expr from ufl.protocols import id_or_none # Export list for ufl.classes @@ -22,18 +19,21 @@ 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): + """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): - error("Expecting integrand to be an Expr instance.") + raise ValueError("Expecting integrand to be an Expr instance.") self._integrand = integrand self._integral_type = integral_type self._ufl_domain = domain @@ -41,18 +41,21 @@ def __init__(self, integrand, integral_type, domain, 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. + 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() @@ -69,74 +72,83 @@ def reconstruct(self, integrand=None, return Integral(integrand, integral_type, domain, subdomain_id, metadata, subdomain_data) def integrand(self): - "Return the integrand expression, which is an ``Expr`` instance." + """Return the integrand expression, which is an ``Expr`` instance.""" return self._integrand def integral_type(self): - "Return the domain type of this integral." + """Return the domain type of this integral.""" return self._integral_type def ufl_domain(self): - "Return the integration domain of this integral." + """Return the integration domain of this integral.""" return self._ufl_domain def subdomain_id(self): - "Return the subdomain id of this integral." + """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 the compiler metadata this integral has been annotated with.""" return self._metadata def subdomain_data(self): - "Return the domain data of this integral." + """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): - error("Cannot multiply an integral with non-constant values.") + 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): - error("An integral can only be multiplied by a " - "globally constant scalar expression.") + 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): - r = "Integral(%s, %s, %s, %s, %s, %s)" % (repr(self._integrand), - repr(self._integral_type), - repr(self._ufl_domain), - repr(self._subdomain_id), - repr(self._metadata), - repr(self._subdomain_data)) - return r + """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): - 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)) + """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)) + hashdata = ( + hash(self._integrand), + self._integral_type, + hash(self._ufl_domain), + self._subdomain_id, + id_or_none(self._subdomain_data), + ) return hash(hashdata) diff --git a/ufl/log.py b/ufl/log.py deleted file mode 100644 index 7a7da9e4b..000000000 --- a/ufl/log.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module provides functions used by the UFL implementation to -output messages. These may be redirected by the user of UFL.""" - -# Copyright (C) 2005-2016 Anders Logg and Martin Sandve Alnaes -# -# This file is part of UFL (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -# -# Modified by Johan Hake, 2009. -# Modified by Massimiliano Leoni, 2016 - -import sys -import types -import logging -from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL # noqa: F401 - -log_functions = ["log", "debug", "info", "deprecate", "warning", "error", - "begin", "end", - "set_level", "push_level", "pop_level", "set_indent", - "add_indent", - "set_handler", "get_handler", "get_logger", "add_logfile", - "set_prefix", - "info_red", "info_green", "info_blue", - "warning_red", "warning_green", "warning_blue"] - -__all__ = log_functions + ["Logger", "log_functions"] +\ - ["DEBUG", "INFO", "DEPRECATE", "WARNING", "ERROR", "CRITICAL"] - - -DEPRECATE = (INFO + WARNING) // 2 - - -# This is used to override emit() in StreamHandler for printing -# without newline -def emit(self, record): - message = self.format(record) - format_string = "%s" if getattr(record, "continued", False) else "%s\n" - self.stream.write(format_string % message) - self.flush() - - -# Colors if the terminal supports it (disabled e.g. when piped to -# file) -if sys.stdout.isatty() and sys.stderr.isatty(): - RED = "\033[1;37;31m%s\033[0m" - BLUE = "\033[1;37;34m%s\033[0m" - GREEN = "\033[1;37;32m%s\033[0m" -else: - RED = "%s" - BLUE = "%s" - GREEN = "%s" - - -# Logger class -class Logger: - - def __init__(self, name, exception_type=Exception): - "Create logger instance." - self._name = name - self._exception_type = exception_type - - # Set up handler - h = logging.StreamHandler(sys.stdout) - h.setLevel(WARNING) - # Override emit() in handler for indentation - h.emit = types.MethodType(emit, h) - self._handler = h - - # Set up logger - self._log = logging.getLogger(name) - assert len(self._log.handlers) == 0 - self._log.addHandler(h) - self._log.setLevel(DEBUG) - - self._logfiles = {} - - # Set initial indentation level - self._indent_level = 0 - - # Setup stack with default logging level - self._level_stack = [DEBUG] - - # Set prefix - self._prefix = "" - - def add_logfile(self, filename=None, mode="a", level=DEBUG): - "Add a log file." - if filename is None: - filename = "%s.log" % self._name - if filename in self._logfiles: - self.warning("Adding logfile %s multiple times." % filename) - return - h = logging.FileHandler(filename, mode) - h.emit = types.MethodType(emit, h) - h.setLevel(level) - self._log.addHandler(h) - self._logfiles[filename] = h - return h - - def get_logfile_handler(self, filename): - "Gets the handler to the file identified by the given file name." - return self._logfiles[filename] - - def log(self, level, *message): - "Write a log message on given log level." - text = self._format_raw(*message) - if len(text) >= 3 and text[-3:] == "...": - self._log.log(level, self._format(*message), - extra={"continued": True}) - else: - self._log.log(level, self._format(*message)) - - def debug(self, *message): - "Write debug message." - self.log(DEBUG, *message) - - def info(self, *message): - "Write info message." - self.log(INFO, *message) - - def info_red(self, *message): - "Write info message in red." - self.log(INFO, RED % self._format_raw(*message)) - - def info_green(self, *message): - "Write info message in green." - self.log(INFO, GREEN % self._format_raw(*message)) - - def info_blue(self, *message): - "Write info message in blue." - self.log(INFO, BLUE % self._format_raw(*message)) - - def deprecate(self, *message): - "Write deprecation message." - self.log(DEPRECATE, RED % self._format_raw(*message)) - - def warning(self, *message): - "Write warning message." - self._log.warning(self._format(*message)) - - def warning_red(self, *message): - "Write warning message in red." - self._log.warning(RED % self._format(*message)) - - def warning_green(self, *message): - "Write warning message in green." - self._log.warning(GREEN % self._format(*message)) - - def warning_blue(self, *message): - "Write warning message in blue." - self._log.warning(BLUE % self._format(*message)) - - def error(self, *message): - "Write error message and raise an exception." - self._log.error(*message) - raise self._exception_type(self._format_raw(*message)) - - def begin(self, *message): - "Begin task: write message and increase indentation level." - self.info(*message) - self.info("-" * len(self._format_raw(*message))) - self.add_indent() - - def end(self): - "End task: write a newline and decrease indentation level." - self.info("") - self.add_indent(-1) - - def push_level(self, level): - "Push a log level on the level stack." - self._level_stack.append(level) - self.set_level(level) - - def pop_level(self): - "Pop log level from the level stack, reverting to before the last push_level." - self._level_stack.pop() - level = self._level_stack[-1] - self.set_level(level) - - def set_level(self, level): - "Set log level." - self._level_stack[-1] = level - self._handler.setLevel(level) - - def set_indent(self, level): - "Set indentation level." - self._indent_level = level - - def add_indent(self, increment=1): - "Add to indentation level." - self._indent_level += increment - - def get_handler(self): - "Get handler for logging." - return self._handler - - def set_handler(self, handler): - """Replace handler for logging. - To add additional handlers instead of replacing the existing one, use - `log.get_logger().addHandler(myhandler)`. - See the logging module for more details. - """ - self._log.removeHandler(self._handler) - self._log.addHandler(handler) - self._handler = handler - handler.emit = types.MethodType(emit, self._handler) - - def get_logger(self): - "Return message logger." - return self._log - - def set_prefix(self, prefix): - "Set prefix for log messages." - self._prefix = prefix - - def _format(self, *message): - "Format message including indentation." - indent = self._prefix + 2 * self._indent_level * " " - return "\n".join([indent + line for line in (message[0] % message[1:]).split("\n")]) - - def _format_raw(self, *message): - "Format message without indentation." - return message[0] % message[1:] - - -# --- Set up global log functions --- - -class UFLException(Exception): - "Base class for UFL exceptions." - pass - - -class UFLValueError(UFLException): - "Value type error." - pass - - -ufl_logger = Logger("UFL", UFLException) - -for foo in log_functions: - exec("%s = ufl_logger.%s" % (foo, foo)) - -set_level(DEPRECATE) # noqa diff --git a/ufl/mathfunctions.py b/ufl/mathfunctions.py index 3d3c877c0..4c2d87996 100644 --- a/ufl/mathfunctions.py +++ b/ufl/mathfunctions.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """This module provides basic mathematical functions.""" - # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -10,14 +8,23 @@ # Modified by Anders Logg, 2008 # Modified by Kristian B. Oelgaard, 2011 -import math import cmath +import math import numbers - -from ufl.log import warning, error +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 -from ufl.constantvalue import is_true_ufl_scalar, Zero, RealValue, FloatValue, IntValue, ComplexValue, ConstantValue, as_ufl """ TODO: Include additional functions available in (need derivatives as well): @@ -43,19 +50,23 @@ # --- Function representations --- + @ufl_type(is_abstract=True, is_scalar=True, num_ops=1) class MathFunction(Operator): - "Base class for all unary scalar math functions." + """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): - error("Expecting 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): @@ -63,19 +74,25 @@ def evaluate(self, x, mapping, component, index_values): else: res = getattr(cmath, self._name)(a) except ValueError: - warning('Value error in evaluation of function %s with argument %s.' % (self._name, a)) + 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))) @@ -86,14 +103,18 @@ def __new__(cls, 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)): @@ -101,14 +122,18 @@ def __new__(cls, 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)): @@ -116,9 +141,11 @@ def __new__(cls, 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) @@ -128,9 +155,12 @@ def evaluate(self, x, mapping, component, index_values): @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)): @@ -138,14 +168,18 @@ def __new__(cls, 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)): @@ -153,14 +187,18 @@ def __new__(cls, 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)): @@ -168,14 +206,18 @@ def __new__(cls, 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)): @@ -183,14 +225,18 @@ def __new__(cls, 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)): @@ -198,14 +244,18 @@ def __new__(cls, 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)): @@ -213,14 +263,18 @@ def __new__(cls, 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)): @@ -228,14 +282,18 @@ def __new__(cls, 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)): @@ -243,14 +301,18 @@ def __new__(cls, 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)): @@ -258,14 +320,18 @@ def __new__(cls, 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)): @@ -273,76 +339,71 @@ def __new__(cls, arg1, arg2): 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): - error("Expecting scalar argument 1.") + raise ValueError("Expecting scalar argument 1.") if not is_true_ufl_scalar(arg2): - error("Expecting scalar argument 2.") + 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: - error('Atan2 does not support complex numbers.') + raise ValueError("Atan2 does not support complex numbers.") except ValueError: - warning('Value error in evaluation of function atan_2 with arguments %s, %s.' % (a, b)) + warnings.warn( + "Value error in evaluation of function atan2 with arguments %s, %s." % (a, b) + ) raise return res def __str__(self): - return "atan_2(%s,%s)" % (self.ufl_operands[0], self.ufl_operands[1]) - - -def _find_erf(): - import math - if hasattr(math, 'erf'): - return math.erf - import scipy.special - if hasattr(scipy.special, 'erf'): - return scipy.special.erf - return None + """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)): - erf = _find_erf() - if erf is not None: - return FloatValue(erf(float(argument))) + return FloatValue(math.erf(float(argument))) if isinstance(argument, (ConstantValue)): - erf = _find_erf() - if erf is not None: - return ComplexValue(erf(complex(argument))) + 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) - erf = _find_erf() - if erf is None: - error("No python implementation of erf available on this system, cannot evaluate. Upgrade python or install scipy.") - return erf(a) + 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", "_classname") + """Base class for all bessel functions.""" + + __slots__ = "_name" - def __init__(self, name, classname, nu, argument): + def __init__(self, name, nu, argument): + """Initialise.""" if not is_true_ufl_scalar(nu): - error("Expecting scalar nu.") + raise ValueError("Expecting scalar nu.") if not is_true_ufl_scalar(argument): - error("Expecting scalar argument.") + raise ValueError("Expecting scalar argument.") # Use integer representation if suitable fnu = float(nu) @@ -354,58 +415,71 @@ def __init__(self, name, classname, nu, argument): Operator.__init__(self, (nu, argument)) - self._classname = classname 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: - error("You must have scipy installed to evaluate bessel functions in python.") + 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' + functype = "n" if name != "i" else "v" else: - nu = self.ufl_operands[0].evaluate(x, mapping, component, - index_values) - functype = 'v' + 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): - return "%s(%s, %s)" % (self._name, self.ufl_operands[0], - self.ufl_operands[1]) + """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): - BesselFunction.__init__(self, "cyl_bessel_j", "BesselJ", 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): - BesselFunction.__init__(self, "cyl_bessel_y", "BesselY", 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): - BesselFunction.__init__(self, "cyl_bessel_i", "BesselI", 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): - BesselFunction.__init__(self, "cyl_bessel_k", "BesselK", nu, argument) + """Initialise.""" + BesselFunction.__init__(self, "cyl_bessel_k", nu, argument) diff --git a/ufl/matrix.py b/ufl/matrix.py new file mode 100644 index 000000000..039d70e23 --- /dev/null +++ b/ufl/matrix.py @@ -0,0 +1,106 @@ +"""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 + ) diff --git a/ufl/measure.py b/ufl/measure.py index 75f90b20f..e41a89adb 100644 --- a/ufl/measure.py +++ b/ufl/measure.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """The Measure class.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -11,17 +10,13 @@ # Modified by Massimiliano Leoni, 2016. import numbers - from itertools import chain -from ufl.log import error, deprecate -from ufl.core.expr import Expr from ufl.checks import is_true_ufl_scalar from ufl.constantvalue import as_ufl -from ufl.utils.dicts import EmptyDict -from ufl.domain import as_domain, AbstractDomain, extract_domains -from ufl.protocols import id_or_none, metadata_equal, metadata_hashdata - +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"] @@ -33,39 +28,35 @@ # Enumeration of valid domain types _integral_types = [ # === Integration over full topological dimension: - ("cell", "dx"), # Over cells of a mesh - # ("macro_cell", "dE"), # Over a group of adjacent cells (TODO: Arbitrary cell group? Not currently used.) - + ("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 - + ("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 - 2: - ("edge", "dl"), # Over edges of a mesh - + ("edge", "dl"), # Over edges of a mesh # === Integration over topological dimension 0 - ("vertex", "dP"), # Over vertices of a mesh - # ("vertex", "dV"), # TODO: Use this over vertices? - # ("point", "dP"), # TODO: Use this over arbitrary points inside cells? - + ("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 specific hacks on the way out: - # TODO: Remove these, firedrake can use metadata instead - # and create the measure objects in firedrake: + ("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 + ("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 = dict((l, s) for l, s in _integral_types) -measure_name_to_integral_type = dict((s, l) for l, s in _integral_types) +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") @@ -74,106 +65,96 @@ 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): - error("Integral type already added with different 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): - error("Measure name already used for another domain 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." + """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) + integral_type = measure_name_to_integral_type.get(integral_type, integral_type) if integral_type not in integral_type_to_measure_name: - error("Invalid integral_type.") + raise ValueError("Invalid integral_type.") return integral_type def integral_types(): - "Return a tuple of all domain type strings." + """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 a tuple of all measure name strings.""" return tuple(sorted(measure_name_to_integral_type.keys())) class Measure(object): - __slots__ = ("_integral_type", - "_domain", - "_subdomain_id", - "_metadata", - "_subdomain_data") """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. - """ - def __init__(self, - integral_type, # "dx" etc - domain=None, - subdomain_id="everywhere", - metadata=None, - subdomain_data=None): - """ - integral_type: - str, 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. + __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 - self._domain = None if domain is None else as_domain(domain) - if not (self._domain is None or isinstance(self._domain, AbstractDomain)): - error("Invalid domain.") + 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")): - # error("Invalid domain data, missing ufl_id() implementation.") + # 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): - error("Invalid subdomain_id %s." % (did,)) + raise ValueError(f"Invalid subdomain_id {did}.") else: if not (subdomain_id in ("everywhere",) or isinstance(subdomain_id, numbers.Integral)): - error("Invalid subdomain_id %s." % (subdomain_id,)) + 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): - error("Invalid metadata.") - self._metadata = metadata or EmptyDict + raise ValueError("Invalid metadata.") + self._metadata = metadata or {} def integral_type(self): """Return the domain type. @@ -190,28 +171,24 @@ def ufl_domain(self): return self._domain def subdomain_id(self): - "Return the domain id of this measure (integer)." + """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. + """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. + 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 }) @@ -219,7 +196,6 @@ def reconstruct(self, 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() @@ -229,46 +205,52 @@ def reconstruct(self, 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) + 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 + """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, rule=None): + 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.""" - - # Deprecation of 'rule' in favour of 'scheme' - if rule is not None: - deprecate("Measure argument 'rule' has been renamed to 'scheme'.") - assert scheme is None or scheme == rule - scheme = rule - # Let syntax dx() mean integral over everywhere - all_args = (subdomain_id, metadata, domain, subdomain_data, - degree, scheme) + 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 subdomain_id is not None and ( + isinstance(subdomain_id, AbstractDomain) or hasattr(subdomain_id, "ufl_domain") + ): if domain is not None: - error("Ambiguous: setting domain both as keyword argument and first argument.") - subdomain_id, domain = "everywhere", as_domain(subdomain_id) + 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. @@ -283,25 +265,15 @@ def __call__(self, subdomain_id=None, metadata=None, domain=None, # 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 __getitem__(self, data): - """This operator supports legacy syntax in python dolfin programs. - - The old documentation reads: Return a new Measure for same - integration type with an attached context for interpreting - domain ids. By default this new Measure integrates over - everywhere, but it can be restricted with a domain id as - usual. Example: dx = dx[boundaries]; L = f*v*dx + g*v+dx(1). - - """ - deprecate("Notation dx[meshfunction] is deprecated. Please use dx(subdomain_data=meshfunction) instead.") - return self(subdomain_data=data) + return self.reconstruct( + subdomain_id=subdomain_id, + domain=domain, + metadata=metadata, + subdomain_data=subdomain_data, + ) def __str__(self): - global integral_type_to_measure_name + """Format as a string.""" name = integral_type_to_measure_name[self._integral_type] args = [] @@ -309,17 +281,15 @@ def __str__(self): 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 EmptyDict if None + 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)) + return "%s(%s)" % (name, ", ".join(args)) def __repr__(self): - "Return a repr string for this Measure." - global integral_type_to_measure_name - + """Return a repr string for this Measure.""" args = [] args.append(repr(self._integral_type)) @@ -327,37 +297,44 @@ def __repr__(self): 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 EmptyDict if None + 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)) + r = "%s(%s)" % (type(self).__name__, ", ".join(args)) return r def __hash__(self): - "Return a hash value for this Measure." - hashdata = (self._integral_type, - self._subdomain_id, - hash(self._domain), - metadata_hashdata(self._metadata), - id_or_none(self._subdomain_data)) + """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." - 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 - metadata_equal(self._metadata, other._metadata)) + """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): @@ -371,7 +348,6 @@ 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. @@ -384,19 +360,16 @@ def __mul__(self, other): return NotImplemented def __rmul__(self, integrand): - """Multiply a scalar expression with measure to construct a form with - a single integral. + """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.integral import Integral from ufl.form import Form + from ufl.integral import Integral # Allow python literals: 1*dx and 1.0*dx if isinstance(integrand, (int, float)): @@ -409,21 +382,37 @@ def __rmul__(self, integrand): # Allow only scalar integrands if not is_true_ufl_scalar(integrand): - error("Can only integrate scalar expressions. The integrand is a " - "tensor expression with value shape %s and free indices with labels %s." % - (integrand.ufl_shape, integrand.ufl_free_indices)) + 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)))) + 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,)): - error("Expecting integer or string domain id.") + 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 @@ -431,19 +420,23 @@ def __rmul__(self, integrand): if domain is None: domains = extract_domains(integrand) if len(domains) == 1: - domain, = domains + (domain,) = domains elif len(domains) == 0: - error("This integral is missing an integration domain.") + raise ValueError("This integral is missing an integration domain.") else: - error("Multiple domains found, making the choice of integration domain ambiguous.") + 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()) + 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]) @@ -451,23 +444,24 @@ 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): @@ -475,6 +469,7 @@ def __add__(self, other): return NotImplemented def __str__(self): + """Format as a string.""" return "{\n " + "\n + ".join(map(str, self._measures)) + "\n}" @@ -487,22 +482,21 @@ class MeasureProduct(object): 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." + """Create MeasureProduct from given list of measures.""" self._measures = measures if len(self._measures) < 2: - error("Expecting at least two measures.") + 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] @@ -511,8 +505,10 @@ def __mul__(self, other): return NotImplemented def __rmul__(self, integrand): - error("TODO: Implement MeasureProduct.__rmul__ to construct integral and form somehow.") + """Multiply.""" + # TODO: Implement MeasureProduct.__rmul__ to construct integral and form somehow. + raise NotImplementedError() def sub_measures(self): - "Return submeasures." + """Return submeasures.""" return self._measures diff --git a/ufl/measureoperators.py b/ufl/measureoperators.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/ufl/objects.py b/ufl/objects.py index ab414091d..64704d275 100644 --- a/ufl/objects.py +++ b/ufl/objects.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"Utility objects for pretty syntax in user code." - +"""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) @@ -10,10 +8,9 @@ # Modified by Anders Logg, 2008 # Modified by Kristian Oelgaard, 2009 -from ufl.core.multiindex import indices from ufl.cell import Cell -from ufl.measure import Measure -from ufl.measure import integral_type_to_measure_name +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 @@ -29,14 +26,16 @@ dX = dx + dC # noqa: F821 # Create objects for builtin known cell types -vertex = Cell("vertex", 0) -interval = Cell("interval", 1) -triangle = Cell("triangle", 2) -tetrahedron = Cell("tetrahedron", 3) -prism = Cell("prism", 3) -pyramid = Cell("pyramid", 3) -quadrilateral = Cell("quadrilateral", 2) -hexahedron = Cell("hexahedron", 3) +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" diff --git a/ufl/operators.py b/ufl/operators.py index 7bf10b85a..2ca1158f5 100644 --- a/ufl/operators.py +++ b/ufl/operators.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- -"""This module extends the form language with free function operators, +"""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.""" - +objects. +""" # Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg # # This file is part of UFL (https://www.fenicsproject.org) @@ -14,71 +15,113 @@ # Modified by Massimiliano Leoni, 2016. import operator +import warnings -from ufl.log import error, warning -from ufl.form import Form -from ufl.constantvalue import Zero, RealValue, ComplexValue, as_ufl -from ufl.differentiation import VariableDerivative, Grad, Div, Curl, NablaGrad, NablaDiv -from ufl.tensoralgebra import Transposed, Inner, Outer, Dot, Cross, \ - Determinant, Inverse, Cofactor, Trace, Deviatoric, Skew, Sym -from ufl.coefficient import Coefficient -from ufl.variable import Variable -from ufl.tensors import as_tensor, as_matrix, as_vector, ListTensor -from ufl.conditional import EQ, NE, \ - AndCondition, OrCondition, NotCondition, Conditional, MaxValue, MinValue -from ufl.algebra import Conj, Real, Imag -from ufl.mathfunctions import Sqrt, Exp, Ln, Erf,\ - Cos, Sin, Tan, Cosh, Sinh, Tanh, Acos, Asin, Atan, Atan2,\ - BesselJ, BesselY, BesselI, BesselK +from ufl import sobolevspace +from ufl.algebra import Conj, Imag, Real from ufl.averaging import CellAvg, FacetAvg -from ufl.core.multiindex import indices -from ufl.indexed import Indexed -from ufl.geometry import SpatialCoordinate, FacetNormal 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): - "UFL operator: The rank of *f*." + """The rank of f.""" f = as_ufl(f) return len(f.ufl_shape) def shape(f): - "UFL operator: The shape of *f*." + """The shape of f.""" f = as_ufl(f) return f.ufl_shape # --- Complex operators --- + def conj(f): - "UFL operator: The complex conjugate of *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. +# Alias because both conj and conjugate are in numpy and we wish to be +# consistent. conjugate = conj def real(f): - "UFL operator: The real part of *f*" + """The real part of f.""" f = as_ufl(f) return Real(f) def imag(f): - "UFL operator: The imaginary part of *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)] @@ -93,39 +136,45 @@ def extind(ii): def elem_op(op, *args): - "UFL operator: Take the elementwise application of operator *op* on scalar values from one or more tensor arguments." + """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): - error("Cannot take elementwise operation with different shapes.") + 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): - "UFL operator: Take the elementwise multiplication of tensors *A* and *B* with the same shape." + """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): - "UFL operator: Take the elementwise division of tensors *A* and *B* with the same shape." + """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): - "UFL operator: Take the elementwise power of tensors *A* and *B* with the same shape." + """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): - "UFL operator: Take the transposed of tensor A." + """Take the transposed of tensor A.""" A = as_ufl(A) if A.ufl_shape == (): return A @@ -133,7 +182,10 @@ def transpose(A): def outer(*operands): - "UFL operator: Take the outer product of two or more operands. The complex conjugate of the first argument is taken." + """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] @@ -150,7 +202,10 @@ def outer(*operands): def inner(a, b): - "UFL operator: Take the inner product of *a* and *b*. The complex conjugate of the second argument is taken." + """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 == (): @@ -158,17 +213,11 @@ def inner(a, b): return Inner(a, b) -# TODO: Something like this would be useful in some cases, but should -# inner just support len(a.ufl_shape) != len(b.ufl_shape) instead? -def _partial_inner(a, b): - "UFL operator: Take the partial inner product of a and b." - ar, br = len(a.ufl_shape), len(b.ufl_shape) - n = min(ar, br) - return contraction(a, list(range(n - ar, n - ar + n)), b, list(range(n))) - - def dot(a, b): - "UFL operator: Take the dot product of *a* and *b*. This won't take the complex conjugate of the second argument." + """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 == (): @@ -176,47 +225,26 @@ def dot(a, b): return Dot(a, b) -def contraction(a, a_axes, b, b_axes): - "UFL operator: Take the contraction of a and b over given axes." - ai, bi = a_axes, b_axes - if len(ai) != len(bi): - error("Contraction must be over the same number of axes.") - ash = a.ufl_shape - bsh = b.ufl_shape - aii = indices(len(a.ufl_shape)) - bii = indices(len(b.ufl_shape)) - cii = indices(len(ai)) - shape = [None] * len(ai) - for i, j in enumerate(ai): - aii[j] = cii[i] - shape[i] = ash[j] - for i, j in enumerate(bi): - bii[j] = cii[i] - if shape[i] != bsh[j]: - error("Shape mismatch in contraction.") - s = a[aii] * b[bii] - cii = set(cii) - ii = tuple(i for i in (aii + bii) if i not in cii) - return as_tensor(s, ii) - - def perp(v): - "UFL operator: Take the perp of *v*, i.e. :math:`(-v_1, +v_0)`." + """Take the perp of v. + + I.e. :math:`(-v_1, +v_0)`. + """ v = as_ufl(v) if v.ufl_shape != (2,): - error("Expecting a 2D vector expression.") - return as_vector((-v[1], v[0])) + raise ValueError("Expecting a 2D vector expression.") + return Perp(v) def cross(a, b): - "UFL operator: Take the cross product of *a* and *b*." + """Take the cross product of a and b.""" a = as_ufl(a) b = as_ufl(b) return Cross(a, b) def det(A): - "UFL operator: Take the determinant of *A*." + """Take the determinant of A.""" A = as_ufl(A) if A.ufl_shape == (): return A @@ -224,7 +252,7 @@ def det(A): def inv(A): - "UFL operator: Take the inverse of *A*." + """Take the inverse of A.""" A = as_ufl(A) if A.ufl_shape == (): return 1 / A @@ -232,35 +260,37 @@ def inv(A): def cofac(A): - "UFL operator: Take the cofactor of *A*." + """Take the cofactor of A.""" A = as_ufl(A) return Cofactor(A) def tr(A): - "UFL operator: Take the trace of *A*." + """Take the trace of A.""" A = as_ufl(A) return Trace(A) def diag(A): - """UFL operator: Take the diagonal part of rank 2 tensor *A* **or** - make a diagonal rank 2 tensor from a rank 1 tensor. + """Diagonal ranl-2 tensor. - Always returns a rank 2 tensor. See also ``diag_vector``.""" + 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 + (n,) = A.ufl_shape elif r == 2: m, n = A.ufl_shape if m != n: - error("Can only take diagonal of square tensors.") + raise ValueError("Can only take diagonal of square tensors.") else: - error("Expecting rank 1 or 2 tensor.") + raise ValueError("Expecting rank 1 or 2 tensor.") # Build matrix row by row rows = [] @@ -272,58 +302,58 @@ def diag(A): def diag_vector(A): - """UFL operator: Take the diagonal part of rank 2 tensor *A* and return as a vector. - - See also ``diag``.""" + """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: - error("Expecting rank 2 tensor.") + raise ValueError("Expecting rank 2 tensor.") m, n = A.ufl_shape if m != n: - error("Can only take diagonal of square tensors.") + 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): - "UFL operator: Take the deviatoric part of *A*." + """Take the deviatoric part of A.""" A = as_ufl(A) return Deviatoric(A) def skew(A): - "UFL operator: Take the skew symmetric part of *A*." + """Take the skew symmetric part of A.""" A = as_ufl(A) return Skew(A) def sym(A): - "UFL operator: Take the symmetric part of *A*." + """Take the symmetric part of A.""" A = as_ufl(A) return Sym(A) # --- Differential operators + def Dx(f, *i): - """UFL operator: Take the partial derivative of *f* with respect - to spatial variable number *i*. Equivalent to ``f.dx(*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 Dt(f): - "UFL operator: The partial derivative of *f* with respect to time." - raise NotImplementedError - - def Dn(f): - """UFL operator: Take the directional derivative of *f* in the - facet normal direction, Dn(f) := dot(grad(f), n).""" + """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) @@ -331,13 +361,14 @@ def Dn(f): def diff(f, v): - """UFL operator: Take the derivative of *f* with respect to the variable *v*. + """Take the derivative of f with respect to the variable v. - If *f* is a form, ``diff`` is applied to each integrand. + 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 @@ -347,20 +378,16 @@ def diff(f, v): elif isinstance(v, (Variable, Coefficient)): return VariableDerivative(f, v) else: - error("Expecting a Variable or SpatialCoordinate in diff.") + raise ValueError("Expecting a Variable or SpatialCoordinate in diff.") def grad(f): - """UFL operator: Take the gradient of *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. @@ -371,14 +398,11 @@ def grad(f): def div(f): - """UFL operator: Take the divergence of *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. @@ -389,16 +413,12 @@ def div(f): def nabla_grad(f): - """UFL operator: Take the gradient of *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. @@ -409,14 +429,11 @@ def nabla_grad(f): def nabla_div(f): - """UFL operator: Take the divergence of *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. @@ -427,7 +444,7 @@ def nabla_div(f): def curl(f): - "UFL operator: Take the curl of *f*." + """Take the curl of f.""" f = as_ufl(f) return Curl(f) @@ -437,20 +454,24 @@ def curl(f): # --- DG operators --- + def jump(v, n=None): - "UFL operator: Take the jump of *v* across a facet." + """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('-') + return v("+") - v("-") r = len(v.ufl_shape) if r == 0: - return v('+') * n('+') + v('-') * n('-') + return v("+") * n("+") + v("-") * n("-") else: - return dot(v('+'), n('+')) + dot(v('-'), n('-')) + return dot(v("+"), n("+")) + dot(v("-"), n("-")) else: - warning("Returning zero from jump of expression without a domain. This may be erroneous if a dolfin.Expression is involved.") + 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 @@ -461,125 +482,118 @@ def jump(v, n=None): def avg(v): - "UFL operator: Take the average of *v* across a facet." + """Take the average of v across a facet.""" v = as_ufl(v) - return 0.5 * (v('+') + v('-')) + return 0.5 * (v("+") + v("-")) def cell_avg(f): - "UFL operator: Take the average of *v* over a cell." + """Take the average of v over a cell.""" return CellAvg(f) def facet_avg(f): - "UFL operator: Take the average of *v* over a facet." + """Take the average of v over a facet.""" return FacetAvg(f) # --- Other operators --- + def variable(e): - """UFL operator: Define a variable representing the given expression, see also - ``diff()``.""" + """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): - """UFL operator: A conditional expression, taking the value of *true_value* - when *condition* evaluates to ``true`` and *false_value* otherwise.""" + """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): - """UFL operator: A boolean expression (left == right) for use with - ``conditional``.""" + """A boolean expression (left == right) for use with conditional.""" return EQ(left, right) def ne(left, right): - """UFL operator: A boolean expression (left != right) for use with - ``conditional``.""" + """A boolean expression (left != right) for use with conditional.""" return NE(left, right) def le(left, right): - """UFL operator: A boolean expression (left <= right) for use with - ``conditional``.""" + """A boolean expression (left <= right) for use with conditional.""" return as_ufl(left) <= as_ufl(right) def ge(left, right): - """UFL operator: A boolean expression (left >= right) for use with - ``conditional``.""" + """A boolean expression (left >= right) for use with conditional.""" return as_ufl(left) >= as_ufl(right) def lt(left, right): - """UFL operator: A boolean expression (left < right) for use with - ``conditional``.""" + """A boolean expression (left < right) for use with conditional.""" return as_ufl(left) < as_ufl(right) def gt(left, right): - """UFL operator: A boolean expression (left > right) for use with - ``conditional``.""" + """A boolean expression (left > right) for use with conditional.""" return as_ufl(left) > as_ufl(right) def And(left, right): - """UFL operator: A boolean expression (left and right) for use with - ``conditional``.""" + """A boolean expression (left and right) for use with conditional.""" return AndCondition(left, right) def Or(left, right): - """UFL operator: A boolean expression (left or right) for use with - ``conditional``.""" + """A boolean expression (left or right) for use with conditional.""" return OrCondition(left, right) def Not(condition): - """UFL operator: A boolean expression (not condition) for use with - ``conditional``.""" + """A boolean expression (not condition) for use with conditional.""" return NotCondition(condition) def sign(x): - "UFL operator: Take the sign (+1 or -1) of *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): - "UFL operator: Take the maximum of *x* and *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): - "UFL operator: Take the minimum of *x* and *y*." + """Take the minimum of x and y.""" x = as_ufl(x) y = as_ufl(y) return MinValue(x, y) -def Max(x, y): # TODO: Deprecate this notation? - "UFL operator: Take the maximum of *x* and *y*." - return max_value(x, y) - - -def Min(x, y): # TODO: Deprecate this notation? - "UFL operator: Take the minimum of *x* and *y*." - return min_value(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)): @@ -590,71 +604,71 @@ def _mathfunction(f, cls): def sqrt(f): - "UFL operator: Take the square root of *f*." + """Take the square root of f.""" return _mathfunction(f, Sqrt) def exp(f): - "UFL operator: Take the exponential of *f*." + """Take the exponential of f.""" return _mathfunction(f, Exp) def ln(f): - "UFL operator: Take the natural logarithm of *f*." + """Take the natural logarithm of f.""" return _mathfunction(f, Ln) def cos(f): - "UFL operator: Take the cosine of *f*." + """Take the cosine of f.""" return _mathfunction(f, Cos) def sin(f): - "UFL operator: Take the sine of *f*." + """Take the sine of f.""" return _mathfunction(f, Sin) def tan(f): - "UFL operator: Take the tangent of *f*." + """Take the tangent of f.""" return _mathfunction(f, Tan) def cosh(f): - "UFL operator: Take the hyperbolic cosine of *f*." + """Take the hyperbolic cosine of f.""" return _mathfunction(f, Cosh) def sinh(f): - "UFL operator: Take the hyperbolic sine of *f*." + """Take the hyperbolic sine of f.""" return _mathfunction(f, Sinh) def tanh(f): - "UFL operator: Take the hyperbolic tangent of *f*." + """Take the hyperbolic tangent of f.""" return _mathfunction(f, Tanh) def acos(f): - "UFL operator: Take the inverse cosine of *f*." + """Take the inverse cosine of f.""" return _mathfunction(f, Acos) def asin(f): - "UFL operator: Take the inverse sine of *f*." + """Take the inverse sine of f.""" return _mathfunction(f, Asin) def atan(f): - "UFL operator: Take the inverse tangent of *f*." + """Take the inverse tangent of f.""" return _mathfunction(f, Atan) -def atan_2(f1, f2): - "UFL operator: Take the inverse tangent with two the arguments *f1* and *f2*." +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('atan_2 is incompatible with complex numbers.') + raise TypeError("atan2 is incompatible with complex numbers.") r = Atan2(f1, f2) if isinstance(r, (RealValue, Zero, int, float)): return float(r) @@ -664,33 +678,33 @@ def atan_2(f1, f2): def erf(f): - "UFL operator: Take the error function of *f*." + """Take the error function of f.""" return _mathfunction(f, Erf) def bessel_J(nu, f): - """UFL operator: cylindrical Bessel function of the first kind.""" + """Cylindrical Bessel function of the first kind.""" nu = as_ufl(nu) f = as_ufl(f) return BesselJ(nu, f) def bessel_Y(nu, f): - """UFL operator: cylindrical Bessel function of the second kind.""" + """Cylindrical Bessel function of the second kind.""" nu = as_ufl(nu) f = as_ufl(f) return BesselY(nu, f) def bessel_I(nu, f): - """UFL operator: regular modified cylindrical Bessel function.""" + """Regular modified cylindrical Bessel function.""" nu = as_ufl(nu) f = as_ufl(f) return BesselI(nu, f) def bessel_K(nu, f): - """UFL operator: irregular modified cylindrical Bessel function.""" + """Irregular modified cylindrical Bessel function.""" nu = as_ufl(nu) f = as_ufl(f) return BesselK(nu, f) @@ -698,16 +712,16 @@ def bessel_K(nu, f): # --- Special function for exterior_derivative + def exterior_derivative(f): - """UFL operator: Take the exterior derivative of *f*. + """Take the exterior derivative of f. - The exterior derivative uses the element family to - determine whether ``id``, ``grad``, ``curl`` or ``div`` should be used. + 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``. + 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 @@ -715,7 +729,12 @@ def exterior_derivative(f): raise NotImplementedError index = int(indices[0]) element = expression.ufl_element() - element = element.extract_component(index)[1] + 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!!! @@ -723,33 +742,32 @@ def exterior_derivative(f): raise NotImplementedError index = int(f0indices[0]) element = f0expr.ufl_element() - element = element.extract_component(index)[1] + 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: - error("Unable to determine element from %s" % f) + raise ValueError(f"Unable to determine element from {f}") - # Extract the family and the geometric dimension - family = element.family() - gdim = element.cell().geometric_dimension() + domain = f.ufl_domain() - # L^2 elements: - if "Disc" in family: - return f + gdim = domain.geometric_dimension() + space = element.sobolev_space - # H^1 elements: - if "Lagrange" in family: + 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) - - # H(curl) elements: - if "curl" in family: + elif space == sobolevspace.HCurl: return curl(f) - - # H(div) elements: - if "Brezzi" in family or "Raviart" in family: + elif space == sobolevspace.HDiv: return div(f) - - error("Unable to determine exterior_derivative. Family is '%s'" % family) + else: + raise ValueError(f"Unable to determine exterior_derivative for element '{element!r}'") diff --git a/ufl/permutation.py b/ufl/permutation.py index 2e2b8cf43..8a71a0593 100644 --- a/ufl/permutation.py +++ b/ufl/permutation.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module provides utility functions for computing permutations -and generating index lists.""" - +"""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) @@ -12,7 +9,7 @@ def compute_indices(shape): - "Compute all index combinations for given shape" + """Compute all index combinations for given shape.""" if len(shape) == 0: return ((),) sub_indices = compute_indices(shape[1:]) @@ -23,20 +20,14 @@ def compute_indices(shape): return tuple(indices) -# functional version: -def compute_indices2(shape): - "Compute all index combinations for given shape" - return ((),) if len(shape) == 0 else tuple((i,) + sub_index for i in range(shape[0]) for sub_index in compute_indices2(shape[1:])) - - def build_component_numbering(shape, symmetry): - """Build a numbering of components within the given value shape, - taking into consideration a symmetry mapping which leaves the + """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) @@ -53,65 +44,3 @@ def build_component_numbering(shape, symmetry): for k, c in enumerate(si2vi): assert vi2si[c] == k return vi2si, si2vi - - -def compute_permutations(k, n, skip=None): - """Compute all permutations of k elements from (0, n) in rising order. - Any elements that are contained in the list skip are not included. - - """ - if k == 0: - return [] - if skip is None: - skip = [] - if k == 1: - return [(i,) for i in range(n) if i not in skip] - pp = compute_permutations(k - 1, n, skip) - permutations = [] - for i in range(n): - if i in skip: - continue - for p in pp: - if i < p[0]: - permutations.append((i,) + p) - return permutations - - -def compute_permutation_pairs(j, k): - """Compute all permutations of j + k elements from (0, j + k) in - rising order within (0, j) and (j, j + k) respectively. - - """ - permutations = [] - pp0 = compute_permutations(j, j + k) - for p0 in pp0: - pp1 = compute_permutations(k, j + k, p0) - for p1 in pp1: - permutations.append((p0, p1)) - return permutations - - -def compute_sign(permutation): - "Compute sign by sorting." - sign = 1 - n = len(permutation) - p = [p for p in permutation] - for i in range(n - 1): - for j in range(n - 1): - if p[j] > p[j + 1]: - (p[j], p[j + 1]) = (p[j + 1], p[j]) - sign = -sign - elif p[j] == p[j + 1]: - return 0 - return sign - - -def compute_order_tuples(k, n): - "Compute all tuples of n integers such that the sum is k" - if n == 1: - return ((k,),) - order_tuples = [] - for i in range(k + 1): - for order_tuple in compute_order_tuples(k - i, n - 1): - order_tuples.append(order_tuple + (i,)) - return tuple(order_tuples) diff --git a/ufl/precedence.py b/ufl/precedence.py index b5ca6c052..3b404dbda 100644 --- a/ufl/precedence.py +++ b/ufl/precedence.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Precedence handling." +"""Precedence handling.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -7,16 +6,17 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import warning - +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'): + if not hasattr(parent, "_precedence"): assign_precedences(build_precedence_list()) # We want child to be evaluated fully first, and if the parent has @@ -40,7 +40,20 @@ def parstr(child, parent, pre="(", post=")", format=str): def build_precedence_list(): - from ufl.classes import Operator, Terminal, Sum, IndexSum, Product, Division, Power, MathFunction, BesselFunction, Abs, Indexed + """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 @@ -55,7 +68,12 @@ def build_precedence_list(): # stronger than +, but weaker than product precedence_list.append((IndexSum,)) - precedence_list.append((Product, Division,)) + precedence_list.append( + ( + Product, + Division, + ) + ) # NB! Depends on language! precedence_list.append((Power, MathFunction, BesselFunction, Abs)) @@ -69,9 +87,11 @@ def build_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, all_ufl_classes, abstract_classes + from ufl.classes import Expr, abstract_classes, all_ufl_classes + pm = {} missing = set() # Assign integer values for each precedence level @@ -95,46 +115,12 @@ def build_precedence_mapping(precedence_list): def assign_precedences(precedence_list): - "Given a precedence list, assign ints to class._precedence." + """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: - msg = "Missing precedence levels for classes:\n" +\ - "\n".join(' %s' % c for c in sorted(missing)) - warning(msg) - - -""" -# Code from uflacs: -import ufl - -def build_precedence_list(): - "Builds a list of operator types by precedence order in the C language." - # FIXME: Add all types we need here. - pl = [] - pl.append((ufl.classes.Conditional,)) - pl.append((ufl.classes.OrCondition,)) - pl.append((ufl.classes.AndCondition,)) - pl.append((ufl.classes.EQ, ufl.classes.NE)) - pl.append((ufl.classes.Condition,)) # <,>,<=,>= - pl.append((ufl.classes.NotCondition,)) # FIXME - pl.append((ufl.classes.Sum,)) - pl.append((ufl.classes.Product, ufl.classes.Division,)) - # The highest precedence items will never need - # parentheses around them or their operands - pl.append((ufl.classes.Power, ufl.classes.MathFunction, ufl.classes.Abs, ufl.classes.BesselFunction, - ufl.classes.Indexed, ufl.classes.Grad, - ufl.classes.PositiveRestricted, ufl.classes.NegativeRestricted, - ufl.classes.Terminal)) - # FIXME: Write a unit test that checks this list against all ufl classes - return pl - -def build_precedence_map(): - from ufl.precedence import build_precedence_mapping - pm, missing = build_precedence_mapping(build_precedence_list()) - if 0 and missing: # Enable to see which types we are missing - print("Missing precedence levels for the types:") - print("\n".join(' %s' % c for c in missing)) - return pm -""" + warnings.warn( + "Missing precedence levels for classes:\n" + + "\n".join(f" {c}" for c in sorted(missing)) + ) diff --git a/ufl/protocols.py b/ufl/protocols.py index bb235f34c..ad7dbb7f7 100644 --- a/ufl/protocols.py +++ b/ufl/protocols.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +"""Protocols.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -15,16 +15,7 @@ def id_or_none(obj): """ if obj is None: return None - elif hasattr(obj, 'ufl_id'): + elif hasattr(obj, "ufl_id"): return obj.ufl_id() else: return id(obj) - - -def metadata_equal(a, b): - return (sorted((k, id(v)) for k, v in list(a.items())) == - sorted((k, id(v)) for k, v in list(b.items()))) - - -def metadata_hashdata(md): - return tuple(sorted((k, id(v)) for k, v in list(md.items()))) diff --git a/ufl/pullback.py b/ufl/pullback.py new file mode 100644 index 000000000..0dc780e52 --- /dev/null +++ b/ufl/pullback.py @@ -0,0 +1,641 @@ +"""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", + "CovariantContravariantPiola", + "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 element.reference_value_shape[:-1] + (gdim,) + + +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 element.reference_value_shape[:-1] + (gdim,) + + +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 element.reference_value_shape[:-2] + (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 element.reference_value_shape[:-2] + (gdim, gdim) + + +class CovariantContravariantPiola(AbstractPullback): + """The covariant contravariant Piola pull back.""" + + def __repr__(self) -> str: + """Return a representation of the object.""" + return "CovariantContravariantPiola()" + + @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, JacobianInverse + + domain = extract_unique_domain(expr) + J = Jacobian(domain) + detJ = JacobianDeterminant(J) + 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((1.0 / detJ) * K[m, i] * 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. + + 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 element.reference_value_shape[:-2] + (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 isinstance(element, type(self._element)) + subelem = element.sub_elements[0] + pvs = subelem.pullback.physical_value_shape(subelem, domain) + return tuple(i + 1 for i in max(self._symmetry.keys())) + pvs + + +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 + """ + return element.reference_value_shape + + +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 + """ + return element.reference_value_shape + + +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 + """ + return element.reference_value_shape + + +identity_pullback = IdentityPullback() +covariant_piola = CovariantPiola() +contravariant_piola = ContravariantPiola() +l2_piola = L2Piola() +double_covariant_piola = DoubleCovariantPiola() +double_contravariant_piola = DoubleContravariantPiola() +covariant_contravariant_piola = CovariantContravariantPiola() +physical_pullback = PhysicalPullback() +custom_pullback = CustomPullback() +undefined_pullback = UndefinedPullback() diff --git a/ufl/referencevalue.py b/ufl/referencevalue.py index 295192d63..5dd82f4c3 100644 --- a/ufl/referencevalue.py +++ b/ufl/referencevalue.py @@ -1,38 +1,36 @@ -# -*- coding: utf-8 -*- -"Representation of the reference value of a function." - +"""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.ufl_type import ufl_type from ufl.core.operator import Operator from ufl.core.terminal import FormArgument -from ufl.log import error +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) +@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." + """Representation of the reference cell value of a form argument.""" + __slots__ = () def __init__(self, f): + """Initialise.""" if not isinstance(f, FormArgument): - error("Can only take reference value of form arguments.") + raise ValueError("Can only take reference value of form arguments.") Operator.__init__(self, (f,)) @property def ufl_shape(self): - return self.ufl_operands[0].ufl_element().reference_value_shape() + """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." - error("Evaluate not implemented.") + """Get child from mapping and return the component asked for.""" + raise NotImplementedError() def __str__(self): - return "reference_value(%s)" % self.ufl_operands[0] + """Format as a string.""" + return f"reference_value({self.ufl_operands[0]})" diff --git a/ufl/restriction.py b/ufl/restriction.py index 82bf3a226..430fefa41 100644 --- a/ufl/restriction.py +++ b/ufl/restriction.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """Restriction operations.""" - # Copyright (C) 2008-2016 Martin Sandve Alnæs # # This file is part of UFL (https://www.fenicsproject.org) @@ -8,43 +6,54 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.core.operator import Operator -from ufl.precedence import parstr 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) + +@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): - return self.ufl_operands[0].evaluate(x, mapping, component, - index_values) + """Evaluate.""" + return self.ufl_operands[0].evaluate(x, mapping, component, index_values) def __str__(self): - return "%s('%s')" % (parstr(self.ufl_operands[0], self), self._side) + """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 = "-" diff --git a/ufl/sobolevspace.py b/ufl/sobolevspace.py index d54034459..6dfa83633 100644 --- a/ufl/sobolevspace.py +++ b/ufl/sobolevspace.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- -"""This module defines a symbolic heirarchy of Sobolev spaces to enable -symbolic reasoning about the spaces in which finite elements lie.""" +"""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) @@ -17,21 +18,26 @@ 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. + """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. - :param name: The name of this space, - :param parents: A set of Sobolev spaces of which this - space is a subspace.""" - + 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. @@ -47,77 +53,67 @@ def __init__(self, name, parents=None): "HCurl": 0, "HEin": 0, "HDivDiv": 0, - "DirectionalH": 0 + "HCurlDiv": 0, + "DirectionalH": 0, }[self.name] def __str__(self): + """Format as a string.""" return self.name def __repr__(self): - r = "SobolevSpace(%s, %s)" % (repr(self.name), repr( - list(self.parents))) - return r + """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. - """ + """Returns the Sobolev space associated with a particular spatial coordinate.""" return self def __contains__(self, other): - """Implement `fe in s` where `fe` is a - :class:`~finiteelement.FiniteElement` and `s` is a - :class:`SobolevSpace`""" + """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) + 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".""" + """In common with intrinsic Python sets, < indicates "is a proper subset of".""" return other in self.parents - def __call__(self, element): - """Syntax shortcut to create a HDivElement or HCurlElement.""" - if self.name == "HDiv": - from ufl.finiteelement import HDivElement - return HDivElement(element) - elif self.name == "HCurl": - from ufl.finiteelement import HCurlElement - return HCurlElement(element) - raise NotImplementedError( - "SobolevSpace has no call operator (only the specific HDiv and HCurl instances)." - ) - @total_ordering class DirectionalSobolevSpace(SobolevSpace): - """Symbolic representation of a Sobolev space with varying smoothness - in differerent spatial directions. + """Directional Sobolev space. + Symbolic representation of a Sobolev space with varying smoothness + in different spatial directions. """ def __init__(self, orders): """Instantiate a DirectionalSobolevSpace object. - :arg orders: an iterable of orders of weak derivatives, where - the position denotes in what spatial variable the - smoothness requirement is enforced. + 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.") + 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) @@ -125,52 +121,51 @@ def __init__(self, orders): self._spatial_indices = range(len(self._orders)) def __getitem__(self, spatial_index): - """Returns the Sobolev space associated with a particular - spatial coordinate. - """ + """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): - """Implement `fe in s` where `fe` is a - :class:`~finiteelement.FiniteElement` and `s` is a - :class:`DirectionalSobolevSpace`""" + """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)) + 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.""" + """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) + 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"]: + elif other.name in ["HDivDiv", "HEin", "HCurlDiv"]: # Don't know how these spaces compare - return NotImplementedError( - "Don't know how to compare with %s" % other.name) + 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) + return any(self._orders[i] > other._order for i in self._spatial_indices) def __str__(self): - return self.name + "(%s)" % ", ".join(map(str, self._orders)) + """Format as a string.""" + return f"{self.name}({', '.join(map(str, self._orders))})" L2 = SobolevSpace("L2") @@ -181,3 +176,4 @@ def __str__(self): HInf = SobolevSpace("HInf", [H2, H1, HDiv, HCurl, L2]) HEin = SobolevSpace("HEin", [L2]) HDivDiv = SobolevSpace("HDivDiv", [L2]) +HCurlDiv = SobolevSpace("HCurlDiv", [L2]) diff --git a/ufl/sorting.py b/ufl/sorting.py index 117c4aa1c..6b9324514 100644 --- a/ufl/sorting.py +++ b/ufl/sorting.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- -"""This module contains a sorting rule for expr objects that -is more robust w.r.t. argument numbering than using repr.""" +"""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) @@ -11,10 +12,8 @@ # Modified by Anders Logg, 2009-2010. # Modified by Johan Hake, 2010. - from functools import cmp_to_key -from ufl.core.expr import Expr from ufl.argument import Argument from ufl.coefficient import Coefficient from ufl.core.multiindex import FixedIndex, MultiIndex @@ -22,6 +21,7 @@ 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 @@ -34,7 +34,7 @@ def _cmp_multi_index(a, b): if x < y: return -1 elif x > y: - return +1 + return 1 else: # Same value, no decision continue @@ -43,7 +43,7 @@ def _cmp_multi_index(a, b): return -1 elif fix2: # Sort fixed before free - return +1 + return 1 else: # Both are Index, no decision, do not depend on count! pass @@ -54,6 +54,7 @@ def _cmp_multi_index(a, b): 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. @@ -61,18 +62,20 @@ def _cmp_label(a, b): 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 + 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) @@ -80,20 +83,21 @@ def _cmp_argument(a, b): if x < y: return -1 elif x > y: - return +1 + 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) + return -1 if x < y else (0 if x == y else 1) # Hack up a MultiFunction-like type dispatch for terminal comparisons -_terminal_cmps = [_cmp_terminal_by_repr] * Expr._ufl_num_typecodes_ +_terminal_cmps = {} _terminal_cmps[MultiIndex._ufl_typecode_] = _cmp_multi_index _terminal_cmps[Argument._ufl_typecode_] = _cmp_argument _terminal_cmps[Coefficient._ufl_typecode_] = _cmp_coefficient @@ -101,8 +105,7 @@ def _cmp_terminal_by_repr(a, b): def cmp_expr(a, b): - "Replacement for cmp(a, b), removed in Python 3, for Expr objects." - + """Replacement for cmp(a, b), removed in Python 3, for Expr objects.""" # Modelled after pre_traversal to avoid recursion: left = [(a, b)] while left: @@ -111,12 +114,16 @@ def cmp_expr(a, b): # First sort quickly by type code x, y = a._ufl_typecode_, b._ufl_typecode_ if x != y: - return -1 if x < y else +1 + 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_: - c = _terminal_cmps[x](a, b) + if x in _terminal_cmps: + c = _terminal_cmps[x](a, b) + else: + c = _cmp_terminal_by_repr(a, b) + if c: return c else: @@ -134,7 +141,7 @@ def cmp_expr(a, b): bops = b.ufl_operands # Sort by children in natural order - for (r, s) in zip(aops, bops): + for r, s in zip(aops, bops): # Skip subtree if objects are the same if r is s: continue @@ -149,18 +156,19 @@ def cmp_expr(a, b): # 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 + 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 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:]: diff --git a/ufl/split_functions.py b/ufl/split_functions.py index 1f3d067ea..4f4cb4aaf 100644 --- a/ufl/split_functions.py +++ b/ufl/split_functions.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -"Algorithm for splitting a Coefficient or Argument into subfunctions." - +"""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) @@ -9,18 +7,22 @@ # # Modified by Anders Logg, 2008 -from ufl.log import error -from ufl.utils.sequences import product -from ufl.finiteelement import MixedElement, TensorElement -from ufl.tensors import as_vector, as_matrix, ListTensor +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): - """UFL operator: If v is a Coefficient or Argument in a mixed space, returns - a tuple with the function components corresponding to the subelements.""" + """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 @@ -40,30 +42,28 @@ def split(v): # 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,) = ops[0].ufl_operands[1] + (end,) = ops[-1].ufl_operands[1] begin = int(begin) end = int(end) + 1 else: - error("Don't know how to split %s." % (v,)) + raise ValueError(f"Don't know how to split {v}.") else: - error("Don't know how to split %s." % (v,)) + 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 not isinstance(element, MixedElement): + if element.num_sub_elements == 0: assert end is None return (v,) - if isinstance(element, TensorElement): - if element.symmetry(): - error("Split not implemented for symmetric tensor elements.") - if len(v.ufl_shape) != 1: - error("Don't know how to split tensor valued mixed functions without flattened index space.") + 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 = product(element.value_shape()) + value_size = v.ufl_function_space().value_size if end is None: end = value_size else: @@ -71,41 +71,48 @@ def split(v): # corresponding to beginning of range j = begin while True: - sub_i, j = element.extract_subelement_component(j) - element = element.sub_elements()[sub_i] + 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 product(element.value_shape()) == (end - begin): + 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()): + for i, e in enumerate(element.sub_elements): # Get shape, size, indices, and v components # corresponding to subelement value - shape = e.value_shape() + 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)] + 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 + (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])]) + subv = as_matrix( + [components[i * shape[1] : (i + 1) * shape[1]] for i in range(shape[0])] + ) else: - error("Don't know how to split functions with sub functions of rank %d." % rank) + 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: - error("Function splitting failed to extract components for whole intended range. Something is wrong.") + raise ValueError( + "Function splitting failed to extract components for whole intended range." + ) return tuple(sub_functions) diff --git a/ufl/tensoralgebra.py b/ufl/tensoralgebra.py index 7f240c4c0..4ec364191 100644 --- a/ufl/tensoralgebra.py +++ b/ufl/tensoralgebra.py @@ -1,20 +1,17 @@ -# -*- coding: utf-8 -*- """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.log import error +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.constantvalue import Zero -from ufl.algebra import Operator, Conj +from ufl.index_combination_utils import merge_nonoverlapping_indices from ufl.precedence import parstr from ufl.sorting import sorted_expr -from ufl.index_combination_utils import merge_nonoverlapping_indices # Algebraic operations on tensors: # FloatValues: @@ -44,13 +41,18 @@ # --- 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 @@ -86,33 +88,42 @@ def __init__(self, operands): @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: - error("Transposed is only defined for rank 2 tensors.") + 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) @@ -122,6 +133,7 @@ def __new__(cls, 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 @@ -129,22 +141,29 @@ def __init__(self, a, b): @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): - return "%s (X) %s" % (parstr(self.ufl_operands[0], self), - parstr(self.ufl_operands[1], 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: - error("Shapes do not match: %s and %s." % (ufl_err_str(a), ufl_err_str(b))) + 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): @@ -161,7 +180,7 @@ def __new__(cls, a, b): return CompoundTensorOperator.__new__(cls) def __init__(self, a, b): - + """Initialise.""" CompoundTensorOperator.__init__(self, (a, b)) fi, fid = merge_nonoverlapping_indices(a, b) @@ -171,26 +190,31 @@ def __init__(self, a, b): ufl_shape = () def __str__(self): - return "%s : %s" % (parstr(self.ufl_operands[0], self), - parstr(self.ufl_operands[1], 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) + scalar = ar == 0 and br == 0 # Checks if not ((ar >= 1 and br >= 1) or scalar): - error("Dot product requires non-scalar arguments, " - "got arguments with ranks %d and %d." % (ar, br)) + raise ValueError( + "Dot product requires non-scalar arguments, " + f"got arguments with ranks {ar} and {br}." + ) if not (scalar or ash[-1] == bsh[0]): - error("Dimension mismatch in dot product.") + raise ValueError("Dimension mismatch in dot product.") # Simplification if isinstance(a, Zero) or isinstance(b, Zero): @@ -203,6 +227,7 @@ def __new__(cls, 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 @@ -210,25 +235,64 @@ def __init__(self, a, b): @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): - return "%s . %s" % (parstr(self.ufl_operands[0], self), - parstr(self.ufl_operands[1], 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): - error("Cross product requires arguments of rank 1, got %s and %s." % ( - ufl_err_str(a), ufl_err_str(b))) + 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): @@ -238,6 +302,7 @@ def __new__(cls, 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 @@ -246,18 +311,21 @@ def __init__(self, a, b): ufl_shape = (3,) def __str__(self): - return "%s x %s" % (parstr(self.ufl_operands[0], self), - parstr(self.ufl_operands[1], 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: - error("Trace of tensor with rank != 2 is undefined.") + raise ValueError("Trace of tensor with rank != 2 is undefined.") # Simplification if isinstance(A, Zero): @@ -266,30 +334,35 @@ def __new__(cls, A): 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): - error("Determinant of tensor with rank != 2 is undefined.") + raise ValueError("Determinant of tensor with rank != 2 is undefined.") if r == 2 and sh[0] != sh[1]: - error("Cannot take determinant of rectangular rank 2 tensor.") + raise ValueError("Cannot take determinant of rectangular rank 2 tensor.") if Afi: - error("Not expecting free indices in determinant.") + raise ValueError("Not expecting free indices in determinant.") # Simplification if isinstance(A, Zero): @@ -300,9 +373,11 @@ def __new__(cls, 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] @@ -310,17 +385,20 @@ def __str__(self): # 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: - error("Not expecting free indices in Inverse.") + raise ValueError("Not expecting free indices in Inverse.") if isinstance(A, Zero): - error("Division by zero!") + raise ValueError("Division by zero!") # Simplification if r == 0: @@ -328,63 +406,76 @@ def __new__(cls, A): # More checks if r != 2: - error("Inverse of tensor with rank != 2 is undefined.") + raise ValueError("Inverse of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: - error("Cannot take inverse of rectangular matrix with dimensions %s." % (sh,)) + 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: - error("Cofactor of tensor with rank != 2 is undefined.") + raise ValueError("Cofactor of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: - error("Cannot take cofactor of rectangular matrix with dimensions %s." % (sh,)) + raise ValueError(f"Cannot take cofactor of rectangular matrix with dimensions {sh}.") if A.ufl_free_indices: - error("Not expecting free indices in Cofactor.") + raise ValueError("Not expecting free indices in Cofactor.") if isinstance(A, Zero): - error("Cannot take cofactor of zero matrix.") + 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: - error("Deviatoric part of tensor with rank != 2 is undefined.") + raise ValueError("Deviatoric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: - error("Cannot take deviatoric part of rectangular matrix with dimensions %s." % (sh,)) + raise ValueError( + f"Cannot take deviatoric part of rectangular matrix with dimensions {sh}." + ) if A.ufl_free_indices: - error("Not expecting free indices in Deviatoric.") + raise ValueError("Not expecting free indices in Deviatoric.") # Simplification if isinstance(A, Zero): @@ -393,27 +484,32 @@ def __new__(cls, A): 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: - error("Skew symmetric part of tensor with rank != 2 is undefined.") + raise ValueError("Skew symmetric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: - error("Cannot take skew part of rectangular matrix with dimensions %s." % (sh,)) + raise ValueError(f"Cannot take skew part of rectangular matrix with dimensions {sh}.") if Afi: - error("Not expecting free indices in Skew.") + raise ValueError("Not expecting free indices in Skew.") # Simplification if isinstance(A, Zero): @@ -422,27 +518,34 @@ def __new__(cls, A): 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: - error("Symmetric part of tensor with rank != 2 is undefined.") + raise ValueError("Symmetric part of tensor with rank != 2 is undefined.") if sh[0] != sh[1]: - error("Cannot take symmetric part of rectangular matrix with dimensions %s." % (sh,)) + raise ValueError( + f"Cannot take symmetric part of rectangular matrix with dimensions {sh}." + ) if Afi: - error("Not expecting free indices in Sym.") + raise ValueError("Not expecting free indices in Sym.") # Simplification if isinstance(A, Zero): @@ -451,7 +554,9 @@ def __new__(cls, A): return CompoundTensorOperator.__new__(cls) def __init__(self, A): + """Initialise.""" CompoundTensorOperator.__init__(self, (A,)) def __str__(self): - return "sym(%s)" % self.ufl_operands[0] + """Format as a string.""" + return f"sym({self.ufl_operands[0]})" diff --git a/ufl/tensors.py b/ufl/tensors.py index 728b36b7e..3edb0759c 100644 --- a/ufl/tensors.py +++ b/ufl/tensors.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """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) @@ -9,28 +7,29 @@ # # Modified by Massimiliano Leoni, 2016. -from ufl.log import error -from ufl.core.ufl_type import ufl_type +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.constantvalue import as_ufl, Zero -from ufl.core.multiindex import Index, FixedIndex, MultiIndex, indices -from ufl.indexed import Indexed +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): - """UFL operator type: Wraps a list of expressions into a tensor valued expression of one higher rank.""" + """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): - error("Expecting only UFL expressions in ListTensor constructor.") + raise ValueError("Expecting only UFL expressions in ListTensor constructor.") # Get properties of the first expression e0 = expressions[0] @@ -40,11 +39,17 @@ def __new__(cls, *expressions): # Obviously, each subexpression must have the same shape if any(sh != e.ufl_shape for e in expressions[1:]): - error("Cannot create a tensor by joining subexpressions with different shapes.") + raise ValueError( + "Cannot create a tensor by joining subexpressions with different shapes." + ) if any(fi != e.ufl_free_indices for e in expressions[1:]): - error("Cannot create a tensor where the components have different free indices.") + 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:]): - error("Cannot create a tensor where the components have different free index dimensions.") + 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): @@ -54,21 +59,28 @@ def __new__(cls, *expressions): 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): - error("Can't combine subtensor expressions with different sets of free indices.") + 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): - error("Can only evaluate scalars, expecting a component " - "tuple of length %d, not %s." % (len(self.ufl_shape), component)) + 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: @@ -77,6 +89,7 @@ def evaluate(self, x, mapping, component, index_values, derivatives=()): return a.evaluate(x, mapping, component, index_values) def __getitem__(self, key): + """Get an item.""" origkey = key if isinstance(key, MultiIndex): @@ -91,6 +104,8 @@ def __getitem__(self, key): 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): @@ -105,46 +120,54 @@ def substring(expressions, indent): 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): - """UFL operator type: Maps the free indices of a scalar valued expression to tensor axes.""" + """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]) + 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): - error("Expecting ufl expression.") + raise ValueError("Expecting ufl expression.") if expression.ufl_shape != (): - error("Expecting scalar valued expression.") + raise ValueError("Expecting scalar valued expression.") if not isinstance(indices, MultiIndex): - error("Expecting a MultiIndex.") + raise ValueError("Expecting a MultiIndex.") if not all(isinstance(i, Index) for i in indices): - error("Expecting sequence of Index objects, not %s." % indices._ufl_err_str_()) + 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]) + 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 @@ -153,14 +176,16 @@ def _ufl_expr_reconstruct_(self, expressions, indices): 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): - error("Expecting a component matching the indices tuple.") + raise ValueError("Expecting a component matching the indices tuple.") # Map component to indices for i, c in zip(indices, component): @@ -174,19 +199,24 @@ def evaluate(self, x, mapping, component, index_values): 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) @@ -195,9 +225,11 @@ def _as_list_tensor(expressions): def from_numpy_to_lists(expressions): + """Convert Numpy array to lists.""" try: - import numpy - if isinstance(expressions, numpy.ndarray): + import numpy as np + + if isinstance(expressions, np.ndarray): if expressions.shape == (): # Unwrap scalar ndarray return expressions.item() @@ -209,7 +241,7 @@ def from_numpy_to_lists(expressions): def as_tensor(expressions, indices=None): - """UFL operator: Make a tensor valued expression. + """Make a tensor valued expression. This works in two different ways, by using indices or lists. @@ -236,7 +268,7 @@ def as_tensor(expressions, indices=None): # Sanity check if not isinstance(expressions, (list, tuple, Expr)): - error("Expecting nested list or tuple.") + raise ValueError("Expecting nested list or tuple.") # Recursive conversion from nested lists to nested ListTensor # objects @@ -265,12 +297,12 @@ def as_tensor(expressions, indices=None): def as_matrix(expressions, indices=None): - "UFL operator: As *as_tensor()*, but limited to rank 2 tensors." + """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: - error("Expecting rank 2 tensor.") + raise ValueError("Expecting rank 2 tensor.") return expressions # To avoid importing numpy unneeded, it's quite slow... @@ -279,23 +311,23 @@ def as_matrix(expressions, indices=None): # Check for expected list structure if not isinstance(expressions, (list, tuple)): - error("Expecting nested list or tuple of Exprs.") + raise ValueError("Expecting nested list or tuple of Exprs.") if not isinstance(expressions[0], (list, tuple)): - error("Expecting nested list or tuple of Exprs.") + raise ValueError("Expecting nested list or tuple of Exprs.") else: if len(indices) != 2: - error("Expecting exactly two indices.") + raise ValueError("Expecting exactly two indices.") return as_tensor(expressions, indices) def as_vector(expressions, index=None): - "UFL operator: As ``as_tensor()``, but limited to rank 1 tensors." + """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: - error("Expecting rank 1 tensor.") + raise ValueError("Expecting rank 1 tensor.") return expressions # To avoid importing numpy unneeded, it's quite slow... @@ -304,22 +336,23 @@ def as_vector(expressions, index=None): # Check for expected list structure if not isinstance(expressions, (list, tuple)): - error("Expecting nested list or tuple of Exprs.") + raise ValueError("Expecting nested list or tuple of Exprs.") else: if not isinstance(index, Index): - error("Expecting a single Index object.") + raise ValueError("Expecting a single Index object.") index = (index,) return as_tensor(expressions, index) def as_scalar(expression): - """Given a scalar or tensor valued expression A, returns either of the tuples:: + """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.""" + such that a is always a scalar valued expression. + """ ii = indices(len(expression.ufl_shape)) if ii: expression = expression[ii] @@ -327,76 +360,54 @@ def as_scalar(expression): def as_scalars(*expressions): - """Given multiple scalar or tensor valued expressions A, returns either of the tuples:: + """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.""" + 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 relabel(A, indexmap): - "UFL operator: Relabel free indices of :math:`A` with new indices, using the given mapping." - ii = tuple(sorted(indexmap.keys())) - jj = tuple(indexmap[i] for i in ii) - if not all(isinstance(i, Index) for i in ii): - error("Expecting Index objects.") - if not all(isinstance(j, Index) for j in jj): - error("Expecting Index objects.") - return as_tensor(A, ii)[jj] - - -# --- Experimental support for dyadic notation: - 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): - "UFL value: A constant unit vector in direction *i* with dimension *d*." + """A constant unit vector in direction *i* with dimension *d*.""" return as_vector(unit_list(i, d)) def unit_vectors(d): - """UFL value: A tuple of constant unit vectors in all directions with - dimension *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): - "UFL value: A constant unit matrix in direction *i*,*j* with dimension *d*." + """A constant unit matrix in direction *i*,*j* with dimension *d*.""" return as_matrix(unit_list2(i, j, d)) def unit_matrices(d): - """UFL value: A tuple of constant unit matrices in all directions with - dimension *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 dyad(d, *iota): - "TODO: Develop this concept, can e.g. write A[i,j]*dyad(j,i) for the transpose." - from ufl.constantvalue import Identity - from ufl.operators import outer # a bit of circular dependency issue here - Id = Identity(d) - i = iota[0] - e = as_vector(Id[i, :], i) - for i in iota[1:]: - e = outer(e, as_vector(Id[i, :], i)) - return e - - 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, () @@ -415,6 +426,7 @@ def unit_indexed_tensor(shape, component): def unwrap_list_tensor(lt): + """Unwrap a list tensor.""" components = [] sh = lt.ufl_shape subs = lt.ufl_operands diff --git a/ufl/utils/__init__.py b/ufl/utils/__init__.py index e69de29bb..dc51750ef 100644 --- a/ufl/utils/__init__.py +++ b/ufl/utils/__init__.py @@ -0,0 +1 @@ +"""Utilities.""" diff --git a/ufl/utils/counted.py b/ufl/utils/counted.py index 66d3fd79f..9aed33b7f 100644 --- a/ufl/utils/counted.py +++ b/ufl/utils/counted.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Utilites for types with a global unique counter attached to each object." +"""Mixin class for types with a global unique counter attached to each object.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -7,37 +6,36 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import itertools -def counted_init(self, count=None, countedclass=None): - "Initialize a counted object, see ExampleCounted below for how to use." - if countedclass is None: - countedclass = type(self) +class Counted: + """Mixin class for globally counted objects.""" - if count is None: - count = countedclass._globalcount + # Mixin classes do not work well with __slots__ so _count must be + # added to the __slots__ of the inheriting class + __slots__ = () - self._count = count + _counter = None - if self._count >= countedclass._globalcount: - countedclass._globalcount = self._count + 1 + 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. -class ExampleCounted(object): - """An example class for classes of objects identified by a global counter. + """ + # 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() - Mimic this class to create globally counted objects within a single type. - """ - # Store the count for each object - __slots__ = ("_count",) + self._count = count if count is not None else next(counted_class._counter) + self._counted_class = counted_class - # Store a global counter with the class - _globalcount = 0 - - # Call counted_init with an optional constructor argument and the class - def __init__(self, count=None): - counted_init(self, count, ExampleCounted) - - # Make the count accessible def count(self): + """Get count.""" return self._count diff --git a/ufl/utils/derivativetuples.py b/ufl/utils/derivativetuples.py deleted file mode 100644 index 498f63a3d..000000000 --- a/ufl/utils/derivativetuples.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -"This module contains a collection of utilities for representing partial derivatives as integer tuples." - -# Copyright (C) 2013-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 - -import itertools - - -def derivative_counts_to_listing(derivative_counts): - """Convert a derivative count tuple to a derivative listing tuple. - - The derivative d^3 / dy^2 dz is represented - in counting form as (0, 2, 1) meaning (dx^0, dy^2, dz^1) - and in listing form as (1, 1, 2) meaning (dy, dy, dz). - """ - derivatives = [] # = 1 - for i, d in enumerate(derivative_counts): - derivatives.extend((i,) * d) # *= d/dx_i^d - return tuple(derivatives) - - -def derivative_listing_to_counts(derivatives, gdim): - """Convert a derivative listing tuple to a derivative count tuple. - - The derivative d^3 / dy^2 dz is represented - in counting form as (0, 2, 1) meaning (dx^0, dy^2, dz^1) - and in listing form as (1, 1, 2) meaning (dy, dy, dz). - """ - derivative_counts = [0] * gdim - for d in derivatives: - derivative_counts[d] += 1 - return tuple(derivative_counts) - - -def compute_derivative_tuples(n, gdim): - """Compute the list of all derivative tuples for derivatives of - given total order n and given geometric dimension gdim. This - function returns two lists. The first is a list of tuples, where - each tuple of length n specifies the coordinate directions of the - n derivatives. The second is a corresponding list of tuples, where - each tuple of length gdim specifies the number of derivatives in - each direction. Both lists have length gdim^n and are ordered as - expected by the UFC function tabulate_basis_derivatives. - - Example: If n = 2 and gdim = 3, then the nice tuples are - - (0, 0) <--> (2, 0, 0) <--> d^2/dxdx - (0, 1) <--> (1, 1, 0) <--> d^2/dxdy - (0, 2) <--> (1, 0, 1) <--> d^2/dxdz - (1, 0) <--> (1, 1, 0) <--> d^2/dydx - (1, 1) <--> (0, 2, 0) <--> d^2/dydy - (1, 2) <--> (0, 1, 1) <--> d^2/dydz - (2, 0) <--> (1, 0, 1) <--> d^2/dzdx - (2, 1) <--> (0, 1, 1) <--> d^2/dzdy - (2, 2) <--> (0, 0, 2) <--> d^2/dzdz - """ - - # Create list of derivatives (note that we have d^n derivatives) - deriv_tuples = [d for d in itertools.product(*(n * [range(0, gdim)]))] - - # Translate from list of derivative tuples to list of tuples - # expressing the number of derivatives in each dimension... - _deriv_tuples = [tuple(len([_d for _d in d if _d == i]) for i in range(gdim)) - for d in deriv_tuples] - - return deriv_tuples, _deriv_tuples diff --git a/ufl/utils/dicts.py b/ufl/utils/dicts.py deleted file mode 100644 index a1d7fdaa8..000000000 --- a/ufl/utils/dicts.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -"""Various dict 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 - - -def split_dict(d, criteria): - """Split a dict d into two dicts based on a criteria on the keys.""" - a = {} - b = {} - for (k, v) in d.items(): - if criteria(k): - a[k] = v - else: - b[k] = v - return a, b - - -def slice_dict(dictionary, keys, default=None): - return tuple(dictionary.get(k, default) for k in keys) - - -def some_key(a_dict): - """Return an arbitrary key from a dictionary.""" - return next(a_dict.keys()) - - -def mergedicts(dicts): - d = dict(dicts[0]) - for d2 in dicts[1:]: - d.update(d2) - return d - - -def mergedicts2(d1, d2): - d = dict(d1) - d.update(d2) - return d - - -def subdict(superdict, keys): - return dict((k, superdict[k]) for k in keys) - - -def dict_sum(items): - """Construct a dict, in between dict(items) and sum(items), by accumulating items for each key.""" - d = {} - for k, v in items: - if k not in d: - d[k] = v - else: - d[k] += v - return d - - -class EmptyDictType(dict): - def __setitem__(self, key, value): - from ufl.log import error - error("This is a frozen unique empty dictionary object, inserting values is an error.") - - def update(self, *args, **kwargs): - from ufl.log import error - error("This is a frozen unique empty dictionary object, inserting values is an error.") - - -EmptyDict = EmptyDictType() diff --git a/ufl/utils/formatting.py b/ufl/utils/formatting.py index 76f55f7b9..7c2d4336a 100644 --- a/ufl/utils/formatting.py +++ b/ufl/utils/formatting.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """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) @@ -12,31 +10,25 @@ def camel2underscore(name): """Convert a CamelCaps string to underscore_syntax.""" letters = [] lastlower = False - for l in name: - thislower = l.islower() + 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("_") - l = l.lower() # noqa: E741 + i = i.lower() lastlower = thislower - letters.append(l) + letters.append(i) return "".join(letters) -def lstr(l): +def lstr(a): """Pretty-print list or tuple, invoking str() on items instead of repr() like str() does.""" - if isinstance(l, list): - return "[" + ", ".join(lstr(item) for item in l) + "]" - elif isinstance(l, tuple): - return "(" + ", ".join(lstr(item) for item in l) + ")" - return str(l) - - -def dstr(d, colsize=80): - """Pretty-print dictionary of key-value pairs.""" - sorted_keys = sorted(d.keys()) - return tstr([(key, d[key]) for key in sorted_keys], colsize) + 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): @@ -53,7 +45,7 @@ def tstr(t, colsize=80): # Pretty-print table s = "" - for (key, value) in t: + for key, value in t: key = str(key) if isinstance(value, str): value = "'%s'" % value @@ -69,11 +61,6 @@ def tstr(t, colsize=80): return s -def sstr(s): - """Pretty-print set.""" - return ", ".join(str(x) for x in s) - - def istr(o): """Format object as string, inserting ? for None.""" if o is None: @@ -84,4 +71,66 @@ def istr(o): def estr(elements): """Format list of elements for printing.""" - return ", ".join(e.shortstr() for e in elements) + 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 diff --git a/ufl/utils/indexflattening.py b/ufl/utils/indexflattening.py index a71758b72..f9935e51a 100644 --- a/ufl/utils/indexflattening.py +++ b/ufl/utils/indexflattening.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"""This module contains a collection of utilities for mapping between multiindices and a flattened index space.""" +"""Collection of utilities for mapping between multiindices and a flattened index space.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # diff --git a/ufl/utils/sequences.py b/ufl/utils/sequences.py index 110a5b75a..71c845349 100644 --- a/ufl/utils/sequences.py +++ b/ufl/utils/sequences.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Various sequence manipulation utilities.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs @@ -8,7 +7,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from functools import reduce -import numpy + +import numpy as np def product(sequence): @@ -19,54 +19,13 @@ def product(sequence): return p -def unzip(seq): - """Inverse operation of zip: - - unzip(zip(a, b)) == (a, b).""" - return [s[0] for s in seq], [s[1] for s in seq] - - -def xor(a, b): - return bool(a) if b else not a - - -def or_tuples(seqa, seqb): - """Return 'or' of all pairs in two sequences of same length.""" - return tuple(a or b for (a, b) in zip(seqa, seqb)) - - -def and_tuples(seqa, seqb): - """Return 'and' of all pairs in two sequences of same length.""" - return tuple(a and b for (a, b) in zip(seqa, seqb)) - - -def iter_tree(tree): - """Iterate over all nodes in a tree represented - by lists of lists of leaves.""" - if isinstance(tree, list): - for node in tree: - for i in iter_tree(node): - yield i - else: - yield tree - - -def recursive_chain(lists): - for l in lists: - if isinstance(l, str): - yield l - else: - for s in recursive_chain(l): - yield s - - def max_degree(degrees): """Maximum degree for mixture of scalar and tuple degrees.""" - # numpy.maximum broadcasts scalar degrees to tuple degrees if - # necessary. reduce applies numpy.maximum pairwise. - degree = reduce(numpy.maximum, map(numpy.asarray, 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 + degree = int(degree) # scalar degree return degree diff --git a/ufl/utils/sorting.py b/ufl/utils/sorting.py index fe73fe890..896ca2099 100644 --- a/ufl/utils/sorting.py +++ b/ufl/utils/sorting.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Utilites for sorting." +"""Utilites for sorting.""" # Copyright (C) 2008-2016 Johan Hake # @@ -7,27 +6,23 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ufl.log import warning +import warnings def topological_sorting(nodes, edges): - """ - Return a topologically sorted list of the nodes - - Implemented algorithm from Wikipedia :P + """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) - continue + break while S: node = S.pop(0) @@ -47,21 +42,18 @@ def topological_sorting(nodes, edges): def sorted_by_count(seq): - "Sort a sequence by the item.count()." + """Sort a sequence by the item.count().""" return sorted(seq, key=lambda x: x.count()) -def sorted_by_ufl_id(seq): - "Sort a sequence by the item.ufl_id()." - return sorted(seq, key=lambda x: x.ufl_id()) - - def sorted_by_key(mapping): - "Sort dict items by key, allowing different key types." + """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) @@ -90,8 +82,13 @@ def canonicalize_metadata(metadata): 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: - warning("Applying str() to a metadata value of type {0}, don't know if this is safe.".format(type(value).__name__)) + 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) diff --git a/ufl/utils/stacks.py b/ufl/utils/stacks.py index 736035856..da4c39614 100644 --- a/ufl/utils/stacks.py +++ b/ufl/utils/stacks.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -"Various utility data structures." +"""Various utility data structures.""" # Copyright (C) 2008-2016 Martin Sandve Alnæs # @@ -12,19 +11,27 @@ 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 dict that can be changed incrementally with 'd.push(k,v)' and have changes rolled back with 'k,v = d.pop()'.""" + """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 = [] diff --git a/ufl/utils/ufltypedicts.py b/ufl/utils/ufltypedicts.py deleted file mode 100644 index e6fc7f93e..000000000 --- a/ufl/utils/ufltypedicts.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -"""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 UFLTypeDict(dict): - def __init__(self): - dict.__init__(self) - - def __getitem__(self, key): - return dict.__getitem__(self, key._ufl_class_) - - def __setitem__(self, key, value): - return dict.__setitem__(self, key._ufl_class_, value) - - def __delitem__(self, key): - return dict.__delitem__(self, key._ufl_class_) - - def __contains__(self, key): - return dict.__contains__(self, key._ufl_class_) - - -class UFLTypeDefaultDict(dict): - def __init__(self, default): - dict.__init__(self) - - def make_default(): - return default - self.setdefault(make_default) - - def __getitem__(self, key): - return dict.__getitem__(self, key._ufl_class_) - - def __setitem__(self, key, value): - return dict.__setitem__(self, key._ufl_class_, value) - - def __delitem__(self, key): - return dict.__delitem__(self, key._ufl_class_) - - def __contains__(self, key): - return dict.__contains__(self, key._ufl_class_) diff --git a/ufl/variable.py b/ufl/variable.py index c52c8d14c..ad671e10c 100644 --- a/ufl/variable.py +++ b/ufl/variable.py @@ -1,61 +1,70 @@ -# -*- coding: utf-8 -*- -"""Defines the Variable and Label classes, used to label -expressions as variables for differentiation.""" +"""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.utils.counted import counted_init -from ufl.log import error +from ufl.constantvalue import as_ufl from ufl.core.expr import Expr -from ufl.core.ufl_type import ufl_type -from ufl.core.terminal import Terminal from ufl.core.operator import Operator -from ufl.constantvalue import as_ufl +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): - __slots__ = ("_count",) +class Label(Terminal, Counted): + """Label.""" - _globalcount = 0 + __slots__ = ("_count", "_counted_class") def __init__(self, count=None): + """Initialise.""" Terminal.__init__(self) - counted_init(self, count, Label) - - def count(self): - return self._count + 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): - error("Label has no shape (it is not a tensor expression).") + """Get the UFL shape.""" + raise ValueError("Label has no shape (it is not a tensor expression).") @property def ufl_free_indices(self): - error("Label has no free indices (it is not a tensor expression).") + """Get the UFL free indices.""" + raise ValueError("Label has no free indices (it is not a tensor expression).") @property def ufl_index_dimensions(self): - error("Label has no free indices (it is not a tensor expression).") + """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 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): @@ -70,9 +79,11 @@ class Variable(Operator): 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: @@ -80,32 +91,39 @@ def __init__(self, expression, label=None): # Checks if not isinstance(expression, Expr): - error("Expecting Expr.") + raise ValueError("Expecting Expr.") if not isinstance(label, Label): - error("Expecting a Label.") + raise ValueError("Expecting a Label.") if expression.ufl_free_indices: - error("Variable cannot wrap an expression with 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): - return (isinstance(other, Variable) and - self.ufl_operands[1] == other.ufl_operands[1] and - self.ufl_operands[0] == other.ufl_operands[0]) + """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): - return "var%d(%s)" % (self.ufl_operands[1].count(), - self.ufl_operands[0]) + """Format as a string.""" + return "var%d(%s)" % (self.ufl_operands[1].count(), self.ufl_operands[0])