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: 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=''): """ 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. **Methods:** """ 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 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('No eng-to-phys conversion provided')
[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. """ x = self._raw_eng_to_phys(value) result = self._post_eng_to_phys(x) 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('No phys-to-eng conversion provided')
[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. """ x = self._pre_phys_to_eng(value) result = self._raw_phys_to_eng(x) return result
[docs] def convert(self, value, origin, target): """ Args: value (float): origin (str): pytac.ENG or pytac.PHYS target (str): pytac.ENG or pytac.PHYS Returns: float: The result value. Raises: UnitsException: invalid conversion. """ if origin == target: return value if origin == pytac.PHYS and target == pytac.ENG: return self.phys_to_eng(value) if origin == pytac.ENG and target == pytac.PHYS: return self.eng_to_phys(value) raise UnitsException("Conversion from {0} to {1} not understood." .format(origin, target))
[docs]class PolyUnitConv(UnitConv): """Linear interpolation for converting between physics and engineering units. **Attributes:** Attributes: p (poly1d): A one-dimensional polynomial of coefficients. 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=''): """ 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. """ super(self.__class__, self).__init__(post_eng_to_phys, pre_phys_to_eng, engineering_units, physics_units) 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: float: 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: float: The converted engineering value from the given physics value. Raises: UnitsException: An error occurred when there exist no roots or more than one root. """ roots = (self.p - physics_value).roots if len(roots) == 1: x = roots[0] return x elif len(roots) == 0: raise UnitsException("A corresponding engineering value does not " "exist.") else: raise UnitsException("There are multiple corresponding " "engineering values: {0}".format(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. 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=''): """ 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. 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) self.x = x self.y = y self.pp = PchipInterpolator(x, y) # 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: float: 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. This expects there to be exactly one solution for x within the range of the x values in self.x, otherwise a UnitsException is raised. Args: physics_value (float): The physics value to be converted to engineering units. Returns: float: The converted engineering value from the given physics value. Raises: UnitsException: if there is not exactly one solution. """ y = [val - physics_value for val in self.y] new_pp = PchipInterpolator(self.x, y) roots = new_pp.roots() unique_root = None for root in roots: if self.x[0] <= root <= self.x[-1]: if unique_root is None: unique_root = root else: # I believe this should never happen because of the # requirement for self.y to be monotonically increasing. raise UnitsException("More than one solution within Pchip " "bounds.") if unique_root is None: raise UnitsException("No solution within Pchip bounds.") return unique_root
[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: float: 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: float: The unconverted given physics value. """ return phys_value