#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
import copy
import numpy
from footprints import FootprintBase
from epygram import config, epygramError
[docs]class RecursiveObject(object):
"""
Generic abstract class implementing useful recursive properties:
- display of object: the *__str__* method returns str(attr) for each of the object's attributes,
with automatical indentation for readability.
- test of (in)equality: the a == b test will be true if a and b have the same attributes and *a.attr == b.attr* for each attribute.
"""
def _strItem(self, item, reclevel=1):
"""
Recursive display of object attributes.
"""
offset = "".rjust(reclevel*4)
itemstring = ""
if isinstance(item, FootprintBase):
itemstring += item.__class__.__name__+" containing:"
for attr in item.__dict__.keys():
if attr == '_attributes':
for i in item.__dict__[attr].keys():
itemstring += "\n"+offset+i+": "+self._strItem(item.__dict__[attr][i], reclevel+1)
elif attr not in ('_puredict', '_observer'): # not supposed to be accessed from outside
itemstring += "\n"+offset+attr+": "+self._strItem(item.__dict__[attr], reclevel+1)
elif isinstance(item, list):
if isinstance(item[0], RecursiveObject):
itemstring = "[" + repr(item[0]) + ", ... (" + str(len(item)) + " objects)]"
else:
itemstring = str(numpy.array(item))
elif isinstance(item, RecursiveObject):
itemstring += item.__class__.__name__+" containing:"
for attr in item.__dict__.keys():
itemstring += "\n"+offset+attr+": "+self._strItem(item.__dict__[attr], reclevel+1)
elif isinstance(item, dict):
for key in sorted(item.keys()):
itemstring += "\n"+offset+key+": "+self._strItem(item[key], reclevel+1)
else:
itemstring = str(item)
return itemstring
def __str__(self):
"""
Recursive display of object: displays each of its attributes with indentation.
"""
return self._strItem(self)
def __eq__(self, other):
"""
Test of equality by recursion on the object's attributes.
"""
if self.__class__ == other.__class__ and self.__dict__.keys() == other.__dict__.keys():
ok = True
for attr in self.__dict__.keys():
if type(self.__dict__[attr]) == float and type(other.__dict__[attr]) == float:
# tolerance for floats
if abs(self.__dict__[attr] - other.__dict__[attr]) > config.epsilon:
ok = False
break
elif type(self.__dict__[attr]) == numpy.ndarray and type(other.__dict__[attr]) == numpy.ndarray:
# and numpy arrays of floats
if (abs(self.__dict__[attr] - other.__dict__[attr]) > config.epsilon).any():
ok = False
break
elif type(self.__dict__[attr]) == type(dict()):
if attr not in ('_puredict', '_observer'): # because they not always contain what we could expect them to...
for k in self.__dict__[attr].keys():
if self.__dict__[attr][k] != other.__dict__[attr][k]:
#print k
#print self.__dict__[attr][k],
#print other.__dict__[attr][k]
ok = False
break
else:
if self.__dict__[attr] != other.__dict__[attr]:
ok = False
break
else:
ok = False
return ok
def __ne__(self, other):
return not self == other
[docs] def copy(self):
""" Returns a copy of the object. """
return copy.copy(self)
[docs] def deepcopy(self):
""" Returns a deepcopy of the object. """
return copy.deepcopy(self)
[docs]class Angle(RecursiveObject):
"""
This class handles an angle.
It enables conversions of units, while saving the original unit and value
of the angle at its construction.
Available units: 'degrees', 'radians', 'cos_sin' (cos, sin), 'DMS' (Degree, Minutes, Seconds).
"""
deg = 'degrees'
rad = 'radians'
trig = 'cos_sin'
dms = 'DMS'
units = set([deg, dms, rad, trig])
def __init__(self, value, unit):
"""
Constructor.
'unit' argument must be among:
- 'degrees',
- 'DMS' - in which case, value is a tuple (degrees, minutes, seconds),
- 'radians',
- 'cos_sin' - in which case, value is a tuple (cos, sin).
"""
if unit in Angle.units:
self.__dict__['_'+unit] = value
if unit in ('degrees', 'radians'):
if unit == 'degrees':
circle = 360.
elif unit == 'radians':
circle = 2. * math.pi
while self.__dict__['_'+unit] > circle/2.:
self.__dict__['_'+unit] -= circle
while self.__dict__['_'+unit] < -circle/2.:
self.__dict__['_'+unit] += circle
else:
raise ValueError("this angle unit is not implemented: "+str(unit))
self._origin_unit = unit
self._origin_value = value
def __eq__(self, other):
"""
Redefinition because of dynamism of buffering new computed values...
"""
if not isinstance(other, Angle):
raise ValueError("cannot compare instances of different classes.")
if abs(self.get('radians') - other.get('radians')) <= config.epsilon:
ok = True
else:
ok = False
return ok
def __ne__(self, other):
return not self == other
[docs] def get(self, unit=None):
"""
Returns the angle in the requested unit.
If no unit is supplied, the origin unit is used.
"""
if unit == None:
unit = self._origin_unit # or a default one ?
elif unit in Angle.units:
if not self.__dict__.has_key('_'+unit):
self._compute(unit)
else:
raise ValueError("this angle unit is not implemented: "+str(unit))
return self.__dict__['_'+unit]
def _compute(self, unit):
"""
Compute the angle in the requested unit, from the original one.
See constructor for more details about units.
"""
# conversion to degrees
if unit == Angle.deg:
if self._origin_unit == Angle.rad:
self.__dict__['_'+unit] = math.degrees(self._origin_value)
elif self._origin_unit == Angle.trig:
self.__dict__['_'+unit] = math.degrees(math.copysign(math.acos(self._origin_value[0]), self._origin_value[1]))
elif self._origin_unit == Angle.dms:
self.__dict__['_'+unit] = self._origin_value[0] + self._origin_value[1]/60. + self._origin_value[2]/3600.
else:
raise NotImplementedError("conversion from this unit ("+self._origin_unit+") is not coded.")
# conversion to radians
elif unit == Angle.rad:
if self._origin_unit == Angle.deg:
self.__dict__['_'+unit] = math.radians(self._origin_value)
elif self._origin_unit == Angle.trig:
self.__dict__['_'+unit] = math.copysign(math.acos(self._origin_value[0]), self._origin_value[1])
elif self._origin_unit == Angle.dms:
self.__dict__['_'+unit] = math.radians(self._origin_value[0] + self._origin_value[1]/60. + self._origin_value[2]/3600.)
else:
raise NotImplementedError("conversion from this unit ("+self._origin_unit+") is not coded.")
# conversion to (cos, sin)
elif unit == Angle.trig:
if self._origin_unit == Angle.deg:
self.__dict__['_'+unit] = (math.cos(math.radians(self._origin_value)), math.sin(math.radians(self._origin_value)))
elif self._origin_unit == Angle.rad:
self.__dict__['_'+unit] = (math.cos(self._origin_value), math.sin(self._origin_value))
elif self._origin_unit == Angle.dms:
anglerad = math.radians(self._origin_value[0] + self._origin_value[1]/60. + self._origin_value[2]/3600.)
self.__dict__['_'+unit] = (math.cos(anglerad), math.sin(anglerad))
else:
raise NotImplementedError("conversion from this unit ("+self._origin_unit+") is not coded.")
# conversion to (degrees, minutes, seconds)
elif unit == Angle.dms:
if self._origin_unit == Angle.deg:
decdeg = self._origin_value
elif self._origin_unit == Angle.rad:
decdeg = math.degrees(self._origin_value)
elif self._origin_unit == Angle.trig:
decdeg = math.degrees(math.copysign(math.acos(self._origin_value[0]), self._origin_value[1]))
else:
raise NotImplementedError("conversion from this unit ("+self._origin_unit+") is not coded.")
sign = int(math.copysign(1, decdeg))
decdeg = decdeg * sign
degrees = int(decdeg)
decmin = (decdeg - degrees)*60.
minutes = int(decmin)
seconds = (decmin - minutes)*60
self.__dict__['_'+unit] = (degrees * sign, minutes, seconds)
else:
raise NotImplementedError("conversion to this unit ("+unit+") is not coded.")
#################
### FUNCTIONS ###
#################
[docs]def find_re_in_list(regexp, a_list):
""" Finds all strings from a list that match a regular expression. """
import re
pattern = re.subn('\.', r'\.', regexp)[0] # protect '.'
pattern = pattern.replace('?', '.') # change unix '?' to python '.' (any char)
pattern = pattern.replace('*', '.*') # change unix '*' to python '.*' (several any char)
pattern += '(?!.)'
found = []
for field in a_list:
if re.match(pattern, field.strip()):
found.append(field)
return found
def nicedeco(decorator):
def new_decorator(f):
g = decorator(f)
g.__name__ = f.__name__
g.__doc__ = f.__doc__
g.__dict__.update(f.__dict__)
return g
return new_decorator
[docs]def degrees_nearest_mod(d, ref):
"""
Returns the angle(s) **d** in the modulo nearest to **ref**.
"""
try:
n = len(d)
scalar = False
except Exception:
n = 1
d = [d]
scalar = True
d = numpy.array(d)
d_inf = d - 360.
d_sup = d + 360.
result = numpy.zeros(n)
for i in range(n):
if abs(d[i]-ref) <= abs(d_sup[i]-ref) and abs(d[i]-ref) <= abs(d_inf[i]-ref):
result[i] = d[i]
elif abs(d_inf[i]-ref) <= abs(d_sup[i]-ref) and abs(d_inf[i]-ref) <= abs(d[i]-ref):
result[i] = d_inf[i]
else:
result[i] = d_sup[i]
if scalar:
result = result[0]
return result
[docs]def make_custom_cmap(filename):
"""
Creates a custom Colormap from a set of RGB colors in a file with the
following formating:
r1,g1,b1;
r2,g2,b2;
...
rn,gn,bn
each value being comprised between 0 and 255
i.e. coming from http://colormap.org.
"""
import matplotlib
with open(filename, 'r') as f:
colors = f.readlines()
for i in range(len(colors)):
colors[i] = colors[i].replace(';','')
colors[i] = colors[i].replace('[','')
colors[i] = colors[i].replace(']','')
colors[i] = colors[i].replace('\n','')
colors[i] = colors[i].split(',')
colors = numpy.array(colors, dtype=numpy.float64)
cm = matplotlib.colors.ListedColormap(colors/255.)
return cm
[docs]def add_cmap(cmap):
"""
Reads and registers the epygram-or-user-colormap called *cmap*,
which must be either in config.epygram_colormaps or config.usercolormaps.
"""
import matplotlib.pyplot as plt
if cmap not in plt.colormaps():
if cmap in config.colormaps.keys():
plt.register_cmap(name=cmap, cmap=make_custom_cmap(config.colormaps[cmap]))
else:
raise epygramError("unknown colormap '"+cmap+"': must be added to userconfig, in usercolormaps.")