Skip to content

hanstt/plutt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

209 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

plutt

gauss

hist("x", x)
hist("x, binsx=10", x, binsx=10)
hist("x, logy", x, logy)
hist("x, fit", x, fit="gauss")
hist2d("x vs x", x, x)
hist2d("y vs x", y, x)
hist2d("y vs x, bins=10/50", y, x, binsx=10, binsy=50)
hist2d("y vs x, logz", y, x, logz)

What's the idea?

Say we have an event stream from a detector outfitted with e.g. scintillator paddles or silicon segments that are read out on both ends. What's an easy way to plot this?

Let's say we have another such detector rotated by 90 degrees. How hard can/should it be to draw one vs the other in a Y-vs-X plot?

The ROOT prompt, as powerful as it is, has its limitations, for example when dealing with zero-suppressed or multi-hit arrays.

Here's some human-readable pseudo-code for the first example:

- Get array for left side -> "left"
- Get array for right side -> "right"
- Match channels between "left" and "right" -> "left2" and "right2"
- Plot "right2" vs "left2"

Typical pseudo-C++ & ROOT could be:

// Init.
auto h1 = new TH1F(name, title, bins_x, min_x, max_x, ...);
auto left_handle = Input.GetHandle("MY_DET_LEFT");
auto right_handle = Input.GetHandle("MY_DET_RIGHT");

// For each event.
auto left = Input.GetData(left_handle);
auto right = Input.GetData(right_handle);
size_t l_i = 0;
size_t r_i = 0;
while (l_i < left->size() && r_i < right->size()) {
	auto l_ch = left[l_i]->channel;
	auto r_ch = right[r_i]->channel;
	if (l_ch == r_ch) {
		h1->Fill(l_ch);
	} else if (l_ch < r_ch) ++l_i;
	} else ++r_i;
}

To solve the 2nd example, we copy and adjust. And we copy-pasta all of that for every detector, refactor, add calibration and complicate the logic states, build, debug, ignore warnings, fight with git or copy between accounts or forget the code ever existed... This is a huge time-sink while expensive beam is vehemently showering our experimental setups.

Equally bad, new data may not fit expected histogram limits, and a faulty detector is caught very late. Monitoring should show everything that is going on, even if we don't think we want it.

Plutt tries to implement the human-readable version via scripting:

l = MY_DET_LEFT
r = MY_DET_RIGHT
matched_l, matched_r = match_index(l, r)
hist("My detector hit-pattern", matched_l:id)

Now, don't forget that computers don't have brains! Don't put yours on the shelf just yet, there's a couple of things to go through!

Building

Suggested packages (Debian):

ccache -- Speed up recompiles.
libnlopt-dev -- Fitting.
libsdl2-dev -- Fast graphics.

Other software:

root -- ROOT files input and THttpServer.
ucesb -- UCESB input.

To compile:

make -jN
./plutt

Make builds a version based on the compiler and build mode, e.g. debug or release. The latter is a script that chooses the binary the same way the Makefile chooses the build directory.

The usual environment variables are available to steer the build, e.g. CPPFLAGS, CFLAGS, LDFLAGS, and LIBS.

Running

./plutt -f myconf.plutt -r mytree myfile1.root myfile2.root

Loads script myconf.plutt, and the input will be two root files, both with a TTree named mytree. A default GUI will be chosen depending on what's built in.

./plutt -f myconf.plutt -g sdl -u ../upexps/myunp/myunp --stream=localhost

The input will be structures from the myunp unpacker which reads event-data from a stream server. The GUI will be the SDL2-driven ImPlutt.

./plutt -f myconf.plutt -u ../ucesb/hbook/struct_writer localhost
../upexps/myunp/myunp --stream=some-host --ntuple=RAW,SERVER,dummy

The input will be structures from the unpacker running on the same host.

./plutt -f myconf.plutt -g sdl -g root:8100 -r myfiles...root

Plots with both ImPlutt and THttpServer, the latter on port 8100.

GUI's

There are currently two GUI's available, depending on built-in support:

  1. If SDL2 and FreeType2 are available via pkg-config, the low-latency client-side ImPlutt plotting will be built.

  2. If root-config is available, ROOT file and THttpServer support will be built, depending on the ROOT build.

The default GUI is chosen from top to bottom from the above list.

ImPlutt GUI

  • Scroll-wheel in a plot:

    • Zoom around the pointer in X and Y.
  • Scroll-wheel on an axis:

    • Zoom around the pointer in either X or Y.
  • Click-drag-release pointer in a plot:

    • The selected area will be zoomed in, unless it's smaller than 5x5 pixels which is typically a mis-click.
  • Click-drag-release pointer on an axis:

    • The selected range will be zoomed in, again with a 5 pixel limit.
  • Pressing c:

    • Starts cut creation mode.
    • Click in the graph to add a vertex to the cut polygon.
    • Press Enter to write the cut polygon to file, see the cmd-line for written filename.
    • Press Backspace to remove the last point.
    • Press Escape to abort the cut.
  • Pressing u:

    • Unzoom X and Y completely.
  • Pressing x/y:

    • Starts X/Y projection view, at most one per plot.
    • w in the original plot to increment the projection width.
    • s to decrement.
    • It's possible to zoom the projected plot, but the other features of the main window plots are disabled.
  • Pressing shift+x:

    • Clears the histogram under the pointer, including statistics and data ranges.

ROOT GUI

Opens a THttpServer on port 8080 by default, direct your web-brower to the address localhost:8080. To run on e.g. port 10000, invoke the GUI like -g root:10000 on the command line.

Let's define the data

It is important to understand the internal data structure before operating on them.

In principle, plutt tries to bundle ID's (e.g. channel indices) and values into signals, which have two important parts:

  1. A list of ID's which have data.
  2. A list of values for the ID's.

To be more detailed, a signal looks pretty much like:

struct {
	// List of ID's with data, -1 means "no ID".
	vector<uint32_t>	id;
	// Offsets into the **value** vector for each ID.
	vector<uint32_t>	end;
	// All values over all ID's.
	vector<Scalar>	v;
};

Plutt creates signals based on typical suffixes from ucesb style unpackers, or user input. For example, given "MYDET" and other existing inputs:

Multi-hit zero-suppressed ucesb style:
	MYDETM
	MYDETMI
	MYDETME
	MYDET
	MYDETv
	-> MYDETMI used as ID's,
	   MYDETME used as value offsets,
	   MYDETv used as values
Can be explicitly defined with:
	MYDET = signal(MYDETMI, MYDETME, MYDETv)

Single-hit zero-suppressed ucesb style:
	MYDET
	MYDETI
	MYDETv
	-> MYDETI used as ID's,
	   MYDETv used as values
Can be explicitly defined with:
	MYDET = signal(MYDETI, MYDETv)

egmw-style:
	MYDET = signal(MYDET_id, MYDET_adc_a)

Typical ROOT-style:
	MYDET = signal(MYDET.my_ch, MYDET.my_value)

Scalar ucesb style:
	MYDET
	-> No ID,
	   MYDET used as value

See below for the "official" definition of the signal syntax.

What's the grammar/syntax?

signal[i]
	Creates a new signal from the i:th entry if it exists, and only the
	first value, e.g.:
		signal.id  = [1, 2]             [2]
		signal.end = [1, 2]   -> [1] -> [1]
		signal.v   = [v1, v2]           [v2]
		                                []
		                         [2] -> []
		                                []
signal[i][j]
	Creates a new signal from the i:th entry and the j:th value if it
	exists, e.g.:
		signal.id  = [1, 2]                            [2]
		signal.end = [2, 4]               -> [1][0] -> [1]
		signal.v   = [v11, v12, v21, v22]              [v21]
		                                               []
		                                     [1][2] -> []
		                                               []
signal:id
	Creates a new signal with the ID's as values:
		id = [-1]
		end = [length(signal.id)]
		v = signal.id
signal:v
	Creates a new signal without any ID's:
		id = [-1]
		end = [length(signal.id)]
		v = signal.v
a = signal(id-name, value-name)
	Creates a signal where the ID's are given by one source and the values
	by another. Note that the sources must have the same lengths!
	Example:
		my_id    = [3, 5]        a:id  = [3, 5]
		my_value = [100, 123] => a:end = [1, 2]
		                         a:v   = [100, 123]
a = signal(id-name, end-name, value-name)
	Creates a signal like above, but also with end offsets into the value
	array. Example:
		my_id    = [2, 4]       => a:id  = [3, 4]
		my_end   = [2, 4]          a:end = [2, 4]
		my_value = [1, 2, 3, 4]    a:v   = [1, 2, 3, 4]
a = merge(b[, ...])
	Creates a signal where the given signals are merged in id order.
	Example:
		a = merge(b, c)
		b.id =  [1, 2]
		b.end = [1, 2]      a.id  = [1, 2, 3]
		b.v   = [b1, b2] => a.end = [2, 3, 4]
		c.id =  [1, 3]      a.v   = [b1, c1, b2, c2]
		c.end = [1, 2]
		c.v   = [c1, c2]
y = f(x)
	Chainable mathematical operations are available with signals and
	constants, which acts on each value individually in a scalar or array.
	Currently supported operations:
		(x)
		x+y
		x-y
		x*y
		x/y
		-x
		cos(x)
		sin(x)
		tan(x)
		acos(x)
		asin(x)
		atan(x)
		sqrt(x)
		exp(x)
		log(x)
		abs(x)
		pow(x, const)
		pow(const, x)
cut(path)
	"path" points to a file which has a histogram title and an x/y pair
	per line which makes a cutting polygon:
		My histo
		0, 0
		10, 0
		0, 10
		10, 10
cut(histogram-name, (x,y)...)
	This is the inline version of the previous syntax, so the same example
	would look like:
		cut("My histo", (0,0), (10,0), (0,10), (10,10))
a = fit(b[, ...])
	This is a 'static' linear fit y=Ax+B of 2D points. The list of
	arguments consists of pairs (y, x), and 'a' will refer to this linear
	fit. Example:
		my_calib = fit((1, 1), (2, 2), (4, 3))
	This will fit a linear function through the three points, which can be
	used to scale histogram axes.
hist(title, x [, args])
	Draws a histogram of the values in x:v, with the given title.
	The optional arguments can be:
		logy
			Logarithmic counts.
		binsx=n
			# horizontal bins.
		contoured/filled
			Bin drawing style.
		transformx=name
			Reference to a static fit, see 'fit' above. This will
			scale the X axis, i.e. 'x' will be transformed by the
			linear fit y=Ax+B and the histogram is filled with the
			'y' values.
		fit="method"
			'method' can be any of:
				gauss
		cut(cut-args)
			This histogram processes the current event only if the
			given cut has seen a hit. For more info about the cut
			syntax, see above.
		drop_stats=n time-unit
			Drops old data statistics, used to size histogram
			extents, that is older than about n time-units;
			s or min.
			Note that it doesn't drop histogram counts, it drops
			knowledge of min/max values to auto-fit ranges, and
			parts of the histogram counts can disappear if they go
			outside the auto-fitted range.
		drop_counts(n time-unit [, m # slices])
			Drops old counts. This can get expensive, use with
			care!
			When enabled, the histogram has 'm' slices in time
			(default 3) that are accumulated for presentation.
			Each slice holds counts during the given time, i.e.
			time=10 min and slices=5 means the histogram will keep
			data for 40-50 min.
			Only one slice is active for filling, and after the
			given time the slices are rotated and the oldest one
			is emptied.
		NOTE: Only one of drop_stats or drop_counts can be active,
		because reasoning about the combo seems like a waste of time!
		Also, drop_stats is a lot cheaper.
hist2d(title, y, x [, args])
	Histograms 'y:v' vs 'x:v', pairing up the n:th entry in both vectors,
	until either vector is exhausted.
	The arguments are similar to "hist", except:
		logy
			Does not exist.
		logz
			Logarithmic count coloring.
		binsy=n
			# vertical bins.
		transformy=name
			Transforms the 'y' entries.
		fit
			Does not exist.
		single[=n time-unit]
			Clear the histogram for every new fill, but keep a fill
			for the given time, default=0 s.
annular(title, r, r_limits, phi, phi_ref)
	Draw an annular histogram of the values r:v and phi:v, with the given
	title.
	r_limits = (r_min, r_max)
		This option maps r:v to min_r..max_r.
	phi_ref = [+-]angle
		Maps phi:v to phi angles, where 0 points to the right and
		positive values are counter clockwise.
b = bitfield(a1, n1, ..., aN, nN)
	Combines signals of given bit-widths into one value. The (ai,ni) pairs
	are combined in ascending order, ie the first pair becomes the least
	significant bits. Non-existant values are treated as 0.
		a1:id  = [1, 2]
		a1:end = [1, 2]
		a1:v   = [1, 2]     b:id  = [1, 2, 3]
		n1 = 8           => b:end = [2, 3, 4]
		a2:id  = [1, 3]     b:v   = [0x301, 0x400, 0x002, 0x500]
		a2:end = [2, 3]
		a2:v   = [3, 4, 5]
		n2 = 8
b = cluster(a)
b, c = cluster(a)
	Makes clusters of 'a', ie large neighbouring entries wrt to a:id are
	grouped. 'b:id' = a:id's weighted and floored, 'b:v' = sum, and the
	optional 'c:v' is the eta function, ie the centroid within the cluster.
		a:id  = [1, 2, 3, 5]    b:id  = [2, 5]
		a:end = [1, 2, 3, 4] => b:end = [1, 2]
		a:v   = [4, 5, 6, 7]    b:v   = [15, 7]
		                        c:id  = [2, 5]
		                        c:end = [1, 2]
		                        c:v   = [0, 0.45]
d = coarse_fine(a, b, c)
	Does on-the-fly fine-time calibration. 'a' = coarse times, 'b' = fine
	times, and 'c' is either a module type identifier or a number for the
	clock period. Identifiers could be 'vftx2', 'tamex3' etc, and c=1e-8
	would be interpreted as each clock cycle taking 10 ns.
	This example relies on previous data for the calibration, let's assume
	a uniform fine-time distribution between 100 and 600 ch:
		a:id  = [1, 2]
		a:end = [1, 2]
		a:v   = [10, 20]      d:id  = [1, 2]
		b:id  = [1, 2]     => d:end = [1, 2]
		b:end = [1, 2]        d:v   = [5001, 10003]
		b:v   = [200, 400]
		c = 'vftx2'
a = cut(cut-args)
a, b = cut(cut-args)
	Extracts entries in a hist/hist2d that pass the cut. 'a' will contain
	the x values and 'b' the y values. For more info about the cut syntax,
	see above.
filter_range(a <= b < c..., (d = e)...)
	For each entry where the conditions in the initial list of conditions
	are met, all the assignments are performed. Note that 'b' and 'e' must
	have the exact same layout, ie b:id  === e:id and b:end === e:end!
		a = 1
		b:id  = [1, 3]
		b:end = [1, 3]       d:id  = [1, 3]
		b:v   = [1, 2, 3] => d:end = [1, 2]
		c = 3                d:v   = [4, 5]
		e:id  = [1, 3]
		e:end = [1, 3]
		e:v   = [4, 5, 6]
b = length(a)
	Returns length of 'a:v':
		a:id  = [1, 2, 3]       b:id  = [0]
		a:end = [1, 2, 4]    => b:end = [1]
		a:v   = [1, 2, 3, 4]    b:v   = [4]
c, d = match_index(a, b)
	Cuts un-matched entries between 'a:id' and 'b:id':
		a:id  = [1, 2, 3]    c:id  = [2]
		a:end = [1, 2, 3]    c:end = [1]
		a:v   = [1, 2, 3] => c:v   = [2]
		b:id  = [2, 4]       d:id  = [2]
		b:end = [1, 2]       d:end = [1]
		b:v   = [4, 5]       d:v   = [4]
c, d = match_value(a, b, c)
	Cuts un-matched entries between 'a:v' and 'b:v' for each matched
	channel. A match means a:id[mi]==b:id[mj] && fabs(a:v[vi]-b:v[vj])<c.
		a:id  = [1, 2, 3]       c:id  = [2]
		a:end = [1, 2, 3]       c:end = [1]
		a:v   = [10, 20, 30] => c:v   = [20]
		b:id  = [2, 3]          d:id  = [2]
		b:end = [1, 2]          d:end = [1]
		b:v   = [21, 32]        d:v   = [21]
		c = 2
b = max(a)
	Returns the single largest entry in 'a:v':
		a:id  = [1, 2, 3]    b:id  = [2]
		a:end = [1, 2, 3] => b:end = [1]
		a:v   = [1, 3, 2]    b:v   = [3]
b = mean_arith(a)
	Calcs arithmetic mean over all 'a:id' for each entry in 'a:v':
		a:id  = [1, 2, 3]          b:id  = [0]
		a:end = [2, 4, 5]       => b:end = [2]
		a:v   = [1, 2, 3, 4, 5]    b:v   = [(1+3+5)/3, (2+4)/2]
c = mean_arith(a, b)
	Calcs aritmetic mean between the two signals for every '*:id' and
	'*:v':
		a:id  = [1, 2]
		a:end = [1, 3]       c:id  = [1, 2, 3]
		a:v   = [1, 2, 3] => c:end = [1, 2, 4]
		b:id  = [2, 3]       c:v   = [1/1, (2+4)/2, 3/1, 5/1]
		b:end = [1, 2]
		b:v   = [4, 5]
b = mean_geom(a)
c = mean_geom(a, b)
	Same as mean_arith except does (v_1 + ... + v_n)^(1/n).
c, d = pedestal(a, b [, args])
	Calculates pedestals of 'a' on-the-fly and cuts "a:v < std * b".
	'args' can be:
		tpat=a
		If 'a:v[0]' is non-zero, the pedestal statistics are updated,
		otherwise they are only applied.
	'c' will contain the cut values, and 'd' the standard deviations.
c = select_index(a, b)
	Selects only 'a:id  == b':
		a:id  = [1, 2]          c:id  = [0]
		a:end = [2, 4]       => c:end = [2]
		a:v   = [1, 2, 3, 4]    c:v   = [3, 4]
		b = 2
d = sub_mod(a, b, c)
	Does a zero-centered modulo-subtraction like '(a-b+n*c+c/2)%c-c/2':
		a:id  = [1, 2]
		a:end = [1, 2]
		a:v   = [1, 2]    d:id  = [1, 2, 3]
		b:id  = [2, 3] => d:end = [1, 2, 3]
		b:end = [1, 2]    d:v   = [1/1, (2+3)/2, 4/1]
		b:v   = [3, 4]
		c = 10
d = tot(a, b, c)
	Does time-over-threshold, 'a' = leading, 'b' = trailing, does matching
	of channels and edges.
b = tpat(a, bitmask)
	Lets values in 'a:v' with any bits marked by the bitmask pass.
		a:id  = [0]         b:id  = [0]
		a:end = [1]      => b:end = [1]
		a:v   = [0x101]     b:v   = [0x1]
		bitmask = 0x83
f = trig_map(a, b, c, d, e)
	Parses file 'a', considers lines prefixed with 'b', then does cyclical
	subtraction of trigger channels 'd', mapped by 'a', from signal
	channels 'c' with range 'e':
		a:
			Det1:1 = 1
			Det1:2 = 1
			Det1:3 = 2
			Det2:1 = 1
		b = "Det1"
		c:id  = [1, 2, 3]
		c:end = [1, 2, 3]    f:id  = [1, 2, 3]
		c:v   = [1, 2, 3] => f:end = [1, 2, 3]
		d:id  = [1, 2]       f:v   = [1-1, 2-1, 3-3]
		d:end = [1, 2]
		d:v   = [1, 3]
		e = 10
d = zero_suppress(a [, b])
	Zero-suppresses 'a:v'. 'b' is an optional cut-off which defaults to 0,
	ie all entries in 'a:v' > 'b' are pass this node:
		a:id  = [1, 2]       d:id  = [2]
		a:end = [1, 3]    => d:end = [1]
		a:v   = [0, 2, 1]    d:v   = [2]
		b = 1

Other functions:

appearance("name")
	Sets GUI style, choose one of: light, dark. The last one in the config
	will override previous invocations.
clock_match(a, b)
	Matches the computer running time to the given signal 'a', which
	should be a monotonically increasing timestamp.
	'b' is the time in seconds for the unit of 'a', eg if 'a' is in ns,
	then b=1e-9.
colormap("name")
	plutt only has "roma" built-in, which can be chosen by leaving out the
	argument, but you can use Scientific Colourmaps if that resides under
	your current working dir.
	If 'name' is eg "lajolla", the file:
		"../ScientificColourMaps7/lajolla/lajolla.lut"
	is loaded.
page("name")
	Starts a new named page for plots. If the user did not create one
	before the first plot, a default named "Default" is made.
ui_rate(a)
	Throttles the ImPlutt UI update rate to 'a' times per second, default
	and maximum is 20.

Examples

  • Plot scalar spectrum:
	TH1I hist...
	hist.Fill(SCALAR);
	-> hist("This be my scalar", SCALAR)
  • Plot channel multiplicity:
	TH1I hist...
	hist.Fill(MULT);
	-> hist("Sup?", MULT)
  • Plot fired channels in log scale:
	TH1I hist...
	canvas->cd(n)->SetLogy();
	for (i = 0; i < SUPP; ++i)
		hist.Fill(SUPPI[i]);
	-> hist("Channel map", SUPP:id, logy)
  • Plot energy against channel with 1000 energy bins:
	TH2I hist...
	for (i = 0; i < SUPP; ++i)
		hist.Fill(SUPPI[i], SUPPv[i]);
	-> hist2d("Energy vs ch", SUPP:v, SUPP:id, binsy=1000)
	   hist2d("Energy vs ch", SUPP, binsy=1000)
  • Plot multi-hit energies against channels:
	i = 0;
	for (j = 0; j < SUPPM; ++j) {
		ch = SUPPMI[j];
		for (; i < SUPPME[j]; ++i)
			hist->Fill(ch, SUPPv[i]);
	}
	-> hist2d("Energy vs ch", MHIT)
  • Match two sides of a single-hit detector and draw geom mean energy:
	// Too much brain-wrangling code...
	-> e1, e2 = match_index(SUPP_S1E, SUPP_S2E)
	   e = mean_geom(e1, e2)
	   hist2d("Energy vs ch", e)
  • Same with multi-hit mapping:
	// Even more unbearable code :o
	-> e1, e2 = match_index(MHIT_S1E, MHIT_S2E)
	   e = mean_geom(e1, e2)
	   hist2d("Energy vs ch", e)
  • X-vs-X of two detectors:
	-> e11, e12 = match_index(DET1_S1E, DET1_S2E)
	   e1 = max(mean_geom(e11, e12))
	   e21, e22 = match_index(DET2_S1E, DET2_S2E)
	   e2 = max(mean_geom(e21, e22))
	   hist2d("x2 vs x1" e2:id, e1:id)
  • ToF between two detectors:
	-> a1, a2 = match_index(START_S1T, START_S2T)
	   a = mean_arith(a1:v, a2:v)
	   b1, b2 = match_index(STOP_S1T, STOP_S2T)
	   b = mean_arith(b1:v, b2:v)
	   hist("ToF", sub_mod(b, a, 1000), logy)

Note that custom mappings can replace the value array v with anything. This program allows a few other suffixes to be used instead, e.g. E.

Licence

This program has been licenced under the LGPL 2.1, which is included in the file COPYING.

About

A scriptable monitor for experimental data.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •