From 4f04bf19ac8871827245a4ec2f1d3de2f37e2e70 Mon Sep 17 00:00:00 2001 From: "Garth N. Wells" Date: Fri, 27 Jan 2023 16:28:17 +0000 Subject: [PATCH 1/8] Updates to avoid `ufl_domain` deprecation warnings (#148) * Updates to avoid ufl_domain deprecation warnings. * Small fix * Formatting fix * Undo change for integrals --- ufl/core/expr.py | 2 +- ufl/form.py | 64 +++++++++++++++++++++--------------------------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/ufl/core/expr.py b/ufl/core/expr.py index d6d51e412..b47ccadca 100644 --- a/ufl/core/expr.py +++ b/ufl/core/expr.py @@ -23,9 +23,9 @@ from ufl.core.ufl_type import UFLType, update_ufl_type_attributes - # --- The base object for all UFL expression tree nodes --- + class Expr(object, metaclass=UFLType): """Base class for all UFL expression types. diff --git a/ufl/form.py b/ufl/form.py index 4baa506ea..9c4ab0eda 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -12,16 +12,16 @@ # Modified by Cecile Daversin-Catty, 2018. import warnings -from itertools import chain from collections import defaultdict +from itertools import chain -from ufl.domain import sort_domains -from ufl.integral import Integral from ufl.checks import is_scalar_constant_expression -from ufl.equation import Equation +from ufl.constantvalue import Zero from ufl.core.expr import Expr, ufl_err_str from ufl.core.ufl_type import UFLType, ufl_type -from ufl.constantvalue import Zero +from ufl.domain import extract_unique_domain, sort_domains +from ufl.equation import Equation +from ufl.integral import Integral # Export list for ufl.classes __all_classes__ = ["Form", "BaseForm", "ZeroBaseForm"] @@ -51,17 +51,14 @@ def _sorted_integrals(integrals): # Order integrals canonically to increase signature stability for d in sort_domains(integrals_dict): for it in sorted(integrals_dict[d]): # str is sortable - for si in sorted( - integrals_dict[d][it], key=lambda x: (type(x).__name__, x) - ): # int/str are sortable + for si in sorted(integrals_dict[d][it], key=lambda x: (type(x).__name__, x)): # int/str are sortable unsorted_integrals = integrals_dict[d][it][si] # TODO: At this point we could order integrals by # metadata and integrand, or even add the - # integrands with the same metadata. This is - # done in - # accumulate_integrands_with_same_metadata in - # algorithms/domain_analysis.py and would - # further increase the signature stability. + # integrands with the same metadata. This is done + # in accumulate_integrands_with_same_metadata in + # algorithms/domain_analysis.py and would further + # increase the signature stability. all_integrals.extend(unsorted_integrals) # integrals_dict[d][it][si] = unsorted_integrals @@ -72,7 +69,8 @@ def _sorted_integrals(integrals): class BaseForm(object, metaclass=UFLType): """Description of an object containing arguments""" - # Slots is kept empty to enable multiple inheritance with other classes. + # Slots is kept empty to enable multiple inheritance with other + # classes __slots__ = () _ufl_is_abstract_ = True _ufl_required_methods_ = ('_analyze_form_arguments', "ufl_domains") @@ -107,10 +105,7 @@ def __add__(self, other): if isinstance(other, (int, float)) and other == 0: # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b]) return self - - elif isinstance( - other, - Zero) and not (other.ufl_shape or other.ufl_free_indices): + elif isinstance(other, Zero) and not (other.ufl_shape or other.ufl_free_indices): # Allow adding ufl Zero as a no-op, needed for sum([a,b]) return self @@ -226,6 +221,7 @@ def __call__(self, *args, **kwargs): repdict[f] = coefficients[f] else: warnings("Coefficient %s is not in form." % ufl_err_str(f)) + if repdict: from ufl.formoperators import replace return replace(self, repdict) @@ -320,8 +316,7 @@ def integrals_by_type(self, integral_type): def integrals_by_domain(self, domain): "Return a sequence of all integrals with a particular integration domain." - return tuple(integral for integral in self.integrals() - if integral.ufl_domain() == domain) + return tuple(integral for integral in self.integrals() if integral.ufl_domain() == domain) def empty(self): "Returns whether the form has no integrals." @@ -361,19 +356,18 @@ def ufl_domain(self): # Check that all are equal TODO: don't return more than one if # all are equal? if not all(domain == domains[0] for domain in domains): - raise ValueError( - "Calling Form.ufl_domain() is only valid if all integrals share domain.") + raise ValueError("Calling Form.ufl_domain() is only valid if all integrals share domain.") # Return the one and only domain return domains[0] def geometric_dimension(self): - "Return the geometric dimension shared by all domains and functions in this form." + """Return the geometric dimension shared by all domains and functions in this form.""" gdims = tuple( set(domain.geometric_dimension() for domain in self.ufl_domains())) if len(gdims) != 1: raise ValueError("Expecting all domains and functions in a form " - f"to share geometric dimension, got {tuple(sorted(gdims))}") + f"to share geometric dimension, got {tuple(sorted(gdims))}") return gdims[0] def domain_numbering(self): @@ -590,16 +584,14 @@ def _analyze_domains(self): from ufl.domain import join_domains, sort_domains # Collect unique integration domains - integration_domains = join_domains( - [itg.ufl_domain() for itg in self._integrals]) + integration_domains = join_domains([itg.ufl_domain() for itg in self._integrals]) # Make canonically ordered list of the domains self._integration_domains = sort_domains(integration_domains) # TODO: Not including domains from coefficients and arguments # here, may need that later - self._domain_numbering = dict( - (d, i) for i, d in enumerate(self._integration_domains)) + self._domain_numbering = dict((d, i) for i, d in enumerate(self._integration_domains)) def _analyze_subdomain_data(self): integration_domains = self.ufl_domains() @@ -650,7 +642,7 @@ def _compute_renumbering(self): # among integration domains k = len(dn) for c in cn: - d = c.ufl_domain() + d = extract_unique_domain(c) if d is not None and d not in renumbering: renumbering[d] = k k += 1 @@ -666,7 +658,7 @@ def _compute_renumbering(self): # Add domains of constants, these may include domains not # among integration domains for c in self._constants: - d = c.ufl_domain() + d = extract_unique_domain(c) if d is not None and d not in renumbering: renumbering[d] = k k += 1 @@ -675,8 +667,7 @@ def _compute_renumbering(self): def _compute_signature(self): from ufl.algorithms.signature import compute_form_signature - self._signature = compute_form_signature(self, - self._compute_renumbering()) + self._signature = compute_form_signature(self, self._compute_renumbering()) def sub_forms_by_domain(form): @@ -707,9 +698,7 @@ def replace_integral_domains(form, common_domain): # TODO: Move elsewhere if common_domain is not None: gdim = common_domain.geometric_dimension() tdim = common_domain.topological_dimension() - if not all((gdim == domain.geometric_dimension() and - tdim == domain.topological_dimension()) - for domain in domains): + if not all((gdim == domain.geometric_dimension() and tdim == domain.topological_dimension()) for domain in domains): raise ValueError("Common domain does not share dimensions with form domains.") reconstruct = False @@ -830,7 +819,10 @@ def __repr__(self): @ufl_type() class ZeroBaseForm(BaseForm): """Description of a zero base form. - ZeroBaseForm is idempotent with respect to assembly and is mostly used for sake of simplifying base-form expressions. + + ZeroBaseForm is idempotent with respect to assembly and is mostly + used for sake of simplifying base-form expressions. + """ __slots__ = ("_arguments", From 6f181baabb3025a737a65d9725ea5183a4d3b710 Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:05:34 +0000 Subject: [PATCH 2/8] fix TensorProductElement repr (#152) --- ufl/finiteelement/tensorproductelement.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ufl/finiteelement/tensorproductelement.py b/ufl/finiteelement/tensorproductelement.py index 8b882cf91..341b018f5 100644 --- a/ufl/finiteelement/tensorproductelement.py +++ b/ufl/finiteelement/tensorproductelement.py @@ -69,9 +69,7 @@ def __init__(self, *elements, **kwargs): self._cell = cell def __repr__(self): - return "TensorProductElement(" + ", ".join( - repr(e) for e in self._sub_elements - ) + f", {repr(self._cell)})" + return "TensorProductElement(" + ", ".join(repr(e) for e in self._sub_elements) + f", cell={repr(self._cell)})" def mapping(self): if all(e.mapping() == "identity" for e in self._sub_elements): From 2d3992dd8b0a39f00e8233eceaec3007fb6d89c6 Mon Sep 17 00:00:00 2001 From: "Garth N. Wells" Date: Fri, 24 Feb 2023 09:23:18 +0000 Subject: [PATCH 3/8] Fix dev version number so pip doesn't report UFL as upgradable (#153) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 77f81636d..48673d5f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ # future [metadata] name = fenics-ufl -version = 2022.3.0.dev0 +version = 2023.2.0.dev0 author = FEniCS Project Contributors email = fenics-dev@googlegroups.com maintainer = FEniCS Project Steering Council From 6eeef938c877634dcb7c8acb101bb2363ed1455b Mon Sep 17 00:00:00 2001 From: "Garth N. Wells" Date: Sun, 26 Feb 2023 21:43:35 +0000 Subject: [PATCH 4/8] Bump min setuptools version and remove setup.py (#154) See https://github.com/pypa/setuptools/pull/3151. --- pyproject.toml | 2 +- setup.cfg | 2 +- setup.py | 16 ---------------- 3 files changed, 2 insertions(+), 18 deletions(-) delete mode 100755 setup.py diff --git a/pyproject.toml b/pyproject.toml index 9bf811c37..cfcfb19c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ [build-system] -requires = ["setuptools>=58", "wheel"] +requires = ["setuptools>=62", "wheel"] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 48673d5f3..44b1f1651 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ include_package_data = True zip_safe = False python_requires = >= 3.7 setup_requires = - setuptools >= 58 + setuptools >= 62 wheel install_requires = numpy diff --git a/setup.py b/setup.py deleted file mode 100755 index daa285d2c..000000000 --- a/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -import setuptools - -try: - import pip - - from packaging import version - if version.parse(pip.__version__) < version.parse("21.3"): - # Issue with older version of pip https://github.com/pypa/pip/issues/7953 - import site - import sys - site.ENABLE_USER_SITE = "--user" in sys.argv[1:] - -except ImportError: - pass - -setuptools.setup() From 43b966ac95d166c5e3be959670e50d06379e2bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Schartum=20Dokken?= Date: Tue, 25 Apr 2023 10:24:17 +0200 Subject: [PATCH 5/8] Replace `pkg_resources` with `importlib.metadata` and bump minimal Python version to 3.8 (#158) * Fix #157 * Bump python version * Add more versions to CI * Add pytest to tsfc * Remove clang from ufl tests * Switch env vars * Apply suggestions from code review * Consistent string-style --- .github/workflows/fenicsx-tests.yml | 2 -- .github/workflows/pythonapp.yml | 2 +- .github/workflows/tsfc-tests.yml | 1 + doc/sphinx/source/conf.py | 8 +++----- setup.cfg | 4 ++-- ufl/__init__.py | 4 ++-- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/fenicsx-tests.yml b/.github/workflows/fenicsx-tests.yml index 7aaf42d7f..f9c7d811f 100644 --- a/.github/workflows/fenicsx-tests.yml +++ b/.github/workflows/fenicsx-tests.yml @@ -55,8 +55,6 @@ jobs: container: fenicsproject/test-env:nightly-openmpi env: - CC: clang - CXX: clang++ PETSC_ARCH: linux-gnu-complex-32 OMPI_ALLOW_RUN_AS_ROOT: 1 diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 3feb86104..695aad6c7 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tsfc-tests.yml b/.github/workflows/tsfc-tests.yml index 601422623..06b10a13d 100644 --- a/.github/workflows/tsfc-tests.yml +++ b/.github/workflows/tsfc-tests.yml @@ -42,5 +42,6 @@ jobs: pip install git+https://github.com/FInAT/FInAT.git#egg=finat pip install git+https://github.com/firedrakeproject/loopy.git#egg=loopy pip install .[ci] + pip install pytest - name: Run tsfc unit tests run: python3 -m pytest tsfc/tests diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 500ba06ed..60a0af8ee 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -12,11 +12,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os -import shlex -import pkg_resources import datetime +import importlib.metadata # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -58,7 +55,8 @@ this_year = datetime.date.today().year copyright = u'%s, FEniCS Project' % this_year author = u'FEniCS Project' -version = pkg_resources.get_distribution("fenics-ufl").version + +version = importlib.metadata.version("fenics-ufl") release = version # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/setup.cfg b/setup.cfg index 44b1f1651..256c55d64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,10 +26,10 @@ classifiers = Operating System :: MacOS :: MacOS X Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Scientific/Engineering :: Mathematics Topic :: Software Development :: Libraries :: Python Modules @@ -37,7 +37,7 @@ classifiers = packages = find: include_package_data = True zip_safe = False -python_requires = >= 3.7 +python_requires = >= 3.8 setup_requires = setuptools >= 62 wheel diff --git a/ufl/__init__.py b/ufl/__init__.py index e6d823003..8b356688a 100644 --- a/ufl/__init__.py +++ b/ufl/__init__.py @@ -241,9 +241,9 @@ # Modified by Massimiliano Leoni, 2016 # Modified by Cecile Daversin-Catty, 2018 -import pkg_resources +import importlib.metadata -__version__ = pkg_resources.get_distribution("fenics-ufl").version +__version__ = importlib.metadata.version("fenics-ufl") # README # Imports here should be what the user sees when doing "from ufl import *", From f195476fd31364fd4f01ba033b318b85a29cae02 Mon Sep 17 00:00:00 2001 From: Nacime Bouziani <48448063+nbouziani@users.noreply.github.com> Date: Fri, 5 May 2023 12:09:47 +0200 Subject: [PATCH 6/8] Fix typo in split (#144) * Fix typo in split * Add test for split on simple elements --------- Co-authored-by: Garth N. Wells Co-authored-by: David A. Ham --- test/test_split.py | 4 ++++ ufl/split_functions.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_split.py b/test/test_split.py index 380def3dc..1b7b4bd10 100755 --- a/test/test_split.py +++ b/test/test_split.py @@ -47,6 +47,10 @@ def test_split(self): assert (d+d,) == Coefficient(v2).ufl_shape assert (2*d*d,) == Coefficient(m2).ufl_shape + # Split simple element + t = TestFunction(f) + assert split(t) == (t,) + # Split twice on nested mixed elements gets # the innermost scalar subcomponents t = TestFunction(f*v) diff --git a/ufl/split_functions.py b/ufl/split_functions.py index 71fa3dcd8..18129698e 100644 --- a/ufl/split_functions.py +++ b/ufl/split_functions.py @@ -50,7 +50,7 @@ def split(v): # Special case: simple element, just return function in a tuple element = v.ufl_element() - if element.num_sub_elements == 0: + if element.num_sub_elements() == 0: assert end is None return (v,) From d2dfbef9a60bede67ac8361712316bf427f9eecc Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Mon, 15 May 2023 10:02:19 +0100 Subject: [PATCH 7/8] fix str (#162) --- test/test_str.py | 5 +++++ ufl/finiteelement/finiteelement.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_str.py b/test/test_str.py index f5021a042..ffab81ea2 100755 --- a/test/test_str.py +++ b/test/test_str.py @@ -103,3 +103,8 @@ def test_str_list_matrix_with_zero(): # FIXME: Add more tests for tensors collapsing # partly or completely into Zero! + + +def test_str_element(): + elem = FiniteElement("Q", quadrilateral, 1) + assert str(elem) == "" diff --git a/ufl/finiteelement/finiteelement.py b/ufl/finiteelement/finiteelement.py index d86ae331e..9ea00b37f 100644 --- a/ufl/finiteelement/finiteelement.py +++ b/ufl/finiteelement/finiteelement.py @@ -153,7 +153,7 @@ def __init__(self, # simplify base though. self._sobolev_space = sobolev_space self._mapping = mapping - self._short_name = short_name + self._short_name = short_name or family self._variant = variant # Finite elements on quadrilaterals and hexahedrons have an IrreducibleInt as degree From e3c6cdd5a83d9aa2c1b8d6ac946589a0f70fafe7 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Mon, 15 May 2023 12:07:08 +0100 Subject: [PATCH 8/8] Remove checks based on family name (#161) * Use sobolev space not family * update comment * don't use checks on family name * flake8 * l * remove check that family is a string * _is_linear in subclasses --- ufl/algorithms/apply_derivatives.py | 11 +++++----- ufl/algorithms/apply_function_pullbacks.py | 4 ++-- ufl/algorithms/apply_restrictions.py | 4 +--- ufl/algorithms/change_to_reference.py | 2 +- ufl/algorithms/compute_form_data.py | 2 -- ufl/algorithms/elementtransformations.py | 2 +- ufl/checks.py | 2 +- ufl/finiteelement/elementlist.py | 8 +++---- ufl/finiteelement/finiteelement.py | 6 ++++++ ufl/finiteelement/finiteelementbase.py | 15 ++++++++++--- ufl/finiteelement/mixedelement.py | 3 +++ ufl/finiteelement/restrictedelement.py | 3 +++ ufl/operators.py | 25 ++++++++-------------- 13 files changed, 48 insertions(+), 39 deletions(-) diff --git a/ufl/algorithms/apply_derivatives.py b/ufl/algorithms/apply_derivatives.py index d60326880..4a7dd3a55 100644 --- a/ufl/algorithms/apply_derivatives.py +++ b/ufl/algorithms/apply_derivatives.py @@ -1268,12 +1268,11 @@ def coefficient_derivative(self, o): def coordinate_derivative(self, o, f, w, v, cd): from ufl.algorithms import extract_unique_elements - spaces = set(c.family() for c in extract_unique_elements(o)) - unsupported_spaces = {"Argyris", "Bell", "Hermite", "Morley"} - if spaces & unsupported_spaces: - raise NotImplementedError( - "CoordinateDerivative is not supported for elements of type {spaces & unsupported_spaces}. " - "This is because their pullback is not implemented in UFL.") + for space in extract_unique_elements(o): + if space.mapping() == "custom": + raise NotImplementedError( + "CoordinateDerivative is not supported for elements with custom pull back.") + _, w, v, cd = o.ufl_operands rules = CoordinateDerivativeRuleset(w, v, cd) key = (CoordinateDerivativeRuleset, w, v, cd) diff --git a/ufl/algorithms/apply_function_pullbacks.py b/ufl/algorithms/apply_function_pullbacks.py index 5bd12cf83..58930a113 100644 --- a/ufl/algorithms/apply_function_pullbacks.py +++ b/ufl/algorithms/apply_function_pullbacks.py @@ -68,7 +68,7 @@ def apply_known_single_pullback(r, element): domain = extract_unique_domain(r) if mapping == "physical": return r - elif mapping == "identity": + elif mapping == "identity" or mapping == "custom": return r elif mapping == "contravariant Piola": J = Jacobian(domain) @@ -122,7 +122,7 @@ def apply_single_function_pullbacks(r, element): if mapping in {"physical", "identity", "contravariant Piola", "covariant Piola", "double contravariant Piola", "double covariant Piola", - "L2 Piola"}: + "L2 Piola", "custom"}: # Base case in recursion through elements. If the element # advertises a mapping we know how to handle, do that # directly. diff --git a/ufl/algorithms/apply_restrictions.py b/ufl/algorithms/apply_restrictions.py index f53b98fac..9cca28b1e 100644 --- a/ufl/algorithms/apply_restrictions.py +++ b/ufl/algorithms/apply_restrictions.py @@ -137,12 +137,10 @@ def coefficient(self, o): def facet_normal(self, o): D = extract_unique_domain(o) e = D.ufl_coordinate_element() - f = e.family() - d = e.degree() gd = D.geometric_dimension() td = D.topological_dimension() - if f == "Lagrange" and d == 1 and gd == td: + if e._is_linear() and gd == td: # For meshes with a continuous linear non-manifold # coordinate field, the facet normal from side - points in # the opposite direction of the one from side +. We must diff --git a/ufl/algorithms/change_to_reference.py b/ufl/algorithms/change_to_reference.py index b3b4e07e4..168cad610 100644 --- a/ufl/algorithms/change_to_reference.py +++ b/ufl/algorithms/change_to_reference.py @@ -279,7 +279,7 @@ def ndarray(shape): # covariant_hcurl_mapping = JinvT * PullbackOf(o) ec, = ec emapping = K[:, ec] # Column of K is row of K.T - elif mapping == "identity": + elif mapping == "identity" or mapping == "custom": emapping = None else: raise ValueError(f"Unknown mapping: {mapping}") diff --git a/ufl/algorithms/compute_form_data.py b/ufl/algorithms/compute_form_data.py index e665a880e..1b5d4ca5c 100644 --- a/ufl/algorithms/compute_form_data.py +++ b/ufl/algorithms/compute_form_data.py @@ -139,8 +139,6 @@ def _compute_form_data_elements(self, arguments, coefficients, domains): def _check_elements(form_data): for element in chain(form_data.unique_elements, form_data.unique_sub_elements): - if element.family() is None: - raise ValueError(f"Found element with undefined family: {element}") if element.cell() is None: raise ValueError(f"Found element with undefined cell: {element}") diff --git a/ufl/algorithms/elementtransformations.py b/ufl/algorithms/elementtransformations.py index 43e659cb6..ae182d5ce 100644 --- a/ufl/algorithms/elementtransformations.py +++ b/ufl/algorithms/elementtransformations.py @@ -36,7 +36,7 @@ def tear(element): def _increase_degree(element, degree_rise): if isinstance(element, (FiniteElement, VectorElement, TensorElement)): # Can't increase degree for reals - if element.family() == "Real": + if element._is_globally_constant(): return element return element.reconstruct(degree=(element.degree() + degree_rise)) elif isinstance(element, MixedElement): diff --git a/ufl/checks.py b/ufl/checks.py index a3cad0e8b..11502cb65 100644 --- a/ufl/checks.py +++ b/ufl/checks.py @@ -54,7 +54,7 @@ def is_globally_constant(expr): continue elif isinstance(e, FormArgument): # Accept only Real valued Arguments and Coefficients - if e.ufl_element().family() == "Real": + if e.ufl_element()._is_globally_constant(): continue else: return False diff --git a/ufl/finiteelement/elementlist.py b/ufl/finiteelement/elementlist.py index a470279f3..b976b7e43 100644 --- a/ufl/finiteelement/elementlist.py +++ b/ufl/finiteelement/elementlist.py @@ -106,8 +106,8 @@ def show_elements(): (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) # Elements not in the periodic table -register_element("Argyris", "ARG", 0, H2, "identity", (5, 5), ("triangle",)) -register_element("Bell", "BELL", 0, H2, "identity", (5, 5), ("triangle",)) +register_element("Argyris", "ARG", 0, H2, "custom", (5, 5), ("triangle",)) +register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), @@ -115,12 +115,12 @@ def show_elements(): # TODO: Implement generic Tear operator for elements instead of this: register_element("Discontinuous Raviart-Thomas", "DRT", 1, L2, "contravariant Piola", (1, None), simplices[1:]) -register_element("Hermite", "HER", 0, H1, "identity", (3, 3), simplices) +register_element("Hermite", "HER", 0, H1, "custom", (3, 3), simplices) register_element("Kong-Mulder-Veldhuizen", "KMV", 0, H1, "identity", (1, None), simplices[1:]) register_element("Mardal-Tai-Winther", "MTW", 1, H1, "contravariant Piola", (3, 3), ("triangle",)) -register_element("Morley", "MOR", 0, H2, "identity", (2, 2), ("triangle",)) +register_element("Morley", "MOR", 0, H2, "custom", (2, 2), ("triangle",)) # Special elements register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), diff --git a/ufl/finiteelement/finiteelement.py b/ufl/finiteelement/finiteelement.py index 9ea00b37f..ca7071698 100644 --- a/ufl/finiteelement/finiteelement.py +++ b/ufl/finiteelement/finiteelement.py @@ -189,6 +189,12 @@ def __repr__(self): """Format as string for evaluation as Python object.""" return self._repr + def _is_globally_constant(self): + return self.family() == "Real" + + def _is_linear(self): + return self.family() == "Lagrange" and self.degree() == 1 + def mapping(self): """Return the mapping type for this element .""" return self._mapping diff --git a/ufl/finiteelement/finiteelementbase.py b/ufl/finiteelement/finiteelementbase.py index 7d31006e0..8f1b71b51 100644 --- a/ufl/finiteelement/finiteelementbase.py +++ b/ufl/finiteelement/finiteelementbase.py @@ -26,8 +26,6 @@ class FiniteElementBase(ABC): def __init__(self, family, cell, degree, quad_scheme, value_shape, reference_value_shape): """Initialize basic finite element data.""" - if not isinstance(family, str): - raise ValueError("Invalid family type.") if not (degree is None or isinstance(degree, (int, tuple))): raise ValueError("Invalid degree type.") if not isinstance(value_shape, tuple): @@ -62,6 +60,17 @@ def mapping(self): """Return the mapping type for this element.""" pass + def _is_globally_constant(self): + """Check if the element is a global constant. + + For Real elements, this should return True. + """ + return False + + def _is_linear(self): + """Check if the element is Lagrange degree 1.""" + return False + def _ufl_hash_data_(self): return repr(self) @@ -109,7 +118,7 @@ def cell(self): def is_cellwise_constant(self, component=None): """Return whether the basis functions of this element is spatially constant over each cell.""" - return self.family() == "Real" or self.degree() == 0 + return self._is_globally_constant() or self.degree() == 0 def value_shape(self): "Return the shape of the value space on the global domain." diff --git a/ufl/finiteelement/mixedelement.py b/ufl/finiteelement/mixedelement.py index 482dfc73d..b67fc9f48 100644 --- a/ufl/finiteelement/mixedelement.py +++ b/ufl/finiteelement/mixedelement.py @@ -91,6 +91,9 @@ def __init__(self, *elements, **kwargs): def __repr__(self): return "MixedElement(" + ", ".join(repr(e) for e in self._sub_elements) + ")" + def _is_linear(self): + return all(i._is_linear() for i in self._sub_elements) + def reconstruct_from_elements(self, *elements): "Reconstruct a mixed element from new subelements." if all(a == b for (a, b) in zip(elements, self._sub_elements)): diff --git a/ufl/finiteelement/restrictedelement.py b/ufl/finiteelement/restrictedelement.py index 90640e994..82cd581f4 100644 --- a/ufl/finiteelement/restrictedelement.py +++ b/ufl/finiteelement/restrictedelement.py @@ -48,6 +48,9 @@ def is_cellwise_constant(self): """ return self._element.is_cellwise_constant() + def _is_linear(self): + return self._element._is_linear() + def sub_element(self): "Return the element which is restricted." return self._element diff --git a/ufl/operators.py b/ufl/operators.py index c387b5ec3..3295ca077 100644 --- a/ufl/operators.py +++ b/ufl/operators.py @@ -36,6 +36,7 @@ from ufl.geometry import SpatialCoordinate, FacetNormal from ufl.checks import is_cellwise_constant from ufl.domain import extract_domains +from ufl import sobolevspace # --- Basic operators --- @@ -691,7 +692,7 @@ def bessel_K(nu, f): def exterior_derivative(f): """UFL operator: Take the exterior derivative of *f*. - The exterior derivative uses the element family to + The exterior derivative uses the element Sobolev space to determine whether ``id``, ``grad``, ``curl`` or ``div`` should be used. Note that this uses the ``grad`` and ``div`` operators, @@ -720,26 +721,18 @@ def exterior_derivative(f): except Exception: raise ValueError(f"Unable to determine element from {f}") - # Extract the family and the geometric dimension - family = element.family() gdim = element.cell().geometric_dimension() + space = element.sobolev_space() - # L^2 elements: - if "Disc" in family: + if space == sobolevspace.L2: return f - - # H^1 elements: - if "Lagrange" in family: + elif space == sobolevspace.H1: if gdim == 1: return grad(f)[0] # Special-case 1D vectors as scalars return grad(f) - - # H(curl) elements: - if "curl" in family: + elif space == sobolevspace.HCurl: return curl(f) - - # H(div) elements: - if "Brezzi" in family or "Raviart" in family: + elif space == sobolevspace.HDiv: return div(f) - - raise ValueError(f"Unable to determine exterior_derivative. Family is '{family}'") + else: + raise ValueError(f"Unable to determine exterior_derivative for element '{element!r}'")