# Set of helper routines for contour plots
import numpy as np
from numpy import ma
from matplotlib.colors import Normalize
from pylab import cm as pcm
from . import cm
class LogNorm2Sided(Normalize):
# {{{
"""
Normalize a given value to the 0-1 range on a two-sided log scale;
values from vmin to -vin are mapped logarithmically from 0 to 0.5-cin;
values from -vin to +vin are mapped linearly from 0.5-cin to 0.5+cin,
and values from +vin to vmax are mapped logarithmically from 0.5+cin to 1.
"""
def __init__(self, vmin=None, vmax=None, clip=False, vin=None, cin=0.01):
self.vin = vin
self.cin = cin
Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
if clip is None:
clip = self.clip
if np.iterable(value):
vtype = 'array'
val = ma.asarray(value).astype(np.float)
else:
vtype = 'scalar'
val = ma.array([value]).astype(np.float)
self.autoscale_None(val)
vmin, vmax = self.vmin, self.vmax
vin, cin = self.vin, self.cin
if vmin > vmax:
raise ValueError("minvalue must be less than or equal to maxvalue")
elif vmin > 0:
raise ValueError("minvalue must be less than 0")
elif vmax < 0:
raise ValueError("maxvalue must be greater than 0")
elif vmin==vmax:
result = 0.0 * val
else:
if clip:
mask = ma.getmask(val)
val = ma.array(np.clip(val.filled(vmax), vmin, vmax),
mask=mask)
ipos = (val > vin)
ineg = (val < -vin)
izero = ~(ipos | ineg)
result = ma.empty_like(val)
result[izero] = 0.5 + cin * val[izero] / vin
result[ipos] = 0.5 + cin + (0.5 - cin) * \
(ma.log(val[ipos]) - np.log(vin)) / (np.log(vmax) - np.log(vin))
result[ineg] = 0.5 - cin - (0.5 - cin) * \
(ma.log(-val[ineg]) - np.log(vin)) / (np.log(-vmin) - np.log(vin))
result.mask = ma.getmask(val)
if vtype == 'scalar':
result = result[0]
return result
def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")
vmin, vmax = self.vmin, self.vmax
vin, cin = self.vin, self.cin
if np.iterable(value):
val = ma.asarray(value)
ipos = (val > (0.5 + cin))
ineg = (val < (0.5 - cin))
izero = ~(ipos | ineg)
result = ma.empty_like(val)
result[izero] = (val[izero] - 0.5) * vin/cin
result[ipos] = vin * pow((vmax/vin), (val[ipos] - (0.5 + cin))/(0.5 - cin))
result[ineg] = -vin * pow((-vmin/vin), ((0.5 - cin) - val[min])/(0.5 - cin))
r = vmin * ma.power((vmax/vmin), val)
else:
if value > 0.5 + cin: r = vin * pow((vmax/vin), (value - (0.5 + cin))/(0.5 - cin))
elif value < 0.5 - cin: r = -vin * pow((-vmin/vin), ((0.5 - cin) - value)/(0.5 - cin))
else: r = (value - 0.5) * vin / cin
return r
def autoscale(self, A):
'''
Set *vmin*, *vmax* to min, max of *A*.
'''
A = ma.masked_less_equal(np.abs(A), 1e-16, copy=False)
self.vmin = -ma.max(A)
self.vmax = ma.max(A)
self.vin = ma.min(A)
def autoscale_None(self, A):
' autoscale only None-valued vmin or vmax'
if self.vmin is not None and self.vmax is not None and self.vin is not None:
return
A = ma.masked_less_equal(np.abs(A), 1e-16, copy=False)
if self.vmin is None:
self.vmin = -ma.max(A)
if self.vmax is None:
self.vmax = ma.max(A)
if self.vin is None:
self.vin = ma.min(A)
# }}}
def guessclimits(z, style=None, ndiv=None, clf=True):
# {{{
''' Guesses a round contour interval and style to display
the array z. '''
if style not in [None, 'div', 'seq']:
raise ValueError("style '%s' not recognized. Must be one of None, 'div', or 'seq'." % style)
if ndiv is not None and ndiv <= 0.:
raise ValueError("ndiv must be None or an integer greater than 0.")
zn = z.ravel()
zn = zn[np.isfinite(zn)]
if len(zn) < 1:
if style is None: style = 'div'
if style == 'div':
if ndiv is None: ndiv = 3
kws = dict(ndiv=ndiv, style=style)
elif style == 'seq':
if ndiv is None: ndiv = 5
kws = dict(ndiv=ndiv, min=0., style=style)
return 1., kws
pcs = [0, 2., 50, 98, 100]
mn, p2, md, p98, mx = np.percentile(zn, pcs)
dmn = md - mn
dp2 = md - p2
dp98 = p98 - md
dmx = mx - md
# If ratio of the spacing between the minimum/maximum and the median
# and the 2nd/98th percentile and the median are too large,
# consider the minimum/maximum values outliers, do not include them
# in the range
if mx - mn > 0.:
if dp2 > 0 and dmn / dp2 > 50:
dl = dp2
lo = p2
else:
dl = dmn
lo = mn
if dp98 > 0 and dmx / dp98 > 50:
dm = dp98
hi = p98
else:
dm = dmx
hi = mx
else:
lo = mn - 1.
dl = 1.0
dm = 1.0
hi = mn + 1.
if style is None: style = 'div'
if style is None: # Guess style of cmap to use
if p2 < 0 and p98 > 0: style = 'div'
else: style = 'seq'
if style == 'div':
if ndiv is None:
if clf: ndiv = 3
else: ndiv = 10
div = max(-lo, hi) / ndiv
# Handle degenerate case
if div == 0.: return 1., ndiv, style
p = np.floor(np.log10(np.abs(div)))
m = div / 10**p
if m > 6.: spc = 10.; nf = 5
elif m > 5.: spc = 6. ; nf = 6
elif m > 4.: spc = 5. ; nf = 6
elif m > 3.: spc = 4. ; nf = 4
elif m > 2.: spc = 3. ; nf = 6
elif m > 1.5: spc = 2. ; nf = 6
elif m > 1.: spc = 1.5; nf = 6
else: spc = 1. ; nf = 5
#print style, div, p, m, spc
spc *= 10**p
if clf: kws = dict(ndiv=ndiv, nf=nf, style=style)
else: kws = dict(range=ndiv*spc)
elif style == 'seq':
if ndiv is None:
if clf: ndiv = 5
else: ndiv = 10
div = (dm + dl) / ndiv
p = np.floor(np.log10(np.abs(div)))
m = div / 10**p
if m > 6.: spc = 10.; nf = 5
elif m > 5.: spc = 6. ; nf = 6
elif m > 4.: spc = 5. ; nf = 5
elif m > 3.: spc = 4. ; nf = 4
elif m > 2.: spc = 3. ; nf = 6
elif m > 1.5: spc = 2. ; nf = 6
else: spc = 1.5; nf = 6
#print style, p, m, spc
spc *= 10**p
min = lo - np.remainder(lo, spc)
if clf: kws = dict(ndiv=ndiv, min=min, nf=nf, style=style)
else: kws = dict(range=ndiv*spc/2., min=min)
return spc, kws
# }}}
[docs]def cldict(cdelt, range=None, min=None, mid=0, cidelt=0., nozero=False, **kwargs):
# {{{
''' Returns kwargs to :meth:`showvar` for a line contour plot.
'''
if nozero: cidelt = 2*cdelt
if range is None: range = 10*cdelt
if cidelt > 0.:
cl = np.concatenate([np.arange(-range, -cdelt, cdelt),
np.arange(-cdelt, cdelt, cidelt),
np.arange(cdelt, range + cdelt/2., cdelt)])
elif min is None:
cl = np.arange(mid - range, mid + range + cdelt/2, cdelt)
else:
cl = np.arange(min, min + 2 * range + cdelt/2, cdelt)
kwa = dict(clevs = None,
clines = cl,
linewidths = 1.,
colors='k')
kwa.update(kwargs)
return kwa
# }}}
[docs]def clfdict(cdelt, min=None, mid=0., nf=6, nl=2, ndiv=3, nozero=False, style=None, clr=True, **kwargs):
# {{{
'''
Returns kwargs to :meth:`showvar` for a filled contour plot.
Parameters
----------
cdelt : float
Spacing of a single division. Each division is spanned by a certain number of filled
contours and contour lines, and the colorbar will span a set number of divisions. See notes.
min : float or None (optional)
If specified, contours span ndiv equal divisions starting with the value
min. If None, the colorbar is centred on the value of mid. Default is None.
mid : float (optional)
If min is None, contours span ndiv equal divisions both above and below the value of mid.
nf : integer (optional)
Number of filled contours per division. Default is 6.
nl : integer (optional)
Number of contour lines per division. Default is 2.
nozero : boolean (optional)
If True, the contour line at mid is omitted. Defalt is False.
style : string (optional)
Either 'seq' or 'div'. Deterimines which style of colourmap to use; a
sequential or divergent. Default is 'div', unless ''min'' is set to
something other than None.
'''
if style is None:
# No style has been explicitly specified; use 'div' unless 'min' is explicitly set
if min is not None: style = 'seq'
else: style = 'dev'
if not style in ['div', 'seq']: raise ValueError("style must be one of 'div' or 'seq'")
if style == 'div': crange = float(cdelt * ndiv * 2)
else: crange = float(cdelt * ndiv)
if min is not None:
min = float(min)
cmin = min
cmax = min + crange
mid = (cmin + cmax) / 2.
else:
cmin = mid - crange / 2.
cmax = mid + crange / 2.
cmp = cm.get_cm(style, ndiv)
if style == 'div':
tks = np.linspace(cmin, cmax, 2*ndiv + 1)
cf = np.concatenate([np.linspace(cmin, mid, nf * ndiv + 1)[:-1],
np.linspace(mid, cmax, nf * ndiv + 1)[1:]])
if nozero:
cl = np.concatenate([np.linspace(cmin, 0, nl * ndiv + 1)[:-1],
np.linspace(0, cmax, nl * ndiv + 1)[1:]])
else:
cl = np.linspace(cmin, cmax, 2 * nl * ndiv + 1)
else:
tks = np.linspace(cmin, cmax, ndiv + 1)
cf = np.linspace(cmin, cmax, nf * ndiv + 1)
cl = np.linspace(cmin, cmax, nl * ndiv + 1)
if nl == 0: cl = None
kwcb = kwargs.pop('colorbar', {})
cb = dict(ticks = tks)
if kwcb is False:
cb = False
else:
cb.update(kwcb)
if not clr:
cmp = pcm.gray
cb = False
kwa = dict(clevs = cf,
clines = cl,
colorbar = cb,
linewidths = 1.,
cmap=cmp)
kwa.update(kwargs)
return kwa
# }}}
def lfmt(x, pos=None):
# {{{
x = float(x)
if x == 0.: return '0'
e = float(np.floor(np.log10(np.abs(x))))
if abs(e) < 2:
return '%.1f' % x
else:
return r'%d$\!\times$10$^{%d}$' % (np.round(x/10**e), e)
# }}}
[docs]def log1sdict(cmin, cdelt = 10., nf=6, nl=2, ndiv=5, **kwargs):
# {{{
''' Returns kwargs to :meth:`showvar` for a one-sided
logarithmically-spaced contour plot. '''
from matplotlib.colors import LogNorm
cmax = cmin * cdelt ** ndiv
tks = cmin * cdelt ** np.arange(0., ndiv+1)
if nf > 0:
cf = cmin * cdelt ** np.linspace(0., ndiv, nf*ndiv+1)
else:
cf = None
if nl > 0:
cl = cmin * cdelt ** np.linspace(0., ndiv, nl*ndiv+1)
else:
cl = None
nrm = LogNorm(vmin=cmin, vmax=cmax)
cmp = cm.get_cm('seq', ndiv)
kwcb = kwargs.pop('colorbar', {})
cb = dict(ticks=tks, width=1.4, rl=0.01, rr=0.16, ticklabels = [lfmt(x) for x in tks])
if kwcb is False:
cb = False
else:
cb.update(kwcb)
kwa = dict(clevs = cf,
clines = cl,
norm = nrm,
colorbar = cb,
cmap=cmp)
kwa.update(kwargs)
return kwa
# }}}
[docs]def log2sdict(cmin, cdelt = 10, nf=6, nl=2, ndiv=3, nozero=False, **kwargs):
# {{{
''' Returns kwargs to :meth:`showvar` for a two-sided logarithmically-spaced contour plot. '''
ce = cmin * cdelt ** np.linspace(0, ndiv-1, nf*(ndiv - 1)+1)
cel = cmin * cdelt ** np.linspace(0, ndiv-1, nl*(ndiv - 1)+1)
tks = cmin * cdelt ** np.arange(ndiv)
cmax = tks[-1]
tks = np.concatenate([-tks[::-1], [0.], tks])
if nf > 0:
#ci = np.linspace(-cmin, cmin, 2*nf+1)[1:-1]
ci = np.concatenate([np.linspace(-cmin, 0, nf + 1)[1:-1],
np.linspace(0, cmin, nf + 1)[1:-1]])
cf = np.concatenate([-ce[::-1], ci, ce])
else:
cf = None
if nl > 0:
if nozero:
cil = np.concatenate([np.linspace(-cmin, 0, nl+1)[1:-1], np.linspace(0, cmin, nl+1)[1:-1]])
else:
cil = np.linspace(-cmin, cmin, 2*nl+1)[1:-1]
cl = np.concatenate([-cel[::-1], cil, cel])
else:
cl = None
cin = 1. / (2 * ndiv)
nrm = LogNorm2Sided(vmin=-cmax, vmax=cmax, vin=cmin, cin=cin)
cmp = cm.get_cm('div', ndiv)
kwcb = kwargs.pop('colorbar', {})
cb = dict(ticks=tks, width=1.4, rl=0.01, rr=0.16, ticklabels = [lfmt(x) for x in tks])
cb.update(kwcb)
if kwcb is False:
cb = False
else:
cb.update(kwcb)
kwa = dict(clevs = cf,
clines = cl,
norm = nrm,
colorbar = cb,
cmap=cmp)
kwa.update(kwargs)
return kwa
# }}}
__all__ = ['clfdict', 'cldict', 'log1sdict', 'log2sdict', 'cm']