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

Minimize a SpaceCore functional with a compiled, convergence-aware optax loop.

  • 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, opt, *, max_iter=1000, tol=1e-06, project=None, verbose=1, log_every=50, history_every=None, record_history=True, progress_callback=None)[source]#

Minimize a SpaceCore functional with a compiled, convergence-aware optax loop.

The whole loop runs inside jax.jit(jax.lax.while_loop(...)). The fused F.value_and_grad is evaluated once per iteration and cached, so the stopping test grad_norm <= tol and the progress log reuse cached values with no recomputation and no per-iteration host sync. Cadence parameters (log_every / history_every) govern logging only, never convergence.

Progress columns: iteration, functional value F(x), objective delta ΔF = F_k - F_{k-1}, and the coordinate-gradient norm.

Parameters:
  • F (Functional) – Objective with an inner-product, JAX-backed domain X = F.domain and an implemented F.grad (hence F.value_and_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.

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

  • max_iter (int) – Iteration cap (non-negative).

  • tol (float) – Convergence tolerance on the coordinate-gradient L2 norm (non-negative).

  • project (callable, optional) – Optional retraction applied to the parameters after each optax update (e.g. projection onto a constraint set). Default: identity.

  • verbose (int) – 0 silent, 1 final summary, 2 per-log_every progress rows.

  • log_every (int) – Live-logging and history-sampling cadences. history_every defaults to log_every.

  • history_every (int) – Live-logging and history-sampling cadences. history_every defaults to log_every.

  • record_history (bool) – Whether to record the iteration/value/value_delta/grad_norm history.

  • progress_callback (callable, optional) – Called once per recorded row after the loop (compiled loops cannot call Python callbacks live) as progress_callback(row_dict).

Returns:

Final statistics, the minimizer x_element, the recorded history, and compile-vs-execution timing.

Return type:

OptaxResult

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

  • ValueError – If max_iter/tol are negative or log_every/history_every are non-positive.

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

See also

spacecore.optimize.minimize_scipy

SciPy minimize with the same handoff.

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)

res = sc.minimize_optax(F, X.zeros(), optax.adam(1e-1), max_iter=1000, tol=1e-6)
# res.x_element is approximately (1.0, 2.0); res.success is True