From 89ccca4f259b2418d0acc762086ee3906e2949cd Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 3 Mar 2026 23:16:02 +0100 Subject: [PATCH 01/26] Fix typo in docstring of derivative_block. --- python/dolfinx/fem/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 242c88ded1..4361f0c7d2 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -691,7 +691,7 @@ def derivative_block( This is commonly used to derive a block Jacobian from a block residual. - If ``F_i`` is a list of forms, the Jacobian is a list of lists with + If ``F`` is a list of rank one forms, the Jacobian is a list of lists with :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using ``ufl.derivative`` called component-wise. From 48a3714a5a810a1a433eb484b525856d5b8f801e Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 3 Mar 2026 23:40:29 +0100 Subject: [PATCH 02/26] Add logic in derivative_block to take functionals and return residuals. --- python/dolfinx/fem/forms.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 4361f0c7d2..8ab9dae693 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -699,6 +699,23 @@ def derivative_block( \\frac{\\partial F}{\\partial u}[\\delta u]`. This is identical to calling ``ufl.derivative`` directly. """ # noqa: D301 + + if isinstance(F, ufl.Form) and not F.arguments(): # if F is a functional + if isinstance(u, Function): + if du is None: + du = ufl.TestFunction(u.function_space) + elif not isinstance(du, ufl.Argument): + raise ValueError("When F is a functional of a single function, du must be a ufl.Argument") + elif all([isinstance(u_i, Function) for u_i in u]): + if du is None: + du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) + elif len(u) != len(du) or not all([isinstance(du_i, ufl.Argument) for du_i in du]): + raise ValueError("When F is a functional of N functions, du must be a sequence containing N ufl.Argument") + else: + raise ValueError("u must be a function or sequence of functions") + return ufl.extract_blocks(ufl.derivative(F, u, du)) + + if isinstance(F, ufl.Form): if not isinstance(u, Function): raise ValueError("Must provide a single function when F is a UFL form") From d0e4358cd6072f2e931c72088ccd0273ff561477 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 3 Mar 2026 23:42:16 +0100 Subject: [PATCH 03/26] Update docstring of derivative_block to cover new features on functionals --- python/dolfinx/fem/forms.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 8ab9dae693..a96705813b 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -686,18 +686,27 @@ def derivative_block( u: Function | Sequence[Function], du: ufl.Argument | Sequence[ufl.Argument] | None = None, ) -> ufl.Form | Sequence[Sequence[ufl.Form]]: - """Return the UFL derivative of a (list of) UFL rank one form(s). + """Return the UFL derivative of a UFL rank zero form, or the UFL derivative + of a (list of) rank one form(s). - This is commonly used to derive a block Jacobian from a block - residual. + This is commonly used to derive a block residual from a functional, or to + derive a block Jacobian from a block residual. - If ``F`` is a list of rank one forms, the Jacobian is a list of lists with - :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using - ``ufl.derivative`` called component-wise. + If ``F`` is a univariate rank zero form, the residual is computed as + :math:`R = \\frac{\\partial F}{\\partial u}[\\delta u]`. + + If ``F`` is a multivariate rank zero form, the residual is a list with + :math:`R_i = \\frac{\\partial F}{\\partial u_i}[\\delta u_i]`, where + :math:`\\delta u_i` is a test subfunction of the mixed space defined by u. + + If ``F`` is a rank one form, the Jacobian is computed as :math:`J = + \\frac{\\partial F}{\\partial u}[\\delta u]`. - If ``F`` is a form, the Jacobian is computed as :math:`J = - \\frac{\\partial F}{\\partial u}[\\delta u]`. This is identical to - calling ``ufl.derivative`` directly. + All three operations above are identical to calling ``ufl.derivative`` directly. + + If ``F`` is a list of rank one forms, the Jacobian is a list of lists with + :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using ``ufl.derivative`` + called component-wise. """ # noqa: D301 if isinstance(F, ufl.Form) and not F.arguments(): # if F is a functional From 6440dfa1d0d02d123930364cb23d22d33766f506 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Fri, 6 Mar 2026 19:05:40 +0100 Subject: [PATCH 04/26] Only allow test functions in 'du' when deriving functionals. --- python/dolfinx/fem/forms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index a96705813b..51db2ff2be 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -713,13 +713,13 @@ def derivative_block( if isinstance(u, Function): if du is None: du = ufl.TestFunction(u.function_space) - elif not isinstance(du, ufl.Argument): - raise ValueError("When F is a functional of a single function, du must be a ufl.Argument") + elif not (isinstance(du, ufl.Argument) and du.number() == 0): + raise ValueError("When F is a functional of a single function, du must be a test function") elif all([isinstance(u_i, Function) for u_i in u]): if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) - elif len(u) != len(du) or not all([isinstance(du_i, ufl.Argument) for du_i in du]): - raise ValueError("When F is a functional of N functions, du must be a sequence containing N ufl.Argument") + elif len(u) != len(du) or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du]): + raise ValueError("When F is a functional of N functions, du must be a sequence containing N test functions") else: raise ValueError("u must be a function or sequence of functions") return ufl.extract_blocks(ufl.derivative(F, u, du)) From 5e41beaf0ace06d199a13cccdfd0628889e979cf Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sat, 7 Mar 2026 01:19:12 +0100 Subject: [PATCH 05/26] Format Python code properly in docstring. --- python/dolfinx/fem/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 51db2ff2be..2a883ac0d9 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -702,10 +702,10 @@ def derivative_block( If ``F`` is a rank one form, the Jacobian is computed as :math:`J = \\frac{\\partial F}{\\partial u}[\\delta u]`. - All three operations above are identical to calling ``ufl.derivative`` directly. + All three operations above are identical to calling {py:func}`ufl.derivative` directly. If ``F`` is a list of rank one forms, the Jacobian is a list of lists with - :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using ``ufl.derivative`` + :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using {py:func}`ufl.derivative` called component-wise. """ # noqa: D301 From f77a9854a403eeebf9b5c60ad92a464bd18053f0 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 8 Mar 2026 20:44:57 +0100 Subject: [PATCH 06/26] Update the type annotation of derivative_block's output for the new functional case. --- python/dolfinx/fem/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 2a883ac0d9..1d622329ec 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -685,7 +685,7 @@ def derivative_block( F: ufl.Form | Sequence[ufl.Form], u: Function | Sequence[Function], du: ufl.Argument | Sequence[ufl.Argument] | None = None, -) -> ufl.Form | Sequence[Sequence[ufl.Form]]: +) -> ufl.Form | Sequence[ufl.Form] | Sequence[Sequence[ufl.Form]]: """Return the UFL derivative of a UFL rank zero form, or the UFL derivative of a (list of) rank one form(s). From 9cdafc5cf26cba817c2da9c20c864d318124b18a Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sat, 7 Mar 2026 02:29:26 +0100 Subject: [PATCH 07/26] Add unit tests covering new and previous features of derivative_block. --- python/test/unit/fem/test_forms.py | 65 +++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index d08c07f295..fc54f04e41 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -15,9 +15,11 @@ import basix.ufl import dolfinx from dolfinx.fem import IntegralType, extract_function_spaces, form, functionspace -from dolfinx.fem.forms import form_cpp_class +from dolfinx.fem.forms import form_cpp_class, derivative_block from dolfinx.mesh import create_unit_square -from ufl import Measure, SpatialCoordinate, TestFunction, TrialFunction, dx, inner +from ufl import Measure, SpatialCoordinate, TestFunction, TrialFunction, dx, inner, Form as ufl_form, TrialFunctions, TestFunctions, MixedFunctionSpace + +from collections.abc import Sequence def test_extract_forms(): @@ -132,3 +134,62 @@ def test_multiple_measures_one_subdomain_data(): J_local = dolfinx.fem.assemble_scalar(J) J_global = comm.allreduce(J_local, op=MPI.SUM) assert np.isclose(J_global, 1 / 3 + 1 / 2) + + +def test_derivative_block(): + """Test the function derivative_block""" + mesh = dolfinx.mesh.create_unit_interval(MPI.COMM_WORLD, 10) + V0 = functionspace(mesh, ("Lagrange", 1)) + V1 = functionspace(mesh, ("Lagrange", 2)) + V = MixedFunctionSpace(V0, V1) + + f0, f1 = dolfinx.fem.Function(V0), dolfinx.fem.Function(V1) + v0, v1 = TestFunctions(V) + u0, u1 = TrialFunctions(V) + + + F = f0**2 * dx # univariate functional + + with pytest.raises(ValueError): + derivative_block(F, f0, u0) # third argument not a test function + + R = derivative_block(F, f0) + assert isinstance(R, ufl_form) and len(R.arguments()) == 1 + + R = derivative_block(F, f0, v0) + assert isinstance(R, Sequence) and len(R) == 1 + assert isinstance(R[0], ufl_form) and len(R[0].arguments()) == 1 + + v0_no_mixed_space = TestFunction(V0) + R = derivative_block(F, f0, v0_no_mixed_space) + assert isinstance(R, ufl_form) and len(R.arguments()) == 1 + + J = derivative_block(R, f0) + assert isinstance(J, ufl_form) and len(J.arguments()) == 2 + + J = derivative_block(R, f0, u0) + assert isinstance(J, ufl_form) and len(J.arguments()) == 2 + + + F = f0**2 * f1 * dx # multivariate functional + + with pytest.raises(ValueError): + derivative_block(F, [f0, f1], [v0]) # third argument has wrong length + + with pytest.raises(ValueError): + derivative_block(F, [f0, f1], [u0, v1]) # third argument contains a non test function + + R = derivative_block(F, [f0, f1]) + assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) + + R = derivative_block(F, [f0, f1], [v0, v1]) + assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) + + with pytest.raises(AssertionError): + derivative_block(R, [f0, f1], [u0]) # third argument has wrong length + + J = derivative_block(R, [f0, f1]) + assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) + + J = derivative_block(R, [f0, f1], [u0, u1]) + assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) From 2e62aaa4b3263cf4e0c2b1f7e7f48a1dfe33db25 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sat, 7 Mar 2026 23:48:52 +0100 Subject: [PATCH 08/26] Replace asserts for ValueErrors, to be consistent with the rest of derivative_block. --- python/dolfinx/fem/forms.py | 7 ++++--- python/test/unit/fem/test_forms.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 1d622329ec..cd9dbbabdb 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -732,10 +732,11 @@ def derivative_block( du = ufl.TrialFunction(u.function_space) return ufl.derivative(F, u, du) else: - assert all([isinstance(Fi, ufl.Form) for Fi in F]), "F must be a sequence of UFL forms" - assert len(F) == len(u), "Number of forms and functions must be equal" + if not all([isinstance(Fi, ufl.Form) for Fi in F]): + raise ValueError("F must be a sequence of UFL forms") if du is not None: - assert len(F) == len(du), "Number of forms and du must be equal" + if len(F) != len(du) + raise ValueError("Number of forms and du must be equal") else: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index fc54f04e41..b14c5db72b 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -185,7 +185,7 @@ def test_derivative_block(): R = derivative_block(F, [f0, f1], [v0, v1]) assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) - with pytest.raises(AssertionError): + with pytest.raises(ValueError): derivative_block(R, [f0, f1], [u0]) # third argument has wrong length J = derivative_block(R, [f0, f1]) From 59b64f75577e53efef9caa33ba8642b28a03ea24 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 8 Mar 2026 19:26:39 +0100 Subject: [PATCH 09/26] Only allow trial functions in 'du' when deriving residuals. --- python/dolfinx/fem/forms.py | 6 ++++-- python/test/unit/fem/test_forms.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index cd9dbbabdb..674ca4e4c2 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -730,13 +730,15 @@ def derivative_block( raise ValueError("Must provide a single function when F is a UFL form") if du is None: du = ufl.TrialFunction(u.function_space) + elif not (isinstance(du, ufl.Argument) and du.number() == 1): + raise ValueError("When F is a rank-one UFL form, du must be a trial function") return ufl.derivative(F, u, du) else: if not all([isinstance(Fi, ufl.Form) for Fi in F]): raise ValueError("F must be a sequence of UFL forms") if du is not None: - if len(F) != len(du) - raise ValueError("Number of forms and du must be equal") + if len(F) != len(du) or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du]): + raise ValueError("Number of forms and du must be equal, and du must only contain trial functions") else: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index b14c5db72b..721250d4a6 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -164,6 +164,9 @@ def test_derivative_block(): R = derivative_block(F, f0, v0_no_mixed_space) assert isinstance(R, ufl_form) and len(R.arguments()) == 1 + with pytest.raises(ValueError): + J = derivative_block(R, f0, f1) # third argument wrongly isn't a trial function + J = derivative_block(R, f0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 @@ -188,6 +191,9 @@ def test_derivative_block(): with pytest.raises(ValueError): derivative_block(R, [f0, f1], [u0]) # third argument has wrong length + with pytest.raises(ValueError): + J = derivative_block(R, [f0, f1], [u0, f1]) # third argument contains a non-trial function + J = derivative_block(R, [f0, f1]) assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) From 4fbb8f600cd51f4b37f73774fa0c5fb06726446f Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 8 Mar 2026 19:52:06 +0100 Subject: [PATCH 10/26] Bulletproof derivative_block against more combinations of bad user input. --- python/dolfinx/fem/forms.py | 54 +++++++++++++++++++++--------- python/test/unit/fem/test_forms.py | 19 +++++++++-- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 674ca4e4c2..d638808e7a 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -713,32 +713,54 @@ def derivative_block( if isinstance(u, Function): if du is None: du = ufl.TestFunction(u.function_space) - elif not (isinstance(du, ufl.Argument) and du.number() == 0): - raise ValueError("When F is a functional of a single function, du must be a test function") - elif all([isinstance(u_i, Function) for u_i in u]): + elif not isinstance(du, ufl.Argument) or du.number() != 0: + raise ValueError( + "When F is a functional of a single function, du must be a test function" + ) + elif isinstance(u, Sequence): + if not all([isinstance(u_i, Function) for u_i in u]): + raise ValueError("u contains some non ufl.Function elements") if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) - elif len(u) != len(du) or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du]): - raise ValueError("When F is a functional of N functions, du must be a sequence containing N test functions") + elif (not isinstance(du, Sequence) + or not len(u) == len(du) + or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du])): + raise ValueError( + "When F is a functional of N functions, du must be a sequence " + "containing N test functions" + ) else: raise ValueError("u must be a function or sequence of functions") return ufl.extract_blocks(ufl.derivative(F, u, du)) - - if isinstance(F, ufl.Form): + elif isinstance(F, ufl.Form) and len(F.arguments()) == 1: if not isinstance(u, Function): - raise ValueError("Must provide a single function when F is a UFL form") + raise ValueError("When F is a rank-one UFL form, u must be a ufl.Function") if du is None: du = ufl.TrialFunction(u.function_space) - elif not (isinstance(du, ufl.Argument) and du.number() == 1): + elif not isinstance(du, ufl.Argument) or du.number() != 1: raise ValueError("When F is a rank-one UFL form, du must be a trial function") return ufl.derivative(F, u, du) - else: - if not all([isinstance(Fi, ufl.Form) for Fi in F]): - raise ValueError("F must be a sequence of UFL forms") - if du is not None: - if len(F) != len(du) or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du]): - raise ValueError("Number of forms and du must be equal, and du must only contain trial functions") - else: + elif isinstance(F, Sequence): + if not all([isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F]): + raise ValueError("F contains some non rank-one ufl.Form elements") + if not isinstance(u, Sequence): + raise ValueError("When F is a sequence, u must be a sequence") + if not all([isinstance(u_i, Function) for u_i in u]): + raise ValueError("u contains some non ufl.Function elements") + if du is None: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] + elif (not isinstance(du, Sequence) + or not len(u) == len(du) + or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du])): + raise ValueError( + "When F is a list of N rank-one forms, du must be a sequence " + "containing N trial functions" + ) return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] + + else: + raise ValueError( + "F must be either a UFL form (with rank zero or one), or a sequence of " + "rank-one UFL forms." + ) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 721250d4a6..01af990f97 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -150,6 +150,9 @@ def test_derivative_block(): F = f0**2 * dx # univariate functional + with pytest.raises(ValueError): + derivative_block(F, u0, v0) # second argument not a ufl.Function + with pytest.raises(ValueError): derivative_block(F, f0, u0) # third argument not a test function @@ -165,7 +168,10 @@ def test_derivative_block(): assert isinstance(R, ufl_form) and len(R.arguments()) == 1 with pytest.raises(ValueError): - J = derivative_block(R, f0, f1) # third argument wrongly isn't a trial function + derivative_block(R, u0, u0) # second argument not a ufl.Function + + with pytest.raises(ValueError): + derivative_block(R, f0, v0) # third argument not a trial function J = derivative_block(R, f0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 @@ -182,6 +188,12 @@ def test_derivative_block(): with pytest.raises(ValueError): derivative_block(F, [f0, f1], [u0, v1]) # third argument contains a non test function + with pytest.raises(ValueError): + derivative_block(F, f0, [u0, u1]) # second argument not a sequence + + with pytest.raises(ValueError): + derivative_block(F, [f0, f1], u0) # third argument not a sequence + R = derivative_block(F, [f0, f1]) assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) @@ -192,7 +204,10 @@ def test_derivative_block(): derivative_block(R, [f0, f1], [u0]) # third argument has wrong length with pytest.raises(ValueError): - J = derivative_block(R, [f0, f1], [u0, f1]) # third argument contains a non-trial function + derivative_block(R, [f0, u1], [u0, u1]) # second argument contains a non ufl.Function + + with pytest.raises(ValueError): + derivative_block(R, [f0, f1], u0) # third argument not a sequence J = derivative_block(R, [f0, f1]) assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) From d1ff4f742fb8f0b0d48218129d8687784b543185 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Mon, 9 Mar 2026 02:10:18 +0100 Subject: [PATCH 11/26] Split the logic of derivative_block into smaller functions for readability. --- python/dolfinx/fem/forms.py | 121 ++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index d638808e7a..b8cae6d231 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -681,6 +681,76 @@ def create_form( return Form(f, form.ufcx_form, form.code) +def _derive_univariate_residual( + F: ufl.Form, + u: Function, + du: ufl.Argument | None = None, +) -> ufl.Form: + if du is None: + du = ufl.TestFunction(u.function_space) + elif not isinstance(du, ufl.Argument) or du.number() != 0: + raise ValueError( + "When F is a functional of a single function, du must be a test function" + ) + return ufl.extract_blocks(ufl.derivative(F, u, du)) + + +def _derive_multivariate_residual( + F: ufl.Form, + u: Sequence[ufl.Form], + du: Sequence[ufl.Argument] | None = None, +) -> Sequence[ufl.Form]: + if not all([isinstance(u_i, Function) for u_i in u]): + raise ValueError("u contains some non ufl.Function elements") + if du is None: + du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) + elif (not isinstance(du, Sequence) + or not len(u) == len(du) + or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du])): + raise ValueError( + "When F is a functional of N functions, du must be a sequence " + "containing N test functions" + ) + return ufl.extract_blocks(ufl.derivative(F, u, du)) + + +def _derive_univariate_jacobian( + F: ufl.Form, + u: Function, + du: ufl.Argument | None = None, +) -> ufl.Form: + if not isinstance(u, Function): + raise ValueError("When F is a rank-one UFL form, u must be a ufl.Function") + if du is None: + du = ufl.TrialFunction(u.function_space) + elif not isinstance(du, ufl.Argument) or du.number() != 1: + raise ValueError("When F is a rank-one UFL form, du must be a trial function") + return ufl.derivative(F, u, du) + + +def _derive_multivariate_jacobian( + F: Sequence[ufl.Form], + u: Sequence[ufl.Form], + du: Sequence[ufl.Argument] | None = None, +) -> Sequence[Sequence[ufl.Form]]: + if not all([isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F]): + raise ValueError("F contains some non rank-one ufl.Form elements") + if not isinstance(u, Sequence): + raise ValueError("When F is a sequence, u must be a sequence") + if not all([isinstance(u_i, Function) for u_i in u]): + raise ValueError("u contains some non ufl.Function elements") + if du is None: + du = [ufl.TrialFunction(u_i.function_space) for u_i in u] + elif (not isinstance(du, Sequence) + or not len(u) == len(du) + or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du])): + raise ValueError( + "When F is a list of N rank-one forms, du must be a sequence " + "containing N trial functions" + ) + return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] + + def derivative_block( F: ufl.Form | Sequence[ufl.Form], u: Function | Sequence[Function], @@ -709,56 +779,17 @@ def derivative_block( called component-wise. """ # noqa: D301 - if isinstance(F, ufl.Form) and not F.arguments(): # if F is a functional + if isinstance(F, ufl.Form) and not F.arguments(): if isinstance(u, Function): - if du is None: - du = ufl.TestFunction(u.function_space) - elif not isinstance(du, ufl.Argument) or du.number() != 0: - raise ValueError( - "When F is a functional of a single function, du must be a test function" - ) + return _derive_univariate_residual(F, u, du) elif isinstance(u, Sequence): - if not all([isinstance(u_i, Function) for u_i in u]): - raise ValueError("u contains some non ufl.Function elements") - if du is None: - du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) - elif (not isinstance(du, Sequence) - or not len(u) == len(du) - or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du])): - raise ValueError( - "When F is a functional of N functions, du must be a sequence " - "containing N test functions" - ) + return _derive_multivariate_residual(F, u, du) else: - raise ValueError("u must be a function or sequence of functions") - return ufl.extract_blocks(ufl.derivative(F, u, du)) - + raise ValueError("u must be either a ufl.Function or a sequence of ufl.Function") elif isinstance(F, ufl.Form) and len(F.arguments()) == 1: - if not isinstance(u, Function): - raise ValueError("When F is a rank-one UFL form, u must be a ufl.Function") - if du is None: - du = ufl.TrialFunction(u.function_space) - elif not isinstance(du, ufl.Argument) or du.number() != 1: - raise ValueError("When F is a rank-one UFL form, du must be a trial function") - return ufl.derivative(F, u, du) + return _derive_univariate_jacobian(F, u, du) elif isinstance(F, Sequence): - if not all([isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F]): - raise ValueError("F contains some non rank-one ufl.Form elements") - if not isinstance(u, Sequence): - raise ValueError("When F is a sequence, u must be a sequence") - if not all([isinstance(u_i, Function) for u_i in u]): - raise ValueError("u contains some non ufl.Function elements") - if du is None: - du = [ufl.TrialFunction(u_i.function_space) for u_i in u] - elif (not isinstance(du, Sequence) - or not len(u) == len(du) - or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du])): - raise ValueError( - "When F is a list of N rank-one forms, du must be a sequence " - "containing N trial functions" - ) - return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] - + return _derive_multivariate_jacobian(F, u, du) else: raise ValueError( "F must be either a UFL form (with rank zero or one), or a sequence of " From b84804a591424ae94111ae54911746e9e85002bb Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Mon, 9 Mar 2026 02:45:45 +0100 Subject: [PATCH 12/26] Avoid applying ufl.extract_blocks to the result from univariate functionals. --- python/dolfinx/fem/forms.py | 2 +- python/test/unit/fem/test_forms.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index b8cae6d231..0dce6d13e9 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -692,7 +692,7 @@ def _derive_univariate_residual( raise ValueError( "When F is a functional of a single function, du must be a test function" ) - return ufl.extract_blocks(ufl.derivative(F, u, du)) + return ufl.derivative(F, u, du) def _derive_multivariate_residual( diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 01af990f97..a4c25bd6c6 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -160,11 +160,6 @@ def test_derivative_block(): assert isinstance(R, ufl_form) and len(R.arguments()) == 1 R = derivative_block(F, f0, v0) - assert isinstance(R, Sequence) and len(R) == 1 - assert isinstance(R[0], ufl_form) and len(R[0].arguments()) == 1 - - v0_no_mixed_space = TestFunction(V0) - R = derivative_block(F, f0, v0_no_mixed_space) assert isinstance(R, ufl_form) and len(R.arguments()) == 1 with pytest.raises(ValueError): From 435c0e9f2fde208eabf37e2fe3302064d1f37314 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Mon, 9 Mar 2026 03:12:37 +0100 Subject: [PATCH 13/26] Simplify calls to 'all', as its argument doesn't need to come in a list. --- python/dolfinx/fem/forms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 0dce6d13e9..3e17bee395 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -700,13 +700,13 @@ def _derive_multivariate_residual( u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, ) -> Sequence[ufl.Form]: - if not all([isinstance(u_i, Function) for u_i in u]): + if not all(isinstance(u_i, Function) for u_i in u): raise ValueError("u contains some non ufl.Function elements") if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) elif (not isinstance(du, Sequence) or not len(u) == len(du) - or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du])): + or not all(isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du)): raise ValueError( "When F is a functional of N functions, du must be a sequence " "containing N test functions" @@ -733,17 +733,17 @@ def _derive_multivariate_jacobian( u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, ) -> Sequence[Sequence[ufl.Form]]: - if not all([isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F]): + if not all(isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F): raise ValueError("F contains some non rank-one ufl.Form elements") if not isinstance(u, Sequence): raise ValueError("When F is a sequence, u must be a sequence") - if not all([isinstance(u_i, Function) for u_i in u]): + if not all(isinstance(u_i, Function) for u_i in u): raise ValueError("u contains some non ufl.Function elements") if du is None: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] elif (not isinstance(du, Sequence) or not len(u) == len(du) - or not all([isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du])): + or not all(isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du)): raise ValueError( "When F is a list of N rank-one forms, du must be a sequence " "containing N trial functions" From c580756f08df7c1d5b64dabbd766940e9c4c2794 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 10 Mar 2026 21:47:08 +0100 Subject: [PATCH 14/26] In test_derivative_block, rename to 'M' variables holding functionals, and rename to 'F' variables holding residuals. --- python/test/unit/fem/test_forms.py | 50 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index a4c25bd6c6..bf78d49872 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -148,64 +148,64 @@ def test_derivative_block(): u0, u1 = TrialFunctions(V) - F = f0**2 * dx # univariate functional + M = f0**2 * dx # univariate functional with pytest.raises(ValueError): - derivative_block(F, u0, v0) # second argument not a ufl.Function + derivative_block(M, u0, v0) # second argument not a ufl.Function with pytest.raises(ValueError): - derivative_block(F, f0, u0) # third argument not a test function + derivative_block(M, f0, u0) # third argument not a test function - R = derivative_block(F, f0) - assert isinstance(R, ufl_form) and len(R.arguments()) == 1 + F = derivative_block(M, f0) + assert isinstance(F, ufl_form) and len(F.arguments()) == 1 - R = derivative_block(F, f0, v0) - assert isinstance(R, ufl_form) and len(R.arguments()) == 1 + F = derivative_block(M, f0, v0) + assert isinstance(F, ufl_form) and len(F.arguments()) == 1 with pytest.raises(ValueError): - derivative_block(R, u0, u0) # second argument not a ufl.Function + derivative_block(F, u0, u0) # second argument not a ufl.Function with pytest.raises(ValueError): - derivative_block(R, f0, v0) # third argument not a trial function + derivative_block(F, f0, v0) # third argument not a trial function - J = derivative_block(R, f0) + J = derivative_block(F, f0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 - J = derivative_block(R, f0, u0) + J = derivative_block(F, f0, u0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 - F = f0**2 * f1 * dx # multivariate functional + M = f0**2 * f1 * dx # multivariate functional with pytest.raises(ValueError): - derivative_block(F, [f0, f1], [v0]) # third argument has wrong length + derivative_block(M, [f0, f1], [v0]) # third argument has wrong length with pytest.raises(ValueError): - derivative_block(F, [f0, f1], [u0, v1]) # third argument contains a non test function + derivative_block(M, [f0, f1], [u0, v1]) # third argument contains a non test function with pytest.raises(ValueError): - derivative_block(F, f0, [u0, u1]) # second argument not a sequence + derivative_block(M, f0, [u0, u1]) # second argument not a sequence with pytest.raises(ValueError): - derivative_block(F, [f0, f1], u0) # third argument not a sequence + derivative_block(M, [f0, f1], u0) # third argument not a sequence - R = derivative_block(F, [f0, f1]) - assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) + F = derivative_block(M, [f0, f1]) + assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F]) - R = derivative_block(F, [f0, f1], [v0, v1]) - assert all([isinstance(R_i, ufl_form) and len(R_i.arguments()) == 1 for R_i in R]) + F = derivative_block(M, [f0, f1], [v0, v1]) + assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F]) with pytest.raises(ValueError): - derivative_block(R, [f0, f1], [u0]) # third argument has wrong length + derivative_block(F, [f0, f1], [u0]) # third argument has wrong length with pytest.raises(ValueError): - derivative_block(R, [f0, u1], [u0, u1]) # second argument contains a non ufl.Function + derivative_block(F, [f0, u1], [u0, u1]) # second argument contains a non ufl.Function with pytest.raises(ValueError): - derivative_block(R, [f0, f1], u0) # third argument not a sequence + derivative_block(F, [f0, f1], u0) # third argument not a sequence - J = derivative_block(R, [f0, f1]) + J = derivative_block(F, [f0, f1]) assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) - J = derivative_block(R, [f0, f1], [u0, u1]) + J = derivative_block(F, [f0, f1], [u0, u1]) assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) From 2f6ba5163d19fcf27feaaf491c8e513301623d22 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 10 Mar 2026 23:48:48 +0100 Subject: [PATCH 15/26] Rename function derive_multivariate_residual to derive_block_residual and derive_multivariate_jacobian to derive_block_jacobian. --- python/dolfinx/fem/forms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 3e17bee395..f77e741d88 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -695,7 +695,7 @@ def _derive_univariate_residual( return ufl.derivative(F, u, du) -def _derive_multivariate_residual( +def _derive_block_residual( F: ufl.Form, u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, @@ -728,7 +728,7 @@ def _derive_univariate_jacobian( return ufl.derivative(F, u, du) -def _derive_multivariate_jacobian( +def _derive_block_jacobian( F: Sequence[ufl.Form], u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, @@ -783,13 +783,13 @@ def derivative_block( if isinstance(u, Function): return _derive_univariate_residual(F, u, du) elif isinstance(u, Sequence): - return _derive_multivariate_residual(F, u, du) + return _derive_block_residual(F, u, du) else: raise ValueError("u must be either a ufl.Function or a sequence of ufl.Function") elif isinstance(F, ufl.Form) and len(F.arguments()) == 1: return _derive_univariate_jacobian(F, u, du) elif isinstance(F, Sequence): - return _derive_multivariate_jacobian(F, u, du) + return _derive_block_jacobian(F, u, du) else: raise ValueError( "F must be either a UFL form (with rank zero or one), or a sequence of " From 294c39ba96df9840e5fa172a940d2b4d8d0e05e7 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 10 Mar 2026 23:55:55 +0100 Subject: [PATCH 16/26] Amend docstring of derivative_block to be more precise about its return value. --- python/dolfinx/fem/forms.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index f77e741d88..e54795c538 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -762,19 +762,20 @@ def derivative_block( This is commonly used to derive a block residual from a functional, or to derive a block Jacobian from a block residual. - If ``F`` is a univariate rank zero form, the residual is computed as + If ``F`` is a univariate rank zero form, returns the residual computed as :math:`R = \\frac{\\partial F}{\\partial u}[\\delta u]`. - If ``F`` is a multivariate rank zero form, the residual is a list with - :math:`R_i = \\frac{\\partial F}{\\partial u_i}[\\delta u_i]`, where - :math:`\\delta u_i` is a test subfunction of the mixed space defined by u. - - If ``F`` is a rank one form, the Jacobian is computed as :math:`J = + If ``F`` is a rank one form, returns the Jacobian computed as :math:`J = \\frac{\\partial F}{\\partial u}[\\delta u]`. - All three operations above are identical to calling {py:func}`ufl.derivative` directly. + The two operations above are identical to calling {py:func}`ufl.derivative` directly. + + If ``F`` is a multivariate rank zero form, returns the block residual as a list with + :math:`R_i = \\frac{\\partial F}{\\partial u_i}[\\delta u_i]`, where + :math:`\\delta u_i` is a test subfunction of the mixed space defined by u. This is + equivalent to calling {py:func}`ufl.extract_blocks` on the result from {py:func}`ufl.derivative`. - If ``F`` is a list of rank one forms, the Jacobian is a list of lists with + If ``F`` is a list of rank one forms, returns the block Jacobian as a list of lists with :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using {py:func}`ufl.derivative` called component-wise. """ # noqa: D301 From 73132191f7a69dc7bf0964a027cf13ca371b1603 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Wed, 11 Mar 2026 22:57:12 +0100 Subject: [PATCH 17/26] Remove input check that validated that u is a ufl.Function, or a sequence of them. Remove the corresponding tests from test_derivative_block.. --- python/dolfinx/fem/forms.py | 4 ---- python/test/unit/fem/test_forms.py | 6 ------ 2 files changed, 10 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index e54795c538..f496969c96 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -700,8 +700,6 @@ def _derive_block_residual( u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, ) -> Sequence[ufl.Form]: - if not all(isinstance(u_i, Function) for u_i in u): - raise ValueError("u contains some non ufl.Function elements") if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) elif (not isinstance(du, Sequence) @@ -737,8 +735,6 @@ def _derive_block_jacobian( raise ValueError("F contains some non rank-one ufl.Form elements") if not isinstance(u, Sequence): raise ValueError("When F is a sequence, u must be a sequence") - if not all(isinstance(u_i, Function) for u_i in u): - raise ValueError("u contains some non ufl.Function elements") if du is None: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] elif (not isinstance(du, Sequence) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index bf78d49872..5ef9b54fef 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -150,9 +150,6 @@ def test_derivative_block(): M = f0**2 * dx # univariate functional - with pytest.raises(ValueError): - derivative_block(M, u0, v0) # second argument not a ufl.Function - with pytest.raises(ValueError): derivative_block(M, f0, u0) # third argument not a test function @@ -198,9 +195,6 @@ def test_derivative_block(): with pytest.raises(ValueError): derivative_block(F, [f0, f1], [u0]) # third argument has wrong length - with pytest.raises(ValueError): - derivative_block(F, [f0, u1], [u0, u1]) # second argument contains a non ufl.Function - with pytest.raises(ValueError): derivative_block(F, [f0, f1], u0) # third argument not a sequence From 1b431ccf7b0406b70b4cd157b3bb6a4d8d99c17b Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Wed, 11 Mar 2026 23:00:18 +0100 Subject: [PATCH 18/26] Remove input check that validated that u and du have the same length. Remove the corresponding test from test_derivative_block. --- python/dolfinx/fem/forms.py | 7 +++---- python/test/unit/fem/test_forms.py | 3 --- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index f496969c96..5748ac87b3 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -702,12 +702,11 @@ def _derive_block_residual( ) -> Sequence[ufl.Form]: if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) - elif (not isinstance(du, Sequence) - or not len(u) == len(du) + elif (not isinstance(du, Sequence) or not all(isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du)): raise ValueError( - "When F is a functional of N functions, du must be a sequence " - "containing N test functions" + "When F is a functional of multiple functions, du must be a sequence of test " + "functions" ) return ufl.extract_blocks(ufl.derivative(F, u, du)) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 5ef9b54fef..b972913beb 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -174,9 +174,6 @@ def test_derivative_block(): M = f0**2 * f1 * dx # multivariate functional - with pytest.raises(ValueError): - derivative_block(M, [f0, f1], [v0]) # third argument has wrong length - with pytest.raises(ValueError): derivative_block(M, [f0, f1], [u0, v1]) # third argument contains a non test function From 0f19a51dff93acc0a4342f0d6489c0e963246c58 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Wed, 11 Mar 2026 23:03:09 +0100 Subject: [PATCH 19/26] Amend some cases in test_derivative_block, which had more wrong inputs than the test intended. --- python/test/unit/fem/test_forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index b972913beb..13c4368361 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -178,10 +178,10 @@ def test_derivative_block(): derivative_block(M, [f0, f1], [u0, v1]) # third argument contains a non test function with pytest.raises(ValueError): - derivative_block(M, f0, [u0, u1]) # second argument not a sequence + derivative_block(M, f0, [v0, v1]) # second argument not a sequence with pytest.raises(ValueError): - derivative_block(M, [f0, f1], u0) # third argument not a sequence + derivative_block(M, [f0, f1], v0) # third argument not a sequence F = derivative_block(M, [f0, f1]) assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F]) From 0141b99b8d59aed3abbca6599015ac205e14c352 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Wed, 11 Mar 2026 23:53:55 +0100 Subject: [PATCH 20/26] Use the suffix 'block' when naming block residuals and block jacobians in test_derivative_block. --- python/test/unit/fem/test_forms.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 13c4368361..aa4d0f25f1 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -172,31 +172,31 @@ def test_derivative_block(): assert isinstance(J, ufl_form) and len(J.arguments()) == 2 - M = f0**2 * f1 * dx # multivariate functional + M_block = f0**2 * f1 * dx # multivariate functional with pytest.raises(ValueError): - derivative_block(M, [f0, f1], [u0, v1]) # third argument contains a non test function + derivative_block(M_block, [f0, f1], [u0, v1]) # third argument contains a non test function with pytest.raises(ValueError): - derivative_block(M, f0, [v0, v1]) # second argument not a sequence + derivative_block(M_block, f0, [v0, v1]) # second argument not a sequence with pytest.raises(ValueError): - derivative_block(M, [f0, f1], v0) # third argument not a sequence + derivative_block(M_block, [f0, f1], v0) # third argument not a sequence - F = derivative_block(M, [f0, f1]) - assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F]) + F_block = derivative_block(M_block, [f0, f1]) + assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F_block]) - F = derivative_block(M, [f0, f1], [v0, v1]) - assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F]) + F_block = derivative_block(M_block, [f0, f1], [v0, v1]) + assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F_block]) with pytest.raises(ValueError): - derivative_block(F, [f0, f1], [u0]) # third argument has wrong length + derivative_block(F_block, [f0, f1], [u0]) # third argument has wrong length with pytest.raises(ValueError): - derivative_block(F, [f0, f1], u0) # third argument not a sequence + derivative_block(F_block, [f0, f1], u0) # third argument not a sequence - J = derivative_block(F, [f0, f1]) - assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) + J_block = derivative_block(F_block, [f0, f1]) + assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J_block for J_ij in J_i]) - J = derivative_block(F, [f0, f1], [u0, u1]) - assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J for J_ij in J_i]) + J_block = derivative_block(F_block, [f0, f1], [u0, u1]) + assert all([isinstance(J_ij, ufl_form) and len(J_ij.arguments()) == 2 for J_i in J_block for J_ij in J_i]) From 30ef1a9ff6c32570da33e96f1a0fbb1a261d62a0 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Fri, 13 Mar 2026 21:01:37 +0100 Subject: [PATCH 21/26] Improve derivative_block's docstring to make clear the type of return values in each case that it supports. --- python/dolfinx/fem/forms.py | 42 ++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 5748ac87b3..0af701dacf 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -757,22 +757,34 @@ def derivative_block( This is commonly used to derive a block residual from a functional, or to derive a block Jacobian from a block residual. - If ``F`` is a univariate rank zero form, returns the residual computed as - :math:`R = \\frac{\\partial F}{\\partial u}[\\delta u]`. + Four cases are supported: + + 1. ``F`` is a rank-zero ``ufl.Form``, and ``u`` is a ``ufl.Function``. + Returns a ``ufl.Form`` representing the residual :math:`R = + \\frac{\\partial F}{\\partial u}[\\delta u]`. This is equivalent to + calling {py:func}`ufl.derivative` directly. + + 2. ``F`` is a rank-zero `ufl.Form``, and ``u`` is a list of ``ufl.Function``. + Returns a list of ``ufl.Form`` representing the block residual :math:`R`, + with :math:`R_i = \\frac{\\partial F}{\\partial u_i}[\\delta u_i]`, where + :math:`\\delta u_i` is a test subfunction of the mixed space defined by + ``u``. This is equivalent to calling {py:func}`ufl.extract_blocks` on the + result from {py:func}`ufl.derivative`. + + 3. ``F`` is a rank-one `ufl.Form``, and ``u`` is a ``ufl.Function``. + Returns a ``ufl.Form`` representing the Jacobian :math:`J = + \\frac{\\partial F}{\\partial u}[\\delta u]`. This is equivalent to + calling {py:func}`ufl.derivative` directly. + + 4. ``F`` is a list of rank-one `ufl.Form``, and ``u`` is a list of + ``ufl.Function``. Returns a list of lists representing the block Jacobian + :math:`J`, with :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` + using {py:func}`ufl.derivative` called component-wise. - If ``F`` is a rank one form, returns the Jacobian computed as :math:`J = - \\frac{\\partial F}{\\partial u}[\\delta u]`. - - The two operations above are identical to calling {py:func}`ufl.derivative` directly. - - If ``F`` is a multivariate rank zero form, returns the block residual as a list with - :math:`R_i = \\frac{\\partial F}{\\partial u_i}[\\delta u_i]`, where - :math:`\\delta u_i` is a test subfunction of the mixed space defined by u. This is - equivalent to calling {py:func}`ufl.extract_blocks` on the result from {py:func}`ufl.derivative`. - - If ``F`` is a list of rank one forms, returns the block Jacobian as a list of lists with - :math:`J_{ij} = \\frac{\\partial F_i}{u_j}[\\delta u_j]` using {py:func}`ufl.derivative` - called component-wise. + Args: + F: UFL form(s) to be derived. + u: Function(s) with respect to the derivative is computed. + du: UFL argument(s) representing the direction of the derivative. """ # noqa: D301 if isinstance(F, ufl.Form) and not F.arguments(): From 41ae81a0d091dc6676670b226f2b789ec5ac869c Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 12 Apr 2026 18:55:38 +0200 Subject: [PATCH 22/26] Remove checks that verify the test/trial arguments in derivative_block. --- python/dolfinx/fem/forms.py | 26 +++++--------------------- python/test/unit/fem/test_forms.py | 9 --------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 0af701dacf..3542722bc9 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -688,10 +688,6 @@ def _derive_univariate_residual( ) -> ufl.Form: if du is None: du = ufl.TestFunction(u.function_space) - elif not isinstance(du, ufl.Argument) or du.number() != 0: - raise ValueError( - "When F is a functional of a single function, du must be a test function" - ) return ufl.derivative(F, u, du) @@ -702,12 +698,6 @@ def _derive_block_residual( ) -> Sequence[ufl.Form]: if du is None: du = ufl.TestFunctions(ufl.MixedFunctionSpace(*(u_i.function_space for u_i in u))) - elif (not isinstance(du, Sequence) - or not all(isinstance(du_i, ufl.Argument) and du_i.number() == 0 for du_i in du)): - raise ValueError( - "When F is a functional of multiple functions, du must be a sequence of test " - "functions" - ) return ufl.extract_blocks(ufl.derivative(F, u, du)) @@ -720,8 +710,6 @@ def _derive_univariate_jacobian( raise ValueError("When F is a rank-one UFL form, u must be a ufl.Function") if du is None: du = ufl.TrialFunction(u.function_space) - elif not isinstance(du, ufl.Argument) or du.number() != 1: - raise ValueError("When F is a rank-one UFL form, du must be a trial function") return ufl.derivative(F, u, du) @@ -730,19 +718,15 @@ def _derive_block_jacobian( u: Sequence[ufl.Form], du: Sequence[ufl.Argument] | None = None, ) -> Sequence[Sequence[ufl.Form]]: - if not all(isinstance(F_i, ufl.Form) and len(F_i.arguments()) == 1 for F_i in F): - raise ValueError("F contains some non rank-one ufl.Form elements") if not isinstance(u, Sequence): raise ValueError("When F is a sequence, u must be a sequence") if du is None: du = [ufl.TrialFunction(u_i.function_space) for u_i in u] - elif (not isinstance(du, Sequence) - or not len(u) == len(du) - or not all(isinstance(du_i, ufl.Argument) and du_i.number() == 1 for du_i in du)): - raise ValueError( - "When F is a list of N rank-one forms, du must be a sequence " - "containing N trial functions" - ) + elif (not isinstance(du, Sequence) or not len(u) == len(du)): + raise ValueError( + "When F is a list of N forms, du must be a sequence " + "containing N functions" + ) return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index aa4d0f25f1..5c732b87d1 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -150,9 +150,6 @@ def test_derivative_block(): M = f0**2 * dx # univariate functional - with pytest.raises(ValueError): - derivative_block(M, f0, u0) # third argument not a test function - F = derivative_block(M, f0) assert isinstance(F, ufl_form) and len(F.arguments()) == 1 @@ -162,9 +159,6 @@ def test_derivative_block(): with pytest.raises(ValueError): derivative_block(F, u0, u0) # second argument not a ufl.Function - with pytest.raises(ValueError): - derivative_block(F, f0, v0) # third argument not a trial function - J = derivative_block(F, f0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 @@ -174,9 +168,6 @@ def test_derivative_block(): M_block = f0**2 * f1 * dx # multivariate functional - with pytest.raises(ValueError): - derivative_block(M_block, [f0, f1], [u0, v1]) # third argument contains a non test function - with pytest.raises(ValueError): derivative_block(M_block, f0, [v0, v1]) # second argument not a sequence From 81c86888f2156f7a7d0ceb6d47ed18bb6fa604d2 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 12 Apr 2026 18:56:48 +0200 Subject: [PATCH 23/26] Let UFL handle malformed second arguments to ufl.derivative. --- python/dolfinx/fem/forms.py | 2 -- python/test/unit/fem/test_forms.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index 3542722bc9..fe8f878880 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -706,8 +706,6 @@ def _derive_univariate_jacobian( u: Function, du: ufl.Argument | None = None, ) -> ufl.Form: - if not isinstance(u, Function): - raise ValueError("When F is a rank-one UFL form, u must be a ufl.Function") if du is None: du = ufl.TrialFunction(u.function_space) return ufl.derivative(F, u, du) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 5c732b87d1..fd735d5aef 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -156,9 +156,6 @@ def test_derivative_block(): F = derivative_block(M, f0, v0) assert isinstance(F, ufl_form) and len(F.arguments()) == 1 - with pytest.raises(ValueError): - derivative_block(F, u0, u0) # second argument not a ufl.Function - J = derivative_block(F, f0) assert isinstance(J, ufl_form) and len(J.arguments()) == 2 From eafb50635665853afac7e3ff0a80a0cf85dcb1d7 Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 12 Apr 2026 18:58:06 +0200 Subject: [PATCH 24/26] Fix minor typo. --- python/dolfinx/fem/forms.py | 2 +- python/test/unit/fem/test_forms.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index fe8f878880..cc6e3f9df9 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -725,7 +725,7 @@ def _derive_block_jacobian( "When F is a list of N forms, du must be a sequence " "containing N functions" ) - return [[ufl.derivative(Fi, u_j, du_j) for u_j, du_j in zip(u, du)] for Fi in F] + return [[ufl.derivative(F_i, u_j, du_j) for u_j, du_j in zip(u, du)] for F_i in F] def derivative_block( diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index fd735d5aef..d5a3b7a359 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -165,12 +165,6 @@ def test_derivative_block(): M_block = f0**2 * f1 * dx # multivariate functional - with pytest.raises(ValueError): - derivative_block(M_block, f0, [v0, v1]) # second argument not a sequence - - with pytest.raises(ValueError): - derivative_block(M_block, [f0, f1], v0) # third argument not a sequence - F_block = derivative_block(M_block, [f0, f1]) assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F_block]) From c94308dbd269e3fbfe1ddc721ec56cd26074308a Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Sun, 12 Apr 2026 19:26:19 +0200 Subject: [PATCH 25/26] Add unit test that ensures _derive_block_jacobian doesn't break with bad second argument. Clean up import no longer needed. --- python/test/unit/fem/test_forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index d5a3b7a359..9983790f20 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -19,8 +19,6 @@ from dolfinx.mesh import create_unit_square from ufl import Measure, SpatialCoordinate, TestFunction, TrialFunction, dx, inner, Form as ufl_form, TrialFunctions, TestFunctions, MixedFunctionSpace -from collections.abc import Sequence - def test_extract_forms(): """Test extraction on unique function spaces for rows and columns of @@ -171,6 +169,9 @@ def test_derivative_block(): F_block = derivative_block(M_block, [f0, f1], [v0, v1]) assert all([isinstance(F_i, ufl_form) and len(F_i.arguments()) == 1 for F_i in F_block]) + with pytest.raises(ValueError): + derivative_block(F_block, f0) # second argument not a sequence + with pytest.raises(ValueError): derivative_block(F_block, [f0, f1], [u0]) # third argument has wrong length From cc33a644c5b2802ff0112aa103018121e55e011d Mon Sep 17 00:00:00 2001 From: Jose Antonio Fernandez Vicente Date: Tue, 21 Apr 2026 18:42:51 +0200 Subject: [PATCH 26/26] Update copyright notice --- python/dolfinx/fem/forms.py | 2 +- python/test/unit/fem/test_forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/dolfinx/fem/forms.py b/python/dolfinx/fem/forms.py index cc6e3f9df9..db86d82813 100644 --- a/python/dolfinx/fem/forms.py +++ b/python/dolfinx/fem/forms.py @@ -1,5 +1,5 @@ # Copyright (C) 2017-2026 Chris N. Richardson, Garth N. Wells, -# Michal Habera, Jørgen S. Dokken and Jack S. Hale +# Michal Habera, Jørgen S. Dokken, Jack S. Hale and Jose Fernandez # # This file is part of DOLFINx (https://www.fenicsproject.org) # diff --git a/python/test/unit/fem/test_forms.py b/python/test/unit/fem/test_forms.py index 9983790f20..35f6c730f6 100644 --- a/python/test/unit/fem/test_forms.py +++ b/python/test/unit/fem/test_forms.py @@ -1,6 +1,6 @@ """Tests for DOLFINx integration of various form operations.""" -# Copyright (C) 2021 Garth N. Wells +# Copyright (C) 2021-2026 Garth N. Wells and Jose Fernandez # # This file is part of DOLFINx (https://www.fenicsproject.org) #