# Creating a custom unary operator within ufl

**URL:** https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151
**Category:** UFL
**Tags:** dolfinx
**Created:** [August 25, 2025, 6:14pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151 "2025-08-25T18:14:23Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![Javier\_Sierra](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/javier_sierra/32/7903_2.png) [@Javier\_Sierra](https://fenicsproject.discourse.group/u/Javier_Sierra)
#### Post date: [August 25, 2025, 6:14pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/1 "2025-08-25T18:14:24Z")

</div>

Dear community,

I would like to create a custom unary operator, similar to the MathFunction module within ufl that respects `dolfinx.fem.form ` and with a custom behaviour under `ufl.derivative`.

What I would like to do is to define a custom grad\_z operator which is evaluated to zero for a linear form `F(u_0)` and it is evaluated to `1j *`_`k * u`_ for its derivative. That is, if we consider for instance the linear form

```auto
w = dolfinx.fem.Function(V)
v = ufl.TestFunction(V)
u = ufl.TrialFunction(V)
a_form = ufl.inner(grad_z(w,k=2.0), v) * ufl.dx
# This should be equivalent to ufl.inner(0 * w, v) * ufl.dx
J_form = ufl.derivative(a_form, w, u) # This should be equivalent to ufl.inner(1j * 2 * u, v)

```

I’ve seen that there is the package **[dolfinx-external-operator](https://github.com/a-latyshev/dolfinx-external-operator/tree/main),** however, it would be better if I could do it within dolfinx+python modifications. I have tried to create custom Operators inheriting from `ufl.core.operator.Operator ` or other differential operators but with no success (most of my attempts did not compile under dolfinx.fem.form). Do you have any ideas / advices?

Thank you in advance!

---

<div class="post-metadata">

### Author: ![dokken](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/dokken/32/1560_2.png) [@dokken](https://fenicsproject.discourse.group/u/dokken)
#### Post date: [August 25, 2025, 6:25pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/2 "2025-08-25T18:25:33Z")

</div>

I would suggest you add your attempt here as well, to make life easier for others that would like to help you.

To allow differentiation, you need to register a differentiation rule, similar to [ufl/ufl/algorithms/apply\_derivatives.py at main · FEniCS/ufl · GitHub](https://github.com/FEniCS/ufl/blob/main/ufl/algorithms/apply_derivatives.py#L489)

---

<div class="post-metadata">

### Author: ![Javier\_Sierra](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/javier_sierra/32/7903_2.png) [@Javier\_Sierra](https://fenicsproject.discourse.group/u/Javier_Sierra)
#### Post date: [August 25, 2025, 7:31pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/3 "2025-08-25T19:31:45Z")

</div>

Thank you very much. I am currently using `dolfinx 0.9.0` and `ufl 2024.2.0`, so I do use the latest modifications with `DAGTransverser`.

My latest attempt, based on your suggestion is:

```auto
import dolfinx
import ufl
from ufl import diff, grad, dx, TrialFunction, TestFunction
from dolfinx import fem, mesh
from mpi4py import MPI
import numpy as np

# Create mesh and function space
domain = mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)
V = fem.functionspace(domain, ("Lagrange", 1))

# Usage
u = TrialFunction(V)
v = TestFunction(V)
w = dolfinx.fem.Function(V)
q_guess = ufl.as_vector([2.0])
w.interpolate(dolfinx.fem.Expression(q_guess, V.element.interpolation_points(),comm=domain.comm))

def dz(f, k):
    f = ufl.as_ufl(f)
    return 0.0 * f
def dz_der(self, o, df):
    """Differentiate a dz."""
    f, k = o.ufl_operands
    return k * df
# UFL will automatically use _ufl_derivative_ method
from ufl.algorithms.apply_derivatives import GenericDerivativeRuleset
GenericDerivativeRuleset.dz = dz_der

a_form = ufl.inner(dz(w, 2.0), v) * ufl.dx
F = dolfinx.fem.form(a_form)
J_form = ufl.derivative(a_form, w, u)
J = dolfinx.fem.form(J_form)

```

However, doing so, within a\_form it raises:

```auto
  File "/lib/python3.10/site-packages/ufl/measure.py", line 422, in __rmul__
    raise ValueError("This integral is missing an integration domain.")
ValueError: This integral is missing an integration domain.

```

because `dz(w, k) is ufl.Zero`. In addition if I would have defined

```auto
def dz(f,k):
    f = ufl.as_ufl(f)
    return 1e-30 * f

```

: , `J_form = ufl.derivative(a_form, w, u)` is not the derivative I would like but

```auto
'{ d/dfj { 1e-30 * f * (conj((v_0))) }, with fh=ExprList(*(f,)), dfh/dfj = ExprList(*(v_1,)), and coefficient derivatives ExprMapping(*()) } * dx(<Mesh #0>[everywhere], {})'

```

which I have confirmed that is not the behaviour I wanted by evaluating:

```auto
a_form = ufl.inner(dz(w, 2.0), w) * ufl.dx
J_form = ufl.derivative(a_form, w, u)
J_form = ufl.action(J_form, w)
J = dolfinx.fem.form(J_form)
print(dolfinx.fem.assemble_scalar(J))

```

which gives `8e-30`. The desired behaviour would give something of order unity

Thanks, for the help.

---

<div class="post-metadata">

### Author: ![dokken](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/dokken/32/1560_2.png) [@dokken](https://fenicsproject.discourse.group/u/dokken)
#### Post date: [August 26, 2025, 8:33am UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/4 "2025-08-26T08:33:47Z")

</div>

Here is a function that does what you want:

```python
from mpi4py import MPI
import basix.ufl
import ufl
import dolfinx.fem
import numpy as np
from ufl.constantvalue import as_ufl, Zero
from ufl.core.operator import Operator
from ufl.core.ufl_type import ufl_type
from ufl.algorithms.apply_derivatives import GenericDerivativeRuleset
from ufl.corealg.dag_traverser import DAGTraverser

@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0)
class DZ(Operator):
    """Custom operator."""

    __slots__ = ("_z", "_f")

    def __init__ (self, f, z):
        """Create a custom operator."""
        self._f = as_ufl(f)
        self._z = as_ufl(z)
    
    def __str__ (self):
        return f"dz({self._f}, {self._z})"

    @property
    def ufl_operands(self):
        return (self._f, self._z)

    @property
    def _hash(self)->int:
        return hash(("dz", self._f,self._z))

    def __hash__ (self)-> int:
        return self._hash()

def dz(f, z):
    """Create a custom operator."""
    return DZ(f, z)

@DAGTraverser.postorder
def custom_derivative(self, o, df, dk):
    f, k, = o.ufl_operands
    return k*df

GenericDerivativeRuleset.process.register(DZ, custom_derivative)

domain = dolfinx.mesh.create_unit_interval(MPI.COMM_WORLD, 10)

s_el = basix.ufl.element("Lagrange", basix.CellType.interval, 2)
V = dolfinx.fem.functionspace(domain, s_el)
u = dolfinx.fem.Function(V)
u.interpolate(lambda x: x[0])

q = dz(u, 2.0)
a = q*ufl.dx(domain=domain)

du = ufl.TestFunction(V)
J = ufl.derivative(a, u, du)

J_compiled = dolfinx.fem.form(J)

vec = dolfinx.fem.assemble_vector(J_compiled)

ref = dolfinx.fem.assemble_vector(dolfinx.fem.form(2*du*ufl.dx(domain=domain)))

np.testing.assert_allclose(vec.array, ref.array)

```

please note that you should never try to assemlble the `a` form, as you know it is zero, and there is no easy way of ensuring that this gets passed correctly through ufl (as ufl will automatically remove zero values, before we get to differentiate).

I am not really sure what you what this operation for, as it seems rather strange to have a zero-operator whose derivative is non-zero.

---

<div class="post-metadata">

### Author: ![Javier\_Sierra](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/javier_sierra/32/7903_2.png) [@Javier\_Sierra](https://fenicsproject.discourse.group/u/Javier_Sierra)
#### Post date: [August 26, 2025, 2:56pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/5 "2025-08-26T14:56:22Z")

</div>

Thank you very much dokken!

A naive question in ` __slots__ ` does the order of the arguments matter? Because in your answer, the arguments are in the opposite order to the ` __init__ ` or `ufl_operands`.

In addition, there were several issues with the Operator DZ, when trying to compile `dolfinx.fem.form(a)`. I solved that by including a handle for `ffcx` and a lambda function.

I leave here the complete minimal example

```python3

import dolfinx
from mpi4py import MPI
import basix.ufl
import ufl
import dolfinx.fem
import numpy as np
from ufl.constantvalue import as_ufl, Zero
from ufl.core.operator import Operator
from ufl.core.ufl_type import ufl_type
from ufl.algorithms.apply_derivatives import GenericDerivativeRuleset
from ufl.corealg.dag_traverser import DAGTraverser
from ffcx.ir.analysis import reconstruct
from ufl.constantvalue import (
    ComplexValue,
    ConstantValue,
    FloatValue,
    IntValue,
    RealValue,
    Zero,
    as_ufl,
    is_true_ufl_scalar,
)
from ffcx.codegeneration import lnodes

@ufl_type(num_ops=1, inherit_shape_from_operand=0, inherit_indices_from_operand=0)
class DZ(Operator):
    """Custom operator."""
    __slots__ = ("_f", "_k")
    
    def __init__ (self, f, k):
        """Create a custom operator."""
        self._f = as_ufl(f)
        self._k = as_ufl(k)

    @property
    def ufl_operands(self):
        return (self._f, self._k)

    @property
    def _hash(self)->int:
        return hash(("dz", self._f,self._k))

    def __hash__ (self)-> int:
        return self._hash()

def dz(f, k):
    """Create a custom operator."""
    k = as_ufl(k)
    f = as_ufl(f)
    return DZ(f, k)

@DAGTraverser.postorder
def custom_derivative(self, o, df, dk):
    f, k, = o.ufl_operands
    return k*df

def handle_dz(o, ops):
    """Handle a binary operator."""
    if len(ops) != 2:
        raise RuntimeError("Expecting two operands.")
    return [o._ufl_expr_reconstruct_(a, b) for a, b in zip(ops[0], ops[1])]

# Need to register the derivative, the reconstruction handle for ffcx and the lambda function (lnodes)
lnodes._ufl_call_lookup[DZ] = lambda x, a, b: 0.0 * a
reconstruct._reconstruct_call_lookup[DZ] = handle_dz
GenericDerivativeRuleset.process.register(DZ, custom_derivative)
domain = dolfinx.mesh.create_unit_interval(MPI.COMM_WORLD, 10)

s_el = basix.ufl.element("Lagrange", basix.CellType.interval, 2, shape=(4,))
V = dolfinx.fem.functionspace(domain, s_el)
u = dolfinx.fem.Function(V)
u.interpolate(lambda x: [x[0], x[0], x[1], x[1]])

q = dz(u[0], 2.0)
a = q*ufl.dx(domain=domain)
F = dolfinx.fem.form(a)
f = dolfinx.fem.assemble_scalar(F)

du = ufl.TestFunction(V)
J = ufl.derivative(a, u, du)
J_compiled = dolfinx.fem.form(J)
vec = dolfinx.fem.assemble_vector(J_compiled)
ref = dolfinx.fem.assemble_vector(dolfinx.fem.form(2*du[0]*ufl.dx(domain=domain)))

np.testing.assert_allclose(vec.array, ref.array)
np.testing.assert_allclose(f, 0.0)

```

Thanks!

The motivation for the definition of a new ufl operator it was to account for the Fourier derivative of the linearization of nonlinear equations that are homogeneous in the z direction. For instance, if we consider a 2D field f\_0(x,y) but we want to analyse a linearization of this field

f(x,y,z) \mapsto f\_0(x,y) + \epsilon f'(x,y) \exp (i \beta z) for \beta \in \mathbb{R} and \epsilon \ll 1,

it induces a slow derivative \partial\_z f \mapsto i \beta f'. However, the evaluation of \partial\_z f\_0 \mapsto 0.

---

<div class="post-metadata">

### Author: ![dokken](https://yyz2.discourse-cdn.com/free1/user_avatar/fenicsproject.discourse.group/dokken/32/1560_2.png) [@dokken](https://fenicsproject.discourse.group/u/dokken)
#### Post date: [August 26, 2025, 4:26pm UTC](https://fenicsproject.discourse.group/t/creating-a-custom-unary-operator-within-ufl/18151/6 "2025-08-26T16:26:00Z")

</div>

> [@Javier\_Sierra](#):
>
> A naive question in ` __slots__ ` does the order of the arguments matter? Because in your answer, the arguments are in the opposite order to the ` __init__ ` or `ufl_operands`.

It shouldn’t matter

> [@Javier\_Sierra](#):
>
> In addition, there were several issues with the Operator DZ, when trying to compile `dolfinx.fem.form(a)`. I solved that by including a handle for `ffcx` and a lambda function.

Good that you found a solution, I didn’t have the time to start patching/attaching data to ffcx handlers.
