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

FEniCS / ufl / 19027714355

31 Oct 2025 02:01PM UTC coverage: 76.764% (+0.08%) from 76.686%
19027714355

push

github

web-flow
add CellSequence class (#429)

59 of 83 new or added lines in 7 files covered. (71.08%)

1 existing line in 1 file now uncovered.

9227 of 12020 relevant lines covered (76.76%)

0.77 hits per line

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

92.52
/ufl/algorithms/compute_form_data.py
1
"""This module provides the compute_form_data function.
2

3
Form compilers will typically call compute_form_dataprior to code
4
generation to preprocess/simplify a raw input form given by a user.
5
"""
6
# Copyright (C) 2008-2016 Martin Sandve Alnæs
7
#
8
# This file is part of UFL (https://www.fenicsproject.org)
9
#
10
# SPDX-License-Identifier:    LGPL-3.0-or-later
11

12
from itertools import chain
1✔
13

14
from ufl.algorithms.analysis import extract_coefficients, extract_sub_elements, unique_tuple
1✔
15
from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
1✔
16

17
# These are the main symbolic processing steps:
18
from ufl.algorithms.apply_coefficient_split import CoefficientSplitter
1✔
19
from ufl.algorithms.apply_derivatives import apply_coordinate_derivatives, apply_derivatives
1✔
20
from ufl.algorithms.apply_function_pullbacks import apply_function_pullbacks
1✔
21
from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering
1✔
22
from ufl.algorithms.apply_integral_scaling import apply_integral_scaling
1✔
23
from ufl.algorithms.apply_restrictions import apply_restrictions, default_restriction_map
1✔
24
from ufl.algorithms.check_arities import check_form_arity
1✔
25
from ufl.algorithms.comparison_checker import do_comparison_check
1✔
26

27
# See TODOs at the call sites of these below:
28
from ufl.algorithms.domain_analysis import (
1✔
29
    build_integral_data,
30
    group_form_integrals,
31
    reconstruct_form_from_integral_data,
32
)
33
from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree
1✔
34
from ufl.algorithms.formdata import FormData
1✔
35
from ufl.algorithms.formtransformations import compute_form_arities
1✔
36
from ufl.algorithms.remove_complex_nodes import remove_complex_nodes
1✔
37
from ufl.algorithms.remove_component_tensors import remove_component_tensors
1✔
38
from ufl.algorithms.replace import replace
1✔
39
from ufl.classes import Coefficient, Form, FunctionSpace, GeometricFacetQuantity
1✔
40
from ufl.constantvalue import Zero
1✔
41
from ufl.corealg.traversal import traverse_unique_terminals
1✔
42
from ufl.domain import MeshSequence, extract_domains, extract_unique_domain
1✔
43
from ufl.utils.sequences import max_degree
1✔
44

45

46
def _auto_select_degree(elements):
1✔
47
    """Automatically select degree for all elements of the form.
48

49
    This is be used in cases where the degree has not been specified by the user.
50
    This feature is used by DOLFIN to allow the specification of Expressions with
51
    undefined degrees.
52
    """
53
    # Use max degree of all elements, at least 1 (to work with
54
    # Lagrange elements)
55
    return max_degree({e.embedded_superdegree for e in elements} - {None} | {1})
1✔
56

57

58
def _compute_element_mapping(form):
1✔
59
    """Compute element mapping for element replacement."""
60
    # The element mapping is a slightly messy concept with two use
61
    # cases:
62
    # - Expression with missing cell or element TODO: Implement proper
63
    #   Expression handling in UFL and get rid of this
64
    # - Constant with missing cell TODO: Fix anything that needs to be
65
    #   worked around to drop this requirement
66

67
    # Extract all elements and include subelements of mixed elements
68
    elements = [obj.ufl_element() for obj in chain(form.arguments(), form.coefficients())]
1✔
69
    elements = extract_sub_elements(elements)
1✔
70

71
    # Try to find a common degree for elements
72
    common_degree = _auto_select_degree(elements)
1✔
73

74
    # Compute element map
75
    element_mapping = {}
1✔
76
    for element in elements:
1✔
77
        # Flag for whether element needs to be reconstructed
78
        reconstruct = False
1✔
79

80
        # Set cell
81
        cell = element.cell
1✔
82
        if cell is None:
1✔
83
            domains = form.ufl_domains()
×
84
            if not all(domains[0].ufl_cell() == d.ufl_cell() for d in domains):
×
85
                raise ValueError(
×
86
                    "Cannot replace unknown element cell without unique common cell in form."
87
                )
88
            cell = domains[0].ufl_cell()
×
89
            reconstruct = True
×
90

91
        # Set degree
92
        degree = element.embedded_superdegree
1✔
93
        if degree is None:
1✔
94
            degree = common_degree
×
95
            reconstruct = True
×
96

97
        # Reconstruct element and add to map
98
        if reconstruct:
1✔
99
            element_mapping[element] = element.reconstruct(cell=cell, degree=degree)
×
100
        else:
101
            element_mapping[element] = element
1✔
102

103
    return element_mapping
1✔
104

105

106
def _compute_max_subdomain_ids(integral_data):
1✔
107
    """Compute the maximum subdomain ids."""
108
    max_subdomain_ids = {}
1✔
109
    for itg_data in integral_data:
1✔
110
        it = itg_data.integral_type
1✔
111
        for integral in itg_data.integrals:
1✔
112
            # Convert string for default integral to -1
113
            sids = (-1 if isinstance(si, str) else si for si in integral.subdomain_id())
1✔
114
            newmax = max(sids) + 1
1✔
115
            prevmax = max_subdomain_ids.get(it, 0)
1✔
116
            max_subdomain_ids[it] = max(prevmax, newmax)
1✔
117
    return max_subdomain_ids
1✔
118

119

120
def _compute_form_data_elements(self, arguments, coefficients, domains):
1✔
121
    """Compute form data elements."""
122
    self.argument_elements = tuple(f.ufl_element() for f in arguments)
1✔
123
    self.coefficient_elements = tuple(f.ufl_element() for f in coefficients)
1✔
124
    self.coordinate_elements = tuple(domain.ufl_coordinate_element() for domain in domains)
1✔
125

126
    # TODO: Include coordinate elements from argument and coefficient
127
    # domains as well? Can they differ?
128

129
    # Note: Removed self.elements and self.sub_elements to make sure
130
    #       code that depends on the selection of argument +
131
    #       coefficient elements blow up, as opposed to silently
132
    #       almost working, with the introduction of the coordinate
133
    #       elements here.
134

135
    all_elements = self.argument_elements + self.coefficient_elements + self.coordinate_elements
1✔
136
    all_sub_elements = extract_sub_elements(all_elements)
1✔
137

138
    self.unique_elements = unique_tuple(all_elements)
1✔
139
    self.unique_sub_elements = unique_tuple(all_sub_elements)
1✔
140

141

142
def _check_elements(form_data):
1✔
143
    """Check elements."""
144
    for element in chain(form_data.unique_elements, form_data.unique_sub_elements):
1✔
145
        if element.cell is None:
1✔
146
            raise ValueError(f"Found element with undefined cell: {element}")
×
147

148

149
def _check_facet_geometry(integral_data):
1✔
150
    """Check facet geometry."""
151
    for itg_data in integral_data:
1✔
152
        for itg in itg_data.integrals:
1✔
153
            for expr in traverse_unique_terminals(itg.integrand()):
1✔
154
                cls = expr._ufl_class_
1✔
155
                if issubclass(cls, GeometricFacetQuantity):
1✔
156
                    domain = extract_unique_domain(expr, expand_mesh_sequence=False)
1✔
157
                    if isinstance(domain, MeshSequence):
1✔
NEW
158
                        raise RuntimeError(
×
159
                            f"Not expecting a terminal object on a "
160
                            f"mesh sequence at this stage: found {expr!r}"
161
                        )
162
                    it = itg_data.domain_integral_type_map[domain]
1✔
163
                    # Facet geometry is only valid in facet integrals.
164
                    # Allowing custom integrals to pass as well, although
165
                    # that's not really strict enough.
166
                    if not ("facet" in it or "custom" in it or "interface" in it):
1✔
167
                        # Not a facet integral
UNCOV
168
                        raise ValueError(f"Integral of type {it} cannot contain a {cls.__name__}.")
×
169

170

171
def _check_form_arity(preprocessed_form):
1✔
172
    """Check that we don't have a mixed linear/bilinear form or anything like that."""
173
    # FIXME: This is slooow and should be moved to form compiler
174
    # and/or replaced with something faster
175
    if 1 != len(compute_form_arities(preprocessed_form)):
×
176
        raise ValueError("All terms in form must have same rank.")
×
177

178

179
def _build_coefficient_replace_map(coefficients, element_mapping=None):
1✔
180
    """Create new Coefficient objects with count starting at 0.
181

182
    Returns:
183
        mapping from old to new objects, and lists of the new objects
184
    """
185
    if element_mapping is None:
1✔
186
        element_mapping = {}
×
187

188
    new_coefficients = []
1✔
189
    replace_map = {}
1✔
190
    for i, f in enumerate(coefficients):
1✔
191
        old_e = f.ufl_element()
1✔
192
        new_e = element_mapping.get(old_e, old_e)
1✔
193
        # XXX: This is a hack to ensure that if the original
194
        # coefficient had a domain, the new one does too.
195
        # This should be overhauled with requirement that Expressions
196
        # always have a domain.
197
        domain = extract_unique_domain(f, expand_mesh_sequence=False)
1✔
198
        if domain is not None:
1✔
199
            new_e = FunctionSpace(domain, new_e)
1✔
200
        new_f = Coefficient(new_e, count=i)
1✔
201
        new_coefficients.append(new_f)
1✔
202
        replace_map[f] = new_f
1✔
203

204
    return new_coefficients, replace_map
1✔
205

206

207
def attach_estimated_degrees(form):
1✔
208
    """Attach estimated polynomial degree to a form's integrals.
209

210
    Args:
211
        form: The Form` to inspect.
212

213
    Returns:
214
        A new Form with estimate degrees attached.
215
    """
216
    integrals = form.integrals()
1✔
217

218
    new_integrals = []
1✔
219
    for integral in integrals:
1✔
220
        md = {}
1✔
221
        md.update(integral.metadata())
1✔
222
        degree = estimate_total_polynomial_degree(integral.integrand())
1✔
223
        md["estimated_polynomial_degree"] = degree
1✔
224
        new_integrals.append(integral.reconstruct(metadata=md))
1✔
225
    return Form(new_integrals)
1✔
226

227

228
def preprocess_form(form, complex_mode):
1✔
229
    """Preprocess a form."""
230
    # Note: Default behaviour here will process form the way that is
231
    # currently expected by vanilla FFC
232

233
    # Check that the form does not try to compare complex quantities:
234
    # if the quantites being compared are 'provably' real, wrap them
235
    # with Real, otherwise throw an error.
236
    if complex_mode:
1✔
237
        form = do_comparison_check(form)
1✔
238

239
    # Lower abstractions for tensor-algebra types into index notation,
240
    # reducing the number of operators later algorithms and form
241
    # compilers need to handle
242
    form = apply_algebra_lowering(form)
1✔
243

244
    # After lowering to index notation, remove any complex nodes that
245
    # have been introduced but are not wanted when working in real mode,
246
    # allowing for purely real forms to be written
247
    if not complex_mode:
1✔
248
        form = remove_complex_nodes(form)
1✔
249

250
    # Apply differentiation before function pullbacks, because for
251
    # example coefficient derivatives are more complicated to derive
252
    # after coefficients are rewritten, and in particular for
253
    # user-defined coefficient relations it just gets too messy
254
    form = apply_derivatives(form)
1✔
255

256
    return form
1✔
257

258

259
def compute_form_data(
1✔
260
    form,
261
    do_apply_function_pullbacks=False,
262
    do_apply_integral_scaling=False,
263
    do_apply_geometry_lowering=False,
264
    preserve_geometry_types=(),
265
    do_apply_default_restrictions=True,
266
    do_apply_restrictions=True,
267
    do_estimate_degrees=True,
268
    do_append_everywhere_integrals=True,
269
    do_replace_functions=False,
270
    coefficients_to_split=None,
271
    complex_mode=False,
272
    do_remove_component_tensors=False,
273
):
274
    """Compute form data.
275

276
    The default arguments configured to behave the way old FFC expects.
277
    """
278
    # TODO: Move this to the constructor instead
279
    self = FormData()
1✔
280

281
    # --- Store untouched form for reference.
282
    # The user of FormData may get original arguments,
283
    # original coefficients, and form signature from this object.
284
    # But be aware that the set of original coefficients are not
285
    # the same as the ones used in the final UFC form.
286
    # See 'reduced_coefficients' below.
287
    self.original_form = form
1✔
288

289
    # --- Pass form integrands through some symbolic manipulation
290

291
    form = preprocess_form(form, complex_mode)
1✔
292

293
    # --- Group form integrals
294
    # TODO: Refactor this, it's rather opaque what this does
295
    # TODO: Is self.original_form.ufl_domains() right here?
296
    #       It will matter when we start including 'num_domains' in ufc form.
297
    form = group_form_integrals(
1✔
298
        form,
299
        self.original_form.ufl_domains(),
300
        do_append_everywhere_integrals=do_append_everywhere_integrals,
301
    )
302

303
    # Estimate polynomial degree of integrands now, before applying
304
    # any pullbacks and geometric lowering.  Otherwise quad degrees
305
    # blow up horrifically.
306
    if do_estimate_degrees:
1✔
307
        form = attach_estimated_degrees(form)
1✔
308

309
    if do_apply_function_pullbacks:
1✔
310
        # Rewrite coefficients and arguments in terms of their
311
        # reference cell values with Piola transforms and symmetry
312
        # transforms injected where needed.
313
        # Decision: Not supporting grad(dolfin.Expression) without a
314
        #           Domain.  Current dolfin works if Expression has a
315
        #           cell but this should be changed to a mesh.
316
        form = apply_function_pullbacks(form)
1✔
317

318
    # Scale integrals to reference cell frames
319
    if do_apply_integral_scaling:
1✔
320
        form = apply_integral_scaling(form)
1✔
321

322
    # Lower abstractions for geometric quantities into a smaller set
323
    # of quantities, allowing the form compiler to deal with a smaller
324
    # set of types and treating geometric quantities like any other
325
    # expressions w.r.t. loop-invariant code motion etc.
326
    if do_apply_geometry_lowering:
1✔
327
        form = apply_geometry_lowering(form, preserve_geometry_types)
1✔
328

329
    # Apply differentiation again, because the algorithms above can
330
    # generate new derivatives or rewrite expressions inside
331
    # derivatives
332
    if do_apply_function_pullbacks or do_apply_geometry_lowering:
1✔
333
        form = apply_derivatives(form)
1✔
334

335
        # Neverending story: apply_derivatives introduces new Jinvs,
336
        # which needs more geometry lowering
337
        if do_apply_geometry_lowering:
1✔
338
            form = apply_geometry_lowering(form, preserve_geometry_types)
1✔
339
            # Lower derivatives that may have appeared
340
            form = apply_derivatives(form)
1✔
341

342
    form = apply_coordinate_derivatives(form)
1✔
343

344
    # If in real mode, remove any complex nodes introduced during form processing.
345
    if not complex_mode:
1✔
346
        form = remove_complex_nodes(form)
1✔
347

348
    # Remove component tensors
349
    if do_remove_component_tensors:
1✔
350
        form = remove_component_tensors(form)
1✔
351

352
    # --- Group integrals into IntegralData objects
353
    # Most of the heavy lifting is done above in group_form_integrals.
354
    self.integral_data = build_integral_data(form.integrals())
1✔
355

356
    # --- Create replacements for arguments and coefficients
357

358
    # Figure out which form coefficients each integral should enable
359
    for itg_data in self.integral_data:
1✔
360
        itg_coeffs = set()
1✔
361
        # Get all coefficients in integrand
362
        for itg in itg_data.integrals:
1✔
363
            itg_coeffs.update(extract_coefficients(itg.integrand()))
1✔
364
        # Store with IntegralData object
365
        itg_data.integral_coefficients = itg_coeffs
1✔
366

367
    # Figure out which coefficients from the original form are
368
    # actually used in any integral (Differentiation may reduce the
369
    # set of coefficients w.r.t. the original form)
370
    reduced_coefficients_set = set()
1✔
371
    for itg_data in self.integral_data:
1✔
372
        reduced_coefficients_set.update(itg_data.integral_coefficients)
1✔
373
    self.reduced_coefficients = sorted(reduced_coefficients_set, key=lambda c: c.count())
1✔
374
    self.num_coefficients = len(self.reduced_coefficients)
1✔
375
    self.original_coefficient_positions = [
1✔
376
        i for i, c in enumerate(self.original_form.coefficients()) if c in self.reduced_coefficients
377
    ]
378

379
    # Store back into integral data which form coefficients are used
380
    # by each integral
381
    for itg_data in self.integral_data:
1✔
382
        itg_data.enabled_coefficients = [
1✔
383
            bool(coeff in itg_data.integral_coefficients) for coeff in self.reduced_coefficients
384
        ]
385

386
    # --- Collect some trivial data
387

388
    # Get rank of form from argument list (assuming not a mixed arity form)
389
    self.rank = len(self.original_form.arguments())
1✔
390

391
    # Extract common geometric dimension (topological is not common!)
392
    self.geometric_dimension = self.original_form.integrals()[0].ufl_domain().geometric_dimension
1✔
393

394
    # --- Build mapping from old incomplete element objects to new
395
    # well defined elements.  This is to support the Expression
396
    # construct in dolfin which subclasses Coefficient but doesn't
397
    # provide an element, and the Constant construct that doesn't
398
    # provide the domain that a Coefficient is supposed to have. A
399
    # future design iteration in UFL/UFC/FFC/DOLFIN may allow removal
400
    # of this mapping with the introduction of UFL types for
401
    # Expression-like functions that can be evaluated in quadrature
402
    # points.
403
    self.element_replace_map = _compute_element_mapping(self.original_form)
1✔
404

405
    # Mappings from elements and coefficients that reside in form to
406
    # objects with canonical numbering as well as completed cells and
407
    # elements
408
    renumbered_coefficients, function_replace_map = _build_coefficient_replace_map(
1✔
409
        self.reduced_coefficients, self.element_replace_map
410
    )
411
    self.function_replace_map = function_replace_map
1✔
412

413
    # --- Store various lists of elements and sub elements (adds
414
    #     members to self)
415
    _compute_form_data_elements(
1✔
416
        self,
417
        self.original_form.arguments(),
418
        renumbered_coefficients,
419
        self.original_form.ufl_domains(),
420
    )
421

422
    # --- Store number of domains for integral types
423
    # TODO: Group this by domain first. For now keep a backwards
424
    # compatible data structure.
425
    self.max_subdomain_ids = _compute_max_subdomain_ids(self.integral_data)
1✔
426

427
    # --- Apply replace(integrand, self.function_replace_map)
428
    if do_replace_functions:
1✔
429
        for itg_data in self.integral_data:
1✔
430
            new_integrals = []
1✔
431
            for integral in itg_data.integrals:
1✔
432
                integrand = replace(integral.integrand(), self.function_replace_map)
1✔
433
                new_integrals.append(integral.reconstruct(integrand=integrand))
1✔
434
            itg_data.integrals = new_integrals
1✔
435

436
    # --- Split mixed coefficients with their components
437
    if coefficients_to_split is None:
1✔
438
        self.coefficient_split = {}
1✔
439
    else:
440
        if not do_replace_functions:
1✔
441
            raise ValueError("Must call with do_replace_functions=True")
×
442
        for itg_data in self.integral_data:
1✔
443
            new_integrals = []
1✔
444
            for integral in itg_data.integrals:
1✔
445
                # Propagate restrictions as required by CoefficientSplitter.
446
                # Can not yet apply default restrictions at this point
447
                # as restrictions can only be applied to the components.
448
                new_integral = apply_restrictions(integral)
1✔
449
                new_integrals.append(new_integral)
1✔
450
            itg_data.integrals = new_integrals
1✔
451
        # Split coefficients that are contained in ``coefficients_to_split``
452
        # into components, and store a dict in ``self`` that maps
453
        # each coefficient to its components.
454
        coefficient_split = {}
1✔
455
        for o in self.reduced_coefficients:
1✔
456
            if o in coefficients_to_split:
1✔
457
                c = self.function_replace_map[o]
1✔
458
                mesh = extract_unique_domain(c, expand_mesh_sequence=False)
1✔
459
                elem = c.ufl_element()
1✔
460
                coefficient_split[c] = [
1✔
461
                    Coefficient(FunctionSpace(m, e))
462
                    for m, e in zip(mesh.iterable_like(elem), elem.sub_elements)
463
                ]
464
        self.coefficient_split = coefficient_split
1✔
465
        coeff_splitter = CoefficientSplitter(self.coefficient_split)
1✔
466
        for itg_data in self.integral_data:
1✔
467
            new_integrals = []
1✔
468
            for integral in itg_data.integrals:
1✔
469
                integrand = coeff_splitter(integral.integrand())
1✔
470
                # Potentially need to call `remove_component_tensors()` here, but
471
                # early-simplifications of Indexed objects seem sufficient.
472
                if not isinstance(integrand, Zero):
1✔
473
                    new_integrals.append(integral.reconstruct(integrand=integrand))
1✔
474
            itg_data.integrals = new_integrals
1✔
475

476
    # Propagate restrictions to terminals
477
    if do_apply_restrictions:
1✔
478
        for itg_data in self.integral_data:
1✔
479
            # Need the following if block in case not all participating domains
480
            # have been included in the Measure (backwards compat).
481
            if all(
1✔
482
                not integral_type.startswith("interior_facet")
483
                for _, integral_type in itg_data.domain_integral_type_map.items()
484
            ):
485
                continue
1✔
486
            if do_apply_default_restrictions:
1✔
487
                default_restrictions = {
1✔
488
                    domain: default_restriction_map[integral_type]
489
                    for domain, integral_type in itg_data.domain_integral_type_map.items()
490
                }
491
                # Need the following dict update in case not all participating domains
492
                # have been included in the Measure (backwards compat).
493
                extra = {
1✔
494
                    domain: default_restriction_map[itg_data.integral_type]
495
                    for integral in itg_data.integrals
496
                    for domain in extract_domains(integral)
497
                    if domain not in default_restrictions
498
                }
499
                default_restrictions.update(extra)
1✔
500
            else:
501
                default_restrictions = None
×
502
            new_integrals = []
1✔
503
            for integral in itg_data.integrals:
1✔
504
                new_integral = apply_restrictions(
1✔
505
                    integral,
506
                    default_restrictions=default_restrictions,
507
                )
508
                new_integrals.append(new_integral)
1✔
509
            itg_data.integrals = new_integrals
1✔
510

511
    # --- Checks
512
    _check_elements(self)
1✔
513
    _check_facet_geometry(self.integral_data)
1✔
514

515
    # TODO: This is a very expensive check... Replace with something
516
    # faster!
517
    preprocessed_form = reconstruct_form_from_integral_data(self.integral_data)
1✔
518

519
    # TODO: Test how fast this is
520
    check_form_arity(preprocessed_form, self.original_form.arguments(), complex_mode)
1✔
521

522
    # TODO: This member is used by unit tests, change the tests to
523
    # remove this!
524
    self.preprocessed_form = preprocessed_form
1✔
525

526
    return self
1✔
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

© 2026 Coveralls, Inc