From bafefcd23a055e3a610ae91e33c1fa56c6817c3a Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:02:30 +0100 Subject: [PATCH 1/7] Introduce Form.arity --- test/test_form.py | 13 +++++++++++++ ufl/form.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/test/test_form.py b/test/test_form.py index 0d680f82e..225db2740 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -37,6 +37,11 @@ def domain(): return Mesh(LagrangeElement(cell, 1, (2,))) +@pytest.fixture +def functional(domain): + return 1 * dx(domain) + + @pytest.fixture def mass(domain): cell = triangle @@ -104,6 +109,14 @@ def test_form_arguments(mass, stiffness, convection, load): assert ((f * v) * u * dx + (u * 3) * (v / 2) * dx(2)).arguments() == (v, u) +def test_form_arity(functional, mass, stiffness, convection, load) -> None: + assert functional.arity == 0 + assert mass.arity == 2 + assert stiffness.arity == 2 + assert convection.arity == 2 + assert load.arity == 1 + + def test_form_coefficients(element, domain): space = FunctionSpace(domain, element) v = TestFunction(space) diff --git a/ufl/form.py b/ufl/form.py index 9388d8f25..db7f1eb66 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -340,6 +340,11 @@ def empty(self): """Returns whether the form has no integrals.""" return len(self.integrals()) == 0 + @property + def arity(self): + """Arity of the form.""" + return len(self.arguments()) + def ufl_domains(self): """Return the geometric integration domains occuring in the form. From d2632ae477dfba9020f269b29564ccbb90a66a73 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 15 May 2026 16:02:29 +0200 Subject: [PATCH 2/7] Change to int | None --- test/test_form.py | 6 ++++++ ufl/form.py | 25 ++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/test/test_form.py b/test/test_form.py index 225db2740..7000f0f0c 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -116,6 +116,12 @@ def test_form_arity(functional, mass, stiffness, convection, load) -> None: assert convection.arity == 2 assert load.arity == 1 + assert (functional + load).arity is None + assert (functional + mass).arity is None + assert (functional + stiffness).arity is None + assert (load + mass).arity is None + assert (load + stiffness).arity is None + def test_form_coefficients(element, domain): space = FunctionSpace(domain, element) diff --git a/ufl/form.py b/ufl/form.py index db7f1eb66..3a4f009f7 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -341,9 +341,28 @@ def empty(self): return len(self.integrals()) == 0 @property - def arity(self): - """Arity of the form.""" - return len(self.arguments()) + def arity(self) -> int | None: + """Arity of the form. + + Returns: + Number of arguments if all integrals share the argument count, otherwise None. + """ + if not self._integrals: + return 0 + + from ufl.algorithms.analysis import extract_terminals_with_domain + + arity = None + for integral in self._integrals: + args, _, _ = extract_terminals_with_domain(integral.integrand()) + _arity = len(set(args)) + + if arity is None: + arity = _arity + elif arity != _arity: + return None + + return arity def ufl_domains(self): """Return the geometric integration domains occuring in the form. From 7f7ad3b41708fc8b547d393e2dba4ba1fdc354db Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 15 May 2026 16:23:47 +0200 Subject: [PATCH 3/7] Add Mixed test --- test/test_form.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/test_form.py b/test/test_form.py index 7000f0f0c..ce2f121b4 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -19,9 +19,12 @@ grad, inner, nabla_grad, + tetrahedron, triangle, ) +from ufl.argument import TestFunctions, TrialFunctions from ufl.form import BaseForm +from ufl.functionspace import MixedFunctionSpace @pytest.fixture @@ -123,6 +126,24 @@ def test_form_arity(functional, mass, stiffness, convection, load) -> None: assert (load + stiffness).arity is None +def test_form_arity_mixed(domain) -> None: + domain = Mesh(LagrangeElement(triangle, 1, (2,))) + V = FunctionSpace(domain, LagrangeElement(triangle, 1, (2,))) + W = FunctionSpace(domain, LagrangeElement(triangle, 1, (3,))) + + U = MixedFunctionSpace(V, W) + + v, sigma = TestFunctions(U) + u, tau = TrialFunctions(U) + + linear = v[0] * dx + sigma[0] * dx + bilinear = v[0] * u[0] * dx + sigma[0] * tau[0] * dx + + assert linear.arity == 1 + assert bilinear.arity == 2 + assert (linear + bilinear).arity is None + + def test_form_coefficients(element, domain): space = FunctionSpace(domain, element) v = TestFunction(space) From d2ece4275176d0be208788ff4abe63dacfb0245d Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 15 May 2026 16:28:26 +0200 Subject: [PATCH 4/7] Fix: mixed combined in integral --- test/test_form.py | 5 +++++ ufl/form.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_form.py b/test/test_form.py index ce2f121b4..589398d95 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -143,6 +143,11 @@ def test_form_arity_mixed(domain) -> None: assert bilinear.arity == 2 assert (linear + bilinear).arity is None + linear_combined = (v[0] + sigma[0]) * dx + bilinear_combined = (v[0] * u[0] + sigma[0] * tau[0]) * dx + assert linear_combined.arity == 1 + assert bilinear_combined.arity == 2 + def test_form_coefficients(element, domain): space = FunctionSpace(domain, element) diff --git a/ufl/form.py b/ufl/form.py index 3a4f009f7..ad2170089 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -355,7 +355,7 @@ def arity(self) -> int | None: arity = None for integral in self._integrals: args, _, _ = extract_terminals_with_domain(integral.integrand()) - _arity = len(set(args)) + _arity = max((arg.number() + 1 for arg in args), default=0) if arity is None: arity = _arity From e516988fddb84544b8e0506ce1d1c13e1b1e9823 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 15 May 2026 17:05:11 +0200 Subject: [PATCH 5/7] Defensive about MixedFunctionSpaces --- test/test_form.py | 19 +++++++++++++++---- ufl/form.py | 7 +++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/test/test_form.py b/test/test_form.py index 589398d95..99c716bff 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -143,10 +143,21 @@ def test_form_arity_mixed(domain) -> None: assert bilinear.arity == 2 assert (linear + bilinear).arity is None - linear_combined = (v[0] + sigma[0]) * dx - bilinear_combined = (v[0] * u[0] + sigma[0] * tau[0]) * dx - assert linear_combined.arity == 1 - assert bilinear_combined.arity == 2 + # combined mixed form integrals (unsupported) + with pytest.raises( + RuntimeError, match=r"Arity does not support mixed arguments in an integral." + ): + assert ((v[0] + sigma[0]) * dx).arity == 1 + + with pytest.raises( + RuntimeError, match=r"Arity does not support mixed arguments in an integral." + ): + assert ((v[0] * u[0] + sigma[0] * tau[0]) * dx).arity == 2 + + with pytest.raises( + RuntimeError, match=r"Arity does not support mixed arguments in an integral." + ): + assert ((v[0] + sigma[0] + v[0] * u[0] + sigma[0] * tau[0]) * dx).arity is None def test_form_coefficients(element, domain): diff --git a/ufl/form.py b/ufl/form.py index ad2170089..23e7a3dad 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -344,6 +344,9 @@ def empty(self): def arity(self) -> int | None: """Arity of the form. + Note: + Mixed function spaces are supported if the parts are not mixed in a single integral. + Returns: Number of arguments if all integrals share the argument count, otherwise None. """ @@ -355,6 +358,10 @@ def arity(self) -> int | None: arity = None for integral in self._integrals: args, _, _ = extract_terminals_with_domain(integral.integrand()) + + if len(set(arg.part() for arg in args)) > 1: + raise RuntimeError("Arity does not support mixed arguments in an integral.") + _arity = max((arg.number() + 1 for arg in args), default=0) if arity is None: From 0a92da5d1f6505eab3ff73459630f1d9467f267d Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 15 May 2026 17:05:55 +0200 Subject: [PATCH 6/7] ruff --- test/test_form.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_form.py b/test/test_form.py index 99c716bff..25ac96988 100755 --- a/test/test_form.py +++ b/test/test_form.py @@ -19,7 +19,6 @@ grad, inner, nabla_grad, - tetrahedron, triangle, ) from ufl.argument import TestFunctions, TrialFunctions From 73547e48ce58c3865a875958a37ff2e62e5a9858 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:26:06 +0200 Subject: [PATCH 7/7] Use extract_arguments --- ufl/form.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ufl/form.py b/ufl/form.py index 23e7a3dad..c155bf215 100644 --- a/ufl/form.py +++ b/ufl/form.py @@ -353,11 +353,11 @@ def arity(self) -> int | None: if not self._integrals: return 0 - from ufl.algorithms.analysis import extract_terminals_with_domain + from ufl.algorithms.analysis import extract_arguments arity = None for integral in self._integrals: - args, _, _ = extract_terminals_with_domain(integral.integrand()) + args = extract_arguments(integral.integrand()) if len(set(arg.part() for arg in args)) > 1: raise RuntimeError("Arity does not support mixed arguments in an integral.")