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

FEniCS / ufl / 17557791029

08 Sep 2025 04:38PM UTC coverage: 76.363% (+0.4%) from 75.917%
17557791029

Pull #401

github

web-flow
Merge branch 'main' into schnellerhase/remove-type-system
Pull Request #401: Removal of custom type system

495 of 534 new or added lines in 42 files covered. (92.7%)

6 existing lines in 2 files now uncovered.

9133 of 11960 relevant lines covered (76.36%)

0.76 hits per line

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

83.82
/ufl/algorithms/domain_analysis.py
1
"""Algorithms for building canonical data structure for integrals over subdomains."""
2

3
# Copyright (C) 2009-2016 Anders Logg and Martin Sandve Alnæs
4
#
5
# This file is part of UFL (https://www.fenicsproject.org)
6
#
7
# SPDX-License-Identifier:    LGPL-3.0-or-later
8

9
import numbers
1✔
10
from collections import defaultdict
1✔
11

12
import ufl
1✔
13
from ufl.algorithms.coordinate_derivative_helpers import (
1✔
14
    attach_coordinate_derivatives,
15
    strip_coordinate_derivatives,
16
)
17
from ufl.algorithms.renumbering import renumber_indices
1✔
18
from ufl.core.compute_expr_hash import compute_expr_hash
1✔
19
from ufl.form import Form
1✔
20
from ufl.integral import Integral
1✔
21
from ufl.protocols import id_or_none
1✔
22
from ufl.sorting import cmp_expr, sorted_expr
1✔
23
from ufl.utils.sorting import canonicalize_metadata, sorted_by_key
1✔
24

25

26
class IntegralData:
1✔
27
    """Utility class.
28

29
    This class has members (domain, integral_type, subdomain_id,
30
    integrals, metadata), where metadata is an empty dictionary that may
31
    be used for associating metadata with each object.
32
    """
33

34
    __slots__ = (
1✔
35
        "domain",
36
        "enabled_coefficients",
37
        "integral_coefficients",
38
        "integral_type",
39
        "integrals",
40
        "metadata",
41
        "subdomain_id",
42
    )
43

44
    def __init__(self, domain, integral_type, subdomain_id, integrals, metadata):
1✔
45
        """Initialise."""
46
        if 1 != len(set(itg.ufl_domain() for itg in integrals)):
1✔
47
            raise ValueError("Multiple domains mismatch in integral data.")
×
48
        if not all(integral_type == itg.integral_type() for itg in integrals):
1✔
49
            raise ValueError("Integral type mismatch in integral data.")
×
50
        if not all(subdomain_id == itg.subdomain_id() for itg in integrals):
1✔
51
            raise ValueError("Subdomain id mismatch in integral data.")
×
52

53
        self.domain = domain
1✔
54
        self.integral_type = integral_type
1✔
55
        self.subdomain_id = subdomain_id
1✔
56

57
        self.integrals = integrals
1✔
58

59
        # This is populated in preprocess using data not available at
60
        # this stage:
61
        self.integral_coefficients = None
1✔
62
        self.enabled_coefficients = None
1✔
63

64
        # TODO: I think we can get rid of this with some refactoring
65
        # in ffc:
66
        self.metadata = metadata
1✔
67

68
    def __lt__(self, other):
1✔
69
        """Check if self is less than other."""
70
        # To preserve behaviour of extract_integral_data:
71
        return (self.integral_type, self.subdomain_id, self.integrals, self.metadata) < (
×
72
            other.integral_type,
73
            other.subdomain_id,
74
            other.integrals,
75
            other.metadata,
76
        )
77

78
    def __eq__(self, other):
1✔
79
        """Check for equality."""
80
        # Currently only used for tests:
81
        return (
×
82
            self.integral_type == other.integral_type
83
            and self.subdomain_id == other.subdomain_id
84
            and self.integrals == other.integrals
85
            and self.metadata == other.metadata
86
        )
87

88
    def __hash__(self):
1✔
89
        """Return hash."""
NEW
90
        return compute_expr_hash(self)
×
91

92
    def __str__(self):
1✔
93
        """Format as a string."""
94
        s = f"IntegralData over domain({self.integral_type}, {self.subdomain_id})"
×
95
        s += " with integrals:\n"
×
96
        s += "\n\n".join(map(str, self.integrals))
×
97
        s += "\nand metadata:\n{metadata}"
×
98
        return s
×
99

100

101
class ExprTupleKey:
1✔
102
    """Tuple comparison helper."""
103

104
    __slots__ = ("x",)
1✔
105

106
    def __init__(self, x):
1✔
107
        """Initialise."""
108
        self.x = x
1✔
109

110
    def __lt__(self, other):
1✔
111
        """Check if self is less than other."""
112
        # Comparing expression first
113
        c = cmp_expr(self.x[0], other.x[0])
×
114
        if c < 0:
×
115
            return True
×
116
        elif c > 0:
×
117
            return False
×
118
        else:
119
            # Comparing form compiler data
120
            mds = canonicalize_metadata(self.x[1])
×
121
            mdo = canonicalize_metadata(other.x[1])
×
122
            return mds < mdo
×
123

124

125
def group_integrals_by_domain_and_type(integrals, domains):
1✔
126
    """Group integrals by domain and type.
127

128
    Args:
129
        integrals: list of Integral objects
130
        domains: list of AbstractDomain objects from the parent Form
131

132
    Returns:
133
        Dictionary mapping (domain, integral_type) to list(Integral)
134
    """
135
    integrals_by_domain_and_type = defaultdict(list)
1✔
136
    for itg in integrals:
1✔
137
        if itg.ufl_domain() is None:
1✔
138
            raise ValueError("Integral has no domain.")
×
139
        key = (itg.ufl_domain(), itg.integral_type())
1✔
140

141
        # Append integral to list of integrals with shared key
142
        integrals_by_domain_and_type[key].append(itg)
1✔
143

144
    return integrals_by_domain_and_type
1✔
145

146

147
def integral_subdomain_ids(integral):
1✔
148
    """Get a tuple of integer subdomains or a valid string subdomain from integral."""
149
    did = integral.subdomain_id()
1✔
150
    if isinstance(did, numbers.Integral):
1✔
151
        return (did,)
1✔
152
    elif isinstance(did, tuple):
1✔
153
        if not all(isinstance(d, numbers.Integral) for d in did):
×
154
            raise ValueError("Expecting only integer subdomains in tuple.")
×
155
        return did
×
156
    elif did in ("everywhere", "otherwise"):
1✔
157
        # TODO: Define list of valid strings somewhere more central
158
        return did
1✔
159
    else:
160
        raise ValueError(f"Invalid domain id {did}.")
×
161

162

163
def rearrange_integrals_by_single_subdomains(
1✔
164
    integrals: list[Integral], do_append_everywhere_integrals: bool
165
) -> dict[int, list[Integral]]:
166
    """Rearrange integrals over multiple subdomains to single subdomain integrals.
167

168
    Args:
169
        integrals: List of integrals
170
        do_append_everywhere_integrals: Boolean indicating if integrals
171
        defined on the whole domain should
172
            just be restricted to the set of input subdomain ids.
173

174
    Returns:
175
        The integrals reconstructed with single subdomain_id
176
    """
177
    # Split integrals into lists of everywhere and subdomain integrals
178
    everywhere_integrals = []
1✔
179
    subdomain_integrals = []
1✔
180
    for itg in integrals:
1✔
181
        dids = integral_subdomain_ids(itg)
1✔
182
        if dids == "otherwise":
1✔
183
            raise ValueError("'otherwise' integrals should never occur before preprocessing.")
×
184
        elif dids == "everywhere":
1✔
185
            everywhere_integrals.append(itg)
1✔
186
        else:
187
            subdomain_integrals.append((dids, itg))
1✔
188

189
    # Fill single_subdomain_integrals with lists of integrals from
190
    # subdomain_integrals, but split and restricted to single
191
    # subdomain ids
192
    single_subdomain_integrals = defaultdict(list)
1✔
193
    for dids, itg in subdomain_integrals:
1✔
194
        # Region or single subdomain id
195
        for did in dids:
1✔
196
            # Restrict integral to this subdomain!
197
            single_subdomain_integrals[did].append(itg.reconstruct(subdomain_id=did))
1✔
198

199
    # Add everywhere integrals to each single subdomain id integral
200
    # list
201
    otherwise_integrals = []
1✔
202
    for ev_itg in everywhere_integrals:
1✔
203
        # Restrict everywhere integral to 'otherwise'
204
        otherwise_integrals.append(ev_itg.reconstruct(subdomain_id="otherwise"))
1✔
205

206
        # Restrict everywhere integral to each subdomain
207
        # and append to each integral list
208
        if do_append_everywhere_integrals:
1✔
209
            for subdomain_id in sorted(single_subdomain_integrals.keys()):
1✔
210
                single_subdomain_integrals[subdomain_id].append(
×
211
                    ev_itg.reconstruct(subdomain_id=subdomain_id)
212
                )
213

214
    if otherwise_integrals:
1✔
215
        single_subdomain_integrals["otherwise"] = otherwise_integrals
1✔
216

217
    return single_subdomain_integrals
1✔
218

219

220
def accumulate_integrands_with_same_metadata(integrals):
1✔
221
    """Accumulate integrands with the same metedata.
222

223
    Args:
224
        integrals: a list of integrals
225

226
    Returns:
227
        A list of the form [(integrand0, metadata0), (integrand1,
228
        metadata1), ...] where integrand0 < integrand1 by the canonical
229
        ufl expression ordering criteria.
230
    """
231
    # Group integrals by compiler data hash
232
    by_cdid = {}
1✔
233
    for itg in integrals:
1✔
234
        cd = itg.metadata()
1✔
235
        cdid = hash(canonicalize_metadata(cd))
1✔
236
        if cdid not in by_cdid:
1✔
237
            by_cdid[cdid] = ([], cd)
1✔
238
        by_cdid[cdid][0].append(itg)
1✔
239

240
    # Accumulate integrands separately for each compiler data object
241
    # id
242
    for cdid in by_cdid:
1✔
243
        integrals, cd = by_cdid[cdid]
1✔
244
        # Ensure canonical sorting of more than two integrands
245
        integrands = sorted_expr(itg.integrand() for itg in integrals)
1✔
246
        integrands_sum = sum(integrands[1:], integrands[0])
1✔
247
        by_cdid[cdid] = (integrands_sum, cd)
1✔
248

249
    # Sort integrands canonically by integrand first then compiler
250
    # data
251
    return sorted(by_cdid.values(), key=ExprTupleKey)
1✔
252

253

254
def build_integral_data(integrals):
1✔
255
    """Build integral data given a list of integrals.
256

257
    The integrals you pass in here must have been rearranged and
258
    gathered (removing the "everywhere" subdomain_id). To do this, you
259
    should call group_form_integrals.
260

261
    Args:
262
        integrals: An iterable of Integral objects.
263

264
    Returns:
265
        A tuple of IntegralData objects.
266
    """
267
    itgs = defaultdict(list)
1✔
268

269
    # --- Merge integral data that has the same integrals,
270
    for integral in integrals:
1✔
271
        integral_type = integral.integral_type()
1✔
272
        ufl_domain = integral.ufl_domain()
1✔
273
        subdomain_ids = integral.subdomain_id()
1✔
274
        if "everywhere" in subdomain_ids:
1✔
275
            raise ValueError(
×
276
                "'everywhere' not a valid subdomain id. "
277
                "Did you forget to call group_form_integrals?"
278
            )
279

280
        # Group for integral data (One integral data object for all
281
        # integrals with same domain, itype, (but possibly different metadata).
282
        itgs[(ufl_domain, integral_type, subdomain_ids)].append(integral)
1✔
283

284
    # Build list with canonical ordering, iteration over dicts
285
    # is not deterministic across python versions
286
    def keyfunc(item):
1✔
287
        (d, itype, sid), integrals = item
1✔
288
        sid_int = tuple(-1 if i == "otherwise" else i for i in sid)
1✔
289
        return (d._ufl_sort_key_(), itype, (type(sid).__name__,), sid_int)
1✔
290

291
    integral_datas = []
1✔
292
    for (d, itype, sid), integrals in sorted(itgs.items(), key=keyfunc):
1✔
293
        integral_datas.append(IntegralData(d, itype, sid, integrals, {}))
1✔
294
    return integral_datas
1✔
295

296

297
def group_form_integrals(form, domains, do_append_everywhere_integrals=True):
1✔
298
    """Group integrals by domain and type, performing canonical simplification.
299

300
    Args:
301
        form: the Form to group the integrals of.
302
        domains: an iterable of Domains.
303
        do_append_everywhere_integrals: Boolean indicating if integrals
304
        defined on the whole domain should
305
            just be restricted to the set of input subdomain ids.
306

307
    Returns:
308
        A new Form with gathered integrands.
309
    """
310
    # Group integrals by domain and type
311
    integrals_by_domain_and_type = group_integrals_by_domain_and_type(form.integrals(), domains)
1✔
312

313
    integrals = []
1✔
314
    for domain in domains:
1✔
315
        for integral_type in ufl.measure.integral_types():
1✔
316
            # Get integrals with this domain and type
317
            ddt_integrals = integrals_by_domain_and_type.get((domain, integral_type))
1✔
318
            if ddt_integrals is None:
1✔
319
                continue
1✔
320

321
            # Group integrals by subdomain id, after splitting e.g.
322
            #   f*dx((1,2)) + g*dx((2,3)) -> f*dx(1) + (f+g)*dx(2) + g*dx(3)
323
            # (note: before this call, 'everywhere' is a valid subdomain_id,
324
            # and after this call, 'otherwise' is a valid subdomain_id)
325
            single_subdomain_integrals = rearrange_integrals_by_single_subdomains(
1✔
326
                ddt_integrals, do_append_everywhere_integrals
327
            )
328

329
            for subdomain_id, ss_integrals in sorted_by_key(single_subdomain_integrals):
1✔
330
                # strip the coordinate derivatives from all integrals
331
                # this yields a list of the form [(coordinate derivative, integral), ...]
332
                stripped_integrals_and_coordderivs = strip_coordinate_derivatives(ss_integrals)
1✔
333

334
                # now group the integrals by the coordinate derivative
335
                def calc_hash(cd):
1✔
336
                    return sum(
1✔
337
                        sum(tuple_elem._ufl_compute_hash_() for tuple_elem in tuple_)
338
                        for tuple_ in cd
339
                    )
340

341
                coordderiv_integrals_dict = {}
1✔
342
                for integral, coordderiv in stripped_integrals_and_coordderivs:
1✔
343
                    coordderivhash = calc_hash(coordderiv)
1✔
344
                    if coordderivhash in coordderiv_integrals_dict:
1✔
345
                        coordderiv_integrals_dict[coordderivhash][1].append(integral)
×
346
                    else:
347
                        coordderiv_integrals_dict[coordderivhash] = (coordderiv, [integral])
1✔
348

349
                # cd_integrals_dict is now a dict of the form
350
                # { hash: (CoordinateDerivative, [integral, integral, ...]), ... }
351
                # we can now put the integrals back together and then afterwards
352
                # apply the CoordinateDerivative again
353

354
                for cdhash, samecd_integrals in sorted_by_key(coordderiv_integrals_dict):
1✔
355
                    # Accumulate integrands of integrals that share the
356
                    # same compiler data
357
                    integrands_and_cds = accumulate_integrands_with_same_metadata(
1✔
358
                        samecd_integrals[1]
359
                    )
360

361
                    for integrand, metadata in integrands_and_cds:
1✔
362
                        integral = Integral(
1✔
363
                            integrand, integral_type, domain, subdomain_id, metadata, None
364
                        )
365
                        integral = attach_coordinate_derivatives(integral, samecd_integrals[0])
1✔
366
                        integrals.append(integral)
1✔
367

368
    # Group integrals by common integrand
369
    # u.dx(0)*dx(1) + u.dx(0)*dx(2) -> u.dx(0)*dx((1,2))
370
    # to avoid duplicate kernels generated after geometry lowering
371
    unique_integrals = defaultdict(tuple)
1✔
372
    metadata_table = defaultdict(dict)
1✔
373
    for integral in integrals:
1✔
374
        integral_type = integral.integral_type()
1✔
375
        ufl_domain = integral.ufl_domain()
1✔
376
        metadata = integral.metadata()
1✔
377
        meta_hash = hash(canonicalize_metadata(metadata))
1✔
378
        subdomain_id = integral.subdomain_id()
1✔
379
        subdomain_data = id_or_none(integral.subdomain_data())
1✔
380
        integrand = renumber_indices(integral.integrand())
1✔
381
        unique_integrals[(integral_type, ufl_domain, meta_hash, integrand, subdomain_data)] += (
1✔
382
            subdomain_id,
383
        )
384
        metadata_table[(integral_type, ufl_domain, meta_hash, integrand, subdomain_data)] = metadata
1✔
385

386
    grouped_integrals = []
1✔
387
    for integral_data, subdomain_ids in unique_integrals.items():
1✔
388
        (integral_type, ufl_domain, metadata, integrand, subdomain_data) = integral_data
1✔
389
        integral = Integral(
1✔
390
            integrand,
391
            integral_type,
392
            ufl_domain,
393
            subdomain_ids,
394
            metadata_table[integral_data],
395
            subdomain_data,
396
        )
397
        grouped_integrals.append(integral)
1✔
398

399
    return Form(grouped_integrals)
1✔
400

401

402
def reconstruct_form_from_integral_data(integral_data):
1✔
403
    """Reconstruct a form from integral data."""
404
    integrals = []
1✔
405
    for ida in integral_data:
1✔
406
        integrals.extend(ida.integrals)
1✔
407
    return Form(integrals)
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

© 2025 Coveralls, Inc