Source code for indica.converters.abstractconverter

"""Provides an abstract interface for coordinate conversion.
"""

from abc import ABC
from abc import abstractmethod
from typing import Callable
from typing import cast
from typing import Dict
from typing import Optional
from typing import Tuple

from xarray import DataArray
from xarray import zeros_like

from ..abstract_equilibrium import AbstractEquilibrium
from ..numpy_typing import LabeledArray

Coordinates = Tuple[LabeledArray, LabeledArray]
OptionalCoordinates = Tuple[Optional[LabeledArray], Optional[LabeledArray]]


[docs]class EquilibriumException(Exception): """Exception raised if a converter object's equilibrium object is set twice."""
[docs]class CoordinateTransform(ABC): """Class for converting between different coordinate systems. This is an abstract base class; each coordinate system should provide its own implementation. Subclasses should allow each instance to have a "default grid" on which to calculate results. This can be cached for efficient retrieval. Note that not all coordinate systems will have an actual x2 dimension (for example, the lines-of-site for soft X-ray data). However, 2 coordinates are still needed to map to the global coordinate system. Therefore, x2 is treated as a "pseudo-coordinate",in these cases, with values between 0 and 1 specifying the position along the grid-line for x1. 0 is the start and 1 is the end (possibly overlapping, if the coordinate system is periodic). Parameters ---------- default_x1 The default grid to use for the first spatial coordinate. default_x1 The default grid to use for the second spatial coordinate. default_R The default grid to use for the R-coordinate when converting to this coordinate system. default_z The default grid to use for the z-coordinate when converting to this coordinate system. default_t The default grid to use for time. Attributes ---------- x1_name: str Name for the first spacial coordinate. May be class- or instance-specific. x2_name: str Name for the second spacial coordinate. May be class- or instance-specific. """ _CONVERSION_METHODS: Dict[str, str] = {} _INVERSE_CONVERSION_METHODS: Dict[str, str] = {} equilibrium: AbstractEquilibrium x1_name: str x2_name: str
[docs] def set_equilibrium(self, equilibrium: AbstractEquilibrium, force: bool = False): """Initialise the object using a set of equilibrium data. If it has already been initialised with the same equilibrium data then do nothing. If already initialised with a different equilibrium, throw an :py:class:`abstractconverter.EquilibriumException` unless ``force == True``. Parameters ---------- equilibrium A set of equilibrium data with which to calculate coordinate transforms. force : bool If true, re-initialise the transform if provided with a new set of equilibrium data. """ if not hasattr(self, "equilibrium") or force: self.equilibrium = equilibrium elif self.equilibrium != equilibrium: raise EquilibriumException("Attempt to set equilibrium twice.")
[docs] def get_converter( self, other: "CoordinateTransform", reverse=False ) -> Optional[Callable[[LabeledArray, LabeledArray, LabeledArray], Coordinates]]: """Checks if there is a shortcut to convert between these coordiantes, returning it if so. This can sometimes save the step of converting to (R, z) coordinates first. Parameters ---------- other The other transform whose coordinate system you want to convert to. reverse If True, try to return a function which converts _from_ ``other`` to this coordinate system. Returns ------- : If a shortcut function is available, return it. Otherwise, None. Note ---- Implementations should call ``other.get_converter(self, reverse=True``. For obvious reasons, however, they should **only do this when ``reverse == False``**. """ if reverse: return None return other.get_converter(self, True)
[docs] def convert_to( self, other: "CoordinateTransform", x1: LabeledArray, x2: LabeledArray, t: LabeledArray, ) -> Coordinates: """General routine to map coordinates from this system to those used in ``other``. Array broadcasting will be performed as necessary. If this transform class provides a specialised method for doing this (specified in :py:attr:`_CONVERSION_METHODS`) then that is used. Otherwise, the coordinates are converted to R-z using :py:meth:`_convert_to_Rz` and then converted to the other coordinate system using :py:attr:`_convert_from_Rz`. Parameters ---------- other The coordinate system to convert to. x1 The first spatial coordinate in this system. x2 The second spatial coordinate in this system. t The time coordinate (if there is one, otherwise ``None``) Returns ------- x1 The first spatial coordinate in the ``other`` system. x2 The second spatial coordinate in the ``other`` system. """ if self == other: return x1, x2 converter = self.get_converter(other) if converter: return converter(x1, x2, t) R, z = self.convert_to_Rz(x1, x2, t) return other.convert_from_Rz(R, z, t)
[docs] @abstractmethod def convert_to_Rz( self, x1: LabeledArray, x2: LabeledArray, t: LabeledArray, ) -> Coordinates: """Convert from this coordinate to the R-z coordinate system. Each subclass must implement this method. Parameters ---------- x1 The first spatial coordinate in this system. x2 The second spatial coordinate in this system. t The time coordinate Returns ------- R Major radius coordinate z Height coordinate """ raise NotImplementedError( "{} does not implement a 'convert_to_Rz' " "method.".format(self.__class__.__name__) )
[docs] def convert_from_Rz( self, R: LabeledArray, z: LabeledArray, t: LabeledArray, ) -> Coordinates: """Convert from the master coordinate system to this coordinate. Each subclass must implement this method. Parameters ---------- R Major radius coordinate z Height coordinate t Time coordinate Returns ------- x1 The first spatial coordinate in this system. x2 The second spatial coordinate in this system. """ raise NotImplementedError( "{} does not implement a 'convert_from_Rz' " "method.".format(self.__class__.__name__) )
def _abstract_equals(self, other: "CoordinateTransform") -> bool: """Checks that default coordinate values and equilibrium objects are the same on two transform classes. """ if not hasattr(self, "equilibrium"): return not hasattr(other, "equilibrium") elif not hasattr(other, "equilibrium"): return False else: result = self.equilibrium == other.equilibrium result = result and self.x1_name == other.x1_name result = result and self.x2_name == other.x2_name return result @abstractmethod def __eq__(self, other: object) -> bool: """Check that two transforms are describing the same coordinate system.""" raise NotImplementedError( "{} does not implement an '__eq__' method".format(self.__class__.__name__) )
[docs] def distance( self, direction: str, x1: LabeledArray, x2: LabeledArray, t: LabeledArray, ) -> LabeledArray: """Give the distance (in physical space) from the origin along the specified direction. This is useful for when taking spatial integrals and differentials in that direction. Note that distance is calculated using Euclidean lines between points. As such, it will not be accurate for a curved axis. Parameters ---------- direction : str Which dimension to give the distance along. x1 The first spatial coordinate in this system. x2 The second spatial coordinate in this system. t The time coordinate Returns ------- : Distance from the origin in the specified direction. """ R, z = cast(Tuple[DataArray, DataArray], self.convert_to_Rz(x1, x2, t)) if isinstance(R, (int, float)) or isinstance(z, (int, float)): raise ValueError("Arguments x1 and x2 must be xarray DataArray objects.") spacings = (R.diff(direction) ** 2 + z.diff(direction) ** 2) ** 0.5 result = zeros_like(R.broadcast_like(z)) result[{direction: slice(1, None)}] = spacings.cumsum(direction) return result
[docs] def encode(self) -> str: """Returns a JSON representation of this object. Should be sufficient to recreate it identically from scratch (except for the equilibrium).""" raise NotImplementedError
[docs] @staticmethod def decode(json: str) -> "CoordinateTransform": """Takes some JSON and decodes it into a CoordinateTransform object.""" raise NotImplementedError