Source code for alpyne.outputs

from abc import abstractmethod
from collections import namedtuple
from dataclasses import dataclass, field
from enum import Enum
from math import inf
from typing import Callable

from alpyne.utils import parse_number, extended_namedtuple
from alpyne.typing import Number


@dataclass
class _AnalysisObject:
    def __new__(cls, *args, **kwargs):
        if cls is _AnalysisObject:
            raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
        return object.__new__(cls)

    def __post_init__(self):
        """ Post-processing any values, namely to turn any "Infinity"/"-Infinity" to Python numbers """
        for name, value in vars(self).items():
            if isinstance(value, str):
                setattr(self, name, parse_number(value))


@dataclass
class _Statistics(_AnalysisObject):
    count: int = 0
    mean: float = 0.0
    confidence: float = inf
    min: float = inf
    max: float = -inf
    deviation: float = 0.0


[docs] @dataclass class StatisticsDiscrete(_Statistics): sum: float = 0.0
[docs] @dataclass class StatisticsContinuous(_Statistics): integral: float = 0.0
[docs] @dataclass class DataSet(_AnalysisObject): xmin: float = inf xmean: float = 0.0 xmedian: float = 0.0 xmax: float = -inf ymin: float = inf ymean: float = 0.0 ymedian: float = 0.0 ymax: float = -inf plainDataTable: list[list[float]] = field(default_factory=list) @property def x_values(self) -> list[float]: return [row[0] for row in self.plainDataTable] @property def y_values(self) -> list[float]: return [row[1] for row in self.plainDataTable]
[docs] @dataclass class HistogramSmartData(_AnalysisObject): count: int = 0 lowerBound: float = 0.0 intervalWidth: float = 0.1 hits: list[int] = field(default_factory=list) statistics: _Statistics = field(default_factory=_Statistics) def __post_init__(self): super().__post_init__() # given statistics may initially be passed as a dict; convert to the correct type if isinstance(self.statistics, dict): self.statistics = _Statistics(**self.statistics)
[docs] @dataclass class HistogramSimpleData(HistogramSmartData): hitsOutLow: float = 0.0 hitsOutHigh: float = 0.0
[docs] @dataclass class Histogram2DData(_AnalysisObject): hits: list[list[int]] = field(default_factory=list) hitsOutLow: list[int] = field(default_factory=list) hitsOutHigh: list[int] = field(default_factory=list) xMin: float = inf xMax: float = -inf yMin: float = inf yMax: float = -inf
_BaseUnitAttrs = namedtuple('BaseUnitAttrs', ['symbol']) """Baseline named tuple, inherited by the others with a similar name; all enums have a 'symbol' attribute describing the string label for the unit""" _NumericUnitAttrs = extended_namedtuple('NumericUnitAttrs', ['conversion_factor', _BaseUnitAttrs]) """Named tuple used to describe units with only a numeric attribute; used by units describing Amount, Time, Length, and Angle""" _SpaceUnitAttrs = extended_namedtuple('SpaceUnitAttrs', ['length_unit', _BaseUnitAttrs]) # area """Named tuple used to describe units with a length component; used by AreaUnits""" _RateUnitAttrs = extended_namedtuple('RateUnitAttrs', ['time_unit', _BaseUnitAttrs]) # rate """Named tuple used to describe units with a time component; used by RateUnits""" _VelocityUnitAttrs = extended_namedtuple('VelocityUnitAttrs', ['spacial_unit', _RateUnitAttrs]) # accel, speed, flow, rotation speed """Named tuple used to describe units with time and spacial components; used by units describing Acceleration, Speed, Flow, and RotationSpeed""" class _UnitEnum(Enum): """Abstract, base class inherited by other unit classes""" def __init__(self, symbol=None): self.symbol = symbol or "?" @abstractmethod def modifier(self, units: '_UnitEnum') -> float: raise NotImplementedError() @abstractmethod def convert_to(self, this_amount: float, new_units: '_UnitEnum') -> float: raise NotImplementedError()
[docs] class AmountUnits(_UnitEnum): LITER = _NumericUnitAttrs(0.001, 'L') OIL_BARREL = _NumericUnitAttrs(0.158987295, 'barrels') CUBIC_METER = _NumericUnitAttrs(1.0, 'm3') KILOGRAM = _NumericUnitAttrs(1.0, 'kg') TON = _NumericUnitAttrs(1000.0, 'ton') def __init__(self, conversion_factor, symbol): super().__init__(symbol) self.conversion_factor = conversion_factor def modifier(self, units: 'AmountUnits') -> float: return self.conversion_factor / units.conversion_factor def convert_to(self, this_amount: float, new_units: 'AmountUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class TimeUnits(_UnitEnum): MILLISECOND = _NumericUnitAttrs(0.001, "ms") SECOND = _NumericUnitAttrs(1.0, "sec") MINUTE = _NumericUnitAttrs(60.0, "min") HOUR = _NumericUnitAttrs(3600.0, "hr") DAY = _NumericUnitAttrs(86400.0, "day") WEEK = _NumericUnitAttrs(604800.0, "wk") MONTH = _NumericUnitAttrs(2592000.0, "mn") YEAR = _NumericUnitAttrs(3.1536E7, "yr") def __init__(self, conversion_factor, symbol): super().__init__(symbol) self.conversion_factor = conversion_factor def modifier(self, units: 'TimeUnits') -> float: return self.conversion_factor / units.conversion_factor def convert_to(self, this_amount: float, new_units: 'TimeUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class LengthUnits(_UnitEnum): MILLIMETER = _NumericUnitAttrs(0.001, "mm") CENTIMETER = _NumericUnitAttrs(0.01, "cm") METER = _NumericUnitAttrs(1.0, "m") KILOMETER = _NumericUnitAttrs(1000.0, "km") INCH = _NumericUnitAttrs(0.0254, "in") FOOT = _NumericUnitAttrs(0.3048, "ft") YARD = _NumericUnitAttrs(0.9144, "yd") MILE = _NumericUnitAttrs(1609.344, "m") NAUTICAL_MILE = _NumericUnitAttrs(1853.184, "nm") def __init__(self, conversion_factor, symbol): super().__init__(symbol) self.conversion_factor = conversion_factor def modifier(self, units: 'LengthUnits') -> float: return self.conversion_factor / units.conversion_factor def convert_to(self, this_amount: float, new_units: 'LengthUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class AngleUnits(_UnitEnum): TURN = _NumericUnitAttrs(6.283185307179586, 'turn') RADIAN = _NumericUnitAttrs(1.0, 'rad') DEGREE = _NumericUnitAttrs(0.017453292519943295, 'deg') def __init__(self, conversion_factor, symbol): super().__init__(symbol) self.conversion_factor = conversion_factor def modifier(self, units: 'AngleUnits') -> float: return self.conversion_factor / units.conversion_factor def convert_to(self, this_amount: float, new_units: 'AngleUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class AreaUnits(_UnitEnum): SQ_MILLIMETER = _SpaceUnitAttrs(LengthUnits.MILLIMETER, "mm2") SQ_CENTIMETER = _SpaceUnitAttrs(LengthUnits.CENTIMETER, "cm2") SQ_METER = _SpaceUnitAttrs(LengthUnits.METER, "m2") SQ_KILOMETER = _SpaceUnitAttrs(LengthUnits.KILOMETER, "km2") SQ_INCH = _SpaceUnitAttrs(LengthUnits.INCH, "in2") SQ_FOOT = _SpaceUnitAttrs(LengthUnits.FOOT, "ft2") SQ_YARD = _SpaceUnitAttrs(LengthUnits.YARD, "yard2") SQ_MILE = _SpaceUnitAttrs(LengthUnits.MILE, "mile2") SQ_NAUTICAL_MILE = _SpaceUnitAttrs(LengthUnits.NAUTICAL_MILE, "nautmile2") def __init__(self, length_unit: 'LengthUnits', symbol: str): super().__init__(symbol) self.si = length_unit.modifier(LengthUnits.METER) ** 2 self.length_unit = length_unit def modifier(self, units: 'AreaUnits') -> float: return self.si / units.si def convert_to(self, this_amount: float, new_units: 'AreaUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class RateUnits(_UnitEnum): PER_MILLISECOND = _RateUnitAttrs(TimeUnits.MILLISECOND, "per ms") PER_SECOND = _RateUnitAttrs(TimeUnits.SECOND, "per sec") PER_MINUTE = _RateUnitAttrs(TimeUnits.MINUTE, "per min") PER_HOUR = _RateUnitAttrs(TimeUnits.HOUR, "per hr") PER_DAY = _RateUnitAttrs(TimeUnits.DAY, "per day") PER_WEEK = _RateUnitAttrs(TimeUnits.WEEK, "per wk") PER_MONTH = _RateUnitAttrs(TimeUnits.MONTH, "per month") PER_YEAR = _RateUnitAttrs(TimeUnits.YEAR, "per year") def __init__(self, time_unit, symbol): super().__init__(symbol) self.time_unit = time_unit def modifier(self, units: 'RateUnits') -> float: return 1.0 / self.time_unit.modifier(units.time_unit) def convert_to(self, this_amount: float, new_units: 'RateUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class AccelerationUnits(_UnitEnum): MPS_SQ = _VelocityUnitAttrs(LengthUnits.METER, TimeUnits.SECOND, "mps2") FPS_SQ = _VelocityUnitAttrs(LengthUnits.FOOT, TimeUnits.SECOND, "fps2") def __init__(self, length_unit, time_unit, symbol): super().__init__(symbol) self.length_unit = length_unit self.time_unit = time_unit def modifier(self, units: 'AccelerationUnits') -> float: d = self.time_unit.modifier(units.time_unit) return self.length_unit.modifier(units.length_unit) / d * d def convert_to(self, this_amount: float, new_units: 'AccelerationUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class SpeedUnits(_UnitEnum): MPS = _VelocityUnitAttrs(LengthUnits.METER, TimeUnits.SECOND, "meters per second") KPH = _VelocityUnitAttrs(LengthUnits.KILOMETER, TimeUnits.HOUR, "kilometers per hour") FPS = _VelocityUnitAttrs(LengthUnits.FOOT, TimeUnits.SECOND, "feet per second") FPM = _VelocityUnitAttrs(LengthUnits.FOOT, TimeUnits.MINUTE, "feet per minute") MPH = _VelocityUnitAttrs(LengthUnits.MILE, TimeUnits.HOUR, "miles per hour") KN = _VelocityUnitAttrs(LengthUnits.NAUTICAL_MILE, TimeUnits.HOUR, "knots") def __init__(self, length_unit, time_unit, symbol): super().__init__(symbol) self.length_unit = length_unit self.time_unit = time_unit def modifier(self, units: 'SpeedUnits') -> float: return self.length_unit.modifier(units.length_unit) / self.time_unit.modifier(units.time_unit) def convert_to(self, this_amount: float, new_units: 'SpeedUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class FlowRateUnits(_UnitEnum): LITER_PER_SECOND = _VelocityUnitAttrs(AmountUnits.LITER, TimeUnits.SECOND, "liter per second") OIL_BARREL_PER_SECOND = _VelocityUnitAttrs(AmountUnits.OIL_BARREL, TimeUnits.SECOND, "oil barrel per second") CUBIC_METER_PER_SECOND = _VelocityUnitAttrs(AmountUnits.CUBIC_METER, TimeUnits.SECOND, "meter3 per second") KILOGRAM_PER_SECOND = _VelocityUnitAttrs(AmountUnits.KILOGRAM, TimeUnits.SECOND, "kilogram per second") TON_PER_SECOND = _VelocityUnitAttrs(AmountUnits.TON, TimeUnits.SECOND, "ton per second") def __init__(self, amount_unit, time_unit, symbol): super().__init__(symbol) self.amount_unit = amount_unit self.time_unit = time_unit def modifier(self, units: 'FlowRateUnits') -> float: return self.amount_unit.modifier(units.amount_unit) / self.time_unit.modifier(units.time_unit) def convert_to(self, this_amount: float, new_units: 'FlowRateUnits') -> float: return this_amount * self.modifier(new_units)
[docs] class RotationSpeedUnits(_UnitEnum): RPM = _VelocityUnitAttrs(AngleUnits.TURN, TimeUnits.MINUTE, "rotations per minute") RAD_PER_SECOND = _VelocityUnitAttrs(AngleUnits.RADIAN, TimeUnits.SECOND, "radians per second") DEG_PER_SECOND = _VelocityUnitAttrs(AngleUnits.DEGREE, TimeUnits.SECOND, "degrees per second") def __init__(self, angle_unit, time_unit, symbol): super().__init__(symbol) self.angle_unit = angle_unit self.time_unit = time_unit def modifier(self, units: 'RotationSpeedUnits') -> float: return self.angle_unit.modifier(units.angle_unit) / self.time_unit.modifier(units.time_unit) def convert_to(self, this_amount: float, new_units: 'RotationSpeedUnits') -> float: return this_amount * self.modifier(new_units)
[docs] @dataclass class UnitValue: """ A custom type to represent some numerical value with units. """ value: Number unit: _UnitEnum | str # will get converted to correct unit during initialization def __str__(self): return f"{self.value} {self.unit.symbol}" def __repr__(self): return str(self) def __post_init__(self): # convert unit to proper type if isinstance(self.unit, str): # loop thru all unit types, finding the first with a member whose name matches `self.unit`, # then overwrite with the new value for cls in ( AmountUnits, TimeUnits, LengthUnits, AngleUnits, AreaUnits, RateUnits, AccelerationUnits, SpeedUnits, FlowRateUnits, RotationSpeedUnits): try: unit = next(filter(lambda e: e.name == self.unit, cls)) self.unit = unit return except StopIteration: pass raise AttributeError(f"Could not find correct unit-type for provided unit '{self.unit}'") def _check_type_and_convert(self, other): if isinstance(other, (int, float)): return other, self.unit if not isinstance(other, UnitValue): raise NotImplementedError( f"Can only apply an operation on a UnitValue and a number, not type {type(other)}") if type(self.unit) != type(other.unit): raise NotImplementedError( f"Cannot apply operation on UnitValue objects with different unit types (this={self.unit}, other={other.unit})") return other.unit.convert_to(other.value, self.unit), self.unit def _apply_operation(self, other: Number | 'UnitValue', operation: Callable[[Number | 'UnitValue', Number | 'UnitValue'], Number]): other_value, unit = self._check_type_and_convert(other) new_value = operation(self.value, other_value) return UnitValue(new_value, unit) def __call__(self, *args, **kwargs) -> float: if len(args) != 1 or kwargs: raise TypeError("Call to unit value only accepts 1 argument for the new unit to convert to.") new_unit: _UnitEnum = args[0] if not isinstance(new_unit, type(self.unit)): raise TypeError("Argument to unit value must be of the same type as the original units") return self.unit.convert_to(self.value, new_unit) def __add__(self, other: Number | 'UnitValue'): return self._apply_operation(other, lambda a, b: a + b) def __sub__(self, other: Number | 'UnitValue'): return self._apply_operation(other, lambda a, b: a - b) def __mul__(self, other: Number | 'UnitValue'): return self._apply_operation(other, lambda a, b: a * b) def __truediv__(self, other: Number | 'UnitValue'): return self._apply_operation(other, lambda a, b: a / b) def __iadd__(self, other): new_uv = self.__add__(other) self.value = new_uv.value def __isub__(self, other): new_uv = self.__sub__(other) self.value = new_uv.value def __imul__(self, other): new_uv = self.__mul__(other) self.value = new_uv.value def __itruediv__(self, other): new_uv = self.__truediv__(other) self.value = new_uv.value def __int__(self): return int(self.value) def __float__(self): return float(self.value) def __iter__(self): yield self.value yield self.unit