flowtracks 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flowtracks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __version__ = '1.1.0'
flowtracks/an_scene.py ADDED
@@ -0,0 +1,188 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import tables, itertools as it, numpy as np
4
+ from .scene import read_dual_scene, gen_query_string
5
+ from .trajectory import Trajectory
6
+
7
+ class AnalysedScene(object):
8
+ """
9
+ A class for accessing data and analyses of a scene analysed and saved in
10
+ the format used by flowtracks.analysis.analyse().
11
+ """
12
+
13
+ def __init__(self, analysis_file):
14
+ """
15
+ Initializes the objects according to config and data-source metadata
16
+ saved in the analysis file.
17
+
18
+ Arguments:
19
+ analysis_file - path to the HDF file containing analysis results.
20
+ """
21
+ self._file = tables.open_file(analysis_file, "r")
22
+ self._table = self._file.get_node('/analysis')
23
+
24
+ config = self._table.attrs['config']
25
+ self._scene = read_dual_scene(config)
26
+
27
+ # Cache data on user-visible columsn:
28
+ filt = ('trajid', 'time')
29
+ self._keys = []
30
+ self._shapes = []
31
+ desc = self._table.coldescrs
32
+ for name in self._table.colnames:
33
+ if name in filt:
34
+ continue
35
+ self._keys.append(name)
36
+ shape = desc[name].shape
37
+ self._shapes.append(1 if len(shape) == 0 else shape[0])
38
+
39
+ def __del__(self):
40
+ self._file.close()
41
+
42
+ def keys(self):
43
+ """
44
+ Return names that may be used to access data in any of the data sources
45
+ available, whether analyses or inertial particles.
46
+ """
47
+ return list(self._scene.get_particles().keys()) + self._keys
48
+
49
+ def shapes(self):
50
+ """
51
+ Return the number of components per item of each key in the order
52
+ returned by :meth:`keys`.
53
+ """
54
+ return self._scene.get_particles().shapes() + self._shapes
55
+
56
+ def _iter_frame_arrays(self, cond=None):
57
+ """
58
+ Private. Breaks the file down to its frames, and makes arrays of
59
+ them, iteratively. Also allows filtering the frames.
60
+
61
+ Arguments:
62
+ cond - an optional PyTables condition string to apply to each frame.
63
+ """
64
+ query_string = '(time == t)'
65
+ if cond is not None:
66
+ query_string = '&'.join([query_string, cond])
67
+
68
+ for t in range(*self._scene.get_range()):
69
+ yield t, self._table.read_where(query_string)
70
+
71
+ def collect(self, keys, where=None):
72
+ """
73
+ Get values of a given key, either some of them or the ones
74
+ corresponding to a selection given by 'where'
75
+
76
+ Arguments:
77
+ keys - a list of keys to take from the data
78
+ where - a dictionary of derived-results keys, with a tuple
79
+ (min,max,invert) as values. If ``invert`` is false, the search
80
+ range is between min and max. Otherwise it is anywhere except that.
81
+
82
+ Returns:
83
+ a list of arrays, in the order of ``keys``.
84
+ """
85
+ # Divide the where condition into the trajectory conditions and
86
+ # analysis conditions.
87
+ part_cond = None
88
+ an_cond = None
89
+
90
+ pkeys = set(self._scene.get_particles().keys())
91
+ if where is not None:
92
+ pc_add = []
93
+ an_cond_add = []
94
+
95
+ for key, rng in where.items():
96
+ cond_string = gen_query_string(key, rng)
97
+ if key in pkeys:
98
+ pc_add.append(cond_string)
99
+ else:
100
+ an_cond_add.append(cond_string)
101
+
102
+ if len(pc_add) != 0:
103
+ part_cond = ' & '.join(pc_add)
104
+ if len(an_cond_add) != 0:
105
+ an_cond = ' & '.join(an_cond_add)
106
+
107
+ # Iterate over dual frame, each from the right source using the
108
+ # divided conditions.
109
+ res = dict((k, []) for k in keys)
110
+
111
+ dframe_it = it.izip(
112
+ self._scene.get_particles()._iter_frame_arrays(part_cond),
113
+ self._iter_frame_arrays(an_cond))
114
+
115
+ for tr_frm, an_frm in dframe_it:
116
+ # Cross reference matching rows.
117
+ tp, p_arr = tr_frm
118
+ ta, a_arr = an_frm
119
+
120
+ p_trids = p_arr['trajid']
121
+ a_trids = a_arr['trajid']
122
+ trajids = set(p_trids) & set(a_trids)
123
+
124
+ # select only those from the two frames:
125
+ in_p = np.array([True if tr in trajids else False \
126
+ for tr in p_trids])
127
+ in_a = np.array([True if tr in trajids else False \
128
+ for tr in a_trids])
129
+
130
+ if len(in_p) > 0:
131
+ p_arr = p_arr[in_p]
132
+ if len(a_arr) > 0:
133
+ a_arr = a_arr[in_a]
134
+
135
+ # For each dual frame, add the columns of specified keys to the list
136
+ # of that key's results
137
+ for k in keys:
138
+ if k in pkeys:
139
+ res[k].append(p_arr[k])
140
+ else:
141
+ res[k].append(a_arr[k])
142
+
143
+ # stack and return.
144
+ return [np.concatenate(res[k], axis=0) for k in keys]
145
+
146
+ def trajectory_by_id(self, trid):
147
+ """
148
+ Retrieves an inertial trajectory with the respective analysis.
149
+ See ``iter_trajectories`` for the full documentation.
150
+
151
+ Arguments:
152
+ trid - trajectory ID to fetch.
153
+
154
+ Returns:
155
+ a Trajectory object with analysis keys added.
156
+ """
157
+ traj = self._scene.get_particles().trajectory_by_id(trid)
158
+
159
+ # Trim last point of trajectory to match analysis:
160
+ kwds = dict((k, v[:-1]) for k, v in traj.as_dict().items())
161
+ kwds['trajid'] = trid
162
+
163
+ # Fetch by foreign key from analysis:
164
+ query_string = '(trajid == trid)'
165
+ arr = self._table.read_where(query_string)
166
+
167
+ # Update into the Trajectory object:
168
+ for field in arr.dtype.fields:
169
+ if field not in ['trajid', 'time']:
170
+ kwds[field] = arr[field]
171
+
172
+ return Trajectory(**kwds)
173
+
174
+ def iter_trajectories(self):
175
+ """
176
+ Iterator over inertial trajectories. Since the analysis is structured
177
+ around the inertial particles of the internal DualScene, it is possible
178
+ to iterate those trajectories, adding the corresponding fields of
179
+ analysis to the same object. Generates a Trajectory object for each
180
+ inertial particle trajectory in the particles file (in no particular
181
+ order, but the same order every time on the same PyTables version) and
182
+ yields it.
183
+
184
+ Note: since analysis works on segments, it truncates the last point of
185
+ each trajectory.
186
+ """
187
+ for trid in self._scene.get_particles().trajectory_ids():
188
+ yield self.trajectory_by_id(trid)
flowtracks/analysis.py ADDED
@@ -0,0 +1,146 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Created on Mon Aug 11 15:14:21 2014
3
+ """
4
+ Infrastructure for running a frame-by-frame analysis on a DualScene object.
5
+ The main point of interest here is :func:`analysis`, which performs a segment
6
+ iteration over a :class:`~flowtracks.scene.DualScene` and applies to each
7
+ a user-selected list of analyzers. Analysers are instances of a
8
+ :class:`GeneralAnalyser` subclass which implements the necessary methods,
9
+ as described in the base class documentation.
10
+
11
+ There is one base class supplied here, :class:`FluidVelocitiesAnalyser`,
12
+ which ties in the :mod:`flowtracks.interpolation` module for analysing the
13
+ fluid velocity around a particle from its surrounding tracers.
14
+ """
15
+
16
+ import numpy as np, tables
17
+
18
+ def companion_indices(trids, companions):
19
+ """
20
+ Return an array giving for each companion its respective index in the
21
+ trajectory ID array, or a negative number if not found.
22
+ """
23
+ comp = np.full_like(companions, -1)
24
+ idx = np.nonzero(trids[:,None] == companions)
25
+ comp[idx[1]] = idx[0]
26
+ return comp
27
+
28
+ class GeneralAnalyser(object):
29
+ """
30
+ This is the parent class for all analysers to be used by :func:`analysis`.
31
+ It does not do anything but define and document the methods that must be
32
+ implenmented by the child class (in other words, this class is abstract).
33
+ Attempting to use its methods will result in a ``NotImplementedError``.
34
+ """
35
+ def descr(self):
36
+ """
37
+ Need to return a list of tuples, each of the form
38
+ (name, data type, row length), e.g. ('trajid', int, 1)
39
+ """
40
+ raise NotImplementedError
41
+
42
+ def analyse(self, frame, next_frame):
43
+ """
44
+ Arguments:
45
+ frame, next_frame - the Frame object for the currently-analysed frame
46
+ and the one after it, respectively.
47
+
48
+ Returns:
49
+ a list of arrays, each of shape (f,d) where f is the number of
50
+ particles in the current frame, and d is the row length of the
51
+ corresponding item returned by self.descr(). Each array's dtype also
52
+ corresponds to the dtype given to it by self.descr().
53
+ """
54
+ raise NotImplementedError
55
+
56
+ class FluidVelocitiesAnalyser(GeneralAnalyser):
57
+ """
58
+ Finds, for each particle in the ``particles`` set of a frame, the
59
+ so-called *undisturbed* fluid velocity at the particle's position, by
60
+ interpolating from nearby particles in the ``tracers`` set.
61
+ """
62
+ def __init__(self, interp):
63
+ """
64
+ Arguments:
65
+ interp - the Interpolant object to use for finding velocities.
66
+ """
67
+ self._interp = interp
68
+
69
+ def descr(self):
70
+ """
71
+ Return a list of two tuples, each of the form
72
+ (name, data type, row length), describing the arrays returned by
73
+ analyse() for fluid velocity and relative velocity.
74
+ """
75
+ return [('fluid_vel', float, 3), ('rel_vel', float, 3)]
76
+
77
+ def analyse(self, frame, next_frame):
78
+ """
79
+ Arguments:
80
+ frame, next_frame - the Frame object for the currently-analysed frame
81
+ and the one after it, respectively.
82
+
83
+ Returns:
84
+ a list of two arrays, each of shape (f,3) where f is the number of
85
+ particles in the current frame. 1st array - fluid velocity. 2nd array
86
+ - relative velocity.
87
+ """
88
+ if frame.particles.has_property('companion'):
89
+ comp = companion_indices(
90
+ frame.tracers.trajid(), frame.particles.companion())
91
+ else:
92
+ comp = None
93
+ self._interp.set_scene(frame.tracers.pos(), frame.particles.pos(),
94
+ frame.tracers.velocity(), comp)
95
+ vel_interp = self._interp.interpolate()
96
+ rel_vel = frame.particles.velocity() - vel_interp
97
+
98
+ return [vel_interp, rel_vel]
99
+
100
+ def analysis(scene, analysis_file, conf_file, analysers, frame_range=-1):
101
+ """
102
+ Generate the analysis table for a given scene with separate data for
103
+ inertial particles and tracers.
104
+
105
+ Arguments:
106
+ scene - a DualScene object representing an experiment with coordinated
107
+ particles and tracers data streams.
108
+ analysis_file - path to the file where analysis should be saved. If the
109
+ file exists, it will be cloberred.
110
+ conf_file - name of config file used for creating the analysis.
111
+ analysers - a list of GeneralAnalyser subclasses that do the actual
112
+ analysis work and know all that is needed about output shape.
113
+ frame_range - if -1 no adjustment is necessary, otherwise see
114
+ :meth:`DualScene.iter_segments() <flowtracks.scene.DualScene.iter_segments>`
115
+ """
116
+ # Structure the output file:
117
+ descr = [('trajid', int, 1), ('time', int, 1)]
118
+ for analyser in analysers:
119
+ descr.extend(analyser.descr())
120
+ descr = np.dtype(descr)
121
+
122
+ outfile = tables.open_file(analysis_file, "w", title="Analysis results.")
123
+ table = outfile.create_table('/', 'analysis', descr)
124
+ table.attrs['config'] = conf_file
125
+ table.attrs['trajects'] = scene.get_particles_path()
126
+
127
+ for frame, next_frame in scene.iter_segments(frame_range):
128
+ length = len(frame.particles)
129
+ arr = np.empty(length, dtype=descr)
130
+ arr['trajid'] = frame.particles.trajid()
131
+ arr['time'] = frame.particles.time()
132
+
133
+ for analyser in analysers:
134
+ analysis = analyser.analyse(frame, next_frame)
135
+ this_descr = analyser.descr()
136
+
137
+ for res, desc in zip(analysis, this_descr):
138
+ arr[desc[0]] = res
139
+
140
+ table.append(arr)
141
+
142
+ # Wrap up and close.
143
+ table.cols.trajid.create_index()
144
+ table.cols.time.create_index()
145
+ outfile.flush()
146
+ outfile.close()
@@ -0,0 +1,64 @@
1
+ # A Sphinx extension that, in collaboration with the autodoc extension,
2
+ # parses the docstring format used in this project.
3
+
4
+ import re
5
+
6
+ # Section processing regexes:
7
+ end_section = re.compile(r'^\s*$')
8
+ section_title = re.compile(r'^\s*(\w+):')
9
+ def_line = re.compile(r'(\w+(?:,\s*\w+)*)\s+-\s+(.+)')
10
+
11
+ def setup(app):
12
+ app.connect('autodoc-process-docstring', parse_docstring)
13
+
14
+ def parse_docstring(app, what, name, obj, options, lines):
15
+ """
16
+ Divides the docstring into sections, separated by an empty line in the
17
+ docstring. If a section starts with a line like "$some_title:", then it is
18
+ turned into a titled section with $some_title as its title.
19
+
20
+ A line starting with a python identifier, then a dash, then a definition,
21
+ is turned into a definition-list item.
22
+ """
23
+ out_lines = []
24
+ place = 0
25
+ section = []
26
+ bullet = False
27
+ for place in range(len(lines)):
28
+ line = lines[place]
29
+
30
+ # End section: flush into out_lines.
31
+ m = end_section.match(line)
32
+ if m is not None:
33
+ out_lines.extend(section + [line])
34
+ section = []
35
+ bullet = False
36
+ continue
37
+
38
+ if len(section) == 0:
39
+ # Start of a title-section:
40
+ m = section_title.match(line)
41
+ if m is not None:
42
+ section.extend(['*' + m.group(1) + '*', ''])
43
+ continue
44
+
45
+ m = def_line.match(line)
46
+ if m is not None:
47
+ out_lines.extend(section + [''])
48
+ section = []
49
+ bullet = True
50
+ section.extend(['', '* ``' + m.group(1) + '``: ' + m.group(2)])
51
+ continue
52
+
53
+ line = line.strip()
54
+ if bullet:
55
+ line = ' ' + line
56
+ section.append(line)
57
+
58
+ # Flush last section:
59
+ out_lines.extend(section)
60
+ section=[]
61
+
62
+ # In-place modification of `lines` is needed:
63
+ lines[:] = out_lines
64
+
flowtracks/graphics.py ADDED
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Created on Sun Sep 22 16:11:34 2013
3
+
4
+ """
5
+ Various specialized graphing routines. The Probability Density Function
6
+ graphing is best accessed by calling :func:`pdf_graph` on the raw data, but
7
+ you can generate the PDF from the data separately (e.g. using
8
+ :func:`pdf_bins`) and calling :func:`generalized_histogram_disp` on the
9
+ result.
10
+
11
+ The other facility here is a function to plot a time-dependent 3D vector as
12
+ 3 component subplots, which is another customary presentation in fluid
13
+ dynamics circles. See :func:`plot_vectors`.
14
+ """
15
+
16
+ import numpy as np, matplotlib.pyplot as pl
17
+
18
+ def pdf_bins(data, num_bins, log_bins=False):
19
+ """
20
+ Generate a PDF of the given data possibly with logarithmic bins, ready for
21
+ using in a histogram plot.
22
+
23
+ Arguments:
24
+ data - the samples to histogram.
25
+ bins - the number of bins in the histogram.
26
+ log_bins - if True, the bin edges are equally spaced on the log scale,
27
+ otherwise they are linearly spaced (a normal histogram). If True,
28
+ ``data`` should not contain zeros.
29
+
30
+ Returns:
31
+ hist - num_bins-lenght array of density values for each bin.
32
+ bin_edges - array of size num_bins + 1 with the edges of the bins including
33
+ the ending limit of the bins.
34
+ """
35
+ if log_bins:
36
+ data = data[data > 0]
37
+ minv = np.min(data)
38
+ bins = np.logspace(np.log10(minv), np.log10(data.max()), num_bins + 1)
39
+ else:
40
+ bins = num_bins
41
+
42
+ hist, bin_edges = np.histogram(data, bins=bins, density=True)
43
+ return hist, bin_edges
44
+
45
+ def generalized_histogram_disp(hist, bin_edges, log_bins=False,
46
+ log_density=False, marker='o'):
47
+ """
48
+ Draws a given histogram according to the visual custom of the fluid
49
+ dynamics community.
50
+
51
+ Arguments:
52
+ hist - an array containing the number of values (or density) for each bin.
53
+ bin_edges - the start value of each bin, same length as ``hist``.
54
+ log_bins - indicates that the bin edges are log-spaced.
55
+ log_densify - Show the log of the probability density value. May cause
56
+ problems if ``log_bins`` is True.
57
+ marker - marker style for matplotlib.
58
+
59
+ Returns:
60
+ the list of lines drawn, Matplotlib objects.
61
+ """
62
+ if log_bins:
63
+ plt = pl.loglog if log_density else pl.semilogx
64
+ else:
65
+ plt = pl.semilogy if log_density else pl.plot
66
+
67
+ lines = plt(bin_edges, hist, marker)
68
+ pl.ylabel("Probability density [-]")
69
+
70
+ return lines
71
+
72
+ def pdf_graph(data, num_bins, log=False, log_density=False, marker='o'):
73
+ """
74
+ Draw a PDF of the given data, according to the visual custom of
75
+ the fluid dynamics community, and possibly with logarithmic bins.
76
+
77
+ Arguments:
78
+ data - the samples to histogram.
79
+ bins - the number of bins in the histogram.
80
+ log - if True, the bin edges are equally spaced on the log scale, otherwise
81
+ they are linearly spaced (a normal histogram). If True, ``data`` should
82
+ not contain zeros.
83
+ log_density - Show the log of the probability density value. Only if log
84
+ is False.
85
+ marker - override the circle marker with any string acceptable to
86
+ matplotlib.
87
+ """
88
+ hist, bin_edges = pdf_bins(data, num_bins, log)
89
+ generalized_histogram_disp(hist, bin_edges[:-1], log, log_density,
90
+ marker='-' + marker)
91
+
92
+ def plot_vectors(vecs, indep, xlabel, fig=None, marker='-',
93
+ ytick_dens=None, yticks_format=None, unit_str="", common_scale=None,
94
+ arrows=None, arrow_color=None):
95
+ """
96
+ Plot 3D vectors as 3 subplots sharing the same independent axis.
97
+
98
+ Arguments:
99
+ vecs - an (n,3) array, with n vectors to plot against the independent
100
+ variable.
101
+ indep - the corresponding n values of the independent variable.
102
+ xlabel - label for the independent axis.
103
+ fig - an optional figure object to use. If None, one will be created.
104
+ ytick_dens - if not None, place this many yticks on each subplot, instead
105
+ of the automatic tick marks.
106
+ yticks_format - a pyplot formatter object.
107
+ unit_str - a string to add to the Y labels representing the vector's units.
108
+ arrows - an (n,3) array of values to represent as vertical arrows attached
109
+ to each trajectory point.
110
+ arrow_color - a matplotlib color spec for the arrow bodies.
111
+
112
+ Returns:
113
+ fig - the figure object used for plotting.
114
+ """
115
+ fig = pl.figure(None if fig is None else fig.number)
116
+ u = np.zeros(vecs.shape[0])
117
+
118
+ labels = ("X " + unit_str, "Y" + unit_str, "Z" + unit_str)
119
+ for subplt in range(3):
120
+ pl.subplot(3,1,subplt + 1)
121
+ pl.plot(indep, vecs[:,subplt], marker)
122
+ pl.gca().get_xaxis().set_visible(False)
123
+ pl.grid()
124
+ pl.ylabel(labels[subplt])
125
+
126
+ if yticks_format is not None:
127
+ pl.gca().get_yaxis().set_major_formatter(yticks_format)
128
+
129
+ if common_scale is not None:
130
+ pl.ylim(np.r_[-common_scale, common_scale] + vecs[:,subplt].mean())
131
+ if ytick_dens is not None:
132
+ loc, _ = pl.yticks()
133
+ pl.yticks(np.linspace(vecs[:,subplt].min(), vecs[:,subplt].max(),
134
+ ytick_dens))
135
+
136
+ if arrows is not None:
137
+ pl.quiver(indep, vecs[:,subplt], u, arrows[:,subplt],
138
+ scale=30, width=1e-3, color=arrow_color)
139
+
140
+ pl.gca().get_xaxis().set_visible(True)
141
+ pl.xlabel(xlabel)
142
+ return fig