import copy
import datetime
import decimal
import math
import re
import pipeline.infrastructure.utils as utils
from . import unitformat
[docs]class ArcUnits(object):
DEGREE = { 'name' : 'DEGREE' , 'symbol' : 'd' , 'html' : '°' , 'units per circle' : decimal.Decimal(360) }
RADIAN = { 'name' : 'RADIAN' , 'symbol' : 'rad' , 'html' : 'rad' , 'units per circle' : decimal.Decimal(str(2*math.pi)) }
PERCENT = { 'name' : 'PERCENT' , 'symbol' : '%' , 'html' : '%' , 'units per circle' : decimal.Decimal(100) }
HOUR = { 'name' : 'HOUR' , 'symbol' : 'h' , 'html' : 'h' , 'units per circle' : decimal.Decimal(24) }
MINUTE = { 'name' : 'MINUTE' , 'symbol' : 'm' , 'html' : 'm' , 'units per circle' : decimal.Decimal(1440) }
SECOND = { 'name' : 'SECOND' , 'symbol' : 's' , 'html' : 's' , 'units per circle' : decimal.Decimal(86400) }
ARC_MINUTE = { 'name' : 'ARC_MINUTE' , 'symbol' : "'" , 'html' : ''' , 'units per circle' : decimal.Decimal(21600) }
ARC_SECOND = { 'name' : 'ARC_SECOND' , 'symbol' : '"' , 'html' : '"' , 'units per circle' : decimal.Decimal(1296000) }
MILLI_ARC_SECOND = { 'name' : 'MILLI_ARC_SECOND' , 'symbol' : 'mas' , 'html' : 'mas' , 'units per circle' : decimal.Decimal(1296000000) }
[docs]class DistanceUnits(object):
ANGSTROM = { 'name' : 'ANGSTROM' , 'symbol' : '\\u212B', 'metres' : decimal.Decimal('1e-10') }
NANOMETRE = { 'name' : 'NANOMETRE' , 'symbol' : 'nm' , 'metres' : decimal.Decimal('1e-9') }
MICROMETRE = { 'name' : 'MICROMETRE' , 'symbol' : '\\u00B5m', 'metres' : decimal.Decimal('1e-6') }
MILLIMETRE = { 'name' : 'MILLIMETRE' , 'symbol' : 'mm' , 'metres' : decimal.Decimal('0.001') }
CENTIMETRE = { 'name' : 'CENTIMETRE' , 'symbol' : 'cm' , 'metres' : decimal.Decimal('0.01') }
METRE = { 'name' : 'METRE' , 'symbol' : 'm' , 'metres' : decimal.Decimal(1) }
KILOMETRE = { 'name' : 'KILOMETRE' , 'symbol' : 'km' , 'metres' : decimal.Decimal(1000) }
MILE = { 'name' : 'MILE' , 'symbol' : 'mi' , 'metres' : decimal.Decimal('1609.347219') }
ASTRONOMICAL_UNIT = { 'name' : 'ASTRONOMICAL_UNIT' , 'symbol' : 'au' , 'metres' : decimal.Decimal(149597870691) }
LIGHT_SECOND = { 'name' : 'LIGHT_SECOND' , 'symbol' : 'ls' , 'metres' : decimal.Decimal(299792458) }
LIGHT_MINUTE = { 'name' : 'LIGHT_MINUTE' , 'symbol' : 'lm' , 'metres' : decimal.Decimal(17987547480) }
LIGHT_YEAR = { 'name' : 'LIGHT_YEAR' , 'symbol' : 'ly' , 'metres' : decimal.Decimal('9.4607304725808e15') }
PARSEC = { 'name' : 'PARSEC' , 'symbol' : 'pc' , 'metres' : decimal.Decimal('3.085677581306e16') }
KILOPARSEC = { 'name' : 'KILOPARSEC' , 'symbol' : 'kpc' , 'metres' : decimal.Decimal('3.085677581306e19') }
MEGAPARSEC = { 'name' : 'MEGAPARSEC' , 'symbol' : 'Mpc' , 'metres' : decimal.Decimal('3.085677581306e22') }
[docs]class FluxDensityUnits(object):
YOCTOJANSKY = { 'name' : 'YOCTOJANSKY' , 'symbol' : 'yJy' , 'Jy' : decimal.Decimal('1e-24') }
ZEPTOJANSKY = { 'name' : 'ZEPTOJANSKY' , 'symbol' : 'zJy' , 'Jy' : decimal.Decimal('1e-21') }
ATTOJANSKY = { 'name' : 'ATTOJANSKY' , 'symbol' : 'aJy' , 'Jy' : decimal.Decimal('1e-18') }
FEMTOJANSKY = { 'name' : 'FEMTOJANSKY' , 'symbol' : 'fJy' , 'Jy' : decimal.Decimal('1e-15') }
PICOJANSKY = { 'name' : 'PICOJANSKY' , 'symbol' : 'pJy' , 'Jy' : decimal.Decimal('1e-12') }
NANOJANSKY = { 'name' : 'NANOJANSKY' , 'symbol' : 'nJy' , 'Jy' : decimal.Decimal('1e-9') }
MICROJANSKY = { 'name' : 'MICROJANSKY' , 'symbol' : '\\u03BCJy', 'Jy' : decimal.Decimal('1e-6') }
MILLIJANSKY = { 'name' : 'MILLIJANSKY' , 'symbol' : 'mJy' , 'Jy' : decimal.Decimal('0.001') }
CENTIJANSKY = { 'name' : 'CENTIJANSKY' , 'symbol' : 'cJy' , 'Jy' : decimal.Decimal('0.01') }
DECIJANSKY = { 'name' : 'DECIJANSKY' , 'symbol' : 'dJy' , 'Jy' : decimal.Decimal('0.1') }
JANSKY = { 'name' : 'JANSKY' , 'symbol' : 'Jy' , 'Jy' : decimal.Decimal(1) }
DECAJANSKY = { 'name' : 'DECAJANSKY' , 'symbol' : 'daJy' , 'Jy' : decimal.Decimal(10) }
HECTOJANSKY = { 'name' : 'HECTOJANSKY' , 'symbol' : 'hJy' , 'Jy' : decimal.Decimal(100) }
KILOJANSKY = { 'name' : 'KILOJANSKY' , 'symbol' : 'kJy' , 'Jy' : decimal.Decimal(1000) }
MEGAJANSKY = { 'name' : 'MEGAJANSKY' , 'symbol' : 'MJy' , 'Jy' : decimal.Decimal('1e6') }
GIGAJANSKY = { 'name' : 'GIGAJANSKY' , 'symbol' : 'GJy' , 'Jy' : decimal.Decimal('1e9') }
TERAJANSKY = { 'name' : 'TERAJANSKY' , 'symbol' : 'TJy' , 'Jy' : decimal.Decimal('1e12') }
PETAJANSKY = { 'name' : 'PETAJANSKY' , 'symbol' : 'PJy' , 'Jy' : decimal.Decimal('1e15') }
ETAJANSKY = { 'name' : 'ETAJANSKY' , 'symbol' : 'EJy' , 'Jy' : decimal.Decimal('1e18') }
ZETTAJANSKY = { 'name' : 'ZETTAJANSKY' , 'symbol' : 'ZJy' , 'Jy' : decimal.Decimal('1e21') }
YOTTAJANSKY = { 'name' : 'YOTTAJANSKY' , 'symbol' : 'YJy' , 'Jy' : decimal.Decimal('1e24') }
[docs]class FrequencyUnits(object):
YOCTOHERTZ = { 'name' : 'YOCTOHERTZ' , 'symbol' : 'yHz' , 'hz' : decimal.Decimal('1e-24') }
ZEPTOHERTZ = { 'name' : 'ZEPTOHERTZ' , 'symbol' : 'zHz' , 'hz' : decimal.Decimal('1e-21') }
ATTOHERTZ = { 'name' : 'ATTOHERTZ' , 'symbol' : 'aHz' , 'hz' : decimal.Decimal('1e-18') }
FEMTOHERTZ = { 'name' : 'FEMTOHERTZ' , 'symbol' : 'fHz' , 'hz' : decimal.Decimal('1e-15') }
PICOHERTZ = { 'name' : 'PICOHERTZ' , 'symbol' : 'pHz' , 'hz' : decimal.Decimal('1e-12') }
NANOHERTZ = { 'name' : 'NANOHERTZ' , 'symbol' : 'nHz' , 'hz' : decimal.Decimal('1e-9') }
MICROHERTZ = { 'name' : 'MICROHERTZ' , 'symbol' : '\\u03BCHz', 'hz' : decimal.Decimal('1e-6') }
MILLIHERTZ = { 'name' : 'MILLIHERTZ' , 'symbol' : 'mHz' , 'hz' : decimal.Decimal('0.001') }
CENTIHERTZ = { 'name' : 'CENTIHERTZ' , 'symbol' : 'cHz' , 'hz' : decimal.Decimal('0.01') }
DECIHERTZ = { 'name' : 'DECIHERTZ' , 'symbol' : 'dHz' , 'hz' : decimal.Decimal('0.1') }
HERTZ = { 'name' : 'HERTZ' , 'symbol' : 'Hz' , 'hz' : decimal.Decimal(1) }
DECAHERTZ = { 'name' : 'DECAHERTZ' , 'symbol' : 'daHz' , 'hz' : decimal.Decimal(10) }
HECTOHERTZ = { 'name' : 'HECTOHERTZ' , 'symbol' : 'hHz' , 'hz' : decimal.Decimal(100) }
KILOHERTZ = { 'name' : 'KILOHERTZ' , 'symbol' : 'kHz' , 'hz' : decimal.Decimal(1000) }
MEGAHERTZ = { 'name' : 'MEGAHERTZ' , 'symbol' : 'MHz' , 'hz' : decimal.Decimal('1e6') }
GIGAHERTZ = { 'name' : 'GIGAHERTZ' , 'symbol' : 'GHz' , 'hz' : decimal.Decimal('1e9') }
TERAHERTZ = { 'name' : 'TERAHERTZ' , 'symbol' : 'THz' , 'hz' : decimal.Decimal('1e12') }
PETAHERTZ = { 'name' : 'PETAHERTZ' , 'symbol' : 'PHz' , 'hz' : decimal.Decimal('1e15') }
ETAHERTZ = { 'name' : 'ETAHERTZ' , 'symbol' : 'EHz' , 'hz' : decimal.Decimal('1e18') }
ZETTAHERTZ = { 'name' : 'ZETTAHERTZ' , 'symbol' : 'ZHz' , 'hz' : decimal.Decimal('1e21') }
YOTTAHERTZ = { 'name' : 'YOTTAHERTZ' , 'symbol' : 'YHz' , 'hz' : decimal.Decimal('1e24') }
[docs]class LinearVelocityUnits(object):
METRES_PER_SECOND = { 'name' : 'METRES_PER_SECOND' , 'symbol' : 'm/s' , 'mps' : decimal.Decimal(1) }
KILOMETRES_PER_SECOND = { 'name' : 'KILOMETERS_PER_SECOND' , 'symbol' : 'km/s' , 'mps' : decimal.Decimal(1000) }
Z = { 'name' : 'Z' , 'symbol' : 'Z' , 'mps' : decimal.Decimal(299792458) }
[docs]class FileSizeUnits(object):
BYTES = { 'name' : 'BYTES' , 'symbol' : 'b' , 'bytes' : decimal.Decimal(1) }
KILOBYTES = { 'name' : 'KILOBYTES', 'symbol' : 'kb', 'bytes' : decimal.Decimal('1024') }
MEGABYTES = { 'name' : 'MEGABYTES', 'symbol' : 'Mb', 'bytes' : decimal.Decimal('1048576') }
GIGABYTES = { 'name' : 'GIGABYTES', 'symbol' : 'Gb', 'bytes' : decimal.Decimal('1073741824') }
TERABYTES = { 'name' : 'TERABYTES', 'symbol' : 'Tb', 'bytes' : decimal.Decimal('1099511627776') }
[docs]class ComparableUnit(object):
__slots__ = ('value', 'units')
def __getstate__(self):
return self.value, self.units
def __setstate__(self, state):
self.value, self.units = state
def __init__(self):
raise Exception('Must override __init__ of ComparableUnit')
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return other.to_units(self.units) == self.value
def __ne__(self, other):
return not self.__eq__(other)
def __abs__(self):
if self.value < 0:
return self.__class__(-self.value, self.units)
else:
return self.__class__(self.value, self.units)
def __add__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(other.to_units(self.units) + self.value, self.units)
def __truediv__(self, other):
if isinstance(other, self.__class__):
return self.to_units() / other.to_units()
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for /: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(self.value / decimal.Decimal(str(other)), self.units)
def __floordiv__(self, other):
if isinstance(other, self.__class__):
return self.to_units() // other.to_units()
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for //: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(self.value // decimal.Decimal(str(other)), self.units)
def __ge__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for >=: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.value >= other.to_units(self.units)
def __gt__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for >: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.value > other.to_units(self.units)
def __itruediv__(self, other):
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for /=: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
self.value /= other
return self
def __ifloordiv__(self, other):
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for //=: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
self.value //= other
return self
def __le__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for <=: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.value <= other.to_units(self.units)
def __lt__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for <: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.value < other.to_units(self.units)
def __mul__(self, other):
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for *: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(self.value * other, self.units)
def __rmul__(self, other):
if not isinstance(other, (int, float, decimal.Decimal)):
raise TypeError("unsupported operand type(s) for *: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(self.value * other, self.units)
def __repr__(self):
return '%s %s' % (self.value, self.units['symbol'])
def __sub__(self, other):
if not isinstance(other, self.__class__):
raise TypeError("unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__name__,
other.__class__.__name__))
return self.__class__(self.value - other.to_units(self.units), self.units)
[docs] def convert_to(self, newUnits=None):
raise Exception('Must override convert_to of ComparableUnit')
[docs] def to_units(self, otherUnits=None):
raise Exception('Must override to_units of ComparableUnit')
[docs]class Distance(ComparableUnit):
def __init__(self, value=0, units=DistanceUnits.KILOMETRE):
"""Creates a new distance with the given magnitude and units.
If no arguments are given, a new distance of 0 km is created. If no
units are given, kilometres are assumed.
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=DistanceUnits.METRE):
"""Converts this measure of distance to the new units.
After this method is complete this distance will have units of newUnits
and its value will have been converted accordingly.
newUnits
the new units for this distance (default: m)
Returns:
this distance. The reason for this return type is to allow code of
this nature:
kilometers = myDistance.convert_to(DistanceUnits.KILOMETRES).value
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=DistanceUnits.METRE):
"""Returns the magnitude of this distance in otherUnits.
Note that this method does not alter the state of this distance.
Contrast this with convert_to(DistanceUnits).
otherUnits
the units in which to express this distance's magnitude.
Returns:
this distance's value converted to otherUnits.
"""
factor = self.units['metres'] / otherUnits['metres']
return self.value * factor
def __str__(self):
return unitformat.distance.format(self.to_units(DistanceUnits.METRE))
def __repr__(self):
return 'Distance(%s, DistanceUnits.%s)' % (self.value,
self.units['name'])
[docs]class EquatorialArc(ComparableUnit):
def __init__(self, value=0, units=ArcUnits.DEGREE):
"""Creates a new distance with the given magnitude and units.
If no arguments are given, a new arc of 0 degrees is created. If no
units are given, degrees are assumed.
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=ArcUnits.DEGREE):
"""Converts this arc to the new units.
After this method is complete this arc will have units of units and its
value will have been converted accordingly.
newUnits
the new units for this arc. default: degrees
Returns:
this arc. The reason for this return type is to allow code of this
nature:
radians = myArc.convert_to(ArcUnits.RADIAN).value;
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=ArcUnits.DEGREE):
"""Returns the magnitude of this arc in otherUnits.
Note that this method does not alter the state of this arc. Contrast
this with convert_to(ArcUnits).
otherUnits
the units in which to express this arc's magnitude (default: degrees)
Returns:
this arc's value converted to otherUnits.
"""
factor = otherUnits['units per circle'] / self.units['units per circle']
return self.value * factor
[docs] def toDms(self):
"""Returns a representation of this arc in degrees, minutes, and
seconds.
Returns:
a tuple of size three in this order:
An integer holding the number of degrees.
An integer holding the number of arc minutes.
A float holding the number of arc seconds.
"""
degrees = self.to_units(ArcUnits.DEGREE)
dd = abs(degrees)
mm = dd * decimal.Decimal(60)
ss = dd * decimal.Decimal(3600)
dd = int(dd)
mm = int(mm - 60*dd)
ss = float(ss - 3600*dd - 60*mm)
if degrees < 0:
dd *= -1
# sometimes have an overflow condition where secs=60
if ss == 60:
ss = 0
mm += 1
if mm == 60:
dd += 1
mm = 0
return dd, mm, ss
[docs] def toHms(self):
"""Returns a representation of this arc in hours, minutes, and seconds.
Returns:
a tuple of size three in this order:
An integer holding the number of hours.
An integer holding the number of minutes.
A float holding the number of seconds.
"""
return (self / 15).toDms()
def __repr__(self):
return 'EquatorialArc(%s, ArcUnits.%s)' % (self.value,
self.units['name'])
[docs]class FluxDensity(ComparableUnit):
def __init__(self, value=0, units=FluxDensityUnits.JANSKY):
"""Create a new flux density with the given magnitude and units.
If called without arguments, the constructor will create a
default frequency of 0 Janskys
value
the magnitude for this flux density
units
the new units for this flux density
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=FluxDensityUnits.JANSKY):
"""Converts this measure of flux density to the new units.
After this method is complete this flux density will have units of
units and its value will have been converted accordingly.
newUnits
the new units for this flux density (default: Jy)
Returns:
this flux density. The reason for this return type is to allow code
of this nature:
janskies = myFluxDensity.convert_to(FluxDensityUnits.JANSKY)
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=FluxDensityUnits.JANSKY):
"""Returns the magnitude of this flux density in otherUnits.
Note that this method does not alter the state of this flux density.
Contrast this with convert_to(FluxDensityUnits).
otherUnits
the units in which to express this flux density's magnitude.
Returns:
this flux density's value converted to otherUnits.
"""
factor = self.units['Jy'] / otherUnits['Jy']
return self.value * factor
def __str__(self):
return unitformat.flux.format(self.to_units(FluxDensityUnits.JANSKY))
def __repr__(self):
return 'FluxDensity(%s, FluxDensityUnits.%s)' % (self.value,
self.units['name'])
[docs]class LinearVelocity(ComparableUnit):
def __init__(self, value=0, units=LinearVelocityUnits.KILOMETRES_PER_SECOND):
"""Create a new linear velocity with the given magnitude and units.
If called without arguments, the constructor will create a
default linear velocity of 0 kilometres per second.
value
the magnitude for this linear velocity
units
the new units for this linear velocity
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=LinearVelocityUnits.KILOMETRES_PER_SECOND):
"""Converts this measure of linear velocity to the new units.
After this method is complete this linear veloity will have units of
units and its value will have been converted accordingly.
newUnits
the new units for this linear velocity. If newUnits is None an
IllegalArgumentException will be thrown.
Returns:
this linear velocity. The reason for this return type is to allow
code of this nature:
velocity = myLinearVelocity.convert_to(LinearVelocityUnits.Z)
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=LinearVelocityUnits.KILOMETRES_PER_SECOND):
"""Returns the magnitude of this linear velocity in otherUnits.
Note that this method does not alter the state of this linear velocity.
Contrast this with convert_to(LinearVelocityUnits).
otherUnits
the units in which to express this linear velocity's magnitude. If
otherUnits is None an IllegalArgumentException will be thrown.
Returns:
this linear velocity's value converted to otherUnits.
"""
factor = self.units['mps'] / otherUnits['mps']
return self.value * factor
def __str__(self):
mps = self.to_units(LinearVelocityUnits.METRES_PER_SECOND)
return unitformat.velocity.format(mps)
def __repr__(self):
return ('LinearVelocity(%s, '
'LinearVelocityUnits.%s)' % (self.value, self.units['name']))
[docs]class FileSize(ComparableUnit):
def __init__(self, value=0, units=FileSizeUnits.MEGABYTES):
"""Creates a new file size with the given magnitude and units.
If called without arguments, the constructor will create a
default size of 0 megabytes.
value
the magnitude for this file size
units
the new units for this file size
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=FileSizeUnits.MEGABYTES):
"""Converts this measure of file size to the new units.
After this method is complete this file size will have units of
newUnits and its value will have been converted accordingly.
newUnits
the new units for this file size.
Returns:
this file size. The reason for this return type is to allow code of
this nature:
gigabytes = myFileSize.convert_to(FrequencyUnits.GIGABYTES)
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=FileSizeUnits.GIGABYTES):
"""Returns the magnitude of this file size in otherUnits.
Note that this method does not alter the state of this file size.
Contrast this with convert_to(FileSizeUnits).
otherUnits
the units in which to express this file size's magnitude. If
newUnits is None, it will be treated as FileSizeUnits.GIGABYTES.
Returns:
this file size's value converted to otherUnits.
"""
factor = self.units['bytes'] / otherUnits['bytes']
return self.value * factor
def __str__(self):
return unitformat.file_size.format(self.to_units(FileSizeUnits.BYTES))
def __repr__(self):
return 'FileSize(%s, FileSizeUnits.%s)' % (self.value,
self.units['name'])
[docs]class Frequency(ComparableUnit):
def __init__(self, value=0, units=FrequencyUnits.GIGAHERTZ):
"""Creates a new frequency with the given magnitude and units.
If called without arguments, the constructor will create a
default frequency of 0 gigahertz.
value
the magnitude for this frequency
units
the new units for this frequency
"""
if isinstance(value, (float, int)):
value = str(value)
self.value = decimal.Decimal(value)
self.units = units
[docs] def convert_to(self, newUnits=FrequencyUnits.GIGAHERTZ):
"""Converts this measure of frequency to the new units.
After this method is complete this frequency will have units of
newUnits and its value will have been converted accordingly.
newUnits
the new units for this frequency.
Returns:
this frequency. The reason for this return type is to allow code of
this nature:
gigahertz = myFrequency.convert_to(FrequencyUnits.GIGAHERTZ)
"""
self.value = self.to_units(newUnits)
self.units = newUnits
return self
[docs] def to_units(self, otherUnits=FrequencyUnits.GIGAHERTZ):
"""Returns the magnitude of this frequency in otherUnits.
Note that this method does not alter the state of this frequency.
Contrast this with convert_to(FrequencyUnits).
otherUnits
the units in which to express this frequency's magnitude. If
newUnits is None, it will be treated as FrequencyUnits.GIGAHERTZ.
Returns:
this frequency's value converted to otherUnits.
"""
factor = self.units['hz'] / otherUnits['hz']
return self.value * factor
[docs] def str_to_precision(self, precision):
"""
Return the string representation of this Frequency to a fixed number
of decimal places.
:param precision:
:return:
"""
f = unitformat.get_frequency_format(precision)
return f.format(self.to_units(FrequencyUnits.HERTZ))
def __str__(self):
return unitformat.frequency.format(self.to_units(FrequencyUnits.HERTZ))
def __repr__(self):
return 'Frequency(%s, FrequencyUnits.%s)' % (self.value,
self.units['name'])
[docs]class FrequencyRange(object):
__slots__ = ('low', 'high')
def __getstate__(self):
return self.low, self.high
def __setstate__(self, state):
self.low, self.high = state
def __init__(self, frequency1=None, frequency2=None):
"""Creates a new instance with the given endpoints.
This method will set the lower value of its range to the lesser of the
two parameter values. If either parameter is null, it will be
interpreted as a signal to create a new default frequency. If both
parameters are null, a frequency range spanning all positive
frequencies will be returned.
Note that this method makes copies of the parameters; it does not
maintain a reference to either parameter. This is done in order to
maintain the integrity of the relationship between the starting and
ending points of this interval.
frequency1
one endpoint of this range.
frequency2
the other endpoint of this range.
"""
if frequency1 == frequency2 is None:
frequency2 = Frequency(decimal.Decimal('Infinity'))
self.set(frequency1, frequency2)
def __composite_values__(self):
return [self.low, self.high]
def __eq__(self, other):
if isinstance(other, FrequencyRange):
return other.low == self.low and other.high == self.high
return False
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '<FrequencyRange(%s, %s)>' % (self.low, self.high)
[docs] def contains(self, frequency=None):
"""Returns true if this range contains frequency.
The frequency argument be a frequency or a frequency range. If the
argument given is a Frequency range, then FrequencyRange A is said to
contain range B if A's low frequency is less than or equal to B's low
frequency and A's high frequency is greater than or each to B's high
frequency. Notice that this means that if A equals B, it also contains
B.
frequency
the frequency or range to test for inclusion in this range.
Returns:
True if this range contains frequency. If frequency is None, the
return value will be false.
"""
if isinstance(frequency, Frequency):
return frequency >= self.low and frequency <= self.high
if isinstance(frequency, FrequencyRange):
return frequency.low >= self.low and frequency.high <= self.high
return False
[docs] def convert_to(self, newUnits=FrequencyUnits.GIGAHERTZ):
"""Converts both endpoints of this range to the given units.
After this method is complete both endpoints of this range will have
units of units, and their values will have been converted accordingly.
newUnits
the new units for the endpoints of this range. If no units are
specified, it will be treated as FrequencyUnits.GIGAHERTZ.
Returns:
this range.
"""
self.low.convert_to(newUnits)
self.high.convert_to(newUnits)
return self
[docs] def getCentreFrequency(self):
"""Returns the frequency that is midway between the endpoints of this
range.
The units for the returned frequency will be the same as those of the
high frequency of this range.
Returns:
the center of this range.
"""
c = (self.low + self.high) / 2
return c.convert_to(self.high.units)
[docs] def getOverlapWith(self, other):
"""Returns a new range that represents the region of overlap between
this range and other. If there is no overlap, None is returned.
other
another range that may overlap this one.
Returns:
the overlapping region of this range and other.
"""
if self.overlaps(other):
if self.low > other.low:
return FrequencyRange(self.low, other.high)
else:
return FrequencyRange(other.low, self.high)
return None
[docs] def getGapBetween(self, other=None):
"""Returns a new range that represents the region of frequency space
between this range and other. If the other range is coincident with, or
overlaps, this range, None is returned. If the other range is None,
None is returned.
other
another range that might not overlap this one.
Returns:
the frequency gap between this range and other.
"""
if other is None or self.overlaps(other):
return None
if self.low > other.low:
return FrequencyRange(other.high, self.low)
else:
return FrequencyRange(self.high, other.low)
[docs] def getWidth(self):
"""Returns the width of this range.
The units for the returned frequency will be the same as those of the
high frequency of this range.
Returns:
the width of this range.
"""
return self.high - self.low
[docs] def overlaps(self, other=None):
"""Returns true if this frequency range overlaps with other.
Remember that this range is a closed interval, that is, one that
contains both of its endpoints.
If other is None, the return value is false.
other
another range that may overlap this one.
Returns:
true if this range overlaps with other.
"""
if isinstance(other, FrequencyRange):
if self.low < other.low:
return self.high >= other.low
else:
return other.high >= self.low
return False
[docs] def set(self, frequency1=None, frequency2=None):
"""Sets the frequencies of this range.
This method will set the lower value of its range to the lesser of the
two parameter values. If either parameter is None, it will be
interpreted as a signal to create a new default frequency (0 GHz).
Note that this method makes copies of the parameters; it does not
maintain a reference to either parameter. This is done in order to
maintain the integrity of the relationship between the starting and
ending points of this interval.
frequency1
one endpoint of this range.
frequency2
the other endpoint of this range.
"""
if frequency1 is None:
frequency1 = Frequency()
if frequency2 is None:
frequency2 = Frequency()
if frequency1 > frequency2:
self.high = Frequency(frequency1.value, frequency1.units)
self.low = Frequency(frequency2.value, frequency2.units)
else:
self.low = Frequency(frequency1.value, frequency1.units)
self.high = Frequency(frequency2.value, frequency2.units)
[docs]class Latitude(EquatorialArc):
patt = re.compile('\s*' +
'(?P<degs>[-+]?\d+)' +
'\s*:?\s*' +
'(?P<mins>\d+)' +
'\s*:?\s*' +
'(?P<secs>\d+\.?\d*)' +
'\s*')
def __init__(self, value=0, units=ArcUnits.DEGREE):
"""Creates a new latitude with the given magnitude and units.
If no magnitude or units are give, a latitude of 0 degrees will be
created.
If magnitude is not a valid value for latitude, it will be normalised
in a way that will transform it to a legal value. To be legal,
magnitude must be greater than or equal to the negative of one-quarter
of a circle and less than or equal to one-quarter of a circle, in the
given units.
value
the latitude magnitude
units
the units of the latitude
"""
super(Latitude, self).__init__(value, units)
perCircle = self.units['units per circle']
perHalf = perCircle / 2
perQuarter = perCircle / 4
threeQuarters = perHalf + perQuarter
# normalize value to < 360 degrees
self.value -= (self.value // perCircle) * perCircle
if self.value > threeQuarters:
self.value = -perCircle + self.value
if self.value > perQuarter:
self.value = perHalf - self.value
if self.value < -threeQuarters:
self.value = perCircle + self.value
if self.value < -perQuarter:
self.value = -perHalf - self.value
[docs] @staticmethod
def parse(value):
"""Returns a new Latitude based on the given text.
See the parse method of Angle for information on the format of text.
This Latitude class offers two other formats:
dd:mm:ss.sss
dd mm ss.sss
Both of the above are in degrees, arc-minutes, and arc-seconds. For the
first alternative form, whitespace is permitted around the colon
characters. For the second alternative form, any type and number of
whitespace characters may be used in between the three parts.
The parsed value, if not a legal value for latitude, will be normalised
in such a way that it is transformed to a legal value. To be legal,
magnitude must be greater than or equal to the negative of one-quarter
of a circle and less than or equal to one-quarter of a circle, in the
given units.
text
a string that will be converted into a latitude.
Returns:
a new Latitude. If parsing was successful, the value of the
latitude will be based on the parameter string. If it was not, the
returned latitude will be of zero degrees.
Throws:
ValueError - if text is not in the expected form.
"""
m = Latitude.patt.match(value)
try:
y = abs(decimal.Decimal(m.group('degs'))) + decimal.Decimal(m.group('mins'))/60 \
+ decimal.Decimal(m.group('secs'))/3600
except AttributeError:
raise ValueError
# Check & fix for negativity
if m.group('degs').startswith('-'):
y *= -1
return Latitude(y, ArcUnits.DEGREE)
[docs] def isNorthOfEquator(self):
"""Returns True if this latitude is north of the equator.
Returns:
True if this latitude is north of the equator.
"""
return self.value > 0
[docs] def isSouthOfEquator(self):
"""Returns True if this latitude is south of the equator.
Returns:
True if this latitude is south of the equator.
"""
return self.value < 0
[docs] def isNorthOf(self, other):
"""Returns True if this latitude is north of other.
other
the latitude to be tested.
Returns:
True if this latitude is north of other.
"""
return self > other
[docs] def isSouthOf(self, other):
"""Returns True if this latitude is south of other.
other
the latitude to be tested.
Returns:
True if this latitude is south of other.
"""
return self < other
def __repr__(self):
return 'Latitude(%s, ArcUnits.%s)' % (self.value,
self.units['name'])
def __str__(self):
(d, m, s) = self.toDms()
return '%+.2d%s%.2d%s%05.2f%s' % (d, ArcUnits.DEGREE['symbol'],
m, ArcUnits.ARC_MINUTE['symbol'],
utils.round_half_up(s, 2), ArcUnits.ARC_SECOND['symbol'])
[docs]class Longitude(EquatorialArc):
patt = re.compile('\s*' +
'(?P<hours>\d+)' +
'\s*:?\s*' +
'(?P<mins>\d+)' +
'\s*:?\s*' +
'(?P<secs>\d+\.?\d*)' +
'\s*')
def __init__(self, value=0, units=ArcUnits.DEGREE):
"""Creates a new longitude with the given magnitude and units.
If magnitude is not a valid value1 for longitude, it will be normalised
in a way that will transform it to a legal value. To be legal,
magnitude must be greater than or equal to zero and less than one full
circle, in the given units.
If no arguments are given, a longitude of 0 degrees will be created.
value
the magnitude of the longitude
units
the units of longitude
"""
super(Longitude, self).__init__(value, units)
perCircle = self.units['units per circle']
# normalize value to < 360 degrees
self.value -= (self.value // perCircle) * perCircle
if self.value < 0:
self.value += perCircle
[docs] @staticmethod
def parse(value):
"""Returns a new longitude based on the given text.
See the parse method of Angle for information on the format of text.
This Longitude class offers two other formats:
hh:mm:ss.sss
hh mm ss.sss
Both of the above are in hours, minutes, and seconds. For the first
alternative form, whitespace is permitted around the colon characters.
For the second alternative form, any type and number of whitespace
characters may be used in between the three parts.
The parsed value, if not a legal value for longitude, will be
normalised in such a way that it is transformed to a legal value. To be
legal, magnitude must be greater than or equal zero and less than or
equal to one full circle, in the given units.
text
a string that will be converted into a longitude.
Returns:
a new Longitude. If parsing was successful, the value of the
Longitude will be based on the parameter string. If it was not, the
returned longitude will be of zero degrees.
Throws:
ValueError - if text is not in the expected form.
"""
match = Longitude.patt.match(value)
try:
h = decimal.Decimal(match.group('hours')).to_integral()
m = decimal.Decimal(match.group('mins')).to_integral()
s = decimal.Decimal(match.group('secs'))
x = 15*h + 15*m/60 + 15*s/3600
return Longitude(x, ArcUnits.DEGREE)
except AttributeError:
raise ValueError
[docs] def isOpposite(self, other):
"""Returns True if this longitude and other are separated by one half
circle.
other
the other longitude to be tested.
Returns:
True if other is separated from this longitude by one half circle.
"""
halfCircle = self.units['units per circle'] / 2
difference = abs(self.value - other.to_units(self.units))
return difference == halfCircle
[docs] def isEastOf(self, other):
"""Returns true if this longitude is east of other.
One longitude is east of another if there are fewer lines of longitude
to cross by travelling eastward along a given latitude than there would
be by travelling westward along that same latitude.
Two special cases are worth noting. First, a longitude that is equal to
this one is neither east nor west of this one. Second, a longitude that
is opposite this one is both east and west of this one.
other
the longitude to be tested.
Returns:
True if this longitude is east of other.
"""
o = other.to_units(self.units)
# calculate opposite angle
r = self.units['units per circle'] / 2 + self.value
return o < self.value or o >= r
[docs] def isWestOf(self, other):
"""Returns True if this longitude is west of other.
One longitude is west of another if there are fewer lines of longitude
to cross by travelling westward along a given latitude than there would
be by travelling eastward along that same latitude.
Two special cases are worth noting. First, a longitude that is equal to
this one is neither east nor west of this one. Second, a longitude that
is opposite this one is both east and west of this one.
other
the longitude to be tested.
Returns:
True if this longitude is west of other.
"""
o = other.to_units(self.units)
# calculate opposite angle
r = self.units['units per circle'] / 2 + self.value
return not (o <= self.value or o > r)
def __repr__(self):
return 'Longitude(%s, ArcUnits.%s)' % (self.value,
self.units['name'])
def __str__(self):
(h, m, s) = self.toHms()
return '%.2d%s%.2d%s%05.2f%s' % (h, ArcUnits.HOUR['symbol'],
m, ArcUnits.MINUTE['symbol'],
utils.round_half_up(s, 2), ArcUnits.SECOND['symbol'])
[docs]class TemporalCollection(object):
def __init__(self):
self.__contents = {}
self.__milestoneCache = None
def __getitem__(self, i):
return self.__milestones()[i]
[docs] def get(self, when=None):
"""Returns the value that was effective on the given date
"""
if when is None:
when = datetime.datetime.utcnow()
for m in self.__milestones():
thisDate = m
if thisDate <= when:
return self.__contents.get(thisDate)
raise LookupError("No records that early")
[docs] def put(self, at, item):
# The item is valid from the supplied date onwards
self.__contents[at] = copy.deepcopy(item)
self.__milestoneCache = None
def __milestones(self):
"""A list of all the dates where the value changed, returned in order
latest first
"""
if self.__milestoneCache is None:
self.__calculateMilestones()
return self.__milestoneCache
def __calculateMilestones(self):
self.__milestoneCache = list(self.__contents.keys())
self.__milestoneCache.sort(reverse=True)
def __clearMilestoneCache(self):
self.__milestoneCache = None
[docs]class TimeInterval(object):
__slots__ = ('start', 'end')
def __getstate__(self):
return self.start, self.end
def __setstate__(self, state):
self.start, self.end = state
def __init__(self, start=None, end=None):
self.start = start
self.end = end
def __composite_values__(self):
return (self.start, self.end)
def __set_composite_values__(self, start, end):
self.start = start
self.end = end
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return other.start == self.start and other.end == self.end
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return 'TimeInterval(%s, %s)' % (self.start, self.end)
[docs] def contains(self, time):
"""Returns True if time is contained in this interval.
Note that this interval is half-open; it does not include its ending
point. Note also that an interval that is equal to this one is not
contained by this one. The best analogy is that of a rigid box with
infinitely thin walls: a box that is exactly the same as another cannot
fit inside it.
time
the datetime or TimeInterval to be tested for containment.
Returns:
True if time is contained in this interval.
"""
if isinstance(time, datetime.datetime):
return time >= self.start and time < self.end
if isinstance(time, TimeInterval):
if time.end == TimeInterval.FOREVER and self.end == TimeInterval.FOREVER:
return self.start < time.start
return self.contains(time.start) and self.contains(time.end)
return False
[docs] def isEmpty(self):
"""Returns True if this TimeInterval is empty
"""
return self.start > self.end
[docs] def overlaps(self, ti):
"""Returns True if this interval overlaps with the given interval.
"""
if isinstance(ti, TimeInterval):
return ti.contains(self.start) or ti.contains(self.end) or self.contains(ti)
return False
[docs] def startingFrom(start):
"""Returns an open-ended TimeInterval starting from the given time.
"""
return TimeInterval(start, TimeInterval.FOREVER)
[docs] def startingFromNow():
"""Returns an open-ended TimeInterval starting from now.
"""
return TimeInterval(datetime.datetime.utcnow(), TimeInterval.FOREVER)
FOREVER = datetime.datetime(9999, 12, 31)
startingFrom = staticmethod(startingFrom)
startingFromNow = staticmethod(startingFromNow)