Optimization
The optimization API combines a direction rule and a step size rule in
direction.py defines directions, step_size.py defines step sizes, and
solver.py combines them. SPEG is the built-in direction:
The solver checks the norm of the unnormalized gradient against tol
before taking a step. A zero gradient therefore stops the iteration before
normalization. The result records the reason for stopping; reaching a
gradient tolerance does not certify a global minimum.
SPEG
specular.specular_gradient accepts a scalar initial point or a nonempty
one-dimensional vector. The objective returns a real scalar. With scalar
input it receives a scalar. The iteration evaluates vector objectives on
NumPy arrays; differentiation samples use the selected backend's array types.
import specular
scalar_result = specular.specular_gradient(
abs,
initial_point=1.0,
step_size="square_summable_not_summable",
a=0.5,
b=1.0,
max_iter=200,
)
print("Scalar SPEG:", scalar_result.solution, scalar_result.stop_reason)
The following vector example uses a nonsmooth objective:
import numpy as np
import specular
def sum_abs(x):
return np.sum(np.abs(x))
vector_result = specular.specular_gradient(
sum_abs,
initial_point=[1.0, -2.0],
step_size="square_summable_not_summable",
a=0.5,
b=1.0,
max_iter=500,
)
print("Vector SPEG:", vector_result.solution, vector_result.func_val)
Both examples use a decreasing schedule. They illustrate numerical iteration for a fixed budget and do not assert convergence within that budget. Constant step sizes can oscillate near a minimizer.
The default h=None uses the calculation API's automatic finite-difference
step. Set h to a positive scalar to choose it explicitly.
The gradient argument can supply an exact or problem-specific
unnormalized specular gradient instead of numerical differentiation. It
must return a scalar or vector matching the initial point.
Compose rules
The factories return callables with the following interfaces:
direction(n, x)
step_size(n, x, d)
The factories bind the objective, gradient callback, and rule options
once. The solver evaluates the direction, obtains a positive step size,
and accepts x + gamma * d if the update is valid.
This smooth quadratic example supplies its analytical gradient to both SPEG and strong Wolfe search:
import numpy as np
from specular.optimization import make_direction, make_step_size, minimize
def quadratic(x):
return np.dot(x, x)
def quadratic_gradient(x):
return 2.0 * x
direction = make_direction("speg", gradient=quadratic_gradient)
step = make_step_size(
"strong_Wolfe", f=quadratic, gradient=quadratic_gradient
)
composed_result = minimize(
quadratic,
initial_point=[1.0, -2.0],
direction=direction,
step_size=step,
gradient=quadratic_gradient,
max_iter=100,
)
print("Composed SPEG:", composed_result.solution, composed_result.stop_reason)
For the same built-in rules, minimize also accepts their names directly:
result = minimize(
quadratic,
initial_point=[1.0, -2.0],
direction="speg",
step_size="strong_Wolfe",
gradient=quadratic_gradient,
line_search_gradient=quadratic_gradient,
step_options={"c_1": 1e-4, "c_2": 0.9},
)
Custom direction functions use (n, x). Custom step functions passed
directly to minimize use (n, x, d). To adapt a schedule that depends
only on n, pass it through make_step_size:
def descent(n, x):
return -2.0 * x
schedule = make_step_size(lambda n: 0.1)
custom_result = minimize(
quadratic,
initial_point=[1.0, -2.0],
direction=descent,
step_size=schedule,
gradient=quadratic_gradient,
max_iter=100,
record_history=False,
)
print("Custom rules:", custom_result.solution, custom_result.stop_reason)
The named factories also accept optional cached gradient_value, and
step size rules accept f_value. The solver can use these to avoid
repeating evaluations. Custom functions do not need these extra keywords.
Step size rules
Schedule indices begin at n=1 for the first attempted update.
| Rule | Step size or acceptance test | Options |
|---|---|---|
constant |
\(a\) | a=1 |
not_summable |
\(\frac{a}{\sqrt n}\) | a=1 |
square_summable_not_summable |
\(\frac{a}{b+n}\) | a=1, b=1 |
geometric_series |
\(ar^n\) | a=1, r=0.5 |
user_defined |
user_defined_rule(n) |
user_defined_rule callable |
| Callable schedule | schedule(n) |
Bind with make_step_size(schedule) |
| Positive number | Constant step | Bind with make_step_size(0.1) |
armijo |
Sufficient decrease | c_1=1e-4, rho=0.5 |
Wolfe |
Sufficient decrease and curvature | c_1=1e-4, c_2=0.9, rho=0.5 |
strong_Wolfe |
Sufficient decrease and strong curvature | c_1=1e-4, c_2=0.9, rho=0.5 |
exact |
Bounded numerical line minimization | tol=1e-8 |
Line searches also accept t_0=1, max_alpha=1e8, max_iter=100, and
h=None. Rule names are case-insensitive. The legacy c_3 option is an
alias for the strong Wolfe curvature constant.
Pass rule options with step_options in minimize, or as keyword
arguments to make_step_size. The SPEG convenience function also accepts
them directly, as in specular_gradient(f, x0, a=0.1). Unknown options are
rejected rather than silently ignored. The solver's max_iter
sets the number of optimization updates; step_options={"max_iter": ...}
sets a line search's own budget. For exact, that budget counts objective
evaluations, including the initial value when it is not cached.
exact is a historical rule name. Its implementation numerically searches
[0, max_alpha]; it does not promise an exact or global minimizer for an
arbitrary line objective. Choose max_alpha for the scale of the problem.
These rules require no additional dependency.
Classical and specular search derivatives
Ordinary armijo, Wolfe, and strong_Wolfe use a classical gradient
estimated by centered differences by default. This is separate from the
specular gradient that generates the SPEG direction. Set
line_search_gradient in the solver to supply an analytical classical
gradient.
The specular_armijo, specular_Wolfe, and specular_strong_Wolfe
variants use the specular gradient in their slope tests by default.
An explicit line_search_gradient overrides that choice. When constructing
a step rule directly, use make_step_size(..., gradient=...) for the
gradient used by its tests.
Armijo and Wolfe searches require a negative initial slope computed from
their raw gradient and the search direction. Bounded exact minimization
uses objective values only. Its specular_exact alias performs the same
derivative-free search.
The search derivative is always unnormalized. The normalized SPEG direction alone cannot supply the derivative information for the tests. Replacing classical derivatives with specular derivatives, or applying classical tests at nonsmooth points, does not automatically inherit the classical convergence guarantees. A search can fail if it cannot find an acceptable step within its bounds and budget.
Results and execution
OptimizationResult exposes:
| Attribute | Meaning |
|---|---|
solution |
Last accepted point |
func_val |
Objective value at that point |
iteration |
Number of accepted updates |
runtime |
Elapsed solver time in seconds |
success |
Whether the convergence criterion was met |
stop_reason |
Why the iteration stopped |
history |
NumPy arrays under variables, values, and step_sizes |
method |
Direction name, or custom for a callable direction |
The variables and values histories include the initial point and accepted
iterates, with length iteration + 1. variables has shape
(iteration + 1,) for scalar states or (iteration + 1, dimension) for
vectors. step_sizes has one entry per accepted update, so its length is
iteration. Failed searches
do not append an iterate or change the last accepted solution. Use
record_history=False to leave the history arrays empty and avoid storing
the trajectory. The final point and objective remain available in the result.
last_record() returns (solution, func_val, runtime). get_history()
returns (history["variables"], history["values"], runtime).
Stopping reasons distinguish a gradient below tolerance, an exhausted iteration budget, a failed line search, a zero direction despite a larger gradient, an update lost to floating-point precision, and non-finite proposed points or objective values. Invalid arguments and callback shape errors raise exceptions.
Backend selection
The optimizer runs an eager Python loop with NumPy state. Its default specular differentiation uses the currently selected calculation backend, then converts values back to NumPy for iteration. Selecting Numba or JAX does not compile the whole optimizer or make it a JAX-transformable solver.
Use set_backend or use_backend just as for the calculation and ODE APIs.
For example, after installing the optional Numba backend:
with specular.use_backend("numba"):
result = specular.specular_gradient(
sum_abs, [1.0, -2.0], step_size=0.1, max_iter=20
)
The objective must be compatible with the selected backend. In particular,
JAX differentiation needs a JAX-compatible objective, such as one written
with jax.numpy. Ordinary centered line-search differences run in NumPy;
specular line searches use the selected calculation backend. Explicit
gradient callbacks perform their own computation. See
backend support for installation and dtype details.
API reference
See specular.optimization for individual function and result references.