yade-dev team mailing list archive
-
yade-dev team
-
Mailing list archive
-
Message #07091
[Branch ~yade-dev/yade/trunk] Rev 2740: 1. Fix OpenMPArrayAccumulator initial zeroing, re-enable plot test
------------------------------------------------------------
revno: 2740
committer: Václav Šmilauer <eu@xxxxxxxx>
branch nick: yade
timestamp: Tue 2011-02-15 12:36:29 +0100
message:
1. Fix OpenMPArrayAccumulator initial zeroing, re-enable plot test
2. Add Matrix3 ctor from Quaternion and vice versa in Python
3. Fix some bugs in L3Geom initial axes orientation
4. Plots now display pointed triangles instead of circles to mark current position (and orientation); plot part after the current point is transparent
added:
core/EnergyTracker.cpp
modified:
core/EnergyTracker.hpp
core/SConscript
doc/sphinx/tutorial/02-gravity-deposition.py
doc/sphinx/tutorial/make-simulation-video.py
lib/base/openmp-accu.hpp
pkg/dem/DomainLimiter.cpp
pkg/dem/L3Geom.cpp
py/mathWrap/miniEigen.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
=== added file 'core/EnergyTracker.cpp'
--- core/EnergyTracker.cpp 1970-01-01 00:00:00 +0000
+++ core/EnergyTracker.cpp 2011-02-15 11:36:29 +0000
@@ -0,0 +1,12 @@
+#include<yade/core/EnergyTracker.hpp>
+
+Real EnergyTracker::total() const { Real ret=0; size_t sz=energies.size(); for(size_t id=0; id<sz; id++) ret+=energies.get(id); return ret; }
+py::list EnergyTracker::keys_py() const { py::list ret; FOREACH(pairStringInt p, names) ret.append(p.first); return ret; }
+py::list EnergyTracker::items_py() const { py::list ret; FOREACH(pairStringInt p, names) ret.append(py::make_tuple(p.first,energies.get(p.second))); return ret; }
+py::dict EnergyTracker::perThreadData() const {
+ py::dict ret;
+ std::vector<std::vector<Real> > dta=energies.getPerThreadData();
+ FOREACH(pairStringInt p,names) ret[p.first]=dta[p.second];
+ return ret;
+}
+
=== modified file 'core/EnergyTracker.hpp'
--- core/EnergyTracker.hpp 2011-02-07 09:00:07 +0000
+++ core/EnergyTracker.hpp 2011-02-15 11:36:29 +0000
@@ -43,12 +43,13 @@
int id=-1; set(val,name,id);
}
void clear(){ energies.clear(); names.clear(); resetStep.clear();}
- Real total(){ Real ret=0; size_t sz=energies.size(); for(size_t id=0; id<sz; id++) ret+=energies.get(id); return ret; }
-
- py::list keys_py(){ py::list ret; FOREACH(pairStringInt p, names) ret.append(p.first); return ret; }
- py::list items_py(){ py::list ret; FOREACH(pairStringInt p, names) ret.append(py::make_tuple(p.first,energies.get(p.second))); return ret; }
void resetResettables(){ size_t sz=energies.size(); for(size_t id=0; id<sz; id++){ if(resetStep[id]) energies.reset(id); } }
+ Real total() const;
+ py::list keys_py() const;
+ py::list items_py() const;
+ py::dict perThreadData() const;
+
typedef std::map<std::string,int> mapStringInt;
typedef std::pair<std::string,int> pairStringInt;
@@ -64,6 +65,7 @@
.def("keys",&EnergyTracker::keys_py,"Return defined energies.")
.def("items",&EnergyTracker::items_py,"Return contents as list of (name,value) tuples.")
.def("total",&EnergyTracker::total,"Return sum of all energies.")
+ .add_property("_perThreadData",&EnergyTracker::perThreadData,"Contents as dictionary, where each value is tuple of individual threads' values (for debugging)")
)
};
REGISTER_SERIALIZABLE(EnergyTracker);
=== modified file 'core/SConscript'
--- core/SConscript 2011-01-29 22:47:18 +0000
+++ core/SConscript 2011-02-15 11:36:29 +0000
@@ -26,6 +26,7 @@
'Bound.cpp',
'Cell.cpp',
'PartialEngine.cpp',
+ 'EnergyTracker.cpp',
'Engine.cpp',
'FileGenerator.cpp',
'FrontEnd.cpp',
=== modified file 'doc/sphinx/tutorial/02-gravity-deposition.py'
--- doc/sphinx/tutorial/02-gravity-deposition.py 2011-01-20 16:35:46 +0000
+++ doc/sphinx/tutorial/02-gravity-deposition.py 2011-02-15 11:36:29 +0000
@@ -30,7 +30,7 @@
# call the checkUnbalanced function (defined below) every 2 seconds
PyRunner(command='checkUnbalanced()',realPeriod=2),
# call the addPlotData function every 200 steps
- PyRunner(command='addPlotData()',iterPeriod=200)
+ PyRunner(command='addPlotData()',iterPeriod=100)
]
O.dt=.5*utils.PWaveTimeStep()
=== modified file 'doc/sphinx/tutorial/make-simulation-video.py'
--- doc/sphinx/tutorial/make-simulation-video.py 2011-01-20 16:35:46 +0000
+++ doc/sphinx/tutorial/make-simulation-video.py 2011-02-15 11:36:29 +0000
@@ -14,7 +14,7 @@
simulations=[
('01-bouncing-sphere.py',u'Bouncing sphere',100000),
- ('02-gravity-deposition.py',u'Gravity deposition',200),
+ ('02-gravity-deposition.py',u'Gravity deposition',100),
('03-oedometric-test.py',u'Oedometric test',500),
('04-periodic-simple-shear.py',u'Simple shear\nwith\nperiodic boundary',100),
('06-periodic-triaxial-test.py',u'Periodic triaxal test\nwith clumps',200)
=== modified file 'lib/base/openmp-accu.hpp'
--- lib/base/openmp-accu.hpp 2011-02-08 15:29:38 +0000
+++ lib/base/openmp-accu.hpp 2011-02-15 11:36:29 +0000
@@ -37,7 +37,6 @@
if(nCL_new>nCL){
for(size_t th=0; th<nThreads; th++){
void* oldChunk=(void*)chunks[th];
- // FIXME: currently we allocate 4Ã the memory necessary, otherwise there is crash when accessing past its half -- why?? (http://www.abclinuxu.cz/poradna/programovani/show/318324)
int succ=posix_memalign((void**)(&chunks[th]),/*alignment*/CLS,/*size*/ nCL_new*CLS);
if(succ!=0) throw std::runtime_error("OpenMPArrayAccumulator: posix_memalign failed to allocate memory.");
if(oldChunk){ // initialized to NULL initially, that must not be copied and freed
@@ -50,7 +49,7 @@
// if nCL_new<nCL, do not deallocate memory
// if nCL_new==nCL, only update sz
// reset items that were added
- for(size_t s=sz; s>n; s++){ for(size_t th=0; th<nThreads; th++) chunks[th][s]=ZeroInitializer<T>(); }
+ for(size_t s=sz; s<n; s++){ for(size_t th=0; th<nThreads; th++) chunks[th][s]=ZeroInitializer<T>(); }
sz=n;
}
// clear (does not deallocate storage, anyway)
@@ -67,6 +66,8 @@
void reset(size_t ix){ set(ix,ZeroInitializer<T>()); }
// fill all memory with zeros; the caller is responsible for assuring that such value is meaningful when converted to T
// void memsetZero(){ for(size_t th=0; th<nThreads; th++) memset(&chunks[th],0,CLS*nCL); }
+ // get all stored data, organized first by index, then by threads; only used for debugging
+ std::vector<std::vector<T> > getPerThreadData() const { std::vector<std::vector<T> > ret; for(size_t ix=0; ix<sz; ix++){ std::vector<T> vi; for(size_t th=0; th<nThreads; th++) vi.push_back(chunks[th][ix]); ret.push_back(vi); } return ret; }
};
/* Class accumulating results of type T in parallel sections. Summary value (over all threads) can be read or reset in non-parallel sections only.
@@ -107,6 +108,8 @@
// .def_readonly("myAccu",&OpenMPAccumulator::get,"documentation")
T get() const { T ret(ZeroInitializer<T>()); for(int i=0; i<nThreads; i++) ret+=*(T*)(data+i*eSize); return ret; }
void set(const T& value){ reset(); /* set value for the 0th thread */ *(T*)(data)=value; }
+ // only useful for debugging
+ std::vector<T> getPerThreadData() const { std::vector<T> ret; for(int i=0; i<nThreads; i++) ret.push_back(*(T*)(data+i*eSize)); return ret; }
};
#else
template<typename T>
@@ -123,6 +126,7 @@
void add (size_t ix, const T& diff){ data[ix]+=diff; }
void set(size_t ix, const T& val){ data[ix]=val; }
void reset(size_t ix){ data[ix]=ZeroInitializer<T>(); }
+ std::vector<std::vector<T> > getPerThreadData() const { std::vector<std::vector<T> > ret; for(size_t ix=0; ix<data.size(); ix++){ std::vector<T> vi; vi.push_back(data[ix]); ret.push_back(vi); } return ret; }
};
// single-threaded version of the accumulator above
@@ -135,6 +139,8 @@
void reset(){ data=ZeroInitializer<T>(); }
T get() const { return data; }
void set(const T& val){ data=val; }
+ // debugging only
+ std::vector<T> getPerThreadData() const { std::vector<T> ret; ret.push_back(data); return ret; }
};
#endif
=== modified file 'pkg/dem/DomainLimiter.cpp'
--- pkg/dem/DomainLimiter.cpp 2011-01-29 22:47:18 +0000
+++ pkg/dem/DomainLimiter.cpp 2011-02-15 11:36:29 +0000
@@ -104,7 +104,7 @@
axX=gsc->normal; /* just in case */ axX.normalize();
if(doInit){ // initialization of the new interaction -- define local axes
// take vector in the y or z direction, depending on its length; arbitrary, but one of them is sure to be non-zero
- axY=axX.cross(axX[1]>axX[2]?Vector3r::UnitY():Vector3r::UnitZ());
+ axY=axX.cross(abs(axX[1])<abs(axX[2])?Vector3r::UnitY():Vector3r::UnitZ());
axY.normalize();
axZ=axX.cross(axY);
LOG_DEBUG("Initial axes x="<<axX<<", y="<<axY<<", z="<<axZ);
=== modified file 'pkg/dem/L3Geom.cpp'
--- pkg/dem/L3Geom.cpp 2011-02-14 08:05:09 +0000
+++ pkg/dem/L3Geom.cpp 2011-02-15 11:36:29 +0000
@@ -97,7 +97,7 @@
// g.trsf.setFromTwoVectors(Vector3r::UnitX(),g.normal); // quaternion just from the X-axis; does not seem to work for some reason?!
const Vector3r& locX(g.normal);
// initial local y-axis orientation, in the xz or xy plane, depending on which component is larger to avoid singularities
- Vector3r locY=normal.cross(normal[1]<normal[2]?Vector3r::UnitY():Vector3r::UnitZ()); locY-=locX*locY.dot(locX); locY.normalize();
+ Vector3r locY=normal.cross(abs(normal[1])<abs(normal[2])?Vector3r::UnitY():Vector3r::UnitZ()); locY-=locX*locY.dot(locX); locY.normalize();
Vector3r locZ=normal.cross(locY);
#ifdef L3_TRSF_QUATERNION
Matrix3r trsf; trsf.row(0)=locX; trsf.row(1)=locY; trsf.row(2)=locZ;
=== modified file 'py/mathWrap/miniEigen.cpp'
--- py/mathWrap/miniEigen.cpp 2011-01-29 22:47:18 +0000
+++ py/mathWrap/miniEigen.cpp 2011-02-15 11:36:29 +0000
@@ -200,6 +200,7 @@
bp::class_<Matrix3r>("Matrix3","3x3 float matrix.\n\nSupported operations (``m`` is a Matrix3, ``f`` if a float/int, ``v`` is a Vector3): ``-m``, ``m+m``, ``m+=m``, ``m-m``, ``m-=m``, ``m*f``, ``f*m``, ``m*=f``, ``m/f``, ``m/=f``, ``m*m``, ``m*=m``, ``m*v``, ``v*m``, ``m==m``, ``m!=m``.",bp::init<>())
.def(bp::init<Matrix3r const &>((bp::arg("m"))))
+ .def(bp::init<Quaternionr const &>((bp::arg("q"))))
.def("__init__",bp::make_constructor(&Matrix3r_fromElements,bp::default_call_policies(),(bp::arg("m00"),bp::arg("m01"),bp::arg("m02"),bp::arg("m10"),bp::arg("m11"),bp::arg("m12"),bp::arg("m20"),bp::arg("m21"),bp::arg("m22"))))
.def_pickle(Matrix3r_pickle())
//
@@ -238,6 +239,7 @@
.def("__init__",bp::make_constructor(&Quaternionr_fromAxisAngle,bp::default_call_policies(),(bp::arg("axis"),bp::arg("angle"))))
.def("__init__",bp::make_constructor(&Quaternionr_fromAngleAxis,bp::default_call_policies(),(bp::arg("angle"),bp::arg("axis"))))
.def(bp::init<Real,Real,Real,Real>((bp::arg("w"),bp::arg("x"),bp::arg("y"),bp::arg("z")),"Initialize from coefficients.\n\n.. note:: The order of coefficients is *w*, *x*, *y*, *z*. The [] operator numbers them differently, 0â¦4 for *x* *y* *z* *w*!"))
+ .def(bp::init<Matrix3r>((bp::arg("rotMatrix")))) //,"Initialize from given rotation matrix.")
.def(bp::init<Quaternionr>((bp::arg("other"))))
.def_pickle(Quaternionr_pickle())
// properties
=== modified file 'py/plot.py'
--- py/plot.py 2011-02-09 12:46:18 +0000
+++ py/plot.py 2011-02-15 11:36:29 +0000
@@ -69,6 +69,11 @@
"Linewidth (in points) to make *x* and *y* axes better visible; not activated if non-positive."
current=-1
"Point that is being tracked with a scatter point. -1 is for the last point, set to *nan* to disable."
+afterCurrentAlpha=.2
+"Color alpha value for part of lines after :yref:`yade.plot.current`, between 0 (invisible) to 1 (full color)"
+scatterMarkerKw=dict(verts=[(0.,0.),(-30.,10.),(-25,0),(-30.,-10.)],marker=None)
+"Parameters for the current position marker"
+
componentSeparator='_'
componentSuffixes={Vector2:{0:'x',1:'y'},Vector3:{0:'x',1:'y',2:'z'},Matrix3:{(0,0):'xx',(1,1):'yy',(2,2):'zz',(0,1):'xy',(0,2):'xz',(1,2):'yz',(1,0):'yz',(2,0):'zx',(2,1):'zy'}}
@@ -141,10 +146,7 @@
... ]
>>> O.trackEnergy=True
>>> O.run(2,True)
- """
- '''
- #The test is disabled due to https://bugs.launchpad.net/yade/+bug/715739
- pprint(plot.data) #doctest: +ELLIPSIS
+ >>> pprint(plot.data) #doctest: +ELLIPSIS
{'gravWork': [0.0, -25.13274...],
'i': [0, 1],
'kinRot': [0.0, 0.0],
@@ -153,8 +155,6 @@
'total energy': [0.0, -7.5398...]}
.. plot::
- '''
- """
from yade import *
from yade import plot,utils
O.reset()
@@ -303,8 +303,8 @@
class LineRef:
"""Holds reference to plot line and to original data arrays (which change during the simulation),
and updates the actual line using those data upon request."""
- def __init__(self,line,scatter,xdata,ydata,dataName=None):
- self.line,self.scatter,self.xdata,self.ydata,self.dataName=line,scatter,xdata,ydata,dataName
+ def __init__(self,line,scatter,line2,xdata,ydata,dataName=None):
+ self.line,self.scatter,self.line2,self.xdata,self.ydata,self.dataName=line,scatter,line2,xdata,ydata,dataName
def update(self):
if isinstance(self.line,matplotlib.image.AxesImage):
# image name
@@ -317,22 +317,49 @@
else:
# regular data
import numpy
- self.line.set_xdata(self.xdata)
- self.line.set_ydata(self.ydata)
+ # current==-1 avoids copy slicing data in the else part
+ if current==None or current==-1 or afterCurrentAlpha==1:
+ self.line.set_xdata(self.xdata); self.line.set_ydata(self.ydata)
+ self.line2.set_xdata([]); self.line2.set_ydata([])
+ else:
+ try: # try if we can extend the first part by one so that lines are connected
+ self.xdata[:current+1]; preCurrEnd=current+1
+ except IndexError: preCurrEnd=current
+ preCurrEnd=current+(1 if len(self.xdata)>current else 0)
+ self.line.set_xdata(self.xdata[:preCurrEnd]); self.line.set_ydata(self.ydata[:preCurrEnd])
+ self.line2.set_xdata(self.xdata[current:]); self.line2.set_ydata(self.ydata[current:])
try:
- x,y=[self.xdata[current]],[self.ydata[current]]
+ x,y=self.xdata[current],self.ydata[current]
except IndexError: x,y=0,0
# this could be written in a nicer way, very likely
try:
- pt=numpy.ndarray((2,),buffer=numpy.array([x,y]))
- if self.scatter: self.scatter.set_offsets(pt)
+ pt=numpy.ndarray((2,),buffer=numpy.array([float(x),float(y)]))
+ if self.scatter:
+ self.scatter.set_offsets(pt)
+ # change rotation of the marker (possibly incorrect)
+ try:
+ dx,dy=self.xdata[current]-self.xdata[current-1],self.ydata[current]-self.ydata[current-1]
+ # smoothing from last n values, if possible
+ # FIXME: does not show arrow at all if less than window values
+ #try:
+ # window=10
+ # dx,dy=[numpy.average(numpy.diff(dta[current-window:current])) for dta in self.xdata,self.ydata]
+ #except IndexError: pass
+ # there must be an easier way to find on-screen derivative angle, ask on the matplotlib mailing list
+ axes=self.line.get_axes()
+ p=axes.patch; xx,yy=p.get_verts()[:,0],p.get_verts()[:,1]; size=max(xx)-min(xx),max(yy)-min(yy)
+ aspect=(size[1]/size[0])*(1./axes.get_data_ratio())
+ angle=math.atan(aspect*dy/dx)
+ if dx<0: angle-=math.pi
+ self.scatter.set_transform(matplotlib.transforms.Affine2D().rotate(angle))
+ except IndexError: pass
except TypeError: pass # this happens at i386 with empty data, saying TypeError: buffer is too small for requested array
currLineRefs=[]
liveTimeStamp=0 # timestamp when live update was started, so that the old thread knows to stop if that changes
nan=float('nan')
-def createPlots(subPlots=True,scatterSize=20,wider=False):
+def createPlots(subPlots=True,scatterSize=60,wider=False):
global currLineRefs
figs=set([l.line.get_axes().get_figure() for l in currLineRefs]) # get all current figures
for f in figs: pylab.close(f) # close those
@@ -352,7 +379,7 @@
if len(imgData[pStrip])==0 or imgData[pStrip][-1]==None: img=Image.new('RGBA',(1,1),(0,0,0,0))
else: img=Image.open(imgData[pStrip][-1])
img=pylab.imshow(img,origin='lower')
- currLineRefs.append(LineRef(img,None,imgData[pStrip],None,pStrip))
+ currLineRefs.append(LineRef(img,None,None,imgData[pStrip],None,pStrip))
pylab.gca().set_axis_off()
continue
plots_p=[addPointTypeSpecifier(o) for o in tuplifyYAxis(plots[p])]
@@ -389,17 +416,19 @@
if len(ySpecs2)==0:
print 'yade.plot: creating fake plot, since there are no y-data yet'
line,=pylab.plot([nan],[nan])
- currLineRefs.append(LineRef(line,None,[nan],[nan]))
+ line2,=pylab.plot([nan],[nan])
+ currLineRefs.append(LineRef(line,None,line2,[nan],[nan]))
# set different color series for y1 and y2 so that they are recognizable
if pylab.rcParams.has_key('axes.color_cycle'): pylab.rcParams['axes.color_cycle']='b,g,r,c,m,y,k' if not isY1 else 'm,y,k,b,g,r,c'
for d in ySpecs2:
yNames.add(d)
line,=pylab.plot(data[pStrip],data[d[0]],d[1],label=xlateLabel(d[0]))
+ line2,=pylab.plot([],[],d[1],color=line.get_color(),alpha=afterCurrentAlpha)
# use (0,0) if there are no data yet
scatterPt=[0,0] if len(data[pStrip])==0 else (data[pStrip][current],data[d[0]][current])
# if current value is NaN, use zero instead
- scatter=pylab.scatter(scatterPt[0] if not math.isnan(scatterPt[0]) else 0,scatterPt[1] if not math.isnan(scatterPt[1]) else 0,s=scatterSize,color=line.get_color())
- currLineRefs.append(LineRef(line,scatter,data[pStrip],data[d[0]]))
+ scatter=pylab.scatter(scatterPt[0] if not math.isnan(scatterPt[0]) else 0,scatterPt[1] if not math.isnan(scatterPt[1]) else 0,s=scatterSize,color=line.get_color(),**scatterMarkerKw)
+ currLineRefs.append(LineRef(line,scatter,line2,data[pStrip],data[d[0]]))
axes=line.get_axes()
labelLoc=(legendLoc[0 if isY1 else 1] if y2Exists>0 else 'best')
l=pylab.legend(loc=labelLoc)
@@ -411,7 +440,6 @@
if isY1:
pylab.ylabel((', '.join([xlateLabel(_p[0]) for _p in ySpecs2])) if p not in xylabels or not xylabels[p][1] else xylabels[p][1])
pylab.xlabel(xlateLabel(pStrip) if (p not in xylabels or not xylabels[p][0]) else xylabels[p][0])
- ## should be done for y2 as well, but in that case the 10^.. label goes to y1 axis (bug in matplotlib, present in versions .99--1.0.5, and possibly beyond)
else:
pylab.ylabel((', '.join([xlateLabel(_p[0]) for _p in ySpecs2])) if (p not in xylabels or len(xylabels[p])<3 or not xylabels[p][2]) else xylabels[p][2])
# if there are callable/dict ySpecs, save them inside the axes object, so that the live updater can use those
@@ -447,7 +475,7 @@
for f in ax.yadeYFuncs:
if callable(f): yy.update(f())
elif hasattr(f,'keys'): yy.update(f.keys())
- else: raise ValueError("Internal error: ax.yadeYFuncs items must be callables or dictrionary-like objects and nothing else.")
+ else: raise ValueError("Internal error: ax.yadeYFuncs items must be callables or dictionary-like objects and nothing else.")
#print 'callables y names:',yy
news=yy-ax.yadeYNames
if not news: continue
@@ -458,9 +486,10 @@
if not new in data.keys(): data[new]=len(data[ax.yadeXName])*[nan] # create data entry if necessary
#print 'data',len(data[ax.yadeXName]),len(data[new]),data[ax.yadeXName],data[new]
line,=ax.plot(data[ax.yadeXName],data[new],label=xlateLabel(new)) # no line specifier
+ line2,=ax.plot([],[],color=line.get_color(),alpha=afterCurrentAlpha)
scatterPt=(0 if len(data[ax.yadeXName])==0 or math.isnan(data[ax.yadeXName][current]) else data[ax.yadeXName][current]),(0 if len(data[new])==0 or math.isnan(data[new][current]) else data[new][current])
- scatter=ax.scatter(scatterPt[0],scatterPt[1],color=line.get_color())
- currLineRefs.append(LineRef(line,scatter,data[ax.yadeXName],data[new]))
+ scatter=ax.scatter(scatterPt[0],scatterPt[1],s=60,color=line.get_color(),**scatterMarkerKw)
+ currLineRefs.append(LineRef(line,scatter,line2,data[ax.yadeXName],data[new]))
ax.set_ylabel(ax.get_ylabel()+(', ' if ax.get_ylabel() else '')+xlateLabel(new))
# it is possible that the legend has not yet been created
l=ax.legend(loc=ax.yadeLabelLoc)
=== modified file 'py/utils.py'
--- py/utils.py 2011-01-12 16:22:18 +0000
+++ py/utils.py 2011-02-15 11:36:29 +0000
@@ -28,14 +28,14 @@
For example, variables *a*, *b* and *c* are defined. To save them, use::
>>> from yade import utils
- >>> utils.saveVars('mark',a=1,b=2,c=3)
- >>> from yade.params.mark import *
+ >>> utils.saveVars('something',a=1,b=2,c=3)
+ >>> from yade.params.something import *
>>> a,b,c
(1, 2, 3)
those variables will be save in the .xml file, when the simulation itself is saved. To recover those variables once the .xml is loaded again, use
- >>> utils.loadVars('mark')
+ >>> utils.loadVars('something')
and they will be defined in the yade.params.\ *mark* module. The *loadNow* parameter calls :yref:`yade.utils.loadVars` after saving automatically.
"""