Source code for differt2d.optimize

"""
Optimization toolbox.

All the functions present in this toolbox support
Just-in-time compilation with :func:`jax.jit`.

Examples
--------
>>> from differt2d.optimize import minimize
>>> import chex
>>> import jax
>>> import jax.numpy as jnp
>>> @jax.jit
... def parabola_min(a, b, c):
...     def f(x):
...         x = a * (x + b) + c
...         return jnp.dot(x, x)
...
...     return minimize(f, jnp.array(0.0))
>>>
>>> x, y = parabola_min(2.0, 1.0, 1.0)
>>> chex.assert_trees_all_close(x, -1.5, rtol=1e-2)
>>> chex.assert_trees_all_close(y, +0.0, atol=1e-3)

"""

import sys
from typing import Any, Callable, Optional

import equinox as eqx
import jax
import jax.numpy as jnp
import optax
from jaxtyping import Array, Float, PRNGKeyArray, jaxtyped

if sys.version_info >= (3, 11):
    from typing import TypeVarTuple, Unpack
else:  # pragma: no cover
    from typing_extensions import TypeVarTuple, Unpack

Ts = TypeVarTuple("Ts")


[docs] @eqx.filter_jit @jaxtyped(typechecker=None) def minimize( fun: Callable[[Float[Array, " n"], *Ts], Float[Array, " "]], x0: Float[Array, " n"], args: tuple[Unpack[Ts]] = (), steps: int = 100, optimizer: Optional[optax.GradientTransformation] = None, ) -> tuple[Float[Array, " n"], Float[Array, " "]]: """ Minimizes a scalar function of one or more variables. :param fun: The objective function to be minimized. :param x0: The initial guess. :param args: Positional arguments passed to ``fun``. :param steps: The number of steps to perform. :param optimizer: The optimizer to use. If not provided, uses :func:`optax.adam` with a learning rate of ``0.1``. :return: The solution array and the corresponding loss. :Examples: >>> from differt2d.optimize import minimize >>> import chex >>> import jax.numpy as jnp >>> def f(x, offset=1.0): ... x = x - offset ... return jnp.dot(x, x) >>> >>> x, y = minimize(f, jnp.zeros(10)) >>> chex.assert_trees_all_close(x, jnp.ones(10), rtol=1e-2) >>> chex.assert_trees_all_close(y, 0.0, atol=1e-4) >>> >>> # It is also possible to pass positional arguments >>> x, y = minimize(f, jnp.zeros(10), args=(2.0,)) >>> chex.assert_trees_all_close(x, 2.0 * jnp.ones(10), rtol=1e-2) >>> chex.assert_trees_all_close(y, 0.0, atol=1e-3) """ optimizer = optimizer or optax.adam(learning_rate=0.1) f_and_df = jax.value_and_grad(fun) opt_state = optimizer.init(x0) def f(carry, _): x, opt_state = carry loss, grads = f_and_df(x, *args) updates, opt_state = optimizer.update(grads, opt_state) x = x + updates carry = (x, opt_state) return carry, loss (x, _), losses = jax.lax.scan(f, init=(x0, opt_state), xs=None, length=steps) return x, losses[-1]
[docs] @eqx.filter_jit @jaxtyped(typechecker=None) def minimize_random_uniform( fun: Callable[[Float[Array, " {n}"], *Ts], Float[Array, " "]], key: PRNGKeyArray, n: int, **kwargs: Any, ) -> tuple[Float[Array, " {n}"], Float[Array, " "]]: """ Minimizes a scalar function of one or more variables, with initial guess drawn randomly from a uniform distribution. :param fun: The objective function to be minimized. :param key: The random key used to generate the initial guess. :param n: The size of the random vector to generate. :param kwargs: Keyword arguments passed to :func:`minimize`. :return: The solution array and the corresponding loss. :Examples: >>> from differt2d.optimize import minimize_random_uniform >>> import chex >>> import jax >>> import jax.numpy as jnp >>> def f(x): ... x = x - 1.0 ... return jnp.dot(x, x) >>> >>> x, y = minimize_random_uniform(f, jax.random.PRNGKey(1234), 10) >>> chex.assert_trees_all_close(x, jnp.ones(10), rtol=1e-2) >>> chex.assert_trees_all_close(y, 0.0, atol=1e-3) """ x0 = jax.random.uniform(key, shape=(n,)) return minimize(fun, x0, **kwargs)
[docs] @eqx.filter_jit @jaxtyped(typechecker=None) def minimize_many_random_uniform( fun: Callable[[Float[Array, " {n}"], *Ts], Float[Array, " "]], key: PRNGKeyArray, n: int, many: int = 10, **kwargs: Any, ) -> tuple[Float[Array, " {n}"], Float[Array, " "]]: """ Minimizes many times a scalar function of one or more variables, with initial guess drawn randomly from a uniform distribution, and returns the best minimum out of the :code:`many` trials. :param fun: The objective function to be minimized. :param key: The random key used to generate the initial guesses. :param n: The size of the random vector to generate. :param many: How many times the minimization should be performed. :param kwargs: Keyword arguments passed to :func:`minimize_random_uniform`. :return: The solution array and the corresponding loss. :Examples: >>> from differt2d.optimize import minimize_many_random_uniform >>> import chex >>> import jax >>> import jax.numpy as jnp >>> def f(x): ... x = x - 1.0 ... return jnp.dot(x, x) >>> >>> x, y = minimize_many_random_uniform(f, jax.random.PRNGKey(1234), 10) >>> chex.assert_trees_all_close(x, jnp.ones(10), rtol=1e-2) >>> chex.assert_trees_all_close(y, 0.0, atol=1e-4) """ if many == 1: return minimize_random_uniform(fun, key, n, **kwargs) keys = jax.random.split(key, num=many) xs, losses = jax.vmap(lambda key: minimize_random_uniform(fun, key, n, **kwargs))( keys ) i_min = jnp.argmin(losses) return xs[i_min, :], losses[i_min]