from __future__ import annotations
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Callable, Union
import numpy as np
from pylbo._version import VersionHandler
from pylbo.exceptions import (
BackgroundNotPresent,
EigenfunctionsNotPresent,
EigenvectorsNotPresent,
MatricesNotPresent,
ResidualsNotPresent,
)
from pylbo.utilities.datfiles.file_reader import LegolasFileReader
from pylbo.utilities.logger import pylboLogger
from pylbo.utilities.toolbox import get_values, transform_to_numpy
from pylbo.visualisation.continua import calculate_continua
[docs]def ensure_dataset(data: any) -> None:
"""Ensures that the given data is a :class:`LegolasDataSet`."""
if not isinstance(data, LegolasDataSet):
raise TypeError(f"expected a LegolasDataSet, got {type(data)}")
[docs]def ensure_dataseries(data: any) -> None:
"""Ensures that the given data is a :class:`LegolasDataSeries`."""
if not isinstance(data, LegolasDataSeries):
raise TypeError(f"expected a LegolasDataSeries, got {type(data)}")
[docs]class LegolasDataContainer(ABC):
"""
Baseclass for a Legolas data container.
"""
@abstractmethod
[docs] def continua(self):
pass
@abstractmethod
[docs] def parameters(self):
pass
@abstractmethod
[docs] def has_efs(self):
pass
@abstractmethod
[docs] def ef_grid(self):
pass
@abstractmethod
[docs] def ef_names(self):
pass
@abstractmethod
[docs] def has_background(self):
pass
@abstractmethod
[docs] def has_derived_efs(self):
pass
@abstractmethod
[docs] def derived_ef_names(self):
pass
@abstractmethod
[docs] def has_ef_subset(self):
pass
@abstractmethod
[docs] def has_matrices(self):
pass
@abstractmethod
[docs] def has_eigenvectors(self):
pass
@abstractmethod
[docs] def has_residuals(self):
pass
@abstractmethod
[docs] def get_sound_speed(self, which_values=None):
pass
@abstractmethod
[docs] def get_alfven_speed(self, which_values=None):
pass
@abstractmethod
[docs] def get_tube_speed(self, which_values=None):
pass
@abstractmethod
[docs] def get_reynolds_nb(self, which_values=None):
pass
@abstractmethod
[docs] def get_magnetic_reynolds_nb(self, which_values=None):
pass
@abstractmethod
[docs] def get_k0_squared(self):
pass
[docs]class LegolasDataSet(LegolasDataContainer):
"""
Main container for a single Legolas dataset.
Parameters
----------
datfile : str, ~os.PathLike
Path to the datfile.
Attributes
----------
header : dict
The datfile header.
grid : numpy.ndarray
The base grid.
grid_gauss : numpy.ndarray
The Gaussian grid.
equilibria : dict
Dictionary containing the equilibrium arrays.
eigenvalues : numpy.ndarray
Array containing the complex eigenvalues.
geometry : str
The geometry.
scale_factor : numpy.ndarray
Array with the scale factor. One for Cartesian geometries, r for cylindrical.
x_start : float
Start of the grid.
x_end : float
End of the grid
gridpoints : int
The number of base gridpoints.
gauss_gridpoints : int
The number of Gaussian gridpoints.
matrix_gridpoints : int
The dimension of the matrix.
ef_gridpoints : int
The number of eigenfunction gridpoints.
gamma : float
The ratio of specific heats.
eq_type : str
The type of equilibrium selected.
cgs : bool
If `True`, all units are in cgs.
units : dict
Dictionary containing the unit normalisations.
eq_names : numpy.ndarray
Array containing the names of the equilibrium arrays.
"""
def __init__(self, datfile):
self.datfile = Path(datfile)
self.filereader = LegolasFileReader(self.datfile)
self.header = self.filereader.get_header()
self.grid = self.filereader.read_grid(self.header)
self.grid_gauss = self.filereader.read_gaussian_grid(self.header)
self.equilibria = self.filereader.read_equilibrium_arrays(self.header)
self.eigenvalues = self.filereader.read_eigenvalues(self.header)
self.geometry = self.header["geometry"]
if self.geometry == "Cartesian":
self.scale_factor = np.ones_like(self.grid_gauss)
self.d_scale_factor = np.zeros_like(self.grid_gauss)
pylboLogger.debug("dataset: scale factor set to unity.")
else:
self.scale_factor = self.grid_gauss
self.d_scale_factor = np.ones_like(self.grid_gauss)
pylboLogger.debug("dataset: scale factor set to radial coordinate.")
self.x_start = self.header["x_start"]
self.x_end = self.header["x_end"]
self.gridpoints = self.header["gridpoints"]
self.gauss_gridpoints = self.header["gauss_gridpoints"]
self.ef_gridpoints = self.header["ef_gridpoints"]
self.gamma = self.header["gamma"]
self.eq_type = self.header["eq_type"]
self._parameters = self.header["parameters"]
self.units = self.header["units"]
self.cgs = self.units["cgs"]
self.eq_names = self.header["equilibrium_names"]
self._ensure_compatibility()
self._continua = calculate_continua(self)
def __iter__(self):
yield self
@property
[docs] def legolas_version(self) -> VersionHandler:
return self.filereader.legolas_version
@property
[docs] def k2_str(self) -> str:
"""Returns the :math:`k_2` string."""
return "$k_y$" if self.geometry.lower() == "cartesian" else "$m$"
@property
[docs] def k3_str(self) -> str:
"""Returns the :math:`k_3` string."""
return "$k_z$" if self.geometry.lower() == "cartesian" else "$k$"
@property
[docs] def u1_str(self) -> str:
"""Returns the :math:`u_1` string."""
return "x" if self.geometry.lower() == "cartesian" else "r"
@property
[docs] def u2_str(self) -> str:
"""Returns the :math:`u_2` string."""
return "y" if self.geometry.lower() == "cartesian" else r"$\theta$"
@property
[docs] def u3_str(self) -> str:
"""Returns the :math:`u_3` string."""
return "z"
@property
[docs] def continua(self) -> dict:
"""Returns the continua in a dict with the continua names as keys."""
return self._continua
@property
[docs] def parameters(self) -> dict:
"""Returns the parameters in a dict with the parameter names as keys"""
return self._parameters
@property
[docs] def has_background(self) -> bool:
"""Returns `True` if background is present."""
return self.header["has_background"]
@property
[docs] def has_efs(self) -> bool:
"""Returns `True` if eigenfunctions are present."""
return self.header["has_efs"]
@property
[docs] def ef_grid(self) -> np.ndarray:
"""Returns the eigenfunction grid, None if eigenfunctions are not present."""
if not self.has_efs:
return None
if getattr(self, "_ef_grid", None) is None:
self._ef_grid = self.filereader.read_ef_grid(self.header)
return self._ef_grid
@property
[docs] def ef_names(self) -> np.ndarray:
"""Retrieves the eigenfunction names, None if eigenfunctions are not present."""
return self.header.get("ef_names", None)
@property
[docs] def has_derived_efs(self) -> bool:
"""Returns `True` if derived eigenfunctions are present."""
return self.header["has_derived_efs"]
@property
[docs] def derived_ef_names(self) -> np.ndarray:
"""Retrieves the derived eigenfunction names, None if not present."""
return self.header.get("derived_ef_names", None)
@property
[docs] def has_ef_subset(self) -> bool:
"""Returns `True` if the dataset contains a subset of the eigenfunctions."""
return self.header["ef_subset_used"]
@property
[docs] def has_matrices(self) -> bool:
"""Checks if matrices are present."""
return self.header["has_matrices"]
@property
[docs] def has_eigenvectors(self) -> bool:
"""Checks if eigenvectors are present."""
return self.header["has_eigenvectors"]
@property
[docs] def has_residuals(self) -> bool:
"""Checks if residuals are present."""
return self.header["has_residuals"]
@property
[docs] def is_mhd(self) -> bool:
"""Checks if the dataset is MHD."""
return "mhd" in self.header.get("physics_type", None) and any(
self.equilibria["B0"] != 0
)
[docs] def _ensure_compatibility(self) -> None:
"""
Makes sure that the dataset is backwards compatible with new changes.
Mainly used for older (<2.0.0) datasets.
"""
if not self.has_background:
return
bg_keys_added_in_v200 = ["L0"]
for key in bg_keys_added_in_v200:
if self.equilibria.get(key, None) is None:
pylboLogger.debug(f"added '{key}' to equilibrium of '{self.datfile}'")
self.equilibria[key] = np.zeros_like(self.grid_gauss)
[docs] def get_sound_speed(self, which_values=None) -> Union[float, np.ndarray]:
"""
Calculates the sound speed based on the equilibrium arrays,
given by :math:`c_s = \\sqrt{\\frac{\\gamma p_0}{\\rho_0}}`.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
float or numpy.ndarray
Array with the sound speed at every grid point, or a float corresponding
to the value of `which_values` if provided.
"""
if not self.has_background:
raise BackgroundNotPresent(self.datfile, "get sound speed")
pressure = self.equilibria["T0"] * self.equilibria["rho0"]
cs = np.sqrt(self.gamma * pressure / self.equilibria["rho0"])
return get_values(cs, which_values)
[docs] def get_alfven_speed(self, which_values=None) -> Union[float, np.ndarray]:
"""
Calculates the Alfvén speed based on the equilibrium arrays,
given by :math:`c_A = \\sqrt{\\frac{B_0^2}{\\rho_0}}`.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
float or numpy.ndarray
Array with the Alfvén speed at every grid point,
or a float corresponding to the value of `which_values` if provided.
"""
if not self.has_background:
raise BackgroundNotPresent(self.datfile, "get Alfvén speed")
cA = self.equilibria["B0"] / np.sqrt(self.equilibria["rho0"])
return get_values(cA, which_values)
[docs] def get_tube_speed(self, which_values=None) -> Union[float, np.ndarray]:
"""
Calculates the tube speed for a cylinder, given by
:math:`c_t = \\frac{c_s c_A}{\\sqrt{c_s^2 + c_A^2}}`.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
float or numpy.ndarray
Array with the tube speed at every grid point,
or a float corresponding to the value of `which_values` if provided.
Returns `None` if the geometry is not cylindrical.
"""
if not self.has_background:
raise BackgroundNotPresent(self.datfile, "get tube speed")
if not self.geometry == "cylindrical":
pylboLogger.warning(
"geometry is not cylindrical, unable to calculate tube speed"
)
return None
cA = self.get_alfven_speed()
cs = self.get_sound_speed()
ct = cs * cA / np.sqrt(cs**2 + cA**2)
return get_values(ct, which_values)
[docs] def get_reynolds_nb(self, which_values=None) -> Union[float, np.ndarray]:
"""
Calculates the Reynolds number, defined as
:math:`R_e = \\frac{ac_s}{\\eta}` where the slabsize is given by
:math:`a = x_{end} - x_{start}`.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
float or numpy.ndarray
Array with the Reynolds number at every grid point,
or a float corresponding to the value of `which_values` if provided.
Returns `None` if the resistivity is zero somewhere on the domain.
"""
if not self.has_background:
raise BackgroundNotPresent(self.datfile, "get Reynolds number")
cs = self.get_sound_speed()
a = self.x_end - self.x_start
eta = self.equilibria["eta"]
if (eta == 0).any():
pylboLogger.warning(
"resistivity is zero somewhere on the domain, unable to "
"calculate the Reynolds number"
)
return None
Re = a * cs / eta
return get_values(Re, which_values)
[docs] def get_magnetic_reynolds_nb(self, which_values=None) -> Union[float, np.ndarray]:
"""
Calculates the magnetic Reynolds number, defined as
:math:`R_m = \\frac{ac_A}{\\eta}` where the slabsize is given by
:math:`a = x_{end} - x_{start}`.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
float or numpy.ndarray
Array with the magnetic Reynolds number at every grid point,
or a float corresponding to the value of `which_values` if provided.
Returns `None` if the resistivity is zero somewhere on the domain.
"""
if not self.has_background:
raise BackgroundNotPresent(self.datfile, "get magnetic Reynolds number")
cA = self.get_alfven_speed()
a = self.x_end - self.x_start
eta = self.equilibria["eta"]
if (eta == 0).any():
pylboLogger.warning(
"resistivity is zero somewhere on the domain, unable to "
"calculate the magnetic Reynolds number"
)
return None
Rm = a * cA / eta
return get_values(Rm, which_values)
[docs] def get_k0_squared(self) -> float:
"""
Calculates the squared wave number, defined as
:math:`k_0^2 = k_2^2 + k_3^2`.
"""
return self.parameters.get("k2") ** 2 + self.parameters.get("k3") ** 2
[docs] def get_matrix_B(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Retrieves the matrix B from the datfile.
Returns
-------
Tuple(rows: numpy.ndarray, cols: numpy.ndarray, vals: numpy.ndarray)
Tuple containing the rows, columns and values of the non-zero B-matrix
elements. Rows and columns are integers, values are real.
Raises
------
MatricesNotPresent
If the matrices were not saved to the datfile.
"""
if not self.has_matrices:
raise MatricesNotPresent(self.datfile)
return self.filereader.read_matrix_B(self.header)
[docs] def get_matrix_A(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Retrieves the matrix A from the datfile.
Returns
-------
Tuple(rows: numpy.ndarray, cols: numpy.ndarray, vals: numpy.ndarray)
Tuple containing the rows, columns and values of the non-zero A-matrix
elements. Rows and columns are integers, values are complex.
Raises
------
MatricesNotPresent
If the matrices were not saved to the datfile.
"""
if not self.has_matrices:
raise MatricesNotPresent(self.datfile)
return self.filereader.read_matrix_A(self.header)
[docs] def get_eigenvectors(self) -> np.ndarray:
"""
Retrieves the eigenvectors from the datfile.
Returns
-------
numpy.ndarray
Array containing the eigenvectors. One eigenvector
in each column.
Raises
------
EigenvectorsNotPresent
If the eigenvectors were not saved to the datfile.
"""
if not self.has_eigenvectors:
raise EigenvectorsNotPresent(self.datfile)
return self.filereader.read_eigenvectors(self.header)
[docs] def get_residuals(self) -> np.ndarray:
"""
Retrieves the residuals from the datfile.
Returns
-------
numpy.ndarray
Array containing the residuals.
Raises
------
ResidualsNotPresent
If the residuals were not saved to the datfile.
"""
if not self.has_residuals:
raise ResidualsNotPresent(self.datfile)
return self.filereader.read_residuals(self.header)
[docs] def _get_eigenfunction_like(
self, ev_guesses: np.ndarray, ev_idxs: np.ndarray, getter_func: Callable
) -> np.ndarray:
"""
Returns the eigenfunctions based on the supplied getter function.
Parameters
----------
ev_guesses : complex, numpy.ndarray
Eigenvalue guesses.
ev_idxs : int, numpy.ndarray
Indices of the eigenvalues to retrieve.
getter_func : function
Function to retrieve the eigenfunctions.
Returns
-------
numpy.ndarray
Array containing the eigenfunctions, items are dictionaries.
"""
if ev_guesses is not None and ev_idxs is not None:
raise ValueError(
"get_eigenfunctions: either provide guesses or indices but not both"
)
if ev_guesses is not None:
idxs, _ = self.get_nearest_eigenvalues(ev_guesses)
else:
idxs = transform_to_numpy(ev_idxs)
eigenfunctions = np.array([{}] * len(idxs), dtype=dict)
for i, ef_idx in enumerate(idxs):
efs = getter_func(self.header, ef_idx)
if efs is not None:
efs["eigenvalue"] = self.eigenvalues[ef_idx]
eigenfunctions[i] = efs
return eigenfunctions
[docs] def get_eigenfunctions(self, ev_guesses=None, ev_idxs=None) -> np.ndarray:
"""
Returns the eigenfunctions based on given eigenvalue guesses or their
indices. An array will be returned where every item is a dictionary, containing
both the eigenvalue and its eigenfunctions. Either eigenvalue guesses or
indices can be supplied, but not both.
Parameters
----------
ev_guesses : complex, numpy.ndarray
Eigenvalue guesses.
ev_idxs : int, numpy.ndarray
Indices corresponding to the eigenvalues that need to be retrieved.
Returns
-------
numpy.ndarray
Array containing the eigenfunctions and eigenvalues corresponding to the
supplied indices. Every index in this array contains a dictionary with the
eigenfunctions and corresponding eigenvalue.
The keys of each dictionary are the eigenfunction names.
"""
if not self.has_efs:
raise EigenfunctionsNotPresent("eigenfunctions not written to datfile")
return self._get_eigenfunction_like(
ev_guesses, ev_idxs, getter_func=self.filereader.read_eigenfunction
)
[docs] def get_derived_eigenfunctions(self, ev_guesses=None, ev_idxs=None) -> np.ndarray:
"""
Returns the derived eigenfunctions based on given eigenvalue guesses or their
indices. An array will be returned where every item is a dictionary, containing
both the eigenvalue and its quantities. Either eigenvalue guesses or
indices can be supplied, but not both.
Parameters
----------
ev_guesses : complex, numpy.ndarray
Eigenvalue guesses.
ev_idxs : int, numpy.ndarray
Indices corresponding to the eigenvalues that need to be retrieved.
Returns
-------
numpy.ndarray
Array containing the derived eigenfunctions and eigenvalues
corresponding to the supplied indices. Every index in this array
contains a dictionary with the derived eigenfunctions and
corresponding eigenvalue. The keys of each dictionary are the
corresponding eigenfunction names.
"""
if not self.has_derived_efs:
raise EigenfunctionsNotPresent(
"derived eigenfunctions not written to datfile"
)
return self._get_eigenfunction_like(
ev_guesses,
ev_idxs,
getter_func=self.filereader.read_derived_eigenfunction,
)
[docs] def get_nearest_eigenvalues(self, ev_guesses) -> tuple(np.ndarray, np.ndarray):
"""
Calculates the eigenvalues nearest to a given guess based on
the distance between two points.
Parameters
----------
ev_guesses : float, complex, list of float, list of complex
The guesses for the eigenvalues. These can be a single float/complex value,
or a list/Numpy array of floats/complex values.
Returns
-------
Tuple(numpy.ndarray, numpy.ndarray)
The indices of the nearest eigenvalues in the :attr:`eigenvalues` array.
The nearest eigenvalues to the provided guesses, corresponding with the
indices `idxs`.
"""
idxs, eigenvals = self.get_eigenvalues_at_distance(ev_guesses, min_distance=0.0)
return idxs, eigenvals
[docs] def get_eigenvalues_at_distance(
self, ev_guesses, min_distance=0.0
) -> tuple(np.ndarray, np.ndarray):
"""
Calculates the nearest eigenvalues nearest to a given guess
but at a minimum distance away.
Parameters
----------
ev_guesses : float, complex, list of float, list of complex
The guesses for the eigenvalues. These can be a single float/complex value,
or a list/Numpy array of floats/complex values.
min_distance : float
Minimum distance from the guess the eigenvalue should have.
Returns
-------
Tuple(numpy.ndarray, numpy.ndarray)
The indices of the nearest eigenvalues at the minimum distance
in the :attr:`eigenvalues` array.
The nearest eigenvalues at a minimum distance to the provided guesses,
corresponding with the indices `idxs`.
"""
ev_guesses = transform_to_numpy(ev_guesses)
idxs = np.empty(shape=len(ev_guesses), dtype=int)
eigenvals = np.empty(shape=len(ev_guesses), dtype=complex)
for i, ev_guess in enumerate(ev_guesses):
# distance from guess to all eigenvalues
distances = (self.eigenvalues.real - ev_guess.real) ** 2 + (
self.eigenvalues.imag - ev_guess.imag
) ** 2
# we don't want eigenvalues closer than min_distance
with np.errstate(invalid="ignore"):
mask = distances < min_distance**2
distances[mask] = np.nan
# closest distance (squared)
idx = np.nanargmin(distances)
idxs[i] = idx
eigenvals[i] = self.eigenvalues[idx]
return idxs, eigenvals
[docs] def get_omega_max(self, real=True, strip=False, range_omega=(0.0, 1e24)):
"""
Calculates the maximum of the real or imaginary part of a spectrum.
Parameters
----------
real : bool
Returns the largest real part if True (default option),
returns the largest imaginary part if False.
strip : bool
Look for maximum in a horizontal half-plane if True. Default False.
range_omega : tuple of floats
The horizontal range of the strip if strip=True.
Returns
-------
omega_max : complex
The eigenvalue that has the largest real or imaginary part in
the chosen strip. Default: in the whole complex plane.
"""
eigvals = np.copy(self.eigenvalues)
if strip:
omega_min, omega_max = range_omega
# all eigvals outside of strip locally get replaced by NaN
mask = (np.real(self.eigenvalues) - omega_min) * (
np.real(self.eigenvalues) - omega_max
) > 0
eigvals[mask] = np.nan
if real:
idx = np.nanargmax(np.real(eigvals))
else:
idx = np.nanargmax(np.imag(eigvals))
return self.eigenvalues[idx]
[docs]class LegolasDataSeries(LegolasDataContainer):
def __init__(self, datfiles):
self.datasets = [LegolasDataSet(datfile) for datfile in datfiles]
self.geometry = set([ds.geometry for ds in self.datasets])
if len(self.geometry) == 1:
self.geometry = self.geometry.pop()
def __iter__(self):
for ds in self.datasets:
yield ds
def __getitem__(self, idx):
if isinstance(idx, slice):
return LegolasDataSeries(
[ds.datfile for ds in self.datasets[idx.start : idx.stop : idx.step]]
)
else:
return self.datasets[idx]
def __len__(self):
return len(self.datasets)
@property
[docs] def continua(self) -> dict:
"""
Returns the continua. Each key corresponds to a multiple Numpy arrays,
one for each dataset.
"""
continua = self[0].continua
if continua is None:
return np.array([None] * len(self), dtype=object)
keys = continua.keys()
_continua = {key: [] for key in keys}
for ds in self:
for key in keys:
_continua[key].append(ds.continua[key])
return {key: np.array(values) for key, values in _continua.items()}
@property
[docs] def parameters(self) -> dict:
"""
Returns the parameters. Each key corresponds to multiple Numpy arrays,
one for each dataset.
"""
keys = self[0].parameters.keys()
_params = {key: [] for key in keys}
for ds in self:
for key in keys:
_params[key].append(ds.parameters[key])
return {key: np.array(values) for key, values in _params.items()}
@property
[docs] def has_background(self) -> np.ndarray:
"""Returns `True` if background is present."""
return np.array([ds.has_background for ds in self.datasets], dtype=bool)
@property
[docs] def has_efs(self) -> np.ndarray:
"""Returns `True` if eigenfunctions are present."""
return np.array([ds.has_efs for ds in self.datasets], dtype=bool)
@property
[docs] def ef_names(self) -> np.ndarray:
"""Returns the eigenfunction names."""
return np.array([ds.ef_names for ds in self.datasets], dtype=object)
@property
[docs] def ef_grid(self) -> np.ndarray:
"""Returns the eigenfunction grid."""
return np.array([ds.ef_grid for ds in self.datasets], dtype=object)
@property
[docs] def has_derived_efs(self) -> np.ndarray:
"""Returns `True` at index `i` if eigenfunctions are present in dataset `i`."""
return np.array([ds.has_derived_efs for ds in self.datasets])
@property
[docs] def derived_ef_names(self) -> np.ndarray:
"""Returns the derived eigenfunction names."""
return np.array([ds.derived_ef_names for ds in self.datasets], dtype=object)
@property
[docs] def has_ef_subset(self) -> np.ndarray:
"""Returns `True` at index `i` if the `i`-th dataset contains a subset."""
return np.array([ds.has_ef_subset for ds in self.datasets], dtype=object)
@property
[docs] def has_matrices(self) -> np.ndarray:
"""Returns `True` at index `i` if the `i`-th dataset contains matrices."""
return np.array([ds.has_matrices for ds in self.datasets], dtype=object)
@property
[docs] def has_eigenvectors(self) -> np.ndarray:
"""Returns `True` at index `i` if the `i`-th dataset contains eigenvectorst."""
return np.array([ds.has_eigenvectors for ds in self.datasets], dtype=object)
@property
[docs] def has_residuals(self) -> np.ndarray:
"""Returns `True` at index `i` if the `i`-th dataset contains residuals."""
return np.array([ds.has_residuals for ds in self.datasets], dtype=object)
[docs] def get_sound_speed(self, which_values=None) -> np.ndarray:
"""
Calculates the sound speed for the various datasets.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
sound speeds. Elements are either arrays themselves or floats, depending
on the value of `which_values`.
"""
return np.array([ds.get_sound_speed(which_values) for ds in self.datasets])
[docs] def get_alfven_speed(self, which_values=None) -> np.ndarray:
"""
Calculates the Alfvén speed for the various datasets.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
Alfvén speeds. Elements are either arrays themselves or floats, depending
on the value of `which_values`.
"""
return np.array([ds.get_alfven_speed(which_values) for ds in self.datasets])
[docs] def get_tube_speed(self, which_values=None) -> np.ndarray:
"""
Calculates the tube speed for the various datasets.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
tube speeds. Elements are either arrays themselves or floats, depending
on the value of `which_values`. Elements are None if the geometry is
not cylindrical.
"""
return np.array([ds.get_tube_speed(which_values) for ds in self.datasets])
[docs] def get_reynolds_nb(self, which_values=None) -> np.ndarray:
"""
Calculates the Reynolds number for the various datasets.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
Reynolds number. Elements are either arrays themselves or floats, depending
on the value of `which_values`.
Elements are None if the resistivity is zero.
"""
return np.array([ds.get_reynolds_nb(which_values) for ds in self.datasets])
[docs] def get_magnetic_reynolds_nb(self, which_values=None) -> np.ndarray:
"""
Calculates the magnetic Reynolds number for the various datasets.
Parameters
----------
which_values : str
Callback to :meth:`get_values`, either "average"/"minimum"/"maximum".
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
magnetic Reynolds number. Elements are either arrays themselves
or floats, depending on the value of `which_values`.
Elements are None if the resistivity is zero.
"""
return np.array(
[ds.get_magnetic_reynolds_nb(which_values) for ds in self.datasets]
)
[docs] def get_k0_squared(self) -> np.ndarray:
"""
Calculates the squared wave number for the various datasets.
Returns
-------
numpy.ndarray
A Numpy array of same length as the number of datasets, containing the
squared wavenumber for each.
"""
return np.array([ds.get_k0_squared() for ds in self.datasets], dtype=float)
[docs] def get_omega_max(self, real=True):
"""
Calculates the maximum of the real or imaginary part of the spectrum for
the various datasets.
Parameters
----------
real : bool
Returns the largest real part if True (default option),
returns the largest imaginary part if False.
Returns
-------
omega_max : numpy.ndarray
A Numpy array of same length as the number of datasets, containing tuples
of the eigenvalue that has the largest real or imaginary part.
"""
return np.array([ds.get_omega_max(real) for ds in self.datasets])