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

FEniCS / ufl / 18532473541

15 Oct 2025 02:32PM UTC coverage: 76.622% (+0.7%) from 75.964%
18532473541

push

github

web-flow
Handle restrictions on MeshSequence (#264)

* simplify Indexed(ComponentTensor(Indexed(ListTensor(...), ...), ...), ...) on construction

Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>

* handle restrictions on MeshSequence

* Suggestions (#67)

* Add some type hints and simplify _extract_and_check_domain

* Use future annotations and set ruff target

* IntegralData: make domain_integral_type_map positional

* compute_form_data: remove do_assume_single_integral_type

* Add more words

* DROP AFTER 428

* try removing skip 3.14

---------

Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Co-authored-by: Jørgen Schartum Dokken <dokken92@gmail.com>

181 of 209 new or added lines in 12 files covered. (86.6%)

1 existing line in 1 file now uncovered.

9131 of 11917 relevant lines covered (76.62%)

0.77 hits per line

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

92.89
/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 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
            it = itg_data.integral_type
1✔
154
            # Facet geometry is only valid in facet integrals.
155
            # Allowing custom integrals to pass as well, although
156
            # that's not really strict enough.
157
            if not ("facet" in it or "custom" in it or "interface" in it):
1✔
158
                # Not a facet integral
159
                for expr in traverse_unique_terminals(itg.integrand()):
1✔
160
                    cls = expr._ufl_class_
1✔
161
                    if issubclass(cls, GeometricFacetQuantity):
1✔
162
                        raise ValueError(f"Integral of type {it} cannot contain a {cls.__name__}.")
×
163

164

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

172

173
def _build_coefficient_replace_map(coefficients, element_mapping=None):
1✔
174
    """Create new Coefficient objects with count starting at 0.
175

176
    Returns:
177
        mapping from old to new objects, and lists of the new objects
178
    """
179
    if element_mapping is None:
1✔
180
        element_mapping = {}
×
181

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

198
    return new_coefficients, replace_map
1✔
199

200

201
def attach_estimated_degrees(form):
1✔
202
    """Attach estimated polynomial degree to a form's integrals.
203

204
    Args:
205
        form: The Form` to inspect.
206

207
    Returns:
208
        A new Form with estimate degrees attached.
209
    """
210
    integrals = form.integrals()
1✔
211

212
    new_integrals = []
1✔
213
    for integral in integrals:
1✔
214
        md = {}
1✔
215
        md.update(integral.metadata())
1✔
216
        degree = estimate_total_polynomial_degree(integral.integrand())
1✔
217
        md["estimated_polynomial_degree"] = degree
1✔
218
        new_integrals.append(integral.reconstruct(metadata=md))
1✔
219
    return Form(new_integrals)
1✔
220

221

222
def preprocess_form(form, complex_mode):
1✔
223
    """Preprocess a form."""
224
    # Note: Default behaviour here will process form the way that is
225
    # currently expected by vanilla FFC
226

227
    # Check that the form does not try to compare complex quantities:
228
    # if the quantites being compared are 'provably' real, wrap them
229
    # with Real, otherwise throw an error.
230
    if complex_mode:
1✔
231
        form = do_comparison_check(form)
1✔
232

233
    # Lower abstractions for tensor-algebra types into index notation,
234
    # reducing the number of operators later algorithms and form
235
    # compilers need to handle
236
    form = apply_algebra_lowering(form)
1✔
237

238
    # After lowering to index notation, remove any complex nodes that
239
    # have been introduced but are not wanted when working in real mode,
240
    # allowing for purely real forms to be written
241
    if not complex_mode:
1✔
242
        form = remove_complex_nodes(form)
1✔
243

244
    # Apply differentiation before function pullbacks, because for
245
    # example coefficient derivatives are more complicated to derive
246
    # after coefficients are rewritten, and in particular for
247
    # user-defined coefficient relations it just gets too messy
248
    form = apply_derivatives(form)
1✔
249

250
    return form
1✔
251

252

253
def compute_form_data(
1✔
254
    form,
255
    do_apply_function_pullbacks=False,
256
    do_apply_integral_scaling=False,
257
    do_apply_geometry_lowering=False,
258
    preserve_geometry_types=(),
259
    do_apply_default_restrictions=True,
260
    do_apply_restrictions=True,
261
    do_estimate_degrees=True,
262
    do_append_everywhere_integrals=True,
263
    do_replace_functions=False,
264
    coefficients_to_split=None,
265
    complex_mode=False,
266
    do_remove_component_tensors=False,
267
):
268
    """Compute form data.
269

270
    The default arguments configured to behave the way old FFC expects.
271
    """
272
    # TODO: Move this to the constructor instead
273
    self = FormData()
1✔
274

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

283
    # --- Pass form integrands through some symbolic manipulation
284

285
    form = preprocess_form(form, complex_mode)
1✔
286

287
    # --- Group form integrals
288
    # TODO: Refactor this, it's rather opaque what this does
289
    # TODO: Is self.original_form.ufl_domains() right here?
290
    #       It will matter when we start including 'num_domains' in ufc form.
291
    form = group_form_integrals(
1✔
292
        form,
293
        self.original_form.ufl_domains(),
294
        do_append_everywhere_integrals=do_append_everywhere_integrals,
295
    )
296

297
    # Estimate polynomial degree of integrands now, before applying
298
    # any pullbacks and geometric lowering.  Otherwise quad degrees
299
    # blow up horrifically.
300
    if do_estimate_degrees:
1✔
301
        form = attach_estimated_degrees(form)
1✔
302

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

312
    # Scale integrals to reference cell frames
313
    if do_apply_integral_scaling:
1✔
314
        form = apply_integral_scaling(form)
1✔
315

316
    # Lower abstractions for geometric quantities into a smaller set
317
    # of quantities, allowing the form compiler to deal with a smaller
318
    # set of types and treating geometric quantities like any other
319
    # expressions w.r.t. loop-invariant code motion etc.
320
    if do_apply_geometry_lowering:
1✔
321
        form = apply_geometry_lowering(form, preserve_geometry_types)
1✔
322

323
    # Apply differentiation again, because the algorithms above can
324
    # generate new derivatives or rewrite expressions inside
325
    # derivatives
326
    if do_apply_function_pullbacks or do_apply_geometry_lowering:
1✔
327
        form = apply_derivatives(form)
1✔
328

329
        # Neverending story: apply_derivatives introduces new Jinvs,
330
        # which needs more geometry lowering
331
        if do_apply_geometry_lowering:
1✔
332
            form = apply_geometry_lowering(form, preserve_geometry_types)
1✔
333
            # Lower derivatives that may have appeared
334
            form = apply_derivatives(form)
1✔
335

336
    form = apply_coordinate_derivatives(form)
1✔
337

338
    # If in real mode, remove any complex nodes introduced during form processing.
339
    if not complex_mode:
1✔
340
        form = remove_complex_nodes(form)
1✔
341

342
    # Remove component tensors
343
    if do_remove_component_tensors:
1✔
344
        form = remove_component_tensors(form)
1✔
345

346
    # --- Group integrals into IntegralData objects
347
    # Most of the heavy lifting is done above in group_form_integrals.
348
    self.integral_data = build_integral_data(form.integrals())
1✔
349

350
    # --- Create replacements for arguments and coefficients
351

352
    # Figure out which form coefficients each integral should enable
353
    for itg_data in self.integral_data:
1✔
354
        itg_coeffs = set()
1✔
355
        # Get all coefficients in integrand
356
        for itg in itg_data.integrals:
1✔
357
            itg_coeffs.update(extract_coefficients(itg.integrand()))
1✔
358
        # Store with IntegralData object
359
        itg_data.integral_coefficients = itg_coeffs
1✔
360

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

373
    # Store back into integral data which form coefficients are used
374
    # by each integral
375
    for itg_data in self.integral_data:
1✔
376
        itg_data.enabled_coefficients = [
1✔
377
            bool(coeff in itg_data.integral_coefficients) for coeff in self.reduced_coefficients
378
        ]
379

380
    # --- Collect some trivial data
381

382
    # Get rank of form from argument list (assuming not a mixed arity form)
383
    self.rank = len(self.original_form.arguments())
1✔
384

385
    # Extract common geometric dimension (topological is not common!)
386
    self.geometric_dimension = self.original_form.integrals()[0].ufl_domain().geometric_dimension()
1✔
387

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

399
    # Mappings from elements and coefficients that reside in form to
400
    # objects with canonical numbering as well as completed cells and
401
    # elements
402
    renumbered_coefficients, function_replace_map = _build_coefficient_replace_map(
1✔
403
        self.reduced_coefficients, self.element_replace_map
404
    )
405
    self.function_replace_map = function_replace_map
1✔
406

407
    # --- Store various lists of elements and sub elements (adds
408
    #     members to self)
409
    _compute_form_data_elements(
1✔
410
        self,
411
        self.original_form.arguments(),
412
        renumbered_coefficients,
413
        self.original_form.ufl_domains(),
414
    )
415

416
    # --- Store number of domains for integral types
417
    # TODO: Group this by domain first. For now keep a backwards
418
    # compatible data structure.
419
    self.max_subdomain_ids = _compute_max_subdomain_ids(self.integral_data)
1✔
420

421
    # --- Apply replace(integrand, self.function_replace_map)
422
    if do_replace_functions:
1✔
423
        for itg_data in self.integral_data:
1✔
424
            new_integrals = []
1✔
425
            for integral in itg_data.integrals:
1✔
426
                integrand = replace(integral.integrand(), self.function_replace_map)
1✔
427
                new_integrals.append(integral.reconstruct(integrand=integrand))
1✔
428
            itg_data.integrals = new_integrals
1✔
429

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

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

505
    # --- Checks
506
    _check_elements(self)
1✔
507
    _check_facet_geometry(self.integral_data)
1✔
508

509
    # TODO: This is a very expensive check... Replace with something
510
    # faster!
511
    preprocessed_form = reconstruct_form_from_integral_data(self.integral_data)
1✔
512

513
    # TODO: Test how fast this is
514
    check_form_arity(preprocessed_form, self.original_form.arguments(), complex_mode)
1✔
515

516
    # TODO: This member is used by unit tests, change the tests to
517
    # remove this!
518
    self.preprocessed_form = preprocessed_form
1✔
519

520
    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