diff --git a/test/test_classcoverage.py b/test/test_classcoverage.py index e1056a240..0eee27df4 100755 --- a/test/test_classcoverage.py +++ b/test/test_classcoverage.py @@ -115,6 +115,7 @@ Tanh, all_ufl_classes, ) +from ufl.core.ufl_type import UFLRegistry has_repr = set() has_dict = set() @@ -722,15 +723,15 @@ def testAll(self): # e = action(b) # --- Check which classes have been created - ic, _dc = Expr.ufl_disable_profiling() + Expr.ufl_disable_profiling() + ic = UFLRegistry().object_tracking constructed = set() - unused = set(Expr._ufl_all_classes_) - for cls in Expr._ufl_all_classes_: - tc = cls._ufl_typecode_ - if ic[tc]: + unused = set(UFLRegistry().all_classes) + for cls in ic.keys(): + if ic[cls][0] > 0: constructed.add(cls) - if cls._ufl_is_abstract_: + else: unused.remove(cls) if unused: diff --git a/test/test_custom_type.py b/test/test_custom_type.py new file mode 100644 index 000000000..de6d84d9f --- /dev/null +++ b/test/test_custom_type.py @@ -0,0 +1,54 @@ +# Copyright (C) 2025 Paul T. Kühner +# +# This file is part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +from utils import LagrangeElement + +from ufl import Mesh, triangle +from ufl.algebra import Product +from ufl.algorithms.analysis import extract_constants, has_exact_type +from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering +from ufl.constant import Constant +from ufl.core.ufl_type import ufl_type +from ufl.geometry import SpatialCoordinate +from ufl.operators import diff + + +@ufl_type() +class LabeledConstant(Constant): + def __init__(self, domain, shape=(), count=None, label: str = "c"): + Constant.__init__(self, domain, shape, count) + self._label = label + + @property + def label(self) -> str: + return self._label + + +def test(): + domain = Mesh(LagrangeElement(triangle, 1, (2,))) + a = LabeledConstant(domain, label="a") + b = LabeledConstant(domain, label="b") + + assert a.label == "a" + assert b.label == "b" + + ab = a * b + assert isinstance(ab, Product) + # assert ab.ufl_operands == (a, b) + + assert apply_algebra_lowering(ab) == ab + + x = SpatialCoordinate(domain) + + a_dx = diff(a, x) + assert a_dx == 0 + assert a_dx.ufl_shape == (2,) + + # TODO: expression does not get simplified, so can't check here for ax_dx == a + ax_dx = diff(a * x, x) + assert has_exact_type(ax_dx, LabeledConstant) + assert extract_constants(ax_dx)[0].label == "a" diff --git a/ufl/algebra.py b/ufl/algebra.py index f3b2a3f0f..fe07c739c 100644 --- a/ufl/algebra.py +++ b/ufl/algebra.py @@ -20,13 +20,7 @@ # --- Algebraic operators --- -@ufl_type( - num_ops=2, - inherit_shape_from_operand=0, - inherit_indices_from_operand=0, - binop="__add__", - rbinop="__radd__", -) +@ufl_type() class Sum(Operator): """Sum.""" @@ -103,8 +97,23 @@ def __str__(self): """Format as a string.""" return " + ".join([parstr(o, self) for o in self.ufl_operands]) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(num_ops=2, binop="__mul__", rbinop="__rmul__") + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class Product(Operator): """The product of two or more UFL objects.""" @@ -201,7 +210,7 @@ def __str__(self): return " * ".join((parstr(a, self), parstr(b, self))) -@ufl_type(num_ops=2, inherit_indices_from_operand=0, binop="__div__", rbinop="__rdiv__") +@ufl_type() class Division(Operator): """Division.""" @@ -265,8 +274,18 @@ def __str__(self): """Format as a string.""" return f"{parstr(self.ufl_operands[0], self)} / {parstr(self.ufl_operands[1], self)}" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=2, inherit_indices_from_operand=0, binop="__pow__", rbinop="__rpow__") +@ufl_type() class Power(Operator): """Power.""" @@ -327,8 +346,18 @@ def __str__(self): a, b = self.ufl_operands return f"{parstr(a, self)} ** {parstr(b, self)}" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0, unop="__abs__") + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class Abs(Operator): """Absolute value.""" @@ -362,8 +391,23 @@ def __str__(self): (a,) = self.ufl_operands return f"|{parstr(a, self)}|" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class Conj(Operator): """Complex conjugate.""" @@ -397,8 +441,23 @@ def __str__(self): (a,) = self.ufl_operands return f"conj({parstr(a, self)})" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class Real(Operator): """Real part.""" @@ -434,8 +493,23 @@ def __str__(self): (a,) = self.ufl_operands return f"Re[{parstr(a, self)}]" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) + +@ufl_type() class Imag(Operator): """Imaginary part.""" @@ -468,3 +542,18 @@ def __str__(self): """Format as a string.""" (a,) = self.ufl_operands return f"Im[{parstr(a, self)}]" + + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions diff --git a/ufl/algorithms/apply_coefficient_split.py b/ufl/algorithms/apply_coefficient_split.py index 1db615e17..e1abc3c38 100644 --- a/ufl/algorithms/apply_coefficient_split.py +++ b/ufl/algorithms/apply_coefficient_split.py @@ -105,8 +105,9 @@ def _( if reference_value: raise RuntimeError(f"Can not apply ReferenceValue on a ReferenceValue: got {o}") (op,) = o.ufl_operands - if not op._ufl_terminal_modifiers_: - raise ValueError(f"Must be a terminal modifier: {op!r}.") + # TODO: correct? + # if not op._ufl_is_terminal_modifier_: + # raise ValueError(f"Must be a terminal modifier: {op!r}.") return self( op, reference_value=True, @@ -124,8 +125,9 @@ def _( ) -> Expr: """Handle ReferenceGrad.""" (op,) = o.ufl_operands - if not op._ufl_terminal_modifiers_: - raise ValueError(f"Must be a terminal modifier: {op!r}.") + # TODO: + # if not op._ufl_is_terminal_modifier_: + # raise ValueError(f"Must be a terminal modifier: {op!r}.") return self( op, reference_value=reference_value, @@ -145,8 +147,9 @@ def _( if restricted is not None: raise RuntimeError(f"Can not apply Restricted on a Restricted: got {o}") (op,) = o.ufl_operands - if not op._ufl_terminal_modifiers_: - raise ValueError(f"Must be a terminal modifier: {op!r}.") + # TODO: + # if not op._ufl_is_terminal_modifier_: + # raise ValueError(f"Must be a terminal modifier: {op!r}.") return self( op, reference_value=reference_value, diff --git a/ufl/algorithms/apply_derivatives.py b/ufl/algorithms/apply_derivatives.py index 1194ace3a..3893817de 100644 --- a/ufl/algorithms/apply_derivatives.py +++ b/ufl/algorithms/apply_derivatives.py @@ -166,13 +166,11 @@ def __init__( def unexpected(self, o): """Raise error about unexpected type.""" - raise ValueError(f"Unexpected type {o._ufl_class_.__name__} in AD rules.") + raise ValueError(f"Unexpected type {type(o).__name__} in AD rules.") def override(self, o): """Raise error about overriding.""" - raise ValueError( - f"Type {o._ufl_class_.__name__} must be overridden in specialized AD rule set." - ) + raise ValueError(f"Type {type(o).__name__} must be overridden in specialized AD rule set.") # --- Some types just don't have any derivative, this is just to # --- make algorithm structure generic @@ -214,7 +212,7 @@ def process(self, o: Expr) -> Expr: def _(self, o: Expr) -> Expr: """Raise error.""" raise ValueError( - f"Missing differentiation handler for type {o._ufl_class_.__name__}. " + f"Missing differentiation handler for type {type(o).__name__}. " "Have you added a new type?" ) @@ -222,8 +220,7 @@ def _(self, o: Expr) -> Expr: def _(self, o: Expr) -> Expr: """Raise error.""" raise ValueError( - f"Unhandled derivative type {o._ufl_class_.__name__}, " - "nested differentiation has failed." + f"Unhandled derivative type {type(o).__name__}, nested differentiation has failed." ) @process.register(Label) @@ -321,7 +318,7 @@ def _(self, o: Expr) -> Expr: @process.register(Indexed) @DAGTraverser.postorder - def _(self, o: Indexed, Ap: Expr, ii: MultiIndex) -> Expr: + def _(self, o: Indexed, Ap: Expr, ii: MultiIndex) -> Indexed: """Differentiate an indexed.""" # Propagate zeros if isinstance(Ap, Zero): @@ -905,8 +902,8 @@ def _(self, o: Expr) -> Expr: if not f._ufl_is_in_reference_frame_: raise RuntimeError("Expecting a reference frame type") while not f._ufl_is_terminal_: - (f,) = f.ufl_operands - element = f.ufl_function_space().ufl_element() + (f,) = f.ufl_operands # type: ignore + element = f.ufl_function_space().ufl_element() # type: ignore if element.num_sub_elements != len(domain): raise RuntimeError(f"{element.num_sub_elements} != {len(domain)}") # Get monolithic representation of rgrad(o); o might live in a mixed space. diff --git a/ufl/algorithms/apply_geometry_lowering.py b/ufl/algorithms/apply_geometry_lowering.py index dc62d8baa..80cdcd8e3 100644 --- a/ufl/algorithms/apply_geometry_lowering.py +++ b/ufl/algorithms/apply_geometry_lowering.py @@ -43,6 +43,7 @@ ) from ufl.compound_expressions import cross_expr, determinant_expr, inverse_expr from ufl.core.multiindex import Index, indices +from ufl.core.ufl_type import UFLRegistry from ufl.corealg.map_dag import map_expr_dag from ufl.corealg.multifunction import MultiFunction, memoized_handler from ufl.domain import extract_unique_domain @@ -58,7 +59,7 @@ def __init__(self, preserve_types=()): """Initialise.""" MultiFunction.__init__(self) # Store preserve_types as boolean lookup table - self._preserve_types = [False] * Expr._ufl_num_typecodes_ + self._preserve_types = [False] * UFLRegistry().number_registered_classes for cls in preserve_types: self._preserve_types[cls._ufl_typecode_] = True diff --git a/ufl/algorithms/apply_restrictions.py b/ufl/algorithms/apply_restrictions.py index 677ee0048..a9db40f7b 100644 --- a/ufl/algorithms/apply_restrictions.py +++ b/ufl/algorithms/apply_restrictions.py @@ -105,9 +105,7 @@ def _require_restriction(self, o): if r is None: return o else: - raise ValueError( - f"Discontinuous type {o._ufl_class_.__name__} must be restricted." - ) + raise ValueError(f"Discontinuous type {type(o).__name__} must be restricted.") elif self.current_restriction in ["+", "-"]: if r not in ["+", "-"]: raise ValueError( @@ -167,9 +165,7 @@ def _opposite(self, o): if r is None: return o else: - raise ValueError( - f"Discontinuous type {o._ufl_class_.__name__} must be restricted." - ) + raise ValueError(f"Discontinuous type {type(o).__name__} must be restricted.") elif self.current_restriction in ["+", "-"]: if r is None: raise ValueError( @@ -187,7 +183,7 @@ def _opposite(self, o): def _missing_rule(self, o): """Raise an error.""" - raise ValueError(f"Missing rule for {o._ufl_class_.__name__}") + raise ValueError(f"Missing rule for {type(o).__name__}") # --- Rules for operators diff --git a/ufl/algorithms/balancing.py b/ufl/algorithms/balancing.py index 28f3fbfaa..9a366826b 100644 --- a/ufl/algorithms/balancing.py +++ b/ufl/algorithms/balancing.py @@ -20,7 +20,7 @@ from ufl.corealg.multifunction import MultiFunction modifier_precedence = { - m._ufl_handler_name_: i + m._ufl_handler_name_: i # type: ignore for i, m in enumerate( [ ReferenceValue, diff --git a/ufl/algorithms/change_to_reference.py b/ufl/algorithms/change_to_reference.py index 17b106ad7..cb0fca5f3 100644 --- a/ufl/algorithms/change_to_reference.py +++ b/ufl/algorithms/change_to_reference.py @@ -138,7 +138,7 @@ def grad(self, o): rv = True (o,) = o.ufl_operands else: - raise ValueError(f"Invalid type {o._ufl_class_.__name__}") + raise ValueError(f"Invalid type {type(o).__name__}") f = o if rv: f = ReferenceValue(f) diff --git a/ufl/algorithms/check_arities.py b/ufl/algorithms/check_arities.py index 2d51bbe1d..53839633d 100644 --- a/ufl/algorithms/check_arities.py +++ b/ufl/algorithms/check_arities.py @@ -45,7 +45,7 @@ def nonlinear_operator(self, o): for t in traverse_unique_terminals(o): if t._ufl_typecode_ == Argument._ufl_typecode_: raise ArityMismatch( - f"Applying nonlinear operator {o._ufl_class_.__name__} to " + f"Applying nonlinear operator {type(o).__name__} to " f"expression depending on form argument {t}." ) return self._et diff --git a/ufl/algorithms/compute_form_data.py b/ufl/algorithms/compute_form_data.py index 2dcf641fc..b24940767 100644 --- a/ufl/algorithms/compute_form_data.py +++ b/ufl/algorithms/compute_form_data.py @@ -151,7 +151,7 @@ def _check_facet_geometry(integral_data): for itg_data in integral_data: for itg in itg_data.integrals: for expr in traverse_unique_terminals(itg.integrand()): - cls = expr._ufl_class_ + cls = type(expr) if issubclass(cls, GeometricFacetQuantity): domain = extract_unique_domain(expr, expand_mesh_sequence=False) if isinstance(domain, MeshSequence): diff --git a/ufl/algorithms/domain_analysis.py b/ufl/algorithms/domain_analysis.py index 8a607115f..37a03d009 100644 --- a/ufl/algorithms/domain_analysis.py +++ b/ufl/algorithms/domain_analysis.py @@ -18,6 +18,7 @@ strip_coordinate_derivatives, ) from ufl.algorithms.renumbering import renumber_indices +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.domain import Mesh, sort_domains from ufl.form import Form from ufl.integral import Integral @@ -125,6 +126,10 @@ def __eq__(self, other): and self.domain_integral_type_map == other.domain_integral_type_map ) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __str__(self): """Format as a string.""" s = f"IntegralData over domain({self.integral_type}, {self.subdomain_id})" diff --git a/ufl/algorithms/estimate_degrees.py b/ufl/algorithms/estimate_degrees.py index 0c07c8507..285d0dfe2 100644 --- a/ufl/algorithms/estimate_degrees.py +++ b/ufl/algorithms/estimate_degrees.py @@ -128,14 +128,14 @@ def _max_degrees(self, v, *ops): def _not_handled(self, v, *args): """Apply to _not_handled.""" - raise ValueError(f"Missing degree handler for type {v._ufl_class_.__name__}") + raise ValueError(f"Missing degree handler for type {type(v).__name__}") def expr(self, v, *ops): """Apply to expr. For most operators we take the max degree of its operands. """ - warnings.warn(f"Missing degree estimation handler for type {v._ufl_class_.__name__}") + warnings.warn(f"Missing degree estimation handler for type {type(v).__name__}") return self._add_degrees(v, *ops) # Utility types with no degree concept diff --git a/ufl/algorithms/expand_indices.py b/ufl/algorithms/expand_indices.py index 6cbe47fe4..f24b68578 100644 --- a/ufl/algorithms/expand_indices.py +++ b/ufl/algorithms/expand_indices.py @@ -88,7 +88,7 @@ def scalar_value(self, x): if s: raise ValueError(f"Free index set mismatch, these indices have no value assigned: {s}.") - return x._ufl_class_(x.value()) + return type(x)(x.value()) def conditional(self, x): """Apply to conditional.""" diff --git a/ufl/algorithms/transformer.py b/ufl/algorithms/transformer.py index e49893e3e..d75a54e5a 100644 --- a/ufl/algorithms/transformer.py +++ b/ufl/algorithms/transformer.py @@ -118,7 +118,7 @@ def visit(self, o): def undefined(self, o): """Trigger error.""" - raise ValueError(f"No handler defined for {o._ufl_class_.__name__}.") + raise ValueError(f"No handler defined for {type(o).__name__}.") def reuse(self, o): """Reuse Expr (ignore children).""" diff --git a/ufl/argument.py b/ufl/argument.py index 043f23dc8..3a2f16e58 100644 --- a/ufl/argument.py +++ b/ufl/argument.py @@ -15,7 +15,9 @@ # Modified by Ignacia Fierro-Piccardo 2023. import numbers +from abc import ABC +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.terminal import FormArgument from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual, is_primal @@ -30,11 +32,10 @@ # --- Class representing an argument (basis function) in a form --- -class BaseArgument: +class BaseArgument(ABC): """UFL value: Representation of an argument to a form.""" __slots__ = () - _ufl_is_abstract_ = True def __getnewargs__(self): """Get new args.""" @@ -185,6 +186,10 @@ def __repr__(self): """Representation.""" return self._repr + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + @ufl_type() class Coargument(BaseForm, BaseArgument): diff --git a/ufl/averaging.py b/ufl/averaging.py index 898a45db0..46bdfb670 100644 --- a/ufl/averaging.py +++ b/ufl/averaging.py @@ -11,9 +11,7 @@ from ufl.core.ufl_type import ufl_type -@ufl_type( - inherit_shape_from_operand=0, inherit_indices_from_operand=0, num_ops=1, is_evaluation=True -) +@ufl_type() class CellAvg(Operator): """Cell average.""" @@ -42,10 +40,18 @@ def __str__(self): """Format as a string.""" return f"cell_avg({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type( - inherit_shape_from_operand=0, inherit_indices_from_operand=0, num_ops=1, is_evaluation=True -) +@ufl_type() class FacetAvg(Operator): """Facet average.""" @@ -73,3 +79,13 @@ def evaluate(self, x, mapping, component, index_values): def __str__(self): """Format as a string.""" return f"facet_avg({self.ufl_operands[0]})" + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions diff --git a/ufl/classes.py b/ufl/classes.py index bf8463d55..8f1e76a6e 100644 --- a/ufl/classes.py +++ b/ufl/classes.py @@ -14,7 +14,6 @@ # Modified by Andrew T. T. McRae, 2014 # Modified by Paul T. Kühner, 2025 -import ufl.core.expr from ufl import exproperators as __exproperators from ufl.action import Action from ufl.adjoint import Adjoint @@ -65,6 +64,7 @@ from ufl.core.multiindex import FixedIndex, Index, IndexBase, MultiIndex from ufl.core.operator import Operator from ufl.core.terminal import FormArgument, Terminal +from ufl.core.ufl_type import UFLRegistry from ufl.differentiation import ( BaseFormCoordinateDerivative, BaseFormDerivative, @@ -426,16 +426,12 @@ "ZeroBaseForm", "ZeroBaseForm", "__exproperators", - "abstract_classes", "all_ufl_classes", "nonterminal_classes", "terminal_classes", - "ufl_classes", ] # Collect all classes in sets automatically classified by some properties -all_ufl_classes = set(ufl.core.expr.Expr._ufl_all_classes_) -abstract_classes = set(c for c in all_ufl_classes if c._ufl_is_abstract_) -ufl_classes = set(c for c in all_ufl_classes if not c._ufl_is_abstract_) -terminal_classes = set(c for c in all_ufl_classes if c._ufl_is_terminal_) -nonterminal_classes = set(c for c in all_ufl_classes if not c._ufl_is_terminal_) +all_ufl_classes = set(UFLRegistry().all_classes) +terminal_classes = set(c for c in all_ufl_classes if c._ufl_is_terminal_) # type: ignore +nonterminal_classes = set(c for c in all_ufl_classes if not c._ufl_is_terminal_) # type: ignore diff --git a/ufl/coefficient.py b/ufl/coefficient.py index 0bcf2d020..121d2b840 100644 --- a/ufl/coefficient.py +++ b/ufl/coefficient.py @@ -12,8 +12,9 @@ # Modified by Ignacia Fierro-Piccardo 2023. from ufl.argument import Argument +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.terminal import FormArgument -from ufl.core.ufl_type import ufl_type +from ufl.core.ufl_type import UFLType, ufl_type from ufl.duals import is_dual, is_primal from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace, MixedFunctionSpace @@ -23,7 +24,7 @@ # --- The Coefficient class represents a coefficient in a form --- -class BaseCoefficient(Counted): +class BaseCoefficient(UFLType, Counted): """UFL form argument type: Parent Representation of a form coefficient.""" # Slots are disabled here because they cause trouble in PyDOLFIN @@ -31,7 +32,6 @@ class BaseCoefficient(Counted): # __slots__ = ("_count", "_ufl_function_space", "_repr", "_ufl_shape") _ufl_noslots_ = True __slots__ = () - _ufl_is_abstract_ = True def __getnewargs__(self): """Get new args.""" @@ -100,6 +100,10 @@ def __eq__(self, other): return True return self._count == other._count and self._ufl_function_space == other._ufl_function_space + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + @ufl_type() class Cofunction(BaseCoefficient, BaseForm): @@ -197,6 +201,10 @@ def __eq__(self, other): return True return self._count == other._count and self._ufl_function_space == other._ufl_function_space + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __repr__(self): """Representation.""" return self._repr diff --git a/ufl/conditional.py b/ufl/conditional.py index 503e861fd..ebfeb6d52 100644 --- a/ufl/conditional.py +++ b/ufl/conditional.py @@ -21,10 +21,13 @@ # is a boolean type not a float type -@ufl_type(is_abstract=True, is_scalar=True) +@ufl_type() class Condition(Operator): """Condition.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __init__(self, operands): @@ -39,7 +42,7 @@ def __bool__(self): __nonzero__ = __bool__ -@ufl_type(is_abstract=True, num_ops=2) +@ufl_type() class BinaryCondition(Condition): """Binary condition.""" @@ -130,7 +133,7 @@ def __bool__(self): __nonzero__ = __bool__ -@ufl_type(binop="__le__") +@ufl_type() class LE(BinaryCondition): """Less than or equal condition.""" @@ -147,7 +150,7 @@ def evaluate(self, x, mapping, component, index_values): return bool(a <= b) -@ufl_type(binop="__ge__") +@ufl_type() class GE(BinaryCondition): """Greater than or equal to condition.""" @@ -164,7 +167,7 @@ def evaluate(self, x, mapping, component, index_values): return bool(a >= b) -@ufl_type(binop="__lt__") +@ufl_type() class LT(BinaryCondition): """Less than condition.""" @@ -181,7 +184,7 @@ def evaluate(self, x, mapping, component, index_values): return bool(a < b) -@ufl_type(binop="__gt__") +@ufl_type() class GT(BinaryCondition): """Greater than condition.""" @@ -232,7 +235,7 @@ def evaluate(self, x, mapping, component, index_values): return bool(a or b) -@ufl_type(num_ops=1) +@ufl_type() class NotCondition(Condition): """Not condition.""" @@ -254,7 +257,7 @@ def __str__(self): return f"!({self.ufl_operands[0]!s})" -@ufl_type(num_ops=3, inherit_shape_from_operand=1, inherit_indices_from_operand=1) +@ufl_type() class Conditional(Operator): """Conditional expression. @@ -316,14 +319,32 @@ def __str__(self): """Format as a string.""" return "{} ? {} : {}".format(*tuple(parstr(o, self) for o in self.ufl_operands)) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[1].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[1].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[1].ufl_index_dimensions + # --- Specific functions higher level than a conditional --- -@ufl_type(is_scalar=True, num_ops=1) +@ufl_type() class MinValue(Operator): """Take the minimum of two values.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __init__(self, left, right): @@ -352,10 +373,13 @@ def __str__(self): return "min_value({}, {})".format(*self.ufl_operands) -@ufl_type(is_scalar=True, num_ops=1) +@ufl_type() class MaxValue(Operator): """Take the maximum of two values.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __init__(self, left, right): diff --git a/ufl/constant.py b/ufl/constant.py index ce0f0a2f5..c66f2d5c3 100644 --- a/ufl/constant.py +++ b/ufl/constant.py @@ -6,6 +6,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.domain import as_domain @@ -67,6 +68,10 @@ def __eq__(self, other): and self._ufl_shape == self._ufl_shape ) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def _ufl_signature_data_(self, renumbering): """Signature data for constant depends on renumbering.""" return ( diff --git a/ufl/constantvalue.py b/ufl/constantvalue.py index 324ee9b64..7ebe7d413 100644 --- a/ufl/constantvalue.py +++ b/ufl/constantvalue.py @@ -16,6 +16,7 @@ # --- Helper functions imported here for compatibility--- from ufl.checks import is_python_scalar, is_true_ufl_scalar, is_ufl_scalar # noqa: F401 +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.expr import Expr from ufl.core.multiindex import FixedIndex, Index from ufl.core.terminal import Terminal @@ -36,7 +37,7 @@ def format_float(x): # --- Base classes for constant types --- -@ufl_type(is_abstract=True) +@ufl_type() class ConstantValue(Terminal): """Constant value.""" @@ -56,13 +57,14 @@ def ufl_domains(self): # TODO: Add geometric dimension/domain and Argument dependencies to Zero? -@ufl_type(is_literal=True) +@ufl_type() class Zero(ConstantValue): """Representation of a zero valued expression. Class for representing zero tensors of different shapes. """ + _ufl_is_literal_ = True __slots__ = ("ufl_free_indices", "ufl_index_dimensions", "ufl_shape") _cache: dict[tuple[int], "Zero"] = {} @@ -161,6 +163,10 @@ def __eq__(self, other): else: return False + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __neg__(self): """Negate.""" return self @@ -199,10 +205,13 @@ def zero(*shape): # --- Scalar value types --- -@ufl_type(is_abstract=True, is_scalar=True) +@ufl_type() class ScalarValue(ConstantValue): """A constant scalar value.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = ("_value",) def __init__(self, value): @@ -230,7 +239,7 @@ def __eq__(self, other): can still succeed. These will however not have the same hash value and therefore not collide in a dict. """ - if isinstance(other, self._ufl_class_): + if isinstance(other, type(self)): return self._value == other._value elif isinstance(other, int | float): # FIXME: Disallow this, require explicit 'expr == @@ -239,6 +248,10 @@ def __eq__(self, other): else: return False + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __str__(self): """Format as a string.""" return str(self._value) @@ -272,10 +285,11 @@ def imag(self): return self._value.imag -@ufl_type(wraps_type=complex, is_literal=True) +@ufl_type() class ComplexValue(ScalarValue): """Representation of a constant, complex scalar.""" + _ufl_is_literal_ = True __slots__ = () def __getnewargs__(self): @@ -318,17 +332,21 @@ def __int__(self): raise TypeError("ComplexValues cannot be cast to int") -@ufl_type(is_abstract=True, is_scalar=True) +@ufl_type() class RealValue(ScalarValue): """Abstract class used to differentiate real values from complex ones.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () -@ufl_type(wraps_type=float, is_literal=True) +@ufl_type() class FloatValue(RealValue): """Representation of a constant scalar floating point value.""" + _ufl_is_literal_ = True __slots__ = () def __getnewargs__(self): @@ -352,10 +370,11 @@ def __repr__(self): return r -@ufl_type(wraps_type=int, is_literal=True) +@ufl_type() class IntValue(RealValue): """Representation of a constant scalar integer value.""" + _ufl_is_literal_ = True __slots__ = () _cache: dict[int, "IntValue"] = {} @@ -436,6 +455,10 @@ def __eq__(self, other): """Check equalty.""" return isinstance(other, Identity) and self._dim == other._dim + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + # --- Permutation symbol --- @@ -480,6 +503,10 @@ def __eq__(self, other): """Check equalty.""" return isinstance(other, PermutationSymbol) and self._dim == other._dim + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __eps(self, x): """Get eps. diff --git a/ufl/core/base_form_operator.py b/ufl/core/base_form_operator.py index edeeb09e5..9a7fdd763 100644 --- a/ufl/core/base_form_operator.py +++ b/ufl/core/base_form_operator.py @@ -18,6 +18,7 @@ from ufl.argument import Argument, Coargument from ufl.constantvalue import as_ufl +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual @@ -28,7 +29,7 @@ __all__ = ["BaseFormOperator"] -@ufl_type(num_ops="varying", is_differential=True) +@ufl_type() class BaseFormOperator(Operator, BaseForm, Counted): """Base form operator.""" @@ -182,15 +183,20 @@ def __repr__(self): return r def __hash__(self): - """Hash code for use in dicts.""" - hashdata = ( - type(self), - tuple(hash(op) for op in self.ufl_operands), - tuple(hash(arg) for arg in self._argument_slots), - self.derivatives, - hash(self.ufl_function_space()), - ) - return hash(hashdata) + """Return hash.""" + return compute_expr_hash(self) + + # TODO: is this not needed? + # def __hash__(self): + # """Hash code for use in dicts.""" + # hashdata = ( + # type(self), + # tuple(hash(op) for op in self.ufl_operands), + # tuple(hash(arg) for arg in self._argument_slots), + # self.derivatives, + # hash(self.ufl_function_space()), + # ) + # return hash(hashdata) def __eq__(self, other): """Check for equality.""" diff --git a/ufl/core/expr.py b/ufl/core/expr.py index 70dfcfd0f..09748836d 100644 --- a/ufl/core/expr.py +++ b/ufl/core/expr.py @@ -20,12 +20,14 @@ import warnings if typing.TYPE_CHECKING: - import ufl.core.terminal + from ufl.core.terminal import FormArgument -from ufl.core.ufl_type import UFLObject, UFLType, update_ufl_type_attributes +from ufl.core.compute_expr_hash import compute_expr_hash +from ufl.core.ufl_type import UFLRegistry, UFLType, ufl_type -class Expr(metaclass=UFLType): +@ufl_type() +class Expr(UFLType): """Base class for all UFL expression types. *Instance properties* @@ -86,10 +88,17 @@ class MyOperator(Operator): # --- the top --- # This is to freeze member variables for objects of this class and # save memory by skipping the per-instance dict. + ufl_operands: tuple["FormArgument", ...] + ufl_shape: tuple[int, ...] + ufl_free_indices: tuple[int, ...] + ufl_index_dimensions: tuple - __slots__ = ("__weakref__", "_hash") + _ufl_is_terminal_modifier_: bool = False + _ufl_is_in_reference_frame_: bool = False # _ufl_noslots_ = True + __slots__ = ("__weakref__", "_hash") + # --- Basic object behaviour --- def __getnewargs__(self): @@ -107,118 +116,7 @@ def __init__(self): """Initialise.""" self._hash = None - # This shows the principal behaviour of the hash function attached - # in ufl_type: - # def __hash__(self): - # if self._hash is None: - # self._hash = self._ufl_compute_hash_() - # return self._hash - - # --- Type traits are added to subclasses by the ufl_type class - # --- decorator --- - - # Note: Some of these are modified after the Expr class definition - # because Expr is not defined yet at this point. Note: Boolean - # type traits that categorize types are mostly set to None for - # Expr but should be True or False for any non-abstract type. - - # A reference to the UFL class itself. This makes it possible to - # do type(f)._ufl_class_ and be sure you get the actual UFL class - # instead of a subclass from another library. - _ufl_class_: type = None # type: ignore - - # The handler name. This is the name of the handler function you - # implement for this type in a multifunction. - _ufl_handler_name_ = "expr" - - # Number of operands, "varying" for some types, or None if not - # applicable for abstract types. - _ufl_num_ops_ = None - - # Type trait: If the type is a literal. - _ufl_is_literal_ = None - - _ufl_is_terminal_: bool - - # Type trait: If the type is classified as a 'terminal modifier', - # for form compiler use. - _ufl_is_terminal_modifier_ = None - - # Type trait: If the type is a shaping operator. Shaping - # operations include indexing, slicing, transposing, i.e. not - # introducing computation of a new value. - _ufl_is_shaping_ = False - - # Type trait: If the type is in reference frame. - _ufl_is_in_reference_frame_ = None - - # Type trait: If the type is a restriction to a geometric entity. - _ufl_is_restriction_ = None - - # Type trait: If the type is evaluation in a particular way. - _ufl_is_evaluation_ = None - - # Type trait: If the type is a differential operator. - _ufl_is_differential_ = None - - # Type trait: If the type is purely scalar, having no shape or - # indices. - _ufl_is_scalar_ = None - - # Type trait: If the type never has free indices. - _ufl_is_index_free_ = False - - # --- All subclasses must define these object attributes --- - - # Each subclass of Expr is checked to have these properties in - # ufl_type - _ufl_required_properties_ = ( - # A tuple of operands, all of them Expr instances. - "ufl_operands", - # A tuple of ints, the value shape of the expression. - "ufl_shape", - # A tuple of free index counts. - "ufl_free_indices", - # A tuple providing the int dimension for each free index. - "ufl_index_dimensions", - ) - - ufl_operands: tuple["ufl.core.terminal.FormArgument", ...] - ufl_shape: tuple[int, ...] - _ufl_typecode_: int - ufl_free_indices: tuple[int, ...] - - # Each subclass of Expr is checked to have these methods in - # ufl_type - # FIXME: Add more and enable all - _ufl_required_methods_: tuple[str, ...] = ( - # To compute the hash on demand, this method is called. - "_ufl_compute_hash_", - # The data returned from this method is used to compute the - # signature of a form - "_ufl_signature_data_", - # The == operator must be implemented to compare for identical - # representation, used by set() and dict(). The __hash__ - # operator is added by ufl_type. - "__eq__", - # To reconstruct an object of the same type with operands or - # properties changed. - "_ufl_expr_reconstruct_", # Implemented in Operator and Terminal so this should never fail - "ufl_domains", - # "ufl_cell", - # "ufl_domain", - # "__str__", - # "__repr__", - ) - - # --- Global variables for collecting all types --- - - # A global dict mapping language_operator_name to the type it - # produces - _ufl_language_operators_: dict[str, object] = {} - # List of all terminal modifier types - _ufl_terminal_modifiers_: list[UFLObject] = [] # --- Mechanism for profiling object creation and deletion --- @@ -228,27 +126,24 @@ def __init__(self): def _ufl_profiling__init__(self): """Replacement constructor with object counting.""" Expr._ufl_regular__init__(self) - Expr._ufl_obj_init_counts_[self._ufl_typecode_] += 1 + UFLRegistry().register_object_creation(type(self)) def _ufl_profiling__del__(self): """Replacement destructor with object counting.""" - Expr._ufl_obj_del_counts_[self._ufl_typecode_] -= 1 + UFLRegistry().register_object_destruction(type(self)) @staticmethod def ufl_enable_profiling(): """Turn on the object counting mechanism and reset counts to zero.""" Expr.__init__ = Expr._ufl_profiling__init__ setattr(Expr, "__del__", Expr._ufl_profiling__del__) - for i in range(len(Expr._ufl_obj_init_counts_)): - Expr._ufl_obj_init_counts_[i] = 0 - Expr._ufl_obj_del_counts_[i] = 0 + UFLRegistry().reset_object_tracking() @staticmethod def ufl_disable_profiling(): """Turn off the object counting mechanism. Return object init and del counts.""" Expr.__init__ = Expr._ufl_regular__init__ delattr(Expr, "__del__") - return (Expr._ufl_obj_init_counts_, Expr._ufl_obj_del_counts_) # === Abstract functions that must be implemented by subclasses === @@ -284,7 +179,7 @@ def ufl_domain(self): def evaluate(self, x, mapping, component, index_values): """Evaluate expression at given coordinate with given values for terminals.""" - raise ValueError(f"Symbolic evaluation of {self._ufl_class_.__name__} not available.") + raise ValueError(f"Symbolic evaluation of {type(self).__name__} not available.") def _ufl_evaluate_scalar_(self): if self.ufl_shape or self.ufl_free_indices: @@ -351,7 +246,7 @@ def __str__(self): def _ufl_err_str_(self): """Return a short string to represent this Expr in an error message.""" - return f"<{self._ufl_class_.__name__} id={id(self)}>" + return f"<{type(self).__name__} id={id(self)}>" def _simplify_indexed(self, multiindex): """Return a simplified Expr used in the constructor of Indexed(self, multiindex).""" @@ -367,6 +262,10 @@ def __eq__(self, other): """ raise NotImplementedError(self.__class__.__eq__) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __len__(self): """Length of expression. Used for iteration over vector expressions.""" s = self.ufl_shape @@ -491,15 +390,6 @@ def __getitem__(self, index): pass -# Initializing traits here because Expr is not defined in the class -# declaration -Expr._ufl_class_ = Expr # type: ignore - -# Update Expr with metaclass properties (e.g. typecode or handler name) -# Explicitly done here instead of using `@ufl_type` to avoid circular imports. -update_ufl_type_attributes(Expr) - - def ufl_err_str(expr): """Return a UFL error string.""" if hasattr(expr, "_ufl_err_str_"): diff --git a/ufl/core/external_operator.py b/ufl/core/external_operator.py index 4aa11470f..1627c451e 100644 --- a/ufl/core/external_operator.py +++ b/ufl/core/external_operator.py @@ -14,10 +14,11 @@ # Modified by Nacime Bouziani, 2023 from ufl.core.base_form_operator import BaseFormOperator +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.ufl_type import ufl_type -@ufl_type(num_ops="varying", is_differential=True) +@ufl_type() class ExternalOperator(BaseFormOperator): """External operator.""" @@ -82,9 +83,7 @@ def grad(self): def assemble(self, *args, **kwargs): """Assemble the external operator.""" - raise NotImplementedError( - f"Symbolic evaluation of {self._ufl_class_.__name__} not available." - ) + raise NotImplementedError(f"Symbolic evaluation of {type(self).__name__} not available.") def _ufl_expr_reconstruct_( self, *operands, function_space=None, derivatives=None, argument_slots=None, add_kwargs={} @@ -121,3 +120,7 @@ def __eq__(self, other): and self.derivatives == other.derivatives and self.ufl_function_space() == other.ufl_function_space() ) + + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) diff --git a/ufl/core/interpolate.py b/ufl/core/interpolate.py index fada73491..24ea2f039 100644 --- a/ufl/core/interpolate.py +++ b/ufl/core/interpolate.py @@ -11,13 +11,14 @@ from ufl.argument import Argument, Coargument from ufl.constantvalue import as_ufl from ufl.core.base_form_operator import BaseFormOperator +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace -@ufl_type(num_ops="varying", is_differential=True) +@ufl_type() class Interpolate(BaseFormOperator): """Symbolic representation of the interpolation operator.""" @@ -103,6 +104,10 @@ def __eq__(self, other): and self.ufl_function_space() == other.ufl_function_space() ) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + # Helper function def interpolate(expr, v): diff --git a/ufl/core/multiindex.py b/ufl/core/multiindex.py index 6416cc4d1..dbf5796fe 100644 --- a/ufl/core/multiindex.py +++ b/ufl/core/multiindex.py @@ -8,6 +8,7 @@ # # Modified by Massimiliano Leoni, 2016. +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.utils.counted import Counted @@ -179,6 +180,10 @@ def evaluate(self, x, mapping, component, index_values): component.append(index_values[i]) return tuple(component) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + @property def ufl_shape(self): """Get the UFL shape. diff --git a/ufl/core/operator.py b/ufl/core/operator.py index a6da722ca..d049ca57e 100644 --- a/ufl/core/operator.py +++ b/ufl/core/operator.py @@ -12,7 +12,7 @@ from ufl.core.ufl_type import ufl_type -@ufl_type(is_abstract=True, is_terminal=False) +@ufl_type() class Operator(Expr): """Base class for all operators, i.e. non-terminal expression types.""" @@ -31,7 +31,7 @@ def __init__(self, operands=None): def _ufl_expr_reconstruct_(self, *operands): """Return a new object of the same type with new operands.""" - return self._ufl_class_(*operands) + return type(self)(*operands) def _ufl_signature_data_(self): """Get UFL signature data.""" @@ -44,4 +44,4 @@ def _ufl_compute_hash_(self): def __repr__(self): """Default repr string construction for operators.""" # This should work for most cases - return f"{self._ufl_class_.__name__}({', '.join(map(repr, self.ufl_operands))})" + return f"{type(self).__name__}({', '.join(map(repr, self.ufl_operands))})" diff --git a/ufl/core/terminal.py b/ufl/core/terminal.py index 47c6c16d9..93d0e5790 100644 --- a/ufl/core/terminal.py +++ b/ufl/core/terminal.py @@ -13,17 +13,19 @@ import warnings +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.expr import Expr from ufl.core.ufl_type import ufl_type -@ufl_type(is_abstract=True, is_terminal=True) +@ufl_type() class Terminal(Expr): """Base class for terminal objects. A terminal node in the UFL expression tree. """ + _ufl_is_terminal_ = True __slots__ = () def __init__(self): @@ -92,11 +94,15 @@ def __eq__(self, other): """Default comparison of terminals just compare repr strings.""" return repr(self) == repr(other) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + # --- Subgroups of terminals --- -@ufl_type(is_abstract=True) +@ufl_type() class FormArgument(Terminal): """An abstract class for a form argument (a thing in a primal finite element space).""" diff --git a/ufl/core/ufl_type.py b/ufl/core/ufl_type.py index 31c502a97..ab0e0af85 100644 --- a/ufl/core/ufl_type.py +++ b/ufl/core/ufl_type.py @@ -13,14 +13,14 @@ import abc import typing -import ufl.core as core -from ufl.core.compute_expr_hash import compute_expr_hash from ufl.utils.formatting import camel2underscore class UFLObject(abc.ABC): """A UFL Object.""" + _ufl_is_terminal_: bool + @abc.abstractmethod def _ufl_hash_data_(self) -> typing.Hashable: """Return hashable data that uniquely defines this object.""" @@ -49,392 +49,94 @@ def __ne__(self, other): return not self.__eq__(other) -def get_base_attr(cls, name): - """Return first non-``None`` attribute of given name among base classes.""" - for base in cls.mro(): - if hasattr(base, name): - attr = getattr(base, name) - if attr is not None: - return attr - return None - - -def set_trait(cls, basename, value, inherit=False): - """Assign a trait to class with namespacing ``_ufl_basename_`` applied. - - If trait value is ``None``, optionally inherit it from the closest - base class that has it. - """ - name = "_ufl_" + basename + "_" - if value is None and inherit: - value = get_base_attr(cls, name) - setattr(cls, name, value) - - -def determine_num_ops(cls, num_ops, unop, binop, rbinop): - """Determine number of operands for this type.""" - # Try to determine num_ops from other traits or baseclass, or - # require num_ops to be set for non-abstract classes if it cannot - # be determined automatically - if num_ops is not None: - return num_ops - elif cls._ufl_is_terminal_: - return 0 - elif unop: - return 1 - elif binop or rbinop: - return 2 - else: - # Determine from base class - return get_base_attr(cls, "_ufl_num_ops_") - - -def check_is_terminal_consistency(cls): - """Check for consistency in ``is_terminal`` trait among superclasses.""" - if cls._ufl_is_terminal_ is None: - msg = ( - f"Class {cls.__name__} has not specified the is_terminal trait." - " Did you forget to inherit from Terminal or Operator?" - ) - raise TypeError(msg) - - base_is_terminal = get_base_attr(cls, "_ufl_is_terminal_") - if base_is_terminal is not None and cls._ufl_is_terminal_ != base_is_terminal: - msg = ( - f"Conflicting given and automatic 'is_terminal' trait for class {cls.__name__}." - " Check if you meant to inherit from Terminal or Operator." - ) - raise TypeError(msg) - - -def check_abstract_trait_consistency(cls): - """Check that the first base classes up to ``Expr`` are other UFL types.""" - for base in cls.mro(): - if base is core.expr.Expr: - break - if not issubclass(base, core.expr.Expr) and base._ufl_is_abstract_: - msg = ( - "Base class {0.__name__} of class {1.__name__} " - "is not an abstract subclass of {2.__name__}." - ) - raise TypeError(msg.format(base, cls, core.expr.Expr)) - - -def check_has_slots(cls): - """Check if type has __slots__ unless it is marked as exception with _ufl_noslots_.""" - if "_ufl_noslots_" in cls.__dict__: - return - - if "__slots__" not in cls.__dict__: - msg = ( - "Class {0.__name__} is missing the __slots__ " - "attribute and is not marked with _ufl_noslots_." - ) - raise TypeError(msg.format(cls)) - - # Check base classes for __slots__ as well, skipping object which is the last one - for base in cls.mro()[1:-1]: - if "__slots__" not in base.__dict__: - msg = "Class {0.__name__} is has a base class {1.__name__} with __slots__ missing." - raise TypeError(msg.format(cls, base)) - - -def check_type_traits_consistency(cls): - """Execute a variety of consistency checks on the ufl type traits.""" - # Check for consistency in global type collection sizes - Expr = core.expr.Expr - assert Expr._ufl_num_typecodes_ == len(Expr._ufl_all_handler_names_) - assert Expr._ufl_num_typecodes_ == len(Expr._ufl_all_classes_) - assert Expr._ufl_num_typecodes_ == len(Expr._ufl_obj_init_counts_) - assert Expr._ufl_num_typecodes_ == len(Expr._ufl_obj_del_counts_) - - # Check that non-abstract types always specify num_ops - if not cls._ufl_is_abstract_: - if cls._ufl_num_ops_ is None: - msg = "Class {0.__name__} has not specified num_ops." - raise TypeError(msg.format(cls)) - - # Check for non-abstract types that num_ops has the right type - if not cls._ufl_is_abstract_: - if not (isinstance(cls._ufl_num_ops_, int) or cls._ufl_num_ops_ == "varying"): - msg = 'Class {0.__name__} has invalid num_ops value {1} (integer or "varying").' - raise TypeError(msg.format(cls, cls._ufl_num_ops_)) - - # Check that num_ops is not set to nonzero for a terminal - if cls._ufl_is_terminal_ and cls._ufl_num_ops_ != 0: - msg = "Class {0.__name__} has num_ops > 0 but is terminal." - raise TypeError(msg.format(cls)) - - # Check that a non-scalar type doesn't have a scalar base class. - if not cls._ufl_is_scalar_: - if get_base_attr(cls, "_ufl_is_scalar_"): - msg = "Non-scalar class {0.__name__} is has a scalar base class." - raise TypeError(msg.format(cls)) - - -def check_implements_required_methods(cls): - """Check if type implements the required methods.""" - if not cls._ufl_is_abstract_: - for attr in core.expr.Expr._ufl_required_methods_: - if not hasattr(cls, attr): - msg = "Class {0.__name__} has no {1} method." - raise TypeError(msg.format(cls, attr)) - elif not callable(getattr(cls, attr)): - msg = "Required method {1} of class {0.__name__} is not callable." - raise TypeError(msg.format(cls, attr)) - - -def check_implements_required_properties(cls): - """Check if type implements the required properties.""" - if not cls._ufl_is_abstract_: - for attr in core.expr.Expr._ufl_required_properties_: - if not hasattr(cls, attr): - msg = "Class {0.__name__} has no {1} property." - raise TypeError(msg.format(cls, attr)) - elif callable(getattr(cls, attr)): - msg = "Required property {1} of class {0.__name__} is a callable method." - raise TypeError(msg.format(cls, attr)) - - -def attach_implementations_of_indexing_interface( - cls, inherit_shape_from_operand, inherit_indices_from_operand -): - """Attach implementations of indexing interface.""" - # Scalar or index-free? Then we can simplify the implementation of - # tensor properties by attaching them here. - if cls._ufl_is_scalar_: - cls.ufl_shape = () - - if cls._ufl_is_scalar_ or cls._ufl_is_index_free_: - cls.ufl_free_indices = () - cls.ufl_index_dimensions = () - - # Automate direct inheriting of shape and indices from one of the - # operands. This simplifies refactoring because a lot of types do - # this. - if inherit_shape_from_operand is not None: - - def _inherited_ufl_shape(self): - return self.ufl_operands[inherit_shape_from_operand].ufl_shape - - cls.ufl_shape = property(_inherited_ufl_shape) - - if inherit_indices_from_operand is not None: - - def _inherited_ufl_free_indices(self): - return self.ufl_operands[inherit_indices_from_operand].ufl_free_indices - - def _inherited_ufl_index_dimensions(self): - return self.ufl_operands[inherit_indices_from_operand].ufl_index_dimensions - - cls.ufl_free_indices = property(_inherited_ufl_free_indices) - cls.ufl_index_dimensions = property(_inherited_ufl_index_dimensions) - - -def update_global_expr_attributes(cls): - """Update global attributres. - - Update global ``Expr`` attributes, mainly by adding *cls* to global - collections of ufl types. - """ - if cls._ufl_is_terminal_modifier_: - core.expr.Expr._ufl_terminal_modifiers_.append(cls) - - # Add to collection of language operators. This collection is - # used later to populate the official language namespace. - # TODO: I don't think this functionality is fully completed, check - # it out later. - if not cls._ufl_is_abstract_ and hasattr(cls, "_ufl_function_"): - cls._ufl_function_.__func__.__doc__ = cls.__doc__ - core.expr.Expr._ufl_language_operators_[cls._ufl_handler_name_] = cls._ufl_function_ - - -def update_ufl_type_attributes(cls): - """Update UFL type attributes.""" - # Determine integer typecode by incrementally counting all types - cls._ufl_typecode_ = UFLType._ufl_num_typecodes_ - UFLType._ufl_num_typecodes_ += 1 - - UFLType._ufl_all_classes_.append(cls) - - # Determine handler name by a mapping from "TypeName" to "type_name" - cls._ufl_handler_name_ = camel2underscore(cls.__name__) - UFLType._ufl_all_handler_names_.add(cls._ufl_handler_name_) - - # Append space for counting object creation and destriction of - # this this type. - UFLType._ufl_obj_init_counts_.append(0) - UFLType._ufl_obj_del_counts_.append(0) - - -def ufl_type( - is_abstract=False, - is_terminal=None, - is_scalar=False, - is_index_free=False, - is_shaping=False, - is_literal=False, - is_terminal_modifier=False, - is_in_reference_frame=False, - is_restriction=False, - is_evaluation=False, - is_differential=None, - use_default_hash=True, - num_ops=None, - inherit_shape_from_operand=None, - inherit_indices_from_operand=None, - wraps_type=None, - unop=None, - binop=None, - rbinop=None, -): - """Decorator to apply to every subclass in the UFL ``Expr`` and ``BaseForm`` hierarchy. - - This decorator contains a number of checks that are intended to - enforce uniform behaviour across UFL types. - - The rationale behind the checks and the meaning of the optional - arguments should be sufficiently documented in the source code - below. - """ +def ufl_type(): + """Decorator to apply to every subclass in the UFL ``Expr`` and ``BaseForm`` hierarchy.""" def _ufl_type_decorator_(cls): """UFL type decorator.""" # Update attributes for UFLType instances (BaseForm and Expr objects) - update_ufl_type_attributes(cls) - if not issubclass(cls, core.expr.Expr): - # Don't need anything else for non Expr subclasses - return cls - - # is_scalar implies is_index_freeg - if is_scalar: - _is_index_free = True - else: - _is_index_free = is_index_free - - # Store type traits - cls._ufl_class_ = cls - set_trait(cls, "is_abstract", is_abstract, inherit=False) - - set_trait(cls, "is_terminal", is_terminal, inherit=True) - set_trait(cls, "is_literal", is_literal, inherit=True) - set_trait(cls, "is_terminal_modifier", is_terminal_modifier, inherit=True) - set_trait(cls, "is_shaping", is_shaping, inherit=True) - set_trait(cls, "is_in_reference_frame", is_in_reference_frame, inherit=True) - set_trait(cls, "is_restriction", is_restriction, inherit=True) - set_trait(cls, "is_evaluation", is_evaluation, inherit=True) - set_trait(cls, "is_differential", is_differential, inherit=True) - - set_trait(cls, "is_scalar", is_scalar, inherit=True) - set_trait(cls, "is_index_free", _is_index_free, inherit=True) - - # Number of operands can often be determined automatically - _num_ops = determine_num_ops(cls, num_ops, unop, binop, rbinop) - set_trait(cls, "num_ops", _num_ops) - - # Attach builtin type wrappers to Expr - """# These are currently handled in the as_ufl implementation in constantvalue.py - if wraps_type is not None: - if not isinstance(wraps_type, type): - msg = "Expecting a type, not a {0.__name__} for the - wraps_type argument in definition of {1.__name__}." - raise TypeError(msg.format(type(wraps_type), cls)) - - def _ufl_from_type_(value): - return cls(value) - from_type_name = "_ufl_from_{0}_".format(wraps_type.__name__) - setattr(Expr, from_type_name, staticmethod(_ufl_from_type_)) - """ - - # Attach special function to Expr. - # Avoids the circular dependency problem of making - # Expr.__foo__ return a Foo that is a subclass of Expr. - """# These are currently attached in exproperators.py - if unop: - def _ufl_expr_unop_(self): - return cls(self) - setattr(Expr, unop, _ufl_expr_unop_) - if binop: - def _ufl_expr_binop_(self, other): - try: - other = Expr._ufl_coerce_(other) - except: - return NotImplemented - return cls(self, other) - setattr(Expr, binop, _ufl_expr_binop_) - if rbinop: - def _ufl_expr_rbinop_(self, other): - try: - other = Expr._ufl_coerce_(other) - except: - return NotImplemented - return cls(other, self) - setattr(Expr, rbinop, _ufl_expr_rbinop_) - """ - - # Make sure every non-abstract class has its own __hash__ and - # __eq__. Python 3 will set __hash__ to None if cls has - # __eq__, but we've implemented it in a separate function and - # want to inherit/use that for all types. Allow overriding by - # setting use_default_hash=False. - if use_default_hash: - cls.__hash__ = compute_expr_hash - - # NB! This function conditionally adds some methods to the - # class! This approach significantly reduces the amount of - # small functions to implement across all the types but of - # course it's a bit more opaque. - attach_implementations_of_indexing_interface( - cls, inherit_shape_from_operand, inherit_indices_from_operand - ) - - # Update Expr - update_global_expr_attributes(cls) - - # Apply a range of consistency checks to detect bugs in type - # implementations that Python doesn't check for us, including - # some checks that a static language compiler would do for us - check_abstract_trait_consistency(cls) - check_has_slots(cls) - check_is_terminal_consistency(cls) - check_implements_required_methods(cls) - check_implements_required_properties(cls) - check_type_traits_consistency(cls) + # Determine integer typecode by incrementally counting all types + cls._ufl_typecode_ = UFLRegistry().number_registered_classes + UFLRegistry().register_class(cls) + + assert UFLRegistry().number_registered_classes == len(UFLRegistry().all_classes) + + # Determine handler name by a mapping from "TypeName" to "type_name" + cls._ufl_handler_name_ = camel2underscore(cls.__name__) return cls return _ufl_type_decorator_ -class UFLType(type): - """Base class for all UFL types. +class UFLRegistry: + """Maintains global informations of the registered types.""" - Equip UFL types with some ufl specific properties. - """ + _instance: UFLRegistry | None = None + _all_classes: list[type] - # A global counter of the number of typecodes assigned. - _ufl_num_typecodes_ = 0 + # TODO: profiling should be maintained in an own profiling class/registry + _obj_tracking: dict[type, tuple[int, int]] - # Set the handler name for UFLType - _ufl_handler_name_ = "ufl_type" + def __new__(cls) -> UFLRegistry: + """Create singleton UFLRegistry.""" + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._all_classes = [] + cls._instance._obj_tracking = {} + return cls._instance - # A global array of all Expr and BaseForm subclasses, indexed by typecode - _ufl_all_classes_: list[UFLType] = [] + @property + def all_classes(self) -> list[type]: # list[UFLType] + """Return list of all Expr and BaseForm subclasses, indexed by typecode.""" + return self._all_classes + + def register_class(self, c: type) -> None: + """Register an UFLType with the registry.""" + assert c not in self.all_classes + self._all_classes.append(c) + + @property + def number_registered_classes(self) -> int: + """Return number of registered classes.""" + return len(self._all_classes) + + def register_object_creation(self, c: type) -> None: + """Profiling.""" + data = self._obj_tracking.get(c, (0, 0)) + self._obj_tracking[c] = (data[0] + 1, data[1]) + + def register_object_destruction(self, c: type) -> None: + """Profiling.""" + data = self._obj_tracking.get(c, (0, 0)) + self._obj_tracking[c] = (data[0], data[1] - 1) + + def reset_object_tracking(self) -> None: + """Profiling.""" + self._obj_tracking = {} + + @property + def object_tracking(self) -> dict[type, tuple[int, int]]: + """Profiling.""" + return self._obj_tracking + + +class UFLType: + """Base class for all UFL types. + + Equip UFL types with some ufl specific properties. + """ - # A global set of all handler names added - _ufl_all_handler_names_: set[str] = set() + # TODO: can we move this assignment into __new__? + _ufl_typecode_: int - # A global array of the number of initialized objects for each - # typecode - _ufl_obj_init_counts_: list[int] = [] + __slots__: tuple[str, ...] = () - # A global array of the number of deleted objects for each - # typecode - _ufl_obj_del_counts_: list[int] = [] + # TODO: ufl_handler_name type name -> remove + _ufl_handler_name_: str = "ufl_type" - # Type trait: If the type is abstract. An abstract class cannot - # be instantiated and does not need all properties specified. - _ufl_is_abstract_ = True + # TODO: _ufl_is_terminal iff. is Cofunction or Terminal -> remove + _ufl_is_terminal_: bool = False - # Type trait: If the type is terminal. - _ufl_is_terminal_: bool = None # type: ignore + # TODO:_ufl_is_literal_ iff. is Zero, ComplexValue, FloatValue or IntValue -> remove + _ufl_is_literal_: bool = False diff --git a/ufl/corealg/map_dag.py b/ufl/corealg/map_dag.py index 4b1d8bf24..cf6a298d4 100644 --- a/ufl/corealg/map_dag.py +++ b/ufl/corealg/map_dag.py @@ -7,7 +7,7 @@ # # Modified by Massimiliano Leoni, 2016 -from ufl.core.expr import Expr +from ufl.core.ufl_type import UFLRegistry from ufl.corealg.multifunction import MultiFunction from ufl.corealg.traversal import cutoff_unique_post_traversal, unique_post_traversal @@ -84,8 +84,8 @@ def map_expr_dags(function, expressions, compress=True, vcache=None, rcache=None handlers = function._handlers # Optimization else: # Regular function: no skipping supported - cutoff_types = [False] * Expr._ufl_num_typecodes_ - handlers = [function] * Expr._ufl_num_typecodes_ + cutoff_types = [False] * UFLRegistry().number_registered_classes + handlers = [function] * UFLRegistry().number_registered_classes # Create visited set here to share between traversal calls visited = set() diff --git a/ufl/corealg/multifunction.py b/ufl/corealg/multifunction.py index 0042213fb..00d17c171 100644 --- a/ufl/corealg/multifunction.py +++ b/ufl/corealg/multifunction.py @@ -9,8 +9,7 @@ from inspect import signature -from ufl.core.expr import Expr -from ufl.core.ufl_type import UFLType +from ufl.core.ufl_type import UFLRegistry, UFLType def get_num_args(function): @@ -56,13 +55,13 @@ def __init__(self): algorithm_class = type(self) cache_data = MultiFunction._handlers_cache.get(algorithm_class) if not cache_data: - handler_names = [None] * len(Expr._ufl_all_classes_) + handler_names = [None] * len(UFLRegistry().all_classes) # Iterate over the inheritance chain for each Expr # subclass (NB! This assumes that all UFL classes inherits # from a single Expr subclass and that the first # superclass is always from the UFL Expr hierarchy!) - for classobject in Expr._ufl_all_classes_: + for classobject in UFLRegistry().all_classes: for c in classobject.mro(): # Register classobject with handler for the first # encountered superclass @@ -96,7 +95,7 @@ def __call__(self, o, *args): def undefined(self, o, *args): """Trigger error for types with missing handlers.""" - raise ValueError(f"No handler defined for {o._ufl_class_.__name__}.") + raise ValueError(f"No handler defined for {type(o).__name__}.") def reuse_if_untouched(self, o, *ops): """Reuse object if operands are the same objects. diff --git a/ufl/differentiation.py b/ufl/differentiation.py index f660840a3..1a49a6316 100644 --- a/ufl/differentiation.py +++ b/ufl/differentiation.py @@ -24,7 +24,7 @@ # --- Basic differentiation objects --- -@ufl_type(is_abstract=True, is_differential=True) +@ufl_type() class Derivative(Operator): """Base class for all derivative types.""" @@ -35,7 +35,7 @@ def __init__(self, operands): Operator.__init__(self, operands) -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class CoefficientDerivative(Derivative): """Derivative of form integrand w.r.t. the degrees of freedom in a discrete Coefficient.""" @@ -66,8 +66,23 @@ def __str__(self): f"{self.ufl_operands[2]}, and coefficient derivatives {self.ufl_operands[3]}" ) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class CoordinateDerivative(CoefficientDerivative): """Derivative of the integrand of a form w.r.t. the SpatialCoordinates.""" @@ -80,15 +95,27 @@ def __str__(self): f"{self.ufl_operands[2]}, and coordinate derivatives {self.ufl_operands[3]}" ) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class BaseFormDerivative(CoefficientDerivative, BaseForm): """Derivative of a base form w.r.t the degrees of freedom in a discrete Coefficient.""" _ufl_noslots_ = True - _ufl_required_methods_: tuple[str, ...] = ( - CoefficientDerivative._ufl_required_methods_ + BaseForm._ufl_required_methods_ - ) def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): """Initalise.""" @@ -128,8 +155,23 @@ def arg_type(x): ) self._coefficients = base_form_coeffs + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class BaseFormCoordinateDerivative(BaseFormDerivative, CoordinateDerivative): """Derivative of a base form w.r.t. the SpatialCoordinates.""" @@ -141,8 +183,23 @@ def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): self, base_form, coefficients, arguments, coefficient_derivatives ) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class BaseFormOperatorDerivative(BaseFormDerivative, BaseFormOperator): """Derivative of a base form operator w.r.t the degrees of freedom in a discrete Coefficient.""" @@ -187,8 +244,23 @@ def argument_slots(self, outer_form=False): ) return argument_slots + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(num_ops=4, inherit_shape_from_operand=0, inherit_indices_from_operand=0) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class BaseFormOperatorCoordinateDerivative(BaseFormOperatorDerivative, CoordinateDerivative): """Derivative of a base form operator w.r.t. the SpatialCoordinates.""" @@ -200,8 +272,23 @@ def __init__(self, base_form, coefficients, arguments, coefficient_derivatives): self, base_form, coefficients, arguments, coefficient_derivatives ) + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(num_ops=2) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class VariableDerivative(Derivative): """Variable Derivative.""" @@ -246,7 +333,7 @@ def __str__(self): # --- Compound differentiation objects --- -@ufl_type(is_abstract=True) +@ufl_type() class CompoundDerivative(Derivative): """Base class for all compound derivative types.""" @@ -257,10 +344,11 @@ def __init__(self, operands): Derivative.__init__(self, operands) -@ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) +@ufl_type() class Grad(CompoundDerivative): """Grad.""" + _ufl_is_terminal_modifier_ = True __slots__ = ("_dim",) def __new__(cls, f): @@ -284,7 +372,7 @@ def _ufl_expr_reconstruct_(self, op): if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in Grad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) - return self._ufl_class_(op) + return type(self)(op) def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get child from mapping and return the component asked for.""" @@ -304,13 +392,23 @@ def __str__(self): """Format as a string.""" return f"grad({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions -@ufl_type( - num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True -) + +@ufl_type() class ReferenceGrad(CompoundDerivative): """Reference grad.""" + _ufl_is_terminal_modifier_ = True + _ufl_is_in_reference_frame_ = True __slots__ = ("_dim",) def __new__(cls, f): @@ -336,7 +434,7 @@ def _ufl_expr_reconstruct_(self, op): if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in ReferenceGrad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) - return self._ufl_class_(op) + return type(self)(op) def evaluate(self, x, mapping, component, index_values, derivatives=()): """Get child from mapping and return the component asked for.""" @@ -356,11 +454,22 @@ def __str__(self): """Format as a string.""" return f"reference_grad({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices -@ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class Div(CompoundDerivative): """Div.""" + _ufl_is_terminal_modifier_ = True __slots__ = () def __new__(cls, f): @@ -387,13 +496,23 @@ def __str__(self): """Format as a string.""" return f"div({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices -@ufl_type( - num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True -) + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class ReferenceDiv(CompoundDerivative): """Reference divergence.""" + _ufl_is_terminal_modifier_ = True + _ufl_is_in_reference_frame_ = True __slots__ = () def __new__(cls, f): @@ -420,8 +539,18 @@ def __str__(self): """Format as a string.""" return f"reference_div({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices -@ufl_type(num_ops=1, inherit_indices_from_operand=0) + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class NablaGrad(CompoundDerivative): """Nabla grad.""" @@ -448,7 +577,7 @@ def _ufl_expr_reconstruct_(self, op): if self.ufl_operands[0].ufl_free_indices != op.ufl_free_indices: raise ValueError("Free index mismatch in NablaGrad reconstruct.") return Zero(self.ufl_shape, self.ufl_free_indices, self.ufl_index_dimensions) - return self._ufl_class_(op) + return type(self)(op) @property def ufl_shape(self): @@ -459,8 +588,18 @@ def __str__(self): """Format as a string.""" return f"nabla_grad({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions -@ufl_type(num_ops=1, inherit_indices_from_operand=0) + +@ufl_type() class NablaDiv(CompoundDerivative): """Nabla div.""" @@ -490,14 +629,25 @@ def __str__(self): """Format as a string.""" return f"nabla_div({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + _curl_shapes = {(): (2,), (2,): (), (3,): (3,)} -@ufl_type(num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True) +@ufl_type() class Curl(CompoundDerivative): """Compound derivative.""" + _ufl_is_terminal_modifier_ = True __slots__ = ("ufl_shape",) def __new__(cls, f): @@ -524,13 +674,23 @@ def __str__(self): """Format as a string.""" return f"curl({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type( - num_ops=1, inherit_indices_from_operand=0, is_terminal_modifier=True, is_in_reference_frame=True -) +@ufl_type() class ReferenceCurl(CompoundDerivative): """Reference curl.""" + _ufl_is_terminal_modifier_ = True + _ufl_is_in_reference_frame_ = True __slots__ = ("ufl_shape",) def __new__(cls, f): @@ -556,3 +716,13 @@ def __init__(self, f): def __str__(self): """Format as a string.""" return f"reference_curl({self.ufl_operands[0]})" + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions diff --git a/ufl/exprcontainers.py b/ufl/exprcontainers.py index b621c29ca..5a7f1c937 100644 --- a/ufl/exprcontainers.py +++ b/ufl/exprcontainers.py @@ -14,7 +14,7 @@ # --- Non-tensor types --- -@ufl_type(num_ops="varying") +@ufl_type() class ExprList(Operator): """List of Expr objects. For internal use, never to be created by end users.""" @@ -72,7 +72,7 @@ def index_dimensions(self): raise ValueError("A non-tensor type has no index_dimensions.") -@ufl_type(num_ops="varying") +@ufl_type() class ExprMapping(Operator): """Mapping of Expr objects. For internal use, never to be created by end users.""" diff --git a/ufl/form.py b/ufl/form.py index 6e644b740..9121808d3 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -19,6 +19,7 @@ from ufl.checks import is_scalar_constant_expression from ufl.constant import Constant from ufl.constantvalue import Zero +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.expr import Expr, ufl_err_str from ufl.core.terminal import FormArgument from ufl.core.ufl_type import UFLType, ufl_type @@ -88,22 +89,11 @@ def keyfunc(item): return tuple(all_integrals) # integrals_dict -@ufl_type() -class BaseForm(metaclass=UFLType): +class BaseForm(UFLType): """Description of an object containing arguments.""" ufl_operands: tuple[FormArgument, ...] - # Slots is kept empty to enable multiple inheritance with other - # classes - __slots__ = () - _ufl_is_abstract_ = True - _ufl_required_methods_: tuple[str, ...] = ( - "_analyze_form_arguments", - "_analyze_domains", - "ufl_domains", - ) - def __init__(self): """Initialise.""" # Internal variables for caching form argument/coefficient data @@ -156,6 +146,10 @@ def __eq__(self, other): """ return Equation(self, other) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __radd__(self, other): """Add.""" # Ordering of form additions make no difference @@ -711,7 +705,6 @@ class FormSum(BaseForm): "_weights", "ufl_operands", ) - _ufl_required_methods_ = "_analyze_form_arguments" # type: ignore def __new__(cls, *args, **kwargs): """Create a new FormSum.""" @@ -916,6 +909,12 @@ def __eq__(self, other): else: return False + def __hash__(self): + """Hash.""" + if self._hash is None: + self._hash = hash(("ZeroBaseForm", hash(self._arguments))) + return self._hash + def __str__(self): """Format as a string.""" return "ZeroBaseForm({})".format(", ".join(str(arg) for arg in self._arguments)) @@ -923,9 +922,3 @@ def __str__(self): def __repr__(self): """Representation.""" return "ZeroBaseForm({})".format(", ".join(repr(arg) for arg in self._arguments)) - - def __hash__(self): - """Hash.""" - if self._hash is None: - self._hash = hash(("ZeroBaseForm", hash(self._arguments))) - return self._hash diff --git a/ufl/geometry.py b/ufl/geometry.py index 0d8dbbf07..a0d7ccd93 100644 --- a/ufl/geometry.py +++ b/ufl/geometry.py @@ -6,6 +6,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.terminal import Terminal from ufl.core.ufl_type import ufl_type from ufl.domain import MeshSequence, as_domain, extract_unique_domain @@ -107,7 +108,7 @@ # --- Expression node types -@ufl_type(is_abstract=True) +@ufl_type() class GeometricQuantity(Terminal): """Geometric quantity.""" @@ -137,15 +138,15 @@ def is_cellwise_constant(self): def _ufl_signature_data_(self, renumbering): """Signature data of geometric quantities depend on the domain numbering.""" - return (self._ufl_class_.__name__,) + self._domain._ufl_signature_data_(renumbering) + return (type(self).__name__,) + self._domain._ufl_signature_data_(renumbering) def __str__(self): """Format as a string.""" - return self._ufl_class_.name + return type(self).name def __repr__(self): """Representation.""" - r = f"{self._ufl_class_.__name__}({self._domain!r})" + r = f"{type(self).__name__}({self._domain!r})" return r def _ufl_compute_hash_(self): @@ -154,24 +155,28 @@ def _ufl_compute_hash_(self): def __eq__(self, other): """Check equality.""" - return isinstance(other, self._ufl_class_) and other._domain == self._domain + return isinstance(other, type(self)) and other._domain == self._domain + + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) -@ufl_type(is_abstract=True) +@ufl_type() class GeometricCellQuantity(GeometricQuantity): """Geometric cell quantity.""" __slots__ = () -@ufl_type(is_abstract=True) +@ufl_type() class GeometricFacetQuantity(GeometricQuantity): """Geometric facet quantity.""" __slots__ = () -@ufl_type(is_abstract=True) +@ufl_type() class GeometricRidgeQuantity(GeometricQuantity): """Geometric ridge quantity.""" diff --git a/ufl/indexed.py b/ufl/indexed.py index 4e2739f11..a747daf07 100644 --- a/ufl/indexed.py +++ b/ufl/indexed.py @@ -15,10 +15,11 @@ from ufl.precedence import parstr -@ufl_type(is_shaping=True, num_ops=2, is_terminal_modifier=True) +@ufl_type() class Indexed(Operator): """Indexed expression.""" + _ufl_is_terminal_modifier_ = True __slots__ = ( "_initialised", "ufl_free_indices", diff --git a/ufl/indexsum.py b/ufl/indexsum.py index a0b9e0a1e..d486fd283 100644 --- a/ufl/indexsum.py +++ b/ufl/indexsum.py @@ -18,7 +18,7 @@ # --- Sum over an index --- -@ufl_type(num_ops=2) +@ufl_type() class IndexSum(Operator): """Index sum.""" diff --git a/ufl/mathfunctions.py b/ufl/mathfunctions.py index 3caba472e..5f2660eda 100644 --- a/ufl/mathfunctions.py +++ b/ufl/mathfunctions.py @@ -51,10 +51,13 @@ # --- Function representations --- -@ufl_type(is_abstract=True, is_scalar=True, num_ops=1) +@ufl_type() class MathFunction(Operator): """Base class for all unary scalar math functions.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () # Freeze member variables for objects in this class __slots__ = ("_name",) @@ -322,10 +325,13 @@ def __init__(self, argument): MathFunction.__init__(self, "atan", argument) -@ufl_type(is_scalar=True, num_ops=2) +@ufl_type() class Atan2(Operator): """Inverse tangent with two inputs.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __new__(cls, arg1, arg2): @@ -388,10 +394,13 @@ def evaluate(self, x, mapping, component, index_values): return math.erf(a) -@ufl_type(is_abstract=True, is_scalar=True, num_ops=2) +@ufl_type() class BesselFunction(Operator): """Base class for all bessel functions.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = "_name" def __init__(self, name, nu, argument): diff --git a/ufl/precedence.py b/ufl/precedence.py index 3b404dbda..e32a09806 100644 --- a/ufl/precedence.py +++ b/ufl/precedence.py @@ -6,7 +6,6 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -import warnings # FIXME: This code is crap... @@ -90,7 +89,7 @@ def build_precedence_mapping(precedence_list): Utility function used by some external code. """ - from ufl.classes import Expr, abstract_classes, all_ufl_classes + from ufl.classes import Expr, all_ufl_classes pm = {} missing = set() @@ -102,9 +101,9 @@ def build_precedence_mapping(precedence_list): k += 1 # Check for missing classes, fill in subclasses for c in all_ufl_classes: - if c not in abstract_classes and c not in pm: + if c not in pm: b = c.__bases__[0] - while b is not Expr: + while b is not Expr and len(b.__bases__) > 0: if b in pm: pm[c] = pm[b] break @@ -116,11 +115,11 @@ def build_precedence_mapping(precedence_list): def assign_precedences(precedence_list): """Given a precedence list, assign ints to class._precedence.""" - pm, missing = build_precedence_mapping(precedence_list) + pm, _missing = build_precedence_mapping(precedence_list) for c, p in sorted(pm.items(), key=lambda x: x[0].__name__): c._precedence = p - if missing: - warnings.warn( - "Missing precedence levels for classes:\n" - + "\n".join(f" {c}" for c in sorted(missing)) - ) + # TODO: problem if this warns? + # if missing: + # warnings.warn( + # "Missing precedence levels for classes:\n" + "\n".join(f" {c}" for c in missing) + # ) diff --git a/ufl/referencevalue.py b/ufl/referencevalue.py index 5dd82f4c3..65ca9c12e 100644 --- a/ufl/referencevalue.py +++ b/ufl/referencevalue.py @@ -10,10 +10,14 @@ from ufl.core.ufl_type import ufl_type -@ufl_type(num_ops=1, is_index_free=True, is_terminal_modifier=True, is_in_reference_frame=True) +@ufl_type() class ReferenceValue(Operator): """Representation of the reference cell value of a form argument.""" + ufl_free_indices = () + ufl_index_dimensions = () + _ufl_is_terminal_modifier_ = True + _ufl_is_in_reference_frame_ = True __slots__ = () def __init__(self, f): diff --git a/ufl/restriction.py b/ufl/restriction.py index b4d231125..5c55db0c7 100644 --- a/ufl/restriction.py +++ b/ufl/restriction.py @@ -13,13 +13,7 @@ # --- Restriction operators --- -@ufl_type( - is_abstract=True, - num_ops=1, - inherit_shape_from_operand=0, - inherit_indices_from_operand=0, - is_restriction=True, -) +@ufl_type() class Restricted(Operator): """Restriction.""" @@ -49,18 +43,35 @@ def __str__(self): """Format as a string.""" return f"{parstr(self.ufl_operands[0], self)}({self._side})" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape -@ufl_type(is_terminal_modifier=True) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class PositiveRestricted(Restricted): """Positive restriction.""" + _ufl_is_terminal_modifier_ = True __slots__ = () _side = "+" -@ufl_type(is_terminal_modifier=True) +@ufl_type() class NegativeRestricted(Restricted): """Negative restriction.""" + _ufl_is_terminal_modifier_ = True __slots__ = () _side = "-" diff --git a/ufl/tensoralgebra.py b/ufl/tensoralgebra.py index 4b608b526..fbd796fbd 100644 --- a/ufl/tensoralgebra.py +++ b/ufl/tensoralgebra.py @@ -42,7 +42,7 @@ # --- Classes representing compound tensor algebra operations --- -@ufl_type(is_abstract=True) +@ufl_type() class CompoundTensorOperator(Operator): """Compount tensor operator.""" @@ -86,7 +86,7 @@ def __init__(self, operands): # pass -@ufl_type(is_shaping=True, num_ops=1, inherit_indices_from_operand=0) +@ufl_type() class Transposed(CompoundTensorOperator): """Transposed tensor.""" @@ -115,8 +115,18 @@ def __str__(self): """Format as a string.""" return f"{parstr(self.ufl_operands[0], self)}^T" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions -@ufl_type(num_ops=2) + +@ufl_type() class Outer(CompoundTensorOperator): """Outer.""" @@ -149,7 +159,7 @@ def __str__(self): return f"{parstr(self.ufl_operands[0], self)} (X) {parstr(self.ufl_operands[1], self)}" -@ufl_type(num_ops=2) +@ufl_type() class Inner(CompoundTensorOperator): """Inner.""" @@ -191,7 +201,7 @@ def __str__(self): return f"{parstr(self.ufl_operands[0], self)} : {parstr(self.ufl_operands[1], self)}" -@ufl_type(num_ops=2) +@ufl_type() class Dot(CompoundTensorOperator): """Dot.""" @@ -240,10 +250,12 @@ def __str__(self): return f"{parstr(self.ufl_operands[0], self)} . {parstr(self.ufl_operands[1], self)}" -@ufl_type(is_index_free=True, num_ops=1) +@ufl_type() class Perp(CompoundTensorOperator): """Perp.""" + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __new__(cls, A): @@ -273,7 +285,7 @@ def __str__(self): return f"perp({self.ufl_operands[0]})" -@ufl_type(num_ops=2) +@ufl_type() class Cross(CompoundTensorOperator): """Cross.""" @@ -312,7 +324,7 @@ def __str__(self): return f"{parstr(self.ufl_operands[0], self)} x {parstr(self.ufl_operands[1], self)}" -@ufl_type(num_ops=1, inherit_indices_from_operand=0) +@ufl_type() class Trace(CompoundTensorOperator): """Trace.""" @@ -340,11 +352,24 @@ def __str__(self): """Format as a string.""" return f"tr({self.ufl_operands[0]})" + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices -@ufl_type(is_scalar=True, num_ops=1) + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + + +@ufl_type() class Determinant(CompoundTensorOperator): """Determinant.""" + ufl_shape = () + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __new__(cls, A): @@ -380,10 +405,12 @@ def __str__(self): # TODO: Drop Inverse and represent it as product of Determinant and # Cofactor? -@ufl_type(is_index_free=True, num_ops=1) +@ufl_type() class Inverse(CompoundTensorOperator): """Inverse.""" + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __new__(cls, A): @@ -423,10 +450,12 @@ def __str__(self): return f"{parstr(self.ufl_operands[0], self)}^-1" -@ufl_type(is_index_free=True, num_ops=1) +@ufl_type() class Cofactor(CompoundTensorOperator): """Cofactor.""" + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __init__(self, A): @@ -454,7 +483,7 @@ def __str__(self): return f"cofactor({self.ufl_operands[0]})" -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class Deviatoric(CompoundTensorOperator): """Deviatoric.""" @@ -488,8 +517,23 @@ def __str__(self): """Format as a string.""" return f"dev({self.ufl_operands[0]})" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class Skew(CompoundTensorOperator): """Skew.""" @@ -522,8 +566,23 @@ def __str__(self): """Format as a string.""" return f"skew({self.ufl_operands[0]})" + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0) +@ufl_type() class Sym(CompoundTensorOperator): """Sym.""" @@ -557,3 +616,18 @@ def __init__(self, A): def __str__(self): """Format as a string.""" return f"sym({self.ufl_operands[0]})" + + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape + + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions diff --git a/ufl/tensors.py b/ufl/tensors.py index 64c8c2193..c3e7ed501 100644 --- a/ufl/tensors.py +++ b/ufl/tensors.py @@ -18,7 +18,7 @@ # --- Classes representing tensors of UFL expressions --- -@ufl_type(is_shaping=True, num_ops="varying", inherit_indices_from_operand=0) +@ufl_type() class ListTensor(Operator): """Wraps a list of expressions into a tensor valued expression of one higher rank.""" @@ -177,8 +177,18 @@ def substring(expressions, indent): return substring(self.ufl_operands, 0) + @property + def ufl_free_indices(self): + """Return free indices.""" + return self.ufl_operands[0].ufl_free_indices + + @property + def ufl_index_dimensions(self): + """Retrun index dimensions.""" + return self.ufl_operands[0].ufl_index_dimensions + -@ufl_type(is_shaping=True, num_ops="varying") +@ufl_type() class ComponentTensor(Operator): """Maps the free indices of a scalar valued expression to tensor axes.""" diff --git a/ufl/utils/formatting.py b/ufl/utils/formatting.py index d3e0ad2bc..45c45c1a3 100644 --- a/ufl/utils/formatting.py +++ b/ufl/utils/formatting.py @@ -89,7 +89,7 @@ def _tree_format_expression(expression, indentation, parentheses): _tree_format_expression(o, indentation + 1, parentheses) for o in expression.ufl_operands ] - s = f"{ind}{expression._ufl_class_.__name__}\n" + s = f"{ind}{type(expression).__name__}\n" if parentheses and len(sops) > 1: s += f"{ind}(\n" s += "\n".join(sops) diff --git a/ufl/variable.py b/ufl/variable.py index 56cf4b9be..edc93b123 100644 --- a/ufl/variable.py +++ b/ufl/variable.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from ufl.constantvalue import as_ufl +from ufl.core.compute_expr_hash import compute_expr_hash from ufl.core.expr import Expr from ufl.core.operator import Operator from ufl.core.terminal import Terminal @@ -65,7 +66,7 @@ def _ufl_signature_data_(self, renumbering): return ("Label", renumbering[self]) -@ufl_type(is_shaping=True, is_index_free=True, num_ops=1, inherit_shape_from_operand=0) +@ufl_type() class Variable(Operator): """A Variable is a representative for another expression. @@ -79,6 +80,8 @@ class Variable(Operator): df = diff(f, e) """ + ufl_free_indices = () + ufl_index_dimensions = () __slots__ = () def __init__(self, expression, label=None): @@ -123,6 +126,15 @@ def __eq__(self, other): and self.ufl_operands[0] == other.ufl_operands[0] ) + def __hash__(self): + """Return hash.""" + return compute_expr_hash(self) + def __str__(self): """Format as a string.""" return f"var{self.ufl_operands[1].count()}({self.ufl_operands[0]})" + + @property + def ufl_shape(self): + """Return shape.""" + return self.ufl_operands[0].ufl_shape