Source code for simultipac.particle_monitor.vector

"""Define simple classes to lighten :class:`.Particle`."""

import logging
from collections.abc import Iterable, Sequence

import numpy as np
from numpy.typing import NDArray

from simultipac.particle_monitor.converters import (
    adim_momentum_to_eV,
    adim_momentum_to_speed_mm_per_ns,
)


[docs] class Vector: """Hold a vector with three coordinates."""
[docs] def __init__( self, x: Sequence[float] | None = None, y: Sequence[float] | None = None, z: Sequence[float] | None = None, is_extrapolated: bool = False, ) -> None: """Create empty lists.""" self.x: list[float] = list(x) if x is not None else [] self.y: list[float] = list(y) if y is not None else [] self.z: list[float] = list(z) if z is not None else [] self._array: NDArray[np.float64] | None = None self._is_extrapolated = is_extrapolated self._extrapolated: Vector if not is_extrapolated: self._extrapolated = Vector(is_extrapolated=True) self._is_reordered = False self._is_normalized = False
[docs] def append(self, coords: Sequence[float]) -> None: """Append new coordinates. Reset ``array``.""" if self._is_normalized or self._is_reordered: logging.warning( "Normal behavior is following: load all data first, modify it " "(sorting, normalization) afterwards." ) self.x.append(coords[0]) self.y.append(coords[1]) self.z.append(coords[2]) if self._array is not None: self._array = None
[docs] def reorder(self, ordered_time_idx: NDArray[np.int64]) -> None: """Sort coordinates by increasing time values.""" self.x = [self.x[i] for i in ordered_time_idx] self.y = [self.y[i] for i in ordered_time_idx] self.z = [self.z[i] for i in ordered_time_idx] if self._array is not None: self._array = None
[docs] def extrapolate(self, *args, **kwargs) -> None: """Extrapolate vector one time step further.""" if self._array is not None: self._array = None self._is_extrapolated = True
[docs] def normalize(self) -> None: """Normalize to proper units.""" if self._array is not None: self._array = None self._is_normalized = True
@property def array(self) -> NDArray[np.float64]: """2D array, of shape (N, 3) where N is number of time steps. .. note:: ``array[10, 0]``: x coordinate at 11th time step ``array[0, 10]``: NO """ if self._array is None: self._array = np.column_stack([self.x, self.y, self.z]) return self._array @property def to_list(self) -> list[NDArray[np.float64]]: """List of positions, each of size 3.""" return list(self.array) @property def last(self) -> NDArray[np.float64]: """1D array containing last coordinates.""" return self.array[-1, :] @property def first(self) -> NDArray[np.float64]: """1D array containing first coordinates.""" return self.array[0, :] @property def n_steps(self) -> int: """Return number of stored time steps.""" return len(self.x) @property def extrapolated(self) -> NDArray[np.float64]: """Shortcut to ``self._extrapolated.array``.""" return self._extrapolated.array
[docs] class Momentum(Vector): """Specialized class for momentum."""
[docs] def __init__( self, x: Sequence[float] | None = None, y: Sequence[float] | None = None, z: Sequence[float] | None = None, is_extrapolated: bool = False, ) -> None: super().__init__(x, y, z, is_extrapolated)
[docs] def extrapolate( self, known_times: NDArray[np.float64], desired_times: NDArray[np.float64], poly_fit_deg: int, n_points: int = 3, ) -> None: """Extrapolate the momentum. Parameters ---------- known_times : NDArray 1D array containing x-data used for extrapolation. desired_times : NDArray 1D array containing time momentum should be extrapolated on. Should not start at 0. poly_fit_deg : int Degree of the polynomial fit. n_points : int Number of time steps to extrapolate on. """ polynom = np.polyfit( known_times[-n_points:], self.array[-n_points:], poly_fit_deg ) polynom = np.flip(polynom, axis=0) n_time_subdivisions = desired_times.shape[0] for i in range(n_time_subdivisions): new = [0.0, 0.0, 0.0] for deg in range(poly_fit_deg + 1): for j in range(3): new[j] += polynom[deg, j] * desired_times[i] ** deg self._extrapolated.append(new) return super().extrapolate()
[docs] def emission_energy(self, mass_eV: float) -> float: """Get first energy in eV.""" return adim_momentum_to_eV(self.first, mass_eV)
[docs] def collision_energy(self, mass_eV: float) -> float: """Get last energy in eV.""" return adim_momentum_to_eV(self.last, mass_eV)
[docs] class Position(Vector): """Specialized class for position."""
[docs] def extrapolate( self, momentum: NDArray[np.float64] | Momentum, delta_t: Iterable[float] | NDArray[np.float64], ) -> None: """Extrapolate the position using the last known momentum. This is a first-order approximation. We consider that the momentum is constant over ``desired_time``. Not adapted to extrapolation on long time spans. .. todo:: Check is position is normalized or not. Parameters ---------- momentum : NDArray | Momentum 1D array containing last known momentum, adimensionned. You can also directly provide the :class:`Momentum` instance. delta_t : Iterable[float] | NDArray[np.float64] 1D array containing time at which position should be extrapolated. Should look like ``[delta, 2*delta, 3*delta]``. """ if isinstance(momentum, Momentum): momentum = momentum.last last_speed = adim_momentum_to_speed_mm_per_ns(momentum, force_2d=False) for time in delta_t: new_pos = [self.last[i] + last_speed[i] * time for i in range(3)] self._extrapolated.append(new_pos) return super().extrapolate()
[docs] def normalize(self) -> None: """Change units to :unit:`mm`.""" self.x = [x * 1e3 for x in self.x] self.y = [y * 1e3 for y in self.y] self.z = [z * 1e3 for z in self.z] return super().normalize()