##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
## ##
## This file forms part of the Underworld geophysics modelling application. ##
## ##
## For full license and copyright information, please refer to the LICENSE.md file ##
## located at the project root, or contact the authors. ##
## ##
##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
import underworld as _underworld
import underworld._stgermain as _stgermain
import underworld.swarm as _swarmMod
import underworld.mesh as _uwmesh
from underworld.function import Function as _Function
import libUnderworld as _libUnderworld
#TODO: Drawing Objects to implement
# IsoSurface, IsoSurfaceCrossSection
# MeshSurface/MeshSampler (surface/volumes using MeshCrossSection sampler)
# Contour, ContourCrossSection
# HistoricalSwarmTrajectory
# VectorArrowMeshCrossSection?
#
# Maybe later...
# TextureMap
# SwarmShapes, SwarmRGB, SwarmVectors
# EigenVectors, EigenVectorCrossSection
# FeVariableSurface
#Some preset colourmaps
# aim to reduce banding artifacts by being either
# - isoluminant
# - smoothly increasing in luminance
# - diverging in luminance about centre value
colourMaps = {}
#Isoluminant blue-orange
colourMaps["isolum"] = "#288FD0 #50B6B8 #989878 #C68838 #FF7520"
#Diverging blue-yellow-orange
colourMaps["diverge"] = "#288FD0 #fbfb9f #FF7520"
#Isoluminant rainbow blue-green-orange
colourMaps["rainbow"] = "#5ed3ff #6fd6de #7ed7be #94d69f #b3d287 #d3ca7b #efc079 #ffb180"
#CubeLaw indigo-blue-green-yellow
colourMaps["cubelaw"] = "#440088 #831bb9 #578ee9 #3db6b6 #6ce64d #afeb56 #ffff88"
#CubeLaw indigo-blue-green-orange-yellow
colourMaps["cubelaw2"] = "#440088 #1b83b9 #6cc35b #ebbf56 #ffff88"
#CubeLaw heat blue-magenta-yellow)
colourMaps["smoothheat"] = "#440088 #831bb9 #c66f5d #ebbf56 #ffff88"
#Paraview cool-warm (diverging)
colourMaps["coolwarm"] = "#3b4cc0 #7396f5 #b0cbfc #dcdcdc #f6bfa5 #ea7b60 #b50b27"
[docs]class ColourMap(_stgermain.StgCompoundComponent):
"""
The ColourMap class provides functionality for mapping colours to numerical
values.
Parameters
----------
colours: str, list
List of colours to use for drawing object colour map. Provided as a string
or as a list of strings. Example, "red blue", or ["red", "blue"]
This should not be specified if 'colourMap' is specified.
valueRange: tuple, list
User defined value range to apply to colour map. Provided as a
tuple of floats (minValue, maxValue). If none is provided, the
value range will be determined automatically.
logScale: bool
Bool to determine if the colourMap should use a logarithmic scale.
discrete: bool
Bool to determine if a discrete colour map should be used.
Discrete colour maps do not interpolate between colours and instead
use nearest neighbour for colouring.
"""
_selfObjectName = "_cm"
_objectsDict = { "_cm": "lucColourMap" }
def __init__(self, colours=colourMaps["diverge"], valueRange=None, logScale=False, discrete=False, **kwargs):
if not hasattr(self, "properties"):
self.properties = {}
if not isinstance(colours,(str,list)):
raise TypeError("'colours' object passed in must be of python type 'str' or 'list'")
if isinstance(colours,(list)):
self.properties.update({"colours" : ' '.join(colours)})
else:
self.properties.update({"colours" : colours})
#User-defined props in kwargs
self.properties.update(kwargs)
dict((k.lower(), v) for k, v in self.properties.iteritems())
if valueRange != None:
# is valueRange correctly defined, ie list of length 2 made of numbers
if not isinstance( valueRange, (list,tuple)):
raise TypeError("'valueRange' objected passed in must be of type 'list' or 'tuple'")
if len(valueRange) != 2:
raise ValueError("'valueRange' must have 2 real values")
for item in valueRange:
if not isinstance( item, (int, float) ):
raise TypeError("'valueRange' must contain real numbers")
if not valueRange[0] < valueRange[1]:
raise ValueError("The first number of the valueRange list must be smaller than the second number")
# valueRange arg is good
self.properties.update({"range" : [valueRange[0], valueRange[1]]})
else:
self.properties.update({"range" : [0.0, 0.0]}) # ignored
if not isinstance(logScale, bool):
raise TypeError("'logScale' parameter must be of 'bool' type.")
self._logScale = logScale
self.properties.update({"logscale" : logScale})
if not isinstance(discrete, bool):
raise TypeError("'discrete' parameter must be of 'bool' type.")
self.properties.update({"discrete" : discrete})
# build parent
super(ColourMap,self).__init__()
def _add_to_stg_dict(self,componentDictionary):
# call parents method
super(ColourMap,self)._add_to_stg_dict(componentDictionary)
#dict methods
def update(self, newdict):
self.properties.update(newdict)
def __getitem__(self, key):
return self.properties[key]
def __setitem__(self, key, item):
self.properties[key] = item
def _getProperties(self):
#Convert properties to string
return '\n'.join(['%s=%s' % (k,v) for k,v in self.properties.iteritems()]);
[docs]class Drawing(_stgermain.StgCompoundComponent):
"""
This is the base class for all drawing objects but can also be instantiated
as is for direct/custom drawing.
Note that the defaults here are often overridden by the child objects.
Parameters
----------
colours: str, list.
See ColourMap class docstring for further information
colourMap: glucifer.objects.ColourMap
A ColourMap object for the object to use.
This should not be specified if 'colours' is specified.
opacity: float
Opacity of object. If provided, must take values from 0. to 1.
colourBar: bool
Bool to determine if a colour bar should be rendered.
valueRange: tuple, list
See ColourMap class docstring for further information
logScale: bool
See ColourMap class docstring for further information
discrete: bool
See ColourMap class docstring for further information
"""
_selfObjectName = "_dr"
_objectsDict = { "_dr": "lucDrawingObject" } # child should replace _dr with own derived type
def __init__(self, name=None, colours=None, colourMap=None, colourBar=False,
valueRange=None, logScale=False, discrete=False,
*args, **kwargs):
if not hasattr(self, "properties"):
self.properties = {}
if colours and colourMap:
raise RuntimeError("You should specify 'colours' or a 'colourMap', but not both.")
if colourMap:
self._colourMap = colourMap
elif colours:
self._colourMap = ColourMap(colours=colours, valueRange=valueRange, logScale=logScale)
else:
self._colourMap = ColourMap(valueRange=valueRange, logScale=logScale)
#User-defined props in kwargs
self.properties.update(kwargs)
dict((k.lower(), v) for k, v in self.properties.iteritems())
if not isinstance(colourBar, bool):
raise TypeError("'colourBar' parameter must be of 'bool' type.")
self._colourBar = None
if colourBar:
#Create the associated colour bar
self._colourBar = ColourBar(colourMap=self._colourMap)
if name:
self.properties["name"] = str(name)
self.resetDrawing()
# build parent
super(Drawing,self).__init__(*args)
def _add_to_stg_dict(self,componentDictionary):
# call parents method
super(Drawing,self)._add_to_stg_dict(componentDictionary)
# add an empty(ish) drawing object. children should fill it out.
componentDictionary[self._dr.name].update( {
"properties" :self._getProperties(),
"ColourMap" :self._colourMap._cm.name
} )
#dict methods
def update(self, newdict):
self.properties.update(newdict)
def __getitem__(self, key):
return self.properties[key]
def __setitem__(self, key, item):
self.properties[key] = item
def _getProperties(self):
#Convert properties to string
return '\n'.join(['%s=%s' % (k,v) for k,v in self.properties.iteritems()]);
def resetDrawing(self):
#Clear direct drawing data
self.vertices = []
self.vectors = []
self.scalars = []
self.labels = []
self.geomType = None
#Direct drawing methods
[docs] def label(self, text, pos=(0.,0.,0.), font="sans", scaling=1):
"""
Writes a label string
Parameters
----------
text: str
label text.
pos: tuple
X,Y,Z position to place the label.
font : str
label font (small/fixed/sans/serif/vector).
scaling : float
label font scaling (for "vector" font only).
"""
self.geomType = _libUnderworld.gLucifer.lucLabelType
self.vertices.append(pos)
self.labels.append(text)
self.properties.update({"font" : font, "fontscale" : scaling}) #Merge
[docs] def point(self, pos=(0.,0.,0.)):
"""
Draws a point
Parameters
----------
pos : tuple
X,Y,Z position to place the point
"""
self.geomType = _libUnderworld.gLucifer.lucPointType
self.vertices.append(pos)
[docs] def line(self, start=(0.,0.,0.), end=(0.,0.,0.)):
"""
Draws a line
Parameters
----------
start : tuple
X,Y,Z position to start line
end : tuple
X,Y,Z position to end line
"""
self.geomType = _libUnderworld.gLucifer.lucLineType
self.vertices.append(start)
self.vertices.append(end)
[docs] def vector(self, position=(0.,0.,0.), vector=(0.,0.,0.)):
"""
Draws a vector
Parameters
----------
position : tuple
X,Y,Z position to centre vector on
vector : tuple
X,Y,Z vector value
"""
self.geomType = _libUnderworld.gLucifer.lucVectorType
self.vertices.append(position)
self.vectors.append(vector)
@property
def colourBar(self):
"""
colourBar (dict): return colour bar of drawing object, create if
doesn't yet exist.
"""
if not self._colourBar:
self._colourBar = ColourBar(colourMap=self._colourMap)
return self._colourBar
[docs]class ColourBar(Drawing):
"""
The ColourBar drawing object draws a colour bar for the provided colour map.
Parameters
----------
colourMap: glucifer.objects.ColourMap
Colour map for which the colour bar will be drawn.
"""
def __init__(self, colourMap, *args, **kwargs):
#Default properties
self.properties = {"colourbar" : 1}
# build parent
super(ColourBar,self).__init__(colourMap=colourMap, *args, **kwargs)
[docs]class CrossSection(Drawing):
"""
This drawing object class defines a cross-section plane, derived classes
plot data over this cross section
See parent class for further parameter details. Also see property docstrings.
Parameters
---------
mesh : underworld.mesh.FeMesh
Mesh over which cross section is rendered.
fn : underworld.function.Function
Function used to determine values to render.
crossSection : str
Cross Section definition, eg. z=0.
resolution : unsigned
Surface rendered sampling resolution.
"""
_objectsDict = { "_dr": "lucCrossSection" }
def __init__(self, mesh, fn, crossSection="", resolution=100,
colours=None, colourMap=None, colourBar=True,
valueRange=None, logScale=False, discrete=False, offsetEdges=None,
*args, **kwargs):
self._fn = _underworld.function.Function.convert(fn)
if not isinstance(mesh,_uwmesh.FeMesh):
raise TypeError("'mesh' object passed in must be of type 'FeMesh'")
self._mesh = mesh
if not isinstance(crossSection,str):
raise ValueError("'crossSection' parameter must be of python type 'str'")
self._crossSection = crossSection
self._offsetEdges = offsetEdges
if not isinstance(resolution,int):
raise TypeError("'resolution' parameter must be of python type 'int'")
if resolution < 1:
raise ValueError("'resolution' parameter must be greater than zero")
self._resolution = resolution
# build parent
super(CrossSection,self).__init__(colours=colours, colourMap=colourMap, colourBar=colourBar,
valueRange=valueRange, logScale=logScale, discrete=discrete, *args, **kwargs)
def _setup(self):
_libUnderworld.gLucifer._lucCrossSection_SetFn( self._cself, self._fn._fncself )
if self._offsetEdges != None:
self._dr.offsetEdges = self._offsetEdges
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# call parents method
super(CrossSection,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name].update( {
"Mesh": self._mesh._cself.name,
"crossSection": self._crossSection,
"resolution" : self._resolution
} )
@property
def crossSection(self):
""" crossSection (str): Cross Section definition, eg;: z=0.
"""
return self._crossSection
[docs]class SurfaceOnMesh(CrossSection):
"""
This drawing object class draws a surface using the provided scalar field.
This object differs from the `Surface` class in that it samples the mesh
nodes directly, as opposed to sampling across a regular grid. This class
should be used in particular where a mesh has been deformed.
See parent class for further parameter details. Also see property docstrings.
Notes
-----
The interface to this object will be revised in future versions.
Parameters
---------
mesh : underworld.mesh.FeMesh
Mesh over which cross section is rendered.
fn : underworld.function.Function
Function used to determine values to render.
drawSides : str
Sides (x,y,z,X,Y,Z) for which the surface should be drawn.
For example, "xyzXYZ" would render the provided function across
all surfaces of the domain in 3D. In 2D, this object always renders
across the entire domain.
"""
# let's just build both objects because we aint sure yet which one we want to use yet
_objectsDict = { "_dr" : "lucScalarFieldOnMesh" }
def __init__(self, mesh, fn, drawSides="xyzXYZ",
colours=None, colourMap=None, colourBar=True,
valueRange=None, logScale=False, discrete=False,
*args, **kwargs):
if not isinstance(drawSides,str):
raise ValueError("'drawSides' parameter must be of python type 'str'")
self._drawSides = drawSides
#Default properties
self.properties = {"cullface" : True}
# TODO: disable lighting if 2D (how to get dims?)
#self.properties["lit"] = False
# build parent
super(SurfaceOnMesh,self).__init__( mesh=mesh, fn=fn,
colours=colours, colourMap=colourMap, colourBar=colourBar,
valueRange=valueRange, logScale=logScale, discrete=discrete, *args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# append random string to provided name to ensure unique component names
# call parents method
super(SurfaceOnMesh,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name]["drawSides"] = self._drawSides
componentDictionary[self._dr.name][ "Mesh"] = self._mesh._cself.name
def _setup(self):
_libUnderworld.gLucifer._lucMeshCrossSection_SetFn( self._cself, self._fn._fncself )
def __del__(self):
super(SurfaceOnMesh,self).__del__()
[docs]class Surface(CrossSection):
"""
This drawing object class draws a surface using the provided scalar field.
See parent class for further parameter details. Also see property docstrings.
Parameters
---------
mesh : underworld.mesh.FeMesh
Mesh over which cross section is rendered.
fn : underworld.function.Function
Function used to determine values to render.
drawSides : str
Sides (x,y,z,X,Y,Z) for which the surface should be drawn.
For example, "xyzXYZ" would render the provided function across
all surfaces of the domain in 3D. In 2D, this object always renders
across the entire domain.
"""
# let's just build both objects because we aint sure yet which one we want to use yet
_objectsDict = { "_dr" : "lucScalarField" }
def __init__(self, mesh, fn, drawSides="xyzXYZ",
colours=None, colourMap=None, colourBar=True,
valueRange=None, logScale=False, discrete=False,
*args, **kwargs):
if not isinstance(drawSides,str):
raise ValueError("'drawSides' parameter must be of python type 'str'")
self._drawSides = drawSides
#Default properties
self.properties = {"cullface" : True}
# TODO: disable lighting if 2D (how to get dims?)
#self.properties["lit"] = False
# build parent
super(Surface,self).__init__( mesh=mesh, fn=fn,
colours=colours, colourMap=colourMap, colourBar=colourBar,
valueRange=valueRange, logScale=logScale, discrete=discrete, *args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# append random string to provided name to ensure unique component names
# call parents method
super(Surface,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name]["drawSides"] = self._drawSides
componentDictionary[self._dr.name][ "Mesh"] = self._mesh._cself.name
def _setup(self):
_libUnderworld.gLucifer._lucCrossSection_SetFn( self._cself, self._fn._fncself )
def __del__(self):
super(Surface,self).__del__()
[docs]class Points(Drawing):
"""
This drawing object class draws a swarm of points.
See parent class for further parameter details. Also see property docstrings.
Parameters
---------
swarm : underworld.swarm.Swarm
Swarm which provides locations for point rendering.
fn_colour : underworld.function.Function
Function used to determine colour to render particle.
This function should return float/double values.
fn_mask : underworld.function.Function
Function used to determine if a particle should be rendered.
This function should return bool values.
fn_size : underworld.function.Function
Function used to determine size to render particle.
This function should return float/double values.
"""
_objectsDict = { "_dr": "lucSwarmViewer" }
def __init__(self, swarm, fn_colour=None, fn_mask=None, fn_size=None, colourVariable=None,
colours=None, colourMap=None, colourBar=True,
valueRange=None, logScale=False, discrete=False,
*args, **kwargs):
if not isinstance(swarm,_swarmMod.Swarm):
raise TypeError("'swarm' object passed in must be of type 'Swarm'")
self._swarm = swarm
self._fn_colour = None
if fn_colour != None:
self._fn_colour = _underworld.function.Function.convert(fn_colour)
self._fn_mask = None
if fn_mask != None:
self._fn_mask = _underworld.function.Function.convert(fn_mask)
self._fn_size = None
if fn_size != None:
self._fn_size = _underworld.function.Function.convert(fn_size)
# build parent
super(Points,self).__init__(
colours=colours, colourMap=colourMap, colourBar=colourBar,
valueRange=valueRange, logScale=logScale, discrete=discrete, *args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
super(Points,self)._add_to_stg_dict(componentDictionary)
componentDictionary[ self._cself.name ][ "Swarm" ] = self._swarm._cself.name
def _setup(self):
fnc_ptr = None
if self._fn_colour:
fnc_ptr = self._fn_colour._fncself
fnm_ptr = None
if self._fn_mask:
fnm_ptr = self._fn_mask._fncself
fns_ptr = None
if self._fn_size:
fns_ptr = self._fn_size._fncself
_libUnderworld.gLucifer._lucSwarmViewer_SetFn( self._cself, fnc_ptr, fnm_ptr, fns_ptr, None )
class _GridSampler3D(CrossSection):
""" This drawing object class samples a regular grid in 3D.
resolutionI : unsigned
Number of samples in the I direction.
resolutionJ : unsigned
Number of samples in the J direction.
resolutionK : unsigned
Number of samples in the K direction.
"""
_objectsDict = { "_dr": None } #Abstract class, Set by child
def __init__(self, resolutionI=16, resolutionJ=16, resolutionK=16, *args, **kwargs):
if resolutionI:
if not isinstance(resolutionI,int):
raise TypeError("'resolutionI' object passed in must be of python type 'int'")
if resolutionJ:
if not isinstance(resolutionJ,int):
raise TypeError("'resolutionJ' object passed in must be of python type 'int'")
if resolutionK:
if not isinstance(resolutionK,int):
raise TypeError("'resolutionK' object passed in must be of python type 'int'")
self._resolutionI = resolutionI
self._resolutionJ = resolutionJ
self._resolutionK = resolutionK
# build parent
super(_GridSampler3D,self).__init__(*args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# call parents method
super(_GridSampler3D,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name].update( {
"resolutionX": self._resolutionI,
"resolutionY": self._resolutionJ,
"resolutionZ": self._resolutionK
} )
[docs]class VectorArrows(_GridSampler3D):
"""
This drawing object class draws vector arrows corresponding to the provided vector field.
See parent class for further parameter details. Also see property docstrings.
Parameters
---------
mesh : underworld.mesh.FeMesh
Mesh over which vector arrows are rendered.
fn : underworld.function.Function
Function used to determine vectors to render.
Function should return a vector of floats/doubles of appropriate
dimensionality.
arrowHead : float
The size of the head of the arrow compared with the arrow length.
Must be in [0.,1.].
scaling : float
Scaling for entire arrow.
glyphs : int
Type of glyph to render for vector arrow.
0: Line, 1 or more: 3d arrow, higher number => better quality.
resolutionI : unsigned
Number of samples in the I direction.
resolutionJ : unsigned
Number of samples in the J direction.
resolutionK : unsigned
Number of samples in the K direction.
"""
_objectsDict = { "_dr": "lucVectorArrows" }
def __init__(self, mesh, fn,
resolutionI=16, resolutionJ=16, resolutionK=16,
*args, **kwargs):
# build parent
super(VectorArrows,self).__init__( mesh=mesh, fn=fn, resolutionI=resolutionI, resolutionJ=resolutionJ, resolutionK=resolutionK,
colours=None, colourMap=None, colourBar=False, *args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# call parents method
super(VectorArrows,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name].update( {} )
[docs]class Volume(_GridSampler3D):
"""
This drawing object class draws a volume using the provided scalar field.
See parent class for further parameter details. Also see property docstrings.
Parameters
---------
mesh : underworld.mesh.FeMesh
Mesh over which object is rendered.
fn : underworld.function.Function
Function used to determine colour values.
Function should return a vector of floats/doubles of appropriate
dimensionality.
resolutionI : unsigned
Number of samples in the I direction.
resolutionJ : unsigned
Number of samples in the J direction.
resolutionK : unsigned
Number of samples in the K direction.
"""
_objectsDict = { "_dr": "lucFieldSampler" }
def __init__(self, mesh, fn,
resolutionI=64, resolutionJ=64, resolutionK=64,
colours=None, colourMap=None, colourBar=True,
valueRange=None, logScale=False, discrete=False,
*args, **kwargs):
# build parent
if mesh.dim == 2:
raise ValueError("Volume rendered requires a three dimensional mesh.")
super(Volume,self).__init__( mesh=mesh, fn=fn, resolutionI=resolutionI, resolutionJ=resolutionJ, resolutionK=resolutionK,
colours=colours, colourMap=colourMap, colourBar=colourBar,
valueRange=valueRange, logScale=logScale, discrete=discrete, *args, **kwargs)
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# call parents method
super(Volume,self)._add_to_stg_dict(componentDictionary)
[docs]class Mesh(Drawing):
"""
This drawing object class draws a mesh.
See parent class for further parameter details. Also see property docstrings.
Parameters
----------
mesh : underworld.mesh.FeMesh
Mesh to render.
nodeNumbers : bool
Bool to determine whether global node numbers should be rendered.
segmentsPerEdge : unsigned
Number of segments to render per cell/element edge. For higher
order mesh, more segments are useful to render mesh curvature correctly.
"""
_objectsDict = { "_dr": "lucMeshViewer" }
def __init__( self, mesh, nodeNumbers=False, segmentsPerEdge=1, *args, **kwargs ):
if not isinstance(mesh,_uwmesh.FeMesh):
raise TypeError("'mesh' object passed in must be of type 'FeMesh'")
self._mesh = mesh
if not isinstance(nodeNumbers,bool):
raise TypeError("'nodeNumbers' flag must be of type 'bool'")
self._nodeNumbers = nodeNumbers
if not isinstance(segmentsPerEdge,int) or segmentsPerEdge < 1:
raise TypeError("'segmentsPerEdge' must be a positive 'int'")
self._segmentsPerEdge = segmentsPerEdge
#Default properties
self.properties = {"linesmooth" : False, "lit" : False, "font" : "small", "fontscale" : 0.5,
"pointsize" : 5 if self._nodeNumbers else 1,
"pointtype" : 2 if self._nodeNumbers else 4}
# build parent
super(Mesh,self).__init__( colours=None, colourMap=None, colourBar=False, *args, **kwargs )
def _add_to_stg_dict(self,componentDictionary):
# lets build up component dictionary
# append random string to provided name to ensure unique component names
# call parents method
super(Mesh,self)._add_to_stg_dict(componentDictionary)
componentDictionary[self._dr.name][ "Mesh"] = self._mesh._cself.name
componentDictionary[self._dr.name]["nodeNumbers"] = self._nodeNumbers
componentDictionary[self._dr.name][ "segments"] = self._segmentsPerEdge