Source code for at.latticetools.matching

"""Matching of lattice parameters.

The matching acts on a :py:class:`.VariableList` to statisfy the constraints
expressed in an :py:class:`.ObservableList`.

Examples of use of such classes are available in:

* :doc:`/p/notebooks/variables`
* :doc:`/p/notebooks/observables`

Examples of matching are given in :doc:`/p/notebooks/matching`.
"""

from __future__ import annotations

__all__ = ["match", "ring_match"]

import numpy as np
from scipy.optimize import least_squares

from .observablelist import ObservableList
from ..lattice import Lattice, VariableList
from ..lattice.lattice_variables import ElementVariable


[docs] def match( variables: VariableList, constraints: ObservableList, *, method: str | None = None, verbose: int = 2, max_nfev: int = 1000, optim_kw: dict | None = None, **eval_kw, ) -> np.ndarray: r"""Observable matching. Minimisation is performed by the :py:func:`~scipy.optimize.least_squares` function. Args: variables: Variable parameters constraints: Constraints to fulfill method: Minimisation algorithm (see :py:func:`~scipy.optimize.least_squares`). If :py:obj:`None`, use 'lm' for unbounded problems, 'trf' otherwise, verbose: Level of verbosity, max_nfev: Maximum number of function evaluation, optim_kw: Dictionary of optimiser keyword arguments sent to :py:func:`~scipy.optimize.least_squares`, Keyword Args: \*\*eval_kw: Evaluation keywords provided to the :py:meth:`ObservableList.evaluate` method. For instance *"ring"* (for lattice-dependent observables), *"dp"*, *"dct"*, *"orbit"*, *"twiss_in"*, *"r_in"*… Default values are taken from the ObservableList. Returns: Matching result: solution for the variable values. Raises: RuntimeError: If optimisation fails """ def fun(vals) -> np.ndarray: """Evaluation function for the minimiser.""" variables.set(vals, **eval_kw) constraints.evaluate(**eval_kw) return constraints.get_flat_weighted_deviations(err=1.0e6) if optim_kw is None: optim_kw = {} vini = variables.get(initial=True, check_bounds=True, **eval_kw) bounds = np.array([var.bounds for var in variables]) x_scale = np.array([var.delta for var in variables]) constraints.evaluate(initial=True, **eval_kw) constraints.check() ntargets = constraints.flat_values.size if method is None: if np.any(np.isfinite(bounds)) or ntargets < len(variables): method = "trf" else: method = "lm" if verbose >= 1: print( f"\n{ntargets} constraints, {len(variables)} variables," f" using method {method}\n" ) opt = least_squares( fun, vini, bounds=(bounds[:, 0], bounds[:, 1]), x_scale=x_scale, verbose=verbose, max_nfev=max_nfev, method=method, **optim_kw, ) if verbose >= 1: print("\nConstraints:") print(constraints) print("\nVariables:") print(variables) return opt.x
[docs] def ring_match( ring: Lattice, variables: VariableList, constraints: ObservableList, *, copy: bool = False, method: str | None = None, verbose: int = 2, max_nfev: int = 1000, **kwargs, ) -> Lattice | None: r"""Match constraints by varying variables. Minimisation is performed by the :py:func:`~scipy.optimize.least_squares` function. Args: ring: Lattice description variables: Variable parameters constraints: Constraints to fulfill copy: If :py:obj:`True`, return a modified copy of *ring*, otherwise perform the match in-line method: Minimisation algorithm (see :py:func:`~scipy.optimize.least_squares`). If :py:obj:`None`, use 'lm' for unbounded problems, 'trf' otherwise, verbose: Level of verbosity, max_nfev: Maximum number of function evaluation, Keyword Args: dp (float | None): Momentum deviation. Default taken from the ObservableList, dct (float | None): Path lengthening. Default taken from the ObservableList, df (float | None): Deviation from the nominal RF frequency. Default taken from the ObservableList, orbit (Orbit | None): Initial orbit. Avoids looking for the closed orbit if it is already known. Used for :py:class:`.MatrixObservable` and :py:class:`.LocalOpticsObservable`. Default taken from the ObservableList, twiss_in: Initial conditions for transfer line optics See :py:func:`.get_optics`. Used for :py:class:`.LocalOpticsObservable`. Default taken from the ObservableList, r_in (Orbit | None): Initial trajectory, used for :py:class:`.TrajectoryObservable`. Default taken from the ObservableList, \*\*kwargs: The other keyword arguments sent to, :py:func:`~scipy.optimize.least_squares`. Returns: Modified Lattice if copy=True, None otherwise Raises: TypeError: If copy=True and ElementVariable is present RuntimeError: If optimisation fails Examples: See :doc:`/p/notebooks/matching` """ # Separate the keywords for evaluation eval_kw = {} for key in ["dp", "dct", "df", "orbit", "twiss_in", "r_in"]: v = kwargs.pop(key, None) if v is not None: eval_kw[key] = v if copy: # Check that there is no ElementVariable for var in variables: if isinstance(var, ElementVariable): msg = "When 'copy' is True, no ElementVariable is accepted." raise TypeError(msg) newring = ring.deepcopy() match( variables, constraints, ring=newring, method=method, verbose=verbose, max_nfev=max_nfev, optim_kw=kwargs, **eval_kw, ) return newring else: match( variables, constraints, ring=ring, method=method, verbose=verbose, max_nfev=max_nfev, optim_kw=kwargs, **eval_kw, ) return None
Lattice.match = ring_match