Customizing Visualizations
As is shown in the examples, OpenPathSampling includes several useful tools for visualization aspects of the calculation. In particular, there is the move “decision tree” visualizer, which provides a visual layout of the move scheme being used, and the path “history tree” visualizer, which gives an overview of what happened to a given replica during a path sampling simulation.
The following two examples show how to use and customize each of those visualization tools.
import openpathsampling as p
from IPython.display import SVG
import openpathsampling.visualize as vis
Load the results from the example toy simulation stored in mstis.nc
st = p.Storage('mstis.nc', mode='r')
Load the first (and only) scheme we used in the process. We want to have a look at what it does.
scheme = st.schemes.first
reload(vis)
Create the builder using a factory function. You can do it directly but this way it is faster and easier.
builder = vis.MoveTreeBuilder.from_scheme(scheme)
Options¶
Analysis¶
The most used option is to specify if canonical moves should be collapsed or shown. Default is collapsed so canonical moves are display as a single move (although internally they might do several other canonical moves themselves).
This is to completely show the canonical moves, in our case the MinusMoves
builder.options.analysis['only_canonical'] = False
Show the mover. You see a tree depicting the dependency on the left side. The higher levels (more to the left) run independently of lower levels. This is the way the moves are constructed and allow for better analysis and reusing of submoves. On the right you see a list of ensembles and which ensembles are affected by a specific mover. Green indicates that a sample from this ensemble might be used as input for the move. "Might" means that there is a possibility given the context (see below) and the specific mover that a sample in the ensemble is requested and thus necessary to not cause a RuntimeError. Input usually means that a sample is also remove from this ensemble (although the final ensembles can be the same). A red color similar indicates that a sample will be placed in this ensemble (usually taken from the input ensembles). In almost all cases the samples from the input are removed, optionally altered and placed in the output ensembles.
If a mover really affects an ensembles depends on two things:
- The definition of the mover itself. So a shooting mover has one specific ensembles if shoots it. A RepEx move has two ensembles, etc.
- The second is the context (the sampleset that is moved).
What is this context ?¶
This is difficult to understand and depends on the fact that during a simulation an ensembles can be occupied by more than one samples at a time. (Note that replicas are unique). This means that two subsequent moves might act on different replicas in the same ensemble. As an example consider two shoots in ensemble A. You might think that both moves combined will pick the samples from A and move it twice and finally there is again a sample in A. If you start with two samples in A. Then both moves might pick different samples and the combined move actually does take two separate samples and move each once. This is the effect of the context. In most cases we run with one sample per ensemble max. This will be the default setting if this context is not specified. In most cases you specify the context by giving an initial sampleset and the MoveTreeBuilder will figure out all possible contexts for each submover and use that context.
SVG(builder.svg())
In the minus moves you see that a sample is shifted to a hidden ensembles, then swapped and moved back.
Inspection¶
Inspection is still an experimental feature and might change in the future. As an example we want to know what we need to use as a sampleset to run the mover safely.
A Pathmover has a .in_out
objects that keeps the information on all possible changes in replicas. We can ask for the minimal set of inputs .ins_minimal
. There are more functions that we are not going to explain here, but in a future larger tutorial on pathmove inspection.
minimal = dict(scheme.root_mover.in_out.ins_minimal)
minimal
And compare if the scheme will actually match the requirements
all(ens in minimal for ens in scheme.list_initial_ensembles())
Checking for equality (neglecting multiplicity since we only have a single sample per ensemble in our case)
set(minimal) == set(scheme.list_initial_ensembles())
(tutorial_movetree_visualization.ipynb; tutorial_movetree_visualization.py)
Tutorial on PathTree Visualization¶
We will use the results from the mstis example. So make sure this has been run (not analyzed) before and the mstis.nc
file exists.
Imports and Setup¶
The usual import
import openpathsampling as paths
Import visualization as vis
for convenience
import openpathsampling.visualize as vis
And use output for SVG in IPython notebooks
from IPython.display import SVG, display
Load the storage into memory.
storage = paths.AnalysisStorage("../toy_model_mstis/mstis.nc")
Track down the list of samples that lead to the existance of the last sample in storage (called the heritage)
Let's go¶
Create a PathTree
object that will create the path tree SVG us. In most cases we are investigating samples in the context of simuation MC steps and hence we will initialize the PathTree
object with the steps, that we are interested in.
Each tree displays a specific ordered list of samples. These needs to be specified, which we do using a SampleList
generating object, a SampleListGenerator
. Typical generators are the ReplicaEvolution
or SampleAncestor
or EnsembleEvolution
.
pt = vis.PathTree(
storage.steps[0:100],
vis.ReplicaEvolution(replica=2, accepted=True)
)
pt.options.movers['ReplicaExchangeMover']['hide'] = False
pt.options.ui['legends'] = ['step', 'active']
Reset options¶
This is useful to return to the stardard view
pt.reset_options()
pt.options.movers['default']['new'] = 'single'
SVG(pt.svg())
Options¶
.generator
¶
The generator is a itself a SampleList
object. It has some options about how the contained samples should be analyzed for their correlation.
time_symmetric¶
time_symmetric : bool
If `True` snapshots and its reversed counterpart are treated as the
same configuration
Example¶
First we show what happens without time_symmetric: After a reversal move no connection between the previous trajectory is made since all frames are reversed and hence no frame is shared. With time_symmetric=True
this is fixed and reversed trajectories are placed underneath each other. You should leave this option on.
pt.generator.time_symmetric = True # default
display(SVG(pt.svg()))
pt.generator.time_symmetric = False
display(SVG(pt.svg()))
pt.generator.time_symmetric = True
flip_time_direction¶
flip_time_direction : bool
If `True` then the use of a reversal in time evolution of trajectories
(so far only Reversesal moves to that) will cause the plot to also
reverse its current "sense of time". If False time is always drawn from
left to right. While having the disadvantage of distorting the view of
time, this has the advantage that snapshots after a reversal will still
be align beneath and so the apparent correlation between paths after a
reversal is directly visible.
Example¶
See the effect of a flip in time direction. If you flip the time direction between left and right reversed trajectories underneath each other are identical and hence you do not need to add a new frame block. It is also useful to visualize where actually new snapshots are generated and how strong correlations are because of shared frames. Note that backward and forward also flips the side.
pt.generator.flip_time_direction = False # default
display(SVG(pt.svg()))
pt.generator.flip_time_direction = True
display(SVG(pt.svg()))
pt.generator.flip_time_direction = False
Set some options for the SVG output before rendering it.
.options.css
¶
CSS are things that affect the css directly like shape, size and overall appearance.
scale_x, scale_y, zoom¶
scale_x
and scale_y
is the distance in pixels between two blocks. Since the figure can be scaled arbitrarily only the relative number matters. Still it is useful to pick a default size in the browser.
zoom
will be applied to the whole image and might be useful for saving and printing.
pt.options.css['scale_x'] = 2
pt.options.css['scale_y'] = 24
pt.options.css['zoom'] = 1.0
width¶
width
applies to the output scaling in a browser. Typical choices are inherit
which will leave the actual number of pixels and result in the same block size independent of the number of snapshots per trajectory. xxx%
which will scale the whole image to fit a certain percentage of the available space. xxxpx
will scale the whole image to fit exactly into the given number of pixels. In general all css conform widths are supported.
pt.options.css['width'] = 'inherit'
Example¶
pt.options.css['width'] = 'inherit' # default
display(SVG(pt.svg()))
pt.options.css['width'] = '50%' # use 50 percent of the screen
display(SVG(pt.svg()))
pt.options.css['width'] = '100%' # use 100 percent of the screen
display(SVG(pt.svg()))
pt.options.css['width'] = 'inherit'
pt.reset_options()
horizontal_gap¶
horizontal_gap
is a bool and if set to true it will make each snapshot be separated by a little gap. If turned off you will get continuous blocks.
pt.options.css['horizontal_gap'] = False
Example¶
# this is explained later in detail. It means that the default
# behaviour on how to plot new frames is as single-frame blocks
pt.options.movers['default']['new'] = 'single'
pt.options.css['horizontal_gap'] = False # default
display(SVG(pt.svg()))
pt.options.css['horizontal_gap'] = 0.1 # use 10 percent gaps
display(SVG(pt.svg()))
pt.options.css['horizontal_gap'] = False
mark_transparent¶
mark_transparent
sets the reason when samples will be shown transparent.
rejected
(default): transparent, if the sample originated in a rejected step. Useful for analyzing stepsauxiliary
: transparent, if the sample is not necessary to construct the last sample andsubmove
: transparent, if the sub_move was rejected. This is different from 1. in that 1. only check acceptance of the root_mover
pt.options.css['mark_transparent'] = 'rejected'
Example¶
pt.reset_options()
pt.generator.accepted = False # tell the generator to also show rejected ones
pt.options.css['mark_transparent'] = 'rejected' # default
display(SVG(pt.svg()))
pt.generator.accepted = True # back to only accepted
.options.ui
¶
UI are things that affect which content will be shown, such as additional legends, hints, virtual samples, etc.
legends¶
correlation
, sample
, ensemble
, replica
, bias
, step
are parts of the legends that can be chosen.
pt.options.ui['legends'] = [ 'correlation', 'sample', 'ensemble', 'step', 'bias', 'replica']
pt.reset_options()
cv¶
The last option sets whether to show the value of a collective variable
pt.options.ui['cv'] = True
pt.reset_options()
.options.movers
¶
The mover category contains information on how to plot certain mover types.
pt.options.movers[mover_type]['name'] = '...'
pt.options.movers[mover_type]['suffix'] = '...'
pt.options.movers[mover_type]['overlap_label'] = '...'
pt.options.movers[mover_type]['label_position'] = 'left' or 'right'
pt.options.movers[mover_type]['overlap'] = '...'
pt.options.movers[mover_type]['new'] = '...'
pt.options.movers[mover_type]['reversed'] = '...'
pt.options.movers[mover_type]['full'] = '...'
pt.options.movers[mover_type]['cls'] = '...'
.op
¶
.op
will reference a function that is used to determined the text inside each snapshot. If None
nothing will be displayed. A common choice if the .idx
function of the used storage. This will use the index of the snapshot stored. If it is set to None
it will be ignored. Note, that if you will join blocks then this will also be ignored since no blocks will be plotted.
Example¶
pt.op = None # default
cvB = storage.volumes['B']
pt.options.css['scale_x'] = 12 # Adjust the width of each snapshot
pt.options.movers['default']['new'] = 'single' # only in single mode you can see ops
pt.op = lambda x: '%d' %(min(100 * cvB(x), 99))
display(SVG(pt.svg()))
pt.op = None
pt.reset_options()
.states
¶
.states
is a dictionary which will reference boolean CVs with a color. If the CV will return True for a snapshots a block of the given color is put below the snapshot. This way certain states can be marked and seen if trajectories are of the correct type.
Example¶
pt.states = {} # default
pt.states = {'black' : storage.volumes[0]} # mark if in volume[0]
pt.options.css['scale_x'] = 12 # Adjust the width of each snapshot
pt.options.movers['default']['new'] = 'single' # only in single mode you can see ops
display(SVG(pt.svg()))
pt.states = {}
pt.reset_options()
Concepts¶
Trajectory Parts¶
The frames/snapshots in each trajectory that is plotted are divided in 4 categories. These categories are mutually exclusive and you can specify the plotting style for each separately. These parts are
1. new¶
all snapshots that are new and have not been plotted before (above). These are usually the frames that originate from forward/backward shooting or extension moves. It can also mean frames that are new because only the reversed snapshot had been plotted. This is only the case if you use time_symmetric = False
otherwise this cannot happen.
overlap¶
the part that is exactly in common with the previous one. This part will be align with the previous trajectory above.
reversed¶
the part that is in common with the previous one if you would reverse it. If you allow time_symmetric = True
then also a reversed (sub-)part will be aligned and this part takes the place of overlap.
full¶
In the only case that a trajectory is completely repeated. This overrides overlap it will not be used for reversed. It is only meant to do something different from overlap in specific cases.
Trajectory Plotting Modes¶
Each of the trajectory parts can be plotted in one of these modes:
1. block¶
All frames of this part will be drawn as only one big block. Non of the single frames can be seen and this disables horizontal_gap
and .op
to write a value and so the coloring function to color single frames. This is most common for the reversal part and sometimes for new.
2. single¶
Each frame will be s separate little block and you can do all additional features that block or line cannot. This is used mostly for new
parts so that each frame has exactly one single small block but not multiple ones.
3. line¶
This is a variant of block, but is only a thin line with the name of the move in the middle. Useful for otherwise hidden parts that you want to show. Used, e.g. for the overlap part in an extension move or the truncation part in a subpart mover.
4. hidden or None
¶
This will not show the block and in this case frames will not count as plotted before and a following repeat of a frame will be treated as new
. Often used to omit the repeated parts in forward or backward shoots.
SampleLists(Generators)¶
Let's start fresh and set some options altogether.
ReplicaEvolution¶
ptb = vis.PathTree(
storage.steps[0:100], # use only the first 100 steps
vis.ReplicaEvolution(1, True)
)
SVG(ptb.svg())
You can always change setting of the generator or replace the generator completely. Changing the steps is possible as well. The necessary analysis is internally triggered as necessary.
ptb.generator.replica = 4
SVG(ptb.svg())
EnsembleEvolution¶
ensemble = storage.samplesets[0].ensembles[2]
ptb.generator = vis.EnsembleEvolution(ensemble, True)
SVG(ptb.svg())
SampleAncestors¶
ptb.steps = storage.steps[0:500] # use some more steps
ptb.generator = vis.SampleAncestors(storage.steps[499].active[1])
SVG(ptb.svg())
Correlation¶
The SampleList
object residing in .generator
can check for decorrelation/correlation in the list that is generates. The meaning depends on what list of samples is analyzed, but looking replicas or ensembles you can count the number of completely independent trajectories, i.e. trajectories that do not have any snapshot in common.
decorrelated = ptb.generator.decorrelated
print "We have " + str(len(decorrelated)) + " decorrelated samples."
An let's look at the steps these were generated
cycles = map(ptb.steps.get_mccycle, decorrelated)
print cycles
print 'Average of %.1f cycles per decorrelated sample' % (1.0 * (cycles[-1] - cycles[0]) / (len(cycles) - 1))
CSS Styling¶
Most attributes of the tree, i.e. colors, line thickness, font properties are set using CSS. There is a file vis.css
that contains the default styling but you can override the base style file to be used and also add custom styles. The default file can be found in the resource directory. Use paths.resources_directory
to find it on your machine. In general we advise to look at a tutorial on CSS and CSS selectors in particular to understand how to select specific parts of the graphics and change its properties.
'[...]' + paths.resources_directory[-44:]
Let's first look how we can alter some simple thing: the color for the reversal moves from yellow to blye.
# make sure we have really default settings
pt.reset_options()
pt.reset_css()
pt.add_css('#self .reversal { fill: blue; stroke: blue;}')
SVG(pt.svg())
There a few things strange here and need explanation.
.reversal
: this is the class identifier for things that have to do with reversal and use its color. Look at thevis.css
to get a notion of what you have to change.fill: blue
:fill
is the property the we want to set. So this affects all graphics directives that utilize thefill
property to determine their color. E.g. lined do not. They use thestroke
property#self
: this is an ID selector and would pick all objects that are located in an object with anid="me"
attribute. Note that this ID will be replaced with a unique identifier the is changed for every figure so that you can make changes for each figure independently.
If you do NOT use #self
at the beginning you MIGHT change properties for ALL figures in your notebook!!! This depends on the selection precedence of CSS. There might exist several rules that could determine the actual color of an object and the question is which one will be use to finally determine the color (or any other attribute). In general the rule which is more specific is used. Say
line { stroke: black; }
is very unspecific and would apply to really all line
objects in all SVG in your browser window.
#self .tree line { stroke-width: 1px }
is very specific and only applies to objects in #self
inside an object of class tree
of type line
and take precedence over the first statement. In most cases you should use #self
Some more examples. Rotate the text of the step numbers by 90 Degrees
pt.reset_css()
pt.add_css('#self .legend-step .label:not(.head) text { transform: rotate(90deg)}')
pt.add_css('#self .legend-step .label.head text { transform: rotate(45deg) }')
SVG(pt.svg())
pt.reset_css()
Extended Examples¶
Some larger examples to show some of the capabilities
Change thickness of connector lines¶
pt.reset_css()
pt.add_css('#self .shooting-hooks line { stroke-width:8.5px; stroke-linecap: round}')
SVG(pt.svg())
pt.reset_css()
Boxcar Style¶
Useful especially for short trajectories. You need to set the plotting style to the trajectory parts to single
to see it and set horizontal_gap = True
pt.states = {}
pt.options.movers['default']['new'] = 'single'
pt.options.movers['default']['reversed'] = 'line'
pt.options.ui['legends'] = []
pt.options.css['horizontal_gap'] = 0.05 # True is the same as 0.05
display(SVG(pt.svg()))
pt.reset_options()
No Legends¶
pt.reset_options()
pt.states = {
'red': storage.volumes['A'],
'green': storage.volumes['B'],
'blue': storage.volumes['C']
}
pt.options.ui['legends'] = [] # no legends
SVG(pt.svg())
Colored Snapshots¶
Let's do something fancy and color each snapshot
# load all 3 collective variables
cvA = storage.cvs['opA']
cvB = storage.cvs['opB']
cvC = storage.cvs['opC']
# and compute their maximum
mxA = max(cvA(storage.snapshots.all()))
mxB = max(cvB(storage.snapshots.all()))
mxC = max(cvC(storage.snapshots.all()))
For this we need a color function that returns a valid SVG color for each snapshot.
svg_colorfunction = lambda x: 'rgb(' + \
str(int(255 * (1 - cvA(x) / mxA))) + ',' + \
str(int(255 * (1 - cvB(x) / mxB))) + ',' + \
str(int(255 * (1 - cvC(x) / mxC))) + ')'
It looks a little complicated, so let's see what it does. And compute the color of snapshot zero.
svg_colorfunction(storage.snapshots[0])
Here we color according to the proximity to states A
(red), B
(green) and C
(blue). You see, that all paths that are not in the right ensemble are rejected!
# set all trajectory parts to single mode to see something
# as an example this will change the default way new parts
# (usually shooting / extention) are displayed. `single`
# means each snapshot as a single block
ptb.options.movers['default']['new'] = 'single'
ptb.options.movers['default']['reversed'] = 'single'
ptb.options.movers['default']['overlap'] = 'none'
ptb.options.movers['default']['full'] = 'single'
# and set the color function we defined before
ptb.coloring = svg_colorfunction
SVG(ptb.svg())
Values on single frame/snapshots¶
You can add a value to each snapshot
# Use the `.idx` function of the storage as values for each snapshot.
ptb.op = lambda x: '%d' %(100 * cvB(x))
# Adjust the width of each snapshot
ptb.options.css['scale_x'] = 10
# Set the width to use native pixels and reflect the actual size of the SVG
ptb.options.css['width'] = '100%'
# Use the default setting and say that all new snapshots should be plotted as single
ptb.options.movers['default']['new'] = 'single'
ptb.options.movers['default']['overlap'] = 'none'
ptb.options.movers['default']['reversed'] = 'line'
ptb.options.movers['default']['full'] = 'line'
# override the default for ForwardShootMoves. These will have no cvs in them
ptb.options.movers['ForwardShootMover']['new'] = 'block'
ptb.coloring = svg_colorfunction
# Allow to flip the time direction to better match reversible moves. BEWARE that the direction
# of time has no meaning here. It is exclusivey to study decorrelation of generated paths.
ptb.generator.flip_time_direction = False
SVG(ptb.svg())
The End¶
storage.close()
Appendix¶
Current vis.css
¶
.opstree text, .movetree text {
alignment-baseline: central;
font-size: 10px;
text-anchor: middle;
font-family: Futura-CondensedMedium;
font-weight: lighter;
stroke: none !important;
}
.opstree .block text, .movetree .block text {
alignment-baseline: central;
font-size: 8px;
text-anchor: middle;
font-family: Futura-CondensedMedium;
font-weight: lighter;
stroke: none !important;
}
.opstree text.shadow {
stroke-width: 3;
stroke: white !important;
}
.opstree .left.label .shift text {
text-anchor: end;
}
.opstree .right.label .shift text {
text-anchor: start;
}
.opstree .block text, .movetree .block text {
fill: white !important;
stroke: none !important;
}
.opstree .block {
stroke: none !important;
}
.opstree g.block:hover rect {
opacity: 0.5;
}
.opstree .repex {
fill: blue;
stroke: blue;
}
.opstree .extend {
fill: blue;
stroke: blue;
}
.opstree .truncate {
fill: blue;
stroke: blue;
}
.opstree .new {
fill: black;
stroke: black;
}
.opstree .unknown {
fill: gray;
stroke: gray;
}
.opstree .hop {
fill: blue;
stroke: blue;
}
.opstree .correlation {
fill: black;
stroke: black;
}
.opstree .shooting.bw {
fill: green;
stroke: green;
}
.opstree .shooting.fw {
fill: red;
stroke: red;
}
.opstree .shooting.overlap {
fill: #666;
stroke: #666;
}
.opstree .reversal {
fill: gold;
stroke: gold;
}
.opstree .virtual {
opacity: 0.1;
fill:gray;
stroke: none;
}
.opstree line {
stroke-width: 2px;
}
.opstree .label {
fill: black !important;
}
.opstree .h-connector {
stroke-width: 0.1px;
stroke-dasharray: 3 3;
}
.opstree .rejected {
opacity: 0.25;
}
.opstree .level {
opacity: 0.25;
}
.opstree .orange {
fill: orange;
}
.tableline {
fill: gray;
opacity: 0.0;
}
.tableline:hover {
opacity: 0.2;
}
.opstree .left.label g.shift {
transform: translateX(-6px);
}
.opstree .right.label g.shift {
transform: translateX(+6px);
}
.opstree .infobox text {
text-anchor: start;
}
.opstree .shade {
stroke: none;
}
.movetree .label .shift {
transform: translateX(-18px);
}
.movetree .label text {
text-anchor: end;
}
.movetree .v-connector {
stroke: black;
}
.movetree .v-hook {
stroke: black;
}
.movetree .h-connector {
stroke: black;
}
.movetree .ensembles .head .shift {
transform: translateY(0px) rotate(270deg) ;
}
.movetree .ensembles .head text {
text-anchor: start;
}
.movetree .connector.input {
fill: green;
}
.movetree .connector.output {
fill: red;
}
.movetree .unknown {
fill: gray;
}
(tutorial_pathtree_visualization.ipynb; tutorial_pathtree_visualization.py)