External Optimizer Adapters API#

Thin adapter functions that drive a mature external optimizer (SciPy, optax) from a SpaceCore Functional. They evaluate the objective through F.value and convert the metric (Riesz) gradient F.grad to the coordinate gradient an external optimizer expects with X.riesz – the identity on a Euclidean space and mandatory on a weighted one. The external optimizer owns the loop, line search, and convergence; the adapter only translates the objective and its geometry. This adapter surface is specified by ADR-018.

SciPy#

spacecore.minimize_scipy

Minimize a SpaceCore functional with scipy.optimize.minimize().

spacecore.line_search_scipy

Wolfe line search along d with scipy.optimize.line_search().

  • minimize_scipy drives scipy.optimize.minimize(), flattening elements to and from SciPy’s coordinate vector and supplying X.riesz(F.grad(x)) as the Jacobian. Returns the SciPy OptimizeResult with an added x_element field (the minimizer unflattened into F.domain).

  • line_search_scipy drives scipy.optimize.line_search() along a search direction with the same gradient handoff.

optax#

spacecore.minimize_optax

Run an optax optimizer on a SpaceCore functional with pytree pass-through.

  • minimize_optax runs the canonical optax update loop with pytree pass-through (no flatten/unflatten); the coordinate gradient is the same X.riesz(F.grad(x)) handoff. Requires a JAX-backed domain and the optional optax dependency (pip install spacecore[optax]).

Information lost at the external boundary#

  • Structure. SciPy sees a flat coordinate vector; bounds and constraints are expressed in flattened coordinates.

  • Geometry. The external optimizer works in the flat Euclidean coordinate metric; the domain geometry survives only through the X.riesz gradient conversion.

  • Field. The SciPy adapters require a real domain and reject complex spaces.

  • Tracing. Jitted external solves may require a context built with check_level="none".

Autodoc#

spacecore.minimize_scipy(F, x0, *, method='L-BFGS-B', jac=True, **kw)[source]#

Minimize a SpaceCore functional with scipy.optimize.minimize().

The objective is evaluated through F.value and the Jacobian through X.riesz(F.grad(x)) – the metric-to-coordinate gradient handoff that is the identity on a Euclidean space and mandatory on a weighted one. SpaceCore elements are flattened to and from SciPy’s flat coordinate array with F.domain.flatten/unflatten.

Parameters:
  • F (Functional) – Objective with a real, inner-product domain X = F.domain. F.grad must be implemented unless a gradient-free jac is selected.

  • x0 (array-like) – Initial guess, an element of F.domain.

  • method (str, optional) – SciPy minimize method. Default "L-BFGS-B".

  • jac (bool, str, callable, or None, optional) – Gradient policy. True (default) supplies SpaceCore’s X.riesz(F.grad(x)) coordinate gradient. False or None lets SciPy approximate the gradient by finite differences. A real finite-difference string ("2-point" or "3-point") or a callable over the flat coordinate vector is forwarded to SciPy unchanged. The complex-step mode "cs" is rejected because it evaluates the objective at complex perturbations the real domain cannot represent.

  • **kw – Forwarded to scipy.optimize.minimize() (e.g. bounds, constraints, tol, options). bounds and constraints are interpreted in flattened coordinates. The SciPy args parameter is not supported (a Functional takes only its domain element).

Returns:

The SciPy result, unchanged, with one added field x_element: the minimizer result.x unflattened back into an element of F.domain. result.x stays the flat coordinate array, by SciPy convention.

Return type:

scipy.optimize.OptimizeResult

Raises:
  • TypeError – If F is not a Functional or its domain has no inner-product geometry.

  • ValueError – If F.domain is over the complex field, if jac="cs", or if the unsupported SciPy args parameter is passed.

See also

line_search_scipy

Wolfe line search along a direction with the same handoff.

spacecore.optimize.minimize_optax

Optax loop with pytree pass-through.

Notes

The external optimizer owns iteration, line search, and convergence; this adapter only translates the objective and its geometry. The minimizer location of a smooth problem does not depend on the metric (a metric gradient and the coordinate gradient vanish together), but SciPy’s steps do: feeding SciPy the raw metric gradient F.grad on a weighted space is inconsistent with fun and degrades or breaks convergence. X.riesz removes that trap.

Examples

Minimize f(x) = 1/2 x^T diag(3, 1) x - 3 x_0 - 2 x_1 on a Euclidean space; the minimizer is (1, 2).

>>> import numpy as np
>>> import spacecore as sc
>>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
>>> X = sc.DenseCoordinateSpace((2,), ctx)
>>> Q = sc.DenseLinOp(ctx.asarray([[3.0, 0.0], [0.0, 1.0]]), X, X, ctx)
>>> linear = sc.InnerProductFunctional(ctx.asarray([-3.0, -2.0]), X)
>>> F = sc.LinOpQuadraticForm(Q, linear)
>>> result = sc.minimize_scipy(F, X.zeros(), options={"gtol": 1e-10})
>>> bool(result.success)
True
>>> np.allclose(np.asarray(result.x_element), [1.0, 2.0])
True
spacecore.line_search_scipy(F, x, d, **kw)[source]#

Wolfe line search along d with scipy.optimize.line_search().

Evaluates F.value and the coordinate gradient X.riesz(F.grad(x)) over the flat coordinate representation, so SciPy’s slope coordinate_gradient . d is the correct directional derivative df(d) = <F.grad(x), d>_X even on a weighted space.

Parameters:
  • F (Functional) – Objective with a real, inner-product domain X = F.domain.

  • x (array-like) – Current iterate, an element of F.domain.

  • d (array-like) – Search direction, an element of F.domain interpreted as a coordinate displacement (the new iterate is x + alpha * d). The adapter does not transform d: both -F.grad(x) (natural/metric steepest descent) and -X.riesz(F.grad(x)) (coordinate steepest descent) are valid descent directions.

  • **kw – Forwarded to scipy.optimize.line_search() (e.g. gfk, old_fval, c1, c2, amax). The SciPy args parameter is not supported (a Functional takes only its domain element).

Returns:

The SciPy line_search tuple (alpha, fc, gc, new_fval, old_fval, new_slope). alpha is the step length and is None when the line search fails to satisfy the Wolfe conditions.

Return type:

tuple

Raises:
  • TypeError – If F is not a Functional or its domain has no inner-product geometry.

  • ValueError – If F.domain is over the complex field, or if the unsupported SciPy args parameter is passed.

Examples

A descent step on a convex quadratic returns a positive step length.

>>> import numpy as np
>>> import spacecore as sc
>>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64)
>>> X = sc.DenseCoordinateSpace((2,), ctx)
>>> Q = sc.DenseLinOp(ctx.asarray([[3.0, 0.0], [0.0, 1.0]]), X, X, ctx)
>>> F = sc.LinOpQuadraticForm(Q)
>>> x = ctx.asarray([1.0, 1.0])
>>> d = X.scale(-1.0, F.grad(x))
>>> alpha = sc.line_search_scipy(F, x, d)[0]
>>> bool(alpha > 0.0)
True
spacecore.minimize_optax(F, x0, optimizer, *, steps, callback=None)[source]#

Run an optax optimizer on a SpaceCore functional with pytree pass-through.

Executes the canonical optax update loop for steps iterations:

grad = X.riesz(F.grad(params))            # metric -> coordinate gradient
updates, opt_state = optimizer.update(grad, opt_state, params)
params = optax.apply_updates(params, updates)

The optax optimizer owns the update rule (learning rate, momentum, preconditioning); this adapter only supplies the coordinate gradient and runs the fixed-length loop. There is no convergence test – optax is a gradient-transformation library, so iteration count is the caller’s contract.

Parameters:
  • F (Functional) – Objective with an inner-product domain X = F.domain and an implemented F.grad.

  • x0 (pytree) – Initial parameters, an element of F.domain. A raw array or tuple/list/dict of arrays is used as-is; a bound TreeElement is normalized to its raw pytree.

  • optimizer (optax.GradientTransformation) – An optax optimizer such as optax.adam(1e-2).

  • steps (int) – Number of update steps to run. Must be non-negative.

  • callback (callable, optional) – Called as callback(step, params) after each update, e.g. to record a loss trajectory.

Returns:

The final parameters, an element of F.domain.

Return type:

pytree

Raises:
  • TypeError – If F is not a Functional, its domain has no inner-product geometry, or its domain is not JAX-backed (optax produces JAX arrays).

  • ValueError – If steps is negative.

  • ImportError – If optax is not installed (pip install spacecore[optax]).

See also

spacecore.optimize.minimize_scipy

SciPy minimize with the same handoff.

Notes

On a Euclidean space X.riesz is the identity. On a weighted space it multiplies the metric gradient by the diagonal weights, which is exactly the coordinate gradient optax’s Euclidean update expects (ADR-018).

Examples

import numpy as np
import optax
import spacecore as sc

ctx = sc.Context(sc.JaxOps(), dtype=np.float32, check_level="none")
X = sc.DenseCoordinateSpace((2,), ctx)
Q = sc.DenseLinOp(ctx.asarray([[3.0, 0.0], [0.0, 1.0]]), X, X, ctx)
linear = sc.InnerProductFunctional(ctx.asarray([-3.0, -2.0]), X)
F = sc.LinOpQuadraticForm(Q, linear)

x_star = sc.minimize_optax(F, X.zeros(), optax.adam(1e-1), steps=500)
# x_star is approximately (1.0, 2.0)