Coverage for emd/_cycles_support.py: 65%

148 statements  

« prev     ^ index     » next       coverage.py v7.6.11, created at 2025-03-08 15:44 +0000

1#!/usr/bin/python 

2 

3# vim: set expandtab ts=4 sw=4: 

4 

5"""Low-level functions for handling single cycle analyses.""" 

6 

7import numpy as np 

8 

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. 

55 

56 

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 

61 

62 starts = np.r_[0, starts] 

63 stops = np.r_[stops, len(cycle_vect)] 

64 

65 slice_cache = [slice(starts[ii], stops[ii]) for ii in range(len(starts))] 

66 

67 return slice_cache 

68 

69 

70def _slice_len(sli): 

71 """Find the length of array returned by a slice.""" 

72 return sli.stop - sli.start + 1 

73 

74 

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 

83 

84 

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] 

88 

89 

90# -------------------------------------- 

91 

92 

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]) 

103 

104 

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,)) 

109 

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 

118 

119 

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,)) 

124 

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 

133 

134 

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 

142 

143 

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 

151 

152# -------------------------------------- 

153 

154 

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 

162 

163 

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 

171 

172 

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 

181 

182 

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 

190 

191 

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) 

196 

197 

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) 

202 

203 

204# ------------------------------------- 

205 

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 

213 

214 

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) 

225 

226 

227def map_sample_to_cycle(cycle_vect, ii): 

228 """Which cycle is the ii-th sample in?.""" 

229 return cycle_vect[ii] 

230 

231 

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] 

235 

236 

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 

242 

243 

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) 

248 

249 

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) 

254 

255 

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) 

262 

263 

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 

271 

272 

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] 

276 

277 

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) 

284 

285 

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 

294 

295 

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) 

301 

302 

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)