• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FEniCS / ufl / 18629405325

19 Oct 2025 10:56AM UTC coverage: 77.06% (+0.4%) from 76.622%
18629405325

Pull #401

github

schnellerhase
Ruff
Pull Request #401: Removal of custom type system

494 of 533 new or added lines in 41 files covered. (92.68%)

6 existing lines in 2 files now uncovered.

9325 of 12101 relevant lines covered (77.06%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.49
/ufl/form.py
1
"""The Form class."""
2
# Copyright (C) 2008-2016 Martin Sandve Alnæs
3
#
4
# This file is part of UFL (https://www.fenicsproject.org)
5
#
6
# SPDX-License-Identifier:    LGPL-3.0-or-later
7
#
8
# Modified by Anders Logg, 2009-2011.
9
# Modified by Massimiliano Leoni, 2016.
10
# Modified by Cecile Daversin-Catty, 2018.
11
# Modified by Nacime Bouziani, 2020.
12
# Modified by Jørgen S. Dokken 2023.
13

14
import numbers
1✔
15
import warnings
1✔
16
from collections import defaultdict
1✔
17
from itertools import chain
1✔
18

19
from ufl.checks import is_scalar_constant_expression
1✔
20
from ufl.constant import Constant
1✔
21
from ufl.constantvalue import Zero
1✔
22
from ufl.core.compute_expr_hash import compute_expr_hash
1✔
23
from ufl.core.expr import Expr, ufl_err_str
1✔
24
from ufl.core.terminal import FormArgument
1✔
25
from ufl.core.ufl_type import UFLType, ufl_type
1✔
26
from ufl.domain import extract_unique_domain, sort_domains
1✔
27
from ufl.equation import Equation
1✔
28
from ufl.integral import Integral
1✔
29
from ufl.utils.counted import Counted
1✔
30
from ufl.utils.sorting import sorted_by_count
1✔
31

32
# Export list for ufl.classes
33
__all_classes__ = ["Form", "BaseForm", "ZeroBaseForm"]
1✔
34

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

37

38
def _sorted_integrals(integrals):
1✔
39
    """Sort integrals for a stable signature computation.
40

41
    Sort integrals by domain id, integral type, subdomain id for a more
42
    stable signature computation.
43
    """
44
    # Group integrals in multilevel dict by keys
45
    # [domain][integral_type][subdomain_id]
46
    integrals_dict = defaultdict(
1✔
47
        lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
48
    )
49
    for integral in integrals:
1✔
50
        d = integral.ufl_domain()
1✔
51
        if d is None:
1✔
52
            raise ValueError(
×
53
                "Each integral in a form must have a uniquely defined integration domain."
54
            )
55
        it = integral.integral_type()
1✔
56
        si = integral.subdomain_id()
1✔
57
        # Make a sortable key.
58
        extra_domain_itype_sortable = tuple(
1✔
59
            (d_._ufl_sort_key_(), itype_)
60
            for d_, itype_ in integral.extra_domain_integral_type_map().items()
61
        )
62
        integrals_dict[d][it][extra_domain_itype_sortable][si].append(integral)
1✔
63

64
    all_integrals = []
1✔
65

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

74
    # Order integrals canonically to increase signature stability
75
    for d in sort_domains(integrals_dict):
1✔
76
        for it in sorted(integrals_dict[d]):  # str is sortable
1✔
77
            for extra_domain_itype_sortable in sorted(integrals_dict[d][it]):
1✔
78
                for si in sorted(integrals_dict[d][it][extra_domain_itype_sortable], key=keyfunc):
1✔
79
                    unsorted_integrals = integrals_dict[d][it][extra_domain_itype_sortable][si]
1✔
80
                    # TODO: At this point we could order integrals by
81
                    #       metadata and integrand, or even add the
82
                    #       integrands with the same metadata. This is done
83
                    #       in accumulate_integrands_with_same_metadata in
84
                    #       algorithms/domain_analysis.py and would further
85
                    #       increase the signature stability.
86
                    all_integrals.extend(unsorted_integrals)
1✔
87
                    # integrals_dict[d][it][extra_domain_itype_sortable][si] = unsorted_integrals
88

89
    return tuple(all_integrals)  # integrals_dict
1✔
90

91

92
class BaseForm(UFLType):
1✔
93
    """Description of an object containing arguments."""
94

95
    ufl_operands: tuple[FormArgument, ...]
1✔
96

97
    def __init__(self):
1✔
98
        """Initialise."""
99
        # Internal variables for caching form argument/coefficient data
100
        self._arguments = None
1✔
101
        self._coefficients = None
1✔
102

103
    # --- Accessor interface ---
104
    def arguments(self):
1✔
105
        """Return all ``Argument`` objects found in form."""
106
        if self._arguments is None:
1✔
107
            self._analyze_form_arguments()
1✔
108
        return self._arguments
1✔
109

110
    def coefficients(self):
1✔
111
        """Return all ``Coefficient`` objects found in form."""
112
        if self._coefficients is None:
1✔
113
            self._analyze_form_arguments()
1✔
114
        return self._coefficients
1✔
115

116
    def geometric_quantities(self):
1✔
117
        """Return all ``GeometricQuantity`` objects found in form."""
118
        if self._geometric_quantities is None:
1✔
119
            self._analyze_form_arguments()
×
120
        return self._geometric_quantities
1✔
121

122
    def ufl_domain(self):
1✔
123
        """Return the single geometric integration domain occuring in the base form.
124

125
        Fails if multiple domains are found.
126
        """
127
        try:
×
128
            (domain,) = set(self.ufl_domains())
×
129
        except ValueError:
×
130
            raise ValueError(f"{type(self).__name__} must have exactly one domain.")
×
131
        # Return the one and only domain
132
        return domain
×
133

134
    def empty(self):
1✔
135
        """Returns whether the BaseForm has no components."""
136
        return False
×
137

138
    # --- Operator implementations ---
139

140
    def __eq__(self, other):
1✔
141
        """Delayed evaluation of the == operator.
142

143
        Just 'lhs_form == rhs_form' gives an Equation,
144
        while 'bool(lhs_form == rhs_form)' delegates
145
        to lhs_form.equals(rhs_form).
146
        """
147
        return Equation(self, other)
1✔
148

149
    def __hash__(self):
1✔
150
        """Return hash."""
NEW
151
        return compute_expr_hash(self)
×
152

153
    def __radd__(self, other):
1✔
154
        """Add."""
155
        # Ordering of form additions make no difference
156
        return self.__add__(other)
1✔
157

158
    def __add__(self, other):
1✔
159
        """Add."""
160
        if isinstance(other, numbers.Number) and other == 0:
1✔
161
            # Allow adding 0 or 0.0 as a no-op, needed for sum([a,b])
162
            return self
1✔
163
        elif isinstance(other, Zero):
1✔
164
            # Allow adding ufl Zero as a no-op, needed for sum([a,b])
165
            return self
1✔
166

167
        elif isinstance(other, ZeroBaseForm):
1✔
168
            # Simplify addition with ZeroBaseForm
169
            return self
1✔
170

171
        # For `ZeroBaseForm(...) + B` with B a BaseForm.
172
        # We could overwrite ZeroBaseForm.__add__ but that implies
173
        # duplicating cases with `0` and `ufl.Zero`.
174
        elif isinstance(self, ZeroBaseForm):
1✔
175
            # Simplify addition with ZeroBaseForm
176
            return other
1✔
177

178
        elif isinstance(other, BaseForm):
1✔
179
            # Add integrals from both forms
180
            return FormSum((self, 1), (other, 1))
1✔
181

182
        else:
183
            # Let python protocols do their job if we don't handle it
184
            return NotImplemented
×
185

186
    def __sub__(self, other):
1✔
187
        """Subtract other form from this one."""
188
        return self + (-other)
1✔
189

190
    def __rsub__(self, other):
1✔
191
        """Subtract this form from other."""
192
        return other + (-self)
×
193

194
    def __neg__(self):
1✔
195
        """Negate all integrals in form.
196

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

206
    def __rmul__(self, scalar):
1✔
207
        """Multiply all integrals in form with constant scalar value."""
208
        # This enables the handy "0*form" or "dt*form" syntax
209
        if is_scalar_constant_expression(scalar):
1✔
210
            return FormSum((self, scalar))
1✔
211
        return NotImplemented
×
212

213
    def __mul__(self, coefficient):
1✔
214
        """Take the action of this form on the given coefficient."""
215
        if isinstance(coefficient, Expr):
1✔
216
            from ufl.formoperators import action
1✔
217

218
            return action(self, coefficient)
1✔
219
        return NotImplemented
×
220

221
    def __ne__(self, other):
1✔
222
        """Immediately evaluate the != operator (as opposed to the == operator)."""
223
        return not self.equals(other)
1✔
224

225
    def __call__(self, x):
1✔
226
        """Take the action of this form on ``x``."""
227
        from ufl.formoperators import action
1✔
228

229
        return action(self, x)
1✔
230

231
    def _ufl_compute_hash_(self):
1✔
232
        """Compute the hash."""
233
        # Ensure compatibility with MultiFunction
234
        # `hash(self)` will call the `__hash__` method of the subclass.
235
        return hash(self)
1✔
236

237
    def _ufl_expr_reconstruct_(self, *operands):
1✔
238
        """Return a new object of the same type with new operands."""
239
        return type(self)(*operands)
×
240

241
    __matmul__ = __mul__
1✔
242

243

244
@ufl_type()
1✔
245
class Form(BaseForm):
1✔
246
    """Description of a weak form consisting of a sum of integrals over subdomains."""
247

248
    __slots__ = (
1✔
249
        "_arguments",
250
        "_base_form_operators",
251
        # --- Dict that external frameworks can place framework-specific
252
        #     data in to be carried with the form
253
        #     Never use this internally in ufl!
254
        "_cache",
255
        "_coefficient_numbering",
256
        "_coefficients",
257
        "_constant_numbering",
258
        "_constants",
259
        "_domain_numbering",
260
        "_geometric_quantities",
261
        "_hash",
262
        # --- List of Integral objects (a Form is a sum of these
263
        # Integrals, everything else is derived)
264
        "_integrals",
265
        # --- Internal variables for caching various data
266
        "_integration_domains",
267
        "_signature",
268
        "_subdomain_data",
269
        "_terminal_numbering",
270
    )
271

272
    def __init__(self, integrals):
1✔
273
        """Initialise."""
274
        BaseForm.__init__(self)
1✔
275
        # Basic input checking (further compatibilty analysis happens
276
        # later)
277
        if not all(isinstance(itg, Integral) for itg in integrals):
1✔
278
            raise ValueError("Expecting list of integrals.")
×
279

280
        # Store integrals sorted canonically to increase signature
281
        # stability
282
        self._integrals = _sorted_integrals(integrals)
1✔
283

284
        # Internal variables for caching domain data
285
        self._integration_domains = None
1✔
286
        self._domain_numbering = None
1✔
287

288
        # Internal variables for caching subdomain data
289
        self._subdomain_data = None
1✔
290

291
        # Internal variables for caching form argument data
292
        self._coefficients = None
1✔
293
        self._coefficient_numbering = None
1✔
294
        self._constant_numbering = None
1✔
295
        self._terminal_numbering = None
1✔
296

297
        # Internal variables for caching base form operator data
298
        self._base_form_operators = None
1✔
299

300
        from ufl.algorithms.analysis import extract_constants
1✔
301

302
        self._constants = extract_constants(self)
1✔
303

304
        # Internal variables for caching of hash and signature after
305
        # first request
306
        self._hash = None
1✔
307
        self._signature = None
1✔
308

309
        # Never use this internally in ufl!
310
        self._cache = {}
1✔
311

312
    # --- Accessor interface ---
313

314
    def integrals(self):
1✔
315
        """Return a sequence of all integrals in form."""
316
        return self._integrals
1✔
317

318
    def integrals_by_type(self, integral_type):
1✔
319
        """Return a sequence of all integrals with a particular domain type."""
320
        return tuple(
1✔
321
            integral for integral in self.integrals() if integral.integral_type() == integral_type
322
        )
323

324
    def integrals_by_domain(self, domain):
1✔
325
        """Return a sequence of all integrals with a particular integration domain."""
326
        return tuple(integral for integral in self.integrals() if integral.ufl_domain() == domain)
×
327

328
    def empty(self):
1✔
329
        """Returns whether the form has no integrals."""
330
        return len(self.integrals()) == 0
1✔
331

332
    def ufl_domains(self):
1✔
333
        """Return the geometric integration domains occuring in the form.
334

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

338
        The return type is a tuple even if only a single domain exists.
339
        """
340
        if self._integration_domains is None:
1✔
341
            self._analyze_domains()
1✔
342
        return self._integration_domains
1✔
343

344
    def ufl_cell(self):
1✔
345
        """Return the single cell this form is defined on.
346

347
        Fails if multiple cells are found.
348
        """
349
        return self.ufl_domain().ufl_cell()
×
350

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

361
    def domain_numbering(self):
1✔
362
        """Return a contiguous numbering of domains in a mapping ``{domain:number}``."""
363
        if self._domain_numbering is None:
1✔
364
            self._analyze_domains()
1✔
365
        return self._domain_numbering
1✔
366

367
    def subdomain_data(self):
1✔
368
        """Returns a mapping on the form ``{domain:{integral_type: subdomain_data}}``."""
369
        if self._subdomain_data is None:
1✔
370
            self._analyze_subdomain_data()
1✔
371
        return self._subdomain_data
1✔
372

373
    def coefficients(self):
1✔
374
        """Return all ``Coefficient`` objects found in form."""
375
        if self._coefficients is None:
1✔
376
            self._analyze_form_arguments()
1✔
377
        return self._coefficients
1✔
378

379
    def base_form_operators(self):
1✔
380
        """Return all ``BaseFormOperator`` objects found in form."""
381
        if self._base_form_operators is None:
1✔
382
            self._analyze_base_form_operators()
1✔
383
        return self._base_form_operators
1✔
384

385
    def coefficient_numbering(self):
1✔
386
        """Return a contiguous numbering of coefficients in a mapping ``{coefficient:number}``."""
387
        # cyclic import
388
        from ufl.coefficient import Coefficient
×
389

390
        if self._coefficient_numbering is None:
×
391
            self._coefficient_numbering = {
×
392
                expr: num
393
                for expr, num in self.terminal_numbering().items()
394
                if isinstance(expr, Coefficient)
395
            }
396
        return self._coefficient_numbering
×
397

398
    def constants(self):
1✔
399
        """Get constants."""
400
        return self._constants
1✔
401

402
    def constant_numbering(self):
1✔
403
        """Return a contiguous numbering of constants in a mapping ``{constant:number}``."""
404
        if self._constant_numbering is None:
×
405
            self._constant_numbering = {
×
406
                expr: num
407
                for expr, num in self.terminal_numbering().items()
408
                if isinstance(expr, Constant)
409
            }
410
        return self._constant_numbering
×
411

412
    def terminal_numbering(self):
1✔
413
        """Return a contiguous numbering for all counted objects in the form.
414

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

417
        The numbering is computed per type so :class:`Coefficient`,
418
        :class:`Constant`, etc will each be numbered from zero.
419
        """
420
        # cyclic import
421
        from ufl.algorithms.analysis import extract_type
1✔
422

423
        if self._terminal_numbering is None:
1✔
424
            exprs_by_type = defaultdict(set)
1✔
425
            for counted_expr in extract_type(self, Counted):
1✔
426
                exprs_by_type[counted_expr._counted_class].add(counted_expr)
1✔
427

428
            numbering = {}
1✔
429
            for exprs in exprs_by_type.values():
1✔
430
                for i, expr in enumerate(sorted_by_count(exprs)):
1✔
431
                    numbering[expr] = i
1✔
432
            self._terminal_numbering = numbering
1✔
433
        return self._terminal_numbering
1✔
434

435
    def signature(self):
1✔
436
        """Signature for use with jit cache (independent of incidental numbering of indices etc)."""
437
        if self._signature is None:
1✔
438
            self._compute_signature()
1✔
439
        return self._signature
1✔
440

441
    # --- Operator implementations ---
442

443
    def __hash__(self):
1✔
444
        """Hash."""
445
        if self._hash is None:
1✔
446
            self._hash = hash(tuple(hash(itg) for itg in self.integrals()))
1✔
447
        return self._hash
1✔
448

449
    def __ne__(self, other):
1✔
450
        """Immediate evaluation of the != operator (as opposed to the == operator)."""
451
        return not self.equals(other)
1✔
452

453
    def equals(self, other):
1✔
454
        """Evaluate ``bool(lhs_form == rhs_form)``."""
455
        if type(other) is not Form:
1✔
456
            return False
1✔
457
        if len(self._integrals) != len(other._integrals):
1✔
458
            return False
×
459
        if hash(self) != hash(other):
1✔
460
            return False
×
461
        return all(a == b for a, b in zip(self._integrals, other._integrals))
1✔
462

463
    def __radd__(self, other):
1✔
464
        """Add."""
465
        # Ordering of form additions make no difference
466
        return self.__add__(other)
1✔
467

468
    def __add__(self, other):
1✔
469
        """Add."""
470
        if isinstance(other, Form):
1✔
471
            # Add integrals from both forms
472
            return Form(list(chain(self.integrals(), other.integrals())))
1✔
473

474
        if isinstance(other, ZeroBaseForm):
1✔
475
            # Simplify addition with ZeroBaseForm
476
            return self
1✔
477

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

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

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

490
        else:
491
            # Let python protocols do their job if we don't handle it
492
            return NotImplemented
×
493

494
    def __sub__(self, other):
1✔
495
        """Subtract other form from this one."""
496
        return self + (-other)
1✔
497

498
    def __rsub__(self, other):
1✔
499
        """Subtract this form from other."""
500
        return other + (-self)
×
501

502
    def __neg__(self):
1✔
503
        """Negate all integrals in form.
504

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

510
    def __rmul__(self, scalar):
1✔
511
        """Multiply all integrals in form with constant scalar value."""
512
        # This enables the handy "0*form" or "dt*form" syntax
513
        if is_scalar_constant_expression(scalar):
1✔
514
            return Form([scalar * itg for itg in self.integrals()])
1✔
515
        return NotImplemented
×
516

517
    def __mul__(self, coefficient):
1✔
518
        """UFL form operator: Take the action of this form on the given coefficient."""
519
        if isinstance(coefficient, Expr):
1✔
520
            from ufl.formoperators import action
1✔
521

522
            return action(self, coefficient)
1✔
523
        return NotImplemented
×
524

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

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

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

535
        Example:
536
          V = FiniteElement("CG", triangle, 1)
537
          v = TestFunction(V)
538
          u = TrialFunction(V)
539
          f = Coefficient(V)
540
          g = Coefficient(V)
541
          a = g*inner(grad(u), grad(v))*dx
542
          M = a(f, f, coefficients={ g: 1 })
543

544
        Is equivalent to M == grad(f)**2*dx.
545
        """
546
        repdict = {}
1✔
547

548
        if args:
1✔
549
            arguments = self.arguments()
1✔
550
            if len(arguments) != len(args):
1✔
551
                raise ValueError(f"Need {len(arguments)} arguments to form(), got {len(args)}.")
×
552
            repdict.update(zip(arguments, args))
1✔
553

554
        coefficients = kwargs.pop("coefficients", None)
1✔
555
        if kwargs:
1✔
556
            raise ValueError(f"Unknown kwargs {list(kwargs)}")
×
557

558
        if coefficients is not None:
1✔
559
            coeffs = self.coefficients()
1✔
560
            for f in coefficients:
1✔
561
                if f in coeffs:
1✔
562
                    repdict[f] = coefficients[f]
1✔
563
                else:
564
                    warnings.warn(f"Coefficient {ufl_err_str(f)} is not in form.")
×
565
        if repdict:
1✔
566
            from ufl.formoperators import replace
1✔
567

568
            return replace(self, repdict)
1✔
569
        else:
570
            return self
×
571

572
    __matmul__ = __mul__
1✔
573

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

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

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

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

597
    def _analyze_domains(self):
1✔
598
        """Analyze domains."""
599
        from ufl.domain import join_domains, sort_domains
1✔
600

601
        # Collect integration domains.
602
        self._integration_domains = sort_domains(
1✔
603
            join_domains([itg.ufl_domain() for itg in self._integrals])
604
        )
605
        # Collect domains in extra_domain_integral_type_map.
606
        domains_in_extra_domain_integral_type_map = join_domains(
1✔
607
            [d for itg in self._integrals for d in itg.extra_domain_integral_type_map()]
608
        )
609
        domains_in_extra_domain_integral_type_map -= set(self._integration_domains)
1✔
610
        # Collect domains in integrands.
611
        domains_in_integrands = set()
1✔
612
        for o in chain(
1✔
613
            self.arguments(), self.coefficients(), self.constants(), self.geometric_quantities()
614
        ):
615
            domain = extract_unique_domain(o, expand_mesh_sequence=False)
1✔
616
            domains_in_integrands.update(domain.meshes)
1✔
617
        domains_in_integrands -= set(self._integration_domains)
1✔
618
        all_domains = self._integration_domains + sort_domains(
1✔
619
            join_domains(domains_in_extra_domain_integral_type_map | domains_in_integrands)
620
        )
621
        # Let problem solving environments access all domains via
622
        # self._domain_numbering.keys() (wrapped in extract_domains()).
623
        self._domain_numbering = {d: i for i, d in enumerate(all_domains)}
1✔
624

625
    def _analyze_subdomain_data(self):
1✔
626
        """Analyze subdomain data."""
627
        integration_domains = self.ufl_domains()
1✔
628
        integrals = self.integrals()
1✔
629

630
        # Make clear data structures to collect subdomain data in
631
        subdomain_data = {}
1✔
632
        for domain in integration_domains:
1✔
633
            subdomain_data[domain] = {}
1✔
634

635
        for integral in integrals:
1✔
636
            # Get integral properties
637
            domain = integral.ufl_domain()
1✔
638
            it = integral.integral_type()
1✔
639
            sd = integral.subdomain_data()
1✔
640

641
            # Collect subdomain data
642
            if subdomain_data[domain].get(it) is None:
1✔
643
                subdomain_data[domain][it] = [sd]
1✔
644
            else:
645
                subdomain_data[domain][it].append(sd)
×
646
        self._subdomain_data = subdomain_data
1✔
647

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

652
        arguments, coefficients, geometric_quantities = extract_terminals_with_domain(self)
1✔
653

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

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

663
        base_form_ops = extract_base_form_operators(self)
1✔
664
        self._base_form_operators = tuple(sorted(base_form_ops, key=lambda x: x.count()))
1✔
665

666
    def _compute_renumbering(self):
1✔
667
        """Compute renumbering."""
668
        dn = self.domain_numbering()
1✔
669
        tn = self.terminal_numbering()
1✔
670
        renumbering = {}
1✔
671
        renumbering.update(dn)
1✔
672
        renumbering.update(tn)
1✔
673
        return renumbering
1✔
674

675
    def _compute_signature(self):
1✔
676
        """Compute signature."""
677
        from ufl.algorithms.signature import compute_form_signature
1✔
678

679
        self._signature = compute_form_signature(self, self._compute_renumbering())
1✔
680

681

682
def as_form(form):
1✔
683
    """Convert to form if not a form, otherwise return form."""
684
    if not isinstance(form, BaseForm) and form != 0:
1✔
685
        raise ValueError(f"Unable to convert object to a UFL form: {ufl_err_str(form)}")
×
686
    return form
1✔
687

688

689
@ufl_type()
1✔
690
class FormSum(BaseForm):
1✔
691
    """Form sum.
692

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

698
    __slots__ = (
1✔
699
        "_arguments",
700
        "_coefficients",
701
        "_components",
702
        "_domain_numbering",
703
        "_domains",
704
        "_hash",
705
        "_weights",
706
        "ufl_operands",
707
    )
708

709
    def __new__(cls, *args, **kwargs):
1✔
710
        """Create a new FormSum."""
711
        # All the components are `ZeroBaseForm`
712
        if all(component == 0 for component, _ in args):
1✔
713
            # Assume that the arguments of all the components have
714
            # consistent with each other  and select the first one to
715
            # define the arguments of `ZeroBaseForm`.
716
            # This might not always be true but `ZeroBaseForm`'s
717
            # arguments are not checked anywhere because we can't
718
            # reliably always infer them.
719
            ((arg, _), *_) = args
×
720
            arguments = arg.arguments()
×
721
            return ZeroBaseForm(arguments)
×
722

723
        return super().__new__(cls)
1✔
724

725
    def __init__(self, *components):
1✔
726
        """Initialise."""
727
        BaseForm.__init__(self)
1✔
728

729
        # Remove `ZeroBaseForm` components
730
        filtered_components = [(component, w) for component, w in components if component != 0]
1✔
731

732
        weights = []
1✔
733
        full_components = []
1✔
734
        for component, w in filtered_components:
1✔
735
            if isinstance(component, FormSum):
1✔
736
                full_components.extend(component.components())
1✔
737
                weights.extend([w * wc for wc in component.weights()])
1✔
738
            else:
739
                full_components.append(component)
1✔
740
                weights.append(w)
1✔
741

742
        self._arguments = None
1✔
743
        self._coefficients = None
1✔
744
        self._domains = None
1✔
745
        self._domain_numbering = None
1✔
746
        self._hash = None
1✔
747
        self._weights = weights
1✔
748
        self._components = full_components
1✔
749
        self._sum_variational_components()
1✔
750
        self.ufl_operands = self._components
1✔
751

752
    def components(self):
1✔
753
        """Get components."""
754
        return self._components
1✔
755

756
    def weights(self):
1✔
757
        """Get weights."""
758
        return self._weights
1✔
759

760
    def _sum_variational_components(self):
1✔
761
        """Sum variational components."""
762
        var_forms = None
1✔
763
        other_components = []
1✔
764
        new_weights = []
1✔
765
        for i, component in enumerate(self._components):
1✔
766
            if isinstance(component, Form):
1✔
767
                if var_forms:
1✔
768
                    var_forms = var_forms + (self._weights[i] * component)
1✔
769
                else:
770
                    var_forms = self._weights[i] * component
1✔
771
            else:
772
                other_components.append(component)
1✔
773
                new_weights.append(self._weights[i])
1✔
774
        if var_forms:
1✔
775
            other_components.insert(0, var_forms)
1✔
776
            new_weights.insert(0, 1)
1✔
777
        self._components = other_components
1✔
778
        self._weights = new_weights
1✔
779

780
    def _analyze_form_arguments(self):
1✔
781
        """Return all ``Argument`` objects found in form."""
782
        arguments = []
×
783
        coefficients = []
×
784
        for component in self._components:
×
785
            arguments.extend(component.arguments())
×
786
            coefficients.extend(component.coefficients())
×
787
        # Define canonical numbering of arguments and coefficients
788
        self._arguments = tuple(sorted(set(arguments), key=lambda x: x.number()))
×
789
        self._coefficients = tuple(sorted(set(coefficients), key=lambda x: x.count()))
×
790

791
    def _analyze_domains(self):
1✔
792
        """Analyze which domains can be found in FormSum."""
793
        from ufl.domain import join_domains, sort_domains
×
794

795
        # Collect unique domains
796
        self._domains = sort_domains(
×
797
            join_domains(chain.from_iterable(c.ufl_domains() for c in self.components()))
798
        )
799

800
    def ufl_domains(self):
1✔
801
        """Return all domains found in the base form."""
802
        if self._domains is None:
×
803
            self._analyze_domains()
×
804
        return self._domains
×
805

806
    def __hash__(self):
1✔
807
        """Hash."""
808
        if self._hash is None:
×
809
            self._hash = hash(
×
810
                tuple((hash(c), hash(w)) for c, w in zip(self.components(), self.weights()))
811
            )
812
        return self._hash
×
813

814
    def equals(self, other):
1✔
815
        """Evaluate ``bool(lhs_form == rhs_form)``."""
816
        if type(other) is not FormSum:
1✔
817
            return False
1✔
818
        if self is other:
1✔
819
            return True
×
820
        return (
1✔
821
            len(self.components()) == len(other.components())
822
            and all(a == b for a, b in zip(self.components(), other.components()))
823
            and all(a == b for a, b in zip(self.weights(), other.weights()))
824
        )
825

826
    def empty(self):
1✔
827
        """Returns whether the FormSum has no components."""
828
        return len(self.components()) == 0
×
829

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

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

849

850
@ufl_type()
1✔
851
class ZeroBaseForm(BaseForm):
1✔
852
    """Description of a zero base form.
853

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

858
    __slots__ = (
1✔
859
        "_arguments",
860
        "_coefficients",
861
        "_domains",
862
        "_hash",
863
        # Pyadjoint compatibility
864
        "form",
865
        "ufl_operands",
866
    )
867

868
    def __init__(self, arguments):
1✔
869
        """Initialise."""
870
        BaseForm.__init__(self)
1✔
871
        self._arguments = arguments
1✔
872
        self.ufl_operands = arguments
1✔
873
        self._hash = None
1✔
874
        self._domains = None
1✔
875
        self.form = None
1✔
876

877
    def _analyze_form_arguments(self):
1✔
878
        """Analyze form arguments."""
879
        # `self._arguments` is already set in `BaseForm.__init__`
880
        self._coefficients = ()
1✔
881

882
    def _analyze_domains(self):
1✔
883
        """Analyze which domains can be found in ZeroBaseForm."""
884
        from ufl.domain import join_domains, sort_domains
×
885

886
        # Collect unique domains
887
        self._domains = sort_domains(
×
888
            join_domains(chain.from_iterable(e.ufl_domains() for e in self.ufl_operands))
889
        )
890

891
    def ufl_domains(self):
1✔
892
        """Return all domains found in the base form."""
893
        if self._domains is None:
×
894
            self._analyze_domains()
×
895
        return self._domains
×
896

897
    def __ne__(self, other):
1✔
898
        """Overwrite BaseForm.__neq__ which relies on `equals`."""
899
        return not self == other
1✔
900

901
    def __eq__(self, other):
1✔
902
        """Check equality."""
903
        if type(other) is ZeroBaseForm:
1✔
904
            if self is other:
1✔
905
                return True
×
906
            return self._arguments == other._arguments
1✔
907
        elif isinstance(other, int | float):
1✔
908
            return other == 0
1✔
909
        else:
910
            return False
×
911

912
    def __hash__(self):
1✔
913
        """Hash."""
914
        if self._hash is None:
1✔
915
            self._hash = hash(("ZeroBaseForm", hash(self._arguments)))
1✔
916
        return self._hash
1✔
917

918
    def __str__(self):
1✔
919
        """Format as a string."""
920
        return "ZeroBaseForm({})".format(", ".join(str(arg) for arg in self._arguments))
×
921

922
    def __repr__(self):
1✔
923
        """Representation."""
924
        return "ZeroBaseForm({})".format(", ".join(repr(arg) for arg in self._arguments))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc