.. _ufunc_examples:

Grid Ufunc Examples
-------------------

This page contains examples of different Grid Ufuncs that you might find useful.
They are intended to be more advanced or realistic cases than the simple differencing operations we showed in the introduction to :ref:`grid_ufuncs`.

As each of these cases is a vector calculus operator, we will demonstrate their use on an example two-dimensional vector field.

Firstly we need a two-dimensional grid, and we use similar coordinate names to the 1D example we used when first introducing :ref:`grids`.

.. ipython:: python

    import numpy as np
    import xarray as xr

    grid_ds = xr.Dataset(
        coords={
            "x_c": (
                ["x_c"],
                np.arange(1, 20),
                {"axis": "X"},
            ),
            "x_g": (
                ["x_g"],
                np.arange(0.5, 19),
                {"axis": "X", "c_grid_axis_shift": -0.5},
            ),
            "y_c": (
                ["y_c"],
                np.arange(1, 20),
                {"axis": "Y"},
            ),
            "y_g": (
                ["y_g"],
                np.arange(0.5, 19),
                {"axis": "Y", "c_grid_axis_shift": -0.5},
            ),
        }
    )
    grid_ds


.. ipython:: python

    from xgcm import Grid

    grid = Grid(
        grid_ds,
        coords={
            "X": {"center": "x_c", "left": "x_g"},
            "Y": {"center": "y_c", "left": "y_g"},
        },
        autoparse_metadata=False,
    )
    grid


Now we need some data.
We will create a 2D vector field, with components ``U`` and ``V``.
We will treat these velocities as if they lie on a vector C-grid, so as velocities they will lie at the cell faces.

.. ipython:: python

    U = np.sin(grid_ds.x_g * 2 * np.pi / 9.5) * np.cos(grid_ds.y_c * 2 * np.pi / 19)
    V = np.cos(grid_ds.x_c * 2 * np.pi / 19) * np.sin(grid_ds.y_g * 2 * np.pi / 19)

    ds = xr.Dataset({"V": V, "U": U})
    ds


Interpolation
~~~~~~~~~~~~~

It would be nice to see what our vector field looks like before we start doing calculus with it,
but the ``U`` and ``V`` velocities are defined on different points,
so plotting the vectors as arrows originating from a common point would technically be incorrect for our C-grid data.
We can fix this by interpolating the vectors onto co-located points.

.. ipython:: python

    colocated = xr.Dataset()
    colocated["U"] = grid.interp(U, axis="X", to="center")
    colocated["V"] = grid.interp(V, axis="Y", to="center")
    colocated

We can now show what this co-located vector field looks like

.. ipython:: python

    @savefig example_vector_field.png width=4in
    colocated.plot.quiver("x_c", "y_c", u="U", v="V")


Divergence
~~~~~~~~~~

Let's first import the decorator.

.. ipython:: python

    from xgcm import as_grid_ufunc


In two dimensions, the divergence operator accepts two vector components and returns one scalar result.
A divergence is the sum of multiple partial derivatives, so first let's define a derivative function like this

.. ipython:: python

    def diff_forward_1d(a):
        return a[..., 1:] - a[..., :-1]

.. ipython:: python

    def diff(arr, axis):
        """First order forward difference along any axis"""
        return np.apply_along_axis(diff_forward_1d, axis, arr)

Each vector component will be differentiated along one axis, and doing so with a first order forward difference would
shift the data's position along that axis.
Therefore our signature should look something like this ``"(X:left,Y:center),(X:center,Y:left)->(X:center,Y:center)"``.

We also need to pad the data to replace the elements that will be removed by the `diff` function, so
our grid ufunc can be defined like this

.. ipython:: python

    @as_grid_ufunc(
        "(X:left,Y:center),(X:center,Y:left)->(X:center,Y:center)",
        boundary_width={"X": (0, 1), "Y": (0, 1)},
    )
    def divergence(u, v):
        u_diff_x = diff(u, axis=-2)
        v_diff_y = diff(v, axis=-1)
        # Need to trim off elements so that the two arrays have same shape
        div = u_diff_x[..., :-1] + v_diff_y[..., :-1, :]
        return div

Here we have treated the components of the ``(U, V)`` vector as independent scalars.

Now we can compute the divergence of our example vector field

.. ipython:: python

    div = divergence(grid, ds["U"], ds["V"], axis=[("X", "Y"), ("X", "Y")])

We can see the result lies on the expected coordinate positions

.. ipython:: python

    div.coords

and the resulting divergence looks like it corresponds with the arrows of the vector field above

.. ipython:: python

    import matplotlib.pyplot as plt

    div.plot(x="x_c", y="y_c")
    colocated.plot.quiver("x_c", "y_c", u="U", v="V")

    @savefig div_vector_field.png width=4in
    plt.gcf()



Gradient
~~~~~~~~

The gradient is almost like the opposite of divergence in the sense that it accepts one scalar and returns multiple vectors.

Let's first define a tracer field ``T``, which we imagine will start off localised near the center of the domain.

.. ipython:: python

    def gaussian(x_coord, y_coord, x_pos, y_pos, A, w):
        return A * np.exp(
            -0.5 * ((x_coord - x_pos) ** 2 + (y_coord - y_pos) ** 2) / w**2
        )

.. ipython:: python

    ds["T"] = gaussian(grid_ds.x_c, grid_ds.y_c, x_pos=7.5, y_pos=7.5, A=50, w=2)

    @savefig tracer_field.png width=4in
    ds["T"].plot.contourf(x="x_c", vmax=60)

Computing the first-order gradient will again move the data onto different grid positions,
so the signature for a gradient ufunc will need to reflect this
and our definition is similar to the derivative case.

.. ipython:: python

    def gradient(a):
        a_diff_x = diff(a, axis=-2)
        a_diff_y = diff(a, axis=-1)
        # Need to trim off elements so that the two arrays have same shape
        return a_diff_x[..., :-1], a_diff_y[..., :-1, :]

Now we can compute the gradient of our example scalar field

.. ipython:: python

    ds["grad_T_x"], ds["grad_T_y"] = grid.apply_as_grid_ufunc(
        gradient,
        ds["T"],
        axis=[("X", "Y")],
        signature="(X:center,Y:center)->(X:left,Y:center),(X:center,Y:left)",
        boundary_width={"X": (1, 0), "Y": (1, 0)},
    )

.. note::

    Notice we used the ``apply_as_grid_ufunc`` syntax here instead of the ``as_grid_ufunc`` decorator.
    The result is the same.

Again in order to plot this as a vector field we should first interpolate it

.. ipython:: python

    colocated["grad_T_x"] = grid.interp(ds["grad_T_x"], axis="X", to="center")
    colocated["grad_T_y"] = grid.interp(ds["grad_T_y"], axis="Y", to="center")
    colocated

Now we can plot the gradient of the tracer field as a vector field

.. ipython:: python

    ds["T"].plot.contourf(x="x_c", vmax=60)
    colocated.plot.quiver(
        "x_c", "y_c", u="grad_T_x", v="grad_T_y", color="0.5", scale=200
    )

    @savefig gradient_scalar_field.png width=4in
    plt.gcf()


Advection
~~~~~~~~~

We can also do "mixed" operations that involve both vectors and scalars,
such as calculating the advective flux of a scalar tracer field due to a vector flow field.

Now we can define a simple flux operator (which internally calls our previous gradient function)

.. ipython:: python

    def interp_forward_1d(a):
        return (a[..., :-1] + a[..., 1:]) / 2.0

.. ipython:: python

    def interp_forward(arr, axis):
        """First order forward interpolation along any axis"""
        return np.apply_along_axis(interp_forward_1d, axis, arr)

.. ipython:: python

    @as_grid_ufunc(
        "(X:left,Y:center),(X:center,Y:left),(X:center,Y:center)->(X:left,Y:center),(X:center,Y:left)",
        boundary_width={"X": (1, 0), "Y": (1, 0)},
    )
    def flux(u, v, T):
        """First order flux"""
        T_at_U_position = interp_forward(T, axis=-2)
        T_at_V_position = interp_forward(T, axis=-1)
        T_flux_x = u[..., :-1, :-1] * T_at_U_position[..., :-1]
        T_flux_y = v[..., :-1, :-1] * T_at_V_position[..., :-1, :]
        return T_flux_x, T_flux_y

We can use this operator in conjunction with our divergence operator in order to build an advection operator,
with which we can solve the basic continuity equation

.. math::

   \frac{\partial T}{\partial t} + \nabla  \cdot ( \mathbf{u} T ) = 0


.. ipython:: python

    def advect(T, U, V, delta_t):
        """Simple solution to the continuity equation for a single timestep of length delta_t."""
        T_flux_x, T_flux_y = flux(grid, U, V, T, axis=[("X", "Y")] * 3)
        advected_T = T - delta_t * divergence(
            grid, T_flux_x, T_flux_y, axis=[("X", "Y")] * 2
        )
        return advected_T

Evaluating this function updates our tracer to what the tracer field might look like one (arbitrary-length) timestep later:

.. ipython:: python

    new_T = advect(ds["T"], ds["U"], ds["V"], delta_t=3)

    new_T.plot.contourf(x="x_c", vmin=0, vmax=60)
    colocated.plot.quiver("x_c", "y_c", u="U", v="V")

    @savefig advected_field.png width=4in
    plt.gcf()


Vorticity
~~~~~~~~~

We can compute vector fields from vector fields too, such as vorticity.

.. ipython:: python

    @as_grid_ufunc(
        "(X:left,Y:center),(X:center,Y:left)->(X:left,Y:left)",
        boundary_width={"X": (1, 0), "Y": (1, 0)},
    )
    def vorticity(u, v):
        v_diff_x = diff(v, axis=-2)
        u_diff_y = diff(u, axis=-1)
        return v_diff_x[..., 1:] - u_diff_y[..., 1:, :]


.. ipython:: python

    vort = vorticity(grid, ds["U"], ds["V"], axis=[("X", "Y"), ("X", "Y")])

.. ipython:: python

    vort.plot(x="x_g", y="y_g")
    colocated.plot.quiver("x_c", "y_c", u="U", v="V")

    @savefig vort_vector_field.png width=4in
    plt.gcf()
