==========================
Compact Finite Differences
==========================

.. versionadded:: 0.13

Compact (or implicit) finite differences achieve spectral-like resolution with
small stencils by coupling derivative approximations at neighboring grid points.
The classical reference is Lele (1992).


Background
----------

Standard ("explicit") finite differences express a derivative as a weighted sum
of function values:

.. math::
   f'_i = \sum_k c_k \, f_{i+k}

Compact finite differences generalize this by including derivative values on the
left-hand side:

.. math::
   \sum_k \alpha_k \, f'_{i+k} = \sum_k c_k \, f_{i+k}

The classic tridiagonal case uses :math:`\alpha_{-1} = \alpha_1 = 1/3` and five
function values. Despite needing only nearest neighbors, this scheme reaches
6th-order accuracy — the same as a 7-point explicit stencil.

The price is that computing the derivative requires solving a banded linear
system. For a tridiagonal left-hand side this costs :math:`O(N)` and is fast in
practice.


Usage
-----

There are two ways to set up a compact scheme.

**Explicit scheme definition** — pick the left-hand side coefficients
(:math:`\alpha`) and right-hand side offsets, and *findiff* solves for the
matching :math:`c_k`:

.. code:: python

    import numpy as np
    from findiff import Diff, CompactScheme

    x = np.linspace(0, 2 * np.pi, 100, endpoint=False)
    dx = x[1] - x[0]

    # Lele (1992) tridiagonal scheme, Section 2.1.1
    scheme = CompactScheme(
        deriv=1,
        left={-1: 1/3, 0: 1, 1: 1/3},
        right=[-3, -2, -1, 0, 1, 2, 3],
    )

    d_dx = Diff(0, dx, scheme=scheme, periodic=True)

    f = np.sin(x)
    df = d_dx(f)            # 6th-order accurate

**Shortcut** — let *findiff* choose a scheme that meets a target accuracy:

.. code:: python

    d_dx = Diff(0, dx, compact=3, acc=6, periodic=True)

The ``compact`` parameter sets the number of left-hand side points (must be an
odd integer). *findiff* will widen the right-hand side stencil until the
requested accuracy is reached.


Higher derivatives
------------------

Use the ``**`` operator:

.. code:: python

    d2_dx2 = d_dx ** 2

This automatically constructs a new compact scheme for the second derivative
with matching accuracy.


Non-periodic boundaries
-----------------------

For non-periodic grids the interior stencil extends past the boundary at the
first and last few grid points. *findiff* handles this with one-sided compact
stencils (Visbal & Gaitonde, 2002): the left-hand side alphas and right-hand
side offsets are restricted to points that exist, and the system is re-solved for
the boundary rows.

.. code:: python

    x = np.linspace(0, 1, 100)
    dx = x[1] - x[0]

    d_dx = Diff(0, dx, scheme=scheme, periodic=False)
    df = d_dx(np.sin(x))


Multi-dimensional grids
-----------------------

Compact differentiation extends to N-dimensional arrays via Kronecker products:

.. code:: python

    x = y = np.linspace(0, 2 * np.pi, 100, endpoint=False)
    dx = x[1] - x[0]
    X, Y = np.meshgrid(x, y, indexing='ij')

    d_dx = Diff(0, dx, compact=3, acc=6, periodic=True)
    d_dy = Diff(1, dx, compact=3, acc=6, periodic=True)

    f = np.sin(X) * np.cos(Y)
    laplacian = (d_dx**2 + d_dy**2)(f)

You can freely mix compact and standard finite difference operators in the same
expression.


Matrix representation
---------------------

The matrix form of a compact operator is :math:`L^{-1} R`. Obtain it with
the usual ``matrix()`` call:

.. code:: python

    M = d_dx.matrix((100,))
    df = M.dot(f)

Note that inverting the banded :math:`L` introduces fill-in, so the matrix is
denser than for explicit finite differences.


Limitations
-----------

- Only equidistant grids are supported. Passing a non-uniform grid raises
  ``NotImplementedError``.
- The ``stencil()`` method is not supported for compact schemes.


References
----------

- S. K. Lele, "Compact Finite Difference Schemes with Spectral-like Resolution",
  *J. Comp. Phys.* **103**, 16-42 (1992).
- M. R. Visbal and D. V. Gaitonde, "On the Use of Higher-Order Finite-Difference
  Schemes on Curvilinear and Deformed Meshes", *J. Comp. Phys.* **181**, 155-185
  (2002).
