Source code for lezargus.library.conversion

"""Functions to convert things into something else.

Any and all generic conversions (string, units, or otherwise) can be found in
here. Extremely standard conversion functions are welcome in here, but,
sometimes, a simple multiplication factor is more effective.
"""

# isort: split
# Import required to remove circular dependencies from type checking.
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from lezargus.library import hint
# isort: split

import astropy.io.fits
import astropy.units
import numpy as np

from lezargus.library import logging


[docs] def convert_units( value: float | hint.NDArray, value_unit: hint.Unit | str, result_unit: hint.Unit | str, ) -> float | hint.NDArray: """Convert a value from one unit to another unit. We convert values using Astropy, however, we only convert raw numbers and so we do not handle Quantity variables. The unit arguments are parsed with :py:func:`parse_astropy_unit` if it is not a unit. This function is vectorized properly, of course, as it is generally just multiplication. Parameters ---------- value : float or ndarray The value to convert. value_unit : Unit or str The unit of the value we are converting. Parsing is attempted if it is not an Astropy Unit. result_unit : Unit or str The unit that we are converting to. Parsing is attempted if it is not an Astropy Unit. Returns ------- result : float or ndarray The result after the unit conversion. """ # We parse the units so we can use Astropy to do the unit conversions. value_unit = parse_astropy_unit(unit_string=value_unit) result_unit = parse_astropy_unit(unit_string=result_unit) # Determine the conversion factor and convert between the two. try: conversion_factor = float(value_unit.to(result_unit)) except astropy.units.UnitConversionError as error: # The unit failed to convert. Astropy's message is actually pretty # informative so we bootstrap it. astropy_error_message = str(error) logging.critical( critical_type=logging.ArithmeticalError, message=f"Unit conversion failed: {astropy_error_message}", ) # Applying the conversion. result = value * conversion_factor return result
[docs] def parse_numpy_dtype(dtype_string: str | type) -> type: """Parse a data type string to an Numpy data type. We only support built-in normal Python types and the Numpy types. Unsupported types will not be converted by this function, and will raise an error. Parameters ---------- dtype_string : str The data type, either as a string representation (typical) or a type (for compatibility reasons). Only Numpy canonical names are accepted. Returns ------- numpy_type : type The data type after the conversion. """ # We need to determine the type. If a type has been provided, it is easy # to just go through all of the same process. if isinstance(dtype_string, type): dtype_string = dtype_string.__name__ elif isinstance(dtype_string, str): # All good. pass else: logging.critical( critical_type=logging.InputError, message=( f"Object of type {type(dtype_string)} is not supported for data" " type interpretation." ), ) dtype_string = str(dtype_string).casefold() # To avoid PLR0912, and to make it easier to add new options, a full # dictionary is used instead. type_dictionary = { "int": int, "int64": np.int64, "int32": np.int32, # Floats... "float": float, "float64": np.float64, "float32": np.float32, "float16": np.float16, # Strings... "str": str, # Other things? "object": object, } # Getting the type. Note, because None itself is also a valid type, # we need to be more creative for the case of a missing entry. numpy_type = type_dictionary.get(dtype_string, logging.ExpectedCaughtError) if numpy_type == logging.ExpectedCaughtError: logging.error( error_type=logging.InputError, message=f"Data type string {dtype_string} not implemented.", ) logging.critical( critical_type=logging.DevelopmentError, message=( f"Data type string {dtype_string} does not have a" " conversion, it could implemented." ), ) # All done. return numpy_type
[docs] def parse_astropy_unit(unit_string: str | hint.Unit | None) -> hint.Unit: """Parse a unit string to an Astropy Unit class. Although for most cases, it is easier to use the Unit instantiation class directly, Astropy does not properly understand some unit conventions so we need to parse them in manually. Because of this, we just build a unified interface for all unit strings in general. Parameters ---------- unit_string : str or Astropy Unit or None. The unit string to parse into an Astropy unit. If it is None, then we return a dimensionless quantity unit. Returns ------- unit_instance : Unit The unit instance after parsing. """ # If it is already a unit, just return it. if isinstance(unit_string, astropy.units.UnitBase): return unit_string # We check for a few input cases which Astropy does not natively know # but we do. # ...for dimensionless unit entries... unit_string = "" if unit_string is None else unit_string # ...for flams, the unit of spectral density over wavelength... unit_string = "erg / (AA cm^2 s)" if unit_string == "flam" else unit_string # Finally, converting the string. try: unit_instance = astropy.units.Unit(unit_string, parse_strict="raise") except ValueError: # The unit string provided is likely not something we can parse. logging.critical( critical_type=logging.InputError, message=( "Input unit string cannot be parsed to an Astropy unit" f" {unit_string}." ), ) # All done. return unit_instance