# Source code for ufl.form

    """The Form class."""
    # Copyright (C) 2008-2016 Martin Sandve Alnæs
    #
    # This file is part of UFL (https://www.fenicsproject.org)
    #
    # SPDX-License-Identifier:    LGPL-3.0-or-later
    #
    # Modified by Anders Logg, 2009-2011.
    # Modified by Massimiliano Leoni, 2016.
    # Modified by Cecile Daversin-Catty, 2018.
    # Modified by Nacime Bouziani, 2020.
    # Modified by Jørgen S. Dokken 2023.

    import numbers
    import warnings
    from collections import defaultdict
    from itertools import chain

    from ufl.checks import is_scalar_constant_expression
    from ufl.constant import Constant
    from ufl.constantvalue import Zero
    from ufl.core.expr import Expr, ufl_err_str
    from ufl.core.terminal import FormArgument
    from ufl.core.ufl_type import UFLType, ufl_type
    from ufl.domain import extract_unique_domain, sort_domains
    from ufl.equation import Equation
    from ufl.integral import Integral
    from ufl.utils.counted import Counted
    from ufl.utils.sorting import sorted_by_count

    # Export list for ufl.classes
    __all_classes__ = ["Form", "BaseForm", "ZeroBaseForm"]

    # --- The Form class, representing a complete variational form or functional ---

    def _sorted_integrals(integrals):
        """Sort integrals for a stable signature computation.

        Sort integrals by domain id, integral type, subdomain id for a more
        stable signature computation.
        """
        # Group integrals in multilevel dict by keys
        # [domain][integral_type][subdomain_id]
        integrals_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
        for integral in integrals:
            d = integral.ufl_domain()
            if d is None:
                raise ValueError(
                    "Each integral in a form must have a uniquely defined integration domain."
                )
            it = integral.integral_type()
            si = integral.subdomain_id()
            integrals_dict[d][it][si].append(integral)

        all_integrals = []

        def keyfunc(item):
            if isinstance(item, numbers.Integral):
                sid_int = item
            else:
                # As subdomain ids can be either int or tuples, we need to compare them
                sid_int = tuple(-1 if i == "otherwise" else i for i in item)
            return (type(item).__name__, sid_int)

        # Order integrals canonically to increase signature stability
        for d in sort_domains(integrals_dict):
            for it in sorted(integrals_dict[d]):  # str is sortable
                for si in sorted(integrals_dict[d][it], key=keyfunc):
                    unsorted_integrals = integrals_dict[d][it][si]
                    # TODO: At this point we could order integrals by
                    #       metadata and integrand, or even add the
                    #       integrands with the same metadata. This is done
                    #       in accumulate_integrands_with_same_metadata in
                    #       algorithms/domain_analysis.py and would further
                    #       increase the signature stability.
                    all_integrals.extend(unsorted_integrals)
                    # integrals_dict[d][it][si] = unsorted_integrals

        return tuple(all_integrals)  # integrals_dict

    @ufl_type()
    class BaseForm(metaclass=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
            self._arguments = None
            self._coefficients = None

        # --- Accessor interface ---

        def arguments(self):
            """Return all ``Argument`` objects found in form."""
            if self._arguments is None:
                self._analyze_form_arguments()
            return self._arguments

        def coefficients(self):
            """Return all ``Coefficient`` objects found in form."""
            if self._coefficients is None:
                self._analyze_form_arguments()
            return self._coefficients

        def geometric_quantities(self):
            """Return all ``GeometricQuantity`` objects found in form."""
            if self._geometric_quantities is None:
                self._analyze_form_arguments()
            return self._geometric_quantities

        def ufl_domain(self):
            """Return the single geometric integration domain occuring in the base form.

            Fails if multiple domains are found.
            """
            try:
                (domain,) = set(self.ufl_domains())
            except ValueError:
                raise ValueError(f"{type(self).__name__} must have exactly one domain.")
            # Return the one and only domain
            return domain

        def empty(self):
            """Returns whether the BaseForm has no components."""
            return False

        # --- Operator implementations ---

        def __eq__(self, other):
            """Delayed evaluation of the == operator.

            Just 'lhs_form == rhs_form' gives an Equation,
            while 'bool(lhs_form == rhs_form)' delegates
            to lhs_form.equals(rhs_form).
            """
            return Equation(self, other)

        def __radd__(self, other):
            """Add."""
            # Ordering of form additions make no difference
            return self.__add__(other)

        def __add__(self, other):
            """Add."""
            if isinstance(other, numbers.Number) and other == 0:
                # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b])
                return self
            elif isinstance(other, Zero):
                # Allow adding ufl Zero as a no-op, needed for sum([a,b])
                return self

            elif isinstance(other, ZeroBaseForm):
                # Simplify addition with ZeroBaseForm
                return self

            # For `ZeroBaseForm(...) + B` with B a BaseForm.
            # We could overwrite ZeroBaseForm.__add__ but that implies
            # duplicating cases with `0` and `ufl.Zero`.
            elif isinstance(self, ZeroBaseForm):
                # Simplify addition with ZeroBaseForm
                return other

            elif isinstance(other, BaseForm):
                # Add integrals from both forms
                return FormSum((self, 1), (other, 1))

            else:
                # Let python protocols do their job if we don't handle it
                return NotImplemented

        def __sub__(self, other):
            """Subtract other form from this one."""
            return self + (-other)

        def __rsub__(self, other):
            """Subtract this form from other."""
            return other + (-self)

        def __neg__(self):
            """Negate all integrals in form.

            This enables the handy "-form" syntax for e.g. the
            linearized system (J, -F) from a nonlinear form F.
            """
            if isinstance(self, ZeroBaseForm):
                # `-` doesn't change anything for ZeroBaseForm.
                # This also facilitates simplifying FormSum containing ZeroBaseForm objects.
                return self
            return FormSum((self, -1))

        def __rmul__(self, scalar):
            """Multiply all integrals in form with constant scalar value."""
            # This enables the handy "0*form" or "dt*form" syntax
            if is_scalar_constant_expression(scalar):
                return FormSum((self, scalar))
            return NotImplemented

        def __mul__(self, coefficient):
            """Take the action of this form on the given coefficient."""
            if isinstance(coefficient, Expr):
                from ufl.formoperators import action

                return action(self, coefficient)
            return NotImplemented

        def __ne__(self, other):
            """Immediately evaluate the != operator (as opposed to the == operator)."""
            return not self.equals(other)

        def __call__(self, x):
            """Take the action of this form on ``x``."""
            from ufl.formoperators import action

            return action(self, x)

        def _ufl_compute_hash_(self):
            """Compute the hash."""
            # Ensure compatibility with MultiFunction
            # `hash(self)` will call the `__hash__` method of the subclass.
            return hash(self)

        def _ufl_expr_reconstruct_(self, *operands):
            """Return a new object of the same type with new operands."""
            return type(self)(*operands)

        __matmul__ = __mul__

    @ufl_type()
    class Form(BaseForm):
        """Description of a weak form consisting of a sum of integrals over subdomains."""

        __slots__ = (
            "_arguments",
            "_base_form_operators",
            # --- Dict that external frameworks can place framework-specific
            #     data in to be carried with the form
            #     Never use this internally in ufl!
            "_cache",
            "_coefficient_numbering",
            "_coefficients",
            "_constant_numbering",
            "_constants",
            "_domain_numbering",
            "_geometric_quantities",
            "_hash",
            # --- List of Integral objects (a Form is a sum of these
            # Integrals, everything else is derived)
            "_integrals",
            # --- Internal variables for caching various data
            "_integration_domains",
            "_signature",
            "_subdomain_data",
            "_terminal_numbering",
        )

        def __init__(self, integrals):
            """Initialise."""
            BaseForm.__init__(self)
            # Basic input checking (further compatibilty analysis happens
            # later)
            if not all(isinstance(itg, Integral) for itg in integrals):
                raise ValueError("Expecting list of integrals.")

            # Store integrals sorted canonically to increase signature
            # stability
            self._integrals = _sorted_integrals(integrals)

            # Internal variables for caching domain data
            self._integration_domains = None
            self._domain_numbering = None

            # Internal variables for caching subdomain data
            self._subdomain_data = None

            # Internal variables for caching form argument data
            self._coefficients = None
            self._coefficient_numbering = None
            self._constant_numbering = None
            self._terminal_numbering = None

            # Internal variables for caching base form operator data
            self._base_form_operators = None

            from ufl.algorithms.analysis import extract_constants

            self._constants = extract_constants(self)

            # Internal variables for caching of hash and signature after
            # first request
            self._hash = None
            self._signature = None

            # Never use this internally in ufl!
            self._cache = {}

        # --- Accessor interface ---

        def integrals(self):
            """Return a sequence of all integrals in form."""
            return self._integrals

        def integrals_by_type(self, integral_type):
            """Return a sequence of all integrals with a particular domain type."""
            return tuple(
                integral for integral in self.integrals() if integral.integral_type() == integral_type
            )

        def integrals_by_domain(self, domain):
            """Return a sequence of all integrals with a particular integration domain."""
            return tuple(integral for integral in self.integrals() if integral.ufl_domain() == domain)

        def empty(self):
            """Returns whether the form has no integrals."""
            return len(self.integrals()) == 0

        def ufl_domains(self):
            """Return the geometric integration domains occuring in the form.

            NB! This does not include domains of coefficients defined on
            other meshes.

            The return type is a tuple even if only a single domain exists.
            """
            if self._integration_domains is None:
                self._analyze_domains()
            return self._integration_domains

        def ufl_cell(self):
            """Return the single cell this form is defined on.

            Fails if multiple cells are found.
            """
            return self.ufl_domain().ufl_cell()

        def geometric_dimension(self):
            """Return the geometric dimension shared by all domains and functions in this form."""
            gdims = tuple(set(domain.geometric_dimension() for domain in self.ufl_domains()))
            if len(gdims) != 1:
                raise ValueError(
                    "Expecting all domains and functions in a form "
                    f"to share geometric dimension, got {tuple(sorted(gdims))}"
                )
            return gdims[0]

        def domain_numbering(self):
            """Return a contiguous numbering of domains in a mapping ``{domain:number}``."""
            if self._domain_numbering is None:
                self._analyze_domains()
            return self._domain_numbering

        def subdomain_data(self):
            """Returns a mapping on the form ``{domain:{integral_type: subdomain_data}}``."""
            if self._subdomain_data is None:
                self._analyze_subdomain_data()
            return self._subdomain_data

        def coefficients(self):
            """Return all ``Coefficient`` objects found in form."""
            if self._coefficients is None:
                self._analyze_form_arguments()
            return self._coefficients

        def base_form_operators(self):
            """Return all ``BaseFormOperator`` objects found in form."""
            if self._base_form_operators is None:
                self._analyze_base_form_operators()
            return self._base_form_operators

        def coefficient_numbering(self):
            """Return a contiguous numbering of coefficients in a mapping ``{coefficient:number}``."""
            # cyclic import
            from ufl.coefficient import Coefficient

            if self._coefficient_numbering is None:
                self._coefficient_numbering = {
                    expr: num
                    for expr, num in self.terminal_numbering().items()
                    if isinstance(expr, Coefficient)
                }
            return self._coefficient_numbering

        def constants(self):
            """Get constants."""
            return self._constants

        def constant_numbering(self):
            """Return a contiguous numbering of constants in a mapping ``{constant:number}``."""
            if self._constant_numbering is None:
                self._constant_numbering = {
                    expr: num
                    for expr, num in self.terminal_numbering().items()
                    if isinstance(expr, Constant)
                }
            return self._constant_numbering

        def terminal_numbering(self):
            """Return a contiguous numbering for all counted objects in the form.

            The returned object is mapping from terminal to its number (an integer).

            The numbering is computed per type so :class:`Coefficient`,
            :class:`Constant`, etc will each be numbered from zero.
            """
            # cyclic import
            from ufl.algorithms.analysis import extract_type

            if self._terminal_numbering is None:
                exprs_by_type = defaultdict(set)
                for counted_expr in extract_type(self, Counted):
                    exprs_by_type[counted_expr._counted_class].add(counted_expr)

                numbering = {}
                for exprs in exprs_by_type.values():
                    for i, expr in enumerate(sorted_by_count(exprs)):
                        numbering[expr] = i
                self._terminal_numbering = numbering
            return self._terminal_numbering

        def signature(self):
            """Signature for use with jit cache (independent of incidental numbering of indices etc)."""
            if self._signature is None:
                self._compute_signature()
            return self._signature

        # --- Operator implementations ---

        def __hash__(self):
            """Hash."""
            if self._hash is None:
                self._hash = hash(tuple(hash(itg) for itg in self.integrals()))
            return self._hash

        def __ne__(self, other):
            """Immediate evaluation of the != operator (as opposed to the == operator)."""
            return not self.equals(other)

        def equals(self, other):
            """Evaluate ``bool(lhs_form == rhs_form)``."""
            if type(other) is not Form:
                return False
            if len(self._integrals) != len(other._integrals):
                return False
            if hash(self) != hash(other):
                return False
            return all(a == b for a, b in zip(self._integrals, other._integrals))

        def __radd__(self, other):
            """Add."""
            # Ordering of form additions make no difference
            return self.__add__(other)

        def __add__(self, other):
            """Add."""
            if isinstance(other, Form):
                # Add integrals from both forms
                return Form(list(chain(self.integrals(), other.integrals())))

            if isinstance(other, ZeroBaseForm):
                # Simplify addition with ZeroBaseForm
                return self

            elif isinstance(other, BaseForm):
                # Create form sum if form is of other type
                return FormSum((self, 1), (other, 1))

            elif isinstance(other, int | float) and other == 0:
                # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b])
                return self

            elif isinstance(other, Zero) and not (other.ufl_shape or other.ufl_free_indices):
                # Allow adding ufl Zero as a no-op, needed for sum([a,b])
                return self

            else:
                # Let python protocols do their job if we don't handle it
                return NotImplemented

        def __sub__(self, other):
            """Subtract other form from this one."""
            return self + (-other)

        def __rsub__(self, other):
            """Subtract this form from other."""
            return other + (-self)

        def __neg__(self):
            """Negate all integrals in form.

            This enables the handy "-form" syntax for e.g. the
            linearized system (J, -F) from a nonlinear form F.
            """
            return Form([-itg for itg in self.integrals()])

        def __rmul__(self, scalar):
            """Multiply all integrals in form with constant scalar value."""
            # This enables the handy "0*form" or "dt*form" syntax
            if is_scalar_constant_expression(scalar):
                return Form([scalar * itg for itg in self.integrals()])
            return NotImplemented

        def __mul__(self, coefficient):
            """UFL form operator: Take the action of this form on the given coefficient."""
            if isinstance(coefficient, Expr):
                from ufl.formoperators import action

                return action(self, coefficient)
            return NotImplemented

        def __call__(self, *args, **kwargs):
            """UFL form operator: Evaluate form by replacing arguments and coefficients.

            Replaces form.arguments() with given positional arguments in
            same number and ordering. Number of positional arguments must
            be 0 or equal to the number of Arguments in the form.

            The optional keyword argument coefficients can be set to a dict
            to replace Coefficients with expressions of matching shapes.

            Example:
              V = FiniteElement("CG", triangle, 1)
              v = TestFunction(V)
              u = TrialFunction(V)
              f = Coefficient(V)
              g = Coefficient(V)
              a = g*inner(grad(u), grad(v))*dx
              M = a(f, f, coefficients={ g: 1 })

            Is equivalent to M == grad(f)**2*dx.
            """
            repdict = {}

            if args:
                arguments = self.arguments()
                if len(arguments) != len(args):
                    raise ValueError(f"Need {len(arguments)} arguments to form(), got {len(args)}.")
                repdict.update(zip(arguments, args))

            coefficients = kwargs.pop("coefficients", None)
            if kwargs:
                raise ValueError(f"Unknown kwargs {list(kwargs)}")

            if coefficients is not None:
                coeffs = self.coefficients()
                for f in coefficients:
                    if f in coeffs:
                        repdict[f] = coefficients[f]
                    else:
                        warnings.warn(f"Coefficient {ufl_err_str(f)} is not in form.")
            if repdict:
                from ufl.formoperators import replace

                return replace(self, repdict)
            else:
                return self

        __matmul__ = __mul__

        # --- String conversion functions, for UI purposes only ---

        def __str__(self):
            """Compute shorter string representation of form. This can be huge for complicated forms."""
            # Warning used for making sure we don't use this in the general pipeline:
            # warning("Calling str on form is potentially expensive and
            # should be avoided except during debugging.") Not caching this
            # because it can be huge
            s = "\n  +  ".join(map(str, self.integrals()))
            return s or "<empty Form>"

        def __repr__(self):
            """Compute repr string of form. This can be huge for complicated forms."""
            # Warning used for making sure we don't use this in the general pipeline:
            # warning("Calling repr on form is potentially expensive and
            # should be avoided except during debugging.") Not caching this
            # because it can be huge
            itgs = ", ".join(map(repr, self.integrals()))
            r = "Form([" + itgs + "])"
            return r

        # --- Analysis functions, precomputation and caching of various quantities

        def _analyze_domains(self):
            """Analyze domains."""
            from ufl.domain import join_domains, sort_domains

            # Collect integration domains.
            self._integration_domains = sort_domains(
                join_domains([itg.ufl_domain() for itg in self._integrals])
            )
            # Collect domains in integrands.
            domains_in_integrands = set()
            for o in chain(
                self.arguments(), self.coefficients(), self.constants(), self.geometric_quantities()
            ):
                domain = extract_unique_domain(o, expand_mesh_sequence=False)
                domains_in_integrands.update(domain.meshes)
            domains_in_integrands -= set(self._integration_domains)
            all_domains = self._integration_domains + sort_domains(join_domains(domains_in_integrands))
            # Let problem solving environments access all domains via
            # self._domain_numbering.keys() (wrapped in extract_domains()).
            self._domain_numbering = {d: i for i, d in enumerate(all_domains)}

        def _analyze_subdomain_data(self):
            """Analyze subdomain data."""
            integration_domains = self.ufl_domains()
            integrals = self.integrals()

            # Make clear data structures to collect subdomain data in
            subdomain_data = {}
            for domain in integration_domains:
                subdomain_data[domain] = {}

            for integral in integrals:
                # Get integral properties
                domain = integral.ufl_domain()
                it = integral.integral_type()
                sd = integral.subdomain_data()

                # Collect subdomain data
                if subdomain_data[domain].get(it) is None:
                    subdomain_data[domain][it] = [sd]
                else:
                    subdomain_data[domain][it].append(sd)
            self._subdomain_data = subdomain_data

        def _analyze_form_arguments(self):
            """Analyze which Argument and Coefficient objects can be found in the form."""
            from ufl.algorithms.analysis import extract_terminals_with_domain

            arguments, coefficients, geometric_quantities = extract_terminals_with_domain(self)

            # Define canonical numbering of arguments and coefficients
            self._arguments = tuple(sorted(set(arguments), key=lambda x: x.number()))
            self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count()))
            self._geometric_quantities = geometric_quantities  # sorted by (type, domain)

        def _analyze_base_form_operators(self):
            """Analyze which BaseFormOperator objects can be found in the form."""
            from ufl.algorithms.analysis import extract_base_form_operators

            base_form_ops = extract_base_form_operators(self)
            self._base_form_operators = tuple(sorted(base_form_ops, key=lambda x: x.count()))

        def _compute_renumbering(self):
            """Compute renumbering."""
            dn = self.domain_numbering()
            tn = self.terminal_numbering()
            renumbering = {}
            renumbering.update(dn)
            renumbering.update(tn)
            return renumbering

        def _compute_signature(self):
            """Compute signature."""
            from ufl.algorithms.signature import compute_form_signature

            self._signature = compute_form_signature(self, self._compute_renumbering())

    def as_form(form):
        """Convert to form if not a form, otherwise return form."""
        if not isinstance(form, BaseForm) and form != 0:
            raise ValueError(f"Unable to convert object to a UFL form: {ufl_err_str(form)}")
        return form

    @ufl_type()
    class FormSum(BaseForm):
        """Form sum.

        Description of a weighted sum of variational forms and form-like objects
        components is the list of Forms to be summed
        arg_weights is a list of tuples of component index and weight
        """

        __slots__ = (
            "_arguments",
            "_coefficients",
            "_components",
            "_domain_numbering",
            "_domains",
            "_hash",
            "_weights",
            "ufl_operands",
        )
        _ufl_required_methods_ = "_analyze_form_arguments"  # type: ignore

        def __new__(cls, *args, **kwargs):
            """Create a new FormSum."""
            # All the components are `ZeroBaseForm`
            if all(component == 0 for component, _ in args):
                # Assume that the arguments of all the components have
                # consistent with each other  and select the first one to
                # define the arguments of `ZeroBaseForm`.
                # This might not always be true but `ZeroBaseForm`'s
                # arguments are not checked anywhere because we can't
                # reliably always infer them.
                ((arg, _), *_) = args
                arguments = arg.arguments()
                return ZeroBaseForm(arguments)

            return super().__new__(cls)

        def __init__(self, *components):
            """Initialise."""
            BaseForm.__init__(self)

            # Remove `ZeroBaseForm` components
            filtered_components = [(component, w) for component, w in components if component != 0]

            weights = []
            full_components = []
            for component, w in filtered_components:
                if isinstance(component, FormSum):
                    full_components.extend(component.components())
                    weights.extend([w * wc for wc in component.weights()])
                else:
                    full_components.append(component)
                    weights.append(w)

            self._arguments = None
            self._coefficients = None
            self._domains = None
            self._domain_numbering = None
            self._hash = None
            self._weights = weights
            self._components = full_components
            self._sum_variational_components()
            self.ufl_operands = self._components

        def components(self):
            """Get components."""
            return self._components

        def weights(self):
            """Get weights."""
            return self._weights

        def _sum_variational_components(self):
            """Sum variational components."""
            var_forms = None
            other_components = []
            new_weights = []
            for i, component in enumerate(self._components):
                if isinstance(component, Form):
                    if var_forms:
                        var_forms = var_forms + (self._weights[i] * component)
                    else:
                        var_forms = self._weights[i] * component
                else:
                    other_components.append(component)
                    new_weights.append(self._weights[i])
            if var_forms:
                other_components.insert(0, var_forms)
                new_weights.insert(0, 1)
            self._components = other_components
            self._weights = new_weights

        def _analyze_form_arguments(self):
            """Return all ``Argument`` objects found in form."""
            arguments = []
            coefficients = []
            for component in self._components:
                arguments.extend(component.arguments())
                coefficients.extend(component.coefficients())
            # Define canonical numbering of arguments and coefficients
            self._arguments = tuple(sorted(set(arguments), key=lambda x: x.number()))
            self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count()))

        def _analyze_domains(self):
            """Analyze which domains can be found in FormSum."""
            from ufl.domain import join_domains, sort_domains

            # Collect unique domains
            self._domains = sort_domains(
                join_domains(chain.from_iterable(c.ufl_domains() for c in self.components()))
            )

        def ufl_domains(self):
            """Return all domains found in the base form."""
            if self._domains is None:
                self._analyze_domains()
            return self._domains

        def __hash__(self):
            """Hash."""
            if self._hash is None:
                self._hash = hash(
                    tuple((hash(c), hash(w)) for c, w in zip(self.components(), self.weights()))
                )
            return self._hash

        def equals(self, other):
            """Evaluate ``bool(lhs_form == rhs_form)``."""
            if type(other) is not FormSum:
                return False
            if self is other:
                return True
            return (
                len(self.components()) == len(other.components())
                and all(a == b for a, b in zip(self.components(), other.components()))
                and all(a == b for a, b in zip(self.weights(), other.weights()))
            )

        def empty(self):
            """Returns whether the FormSum has no components."""
            return len(self.components()) == 0

        def __str__(self):
            """Compute shorter string representation of form. This can be huge for complicated forms."""
            # Warning used for making sure we don't use this in the general pipeline:
            # warning("Calling str on form is potentially expensive and
            # should be avoided except during debugging.")
            # Not caching this because it can be huge
            s = "\n  +  ".join(f"{w}*{c}" for c, w in zip(self.components(), self.weights()))
            return s or "<empty FormSum>"

        def __repr__(self):
            """Compute repr string of form. This can be huge for complicated forms."""
            # Warning used for making sure we don't use this in the general pipeline:
            # warning("Calling repr on form is potentially expensive and
            # should be avoided except during debugging.")
            # Not caching this because it can be huge
            itgs = ", ".join(f"{w!r}*{c!r}" for c, w in zip(self.components(), self.weights()))
            r = "FormSum([" + itgs + "])"
            return r

    @ufl_type()
    class ZeroBaseForm(BaseForm):
        """Description of a zero base form.

        ZeroBaseForm is idempotent with respect to assembly and is mostly
        used for sake of simplifying base-form expressions.
        """

        __slots__ = (
            "_arguments",
            "_coefficients",
            "_domains",
            "_hash",
            # Pyadjoint compatibility
            "form",
            "ufl_operands",
        )

        def __init__(self, arguments):
            """Initialise."""
            BaseForm.__init__(self)
            self._arguments = arguments
            self.ufl_operands = arguments
            self._hash = None
            self._domains = None
            self.form = None

        def _analyze_form_arguments(self):
            """Analyze form arguments."""
            # `self._arguments` is already set in `BaseForm.__init__`
            self._coefficients = ()

        def _analyze_domains(self):
            """Analyze which domains can be found in ZeroBaseForm."""
            from ufl.domain import join_domains, sort_domains

            # Collect unique domains
            self._domains = sort_domains(
                join_domains(chain.from_iterable(e.ufl_domains() for e in self.ufl_operands))
            )

        def ufl_domains(self):
            """Return all domains found in the base form."""
            if self._domains is None:
                self._analyze_domains()
            return self._domains

        def __ne__(self, other):
            """Overwrite BaseForm.__neq__ which relies on `equals`."""
            return not self == other

        def __eq__(self, other):
            """Check equality."""
            if type(other) is ZeroBaseForm:
                if self is other:
                    return True
                return self._arguments == other._arguments
            elif isinstance(other, int | float):
                return other == 0
            else:
                return False

        def __str__(self):
            """Format as a string."""
            return "ZeroBaseForm({})".format(", ".join(str(arg) for arg in self._arguments))

        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