Source code for glucifer.lavavu

"""
LavaVu python interface: viewer utils & wrapper

NOTE: regarding sync of state between python and library
- sync from python to lavavu is immediate,
    property setting must always trigger a sync to lavavu
- sync from lavavu to python is lazy, always need to call _get()
    before using state data
#TODO:
 - zoom to fit in automated image output broken, initial timestep differs, margins out
 - translation setting different if window aspect ratio changes
"""
import json
import math
import sys
import os
import glob
import control
import numpy

if __name__ != 'glucifer.lavavu' and 'glucifer.lavavu' in sys.modules:
    #Already imported, some paths issue causes double import
    raise RuntimeError("LavaVu module provided by glucifer exists, please don't import separately")
[docs]def is_ipython(): try: if __IPYTHON__: return True else: return False except: return False
[docs]def is_notebook(): if 'IPython' not in sys.modules: # IPython hasn't been imported, definitely not return False try: from IPython import get_ipython from IPython.display import display,Image,HTML except: return False # check for `kernel` attribute on the IPython instance return getattr(get_ipython(), 'kernel', None) is not None
#Attempt to import swig module libpath = "bin" version = "" try: #This file should be found one dir above bin dir containing built modules binpath = os.path.join(os.path.dirname(__file__), 'bin') sys.path.append(binpath) import LavaVuPython modpath = os.path.abspath(os.path.dirname(__file__)) libpath = os.path.join(modpath, "bin") version = LavaVuPython.version except (Exception) as e: print("LavaVu visualisation module load failed: " + str(e)) raise TOL_DEFAULT = 0.0001 #Default error tolerance for image tests geomtypes = {"labels": LavaVuPython.lucLabelType, "points": LavaVuPython.lucPointType, "quads": LavaVuPython.lucGridType, "triangles": LavaVuPython.lucTriangleType, "vectors": LavaVuPython.lucVectorType, "tracers": LavaVuPython.lucTracerType, "lines": LavaVuPython.lucLineType, "shapes": LavaVuPython.lucShapeType, "volume": LavaVuPython.lucVolumeType} datatypes = {"vertices": LavaVuPython.lucVertexData, "normals": LavaVuPython.lucNormalData, "vectors": LavaVuPython.lucVectorData, "indices": LavaVuPython.lucIndexData, "colours": LavaVuPython.lucRGBAData, "texcoords": LavaVuPython.lucTexCoordData, "luminance": LavaVuPython.lucLuminanceData, "rgb": LavaVuPython.lucRGBData, "values": LavaVuPython.lucMaxDataType}
[docs]def convert_keys(dictionary): """Recursively converts dictionary keys and unicode values to utf-8 strings.""" if isinstance(dictionary, list): for i in range(len(dictionary)): dictionary[i] = convert_keys(dictionary[i]) return dictionary if not isinstance(dictionary, dict): if isinstance(dictionary, unicode): return dictionary.encode('utf-8') return dictionary return dict((k.encode('utf-8'), convert_keys(v)) for k, v in dictionary.items())
#Echo image test fail output to console echo_fails = False default_args = [] #Wrapper class for drawing object #handles property updating via internal dict
[docs]class Object(dict): """ The Object class provides an interface to a LavaVu drawing object Object instances are created internally in the Viewer class and can be retrieved from the object list New objects are also created using viewer methods Parameters ---------- **kwargs: Initial set of properties passed to the created object Example ------- Create a viewer, load some test data, get objects >>> import lavavu >>> lv = lavavu.Viewer() >>> lv.test() >>> print(lv.objects) Object properties can be passed in when created or set by using as a dictionary: >>> obj = lv.points(pointtype="sphere") >>> obj["pointsize"] = 5 A list of available properties can be found here: https://github.com/OKaluza/LavaVu/wiki/Property-Reference or by using the online help: >>> obj.help('opacity') """ def __init__(self, idict, instance, *args, **kwargs): self.dict = idict self.instance = instance #Create a control factory self.control = control.ControlFactory(self) #Init prop dict for tab completion super(Object, self).__init__(**self.instance.properties) @property def name(self): """ Get the object's name property Returns ------- name: str The name of the object """ return str(self.dict["name"]) def _setprops(self, props): #Replace props with new data from app self.dict.clear() self.dict.update(props) def _set(self): #Send updated props (via ref in case name changed) self.instance._setupobject(self.ref, **self.dict) def __getitem__(self, key): self.instance._get() #Ensure in sync if not key in self.instance.properties: raise ValueError(key + " : Invalid property name") if key in self.dict: return self.dict[key] #Default to the property lookup dict return super(Object, self).__getitem__(key) def __setitem__(self, key, value): if not key in self.instance.properties: raise ValueError(key + " : Invalid property name") #self.instance._get() #Ensure in sync #Set new value and send self.dict[key] = value self._set() def __contains__(self, key): return key in self.dict def __repr__(self): return str(self.ref) def __str__(self): #Default string representation self.instance._get() #Ensure in sync return str('\n'.join(['%s=%s' % (k,json.dumps(v)) for k,v in self.dict.iteritems()])) #return '[' + ', '.join(self.dict.keys()) + ']' #Interface for setting filters
[docs] def include(self, *args, **kwargs): """ Filter data by including a range of values shortcut for: filter(... , out=False) Parameters ---------- label: str Data label to filter on values: number,list,tuple value range single value, list or tuple if a single value the filter applies to only this value: x == value if a list eg: [0,1] range is inclusive 0 <= x <= 1 if a tuple eg: (0,1) range is exclusive 0 < x < 1 Returns ------- filter: int The filter id created """ return self.filter(*args, out=False, **kwargs)
[docs] def includemap(self, *args, **kwargs): """ Filter data by including a range of mapped values shortcut for: filter(... , out=False, map=True) Parameters ---------- label: str Data label to filter on values: number,list,tuple value range single value, list or tuple if a single value the filter applies to only this value: x == value if a list eg: [0,1] range is inclusive 0 <= x <= 1 if a tuple eg: (0,1) range is exclusive 0 < x < 1 Returns ------- filter: int The filter id created """ return self.filter(*args, out=False, map=True, **kwargs)
[docs] def exclude(self, *args, **kwargs): """ Filter data by excluding a range of values shortcut for: filter(... , out=True) Parameters ---------- label: str Data label to filter on values: number,list,tuple value range single value, list or tuple if a single value the filter applies to only this value: x == value if a list eg: [0,1] range is inclusive 0 <= x <= 1 if a tuple eg: (0,1) range is exclusive 0 < x < 1 Returns ------- filter: int The filter id created """ return self.filter(*args, out=True, **kwargs)
[docs] def excludemap(self, *args, **kwargs): """ Filter data by excluding a range of mapped values shortcut for: filter(... , out=True, map=True) Parameters ---------- label: str Data label to filter on values: number,list,tuple value range single value, list or tuple if a single value the filter applies to only this value: x == value if a list eg: [0,1] range is inclusive 0 <= x <= 1 if a tuple eg: (0,1) range is exclusive 0 < x < 1 Returns ------- filter: int The filter id created """ return self.filter(*args, out=True, map=True, **kwargs)
[docs] def filter(self, label, values, out=False, map=False): """ Filter data by including a range of values Parameters ---------- label: str Data label to filter on values: number,list,tuple value range single value, list or tuple if a single value the filter applies to only this value: x == value if a list eg: [0,1] range is inclusive 0 <= x <= 1 if a tuple eg: (0,1) range is exclusive 0 < x < 1 out: boolean Set this flag to filter out values instead of including them map: boolean Set this flag to filter by normalised values mapped to [0,1] instead of actual min/max of the data range Returns ------- filter: int The filter id created """ #Pass a single value to include/exclude exact value #Pass a tuple for exclusive range (min < val < max) # list for inclusive range (min <= val <= max) self.instance._get() #Ensure have latest data if isinstance(values, float) or isinstance(values, int): values = [values,values] filter = {"by" : label, "minimum" : values[0], "maximum" : values[1], "map" : map, "out" : out, "inclusive" : False} if isinstance(values, list) or values[0] == values[1]: filter["inclusive"] = True if not "filters" in self: self["filters"] = [] self["filters"].append(filter) self._set() return len(self["filters"])-1
[docs] def datasets(self): """ Retrieve available data sets on this object Returns ------- data: str A string representation of the data objects available """ #Return json data set list #TODO: use Geometry wrapper? return json.dumps(self.dict["data"])
[docs] def append(self): """ Append a new data element to the object Object data is sometimes dependant on individual elements to determine where one part ends and another starts, eg: line segments, grids This allows manually closing the active element so all new data is loaded into a new element """ #TODO: use ref? requires new function in LavaVu self.instance.append(self.id) #self.name)
[docs] def triangles(self, data, split=0): """ Load triangle data, This is the same as loading vertices into a triangle mesh object but allows decomposing the mesh into smaller triangles with the split option Parameters ---------- split: int Split triangles this many times on loading """ if split > 1: self.instance.app.loadTriangles(self.ref, data, self.name, split) else: self.vertices(data)
def _loadScalar(self, data, dtype): #Passes a scalar dataset (float/uint8/uint32) if not isinstance(data, numpy.ndarray): data = numpy.asarray(data, dtype=numpy.float32) if data.dtype == numpy.float32: self.instance.app.arrayFloat(self.ref, data.ravel(), dtype) elif data.dtype == numpy.uint32: self.instance.app.arrayUInt(self.ref, data.ravel(), dtype) elif data.dtype == numpy.uint8: self.instance.app.arrayUChar(self.ref, data.ravel(), dtype) def _loadVector(self, data, dtype): #Passes a vector dataset (float) if not isinstance(data, numpy.ndarray) or data.dtype != numpy.float32: data = numpy.asarray(data, dtype=numpy.float32) self.instance.app.arrayFloat(self.ref, data.ravel(), dtype)
[docs] def data(self, filter=None): """ Return internal geometry data Returns a Geometry() object that can be iterated through containing all data elements Elements contain vertex/normal/value etc. data as numpy arrays Parameters ---------- filter: str Limit the data returned to this type (labels, points, grid, triangles, vectors, tracers, lines, shapes, volume) Returns ------- data: Geometry An object holding the data elements retrieved Example ------- >>> data = obj.data() >>> for el in obj.data: >>> print(el) """ return Geometry(self, filter)
[docs] def vertices(self, data=None): """ Load vertex data for object Parameters ---------- data: list,array Pass a list or numpy float32 array of vertices """ self._loadVector(data, LavaVuPython.lucVertexData)
[docs] def normals(self, data): """ Load normal data for object Parameters ---------- data: list,array Pass a list or numpy float32 array of normals """ self._loadVector(data, LavaVuPython.lucNormalData)
[docs] def vectors(self, data): """ Load vector data for object Parameters ---------- data: list,array Pass a list or numpy float32 array of vectors """ self._loadVector(data, LavaVuPython.lucVectorData)
[docs] def values(self, data, label="default"): """ Load value data for object Parameters ---------- data: list,array Pass a list or numpy float32 array of values label: str Label for this data set """ if not isinstance(data, numpy.ndarray) or data.dtype != numpy.float32: data = numpy.asarray(data, dtype=numpy.float32) self.instance.app.arrayFloat(self.ref, data.ravel(), label)
[docs] def colours(self, data): """ Load colour data for object Parameters ---------- data: str,list,array Pass a list or numpy uint32 array of colours if a string or list of strings is provided, colours are parsed as html colour string values if a numpy array is passed, colours are loaded as 4 byte ARGB unsigned integer values """ if isinstance(data, numpy.ndarray): self._loadScalar(data, LavaVuPython.lucRGBAData) return #Convert to list of strings if isinstance(data, str): data = data.split() if len(data) < 1: return #Load as string array or unsigned int array if isinstance(data[0], list): #Convert to strings for json parsing first data = [str(i) for i in data] if isinstance(data[0], str): #Each element will be parsed as a colour string self.instance.app.loadColours(self.ref, data) else: #Plain list, assume unsigned colour data data = numpy.asarray(data, dtype=numpy.uint32) self.colours(data)
[docs] def indices(self, data): """ Load index data for object Parameters ---------- data: list,array Pass a list or numpy uint32 array of indices indices are loaded as 32 bit unsigned integer values """ #Accepts only uint32 indices if not isinstance(data, numpy.ndarray) or data.dtype != numpy.uint32: data = numpy.asarray(data, dtype=numpy.uint32) self._loadScalar(data, LavaVuPython.lucIndexData)
[docs] def texture(self, data, width, height, channels=4, flip=True): """ Load raw texture data for object Parameters ---------- data: list,array Pass a list or numpy uint32 or uint8 array texture data is loaded as raw image data width: int image width in pixels height: int image height in pixels channels: int colour channels/depth in bytes (1=luminance, 3=RGB, 4=RGBA) flip: boolean flip the texture vertically after loading (default is enabled as usually required for OpenGL but can be disabled) """ if not isinstance(data, numpy.ndarray): data = numpy.asarray(data, dtype=numpy.uint32) if data.dtype == numpy.uint32: self.instance.app.textureUInt(self.ref, data.ravel(), width, height, channels, flip) elif data.dtype == numpy.uint8: self.instance.app.textureUChar(self.ref, data.ravel(), width, height, channels, flip)
[docs] def labels(self, data): """ Load label data for object Parameters ---------- data: list,str Pass a label or list of labels to be applied, one per vertex """ if isinstance(data, str): data = [data] self.instance.app.loadLabels(self.ref, data)
[docs] def colourmap(self, data, reverse=False, monochrome=False, **kwargs): """ Load colour map data for object Parameters ---------- data: list,str Provided colourmap data can be - a string, - list of colour strings, - list of position,value tuples - or a built in colourmap name Creates a colourmap named objectname_colourmap if object doesn't already have a colourmap reverse: boolean Reverse the order of the colours after loading monochrome: boolean Convert to greyscale """ #Load colourmap on this object ret = None if self.ref.colourMap is None: self.ref.colourMap = self.instance.app.addColourMap(self.name + "_colourmap") self["colourmap"] = self.ref.colourMap.name self.ref.colourMap._setup(self.instance.app, data, reverse, monochrome, str(json.dumps(kwargs))) return self.ref.colourMap.name
[docs] def reload(self): """ Fully reload the object's data, recalculating all parameters such as colours that may be cached on the GPU, required after changing some properties so the changes are reflected in the visualisation """ self.instance.app.reloadObject(self.ref)
[docs] def select(self): """ Set this object as the selected object """ self.instance.app.aobject = self.ref
[docs] def file(self, *args, **kwargs): """ Load data from a file into this object Parameters ---------- filename: str Name of the file to load """ #Load file with this object selected (import) self.select() self.instance.file(*args, obj=self, **kwargs) self.instance.app.aobject = None
[docs] def files(self, *args, **kwargs): """ Load data from a series of files into this object (using wildcards or a list) Parameters ---------- files: str Specification of the files to load """ #Load file with this object selected (import) self.select() self.instance.files(*args, obj=self, **kwargs) self.instance.app.aobject = None
[docs] def colourbar(self, **kwargs): """ Create a new colourbar using this object's colourmap Returns ------- colourbar: Object The colourbar object created """ #Create a new colourbar for this object return self.instance.colourbar(self, **kwargs)
[docs] def clear(self): """ Clear all visualisation data from this object """ self.instance.app.clearObject(self.ref)
[docs] def cleardata(self, typename=""): """ Clear specific visualisation data/values from this object Parameters ---------- typename: str Optional filter naming type of data to be cleared, Either a built in type: (vertices/normals/vectors/indices/colours/texcoords/luminance/rgb/values) or a user defined data label """ if typename in datatypes: #Found data type name dtype = datatypes[typename] self.instance.app.clearData(self.ref, dtype) else: #Assume values by label (or all values if blank) self.instance.app.clearValues(self.ref, typename)
[docs] def update(self, filter=None, compress=True): """ Write the objects's visualisation data back to the database Parameters ---------- filter: str Optional filter to type of geometry to be updated, if omitted all will be written (labels, points, grid, triangles, vectors, tracers, lines, shapes, volume) compress: boolean Use zlib compression when writing the geometry data """ #Update object data at current timestep if filter is None: #Re-writes all data to db for this object self.instance.app.update(self.ref, compress) else: #Re-writes data to db for this object and geom type self.instance.app.update(self.ref, geomtypes[filter], compress)
[docs] def getcolourmap(self, string=True): """ Return the colour map data from the object as a string or list Either return format can be used to create/modify a colourmap with colourmap() Parameters ---------- string: boolean The default is to return the data as a string of colours separated by semi-colons To instead return a list of (position,[R,G,B,A]) tuples for easier automated processing in python, set this to False Returns ------- mapdata: str/list The formatted colourmap data """ cmid = self["colourmap"] return self.instance.getcolourmap(cmid, string)
[docs] def isosurface(self, name=None, convert=False, updatedb=False, compress=True, **kwargs): """ Generate an isosurface from a volume data set using the marching cubes algorithm Parameters ---------- name: str Name of the created object, automatically assigned if not provided convert: bool Setting this flag to True will replace the existing volume object with the newly created isosurface by deleting the volume data and loading the mesh data into the preexisting object updatedb: bool Setting this flag to True will write the newly created/modified data to the database when done compress: boolean Use zlib compression when writing the geometry data **kwargs: Initial set of properties passed to the created object (Must include "isovalues=[]") Returns ------- obj: Object The isosurface object created/converted """ #Generate and return an isosurface object, #pass properties as kwargs (eg: isovalues=[]) isobj = self if not convert: #Create a new object for the surface if name is None: name = self.name + "_surface" isobj = self.instance.add(name, **kwargs) isobj["geometry"] = "triangles" else: #Convert existing object (self) set properties self.instance._setupobject(self.ref, **kwargs) #Create surface, If requested, write the new data to the database self.instance.app.isoSurface(isobj.ref, self.ref, convert) #Re-write modified types to the database if updatedb: self.instance.app.update(isobj.ref, LavaVuPython.lucVolumeType, compress) self.instance.app.update(isobj.ref, LavaVuPython.lucTriangleType, compress) return isobj
[docs] def help(self, cmd=""): """ Get help on an object method or property Parameters ---------- cmd: str Command to get help with, if ommitted displays general introductory help If cmd is a property or is preceded with '@' will display property help """ self.instance.help(cmd, self)
#Wrapper dict+list of objects
[docs]class Objects(dict): """ The Objects class is used internally to manage and synchronise the drawing object list """ def __init__(self, instance): self.instance = instance def _sync(self): #Sync the object list with the viewer self.list = [] #Loop through retrieved object list for obj in self.instance.state["objects"]: #Exists in our own list? if obj["name"] in self: #Update object with new properties self[obj["name"]]._setprops(obj) self.list.append(self[obj["name"]]) else: #Create a new object wrapper o = Object(obj, self.instance) self[obj["name"]] = o self.list.append(o) #Flag sync self[obj["name"]].found = True #Save the object id and reference (use id # to get) _id = len(self.list) self.list[-1].id = _id self.list[-1].ref = self.instance.app.getObject(_id) #Delete any objects from stored dict that are no longer present for name in self.keys(): if not self[name].found: del self[name] else: self[name].found = False def __repr__(self): rep = '{\n' for key in self.keys(): rep += '"' + key + '": {},' rep += '}\n' def __str__(self): #Default string representation is a comma separated list return ', '.join(self.keys())
[docs]class Fig(dict): """ The Fig class wraps a figure """ def __init__(self, instance, name): self.instance = instance self.name = name #Init prop dict for tab completion super(Fig, self).__init__(**self.instance.properties) def __getitem__(self, key): if not key in self.instance.properties: raise ValueError(key + " : Invalid property name") #Activate this figure on viewer self.load() #Return key on viewer instance if key in self.instance: return self.instance[key] #Default to the property lookup dict return super(Fig, self).__getitem__(key) def __setitem__(self, key, value): if not key in self.instance.properties: raise ValueError(key + " : Invalid property name") #Activate this figure on viewer self.load() #Set new value self.instance[key] = value #Save changes self.save() def __repr__(self): return '{"' + self.name + '"}' def __str__(self): return self.name def load(self): #Activate this figure on viewer self.instance.figure(self.name) def save(self): #Save changes self.instance.savefigure(self.name) def show(self, *args, **kwargs): #Activate this figure on viewer self.load() #Render self.instance.display(*args, **kwargs) def image(self, *args, **kwargs): #Activate this figure on viewer self.load() #Render return self.instance.image(*args, **kwargs)
[docs]class Viewer(dict): """ *The Viewer class provides an interface to a LavaVu session* Parameters ---------- arglist: list list of additional init arguments to pass database: str initial database (or model) file to load figure: int initial figure id to display timestep: int initial timestep to display port: int web server port verbose: boolean verbose output to command line for debugging interactive: boolean begin in interactive mode, opens gui window and passes control to event loop immediately hidden: boolean begin hidden, for offscreen rendering or web browser control cache: boolean cache all model timesteps in loaded database, everything loaded into memory on startup (assumes enough memory is available) quality: integer Render sampling quality, render 2^N times larger image and downsample output For anti-aliasing image rendering where GPU multisample anti-aliasing is not available writeimage: boolean Write images and quit, create images for all figures/timesteps in loaded database then exit resolution: list, tuple Window/image resolution in pixels [x,y] script: list List of script commands to run after initialising initscript: boolean Set to False to disable loading any "init.script" file found in current directory usequeue: boolean Set to True to add all commands to a background queue for processing rather than immediate execution **kwargs: Remaining keyword args will be passed to the created viewer and parsed into the initial set of global properties Example ------- Create a viewer, setting the initial background colour to white >>> import lavavu >>> lv = lavavu.Viewer(background="white") Objects can be added by loading files: >>> obj = lv.file('model.obj') Or creating empty objects and loading data: >>> obj = lv.points('mypoints') >>> obj.vertices([0,0,0], [1,1,1]) Viewer commands can be called as methods on the viewer object: >>> lv.rotate('x', 45) Viewer properties can be set by using it like a dictionary: >>> lv["background"] = "grey50" A list of available commands and properties can be found in the wiki: https://github.com/OKaluza/LavaVu/wiki/Scripting-Commands-Reference https://github.com/OKaluza/LavaVu/wiki/Property-Reference or by using the online help: >>> lv.help('rotate') >>> lv.help('opacity') """
[docs] def __init__(self, arglist=None, database=None, figure=None, timestep=None, port=0, verbose=False, interactive=False, hidden=True, cache=False, quality=2, writeimage=False, resolution=None, script=None, initscript=False, usequeue=False, binpath=libpath, omegalib=False, *args, **kwargs): """ Create and init viewer instance Parameters ---------- (see Viewer class docs for setup args) binpath: str Override the executable path omegalib: boolean For use in VR mode, disables some conflicting functionality and parsed into the initial set of global properties """ self.resolution = (640,480) self._ctr = 0 self.app = None self._objects = Objects(self) self.state = {} try: self.app = LavaVuPython.LavaVu(binpath, omegalib) #Get property dict self.properties = convert_keys(json.loads(self.app.propertyList())) #Init prop dict for tab completion super(Viewer, self).__init__(**self.properties) self.setup(arglist, database, figure, timestep, port, verbose, interactive, hidden, cache, quality, writeimage, resolution, script, initscript, usequeue, *args, **kwargs) #Control setup, expect html files in same path as viewer binary control.htmlpath = os.path.join(binpath, "html") if not os.path.isdir(control.htmlpath): control.htmlpath = None print("Can't locate html dir, interactive view disabled") #Create a control factory self.control = control.ControlFactory(self) #Get available commands self._cmdcategories = self.app.commandList() self._cmds = {} self._allcmds = [] for c in self._cmdcategories: self._cmds[c] = self.app.commandList(c) self._allcmds += self._cmds[c] #Create command methods selfdir = dir(self) for key in self._allcmds: #Check if a method exists already if key in selfdir: existing = getattr(self, key, None) if existing: #Add the lavavu doc entry to the docstring doc = "" if existing.__doc__: if "Wraps LavaVu" in existing.__doc__: continue #Already modified doc += existing.__doc__ + '\n----\nWraps LavaVu script command of the same name:\n > **' + key + '**:\n' doc += self.app.helpCommand(key, False) #These should all be wrapper for the matching lavavu commands #(Need to ensure we don't add methods that clash) existing.__func__.__doc__ = doc else: #Use a closure to define a new method that runs this command def cmdmethod(name): def method(*args, **kwargs): args = [name] + [str(a) for a in args] self.commands(' '.join(args)) return method #Create method that runs this command: method = cmdmethod(key) #Set docstring method.__doc__ = self.app.helpCommand(key, False) #Add the new method self.__setattr__(key, method) #Add object by geom type shortcut methods #(allows calling add by geometry type, eg: obj = lavavu.lines()) for key in geomtypes.keys(): #Use a closure to define a new method to call addtype with this type def addmethod(name): def method(*args, **kwargs): return self._addtype(name, *args, **kwargs) return method method = addmethod(key) #Set docstring method.__doc__ = "Add a " + key + " visualisation object,\nany data loaded into the object will be plotted as " + key self.__setattr__(key, method) except (RuntimeError) as e: print("LavaVu Init error: " + str(e)) pass
[docs] def setup(self, arglist=None, database=None, figure=None, timestep=None, port=0, verbose=False, interactive=False, hidden=True, cache=False, quality=2, writeimage=False, resolution=None, script=None, initscript=False, usequeue=False, **kwargs): """ Execute the viewer, initialising with provided arguments and entering event loop if requested Parameters ---------- see __init__ docs """ #Convert options to args global default_args args = default_args[:] if not initscript: args += ["-S"] if verbose: args += ["-v"] #Automation: scripted mode, no interaction if not interactive: args += ["-a"] #Hidden window if hidden: args += ["-h"] #Timestep cache if cache: args += ["-c1"] #Subsample anti-aliasing for image output args += ["-z" + str(quality)] #Timestep range if timestep != None: if isinstance(timestep, int): args += ["-" + str(timestep)] if isinstance(timestep, (tuple, list)) and len(timestep) > 1: args += ["-" + str(timestep[0]), "-" + str(timestep[1])] #Web server args += ["-p" + str(port)] #Database file if database: args += [database] #Initial figure if figure != None: args += ["-f" + str(figure)] #Resolution if resolution != None and isinstance(resolution,tuple) or isinstance(resolution,list): #Output res args += ["-x" + str(resolution[0]) + "," + str(resolution[1])] #Interactive res args += ["-r" + str(resolution[0]) + "," + str(resolution[1])] self.resolution = resolution #Save image and quit if writeimage: args += ["-I"] if script and isinstance(script,list): args += script if arglist: if isinstance(arglist, list): args += arglist else: args += [str(arglist)] self.queue = usequeue if verbose: print(args) try: self.app.run(args) if database: #Load objects/state self._get() #Additional keyword args = properties for key in kwargs: self[key] = kwargs[key] except (RuntimeError) as e: print("LavaVu Run error: " + str(e)) pass
#dict methods def __getitem__(self, key): #Get view/global property self._get() view = self.state["views"][0] if key in view: return view[key] elif key in self.state: return self.state[key] elif key in self.state["properties"]: return self.state["properties"][key] elif key in self.properties: return self.properties[key][0] else: raise ValueError(key + " : Invalid property name") return None def __setitem__(self, key, item): #Set view/global property if not key in self.properties: raise ValueError(key + " : Invalid property name") self._get() #self.state = json.loads(self.app.getState()) view = self.state["views"][0] if key in view: view[key] = item elif key in self.state: self.state[key] = item else: self.state["properties"][key] = item self._set() def __contains__(self, key): return key in self.state or key in self.state["properties"] or key in self.state["views"][0] def __repr__(self): self._get() #return '{"' + self.state["properties"]["caption"] + '"}' return '{}' def __str__(self): #View/global props to string self._get() properties = self.state["properties"] properties.update(self.state["views"][0]) return str('\n'.join([' %s=%s' % (k,json.dumps(v)) for k,v in properties.iteritems()])) @property def objects(self): """ Returns the active objects Returns ------- objects: Objects(dict) An dictionary wrapper containing the list of available visualisation objects Can be printed, iterated or accessed as a dictionary by object name """ self._get() return self._objects @property def colourmaps(self): """ Returns the list of active colourmaps Returns ------- colourmaps: (dict) An dictionary containing the list of available colourmaps """ self._get() maps = {} for cm in self.state["colourmaps"]: maps[cm["name"]] = cm return maps @property def figures(self): """ Retrieve the saved figures from loaded model Dict returned contains Fig objects which can be used to modify the figure Returns ------- figures: dict A dictionary of all available figures by name """ if not self.app.amodel: return {} figs = self.app.amodel.fignames figures = {} for fig in figs: figures[fig] = Fig(self, name=fig) return figures @property def steps(self): """ Retrieve the time step data from loaded model Returns ------- timesteps: list A list of all available time steps """ return self.timesteps()
[docs] def Figure(self, name, objects=None, **kwargs): """ Create a figure Parameters ---------- name: str Name of the figure objects: list List of objects or object names to include in the figure, others will be hidden Returns ------- figure: Figure Figure object """ #Show only objects in list if objects and isinstance(objects, list): #Hide all first for o in self._objects: self._objects[o]["visible"] = False #Show by name or Object for o in objects: if isinstance(o, str): if o in self._objects: o["visible"] = True else: if o in self._objects.list: o["visible"] = True #Create a new figure self.savefigure(name) #Sync list figs = self.figures #Get figure wrapper object fig = figs[name] #Additional keyword args = properties for key in kwargs: fig[key] = kwargs[key] #Return figure return fig
@property def step(self): """ step (int): Returns current timestep """ return self.timestep() @step.setter def step(self, value): """ step (int): Sets current timestep """ self.timestep(value) def _get(self): #Import state from lavavu self.state = convert_keys(json.loads(self.app.getState())) self._objects._sync() def _set(self): #Export state to lavavu #(include current object list state) #self.state["objects"] = [obj.dict for obj in self._objects.list] self.app.setState(json.dumps(self.state))
[docs] def commands(self, cmds): """ Execute viewer commands https://github.com/OKaluza/LavaVu/wiki/Scripting-Commands-Reference These commands can also be executed individually by calling them as methods of the viewer object Parameters ---------- cmds: list, str Command(s) to execute """ if isinstance(cmds, list): cmds = '\n'.join(cmds) if self.queue: #Thread safe queue requested self.app.queueCommands(cmds) else: self.app.parseCommands(cmds)
[docs] def help(self, cmd="", obj=None): """ Get help on a command or property Parameters ---------- cmd: str Command to get help with, if ommitted displays general introductory help If cmd is a property or is preceded with '@' will display property help """ if obj is None: obj = self md = "" if not len(cmd): md += _docmd(obj.__doc__) elif cmd in dir(obj) and callable(getattr(obj, cmd)): md = '### ' + cmd + ' \n' md += _docmd(getattr(obj, cmd).__doc__) else: if cmd[0] != '@': cmd = '@' + cmd md = self.app.helpCommand(cmd) _markdown(md)
[docs] def __call__(self, cmds): """ Run a LavaVu script Parameters ---------- cmds: str String containing commands to run, separate commands with semi-colons" Example ------- >>> lv('reset; translate -2') """ self.commands(cmds)
def _setupobject(self, ref=None, **kwargs): #Strip data keys from kwargs and put aside for loading datasets = {} cmapstr = None for key in kwargs.keys(): if key in ["vertices", "normals", "vectors", "colours", "indices", "values", "labels"]: datasets[key] = kwargs.pop(key, None) #Call function to add/setup the object, all other args passed to properties dict if ref is None: ref = self.app.createObject(str(json.dumps(kwargs))) else: self.app.setObject(ref, str(json.dumps(kwargs))) #Get the created/update object obj = self.Object(ref) #Read any property data sets (allows object creation and load with single prop dict) for key in datasets: #Get the load function matching the data set (eg: vertices() ) and call on data func = getattr(obj, key) func(datasets[key]) #Return wrapper obj return obj
[docs] def add(self, name, **kwargs): """ Add a visualisation object Parameters ---------- name: str Name to apply to the created object If an object of this name exists, it will be returned instead of created Returns ------- obj: Object The object created """ if isinstance(self._objects, Objects) and name in self._objects: print("Object exists: " + name) return self._objects[name] #Put provided name in properties if name and len(name): kwargs["name"] = name #Adds a new object, all other args passed to properties dict return self._setupobject(ref=None, **kwargs)
#Shortcut for adding specific geometry types def _addtype(self, typename, name=None, **kwargs): #Set name to typename if none provided if not name: self._ctr += 1 name = typename + str(self._ctr) kwargs["geometry"] = typename return self.add(name, **kwargs) def grid(self, name=None, width=0, height=0, dims=[2,2], vertices=None, *args, **kwargs): #Creates a quads object, obj = self._addtype("quads", name, dims=dims, *args, **kwargs) if width > 0 and height > 0 and vertices is None: vertices = [] yc = 0.0 for y in range(dims[1]): xc = 0.0 for x in range(dims[0]): vertices.append([xc, yc, 0]) xc += width / float(dims[0]) yc += height / float(dims[1]) obj.vertices(vertices) return obj
[docs] def Object(self, identifier=None, **kwargs): """ Get or create a visualisation object Parameters ---------- identifier: str,int,Object (Optional) If a string, lookup an object by name If a number, lookup object by index If an object reference, lookup the Object by reference If omitted, return the last object in the list If no matching object found and string identifier provided, creates an empty object **kwargs: Set of properties passed to the object Returns ------- obj: Object The object located """ #Return object by name/ref or last in list if none provided #Get state and update object list self._get() o = None if len(self._objects.list) == 0: print("WARNING: No objects exist!") #If name passed, find this object in updated list, if not just use the last elif isinstance(identifier, str): for obj in self._objects.list: if obj.name == identifier: o = obj break #Not found? Create if not o: return self.add(identifier, **kwargs) elif isinstance(identifier, int): #Lookup by index if len(self._objects.list) >= identifier: o = self._objects.list[identifier-1] elif isinstance(identifier, LavaVuPython.DrawingObject): #Lookup by swig wrapped object for obj in self._objects.list: #Can't compare swig wrapper objects directly, #so use the name if obj.name == identifier.name(): o = obj break else: #Last resort: last object in list o = self._objects.list[-1] if o is not None: self.app.setObject(o.ref, str(json.dumps(kwargs))) return o print "WARNING: Object not found and could not be created: ",identifier return None
[docs] def file(self, filename, obj=None, **kwargs): """ Load a database or model file Parameters ---------- filename: str Name of the file to load obj: Object Vis object to load the file data into, if not provided a default will be created """ #Load a new object from file self.app.loadFile(filename) #Get last object added if none provided if obj is None: obj = self.Object() #Setups up new object, all other args passed to properties dict return self._setupobject(obj.ref, **kwargs)
[docs] def files(self, filespec, obj=None, **kwargs): """ Load data from a series of files (using wildcards or a list) Parameters ---------- files: str Specification of the files to load obj: Object Vis object to load the data into, if not provided a default will be created """ #Load list of files with glob filelist = glob.glob(filespec) obj = None for infile in sorted(filelist): obj = self.file(infile, kwargs) return obj
[docs] def colourbar(self, obj=None, **kwargs): """ Create a new colourbar Parameters ---------- obj: Object (optional) Vis object the colour bar applies to Returns ------- colourbar: Object The colourbar object created """ #Create a new colourbar if obj is None: ref = self.app.colourBar() else: ref = self.app.colourBar(obj.ref) if not ref: return #Update list self._get() #Ensure in sync #Setups up new object, all other args passed to properties dict return self._setupobject(ref, **kwargs)
[docs] def defaultcolourmaps(self): """ Get the list of default colour map names Returns ------- colourmaps: list of str Names of all predefined colour maps """ return list(LavaVuPython.ColourMap.getDefaultMapNames())
[docs] def defaultcolourmap(self, name): """ Get content of a default colour map Parameters ---------- name: str Name of the built in colourmap to return Returns ------- data: str Colourmap data formatted as a string """ return LavaVuPython.ColourMap.getDefaultMap(name)
[docs] def colourmap(self, name, data, reverse=False, monochrome=False, **kwargs): """ Load or create a colour map Parameters ---------- name: str Name of the colourmap, if this colourmap name is found the data will replace the existing colourmap, otherwise a new colourmap will be created data: list,str Provided colourmap data can be - a string, - list of colour strings, - list of position,value tuples - or a built in colourmap name reverse: boolean Reverse the order of the colours after loading monochrome: boolean Convert to greyscale Returns ------- colourmap: int The name of the colourmap loaded/created """ cmap = self.app.addColourMap(name) cmap._setup(self.app, data, reverse, monochrome, str(json.dumps(kwargs))) #TODO: Dict of colourmaps by name stored on Viewer (lv.colourmaps) return cmap.name
[docs] def getcolourmap(self, mapid, string=True): """ Return colour map data as a string or list Either return format can be used to create/modify a colourmap with colourmap() Parameters ---------- mapid: string/int Name or index of the colourmap to retrieve string: boolean The default is to return the data as a string of colours separated by semi-colons To instead return a list of (position,[R,G,B,A]) tuples for easier automated processing in python, set this to False Returns ------- mapdata: str/list The formatted colourmap data """ #Return colourmap as a string/array that can be passed to re-create the map self._get() #Ensure in sync arr = [] cmaps = self.state["colourmaps"] cmap = None if isinstance(mapid,str): for cm in cmaps: if cm["name"] == mapid: cmap = cm break return '' if string else arr else: if mapid < 0 or mapid >= len(cmaps): return '' if string else arr cm = cmaps[mapid] if string: cmstr = '"""' for c in cm["colours"]: cmstr += "%6.4f=%s; " % (c["position"],c["colour"]) cmstr += '"""\n' return cmstr else: import re for c in cm["colours"]: comp = re.findall(r"[\d\.]+", c["colour"]) comp = [int(comp[0]), int(comp[1]), int(comp[2]), int(255*float(comp[3]))] arr.append((c["position"], comp)) return arr
[docs] def clear(self, objects=True, colourmaps=True): """ Clears all data from the visualisation Including objects and colourmaps by default Parameters ---------- objects: boolean including all objects, if set to False will clear the objects but not delete them colourmaps: boolean including all colourmaps """ self.app.clearAll(objects, colourmaps) self._get() #Ensure in sync
[docs] def store(self, filename="state.json"): """ Store current visualisation state as a json file (Includes all properties, colourmaps and defined objects but not their geometry data) Parameters ---------- filename: str Name of the file to save """ with open(filename, "w") as state_file: state_file.write(self.app.getState())
[docs] def restore(self, filename="state.json"): """ Restore visualisation state from a json file (Includes all properties, colourmaps and defined objects but not their geometry data) Parameters ---------- filename: str Name of the file to load """ with open(filename, "r") as state_file: self.app.setState(state_file.read())
[docs] def timesteps(self): """ Retrieve the time step data from loaded model Returns ------- timesteps: list A list of all available time steps """ return json.loads(self.app.getTimeSteps())
[docs] def addstep(self, step=-1): """ Add a new time step Parameters ---------- step: int (Optional) Time step number, default is current + 1 """ self.app.addTimeStep(step)
[docs] def render(self): """ Render a new frame, explicit display update """ self.app.render()
[docs] def init(self): """ Re-initialise the viewer window """ self.app.init()
[docs] def update(self, filter=None, compress=True): """ Write visualisation data back to the database Parameters ---------- filter: str Optional filter to type of geometry to be updated, if omitted all will be written (labels, points, grid, triangles, vectors, tracers, lines, shapes, volume) compress: boolean Use zlib compression when writing the geometry data """ for obj in self._objects.list: #Update object data at current timestep if filter is None: #Re-writes all data to db for object self.app.update(obj.ref, compress) else: #Re-writes data to db for object and geom type self.app.update(obj.ref, geomtypes[filter], compress) #Update figure self.savefigure()
[docs] def image(self, filename="", resolution=None, transparent=False): """ Save or get an image of current display Parameters ---------- filename: str Name of the file to save (should be .jpg or .png), if not provided the image will be returned as a base64 encoded data url resolution: list, tuple Image resolution in pixels [x,y] transparent: boolean Creates a PNG image with a transparent background Returns ------- image: str filename of saved image or encoded image as string data """ if resolution is None: return self.app.image(filename, 0, 0, 0, transparent); return self.app.image(filename, resolution[0], resolution[1], 0, transparent);
[docs] def frame(self, resolution=None): """ Get an image frame, returns current display as base64 encoded jpeg data url Parameters ---------- resolution: list, tuple Image resolution in pixels [x,y] Returns ------- image: str encoded image as string data """ #Jpeg encoded frame data if not resolution: resolution = self.resolution return self.app.image("", resolution[0], resolution[1], 90);
[docs] def display(self, resolution=(0,0), transparent=False): """ Show the current display as inline image within an ipython notebook. If IPython is installed, displays the result image content inline If IPython is not installed, will save the result with a default filename in the current directory Parameters ---------- resolution: list, tuple Image resolution in pixels [x,y] transparent: boolean Creates a PNG image with a transparent background """ if is_notebook(): from IPython.display import display,Image,HTML #Return inline image result img = self.app.image("", resolution[0], resolution[1], 0, transparent) display(HTML("<img src='%s'>" % img)) else: #Not in IPython, call default image save routines (autogenerated filenames) self.app.image("*", resolution[0], resolution[1], 0, transparent)
[docs] def webgl(self, resolution=(640,480)): """ Shows the generated model inline within an ipython notebook. If IPython is installed, displays the result WebGL content inline If IPython is not installed, will save the result with a default filename in the current directory Parameters ---------- resolution: list, tuple Display window resolution in pixels [x,y] """ try: if is_notebook(): #TODO: build single html file instead with inline scripts/data/custom controls #Create link to web content directory if not os.path.isdir("html"): htmldir = os.path.join(binpath, 'html') os.symlink(htmldir, 'html') from IPython.display import display,Image,HTML #Write files to disk first, can be passed directly on url but is slow for large datasets filename = "input.json" text_file = open("html/" + filename, "w") text_file.write(self.app.web()); text_file.close() from IPython.display import IFrame display(IFrame("html/viewer.html#" + filename, width=resolution[0], height=resolution[1])) else: self.app.web(True) except (Exception) as e: print("WebGL output error: " + str(e)) pass
[docs] def video(self, filename="", fps=10, resolution=(0,0)): """ Shows the generated video inline within an ipython notebook. If IPython is installed, displays the result video content inline If IPython is not installed, will save the result in the current directory Parameters ---------- filename: str Name of the file to save, if not provided a default will be used fps: int Frames to output per second of video resolution: list, tuple Video resolution in pixels [x,y] """ try: fn = self.app.video(filename, fps, resolution[0], resolution[1]) if is_notebook(): from IPython.display import display,HTML html = """ <video src="---FN---" controls loop> Sorry, your browser doesn't support embedded videos, </video><br> <a href="---FN---">Download Video</a> """ #Get a UUID based on host ID and current time import uuid uid = uuid.uuid1() html = html.replace('---FN---', fn + "?" + str(uid)) display(HTML(html)) except (Exception) as e: print("Video output error: " + str(e)) pass
[docs] def imageBytes(self, width=640, height=480, channels=3): """ Return raw image data Parameters ---------- width: int Image width in pixels height: int Image height in pixels channels: int colour channels/depth in bytes (1=luminance, 3=RGB, 4=RGBA) Returns ------- image: array Numpy array of the image data requested """ img = numpy.zeros(shape=(width,height,channels), dtype=numpy.uint8) self.imageBuffer(img) return img
[docs] def testimages(self, imagelist=None, tolerance=TOL_DEFAULT, expectedPath='expected/', outputPath='./', clear=True): """ Compare a list of images to expected images for testing Parameters ---------- imagelist: list List of test images to compare with expected results If not provided, will process all jpg and png images in working directory tolerance: float Tolerance level of difference before a comparison fails, default 0.0001 expectedPath: str Where to find the expected result images (should have the same filenames as output images) outputPath: str Where to find the output images clear: boolean If the test passes the output images will be deleted, set to False to disable deletion """ results = [] if not os.path.isdir(expectedPath): print "No expected data, copying found images to expected folder..." os.makedirs(expectedPath) from shutil import copyfile if not imagelist: #Get all images in cwd imagelist = glob.glob("*.png") imagelist += glob.glob("*.jpg") print imagelist for image in imagelist: copyfile(image, os.path.join(expectedPath, image)) if not imagelist: #Default to all png images in expected dir cwd = os.getcwd() os.chdir(expectedPath) imagelist = glob.glob("*.png") imagelist += glob.glob("*.jpg") imagelist.sort(key=os.path.getmtime) os.chdir(cwd) for f in sorted(imagelist): outfile = outputPath+f expfile = expectedPath+f results.append(self.testimage(expfile, outfile, tolerance)) #Combined result overallResult = all(results) if not overallResult: raise RuntimeError("Image tests failed due to one or more image comparisons above tolerance level!") if clear: try: for f in imagelist: os.remove(f) except: pass print("-------------\nTests Passed!\n-------------")
[docs] def testimage(self, expfile, outfile, tolerance=TOL_DEFAULT): """ Compare two images Parameters ---------- expfile: str Expected result image outfile: str Test output image tolerance: float Tolerance level of difference before a comparison fails, default 0.0001 Returns ------- result: boolean True if test passes, difference <= tolerance False if test fails, difference > tolerance """ if not os.path.exists(expfile): print("Test skipped, Reference image '%s' not found!" % expfile) return 0 if len(outfile) and not os.path.exists(outfile): raise RuntimeError("Generated image '%s' not found!" % outfile) diff = self.app.imageDiff(outfile, expfile) result = diff <= tolerance reset = '\033[0m' red = '\033[91m' green = '\033[92m' failed = red + 'FAIL' + reset passed = green + 'PASS' + reset if not result: print("%s: %s Image comp errors %f, not"\ " within tolerance %g of reference image."\ % (failed, outfile, diff, tolerance)) global echo_fails if echo_fails: print("__________________________________________") if len(outfile): print(outfile) with open(outfile, mode='rb') as f: data = f.read() import base64 print("data:image/png;base64,",base64.b64encode(data)) else: print("Buffer:") print(self.app.image("")) print("__________________________________________") else: print("%s: %s Image comp errors %f, within tolerance %f"\ " of ref image."\ % (passed, outfile, diff, tolerance)) return result
[docs] def serve(self): """ Run a web server in python (experimental) This uses server.py to launch a simple web server Not threaded like the mongoose server used in C++ to is limited Currently recommended to use threaded web server by supplying port=#### argument when creating viewer instead """ try: import server server.serve(self) except (Exception) as e: print("LavaVu error: " + str(e)) print("Web Server failed to run") import traceback traceback.print_exc() pass
[docs] def window(self): """ Create and display an interactive viewer instance This shows an active viewer window to the visualisation that can be controlled with the mouse or html widgets """ if self in control.windows: print "Viewer window exists, only one instance per Viewer permitted" return self.control.Window() self.control.show()
[docs] def redisplay(self): """ Update the display of any interactive viewer This is required after changing vis properties from python so the changes are reflected in the viewer """ #Issue redisplay to active viewer self.control.redisplay()
[docs] def camera(self): """ Get the current camera viewpoint Displays camera in python code form that can be pasted and executed to restore the same camera view """ self._get() me = getVariableName(self) if not me: me = "lv" qrot = self.state["views"][0]["rotate"] rot = self.state["views"][0]["xyzrotate"] tr = self.state["views"][0]["translate"] for r in range(3): rot[r] = round(rot[r], 3) tr[r] = round(tr[r], 3) qrot[3] = round(qrot[3], 3) print(me + ".translation(" + str(tr)[1:-1] + ")") print(me + ".rotation(" + str(rot)[1:-1] + ")") #Also print in terminal for debugging self.commands("camera") return {"translation" : tr, "xyzrotation" : rot, "rotation" : qrot}
[docs] def getview(self): """ Get current view settings Returns ------- view: str json string containing saved view settings """ self._get() return json.dumps(self.state["views"][0])
[docs] def setview(self, view): """ Set current view settings Parameters ---------- view: str json string containing saved view settings """ self.state["views"][0] = convert_keys(json.loads(view))
#Wrapper for list of geomdata objects
[docs]class Geometry(list): """ The Geometry class provides an interface to a drawing object's internal data Geometry instances are created internally in the Object class with the data() method Example ------- Get all object data >>> data = obj.data() Get only triangle data >>> data = obj.data("triangles") Loop through data >>> data = obj.data() >>> for el in obj.data: >>> print(el) """ def __init__(self, obj, filter=None): self.obj = obj #Get a list of geometry data objects for a given drawing object count = self.obj.instance.app.getGeometryCount(self.obj.ref) for idx in range(count): g = self.obj.instance.app.getGeometry(self.obj.ref, idx) #By default all elements are returned, even if object has multiple types #Filter can be set to a type name to exclude other geometry types if filter is None or g.type == geomtypes[filter]: self.append(GeomData(g, obj.instance)) def __str__(self): return '[' + ', '.join([str(i) for i in self]) + ']'
#Wrapper class for GeomData geometry object
[docs]class GeomData(object): """ The GeomData class provides an interface to a single object data element GeomData instances are created internally from the Geometry class copy(), get() and set() methods provide access to the data types Example ------- Get the most recently used object data element >>> data = obj.data() >>> el = data[-1] Get a copy of the rgba colours (if any) >>> colours = el.copy("rgba") Get a view of the vertex data >>> verts = el.get("vertices") WARNING: this reference may be deleted by other calls, only use get() if you are processing the data immediately and not relying on it continuing to exist later Get a copy of some value data by label >>> verts = el.copy("colourvals") Load some new values for these vertices >>> el.set("sampledfield", newdata) """ def __init__(self, data, instance): self.data = data self.instance = instance
[docs] def get(self, typename): """ Get a data element from a set of geometry data Warning... other than for internal use, should always immediately make copies of the data there is no guarantee memory will not be released! Parameters ---------- typename: str Type of data to be retrieved (vertices/normals/vectors/indices/colours/texcoords/luminance/rgb/values) Returns ------- data: array Numpy array view of the data set requested """ array = None if typename in datatypes: if typename in ["luminance", "rgb"]: #Get uint8 data array = self.instance.app.geometryArrayViewUChar(self.data, datatypes[typename]) elif typename in ["indices", "colours"]: #Get uint32 data array = self.instance.app.geometryArrayViewUInt(self.data, datatypes[typename]) else: #Get float32 data array = self.instance.app.geometryArrayViewFloat(self.data, datatypes[typename]) else: #Get float32 data array = self.instance.app.geometryArrayViewFloat(self.data, typename) return array
[docs] def copy(self, typename): """ Get a copy of a data element from a set of geometry data This is a safe version of get() that copies the data before returning so can be assured it will remain valid Parameters ---------- typename: str Type of data to be retrieved (vertices/normals/vectors/indices/colours/texcoords/luminance/rgb/values) Returns ------- data: array Numpy array containing a copy of the data set requested """ #Safer data access, makes a copy to ensure we still have access #to the data no matter what viewer does with it return numpy.copy(self.get(typename))
[docs] def set(self, typename, array): """ Set a data element in a set of geometry data Parameters ---------- typename: str Type of data to set (vertices/normals/vectors/indices/colours/texcoords/luminance/rgb/values) array: array Numpy array holding the data to be written """ if typename in datatypes: if typename in ["luminance", "rgb"]: #Set uint8 data self.instance.app.geometryArrayUInt8(self.data, array, datatypes[typename]) elif typename in ["indices", "colours"]: #Set uint32 data self.instance.app.geometryArrayUInt32(self.data, array, datatypes[typename]) else: #Set float32 data self.instance.app.geometryArrayFloat(self.data, array, datatypes[typename]) else: #Set float32 data self.instance.app.geometryArrayFloat(self.data, array, typename)
def __str__(self): return [key for key, value in geomtypes.items() if value == self.data.type][0]
[docs]def cubeHelix(samples=16, start=0.5, rot=-0.9, sat=1.0, gamma=1., alpha=None): """ Create CubeHelix spectrum colourmap with monotonically increasing/descreasing intensity Implemented from FORTRAN 77 code from D.A. Green, 2011, BASI, 39, 289. "A colour scheme for the display of astronomical intensity images" http://adsabs.harvard.edu/abs/2011arXiv1108.5083G Parameters ---------- samples: int Number of colour samples to produce start: float Start colour [0,3] 1=red,2=green,3=blue rot: float Rotations through spectrum, negative to reverse direction sat: float Colour saturation grayscale to full [0,1], >1 to oversaturate gamma: float Gamma correction [0,1] alpha: list,tuple Alpha [min,max] for transparency ramp Returns ------- colours: list List of colours ready to be loaded by colourmap() """ colours = [] if not isinstance(alpha,list) and not isinstance(alpha,tuple): #Convert as boolean if alpha: alpha = [0,1] for i in range(0,samples+1): fract = i / float(samples) angle = 2.0 * math.pi * (start / 3.0 + 1.0 + rot * fract) amp = sat * fract * (1 - fract) fract = pow(fract, gamma) r = fract + amp * (-0.14861 * math.cos(angle) + 1.78277 * math.sin(angle)) g = fract + amp * (-0.29227 * math.cos(angle) - 0.90649 * math.sin(angle)) b = fract + amp * (+1.97294 * math.cos(angle)) r = max(min(r, 1.0), 0.0) g = max(min(g, 1.0), 0.0) b = max(min(b, 1.0), 0.0) a = 1.0 if alpha: a = alpha[0] + (alpha[1]-alpha[0]) * fract colours.append((fract, 'rgba(%d,%d,%d,%d)' % (r*0xff, g*0xff, b*0xff, a*0xff))) return colours
[docs]def loadCPT(fn, positions=True): """ Create a colourmap from a CPT colour table file Parameters ---------- positions: boolean Set this to false to ignore any positional data and load only the colours from the file Returns ------- colours: string Colour string ready to be loaded by colourmap() """ result = "" values = [] colours = [] hexcolours = [] hsv = False hinge = None def addColour(val, colour): if len(values) and val == values[-1]: if colour == colours[-1]: return #Skip duplicates val += 0.001 #Add a small increment values.append(val) if isinstance(colour, str): if '/' in colour: colour = colour.split('/') if '-' in colour: colour = colour.split('-') if isinstance(colour, list): if hsv: colour = [float(v) for v in colour] import colorsys colour = colorsys.hsv_to_rgb(colour[0]/360.0,colour[1],colour[2]) colour = [int(v*255) for v in colour] else: colour = [int(v) for v in colour] if len(colours) == 0 or colour != colours[-1]: if isinstance(colour, str): hexcolours.append(colour) else: hexcolours.append("#%02x%02x%02x" % (colour[0],colour[1],colour[2])) colours.append(colour) with open(fn, "r") as cpt_file: for line in cpt_file: if "COLOR_MODEL" in line and 'hsv' in line.lower(): hsv = True continue if "HINGE" in line: line = line.split('=') hinge = float(line[1]) continue if line[0] == '#': continue if line[0] == 'B': continue if line[0] == 'F': continue if line[0] == 'N': continue line = line.split() #RGB/HSV space separated? if len(line) > 7: addColour(float(line[0]), [int(line[1]), int(line[2]), int(line[3])]) addColour(float(line[4]), [int(line[5]), int(line[6]), int(line[7])]) #Pass whole strings if / or - separated elif len(line) > 1: addColour(float(line[0]), line[1]) if len(line) > 3: addColour(float(line[2]), line[3]) minval = min(values) maxval = max(values) vrange = maxval - minval #print "HINGE: ",hinge,"MIN",minval,"MAX",maxval if positions: for v in range(len(values)): #Centre hinge value if hinge is not None: if values[v] == hinge: values[v] = 0.5 elif values[v] < hinge: values[v] = 0.5 * (values[v] - minval) / (hinge - minval) elif values[v] > hinge: values[v] = 0.5 * (values[v] - hinge) / (maxval - hinge) + 0.5 else: values[v] = (values[v] - minval) / vrange if isinstance(colours[v], str): result += "%.5f=%s; " % (values[v], colours[v]) else: result += "%.5f=rgb(%d,%d,%d); " % (values[v], colours[v][0], colours[v][1], colours[v][2]) else: for v in range(len(hexcolours)): #print "(%f)%s" % (values[v], hexcolours[v]), result += hexcolours[v] + " " return result
[docs]def getVariableName(var): """ Attempt to find the name of a variable from the main module namespace Parameters ---------- var: object The variable in question Returns ------- name: str Name of the variable """ import __main__ as main_mod for name in dir(main_mod): val = getattr(main_mod, name) if val is var: return name return None
[docs]def printH5(h5): """ Print info about HDF5 data set (requires h5py) Parameters ---------- h5: hdf5 object HDF5 Dataset loaded with h5py """ print("------ ",h5.filename," ------") ks = h5.keys() for key in ks[:10]: print(h5[key]) for item in h5.attrs.keys(): print(item + ":", h5.attrs[item])
def _docmd(doc): """Convert a docstring to markdown""" if doc is None: return '' def codeblock(lines): return ['```python'] + [' ' + l for l in lines] + ['```'] md = [] code = [] indent = 0 lastindent = 0 for line in doc.split('\n'): indent = len(line) line = line.strip() indent -= len(line) if len(line) and len(md) and line[0] == '-' and line == len(md[-1].strip()) * '-': #Replace '-----' heading underline with '#### heading" md[-1] = "#### " + md[-1] elif line.startswith('>>> '): #Python code code += [line[4:]] else: #Add code block if len(code): md += codeblock(code) code = [] elif len(md) and indent == lastindent + 4: #Indented block, preserve indent md += ['&nbsp;&nbsp;&nbsp;&nbsp;' + line + ' '] #Keep indenting at this level until indent level changes continue else: md += [line + ' '] #Preserve line breaks lastindent = indent if len(code): md += codeblock(code) return '\n'.join(md) def _markdown(mdstr): """Display markdown in IPython if available, otherwise just print it""" if is_notebook(): from IPython.display import display,Markdown display(Markdown(mdstr)) else: #Not in IPython, just print print(mdstr)