Internal data

Overview

This chapter explains how to obtain various data from simulation and how to plot them. Using per-simulation labels for things is exaplained, and scene tags are introduced.

Plotting

In the previous chapter, a simple simulation of a sphere falling onto the plane was created. The only output so far was the 3d view. Let’s see how we can plot some data, such as \(z(t)\), i.e. the evolution of the \(z\)-coordinate of the sphere over time. It consists of two separate tasks:

  • Collecting data to plot, since we need to keep history;

  • Saying how to plot which of the collected data.

Plot data are stored in S.plot.data, keyed by some unique name, in series (“rows”); data are always added to all “columns” (a Not a Number is automatically added to rows which are not defined at the time-point).

We can define what to plot by setting S.plot.plots. It is a dictionary, which is given in the form of {key:value,...}. The key is the \(x\)-axis data key, value is a tuple. Better give an example, in the following.

Simplified interface

With the simplified interface, expressions to be evaluated are used as plot keys directly:

S.plot.plots={'S.time': ('S.dem.par[1].pos[2]',)}

This would plot \(z\)-position (pos[2], as pos is a vector in 3d) of the 1st particle (the sphere) against time. We need to add an engine which will save the data for us:

S.engines=S.engines+[PyRunner(10,'S.plot.autoData()')]

PyRunner runs an arbitrary python code every n steps; so we use previous-defined engines and add the PyRunner to it, which will call S.plot.autoData() every 10 steps. PyRunner automatically sets several variables which can be used inside expressions – one of them is S, the current scene (for which the engine is called).

from past.builtins import execfile
execfile('basic-1.py')                                  # re-use the simple setup for brevity
S.plot.plots={'S.time': ('S.dem.par[1].pos[2]',)}       # define what to plot
S.engines=S.engines+[PyRunner(10,'S.plot.autoData()')]  # add the engine
S.throttle=0                                            # run at full speed now
S.run(2000,True)                                        # run 2000 steps
S.plot.plot()                                           # show the plot

(Source code, png, hires.png, pdf)

../_images/data-1.png

Separate \(y\) axes

To make the plot prettier, \(x\)-axis and \(y\)-axis title can be set using the name=expression syntax. Multiple variables can be plotted on one \(y\)-axis; using None for key will put the rest on the \(y_2\) (right) \(y\)-axis. The following will plot \(z\) positions of both particles on the left and \(E_k\) (kinetic energy) of the sphere on the right:

S.plot.plots={'t=S.time': (     # complex expressions can span multiple lines in python :)
   '$z_1$=S.dem.par[1].pos[2]',
   '$z_0$=S.dem.par[0].pos[2]',
   None,                        # remaining things go to the y2 axis
   '$E_k$=S.dem.par[1].Ek'
)}

(Source code, png, hires.png, pdf)

../_images/data-2.png

Note that we used math expressions $...$ for symbols in line labels; the syntax is very similar to LaTeX and is documented in detail in the underlying plotting library, Matplotlib.

Subfigures

The S.plot.plots dictionary can contain multiple keys; each of them then creates a new sub-figure.

There is a limited support for specifying style of individual lines by using (key,format) instead of key, where format is Matlab-like syntax explained in detail here; it consists of color and line/marker abbreviation such as 'g--' specifying dashed green line.

This example also demonstrates that **expression denotes a dictionary-like object which contains values to be plotted (this syntax is similar to keyword argument syntax in Python); this feature is probably only useful with energy tracking where keys are not known a-priori.

We add another figure showing energies on the left; energy total (“\(\sum\) energy”) should be zero, as energy should be conserved, and relative error will be plotted on the right:

S.plot.plots={
   't=S.time':(
      '$z_1$=S.dem.par[1].pos[2]',
      '$z_0$=S.dem.par[0].pos[2]',
    ),
    ' t=S.time':(                                 # note the space before 't': explained below
      '**S.energy',                               # plot all energy fields on the first axis
      r'$\sum$ energy=S.energy.total()',          # compute total energy
      None,                                       # put the rest on the y2 axis
      ('relative error=S.energy.relErr()','g--')  # plot relative error of energy measures, dashed green
    )
}

# energy is not tracked by default, it must be turned on explicitly
S.trackEnergy=True

Note

We had to prepend space to the second \(x\)-axis in ” t=S.time”; otherwise, the second value would overwrite the first one, as dictionary must have unique keys. Spaces are stripped away, so they don’t appear in the output.

(Source code, png, hires.png, pdf)

../_images/data-3.png

Complex interface

The complex interface requires one to assign keys to data using S.plot.addData. The advantage is that arbitrary computations can be performed on the data before they are stored. One usually creates a function to store the data in this way:

# use PyRunner to call our function periodically
S.engines=S.engines+[PyRunner(5,'myAddPlotData(S)')]
# define function which will be called by the PyRunner above
def myAddPlotData(S):
   ## do any computations here
   uf=woo.utils.unbalancedForce(S)
   ## store the data, use any keys you want
   S.plot.addData(t=S.time,i=S.step,z1=S.dem.par[1].pos[1],Ek=S.dem.par[1].Ek,unbalanced=uf)
# specify plots as usual
S.plot.plots={'t':('z1',None,('Ek','r.-')),'i':('unbalanced',)}

(Source code, png, hires.png, pdf)

../_images/data-4.png

Save plot data

Collected data can be saved to file using S.plot.saveDataTxt. This function creates (optionally compressed) text file with numbers written in columns; it can be readily loaded into NumPy via numpy.genfromtxt or into a spreadsheet program:

S.plot.saveDataTxt('basic-1.data.txt')

produces (only the beginning of the file is shown):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Ek	i	t	unbalanced	z1
0.0	0	0.0	nan	0.0
0.08358947705644633	5	0.009000000000000001	nan	0.0
0.3343579082257853	10	0.018	nan	0.0
0.7523052935080173	15	0.026999999999999996	nan	0.0
1.3374316329031413	20	0.036000000000000004	nan	0.0
2.0897369264111574	25	0.04500000000000002	nan	0.0
3.009221174032065	30	0.054000000000000034	nan	0.0
4.095884375765865	35	0.06300000000000004	nan	0.0
5.349726531612559	40	0.07200000000000002	nan	0.0
6.770747641572144	45	0.081	nan	0.0
8.35894770564462	50	0.08999999999999998	nan	0.0
10.11432672382999	55	0.09899999999999996	nan	0.0
12.03688469612825	60	0.10799999999999994	nan	0.0
14.126621622539405	65	0.11699999999999992	nan	0.0

Labels

When plotting, we were accessing the sphere using the S.dem.par[1] notation; that is not convenient. For this reason, there exists a special place for putting labeled objects, S.lab (it is a per-scene isntance of the woo.core.LabelMapper object) which can then be accessed by readable names.

Engines, when they have label set in the constructor (as is the case with woo.dem.DemField.minimalEngines), are added to S.lab automatically, other objects are to be added manually as needed.

Woo[1]: S=Scene(engines=[PyRunner(10,'print("Step number %d"%S.step)',label='foo')],fields=[DemField()])

Woo[2]: S.engines
Out[2]: [<PyRunner @ 0x2ab6c30>]

Woo[3]: S.lab.foo            # the same engine accessed using the label
Out[3]: <PyRunner @ 0x2ab6c30>

Woo[4]: S.lab.foo.command    # its properties can be accessed
Out[4]: 'print("Step number %d"%S.step)'

Woo[5]: s=Sphere.make((0,1,2),1)    # create a new spherical particle

Woo[6]: S.lab.sphere=s              # give it the label "sphere"

Woo[7]: S.dem.par.add(s)            # add it to the simulation
Out[7]: 0

Woo[8]: S.dem.par[0]==S.lab.sphere  # it is one and the same particle
Out[8]: True

Woo[9]: S.dem.par[0].pos[2], S.lab.sphere.pos[2]  # the z-position can be accessed both ways
Out[9]: (2.0, 2.0)

Tags

Each Scene defines tags of which some are filled-in automatically. It is a dictionary-like object. Some of the useful tags are id (unique identifier of the simulation, composed of time, date and process number) and title (by default empty, but used in batch simulations), tid (title+id concatenated, for readability and uniqueness).

Woo[10]: S.tags.keys()
Out[10]: ['id', 'idt', 'isoTime', 'tid', 'title', 'user']

Woo[11]: S.tags['isoTime']
Out[11]: '2021-12-16T10:34:19'

Woo[12]: S.tags['idt']
Out[12]: '2021-12-16T10:34:19p273650'

Woo[13]: S.tags['id']
Out[13]: '2021-12-16T10:34:19p273650'

Woo[14]: S.tags['whatever']='whatever you want'

Woo[15]: S.tags['whatever']
Out[15]: 'whatever you want'

Tags can be embedded in some parameters in {} and are expanded by woo.core.Scene.expandTags (e.g. VtkExport.out can contain {tid}, ensuring output data will not overwrite each other):

Woo[16]: S.expandTags('/this/is/some/filename-{tid}')
Out[16]: '/this/is/some/filename-2021-12-16T10:34:19p273650'

Tip

Report issues or inclarities to github.