Coverage for emd/_cycles_support.py: 65%
148 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 10:07 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 10:07 +0000
1#!/usr/bin/python
3# vim: set expandtab ts=4 sw=4:
5"""Low-level functions for handling single cycle analyses."""
7import numpy as np
9# --------------------------------------
10#
11# This module contains functions to support analysis on individual and groups
12# of cycles. Indexing between samples, cycles and chains/sequences of cycles
13# quickly gets complicated. The functions here handle this mapping, implement
14# some options for projecting information between levels and compute metrics on
15# specific levels
16#
17# We have four levels of information to work with:
18#
19# 1 - samples A time-series containing oscillations [ time x 1 ]
20# 2 - cycles Every conceivable cycle within a time series [ cycles x 1 ]
21# 3 - subset A user-defined masked subset of all cycles [ subset x 1 ]
22# 4 - chains A sequency of continuously occurring cycles within the subset [ chains x 1 ]
23#
24# The 'subset' level is worth clarifying - whilst all cycles contains every
25# single possible cycle even if distorted or incomplete, the subset is any
26# restricted set of cycles which are going to be analysed further. This choice
27# of subset is user-defined and may be as simple, complex or arbitrary as
28# necessary. All we need here is the indices of cycles to be retained.
29#
30# Information in each level is stored in vectors of the lengths defined above.
31# We can move this information between levels if we have a set of mapping
32# vectors.
33#
34# 1 - samples <-> cycles Which cycle does each sample belong to [ time x 1 ]
35# 2 - cycles <-> subset Which subset-cycle does each cycle correspond to (-1 for None) [ cycles x 1 ]
36# 3 - subset <-> chains Which chain does each subset cycle belong to [ subset x 1 ]
37#
38# There are two possible definitions of a 'cycle' - a standard form and an
39# augmented form. These are illustrated below.#
40#
41# standard: abcd
42# augmented: 12345
43# /\ /\
44# \/ \/ \/
45#
46# The standard cycle contains a single period of an oscillation spanning the
47# full 2pi range of phase. This can be split into four quadrants around its
48# control points illustrated by abcd in the schematic above.
49#
50# The augmented cycle includes an additional 5th segment which overlaps with
51# the previous cycle - this is illustrated by the 12345 above.
52#
53# Metrics can be computed from either definition and stored in a
54# cycles/subset/chain vector in the same way.
57def make_slice_cache(cycle_vect):
58 """Create a list of slice objects from a cycle_vect."""
59 starts = np.where(np.diff(cycle_vect, axis=0) == 1)[0] + 1
60 stops = starts
62 starts = np.r_[0, starts]
63 stops = np.r_[stops, len(cycle_vect)]
65 slice_cache = [slice(starts[ii], stops[ii]) for ii in range(len(starts))]
67 return slice_cache
70def _slice_len(sli):
71 """Find the length of array returned by a slice."""
72 return sli.stop - sli.start + 1
75def augment_slice(s, phase):
76 """Augment a slice to include the closest trough to the left."""
77 xx = np.where(np.flipud(phase[:s.start]) < 1.5*np.pi)[0]
78 if len(xx) == 0:
79 return None
80 start_diff = xx[0]
81 s2 = slice(s.start - start_diff, s.stop)
82 return s2
85def make_aug_slice_cache(slice_cache, phase, func=augment_slice):
86 """Build a slice cache of augmented slices defined by some function."""
87 return [func(s, phase) for s in slice_cache]
90# --------------------------------------
93def get_slice_stat_from_samples(vals, slices, func=np.mean):
94 """Compute a stat from each slice in a list."""
95 if isinstance(vals, tuple):
96 out = np.zeros((len(slices), ))
97 for idx, s in enumerate(slices):
98 args = [v[s] for v in vals]
99 out[idx] = func(*args)
100 return out
101 else:
102 return np.array([func(vals[s]) if s is not None else np.nan for s in slices])
105def get_cycle_stat_from_samples(vals, cycle_vect, func=np.mean):
106 """Compute a metric across all samples from each cycle."""
107 ncycles = np.max(cycle_vect) + 1
108 out = np.zeros((ncycles,))
110 for ii in range(ncycles):
111 inds = map_cycle_to_samples(cycle_vect, ii)
112 if isinstance(vals, tuple):
113 args = [v[inds] for v in vals]
114 out[ii] = func(*args)
115 else:
116 out[ii] = func(vals[inds])
117 return out
120def get_augmented_cycle_stat_from_samples(vals, cycle_vect, phase, func=np.mean):
121 """Compute a metric across all augmented samples from each cycle."""
122 ncycles = np.max(cycle_vect) + 1
123 out = np.zeros((ncycles,))
125 for ii in range(ncycles):
126 inds = map_cycle_to_samples_augmented(cycle_vect, ii, phase)
127 if isinstance(vals, tuple):
128 args = [v[inds] for v in vals]
129 out[ii] = func(*args)
130 else:
131 out[ii] = func(vals[inds])
132 return out
135def get_subset_stat_from_samples(vals, subset_vect, cycle_vect, func=np.mean):
136 """Compute a metric across all samples from each cycle in a subset."""
137 ncycles = np.max(subset_vect) + 1
138 out = np.zeros((ncycles,))
139 for ii in range(ncycles):
140 out[ii] = func(vals[map_subset_to_sample(subset_vect, cycle_vect, ii)])
141 return out
144def get_chain_stat_from_samples(vals, chain_vect, subset_vect, cycle_vect, func=np.mean):
145 """Compute a metric across all samples from each chain."""
146 nchains = np.max(chain_vect) + 1
147 out = np.zeros((nchains,))
148 for ii in range(nchains):
149 out[ii] = func(vals[map_chain_to_samples(chain_vect, subset_vect, cycle_vect, ii)])
150 return out
152# --------------------------------------
155def project_cycles_to_samples(vals, cycle_vect):
156 """Transform per-cycle data to full sample vector."""
157 out = np.zeros_like(cycle_vect).astype(float) * np.nan
158 for ii in range(len(vals)):
159 inds = map_cycle_to_samples(cycle_vect, ii)
160 out[inds] = vals[ii]
161 return out
164def project_subset_to_cycles(vals, subset_vect):
165 """Transform per-cycle data from a subset of cycles to a vector of all cycles."""
166 out = np.zeros_like(subset_vect).astype(float) * np.nan
167 for ii in range(len(vals)):
168 inds = map_subset_to_cycle(subset_vect, ii)
169 out[inds] = vals[ii]
170 return out
173def project_subset_to_samples(vals, subset_vect, cycle_vect):
174 """Transform per-cycle data from a subset of cycles to full sample vector."""
175 cycle_vals = project_subset_to_cycles(vals, subset_vect)
176 out = np.zeros_like(cycle_vect).astype(float) * np.nan
177 for ii in range(len(cycle_vals)):
178 inds = map_cycle_to_samples(cycle_vect, ii)
179 out[inds] = cycle_vals[ii]
180 return out
183def project_chain_to_subset(vals, chain_vect):
184 """Transform per-chain data to a subset vector."""
185 out = np.zeros_like(chain_vect).astype(float) * np.nan
186 for ii in range(len(vals)):
187 inds = map_chain_to_subset(chain_vect, ii)
188 out[inds] = vals[ii]
189 return out
192def project_chain_to_cycles(vals, chain_vect, subset_vect):
193 """Transform per-chain data to an all cycles vector."""
194 subset_vals = project_chain_to_subset(vals, chain_vect)
195 return project_subset_to_cycles(subset_vals, subset_vect)
198def project_chain_to_samples(vals, chain_vect, subset_vect, cycle_vect):
199 """Transform per-chain data to an all samples vector."""
200 cycle_vals = project_chain_to_cycles(vals, chain_vect, subset_vect)
201 return project_cycles_to_samples(cycle_vals, cycle_vect)
204# -------------------------------------
206def map_cycle_to_samples(cycle_vect, ii):
207 """Which samples does the iith cycle contain?."""
208 sample_inds = np.where(cycle_vect == ii)[0]
209 if np.all(np.diff(sample_inds) == 1) is False:
210 # Mapped samples are not continuous!
211 raise ValueError
212 return sample_inds
215def map_cycle_to_samples_augmented(cycle_vect, ii, phase):
216 """Which samples does the augmented iith cycle contain?."""
217 prev = np.where(cycle_vect == ii-1)[0]
218 prev_segment_inds = np.where(phase[prev] > 1.5*np.pi)[0]
219 if len(prev_segment_inds) == 0:
220 # No candidate trough in previous cycle
221 return None
222 trough_in_prev = prev[prev_segment_inds[0]]
223 stop = np.where(cycle_vect == ii)[0][-1] + 1
224 return np.arange(trough_in_prev, stop)
227def map_sample_to_cycle(cycle_vect, ii):
228 """Which cycle is the ii-th sample in?."""
229 return cycle_vect[ii]
232def map_subset_to_cycle(subset_vect, ii):
233 """Which of all cycles does the ii-th subset cycle correspond to?."""
234 return np.where(subset_vect == ii)[0]
237def map_cycle_to_subset(subset_vect, ii):
238 """Which subset cycle does ii-th overall cycle correspond to? else None."""
239 # answer might be -1...
240 subset_ind = subset_vect[ii]
241 return subset_ind if subset_ind > -1 else None
244def map_subset_to_sample(subset_vect, cycle_vect, ii):
245 """Which samples does ii-th subset cycle contain?."""
246 all_cycle_ind = map_subset_to_cycle(subset_vect, ii)
247 return map_cycle_to_samples(cycle_vect, all_cycle_ind)
250def map_subset_to_sample_augmented(subset_vect, cycle_vect, ii, phase):
251 """Which samples does ii-th subset cycle contain?."""
252 all_cycle_ind = map_subset_to_cycle(subset_vect, ii)
253 return map_cycle_to_samples_augmented(cycle_vect, all_cycle_ind, phase)
256def map_sample_to_subset(subset_vect, cycle_vect, ii):
257 """Which subset cycle does the ii-th sample belong to?."""
258 all_cycle_ind = map_sample_to_cycle(cycle_vect, ii)
259 if all_cycle_ind is None:
260 return None
261 return map_cycle_to_subset(subset_vect, all_cycle_ind)
264def map_chain_to_subset(chain_vect, ii):
265 """Which subset cycles does the ii-th chain contain?."""
266 subset_inds = np.where(chain_vect == ii)[0]
267 if (len(subset_inds) > 1) and (np.all(np.diff(subset_inds) == 1) is False):
268 # Mapped subset is not continuous
269 raise ValueError
270 return subset_inds
273def map_subset_to_chain(chain_vect, ii):
274 """Which chain does the iith subset cycle belong to? else None."""
275 return chain_vect[ii]
278def map_cycle_to_chain(chain_vect, subset_vect, ii):
279 """Which chain does the ii-th overall cycle belong to? else None."""
280 subset_cycle_ind = map_cycle_to_subset(subset_vect, ii)
281 if subset_cycle_ind is None:
282 return None
283 return map_subset_to_chain(chain_vect, subset_cycle_ind)
286def map_chain_to_cycle(chain_vect, subset_vect, ii):
287 """Which of all cycles does the ii-th chain contain."""
288 subset_ind = map_chain_to_subset(chain_vect, ii)
289 cycle_ind = np.squeeze([map_subset_to_cycle(subset_vect, jj) for jj in subset_ind])
290 if (len(cycle_ind) > 1) and (np.all(np.diff(cycle_ind) == 1) is False):
291 # Mapped cycles are not continuous!
292 raise ValueError
293 return cycle_ind
296def map_chain_to_samples(chain_vect, subset_vect, cycle_vect, ii):
297 """Which samples does the ii-th chain contain?."""
298 subset_inds = map_chain_to_subset(chain_vect, ii)
299 sample_inds = [map_subset_to_sample(subset_vect, cycle_vect, jj) for jj in subset_inds]
300 return np.hstack(sample_inds)
303def map_sample_to_chain(chain_vect, subset_vect, cycle_vect, ii):
304 """Which chain does the ii-th sample belong to? else None."""
305 subset_ind = map_sample_to_subset(subset_vect, cycle_vect, ii)
306 if subset_ind is None:
307 return None
308 return map_subset_to_chain(chain_vect, subset_ind)