Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework derivative discretizations #57

Merged
merged 26 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9ff0d24
feat: add default side to derivatives
alexfikl Jul 14, 2024
6aef8fd
feat: define new diff interface
alexfikl Jul 14, 2024
7e96547
feat(diff): port Caputo L1 to new diff interface
alexfikl Jul 14, 2024
c4dfa64
feat: add example for constructing matrix
alexfikl Jul 14, 2024
bef7801
feat(grid): add helper to create a midpoint grid
alexfikl Jul 14, 2024
c263236
feat(diff): adapt ModifiedL1 to new interface
alexfikl Jul 14, 2024
b0396b7
feat(diff): adapt L2 to new interface
alexfikl Jul 18, 2024
8b214d2
docs: improve docs for new diff functionality
alexfikl Aug 7, 2024
29cf7ff
feat: add fallbacks for diff functions
alexfikl Aug 7, 2024
8b775c5
feat: more work on the L2 method
alexfikl Aug 7, 2024
07d1a71
fix: typing imports
alexfikl Aug 7, 2024
7bd1e5f
feat(diff): move fallbacks
alexfikl Aug 8, 2024
62bf5f0
docs: update diff docs
alexfikl Aug 8, 2024
5bccc77
feat: add L1D method that takes a callable only
alexfikl Aug 8, 2024
e778dfa
feat: finish up L2 reimplementation
alexfikl Aug 8, 2024
ea94b7f
feat: finish up the L2 method
alexfikl Aug 9, 2024
2909845
feat(caputo): add some methods for testing
alexfikl Aug 11, 2024
338bcfe
docs: fix some typos
alexfikl Aug 12, 2024
5543d8d
docs: add changelog
alexfikl Aug 12, 2024
1329bc8
feat(diff): rename SpectralJacobi to Jacobi
alexfikl Aug 12, 2024
a41ea6e
feat(diff): expand RL methods to match Caputo
alexfikl Aug 12, 2024
a5a5c51
test: check diff interface consistency
alexfikl Aug 12, 2024
a37460c
feat: speed up caputo derivative evaluation
alexfikl Aug 12, 2024
dc8899e
test: fix differint comparison for L2 method
alexfikl Aug 12, 2024
a8a973f
chore: bump dependencies
alexfikl Aug 12, 2024
fc8d407
chore: allow hatch direct references for differint
alexfikl Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
Changelog
=========

pycaputo (TBD)
--------------

Features
^^^^^^^^

* Add a :mod:`pycaputo.typing` module containing some helpful typing definitions
(mostly previously in :mod:`pycaputo.utils`).
* Reworked :func:`~functools.singledispatch` functions for
:class:`~pycaputo.differentiation.DerivativeMethod`. We now have
:func:`~pycaputo.differentiation.quadrature_weights`,
:func:`~pycaputo.differentiation.differentiation_matrix`,
:func:`~pycaputo.differentiation.diffs`, and
:func:`~pycaputo.differentiation.diff`. Not all of these need to be implemented
and most methods are not ported yet. Currently only the
:class:`~pycaputo.differentiation.caputo.L1` and
:class:`~pycaputo.differentiation.caputo.L2` methods implement these new functions.
* Introduce some more specific methods for the Caputo derivative. The
:class:`~pycaputo.differentiation.caputo.L2F` uses the L2 method with function
evaluations outside of the interval of definition. The
:class:`~pycaputo.differentiation.caputo.LXD` allows evaluating arbitrary
Caputo derivatives when the integer derivatives are known.

Changes
^^^^^^^

* Renamed `pycaputo.differentiation.caputo.SpectralJacobi` to
:class:`~pycaputo.differentiation.caputo.Jacobi`.

pycaputo 0.7.0 (July 13, 2024)
------------------------------

Expand Down
41 changes: 35 additions & 6 deletions docs/guide_differentiation.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
A New Differentiation Method
============================

Computing the derivative of a function can be done in two ways. The recommended
method (with included magic) is calling :func:`pycaputo.diff` as
Computing the fractional derivative of a function can be done in two ways. The
recommended method (with included magic) is calling :func:`pycaputo.diff` as

.. code:: python

df = diff(f, p, alpha)

which will automatically select an appropriate method to use given the point set
``p`` and the order ``alpha`` (see also
:func:`pycaputo.differentiation.guess_method_for_order`). To manually call a
specific method, use :func:`pycaputo.differentiation.diff` instead as
:func:`~pycaputo.differentiation.guess_method_for_order`). To manually call a
specific method, use the lower level :func:`pycaputo.differentiation.diff` instead as

.. code:: python

Expand Down Expand Up @@ -40,8 +40,37 @@ abstract methods of the base class. For example,
:start-after: [class-definition-start]
:end-before: [class-definition-end]

Then, we can implement the :func:`~pycaputo.differentiation.diff` method by
registering it with the :func:`~functools.singledispatch` mechanism as
Then, we have defined a set of functions useful for working with a discrete
fractional operator approximations. As mentioned above, the main functionality
is provided by the :func:`~pycaputo.differentiation.diff` method. However, we
also have

* :func:`~pycaputo.differentiation.quadrature_weights`: a function that can provide
the underlying quadrature weights for a given method. Note that not all methods
can be efficiently expressed as a weighted sum with quadrature weights, so they
should not implement this method. However, for those that can, this offers a
pleasant way of reusing and analyzing the weights.

* :func:`~pycaputo.differentiation.differentiation_matrix`: a function that
gathers all the quadrature weights into a matrix operator. By default, this
function is implemented in terms of ``quadrature_weights``, but can be
overwritten by more efficient implementations.

* :func:`~pycaputo.differentiation.diffs`: a function that computes the
fractional operator at a given point on a grid. Note that not all methods can
efficiently evaluate the fractional operator at a single point (e.g. global
spectral methods), so they should not implement this functionality.

* :func:`~pycaputo.differentiation.diff`: a function that computes the fractional
operator at a set of points. By default this is implemented in terms of the
``diffs`` function, but this doesn't always make sense. For example, if we want
to compute the Caputo fractional derivative on a uniform grid, this can be
evaluated more efficiently using the FFT.

In general, we require that the :func:`~pycaputo.differentiation.diff` function be
implemented for all available methods, while the remaining functions are optional.
These methods are all registered with the :func:`~functools.singledispatch` mechanism.
An example setup is provided below.

.. literalinclude:: ../examples/guide-differentiation.py
:language: python
Expand Down
4 changes: 2 additions & 2 deletions docs/misc_others.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ Logging
Typing
------

.. automodule:: pycaputo.typing

.. class:: pycaputo.utils.P

A generic invarint :class:`typing.ParamSpec`.

alias of ParamSpec('P')

.. automodule:: pycaputo.typing

Utils
-----

Expand Down
85 changes: 85 additions & 0 deletions examples/caputo-derivative-l1-spectrum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# SPDX-FileCopyrightText: 2023 Alexandru Fikl <alexfikl@gmail.com>
# SPDX-License-Identifier: MIT

r"""Computes the matrix operator for the Caputo derivative and look at its
spectrum for various values of :math:`\alpha`.

The spectrum of the matrix is quite boring, since the matrix itself is lower
triangular, so all the eigenvalues are on the diagonal. From the method, we know
that

.. math::

W_{n, n} = \frac{1}{\Gamma(2 - \alpha)} \frac{1}{\Delta x^\alpha}.

i.e. the diagonal is constant. This mostly serves as an example of how the
matrices can be constructed.
"""

from __future__ import annotations

import numpy as np

from pycaputo.differentiation import quadrature_weights
from pycaputo.differentiation.caputo import L1
from pycaputo.grid import make_uniform_points
from pycaputo.logging import get_logger
from pycaputo.typing import Array

logger = get_logger("caputo_derivative_l1_spectrum")


# {{{ matrix


def get_l1_matrix(alpha: float, *, n: int, a: float, b: float) -> Array:
W = np.zeros((n, n))

meth = L1(alpha=alpha)
points = make_uniform_points(n, a=0, b=1)

for i in range(1, n):
W[i, : i + 1] = quadrature_weights(meth, points, i + 1)
W[0, 0] = W[1, 1]

return W


# }}}


# {{{ conditioning

n = 256
alphas = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95]

for alpha in alphas:
W = get_l1_matrix(alpha, n=n, a=0.0, b=1.0)
kappa = np.linalg.cond(W)
logger.info("alpha = %.2f kappa = %.8e", alpha, kappa)

# }}}

# {{{ plot

try:
import matplotlib # noqa: F401
except ImportError as exc:
raise SystemExit(0) from exc

from pycaputo.utils import figure, set_recommended_matplotlib

set_recommended_matplotlib()

with figure("caputo-derivative-l1-spectrum") as fig:
ax = fig.gca()

for alpha in alphas:
W = get_l1_matrix(alpha, n=n, a=0.0, b=1.0)
sigma = np.linalg.eigvals(W)
ax.plot(sigma.real, sigma.imag, "o")

ax.set_xlabel(r"$\Re$")
ax.set_ylabel(r"$\Im$")

# }}}
4 changes: 2 additions & 2 deletions examples/caputo-derivative-quadratic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import numpy as np

from pycaputo.differentiation import diff
from pycaputo.differentiation.caputo import SpectralJacobi
from pycaputo.differentiation.caputo import Jacobi
from pycaputo.grid import make_jacobi_gauss_lobatto_points
from pycaputo.typing import Array

Expand Down Expand Up @@ -58,7 +58,7 @@ def f(x: Array) -> Array:
p = make_jacobi_gauss_lobatto_points(32, a=0.0, b=2.0)

for alpha in alphas:
method = SpectralJacobi(alpha=alpha)
method = Jacobi(alpha=alpha)

df_num = diff(method, f, p)
ax.plot(p.x, df_num, color="k", alpha=0.2)
Expand Down
2 changes: 1 addition & 1 deletion examples/caputo-gradient-spectral.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def df(x: Array, alpha: float) -> Array:
from pycaputo.differentiation import caputo

alpha = 0.9
method = caputo.SpectralJacobi(alpha=0.9)
method = caputo.Jacobi(alpha=0.9)

# make a set of points at which to evaluate the gradient
rng = np.random.default_rng(42)
Expand Down
39 changes: 37 additions & 2 deletions examples/guide-differentiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from pycaputo.derivatives import RiemannLiouvilleDerivative, Side
from pycaputo.grid import Points
from pycaputo.typing import Array, ArrayOrScalarFunction
from pycaputo.typing import Array, ArrayOrScalarFunction, Scalar

# {{{

Expand Down Expand Up @@ -42,7 +42,42 @@ def d(self) -> RiemannLiouvilleDerivative:
# {{{

# [register-start]
from pycaputo.differentiation import diff
from pycaputo.differentiation import (
diff,
differentiation_matrix,
diffs,
quadrature_weights,
)


@quadrature_weights.register(RiemannLiouvilleDerivativeMethod)
def _quadrature_weights_rl(
m: RiemannLiouvilleDerivativeMethod,
p: Points,
n: int,
) -> Array:
# ... add an actual implementation here ...
return np.zeros(n, dtype=p.x.dtype)


@differentiation_matrix.register(RiemannLiouvilleDerivativeMethod)
def _differentiation_matrix_rl(
m: RiemannLiouvilleDerivativeMethod,
p: Points,
) -> Array:
# ... add an actual implementation here ...
return np.zeros((p.size, p.size), dtype=p.x.dtype)


@diffs.register(RiemannLiouvilleDerivativeMethod)
def _diffs_rl(
m: RiemannLiouvilleDerivativeMethod,
f: ArrayOrScalarFunction,
p: Points,
n: int,
) -> Scalar:
# ... add an actual implementation here ...
return np.array(0.0)


@diff.register(RiemannLiouvilleDerivativeMethod)
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ dependencies = [
"typing-extensions; python_version<'3.10'",
]
optional-dependencies.dev = [
"differint",
"differint @ git+https://github.com/alexfikl/differint.git@fix-l2-index#egg=differint",
"doc8",
"matplotlib",
"mypy",
Expand All @@ -69,6 +69,9 @@ optional-dependencies.vis = [
urls.Documentation = "https://pycaputo.readthedocs.io"
urls.Repository = "https://github.com/alexfikl/pycaputo"

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build.targets.sdist]
exclude = [
".github",
Expand Down
6 changes: 3 additions & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contourpy==1.2.1
# via matplotlib
cycler==0.12.1
# via matplotlib
differint==1.0.0
differint @ git+https://github.com/alexfikl/differint.git@ab981f6bcd9435a46d85470a1ab01cbcf536837d#egg=differint
# via pycaputo (pyproject.toml)
doc8==1.1.1
# via pycaputo (pyproject.toml)
Expand Down Expand Up @@ -183,9 +183,9 @@ typos==1.23.6
# via pycaputo (pyproject.toml)
urllib3==2.2.2
# via requests
uv==0.2.34
uv==0.2.35
# via pycaputo (pyproject.toml)
zipp==3.19.2 ; python_version < '3.10'
zipp==3.20.0 ; python_version < '3.10'
# via
# importlib-metadata
# importlib-resources
18 changes: 8 additions & 10 deletions src/pycaputo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@ def diff(
:arg d: a fractional operator. If this is just a number, the standard
Caputo derivative will be used.
"""
from pycaputo.differentiation import diff as _diff
from pycaputo.differentiation import guess_method_for_order
import pycaputo.differentiation as fracd

if d is None:
raise ValueError("'d' is required if 'method is not given")

if not isinstance(d, FractionalOperator):
d = CaputoDerivative(d, side=Side.Left)

m = guess_method_for_order(p, d)
m = fracd.guess_method_for_order(p, d)

return _diff(m, f, p)
return fracd.diff(m, f, p)


def quad(
Expand All @@ -56,18 +55,17 @@ def quad(
:arg d: a fractional operator. If this is just a number, the standard
Riemann-Liouville integral will be used.
"""
from pycaputo.quadrature import guess_method_for_order
from pycaputo.quadrature import quad as _quad
import pycaputo.quadrature as fracq

if d is None:
raise ValueError("'d' is required if 'method' is not given")

if not isinstance(d, FractionalOperator):
d = RiemannLiouvilleDerivative(d, side=Side.Left)

m = guess_method_for_order(p, d)
m = fracq.guess_method_for_order(p, d)

return _quad(m, f, p)
return fracq.quad(m, f, p)


def grad(
Expand Down Expand Up @@ -118,12 +116,12 @@ def f_i(y: Array) -> Array:
def make_component_p(i: tuple[int, ...]) -> Points:
return p.translate(a[i], x[i])

from pycaputo.differentiation import diff as _diff
import pycaputo.differentiation as fracd

result = np.empty_like(x)
for i in np.ndindex(x.shape):
# FIXME: this should just compute the gradient at -1
result[i] = _diff(m, make_component_f(i), make_component_p(i))[-1]
result[i] = fracd.diff(m, make_component_f(i), make_component_p(i))[-1]

return result

Expand Down
Loading