Skip to content
Specular Differentiation

specular.optimization.make_direction

make_direction(method='speg', *, f=None, gradient=None, h=None)

Create a SPEG or custom direction callable.

Parameters

Parameter Description
method="speg" "speg", its alias "specular_gradient", or a custom callable accepting (n, x).
f=None Scalar-valued objective for numerical specular differentiation. Required for a named method unless gradient is supplied.
gradient=None Raw specular gradient callback gradient(x). Takes precedence over numerical differentiation of f.
h=None Concrete, finite, positive scalar differentiation interval. If omitted, the selected backend chooses intervals automatically.

Returns

A callable with the interface:

direction(n, x, *, gradient_value=None)

It returns a finite real scalar or a one-dimensional NumPy array with the same shape as x. The iteration index n must be an integer at least one.

Notes

SPEG returns the negative unit vector of the raw specular gradient, or zero when that gradient is exactly zero. gradient_value can reuse a previously computed raw gradient. This cached value must match the shape of x.

A custom method(n, x) is used directly, without normalization, and ignores gradient_value. Callbacks receive private copies of vector points. Both points and returned directions must be finite scalars or nonempty one-dimensional vectors with exactly matching shapes.

Numerical differentiation uses the currently selected calculation backend. An explicit gradient callback performs its own computation.

Examples

import numpy as np

from specular.optimization import make_direction

direction = make_direction("speg", gradient=lambda x: 2.0 * x)
print(direction(1, np.array([3.0, 4.0])))
# [-0.6 -0.8]

custom_direction = make_direction(lambda n, x: -2.0 * x)
print(custom_direction(1, 3.0))
# -6.0
Source code in specular/optimization/direction.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def make_direction(
    method: str | Callable[[int, Point], Any] = "speg",
    *,
    f: Callable[[Point], Any] | None = None,
    gradient: Callable[[Point], Any] | None = None,
    h: Any = None,
) -> Direction:
    r"""Create ``direction(n, x, *, gradient_value=None)``.

    ``"speg"`` (also ``"specular_gradient"``) returns the negative unit
    specular gradient, or zero when the gradient is exactly zero. Supply a
    raw ``gradient(x)`` callback or an objective ``f`` for numerical specular
    differentiation using the currently selected backend. ``h=None`` keeps
    that backend's automatic differentiation interval.

    A callable ``method(n, x)`` supplies a custom direction without additional
    normalization. Iterations are numbered from one. All returned directions
    must be finite and match the scalar or vector shape of ``x`` exactly.
    Callbacks receive a private copy of vector points.

    ``gradient_value`` optionally reuses a previously evaluated raw gradient
    for SPEG. Custom methods do not use this cache.
    """
    if not isinstance(method, str) and not callable(method):
        raise TypeError("method must be a direction name or a callable")
    if isinstance(method, str) and method not in {"speg", "specular_gradient"}:
        raise ValueError(f"unknown direction method: {method!r}")
    if f is not None and not callable(f):
        raise TypeError("f must be callable")
    if gradient is not None and not callable(gradient):
        raise TypeError("gradient must be callable")
    validated_h = None if h is None else calculation._positive_step(h)
    if isinstance(method, str) and f is None and gradient is None:
        raise ValueError("SPEG requires f or a raw gradient callback")

    def direction(n: int, x: Any, *, gradient_value: Any = None) -> Point:
        if isinstance(n, (bool, np.bool_)):
            raise TypeError("n must be an integer")
        try:
            iteration = operator.index(n)
        except TypeError as exc:
            raise TypeError("n must be an integer") from exc
        if iteration < 1:
            raise ValueError("n must be at least one")
        point = _as_point(x)
        shape = np.shape(point)
        if callable(method):
            value = _as_point(method(iteration, point), name="direction")
        else:
            if gradient_value is not None:
                raw = gradient_value
            elif gradient is not None:
                raw = gradient(point)
            elif shape == ():
                raw = calculation.derivative(f, point, h=validated_h)
            else:
                raw = calculation.gradient(f, point, h=validated_h)
            value = _as_point(raw, name="gradient")
        if np.shape(value) != shape:
            name = "direction" if callable(method) else "gradient"
            raise ValueError(f"{name} must have exactly the same shape as x")
        return value if callable(method) else _negative_unit(value)

    return direction