From 7d7c676a82b62a3217c1d12ceb088fc3349322a7 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 17 Jan 2025 19:18:06 +0000 Subject: [PATCH 1/6] Indexed: avoid contraction of repeated indices (#338) * Indexed: avoid contraction of repeated indices * Untangle ComponentTensor early * Indexed: _simplify_indexed * ruff * comments * Remove untangling of ComponentTensor from apply_derivatives * add tests * Suggestions from review * add another test * Update test/test_simplify.py --- test/test_simplify.py | 77 ++++++++++++++++++++++++++++- ufl/algorithms/apply_derivatives.py | 38 +------------- ufl/algorithms/expand_indices.py | 2 +- ufl/core/expr.py | 4 ++ ufl/corealg/map_dag.py | 2 +- ufl/index_combination_utils.py | 8 ++- ufl/indexed.py | 15 ++---- ufl/tensors.py | 65 +++++++++++++++++------- 8 files changed, 139 insertions(+), 72 deletions(-) diff --git a/test/test_simplify.py b/test/test_simplify.py index dcd3e06ba..80599dca6 100755 --- a/test/test_simplify.py +++ b/test/test_simplify.py @@ -32,11 +32,13 @@ triangle, ) from ufl.algorithms import compute_form_data -from ufl.core.multiindex import FixedIndex, MultiIndex +from ufl.constantvalue import Zero +from ufl.core.multiindex import FixedIndex, Index, MultiIndex, indices from ufl.finiteelement import FiniteElement from ufl.indexed import Indexed from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 +from ufl.tensors import ComponentTensor, ListTensor def xtest_zero_times_argument(self): @@ -193,3 +195,76 @@ def test_nested_indexed(self): multiindex = MultiIndex((FixedIndex(0),)) assert Indexed(expr, multiindex) is expr[0] assert Indexed(expr, multiindex) is comps[1] + + +def test_repeated_indexing(self): + # Test that an Indexed with repeated indices does not contract indices + shape = (2, 2) + element = FiniteElement("Lagrange", triangle, 1, shape, identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) + x = Coefficient(space) + C = as_tensor([x, x]) + + fi = FixedIndex(0) + i = Index() + ii = MultiIndex((fi, i, i)) + expr = Indexed(C, ii) + assert i.count() in expr.ufl_free_indices + assert isinstance(expr, Indexed) + B, jj = expr.ufl_operands + assert B is x + assert tuple(jj) == tuple(ii[1:]) + + +def test_untangle_indexed_component_tensor(self): + shape = (2, 2, 2, 2) + element = FiniteElement("Lagrange", triangle, 1, shape, identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) + C = Coefficient(space) + + r = len(shape) + kk = indices(r) + + # Untangle as_tensor(C[kk], kk) -> C + B = as_tensor(Indexed(C, MultiIndex(kk)), kk) + assert B is C + + # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] + jj = kk[2:] + A = as_tensor(Indexed(C, MultiIndex(kk)), jj) + assert A is not C + + ii = kk + expr = Indexed(A, MultiIndex(ii)) + assert isinstance(expr, Indexed) + B, ll = expr.ufl_operands + assert B is C + + rep = dict(zip(jj, ii)) + expected = tuple(rep.get(k, k) for k in kk) + assert tuple(ll) == expected + + +def test_simplify_indexed(self): + element = FiniteElement("Lagrange", triangle, 1, (3,), identity_pullback, H1) + domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2,), identity_pullback, H1)) + space = FunctionSpace(domain, element) + u = Coefficient(space) + z = Zero(()) + i = Index() + j = Index() + # ListTensor + lt = ListTensor(z, z, u[1]) + assert Indexed(lt, MultiIndex((FixedIndex(2),))) == u[1] + # ListTensor -- nested + l0 = ListTensor(z, u[1], z) + l1 = ListTensor(z, z, u[2]) + l2 = ListTensor(u[0], z, z) + ll = ListTensor(l0, l1, l2) + assert Indexed(ll, MultiIndex((FixedIndex(1), FixedIndex(2)))) == u[2] + assert Indexed(ll, MultiIndex((FixedIndex(2), i))) == l2[i] + # ComponentTensor + ListTensor + c = ComponentTensor(Indexed(ll, MultiIndex((i, j))), MultiIndex((j, i))) + assert Indexed(c, MultiIndex((FixedIndex(1), FixedIndex(2)))) == l2[1] diff --git a/ufl/algorithms/apply_derivatives.py b/ufl/algorithms/apply_derivatives.py index da7b61da1..848c405f6 100644 --- a/ufl/algorithms/apply_derivatives.py +++ b/ufl/algorithms/apply_derivatives.py @@ -18,7 +18,6 @@ from ufl.checks import is_cellwise_constant from ufl.classes import ( Coefficient, - ComponentTensor, Conj, ConstantValue, ExprList, @@ -219,28 +218,12 @@ def variable(self, o, df, unused_l): # --- Indexing and component handling - def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in nesting rules + def indexed(self, o, Ap, ii): """Differentiate an indexed.""" # Propagate zeros if isinstance(Ap, Zero): return self.independent_operator(o) - # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] to simplify - # resulting expression - if isinstance(Ap, ComponentTensor): - B, jj = Ap.ufl_operands - if isinstance(B, Indexed): - C, kk = B.ufl_operands - kk = list(kk) - if all(j in kk for j in jj): - rep = dict(zip(jj, ii)) - Cind = [rep.get(k, k) for k in kk] - expr = Indexed(C, MultiIndex(tuple(Cind))) - assert expr.ufl_free_indices == o.ufl_free_indices - assert expr.ufl_shape == o.ufl_shape - return expr - - # Otherwise a more generic approach r = len(Ap.ufl_shape) - len(ii) if r: kk = indices(r) @@ -1450,29 +1433,12 @@ def base_form_coordinate_derivative(self, o, f, dummy_w, dummy_v, dummy_cd): o_[3], ) - def indexed(self, o, Ap, ii): # TODO: (Partially) duplicated in generic rules + def indexed(self, o, Ap, ii): """Apply to an indexed.""" # Reuse if untouched if Ap is o.ufl_operands[0]: return o - # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] to simplify - # resulting expression - if isinstance(Ap, ComponentTensor): - B, jj = Ap.ufl_operands - if isinstance(B, Indexed): - C, kk = B.ufl_operands - - kk = list(kk) - if all(j in kk for j in jj): - rep = dict(zip(jj, ii)) - Cind = [rep.get(k, k) for k in kk] - expr = Indexed(C, MultiIndex(tuple(Cind))) - assert expr.ufl_free_indices == o.ufl_free_indices - assert expr.ufl_shape == o.ufl_shape - return expr - - # Otherwise a more generic approach r = len(Ap.ufl_shape) - len(ii) if r: kk = indices(r) diff --git a/ufl/algorithms/expand_indices.py b/ufl/algorithms/expand_indices.py index 316998341..de994bd12 100644 --- a/ufl/algorithms/expand_indices.py +++ b/ufl/algorithms/expand_indices.py @@ -137,7 +137,7 @@ def index_sum(self, x): # TODO: For the list tensor purging algorithm, do something like: # if index not in self._to_expand: - # return self.expr(x, *[self.visit(o) for o in x.ufl_operands]) + # return self.expr(x, *map(self.visit, x.ufl_operands)) for value in range(x.dimension()): self._index2value.push(index, value) diff --git a/ufl/core/expr.py b/ufl/core/expr.py index 41b6e55a6..93149e2f6 100644 --- a/ufl/core/expr.py +++ b/ufl/core/expr.py @@ -342,6 +342,10 @@ def _ufl_err_str_(self): """Return a short string to represent this Expr in an error message.""" return f"<{self._ufl_class_.__name__} id={id(self)}>" + def _simplify_indexed(self, multiindex): + """Return a simplified Expr used in the constructor of Indexed(self, multiindex).""" + raise NotImplementedError(self.__class__._simplify_indexed) + # --- Special functions used for processing expressions --- def __eq__(self, other): diff --git a/ufl/corealg/map_dag.py b/ufl/corealg/map_dag.py index 9b9196f17..7cd6d11ee 100644 --- a/ufl/corealg/map_dag.py +++ b/ufl/corealg/map_dag.py @@ -100,7 +100,7 @@ def traversal(expression): if cutoff_types[v._ufl_typecode_]: r = handlers[v._ufl_typecode_](v) else: - r = handlers[v._ufl_typecode_](v, *[vcache[u] for u in v.ufl_operands]) + r = handlers[v._ufl_typecode_](v, *(vcache[u] for u in v.ufl_operands)) # Optionally check if r is in rcache, a memory optimization # to be able to keep representation of result compact diff --git a/ufl/index_combination_utils.py b/ufl/index_combination_utils.py index 8bd5087a8..50d4c5e17 100644 --- a/ufl/index_combination_utils.py +++ b/ufl/index_combination_utils.py @@ -215,11 +215,9 @@ def merge_overlapping_indices(afi, afid, bfi, bfid): # Find repeated indices, brute force version for i0 in range(an): - for i1 in range(bn): - if afi[i0] == bfi[i1]: - repeated_indices.append(afi[i0]) - repeated_index_dimensions.append(afid[i0]) - break + if afi[i0] in bfi: + repeated_indices.append(afi[i0]) + repeated_index_dimensions.append(afid[i0]) # Collect only non-repeated indices, brute force version for i, d in sorted(zip(afi + bfi, afid + bfid)): diff --git a/ufl/indexed.py b/ufl/indexed.py index 338033413..815f9d150 100644 --- a/ufl/indexed.py +++ b/ufl/indexed.py @@ -46,9 +46,10 @@ def __new__(cls, expression, multiindex): return Zero(shape=(), free_indices=fi, index_dimensions=fid) try: - # Simplify indexed ListTensor - return expression[multiindex] - except ValueError: + # Simplify if possible + return expression._simplify_indexed(multiindex) + except NotImplementedError: + # Construct a new instance to be initialised self = Operator.__new__(cls) self._initialised = False return self @@ -124,11 +125,3 @@ def __getitem__(self, key): f"Attempting to index with {ufl_err_str(key)}, " f"but object is already indexed: {ufl_err_str(self)}" ) - - def _ufl_expr_reconstruct_(self, expression, multiindex): - """Reconstruct.""" - try: - # Simplify indexed ListTensor - return expression[multiindex] - except ValueError: - return Operator._ufl_expr_reconstruct_(self, expression, multiindex) diff --git a/ufl/tensors.py b/ufl/tensors.py index b54b5eecf..4c00dd8e9 100644 --- a/ufl/tensors.py +++ b/ufl/tensors.py @@ -22,7 +22,7 @@ class ListTensor(Operator): """Wraps a list of expressions into a tensor valued expression of one higher rank.""" - __slots__ = () + __slots__ = ("_initialised",) def __new__(cls, *expressions): """Create a new ListTensor.""" @@ -88,10 +88,15 @@ def sub(e, *indices): if all(i[0] == k for k, i in enumerate(indices)): return sub(e0, 0, 0) - return Operator.__new__(cls) + # Construct a new instance to be initialised + self = Operator.__new__(cls) + self._initialised = False + return self def __init__(self, *expressions): """Initialise.""" + if self._initialised: + return Operator.__init__(self, expressions) # Checks @@ -100,6 +105,7 @@ def __init__(self, *expressions): raise ValueError( "Can't combine subtensor expressions with different sets of free indices." ) + self._initialised = True @property def ufl_shape(self): @@ -120,6 +126,14 @@ def evaluate(self, x, mapping, component, index_values, derivatives=()): else: return a.evaluate(x, mapping, component, index_values) + def _simplify_indexed(self, multiindex): + """Return a simplified Expr used in the constructor of Indexed(self, multiindex).""" + k = multiindex[0] + if isinstance(k, FixedIndex): + sub = self.ufl_operands[int(k)] + return Indexed(sub, MultiIndex(multiindex[1:])) + return Operator._simplify_indexed(self, multiindex) + def __getitem__(self, key): """Get an item.""" origkey = key @@ -128,6 +142,8 @@ def __getitem__(self, key): key = key.indices() if not isinstance(key, tuple): key = (key,) + if len(key) == 0: + return self k = key[0] if isinstance(k, (int, FixedIndex)): sub = self.ufl_operands[int(k)] @@ -160,11 +176,11 @@ def substring(expressions, indent): class ComponentTensor(Operator): """Maps the free indices of a scalar valued expression to tensor axes.""" - __slots__ = ("ufl_free_indices", "ufl_index_dimensions", "ufl_shape") + __slots__ = ("_initialised", "ufl_free_indices", "ufl_index_dimensions", "ufl_shape") def __new__(cls, expression, indices): """Create a new ComponentTensor.""" - # Simplify + # Zero-simplify if isinstance(expression, Zero): fi, fid, sh = remove_indices( expression.ufl_free_indices, @@ -173,11 +189,21 @@ def __new__(cls, expression, indices): ) return Zero(sh, fi, fid) - # Construct - return Operator.__new__(cls) + # Special case for simplification as_tensor(A[ii], ii) -> A + if isinstance(expression, Indexed): + A, ii = expression.ufl_operands + if indices == ii: + return A + + # Construct a new instance to be initialised + self = Operator.__new__(cls) + self._initialised = False + return self def __init__(self, expression, indices): """Initialise.""" + if self._initialised: + return if not isinstance(expression, Expr): raise ValueError("Expecting ufl expression.") if expression.ufl_shape != (): @@ -197,15 +223,21 @@ def __init__(self, expression, indices): self.ufl_free_indices = fi self.ufl_index_dimensions = fid self.ufl_shape = sh - - def _ufl_expr_reconstruct_(self, expressions, indices): - """Reconstruct.""" - # Special case for simplification as_tensor(A[ii], ii) -> A - if isinstance(expressions, Indexed): - A, ii = expressions.ufl_operands - if indices == ii: - return A - return Operator._ufl_expr_reconstruct_(self, expressions, indices) + self._initialised = True + + def _simplify_indexed(self, multiindex): + """Return a simplified Expr used in the constructor of Indexed(self, multiindex).""" + # Untangle as_tensor(C[kk], jj)[ii] -> C[ll] + B, jj = self.ufl_operands + if isinstance(B, Indexed): + C, kk = B.ufl_operands + if all(j in kk for j in jj): + ii = tuple(multiindex) + rep = dict(zip(jj, ii)) + Cind = tuple(rep.get(k, k) for k in kk) + return Indexed(C, MultiIndex(Cind)) + + return Operator._simplify_indexed(self, multiindex) def indices(self): """Get indices.""" @@ -213,8 +245,7 @@ def indices(self): def evaluate(self, x, mapping, component, index_values): """Evaluate.""" - indices = self.ufl_operands[1] - a = self.ufl_operands[0] + a, indices = self.ufl_operands if len(indices) != len(component): raise ValueError("Expecting a component matching the indices tuple.") From a4e94086d993460080db80c84865d8db5be211e1 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 21 Jan 2025 10:01:34 +0000 Subject: [PATCH 2/6] Simplify conditional (#340) * Simplify conditional * Fix bug in ArityMismatch message * Comments --- test/test_check_arities.py | 40 +++++++++++++++++++++++++++++++++ ufl/algorithms/check_arities.py | 9 ++++---- ufl/conditional.py | 17 ++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/test/test_check_arities.py b/test/test_check_arities.py index e2c32f5f5..5508c02b1 100755 --- a/test/test_check_arities.py +++ b/test/test_check_arities.py @@ -9,7 +9,9 @@ TestFunction, TrialFunction, adjoint, + as_tensor, cofac, + conditional, conj, derivative, ds, @@ -84,3 +86,41 @@ def test_product_arity(): with pytest.raises(ArityMismatch): L = inner(v, v) * dx compute_form_data(L, complex_mode=False) + + +def test_zero_simplify_arity(): + """ + Test that adding verious zero-like expressions to a form is simplified, + such that one can compute form data for the integral. + """ + cell = tetrahedron + D = Mesh(FiniteElement("Lagrange", cell, 1, (3,), identity_pullback, H1)) + V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)) + v = TestFunction(V) + u = Coefficient(V) + + nonzero = 1 + with pytest.raises(ArityMismatch): + F = inner(u, v + nonzero) * dx + compute_form_data(F) + z = Coefficient(V) + + # Add a Zero-component (rank-0) of a tensor to a rank-1 tensor + zero = as_tensor([0, z])[0] + F = inner(u, v + zero) * dx + fd = compute_form_data(F) + assert fd.num_coefficients == 1 + + # Add a conditional that should have been simplified to zero (rank-0) + # to a rank-1 tensor + zero = conditional(z < 0, 0, 0) + F = inner(u, v + zero) * dx + fd = compute_form_data(F) + assert fd.num_coefficients == 1 + + # Check that nested zero conditionals are simplifed to zero (rank-0) + # and can be added to a rank-1 tensor + zero = conditional(z < 0, 0, conditional(z == 0, 0, 0)) + F = inner(u, v + zero) * dx + fd = compute_form_data(F) + assert fd.num_coefficients == 1 diff --git a/ufl/algorithms/check_arities.py b/ufl/algorithms/check_arities.py index e1d9b366b..d38f53165 100644 --- a/ufl/algorithms/check_arities.py +++ b/ufl/algorithms/check_arities.py @@ -57,7 +57,8 @@ def sum(self, o, a, b): """Apply to sum.""" if a != b: raise ArityMismatch( - f"Adding expressions with non-matching form arguments {_afmt(a)} vs {_afmt(b)}." + f"Adding expressions with non-matching form arguments " + f"{tuple(map(_afmt, a))} vs {tuple(map(_afmt, b))}." ) return a @@ -86,7 +87,7 @@ def product(self, o, a, b): if len(c) != len(a) + len(b) or len(c) != len({x[0] for x in c}): raise ArityMismatch( "Multiplying expressions with overlapping form arguments " - f"{_afmt(a)} vs {_afmt(b)}." + f"{tuple(map(_afmt, a))} vs {tuple(map(_afmt, b))}." ) # It's fine for argument parts to overlap return c @@ -138,7 +139,7 @@ def variable(self, o, f, a): def conditional(self, o, c, a, b): """Apply to conditional.""" if c: - raise ArityMismatch(f"Condition cannot depend on form arguments ({_afmt(a)}).") + raise ArityMismatch("Condition cannot depend on form arguments.") if a and isinstance(o.ufl_operands[2], Zero): # Allow conditional(c, arg, 0) return a @@ -153,7 +154,7 @@ def conditional(self, o, c, a, b): # conditional(c, test, nonzeroconstant) raise ArityMismatch( "Conditional subexpressions with non-matching form arguments " - f"{_afmt(a)} vs {_afmt(b)}." + f"{tuple(map(_afmt, a))} vs {tuple(map(_afmt, b))}." ) def linear_indexed_type(self, o, a, i): diff --git a/ufl/conditional.py b/ufl/conditional.py index 117808c2b..5f802943a 100644 --- a/ufl/conditional.py +++ b/ufl/conditional.py @@ -264,10 +264,23 @@ class Conditional(Operator): In C++ these take the format `(condition ? true_value : false_value)`. """ - __slots__ = () + __slots__ = ("_initialised",) + + def __new__(cls, condition, true_value, false_value): + """Create a new Conditional.""" + # Simplify + if bool(true_value == false_value): + return true_value + # Construct a new instance to be initialised + self = Operator.__new__(cls) + self._initialised = False + return self def __init__(self, condition, true_value, false_value): """Initialise.""" + if self._initialised: + return + # Checks if not isinstance(condition, Condition): raise ValueError("Expecting condition as first argument.") true_value = as_ufl(true_value) @@ -290,8 +303,8 @@ def __init__(self, condition, true_value, false_value): ) ): raise ValueError("Non-scalar == or != is not allowed.") - Operator.__init__(self, (condition, true_value, false_value)) + self._initialised = True def evaluate(self, x, mapping, component, index_values): """Evaluate.""" From 1ab30c2de2a975b9fc957c6b356541bd15b35e15 Mon Sep 17 00:00:00 2001 From: KarsKnook <57411502+KarsKnook@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:27:13 +0000 Subject: [PATCH 3/6] Bug fix for complex division of numpy.complex (#342) * Bug fix for complex division of numpy.complex * Do not treat int division as a special case * Add test of knook fix * Add documentation --------- Co-authored-by: Kars Knook Co-authored-by: Pablo Brubeck Co-authored-by: jorgensd --- test/test_evaluate.py | 35 ++++++++++++++++++++++++++++++++++- ufl/algebra.py | 7 +------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/test/test_evaluate.py b/test/test_evaluate.py index da01d452b..c808b30ce 100755 --- a/test/test_evaluate.py +++ b/test/test_evaluate.py @@ -3,6 +3,8 @@ import math +import numpy as np + from ufl import ( Argument, Coefficient, @@ -33,12 +35,28 @@ tr, triangle, ) -from ufl.constantvalue import as_ufl +from ufl.constantvalue import ConstantValue, as_ufl from ufl.finiteelement import FiniteElement from ufl.pullback import identity_pullback from ufl.sobolevspace import H1 +class CustomConstant(ConstantValue): + def __init__(self, value): + super().__init__() + self._value = value + + @property + def ufl_shape(self): + return () + + def evaluate(self, x, mapping, component, index_values): + return self._value + + def __repr__(self): + return f"CustomConstant({self._value})" + + def testScalars(): s = as_ufl(123) e = s((5, 7)) @@ -132,6 +150,21 @@ def testAlgebra(): assert e == v +def testConstant(): + """Test that constant division doesn't discard the complex type in the case the value is + a numpy complex type, not a native python complex type. + """ + _a = np.complex128(1 + 1j) + _b = np.complex128(-3 + 2j) + a = CustomConstant(_a) + b = CustomConstant(_b) + expr = a / b + e = expr(()) + + expected = complex(_a) / complex(_b) + assert e == expected + + def testIndexSum(): cell = triangle domain = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1)) diff --git a/ufl/algebra.py b/ufl/algebra.py index dea6fd021..19d50c1d9 100644 --- a/ufl/algebra.py +++ b/ufl/algebra.py @@ -253,12 +253,7 @@ def evaluate(self, x, mapping, component, index_values): a, b = self.ufl_operands a = a.evaluate(x, mapping, component, index_values) b = b.evaluate(x, mapping, component, index_values) - # Avoiding integer division by casting to float - try: - e = float(a) / float(b) - except TypeError: - e = complex(a) / complex(b) - return e + return a / b def __str__(self): """Format as a string.""" From cecd52fd1359817e59ebd453adf2961251672bb6 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Sat, 15 Feb 2025 13:38:03 +0000 Subject: [PATCH 4/6] Fix #343 (#344) * make CI run ffcx demos * remove line causing #343 * explicityl run Symetry * Update ufl/tensors.py * Fix symmetry bug (#345) * Fix symmetry bug * ruff --------- Co-authored-by: Pablo Brubeck --- .github/workflows/fenicsx-tests.yml | 4 ++++ ufl/tensors.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fenicsx-tests.yml b/.github/workflows/fenicsx-tests.yml index fcaf5da5d..3fb3e5acc 100644 --- a/.github/workflows/fenicsx-tests.yml +++ b/.github/workflows/fenicsx-tests.yml @@ -53,6 +53,10 @@ jobs: pip install .[ci] - name: Run FFCx unit tests run: python3 -m pytest -n auto ffcx/test + - name: Run FFCx demos + run: | + python3 -m pytest -n auto ffcx/demo/test_demos.py + python3 -m ffcx ffcx/demo/Symmetry.py dolfinx-tests: name: Run DOLFINx tests diff --git a/ufl/tensors.py b/ufl/tensors.py index 4c00dd8e9..71d76540d 100644 --- a/ufl/tensors.py +++ b/ufl/tensors.py @@ -74,7 +74,6 @@ def sub(e, *indices): return sub(e0, 0) if j == () else sub(e0, 0)[(*j, slice(None))] except ValueError: pass - # Simplify [v[0,:], v[1,:], ..., v[k,:]] -> v if ( all( @@ -85,7 +84,10 @@ def sub(e, *indices): and all(sub(e, 0, 0) == sub(e0, 0, 0) for e in expressions[1:]) ): indices = [sub(e, 0, 1).indices() for e in expressions] - if all(i[0] == k for k, i in enumerate(indices)): + if all( + i[0] == k and all(isinstance(subindex, Index) for subindex in i[1:]) + for k, i in enumerate(indices) + ): return sub(e0, 0, 0) # Construct a new instance to be initialised From 31e5be79daa8bdd0eac73a2ec2de80702dc5c9c7 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Sat, 15 Feb 2025 16:44:36 +0000 Subject: [PATCH 5/6] no need to explitly run Symmetry - have remove xfail from ffc (#347) --- .github/workflows/fenicsx-tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/fenicsx-tests.yml b/.github/workflows/fenicsx-tests.yml index 3fb3e5acc..47dbf597e 100644 --- a/.github/workflows/fenicsx-tests.yml +++ b/.github/workflows/fenicsx-tests.yml @@ -54,9 +54,7 @@ jobs: - name: Run FFCx unit tests run: python3 -m pytest -n auto ffcx/test - name: Run FFCx demos - run: | - python3 -m pytest -n auto ffcx/demo/test_demos.py - python3 -m ffcx ffcx/demo/Symmetry.py + run: python3 -m pytest -n auto ffcx/demo/test_demos.py dolfinx-tests: name: Run DOLFINx tests From e901012250f07691b3b69ede865e5c1cc6269002 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 21 Feb 2025 15:22:20 +0000 Subject: [PATCH 6/6] Fix BaseFormOperator.ufl_function_space (#348) * Fix BaseFormOperator.ufl_function_space * Add a test that would have failed before --- test/test_interpolate.py | 15 +++++++++++++++ ufl/core/base_form_operator.py | 10 +++++----- ufl/core/interpolate.py | 17 ++++++----------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/test/test_interpolate.py b/test/test_interpolate.py index 877f55a35..1aa8b2d17 100644 --- a/test/test_interpolate.py +++ b/test/test_interpolate.py @@ -10,6 +10,7 @@ Adjoint, Argument, Coefficient, + Cofunction, FunctionSpace, Mesh, TestFunction, @@ -69,6 +70,20 @@ def test_symbolic(V1, V2): assert Iu.ufl_operands == (u,) +def test_symbolic_adjoint(V1, V2): + # Set dual of V2 + V2_dual = V2.dual() + + u = Argument(V1, 1) + vstar = Cofunction(V2_dual) + Iu = Interpolate(u, vstar) + + assert Iu.ufl_function_space() == V2_dual + assert Iu.argument_slots() == (vstar, u) + assert Iu.arguments() == (u,) + assert Iu.ufl_operands == (u,) + + def test_action_adjoint(V1, V2): # Set dual of V2 V2_dual = V2.dual() diff --git a/ufl/core/base_form_operator.py b/ufl/core/base_form_operator.py index aae943f75..9cb28cc1d 100644 --- a/ufl/core/base_form_operator.py +++ b/ufl/core/base_form_operator.py @@ -16,6 +16,7 @@ from collections import OrderedDict from ufl.argument import Argument, Coargument +from ufl.coefficient import BaseCoefficient from ufl.constantvalue import as_ufl from ufl.core.operator import Operator from ufl.core.ufl_type import ufl_type @@ -134,20 +135,19 @@ def count(self): def ufl_shape(self): """Return the UFL shape of the coefficient.produced by the operator.""" arg, *_ = self.argument_slots() - if isinstance(arg, BaseForm): + if not isinstance(arg, BaseCoefficient) and isinstance(arg, (BaseForm, Coargument)): arg, *_ = arg.arguments() return arg._ufl_shape def ufl_function_space(self): """Return the function space associated to the operator. - I.e. return the dual of the base form operator's Coargument. + I.e. return the dual of the base form operator's Coargument space. """ arg, *_ = self.argument_slots() - if isinstance(arg, BaseForm): + if not isinstance(arg, BaseCoefficient) and isinstance(arg, (BaseForm, Coargument)): arg, *_ = arg.arguments() - return arg._ufl_function_space - return arg._ufl_function_space.dual() + return arg.ufl_function_space() def _ufl_expr_reconstruct_( self, *operands, function_space=None, derivatives=None, argument_slots=None diff --git a/ufl/core/interpolate.py b/ufl/core/interpolate.py index 3b5e3ba4d..79a75cc2a 100644 --- a/ufl/core/interpolate.py +++ b/ufl/core/interpolate.py @@ -8,14 +8,12 @@ # # Modified by Nacime Bouziani, 2021-2022 -from ufl.action import Action from ufl.argument import Argument, Coargument -from ufl.coefficient import Cofunction from ufl.constantvalue import as_ufl from ufl.core.base_form_operator import BaseFormOperator from ufl.core.ufl_type import ufl_type from ufl.duals import is_dual -from ufl.form import BaseForm, Form +from ufl.form import BaseForm from ufl.functionspace import AbstractFunctionSpace @@ -35,8 +33,7 @@ def __init__(self, expr, v): v: the FunctionSpace to interpolate into or the Coargument defined on the dual of the FunctionSpace to interpolate into. """ - # This check could be more rigorous. - dual_args = (Coargument, Cofunction, Form, Action, BaseFormOperator) + dual_args = (Coargument, BaseForm) if isinstance(v, AbstractFunctionSpace): if is_dual(v): @@ -44,7 +41,7 @@ def __init__(self, expr, v): v = Argument(v.dual(), 0) elif not isinstance(v, dual_args): raise ValueError( - "Expecting the second argument to be FunctionSpace, FiniteElement or dual." + "Expecting the second argument to be FunctionSpace, Coargument, or BaseForm." ) expr = as_ufl(expr) @@ -54,11 +51,9 @@ def __init__(self, expr, v): # Reversed order convention argument_slots = (v, expr) # Get the primal space (V** = V) - if isinstance(v, BaseForm): - arg, *_ = v.arguments() - function_space = arg.ufl_function_space() - else: - function_space = v.ufl_function_space().dual() + arg, *_ = v.arguments() + function_space = arg.ufl_function_space() + # Set the operand as `expr` for DAG traversal purpose. operand = expr BaseFormOperator.__init__(