← Back to team overview

yade-dev team mailing list archive

[Branch ~yade-dev/yade/trunk] Rev 2438: 1. Make batch commands clickable; fix encoding issues when the batch page sends job script/table

 

------------------------------------------------------------
revno: 2438
committer: Chiara Modenese <chia@engs-018373>
branch nick: trunk
timestamp: Wed 2010-09-15 11:06:42 +0100
message:
  1. Make batch commands clickable; fix encoding issues when the batch page sends job script/table 
  2. add utils.maxOverlapRatio for sphere-sphere contacts
  3. add reinitialization counter to InsertionSortCollider
  4. Add VTKRecorder.ascii so that saved XML data might be human-readable (false by default)
modified:
  core/main/yade-batch.in
  gui/qt4/Inspector.py
  gui/qt4/SerializableEditor.py
  pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp
  pkg/common/Engine/GlobalEngine/InsertionSortCollider.hpp
  pkg/dem/Engine/GlobalEngine/VTKRecorder.cpp
  pkg/dem/Engine/GlobalEngine/VTKRecorder.hpp
  py/_utils.cpp
  py/plot.py
  py/utils.py


--
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
=== modified file 'core/main/yade-batch.in'
--- core/main/yade-batch.in	2010-09-13 11:32:30 +0000
+++ core/main/yade-batch.in	2010-09-15 10:06:42 +0000
@@ -17,12 +17,10 @@
 
 
 class JobInfo():
-	def __init__(self,num,id,command,log,nSlots):
+	def __init__(self,num,id,command,hrefCommand,log,nSlots,script,table,lineNo):
 		self.started,self.finished,self.duration,self.exitStatus=None,None,None,None
-		self.command=command; self.num=num; self.log=log; self.id=id; self.nSlots=nSlots; self.infoSocket=None
-		match=re.match('(.* > )(.*)( 2>&1)',self.command)
-		self.commandHref=(match.group(1)+'<a href="jobs/%d/log">'%self.num+match.group(2)+'</a>'+match.group(3)) if match else self.command
-		#print self.commandHref
+		self.command=command; self.hrefCommand=hrefCommand; self.num=num; self.log=log; self.id=id; self.nSlots=nSlots; self.infoSocket=None
+		self.script=script; self.table=table; self.lineNo=lineNo
 		self.hasXmlrpc=False
 		self.status='PENDING'
 		self.threadNum=None
@@ -96,8 +94,7 @@
 			img='<img src="/jobs/%d/plots" width="300px" alt="[plots]">'%(self.num)
 			ret+='<td><a href="/jobs/%d/plots">%s</a></td>'%(self.num,img)
 		else: ret+='<td> (no plots) </td>'
-		if self.status=='PENDING': ret+='<td>%s</td>'%self.command
-		else: ret+='<td>%s</td>'%self.commandHref
+		ret+='<td>%s</td>'%self.hrefCommand
 		ret+='</tr>'
 		return ret
 def t2hhmmss(dt): return '%02d:%02d:%02d'%(dt//3600,(dt%3600)//60,(dt%60))
@@ -126,29 +123,34 @@
 	def do_GET(self):
 		if not self.path or self.path=='/': self.sendGlobal()
 		else:
-			#print 'Document',self.path,'requested'
-			plotMatch=re.match('/jobs/([0-9]+)/plots',self.path)
-			if plotMatch:
-				jobId=int(plotMatch.group(1));
-				if jobId>=len(jobs) or not os.path.exists(jobs[jobId].plotsFile): self.send_error(404)
-				jobs[jobId].updatePlots() # internally checks for last update time
-				self.sendFile(jobs[jobId].plotsFile,contentType=yade.remote.plotImgMimetype,refresh=5)
-				return
-			logMatch=re.match('/jobs/([0-9]+)/log',self.path)
-			if logMatch:
-				jobId=int(logMatch.group(1));
-				if jobId>=len(jobs): self.send_error(404)
+			jobMatch=re.match('/jobs/([0-9]+)/(.*)',self.path)
+			if not jobMatch:
+				self.send_error(404); return
+			jobId=int(jobMatch.group(1))
+			if jobId>=len(jobs):
+				self.send_error(404); return
+			job=jobs[jobId]
+			rest=jobMatch.group(2)
+			if rest=='plots':
+				job.updatePlots() # internally checks for last update time
+				self.sendFile(job.plotsFile,contentType=yade.remote.plotImgMimetype,refresh=(0 if job.status=='DONE' else 5))
+			elif rest=='log':
+				if not os.path.exists(job.log):
+					self.send_error(404); return
 				## once we authenticate properly, send the whole file
-				## self.sendFile(jobs[jobId].log,contentType='text/plain',refresh=5)
+				## self.sendTextFile(jobs[jobId].log,refresh=5)
 				## now we have to filter away the cookie
 				cookieRemoved=False; data=''
-				for l in open(jobs[jobId].log):
+				for l in open(job.log):
 					if not cookieRemoved and l.startswith('TCP python prompt on'):
 						ii=l.find('auth cookie `'); l=l[:ii+13]+'******'+l[ii+19:]; cookieRemoved=True
 					data+=l
-				self.sendHttp(data,contentType='text/plain',refresh=5)
-				return
-			self.send_error(404)
+				self.sendHttp(data,contentType='text/plain;charset=utf-8;',refresh=(0 if job.status=='DONE' else 5))
+			elif rest=='script':
+				self.sendPygmentizedFile(job.script,linenostep=5)
+			elif rest=='table':
+				self.sendPygmentizedFile(job.table,hl_lines=[job.lineNo],linenostep=1)
+			else: self.send_error(404)
 		return
 	def log_request(self,req): pass
 	def sendGlobal(self):
@@ -158,7 +160,13 @@
 		for j in jobs: html+=j.htmlStats()+'\n'
 		html+='</TABLE></BODY></HTML>'
 		self.sendHttp(html,contentType='text/html',refresh=5) # refresh sent as header
+	def sendTextFile(self,fileName,**headers):
+		if not os.path.exists(fileName): self.send_error(404); return
+		import codecs
+		f=codecs.open(f,encoding='utf-8')
+		self.sendHttp(f.read(),contentType='text/plain;charset=utf-8;',**headers)
 	def sendFile(self,fileName,contentType,**headers):
+		if not os.path.exists(fileName): self.send_error(404); return
 		f=open(fileName)
 		self.sendHttp(f.read(),contentType=contentType,**headers)
 	def sendHttp(self,data,contentType,**headers):
@@ -169,6 +177,18 @@
 		self.wfile.write(data)
 		global httpLastServe
 		httpLastServe=time.time()
+	def sendPygmentizedFile(self,f,**kw):
+		if not os.path.exists(f):
+			self.send_error(404); return
+		try:
+			import codecs
+			from pygments import highlight
+			from pygments.lexers import PythonLexer
+			from pygments.formatters import HtmlFormatter
+			data=highlight(codecs.open(f,encoding='utf-8').read(),PythonLexer(),HtmlFormatter(linenos=True,full=True,encoding='utf-8',title=os.path.abspath(f),**kw))
+			self.sendHttp(data,contentType='text/html;charset=utf-8;')
+		except ImportError:
+			self.sendTextFile(f)
 def runHttpStatsServer():
 	maxPort=11000; port=9080
 	while port<maxPort:
@@ -309,7 +329,13 @@
 			nSlots=maxJobs
 		else:
 			logging.warning('WARNING: job #%d will use %d slots but only %d are available'%(i,nSlots,maxJobs))
-	jobs.append(JobInfo(i,params[l]['description'],'PARAM_TABLE=%s:%d DISPLAY= %s %s --threads=%d %s -x %s > %s 2>&1'%(table,l,' '.join(envVars),executable,int(nSlots),'--nice=%s'%nice if nice!=None else '',simul,pipes.quote(logFile)),logFile,nSlots))
+	# compose command-line: build the hyper-linked variant, then strip HTML tags (ugly, but ensures consistency)
+	env='PARAM_TABLE=<a href="/jobs/%d/table">%s:%d</a> DISPLAY= %s '%(i,table,l,' '.join(envVars))
+	cmd='%s --threads=%d %s -x <a href="/jobs/%d/script">%s</a>'%(executable,int(nSlots),'--nice=%s'%nice if nice!=None else '',i,simul)
+	log='> <a href="jobs/%d/log">%s</a> 2>&1'%(i,pipes.quote(logFile))
+	hrefCmd=env+cmd+log
+	fullCmd=re.sub('(<a href="[^">]+">|</a>)','',hrefCmd)
+	jobs.append(JobInfo(i,params[l]['description'],fullCmd,hrefCmd,logFile,nSlots,script=simul,table=table,lineNo=l))
 
 print "Master process pid",os.getpid()
 

=== modified file 'gui/qt4/Inspector.py'
--- gui/qt4/Inspector.py	2010-08-27 08:28:50 +0000
+++ gui/qt4/Inspector.py	2010-09-15 10:06:42 +0000
@@ -11,7 +11,7 @@
 	def __init__(self,parent=None):
 		QWidget.__init__(self,parent)
 		grid=QGridLayout(self); grid.setSpacing(0); grid.setMargin(0)
-		self.serEd=SeqSerializable(parent=None,getter=lambda:O.engines,setter=lambda x:setattr(O,'engines',x),serType=Engine)
+		self.serEd=SeqSerializable(parent=None,getter=lambda:O.engines,setter=lambda x:setattr(O,'engines',x),serType=Engine,path='O.engines')
 		grid.addWidget(self.serEd)
 		self.setLayout(grid)
 #class MaterialsInspector(QWidget):
@@ -41,7 +41,7 @@
 		editor=self.scroll.widget()
 		if not O.periodic and editor: self.scroll.takeWidget()
 		if (O.periodic and not editor) or (editor and editor.ser!=O.cell):
-			self.scroll.setWidget(SerializableEditor(O.cell,parent=self,showType=True))
+			self.scroll.setWidget(SerializableEditor(O.cell,parent=self,showType=True,path='O.cell'))
 	def update(self):
 		self.scroll.takeWidget() # do this before changing periodicity, otherwise the SerializableEditor will raise exception about None object
 		O.periodic=self.periCheckBox.isChecked()
@@ -134,7 +134,7 @@
 	def tryShowBody(self):
 		try:
 			b=O.bodies[self.bodyId]
-			self.serEd=SerializableEditor(b,showType=True,parent=self)
+			self.serEd=SerializableEditor(b,showType=True,parent=self,path='O.bodies[%d]'%self.bodyId)
 		except IndexError:
 			self.serEd=QFrame(self)
 			self.bodyId=-1
@@ -225,7 +225,7 @@
 	def setupInteraction(self):
 		try:
 			intr=O.interactions[self.ids[0],self.ids[1]]
-			self.serEd=SerializableEditor(intr,showType=True,parent=self.scroll)
+			self.serEd=SerializableEditor(intr,showType=True,parent=self.scroll,path='O.interactions[%d,%d]'%(self.ids[0],self.ids[1]))
 			self.scroll.setWidget(self.serEd)
 			self.gotoId1Button.setText('#'+makeBodyLabel(O.bodies[self.ids[0]]))
 			self.gotoId2Button.setText('#'+makeBodyLabel(O.bodies[self.ids[1]]))

=== modified file 'gui/qt4/SerializableEditor.py'
--- gui/qt4/SerializableEditor.py	2010-08-24 12:54:14 +0000
+++ gui/qt4/SerializableEditor.py	2010-09-15 10:06:42 +0000
@@ -256,13 +256,14 @@
 	import logging
 	# each attribute has one entry associated with itself
 	class EntryData:
-		def __init__(self,name,T):
-			self.name,self.T=name,T
+		def __init__(self,name,T,flags=0):
+			self.name,self.T,self.flags=name,T,flags
 			self.lineNo,self.widget=None,None
-	def __init__(self,ser,parent=None,ignoredAttrs=set(),showType=False):
+	def __init__(self,ser,parent=None,ignoredAttrs=set(),showType=False,path=None):
 		"Construct window, *ser* is the object we want to show."
 		QtGui.QFrame.__init__(self,parent)
 		self.ser=ser
+		self.path=(ser.label if hasattr(ser,'label') else path)
 		self.showType=showType
 		self.hot=False
 		self.entries=[]
@@ -326,13 +327,15 @@
 			# hack for Se3, which is returned as (Vector3,Quaternion) in python
 			elif isinstance(val,tuple) and len(val)==2 and val[0].__class__==Vector3 and val[1].__class__==Quaternion: t=Se3FakeType
 			else: t=val.__class__
+			match=re.search(':yattrflags:`\s*([0-9]+)\s*`',doc) # non-empty attribute
+			flags=int(match.group(1)) if match else 0
 			#logging.debug('Attr %s is of type %s'%(attr,((t[0].__name__,) if isinstance(t,tuple) else t.__name__)))
 			self.entries.append(self.EntryData(name=attr,T=t))
 	def getDocstring(self,attr=None):
 		"If attr is *None*, return docstring of the Serializable itself"
 		doc=(getattr(self.ser.__class__,attr).__doc__ if attr else self.ser.__class__.__doc__)
-		if not doc: return None
-		doc=re.sub(':y(attrtype|default):`[^`]*`','',doc)
+		if not doc: return ''
+		doc=re.sub(':y(attrtype|default|attrflags):`[^`]*`','',doc)
 		doc=re.sub(':yref:`([^`]*)`','\\1',doc)
 		import textwrap
 		wrapper=textwrap.TextWrapper(replace_whitespace=False)
@@ -345,6 +348,7 @@
 		if Klass:
 			widget=Klass(self,getter=getter,setter=setter)
 			widget.setFocusPolicy(Qt.StrongFocus)
+			if (entry.flags & AttrFlags.pyReadonly): widget.setEnabled(False)
 			return widget
 		# sequences
 		if entry.T.__class__==tuple:
@@ -352,7 +356,7 @@
 			# sequence of serializables
 			T=entry.T[0]
 			if (issubclass(T,Serializable) or T==Serializable):
-				widget=SeqSerializable(self,getter,setter,T)
+				widget=SeqSerializable(self,getter,setter,T,path=(self.path+'.'+entry.name if self.path else None))
 				return widget
 			if (T in _fundamentalEditorMap):
 				widget=SeqFundamentalEditor(self,getter,setter,T)
@@ -360,7 +364,11 @@
 			return None
 		# a serializable
 		if issubclass(entry.T,Serializable) or entry.T==Serializable:
-			widget=SerializableEditor(getattr(self.ser,entry.name),parent=self,showType=self.showType)
+			obj=getattr(self.ser,entry.name)
+			if hasattr(obj,'label') and obj.label: path=obj.label
+			elif self.path: path=self.path+'.'+entry.name
+			else: path=None
+			widget=SerializableEditor(getattr(self.ser,entry.name),parent=self,showType=self.showType,path=(self.path+'.'+entry.name if self.path else None))
 			widget.setFrameShape(QFrame.Box); widget.setFrameShadow(QFrame.Raised); widget.setLineWidth(1)
 			return widget
 		return None
@@ -373,11 +381,11 @@
 		if self.showType:
 			lab=QLabel(makeSerializableLabel(self.ser,addr=True,href=True))
 			lab.setFrameShape(QFrame.Box); lab.setFrameShadow(QFrame.Sunken); lab.setLineWidth(2); lab.setAlignment(Qt.AlignHCenter); lab.linkActivated.connect(yade.qt.openUrl)
-			lab.setToolTip(self.getDocstring())
+			lab.setToolTip(('<b>'+self.path+'</b><br>' if self.path else '')+self.getDocstring())
 			grid.setWidget(0,QFormLayout.SpanningRole,lab)
 		for entry in self.entries:
 			entry.widget=self.mkWidget(entry)
-			label=QLabel(self); label.setText(serializableHref(self.ser,entry.name)); label.setToolTip(self.getDocstring(entry.name)); label.linkActivated.connect(yade.qt.openUrl)
+			label=QLabel(self); label.setText(serializableHref(self.ser,entry.name)); label.setToolTip(('<b>'+self.path+'.'+entry.name+'</b><br>' if self.path else '')+self.getDocstring(entry.name)); label.linkActivated.connect(yade.qt.openUrl)
 			grid.addRow(label,entry.widget if entry.widget else QLabel('<i>unhandled type</i>'))
 		self.setLayout(grid)
 		self.refreshEvent()
@@ -403,9 +411,9 @@
 	return ret
 
 class SeqSerializableComboBox(QFrame):
-	def __init__(self,parent,getter,setter,serType):
+	def __init__(self,parent,getter,setter,serType,path=None):
 		QFrame.__init__(self,parent)
-		self.getter,self.setter,self.serType=getter,setter,serType
+		self.getter,self.setter,self.serType,self.path=getter,setter,serType,path
 		self.layout=QVBoxLayout(self)
 		topLineFrame=QFrame(self)
 		topLineLayout=QHBoxLayout(topLineFrame);
@@ -440,7 +448,7 @@
 		self.combo.setEnabled(ix>=0)
 		if ix>=0:
 			ser=currSeq[ix]
-			self.seqEdit=SerializableEditor(ser,parent=self,showType=seqSerializableShowType)
+			self.seqEdit=SerializableEditor(ser,parent=self,showType=seqSerializableShowType,path=(self.path+'['+str(ix)+']' if self.path else None))
 			self.scroll.setWidget(self.seqEdit)
 		else:
 			self.scroll.setWidget(QFrame())

=== modified file 'pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp'
--- pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp	2010-08-16 21:31:08 +0000
+++ pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp	2010-09-15 10:06:42 +0000
@@ -249,6 +249,7 @@
 				// the initial sort is in independent in 3 dimensions, may be run in parallel; it seems that there is no time gain running in parallel, though
 				// important to reset loInx for periodic simulation (!!)
 				for(int i=0; i<3; i++) { BB[i].loIdx=0; std::sort(BB[i].vec.begin(),BB[i].vec.end()); }
+				numReinit++;
 			} else { // sortThenCollide
 				if(!periodic) for(int i=0; i<3; i++) insertionSort(BB[i],interactions,scene,false);
 				else for(int i=0; i<3; i++) insertionSortPeri(BB[i],interactions,scene,false);

=== modified file 'pkg/common/Engine/GlobalEngine/InsertionSortCollider.hpp'
--- pkg/common/Engine/GlobalEngine/InsertionSortCollider.hpp	2010-08-24 12:54:14 +0000
+++ pkg/common/Engine/GlobalEngine/InsertionSortCollider.hpp	2010-09-15 10:06:42 +0000
@@ -206,7 +206,9 @@
 		((Real,binCoeff,5,,"Coefficient of bins for velocities, i.e. if ``binCoeff==5``, successive bins have 5 × smaller velocity peak than the previous one. (Passed to VelocityBins)"))
 		((Real,binOverlap,0.8,,"Relative bins hysteresis, to avoid moving body back and forth if its velocity is around the border value. (Passed to VelocityBins)"))
 		((Real,maxRefRelStep,.3,,"(Passed to VelocityBins)"))
-		((int,histInterval,100,,"How often to show velocity bins graphically, if debug logging is enabled for VelocityBins.")),
+		((int,histInterval,100,,"How often to show velocity bins graphically, if debug logging is enabled for VelocityBins."))
+		((int,numReinit,0,Attr::pyReadonly,"Cummulative number of bound array re-initialization."))
+		,
 		/* ctor */
 			#ifdef ISC_TIMING
 				timingDeltas=shared_ptr<TimingDeltas>(new TimingDeltas);

=== modified file 'pkg/dem/Engine/GlobalEngine/VTKRecorder.cpp'
--- pkg/dem/Engine/GlobalEngine/VTKRecorder.cpp	2010-08-19 09:12:52 +0000
+++ pkg/dem/Engine/GlobalEngine/VTKRecorder.cpp	2010-09-15 10:06:42 +0000
@@ -346,6 +346,7 @@
 			{
 			vtkSmartPointer<vtkXMLUnstructuredGridWriter> writer = vtkSmartPointer<vtkXMLUnstructuredGridWriter>::New();
 			if(compress) writer->SetCompressor(compressor);
+			if(ascii) writer->SetDataModeToAscii();
 			string fn=fileName+"spheres."+lexical_cast<string>(scene->iter)+".vtu";
 			writer->SetFileName(fn.c_str());
 			writer->SetInput(spheresUg);
@@ -369,6 +370,7 @@
 			{
 			vtkSmartPointer<vtkXMLUnstructuredGridWriter> writer = vtkSmartPointer<vtkXMLUnstructuredGridWriter>::New();
 			if(compress) writer->SetCompressor(compressor);
+			if(ascii) writer->SetDataModeToAscii();
 			string fn=fileName+"facets."+lexical_cast<string>(scene->iter)+".vtu";
 			writer->SetFileName(fn.c_str());
 			writer->SetInput(facetsUg);
@@ -387,6 +389,7 @@
 			{
 			vtkSmartPointer<vtkXMLPolyDataWriter> writer = vtkSmartPointer<vtkXMLPolyDataWriter>::New();
 			if(compress) writer->SetCompressor(compressor);
+			if(ascii) writer->SetDataModeToAscii();
 			string fn=fileName+"intrs."+lexical_cast<string>(scene->iter)+".vtp";
 			writer->SetFileName(fn.c_str());
 			writer->SetInput(intrPd);
@@ -401,6 +404,7 @@
 			if(recActive[REC_FACETS]) multiblockDataset->SetBlock(i++,facetsUg);
 			if(recActive[REC_INTR]) multiblockDataset->SetBlock(i++,intrPd);
 			vtkSmartPointer<vtkXMLMultiBlockDataWriter> writer = vtkSmartPointer<vtkXMLMultiBlockDataWriter>::New();
+			if(ascii) writer->SetDataModeToAscii();
 			string fn=fileName+lexical_cast<string>(scene->iter)+".vtm";
 			writer->SetFileName(fn.c_str());
 			writer->SetInput(multiblockDataset);

=== modified file 'pkg/dem/Engine/GlobalEngine/VTKRecorder.hpp'
--- pkg/dem/Engine/GlobalEngine/VTKRecorder.hpp	2010-08-24 12:54:14 +0000
+++ pkg/dem/Engine/GlobalEngine/VTKRecorder.hpp	2010-09-15 10:06:42 +0000
@@ -13,6 +13,7 @@
 		virtual void action();
 	YADE_CLASS_BASE_DOC_ATTRS_CTOR(VTKRecorder,PeriodicEngine,"Engine recording snapshots of simulation into series of *.vtu files, readable by VTK-based postprocessing programs such as Paraview. Both bodies (spheres and facets) and interactions can be recorded, with various vector/scalar quantities that are defined on them.\n\n:yref:`PeriodicEngine.initRun` is initialized to ``True`` automatically.",
 		((bool,compress,false,,"Compress output XML files [experimental]."))
+		((bool,ascii,false,,"Store data as readable text in the XML file (sets `vtkXMLWriter <http://www.vtk.org/doc/nightly/html/classvtkXMLWriter.html>`__ data mode to ``vtkXMLWriter::Ascii``, while the default is ``Appended``"))
 		((bool,skipFacetIntr,true,,"Skip interactions with facets, when saving interactions"))
 		((bool,skipNondynamic,false,,"Skip non-dynamic spheres (but not facets)."))
 		#ifdef YADE_VTK_MULTIBLOCK

=== modified file 'py/_utils.cpp'
--- py/_utils.cpp	2010-09-12 08:44:00 +0000
+++ py/_utils.cpp	2010-09-15 10:06:42 +0000
@@ -405,6 +405,21 @@
 	return py::make_tuple(E,maxId);
 }
 
+Real maxOverlapRatio(){
+	Scene* scene=Omega::instance().getScene().get();
+	Real ret=-1;
+	FOREACH(const shared_ptr<Interaction> I, *scene->interactions){
+		if(!I->isReal()) continue;
+		Sphere *s1(dynamic_cast<Sphere*>(Body::byId(I->getId1(),scene)->shape.get())), *s2(dynamic_cast<Sphere*>(Body::byId(I->getId2(),scene)->shape.get()));
+		if((!s1) || (!s2)) continue;
+		ScGeom* geom=dynamic_cast<ScGeom*>(I->interactionGeometry.get());
+		if(!geom) continue;
+		Real rEq=2*s1->radius*s2->radius/(s1->radius+s2->radius);
+		ret=max(ret,geom->penetrationDepth/rEq);
+	}
+	return ret;
+}
+
 Real Shop__getPorosity(Real volume=-1){ return Shop::getPorosity(Omega::instance().getScene(),volume); }
 
 Matrix3r Shop__stressTensorOfPeriodicCell(bool smallStrains=false){return Shop::stressTensorOfPeriodicCell(smallStrains);}
@@ -451,4 +466,5 @@
 	py::def("flipCell",&Shop::flipCell,(py::arg("flip")=Matrix3r(Matrix3r::Zero())),"Flip periodic cell so that angles between $R^3$ axes and transformed axes are as small as possible. This function relies on the fact that periodic cell defines by repetition or its corners regular grid of points in $R^3$; however, all cells generating identical grid are equivalent and can be flipped one over another. This necessiatates adjustment of :yref:`Interaction.cellDist` for interactions that cross boundary and didn't before (or vice versa), and re-initialization of collider. The *flip* argument can be used to specify desired flip: integers, each column for one axis; if zero matrix, best fit (minimizing the angles) is computed automatically.\n\nIn c++, this function is accessible as ``Shop::flipCell``.\n\n.. warning::\n\t This function is currently broken and should not be used.");
 	py::def("getViscoelasticFromSpheresInteraction",getViscoelasticFromSpheresInteraction,(py::arg("m"),py::arg("tc"),py::arg("en"),py::arg("es")),"Get viscoelastic interaction parameters from analytical solution of a pair spheres collision problem. \n\n:Parameters:\n\t`m` : float\n\t\tsphere mass\n\t`tc` : float\n\t\tcollision time\n\t`en` : float\n\t\tnormal restitution coefficient\n\t`es` : float\n\t\ttangential restitution coefficient.\n\n\n:return: \n\tdict with keys:\n\n\tkn : float\n\t\tnormal elastic coefficient computed as:\n\n.. math:: k_n=\\frac{m}{t_c^2}\\left(\\pi^2+(\\ln e_n)^2\\right)\n\n\tcn : float\n\t\tnormal viscous coefficient computed as:\n\n.. math:: c_n=-\\frac{2m}{t_c}\\ln e_n\n\n\n\tkt : float\n\t\ttangential elastic coefficient computed as:\n\n.. math:: k_t=\\frac27\\frac{m}{t_c^2}\\left(\\pi^2+(\\ln e_t)^2\\right)\n\n\tct : float\n\t\ttangential viscous coefficient computed as:\n\n.. math:: c_t=-\\frac27\\frac{m}{t_c}\\ln e_t.\n\n\nFor details see [Pournin2001]_. ");
 	py::def("stressTensorOfPeriodicCell",Shop__stressTensorOfPeriodicCell,(py::args("smallStrains")=false),"Compute overall (macroscopic) stress of periodic cell using equation published in [Kuhl2001]_:\n\n.. math:: \\vec{\\sigma}=\\frac{1}{V}\\sum_cl^c[\\vec{N}^cf_N^c+\\vec{T}^{cT}\\cdot\\vec{f}^c_T],\n\nwhere $V$ is volume of the cell, $l^c$ length of interaction $c$, $f^c_N$ normal force and $\\vec{f}^c_T$ shear force. Sumed are values over all interactions $c$. $\\vec{N}^c$ and $\\vec{T}^{cT}$ are projection tensors (see the original publication for more details):\n\n.. math:: \\vec{N}=\\vec{n}\\otimes\\vec{n}\\rightarrow N_{ij}=n_in_j\n\n.. math:: \\vec{T}^T=\\vec{I}_{sym}\\cdot\\vec{n}-\\vec{n}\\otimes\\vec{n}\\otimes\\vec{n}\\rightarrow T^T_{ijk}=\\frac{1}{2}(\\delta_{ik}\\delta_{jl}+\\delta_{il}\\delta_{jk})n_l-n_in_jn_k\n\n.. math:: \\vec{T}^T\\cdot\\vec{f}_T\\equiv T^T_{ijk}f_k=(\\delta_{ik}n_j/2+\\delta_{jk}n_i/2-n_in_jn_k)f_k=n_jf_i/2+n_if_j/2-n_in_jn_kf_k,\n\nwhere $n$ is unit vector oriented along the interaction (:yref:`normal<GenericSpheresContact::normal>`) and $\\delta$ is Kronecker's delta. As $\\vec{n}$ and $\\vec{f}_T$ are perpendicular (therfore $n_if_i=0$) we can write\n\n.. math:: \\sigma_{ij}=\\frac{1}{V}\\sum l[n_in_jf_N+n_jf^T_i/2+n_if^T_j/2]\n\n:param bool smallStrains: if false (large strains), real values of volume and interaction lengths are computed. If true, only :yref:`refLength<Dem3DofGeom::refLength>` of interactions and initial volume are computed (can save some time).\n\n:return: macroscopic stress tensor as Matrix3");
+	py::def("maxOverlapRatio",maxOverlapRatio,"Return maximum overlap ration in interactions (with :yref:`ScGeom`) of two :yref:`spheres<Sphere>`. The ratio is computed as $\\frac{u_N}{2(r_1 r_2)/r_1+r_2}$, where $u_N$ is the current overlap distance and $r_1$, $r_2$ are radii of the two spheres in contact.");
 }

=== modified file 'py/plot.py'
--- py/plot.py	2010-09-13 11:32:30 +0000
+++ py/plot.py	2010-09-15 10:06:42 +0000
@@ -161,11 +161,12 @@
 			if y1: plots_p_y1.append(d)
 			else: plots_p_y2.append(d)
 			if d[0] not in data.keys(): missing.add(d[0])
-		if len(data.keys())==0 or len(data[data.keys()[0]])==0: # no data at all yet, do not add garbage NaNs
-			for m in missing: data[m]=[]
-		else:
-			print 'Missing columns in plot.data, adding NaN: ',','.join(list(missing))
-			addDataColumns(missing)
+		if missing:
+			if len(data.keys())==0 or len(data[data.keys()[0]])==0: # no data at all yet, do not add garbage NaNs
+				for m in missing: data[m]=[]
+			else:
+				print 'Missing columns in plot.data, adding NaN: ',','.join(list(missing))
+				addDataColumns(missing)
 		# create y1 lines
 		for d in plots_p_y1:
 			line,=pylab.plot(data[pStrip],data[d[0]],d[1])

=== modified file 'py/utils.py'
--- py/utils.py	2010-09-13 11:32:30 +0000
+++ py/utils.py	2010-09-15 10:06:42 +0000
@@ -775,7 +775,7 @@
 	tagsParams=[]
 	# dictParams is what eventually ends up in yade.params.table (default+specified values)
 	dictDefaults,dictParams,dictAssign={},{},{}
-	import os, __builtin__,re
+	import os, __builtin__,re,math
 	if not tableFileLine and not os.environ.has_key('PARAM_TABLE'):
 		if not noTableOk: raise EnvironmentError("PARAM_TABLE is not defined in the environment")
 		O.tags['line']='l!'
@@ -798,7 +798,7 @@
 			elif col in kw.keys(): kw.pop(col) # remove the var from kw, so that it contains only those that were default at the end of this loop
 			#print 'ASSIGN',col,vv[col]
 			tagsParams+=['%s=%s'%(col,vv[col])];
-			dictParams[col]=eval(vv[col])
+			dictParams[col]=eval(vv[col],math.__dict__)
 	# assign remaining (default) keys to python vars
 	defaults=[]
 	for k in kw.keys():