Source code for at.acceptance.acceptance

"""Acceptance computation."""

from __future__ import annotations

__all__ = [
    "get_1d_acceptance",
    "get_acceptance",
    "get_horizontal_acceptance",
    "get_momentum_acceptance",
    "get_vertical_acceptance",
]

import multiprocessing
import warnings
from collections.abc import Sequence

import numpy as np

from at.lattice import AtError, AtWarning

from ..lattice import Lattice, Refpts, frequency_control
from ..tracking import MPMode, gpu_core_count

# noinspection PyProtectedMember
from .boundary import GridMode, boundary_search


[docs] @frequency_control def get_acceptance( ring: Lattice, planes, npoints, amplitudes, nturns: int = 1024, refpts: Refpts | None = None, dp: float | None = None, offset: Sequence[float] | None = None, bounds=None, grid_mode: GridMode = GridMode.RADIAL, use_mp: bool | MPMode = False, gpu_pool: list[int] | None = None, verbose: bool = False, divider: int = 2, shift_zero: float = 1.0e-6, start_method: str | None = None, **kwargs, ): # noinspection PyUnresolvedReferences r"""Compute the acceptance at ``repfts`` observation points. Parameters: ring: Lattice definition planes: max. dimension 2, Plane(s) to scan for the acceptance. Allowed values are: ``'x'``, ``'px'``, ``'y'``, ``'py'``, ``'dp'``, ``'ct'`` npoints: (len(planes),) array: number of points in each dimension amplitudes: (len(planes),) array: set the search range: * :py:attr:`GridMode.CARTESIAN/RADIAL <.GridMode.RADIAL>`: max. amplitude * :py:attr:`.GridMode.RECURSIVE`: initial step nturns: Number of turns for the tracking refpts: Observation points. Default: start of the machine dp: static momentum offset offset: initial orbit. Default: closed orbit bounds: defines the tracked range: range=bounds*amplitude. It can be used to select quadrants. For example, default values are: * :py:attr:`.GridMode.CARTESIAN`: ((-1, 1), (0, 1)) * :py:attr:`GridMode.RADIAL/RECURSIVE <.GridMode.RADIAL>`: ((0, 1), (:math:`\pi`, 0)) grid_mode: defines the evaluation grid: * :py:attr:`.GridMode.CARTESIAN`: full [:math:`\:x, y\:`] grid * :py:attr:`.GridMode.RADIAL`: full [:math:`\:r, \theta\:`] grid * :py:attr:`.GridMode.RECURSIVE`: radial recursive search * :py:attr:`.GridMode.FLOODFILL`: cartesian search from exterior region to stable boundary. Only wiht at.MPMode.CPU tracking use_mp: Flag to activate CPU or GPU multiprocessing (default: False) gpu_pool: List of GPU id to use when use_mp is :py:attr:`at.tracking.MPMode.GPU`. If None specified, if gets first GPU. verbose: Print out some information divider: Value of the divider used in :py:attr:`.GridMode.RECURSIVE` boundary search shift_zero: Epsilon offset applied on all 6 coordinates start_method: Python multiprocessing start method. The default ``None`` uses the python default that is considered safe. Available parameters: ``'fork'``, ``'spawn'``, ``'forkserver'``. The default for linux is ``'fork'``, the default for macOS and Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up the calculation or to solve runtime errors, however it is considered unsafe. pool_size: number of CPUs. Only has effect with at.GridMode.FLOODFILL Returns: boundary: (2,n) array: 2D acceptance survived: (2,n) array: Coordinates of surviving particles tracked: (2,n) array: Coordinates of tracked particles In case of multiple refpts, return values are lists of arrays, with one array per ref. point. Examples: >>> bf, sf, gf = ring.get_acceptance(planes, npoints, amplitudes) >>> plt.plot(*gf, ".") >>> plt.plot(*sf, ".") >>> plt.plot(*bf) >>> plt.show() .. note:: * When``use_mp=True`` all the available CPUs will be used. This behavior can be changed by setting ``at.DConstant.patpass_poolsize`` to the desired value * When multiple ``refpts`` are provided particles are first projected to the beginning of the ring with tracking. Then, all particles are tracked up to ``nturns``. This allows to do most of the work in a single function call and allows for full parallelization. """ if start_method is not None: kwargs["start_method"] = start_method # For backward compatibility (use_mp can be a boolean) if use_mp is True: use_mp = MPMode.CPU # For backwards compatibility px could be xp, and py could be yp deprecated_axis_name = ("xp", "yp") axes = list(planes) # set to a modifiable list for idx_, axis_name in enumerate(planes): if axis_name in deprecated_axis_name: msg = f"Axis name {axis_name} is deprecated." warnings.warn(AtWarning(msg)) axes[idx_] = axes[idx_][::-1] # reverse string planes = tuple(axes) if (grid_mode is GridMode.FLOODFILL) and (use_mp is MPMode.GPU): msg = "floodfill is not implemented for GPU tracking" raise AtError(msg) from None if grid_mode is GridMode.FLOODFILL: if "pool_size" not in kwargs: kwargs["pool_size"] = multiprocessing.cpu_count() if verbose: nproc = multiprocessing.cpu_count() print(f"\n{nproc} cpu found for acceptance calculation") if use_mp is MPMode.CPU: nprocu = nproc print("Multi-process acceptance calculation selected...") if nproc == 1: print("Consider use_mp=False for single core computations") elif use_mp is MPMode.GPU: nprocu = gpu_core_count(gpu_pool) print(f"\n{nprocu} GPU cores found") print("GPU acceptance calculation selected...") kwargs["gpu_pool"] = gpu_pool if gpu_pool is not None else [0] else: nprocu = 1 print("Single process acceptance calculation selected...") if nproc > 1: print("Consider use_mp=True for parallelized computations") npts = np.atleast_1d(npoints) na = 2 if len(npts) == 2: na = npts[1] npp = np.prod(npoints) rpp = 2 * np.ceil(np.log2(npts[0])) * np.ceil(na / nprocu) mpp = npp / nprocu if rpp > mpp: cond = grid_mode is GridMode.RADIAL or grid_mode is GridMode.CARTESIAN else: cond = grid_mode is GridMode.RECURSIVE if rpp > mpp and not cond: print(f"The estimated load for grid mode is {mpp}") print(f"The estimated load for recursive mode is {rpp}") print(f"{GridMode.RADIAL} or {GridMode.CARTESIAN} is recommended") elif rpp < mpp and not cond: print(f"The estimated load for grid mode is {mpp}") print(f"The estimated load for recursive mode is {rpp}") print(f"{GridMode.RECURSIVE} is recommended") b, s, g = boundary_search( ring, planes, npoints, amplitudes, nturns=nturns, obspt=refpts, dp=dp, offset=offset, bounds=bounds, grid_mode=grid_mode, use_mp=use_mp, verbose=verbose, divider=divider, shift_zero=shift_zero, **kwargs, ) return b, s, g
[docs] def get_1d_acceptance( ring: Lattice, plane: str, resolution: float, amplitude: float, nturns: int | None = 1024, refpts: Refpts | None = None, dp: float | None = None, offset: Sequence[float] = None, grid_mode: GridMode | None = GridMode.RADIAL, use_mp: bool | MPMode = False, gpu_pool: list[int] | None = None, verbose: bool | None = False, divider: int | None = 2, shift_zero: float | None = 1.0e-6, start_method: str | None = None, **kwargs, ): r"""Compute the 1D acceptance at ``refpts`` observation points. See :py:func:`get_acceptance` Parameters: ring: Lattice definition plane: Plane to scan for the acceptance. Allowed values are: ``'x'``, ``'px'``, ``'y'``, ``'py'``, ``'dp'``, ``'ct'`` resolution: Minimum distance between 2 grid points amplitude: Search range: * :py:attr:`GridMode.CARTESIAN/RADIAL <.GridMode.RADIAL>`: max. amplitude * :py:attr:`.GridMode.RECURSIVE`: initial step nturns: Number of turns for the tracking refpts: Observation points. Default: start of the machine dp: static momentum offset offset: initial orbit. Default: closed orbit grid_mode: defines the evaluation grid: * :py:attr:`.GridMode.CARTESIAN`: full [:math:`\:x, y\:`] grid * :py:attr:`.GridMode.RADIAL`: full [:math:`\:r, \theta\:`] grid * :py:attr:`.GridMode.RECURSIVE`: radial recursive search use_mp: Flag to activate CPU or GPU multiprocessing (default: False). In case multiprocessing is not enabled, ``grid_mode`` is forced to :py:attr:`.GridMode.RECURSIVE` (most efficient in single core) gpu_pool: List of GPU id to use when use_mp is :py:attr:`at.tracking.MPMode.GPU`. If None specified, if gets first GPU. verbose: Print out some information divider: Value of the divider used in :py:attr:`.GridMode.RECURSIVE` boundary search shift_zero: Epsilon offset applied on all 6 coordinates start_method: Python multiprocessing start method. The default ``None`` uses the python default that is considered safe. Available parameters: ``'fork'``, ``'spawn'``, ``'forkserver'``. The default for linux is ``'fork'``, the default for macOS and Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up the calculation or to solve runtime errors, however it is considered unsafe. Returns: boundary: (len(refpts),2) array: 1D acceptance survived: (n,) array: Coordinates of surviving particles tracked: (n,) array: Coordinates of tracked particles In case of multiple ``tracked`` and ``survived`` are lists of arrays, with one array per ref. point. .. note:: * When``use_mp=True`` all the available CPUs will be used. This behavior can be changed by setting ``at.DConstant.patpass_poolsize`` to the desired value * When multiple ``refpts`` are provided particles are first projected to the beginning of the ring with tracking. Then, all particles are tracked up to ``nturns``. This allows to do most of the work in a single function call and allows for full parallelization. """ if not use_mp: if verbose: print("No parallel calculation selected, force to GridMode.RECURSIVE") grid_mode = GridMode.RECURSIVE assert len(np.atleast_1d(plane)) == 1, "1D acceptance: single plane required" assert np.isscalar(resolution), "1D acceptance: scalar args required" assert np.isscalar(amplitude), "1D acceptance: scalar args required" npoint = np.ceil(amplitude / resolution) if grid_mode is not GridMode.RECURSIVE: assert ( npoint > 1 ), "Grid has only one point: increase amplitude or reduce resolution" b, s, g = get_acceptance( ring, plane, npoint, amplitude, nturns=nturns, dp=dp, refpts=refpts, grid_mode=grid_mode, use_mp=use_mp, gpu_pool=gpu_pool, verbose=verbose, start_method=start_method, divider=divider, shift_zero=shift_zero, offset=offset, **kwargs, ) return np.squeeze(b), s, g
[docs] def get_horizontal_acceptance( ring: Lattice, resolution: float, amplitude: float, *args, **kwargs ): r"""Compute the 1D horizontal acceptance at ``refpts`` observation points. See :py:func:`get_acceptance` Parameters: ring: Lattice definition resolution: Minimum distance between 2 grid points amplitude: Search range: * :py:attr:`GridMode.CARTESIAN/RADIAL <.GridMode.RADIAL>`: max. amplitude * :py:attr:`.GridMode.RECURSIVE`: initial step Keyword Args: nturns: Number of turns for the tracking refpts: Observation points. Default: start of the machine dp: static momentum offset offset: initial orbit. Default: closed orbit grid_mode: defines the evaluation grid: * :py:attr:`.GridMode.CARTESIAN`: full [:math:`\:x, y\:`] grid * :py:attr:`.GridMode.RADIAL`: full [:math:`\:r, \theta\:`] grid * :py:attr:`.GridMode.RECURSIVE`: radial recursive search use_mp: Use python multiprocessing (:py:func:`.patpass`, default use :py:func:`.lattice_pass`). In case multiprocessing is not enabled, ``grid_mode`` is forced to :py:attr:`.GridMode.RECURSIVE` (most efficient in single core) gpu_pool: List of GPU id to use when use_mp is :py:attr:`at.tracking.MPMode.GPU`. If None specified, if gets first GPU. verbose: Print out some information divider: Value of the divider used in :py:attr:`.GridMode.RECURSIVE` boundary search shift_zero: Epsilon offset applied on all 6 coordinates start_method: Python multiprocessing start method. The default ``None`` uses the python default that is considered safe. Available parameters: ``'fork'``, ``'spawn'``, ``'forkserver'``. The default for linux is ``'fork'``, the default for macOS and Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up the calculation or to solve runtime errors, however it is considered unsafe. Returns: boundary: (len(refpts),2) array: 1D acceptance survived: (n,) array: Coordinates of surviving particles tracked: (n,) array: Coordinates of tracked particles In case of multiple ``tracked`` and ``survived`` are lists of arrays, with one array per ref. point. .. note:: * When``use_mp=True`` all the available CPUs will be used. This behavior can be changed by setting ``at.DConstant.patpass_poolsize`` to the desired value * When multiple ``refpts`` are provided particles are first projected to the beginning of the ring with tracking. Then, all particles are tracked up to ``nturns``. This allows to do most of the work in a single function call and allows for full parallelization. """ return get_1d_acceptance(ring, "x", resolution, amplitude, *args, **kwargs)
[docs] def get_vertical_acceptance( ring: Lattice, resolution: float, amplitude: float, *args, **kwargs ): r"""Compute the 1D vertical acceptance at refpts observation points. See :py:func:`get_acceptance` Parameters: ring: Lattice definition resolution: Minimum distance between 2 grid points amplitude: Search range: * :py:attr:`GridMode.CARTESIAN/RADIAL <.GridMode.RADIAL>`: max. amplitude * :py:attr:`.GridMode.RECURSIVE`: initial step Keyword Args: nturns: Number of turns for the tracking refpts: Observation points. Default: start of the machine dp: static momentum offset offset: initial orbit. Default: closed orbit grid_mode: defines the evaluation grid: * :py:attr:`.GridMode.CARTESIAN`: full [:math:`\:x, y\:`] grid * :py:attr:`.GridMode.RADIAL`: full [:math:`\:r, \theta\:`] grid * :py:attr:`.GridMode.RECURSIVE`: radial recursive search use_mp: Use python multiprocessing (:py:func:`.patpass`, default use :py:func:`.lattice_pass`). In case multiprocessing is not enabled, ``grid_mode`` is forced to :py:attr:`.GridMode.RECURSIVE` (most efficient in single core) gpu_pool: List of GPU id to use when use_mp is :py:attr:`at.tracking.MPMode.GPU`. If None specified, if gets first GPU. verbose: Print out some information divider: Value of the divider used in :py:attr:`.GridMode.RECURSIVE` boundary search shift_zero: Epsilon offset applied on all 6 coordinates start_method: Python multiprocessing start method. The default ``None`` uses the python default that is considered safe. Available parameters: ``'fork'``, ``'spawn'``, ``'forkserver'``. The default for linux is ``'fork'``, the default for macOS and Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up the calculation or to solve runtime errors, however it is considered unsafe. Returns: boundary: (len(refpts),2) array: 1D acceptance survived: (n,) array: Coordinates of surviving particles tracked: (n,) array: Coordinates of tracked particles In case of multiple ``tracked`` and ``survived`` are lists of arrays, with one array per ref. point. .. note:: * When``use_mp=True`` all the available CPUs will be used. This behavior can be changed by setting ``at.DConstant.patpass_poolsize`` to the desired value * When multiple ``refpts`` are provided particles are first projected to the beginning of the ring with tracking. Then, all particles are tracked up to ``nturns``. This allows to do most of the work in a single function call and allows for full parallelization. """ return get_1d_acceptance(ring, "y", resolution, amplitude, *args, **kwargs)
[docs] def get_momentum_acceptance( ring: Lattice, resolution: float, amplitude: float, *args, **kwargs ): r"""Compute the 1D momentum acceptance at refpts observation points. See :py:func:`get_acceptance` Parameters: ring: Lattice definition resolution: Minimum distance between 2 grid points amplitude: Search range: * :py:attr:`GridMode.CARTESIAN/RADIAL <.GridMode.RADIAL>`: max. amplitude * :py:attr:`.GridMode.RECURSIVE`: initial step Keyword Args: nturns: Number of turns for the tracking refpts: Observation points. Default: start of the machine dp: static momentum offset offset: initial orbit. Default: closed orbit grid_mode: defines the evaluation grid: * :py:attr:`.GridMode.CARTESIAN`: full [:math:`\:x, y\:`] grid * :py:attr:`.GridMode.RADIAL`: full [:math:`\:r, \theta\:`] grid * :py:attr:`.GridMode.RECURSIVE`: radial recursive search use_mp: Use python multiprocessing (:py:func:`.patpass`, default use :py:func:`.lattice_pass`). In case multiprocessing is not enabled, ``grid_mode`` is forced to :py:attr:`.GridMode.RECURSIVE` (most efficient in single core) gpu_pool: List of GPU id to use when use_mp is :py:attr:`at.tracking.MPMode.GPU`. If None specified, if gets first GPU. verbose: Print out some information divider: Value of the divider used in :py:attr:`.GridMode.RECURSIVE` boundary search shift_zero: Epsilon offset applied on all 6 coordinates start_method: Python multiprocessing start method. The default ``None`` uses the python default that is considered safe. Available parameters: ``'fork'``, ``'spawn'``, ``'forkserver'``. The default for linux is ``'fork'``, the default for macOS and Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up the calculation or to solve runtime errors, however it is considered unsafe. Returns: boundary: (len(refpts),2) array: 1D acceptance survived: (n,) array: Coordinates of surviving particles tracked: (n,) array: Coordinates of tracked particles In case of multiple ``tracked`` and ``survived`` are lists of arrays, with one array per ref. point. .. note:: * When``use_mp=True`` all the available CPUs will be used. This behavior can be changed by setting ``at.DConstant.patpass_poolsize`` to the desired value * When multiple ``refpts`` are provided particles are first projected to the beginning of the ring with tracking. Then, all particles are tracked up to ``nturns``. This allows to do most of the work in a single function call and allows for full parallelization. """ return get_1d_acceptance(ring, "dp", resolution, amplitude, *args, **kwargs)
Lattice.get_acceptance = get_acceptance Lattice.get_horizontal_acceptance = get_horizontal_acceptance Lattice.get_vertical_acceptance = get_vertical_acceptance Lattice.get_momentum_acceptance = get_momentum_acceptance