#!/usr/bin/env python
# -*- coding: utf-8 -*-
from epygram.base import Field, FieldValidity
from epygram import config, util, V2DGeometry, epygramError
import numpy
[docs]class V2DField(Field):
"""
Vertical 2-Dimension (section) field class.
A field is defined by its identifier 'fid',
its data, its geometry, and its validity.
At least for now, it is designed somehow like a collection of V1DFields.
And so is V2DGeometry.
"""
_collector = ('field',)
_footprint = dict(
attr = dict(
geometry = dict(type = V2DGeometry),
validity = dict(
type = FieldValidity,
optional = True,
default = FieldValidity()),
processtype = dict(
optional = True,
info = "Generating process.")
)
)
[docs] def setdata(self, data):
"""
Sets data, checking it to be 2D.
"""
if len(numpy.shape(data)) != 2:
raise epygramError("data must be 2D array.")
super(V2DField, self).setdata(data)
###################
# PRE-APPLICATIVE #
###################
# (but useful and rather standard) !
# [so that, subject to continuation through updated versions,
# including suggestions/developments by users...]
[docs] def plotfield(self, colorbar='vertical', graphicmode='colorshades', minmax=None,
levelsnumber=21, center_cmap_on_0=False, colormap='jet',
zoom=None, title=None, logscale=False, minmax_in_title=True,
contourcolor='k', contourwidth=1, contourlabel=True):
"""
Makes a simple (profile) plot of the field.
Args: \n
- *title* = title for the plot.
- *logscale* = to set Y logarithmic scale
- *minmax*: defines the min and max values for the plot colorbar. \n
Syntax: [min, max]. [0.0, max] also works. Default is min/max of the
field.
- *graphicmode*: among ('colorshades', 'contourlines').
- *levelsnumber*: number of levels for contours and colorbar.
- *colormap*: name of the **matplotlib** colormap to use.
- *center_cmap_on_0*: aligns the colormap center on the value 0.
- *colorbar*: if *False*, hide colorbar the plot; else, befines the
colorbar orientation, among ('horizontal', 'vertical').
Defaults to 'vertical'.
- *zoom*: a dict containing optional limits to zoom on the plot. \n
Syntax: e.g. {'ymax':500, ...}.
- *minmax_in_title*: if True and minmax != None, adds min and max
values in title
- *contourcolor*: color or colormap to be used for 'contourlines'
graphicmode. It can be either a legal html color name, or a colormap name.
- *contourwidth*: width of contours for 'contourlines' graphicmode.
- *contourlabel*: displays labels on contours.
Warning: requires **pyproj** and **matplotlib**.
"""
from pyproj import Geod
import matplotlib.pyplot as plt
plt.rc('font', family='serif')
plt.rc('figure', figsize=config.plotsizes)
# User colormaps
if colormap not in plt.colormaps():
util.add_cmap(colormap)
f = plt.figure()
# coords
p0 = self.geometry.grid[0]
plast = self.geometry.grid[-1]
if p0.coordinate == 'hybrid_pressure':
z = numpy.zeros((self.geometry.dimensions['Z'], self.geometry.dimensions['X']))
levels = numpy.arange(1, self.geometry.dimensions['Z']+1)
for i in range(self.geometry.dimensions['X']):
z[:,i] = levels[:]
else:
z = numpy.array([g.grid['levels'] for g in self.geometry.grid]).transpose()
if p0.coordinate == 'pressure':
z = z /100.
g = Geod(ellps='sphere')
arc = g.inv(p0.hlocation['lon'],
p0.hlocation['lat'],
plast.hlocation['lon'],
plast.hlocation['lat'])
distance = arc[2]
x = numpy.zeros((self.geometry.dimensions['Z'], self.geometry.dimensions['X']))
dists = numpy.linspace(0, distance, self.geometry.dimensions['X'])
for i in range(self.geometry.dimensions['Z']):
x[i,:] = dists[:]
data = self.data
if self.geometry.coordinate in ('hybrid_pressure', 'pressure'):
reverseY = True
else:
reverseY = False
# min/max
m = data.min()
M = data.max()
if minmax != None:
if minmax_in_title:
minmax_in_title = '(min: ' + \
'{: .{precision}{type}}'.format(m, type='E', precision=3) + \
' // max: ' + \
'{: .{precision}{type}}'.format(M, type='E', precision=3) + ')'
try: m = float(minmax[0])
except Exception: m = data.min()
try: M = float(minmax[1])
except Exception: M = data.max()
else:
minmax_in_title = ''
if abs(m-M) > config.epsilon:
levels = numpy.linspace(m, M, levelsnumber)
vmin = vmax = None
if center_cmap_on_0:
vmax = max(abs(m), M)
vmin = -vmax
else:
raise epygramError("cannot plot uniform field.")
L = int((levelsnumber-1)//15) +1
hlevels = [levels[l] for l in range(len(levels)-L/3) if l%L == 0] + [levels[-1]]
# plot
if reverseY:
plt.gca().invert_yaxis()
if logscale:
f.axes[0].set_yscale('log')
plt.grid()
if graphicmode == 'colorshades':
pf = plt.contourf(x, z, data, levels, cmap=colormap,
vmin=vmin, vmax=vmax)
if colorbar:
cb = plt.colorbar(pf, orientation=colorbar, ticks=hlevels)
if minmax_in_title != '':
cb.set_label(minmax_in_title)
elif graphicmode == 'contourlines':
pf = plt.contour(x, z, data, levels=levels, colors=contourcolor,
linewidths=contourwidth)
if contourlabel:
f.axes[0].clabel(pf, colors=contourcolor)
# decoration
surf = z[-1,:]
bottom = max(surf) if reverseY else min(surf)
plt.fill_between(x[-1,:], surf, numpy.ones(len(surf))*bottom, color='k')
if self.geometry.coordinate == 'hybrid_pressure':
Ycoordinate = 'Level \nHybrid-Pressure \ncoordinate'
elif self.geometry.coordinate == 'pressure':
Ycoordinate = 'Pressure (hPa)'
elif self.geometry.coordinate == 'altitude':
Ycoordinate = 'Altitude (m)'
elif self.geometry.coordinate == 'height':
Ycoordinate = 'Height (m)'
elif self.geometry.coordinate == 'potential_vortex':
Ycoordinate = 'Potential \nvortex \n(PVU)'
else:
Ycoordinate = 'unknown \ncoordinate'
f.axes[0].set_xlabel('Distance from left-end point (m).')
f.axes[0].set_ylabel(Ycoordinate)
if zoom != None:
ykw = {}
xkw = {}
for pair in (('bottom', 'ymin'), ('top', 'ymax')):
try: ykw[pair[0]] = zoom[pair[1]]
except Exception: pass
for pair in (('left', 'xmin'), ('right', 'xmax')):
try: xkw[pair[0]] = zoom[pair[1]]
except Exception: pass
f.axes[0].set_ylim(**ykw)
f.axes[0].set_xlim(**xkw)
if title == None:
title = 'Section of ' + str(self.fid['section']) + ' between \n' + \
'<- ('+str(p0.hlocation['lon'])+', '+str(p0.hlocation['lat'])+')' + ' and ' + \
'('+str(plast.hlocation['lon'])+', '+str(plast.hlocation['lat'])+') -> \n' + \
str(self.validity.get())
f.axes[0].set_title(title)
return f
[docs] def stats(self):
"""
Computes some basic statistics on the field, as a dict containing:
{'min', 'max', 'mean', 'std', 'quadmean', 'nonzero'}.
See each of these methods for details.
"""
return {'min':self.min(), 'max':self.max(), 'mean':self.mean(),
'std':self.std(), 'quadmean':self.quadmean(), 'nonzero':self.nonzero()}
[docs] def min(self):
"""
Returns the minimum value of data.
"""
data = self.data
return numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside).min()
[docs] def max(self):
"""
Returns the maximum value of data.
"""
data = self.data
return numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside).max()
[docs] def mean(self):
"""
Returns the mean value of data.
"""
data = self.data
return numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside).mean()
[docs] def std(self):
"""
Returns the standard deviation of data.
"""
data = self.data
return numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside).std()
[docs] def quadmean(self):
"""
Returns the quadratic mean of data.
"""
data = self.data
return numpy.sqrt((numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside)**2).mean())
[docs] def nonzero(self):
"""
Returns the number of non-zero values (whose absolute value > config.epsilon).
"""
data = self.data
return numpy.count_nonzero(abs(numpy.ma.masked_outside(data, -config.mask_outside, config.mask_outside)) > config.epsilon)
#############
# OPERATORS #
#############
def __add__(self, other):
"""
Definition of addition, 'other' being:
- a scalar (integer/float)
- another Field of the same subclass.
Returns a new Field whose data is the resulting operation,
with 'fid' = {'op':'+'} and null validity.
"""
newfield = super(V2DField, self)._add(other,
geometry=self.geometry)
return newfield
def __mul__(self, other):
"""
Definition of multiplication, 'other' being:
- a scalar (integer/float)
- another Field of the same subclass.
Returns a new Field whose data is the resulting operation,
with 'fid' = {'op':'*'} and null validity.
"""
newfield = super(V2DField, self)._mul(other,
geometry=self.geometry)
return newfield
def __sub__(self, other):
"""
Definition of substraction, 'other' being:
- a scalar (integer/float)
- another Field of the same subclass.
Returns a new Field whose data is the resulting operation,
with 'fid' = {'op':'-'} and null validity.
"""
newfield = super(V2DField, self)._sub(other,
geometry=self.geometry)
return newfield
def __div__(self, other):
"""
Definition of division, 'other' being:
- a scalar (integer/float)
- another Field of the same subclass.
Returns a new Field whose data is the resulting operation,
with 'fid' = {'op':'/'} and null validity.
"""
newfield = super(V2DField, self)._div(other,
geometry=self.geometry)
return newfield