From 5fcffdcd6a913c73df555a151656bce33ef11445 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Thu, 16 Jul 2026 15:36:31 +0100 Subject: [PATCH 1/4] add test This exposed a problem in applying BCs to an action --- .../test_restricted_function_space.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/firedrake/regression/test_restricted_function_space.py b/tests/firedrake/regression/test_restricted_function_space.py index 727c4e5101..eb5ebeb797 100644 --- a/tests/firedrake/regression/test_restricted_function_space.py +++ b/tests/firedrake/regression/test_restricted_function_space.py @@ -209,6 +209,35 @@ def test_poisson_inhomogeneous_bcs_high_level_interface(assembled_rhs): assert errornorm(SpatialCoordinate(mesh)[0]**2, u) < 1.e-12 +def test_restrict_action(): + mesh = UnitSquareMesh(4, 4) + V = FunctionSpace(mesh, "CG", 1) + Q = FunctionSpace(mesh, "DG", 0) + + u, v = TrialFunction(V), TestFunction(V) + q, r = TrialFunction(Q), TestFunction(Q) + + M = inner(q, v) * dx # Q x V -> R + I = interpolate(u, Q) # V x Q^* -> R + A = action(M, I) # V x V^* -> R + + solution = Function(V) + bc = DirichletBC(V, 0, "on_boundary") + L = inner(1, v) * dx + + problem = LinearVariationalProblem(A, L, solution, bcs=bc, restrict=True) + + V_res = problem.restricted_space + assert problem.F.arguments()[0].function_space() == V_res + assert all(arg.function_space() == V_res for arg in problem.J.arguments()) + + matrix = assemble(A).petscmat[:, :] + matrix = np.delete(matrix, bc.nodes, axis=0) + matrix = np.delete(matrix, bc.nodes, axis=1) + restricted_matrix = assemble(problem.J).petscmat[:, :] + assert np.allclose(matrix, restricted_matrix) + + @pytest.mark.parametrize("j", [1, 2, 5]) def test_restricted_function_space_coord_change(j): mesh = UnitSquareMesh(1, 2) From a4e5454555ec8af53080533d6aab5c67f9ecf5a2 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Thu, 16 Jul 2026 15:38:07 +0100 Subject: [PATCH 2/4] replace contracted argument --- firedrake/variational_solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firedrake/variational_solver.py b/firedrake/variational_solver.py index 4031bf3c5c..a694d8e1ba 100644 --- a/firedrake/variational_solver.py +++ b/firedrake/variational_solver.py @@ -97,13 +97,13 @@ def __init__(self, F, u, bcs=None, J=None, bcs = [bc.reconstruct(V=V_res, indices=bc._indices) for bc in bcs] self.u_restrict = Function(V_res) v_res, u_res = TestFunction(V_res), TrialFunction(V_res) + v_arg, u_arg = self.J.arguments() if isinstance(F, Form): F_arg, = F.arguments() self.F = replace(F, {F_arg: v_res, self.u: self.u_restrict}) else: - self.F = interpolate(v_res, replace(F, {self.u: self.u_restrict})) + self.F = interpolate(v_res, replace(F, {u_arg: u_res, self.u: self.u_restrict})) - v_arg, u_arg = self.J.arguments() self.J = replace(self.J, {v_arg: v_res, u_arg: u_res, self.u: self.u_restrict}) if self.Jp: v_arg, u_arg = self.Jp.arguments() From 79da694297932c7f65b05e808097fc0d212fba87 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Thu, 16 Jul 2026 15:38:35 +0100 Subject: [PATCH 3/4] lint --- tests/firedrake/regression/test_restricted_function_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/firedrake/regression/test_restricted_function_space.py b/tests/firedrake/regression/test_restricted_function_space.py index eb5ebeb797..2f69137d12 100644 --- a/tests/firedrake/regression/test_restricted_function_space.py +++ b/tests/firedrake/regression/test_restricted_function_space.py @@ -215,7 +215,7 @@ def test_restrict_action(): Q = FunctionSpace(mesh, "DG", 0) u, v = TrialFunction(V), TestFunction(V) - q, r = TrialFunction(Q), TestFunction(Q) + q, _ = TrialFunction(Q), TestFunction(Q) M = inner(q, v) * dx # Q x V -> R I = interpolate(u, Q) # V x Q^* -> R From a38d2d620045d832bc34cf90705cc9eda812ffdd Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Fri, 17 Jul 2026 14:06:36 +0100 Subject: [PATCH 4/4] fixes + parametrise test --- firedrake/variational_solver.py | 26 +++++++++++++++---- .../test_restricted_function_space.py | 6 ++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/firedrake/variational_solver.py b/firedrake/variational_solver.py index a694d8e1ba..21eb1d1185 100644 --- a/firedrake/variational_solver.py +++ b/firedrake/variational_solver.py @@ -96,18 +96,34 @@ def __init__(self, F, u, bcs=None, J=None, V_res = restricted_function_space(V, extract_subdomain_ids(bcs)) bcs = [bc.reconstruct(V=V_res, indices=bc._indices) for bc in bcs] self.u_restrict = Function(V_res) + v_res, u_res = TestFunction(V_res), TrialFunction(V_res) - v_arg, u_arg = self.J.arguments() + + P = interpolate(u_res, V) + Pstar = ufl_expr.adjoint(P) + u_full = interpolate(self.u_restrict, V) + if isinstance(F, Form): F_arg, = F.arguments() self.F = replace(F, {F_arg: v_res, self.u: self.u_restrict}) else: - self.F = interpolate(v_res, replace(F, {u_arg: u_res, self.u: self.u_restrict})) + F_full = replace(F, {self.u: u_full}) + self.F = ufl_expr.action(Pstar, F_full) + + if isinstance(self.J, Form): + v_arg, u_arg = self.J.arguments() + self.J = replace(self.J, {v_arg: v_res, u_arg: u_res, self.u: self.u_restrict}) + else: + J_full = replace(self.J, {self.u: u_full}) + self.J = ufl_expr.action(Pstar, ufl_expr.action(J_full, P)) - self.J = replace(self.J, {v_arg: v_res, u_arg: u_res, self.u: self.u_restrict}) if self.Jp: - v_arg, u_arg = self.Jp.arguments() - self.Jp = replace(self.Jp, {v_arg: v_res, u_arg: u_res, self.u: self.u_restrict}) + if isinstance(self.Jp, Form): + v_arg, u_arg = self.Jp.arguments() + self.Jp = replace(self.Jp, {v_arg: v_res, u_arg: u_res, self.u: self.u_restrict}) + else: + Jp_full = replace(self.Jp, {self.u: u_full}) + self.Jp = ufl_expr.action(Pstar, ufl_expr.action(Jp_full, P)) self.restricted_space = V_res else: self.u_restrict = u diff --git a/tests/firedrake/regression/test_restricted_function_space.py b/tests/firedrake/regression/test_restricted_function_space.py index 2f69137d12..5c4ccd43d4 100644 --- a/tests/firedrake/regression/test_restricted_function_space.py +++ b/tests/firedrake/regression/test_restricted_function_space.py @@ -209,7 +209,8 @@ def test_poisson_inhomogeneous_bcs_high_level_interface(assembled_rhs): assert errornorm(SpatialCoordinate(mesh)[0]**2, u) < 1.e-12 -def test_restrict_action(): +@pytest.mark.parametrize("preassembled", [False, True]) +def test_restrict_action(preassembled): mesh = UnitSquareMesh(4, 4) V = FunctionSpace(mesh, "CG", 1) Q = FunctionSpace(mesh, "DG", 0) @@ -219,6 +220,9 @@ def test_restrict_action(): M = inner(q, v) * dx # Q x V -> R I = interpolate(u, Q) # V x Q^* -> R + if preassembled: + M = assemble(M) + I = assemble(I) A = action(M, I) # V x V^* -> R solution = Function(V)