from __future__ import annotations
import difflib
from typing import List, Union
import numpy as np
from pylbo.data_containers import LegolasDataSet
from pylbo.exceptions import BackgroundNotPresent
from pylbo.utilities.logger import pylboLogger
from pylbo.visualisation.utils import ef_name_to_latex, validate_ef_name
[docs]class ModeVisualisationData:
"""
Class that contains the data used for eigenmode visualisations.
Parameters
----------
ds : ~pylbo.data_containers.LegolasDataSet
The dataset containing the eigenfunctions and modes to visualise.
omega : list[complex]
The (approximate) eigenvalue(s) of the mode(s) to visualise.
ef_name : str
The name of the eigenfunction to visualise.
use_real_part : bool
Whether to use the real part of the eigenmode solution.
complex_factor : complex
A complex factor to multiply the eigenmode solution with.
add_background : bool
Whether to add the equilibrium background to the eigenmode solution.
Attributes
----------
ds : ~pylbo.data_containers.LegolasDataSet
The dataset containing the eigenfunctions and modes to visualise.
omega : list[complex]
The (approximate) eigenvalue(s) of the mode(s) to visualise.
eigenfunction : list[np.ndarray]
The eigenfunction of the mode(s) to visualise.
use_real_part : bool
Whether to use the real part of the eigenmode solution.
complex_factor : complex
The complex factor to multiply the eigenmode solution with.
add_background : bool
Whether to add the equilibrium background to the eigenmode solution.
"""
def __init__(
self,
ds: LegolasDataSet,
omega: list[complex],
ef_name: str = None,
use_real_part: bool = True,
complex_factor: complex = None,
add_background: bool = False,
) -> None:
self.ds = ds
self.use_real_part = use_real_part
self.complex_factor = self._validate_complex_factor(complex_factor)
if add_background and not ds.has_background:
raise BackgroundNotPresent(ds.datfile, "add background to solution")
self.add_background = add_background
self._print_bg_info = True
self._ef_name = None if ef_name is None else validate_ef_name(ds, ef_name)
self._ef_name_latex = None if ef_name is None else self.get_ef_name_latex()
self._all_efs = self._get_all_efs(ds, omega)
self.omega = [all_efs.get("eigenvalue") for all_efs in self._all_efs]
self.eigenfunction = [all_efs.get(self._ef_name) for all_efs in self._all_efs]
@property
[docs] def k2(self) -> float:
"""The k2 wave number of the eigenmode solution."""
return self.ds.parameters["k2"]
@property
[docs] def k3(self) -> float:
"""The k3 wave number of the eigenmode solution."""
return self.ds.parameters["k3"]
@property
[docs] def part_name(self) -> str:
"""
Returns the name of the part of the eigenmode solution to use, i.e.
'real' or 'imag'.
"""
return "real" if self.use_real_part else "imag"
[docs] def _get_all_efs(self, ds: LegolasDataSet, omega: List[complex]) -> np.ndarray:
"""
Returns an array of dicts with all eigenfunctions for every eigenvalue.
The dictionaries will be updated with the derived eigenfunctions if they
are available in the dataset.
Parameters
----------
ds : ~pylbo.data_containers.LegolasDataSet
The dataset containing the eigenfunctions.
omega : list[complex]
The (approximate) eigenvalue(s) of the mode(s) to retrieve the
eigenfunctions from.
Returns
-------
np.ndarray
An array of dicts with all eigenfunctions for every eigenvalue.
"""
arr1 = ds.get_eigenfunctions(omega)
if not ds.has_derived_efs:
return arr1
arr2 = ds.get_derived_eigenfunctions(omega)
arr = np.empty(len(omega), dtype=dict)
for i, (dict1, dict2) in enumerate(zip(arr1, arr2)):
ev1 = dict1.get("eigenvalue")
ev2 = dict2.get("eigenvalue")
if not np.isclose(ev1, ev2, atol=1e-12):
pylboLogger.warning(
f"The eigenvalue of the eigenfunction {ev1:.6e} and the derived "
f"eigenfunction {ev2:.6e} do not match. Using eigenfunctions only."
)
return arr1
arr[i] = {**dict1, **dict2}
return arr
[docs] def get_ef_name_latex(self) -> str:
"""Returns the latex representation of the eigenfunction name."""
return ef_name_to_latex(
self._ef_name, geometry=self.ds.geometry, real_part=self.use_real_part
)
[docs] def _validate_complex_factor(self, complex_factor: complex) -> complex:
"""
Validates the complex factor.
Parameters
----------
complex_factor : complex
The complex factor to validate.
Returns
-------
complex
The complex factor if it is valid, otherwise 1.
"""
return complex_factor if complex_factor is not None else 1
[docs] def get_mode_solution(
self,
ef: np.ndarray,
omega: complex,
u2: Union[float, np.ndarray],
u3: Union[float, np.ndarray],
t: Union[float, np.ndarray],
) -> np.ndarray:
"""
Calculates the full eigenmode solution for given coordinates and time.
If a complex factor was given, the eigenmode solution is multiplied with the
complex factor. If :attr:`use_real_part` is True the real part of the eigenmode
solution is returned, otherwise the complex part.
Parameters
----------
ef : np.ndarray
The eigenfunction to use.
omega : complex
The eigenvalue to use.
u2 : Union[float, np.ndarray]
The y coordinate(s) of the eigenmode solution.
u3 : Union[float, np.ndarray]
The z coordinate(s) of the eigenmode solution.
t : Union[float, np.ndarray]
The time(s) of the eigenmode solution.
Returns
-------
np.ndarray
The real or imaginary part of the eigenmode solution for the given
set of coordinate(s) and time(s).
"""
solution = (
self.complex_factor
* ef
* np.exp(1j * self.k2 * u2 + 1j * self.k3 * u3 - 1j * omega * t)
)
return getattr(solution, self.part_name)
[docs] def get_background(self, shape: tuple[int, ...], name=None) -> np.ndarray:
"""
Returns the background of the eigenmode solution.
Parameters
----------
shape : tuple[int, ...]
The shape of the eigenmode solution.
name : str
The name of the background to use. If None, the background name
will be inferred from the eigenfunction name.
Returns
-------
np.ndarray
The background of the eigenmode solution, sampled on the eigenfunction
grid and broadcasted to the same shape as the eigenmode solution.
"""
if name is None:
name = self._get_background_name()
bg = self.ds.equilibria[name]
bg_sampled = self._sample_background_on_ef_grid(bg)
if self._print_bg_info:
pylboLogger.info(f"background {name} broadcasted to shape {shape}")
return np.broadcast_to(bg_sampled, shape=reversed(shape)).transpose()
[docs] def _sample_background_on_ef_grid(self, bg: np.ndarray) -> np.ndarray:
"""
Samples the background array on the eigenfunction grid.
Parameters
----------
bg : np.ndarray
The background array with Gaussian grid spacing
Returns
-------
np.ndarray
The background array with eigenfunction grid spacing
"""
if self._print_bg_info:
pylboLogger.info(
f"sampling background [{len(bg)}] on eigenfunction grid "
f"[{len(self.ds.ef_grid)}]"
)
return np.interp(self.ds.ef_grid, self.ds.grid_gauss, bg)
[docs] def _get_background_name(self) -> str:
"""
Returns the name of the background.
Returns
-------
str
The closest match between the eigenfunction name and the equilibrium
name.
Raises
------
ValueError
If the eigenfunction name is a magnetic vector potential component.
"""
if self._ef_name in ("a1", "a2", "a3"):
raise ValueError(
"Unable to add a background to the magnetic vector potential."
)
(name,) = difflib.get_close_matches(self._ef_name, self.ds.eq_names, 1)
if self._print_bg_info:
pylboLogger.info(
f"adding background for '{self._ef_name}', closest match is '{name}'"
)
return name