← Back to team overview

yade-dev team mailing list archive

[Branch ~yade-dev/yade/trunk] Rev 1783: 1. New yade.post2d module for 2d post-processing (raw/smooth scalar/vector field plots), with doc...

 

------------------------------------------------------------
revno: 1783
committer: Václav Šmilauer <vaclav@flux>
branch nick: trunk
timestamp: Fri 2009-11-13 12:27:22 +0100
message:
  1. New yade.post2d module for 2d post-processing (raw/smooth scalar/vector field plots), with docs and examples
  2. Add epydoc brief documentation to all python modules
  3. Change constructor of GaussAverage in python to take always the relThreshold parameter
added:
  examples/concrete/uniax-post.py
  py/post2d.py
modified:
  examples/concrete/uniax.py
  gui/py/PythonTCPServer.py
  gui/py/ipython.py
  gui/py/runtime.py
  gui/qt3/qt.py
  lib/smoothing/WeightedAverage2d.hpp
  py/SConscript
  py/WeightedAverage2d.cpp
  py/_packObb.cpp
  py/_packPredicates.cpp
  py/_packSpheres.cpp
  py/eudoxos.py
  py/log.cpp
  py/pack.py
  py/tests/__init__.py
  py/tests/wrapper.py
  py/timing.py
  py/utils.py
  py/yadeWrapper/yadeWrapper.cpp


--
lp:yade
https://code.launchpad.net/~yade-dev/yade/trunk

Your team Yade developers is subscribed to branch lp:yade.
To unsubscribe from this branch go to https://code.launchpad.net/~yade-dev/yade/trunk/+edit-subscription.
=== added file 'examples/concrete/uniax-post.py'
--- examples/concrete/uniax-post.py	1970-01-01 00:00:00 +0000
+++ examples/concrete/uniax-post.py	2009-11-13 11:27:22 +0000
@@ -0,0 +1,38 @@
+#
+# demonstration of the yade.post2d module (see its documentation for details)
+#
+from yade import post2d
+import pylab # the matlab-like interface of matplotlib
+
+# run uniax.py to get this file
+O.load('/tmp/uniax-tension.xml.bz2')
+
+# flattener that project to the xz plane
+flattener=post2d.AxisFlatten(useRef=False,axis=1)
+# return scalar given a Body instance
+extractDmg=lambda b: b.phys['normDmg']
+# will call flattener.planar implicitly
+# the same as: extractVelocity=lambda b: flattener.planar(b,b.phys['velocity'])
+extractVelocity=lambda b: b.phys['velocity']
+
+# create new figure
+pylab.figure()
+# plot raw damage
+post2d.plot(post2d.data(extractDmg,flattener))
+
+# plot smooth damage into new figure
+pylab.figure(); ax,map=post2d.plot(post2d.data(extractDmg,flattener,stDev=2e-3))
+
+# show color scale
+pylab.colorbar(map,orientation='horizontal')
+
+# raw velocity (vector field) plot
+pylab.figure(); post2d.plot(post2d.data(extractVelocity,flattener))
+
+# smooth velocity plot; data are sampled at regular grid
+pylab.figure(); ax,map=post2d.plot(post2d.data(extractVelocity,flattener,stDev=1e-3))
+# save last (current) figure to file
+pylab.gcf().savefig('/tmp/foo.png') 
+
+# show the figures
+pylab.show()

=== modified file 'examples/concrete/uniax.py'
--- examples/concrete/uniax.py	2009-11-04 21:54:10 +0000
+++ examples/concrete/uniax.py	2009-11-13 11:27:22 +0000
@@ -136,6 +136,7 @@
 	if abs(sigma[-1]/extremum)<minMaxRatio or abs(strainer['strain'])>5e-3:
 		if mode=='tension' and doModes & 2: # only if compression is enabled
 			mode='compression'
+			O.save('/tmp/uniax-tension.xml.bz2')
 			print "Damaged, switching to compression... "; O.pause()
 			# important! initTest must be launched in a separate thread;
 			# otherwise O.load would wait for the iteration to finish,

=== modified file 'gui/py/PythonTCPServer.py'
--- gui/py/PythonTCPServer.py	2009-10-22 19:32:22 +0000
+++ gui/py/PythonTCPServer.py	2009-11-13 11:27:22 +0000
@@ -1,3 +1,11 @@
+# encoding: utf-8
+# 2008-2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""
+Remote connections to yade: authenticated python command-line over telnet and anonymous socket for getting some read-only information about current simulation.
+
+These classes are used internally in gui/py/PythonUI_rc.py and are not intended for direct use.
+"""
+
 import SocketServer
 import sys,time
 
@@ -5,7 +13,7 @@
 
 class InfoSocketProvider(SocketServer.BaseRequestHandler):
 	"""Class providing dictionary of important simulation information,
-	without authenticating the access"""
+	without authenticating the access."""
 	def handle(self):
 		import pickle, os
 		O=Omega()
@@ -14,6 +22,11 @@
 		
 
 class PythonConsoleSocketEmulator(SocketServer.BaseRequestHandler):
+	"""Class emulating python command-line over a socket connection.
+
+	The connection is authenticated by requiring a cookie.
+	Only connections from localhost (127.0.0.*) are allowed.
+	"""
 	def setup(self):
 		if not self.client_address[0].startswith('127.0.0'):
 			print "TCP Connection from non-127.0.0.* address %s rejected"%self.client_address[0]
@@ -72,6 +85,7 @@
 		self.request.send('\nBye ' + str(self.client_address) + '\n')
 
 class GenericTCPServer:
+	"Base class for socket server, handling port allocation, initial logging and thead backgrounding."
 	def __init__(self,handler,title,cookie=True,minPort=9000,host='',maxPort=65536,background=True):
 		import socket, random
 		self.port=-1

=== modified file 'gui/py/ipython.py'
--- gui/py/ipython.py	2008-09-21 10:47:46 +0000
+++ gui/py/ipython.py	2009-11-13 11:27:22 +0000
@@ -4,6 +4,8 @@
 #
 # Therefore, it is execfile'd at ipython startup
 #
+"""Yade-specific command-line completion hooks for in ipython (experimental)"""
+
 def yade_completers(self, event):
 	# this should parse the incomplete line and return only the object that pertains to this ['
 	# it will work fine in cases like b['clum<TAB> if b is a Body, but not in b['id']+b2['id

=== modified file 'gui/py/runtime.py'
--- gui/py/runtime.py	2008-05-01 06:29:44 +0000
+++ gui/py/runtime.py	2009-11-13 11:27:22 +0000
@@ -1,1 +1,2 @@
 # this module is populated at initialization from the c++ part of PythonUI
+"""Runtime variables, populated at yade startup."""

=== modified file 'gui/qt3/qt.py'
--- gui/qt3/qt.py	2009-08-05 09:09:52 +0000
+++ gui/qt3/qt.py	2009-11-13 11:27:22 +0000
@@ -1,3 +1,7 @@
+# encoding: utf-8
+# 2008 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""Access/manipulation of the qt3-based yade gui."""
+
 # import module parts in c++ 
 from _qt import *
 

=== modified file 'lib/smoothing/WeightedAverage2d.hpp'
--- lib/smoothing/WeightedAverage2d.hpp	2009-10-11 11:22:25 +0000
+++ lib/smoothing/WeightedAverage2d.hpp	2009-11-13 11:27:22 +0000
@@ -52,7 +52,7 @@
 	const Vector2i& getSize() const{ return nCells;}
 
 	// add new element to the right cell
-	void add(const T& t, Vector2r xy){Vector2i cxy=xy2cell(xy); grid[cxy[0]][cxy[1]].push_back(t);}
+	void add(const T& t, Vector2r xy){bool inGrid; Vector2i cxy=xy2cell(xy,&inGrid); if(!inGrid){ if(cxy[0]<0) cxy[0]=0; if(cxy[0]>=nCells[0]) cxy[0]=nCells[0]-1; if(cxy[1]<0) cxy[1]=0; if(cxy[1]>=nCells[1]) cxy[1]=nCells[1]-1; } grid[cxy[0]][cxy[1]].push_back(t);}
 
 	/* Filters: return list of cells, based on some spatial criterion: rectangle, ellipse, circle (add more if you need that) */
 	vector<Vector2i> rectangleFilter(Vector2r bbLo, Vector2r bbHi) const {
@@ -161,9 +161,10 @@
 	struct Poly2d{vector<Vector2r> vertices; bool inclusive;};
 	vector<Poly2d> clips;
 	public:
-	pyGaussAverage(python::tuple lo, python::tuple hi, python::tuple nCells, Real stDev){
+	pyGaussAverage(python::tuple lo, python::tuple hi, python::tuple nCells, Real stDev, Real relThreshold=3.){
 		shared_ptr<GridContainer<Scalar2d> > g(new GridContainer<Scalar2d>(tuple2vec2r(lo),tuple2vec2r(hi),tuple2vec2i(nCells)));
 		sgda=shared_ptr<SGDA_Scalar2d>(new SGDA_Scalar2d(g,stDev));
+		sgda->relThreshold=relThreshold;
 	}
 	bool pointInsidePolygon(const Vector2r&,const vector<Vector2r>&);
 	bool ptIsClipped(const Vector2r& pt){

=== modified file 'py/SConscript'
--- py/SConscript	2009-10-04 14:37:41 +0000
+++ py/SConscript	2009-11-13 11:27:22 +0000
@@ -28,6 +28,7 @@
 		env.File('timing.py'),
 		env.File('pack.py'),
 		env.File('export.py'),
+		env.File('post2d.py'),
 		env.SharedLibrary('wrapper',['yadeWrapper/yadeWrapper.cpp'],SHLIBPREFIX='',LIBS=linkPlugins(['Shop','BoundingVolumeMetaEngine','GeometricalModelMetaEngine','InteractingGeometryMetaEngine','InteractionGeometryMetaEngine','InteractionPhysicsMetaEngine','PhysicalParametersMetaEngine','ConstitutiveLawDispatcher','InteractionDispatchers','ParallelEngine','Clump','STLImporter',])
 			),
 		env.SharedLibrary('_customConverters',['yadeWrapper/customConverters.cpp'],SHLIBPREFIX='',LIBS=env['LIBS']+linkPlugins(Split("BoundingVolumeEngineUnit GeometricalModelEngineUnit InteractingGeometryEngineUnit InteractionGeometryEngineUnit InteractionPhysicsEngineUnit PhysicalParametersEngineUnit PhysicalActionDamperUnit PhysicalActionApplierUnit ConstitutiveLaw")))

=== modified file 'py/WeightedAverage2d.cpp'
--- py/WeightedAverage2d.cpp	2009-10-11 11:22:25 +0000
+++ py/WeightedAverage2d.cpp	2009-11-13 11:27:22 +0000
@@ -16,7 +16,8 @@
 
 BOOST_PYTHON_MODULE(WeightedAverage2d)
 {
-	boost::python::class_<pyGaussAverage>("GaussAverage",python::init<python::tuple,python::tuple,python::tuple,Real>())
+	boost::python::scope().attr("__doc__")="Smoothing (2d gauss-weighted average) for postprocessing scalars in 2d.";
+	boost::python::class_<pyGaussAverage>("GaussAverage",python::init<python::tuple,python::tuple,python::tuple,Real,Real>(python::args("min","max","nCells","stDev","relThreshold"),"Create empty container for data, which can be added using add and later retrieved using avg."))
 		.def("add",&pyGaussAverage::addPt)
 		.def("avg",&pyGaussAverage::avg)
 		.add_property("stDev",&pyGaussAverage::stDev_get,&pyGaussAverage::stDev_set)

=== modified file 'py/_packObb.cpp'
--- py/_packObb.cpp	2009-07-15 15:10:16 +0000
+++ py/_packObb.cpp	2009-11-13 11:27:22 +0000
@@ -71,6 +71,7 @@
 }
 
 BOOST_PYTHON_MODULE(_packObb){
+	python::scope().attr("__doc__")="Computation of oriented bounding box for cloud of points.";
 	python::def("cloudBestFitOBB",bestFitOBB_py,"Return (Vector3 center, Vector3 halfSize, Quaternion orientation) of\nbest-fit oriented bounding-box for given tuple of points\n(uses brute-force velome minimization, do not use for very large clouds).");
 };
 

=== modified file 'py/_packPredicates.cpp'
--- py/_packPredicates.cpp	2009-08-20 09:38:22 +0000
+++ py/_packPredicates.cpp	2009-11-13 11:27:22 +0000
@@ -322,7 +322,7 @@
 #endif
 
 BOOST_PYTHON_MODULE(_packPredicates){
-
+	python::scope().attr("__doc__")="Spatial predicates for volumes (defined analytically or by triangulation).";
 	// base predicate class
 	python::class_<PredicateWrap,/* necessary, as methods are pure virtual*/ boost::noncopyable>("Predicate")
 		.def("__call__",python::pure_virtual(&Predicate::operator()))

=== modified file 'py/_packSpheres.cpp'
--- py/_packSpheres.cpp	2009-08-22 15:05:20 +0000
+++ py/_packSpheres.cpp	2009-11-13 11:27:22 +0000
@@ -3,6 +3,7 @@
 #include<yade/pkg-dem/SpherePack.hpp>
 
 BOOST_PYTHON_MODULE(_packSpheres){
+	python::scope().attr("__doc__")="Creation, manipulation, IO for generic sphere packings.";
 	python::class_<SpherePack>("SpherePack","Set of spheres as centers and radii",python::init<python::optional<python::list> >(python::args("list"),"Empty constructor, optionally taking list [ ((cx,cy,cz),r), … ] for initial data." ))
 		.def("add",&SpherePack::add,"Add single sphere to packing, given center as 3-tuple and radius")
 		.def("toList",&SpherePack::toList,"Return packing data as python list.")

=== modified file 'py/eudoxos.py'
--- py/eudoxos.py	2009-07-07 10:54:14 +0000
+++ py/eudoxos.py	2009-11-13 11:27:22 +0000
@@ -3,6 +3,12 @@
 #
 # I doubt there functions will be useful for anyone besides me.
 #
+"""Miscillaneous functions that are not believed to be generally usable,
+therefore kept in my "private" module here.
+
+They comprise notably oofem export and various CPM-related functions.
+"""
+
 from yade.wrapper import *
 from math import *
 from yade._eudoxos import * ## c++ implementations

=== modified file 'py/log.cpp'
--- py/log.cpp	2009-08-20 09:38:22 +0000
+++ py/log.cpp	2009-11-13 11:27:22 +0000
@@ -45,6 +45,7 @@
 #endif
 
 BOOST_PYTHON_MODULE(log){
+	python::scope().attr("__doc__") = "Acess and manipulation of log4cxx loggers.";
 	python::def("setLevel",logSetLevel,"Set minimum severity level (constants TRACE,DEBUG,INFO,WARN,ERROR,FATAL) for given logger\nleading 'yade.' will be appended automatically to the logger name; if logger is '', the root logger 'yade' will be operated on.");
 	python::scope().attr("TRACE")=(int)ll_TRACE;
 	python::scope().attr("DEBUG")=(int)ll_DEBUG;

=== modified file 'py/pack.py'
--- py/pack.py	2009-09-19 19:41:52 +0000
+++ py/pack.py	2009-11-13 11:27:22 +0000
@@ -1,5 +1,18 @@
 # encoding: utf-8
-#
+# 2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""
+Creating packings and filling volumes defined by boundary representation or constructive solid geometry.
+
+For examples, see
+	- scripts/test/gts-horse.py
+	- scripts/test/gts-operators.py
+	- scripts/test/gts-random-pack-obb.py
+	- scripts/test/gts-random-pack.py
+	- scripts/test/pack-cloud.py
+	- scripts/test/pack-predicates.py
+	- scripts/test/regular-sphere-pack.py
+"""
+
 import itertools,warnings
 from numpy import arange
 from math import sqrt

=== added file 'py/post2d.py'
--- py/post2d.py	1970-01-01 00:00:00 +0000
+++ py/post2d.py	2009-11-13 11:27:22 +0000
@@ -0,0 +1,254 @@
+# encoding: utf-8
+# 2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""
+Module for 2d postprocessing, containing classes to project points from 3d to 2d in various ways,
+providing basic but flexible framework for extracting arbitrary scalar values from bodies and plotting the
+results. There are 2 basic components: flatteners and extractors.
+
+Flatteners
+==========
+Instance of classes that convert 3d (model) coordinates to 2d (plot) coordinates. Their interface is
+defined by the Flatten class (__call__, planar, normal).
+
+Extractors
+==========
+Callable objects returning scalar or vector value, given a body object. If a 3d vector is returned,
+Flattener.planar is called, which should return only in-plane components of the vector.
+
+Example
+=======
+This example can be found in examples/concrete/uniax-post.py ::
+
+ from yade import post2d
+ import pylab # the matlab-like interface of matplotlib
+
+ O.load('/tmp/uniax-tension.xml.bz2')
+
+ # flattener that project to the xz plane
+ flattener=post2d.AxisFlatten(useRef=False,axis=1)
+ # return scalar given a Body instance
+ extractDmg=lambda b: b.phys['normDmg']
+ # will call flattener.planar implicitly
+ # the same as: extractVelocity=lambda b: flattener.planar(b,b.phys['velocity'])
+ extractVelocity=lambda b: b.phys['velocity']
+
+ # create new figure
+ pylab.figure()
+ # plot raw damage
+ post2d.plot(post2d.data(extractDmg,flattener))
+
+ # plot smooth damage into new figure
+ pylab.figure(); ax,map=post2d.plot(post2d.data(extractDmg,flattener,stDev=2e-3))
+ # show color scale
+ pylab.colorbar(map,orientation='horizontal')
+
+ # raw velocity (vector field) plot
+ pylab.figure(); post2d.plot(post2d.data(extractVelocity,flattener))
+
+ # smooth velocity plot; data are sampled at regular grid
+ pylab.figure(); ax,map=post2d.plot(post2d.data(extractVelocity,flattener,stDev=1e-3))
+ # save last (current) figure to file
+ pylab.gcf().savefig('/tmp/foo.png') 
+
+ # show the figures
+ pylab.show()
+
+"""
+
+class Flatten:
+	"""Abstract class for converting 3d point into 2d. Used by post2d.data2d."""
+	def __init__(self): pass
+	def __call__(self,b):
+		"""Given a Body instance, should return either 2d coordinates as a 2-tuple, or None if the Body should be discarded.""" 
+		pass
+	def planar(self,pos,vec):
+		"Given position and vector value, project the vector value to the flat plane and return its 2 in-plane components."
+	def normal(self,pos,vec):
+		"Given position and vector value, return lenght of the vector normal to the flat plane."
+
+class SpiralFlatten(Flatten):
+	"""Class converting 3d point to 2d based on projection from spiral.
+	The y-axis in the projection corresponds to the rotation axis"""
+	def __init__(self,useRef,thetaRange,dH_dTheta,axis=2,periodStart=0):
+		"""@param useRef: use reference positions rather than actual positions
+		@param thetaRange: (thetaMin,thetaMax) tuple; bodies outside this range will be discarded
+		@param dH_dTheta: inclination of the spiral (per radian)
+		@param axis: axis of rotation of the spiral
+		@param periodStart: height of the spiral for zero angle
+		"""
+		self.useRef,self.thetaRange,self.dH_dTheta,self.axis,self.periodStart=useRef,thetaRange,dH_dTheta,axis,periodStart
+		self.ax1,self.ax2=(axis+1)%3,(axis+2)%3
+	def _getPos(self,b):
+		return b.phys.refPos if self.useRef else b.phys.pos
+	def __call__(self,b):
+		import yade.utils
+		xy,theta=yade.utils.spiralProject(_getPos(b),self.dH_dTheta,self.axis,self.periodStart)
+		if theta<thetaRange[0] or theta>thetaRange[1]: return None
+		return xy
+	def planar(self,b,vec):
+		from math import sqrt
+		pos=_getPos(b)
+		pos[self.axis]=0; pos.Normalize()
+		return pos.Dot(vec),vec[axis]
+	def normal(self,pos,vec):
+		ax=Vector3(0,0,0); ax[axis]=1; pos=_getPos(b)
+		circum=ax.Cross(pos); circum.Normalize()
+		return circum.Dot(vec)
+		
+
+class CylinderFlatten(Flatten):
+	"""Class for converting 3d point to 2d based on projection from circle.
+	The y-axis in the projection corresponds to the rotation axis; the x-axis is distance form the axis.
+	"""
+	def __init__(self,useRef,axis=2):
+		"""@param useRef: use reference positions rather than actual positions.
+		@param axis of the cylinder (0, 1 or 2)
+		"""
+		if axis not in (0,1,2): raise IndexError("axis must be one of 0,1,2 (not %d)"%axis)
+		self.useRef,self.axis=useRef,axis
+	def _getPos(self,b):
+		return b.phys.refPos if self.useRef else b.phys.pos
+	def __call__(self,b):
+		p=_getPos(b)
+		pp=(p[(self.axis+1)%3],p[(self.axis+2)%3])
+		import math.sqrt
+		return math.sqrt(pp[0]**2+pp[2]**2),p[self.axis]
+	def planar(self,b,vec):
+		pos=_getPos(b)
+		from math import sqrt
+		pos[self.axis]=0; pos.Normalize()
+		return pos.Dot(vec),vec[axis]
+	def normal(self,b,vec):
+		pos=_getPos(b)
+		ax=Vector3(0,0,0); ax[axis]=1
+		circum=ax.Cross(pos); circum.Normalize()
+		return circum.Dot(vec)
+		
+
+class AxisFlatten(Flatten):
+	def __init__(self,useRef,axis=2):
+		"""@param useRef: use reference positions rather than actual positions.
+		@param axis: axis normal to the plane (0, 1 or 2); the return value will be simply position with this component dropped."""
+		if axis not in (0,1,2): raise IndexError("axis must be one of 0,1,2 (not %d)"%axis)
+		self.useRef,self.axis=useRef,axis
+		self.ax1,self.ax2=(self.axis+1)%3,(self.axis+2)%3
+	def __call__(self,b):
+		p=b.phys.refPos if self.useRef else b.phys.pos
+		return (p[self.ax1],p[self.ax2])
+	def planar(self,pos,vec):
+		return vec[self.ax1],vec[self.ax2]
+	def normal(self,pos,vec):
+		return vec[self.axis]
+
+def data(extractor,flattener,onlyDynamic=True,stDev=None,relThreshold=3.,div=(50,50),margin=(0,0)):
+	"""Filter all bodies (spheres only), project them to 2d and extract required scalar value;
+	return either discrete array of positions and values, or smoothed data, depending on whether the stDev
+	value is specified.
+
+	@param extractor: callable object that receives Body instance; it should return scalar, a 2-tuple (vector fields) or None (to skip that body)
+	@param flattener: callable object that receives Body instance and returns its 2d coordinates or None (to skip that body)
+	@param onlyDynamic: skip all non-dynamic bodies
+	@param stDev: standard deviation for averaging, enables smoothing; None (default) means raw mode.
+	@param relThreshold: threshold for the gaussian weight function relative to stDev (smooth mode only)
+	@param div: 2-tuple specifying number of cells for the gaussian grid (smooth mode only)
+	@param margin: 2-tuple specifying margin around bounding box for data (smooth mode only)
+	@return: Dictionary always containing keys 'type' (one of 'rawScalar','rawVector','smoothScalar','smoothVector', depending on value of smooth and on return value from extractor), 'x', 'y', 'bbox'.
+	
+		Raw data further contains 'radii'.
+
+		Scalar fields contain 'val' (value from extractor), vector fields have 'valX' and 'valY' (2 components returned by the extractor).
+	"""
+	from miniWm3Wrap import Vector3
+	xx,yy,dd1,dd2,rr=[],[],[],[],[]
+	nDim=0
+	for b in O.bodies:
+		if onlyDynamic and not b.dynamic: continue
+		if b.shape.name!='Sphere': continue
+		xy,d=flattener(b),extractor(b)
+		if xy==None or d==None: continue
+		if nDim==0: nDim=1 if isinstance(d,float) else 2
+		if nDim==1: dd1.append(d);
+		elif len(d)==2:
+			dd1.append(d[0]); dd2.append(d[1])
+		elif len(d)==3:
+			d1,d2=flattener.planar(b,Vector3(d))
+			dd1.append(d1); dd2.append(d2)
+		else:
+			raise RuntimeError("Extractor must return float or 2 or 3 (not %d) floats"%nDim)
+		xx.append(xy[0]); yy.append(xy[1]); rr.append(b.shape['radius'])
+	if stDev==None:
+		bbox=(min(xx),min(yy)),(max(xx),max(yy))
+		if nDim==1: return {'type':'rawScalar','x':xx,'y':yy,'val':dd1,'radii':rr,'bbox':bbox}
+		else: return {'type':'rawVector','x':xx,'y':yy,'valX':dd1,'valY':dd2,'radii':rr,'bbox':bbox}
+	
+	from yade.WeightedAverage2d import GaussAverage
+	import numpy
+	lo,hi=(min(xx),min(yy)),(max(xx),max(yy))
+	llo=lo[0]-margin[0],lo[1]-margin[1]; hhi=hi[0]+margin[0],hi[1]+margin[1]
+	ga=GaussAverage(llo,hhi,div,stDev,relThreshold)
+	ga2=GaussAverage(llo,hhi,div,stDev,relThreshold)
+	for i in range(0,len(xx)):
+		ga.add(dd1[i],(xx[i],yy[i]))
+		if nDim>1: ga2.add(dd2[i],(xx[i],yy[i]))
+	step=[(hhi[i]-llo[i])/float(div[i]) for i in [0,1]]
+	xxx,yyy=[numpy.arange(llo[i]+.5*step[i],hhi[i],step[i]) for i in [0,1]]
+	ddd=numpy.zeros((len(yyy),len(xxx)),float)
+	ddd2=numpy.zeros((len(yyy),len(xxx)),float)
+	for cx in range(0,div[0]):
+		for cy in range(0,div[1]):
+			ddd[cy,cx]=float(ga.avg((xxx[cx],yyy[cy])))
+			if nDim>1: ddd2[cy,cx]=float(ga2.avg((xxx[cx],yyy[cy])))
+	if nDim==1: return {'type':'smoothScalar','x':xxx,'y':yyy,'val':ddd,'bbox':(llo,hhi)} 
+	else: return {'type':'smoothVector','x':xxx,'y':yyy,'valX':ddd,'valY':ddd2,'bbox':(llo,hhi)}
+	
+def plot(data,axes=None,alpha=.5,clabel=True,**kw):
+	"""Given output from post2d.data, plot the scalar as discrete or smooth plot.
+
+	For raw discrete data, plot filled circles with radii of particles, colored by the scalar value.
+
+	For smooth discrete data, plot image with optional contours and contour labels.
+
+	For vector data (raw or smooth), plot quiver (vector field), with arrows colored by the magnitude.
+
+	@param axes: matplotlib.axes instance to plot to; if None, will be created from scratch
+	@param data: return value from post2d.data
+	@param clabel: show contour labels (smooth mode only)
+	@return: tuple of (axes,mappable); mappable can be used in further calls to pylab.colorbar
+	"""
+	import pylab,math
+	if not axes: axes=pylab.gca()
+	if data['type']=='rawScalar':
+		from matplotlib.patches import Circle
+		import matplotlib.collections,numpy
+		patches=[]
+		for x,y,d,r in zip(data['x'],data['y'],data['val'],data['radii']):
+			patches.append(Circle(xy=(x,y),radius=r))
+		coll=matplotlib.collections.PatchCollection(patches,linewidths=0.,**kw)
+		coll.set_array(numpy.array(data['val']))
+		bb=coll.get_datalim(coll.get_transform())
+		axes.add_collection(coll)
+		axes.set_xlim(bb.xmin,bb.xmax); axes.set_ylim(bb.ymin,bb.ymax)
+		axes.grid(True); axes.set_aspect('equal')
+		return axes,coll
+	elif data['type']=='smoothScalar':
+		loHi=data['bbox']
+		img=axes.imshow(data['val'],extent=(loHi[0][0],loHi[1][0],loHi[0][1],loHi[1][1]),origin='lower',aspect='equal',**kw)
+		ct=axes.contour(data['x'],data['y'],data['val'],colors='k',origin='lower',extend='both')
+		axes.update_datalim(loHi)
+		if clabel: axes.clabel(ct,inline=1,fontsize=10)
+		axes.set_xlim(loHi[0][0],loHi[1][0]); axes.set_ylim(loHi[0][1],loHi[1][1])
+		axes.grid(True); axes.set_aspect('equal')
+		return axes,img
+	elif data['type'] in ('rawVector','smoothVector'):
+		import numpy
+		loHi=data['bbox']
+		valX,valY=numpy.array(data['valX']),numpy.array(data['valY']) # rawVector data are plain python lists
+		scalars=numpy.sqrt(valX**2+valY**2)
+		# numpy.sqrt computes element-wise sqrt
+		quiv=axes.quiver(data['x'],data['y'],data['valX'],data['valY'],scalars,**kw)
+		#axes.update_datalim(loHi)
+		axes.set_xlim(loHi[0][0],loHi[1][0]); axes.set_ylim(loHi[0][1],loHi[1][1])
+		axes.grid(True); axes.set_aspect('equal')
+		return axes,quiv
+
+

=== modified file 'py/tests/__init__.py'
--- py/tests/__init__.py	2009-08-05 07:32:18 +0000
+++ py/tests/__init__.py	2009-11-13 11:27:22 +0000
@@ -1,3 +1,5 @@
+# 2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""All defined functionality tests for yade."""
 import unittest 
 
 # add any new test suites to the list here, so that they are picked up by testAll

=== modified file 'py/tests/wrapper.py'
--- py/tests/wrapper.py	2009-09-19 19:41:52 +0000
+++ py/tests/wrapper.py	2009-11-13 11:27:22 +0000
@@ -1,13 +1,16 @@
+# 2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+
+"""
+This test module covers python/c++ transitions, for both classes deriving from Serializable,
+but also for other classes that we wrap (like Wm3).
+"""
+
 import unittest
 from yade.wrapper import *
 from miniWm3Wrap import *
 from yade._customConverters import *
 from math import *
 
-"""
-This test module covers python/c++ transitions, for both classes deriving from Serializable,
-but also for other classes that we wrap (like Wm3).
-"""
 
 # copied from PythonUI_rc, should be in some common place (utils? runtime?)
 def listChildClassesRecursive(base):

=== modified file 'py/timing.py'
--- py/timing.py	2009-07-07 10:54:14 +0000
+++ py/timing.py	2009-11-13 11:27:22 +0000
@@ -1,4 +1,10 @@
 # encoding: utf-8
+# 2008 © Václav Šmilauer <eudoxos@xxxxxxxx>
+"""Functions for accessing timing information stored in engines and functors.
+See https://yade.hmg.inpg.fr/index.php/Speed_profiling_using_TimingInfo_and_TimingDeltas_classes
+for more details on usage."""
+
+
 def _resetEngine(e):
 	if e.timingDeltas: e.timingDeltas.reset()
 	if e.__class__.__name__=='EngineUnit': return
@@ -9,6 +15,7 @@
 	e.execTime,e.execCount=0,0
 
 def reset():
+	"Zero all timing data."
 	for e in O.engines: _resetEngine(e)
 
 _statCols={'label':40,'count':20,'time':20,'relTime':20}
@@ -58,6 +65,28 @@
 	return lines
 
 def stats():
+	"""Print summary table of timing information from engines and functors. Absolute times as well as percentages are given. Sample output::
+	
+		Name                                                    Count                 Time            Rel. time
+		-------------------------------------------------------------------------------------------------------
+		PhysicalActionContainerReseter                      400               9449μs                0.01%      
+		BoundingVolumeMetaEngine                            400            1171770μs                1.15%      
+		PersistentSAPCollider                               400            9433093μs                9.24%      
+		InteractionGeometryMetaEngine                       400           15177607μs               14.87%      
+		InteractionPhysicsMetaEngine                        400            9518738μs                9.33%      
+		ConstitutiveLawDispatcher                           400           64810867μs               63.49%      
+		  ef2_Spheres_Brefcom_BrefcomLaw                                                                       
+			 setup                                           4926145            7649131μs               15.25%  
+			 geom                                            4926145           23216292μs               46.28%  
+			 material                                        4926145            8595686μs               17.14%  
+			 rest                                            4926145           10700007μs               21.33%  
+			 TOTAL                                                             50161117μs              100.00%  
+		"damper"                                            400            1866816μs                1.83%      
+		"strainer"                                          400              21589μs                0.02%      
+		"plotDataCollector"                                 160              64284μs                0.06%      
+		"damageChecker"                                       9               3272μs                0.00%      
+		TOTAL                                                            102077490μs              100.00%      
+	"""
 	print 'Name'.ljust(_statCols['label'])+' '+'Count'.rjust(_statCols['count'])+' '+'Time'.rjust(_statCols['time'])+' '+'Rel. time'.rjust(_statCols['relTime'])
 	print '-'*(sum([_statCols[k] for k in _statCols])+len(_statCols)-1)
 	_engines_stats(O.engines,sum([e.execTime for e in O.engines]),0)

=== modified file 'py/utils.py'
--- py/utils.py	2009-11-12 20:40:12 +0000
+++ py/utils.py	2009-11-13 11:27:22 +0000
@@ -2,7 +2,12 @@
 #
 # utility functions for yade
 #
-# 2008 © Václav Šmilauer <eudoxos@xxxxxxxx>
+# 2008-2009 © Václav Šmilauer <eudoxos@xxxxxxxx>
+
+"""Heap of functions that don't (yet) fit anywhere else.
+
+Devs: please DO NOT ADD more functions here, it is getting too crowded!
+"""
 
 import math,random
 from yade.wrapper import *
@@ -533,8 +538,7 @@
 	return {'negIds':negIds,'posIds':posIds,'axis':axis,'area':min(areas)}
 
 def NormalRestitution2DampingRate(en):
-        """Compute the normal damping rate as a function of the normal coefficient of restitution.
-        """
+	"""Compute the normal damping rate as a function of the normal coefficient of restitution. """
 	if en == 0.0: return 0.999999999
 	if en == 1.0: return 0.0
 	from math import sqrt,log,pi

=== modified file 'py/yadeWrapper/yadeWrapper.cpp'
--- py/yadeWrapper/yadeWrapper.cpp	2009-10-21 15:22:14 +0000
+++ py/yadeWrapper/yadeWrapper.cpp	2009-11-13 11:27:22 +0000
@@ -634,6 +634,7 @@
 
 BOOST_PYTHON_MODULE(wrapper)
 {
+	python::scope().attr("__doc__")="Wrapper for c++ internals of yade.";
 	python::class_<pyOmega>("Omega")
 		.add_property("iter",&pyOmega::iter,"Get current step number")
 		.add_property("stopAtIter",&pyOmega::stopAtIter_get,&pyOmega::stopAtIter_set,"Get/set number of iteration after which the simulation will stop.")