├── ++version.py ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── new_element.md ├── pull_request_template.md ├── scripts │ ├── coveralls_check.py │ ├── trigger_rtd_build.py │ └── version.py └── workflows │ ├── coveralls.yml │ ├── defelement.yml │ ├── release.yml │ ├── run-tests.yml │ ├── style-checks.yml │ └── test-packages.yml ├── .gitignore ├── .readthedocs.yml ├── ADDING_AN_ELEMENT.md ├── CHANGELOG.md ├── CHANGELOG_SINCE_LAST_VERSION.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── VERSION ├── codemeta.json ├── demo ├── custom_element.py ├── lagrange.py ├── nedelec.py ├── stiffness_matrix.py └── test_demos.py ├── docs ├── Makefile ├── conf.py ├── demos │ ├── custom_element.rst │ ├── index.rst │ ├── lagrange.rst │ ├── nedelec.rst │ └── stiffness_matrix.rst ├── index.rst └── requirements.txt ├── img ├── dual_polygon_numbering.png ├── hexahedron_numbering.png ├── interval_numbering.png ├── prism_numbering.png ├── pyramid_numbering.png ├── quadrilateral_numbering.png ├── tetrahedron_numbering.png └── triangle_numbering.png ├── joss ├── paper.bib └── paper.md ├── logo ├── favicon.png ├── favicon.svg ├── logo-1280-640.png ├── logo-1280-640.svg ├── logo.png └── logo.svg ├── prepare_release.py ├── pyproject.toml ├── scripts ├── draw_logo.py ├── draw_references.py └── test_scripts.py ├── symfem ├── __init__.py ├── basis_functions.py ├── caching.py ├── create.py ├── elements │ ├── __init__.py │ ├── abf.py │ ├── ac.py │ ├── alfeld_sorokina.py │ ├── argyris.py │ ├── aw.py │ ├── bddf.py │ ├── bdfm.py │ ├── bdm.py │ ├── bell.py │ ├── bernardi_raugel.py │ ├── bernstein.py │ ├── bfs.py │ ├── bubble.py │ ├── conforming_crouzeix_raviart.py │ ├── crouzeix_raviart.py │ ├── direct_serendipity.py │ ├── dpc.py │ ├── dual.py │ ├── enriched_galerkin.py │ ├── fortin_soulie.py │ ├── gopalakrishnan_lederer_schoberl.py │ ├── guzman_neilan.py │ ├── hct.py │ ├── hermite.py │ ├── hhj.py │ ├── huang_zhang.py │ ├── kmv.py │ ├── lagrange.py │ ├── lagrange_prism.py │ ├── lagrange_pyramid.py │ ├── morley.py │ ├── morley_wang_xu.py │ ├── mtw.py │ ├── nedelec.py │ ├── nedelec_prism.py │ ├── p1_iso_p2.py │ ├── p1_macro.py │ ├── q.py │ ├── rannacher_turek.py │ ├── regge.py │ ├── rhct.py │ ├── rt.py │ ├── serendipity.py │ ├── taylor.py │ ├── tnt.py │ ├── transition.py │ ├── trimmed_serendipity.py │ ├── vector_enriched_galerkin.py │ └── wu_xu.py ├── finite_element.py ├── functionals.py ├── functions.py ├── geometry.py ├── mappings.py ├── moments.py ├── piecewise_functions.py ├── plotting.py ├── polynomials │ ├── __init__.py │ ├── dual.py │ ├── legendre.py │ ├── lobatto.py │ └── polysets.py ├── py.typed ├── quadrature.py ├── references.py ├── symbols.py ├── utils.py └── version.py ├── test ├── __init__.py ├── conftest.py ├── test_against_basix.py ├── test_against_computed_by_hand.py ├── test_alfeld_sorokina.py ├── test_arnold_winther.py ├── test_bell.py ├── test_bernstein.py ├── test_caching.py ├── test_degrees.py ├── test_docs.py ├── test_dof_descriptions.py ├── test_elements.py ├── test_functions.py ├── test_guzman_neilan.py ├── test_hct.py ├── test_hellan_herrmann_johnson.py ├── test_lagrange_polynomial_variants.py ├── test_mapping.py ├── test_nedelec.py ├── test_p1_iso_p2.py ├── test_plotting.py ├── test_polynomials.py ├── test_quadrature.py ├── test_references.py ├── test_reorder.py ├── test_stiffness_matrix.py ├── test_tensor_product.py └── utils.py └── update_readme.py /++version.py: -------------------------------------------------------------------------------- 1 | """Script to increase the version number of Symfem. 2 | 3 | Once this has been run and the code pushed, Symfembot will 4 | automatically create a new version tag on GitHub. 5 | """ 6 | 7 | import json 8 | from datetime import datetime 9 | 10 | # Check that CHANGELOG_SINCE_LAST_VERSION.md is not empty 11 | with open("CHANGELOG_SINCE_LAST_VERSION.md") as f: 12 | changes = f.read().strip() 13 | if changes == "": 14 | raise RuntimeError("CHANGELOG_SINCE_LAST_VERSION.md should not be empty") 15 | 16 | 17 | # Calculate new version number 18 | with open("VERSION") as f: 19 | version = tuple(int(i) for i in f.read().split(".")) 20 | 21 | now = datetime.now() 22 | if now.year == version[0] and now.month == version[1]: 23 | if len(version) == 2: 24 | new_version = (now.year, now.month, 1) 25 | else: 26 | new_version = (now.year, now.month, version[2] + 1) 27 | else: 28 | new_version = (now.year, now.month, 0) 29 | if len(new_version) == 2: 30 | new_version_str = f"{new_version[0]}.{new_version[1]}" 31 | else: 32 | new_version_str = f"{new_version[0]}.{new_version[1]}.{new_version[2]}" 33 | 34 | # VERSION file 35 | with open("VERSION", "w") as f: 36 | f.write(new_version_str) 37 | 38 | # codemeta.json 39 | with open("codemeta.json") as f: 40 | data = json.load(f) 41 | data["version"] = new_version_str 42 | data["dateModified"] = now.strftime("%Y-%m-%d") 43 | with open("codemeta.json", "w") as f: 44 | json.dump(data, f) 45 | 46 | # pyproject.toml 47 | new_setup = "" 48 | with open("pyproject.toml") as f: 49 | for line in f: 50 | if 'version = "' in line: 51 | new_setup += f'version = "{new_version_str}"\n' 52 | else: 53 | new_setup += line 54 | with open("pyproject.toml", "w") as f: 55 | f.write(new_setup) 56 | 57 | # symfem/version.py 58 | with open("symfem/version.py", "w") as f: 59 | f.write(f'"""Version number."""\n\nversion = "{new_version_str}"\n') 60 | 61 | # .github/workflows/test-packages.yml 62 | new_test = "" 63 | url = "https://pypi.io/packages/source/s/symfem/symfem-" 64 | with open(".github/workflows/test-packages.yml") as f: 65 | for line in f: 66 | if "ref:" in line: 67 | new_test += line.split("ref:")[0] 68 | new_test += f"ref: v{new_version_str}\n" 69 | elif url in line: 70 | new_test += line.split(url)[0] 71 | new_test += f"{url}{new_version_str}.tar.gz\n" 72 | elif "cd symfem-" in line: 73 | new_test += line.split("cd symfem-")[0] 74 | new_test += f"cd symfem-{new_version_str}\n" 75 | else: 76 | new_test += line 77 | with open(".github/workflows/test-packages.yml", "w") as f: 78 | f.write(new_test) 79 | 80 | # CITATION.cff 81 | new_citation = "" 82 | with open("CITATION.cff") as f: 83 | for line in f: 84 | if line.startswith("version: "): 85 | new_citation += f"version: {new_version_str}\n" 86 | elif line.startswith("date-released: "): 87 | new_citation += f"date-released: {now.strftime('%Y-%m-%d')}\n" 88 | else: 89 | new_citation += line 90 | with open("CITATION.cff", "w") as f: 91 | f.write(new_citation) 92 | 93 | # LICENSE 94 | new_license = "" 95 | with open("LICENSE") as f: 96 | for line in f: 97 | if "(c)" in line: 98 | new_license += f"Copyright (c) 2020-{now.strftime('%Y')} Matthew Scroggs\n" 99 | else: 100 | new_license += line 101 | with open("LICENSE", "w") as f: 102 | f.write(new_license) 103 | 104 | print(f"Updated version to {new_version_str}") 105 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Minimal code to reproduce the behavior: 15 | ```python 16 | import symfem 17 | assert 1 == 2 18 | ``` 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_element.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New element 3 | about: Suggest a new element 4 | title: 'Add [NAME] element' 5 | labels: enhancement,new element 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the element you want to add** 11 | Briefly describe the element. 12 | 13 | **References** 14 | Link to reference(s) where the element is defined. 15 | 16 | **Additional context** 17 | Add any other context about the element here. 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Changes in this pull request: 2 | 3 | - List 4 | - your 5 | - changes 6 | - here 7 | 8 | Fixes # (issue) 9 | -------------------------------------------------------------------------------- /.github/scripts/coveralls_check.py: -------------------------------------------------------------------------------- 1 | with open(".coveralls_output") as f: 2 | assert int(f.read().split("TOTAL")[-1].split("%")[0].split(" ")[-1]) > 80 3 | -------------------------------------------------------------------------------- /.github/scripts/trigger_rtd_build.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import requests 4 | 5 | URL = "https://readthedocs.org/api/v3/projects/symfem/versions/latest/builds/" 6 | TOKEN = sys.argv[-1] 7 | HEADERS = {"Authorization": f"token {TOKEN}"} 8 | response = requests.post(URL, headers=HEADERS) 9 | print(response.json()) 10 | -------------------------------------------------------------------------------- /.github/scripts/version.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from datetime import datetime 4 | 5 | import github 6 | 7 | access_key = sys.argv[-1] 8 | 9 | git = github.Github(access_key) 10 | 11 | symfem = git.get_repo("mscroggs/symfem") 12 | branch = symfem.get_branch("main") 13 | ref = symfem.get_git_ref("heads/main") 14 | base_tree = symfem.get_git_tree(branch.commit.sha) 15 | 16 | vfile1 = symfem.get_contents("VERSION", branch.commit.sha) 17 | version = vfile1.decoded_content.decode("utf8").strip() 18 | 19 | vfile2 = symfem.get_contents("codemeta.json", branch.commit.sha) 20 | data = json.loads(vfile2.decoded_content) 21 | assert data["version"] == version 22 | 23 | for release in symfem.get_releases(): 24 | if release.tag_name == f"v{version}": 25 | print("release=no") 26 | break 27 | else: 28 | print(f"release={version}") 29 | symfem.create_git_ref(ref=f"refs/heads/v{version}-changelog", sha=branch.commit.sha) 30 | new_branch = symfem.get_branch(f"v{version}-changelog") 31 | changelog_file = symfem.get_contents("CHANGELOG_SINCE_LAST_VERSION.md", new_branch.commit.sha) 32 | changes = changelog_file.decoded_content.decode("utf8").strip() 33 | 34 | if changes == "": 35 | raise RuntimeError("CHANGELOG_SINCE_LAST_VERSION.md should not be empty") 36 | 37 | symfem.create_git_tag_and_release( 38 | f"v{version}", 39 | f"Version {version}", 40 | f"Version {version}", 41 | changes, 42 | branch.commit.sha, 43 | "commit", 44 | ) 45 | 46 | old_changelog_file = symfem.get_contents("CHANGELOG.md", new_branch.commit.sha) 47 | old_changes = old_changelog_file.decoded_content.decode("utf8").strip() 48 | 49 | new_changelog = ( 50 | f"# Version {version} ({datetime.now().strftime('%d %B %Y')})\n\n" 51 | f"{changes}\n\n{old_changes}\n" 52 | ) 53 | 54 | symfem.update_file( 55 | "CHANGELOG.md", 56 | "Update CHANGELOG.md", 57 | new_changelog, 58 | sha=old_changelog_file.sha, 59 | branch=f"v{version}-changelog", 60 | ) 61 | symfem.update_file( 62 | "CHANGELOG_SINCE_LAST_VERSION.md", 63 | "Reset CHANGELOG_SINCE_LAST_VERSION.md", 64 | "", 65 | sha=changelog_file.sha, 66 | branch=f"v{version}-changelog", 67 | ) 68 | 69 | symfem.create_pull( 70 | title="Update changelogs", body="", base="main", head=f"v{version}-changelog" 71 | ) 72 | -------------------------------------------------------------------------------- /.github/workflows/coveralls.yml: -------------------------------------------------------------------------------- 1 | name: 🥼 Coveralls 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | coveralls: 10 | name: Run coverage checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.13" 17 | - name: Load matrix cache 18 | id: cache-restore 19 | uses: actions/cache/restore@v3 20 | with: 21 | path: /home/runner/.cache/symfem 22 | key: symfem-matrix-cache 23 | - run: | 24 | sudo apt-get install -y libeigen3-dev libopenblas-dev liblapack-dev ninja-build 25 | pip install pybind11 26 | name: Install Basix requirements 27 | - run: pip install coverage coveralls pytest-cov 28 | name: Install Coveralls 29 | - uses: actions/checkout@v4 30 | - run: pip install .[test] pytest-xdist 31 | name: Install Symfem 32 | - run: pip install git+https://github.com/FEniCS/basix.git 33 | name: Install Basix 34 | - run: python3 -m pytest -n=auto --cov=symfem test/ > .coveralls_output 35 | name: Run unit tests 36 | continue-on-error: true 37 | - name: Check coverage is over 80% 38 | run: python3 .github/scripts/coveralls_check.py 39 | - name: Upload to Coveralls 40 | if: ${{ github.ref == 'refs/heads/main' }} 41 | env: 42 | COVERALLS_REPO_TOKEN: ${{ secrets.coverall_token }} 43 | run: python3 -m coveralls 44 | -------------------------------------------------------------------------------- /.github/workflows/defelement.yml: -------------------------------------------------------------------------------- 1 | name: 📙 DefElement 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | defelement: 8 | name: Test DefElement build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up Python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.13" 15 | - name: Load matrix cache 16 | id: cache-restore 17 | uses: actions/cache/restore@v3 18 | with: 19 | path: /home/runner/.cache/symfem 20 | key: symfem-matrix-cache 21 | - uses: actions/checkout@v4 22 | - run: pip install .[optional] 23 | name: Install Symfem 24 | 25 | - name: Clone DefElement 26 | uses: actions/checkout@v4 27 | with: 28 | path: ./defelement 29 | repository: DefElement/DefElement 30 | ref: main 31 | - name: Install requirements 32 | run: | 33 | cd defelement 34 | pip install -r requirements.txt 35 | - name: Test DefElement build 36 | run: | 37 | cd defelement 38 | python3 build.py ../_test_html --test auto 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 📦 Releases and packaging 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | check-version: 10 | name: Check version number 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | contents: write 15 | pull-requests: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.13" 23 | 24 | - name: Install PyGitHub 25 | run: python3 -m pip install PyGitHub 26 | - name: Check version number and make release if necessary 27 | run: python .github/scripts/version.py ${{ secrets.GITHUB_TOKEN }} >> $GITHUB_OUTPUT 28 | id: version-check 29 | 30 | - name: Trigger Read the Docs build 31 | run: python .github/scripts/trigger_rtd_build.py ${{ secrets.RTDS_TOKEN }} 32 | if: steps.version-check.outputs.release != 'no' 33 | 34 | - name: Prepare release 35 | run: python3 prepare_release.py --version ${{ steps.version-check.outputs.release }} 36 | - name: Build a wheel for PyPI 37 | run: | 38 | python3 -m pip install build 39 | python3 -m build . 40 | if: steps.version-check.outputs.release != 'no' 41 | - name: Publish to PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | if: steps.version-check.outputs.release != 'no' 44 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "0 7 * * 1" 12 | 13 | jobs: 14 | run-tests: 15 | name: Run tests 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 20 | steps: 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Load matrix cache 26 | id: cache-restore 27 | uses: actions/cache/restore@v3 28 | with: 29 | path: /home/runner/.cache/symfem 30 | key: symfem-matrix-cache 31 | - uses: actions/checkout@v4 32 | - run: pip install .[test] pytest-xdist 33 | name: Install Symfem 34 | - name: Install LaTeΧ 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y texlive-latex-base texlive-latex-extra 38 | - run: python3 -m pytest -n=auto --durations=50 test -W error 39 | name: Run unit tests 40 | - run: python3 -m pytest demo/test_demos.py -W error 41 | name: Run demos 42 | - run: python3 -m pytest scripts/test_scripts.py 43 | name: Run scripts 44 | - name: Save matrix cache 45 | id: cache-save 46 | uses: actions/cache/save@v3 47 | with: 48 | path: /home/runner/.cache/symfem 49 | key: symfem-matrix-cache-${{ github.run_id }} 50 | restore-keys: symfem-matrix-cache 51 | if: ${{ matrix.python-version == '3.13' && github.ref == 'refs/heads/main' }} 52 | 53 | run-tests-against-basix: 54 | name: Run tests against Basix 55 | runs-on: ubuntu-latest 56 | strategy: 57 | matrix: 58 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 59 | steps: 60 | - name: Set up Python 61 | uses: actions/setup-python@v4 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | - name: Load matrix cache 65 | id: cache-restore 66 | uses: actions/cache/restore@v3 67 | with: 68 | path: /home/runner/.cache/symfem 69 | key: symfem-matrix-cache 70 | - run: | 71 | sudo apt-get install -y libopenblas-dev liblapack-dev ninja-build 72 | pip install pybind11 73 | name: Install Basix dependencies 74 | - uses: actions/checkout@v4 75 | - run: pip install .[test] pytest-xdist 76 | name: Install Symfem 77 | - run: pip install git+https://github.com/FEniCS/basix.git 78 | name: Install Basix 79 | - run: python3 -m pytest -n=auto --durations=50 test/test_against_basix.py --has-basix 1 -W error 80 | name: Run unit tests 81 | -------------------------------------------------------------------------------- /.github/workflows/style-checks.yml: -------------------------------------------------------------------------------- 1 | name: 🕶️ Style 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | style-checks: 13 | name: Run style checks 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.13 20 | - uses: actions/checkout@v4 21 | - run: python3 -m pip install -e .[style,docs] 22 | - run: | 23 | python3 -m ruff check . 24 | python3 -m ruff format --check . 25 | name: Run ruff checks 26 | - run: python3 -m mypy . 27 | name: Run mypy checks 28 | - run: | 29 | cd docs 30 | make html SPHINXOPTS="-W" 31 | name: Test docs build 32 | -------------------------------------------------------------------------------- /.github/workflows/test-packages.yml: -------------------------------------------------------------------------------- 1 | name: 🧪📦 Test packages 2 | 3 | on: 4 | schedule: 5 | - cron: "0 7 * * 1" 6 | 7 | jobs: 8 | run-tests-with-pip: 9 | name: Run tests with Symfem installed from pip 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 14 | steps: 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: v2025.3.1 22 | name: Get latest Symfem version 23 | - run: rm -rf symfem VERSION 24 | name: Remove downloaded symfem 25 | - name: Install LaTeΧ 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install -y texlive-latex-base texlive-latex-extra 29 | - run: python3 -m pip install CairoSVG 30 | name: Install optional dependencies 31 | - run: python3 -m pip install symfem 32 | name: Install Symfem 33 | - run: pip install pytest 34 | name: Install pytest 35 | - run: python3 -m pytest test/ 36 | name: Run unit tests 37 | - run: python3 -m pytest demo/test_demos.py 38 | name: Run demos 39 | 40 | run-tests-with-conda: 41 | name: Run tests with Symfem installed from conda-forge 42 | runs-on: ubuntu-latest 43 | strategy: 44 | matrix: 45 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 46 | steps: 47 | - uses: conda-incubator/setup-miniconda@v2 48 | with: 49 | auto-update-conda: true 50 | python-version: ${{ matrix.python-version }} 51 | activate-environment: symfem 52 | - uses: actions/checkout@v4 53 | with: 54 | ref: v2025.3.1 55 | name: Get latest Symfem version 56 | - run: rm -rf symfem VERSION 57 | name: Remove downloaded symfem 58 | - name: Install LaTeΧ 59 | run: | 60 | sudo apt-get update 61 | sudo apt-get install -y texlive-latex-base texlive-latex-extra 62 | - run: | 63 | conda config --add channels conda-forge 64 | conda config --set channel_priority strict 65 | conda install symfem 66 | conda install cairosvg 67 | conda install pytest 68 | pytest test/ 69 | pytest demo/test_demos.py 70 | shell: bash -l {0} 71 | name: Install Symfem and run tests 72 | 73 | run-test-with-pypi-zip: 74 | name: Run tests with Symfem downloaded from PyPI 75 | runs-on: ubuntu-latest 76 | strategy: 77 | matrix: 78 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 79 | steps: 80 | - name: Set up Python 81 | uses: actions/setup-python@v4 82 | with: 83 | python-version: ${{ matrix.python-version }} 84 | - uses: actions/checkout@v4 85 | with: 86 | ref: v2025.3.1 87 | path: symfem-src 88 | - name: Move tests and delete Symfem source 89 | run: | 90 | mv symfem-src/test . 91 | mv symfem-src/README.md . 92 | rm -r symfem-src 93 | - run: | 94 | wget -O symfem.tar.gz https://pypi.io/packages/source/s/symfem/symfem-2025.3.1.tar.gz 95 | tar -xvzf symfem.tar.gz 96 | name: Download and unpack latest version of Symfem 97 | - name: Install LaTeΧ 98 | run: | 99 | sudo apt-get update 100 | sudo apt-get install -y texlive-latex-base texlive-latex-extra 101 | - run: pip install pytest 102 | name: Install pytest 103 | - run: | 104 | cd symfem-2025.3.1 105 | pip install .[optional] 106 | name: Install requirements 107 | - run: | 108 | python3 -m pytest test/ 109 | name: Run tests 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | symfem/VERSION 3 | 4 | .*.swp 5 | 6 | build/ 7 | dist/ 8 | symfem.egg-info/ 9 | docs/_build 10 | .coverage 11 | .coveralls_output 12 | 13 | joss/build.sh 14 | joss/paper.pdf 15 | 16 | _temp/ 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | formats: all 20 | 21 | # We recommend specifying your dependencies to enable reproducible builds: 22 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 23 | python: 24 | install: 25 | - requirements: docs/requirements.txt 26 | -------------------------------------------------------------------------------- /ADDING_AN_ELEMENT.md: -------------------------------------------------------------------------------- 1 | # Adding an element to Symfem 2 | 3 | If you simply want to experiment with elements, you can create a custom element by following the [custom element demo](demo/custom_element.py). 4 | 5 | If you want to add an element to the Symfem library, this guide will tell you how to do this. After making these changes, 6 | you should push them to a fork of Symfem and [open a pull request](CONTRIBUTING.md). 7 | 8 | ## Step 1: Adding an element file 9 | To add an element to Symfem, you should create a new `.py` file in the folder [symfem/elements](symfem/elements). You should name this file 10 | as your element's name in lowercase (with underscores instead of any spaces or dashes). 11 | 12 | You can look at the files currently in the elements folder to see what format your file should take: your file should include 13 | these important features: 14 | 15 | - The documentation string at the start of the file should include DOI links to references where the element is defined 16 | - Your element class must define an `__init__` method that takes `reference` as an input (it may optionally take additional arguments) 17 | - Your element class should define `names` and `references` lists that give all the names that can be used to initialise this element 18 | and the names of all the reference cells the element is defined on. No two elements may use the same name on the same cell. 19 | - Your element class can define `min_order` and/or `max_order` variables if there is a minimum or maximum degree/order that is valid 20 | for this element 21 | - You element class should define the type of continuity that your element has, this can be: 22 | - `L2` (no continuity) 23 | - `C0` (continuous) 24 | - `C1` (continuous with continuous derivatives 25 | - `C{n}` (continuous with `{n}` continuous derivatives) (you must replace `{n}` with a natural number) 26 | - `H(div)` (continuous across facets in the normal direction) 27 | - `H(curl)` (continuous across facets in the tangential direction(s)) 28 | - `inner H(div)` (continuous normal inner products across facets) 29 | - `inner H(curl)` (continuous tangential inner products across facets) 30 | - `integral inner H(div)` (continuous integrals of normal inner products across facets) 31 | 32 | 33 | ## Step 2: Adding the new element to the tests 34 | Once you've added your element file, you need to add you element to the Symfem tests. You can do this by editing the file 35 | [test/utils.py](test/utils.py). For each cell that your element is implemented on, you should add an entry to the dictionary 36 | for that cell. These entries should have the format: 37 | 38 | ```python 39 | "YOUR_ELEMENT": [({"ARG": VALUE}, [1, 2]), ({}, range(3, 4))], 40 | ``` 41 | 42 | You should replace `YOUR_ELEMENT` with one of the values in the `names` list in your element file. For your element, you provide 43 | a list of tuples that will used to generate tests: the first item in the tuple is a list of keyword arguments to be passed when 44 | creating your element (for many elements, this dictionary will be empty), and the second item is a list or range of degrees/orders 45 | to run the test on. 46 | 47 | 48 | ## Step 3: Running the tests 49 | Once you've completed step 2, you can test that your element functions correctly by running (with `YOUR_ELEMENT` replaced by the 50 | element name you used in step 2): 51 | 52 | ```bash 53 | python3 -m pytest test/test_elements.py --elements-to-test YOUR_ELEMENT 54 | ``` 55 | 56 | ## Step 4: Adding the new element to create.py 57 | You must add the name of the new element (including any alternative names) to the docstring of the function `create_element` 58 | in the file [symfem/create.py](symfem/create.py). 59 | 60 | ## Step 5: Adding the new element to README.md 61 | You must add the name of the element to the README. You can do this by running: 62 | 63 | ```bash 64 | python3 update_readme.py 65 | ``` 66 | 67 | ## Step 6: Testing the documentation 68 | To confirm that you have completes steps 4 and 5 correctly, you can run: 69 | 70 | ```bash 71 | python3 -m pytest test/test_docs.py 72 | ``` 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 2025.3.1 (31 March 2025) 2 | 3 | - Allow "gl" variant of Lagrange element 4 | - Implement component-wise integrals of VectorFunction and MatrixFunction 5 | - Implement grad of VectorFunction 6 | - Add Gopalakrishnan-Lederer-Schoberl element 7 | - Use polynomial subdegree to index elements wherever possible 8 | 9 | # Version 2025.3.0 (03 March 2025) 10 | 11 | - Corrected nonconforming Arnold-Winther polyset 12 | - Correct Alfeld-Sorokina range dim 13 | - Correct Guzman-Neilan second kind element 14 | - Add Guzman-Neilan first kind element 15 | - Correct Guzman-Neilan bubbles 16 | - Add discontinuous elements 17 | 18 | # Version 2024.10.0 (05 October 2024) 19 | 20 | - Corrected polyset for Arnold-Winther element 21 | - Added sub- and superdegree functions 22 | 23 | # Version 2024.1.1 (04 January 2024) 24 | 25 | - Added get_mapping function 26 | 27 | # Version 2023.12.11 (18 December 2023) 28 | 29 | - Link to images from correct version 30 | 31 | # Version 2023.12.8 (18 December 2023) 32 | 33 | - Fix readme on PyPI 34 | 35 | # Version 2023.12.5 (18 December 2023) 36 | 37 | - New release to test trusted publisher 38 | 39 | # Version 2023.12.2 (18 December 2023) 40 | 41 | - Release to test workflow changes 42 | 43 | # Version 2023.12.1 (18 December 2023) 44 | 45 | - Release to test new workflow 46 | 47 | # Version 2023.12.0 (15 December 2023) 48 | 49 | - Release to test new token 50 | 51 | # Version 2023.11.0 (24 November 2023) 52 | 53 | - Use pyproject.toml for installation 54 | - Add xx,yy,zz ordering for tabulation 55 | - Correct Lagrange variants 56 | 57 | # Version 2023.8.2 (20 August 2023) 58 | 59 | - Remove tdim input from mappings functions 60 | 61 | # Version 2023.8.1 (18 August 2023) 62 | 63 | - Bugfixes for piecewise functions on an interval 64 | - Added HHJ on a tetrahedron 65 | - Add inverse mapping functions 66 | 67 | # Version 2023.8.0 (08 August 2023) 68 | 69 | - Corrected HCT and rHCT elements 70 | - Added caching of matrix inverses and dual matrices 71 | - Added P1 macro element 72 | - Added Alfeld-Sorokina element 73 | - Corrected C1 and higher order C tests 74 | - Allow element creation on non-default references 75 | - Corrected Bell element 76 | - Corrected legendre and lobatto variants of Lagrange 77 | - Add P1-iso-P2 elemen on interval 78 | 79 | # Version 2023.4.1 (26 April 2023) 80 | 81 | - Added enriched Galerkin element 82 | - Improved plotting 83 | - Added enriched vector Galerkin element 84 | - Fixed bug in integration of piecewise functions 85 | 86 | # Version 2023.1.1 (04 January 2023) 87 | 88 | - Added Huang-Zhang element 89 | - Added adding an element guide 90 | - Increased documentation 91 | 92 | # Version 2022.8.2 (08 August 2022) 93 | 94 | - Remove svgwrite dependency 95 | - Added normalised orthogonal functions 96 | 97 | # Version 2022.8.1 (02 August 2022) 98 | 99 | - Added plotting of DOF diagrams 100 | - Added P1-iso-P2 element 101 | - Added typing 102 | - Added plotting of basis functions 103 | - Added plotting of entity numbering 104 | 105 | # Version 2022.6.1 (18 June 2022) 106 | 107 | - Added Rannacher-Turek element 108 | - Added functions to create orthogonal polynomials on cells 109 | - Added TeX descriptions of DOF functionals 110 | - Corrected Bernstein element 111 | 112 | # Version 2021.12.1 (01 December 2021) 113 | 114 | - Added Regge element on tensor product cells. 115 | - Added tensor product factorisation of Q element. 116 | - Added Arnold-Boffi-Falk element 117 | - Added Arbogast-Correa element 118 | 119 | # Version 2021.10.1 (19 October 2021) 120 | 121 | - Removed discontinuous elements. 122 | - Added pictures of reference cells to readme. 123 | - Added trimmed serendipity Hdiv and Hcurl elements. 124 | - Added TNT scalar, Hdiv and Hcurl elements. 125 | 126 | # Version 2021.8.2 (24 August 2021) 127 | 128 | - New release for JOSS paper 129 | 130 | # Version 2021.8.1 (11 August 2021) 131 | 132 | - Added Guzman-Neilan element 133 | - Added nonconforming Arnold-Winther element 134 | - Wrote JOSS paper 135 | 136 | # 11 August 2021 137 | 138 | - Started keeping changelog 139 | -------------------------------------------------------------------------------- /CHANGELOG_SINCE_LAST_VERSION.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/CHANGELOG_SINCE_LAST_VERSION.md -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: If you use this software, please cite it as below. 3 | authors: 4 | - family-names: Scroggs 5 | given-names: Matthew W. 6 | orcid: 0000-0002-4658-2443 7 | title: Symfem 8 | version: 2025.3.1 9 | date-released: 2025-03-31 10 | license: MIT 11 | url: https://github.com/mscroggs/symfem 12 | preferred-citation: 13 | type: article 14 | authors: 15 | - family-names: Scroggs 16 | given-names: Matthew W. 17 | orcid: https://orcid.org/0000-0002-4658-2443 18 | doi: 10.21105/joss.03556 19 | journal: Journal of Open Source Software 20 | title: "Symfem: a symbolic finite element definition library" 21 | volume: 6 22 | issue: 64 23 | start: 3556 24 | month: 8 25 | year: 2021 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | ### Reporting bugs 4 | If you find a bug in Symfem, please report it on the [issue tracker](https://github.com/mscroggs/symfem/issues/new?assignees=&labels=bug&template=bug_report.md&title=). 5 | 6 | ### Suggesting enhancements 7 | If you want to suggest a new feature or an improvement of a current feature, you can submit this 8 | on the [issue tracker](https://github.com/mscroggs/symfem/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). 9 | 10 | ### Submitting a pull request 11 | If you want to directly submit code to Symfem, you can do this by forking the Symfem repo, then submitting a pull request. 12 | If you want to contribute, but are unsure where to start, have a look at the 13 | [issues labelled "good first issue"](https://github.com/mscroggs/symfem/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 14 | 15 | On opening a pull request, unit tests and flake8 style checks will run. You can click on these in the pull request 16 | to see where (if anywhere) there are errors in your code. 17 | 18 | ### Code of conduct 19 | We expect all our contributors to follow [the Contributor Covenant](CODE_OF_CONDUCT.md). Any unacceptable 20 | behaviour can be reported to Matthew (symfem@mscroggs.co.uk). 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Matthew Scroggs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | Please report (suspected) security vulnerabilities to **[symfem@mscroggs.co.uk](mailto:symfem@mscroggs.co.uk)**. 5 | If the issue is confirmed, we will release a new version with the issue corrected as soon as possible. 6 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2025.3.1 -------------------------------------------------------------------------------- /codemeta.json: -------------------------------------------------------------------------------- 1 | {"@context": "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type": "Code", "author": [{"@id": "0000-0002-4658-2443", "@type": "Person", "email": "mws48@cam.ac.uk", "name": "Matthew Scroggs", "affiliation": "Department of Engineering, University of Cambridge"}], "identifier": "", "codeRepository": "https://github.com/mscroggs/symfem", "datePublished": "2021-01-23", "dateModified": "2025-03-31", "dateCreated": "2021-01-23", "description": "A symbolic finite element definition library", "keywords": "Python, finite element method, numerical analysis", "license": "MIT", "title": "Symfem", "version": "2025.3.1"} -------------------------------------------------------------------------------- /demo/custom_element.py: -------------------------------------------------------------------------------- 1 | """Demo showing how a custom element can be created in Symfem.""" 2 | 3 | import sympy 4 | 5 | import symfem 6 | from symfem.finite_element import CiarletElement 7 | from symfem.functionals import PointEvaluation 8 | from symfem.symbols import x 9 | 10 | 11 | class CustomElement(CiarletElement): 12 | """Custom element on a quadrilateral.""" 13 | 14 | def __init__(self, reference, order): 15 | """Create the element. 16 | 17 | Args: 18 | reference: the reference element 19 | order: the polynomial order 20 | """ 21 | zero = sympy.Integer(0) 22 | one = sympy.Integer(1) 23 | half = sympy.Rational(1, 2) 24 | 25 | # The polynomial set contains 1, x and y 26 | poly_set = [one, x[0], x[1]] 27 | 28 | # The DOFs are point evaluations at vertex 3, 29 | # and the midpoints of edges 0 and 1 30 | dofs = [ 31 | PointEvaluation(reference, (one, one), entity=(0, 3)), 32 | PointEvaluation(reference, (half, zero), entity=(1, 0)), 33 | PointEvaluation(reference, (zero, half), entity=(1, 1)), 34 | ] 35 | 36 | super().__init__(reference, order, poly_set, dofs, reference.tdim, 1) 37 | 38 | names = ["custom quad element"] 39 | references = ["quadrilateral"] 40 | min_order = 1 41 | max_order = 1 42 | continuity = "L2" 43 | value_type = "scalar" 44 | mapping = "identity" 45 | 46 | 47 | # Add the element to symfem 48 | symfem.add_element(CustomElement) 49 | 50 | # Create the element and print its basis functions 51 | element = symfem.create_element("quadrilateral", "custom quad element", 1) 52 | print(element.get_basis_functions()) 53 | 54 | # Run the Symfem tests on the custom element 55 | element.test() 56 | -------------------------------------------------------------------------------- /demo/lagrange.py: -------------------------------------------------------------------------------- 1 | """Demo showing how Symfem can be used to verify properties of a basis. 2 | 3 | The basis functions of a Lagrange element, when restricted to an edge of a cell, 4 | should be equal to the basis functions of a Lagrange space on that edge (or equal to 0). 5 | 6 | In this demo, we verify that this is true for an order 5 Lagrange element on a triangle. 7 | """ 8 | 9 | import sympy 10 | 11 | import symfem 12 | from symfem.symbols import x 13 | from symfem.utils import allequal 14 | 15 | element = symfem.create_element("triangle", "Lagrange", 5) 16 | edge_element = symfem.create_element("interval", "Lagrange", 5) 17 | 18 | # Define a parameter that will go from 0 to 1 on the chosen edge 19 | a = sympy.Symbol("a") 20 | 21 | # Get the basis functions of the Lagrange space and substitute the parameter into the 22 | # functions on the edge 23 | basis = element.get_basis_functions() 24 | edge_basis = [f.subs(x[0], a) for f in edge_element.get_basis_functions()] 25 | 26 | # Get the DOFs on edge 0 (from vertex 1 (1,0) to vertex 2 (0,1)) 27 | # (1 - a, a) is a parametrisation of this edge 28 | dofs = element.entity_dofs(0, 1) + element.entity_dofs(0, 2) + element.entity_dofs(1, 0) 29 | # Check that the basis functions on this edge are equal 30 | for d, edge_f in zip(dofs, edge_basis): 31 | # allequal will simplify the expressions then check that they are equal 32 | assert allequal(basis[d].subs(x[:2], (1 - a, a)), edge_f) 33 | 34 | # Get the DOFs on edge 1 (from vertex 0 (0,0) to vertex 2 (0,1), parametrised (0, a)) 35 | dofs = element.entity_dofs(0, 0) + element.entity_dofs(0, 2) + element.entity_dofs(1, 1) 36 | for d, edge_f in zip(dofs, edge_basis): 37 | assert allequal(basis[d].subs(x[:2], (0, a)), edge_f) 38 | 39 | # Get the DOFs on edge 2 (from vertex 0 (0,0) to vertex 1 (1,0), parametrised (a, 0)) 40 | dofs = element.entity_dofs(0, 0) + element.entity_dofs(0, 1) + element.entity_dofs(1, 2) 41 | for d, edge_f in zip(dofs, edge_basis): 42 | assert allequal(basis[d].subs(x[:2], (a, 0)), edge_f) 43 | -------------------------------------------------------------------------------- /demo/nedelec.py: -------------------------------------------------------------------------------- 1 | """Demo showing how Symfem can be used to verify properties of a basis. 2 | 3 | The polynomial set of a degree k Nedelec first kind space is: 4 | {polynomials of degree < k} UNION {polynomials of degree k such that p DOT x = 0}. 5 | 6 | The basis functions of a Nedelec first kind that are associated with the interior of the cell 7 | have 0 tangential component on the facets of the cell. 8 | 9 | In this demo, we verify that these properties hold for a degree 4 Nedelec first kind 10 | space on a triangle. 11 | """ 12 | 13 | import symfem 14 | from symfem.polynomials import polynomial_set_vector 15 | from symfem.symbols import x 16 | from symfem.utils import allequal 17 | 18 | element = symfem.create_element("triangle", "Nedelec1", 3) 19 | polys = element.get_polynomial_basis() 20 | 21 | # Check that the first 20 polynomials in the polynomial basis are 22 | # the polynomials of degree 3 23 | p3 = polynomial_set_vector(2, 2, 3) 24 | assert len(p3) == 20 25 | for i, j in zip(p3, polys[:20]): 26 | assert i == j 27 | 28 | # Check that the rest of the polynomials in the polynomial basis 29 | # satisfy p DOT x = 0 30 | for p in polys[20:]: 31 | assert p.dot(x[:2]) == 0 32 | 33 | # Get the basis functions associated with the interior of the triangle 34 | basis = element.get_basis_functions() 35 | functions = [basis[d] for d in element.entity_dofs(2, 0)] 36 | 37 | # Check that these functions have 0 tangential component on each edge 38 | # allequal will simplify the expressions then check that they are equal 39 | for f in functions: 40 | assert allequal(f.subs(x[0], 1 - x[1]).dot((1, -1)), 0) 41 | assert allequal(f.subs(x[0], 0).dot((0, 1)), 0) 42 | assert allequal(f.subs(x[1], 0).dot((1, 0)), 0) 43 | -------------------------------------------------------------------------------- /demo/stiffness_matrix.py: -------------------------------------------------------------------------------- 1 | """Demo showing how Symfem can be used to compute a stiffness matrix.""" 2 | 3 | import symfem 4 | from symfem.symbols import x 5 | 6 | # Define the vertived and triangles of the mesh 7 | vertices = [(0, 0), (1, 0), (0, 1), (1, 1)] 8 | triangles = [[0, 1, 2], [1, 3, 2]] 9 | 10 | # Create a matrix of zeros with the correct shape 11 | matrix = [[0 for i in range(4)] for j in range(4)] 12 | 13 | # Create a Lagrange element 14 | element = symfem.create_element("triangle", "Lagrange", 1) 15 | 16 | for triangle in triangles: 17 | # Get the vertices of the triangle 18 | vs = tuple(vertices[i] for i in triangle) 19 | # Create a reference cell with these vertices: this will be used 20 | # to compute the integral over the triangle 21 | ref = symfem.create_reference("triangle", vertices=vs) 22 | # Map the basis functions to the cell 23 | basis = element.map_to_cell(vs) 24 | 25 | for test_i, test_f in zip(triangle, basis): 26 | for trial_i, trial_f in zip(triangle, basis): 27 | # Compute the integral of grad(u)-dot-grad(v) for each pair of basis 28 | # functions u and v. The second input (x) into `ref.integral` tells 29 | # symfem which variables to use in the integral. 30 | integrand = test_f.grad(2).dot(trial_f.grad(2)) 31 | print(integrand) 32 | matrix[test_i][trial_i] += integrand.integral(ref, x) 33 | 34 | print(matrix) 35 | -------------------------------------------------------------------------------- /demo/test_demos.py: -------------------------------------------------------------------------------- 1 | """Run the demos.""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "file", 10 | [ 11 | file 12 | for file in os.listdir(os.path.dirname(os.path.realpath(__file__))) 13 | if file.endswith(".py") and not file.startswith(".") and not file.startswith("test_") 14 | ], 15 | ) 16 | def test_demo(file): 17 | file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), file) 18 | 19 | assert os.system(f"python3 {file_path}") == 0 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/demos/custom_element.rst: -------------------------------------------------------------------------------- 1 | ######################### 2 | Defining a custom element 3 | ######################### 4 | 5 | This demo shows how a custom element with custom functionals can be 6 | defined in Symfem. 7 | 8 | .. literalinclude:: ../../demo/custom_element.py 9 | :language: Python 10 | -------------------------------------------------------------------------------- /docs/demos/index.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Symfem demos 3 | ############ 4 | 5 | .. toctree:: 6 | :titlesonly: 7 | 8 | lagrange 9 | nedelec 10 | stiffness_matrix 11 | custom_element 12 | -------------------------------------------------------------------------------- /docs/demos/lagrange.rst: -------------------------------------------------------------------------------- 1 | ########################### 2 | Checking a Lagrange element 3 | ########################### 4 | 5 | This demo shows how Symfem can be used to confirm that when the basis functions of a Lagrange 6 | element of a triangle are restricted to an edge, then they are equal to the basis functions 7 | of a Lagrange element on that edge. 8 | 9 | .. literalinclude:: ../../demo/lagrange.py 10 | :language: Python 11 | -------------------------------------------------------------------------------- /docs/demos/nedelec.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | Checking a Nedelec element 3 | ########################## 4 | 5 | This demo shows how Symfem can be used to confirm that the polynomial set of a Nedelec 6 | first kind element are as expected. 7 | 8 | .. literalinclude:: ../../demo/nedelec.py 9 | :language: Python 10 | -------------------------------------------------------------------------------- /docs/demos/stiffness_matrix.rst: -------------------------------------------------------------------------------- 1 | ############################ 2 | Computing a stiffness matrix 3 | ############################ 4 | 5 | This demo shows how a stiffness matrix over a simple mesh can be 6 | computed using Symfem. 7 | 8 | .. literalinclude:: ../../demo/stiffness_matrix.py 9 | :language: Python 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autoapi 2 | -------------------------------------------------------------------------------- /img/dual_polygon_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/dual_polygon_numbering.png -------------------------------------------------------------------------------- /img/hexahedron_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/hexahedron_numbering.png -------------------------------------------------------------------------------- /img/interval_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/interval_numbering.png -------------------------------------------------------------------------------- /img/prism_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/prism_numbering.png -------------------------------------------------------------------------------- /img/pyramid_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/pyramid_numbering.png -------------------------------------------------------------------------------- /img/quadrilateral_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/quadrilateral_numbering.png -------------------------------------------------------------------------------- /img/tetrahedron_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/tetrahedron_numbering.png -------------------------------------------------------------------------------- /img/triangle_numbering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/img/triangle_numbering.png -------------------------------------------------------------------------------- /joss/paper.bib: -------------------------------------------------------------------------------- 1 | @article{fiat, 2 | author = {Robert C. Kirby}, 3 | title = {Algorithm 839: {FIAT}, a New Paradigm for Computing Finite Element Basis Functions}, 4 | year = {2004}, 5 | volume = {30}, 6 | number = {4}, 7 | doi = {10.1145/1039813.1039820}, 8 | journal = {ACM Transactions on Mathematical Software}, 9 | pages = {502--516}, 10 | } 11 | 12 | @article{sympy, 13 | title = {{SymPy}: symbolic computing in {P}ython}, 14 | author = {Meurer, Aaron and Smith, Christopher P. and Paprocki, Mateusz and \v{C}ert\'{i}k, Ond\v{r}ej and Kirpichev, Sergey B. and Rocklin, Matthew and Kumar, AMiT and Ivanov, Sergiu and Moore, Jason K. and Singh, Sartaj and Rathnayake, Thilina and Vig, Sean and Granger, Brian E. and Muller, Richard P. and Bonazzi, Francesco and Gupta, Harsh and Vats, Shivam and Johansson, Fredrik and Pedregosa, Fabian and Curry, Matthew J. and Terrel, Andy R. and Rou\v{c}ka, \v{S}t\v{e}p\'{a}n and Saboo, Ashutosh and Fernando, Isuru and Kulal, Sumith and Cimrman, Robert and Scopatz, Anthony}, 15 | year = 2017, 16 | volume = 3, 17 | pages = {e103}, 18 | journal = {PeerJ Computer Science}, 19 | doi = {10.7717/peerj-cs.103} 20 | } 21 | 22 | @misc{basix, 23 | title={Basix: FEniCS runtime basis evaluation library}, 24 | author={Chris Richardson and Matthew Scroggs and Garth Wells}, 25 | url = {https://github.com/FEniCS/basix}, 26 | year = {2021} 27 | } 28 | 29 | @article{fenics, 30 | title = {The FEniCS Project Version 1.5}, 31 | author = {Martin S. Aln{\ae}s and Jan Blechta and Johan Hake and 32 | August Johansson and Benjamin Kehlet and Anders Logg and Chris 33 | Richardson and Johannes Ring and Rognes, Marie E. and Wells, Garth N.}, 34 | year = {2015}, 35 | journal = {Archive of Numerical Software}, 36 | volume = {3}, 37 | number = {100}, 38 | doi = {10.11588/ans.2015.100.20553}, 39 | pages = {9--23}, 40 | } 41 | 42 | @book{ciarlet, 43 | title = {The Finite Element Method for Elliptic Problems}, 44 | author = {Philippe G. Ciarlet}, 45 | year = {2002}, 46 | doi = {10.1137/1.9780898719208} 47 | } 48 | -------------------------------------------------------------------------------- /joss/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Symfem: a symbolic finite element definition library' 3 | tags: 4 | - Python 5 | - finite element method 6 | - basis functions 7 | - symbolic algebra 8 | - numerical analysis 9 | authors: 10 | - name: Matthew W. Scroggs 11 | orcid: 0000-0002-4658-2443 12 | affiliation: 1 13 | affiliations: 14 | - name: Department of Engineering, University of Cambridge 15 | index: 1 16 | date: 15 July 2021 17 | bibliography: paper.bib 18 | --- 19 | 20 | # Summary 21 | 22 | The finite element method (FEM) [@ciarlet] is a popular method for numerically solving a wide 23 | range of partial differential equations (PDEs). To solve a problem using FEM, the PDE is first 24 | written in a weak form, for example: find $u\in V$ such that for all $v\in V,$ 25 | 26 | \begin{equation} 27 | \int_\Omega \nabla u\cdot\nabla v=\int_\Omega fv, 28 | \end{equation} 29 | 30 | where $f$ is a known function, and $\Omega$ is the domain on which the problem is being solved. 31 | This form is then discretised by defining a finite-dimensional subspace of $V$---often called 32 | $V_h$---and looking for a solution $u_h\in V_h$ that satisfies the above equation for all functions 33 | $v_h\in V_h$. These finite-dimensional subspaces are defined by meshing the domain of the problem, 34 | then defining a set of basis functions on each cell in the mesh (and enforcing any desired 35 | continuity between the cells). 36 | 37 | For different applications, there are a wide range of finite-dimensional spaces that can be used. 38 | Symfem is a Python library that can be used to symbolically compute basis functions of these 39 | spaces. The symbolic representations are created using Sympy [@sympy], allowing 40 | them to be easily manipulated using Sympy's functionality once they are created. 41 | 42 | # Statement of need 43 | 44 | In FEM libraries, it is common to define basis functions so that they, and their 45 | derivatives, can quickly and efficiently be evaluated at a collection of points, thereby allowing 46 | full computations to be completed quickly. The libraries FIAT [@fiat] and Basix [@basix]---which 47 | are part of the FEniCS project [@fenics]---implement this functionality as stand-alone libraries. 48 | Many other FEM libraries define their basis functions as part of the core library functionality. 49 | It is not common to be able to compute a symbolic representation of the basis functions. 50 | 51 | Symfem offers a wider range of finite element spaces than other FEM libraries, and the ability 52 | to symbolically compute basis functions. There are a number of situations in which the symbolic 53 | representation of a basis function is useful: it is easy to confirm, for example, that the 54 | derivatives of the basis functions have a certain desired property, or check what they are 55 | equal to when restricted to one face or edge of the cell. 56 | 57 | Symfem can also be used to explore the behaviour of the wide range of spaces it supports, so the 58 | user can decide which spaces to implement in a faster way in their FEM code. Additionally, 59 | Symfem can be used to prototype new finite element spaces, as custom spaces can easily be 60 | added, then it can be checked that the basis functions of the space behave as expected. 61 | 62 | As basis functions are computed symbolically in Symfem, it is much slower than the alternative 63 | libraries. It is therefore not suitable for performing actual finite element calculations. It 64 | should instead be seen as a library for research and experimentation. 65 | 66 | # References 67 | -------------------------------------------------------------------------------- /logo/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/logo/favicon.png -------------------------------------------------------------------------------- /logo/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /logo/logo-1280-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/logo/logo-1280-640.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/logo/logo.png -------------------------------------------------------------------------------- /prepare_release.py: -------------------------------------------------------------------------------- 1 | """Script to prepare files for uploading to PyPI.""" 2 | 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser(description="Prepare PyPI release") 6 | parser.add_argument("--version", metavar="version", default="main", help="Symfem version.") 7 | version = parser.parse_args().version 8 | if version != "main": 9 | version = "v" + version 10 | 11 | with open("README.md") as f: 12 | parts = f.read().split("](") 13 | 14 | content = parts[0] 15 | 16 | for p in parts[1:]: 17 | content += "](" 18 | if not p.startswith("http"): 19 | content += f"https://raw.githubusercontent.com/mscroggs/symfem/{version}/" 20 | content += p 21 | 22 | with open("README.md", "w") as f: 23 | f.write(content) 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "flit_core.buildapi" 3 | requires = ["flit_core >=3.8.0,<4"] 4 | 5 | [project] 6 | name = "symfem" 7 | version = "2025.3.1" 8 | description = "a symbolic finite element definition library" 9 | readme = "README.md" 10 | requires-python = ">=3.8.0" 11 | license = { file = "LICENSE" } 12 | authors = [ 13 | { name = "Matthew Scroggs", email = "symfem@mscroggs.co.uk" } 14 | ] 15 | packages = ["symfem", "symfem.elements", "symfem.polynomials"] 16 | dependencies = ["sympy>=1.10", "appdirs"] 17 | 18 | [project.urls] 19 | homepage = "https://github.com/mscroggs/symfem" 20 | repository = "https://github.com/mscroggs/symfem" 21 | documentation = "https://symfem.readthedocs.io/en/latest/" 22 | 23 | [project.optional-dependencies] 24 | style = ["ruff", "mypy"] 25 | docs = ["sphinx", "sphinx-autoapi"] 26 | optional = ["CairoSVG>=2.6.0"] 27 | test = ["pytest", "symfem[optional]", "numpy"] 28 | 29 | [tool.ruff] 30 | line-length = 100 31 | indent-width = 4 32 | 33 | [tool.ruff.lint.per-file-ignores] 34 | "__init__.py" = ["F401"] 35 | 36 | [tool.ruff.lint.pydocstyle] 37 | convention = "google" 38 | 39 | [tool.mypy] 40 | ignore_missing_imports = true 41 | -------------------------------------------------------------------------------- /scripts/draw_references.py: -------------------------------------------------------------------------------- 1 | """Draw reference cells for the README.""" 2 | 3 | import os 4 | import sys 5 | 6 | import symfem 7 | 8 | TESTING = "test" in sys.argv 9 | 10 | if TESTING: 11 | folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../_temp") 12 | else: 13 | folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../img") 14 | 15 | for shape in [ 16 | "interval", 17 | "triangle", 18 | "tetrahedron", 19 | "quadrilateral", 20 | "hexahedron", 21 | "prism", 22 | "pyramid", 23 | "dual polygon", 24 | ]: 25 | if shape == "dual polygon": 26 | ref = symfem.create_reference("dual polygon(6)") 27 | else: 28 | ref = symfem.create_reference(shape) 29 | 30 | filename = shape.replace(" ", "_") + "_numbering" 31 | ref.plot_entity_diagrams( 32 | f"{folder}/{filename}.png", {"png_width": 207 * (ref.tdim + 1)}, scale=300 33 | ) 34 | -------------------------------------------------------------------------------- /scripts/test_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../_temp") 6 | os.system(f"mkdir {file_path}") 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "file", 11 | [ 12 | file 13 | for file in os.listdir(os.path.dirname(os.path.realpath(__file__))) 14 | if file.endswith(".py") and not file.startswith(".") and not file.startswith("test_") 15 | ], 16 | ) 17 | def test_demo(file): 18 | file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), file) 19 | 20 | assert os.system(f"python3 {file_path} test") == 0 21 | -------------------------------------------------------------------------------- /symfem/__init__.py: -------------------------------------------------------------------------------- 1 | """Symfem: a symbolic finite element definition library.""" 2 | 3 | from symfem.create import add_element, create_element, create_reference 4 | from symfem.version import version as __version__ 5 | 6 | __citation__ = "https://doi.org/10.21105/joss.03556 (Scroggs, 2021)" 7 | __github__ = "https://github.com/mscroggs/symfem" 8 | __git__ = "https://github.com/mscroggs/symfem.git" 9 | -------------------------------------------------------------------------------- /symfem/caching.py: -------------------------------------------------------------------------------- 1 | """Functions to cache matrices.""" 2 | 3 | import os 4 | import typing 5 | from hashlib import sha256 6 | 7 | import sympy 8 | from appdirs import user_cache_dir 9 | 10 | try: 11 | os.mkdir(user_cache_dir()) 12 | except FileExistsError: 13 | pass 14 | CACHE_DIR = user_cache_dir("symfem") 15 | CACHE_FORMAT = "1" 16 | if os.path.isfile(CACHE_DIR): 17 | os.remove(CACHE_DIR) 18 | try: 19 | os.mkdir(CACHE_DIR) 20 | except FileExistsError: 21 | pass 22 | 23 | assert os.path.isdir(CACHE_DIR) 24 | 25 | __all__ = ["load_cached_matrix", "save_cached_matrix", "matrix_to_string", "matrix_from_string"] 26 | 27 | 28 | def load_cached_matrix( 29 | matrix_type: str, cache_id: str, size: typing.Tuple[int, int] 30 | ) -> typing.Union[sympy.matrices.dense.MutableDenseMatrix, None]: 31 | """Load a cached matrix. 32 | 33 | Args: 34 | matrix_type: The type of the matrix. This will be included in the filename. 35 | cache_id: The unique identifier of the matrix within this type 36 | 37 | Returns: 38 | The matrix 39 | """ 40 | hashed_id = sha256(cache_id.encode("utf-8")).hexdigest() 41 | filename = os.path.join(CACHE_DIR, f"{matrix_type}{CACHE_FORMAT}-{hashed_id}.matrix") 42 | try: 43 | with open(filename) as f: 44 | mat = matrix_from_string(f.read()) 45 | if mat.rows != size[0] or mat.cols != size[1]: 46 | return None 47 | return mat 48 | except FileNotFoundError: 49 | return None 50 | 51 | 52 | def save_cached_matrix( 53 | matrix_type: str, cache_id: str, matrix: sympy.matrices.dense.MutableDenseMatrix 54 | ): 55 | """Save a matrix to the cache. 56 | 57 | Args: 58 | matrix_type: The type of the matrix. This will be included in the filename. 59 | cache_id: The unique identifier of the matrix within this type 60 | matrix: The matrix 61 | """ 62 | hashed_id = sha256(cache_id.encode("utf-8")).hexdigest() 63 | filename = os.path.join(CACHE_DIR, f"{matrix_type}{CACHE_FORMAT}-{hashed_id}.matrix") 64 | with open(filename, "w") as f: 65 | f.write(matrix_to_string(matrix)) 66 | 67 | 68 | def matrix_to_string(m: sympy.matrices.dense.MutableDenseMatrix) -> str: 69 | """Convert a matrix to a string. 70 | 71 | Args: 72 | m: The matrix 73 | 74 | Returns: 75 | A representation of the matrix as a string 76 | """ 77 | return ";".join(",".join(f"{m[i, j]!r}" for j in range(m.cols)) for i in range(m.rows)) 78 | 79 | 80 | def matrix_from_string(mstr: str) -> sympy.matrices.dense.MutableDenseMatrix: 81 | """Convert a string to a matrix. 82 | 83 | Args: 84 | mstr: The string in the format output by `matrix_to_string` 85 | 86 | Returns: 87 | The matrix 88 | """ 89 | return sympy.Matrix([[sympy.S(j) for j in i.split(",")] for i in mstr.split(";")]) 90 | -------------------------------------------------------------------------------- /symfem/elements/__init__.py: -------------------------------------------------------------------------------- 1 | """Definitions of symfem elements.""" 2 | -------------------------------------------------------------------------------- /symfem/elements/abf.py: -------------------------------------------------------------------------------- 1 | """Arnold-Boffi-Falk elements on quadrilaterals. 2 | 3 | Thse elements definitions appear in https://dx.doi.org/10.1137/S0036142903431924 4 | (Arnold, Boffi, Falk, 2005) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange 10 | from symfem.elements.q import Nedelec 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import ( 13 | IntegralMoment, 14 | IntegralOfDivergenceAgainst, 15 | ListOfFunctionals, 16 | NormalIntegralMoment, 17 | ) 18 | from symfem.functions import FunctionInput 19 | from symfem.moments import make_integral_moment_dofs 20 | from symfem.references import NonDefaultReferenceError, Reference 21 | from symfem.symbols import x 22 | 23 | __all__ = ["ArnoldBoffiFalk"] 24 | 25 | 26 | class ArnoldBoffiFalk(CiarletElement): 27 | """An Arnold-Boffi-Falk element.""" 28 | 29 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 30 | """Create the element. 31 | 32 | Args: 33 | reference: The reference element 34 | order: The polynomial order 35 | variant: The variant of the element 36 | """ 37 | assert reference.name == "quadrilateral" 38 | if reference.vertices != reference.reference_vertices: 39 | raise NonDefaultReferenceError() 40 | 41 | poly: typing.List[FunctionInput] = [] 42 | poly += [(x[0] ** i * x[1] ** j, 0) for i in range(order + 3) for j in range(order + 1)] 43 | poly += [(0, x[0] ** i * x[1] ** j) for i in range(order + 1) for j in range(order + 3)] 44 | 45 | dofs: ListOfFunctionals = make_integral_moment_dofs( 46 | reference, 47 | edges=(NormalIntegralMoment, Lagrange, order, {"variant": variant}), 48 | faces=(IntegralMoment, Nedelec, order - 1, {"variant": variant}), 49 | ) 50 | 51 | for i in range(order + 1): 52 | dofs.append( 53 | IntegralOfDivergenceAgainst( 54 | reference, 55 | x[0] ** (order + 1) * x[1] ** i, 56 | entity=(2, 0), 57 | mapping="contravariant", 58 | ) 59 | ) 60 | dofs.append( 61 | IntegralOfDivergenceAgainst( 62 | reference, 63 | x[0] ** i * x[1] ** (order + 1), 64 | entity=(2, 0), 65 | mapping="contravariant", 66 | ) 67 | ) 68 | 69 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 70 | self.variant = variant 71 | 72 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 73 | """Return the kwargs used to create this element. 74 | 75 | Returns: 76 | Keyword argument dictionary 77 | """ 78 | return {"variant": self.variant} 79 | 80 | @property 81 | def lagrange_subdegree(self) -> int: 82 | return self.order 83 | 84 | @property 85 | def lagrange_superdegree(self) -> typing.Optional[int]: 86 | return self.order + 2 87 | 88 | @property 89 | def polynomial_subdegree(self) -> int: 90 | return self.order 91 | 92 | @property 93 | def polynomial_superdegree(self) -> typing.Optional[int]: 94 | return self.order * 2 + 2 95 | 96 | names = ["Arnold-Boffi-Falk", "ABF"] 97 | references = ["quadrilateral"] 98 | min_order = 0 99 | continuity = "H(div)" 100 | value_type = "vector" 101 | last_updated = "2025.03" 102 | -------------------------------------------------------------------------------- /symfem/elements/ac.py: -------------------------------------------------------------------------------- 1 | """Arbogast-Correa elements on quadrilaterals. 2 | 3 | This element's definition appears in https://dx.doi.org/10.1137/15M1013705 4 | (Arbogast, Correa, 2016) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.dpc import DPC 10 | from symfem.finite_element import CiarletElement 11 | from symfem.functionals import IntegralAgainst, ListOfFunctionals, NormalIntegralMoment 12 | from symfem.functions import FunctionInput 13 | from symfem.moments import make_integral_moment_dofs 14 | from symfem.polynomials import Hdiv_serendipity, polynomial_set_vector 15 | from symfem.references import NonDefaultReferenceError, Reference 16 | from symfem.symbols import x 17 | 18 | __all__ = ["AC"] 19 | 20 | 21 | class AC(CiarletElement): 22 | """Arbogast-Correa Hdiv finite element.""" 23 | 24 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 25 | """Create the element. 26 | 27 | Args: 28 | reference: The reference element 29 | order: The polynomial order 30 | variant: The variant of the element 31 | """ 32 | if reference.vertices != reference.reference_vertices: 33 | raise NonDefaultReferenceError() 34 | 35 | poly: typing.List[FunctionInput] = [] 36 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 37 | if order == 0: 38 | poly += [(x[0], 0), (0, x[1])] 39 | else: 40 | poly += Hdiv_serendipity(reference.tdim, reference.tdim, order) 41 | poly += [ 42 | (x[0] ** (i + 1) * x[1] ** (order - i), x[0] ** i * x[1] ** (1 + order - i)) 43 | for i in range(order + 1) 44 | ] 45 | 46 | dofs: ListOfFunctionals = make_integral_moment_dofs( 47 | reference, 48 | facets=(NormalIntegralMoment, DPC, order, {"variant": variant}), 49 | ) 50 | 51 | for i in range(order + 1): 52 | for j in range(order + 1 - i): 53 | if i + j > 0: 54 | f = (i * x[0] ** (i - 1) * x[1] ** j, j * x[0] ** i * x[1] ** (j - 1)) 55 | dofs.append( 56 | IntegralAgainst(reference, f, entity=(2, 0), mapping="contravariant") 57 | ) 58 | 59 | for i in range(1, order - 1): 60 | for j in range(1, order - i): 61 | f = ( 62 | x[0] ** i * (1 - x[0]) * x[1] ** (j - 1) * (j * (1 - x[1]) - x[1]), 63 | -(x[0] ** (i - 1)) * (i * (1 - x[0]) - x[0]) * x[1] ** j * (1 - x[1]), 64 | ) 65 | dofs.append(IntegralAgainst(reference, f, entity=(2, 0), mapping="contravariant")) 66 | 67 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 68 | self.variant = variant 69 | 70 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 71 | """Return the kwargs used to create this element. 72 | 73 | Returns: 74 | Keyword argument dictionary 75 | """ 76 | return {"variant": self.variant} 77 | 78 | @property 79 | def lagrange_subdegree(self) -> int: 80 | if self.order < 2: 81 | return self.order 82 | else: 83 | return self.order // 2 84 | 85 | @property 86 | def lagrange_superdegree(self) -> typing.Optional[int]: 87 | return self.order + 1 88 | 89 | @property 90 | def polynomial_subdegree(self) -> int: 91 | return self.order 92 | 93 | @property 94 | def polynomial_superdegree(self) -> typing.Optional[int]: 95 | return self.order + 1 96 | 97 | names = ["Arbogast-Correa", "AC", "AC full", "Arbogast-Correa full"] 98 | references = ["quadrilateral"] 99 | min_order = 0 100 | continuity = "H(div)" 101 | value_type = "vector" 102 | last_updated = "2023.06" 103 | -------------------------------------------------------------------------------- /symfem/elements/argyris.py: -------------------------------------------------------------------------------- 1 | """Argyris elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1017/S000192400008489X 4 | (Arygris, Fried, Scharpf, 1968) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import ( 11 | ListOfFunctionals, 12 | PointComponentSecondDerivativeEvaluation, 13 | PointDirectionalDerivativeEvaluation, 14 | PointEvaluation, 15 | PointNormalDerivativeEvaluation, 16 | ) 17 | from symfem.functions import FunctionInput 18 | from symfem.polynomials import polynomial_set_1d 19 | from symfem.references import NonDefaultReferenceError, Reference 20 | 21 | __all__ = ["Argyris"] 22 | 23 | 24 | class Argyris(CiarletElement): 25 | """Argyris finite element.""" 26 | 27 | def __init__(self, reference: Reference, order: int): 28 | """Create the element. 29 | 30 | Args: 31 | reference: The reference element 32 | order: The polynomial order 33 | """ 34 | assert order == 5 35 | assert reference.name == "triangle" 36 | if reference.vertices != reference.reference_vertices: 37 | raise NonDefaultReferenceError() 38 | 39 | dofs: ListOfFunctionals = [] 40 | for v_n, vs in enumerate(reference.vertices): 41 | dofs.append(PointEvaluation(reference, vs, entity=(0, v_n))) 42 | for i in range(reference.tdim): 43 | direction = tuple(1 if i == j else 0 for j in range(reference.tdim)) 44 | dofs.append( 45 | PointDirectionalDerivativeEvaluation(reference, vs, direction, entity=(0, v_n)) 46 | ) 47 | for i in range(reference.tdim): 48 | for j in range(i + 1): 49 | dofs.append( 50 | PointComponentSecondDerivativeEvaluation( 51 | reference, vs, (i, j), entity=(0, v_n) 52 | ) 53 | ) 54 | for e_n in range(reference.sub_entity_count(1)): 55 | assert isinstance(reference.sub_entity_types[1], str) 56 | sub_ref = reference.sub_entity(1, e_n) 57 | dofs.append( 58 | PointNormalDerivativeEvaluation( 59 | reference, sub_ref.midpoint(), sub_ref, entity=(1, e_n) 60 | ) 61 | ) 62 | poly: typing.List[FunctionInput] = [] 63 | poly += polynomial_set_1d(reference.tdim, order) 64 | 65 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 66 | 67 | @property 68 | def lagrange_subdegree(self) -> int: 69 | return self.order 70 | 71 | @property 72 | def lagrange_superdegree(self) -> typing.Optional[int]: 73 | return self.order 74 | 75 | @property 76 | def polynomial_subdegree(self) -> int: 77 | return self.order 78 | 79 | @property 80 | def polynomial_superdegree(self) -> typing.Optional[int]: 81 | return self.order 82 | 83 | names = ["Argyris"] 84 | references = ["triangle"] 85 | min_order = 5 86 | max_order = 5 87 | continuity = "L2" 88 | value_type = "scalar" 89 | last_updated = "2023.05" 90 | -------------------------------------------------------------------------------- /symfem/elements/bddf.py: -------------------------------------------------------------------------------- 1 | """Brezzi-Douglas-Duran-Fortin elements. 2 | 3 | This element's definition appears in https://doi.org/10.1007/BF01396752 4 | (Brezzi, Douglas, Duran, Fortin, 1987) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.dpc import DPC, VectorDPC 10 | from symfem.finite_element import CiarletElement 11 | from symfem.functionals import IntegralMoment, ListOfFunctionals, NormalIntegralMoment 12 | from symfem.functions import FunctionInput, VectorFunction 13 | from symfem.moments import make_integral_moment_dofs 14 | from symfem.polynomials import polynomial_set_vector 15 | from symfem.references import NonDefaultReferenceError, Reference 16 | from symfem.symbols import x 17 | 18 | __all__ = ["bddf_polyset", "BDDF"] 19 | 20 | 21 | def bddf_polyset(reference: Reference, order: int) -> typing.List[FunctionInput]: 22 | """Create the polynomial basis for a BDDF element. 23 | 24 | Args: 25 | reference: The reference cell 26 | order: The polynomial order 27 | 28 | Returns: 29 | The polynomial basis 30 | """ 31 | assert reference.name == "hexahedron" 32 | 33 | dim = reference.tdim 34 | pset: typing.List[FunctionInput] = [] 35 | pset += polynomial_set_vector(dim, dim, order) 36 | pset.append(VectorFunction((0, 0, x[0] ** (order + 1) * x[1])).curl()) 37 | pset.append(VectorFunction((0, x[0] * x[2] ** (order + 1), 0)).curl()) 38 | pset.append(VectorFunction((x[1] ** (order + 1) * x[2], 0, 0)).curl()) 39 | for i in range(1, order + 1): 40 | pset.append(VectorFunction((0, 0, x[0] * x[1] ** (i + 1) * x[2] ** (order - i))).curl()) 41 | pset.append(VectorFunction((0, x[0] ** (i + 1) * x[1] ** (order - i) * x[2], 0)).curl()) 42 | pset.append(VectorFunction((x[0] ** (order - i) * x[1] * x[2] ** (i + 1), 0, 0)).curl()) 43 | 44 | return pset 45 | 46 | 47 | class BDDF(CiarletElement): 48 | """Brezzi-Douglas-Duran-Fortin Hdiv finite element.""" 49 | 50 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 51 | """Create the element. 52 | 53 | Args: 54 | reference: The reference element 55 | order: The polynomial order 56 | variant: The variant of the element 57 | """ 58 | if reference.vertices != reference.reference_vertices: 59 | raise NonDefaultReferenceError() 60 | 61 | poly = bddf_polyset(reference, order) 62 | 63 | dofs: ListOfFunctionals = make_integral_moment_dofs( 64 | reference, 65 | facets=(NormalIntegralMoment, DPC, order, {"variant": variant}), 66 | cells=(IntegralMoment, VectorDPC, order - 2, {"variant": variant}), 67 | ) 68 | 69 | self.variant = variant 70 | 71 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 72 | 73 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 74 | """Return the kwargs used to create this element. 75 | 76 | Returns: 77 | Keyword argument dictionary 78 | """ 79 | return {"variant": self.variant} 80 | 81 | @property 82 | def lagrange_subdegree(self) -> int: 83 | return self.order // 3 84 | 85 | @property 86 | def lagrange_superdegree(self) -> typing.Optional[int]: 87 | return self.order + 1 88 | 89 | @property 90 | def polynomial_subdegree(self) -> int: 91 | return self.order 92 | 93 | @property 94 | def polynomial_superdegree(self) -> typing.Optional[int]: 95 | return self.order + 1 96 | 97 | names = ["Brezzi-Douglas-Duran-Fortin", "BDDF"] 98 | references = ["hexahedron"] 99 | min_order = 1 100 | continuity = "H(div)" 101 | value_type = "vector" 102 | last_updated = "2023.06" 103 | -------------------------------------------------------------------------------- /symfem/elements/bdm.py: -------------------------------------------------------------------------------- 1 | """Brezzi-Douglas-Marini elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1007/BF01389710 4 | (Brezzi, Douglas, Marini, 1985) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange 10 | from symfem.elements.nedelec import NedelecFirstKind 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import IntegralMoment, ListOfFunctionals, NormalIntegralMoment 13 | from symfem.functions import FunctionInput 14 | from symfem.moments import make_integral_moment_dofs 15 | from symfem.polynomials import polynomial_set_vector 16 | from symfem.references import NonDefaultReferenceError, Reference 17 | 18 | __all__ = ["BDM"] 19 | 20 | 21 | class BDM(CiarletElement): 22 | """Brezzi-Douglas-Marini Hdiv finite element.""" 23 | 24 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 25 | """Create the element. 26 | 27 | Args: 28 | reference: The reference element 29 | order: The polynomial order 30 | variant: The variant of the element 31 | """ 32 | if reference.vertices != reference.reference_vertices: 33 | raise NonDefaultReferenceError() 34 | 35 | poly: typing.List[FunctionInput] = [] 36 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 37 | 38 | dofs: ListOfFunctionals = make_integral_moment_dofs( 39 | reference, 40 | facets=(NormalIntegralMoment, Lagrange, order, {"variant": variant}), 41 | cells=(IntegralMoment, NedelecFirstKind, order - 2, {"variant": variant}), 42 | ) 43 | self.variant = variant 44 | 45 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 46 | 47 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 48 | """Return the kwargs used to create this element. 49 | 50 | Returns: 51 | Keyword argument dictionary 52 | """ 53 | return {"variant": self.variant} 54 | 55 | @property 56 | def lagrange_subdegree(self) -> int: 57 | return self.order 58 | 59 | @property 60 | def lagrange_superdegree(self) -> typing.Optional[int]: 61 | return self.order 62 | 63 | @property 64 | def polynomial_subdegree(self) -> int: 65 | return self.order 66 | 67 | @property 68 | def polynomial_superdegree(self) -> typing.Optional[int]: 69 | return self.order 70 | 71 | names = ["Brezzi-Douglas-Marini", "BDM", "N2div"] 72 | references = ["triangle", "tetrahedron"] 73 | min_order = 1 74 | continuity = "H(div)" 75 | value_type = "vector" 76 | last_updated = "2025.03" 77 | -------------------------------------------------------------------------------- /symfem/elements/bell.py: -------------------------------------------------------------------------------- 1 | """Bell elements on triangle. 2 | 3 | This element's definition is given in https://doi.org/10.1002/nme.1620010108 (Bell, 1969) 4 | """ 5 | 6 | import typing 7 | 8 | from symfem.finite_element import CiarletElement 9 | from symfem.functionals import DerivativePointEvaluation, ListOfFunctionals, PointEvaluation 10 | from symfem.functions import FunctionInput 11 | from symfem.polynomials import polynomial_set_1d 12 | from symfem.references import Reference 13 | from symfem.symbols import x 14 | 15 | __all__ = ["Bell"] 16 | 17 | 18 | class Bell(CiarletElement): 19 | """Bell finite element.""" 20 | 21 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 22 | """Create the element. 23 | 24 | Args: 25 | reference: The reference element 26 | order: The polynomial order 27 | variant: The variant of the element 28 | """ 29 | assert reference.name == "triangle" 30 | dofs: ListOfFunctionals = [] 31 | for v_n, v in enumerate(reference.vertices): 32 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 33 | dofs.append(DerivativePointEvaluation(reference, v, (1, 0), entity=(0, v_n))) 34 | dofs.append(DerivativePointEvaluation(reference, v, (0, 1), entity=(0, v_n))) 35 | dofs.append(DerivativePointEvaluation(reference, v, (2, 0), entity=(0, v_n))) 36 | dofs.append(DerivativePointEvaluation(reference, v, (1, 1), entity=(0, v_n))) 37 | dofs.append(DerivativePointEvaluation(reference, v, (0, 2), entity=(0, v_n))) 38 | self.variant = variant 39 | 40 | poly: typing.List[FunctionInput] = [] 41 | poly += polynomial_set_1d(reference.tdim, 4) 42 | poly.append(x[0] ** 5 - x[1] ** 5) 43 | poly.append(x[0] ** 3 * x[1] ** 2 - x[0] ** 2 * x[1] ** 3) 44 | poly.append(5 * x[0] ** 2 * x[1] ** 3 - x[0] ** 5) 45 | 46 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 47 | 48 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 49 | """Return the kwargs used to create this element. 50 | 51 | Returns: 52 | Keyword argument dictionary 53 | """ 54 | return {"variant": self.variant} 55 | 56 | @property 57 | def lagrange_subdegree(self) -> int: 58 | return 4 59 | 60 | @property 61 | def lagrange_superdegree(self) -> typing.Optional[int]: 62 | return 5 63 | 64 | @property 65 | def polynomial_subdegree(self) -> int: 66 | return 4 67 | 68 | @property 69 | def polynomial_superdegree(self) -> typing.Optional[int]: 70 | return 5 71 | 72 | names = ["Bell"] 73 | references = ["triangle"] 74 | min_order = 4 75 | max_order = 4 76 | continuity = "C1" 77 | value_type = "scalar" 78 | last_updated = "2025.03" 79 | _max_continuity_test_order = 3 80 | -------------------------------------------------------------------------------- /symfem/elements/bernardi_raugel.py: -------------------------------------------------------------------------------- 1 | """Bernardi-Raugel elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.2307/2007793 4 | (Bernardi and Raugel, 1985) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.elements.lagrange import Lagrange 12 | from symfem.finite_element import CiarletElement 13 | from symfem.functionals import ( 14 | DivergenceIntegralMoment, 15 | DotPointEvaluation, 16 | ListOfFunctionals, 17 | NormalIntegralMoment, 18 | ) 19 | from symfem.functions import FunctionInput 20 | from symfem.moments import make_integral_moment_dofs 21 | from symfem.polynomials import polynomial_set_vector 22 | from symfem.references import NonDefaultReferenceError, Reference 23 | from symfem.symbols import x 24 | 25 | __all__ = ["BernardiRaugel"] 26 | 27 | 28 | class BernardiRaugel(CiarletElement): 29 | """Bernardi-Raugel Hdiv finite element.""" 30 | 31 | def __init__(self, reference: Reference, order: int): 32 | """Create the element. 33 | 34 | Args: 35 | reference: The reference element 36 | order: The polynomial order 37 | variant: The variant of the element 38 | """ 39 | if reference.vertices != reference.reference_vertices: 40 | raise NonDefaultReferenceError() 41 | 42 | tdim = reference.tdim 43 | 44 | poly: typing.List[FunctionInput] = [] 45 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 46 | 47 | p = Lagrange(reference, 1, variant="equispaced") 48 | 49 | for i in range(reference.sub_entity_count(reference.tdim - 1)): 50 | sub_e = reference.sub_entity(reference.tdim - 1, i) 51 | bubble = sympy.Integer(1) 52 | for j in reference.sub_entities(reference.tdim - 1)[i]: 53 | bubble *= p.get_basis_function(j) 54 | poly.append(tuple(bubble * j for j in sub_e.normal())) 55 | 56 | dofs: ListOfFunctionals = [] 57 | 58 | # Evaluation at vertices 59 | for n in range(reference.sub_entity_count(0)): 60 | vertex = reference.sub_entity(0, n) 61 | v = vertex.vertices[0] 62 | for i in range(tdim): 63 | direction = tuple(1 if i == j else 0 for j in range(tdim)) 64 | dofs.append( 65 | DotPointEvaluation( 66 | reference, 67 | v, 68 | direction, 69 | entity=(0, n), 70 | mapping="identity", 71 | ) 72 | ) 73 | 74 | dofs += make_integral_moment_dofs( 75 | reference, 76 | facets=(NormalIntegralMoment, Lagrange, 0, "contravariant", {"variant": "equispaced"}), 77 | ) 78 | 79 | if order > 1: 80 | assert order == 2 and reference.name == "tetrahedron" 81 | 82 | for i in range(reference.tdim): 83 | bf = p.get_basis_functions() 84 | poly.append( 85 | tuple( 86 | bf[0] * bf[1] * bf[2] * bf[3] if j == i else 0 87 | for j in range(reference.tdim) 88 | ) 89 | ) 90 | 91 | for e_n, edge in enumerate(reference.edges): 92 | v1 = reference.vertices[edge[0]] 93 | v2 = reference.vertices[edge[1]] 94 | midpoint = tuple(sympy.Rational(i + j, 2) for i, j in zip(v1, v2)) 95 | for i in range(tdim): 96 | direction = tuple(1 if i == j else 0 for j in range(tdim)) 97 | dofs.append( 98 | DotPointEvaluation( 99 | reference, midpoint, direction, entity=(1, e_n), mapping="identity" 100 | ) 101 | ) 102 | 103 | p = Lagrange(reference, 0, variant="equispaced") 104 | for i in range(3): 105 | dofs.append( 106 | DivergenceIntegralMoment( 107 | reference, x[i], p.dofs[0], entity=(3, 0), mapping="identity" 108 | ) 109 | ) 110 | 111 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 112 | 113 | @property 114 | def lagrange_subdegree(self) -> int: 115 | return self.order 116 | 117 | @property 118 | def lagrange_superdegree(self) -> typing.Optional[int]: 119 | return self.order + self.reference.tdim - 1 120 | 121 | @property 122 | def polynomial_subdegree(self) -> int: 123 | return self.order 124 | 125 | @property 126 | def polynomial_superdegree(self) -> typing.Optional[int]: 127 | return self.order + self.reference.tdim - 1 128 | 129 | names = ["Bernardi-Raugel"] 130 | references = ["triangle", "tetrahedron"] 131 | min_order = 1 132 | max_order = {"triangle": 1, "tetrahedron": 2} 133 | continuity = "L2" 134 | value_type = "vector" 135 | last_updated = "2024.10.1" 136 | -------------------------------------------------------------------------------- /symfem/elements/bfs.py: -------------------------------------------------------------------------------- 1 | """Bogner-Fox-Schmit elements on tensor products. 2 | 3 | This element's definition appears in http://contrails.iit.edu/reports/8569 4 | (Bogner, Fox, Schmit, 1966) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import DerivativePointEvaluation, ListOfFunctionals, PointEvaluation 11 | from symfem.functions import FunctionInput 12 | from symfem.polynomials import quolynomial_set_1d 13 | from symfem.references import Reference 14 | 15 | __all__ = ["BognerFoxSchmit"] 16 | 17 | 18 | class BognerFoxSchmit(CiarletElement): 19 | """Bogner-Fox-Schmit finite element.""" 20 | 21 | def __init__(self, reference: Reference, order: int): 22 | """Create the element. 23 | 24 | Args: 25 | reference: The reference element 26 | order: The polynomial order 27 | """ 28 | assert order == 3 29 | dofs: ListOfFunctionals = [] 30 | for v_n, vs in enumerate(reference.vertices): 31 | dofs.append(PointEvaluation(reference, vs, entity=(0, v_n))) 32 | for i in range(reference.tdim): 33 | dofs.append( 34 | DerivativePointEvaluation( 35 | reference, 36 | vs, 37 | tuple(1 if i == j else 0 for j in range(reference.tdim)), 38 | entity=(0, v_n), 39 | ) 40 | ) 41 | 42 | if reference.tdim == 2: 43 | dofs.append( 44 | DerivativePointEvaluation( 45 | reference, vs, (1, 1), entity=(0, v_n), mapping="identity" 46 | ) 47 | ) 48 | 49 | poly: typing.List[FunctionInput] = [] 50 | poly += quolynomial_set_1d(reference.tdim, order) 51 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 52 | 53 | @property 54 | def lagrange_subdegree(self) -> int: 55 | return self.order 56 | 57 | @property 58 | def lagrange_superdegree(self) -> typing.Optional[int]: 59 | return self.order 60 | 61 | @property 62 | def polynomial_subdegree(self) -> int: 63 | return self.order 64 | 65 | @property 66 | def polynomial_superdegree(self) -> typing.Optional[int]: 67 | return self.order * 2 68 | 69 | names = ["Bogner-Fox-Schmit", "BFS"] 70 | references = ["quadrilateral"] 71 | min_order = 3 72 | max_order = 3 73 | continuity = "C0" 74 | # continuity = "C1" 75 | value_type = "scalar" 76 | last_updated = "2023.05" 77 | -------------------------------------------------------------------------------- /symfem/elements/conforming_crouzeix_raviart.py: -------------------------------------------------------------------------------- 1 | """Conforming Crouzeix-Raviart elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1051/m2an/197307R300331 4 | (Crouzeix, Raviart, 1973) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import ListOfFunctionals, PointEvaluation 13 | from symfem.functions import FunctionInput 14 | from symfem.polynomials import polynomial_set_1d 15 | from symfem.references import NonDefaultReferenceError, Reference 16 | from symfem.symbols import x 17 | 18 | __all__ = ["ConformingCrouzeixRaviart"] 19 | 20 | 21 | class ConformingCrouzeixRaviart(CiarletElement): 22 | """Conforming Crouzeix-Raviart finite element.""" 23 | 24 | def __init__(self, reference: Reference, order: int): 25 | """Create the element. 26 | 27 | Args: 28 | reference: The reference element 29 | order: The polynomial order 30 | """ 31 | if reference.vertices != reference.reference_vertices: 32 | raise NonDefaultReferenceError() 33 | assert reference.name == "triangle" 34 | 35 | poly: typing.List[FunctionInput] = [] 36 | poly += polynomial_set_1d(reference.tdim, order) 37 | 38 | poly += [x[0] ** i * x[1] ** (order - i) * (x[0] + x[1]) for i in range(1, order)] 39 | 40 | dofs: ListOfFunctionals = [] 41 | for i, v in enumerate(reference.vertices): 42 | dofs.append(PointEvaluation(reference, v, entity=(0, i))) 43 | if order >= 2: 44 | for i, edge in enumerate(reference.edges): 45 | for p in range(1, order): 46 | v = tuple( 47 | sympy.Rational((order - p) * a + p * b, order) 48 | for a, b in zip(reference.vertices[edge[0]], reference.vertices[edge[1]]) 49 | ) 50 | dofs.append(PointEvaluation(reference, v, entity=(1, i))) 51 | for i in range(1, order): 52 | for j in range(1, order + 1 - i): 53 | point = ( 54 | sympy.Rational(3 * i - 1, 3 * order), 55 | sympy.Rational(3 * j - 1, 3 * order), 56 | ) 57 | dofs.append(PointEvaluation(reference, point, entity=(2, 0))) 58 | 59 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 60 | 61 | @property 62 | def lagrange_subdegree(self) -> int: 63 | return self.order 64 | 65 | @property 66 | def lagrange_superdegree(self) -> typing.Optional[int]: 67 | if self.order == 1: 68 | return 1 69 | return self.order + 1 70 | 71 | @property 72 | def polynomial_subdegree(self) -> int: 73 | return self.order 74 | 75 | @property 76 | def polynomial_superdegree(self) -> typing.Optional[int]: 77 | if self.order == 1: 78 | return 1 79 | return self.order + 1 80 | 81 | names = ["conforming Crouzeix-Raviart", "conforming CR"] 82 | references = ["triangle"] 83 | min_order = 1 84 | continuity = "L2" 85 | value_type = "scalar" 86 | last_updated = "2023.05" 87 | -------------------------------------------------------------------------------- /symfem/elements/crouzeix_raviart.py: -------------------------------------------------------------------------------- 1 | """Crouzeix-Raviart elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1051/m2an/197307R300331 4 | (Crouzeix, Raviart, 1973) 5 | """ 6 | 7 | import typing 8 | from itertools import product 9 | 10 | from symfem.finite_element import CiarletElement 11 | from symfem.functionals import ListOfFunctionals, PointEvaluation 12 | from symfem.functions import FunctionInput 13 | from symfem.polynomials import polynomial_set_1d 14 | from symfem.quadrature import get_quadrature 15 | from symfem.references import Reference 16 | 17 | __all__ = ["CrouzeixRaviart"] 18 | 19 | 20 | class CrouzeixRaviart(CiarletElement): 21 | """Crouzeix-Raviart finite element.""" 22 | 23 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 24 | """Create the element. 25 | 26 | Args: 27 | reference: The reference element 28 | order: The polynomial order 29 | variant: The variant of the element 30 | """ 31 | assert reference.name in ["triangle", "tetrahedron"] 32 | 33 | if order > 1: 34 | assert reference.name == "triangle" 35 | 36 | points, _ = get_quadrature(variant, order + reference.tdim) 37 | 38 | dofs: ListOfFunctionals = [] 39 | 40 | for e_n in range(reference.sub_entity_count(reference.tdim - 1)): 41 | entity = reference.sub_entity(reference.tdim - 1, e_n) 42 | for i in product(range(1, order + 1), repeat=reference.tdim - 1): 43 | if sum(i) < order + reference.tdim - 1: 44 | dofs.append( 45 | PointEvaluation( 46 | reference, 47 | tuple( 48 | o + sum(a[j] * points[b] for a, b in zip(entity.axes, i)) 49 | for j, o in enumerate(entity.origin) 50 | ), 51 | entity=(reference.tdim - 1, e_n), 52 | ) 53 | ) 54 | 55 | points, _ = get_quadrature(variant, order + reference.tdim - 1) 56 | for i in product(range(1, order), repeat=reference.tdim): 57 | if sum(i) < order: 58 | dofs.append( 59 | PointEvaluation( 60 | reference, 61 | tuple( 62 | o + sum(a[j] * points[b] for a, b in zip(reference.axes, i)) 63 | for j, o in enumerate(reference.origin) 64 | ), 65 | entity=(reference.tdim, 0), 66 | ) 67 | ) 68 | 69 | poly: typing.List[FunctionInput] = [] 70 | poly += polynomial_set_1d(reference.tdim, order) 71 | 72 | self.variant = variant 73 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 74 | 75 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 76 | """Return the kwargs used to create this element. 77 | 78 | Returns: 79 | Keyword argument dictionary 80 | """ 81 | return {"variant": self.variant} 82 | 83 | @property 84 | def lagrange_subdegree(self) -> int: 85 | return self.order 86 | 87 | @property 88 | def lagrange_superdegree(self) -> typing.Optional[int]: 89 | return self.order 90 | 91 | @property 92 | def polynomial_subdegree(self) -> int: 93 | return self.order 94 | 95 | @property 96 | def polynomial_superdegree(self) -> typing.Optional[int]: 97 | return self.order 98 | 99 | names = ["Crouzeix-Raviart", "CR", "Crouzeix-Falk", "CF"] 100 | references = ["triangle", "tetrahedron"] 101 | min_order = 1 102 | max_order = {"tetrahedron": 1} 103 | continuity = "L2" 104 | value_type = "scalar" 105 | last_updated = "2023.05" 106 | -------------------------------------------------------------------------------- /symfem/elements/direct_serendipity.py: -------------------------------------------------------------------------------- 1 | """Serendipity elements on tensor product cells. 2 | 3 | This element's definition appears in https://arxiv.org/abs/1809.02192 4 | (Arbogast, Tao, 2018) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.dpc import DPC 10 | from symfem.finite_element import DirectElement 11 | from symfem.references import Reference 12 | from symfem.symbols import x 13 | 14 | __all__ = ["DirectSerendipity"] 15 | 16 | 17 | class DirectSerendipity(DirectElement): 18 | """A direct serendipity element.""" 19 | 20 | def __init__(self, reference: Reference, order: int): 21 | """Create the element. 22 | 23 | Args: 24 | reference: The reference element 25 | order: The polynomial order 26 | """ 27 | basis_functions = [] 28 | basis_entities = [] 29 | 30 | # Functions at vertices 31 | basis_functions += [a * b for a in (1 - x[1], x[1]) for b in (1 - x[0], x[0])] 32 | basis_entities += [(0, v) for v in range(4)] 33 | 34 | # Functions on edges 35 | if order >= 2: 36 | alpha_h = 1 37 | beta_h = 2 38 | gamma_h = 1 39 | xi_h = 2 40 | eta_h = 1 41 | 42 | alpha_v = 1 43 | beta_v = 2 44 | gamma_v = 1 45 | xi_v = 2 46 | eta_v = 1 47 | 48 | lambda_h = alpha_h * (1 - x[1]) + beta_h * x[1] + gamma_h 49 | r_h = -2 * x[1] / ((1 - x[1]) / xi_h + x[1] / eta_h) 50 | lambda_34 = xi_h * (1 - x[1]) + eta_h * x[1] 51 | 52 | lambda_v = alpha_v * (1 - x[0]) + beta_v * x[0] + gamma_v 53 | r_v = -2 * x[0] / ((1 - x[0]) / xi_v + x[0] / eta_v) 54 | lambda_12 = xi_v * (1 - x[0]) + eta_v * x[0] 55 | 56 | for j in range(order - 1): 57 | basis_functions.append((1 - x[1]) * x[1] * lambda_h**j) 58 | basis_entities.append((1, 1)) 59 | for j in range(order - 2): 60 | basis_functions.append((1 - x[1]) * x[1] * lambda_12 * lambda_h**j) 61 | basis_entities.append((1, 2)) 62 | basis_functions.append((1 - x[1]) * x[1] * r_v * lambda_h ** (order - 2)) 63 | basis_entities.append((1, 2)) 64 | 65 | for j in range(order - 1): 66 | basis_functions.append((1 - x[0]) * x[0] * lambda_v**j) 67 | basis_entities.append((1, 0)) 68 | for j in range(order - 2): 69 | basis_functions.append((1 - x[0]) * x[0] * lambda_34 * lambda_v**j) 70 | basis_entities.append((1, 3)) 71 | basis_functions.append((1 - x[0]) * x[0] * r_h * lambda_v ** (order - 2)) 72 | basis_entities.append((1, 3)) 73 | 74 | # Functions in interior 75 | if order >= 4: 76 | for f in DPC(reference, 4, "equispaced").get_basis_functions(): 77 | basis_functions.append(f * x[0] * x[1] * (1 - x[0]) * (1 - x[1])) 78 | basis_entities.append((2, 0)) 79 | 80 | super().__init__(reference, order, basis_functions, basis_entities, reference.tdim, 1) 81 | 82 | @property 83 | def lagrange_subdegree(self) -> int: 84 | raise NotImplementedError() 85 | 86 | @property 87 | def lagrange_superdegree(self) -> typing.Optional[int]: 88 | raise NotImplementedError() 89 | 90 | @property 91 | def polynomial_subdegree(self) -> int: 92 | raise NotImplementedError() 93 | 94 | @property 95 | def polynomial_superdegree(self) -> typing.Optional[int]: 96 | raise NotImplementedError() 97 | 98 | names = ["direct serendipity"] 99 | references = ["quadrilateral"] 100 | min_order = 1 101 | continuity = "C0" 102 | value_type = "scalar non-polynomial" 103 | last_updated = "2023.05" 104 | -------------------------------------------------------------------------------- /symfem/elements/enriched_galerkin.py: -------------------------------------------------------------------------------- 1 | """Enriched Galerkin elements. 2 | 3 | This element's definition appears in https://doi.org/10.1137/080722953 4 | (Sun, Liu, 2009). 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange 10 | from symfem.elements.q import Q 11 | from symfem.finite_element import EnrichedElement 12 | from symfem.references import Reference 13 | 14 | __all__ = ["EnrichedGalerkin"] 15 | 16 | 17 | class EnrichedGalerkin(EnrichedElement): 18 | """An enriched Galerkin element.""" 19 | 20 | def __init__(self, reference: Reference, order: int): 21 | """Create the element. 22 | 23 | Args: 24 | reference: The reference element 25 | order: The polynomial order 26 | """ 27 | if reference.name in ["quadrilateral", "hexahedron"]: 28 | super().__init__([Q(reference, order), Q(reference, 0)]) 29 | else: 30 | super().__init__([Lagrange(reference, order), Lagrange(reference, 0)]) 31 | 32 | @property 33 | def lagrange_subdegree(self) -> int: 34 | return self.order 35 | 36 | @property 37 | def lagrange_superdegree(self) -> typing.Optional[int]: 38 | return self.order 39 | 40 | @property 41 | def polynomial_subdegree(self) -> int: 42 | return self.order 43 | 44 | @property 45 | def polynomial_superdegree(self) -> typing.Optional[int]: 46 | if self.reference.name in ["quadrilateral", "hexahedron"]: 47 | return self.order * self.reference.tdim 48 | return self.order 49 | 50 | names = ["enriched Galerkin", "EG"] 51 | references = ["interval", "triangle", "quadrilateral", "tetrahedron", "hexahedron"] 52 | min_order = 1 53 | continuity = "C0" 54 | value_type = "scalar" 55 | last_updated = "2023.05" 56 | -------------------------------------------------------------------------------- /symfem/elements/fortin_soulie.py: -------------------------------------------------------------------------------- 1 | """Fortin-Soulie elements on a triangle. 2 | 3 | This element's definition appears in https://doi.org/10.1002/nme.1620190405 4 | (Fortin, Soulie, 1973) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import ListOfFunctionals, PointEvaluation 13 | from symfem.functions import FunctionInput 14 | from symfem.polynomials import polynomial_set_1d 15 | from symfem.references import NonDefaultReferenceError, Reference 16 | 17 | __all__ = ["FortinSoulie"] 18 | 19 | 20 | class FortinSoulie(CiarletElement): 21 | """Fortin-Soulie finite element.""" 22 | 23 | def __init__(self, reference: Reference, order: int): 24 | """Create the element. 25 | 26 | Args: 27 | reference: The reference element 28 | order: The polynomial order 29 | """ 30 | assert reference.name == "triangle" 31 | assert order == 2 32 | if reference.vertices != reference.reference_vertices: 33 | raise NonDefaultReferenceError() 34 | 35 | third = sympy.Rational(1, 3) 36 | two_thirds = sympy.Rational(2, 3) 37 | dofs: ListOfFunctionals = [ 38 | PointEvaluation(reference, (two_thirds, third), entity=(1, 0)), 39 | PointEvaluation(reference, (third, two_thirds), entity=(1, 0)), 40 | PointEvaluation(reference, (0, third), entity=(1, 1)), 41 | PointEvaluation(reference, (0, two_thirds), entity=(1, 1)), 42 | PointEvaluation(reference, (sympy.Rational(1, 2), 0), entity=(1, 2)), 43 | PointEvaluation(reference, (third, third), entity=(2, 0)), 44 | ] 45 | 46 | poly: typing.List[FunctionInput] = [] 47 | poly += polynomial_set_1d(reference.tdim, order) 48 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 49 | 50 | @property 51 | def lagrange_subdegree(self) -> int: 52 | return self.order 53 | 54 | @property 55 | def lagrange_superdegree(self) -> typing.Optional[int]: 56 | return self.order 57 | 58 | @property 59 | def polynomial_subdegree(self) -> int: 60 | return self.order 61 | 62 | @property 63 | def polynomial_superdegree(self) -> typing.Optional[int]: 64 | return self.order 65 | 66 | names = ["Fortin-Soulie", "FS"] 67 | references = ["triangle"] 68 | min_order = 2 69 | max_order = 2 70 | continuity = "L2" 71 | value_type = "scalar" 72 | last_updated = "2023.05" 73 | -------------------------------------------------------------------------------- /symfem/elements/hct.py: -------------------------------------------------------------------------------- 1 | """Hsieh-Clough-Tocher elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.2307/2006147 4 | (Ciarlet, 1978) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import ( 13 | DerivativePointEvaluation, 14 | ListOfFunctionals, 15 | PointEvaluation, 16 | PointNormalDerivativeEvaluation, 17 | ) 18 | from symfem.functions import FunctionInput, ScalarFunction 19 | from symfem.piecewise_functions import PiecewiseFunction 20 | from symfem.references import NonDefaultReferenceError, Reference 21 | from symfem.symbols import x 22 | 23 | __all__ = ["HsiehCloughTocher"] 24 | 25 | 26 | class HsiehCloughTocher(CiarletElement): 27 | """Hsieh-Clough-Tocher finite element.""" 28 | 29 | def __init__(self, reference: Reference, order: int): 30 | """Create the element. 31 | 32 | Args: 33 | reference: The reference element 34 | order: The polynomial order 35 | """ 36 | assert order == 3 37 | assert reference.name == "triangle" 38 | if reference.vertices != reference.reference_vertices: 39 | raise NonDefaultReferenceError() 40 | 41 | dofs: ListOfFunctionals = [] 42 | for v_n, vs in enumerate(reference.vertices): 43 | dofs.append(PointEvaluation(reference, vs, entity=(0, v_n))) 44 | dofs.append(DerivativePointEvaluation(reference, vs, (1, 0), entity=(0, v_n))) 45 | dofs.append(DerivativePointEvaluation(reference, vs, (0, 1), entity=(0, v_n))) 46 | for e_n in range(reference.sub_entity_count(1)): 47 | sub_ref = reference.sub_entity(1, e_n) 48 | dofs.append( 49 | PointNormalDerivativeEvaluation( 50 | reference, sub_ref.midpoint(), sub_ref, entity=(1, e_n) 51 | ) 52 | ) 53 | 54 | mid = tuple(sympy.Rational(sum(i), len(i)) for i in zip(*reference.vertices)) 55 | 56 | subs = [ 57 | (reference.vertices[0], reference.vertices[1], mid), 58 | (reference.vertices[1], reference.vertices[2], mid), 59 | (reference.vertices[2], reference.vertices[0], mid), 60 | ] 61 | 62 | piece_list = [ 63 | tuple(ScalarFunction(p) for _ in range(3)) 64 | for p in [ 65 | 1, 66 | x[0], 67 | x[1], 68 | x[0] ** 2, 69 | x[0] * x[1], 70 | x[1] ** 2, 71 | x[0] ** 3, 72 | x[0] ** 2 * x[1], 73 | x[0] * x[1] ** 2, 74 | x[1] ** 3, 75 | ] 76 | ] 77 | piece_list.append( 78 | ( 79 | ScalarFunction( 80 | -23 * x[0] ** 3 + 24 * x[0] ** 2 * x[1] - 12 * x[0] * x[1] ** 2 + 36 * x[1] ** 2 81 | ), 82 | ScalarFunction( 83 | -28 * x[0] ** 3 84 | + 12 * x[0] ** 2 * x[1] 85 | + 9 * x[0] ** 2 86 | - 3 * x[0] 87 | + 32 * x[1] ** 3 88 | + 12 * x[1] 89 | - 1 90 | ), 91 | ScalarFunction( 92 | -15 * x[0] ** 2 93 | - 33 * x[0] * x[1] ** 2 94 | + 30 * x[0] * x[1] 95 | + 22 * x[1] ** 3 96 | + 21 * x[1] ** 2 97 | ), 98 | ) 99 | ) 100 | piece_list.append( 101 | ( 102 | ScalarFunction( 103 | 22 * x[0] ** 3 104 | - 21 * x[0] ** 2 * x[1] 105 | - 12 * x[0] * x[1] ** 2 106 | + 30 * x[0] * x[1] 107 | - 24 * x[1] ** 2 108 | ), 109 | ScalarFunction( 110 | 32 * x[0] ** 3 111 | + 12 * x[0] ** 2 * x[1] 112 | - 21 * x[0] ** 2 113 | + 12 * x[0] 114 | - 28 * x[1] ** 3 115 | - 3 * x[1] 116 | - 1 117 | ), 118 | ScalarFunction( 119 | 15 * x[0] ** 2 + 12 * x[0] * x[1] ** 2 - 23 * x[1] ** 3 - 9 * x[1] ** 2 120 | ), 121 | ) 122 | ) 123 | 124 | poly: typing.List[FunctionInput] = [] 125 | poly += [PiecewiseFunction({i: j for i, j in zip(subs, p)}, 2) for p in piece_list] 126 | poly = reference.map_polyset_from_default(poly) 127 | 128 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 129 | 130 | @property 131 | def lagrange_subdegree(self) -> int: 132 | raise NotImplementedError() 133 | 134 | @property 135 | def lagrange_superdegree(self) -> typing.Optional[int]: 136 | raise NotImplementedError() 137 | 138 | @property 139 | def polynomial_subdegree(self) -> int: 140 | raise NotImplementedError() 141 | 142 | @property 143 | def polynomial_superdegree(self) -> typing.Optional[int]: 144 | raise NotImplementedError() 145 | 146 | names = ["Hsieh-Clough-Tocher", "Clough-Tocher", "HCT", "CT"] 147 | references = ["triangle"] 148 | min_order = 3 149 | max_order = 3 150 | # continuity = "C1" 151 | continuity = "C0" 152 | value_type = "scalar macro" 153 | last_updated = "2023.06" 154 | -------------------------------------------------------------------------------- /symfem/elements/hermite.py: -------------------------------------------------------------------------------- 1 | """Hermite elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1016/0045-7825(72)90006-0 4 | (Ciarlet, Raviart, 1972) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import DerivativePointEvaluation, ListOfFunctionals, PointEvaluation 11 | from symfem.functions import FunctionInput 12 | from symfem.polynomials import polynomial_set_1d 13 | from symfem.references import Reference 14 | 15 | __all__ = ["Hermite"] 16 | 17 | 18 | class Hermite(CiarletElement): 19 | """Hermite finite element.""" 20 | 21 | def __init__(self, reference: Reference, order: int): 22 | """Create the element. 23 | 24 | Args: 25 | reference: The reference element 26 | order: The polynomial order 27 | """ 28 | assert order == 3 29 | dofs: ListOfFunctionals = [] 30 | for v_n, v in enumerate(reference.vertices): 31 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 32 | for i in range(reference.tdim): 33 | dofs.append( 34 | DerivativePointEvaluation( 35 | reference, 36 | v, 37 | tuple(1 if i == j else 0 for j in range(reference.tdim)), 38 | entity=(0, v_n), 39 | ) 40 | ) 41 | for e_n in range(reference.sub_entity_count(2)): 42 | sub_entity = reference.sub_entity(2, e_n) 43 | dofs.append(PointEvaluation(reference, sub_entity.midpoint(), entity=(2, e_n))) 44 | 45 | poly: typing.List[FunctionInput] = [] 46 | poly += polynomial_set_1d(reference.tdim, order) 47 | 48 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 49 | 50 | @property 51 | def lagrange_subdegree(self) -> int: 52 | return self.order 53 | 54 | @property 55 | def lagrange_superdegree(self) -> typing.Optional[int]: 56 | return self.order 57 | 58 | @property 59 | def polynomial_subdegree(self) -> int: 60 | return self.order 61 | 62 | @property 63 | def polynomial_superdegree(self) -> typing.Optional[int]: 64 | return self.order 65 | 66 | names = ["Hermite"] 67 | references = ["interval", "triangle", "tetrahedron"] 68 | min_order = 3 69 | max_order = 3 70 | continuity = "C0" 71 | value_type = "scalar" 72 | last_updated = "2023.05" 73 | -------------------------------------------------------------------------------- /symfem/elements/huang_zhang.py: -------------------------------------------------------------------------------- 1 | """Huang-Zhang element on a quadrilateral. 2 | 3 | This element's definition appears in https://doi.org/10.1007/s11464-011-0094-0 4 | (Huang, Zhang, 2011) and https://doi.org/10.1137/080728949 (Zhang, 2009) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange 10 | from symfem.finite_element import CiarletElement 11 | from symfem.functionals import ( 12 | IntegralAgainst, 13 | ListOfFunctionals, 14 | NormalIntegralMoment, 15 | TangentIntegralMoment, 16 | ) 17 | from symfem.functions import FunctionInput, VectorFunction 18 | from symfem.moments import make_integral_moment_dofs 19 | from symfem.references import NonDefaultReferenceError, Reference 20 | from symfem.symbols import x 21 | 22 | __all__ = ["HuangZhang"] 23 | 24 | 25 | class HuangZhang(CiarletElement): 26 | """Huang-Zhang finite element.""" 27 | 28 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 29 | """Create the element. 30 | 31 | Args: 32 | reference: The reference element 33 | order: The polynomial order 34 | variant: The variant of the element 35 | """ 36 | assert reference.name == "quadrilateral" 37 | if reference.vertices != reference.reference_vertices: 38 | raise NonDefaultReferenceError() 39 | 40 | self.variant = variant 41 | 42 | dofs: ListOfFunctionals = [] 43 | poly: typing.List[FunctionInput] = [] 44 | poly += [ 45 | VectorFunction([x[0] ** i * x[1] ** j, 0]) 46 | for i in range(order + 2) 47 | for j in range(order + 1) 48 | ] 49 | poly += [ 50 | VectorFunction([0, x[0] ** i * x[1] ** j]) 51 | for i in range(order + 1) 52 | for j in range(order + 2) 53 | ] 54 | 55 | dofs += make_integral_moment_dofs( 56 | reference, 57 | facets=(NormalIntegralMoment, Lagrange, order, {"variant": variant}), 58 | ) 59 | dofs += make_integral_moment_dofs( 60 | reference, 61 | facets=(TangentIntegralMoment, Lagrange, order - 1, {"variant": variant}), 62 | ) 63 | 64 | for i in range(order): 65 | for j in range(order - 1): 66 | dofs.append(IntegralAgainst(reference, (x[0] ** i * x[1] ** j, 0), (2, 0))) 67 | dofs.append(IntegralAgainst(reference, (0, x[0] ** j * x[1] ** i), (2, 0))) 68 | 69 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 70 | 71 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 72 | """Return the kwargs used to create this element. 73 | 74 | Returns: 75 | Keyword argument dictionary 76 | """ 77 | return {"variant": self.variant} 78 | 79 | @property 80 | def lagrange_subdegree(self) -> int: 81 | return self.order 82 | 83 | @property 84 | def lagrange_superdegree(self) -> typing.Optional[int]: 85 | return self.order + 1 86 | 87 | @property 88 | def polynomial_subdegree(self) -> int: 89 | return self.order 90 | 91 | @property 92 | def polynomial_superdegree(self) -> typing.Optional[int]: 93 | return self.order * 2 + 1 94 | 95 | names = ["Huang-Zhang", "HZ"] 96 | references = ["quadrilateral"] 97 | min_order = 1 98 | continuity = "H(div)" 99 | value_type = "vector" 100 | last_updated = "2025.03" 101 | -------------------------------------------------------------------------------- /symfem/elements/lagrange_pyramid.py: -------------------------------------------------------------------------------- 1 | """Lagrange elements on a pyramid. 2 | 3 | This element's definition appears in https://doi.org/10.1007/s10915-009-9334-9 4 | (Bergot, Cohen, Durufle, 2010) 5 | """ 6 | 7 | import typing 8 | from itertools import product 9 | 10 | import sympy 11 | 12 | from symfem.finite_element import CiarletElement 13 | from symfem.functionals import IntegralAgainst, ListOfFunctionals, PointEvaluation 14 | from symfem.functions import FunctionInput 15 | from symfem.polynomials import orthonormal_basis, pyramid_polynomial_set_1d 16 | from symfem.quadrature import get_quadrature 17 | from symfem.references import NonDefaultReferenceError, Reference 18 | 19 | __all__ = ["Lagrange"] 20 | 21 | 22 | class Lagrange(CiarletElement): 23 | """Lagrange finite element.""" 24 | 25 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 26 | """Create the element. 27 | 28 | Args: 29 | reference: The reference element 30 | order: The polynomial order 31 | variant: The variant of the element 32 | """ 33 | if reference.vertices != reference.reference_vertices: 34 | raise NonDefaultReferenceError() 35 | 36 | dofs: ListOfFunctionals = [] 37 | if variant == "legendre": 38 | basis = orthonormal_basis(reference.name, order, 0)[0] 39 | for f in basis: 40 | dofs.append(IntegralAgainst(reference, f, (reference.tdim, 0))) 41 | elif order == 0: 42 | dofs = [ 43 | PointEvaluation( 44 | reference, 45 | tuple(sympy.Rational(1, reference.tdim + 1) for i in range(reference.tdim)), 46 | entity=(reference.tdim, 0), 47 | ) 48 | ] 49 | elif variant == "lobatto": 50 | raise NotImplementedError() 51 | else: 52 | if variant == "gl": 53 | points, _ = get_quadrature("legendre", order + 1) 54 | else: 55 | points, _ = get_quadrature(variant, order + 1) 56 | 57 | # Vertices 58 | for v_n, v in enumerate(reference.vertices): 59 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 60 | # Edges 61 | for e_n in range(reference.sub_entity_count(1)): 62 | entity = reference.sub_entity(1, e_n) 63 | for i in range(1, order): 64 | dofs.append( 65 | PointEvaluation( 66 | reference, 67 | tuple( 68 | o + entity.axes[0][j] * points[i] 69 | for j, o in enumerate(entity.origin) 70 | ), 71 | entity=(1, e_n), 72 | ) 73 | ) 74 | # Faces 75 | for e_n in range(reference.sub_entity_count(2)): 76 | entity = reference.sub_entity(2, e_n) 77 | for ii in product(range(1, order), repeat=2): 78 | if len(entity.vertices) == 4 or sum(ii) < order: 79 | dofs.append( 80 | PointEvaluation( 81 | reference, 82 | tuple( 83 | o + sum(a[j] * points[b] for a, b in zip(entity.axes, ii[::-1])) 84 | for j, o in enumerate(entity.origin) 85 | ), 86 | entity=(2, e_n), 87 | ) 88 | ) 89 | 90 | # Interior 91 | for ii in product(range(1, order), repeat=3): 92 | if max(ii[0], ii[1]) + ii[2] < order: 93 | dofs.append( 94 | PointEvaluation( 95 | reference, 96 | tuple( 97 | o + sum(a[j] * points[b] for a, b in zip(reference.axes, ii)) 98 | for j, o in enumerate(reference.origin) 99 | ), 100 | entity=(3, 0), 101 | ) 102 | ) 103 | 104 | poly: typing.List[FunctionInput] = [] 105 | poly += pyramid_polynomial_set_1d(reference.tdim, order) 106 | 107 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 108 | self.variant = variant 109 | 110 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 111 | """Return the kwargs used to create this element. 112 | 113 | Returns: 114 | Keyword argument dictionary 115 | """ 116 | return {"variant": self.variant} 117 | 118 | @property 119 | def lagrange_subdegree(self) -> int: 120 | return self.order 121 | 122 | @property 123 | def lagrange_superdegree(self) -> typing.Optional[int]: 124 | return self.order 125 | 126 | @property 127 | def polynomial_subdegree(self) -> int: 128 | return self.order 129 | 130 | @property 131 | def polynomial_superdegree(self) -> typing.Optional[int]: 132 | return None 133 | 134 | names = ["Lagrange", "P"] 135 | references = ["pyramid"] 136 | min_order = 0 137 | continuity = "C0" 138 | value_type = "scalar" 139 | last_updated = "2024.09" 140 | -------------------------------------------------------------------------------- /symfem/elements/morley.py: -------------------------------------------------------------------------------- 1 | """Morley elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1017/S0001925900004546 4 | (Morley, 1968) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import ListOfFunctionals, PointEvaluation, PointNormalDerivativeEvaluation 11 | from symfem.functions import FunctionInput 12 | from symfem.polynomials import polynomial_set_1d 13 | from symfem.references import NonDefaultReferenceError, Reference 14 | 15 | __all__ = ["Morley"] 16 | 17 | 18 | class Morley(CiarletElement): 19 | """Morley finite element.""" 20 | 21 | def __init__(self, reference: Reference, order: int): 22 | """Create the element. 23 | 24 | Args: 25 | reference: The reference element 26 | order: The polynomial order 27 | """ 28 | if reference.vertices != reference.reference_vertices: 29 | raise NonDefaultReferenceError() 30 | assert order == 2 31 | assert reference.name == "triangle" 32 | dofs: ListOfFunctionals = [] 33 | for v_n, vs in enumerate(reference.vertices): 34 | dofs.append(PointEvaluation(reference, vs, entity=(0, v_n))) 35 | for e_n in range(reference.sub_entity_count(1)): 36 | sub_ref = reference.sub_entity(1, e_n) 37 | midpoint = sub_ref.midpoint() 38 | dofs.append( 39 | PointNormalDerivativeEvaluation(reference, midpoint, sub_ref, entity=(1, e_n)) 40 | ) 41 | 42 | poly: typing.List[FunctionInput] = [] 43 | poly += polynomial_set_1d(reference.tdim, order) 44 | 45 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 46 | 47 | @property 48 | def lagrange_subdegree(self) -> int: 49 | return self.order 50 | 51 | @property 52 | def lagrange_superdegree(self) -> typing.Optional[int]: 53 | return self.order 54 | 55 | @property 56 | def polynomial_subdegree(self) -> int: 57 | return self.order 58 | 59 | @property 60 | def polynomial_superdegree(self) -> typing.Optional[int]: 61 | return self.order 62 | 63 | names = ["Morley"] 64 | references = ["triangle"] 65 | min_order = 2 66 | max_order = 2 67 | continuity = "L2" 68 | value_type = "scalar" 69 | last_updated = "2023.05" 70 | -------------------------------------------------------------------------------- /symfem/elements/morley_wang_xu.py: -------------------------------------------------------------------------------- 1 | """Morley-Wang-Xu elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1090/S0025-5718-2012-02611-1 4 | (Wang, Xu, 2013) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import ( 11 | IntegralAgainst, 12 | IntegralOfDirectionalMultiderivative, 13 | ListOfFunctionals, 14 | PointEvaluation, 15 | ) 16 | from symfem.functions import FunctionInput 17 | from symfem.polynomials import polynomial_set_1d 18 | from symfem.references import NonDefaultReferenceError, Reference 19 | 20 | __all__ = ["MorleyWangXu"] 21 | 22 | 23 | class MorleyWangXu(CiarletElement): 24 | """Morley-Wang-Xu finite element.""" 25 | 26 | def __init__(self, reference: Reference, order: int): 27 | """Create the element. 28 | 29 | Args: 30 | reference: The reference element 31 | order: The polynomial order 32 | """ 33 | assert order <= reference.tdim 34 | if reference.vertices != reference.reference_vertices: 35 | raise NonDefaultReferenceError() 36 | 37 | poly: typing.List[FunctionInput] = [] 38 | poly += polynomial_set_1d(reference.tdim, order) 39 | 40 | dofs: ListOfFunctionals = [] 41 | 42 | if order == 1: 43 | if reference.tdim == 1: 44 | for v_n, v in enumerate(reference.vertices): 45 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 46 | else: 47 | dim = reference.tdim - 1 48 | for facet_n in range(reference.sub_entity_count(dim)): 49 | facet = reference.sub_entity(dim, facet_n) 50 | dofs.append( 51 | IntegralAgainst(reference, 1 / facet.jacobian(), entity=(dim, facet_n)) 52 | ) 53 | elif order == 2: 54 | if reference.tdim == 2: 55 | for v_n, v in enumerate(reference.vertices): 56 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 57 | else: 58 | dim = reference.tdim - 2 59 | for ridge_n in range(reference.sub_entity_count(dim)): 60 | ridge = reference.sub_entity(dim, ridge_n) 61 | dofs.append( 62 | IntegralAgainst(reference, 1 / ridge.jacobian(), entity=(dim, ridge_n)) 63 | ) 64 | dim = reference.tdim - 1 65 | for facet_n in range(reference.sub_entity_count(dim)): 66 | facet = reference.sub_entity(dim, facet_n) 67 | dofs.append( 68 | IntegralOfDirectionalMultiderivative( 69 | reference, 70 | (facet.normal(),), 71 | (1,), 72 | (dim, facet_n), 73 | scale=1 / facet.jacobian(), 74 | ) 75 | ) 76 | else: 77 | assert order == reference.tdim == 3 78 | for v_n, v in enumerate(reference.vertices): 79 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 80 | for e_n, vs in enumerate(reference.sub_entities(1)): 81 | subentity = reference.sub_entity(1, e_n) 82 | volume = subentity.jacobian() 83 | normals = [] 84 | for f_n, f_vs in enumerate(reference.sub_entities(2)): 85 | if vs[0] in f_vs and vs[1] in f_vs: 86 | face = reference.sub_entity(2, f_n) 87 | normals.append(face.normal()) 88 | for orders in [(1, 0), (0, 1)]: 89 | dofs.append( 90 | IntegralOfDirectionalMultiderivative( 91 | reference, tuple(normals), orders, (1, e_n), scale=1 / volume 92 | ) 93 | ) 94 | for f_n, vs in enumerate(reference.sub_entities(2)): 95 | subentity = reference.sub_entity(2, f_n) 96 | volume = subentity.jacobian() 97 | dofs.append( 98 | IntegralOfDirectionalMultiderivative( 99 | reference, (subentity.normal(),), (2,), (2, f_n), scale=1 / volume 100 | ) 101 | ) 102 | 103 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 104 | 105 | @property 106 | def lagrange_subdegree(self) -> int: 107 | return self.order 108 | 109 | @property 110 | def lagrange_superdegree(self) -> typing.Optional[int]: 111 | return self.order 112 | 113 | @property 114 | def polynomial_subdegree(self) -> int: 115 | return self.order 116 | 117 | @property 118 | def polynomial_superdegree(self) -> typing.Optional[int]: 119 | return self.order 120 | 121 | names = ["Morley-Wang-Xu", "MWX"] 122 | references = ["interval", "triangle", "tetrahedron"] 123 | min_order = 1 124 | max_order = {"interval": 1, "triangle": 2, "tetrahedron": 3} 125 | # continuity = "C{order}" 126 | continuity = "C0" 127 | value_type = "scalar" 128 | last_updated = "2023.06" 129 | -------------------------------------------------------------------------------- /symfem/elements/mtw.py: -------------------------------------------------------------------------------- 1 | """Mardal-Tai-Winther elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1137/S0036142901383910 4 | (Mardal, Tai, Winther, 2002) 5 | and https://doi.org/10.1007/s10092-006-0124-6 (Tail, Mardal, 2006) 6 | """ 7 | 8 | import typing 9 | 10 | from symfem.elements.lagrange import Lagrange 11 | from symfem.elements.nedelec import NedelecFirstKind 12 | from symfem.finite_element import CiarletElement 13 | from symfem.functionals import ( 14 | IntegralMoment, 15 | ListOfFunctionals, 16 | NormalIntegralMoment, 17 | TangentIntegralMoment, 18 | ) 19 | from symfem.functions import FunctionInput, VectorFunction 20 | from symfem.moments import make_integral_moment_dofs 21 | from symfem.polynomials import polynomial_set_vector 22 | from symfem.references import NonDefaultReferenceError, Reference 23 | from symfem.symbols import x 24 | 25 | __all__ = ["MardalTaiWinther"] 26 | 27 | 28 | class MardalTaiWinther(CiarletElement): 29 | """Mardal-Tai-Winther Hdiv finite element.""" 30 | 31 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 32 | """Create the element. 33 | 34 | Args: 35 | reference: The reference element 36 | order: The polynomial order 37 | variant: The variant of the element 38 | """ 39 | if reference.vertices != reference.reference_vertices: 40 | raise NonDefaultReferenceError() 41 | 42 | dofs: ListOfFunctionals = make_integral_moment_dofs( 43 | reference, 44 | facets=(NormalIntegralMoment, Lagrange, 1, "contravariant", {"variant": variant}), 45 | ) 46 | 47 | poly: typing.List[FunctionInput] = [] 48 | if reference.name == "triangle": 49 | poly += [ 50 | (1, 0), 51 | (x[0], 0), 52 | (x[1], 0), 53 | (0, 1), 54 | (0, x[0]), 55 | (0, x[1]), 56 | # (x**2 + 2*x*y, -2*x*y - y**2) 57 | (x[0] ** 2 + 2 * x[0] * x[1], -2 * x[0] * x[1] - x[1] ** 2), 58 | # (-x**3 + 2*x**2 + 3*x*y**2, 3*x**2*y - 4*x*y - y**3) 59 | ( 60 | -(x[0] ** 3) + 2 * x[0] ** 2 + 3 * x[0] * x[1] ** 2, 61 | 3 * x[0] ** 2 * x[1] - 4 * x[0] * x[1] - x[1] ** 3, 62 | ), 63 | # (2*x**2*y + x**2 + 3*x*y**2, -2*x*y**2 - 2*x*y - y**3) 64 | ( 65 | 2 * x[0] ** 2 * x[1] + x[0] ** 2 + 3 * x[0] * x[1] ** 2, 66 | -2 * x[0] * x[1] ** 2 - 2 * x[0] * x[1] - x[1] ** 3, 67 | ), 68 | ] 69 | dofs += make_integral_moment_dofs( 70 | reference, 71 | facets=(TangentIntegralMoment, Lagrange, 0, "contravariant", {"variant": variant}), 72 | ) 73 | else: 74 | assert reference.name == "tetrahedron" 75 | 76 | poly += polynomial_set_vector(reference.tdim, reference.tdim, 1) 77 | for p in polynomial_set_vector(reference.tdim, reference.tdim, 1): 78 | poly.append( 79 | VectorFunction( 80 | tuple(i * x[0] * x[1] * x[2] * (1 - x[0] - x[1] - x[2]) for i in p) 81 | ).curl() 82 | ) 83 | 84 | dofs += make_integral_moment_dofs( 85 | reference, 86 | facets=(IntegralMoment, NedelecFirstKind, 0, "contravariant", {"variant": variant}), 87 | ) 88 | 89 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 90 | self.variant = variant 91 | 92 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 93 | """Return the kwargs used to create this element. 94 | 95 | Returns: 96 | Keyword argument dictionary 97 | """ 98 | return {"variant": self.variant} 99 | 100 | @property 101 | def lagrange_subdegree(self) -> int: 102 | return 1 103 | 104 | @property 105 | def lagrange_superdegree(self) -> typing.Optional[int]: 106 | return self.order + self.reference.tdim 107 | 108 | @property 109 | def polynomial_subdegree(self) -> int: 110 | return self.lagrange_subdegree 111 | 112 | @property 113 | def polynomial_superdegree(self) -> typing.Optional[int]: 114 | return self.lagrange_superdegree 115 | 116 | names = ["Mardal-Tai-Winther", "MTW"] 117 | references = ["triangle", "tetrahedron"] 118 | min_order = 1 119 | max_order = 1 120 | continuity = "H(div)" 121 | value_type = "vector" 122 | last_updated = "2025.03" 123 | -------------------------------------------------------------------------------- /symfem/elements/nedelec.py: -------------------------------------------------------------------------------- 1 | """Nedelec elements on simplices. 2 | 3 | These elements' definitions appear in https://doi.org/10.1007/BF01396415 4 | (Nedelec, 1980) and https://doi.org/10.1007/BF01389668 (Nedelec, 1986) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange, VectorLagrange 10 | from symfem.elements.rt import RaviartThomas 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import IntegralMoment, ListOfFunctionals, TangentIntegralMoment 13 | from symfem.functions import FunctionInput 14 | from symfem.moments import make_integral_moment_dofs 15 | from symfem.polynomials import Hcurl_polynomials, polynomial_set_vector 16 | from symfem.references import Reference 17 | 18 | __all__ = ["NedelecFirstKind", "NedelecSecondKind"] 19 | 20 | 21 | class NedelecFirstKind(CiarletElement): 22 | """Nedelec first kind Hcurl finite element.""" 23 | 24 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 25 | """Create the element. 26 | 27 | Args: 28 | reference: The reference element 29 | order: The polynomial order 30 | variant: The variant of the element 31 | """ 32 | poly: typing.List[FunctionInput] = [] 33 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 34 | poly += Hcurl_polynomials(reference.tdim, reference.tdim, order + 1) 35 | dofs: ListOfFunctionals = make_integral_moment_dofs( 36 | reference, 37 | edges=(TangentIntegralMoment, Lagrange, order, {"variant": variant}), 38 | faces=(IntegralMoment, VectorLagrange, order - 1, "covariant", {"variant": variant}), 39 | volumes=(IntegralMoment, VectorLagrange, order - 2, "covariant", {"variant": variant}), 40 | ) 41 | 42 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 43 | self.variant = variant 44 | 45 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 46 | """Return the kwargs used to create this element. 47 | 48 | Returns: 49 | Keyword argument dictionary 50 | """ 51 | return {"variant": self.variant} 52 | 53 | @property 54 | def lagrange_subdegree(self) -> int: 55 | return self.order 56 | 57 | @property 58 | def lagrange_superdegree(self) -> typing.Optional[int]: 59 | return self.order + 1 60 | 61 | @property 62 | def polynomial_subdegree(self) -> int: 63 | return self.order 64 | 65 | @property 66 | def polynomial_superdegree(self) -> typing.Optional[int]: 67 | return self.order + 1 68 | 69 | names = ["Nedelec", "Nedelec1", "N1curl"] 70 | references = ["triangle", "tetrahedron"] 71 | min_order = 0 72 | continuity = "H(curl)" 73 | value_type = "vector" 74 | last_updated = "2025.03" 75 | 76 | 77 | class NedelecSecondKind(CiarletElement): 78 | """Nedelec second kind Hcurl finite element.""" 79 | 80 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 81 | """Create the element. 82 | 83 | Args: 84 | reference: The reference element 85 | order: The polynomial order 86 | variant: The variant of the element 87 | """ 88 | poly: typing.List[FunctionInput] = [] 89 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 90 | 91 | dofs: ListOfFunctionals = make_integral_moment_dofs( 92 | reference, 93 | edges=(TangentIntegralMoment, Lagrange, order, {"variant": variant}), 94 | faces=(IntegralMoment, RaviartThomas, order - 2, "covariant", {"variant": variant}), 95 | volumes=(IntegralMoment, RaviartThomas, order - 3, "covariant", {"variant": variant}), 96 | ) 97 | 98 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 99 | self.variant = variant 100 | 101 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 102 | """Return the kwargs used to create this element. 103 | 104 | Returns: 105 | Keyword argument dictionary 106 | """ 107 | return {"variant": self.variant} 108 | 109 | @property 110 | def lagrange_subdegree(self) -> int: 111 | return self.order 112 | 113 | @property 114 | def lagrange_superdegree(self) -> typing.Optional[int]: 115 | return self.order 116 | 117 | @property 118 | def polynomial_subdegree(self) -> int: 119 | return self.order 120 | 121 | @property 122 | def polynomial_superdegree(self) -> typing.Optional[int]: 123 | return self.order 124 | 125 | names = ["Nedelec2", "N2curl"] 126 | references = ["triangle", "tetrahedron"] 127 | min_order = 1 128 | continuity = "H(curl)" 129 | value_type = "vector" 130 | last_updated = "2023.06" 131 | -------------------------------------------------------------------------------- /symfem/elements/nedelec_prism.py: -------------------------------------------------------------------------------- 1 | """Nedelec elements on prisms.""" 2 | 3 | import typing 4 | 5 | from symfem.elements.lagrange import Lagrange, VectorLagrange 6 | from symfem.elements.q import RaviartThomas as QRT 7 | from symfem.finite_element import CiarletElement 8 | from symfem.functionals import ( 9 | IntegralAgainst, 10 | IntegralMoment, 11 | ListOfFunctionals, 12 | TangentIntegralMoment, 13 | ) 14 | from symfem.functions import FunctionInput 15 | from symfem.moments import make_integral_moment_dofs 16 | from symfem.polynomials import Hcurl_polynomials, polynomial_set_1d, polynomial_set_vector 17 | from symfem.references import NonDefaultReferenceError, Reference 18 | from symfem.symbols import x 19 | 20 | __all__ = ["Nedelec"] 21 | 22 | 23 | class Nedelec(CiarletElement): 24 | """Nedelec Hcurl finite element.""" 25 | 26 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 27 | """Create the element. 28 | 29 | Args: 30 | reference: The reference element 31 | order: The polynomial order 32 | variant: The variant of the element 33 | """ 34 | from symfem import create_reference 35 | 36 | if reference.vertices != reference.reference_vertices: 37 | raise NonDefaultReferenceError() 38 | 39 | poly: typing.List[FunctionInput] = [] 40 | poly += [ 41 | (i[0] * j, i[1] * j, 0) 42 | for i in polynomial_set_vector(2, 2, order) + Hcurl_polynomials(2, 2, order + 1) 43 | for j in polynomial_set_1d(1, order + 1, x[2:]) 44 | ] 45 | poly += [ 46 | (0, 0, i * j) 47 | for i in polynomial_set_1d(2, order + 1, x[:2]) 48 | for j in polynomial_set_1d(1, order, x[2:]) 49 | ] 50 | 51 | dofs: ListOfFunctionals = make_integral_moment_dofs( 52 | reference, 53 | edges=(TangentIntegralMoment, Lagrange, order, {"variant": variant}), 54 | faces={ 55 | "triangle": ( 56 | IntegralMoment, 57 | VectorLagrange, 58 | order - 1, 59 | "covariant", 60 | {"variant": variant}, 61 | ), 62 | "quadrilateral": ( 63 | IntegralMoment, 64 | QRT, 65 | order - 1, 66 | "covariant", 67 | {"variant": variant}, 68 | ), 69 | }, 70 | ) 71 | 72 | triangle = create_reference("triangle") 73 | interval = create_reference("interval") 74 | 75 | if order >= 1: 76 | space1 = Lagrange(triangle, order - 1, variant) 77 | space2 = Lagrange(interval, order - 1, variant) 78 | 79 | for f in space1.get_basis_functions(): 80 | for g in space2.get_basis_functions(): 81 | h = f * g.subs(x[0], x[2]) 82 | dofs.append( 83 | IntegralAgainst( 84 | reference, 85 | (h, 0, 0), 86 | entity=(3, 0), 87 | mapping="covariant", 88 | ) 89 | ) 90 | dofs.append( 91 | IntegralAgainst( 92 | reference, 93 | (0, h, 0), 94 | entity=(3, 0), 95 | mapping="covariant", 96 | ) 97 | ) 98 | 99 | if order >= 2: 100 | space1 = Lagrange(triangle, order - 2, variant) 101 | space2 = Lagrange(interval, order, variant) 102 | 103 | for f in space1.get_basis_functions(): 104 | for g in space2.get_basis_functions(): 105 | dofs.append( 106 | IntegralAgainst( 107 | reference, 108 | (0, 0, f * g.subs(x[0], x[2])), 109 | entity=(3, 0), 110 | mapping="covariant", 111 | ) 112 | ) 113 | 114 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 115 | self.variant = variant 116 | 117 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 118 | """Return the kwargs used to create this element. 119 | 120 | Returns: 121 | Keyword argument dictionary 122 | """ 123 | return {"variant": self.variant} 124 | 125 | @property 126 | def lagrange_subdegree(self) -> int: 127 | return self.order 128 | 129 | @property 130 | def lagrange_superdegree(self) -> typing.Optional[int]: 131 | return self.order + 1 132 | 133 | @property 134 | def polynomial_subdegree(self) -> int: 135 | return self.order 136 | 137 | @property 138 | def polynomial_superdegree(self) -> typing.Optional[int]: 139 | return (self.order + 1) * 2 140 | 141 | names = ["Nedelec", "Ncurl"] 142 | references = ["prism"] 143 | min_order = 0 144 | continuity = "H(curl)" 145 | value_type = "vector" 146 | last_updated = "2025.05" 147 | -------------------------------------------------------------------------------- /symfem/elements/p1_macro.py: -------------------------------------------------------------------------------- 1 | """P1 macro elements. 2 | 3 | This element's definition appears in https://doi.org/10.1007/s00211-018-0970-6 4 | (Christiansen, Hu, 2018) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import IntegralAgainst, ListOfFunctionals, PointEvaluation 13 | from symfem.functions import FunctionInput 14 | from symfem.geometry import SetOfPoints 15 | from symfem.piecewise_functions import PiecewiseFunction 16 | from symfem.references import Reference 17 | from symfem.symbols import x 18 | 19 | __all__ = ["P1Macro"] 20 | 21 | 22 | class P1Macro(CiarletElement): 23 | """P1 macro finite element on a triangle.""" 24 | 25 | def __init__(self, reference: Reference, order: int): 26 | """Create the element. 27 | 28 | Args: 29 | reference: The reference element 30 | order: The polynomial order 31 | """ 32 | third = sympy.Rational(1, 3) 33 | zero = sympy.Integer(0) 34 | one = sympy.Integer(1) 35 | tris: typing.List[SetOfPoints] = [ 36 | ((zero, zero), (one, zero), (third, third)), 37 | ((one, zero), (zero, one), (third, third)), 38 | ((zero, one), (zero, zero), (third, third)), 39 | ] 40 | tris = [tuple(reference.get_point(p) for p in t) for t in tris] 41 | invmap = reference.get_inverse_map_to_self() 42 | poly: typing.List[FunctionInput] = [ 43 | PiecewiseFunction({q: 1 for q in tris}, 2), 44 | PiecewiseFunction({q: x[0] for q in tris}, 2), 45 | PiecewiseFunction({q: x[1] for q in tris}, 2), 46 | PiecewiseFunction( 47 | { 48 | tris[0]: 3 * invmap[1], 49 | tris[1]: 3 * (1 - invmap[0] - invmap[1]), 50 | tris[2]: 3 * invmap[0], 51 | }, 52 | 2, 53 | ), 54 | ] 55 | 56 | dofs: ListOfFunctionals = [] 57 | for v_n, v in enumerate(reference.vertices): 58 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 59 | dofs.append(IntegralAgainst(reference, 1, entity=(2, 0))) 60 | 61 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 62 | 63 | @property 64 | def lagrange_subdegree(self) -> int: 65 | raise NotImplementedError() 66 | 67 | @property 68 | def lagrange_superdegree(self) -> typing.Optional[int]: 69 | raise NotImplementedError() 70 | 71 | @property 72 | def polynomial_subdegree(self) -> int: 73 | raise NotImplementedError() 74 | 75 | @property 76 | def polynomial_superdegree(self) -> typing.Optional[int]: 77 | raise NotImplementedError() 78 | 79 | names = ["P1 macro"] 80 | references = ["triangle"] 81 | min_order = 1 82 | max_order = 1 83 | continuity = "C0" 84 | value_type = "scalar macro" 85 | last_updated = "2023.06" 86 | -------------------------------------------------------------------------------- /symfem/elements/rannacher_turek.py: -------------------------------------------------------------------------------- 1 | """Rannacher-Turek elements on tensor product cells. 2 | 3 | This element's definition appears in https://doi.org/10.1002/num.1690080202 4 | (Rannacher, Turek, 1992) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import ListOfFunctionals, PointEvaluation 11 | from symfem.functions import FunctionInput 12 | from symfem.references import NonDefaultReferenceError, Reference 13 | from symfem.symbols import x 14 | 15 | __all__ = ["RannacherTurek"] 16 | 17 | 18 | class RannacherTurek(CiarletElement): 19 | """Rannacher-Turek finite element.""" 20 | 21 | def __init__(self, reference: Reference, order: int): 22 | """Create the element. 23 | 24 | Args: 25 | reference: The reference element 26 | order: The polynomial order 27 | """ 28 | assert order == 1 29 | if reference.vertices != reference.reference_vertices: 30 | raise NonDefaultReferenceError() 31 | 32 | dofs: ListOfFunctionals = [] 33 | for e_n, vs in enumerate(reference.sub_entities(reference.tdim - 1)): 34 | pt = reference.sub_entity(reference.tdim - 1, e_n).midpoint() 35 | dofs.append(PointEvaluation(reference, pt, entity=(reference.tdim - 1, e_n))) 36 | 37 | poly: typing.List[FunctionInput] = [] 38 | if reference.name == "quadrilateral": 39 | poly += [1, x[0], x[1], x[0] ** 2 - x[1] ** 2] 40 | else: 41 | poly += [1, x[0], x[1], x[2], x[0] ** 2 - x[1] ** 2, x[1] ** 2 - x[2] ** 2] 42 | 43 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 44 | 45 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 46 | """Return the kwargs used to create this element. 47 | 48 | Returns: 49 | Keyword argument dictionary 50 | """ 51 | return {} 52 | 53 | @property 54 | def lagrange_subdegree(self) -> int: 55 | return 0 56 | 57 | @property 58 | def lagrange_superdegree(self) -> typing.Optional[int]: 59 | return 2 60 | 61 | @property 62 | def polynomial_subdegree(self) -> int: 63 | return 1 64 | 65 | @property 66 | def polynomial_superdegree(self) -> typing.Optional[int]: 67 | return 2 68 | 69 | names = ["Rannacher-Turek"] 70 | references = ["quadrilateral", "hexahedron"] 71 | min_order = 1 72 | max_order = 1 73 | continuity = "L2" 74 | value_type = "scalar" 75 | last_updated = "2023.05" 76 | -------------------------------------------------------------------------------- /symfem/elements/rhct.py: -------------------------------------------------------------------------------- 1 | """Reduced Hsieh-Clough-Tocher elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.2307/2006147 4 | (Ciarlet, 1978) 5 | """ 6 | 7 | import typing 8 | 9 | import sympy 10 | 11 | from symfem.finite_element import CiarletElement 12 | from symfem.functionals import DerivativePointEvaluation, ListOfFunctionals, PointEvaluation 13 | from symfem.functions import FunctionInput, ScalarFunction 14 | from symfem.piecewise_functions import PiecewiseFunction 15 | from symfem.references import NonDefaultReferenceError, Reference 16 | from symfem.symbols import x 17 | 18 | __all__ = ["ReducedHsiehCloughTocher"] 19 | 20 | 21 | class ReducedHsiehCloughTocher(CiarletElement): 22 | """Reduced Hsieh-Clough-Tocher finite element.""" 23 | 24 | def __init__(self, reference: Reference, order: int): 25 | """Create the element. 26 | 27 | Args: 28 | reference: The reference element 29 | order: The polynomial order 30 | """ 31 | assert order == 3 32 | assert reference.name == "triangle" 33 | if reference.vertices != reference.reference_vertices: 34 | raise NonDefaultReferenceError() 35 | 36 | dofs: ListOfFunctionals = [] 37 | for v_n, vs in enumerate(reference.vertices): 38 | dofs.append(PointEvaluation(reference, vs, entity=(0, v_n))) 39 | dofs.append(DerivativePointEvaluation(reference, vs, (1, 0), entity=(0, v_n))) 40 | dofs.append(DerivativePointEvaluation(reference, vs, (0, 1), entity=(0, v_n))) 41 | 42 | mid = tuple(sympy.Rational(sum(i), len(i)) for i in zip(*reference.vertices)) 43 | 44 | subs = [ 45 | (reference.vertices[0], reference.vertices[1], mid), 46 | (reference.vertices[1], reference.vertices[2], mid), 47 | (reference.vertices[2], reference.vertices[0], mid), 48 | ] 49 | 50 | piece_list = [ 51 | tuple(ScalarFunction(p) for _ in range(3)) 52 | for p in [1, x[0], x[1], x[0] ** 2, x[0] * x[1], x[1] ** 2, x[0] ** 3 - x[1] ** 3] 53 | ] 54 | piece_list.append( 55 | ( 56 | ScalarFunction( 57 | 4 * x[0] ** 3 - 3 * x[0] * x[1] ** 2 + 2 * x[0] * x[1] + 4 * x[1] ** 2 58 | ), 59 | ScalarFunction( 60 | 7 * x[0] ** 3 61 | + 12 * x[0] ** 2 * x[1] 62 | - 7 * x[0] ** 2 63 | + 9 * x[0] * x[1] ** 2 64 | - 14 * x[0] * x[1] 65 | + 5 * x[0] 66 | + 4 * x[1] 67 | - 1 68 | ), 69 | ScalarFunction(3 * x[0] ** 3 + x[0] ** 2 - 2 * x[1] ** 3 + 5 * x[1] ** 2), 70 | ) 71 | ) 72 | piece_list.append( 73 | ( 74 | ScalarFunction( 75 | 25 * x[0] ** 3 - 24 * x[0] * x[1] ** 2 + 30 * x[0] * x[1] - 24 * x[1] ** 2 76 | ), 77 | ScalarFunction( 78 | 35 * x[0] ** 3 79 | + 33 * x[0] ** 2 * x[1] 80 | - 21 * x[0] ** 2 81 | - 12 * x[0] * x[1] ** 2 82 | + 12 * x[0] 83 | - 28 * x[1] ** 3 84 | - 3 * x[1] 85 | - 1 86 | ), 87 | ScalarFunction( 88 | 3 * x[0] ** 3 89 | + 21 * x[0] ** 2 * x[1] 90 | + 15 * x[0] ** 2 91 | - 23 * x[1] ** 3 92 | - 9 * x[1] ** 2 93 | ), 94 | ) 95 | ) 96 | 97 | poly: typing.List[FunctionInput] = [] 98 | poly += [PiecewiseFunction({i: j for i, j in zip(subs, p)}, 2) for p in piece_list] 99 | poly = reference.map_polyset_from_default(poly) 100 | 101 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 102 | 103 | @property 104 | def lagrange_subdegree(self) -> int: 105 | raise NotImplementedError() 106 | 107 | @property 108 | def lagrange_superdegree(self) -> typing.Optional[int]: 109 | raise NotImplementedError() 110 | 111 | @property 112 | def polynomial_subdegree(self) -> int: 113 | raise NotImplementedError() 114 | 115 | @property 116 | def polynomial_superdegree(self) -> typing.Optional[int]: 117 | raise NotImplementedError() 118 | 119 | names = ["reduced Hsieh-Clough-Tocher", "rHCT"] 120 | references = ["triangle"] 121 | min_order = 3 122 | max_order = 3 123 | # continuity = "C1" 124 | continuity = "C0" 125 | value_type = "scalar macro" 126 | last_updated = "2023.06" 127 | -------------------------------------------------------------------------------- /symfem/elements/rt.py: -------------------------------------------------------------------------------- 1 | """Raviart-Thomas elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1007/BF01396415 4 | (Nedelec, 1980) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import Lagrange, VectorLagrange 10 | from symfem.finite_element import CiarletElement 11 | from symfem.functionals import IntegralMoment, ListOfFunctionals, NormalIntegralMoment 12 | from symfem.functions import FunctionInput 13 | from symfem.moments import make_integral_moment_dofs 14 | from symfem.polynomials import Hdiv_polynomials, polynomial_set_vector 15 | from symfem.references import Reference 16 | 17 | __all__ = ["RaviartThomas"] 18 | 19 | 20 | class RaviartThomas(CiarletElement): 21 | """Raviart-Thomas Hdiv finite element.""" 22 | 23 | def __init__(self, reference: Reference, order: int, variant: str = "equispaced"): 24 | """Create the element. 25 | 26 | Args: 27 | reference: The reference element 28 | order: The polynomial order 29 | variant: The variant of the element 30 | """ 31 | poly: typing.List[FunctionInput] = [] 32 | poly += polynomial_set_vector(reference.tdim, reference.tdim, order) 33 | poly += Hdiv_polynomials(reference.tdim, reference.tdim, order + 1) 34 | 35 | dofs: ListOfFunctionals = make_integral_moment_dofs( 36 | reference, 37 | facets=(NormalIntegralMoment, Lagrange, order, {"variant": variant}), 38 | cells=( 39 | IntegralMoment, 40 | VectorLagrange, 41 | order - 1, 42 | "contravariant", 43 | {"variant": variant}, 44 | ), 45 | ) 46 | 47 | super().__init__(reference, order, poly, dofs, reference.tdim, reference.tdim) 48 | self.variant = variant 49 | 50 | def init_kwargs(self) -> typing.Dict[str, typing.Any]: 51 | """Return the kwargs used to create this element. 52 | 53 | Returns: 54 | Keyword argument dictionary 55 | """ 56 | return {"variant": self.variant} 57 | 58 | @property 59 | def lagrange_subdegree(self) -> int: 60 | return self.order 61 | 62 | @property 63 | def lagrange_superdegree(self) -> typing.Optional[int]: 64 | return self.order + 1 65 | 66 | @property 67 | def polynomial_subdegree(self) -> int: 68 | return self.order 69 | 70 | @property 71 | def polynomial_superdegree(self) -> typing.Optional[int]: 72 | return self.order + 1 73 | 74 | names = ["Raviart-Thomas", "RT", "N1div"] 75 | references = ["triangle", "tetrahedron"] 76 | min_order = 0 77 | continuity = "H(div)" 78 | value_type = "vector" 79 | last_updated = "2025.03" 80 | -------------------------------------------------------------------------------- /symfem/elements/taylor.py: -------------------------------------------------------------------------------- 1 | """Taylor element on an interval, triangle or tetrahedron.""" 2 | 3 | import typing 4 | from itertools import product 5 | 6 | from symfem.elements.lagrange import Lagrange 7 | from symfem.finite_element import CiarletElement 8 | from symfem.functionals import DerivativePointEvaluation, IntegralMoment, ListOfFunctionals 9 | from symfem.functions import FunctionInput 10 | from symfem.moments import make_integral_moment_dofs 11 | from symfem.polynomials import polynomial_set_1d 12 | from symfem.references import Reference 13 | 14 | __all__ = ["Taylor"] 15 | 16 | 17 | class Taylor(CiarletElement): 18 | """Taylor finite element.""" 19 | 20 | def __init__(self, reference: Reference, order: int): 21 | """Create the element. 22 | 23 | Args: 24 | reference: The reference element 25 | order: The polynomial order 26 | """ 27 | dofs: ListOfFunctionals = make_integral_moment_dofs( 28 | reference, 29 | cells=(IntegralMoment, Lagrange, 0, {"variant": "equispaced"}), 30 | ) 31 | for i in product(range(order + 1), repeat=reference.tdim): 32 | if 1 <= sum(i) <= order: 33 | dofs.append( 34 | DerivativePointEvaluation( 35 | reference, reference.midpoint(), i, entity=(reference.tdim, 0) 36 | ) 37 | ) 38 | 39 | poly: typing.List[FunctionInput] = [] 40 | poly += polynomial_set_1d(reference.tdim, order) 41 | 42 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 43 | 44 | @property 45 | def lagrange_subdegree(self) -> int: 46 | return self.order 47 | 48 | @property 49 | def lagrange_superdegree(self) -> typing.Optional[int]: 50 | return self.order 51 | 52 | @property 53 | def polynomial_subdegree(self) -> int: 54 | return self.order 55 | 56 | @property 57 | def polynomial_superdegree(self) -> typing.Optional[int]: 58 | return self.order 59 | 60 | names = ["Taylor", "discontinuous Taylor"] 61 | references = ["interval", "triangle", "tetrahedron"] 62 | min_order = 0 63 | continuity = "L2" 64 | value_type = "scalar" 65 | last_updated = "2023.06" 66 | -------------------------------------------------------------------------------- /symfem/elements/vector_enriched_galerkin.py: -------------------------------------------------------------------------------- 1 | """Enriched vector Galerkin elements. 2 | 3 | This element's definition appears in https://doi.org/10.1016/j.camwa.2022.06.018 4 | (Yi, Hu, Lee, Adler, 2022) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.elements.lagrange import VectorLagrange 10 | from symfem.elements.q import VectorQ 11 | from symfem.finite_element import CiarletElement, EnrichedElement 12 | from symfem.functionals import BaseFunctional, IntegralAgainst 13 | from symfem.functions import FunctionInput, VectorFunction 14 | from symfem.references import NonDefaultReferenceError, Reference 15 | from symfem.symbols import x 16 | 17 | __all__ = ["Enrichment", "VectorEnrichedGalerkin"] 18 | 19 | 20 | class Enrichment(CiarletElement): 21 | """An LF enriched Galerkin element.""" 22 | 23 | def __init__(self, reference: Reference): 24 | """Create the element. 25 | 26 | Args: 27 | reference: The reference element 28 | """ 29 | if reference.vertices != reference.reference_vertices: 30 | raise NonDefaultReferenceError() 31 | f = VectorFunction(tuple(x[i] - j for i, j in enumerate(reference.midpoint()))) 32 | poly: typing.List[FunctionInput] = [f] 33 | size = f.dot(f).integral(reference, x) 34 | dofs: typing.List[BaseFunctional] = [ 35 | IntegralAgainst( 36 | reference, tuple(i / size for i in f), (reference.tdim, 0), "contravariant" 37 | ) 38 | ] 39 | 40 | super().__init__(reference, 1, poly, dofs, reference.tdim, reference.tdim) 41 | 42 | @property 43 | def lagrange_subdegree(self) -> int: 44 | return -1 45 | 46 | @property 47 | def lagrange_superdegree(self) -> typing.Optional[int]: 48 | return 1 49 | 50 | @property 51 | def polynomial_subdegree(self) -> int: 52 | return -1 53 | 54 | @property 55 | def polynomial_superdegree(self) -> typing.Optional[int]: 56 | return 1 57 | 58 | names: typing.List[str] = [] 59 | references = ["triangle", "quadrilateral", "tetrahedron", "hexahedron"] 60 | min_order = 1 61 | max_order = 1 62 | continuity = "C0" 63 | value_type = "vector" 64 | last_updated = "2023.05" 65 | 66 | 67 | class VectorEnrichedGalerkin(EnrichedElement): 68 | """An LF enriched Galerkin element.""" 69 | 70 | def __init__(self, reference: Reference, order: int): 71 | """Create the element. 72 | 73 | Args: 74 | reference: The reference element 75 | order: The polynomial order 76 | """ 77 | if reference.name in ["quadrilateral", "hexahedron"]: 78 | super().__init__([VectorQ(reference, order), Enrichment(reference)]) 79 | else: 80 | super().__init__([VectorLagrange(reference, order), Enrichment(reference)]) 81 | 82 | @property 83 | def lagrange_subdegree(self) -> int: 84 | return self.order 85 | 86 | @property 87 | def lagrange_superdegree(self) -> typing.Optional[int]: 88 | return self.order 89 | 90 | @property 91 | def polynomial_subdegree(self) -> int: 92 | return self.order 93 | 94 | @property 95 | def polynomial_superdegree(self) -> typing.Optional[int]: 96 | if self.reference.name in ["quadrilateral", "hexahedron"]: 97 | return self.order * self.reference.tdim 98 | return self.order 99 | 100 | names = ["enriched vector Galerkin", "locking-free enriched Galerkin", "LFEG"] 101 | references = ["triangle", "quadrilateral", "tetrahedron", "hexahedron"] 102 | min_order = 1 103 | continuity = "C0" 104 | value_type = "vector" 105 | last_updated = "2023.05" 106 | -------------------------------------------------------------------------------- /symfem/elements/wu_xu.py: -------------------------------------------------------------------------------- 1 | """Wu-Xu elements on simplices. 2 | 3 | This element's definition appears in https://doi.org/10.1090/mcom/3361 4 | (Wu, Xu, 2019) 5 | """ 6 | 7 | import typing 8 | 9 | from symfem.finite_element import CiarletElement 10 | from symfem.functionals import ( 11 | DerivativePointEvaluation, 12 | IntegralOfDirectionalMultiderivative, 13 | ListOfFunctionals, 14 | PointEvaluation, 15 | ) 16 | from symfem.functions import FunctionInput 17 | from symfem.polynomials import polynomial_set_1d 18 | from symfem.references import NonDefaultReferenceError, Reference 19 | 20 | __all__ = ["derivatives", "WuXu"] 21 | 22 | 23 | def derivatives(dim: int, order: int) -> typing.List[typing.Tuple[int, ...]]: 24 | """Return all the orders of a multidimensional derivative. 25 | 26 | Args: 27 | dim: The topological dimension 28 | order: The total derivative order 29 | 30 | Returns: 31 | List of derivative order tuples 32 | """ 33 | if dim == 1: 34 | return [(order,)] 35 | 36 | out = [] 37 | for i in range(order + 1): 38 | out += [(i,) + j for j in derivatives(dim - 1, order - i)] 39 | return out 40 | 41 | 42 | class WuXu(CiarletElement): 43 | """Wu-Xu finite element.""" 44 | 45 | def __init__(self, reference: Reference, order: int): 46 | """Create the element. 47 | 48 | Args: 49 | reference: The reference element 50 | order: The polynomial order 51 | """ 52 | if reference.name == "tetrahedron" and reference != reference.default_reference(): 53 | raise NonDefaultReferenceError() 54 | 55 | poly: typing.List[FunctionInput] = [] 56 | if reference.name == "interval": 57 | assert order == 3 58 | poly += polynomial_set_1d(reference.tdim, 2) 59 | else: 60 | assert order == reference.tdim + 1 61 | poly += polynomial_set_1d(reference.tdim, order) 62 | 63 | invmap = reference.get_inverse_map_to_self() 64 | if reference.name == "interval": 65 | bubble = invmap[0] * (1 - invmap[0]) 66 | elif reference.name == "triangle": 67 | bubble = invmap[0] * invmap[1] * (1 - invmap[0] - invmap[1]) 68 | elif reference.name == "tetrahedron": 69 | bubble = invmap[0] * invmap[1] * invmap[2] * (1 - invmap[0] - invmap[1] - invmap[2]) 70 | 71 | poly += [bubble * i for i in polynomial_set_1d(reference.tdim, 1)[1:]] 72 | 73 | dofs: ListOfFunctionals = [] 74 | for v_n, v in enumerate(reference.vertices): 75 | dofs.append(PointEvaluation(reference, v, entity=(0, v_n))) 76 | for i in range(reference.tdim): 77 | dofs.append( 78 | DerivativePointEvaluation( 79 | reference, 80 | v, 81 | tuple(1 if i == j else 0 for j in range(reference.tdim)), 82 | entity=(0, v_n), 83 | ) 84 | ) 85 | for codim in range(1, reference.tdim): 86 | dim = reference.tdim - codim 87 | for e_n, vs in enumerate(reference.sub_entities(codim=codim)): 88 | subentity = reference.sub_entity(dim, e_n) 89 | volume = subentity.jacobian() 90 | normals = [] 91 | if codim == 1: 92 | normals = [subentity.normal()] 93 | elif codim == 2 and reference.tdim == 3: 94 | for f_n, f_vs in enumerate(reference.sub_entities(2)): 95 | if vs[0] in f_vs and vs[1] in f_vs: 96 | face = reference.sub_entity(2, f_n) 97 | normals.append(face.normal()) 98 | else: 99 | raise NotImplementedError 100 | for orders in derivatives(len(normals), len(normals)): 101 | dofs.append( 102 | IntegralOfDirectionalMultiderivative( 103 | reference, tuple(normals), orders, (dim, e_n), scale=1 / volume 104 | ) 105 | ) 106 | 107 | super().__init__(reference, order, poly, dofs, reference.tdim, 1) 108 | 109 | @property 110 | def lagrange_subdegree(self) -> int: 111 | return self.order 112 | 113 | @property 114 | def lagrange_superdegree(self) -> typing.Optional[int]: 115 | if self.reference.name == "interval": 116 | return 3 117 | return self.order + 1 118 | 119 | @property 120 | def polynomial_subdegree(self) -> int: 121 | return self.order 122 | 123 | @property 124 | def polynomial_superdegree(self) -> typing.Optional[int]: 125 | if self.reference.name == "interval": 126 | return 3 127 | return self.order + 1 128 | 129 | names = ["Wu-Xu"] 130 | references = ["interval", "triangle", "tetrahedron"] 131 | min_order = {"interval": 3, "triangle": 3, "tetrahedron": 4} 132 | max_order = {"interval": 3, "triangle": 3, "tetrahedron": 4} 133 | continuity = "C0" 134 | # continuity = "C{order}" 135 | value_type = "scalar" 136 | last_updated = "2025.03" 137 | -------------------------------------------------------------------------------- /symfem/moments.py: -------------------------------------------------------------------------------- 1 | """Functions to create integral moments.""" 2 | 3 | import typing 4 | 5 | from symfem.functionals import BaseFunctional 6 | from symfem.references import Reference 7 | 8 | __all__ = ["MomentType", "SingleMomentTypeInput", "MomentTypeInput", "make_integral_moment_dofs"] 9 | 10 | MomentType = typing.Tuple[ 11 | typing.Type, typing.Type, int, typing.Union[str, None], typing.Dict[str, typing.Any] 12 | ] 13 | SingleMomentTypeInput = typing.Union[ 14 | MomentType, 15 | typing.Tuple[typing.Type, typing.Type, int, str], 16 | typing.Tuple[typing.Type, typing.Type, int, typing.Dict[str, typing.Any]], 17 | typing.Tuple[typing.Type, typing.Type, int], 18 | ] 19 | MomentTypeInput = typing.Union[SingleMomentTypeInput, typing.Dict[str, SingleMomentTypeInput]] 20 | 21 | 22 | def _extract_moment_data(moment_data: MomentTypeInput, sub_type: str) -> MomentType: 23 | """Get the information for a moment. 24 | 25 | Args: 26 | moment_data: The moment data 27 | sub_type: The subentity type 28 | 29 | Returns: 30 | The moment type, finite elment, order, mapping, and keyword arguments for the moment 31 | """ 32 | if isinstance(moment_data, dict): 33 | return _extract_moment_data(moment_data[sub_type], sub_type) 34 | 35 | mapping: typing.Union[str, None] = None 36 | if isinstance(moment_data[-1], dict): 37 | kwargs = moment_data[-1] 38 | if isinstance(moment_data[-2], str): 39 | mapping = moment_data[-2] 40 | else: 41 | kwargs = {} 42 | if isinstance(moment_data[-1], str): 43 | mapping = moment_data[-1] 44 | 45 | assert isinstance(moment_data[0], type) 46 | assert isinstance(moment_data[1], type) 47 | assert isinstance(moment_data[2], int) 48 | 49 | return moment_data[0], moment_data[1], moment_data[2], mapping, kwargs 50 | 51 | 52 | def make_integral_moment_dofs( 53 | reference: Reference, 54 | vertices: typing.Optional[MomentTypeInput] = None, 55 | edges: typing.Optional[MomentTypeInput] = None, 56 | faces: typing.Optional[MomentTypeInput] = None, 57 | volumes: typing.Optional[MomentTypeInput] = None, 58 | cells: typing.Optional[MomentTypeInput] = None, 59 | facets: typing.Optional[MomentTypeInput] = None, 60 | ridges: typing.Optional[MomentTypeInput] = None, 61 | peaks: typing.Optional[MomentTypeInput] = None, 62 | ) -> typing.List[BaseFunctional]: 63 | """Generate DOFs due to integral moments on sub entities. 64 | 65 | Args: 66 | reference: The reference cell. 67 | vertices: DOFs on dimension 0 entities. 68 | edges: DOFs on dimension 1 entities. 69 | faces: DOFs on dimension 2 entities. 70 | volumes: DOFs on dimension 3 entities. 71 | cells: DOFs on codimension 0 entities. 72 | facets: DOFs on codimension 1 entities. 73 | ridges: DOFs on codimension 2 entities. 74 | peaks: DOFs on codimension 3 entities. 75 | 76 | Returns: 77 | A list of DOFs for the element 78 | """ 79 | dofs = [] 80 | 81 | # DOFs per dimension 82 | for dim, moment_data in [ 83 | (0, vertices), 84 | (1, edges), 85 | (2, faces), 86 | (3, volumes), 87 | (reference.tdim - 3, peaks), 88 | (reference.tdim - 2, ridges), 89 | (reference.tdim - 1, facets), 90 | (reference.tdim, cells), 91 | ]: 92 | if moment_data is None: 93 | continue 94 | sub_type = reference.sub_entity_types[dim] 95 | if sub_type is None: 96 | continue 97 | assert dim > 0 98 | for i, vs in enumerate(reference.sub_entities(dim)): 99 | sub_ref = reference.sub_entity(dim, i, False) 100 | IntegralMoment, SubElement, order, mapping, kwargs = _extract_moment_data( 101 | moment_data, sub_ref.name 102 | ) 103 | m_kwargs = {} 104 | if mapping is not None: 105 | m_kwargs["mapping"] = mapping 106 | if order < SubElement.min_order: 107 | continue 108 | sub_element = SubElement(sub_ref.default_reference(), order, **kwargs) 109 | for dn, d in enumerate(sub_element.dofs): 110 | f = sub_element.get_basis_function(dn) 111 | dofs.append(IntegralMoment(reference, f, d, (dim, i), **m_kwargs)) 112 | return dofs 113 | -------------------------------------------------------------------------------- /symfem/polynomials/__init__.py: -------------------------------------------------------------------------------- 1 | """Polynomials.""" 2 | 3 | from symfem.polynomials.dual import l2_dual 4 | from symfem.polynomials.legendre import orthogonal_basis, orthonormal_basis 5 | from symfem.polynomials.lobatto import lobatto_basis, lobatto_dual_basis 6 | from symfem.polynomials.polysets import ( 7 | Hcurl_polynomials, 8 | Hcurl_quolynomials, 9 | Hcurl_serendipity, 10 | Hdiv_polynomials, 11 | Hdiv_quolynomials, 12 | Hdiv_serendipity, 13 | polynomial_set, 14 | polynomial_set_1d, 15 | polynomial_set_vector, 16 | prism_polynomial_set_1d, 17 | prism_polynomial_set_vector, 18 | pyramid_polynomial_set_1d, 19 | pyramid_polynomial_set_vector, 20 | quolynomial_set_1d, 21 | quolynomial_set_vector, 22 | serendipity_indices, 23 | serendipity_set_1d, 24 | serendipity_set_vector, 25 | ) 26 | -------------------------------------------------------------------------------- /symfem/polynomials/dual.py: -------------------------------------------------------------------------------- 1 | """Dual polynomials.""" 2 | 3 | import typing 4 | 5 | import sympy 6 | 7 | from symfem.functions import ScalarFunction 8 | 9 | __all__: typing.List[str] = [] 10 | 11 | 12 | def l2_dual(cell: str, poly: typing.List[ScalarFunction]) -> typing.List[ScalarFunction]: 13 | """Compute the L2 dual of a set of polynomials. 14 | 15 | Args: 16 | cell: The cell type 17 | poly: The set of polynomial 18 | 19 | Returns: 20 | The L2 dual polynomials 21 | """ 22 | from ..create import create_reference 23 | 24 | reference = create_reference(cell) 25 | 26 | matrix = sympy.Matrix([[(p * q).integral(reference) for q in poly] for p in poly]) 27 | minv = matrix.inv("LU") 28 | 29 | out = [] 30 | for i in range(minv.rows): 31 | f = ScalarFunction(0) 32 | for j, p in zip(minv.row(i), poly): 33 | f += j * p 34 | out.append(f) 35 | 36 | return out 37 | -------------------------------------------------------------------------------- /symfem/polynomials/lobatto.py: -------------------------------------------------------------------------------- 1 | """Lobatto polynomials.""" 2 | 3 | import typing 4 | 5 | from symfem.functions import ScalarFunction 6 | from symfem.polynomials.dual import l2_dual 7 | from symfem.polynomials.legendre import orthonormal_basis 8 | from symfem.symbols import x 9 | 10 | __all__: typing.List[str] = [] 11 | 12 | 13 | def lobatto_basis_interval(order: int) -> typing.List[ScalarFunction]: 14 | """Get Lobatto polynomials on an interval. 15 | 16 | Args: 17 | order: The maximum polynomial degree 18 | 19 | Returns: 20 | Lobatto polynomials 21 | """ 22 | legendre = orthonormal_basis("interval", order - 1, 0)[0] 23 | out = [ScalarFunction(1)] 24 | for f in legendre: 25 | out.append(f.integrate((x[0], 0, x[0]))) 26 | return out 27 | 28 | 29 | def lobatto_dual_basis_interval(order: int) -> typing.List[ScalarFunction]: 30 | """Get L2 dual of Lobatto polynomials on an interval. 31 | 32 | Args: 33 | order: The maximum polynomial degree 34 | 35 | Returns: 36 | Dual Lobatto polynomials 37 | """ 38 | return l2_dual("interval", lobatto_basis_interval(order)) 39 | 40 | 41 | def lobatto_basis( 42 | cell: str, order: int, include_endpoints: bool = True 43 | ) -> typing.List[ScalarFunction]: 44 | """Get Lobatto polynomials. 45 | 46 | Args: 47 | cell: The cell type 48 | order: The maximum polynomial degree 49 | include_endpoint: should polynomials that are non-zero on the boundary be included? 50 | 51 | Returns: 52 | Lobatto polynomials 53 | """ 54 | if cell == "interval": 55 | if include_endpoints: 56 | return lobatto_basis_interval(order) 57 | else: 58 | return lobatto_basis_interval(order)[2:] 59 | if cell == "quadrilateral": 60 | interval = lobatto_basis("interval", order, include_endpoints) 61 | return [i * j.subs(x[0], x[1]) for i in interval for j in interval] 62 | if cell == "hexahedron": 63 | interval = lobatto_basis("interval", order, include_endpoints) 64 | return [ 65 | i * j.subs(x[0], x[1]) * k.subs(x[0], x[2]) 66 | for i in interval 67 | for j in interval 68 | for k in interval 69 | ] 70 | raise NotImplementedError(f'Lobatto polynomials not implemented for cell "{cell}"') 71 | 72 | 73 | def lobatto_dual_basis( 74 | cell: str, order: int, include_endpoints: bool = True 75 | ) -> typing.List[ScalarFunction]: 76 | """Get L2 dual of Lobatto polynomials. 77 | 78 | Args: 79 | cell: The cell type 80 | order: The maximum polynomial degree 81 | include_endpoint: should polynomials that are non-zero on the boundary be included? 82 | 83 | Returns: 84 | Lobatto polynomials 85 | """ 86 | if cell == "interval": 87 | if include_endpoints: 88 | return lobatto_dual_basis_interval(order) 89 | else: 90 | return lobatto_dual_basis_interval(order)[2:] 91 | if cell == "quadrilateral": 92 | interval = lobatto_dual_basis("interval", order, include_endpoints) 93 | return [i * j.subs(x[0], x[1]) for i in interval for j in interval] 94 | if cell == "hexahedron": 95 | interval = lobatto_dual_basis("interval", order, include_endpoints) 96 | return [ 97 | i * j.subs(x[0], x[1]) * k.subs(x[0], x[2]) 98 | for i in interval 99 | for j in interval 100 | for k in interval 101 | ] 102 | raise NotImplementedError(f'Lobatto polynomials not implemented for cell "{cell}"') 103 | -------------------------------------------------------------------------------- /symfem/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscroggs/symfem/aad96d4302bb092a6df1130279073957eb03fcc1/symfem/py.typed -------------------------------------------------------------------------------- /symfem/symbols.py: -------------------------------------------------------------------------------- 1 | """Symbols.""" 2 | 3 | import typing 4 | 5 | import sympy 6 | 7 | __all__ = ["x", "t", "AxisVariablesNotSingle", "AxisVariables"] 8 | 9 | x = (sympy.Symbol("x"), sympy.Symbol("y"), sympy.Symbol("z")) 10 | t = (sympy.Symbol("t0"), sympy.Symbol("t1"), sympy.Symbol("t2")) 11 | 12 | AxisVariablesNotSingle = typing.Union[ 13 | typing.Tuple[sympy.core.symbol.Symbol, ...], typing.List[sympy.core.symbol.Symbol] 14 | ] 15 | AxisVariables = typing.Union[AxisVariablesNotSingle, sympy.core.symbol.Symbol] 16 | -------------------------------------------------------------------------------- /symfem/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions.""" 2 | 3 | import typing 4 | 5 | import sympy 6 | 7 | from symfem.functions import ScalarFunction 8 | 9 | __all__ = ["allequal"] 10 | 11 | 12 | def allequal(a: typing.Any, b: typing.Any) -> bool: 13 | """Test if two items that may be nested lists/tuples are equal. 14 | 15 | Args: 16 | a: The first item 17 | b: The second item 18 | 19 | Returns: 20 | a == b? 21 | """ 22 | if isinstance(a, (tuple, list)) and isinstance(b, (tuple, list)): 23 | for i, j in zip(a, b): 24 | if not allequal(i, j): 25 | return False 26 | return True 27 | if isinstance(a, sympy.core.expr.Expr): 28 | a = ScalarFunction(a) 29 | if isinstance(b, sympy.core.expr.Expr): 30 | a = ScalarFunction(b) 31 | return a == b 32 | -------------------------------------------------------------------------------- /symfem/version.py: -------------------------------------------------------------------------------- 1 | """Version number.""" 2 | 3 | version = "2025.3.1" 4 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | """Symfem unit tests.""" 2 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | """Define fixtures and other test helpers.""" 2 | 3 | import pytest 4 | 5 | 6 | def pytest_addoption(parser): 7 | """Add parser options.""" 8 | parser.addoption("--elements-to-test", action="store", default="ALL") 9 | parser.addoption("--cells-to-test", action="store", default="ALL") 10 | parser.addoption("--has-basix", action="store", default="0") 11 | parser.addoption("--speed", action="store", default="slow") 12 | 13 | 14 | @pytest.fixture 15 | def elements_to_test(request): 16 | """Get list of elements to include in tests.""" 17 | data = request.config.getoption("--elements-to-test") 18 | if data == "ALL": 19 | return "ALL" 20 | return data.split(",") 21 | 22 | 23 | @pytest.fixture 24 | def speed(request): 25 | """Get test speed.""" 26 | return request.config.getoption("--speed") 27 | 28 | 29 | @pytest.fixture 30 | def cells_to_test(request): 31 | """Get list of cells to include in tests.""" 32 | data = request.config.getoption("--cells-to-test") 33 | if data == "ALL": 34 | return "ALL" 35 | return data.split(",") 36 | 37 | 38 | @pytest.fixture 39 | def has_basix(request): 40 | """Get has-basix flag.""" 41 | data = request.config.getoption("--has-basix") 42 | return data == "1" 43 | -------------------------------------------------------------------------------- /test/test_against_computed_by_hand.py: -------------------------------------------------------------------------------- 1 | """Test that basis functions agree with examples computed by hand.""" 2 | 3 | import sympy 4 | 5 | from symfem import create_element 6 | from symfem.functions import ScalarFunction 7 | from symfem.symbols import x 8 | from symfem.utils import allequal 9 | 10 | 11 | def test_lagrange(): 12 | space = create_element("triangle", "Lagrange", 1) 13 | assert allequal( 14 | space.tabulate_basis([[0, 0], [0, 1], [1, 0]]), 15 | ((1, 0, 0), (0, 0, 1), (0, 1, 0)), 16 | ) 17 | 18 | 19 | def test_nedelec(): 20 | space = create_element("triangle", "Nedelec", 0) 21 | assert allequal( 22 | space.tabulate_basis([[0, 0], [1, 0], [0, 1]], "xxyyzz"), 23 | ((0, 0, 1, 0, 1, 0), (0, 0, 1, 1, 0, 1), (-1, 1, 0, 0, 1, 0)), 24 | ) 25 | 26 | 27 | def test_rt(): 28 | space = create_element("triangle", "Raviart-Thomas", 0) 29 | assert allequal( 30 | space.tabulate_basis([[0, 0], [1, 0], [0, 1]], "xxyyzz"), 31 | ((0, -1, 0, 0, 0, 1), (-1, 0, -1, 0, 0, 1), (0, -1, 0, -1, 1, 0)), 32 | ) 33 | 34 | 35 | def test_Q(): 36 | space = create_element("quadrilateral", "Q", 1) 37 | assert allequal( 38 | space.tabulate_basis([[0, 0], [1, 0], [0, 1], [1, 1]]), 39 | ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)), 40 | ) 41 | 42 | 43 | def test_dual0(): 44 | space = create_element("dual polygon(4)", "dual", 0) 45 | q = sympy.Rational(1, 4) 46 | assert allequal( 47 | space.tabulate_basis([[q, q], [-q, q], [-q, -q], [q, -q]]), ((1,), (1,), (1,), (1,)) 48 | ) 49 | 50 | 51 | def test_dual1(): 52 | space = create_element("dual polygon(4)", "dual", 1) 53 | h = sympy.Rational(1, 2) 54 | q = sympy.Rational(1, 4) 55 | e = sympy.Rational(1, 8) 56 | assert allequal( 57 | space.tabulate_basis([[0, 0], [q, q], [h, 0]]), 58 | ( 59 | (q, q, q, q), 60 | (sympy.Rational(5, 8), e, e, e), 61 | (sympy.Rational(3, 8), e, e, sympy.Rational(3, 8)), 62 | ), 63 | ) 64 | 65 | 66 | def test_lagrange_pyramid(): 67 | space = create_element("pyramid", "Lagrange", 1) 68 | x_i = x[0] / (1 - x[2]) 69 | y_i = x[1] / (1 - x[2]) 70 | z_i = x[2] / (1 - x[2]) 71 | basis = [ 72 | ScalarFunction(f) 73 | for f in [ 74 | (1 - x_i) * (1 - y_i) / (1 + z_i), 75 | x_i * (1 - y_i) / (1 + z_i), 76 | (1 - x_i) * y_i / (1 + z_i), 77 | x_i * y_i / (1 + z_i), 78 | z_i / (1 + z_i), 79 | ] 80 | ] 81 | assert allequal(basis, space.get_basis_functions()) 82 | 83 | basis = [ 84 | ScalarFunction(f) 85 | for f in [ 86 | (1 - x[0] - x[2]) * (1 - x[1] - x[2]) / (1 - x[2]), 87 | x[0] * (1 - x[1] - x[2]) / (1 - x[2]), 88 | (1 - x[0] - x[2]) * x[1] / (1 - x[2]), 89 | x[0] * x[1] / (1 - x[2]), 90 | x[2], 91 | ] 92 | ] 93 | assert allequal(basis, space.get_basis_functions()) 94 | -------------------------------------------------------------------------------- /test/test_alfeld_sorokina.py: -------------------------------------------------------------------------------- 1 | """Test Alfeld-Sorokina elements.""" 2 | 3 | import sympy 4 | 5 | import symfem 6 | from symfem.symbols import t, x 7 | from symfem.utils import allequal 8 | 9 | half = sympy.Rational(1, 2) 10 | 11 | 12 | def test_continuity(): 13 | e = symfem.create_element("triangle", "Alfeld-Sorokina", 2) 14 | for f in e.get_polynomial_basis(): 15 | # edge from (1,0) to (1/3,1/3) 16 | f1 = f.get_piece((half, 0)) 17 | f2 = f.get_piece((half, half)) 18 | div_f1 = f1.div() 19 | div_f2 = f2.div() 20 | line = (1 - 2 * t[0], t[0]) 21 | f1 = f1.subs(x[:2], line) 22 | f2 = f2.subs(x[:2], line) 23 | div_f1 = div_f1.subs(x[:2], line) 24 | div_f2 = div_f2.subs(x[:2], line) 25 | assert allequal(f1, f2) 26 | assert allequal(div_f1, div_f2) 27 | 28 | # edge from (0,1) to (1/3,1/3) 29 | f1 = f.get_piece((half, half)) 30 | f2 = f.get_piece((0, half)) 31 | div_f1 = f1.div() 32 | div_f2 = f2.div() 33 | line = (t[0], 1 - 2 * t[0]) 34 | f1 = f1.subs(x[:2], line) 35 | f2 = f2.subs(x[:2], line) 36 | div_f1 = div_f1.subs(x[:2], line) 37 | div_f2 = div_f2.subs(x[:2], line) 38 | assert allequal(f1, f2) 39 | assert allequal(div_f1, div_f2) 40 | 41 | # edge from (0,0) to (1/3,1/3) 42 | f1 = f.get_piece((0, half)) 43 | f2 = f.get_piece((half, 0)) 44 | div_f1 = f1.div() 45 | div_f2 = f2.div() 46 | line = (t[0], t[0]) 47 | f1 = f1.subs(x[:2], line) 48 | f2 = f2.subs(x[:2], line) 49 | div_f1 = div_f1.subs(x[:2], line) 50 | div_f2 = div_f2.subs(x[:2], line) 51 | assert allequal(f1, f2) 52 | assert allequal(div_f1, div_f2) 53 | -------------------------------------------------------------------------------- /test/test_arnold_winther.py: -------------------------------------------------------------------------------- 1 | """Test Arnold-Winther element.""" 2 | 3 | import pytest 4 | 5 | from symfem import create_element 6 | from symfem.symbols import x 7 | 8 | 9 | @pytest.mark.parametrize("order", range(3, 7)) 10 | def test_create(order): 11 | create_element("triangle", "Arnold-Winther", order) 12 | 13 | 14 | def test_nc_polyset(): 15 | e = create_element("triangle", "nonconforming AW", 1) 16 | 17 | for p in e.get_polynomial_basis(): 18 | assert p[1, 1].as_sympy().subs(x[1], 0).diff(x[1]).diff(x[1]) == 0 19 | assert p[0, 0].as_sympy().subs(x[0], 0).diff(x[0]).diff(x[0]) == 0 20 | assert ( 21 | p[0, 0].as_sympy() + p[0, 1].as_sympy() + p[1, 0].as_sympy() + p[1, 1].as_sympy() 22 | ).subs(x[0], 1 - x[1]).expand().diff(x[1]).diff(x[1]) == 0 23 | -------------------------------------------------------------------------------- /test/test_bell.py: -------------------------------------------------------------------------------- 1 | """Test Bell elements.""" 2 | 3 | import symfem 4 | from symfem.symbols import t, x 5 | 6 | 7 | def test_bell_polyset(): 8 | b = symfem.create_element("triangle", "Bell", 4) 9 | for p in b.get_polynomial_basis(): 10 | gradp = [p.diff(x[0]), p.diff(x[1])] 11 | for en in range(b.reference.sub_entity_count(1)): 12 | edge = b.reference.sub_entity(1, en) 13 | variables = [o + sum(a[i] * t[0] for a in edge.axes) for i, o in enumerate(edge.origin)] 14 | n = edge.normal() 15 | normal_deriv = gradp[0] * n[0] + gradp[1] * n[1] 16 | normal_deriv = normal_deriv.subs(x, variables) 17 | assert normal_deriv.diff(t[0]).diff(t[0]).diff(t[0]).diff(t[0]) == 0 18 | -------------------------------------------------------------------------------- /test/test_bernstein.py: -------------------------------------------------------------------------------- 1 | """Test Bernstein elements.""" 2 | 3 | import pytest 4 | 5 | import symfem 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "celltype, degree", 10 | [(c, i) for c, n in [("interval", 4), ("triangle", 3), ("tetrahedron", 2)] for i in range(n)], 11 | ) 12 | def test_bernstein(celltype, degree): 13 | b = symfem.create_element(celltype, "Bernstein", degree) 14 | poly = symfem.elements.bernstein.bernstein_polynomials(degree, b.reference.tdim) 15 | 16 | for f in b.get_basis_functions(): 17 | for p in poly: 18 | if f == p: 19 | poly.remove(p) 20 | break 21 | else: 22 | raise ValueError(f"{f} is not a Bernstein polynomial.") 23 | 24 | assert len(poly) == 0 25 | -------------------------------------------------------------------------------- /test/test_caching.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sympy 3 | 4 | import symfem 5 | from symfem.utils import allequal 6 | 7 | 8 | def test_cache(): 9 | e = symfem.create_element("triangle", "Lagrange", 2) 10 | 11 | m1 = e.get_dual_matrix(caching=False, inverse=True) 12 | 13 | def a(*args, **kwargs): 14 | raise RuntimeError() 15 | 16 | oldinv = sympy.Matrix.inv 17 | sympy.Matrix.inv = a 18 | 19 | with pytest.raises(RuntimeError): 20 | e.get_dual_matrix(caching=False, inverse=True) 21 | 22 | m2 = e.get_dual_matrix(inverse=True) 23 | 24 | sympy.Matrix.inv = oldinv 25 | 26 | assert allequal(m1, m2) 27 | -------------------------------------------------------------------------------- /test/test_dof_descriptions.py: -------------------------------------------------------------------------------- 1 | """Test DOF descriptions.""" 2 | 3 | import pytest 4 | 5 | from symfem import create_element 6 | from symfem.finite_element import CiarletElement 7 | 8 | from .utils import test_elements 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("cell_type", "element_type", "order", "kwargs"), 13 | [ 14 | [reference, element, order, kwargs] 15 | for reference, i in test_elements.items() 16 | for element, j in i.items() 17 | for kwargs, k in j 18 | for order in k 19 | ], 20 | ) 21 | def test_element(elements_to_test, cells_to_test, cell_type, element_type, order, kwargs, speed): 22 | """Run tests for each element.""" 23 | if elements_to_test != "ALL" and element_type not in elements_to_test: 24 | pytest.skip() 25 | if cells_to_test != "ALL" and cell_type not in cells_to_test: 26 | pytest.skip() 27 | if speed == "fast": 28 | if order > 2: 29 | pytest.skip() 30 | if order == 2 and cell_type in ["tetrahedron", "hexahedron", "prism", "pyramid"]: 31 | pytest.skip() 32 | 33 | element = create_element(cell_type, element_type, order, **kwargs) 34 | if isinstance(element, CiarletElement): 35 | for d in element.dofs: 36 | print(d.get_tex()[0]) 37 | -------------------------------------------------------------------------------- /test/test_elements.py: -------------------------------------------------------------------------------- 1 | """Test every element.""" 2 | 3 | import pytest 4 | 5 | import symfem 6 | from symfem import create_element 7 | from symfem.functions import VectorFunction 8 | from symfem.symbols import x 9 | from symfem.utils import allequal 10 | 11 | from .utils import test_elements 12 | 13 | 14 | def test_all_tested(): 15 | for e in symfem.create._elementlist: 16 | for r in e.references: 17 | if r == "dual polygon": 18 | continue 19 | for n in e.names: 20 | if n in test_elements[r]: 21 | break 22 | else: 23 | raise ValueError(f"{e.names[0]} on a {r} is not tested") 24 | 25 | 26 | def test_caching(): 27 | for e in symfem.create._elementlist: 28 | assert e.cache 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "ref, element, order", [("triangle", "Hermite", 4), ("tetrahedron", "Crouzeix-Raviart", 2)] 33 | ) 34 | def test_too_high_order(ref, element, order): 35 | with pytest.raises(ValueError): 36 | symfem.create_element(ref, element, order) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "ref, element, order", [("triangle", "Hermite", 2), ("tetrahedron", "bubble", 3)] 41 | ) 42 | def test_too_low_order(ref, element, order): 43 | with pytest.raises(ValueError): 44 | symfem.create_element(ref, element, order) 45 | 46 | 47 | @pytest.mark.parametrize( 48 | ("cell_type", "element_type", "order", "kwargs"), 49 | [ 50 | [reference, element, order, kwargs] 51 | for reference, i in test_elements.items() 52 | for element, j in i.items() 53 | for kwargs, k in j 54 | for order in k 55 | ], 56 | ) 57 | def test_element(elements_to_test, cells_to_test, cell_type, element_type, order, kwargs, speed): 58 | """Run tests for each element.""" 59 | if elements_to_test != "ALL" and element_type not in elements_to_test: 60 | pytest.skip() 61 | if cells_to_test != "ALL" and cell_type not in cells_to_test: 62 | pytest.skip() 63 | if speed == "fast": 64 | if order > 2: 65 | pytest.skip() 66 | if order == 2 and cell_type in ["tetrahedron", "hexahedron", "prism", "pyramid"]: 67 | pytest.skip() 68 | 69 | space = create_element(cell_type, element_type, order, **kwargs) 70 | space.test() 71 | 72 | 73 | @pytest.mark.parametrize("n_tri", [3, 4, 6, 8]) 74 | @pytest.mark.parametrize("order", range(2)) 75 | def test_dual_elements(elements_to_test, cells_to_test, n_tri, order): 76 | if elements_to_test != "ALL" and "dual" not in elements_to_test: 77 | pytest.skip() 78 | if cells_to_test != "ALL" and "dual polygon" not in cells_to_test: 79 | pytest.skip() 80 | 81 | space = create_element(f"dual polygon({n_tri})", "dual", order) 82 | sub_e = create_element("triangle", space.fine_space, space.order) 83 | for f, coeff_list in zip(space.get_basis_functions(), space.dual_coefficients): 84 | for piece, coeffs in zip(f.pieces.items(), coeff_list): 85 | fwd_map = VectorFunction(sub_e.reference.get_map_to(piece[0])) 86 | for dof, value in zip(sub_e.dofs, coeffs): 87 | point = fwd_map.subs(x, dof.point) 88 | assert allequal(value, piece[1].subs(x, point)) 89 | 90 | 91 | @pytest.mark.parametrize("n_tri", [3, 4]) 92 | @pytest.mark.parametrize("element_type", ["BC", "RBC"]) 93 | def test_bc_elements(elements_to_test, cells_to_test, n_tri, element_type): 94 | if elements_to_test != "ALL" and element_type not in elements_to_test: 95 | pytest.skip() 96 | if cells_to_test != "ALL" and "dual polygon" not in cells_to_test: 97 | pytest.skip() 98 | 99 | create_element(f"dual polygon({n_tri})", element_type, 0) 100 | 101 | 102 | @pytest.mark.parametrize( 103 | ("cell_type", "element_type", "order", "kwargs"), 104 | [ 105 | [reference, element, order, kwargs] 106 | for reference, i in test_elements.items() 107 | for element, j in i.items() 108 | for kwargs, k in j 109 | for order in k 110 | ], 111 | ) 112 | def test_degree(elements_to_test, cells_to_test, cell_type, element_type, order, kwargs, speed): 113 | """Check that polynomial subdegree is the canonical degree of every element where possible.""" 114 | if elements_to_test != "ALL" and element_type not in elements_to_test: 115 | pytest.skip() 116 | if cells_to_test != "ALL" and cell_type not in cells_to_test: 117 | pytest.skip() 118 | if speed == "fast": 119 | if order > 2: 120 | pytest.skip() 121 | if order == 2 and cell_type in ["tetrahedron", "hexahedron", "prism", "pyramid"]: 122 | pytest.skip() 123 | 124 | if element_type in ["bubble", "transition"]: 125 | pytest.xfail("Polynomial subdegree not used for this element") 126 | 127 | element = create_element(cell_type, element_type, order, **kwargs) 128 | try: 129 | poly_sub = element.polynomial_subdegree 130 | except NotImplementedError: 131 | pytest.xfail("Polynomial subdegree not implemented for this element") 132 | 133 | assert element.order == poly_sub 134 | -------------------------------------------------------------------------------- /test/test_hct.py: -------------------------------------------------------------------------------- 1 | """Test Hsieh-Clough-Tocher elements.""" 2 | 3 | import pytest 4 | import sympy 5 | 6 | import symfem 7 | from symfem.functions import ScalarFunction 8 | from symfem.symbols import t, x 9 | from symfem.utils import allequal 10 | 11 | half = sympy.Rational(1, 2) 12 | 13 | 14 | @pytest.mark.parametrize("family", ["HCT", "rHCT"]) 15 | def test_c1_continuity(family): 16 | e = symfem.create_element("triangle", family, 3) 17 | for f in e.get_polynomial_basis(): 18 | # edge from (1,0) to (1/3,1/3) 19 | f1 = f.get_piece((half, 0)) 20 | f2 = f.get_piece((half, half)) 21 | grad_f1 = f1.grad(2) 22 | grad_f2 = f2.grad(2) 23 | line = (1 - 2 * t[0], t[0]) 24 | f1 = f1.subs(x[:2], line) 25 | f2 = f2.subs(x[:2], line) 26 | grad_f1 = grad_f1.subs(x[:2], line) 27 | grad_f2 = grad_f2.subs(x[:2], line) 28 | assert allequal(f1, f2) 29 | assert allequal(grad_f1, grad_f2) 30 | 31 | # edge from (0,1) to (1/3,1/3) 32 | f1 = f.get_piece((half, half)) 33 | f2 = f.get_piece((0, half)) 34 | grad_f1 = f1.grad(2) 35 | grad_f2 = f2.grad(2) 36 | line = (t[0], 1 - 2 * t[0]) 37 | f1 = f1.subs(x[:2], line) 38 | f2 = f2.subs(x[:2], line) 39 | grad_f1 = grad_f1.subs(x[:2], line) 40 | grad_f2 = grad_f2.subs(x[:2], line) 41 | assert allequal(f1, f2) 42 | assert allequal(grad_f1, grad_f2) 43 | 44 | # edge from (0,0) to (1/3,1/3) 45 | f1 = f.get_piece((0, half)) 46 | f2 = f.get_piece((half, 0)) 47 | grad_f1 = f1.grad(2) 48 | grad_f2 = f2.grad(2) 49 | line = (t[0], t[0]) 50 | f1 = f1.subs(x[:2], line) 51 | f2 = f2.subs(x[:2], line) 52 | grad_f1 = grad_f1.subs(x[:2], line) 53 | grad_f2 = grad_f2.subs(x[:2], line) 54 | assert allequal(f1, f2) 55 | assert allequal(grad_f1, grad_f2) 56 | 57 | 58 | def test_rcht_linear_normal_derivatives(): 59 | e = symfem.create_element("triangle", "rHCT", 3) 60 | for f in e.get_polynomial_basis(): 61 | # Check that normal derivatives are linear 62 | f1 = f.get_piece((half, 0)).diff(x[1]).subs(x[1], 0) 63 | f2 = f.get_piece((half, half)) 64 | f2 = (f2.diff(x[0]) + f2.diff(x[1])).subs(x[1], 1 - x[0]) 65 | f3 = f.get_piece((0, half)).diff(x[0]).subs(x[0], 0) 66 | assert f1.diff(x[0]).diff(x[0]) == 0 67 | assert f2.diff(x[0]).diff(x[0]) == 0 68 | assert f3.diff(x[1]).diff(x[1]) == 0 69 | 70 | 71 | def test_rhct_integral(): 72 | element = symfem.create_element("triangle", "rHCT", 3) 73 | ref = element.reference 74 | f1 = element.get_basis_function(1).directional_derivative((1, 0)) 75 | f2 = element.get_basis_function(6).directional_derivative((1, 0)) 76 | 77 | integrand = f1 * f2 78 | third = sympy.Rational(1, 3) 79 | integrand.pieces[((0, 0), (1, 0), (third, third))] = ScalarFunction(0) 80 | integrand.pieces[((1, 0), (0, 1), (third, third))] = ScalarFunction(0) 81 | 82 | expr = integrand.pieces[((0, 1), (0, 0), (third, third))].as_sympy() 83 | assert len(integrand.pieces) == 3 84 | 85 | assert sympy.integrate( 86 | sympy.integrate(expr, (x[1], x[0], 1 - 2 * x[0])), (x[0], 0, third) 87 | ) == integrand.integral(ref, x) 88 | -------------------------------------------------------------------------------- /test/test_hellan_herrmann_johnson.py: -------------------------------------------------------------------------------- 1 | """Test Hellan-Herrmann-Johnson element.""" 2 | 3 | import pytest 4 | 5 | from symfem import create_element 6 | from symfem.functions import VectorFunction 7 | from symfem.symbols import x 8 | from symfem.utils import allequal 9 | 10 | 11 | @pytest.mark.parametrize("reference", ["triangle", "tetrahedron"]) 12 | @pytest.mark.parametrize("order", [0, 1, 2]) 13 | def test_create(reference, order): 14 | element = create_element(reference, "HHJ", order) 15 | 16 | # Get the basis functions associated with the interior 17 | basis = element.get_basis_functions() 18 | functions = [basis[d] for d in element.entity_dofs(element.reference.tdim, 0)] 19 | 20 | if reference == "triangle": 21 | # Check that these tensor functions have zero normal normal component on each edge 22 | for f in functions: 23 | M, n = f.subs(x[0], 1 - x[1]), VectorFunction((1, 1)) 24 | assert allequal((M @ n).dot(n), 0) 25 | M, n = f.subs(x[0], 0), VectorFunction((-1, 0)) 26 | assert allequal((M @ n).dot(n), 0) 27 | M, n = f.subs(x[1], 0), VectorFunction((0, -1)) 28 | assert allequal((M @ n).dot(n), 0) 29 | 30 | if reference == "tetrahedron": 31 | # Check that these tensor functions have zero normal normal component on each face 32 | for f in functions: 33 | M, n = f.subs(x[0], 1 - x[1] - x[2]), VectorFunction((1, 1, 1)) 34 | assert allequal((M @ n).dot(n), 0) 35 | M, n = f.subs(x[0], 0), VectorFunction((-1, 0, 0)) 36 | assert allequal((M @ n).dot(n), 0) 37 | M, n = f.subs(x[1], 0), VectorFunction((0, -1, 0)) 38 | assert allequal((M @ n).dot(n), 0) 39 | M, n = f.subs(x[2], 0), VectorFunction((0, 0, -1)) 40 | assert allequal((M @ n).dot(n), 0) 41 | -------------------------------------------------------------------------------- /test/test_lagrange_polynomial_variants.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import symfem 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "cell, order", 8 | [ 9 | (c, o) 10 | for c, m in [ 11 | ("interval", 5), 12 | ("triangle", 4), 13 | ("quadrilateral", 4), 14 | ("tetrahedron", 3), 15 | ("hexahedron", 3), 16 | ("prism", 3), 17 | ("pyramid", 3), 18 | ] 19 | for o in range(m) 20 | ], 21 | ) 22 | def test_legendre(cell, order): 23 | basis = symfem.polynomials.orthonormal_basis(cell, order, 0)[0] 24 | e = symfem.create_element(cell, "P", order, variant="legendre") 25 | assert symfem.utils.allequal(e.get_basis_functions(), basis) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "cell, order", 30 | [ 31 | (c, o) 32 | for c, m in [("interval", 5), ("quadrilateral", 4), ("hexahedron", 3)] 33 | for o in range(m) 34 | ], 35 | ) 36 | def test_lobatto(cell, order): 37 | basis = symfem.polynomials.lobatto_basis(cell, order, False) 38 | e = symfem.create_element(cell, "P", order, variant="lobatto") 39 | assert symfem.utils.allequal(e.get_basis_functions()[-len(basis) :], basis) 40 | -------------------------------------------------------------------------------- /test/test_mapping.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import symfem 4 | from symfem.mappings import MappingNotImplemented 5 | from symfem.references import NonDefaultReferenceError 6 | from symfem.utils import allequal 7 | 8 | from .utils import test_elements 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("cell_type", "element_type", "order", "kwargs"), 13 | [ 14 | [reference, element, order, kwargs] 15 | for reference, i in test_elements.items() 16 | for element, j in i.items() 17 | for kwargs, k in j 18 | for order in k 19 | ], 20 | ) 21 | def test_push_forward( 22 | elements_to_test, cells_to_test, cell_type, element_type, order, kwargs, speed 23 | ): 24 | if elements_to_test != "ALL" and element_type not in elements_to_test: 25 | pytest.skip() 26 | if cells_to_test != "ALL" and cell_type not in cells_to_test: 27 | pytest.skip() 28 | if speed == "fast": 29 | if order > 2: 30 | pytest.skip() 31 | if order == 2 and cell_type in ["tetrahedron", "hexahedron", "prism", "pyramid"]: 32 | pytest.skip() 33 | 34 | if cell_type == "interval": 35 | vertices = [(3,), (1,)] 36 | elif cell_type == "triangle": 37 | vertices = [(1, 1), (2, 2), (1, 4)] 38 | elif cell_type == "quadrilateral": 39 | vertices = [(1, 1), (2, 2), (1, 3), (2, 4)] 40 | elif cell_type == "tetrahedron": 41 | vertices = [(1, 1, 1), (2, 2, 2), (-1, 3, 2), (4, 0, 0)] 42 | elif cell_type == "hexahedron": 43 | vertices = [ 44 | (1, 1, 1), 45 | (2, 2, 2), 46 | (-1, 3, 2), 47 | (0, 4, 3), 48 | (4, 0, 0), 49 | (5, 1, 1), 50 | (2, 2, 1), 51 | (3, 3, 2), 52 | ] 53 | elif cell_type == "prism": 54 | vertices = [(1, 1, 1), (2, 2, 1), (1, 4, 2), (0, 1, 1), (1, 2, 1), (0, 4, 2)] 55 | elif cell_type == "pyramid": 56 | vertices = [(1, 1, 0), (2, 2, 0), (1, 3, 1), (2, 4, 1), (-1, -1, -1)] 57 | 58 | else: 59 | raise ValueError(f"Unsupported cell type: {cell_type}") 60 | 61 | try: 62 | e2 = symfem.create_element(cell_type, element_type, order, vertices=vertices, **kwargs) 63 | except NonDefaultReferenceError: 64 | pytest.xfail("Cannot create element on non-default reference.") 65 | e = symfem.create_element(cell_type, element_type, order, **kwargs) 66 | 67 | try: 68 | assert allequal(e.map_to_cell(vertices), e2.get_basis_functions()) 69 | except MappingNotImplemented: 70 | pytest.xfail("Mapping not implemented for this element.") 71 | 72 | 73 | @pytest.mark.parametrize( 74 | "name, inverse, transpose, mapping", 75 | [ 76 | ("identity", False, False, symfem.mappings.identity), 77 | ("l2", False, False, symfem.mappings.l2), 78 | ("covariant", False, False, symfem.mappings.covariant), 79 | ("contravariant", False, False, symfem.mappings.contravariant), 80 | ("double_covariant", False, False, symfem.mappings.double_covariant), 81 | ("double_contravariant", False, False, symfem.mappings.double_contravariant), 82 | ("identity", True, True, symfem.mappings.identity_inverse_transpose), 83 | ("l2", True, True, symfem.mappings.l2_inverse_transpose), 84 | ("covariant", True, True, symfem.mappings.covariant_inverse_transpose), 85 | ("contravariant", True, True, symfem.mappings.contravariant_inverse_transpose), 86 | ("identity", True, False, symfem.mappings.identity_inverse), 87 | ("l2", True, False, symfem.mappings.l2_inverse), 88 | ("covariant", True, False, symfem.mappings.covariant_inverse), 89 | ("contravariant", True, False, symfem.mappings.contravariant_inverse), 90 | ("double_covariant", True, False, symfem.mappings.double_covariant_inverse), 91 | ("double_contravariant", True, False, symfem.mappings.double_contravariant_inverse), 92 | ], 93 | ) 94 | def test_get_mapping(name, inverse, transpose, mapping): 95 | assert symfem.mappings.get_mapping(name, inverse=inverse, transpose=transpose) == mapping 96 | -------------------------------------------------------------------------------- /test/test_nedelec.py: -------------------------------------------------------------------------------- 1 | """Test Nedelec elements.""" 2 | 3 | import sympy 4 | 5 | from symfem import create_element 6 | from symfem.symbols import x 7 | 8 | 9 | def test_nedelec_2d(): 10 | space = create_element("triangle", "Nedelec", 0) 11 | k = sympy.Symbol("k") 12 | 13 | tdim = 2 14 | 15 | for i, edge in enumerate([((1, 0), (0, 1)), ((0, 0), (0, 1)), ((0, 0), (1, 0))]): 16 | for j, f in enumerate(space.get_basis_functions()): 17 | norm = sympy.sqrt(sum((edge[0][i] - edge[1][i]) ** 2 for i in range(tdim))) 18 | tangent = tuple((edge[1][i] - edge[0][i]) / norm for i in range(tdim)) 19 | line = sympy.Curve( 20 | [(1 - k) * edge[0][i] + k * edge[1][i] for i in range(tdim)], (k, 0, 1) 21 | ) 22 | 23 | result = sympy.line_integrate(f.dot(tangent), line, x[:tdim]) 24 | if i == j: 25 | assert result == 1 26 | else: 27 | assert result == 0 28 | 29 | 30 | def test_nedelec_3d(): 31 | space = create_element("tetrahedron", "Nedelec", 0) 32 | k = sympy.Symbol("k") 33 | 34 | tdim = 3 35 | 36 | for i, edge in enumerate( 37 | [ 38 | ((0, 1, 0), (0, 0, 1)), 39 | ((1, 0, 0), (0, 0, 1)), 40 | ((1, 0, 0), (0, 1, 0)), 41 | ((0, 0, 0), (0, 0, 1)), 42 | ((0, 0, 0), (0, 1, 0)), 43 | ((0, 0, 0), (1, 0, 0)), 44 | ] 45 | ): 46 | for j, f in enumerate(space.get_basis_functions()): 47 | norm = sympy.sqrt(sum((edge[0][i] - edge[1][i]) ** 2 for i in range(tdim))) 48 | tangent = tuple((edge[1][i] - edge[0][i]) / norm for i in range(tdim)) 49 | 50 | integrand = sum(a * b for a, b in zip(f, tangent)) 51 | for d in range(tdim): 52 | integrand = integrand.subs(x[d], (1 - k) * edge[0][d] + k * edge[1][d]) 53 | 54 | integrand *= norm 55 | 56 | result = sympy.integrate(integrand, (k, 0, 1)) 57 | if i == j: 58 | assert result == 1 59 | else: 60 | assert result == 0 61 | -------------------------------------------------------------------------------- /test/test_p1_iso_p2.py: -------------------------------------------------------------------------------- 1 | """Test P1-iso-P2 elements.""" 2 | 3 | import pytest 4 | import sympy 5 | 6 | import symfem 7 | from symfem.symbols import x 8 | 9 | 10 | @pytest.mark.parametrize("cell", ["triangle", "quadrilateral"]) 11 | def test_polyset(cell): 12 | e = symfem.create_element(cell, "P1-iso-P2", 1) 13 | 14 | half = sympy.Rational(1, 2) 15 | if cell == "triangle": 16 | points = [(0, 0), (1, 0), (0, 1), (half, 0), (0, half), (half, half)] 17 | elif cell == "quadrilateral": 18 | points = [(0, 0), (1, 0), (0, 1), (half, 0), (0, half), (half, half)] 19 | else: 20 | raise ValueError(f"Unsupported cell: {cell}") 21 | 22 | for f in e.get_polynomial_basis(): 23 | for p in points: 24 | value = None 25 | for piece in f.pieces: 26 | if p in piece[0]: 27 | if value is None: 28 | value = piece[1].subs(x, p) 29 | assert piece[1].subs(x, p) == value 30 | -------------------------------------------------------------------------------- /test/test_quadrature.py: -------------------------------------------------------------------------------- 1 | """Test quadrature rules.""" 2 | 3 | import pytest 4 | import sympy 5 | 6 | from symfem import quadrature 7 | from symfem.functions import ScalarFunction 8 | from symfem.utils import allequal 9 | 10 | 11 | @pytest.mark.parametrize("order", range(1, 7)) 12 | def test_equispaced(order): 13 | points, weights = quadrature.equispaced(order + 1) 14 | 15 | x = sympy.Symbol("x") 16 | poly = ScalarFunction(x) 17 | 18 | assert allequal( 19 | poly.integrate((x, 0, 1)), sum(i * poly.subs(x, j) for i, j in zip(weights, points)) 20 | ) 21 | 22 | 23 | @pytest.mark.parametrize("order", range(1, 7)) 24 | def test_lobatto(order): 25 | points, weights = quadrature.lobatto(order + 1) 26 | 27 | x = sympy.Symbol("x") 28 | poly = ScalarFunction(x ** (2 * order - 1)) 29 | 30 | assert allequal( 31 | poly.integrate((x, 0, 1)), sum(i * poly.subs(x, j) for i, j in zip(weights, points)) 32 | ) 33 | 34 | 35 | @pytest.mark.parametrize("order", range(1, 3)) 36 | def test_radau(order): 37 | points, weights = quadrature.radau(order + 1) 38 | 39 | x = sympy.Symbol("x") 40 | poly = ScalarFunction(x ** (2 * order - 1)) 41 | 42 | assert allequal( 43 | poly.integrate((x, 0, 1)), sum(i * poly.subs(x, j) for i, j in zip(weights, points)) 44 | ) 45 | 46 | 47 | @pytest.mark.parametrize("order", range(1, 4)) 48 | def test_legendre(order): 49 | points, weights = quadrature.legendre(order) 50 | 51 | x = sympy.Symbol("x") 52 | poly = ScalarFunction(x ** (2 * order - 1)) 53 | 54 | assert allequal( 55 | poly.integrate((x, 0, 1)), sum(i * poly.subs(x, j) for i, j in zip(weights, points)) 56 | ) 57 | -------------------------------------------------------------------------------- /test/test_references.py: -------------------------------------------------------------------------------- 1 | """Test reference elements.""" 2 | 3 | import pytest 4 | 5 | from symfem import references 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "ReferenceClass", 10 | [ 11 | references.Interval, 12 | references.Triangle, 13 | references.Tetrahedron, 14 | references.Quadrilateral, 15 | references.Hexahedron, 16 | ], 17 | ) 18 | def test_reference(ReferenceClass): 19 | ref = ReferenceClass() 20 | assert ref.jacobian() == 1 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ("ReferenceClass", "points"), 25 | [ 26 | (references.Interval, [(1, 0), (1, 2)]), 27 | (references.Triangle, [(1, 0), (3, 0), (1, 1)]), 28 | (references.Tetrahedron, [(1, 0, 1), (2, 0, 1), (1, 1, 1), (1, 0, 3)]), 29 | (references.Quadrilateral, [(1, 0), (3, 0), (1, 1), (3, 1)]), 30 | ( 31 | references.Hexahedron, 32 | [ 33 | (1, 0, 1), 34 | (2, 0, 1), 35 | (1, 1, 1), 36 | (2, 1, 1), 37 | (1, 0, 3), 38 | (2, 0, 3), 39 | (1, 1, 3), 40 | (2, 1, 3), 41 | ], 42 | ), 43 | ], 44 | ) 45 | def test_jacobian(ReferenceClass, points): 46 | ref = ReferenceClass(vertices=points) 47 | assert ref.jacobian() == 2 48 | 49 | 50 | @pytest.mark.parametrize("n_tri", range(3, 10)) 51 | def test_dual_reference(n_tri): 52 | references.DualPolygon(n_tri) 53 | -------------------------------------------------------------------------------- /test/test_reorder.py: -------------------------------------------------------------------------------- 1 | """Test reordering inside tabulate_basis.""" 2 | 3 | import sympy 4 | 5 | import symfem 6 | 7 | half = sympy.Rational(1, 2) 8 | 9 | 10 | def test_xxyyzz(): 11 | element = symfem.create_element("triangle", "RT", 0) 12 | 13 | points = [(0, 1), (1, 0), (0, 0), (half, half)] 14 | r1 = element.tabulate_basis(points, "xyzxyz") 15 | r2 = element.tabulate_basis(points, "xxyyzz") 16 | for i, j in zip(r1, r2): 17 | assert i[0] == j[0] 18 | assert i[1] == j[3] 19 | assert i[2] == j[1] 20 | assert i[3] == j[4] 21 | assert i[4] == j[2] 22 | assert i[5] == j[5] 23 | 24 | 25 | def test_vector(): 26 | element = symfem.create_element("triangle", "RT", 0) 27 | 28 | points = [(0, 1), (1, 0), (0, 0), (half, half)] 29 | r1 = element.tabulate_basis(points, "xyzxyz") 30 | r2 = element.tabulate_basis(points, "xyz,xyz") 31 | for i, j in zip(r1, r2): 32 | assert i[0] == j[0][0] 33 | assert i[1] == j[0][1] 34 | assert i[2] == j[1][0] 35 | assert i[3] == j[1][1] 36 | assert i[4] == j[2][0] 37 | assert i[5] == j[2][1] 38 | -------------------------------------------------------------------------------- /test/test_stiffness_matrix.py: -------------------------------------------------------------------------------- 1 | """Test assembly of a stiffness matrix.""" 2 | 3 | import sympy 4 | 5 | import symfem 6 | 7 | 8 | def test_stiffness_matrix(): 9 | vertices = [(0, 0), (1, 0), (0, 1), (1, 1)] 10 | triangles = [[0, 1, 2], [1, 3, 2]] 11 | 12 | matrix = [[0 for i in vertices] for j in vertices] 13 | 14 | element = symfem.create_element("triangle", "Lagrange", 1) 15 | for triangle in triangles: 16 | vs = [vertices[i] for i in triangle] 17 | ref = symfem.create_reference("triangle", vertices=vs) 18 | basis = element.map_to_cell(vs) 19 | for test_i, test_f in zip(triangle, basis): 20 | for trial_i, trial_f in zip(triangle, basis): 21 | integrand = test_f.grad(2).dot(trial_f.grad(2)) 22 | matrix[test_i][trial_i] += integrand.integral(ref) 23 | 24 | half = sympy.Rational(1, 2) 25 | actual_matrix = [ 26 | [1, -half, -half, 0], 27 | [-half, 1, 0, -half], 28 | [-half, 0, 1, -half], 29 | [0, -half, -half, 1], 30 | ] 31 | 32 | for row1, row2 in zip(matrix, actual_matrix): 33 | for i, j in zip(row1, row2): 34 | assert i == j 35 | -------------------------------------------------------------------------------- /test/test_tensor_product.py: -------------------------------------------------------------------------------- 1 | """Test tensor product factorisations.""" 2 | 3 | import pytest 4 | import sympy 5 | 6 | import symfem 7 | from symfem import create_element 8 | from symfem.utils import allequal 9 | 10 | from .utils import test_elements 11 | 12 | 13 | def make_lattice(cell, N=3): 14 | if cell == "interval": 15 | return [[sympy.Rational(i, N)] for i in range(N + 1)] 16 | if cell == "triangle": 17 | return [ 18 | [sympy.Rational(i, N), sympy.Rational(j, N)] 19 | for i in range(N + 1) 20 | for j in range(N + 1 - i) 21 | ] 22 | if cell == "tetrahedron": 23 | return [ 24 | [sympy.Rational(i, N), sympy.Rational(j, N), sympy.Rational(k, N)] 25 | for i in range(N + 1) 26 | for j in range(N + 1 - i) 27 | for k in range(N + 1 - i - j) 28 | ] 29 | if cell == "quadrilateral": 30 | return [ 31 | [sympy.Rational(i, N), sympy.Rational(j, N)] for i in range(N + 1) for j in range(N + 1) 32 | ] 33 | if cell == "hexahedron": 34 | return [ 35 | [sympy.Rational(i, N), sympy.Rational(j, N), sympy.Rational(k, N)] 36 | for i in range(N + 1) 37 | for j in range(N + 1) 38 | for k in range(N + 1) 39 | ] 40 | if cell == "prism": 41 | return [ 42 | [sympy.Rational(i, N), sympy.Rational(j, N), sympy.Rational(k, N)] 43 | for i in range(N + 1) 44 | for j in range(N + 1 - i) 45 | for k in range(N + 1) 46 | ] 47 | if cell == "pyramid": 48 | return [ 49 | [sympy.Rational(i, N), sympy.Rational(j, N), sympy.Rational(k, N)] 50 | for i in range(N + 1) 51 | for j in range(N + 1) 52 | for k in range(N + 1 - max(i, j)) 53 | ] 54 | 55 | 56 | @pytest.mark.parametrize( 57 | ("cell_type", "element_type", "order", "kwargs"), 58 | [ 59 | [reference, element, order, kwargs] 60 | for reference, i in test_elements.items() 61 | for element, j in i.items() 62 | for kwargs, k in j 63 | for order in k 64 | ], 65 | ) 66 | def test_element(elements_to_test, cells_to_test, cell_type, element_type, order, kwargs, speed): 67 | """Run tests for each element.""" 68 | if elements_to_test != "ALL" and element_type not in elements_to_test: 69 | pytest.skip() 70 | if cells_to_test != "ALL" and cell_type not in cells_to_test: 71 | pytest.skip() 72 | if speed == "fast": 73 | if order > 2: 74 | pytest.skip() 75 | if order == 2 and cell_type in ["tetrahedron", "hexahedron", "prism", "pyramid"]: 76 | pytest.skip() 77 | 78 | element = create_element(cell_type, element_type, order, **kwargs) 79 | try: 80 | factorised_basis = element._get_basis_functions_tensor() 81 | except symfem.finite_element.NoTensorProduct: 82 | pytest.skip("This element does not have a tensor product representation.") 83 | basis = element.get_basis_functions() 84 | 85 | for i, j in zip(basis, factorised_basis): 86 | assert allequal(i, j) 87 | -------------------------------------------------------------------------------- /update_readme.py: -------------------------------------------------------------------------------- 1 | """Script to update the list of available elements in README.md.""" 2 | 3 | import typing 4 | 5 | import symfem 6 | 7 | cells = [ 8 | "interval", 9 | "triangle", 10 | "quadrilateral", 11 | "tetrahedron", 12 | "hexahedron", 13 | "prism", 14 | "pyramid", 15 | "dual polygon", 16 | ] 17 | elementlist: typing.Dict[str, typing.List[str]] = {i: [] for i in cells} 18 | 19 | for e in symfem.create._elementlist: 20 | name = e.names[0] 21 | if len(e.names) > 1: 22 | name += " (alternative names: " + ", ".join(e.names[1:]) + ")" 23 | for r in e.references: 24 | elementlist[r].append(name) 25 | 26 | for j in elementlist.values(): 27 | j.sort(key=lambda x: x.lower()) 28 | 29 | with open("README.md") as f: 30 | pre = f.read().split("# Available cells and elements")[0] 31 | 32 | with open("README.md", "w") as f: 33 | f.write(pre) 34 | f.write("# Available cells and elements\n") 35 | for cell in cells: 36 | f.write(f"## {cell[0].upper()}{cell[1:]}\n") 37 | if cell == "dual polygon": 38 | f.write(f"The reference {cell} (hexagon example shown) has vertices ") 39 | ref = symfem.create_reference("dual polygon(6)") 40 | else: 41 | f.write(f"The reference {cell} has vertices ") 42 | ref = symfem.create_reference(cell) 43 | str_v = [f"{v}" for v in ref.vertices] 44 | if len(ref.vertices) <= 2: 45 | f.write(" and ".join(str_v)) 46 | else: 47 | f.write(", and ".join([", ".join(str_v[:-1]), str_v[-1]])) 48 | f.write(". Its sub-entities are numbered as follows.\n\n") 49 | f.write( 50 | f"![The numbering of a reference {cell}]" 51 | f"(img/{cell.replace(' ', '_')}_numbering.png)\n\n" 52 | ) 53 | 54 | f.write("### List of supported elements\n") 55 | f.write("\n".join([f"- {i}" for i in elementlist[cell]])) 56 | f.write("\n\n") 57 | --------------------------------------------------------------------------------