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

FEniCS / ffcx / 18834018400

23 Oct 2025 12:21PM UTC coverage: 83.0%. Remained the same
18834018400

push

github

web-flow
`AbstractCell` members are now properties (#789)

* Adapt to cell members now properties

* Change ufl branch

* One more

* Change UFL branch for dolfinx CI

* Adapt demos

* Last?

* Change ref branch

* Apply suggestions from code review

26 of 35 new or added lines in 7 files covered. (74.29%)

2 existing lines in 1 file now uncovered.

3730 of 4494 relevant lines covered (83.0%)

0.83 hits per line

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

86.24
/ffcx/ir/analysis/valuenumbering.py
1
# Copyright (C) 2011-2017 Martin Sandve Alnæs
2
#
3
# This file is part of FFCx.(https://www.fenicsproject.org)
4
#
5
# SPDX-License-Identifier:    LGPL-3.0-or-later
6
"""Algorithms for value numbering within computational graphs."""
7

8
import logging
1✔
9

10
import ufl
1✔
11
from ufl.pullback import SymmetricPullback
1✔
12

13
from ffcx.ir.analysis.indexing import (
1✔
14
    map_component_tensor_arg_components,
15
    map_indexed_arg_components,
16
)
17
from ffcx.ir.analysis.modified_terminals import analyse_modified_terminal
1✔
18

19
logger = logging.getLogger("ffcx")
1✔
20

21

22
class ValueNumberer:
1✔
23
    """Maps scalar components to unique values.
24

25
    An algorithm to map the scalar components of an expression node to unique value numbers,
26
    with fallthrough for types that can be mapped to the value numbers
27
    of their operands.
28
    """
29

30
    def __init__(self, G):
1✔
31
        """Initialise."""
32
        self.symbol_count = 0
1✔
33
        self.G = G
1✔
34
        self.V_symbols = []
1✔
35
        self.call_lookup = {
1✔
36
            ufl.classes.Expr: self.expr,
37
            ufl.classes.Argument: self.form_argument,
38
            ufl.classes.Coefficient: self.form_argument,
39
            ufl.classes.Grad: self._modified_terminal,
40
            ufl.classes.ReferenceGrad: self._modified_terminal,
41
            ufl.classes.FacetAvg: self._modified_terminal,
42
            ufl.classes.CellAvg: self._modified_terminal,
43
            ufl.classes.Restricted: self._modified_terminal,
44
            ufl.classes.ReferenceValue: self._modified_terminal,
45
            ufl.classes.Indexed: self.indexed,
46
            ufl.classes.ComponentTensor: self.component_tensor,
47
            ufl.classes.ListTensor: self.list_tensor,
48
            ufl.classes.Variable: self.variable,
49
        }
50

51
    def new_symbols(self, n):
1✔
52
        """Generate new symbols with a running counter."""
53
        begin = self.symbol_count
1✔
54
        end = begin + n
1✔
55
        self.symbol_count = end
1✔
56
        return list(range(begin, end))
1✔
57

58
    def new_symbol(self):
1✔
59
        """Generate new symbol with a running counter."""
60
        begin = self.symbol_count
1✔
61
        self.symbol_count += 1
1✔
62
        return begin
1✔
63

64
    def get_node_symbols(self, expr):
1✔
65
        """Get node symbols."""
66
        idx = [i for i, v in self.G.nodes.items() if v["expression"] == expr][0]
1✔
67
        return self.V_symbols[idx]
1✔
68

69
    def compute_symbols(self):
1✔
70
        """Compute symbols."""
71
        for i, v in self.G.nodes.items():
1✔
72
            expr = v["expression"]
1✔
73
            symbol = None
1✔
74
            # First look for exact type match
75
            f = self.call_lookup.get(type(expr), False)
1✔
76
            if f:
1✔
77
                symbol = f(expr)
1✔
78
            else:
79
                # Look for parent class types instead
80
                for k in self.call_lookup.keys():
1✔
81
                    if isinstance(expr, k):
1✔
82
                        symbol = self.call_lookup[k](expr)
1✔
83
                        break
1✔
84

85
            if symbol is None:
1✔
86
                # Nothing found
87
                raise RuntimeError(f"Not expecting type {type(expr)} here.")
×
88

89
            self.V_symbols.append(symbol)
1✔
90

91
        return self.V_symbols
1✔
92

93
    def expr(self, v):
1✔
94
        """Create new symbols for expressions that represent new values."""
95
        n = ufl.product(v.ufl_shape + v.ufl_index_dimensions)
1✔
96
        return self.new_symbols(n)
1✔
97

98
    def form_argument(self, v):
1✔
99
        """Create new symbols for expressions that represent new values."""
100
        e = v.ufl_function_space().ufl_element()
1✔
101

102
        if isinstance(e.pullback, SymmetricPullback):
1✔
103
            # Build symbols with symmetric components skipped
104
            symbols = []
×
105
            mapped_symbols = {}
×
106
            for c in ufl.permutation.compute_indices(v.ufl_shape):
×
107
                # Build mapped component mc with symmetries from element considered
108
                mc = min(i for i, j in e.pullback._symmetry.items() if j == e.pullback._symmetry[c])
×
109

110
                # Get existing symbol or create new and store with mapped component mc as key
111
                s = mapped_symbols.get(mc)
×
112
                if s is None:
×
113
                    s = self.new_symbol()
×
114
                    mapped_symbols[mc] = s
×
115
                symbols.append(s)
×
116
        else:
117
            n = ufl.product(v.ufl_shape + v.ufl_index_dimensions)
1✔
118
            symbols = self.new_symbols(n)
1✔
119

120
        return symbols
1✔
121

122
    # Handle modified terminals with element symmetries and second derivative symmetries!
123
    # terminals are implemented separately, or maybe they don't need to be?
124

125
    def _modified_terminal(self, v):
1✔
126
        """Handle modified terminal.
127

128
        Modifiers:
129
            terminal: the underlying Terminal object
130
            global_derivatives: tuple of ints, each meaning derivative
131
                in that global direction
132
            local_derivatives: tuple of ints, each meaning derivative in
133
                that local direction
134
            reference_value: bool, whether this is represented in
135
                reference frame
136
            averaged: None, 'facet' or 'cell'
137
            restriction: None, '+' or '-'
138
            component: tuple of ints, the global component of the Terminal
139
                flat_component: single int, flattened local component of the
140
            Terminal, considering symmetry
141
        """
142
        # (1) mt.terminal.ufl_shape defines a core indexing space UNLESS mt.reference_value,
143
        #     in which case the reference value shape of the element must be used.
144
        # (2) mt.terminal.ufl_element().symmetry() defines core symmetries
145
        # (3) averaging and restrictions define distinct symbols, no additional symmetries
146
        # (4) two or more grad/reference_grad defines distinct symbols with additional symmetries
147

148
        # v is not necessary scalar here, indexing in (0,...,0) picks the first scalar component
149
        # to analyse, which should be sufficient to get the base shape and derivatives
150
        if v.ufl_shape:
1✔
151
            mt = analyse_modified_terminal(v[(0,) * len(v.ufl_shape)])
1✔
152
        else:
153
            mt = analyse_modified_terminal(v)
1✔
154

155
        # Get derivatives
156
        num_ld = len(mt.local_derivatives)
1✔
157
        num_gd = len(mt.global_derivatives)
1✔
158
        assert not (num_ld and num_gd)
1✔
159
        if num_ld:
1✔
160
            domain = ufl.domain.extract_unique_domain(mt.terminal)
1✔
161
            tdim = domain.topological_dimension
1✔
162
            d_components = ufl.permutation.compute_indices((tdim,) * num_ld)
1✔
163
        elif num_gd:
1✔
164
            domain = ufl.domain.extract_unique_domiain(mt.terminal)
×
NEW
165
            gdim = domain.geometric_dimension
×
166
            d_components = ufl.permutation.compute_indices((gdim,) * num_gd)
×
167
        else:
168
            d_components = [()]
1✔
169

170
        # Get base shape without the derivative axes
171
        base_components = ufl.permutation.compute_indices(mt.base_shape)
1✔
172

173
        # Build symbols with symmetric components and derivatives skipped
174
        symbols = []
1✔
175
        mapped_symbols = {}
1✔
176
        for bc in base_components:
1✔
177
            for dc in d_components:
1✔
178
                # Build mapped component mc with symmetries from element
179
                # and derivatives combined
180
                mbc = mt.base_symmetry.get(bc, bc)
1✔
181
                mdc = tuple(sorted(dc))
1✔
182
                mc = mbc + mdc
1✔
183

184
                # Get existing symbol or create new and store with
185
                # mapped component mc as key
186
                s = mapped_symbols.get(mc)
1✔
187
                if s is None:
1✔
188
                    s = self.new_symbol()
1✔
189
                    mapped_symbols[mc] = s
1✔
190
                symbols.append(s)
1✔
191

192
        # Consistency check before returning symbols
193
        assert not v.ufl_free_indices
1✔
194
        if ufl.product(v.ufl_shape) != len(symbols):
1✔
195
            raise RuntimeError("Internal error in value numbering.")
×
196
        return symbols
1✔
197

198
    def indexed(self, Aii):
1✔
199
        """Return indexed value.
200

201
        This is implemented as a fall-through operation.
202
        """
203
        # Reuse symbols of arg A for Aii
204
        A = Aii.ufl_operands[0]
1✔
205

206
        # Get symbols of argument A
207
        A_symbols = self.get_node_symbols(A)
1✔
208

209
        # Map A_symbols to Aii_symbols
210
        d = map_indexed_arg_components(Aii)
1✔
211
        symbols = [A_symbols[k] for k in d]
1✔
212
        return symbols
1✔
213

214
    def component_tensor(self, A):
1✔
215
        """Component tensor."""
216
        # Reuse symbols of arg Aii for A
217
        Aii = A.ufl_operands[0]
1✔
218

219
        # Get symbols of argument Aii
220
        Aii_symbols = self.get_node_symbols(Aii)
1✔
221

222
        # Map A_symbols to Aii_symbols
223
        d = map_component_tensor_arg_components(A)
1✔
224
        symbols = [Aii_symbols[k] for k in d]
1✔
225
        return symbols
1✔
226

227
    def list_tensor(self, v):
1✔
228
        """List tensor."""
229
        symbols = []
1✔
230
        for row in v.ufl_operands:
1✔
231
            symbols.extend(self.get_node_symbols(row))
1✔
232
        return symbols
1✔
233

234
    def variable(self, v):
1✔
235
        """Direct reuse of all symbols."""
236
        return self.get_node_symbols(v.ufl_operands[0])
×
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