Source code for pytac.units

"""Classes for use in unit conversion."""
import numpy
from scipy.interpolate import PchipInterpolator

import pytac
from pytac.exceptions import UnitsException


[docs]def unit_function(value): """Default value for the pre and post functions used in unit conversion. Args: value (float): The value to be converted. Returns: float: The result of the conversion. """ return value
[docs]class UnitConv(object): """Class to convert between physics and engineering units. This class does not do conversion but does return values if the target units are the same as the provided units. Subclasses should implement _raw_eng_to_phys() and _raw_phys_to_eng() in order to provide complete unit conversion. The two arguments to this function represent functions that are applied to the result of the initial conversion. One happens after the conversion, the other happens before the conversion back. **Attributes:** Attributes: name (str): An identifier for the unit conversion object. eng_units (str): The unit type of the post conversion engineering value. phys_units (str): The unit type of the post conversion physics value. .. Private Attributes: _post_eng_to_phys (function): Function to be applied after the initial conversion. _pre_phys_to_eng (function): Function to be applied before the initial conversion. """ def __init__(self, post_eng_to_phys=unit_function, pre_phys_to_eng=unit_function, engineering_units='', physics_units='', name=None): """ Args: post_eng_to_phys (function): Function to be applied after the initial conversion. pre_phys_to_eng (function): Function to be applied before the initial conversion. engineering_units (str): The unit type of the post conversion engineering value. physics_units (str): The unit type of the post conversion physics value. name (str): An identifier for the unit conversion object. **Methods:** """ self.name = name self._post_eng_to_phys = post_eng_to_phys self._pre_phys_to_eng = pre_phys_to_eng self.eng_units = engineering_units self.phys_units = physics_units self.lower_limit = None self.upper_limit = None def __str__(self): string_rep = self.__class__.__name__ if self.name is not None: string_rep += " {}".format(self.name) return string_rep
[docs] def set_post_eng_to_phys(self, post_eng_to_phys): """Set the function to be applied after the initial conversion. Args: post_eng_to_phys (function): Function to be applied after the initial conversion. """ self._post_eng_to_phys = post_eng_to_phys
[docs] def set_pre_phys_to_eng(self, pre_phys_to_eng): """Set the function to be applied before the initial conversion. Args: pre_phys_to_eng (function): Function to be applied before the initial conversion. """ self._pre_phys_to_eng = pre_phys_to_eng
def _raw_eng_to_phys(self, value): """Function to be implemented by child classes. Args: value (float): The engineering value to be converted to physics units. """ raise NotImplementedError( '{0}: No eng-to-phys conversion provided'.format(self) )
[docs] def eng_to_phys(self, value): """Function that does the unit conversion. Conversion from engineering to physics units. An additional function may be cast on the initial conversion. Args: value (float): Value to be converted from engineering to physics units. Returns: float: The result value. Raises: UnitsException: If the conversion is invalid; i.e. if there are no solutions, or multiple, within conversion limits. """ if self.lower_limit is not None: if value < self.lower_limit: raise UnitsException("{0}: Input less than lower " "conversion limit ({1})." .format(self, self.lower_limit)) if self.upper_limit is not None: if value > self.upper_limit: raise UnitsException("{0}: Input greater than " "upper conversion limit ({1})." .format(self, self.upper_limit)) results = self._raw_eng_to_phys(value) valid_results = [self._post_eng_to_phys(result) for result in results] if len(valid_results) == 1: result = valid_results[0] elif len(valid_results) == 0: # This will not occur for our existing NullUnitConv, # PchipUintConv, and PolyUnitConv classes. raise UnitsException("{0}: A corresponding physics value " "does not exist.".format(self)) else: # This will not occur for our existing NullUnitConv, # PchipUintConv, and PolyUnitConv classes. raise UnitsException("{0}: There are multiple " "corresponding physics values ({1})." .format(self, valid_results)) return result
def _raw_phys_to_eng(self, value): """Function to be implemented by child classes. Args: value (float): The physics value to be converted to engineering units. """ raise NotImplementedError( '{0}: No phys-to-eng conversion provided'.format(self) )
[docs] def phys_to_eng(self, value): """Function that does the unit conversion. Conversion from physics to engineering units. An additional function may be cast on the initial conversion. Args: value (float): Value to be converted from physics to engineering units. Returns: float: The result value. Raises: UnitsException: If the conversion is invalid; i.e. if there are no solutions, or multiple, within conversion limits. """ adjusted_value = self._pre_phys_to_eng(value) results = self._raw_phys_to_eng(adjusted_value) valid_results = results[:] if self.lower_limit is not None: valid_results = [r for r in valid_results if r >= self.lower_limit] if self.upper_limit is not None: valid_results = [r for r in valid_results if r <= self.upper_limit] if len(valid_results) == 1: return valid_results[0] elif len(valid_results) == 0: raise UnitsException("{0}: none of conversion results {1} " "within conversion limits ({2}, " "{3}).".format(self, results, self.lower_limit, self.upper_limit)) else: raise UnitsException("{0}: There are multiple " "corresponding engineering values ({1})." .format(self, valid_results))
[docs] def convert(self, value, origin, target): """Convert between two different unit types and chek the validity of the result. Args: value (float): the value to be converted origin (str): pytac.ENG or pytac.PHYS target (str): pytac.ENG or pytac.PHYS Returns: float: The resulting value. Raises: UnitsException: If the conversion is invalid; i.e. if there are no solutions, or multiple, within conversion limits. """ if origin == target: return value elif origin == pytac.ENG and target == pytac.PHYS: return self.eng_to_phys(value) elif origin == pytac.PHYS and target == pytac.ENG: return self.phys_to_eng(value) else: raise UnitsException("{0}: Conversion from {1} to {2} " "not understood.".format(self, origin, target))
[docs] def set_conversion_limits(self, lower_limit, upper_limit): """Conversion limits to be applied before or after a conversion take place. Limits should be set in in engineering units. Args: lower_limit (float): the lower conversion limit upper_limit (float): the upper conversion limit """ self.lower_limit = lower_limit self.upper_limit = upper_limit
[docs]class PolyUnitConv(UnitConv): """Linear interpolation for converting between physics and engineering units. **Attributes:** Attributes: p (poly1d): A one-dimensional polynomial of coefficients. name (str): An identifier for the unit conversion object. eng_units (str): The unit type of the post conversion engineering value. phys_units (str): The unit type of the post conversion physics value. .. Private Attributes: _post_eng_to_phys (function): Function to be applied after the initial conversion. _pre_phys_to_eng (function): Function to be applied before the initial conversion. """ def __init__(self, coef, post_eng_to_phys=unit_function, pre_phys_to_eng=unit_function, engineering_units='', physics_units='', name=None): """ Args: coef (array-like): The polynomial's coefficients, in decreasing powers. post_eng_to_phys (float): The value after conversion between ENG and PHYS. pre_eng_to_phys (float): The value before conversion. engineering_units (str): The unit type of the post conversion engineering value. physics_units (str): The unit type of the post conversion physics value. name (str): An identifier for the unit conversion object. """ super(self.__class__, self).__init__(post_eng_to_phys, pre_phys_to_eng, engineering_units, physics_units, name) self.p = numpy.poly1d(coef) def _raw_eng_to_phys(self, eng_value): """Convert between engineering and physics units. Args: eng_value (float): The engineering value to be converted to physics units. Returns: list: Containing the converted physics value from the given engineering value. """ return [self.p(eng_value)] def _raw_phys_to_eng(self, physics_value): """Convert between physics and engineering units. Args: physics_value (float): The physics value to be converted to engineering units. Returns: list: Containing all posible real engineering values converted from the given physics value. """ roots = set((self.p - physics_value).roots) # remove duplicates valid_roots = [] for root in roots: # remove imaginary roots if not numpy.issubdtype(root.dtype, numpy.complexfloating): valid_roots.append(root) return valid_roots
[docs]class PchipUnitConv(UnitConv): """Piecewise Cubic Hermite Interpolating Polynomial unit conversion. **Attributes:** Attributes: x (list): A list of points on the x axis. These must be in increasing order for the interpolation to work. Otherwise, a ValueError is raised. y (list): A list of points on the y axis. These must be in increasing or decreasing order. Otherwise, a ValueError is raised. pp (PchipInterpolator): A pchip one-dimensional monotonic cubic interpolation of points on both x and y axes. name (str): An identifier for the unit conversion object. eng_units (str): The unit type of the post conversion engineering value. phys_units (str): The unit type of the post conversion physics value. .. Private Attributes: _post_eng_to_phys (function): Function to be applied after the initial conversion. _pre_phys_to_eng (function): Function to be applied before the initial conversion. """ def __init__(self, x, y, post_eng_to_phys=unit_function, pre_phys_to_eng=unit_function, engineering_units='', physics_units='', name=None): """ Args: x (list): A list of points on the x axis. These must be in increasing order for the interpolation to work. Otherwise, a ValueError is raised. y (list): A list of points on the y axis. These must be in increasing or decreasing order. Otherwise, a ValueError is raised. engineering_units (str): The unit type of the post conversion engineering value. physics_units (str): The unit type of the post conversion physics value. name (str): An identifier for the unit conversion object. Raises: ValueError: if coefficients are not appropriately monotonic. """ super(self.__class__, self).__init__(post_eng_to_phys, pre_phys_to_eng, engineering_units, physics_units, name) self.x = x self.y = y self.pp = PchipInterpolator(x, y) # Set conversion limits to PChip bounds if they are not already set. if self.lower_limit is None: self.lower_limit = self.x[0] if self.upper_limit is None: self.upper_limit = self.x[-1] # Note that the x coefficients are checked by the PchipInterpolator # constructor. y_diff = numpy.diff(y) if not ((numpy.all(y_diff > 0)) or (numpy.all((y_diff < 0)))): raise ValueError("y coefficients must be monotonically " "increasing or decreasing.") def _raw_eng_to_phys(self, eng_value): """Convert between engineering and physics units. Args: eng_value (float): The engineering value to be converted to physics units. Returns: list: Containing the converted physics value from the given engineering value. """ return [self.pp(eng_value)] def _raw_phys_to_eng(self, physics_value): """Convert between physics and engineering units. Args: physics_value (float): The physics value to be converted to engineering units. Returns: list: Containing all posible real engineering values converted from the given physics value. """ y = [val - physics_value for val in self.y] new_pp = PchipInterpolator(self.x, y) roots = set(new_pp.roots()) # remove duplicates valid_roots = [] for root in roots: # remove imaginary roots if not numpy.issubdtype(root.dtype, numpy.complexfloating): valid_roots.append(root) return valid_roots
[docs]class NullUnitConv(UnitConv): """Returns input value without performing any conversions. **Attributes:** Attributes: eng_units (str): The unit type of the post conversion engineering value. phys_units (str): The unit type of the post conversion physics value. .. Private Attributes: _post_eng_to_phys (function): Always unit_function as no conversion is performed. _pre_phys_to_eng (function): Always unit_function as no conversion is performed. """ def __init__(self, engineering_units='', physics_units=''): """ Args: engineering_units (str): The unit type of the post conversion engineering value. physics_units (str): The unit type of the post conversion physics value. """ super(self.__class__, self).__init__(unit_function, unit_function, engineering_units, physics_units) def _raw_eng_to_phys(self, eng_value): """Doesn't convert between engineering and physics units. Maintains the same syntax as the other UnitConv classes for compatibility, but does not perform any conversion. Args: eng_value (float): The engineering value to be returned unchanged. Returns: list: Containing the unconverted given engineering value. """ return [eng_value] def _raw_phys_to_eng(self, phys_value): """Doesn't convert between physics and engineering units. Maintains the same syntax as the other UnitConv classes for compatibility, but does not perform any conversion. Args: physics_value (float): The physics value to be returned unchanged. Returns: list: Containing the unconverted given physics value. """ return [phys_value]