PYME-extra 1.0.4.post0__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.
Files changed (101) hide show
  1. PYMEcs/Acquire/Actions/__init__.py +0 -0
  2. PYMEcs/Acquire/Actions/custom.py +167 -0
  3. PYMEcs/Acquire/Hardware/LPthreadedSimple.py +248 -0
  4. PYMEcs/Acquire/Hardware/LPthreadedSimpleSim.py +246 -0
  5. PYMEcs/Acquire/Hardware/NikonTiFlaskServer.py +45 -0
  6. PYMEcs/Acquire/Hardware/NikonTiFlaskServerT.py +59 -0
  7. PYMEcs/Acquire/Hardware/NikonTiRESTClient.py +73 -0
  8. PYMEcs/Acquire/Hardware/NikonTiSim.py +35 -0
  9. PYMEcs/Acquire/Hardware/__init__.py +0 -0
  10. PYMEcs/Acquire/Hardware/driftTrackGUI.py +329 -0
  11. PYMEcs/Acquire/Hardware/driftTrackGUI_n.py +472 -0
  12. PYMEcs/Acquire/Hardware/driftTracking.py +424 -0
  13. PYMEcs/Acquire/Hardware/driftTracking_n.py +433 -0
  14. PYMEcs/Acquire/Hardware/fakeCamX.py +15 -0
  15. PYMEcs/Acquire/Hardware/offsetPiezoRESTCorrelLog.py +38 -0
  16. PYMEcs/Acquire/__init__.py +0 -0
  17. PYMEcs/Analysis/MBMcollection.py +552 -0
  18. PYMEcs/Analysis/MINFLUX.py +280 -0
  19. PYMEcs/Analysis/MapUtils.py +77 -0
  20. PYMEcs/Analysis/NPC.py +1176 -0
  21. PYMEcs/Analysis/Paraflux.py +218 -0
  22. PYMEcs/Analysis/Simpler.py +81 -0
  23. PYMEcs/Analysis/Sofi.py +140 -0
  24. PYMEcs/Analysis/__init__.py +0 -0
  25. PYMEcs/Analysis/decSofi.py +211 -0
  26. PYMEcs/Analysis/eventProperties.py +50 -0
  27. PYMEcs/Analysis/fitDarkTimes.py +569 -0
  28. PYMEcs/Analysis/objectVolumes.py +20 -0
  29. PYMEcs/Analysis/offlineTracker.py +130 -0
  30. PYMEcs/Analysis/stackTracker.py +180 -0
  31. PYMEcs/Analysis/timeSeries.py +63 -0
  32. PYMEcs/Analysis/trackFiducials.py +186 -0
  33. PYMEcs/Analysis/zerocross.py +91 -0
  34. PYMEcs/IO/MINFLUX.py +851 -0
  35. PYMEcs/IO/NPC.py +117 -0
  36. PYMEcs/IO/__init__.py +0 -0
  37. PYMEcs/IO/darkTimes.py +19 -0
  38. PYMEcs/IO/picasso.py +219 -0
  39. PYMEcs/IO/tabular.py +11 -0
  40. PYMEcs/__init__.py +0 -0
  41. PYMEcs/experimental/CalcZfactor.py +51 -0
  42. PYMEcs/experimental/FRC.py +338 -0
  43. PYMEcs/experimental/ImageJROItools.py +49 -0
  44. PYMEcs/experimental/MINFLUX.py +1537 -0
  45. PYMEcs/experimental/NPCcalcLM.py +560 -0
  46. PYMEcs/experimental/Simpler.py +369 -0
  47. PYMEcs/experimental/Sofi.py +78 -0
  48. PYMEcs/experimental/__init__.py +0 -0
  49. PYMEcs/experimental/binEventProperty.py +187 -0
  50. PYMEcs/experimental/chaining.py +23 -0
  51. PYMEcs/experimental/clusterTrack.py +179 -0
  52. PYMEcs/experimental/combine_maps.py +104 -0
  53. PYMEcs/experimental/eventProcessing.py +93 -0
  54. PYMEcs/experimental/fiducials.py +323 -0
  55. PYMEcs/experimental/fiducialsNew.py +402 -0
  56. PYMEcs/experimental/mapTools.py +271 -0
  57. PYMEcs/experimental/meas2DplotDh5view.py +107 -0
  58. PYMEcs/experimental/mortensen.py +131 -0
  59. PYMEcs/experimental/ncsDenoise.py +158 -0
  60. PYMEcs/experimental/onTimes.py +295 -0
  61. PYMEcs/experimental/procPoints.py +77 -0
  62. PYMEcs/experimental/pyme2caml.py +73 -0
  63. PYMEcs/experimental/qPAINT.py +965 -0
  64. PYMEcs/experimental/randMap.py +188 -0
  65. PYMEcs/experimental/regExtraCmaps.py +11 -0
  66. PYMEcs/experimental/selectROIfilterTable.py +72 -0
  67. PYMEcs/experimental/showErrs.py +51 -0
  68. PYMEcs/experimental/showErrsDh5view.py +58 -0
  69. PYMEcs/experimental/showShiftMap.py +56 -0
  70. PYMEcs/experimental/snrEvents.py +188 -0
  71. PYMEcs/experimental/specLabeling.py +51 -0
  72. PYMEcs/experimental/splitRender.py +246 -0
  73. PYMEcs/experimental/testChannelByName.py +36 -0
  74. PYMEcs/experimental/timedSpecies.py +28 -0
  75. PYMEcs/experimental/utils.py +31 -0
  76. PYMEcs/misc/ExtraCmaps.py +177 -0
  77. PYMEcs/misc/__init__.py +0 -0
  78. PYMEcs/misc/configUtils.py +169 -0
  79. PYMEcs/misc/guiMsgBoxes.py +27 -0
  80. PYMEcs/misc/mapUtils.py +230 -0
  81. PYMEcs/misc/matplotlib.py +136 -0
  82. PYMEcs/misc/rectsFromSVG.py +182 -0
  83. PYMEcs/misc/shellutils.py +1110 -0
  84. PYMEcs/misc/utils.py +205 -0
  85. PYMEcs/misc/versionCheck.py +20 -0
  86. PYMEcs/misc/zcInfo.py +90 -0
  87. PYMEcs/pyme_warnings.py +4 -0
  88. PYMEcs/recipes/__init__.py +0 -0
  89. PYMEcs/recipes/base.py +75 -0
  90. PYMEcs/recipes/localisations.py +2380 -0
  91. PYMEcs/recipes/manipulate_yaml.py +83 -0
  92. PYMEcs/recipes/output.py +177 -0
  93. PYMEcs/recipes/processing.py +247 -0
  94. PYMEcs/recipes/simpler.py +290 -0
  95. PYMEcs/version.py +2 -0
  96. pyme_extra-1.0.4.post0.dist-info/METADATA +114 -0
  97. pyme_extra-1.0.4.post0.dist-info/RECORD +101 -0
  98. pyme_extra-1.0.4.post0.dist-info/WHEEL +5 -0
  99. pyme_extra-1.0.4.post0.dist-info/entry_points.txt +3 -0
  100. pyme_extra-1.0.4.post0.dist-info/licenses/LICENSE +674 -0
  101. pyme_extra-1.0.4.post0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2380 @@
1
+ from PYME.recipes.base import register_module, ModuleBase, Filter
2
+ from PYME.recipes.traits import Input, Output, Float, Enum, CStr, Bool, Int, List, DictStrStr, DictStrList, ListFloat, ListStr, FileOrURI
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from PYME.IO import tabular
7
+ from PYMEcs.pyme_warnings import warn
8
+
9
+ import logging
10
+ logger = logging.getLogger(__file__)
11
+
12
+ @register_module('CorrectForeshortening')
13
+ class CorrectForeshortening(ModuleBase):
14
+
15
+ inputName = Input('localisations')
16
+ outputName = Output('corrected_f')
17
+
18
+ foreshortening = Float(1.0)
19
+ compensate_MINFLUX_foreshortening = Bool(True)
20
+ apply_pixel_size_correction = Bool(False)
21
+ pixel_size_um = Float(0.072)
22
+
23
+ def run(self, inputName):
24
+ from PYME.IO import tabular
25
+ locs = inputName
26
+
27
+ factor = self.foreshortening
28
+ if self.compensate_MINFLUX_foreshortening:
29
+ logger.info("compensating for MINFLUX fs value of %.2f" % locs.mdh.get('MINFLUX.Foreshortening',1.0))
30
+ factor /= locs.mdh.get('MINFLUX.Foreshortening',1.0)
31
+
32
+ out = tabular.MappingFilter(locs)
33
+ out.addColumn('z',locs['z']*factor)
34
+ out.addColumn('error_z',locs['error_z']*factor)
35
+ if 'z_nc' in locs.keys():
36
+ out.addColumn('z_nc',locs['z_nc']*factor)
37
+
38
+ if self.apply_pixel_size_correction:
39
+ correction = self.pixel_size_um / (inputName.mdh.voxelsize_nm.x / 1e3)
40
+ out.addColumn('x',locs['x']*correction)
41
+ out.addColumn('error_x',locs['error_x']*correction)
42
+ out.addColumn('y',locs['y']*correction)
43
+ out.addColumn('error_y',locs['error_y']*correction)
44
+
45
+ from PYME.IO import MetaDataHandler
46
+ mdh = MetaDataHandler.DictMDHandler(locs.mdh)
47
+ mdh['MINFLUX.Foreshortening'] = self.foreshortening # we overwrite this now
48
+ # mdh['CorrectForeshortening.foreshortening'] = self.foreshortening # this will be set automatically because it is a parameter
49
+ if self.apply_pixel_size_correction:
50
+ mdh['Processing.CorrectForeshortening.PixelSizeCorrection'] = correction
51
+ mdh['voxelsize.x'] = self.pixel_size_um
52
+ mdh['voxelsize.y'] = self.pixel_size_um
53
+ out.mdh = mdh
54
+
55
+ return out
56
+
57
+ @register_module('NNdist')
58
+ class NNdist(ModuleBase):
59
+
60
+ inputName = Input('coalesced')
61
+ outputName = Output('withNNdist')
62
+
63
+ def execute(self, namespace):
64
+ inp = namespace[self.inputName]
65
+ mapped = tabular.MappingFilter(inp)
66
+
67
+ from scipy.spatial import KDTree
68
+ coords = np.vstack([inp[k] for k in ['x','y','z']]).T
69
+ tree = KDTree(coords)
70
+ dd, ii = tree.query(coords,k=3)
71
+ mapped.addColumn('NNdist', dd[:,1])
72
+ mapped.addColumn('NNdist2', dd[:,2])
73
+
74
+ try:
75
+ mapped.mdh = inp.mdh
76
+ except AttributeError:
77
+ pass
78
+
79
+ namespace[self.outputName] = mapped
80
+
81
+ @register_module('ClumpFromTID')
82
+ class ClumpFromTID(ModuleBase):
83
+ """
84
+ Generate a clump index from the tid field (Trace ID from MINFLUX data)
85
+ generates contiguous indices, preferable if existing IDs not contiguous
86
+ (as otherwise will create empty clumps with zero clumpsize after merging)
87
+
88
+ Parameters
89
+ ----------
90
+ inputName: string - name of the data source containing a field named 'tid'
91
+
92
+ Returns
93
+ -------
94
+ outputLocalizations : tabular.MappingFilter that contains new clumpIndex and clumpSize fields (will overwrite existing in inputName if present)
95
+
96
+ """
97
+ inputName = Input('with_TID')
98
+ outputName = Output('with_clumps')
99
+
100
+ def execute(self, namespace):
101
+ inp = namespace[self.inputName]
102
+ mapped = tabular.MappingFilter(inp)
103
+
104
+ # we replace the non-sequential trace ids from MINFLUX data field TID with a set of sequential ids
105
+ # this works better for clumpIndex assumptions in the end (we think)
106
+ uids,revids,idcounts = np.unique(inp['tid'],return_inverse=True,return_counts=True)
107
+ ids = np.arange(1,uids.size+1,dtype='int32')[revids]
108
+ counts = idcounts[revids]
109
+
110
+ mapped.addColumn('clumpIndex', ids)
111
+ mapped.addColumn('clumpSize', counts)
112
+
113
+ try:
114
+ mapped.mdh = inp.mdh
115
+ except AttributeError:
116
+ pass
117
+
118
+ namespace[self.outputName] = mapped
119
+
120
+ @register_module('NNdistMutual')
121
+ class NNdistMutual(ModuleBase):
122
+
123
+ inputChan1 = Input('channel1')
124
+ inputChan2 = Input('channel2')
125
+ outputName = Output('c1withNNdist')
126
+
127
+ def execute(self, namespace):
128
+ inpc1 = namespace[self.inputChan1]
129
+ inpc2 = namespace[self.inputChan2]
130
+ mapped = tabular.MappingFilter(inpc1)
131
+
132
+ from scipy.spatial import KDTree
133
+ coords1 = np.vstack([inpc1[k] for k in ['x','y','z']]).T
134
+ coords2 = np.vstack([inpc2[k] for k in ['x','y','z']]).T
135
+ tree = KDTree(coords2)
136
+ dd, ii = tree.query(coords1,k=2)
137
+ mapped.addColumn('NNdistMutual', dd[:,0])
138
+ mapped.addColumn('NNdistMutual2', dd[:,1])
139
+
140
+ try:
141
+ mapped.mdh = inpc1.mdh
142
+ except AttributeError:
143
+ pass
144
+
145
+ namespace[self.outputName] = mapped
146
+
147
+ @register_module('NNfilter')
148
+ class NNfilter(ModuleBase):
149
+
150
+ inputName = Input('withNNdist')
151
+ outputName = Output('NNfiltered')
152
+ nnMin = Float(0)
153
+ nnMax = Float(1e5)
154
+
155
+
156
+ def execute(self, namespace):
157
+ inp = namespace[self.inputName]
158
+ mapped = tabular.MappingFilter(inp)
159
+
160
+ nn_good = (inp['NNdist'] > self.nnMin) * (inp['NNdist'] < self.nnMax)
161
+ nn2_good = (inp['NNdist2'] > self.nnMin) * (inp['NNdist2'] < self.nnMax)
162
+ nn_class = 1.0*nn_good + 2.0*(np.logical_not(nn_good)*nn2_good)
163
+ mapped.addColumn('NNclass', nn_class)
164
+
165
+ filterKeys = {'NNclass': (0.5,2.5)}
166
+ filtered = tabular.ResultsFilter(mapped, **filterKeys)
167
+
168
+ try:
169
+ filtered.mdh = inp.mdh
170
+ except AttributeError:
171
+ pass
172
+
173
+ namespace[self.outputName] = filtered
174
+
175
+ @register_module('FiducialTrack')
176
+ class FiducialTrack(ModuleBase):
177
+ """
178
+ Extract average fiducial track from input pipeline
179
+
180
+ Parameters
181
+ ----------
182
+
183
+ radiusMultiplier: this number is multiplied with error_x to obtain search radius for clustering
184
+ timeWindow: the window along the time dimension used for clustering
185
+ filterScale: the size of the filter kernel used to smooth the resulting average fiducial track
186
+ filterMethod: enumrated choice of filter methods for smoothing operation (Gaussian, Median or Uniform kernel)
187
+
188
+ Notes
189
+ -----
190
+
191
+ Output is a new pipeline with added fiducial_x, fiducial_y columns
192
+
193
+ """
194
+ import PYMEcs.Analysis.trackFiducials as tfs
195
+ inputName = Input('filtered')
196
+
197
+ radiusMultiplier = Float(5.0)
198
+ timeWindow = Int(25)
199
+ filterScale = Float(11)
200
+ filterMethod = Enum(tfs.FILTER_FUNCS.keys())
201
+ clumpMinSize = Int(50)
202
+ singleFiducial = Bool(True)
203
+
204
+ outputName = Output('fiducialAdded')
205
+
206
+ def execute(self, namespace):
207
+ import PYMEcs.Analysis.trackFiducials as tfs
208
+
209
+ inp = namespace[self.inputName]
210
+ mapped = tabular.MappingFilter(inp)
211
+
212
+ if self.singleFiducial:
213
+ # if all data is from a single fiducial we do not need to align
214
+ # we then avoid problems with incomplete tracks giving rise to offsets between
215
+ # fiducial track fragments
216
+ align = False
217
+ else:
218
+ align = True
219
+
220
+ t, x, y, z, isFiducial = tfs.extractTrajectoriesClump(inp, clumpRadiusVar = 'error_x',
221
+ clumpRadiusMultiplier=self.radiusMultiplier,
222
+ timeWindow=self.timeWindow, clumpMinSize=self.clumpMinSize,
223
+ align=align)
224
+ rawtracks = (t, x, y, z)
225
+ tracks = tfs.AverageTrack(inp, rawtracks, filter=self.filterMethod,
226
+ filterScale=self.filterScale,align=align)
227
+
228
+ # add tracks for all calculated dims to output
229
+ for dim in tracks.keys():
230
+ mapped.addColumn('fiducial_%s' % dim, tracks[dim])
231
+ mapped.addColumn('isFiducial', isFiducial)
232
+
233
+ # propogate metadata, if present
234
+ try:
235
+ mapped.mdh = inp.mdh
236
+ except AttributeError:
237
+ pass
238
+
239
+ namespace[self.outputName] = mapped
240
+
241
+ @property
242
+ def hide_in_overview(self):
243
+ return ['columns']
244
+
245
+ @register_module('FiducialApply')
246
+ class FiducialApply(ModuleBase):
247
+ inputName = Input('filtered')
248
+ outputName = Output('fiducialApplied')
249
+
250
+ def execute(self, namespace):
251
+ inp = namespace[self.inputName]
252
+ mapped = tabular.MappingFilter(inp)
253
+
254
+ for dim in ['x','y','z']:
255
+ try:
256
+ mapped.addColumn(dim, inp[dim]-inp['fiducial_%s' % dim])
257
+ except:
258
+ logger.warn('Could not set dim %s' % dim)
259
+
260
+ # propogate metadata, if present
261
+ try:
262
+ mapped.mdh = inp.mdh
263
+ except AttributeError:
264
+ pass
265
+
266
+ namespace[self.outputName] = mapped
267
+
268
+
269
+ @register_module('MergeClumpsTperiod')
270
+ class MergeClumpsTperiod(ModuleBase):
271
+ """
272
+ Create a new mapping object which derives mapped keys from original ones.
273
+ Also adds the time period of bursts by adding clumpTmin, clumpTmax amd clumpLength columns.
274
+ """
275
+ inputName = Input('clumped')
276
+ outputName = Output('merged')
277
+ labelKey = CStr('clumpIndex')
278
+
279
+ def execute(self, namespace):
280
+ from PYME.Analysis.points.DeClump import pyDeClump
281
+ from PYME.Analysis.points.DeClump import deClump as deClumpC
282
+
283
+ inp = namespace[self.inputName]
284
+
285
+ grouped = pyDeClump.mergeClumps(inp, labelKey=self.labelKey)
286
+
287
+ # we need this because currently the addColumn method of DictSrc is broken
288
+ def addColumn(dictsrc,name, values):
289
+ if not isinstance(values, np.ndarray):
290
+ raise TypeError('New column "%s" is not a numpy array' % name)
291
+
292
+ if not len(values) == len(dictsrc):
293
+ raise ValueError('Columns are different lengths')
294
+
295
+ dictsrc._source[name] = values # this was missing I think
296
+
297
+ # work out tmin and tmax
298
+ I = np.argsort(inp[self.labelKey])
299
+ sorted_src = {k: inp[k][I] for k in [self.labelKey,'t']}
300
+ # tmin and tmax - tentative addition
301
+ NClumps = int(np.max(sorted_src[self.labelKey]) + 1)
302
+ tmin = deClumpC.aggregateMin(NClumps, sorted_src[self.labelKey].astype('i'), sorted_src['t'].astype('f'))
303
+ tmax = -deClumpC.aggregateMin(NClumps, sorted_src[self.labelKey].astype('i'), -1.0*sorted_src['t'].astype('f'))
304
+ if '_source' in dir(grouped): # appears to be a DictSource which currently has a broken addColumn method
305
+ addColumn(grouped,'clumpTmin',tmin) # use our fixed function to add a column
306
+ addColumn(grouped,'clumpTmax',tmax)
307
+ addColumn(grouped,'clumpLength',tmax-tmin+1) # this only works if time is in frame units (otherwise a value different from +1 is needed)
308
+ else:
309
+ grouped.addColumn('clumpTmin',tmin)
310
+ grouped.addColumn('clumpTmax',tmax)
311
+ grouped.addColumn('clumpLength',tmax-tmin+1)
312
+
313
+ try:
314
+ grouped.mdh = inp.mdh
315
+ except AttributeError:
316
+ pass
317
+
318
+ namespace[self.outputName] = grouped
319
+
320
+ # interpolate the key from the source to the selected target of the pipeline
321
+ def finterpDS(target,source,key):
322
+ tsource, idx = np.unique(source['t'], return_index=True)
323
+ fsource = source[key][idx]
324
+ fDS = np.interp(target['t'], tsource, fsource)
325
+ return fDS
326
+
327
+ @register_module('FiducialApplyFromFiducials')
328
+ class FiducialApplyFromFiducials(ModuleBase):
329
+ inputData = Input('filtered')
330
+ inputFiducials = Input('Fiducials')
331
+ outputName = Output('fiducialApplied')
332
+ outputFiducials = Output('corrected_fiducials')
333
+
334
+ def execute(self, namespace):
335
+ inp = namespace[self.inputData]
336
+ fiducial = namespace[self.inputFiducials]
337
+
338
+ mapped = tabular.MappingFilter(inp)
339
+ out_f = tabular.MappingFilter(fiducial)
340
+
341
+ for dim in ['x','y','z']:
342
+ fiducial_dim = finterpDS(inp,fiducial,'fiducial_%s' % dim)
343
+
344
+ mapped.addColumn('fiducial_%s' % dim,fiducial_dim)
345
+ mapped.addColumn(dim, inp[dim]-fiducial_dim)
346
+
347
+ out_f.setMapping(dim, '{0} - fiducial_{0}'.format(dim))
348
+
349
+ # propogate metadata, if present
350
+ try:
351
+ mapped.mdh = inp.mdh
352
+ out_f.mdh = fiducial.mdh
353
+ except AttributeError:
354
+ pass
355
+
356
+ namespace[self.outputName] = mapped
357
+ namespace[self.outputFiducials] = out_f
358
+
359
+
360
+ @register_module('ClusterTimeRange')
361
+ class ClusterTimeRange(ModuleBase):
362
+
363
+ inputName = Input('dbscanClustered')
364
+ IDkey = CStr('dbscanClumpID')
365
+ outputName = Output('withTrange')
366
+
367
+ def execute(self, namespace):
368
+ from scipy.stats import binned_statistic
369
+
370
+ inp = namespace[self.inputName]
371
+ mapped = tabular.MappingFilter(inp)
372
+
373
+ ids = inp[self.IDkey]
374
+ t = inp['t']
375
+ maxid = int(ids.max())
376
+ edges = -0.5+np.arange(maxid+2)
377
+ resmin = binned_statistic(ids, t, statistic='min', bins=edges)
378
+ resmax = binned_statistic(ids, t, statistic='max', bins=edges)
379
+ trange = resmax[0][ids] - resmin[0][ids] + 1
380
+
381
+ mapped.addColumn('trange', trange)
382
+
383
+ # propogate metadata, if present
384
+ try:
385
+ mapped.mdh = inp.mdh
386
+ except AttributeError:
387
+ pass
388
+
389
+ namespace[self.outputName] = mapped
390
+
391
+ @register_module('ClusterStats')
392
+ class ClusterStats(ModuleBase):
393
+
394
+ inputName = Input('with_clumps')
395
+ IDkey = CStr('clumpIndex')
396
+ StatMethod = Enum(['std','min','max', 'mean', 'median', 'count', 'sum'])
397
+ StatKey = CStr('x')
398
+ outputName = Output('withClumpStats')
399
+
400
+ def execute(self, namespace):
401
+ from scipy.stats import binned_statistic
402
+
403
+ inp = namespace[self.inputName]
404
+ mapped = tabular.MappingFilter(inp)
405
+
406
+ ids = inp[self.IDkey] # I imagine this needs to be an int type key
407
+ prop = inp[self.StatKey]
408
+ maxid = int(ids.max())
409
+ edges = -0.5+np.arange(maxid+2)
410
+ resstat = binned_statistic(ids, prop, statistic=self.StatMethod, bins=edges)
411
+
412
+ mapped.addColumn(self.StatKey+"_"+self.StatMethod, resstat[0][ids])
413
+
414
+ # propogate metadata, if present
415
+ try:
416
+ mapped.mdh = inp.mdh
417
+ except AttributeError:
418
+ pass
419
+
420
+ namespace[self.outputName] = mapped
421
+
422
+ @property
423
+ def _key_choices(self):
424
+ #try and find the available column names
425
+ try:
426
+ return sorted(self._parent.namespace[self.inputName].keys())
427
+ except:
428
+ return []
429
+
430
+ @property
431
+ def default_view(self):
432
+ from traitsui.api import View, Group, Item
433
+ from PYME.ui.custom_traits_editors import CBEditor
434
+
435
+ return View(Item('inputName', editor=CBEditor(choices=self._namespace_keys)),
436
+ Item('_'),
437
+ Item('IDkey', editor=CBEditor(choices=self._key_choices)),
438
+ Item('StatKey', editor=CBEditor(choices=self._key_choices)),
439
+ Item('StatMethod'),
440
+ Item('_'),
441
+ Item('outputName'), buttons=['OK'])
442
+
443
+ @register_module('ValidClumps')
444
+ class ValidClumps(ModuleBase):
445
+
446
+ inputName = Input('with_clumps')
447
+ inputValid = Input('valid_clumps')
448
+ IDkey = CStr('clumpIndex')
449
+ outputName = Output('with_validClumps')
450
+
451
+ def execute(self, namespace):
452
+
453
+ inp = namespace[self.inputName]
454
+ valid = namespace[self.inputValid]
455
+ mapped = tabular.MappingFilter(inp)
456
+
457
+ # note: in coalesced data the clumpIndices are float!
458
+ # this creates issues in comparisons unless these are converted to int before comparisons are made!!
459
+ # that is the reason for the rint and astype conversions below
460
+ ids = np.rint(inp[self.IDkey]).astype('i')
461
+ validIDs = np.in1d(ids,np.unique(np.rint(valid[self.IDkey]).astype('i')))
462
+
463
+ mapped.addColumn('validID', validIDs.astype('f')) # should be float or int?
464
+
465
+ # propogate metadata, if present
466
+ try:
467
+ mapped.mdh = inp.mdh
468
+ except AttributeError:
469
+ pass
470
+
471
+ namespace[self.outputName] = mapped
472
+
473
+ @register_module('CopyMapped')
474
+ class CopyMapped(ModuleBase):
475
+ inputName = Input('filtered')
476
+ outputName = Output('filtered-copy')
477
+
478
+ def execute(self, namespace):
479
+ inp = namespace[self.inputName]
480
+ mapped = tabular.MappingFilter(inp)
481
+ namespace[self.outputName] = mapped
482
+
483
+ @register_module('QindexScale')
484
+ class QindexScale(ModuleBase):
485
+ inputName = Input('qindex')
486
+ outputName = Output('qindex-calibrated')
487
+ qIndexkey = CStr('qIndex')
488
+ qindexValue = Float(1.0)
489
+ NEquivalent = Float(1.0)
490
+
491
+ def execute(self, namespace):
492
+ inp = namespace[self.inputName]
493
+ mapped = tabular.MappingFilter(inp)
494
+ qkey = self.qIndexkey
495
+ scaled = inp[qkey]
496
+ qigood = inp[qkey] > 0
497
+ scaled[qigood] = inp[qkey][qigood] * self.NEquivalent / self.qindexValue
498
+
499
+ self.newKey = '%sCal' % qkey
500
+ mapped.addColumn(self.newKey, scaled)
501
+ namespace[self.outputName] = mapped
502
+
503
+ @property
504
+ def _key_choices(self):
505
+ #try and find the available column names
506
+ try:
507
+ return sorted(self._parent.namespace[self.inputName].keys())
508
+ except:
509
+ return []
510
+
511
+ @property
512
+ def default_view(self):
513
+ from traitsui.api import View, Group, Item
514
+ from PYME.ui.custom_traits_editors import CBEditor
515
+
516
+ return View(Item('inputName', editor=CBEditor(choices=self._namespace_keys)),
517
+ Item('_'),
518
+ Item('qIndexkey', editor=CBEditor(choices=self._key_choices)),
519
+ Item('qindexValue'),
520
+ Item('NEquivalent'),
521
+ Item('_'),
522
+ Item('outputName'), buttons=['OK'])
523
+
524
+ @register_module('QindexRatio')
525
+ class QindexRatio(ModuleBase):
526
+ inputName = Input('qindex')
527
+ outputName = Output('qindex-calibrated')
528
+ qIndexDenom = CStr('qIndex1')
529
+ qIndexNumer = CStr('qIndex2')
530
+ qIndexRatio = CStr('qRatio')
531
+
532
+ def execute(self, namespace):
533
+ inp = namespace[self.inputName]
534
+ mapped = tabular.MappingFilter(inp)
535
+ qkey1 = self.qIndexDenom
536
+ qkey2 = self.qIndexNumer
537
+
538
+ v2 = inp[qkey2]
539
+ ratio = np.zeros_like(v2,dtype='float64')
540
+ qigood = v2 > 0
541
+ ratio[qigood] = inp[qkey1][qigood] / v2[qigood]
542
+
543
+ mapped.addColumn(self.qIndexRatio, ratio)
544
+ namespace[self.outputName] = mapped
545
+
546
+ @property
547
+ def _key_choices(self):
548
+ #try and find the available column names
549
+ try:
550
+ return sorted(self._parent.namespace[self.inputName].keys())
551
+ except:
552
+ return []
553
+
554
+ @property
555
+ def default_view(self):
556
+ from traitsui.api import View, Group, Item
557
+ from PYME.ui.custom_traits_editors import CBEditor
558
+
559
+ return View(Item('inputName', editor=CBEditor(choices=self._namespace_keys)),
560
+ Item('_'),
561
+ Item('qIndexDenom', editor=CBEditor(choices=self._key_choices)),
562
+ Item('qIndexNumer', editor=CBEditor(choices=self._key_choices)),
563
+ Item('qIndexRatio'),
564
+ Item('_'),
565
+ Item('outputName'), buttons=['OK'])
566
+
567
+
568
+ @register_module('ObjectVolume')
569
+ class ObjectVolume(ModuleBase):
570
+ inputName = Input('objectID')
571
+ outputName = Output('volumes')
572
+
573
+ def execute(self, namespace):
574
+ from PYMEcs.Analysis.objectVolumes import objectVolumes
575
+ inp = namespace[self.inputName]
576
+ mapped = tabular.MappingFilter(inp)
577
+
578
+ volumes = objectVolumes(np.vstack([inp[k] for k in ('x','y')]).T,inp['objectID'])
579
+
580
+ mapped.addColumn('volumes', volumes)
581
+ namespace[self.outputName] = mapped
582
+
583
+ def uniqueByID(ids,column):
584
+ uids, idx = np.unique(ids.astype('int'), return_index=True)
585
+ ucol = column[idx]
586
+ valid = uids > 0
587
+ return uids[valid], ucol[valid]
588
+
589
+ @register_module('ScatterbyID')
590
+ class ScatterbyID(ModuleBase):
591
+ """Take just certain columns of a variable"""
592
+ inputName = Input('measurements')
593
+ IDkey = CStr('objectID')
594
+ xkey = CStr('qIndex')
595
+ ykey = CStr('objArea')
596
+ outputName = Output('outGraph')
597
+
598
+ def execute(self, namespace):
599
+ meas = namespace[self.inputName]
600
+ ids = meas[self.IDkey]
601
+ uid, x = uniqueByID(ids,meas[self.xkey])
602
+ uid, y = uniqueByID(ids,meas[self.ykey])
603
+
604
+ import pylab
605
+ pylab.figure()
606
+ pylab.scatter(x,y)
607
+
608
+ pylab.grid()
609
+ pylab.xlabel(self.xkey)
610
+ pylab.ylabel(self.ykey)
611
+ #namespace[self.outputName] = out
612
+
613
+ @property
614
+ def _key_choices(self):
615
+ #try and find the available column names
616
+ try:
617
+ return sorted(self._parent.namespace[self.inputName].keys())
618
+ except:
619
+ return []
620
+
621
+ @property
622
+ def default_view(self):
623
+ from traitsui.api import View, Group, Item
624
+ from PYME.ui.custom_traits_editors import CBEditor
625
+
626
+ return View(Item('inputName', editor=CBEditor(choices=self._namespace_keys)),
627
+ Item('_'),
628
+ Item('IDkey', editor=CBEditor(choices=self._key_choices)),
629
+ Item('xkey', editor=CBEditor(choices=self._key_choices)),
630
+ Item('ykey', editor=CBEditor(choices=self._key_choices)),
631
+ Item('_'),
632
+ Item('outputName'), buttons=['OK'])
633
+
634
+ @register_module('HistByID')
635
+ class HistByID(ModuleBase):
636
+ """Plot histogram of a column by ID"""
637
+ inputName = Input('measurements')
638
+ IDkey = CStr('objectID')
639
+ histkey = CStr('qIndex')
640
+ outputName = Output('outGraph')
641
+ nbins = Int(50)
642
+ minval = Float(float('nan'))
643
+ maxval = Float(float('nan'))
644
+
645
+ def execute(self, namespace):
646
+ import math
647
+ meas = namespace[self.inputName]
648
+ ids = meas[self.IDkey]
649
+
650
+ uid, valsu = uniqueByID(ids,meas[self.histkey])
651
+ if math.isnan(self.minval):
652
+ minv = valsu.min()
653
+ else:
654
+ minv = self.minval
655
+ if math.isnan(self.maxval):
656
+ maxv = valsu.max()
657
+ else:
658
+ maxv = self.maxval
659
+
660
+ import matplotlib.pyplot as plt
661
+ plt.figure()
662
+ plt.hist(valsu,self.nbins,range=(minv,maxv))
663
+ plt.xlabel(self.histkey)
664
+
665
+ @property
666
+ def _key_choices(self):
667
+ #try and find the available column names
668
+ try:
669
+ return sorted(self._parent.namespace[self.inputName].keys())
670
+ except:
671
+ return []
672
+
673
+ @property
674
+ def default_view(self):
675
+ from traitsui.api import View, Group, Item
676
+ from PYME.ui.custom_traits_editors import CBEditor
677
+
678
+ return View(Item('inputName', editor=CBEditor(choices=self._namespace_keys)),
679
+ Item('_'),
680
+ Item('IDkey', editor=CBEditor(choices=self._key_choices)),
681
+ Item('histkey', editor=CBEditor(choices=self._key_choices)),
682
+ Item('nbins'),
683
+ Item('minval'),
684
+ Item('maxval'),
685
+ Item('_'),
686
+ Item('outputName'), buttons=['OK'])
687
+
688
+
689
+ @register_module('subClump')
690
+ class subClump(ModuleBase):
691
+ """
692
+ Groups clumps into smaller sub clumps of given max sub clump size
693
+
694
+ Parameters
695
+ ----------
696
+
697
+ labelKey: datasource key serving as input clump index
698
+
699
+ subclumpMaxSize: target size of new subclumps - see also comments in the code
700
+
701
+ clumpColumnName: column name of generated sub clump index
702
+
703
+ sizeColumnName: column name of sub clump sizes
704
+
705
+ """
706
+ inputName = Input('with_clumps')
707
+ labelKey = CStr('clumpIndex')
708
+ subclumpMaxSize = Int(4)
709
+
710
+ clumpColumnName = CStr('subClumpID')
711
+ sizeColumnName = CStr('subClumpSize')
712
+ outputName = Output('with_subClumps')
713
+
714
+ def execute(self, namespace):
715
+
716
+ inp = namespace[self.inputName]
717
+ mapped = tabular.MappingFilter(inp)
718
+
719
+ # subid calculations
720
+ subids = np.zeros_like(inp['x'],dtype='int')
721
+ clumpids = inp[self.labelKey]
722
+ uids,revids,counts = np.unique(clumpids,return_inverse=True, return_counts=True)
723
+ # the clumpsz per se should not be needed, we just use the counts below
724
+ # clumpsz = counts[revids] # NOTE: could the case ID=0 be an issue? These are often points not part of clusters etc
725
+
726
+ # the code below has the task to split "long" clumps into subclumps
727
+ # we use the following strategy:
728
+ # - don't split clumps with less than 2*scMaxSize events
729
+ # - if clumps are larger than that break the clump into subclumps
730
+ # - each subclump has at least scMaxSize events
731
+ # - if there are "extra" events at the end that would not make a full scMaxSize clump
732
+ # then add these to the otherwise previous subclump
733
+ # - as a result generated subclumps have therefore from scMaxSize to 2*scMaxSize-1 events
734
+ # NOTE 1: this strategy is not the only possible one, reassess as needed
735
+ # NOTE 2: we do NOT explicitly sort by time of the events in the clump when grouping into sub clumps
736
+ # (should not matter but think about again)
737
+ # NOTE 3: this has not necessarily been tuned for efficiency - tackle if needed
738
+
739
+ scMaxSize = self.subclumpMaxSize
740
+ curbaseid = 1
741
+ for i,id in enumerate(uids):
742
+ if id > 0:
743
+ if counts[i] > 2*scMaxSize-1:
744
+ cts = counts[i]
745
+ ctrange = np.arange(cts,dtype='int') # a range we can use in splitting the events up into subclumps
746
+ subs = cts // scMaxSize # the number of subclumps we are going to make
747
+ # the last bit of the expression below ensures that events that would not make a full size subClump
748
+ # get added to the last of the subclumps; a subclump has there from scMaxSize to 2*scMaxSize-1 events
749
+ subids[revids == i] = curbaseid + ctrange // scMaxSize - (ctrange >= (subs * scMaxSize))
750
+ curbaseid += subs
751
+ else:
752
+ subids[revids == i] = curbaseid
753
+ curbaseid += 1
754
+
755
+ suids,srevids,scounts = np.unique(subids,return_inverse=True, return_counts=True) # generate counts for new subclump IDs
756
+ subclumpsz = scounts[srevids] # NOTE: could the case ID==0 be an issue? These are often points not part of clusters etc
757
+ # end subid calculations
758
+
759
+ mapped.addColumn(str(self.clumpColumnName), subids)
760
+ mapped.addColumn(str(self.sizeColumnName), subclumpsz)
761
+
762
+ # propogate metadata, if present
763
+ try:
764
+ mapped.mdh = inp.mdh
765
+ except AttributeError:
766
+ pass
767
+
768
+ namespace[self.outputName] = mapped
769
+
770
+
771
+ # a version of David's module which we include here so that we can test/hack a few things
772
+ @register_module('DBSCANClustering2')
773
+ class DBSCANClustering2(ModuleBase):
774
+ """
775
+ Performs DBSCAN clustering on input dictionary
776
+
777
+ Parameters
778
+ ----------
779
+
780
+ searchRadius: search radius for clustering
781
+ minPtsForCore: number of points within SearchRadius required for a given point to be considered a core point
782
+
783
+ Notes
784
+ -----
785
+
786
+ See `sklearn.cluster.dbscan` for more details about the underlying algorithm and parameter meanings.
787
+
788
+ """
789
+
790
+ import multiprocessing
791
+ inputName = Input('filtered')
792
+
793
+ columns = ListStr(['x', 'y', 'z'])
794
+ searchRadius = Float(10)
795
+ minClumpSize = Int(1)
796
+
797
+ #exposes sklearn parallelism. Recipe modules are generally assumed
798
+ #to be single-threaded. Enable at your own risk
799
+ multithreaded = Bool(False)
800
+ numberOfJobs = Int(max(multiprocessing.cpu_count()-1,1))
801
+
802
+ clumpColumnName = CStr('dbscanClumpID')
803
+ sizeColumnName = CStr('dbscanClumpSize')
804
+ outputName = Output('dbscanClustered')
805
+
806
+ def execute(self, namespace):
807
+ from sklearn.cluster import dbscan
808
+ from scipy.stats import binned_statistic
809
+
810
+ inp = namespace[self.inputName]
811
+ mapped = tabular.MappingFilter(inp)
812
+
813
+ # Note that sklearn gives unclustered points label of -1, and first value starts at 0.
814
+ if self.multithreaded:
815
+ core_samp, dbLabels = dbscan(np.vstack([inp[k] for k in self.columns]).T,
816
+ eps=self.searchRadius, min_samples=self.minClumpSize, n_jobs=self.numberOfJobs)
817
+ else:
818
+ #NB try-catch from Christians multithreaded example removed as I think we should see failure here
819
+ core_samp, dbLabels = dbscan(np.vstack([inp[k] for k in self.columns]).T,
820
+ eps=self.searchRadius, min_samples=self.minClumpSize)
821
+
822
+ # shift dbscan labels up by one to match existing convention that a clumpID of 0 corresponds to unclumped
823
+ dbids = dbLabels + 1
824
+ maxid = int(dbids.max())
825
+ edges = -0.5+np.arange(maxid+2)
826
+ resstat = binned_statistic(dbids, np.ones_like(dbids), statistic='sum', bins=edges)
827
+
828
+ mapped.addColumn(str(self.clumpColumnName), dbids)
829
+ mapped.addColumn(str(self.sizeColumnName),resstat[0][dbids])
830
+
831
+ # propogate metadata, if present
832
+ try:
833
+ mapped.mdh = inp.mdh
834
+ except AttributeError:
835
+ pass
836
+
837
+ namespace[self.outputName] = mapped
838
+
839
+
840
+ @property
841
+ def hide_in_overview(self):
842
+ return ['columns']
843
+
844
+ def _view_items(self, params=None):
845
+ from traitsui.api import Item, TextEditor
846
+ return [Item('columns', editor=TextEditor(auto_set=False, enter_set=True, evaluate=ListStr)),
847
+ Item('searchRadius'),
848
+ Item('minClumpSize'),
849
+ Item('multithreaded'),
850
+ Item('numberOfJobs'),
851
+ Item('clumpColumnName'),
852
+ Item('sizeColumnName'),]
853
+
854
+
855
+ @register_module('SnrCalculation')
856
+ class SnrCalculation(ModuleBase):
857
+ inputName = Input('filtered')
858
+ outputName = Output('snr')
859
+
860
+ def execute(self, namespace):
861
+ inp = namespace[self.inputName]
862
+ mapped = tabular.MappingFilter(inp)
863
+
864
+ if 'mdh' not in dir(inp):
865
+ raise RuntimeError('SnrCalculation needs metadata')
866
+ else:
867
+ mdh = inp.mdh
868
+
869
+ nph = inp['nPhotons']
870
+ bgraw = inp['fitResults_background']
871
+ bgph = np.clip((bgraw)*mdh['Camera.ElectronsPerCount']/mdh.getEntry('Camera.TrueEMGain'),1,None)
872
+
873
+ npixroi = (2*mdh.getOrDefault('Analysis.ROISize',5) + 1)**2
874
+ snr = 1.0/npixroi * np.clip(nph,0,None)/np.sqrt(bgph)
875
+
876
+ mapped.addColumn('SNR', snr)
877
+ mapped.addColumn('backgroundPhotons',bgph)
878
+
879
+ mapped.mdh = inp.mdh
880
+
881
+ namespace[self.outputName] = mapped
882
+
883
+ @register_module('TimedSpecies')
884
+ class TimedSpecies(ModuleBase):
885
+ inputName = Input('filtered')
886
+ outputName = Output('timedSpecies')
887
+ Species_1_Name = CStr('Species1')
888
+ Species_1_Start = Float(0)
889
+ Species_1_Stop = Float(1e6)
890
+
891
+ Species_2_Name = CStr('')
892
+ Species_2_Start = Float(0)
893
+ Species_2_Stop = Float(0)
894
+
895
+ Species_3_Name = CStr('')
896
+ Species_3_Start = Float(0)
897
+ Species_3_Stop = Float(0)
898
+
899
+ def execute(self, namespace):
900
+ inp = namespace[self.inputName]
901
+ mapped = tabular.MappingFilter(inp)
902
+ timedSpecies = self.populateTimedSpecies()
903
+
904
+ mapped.addColumn('ColourNorm', np.ones_like(mapped['t'],'float'))
905
+ for species in timedSpecies:
906
+ mapped.addColumn('p_%s' % species['name'],
907
+ (mapped['t'] >= species['t_start'])*
908
+ (mapped['t'] < species['t_end']))
909
+
910
+ if 'mdh' in dir(inp):
911
+ mapped.mdh = inp.mdh
912
+ mapped.mdh['TimedSpecies'] = timedSpecies
913
+
914
+ namespace[self.outputName] = mapped
915
+
916
+ def populateTimedSpecies(self):
917
+ ts = []
918
+ if self.Species_1_Name:
919
+ ts.append({'name' : self.Species_1_Name,
920
+ 't_start': self.Species_1_Start,
921
+ 't_end' : self.Species_1_Stop})
922
+
923
+ if self.Species_2_Name:
924
+ ts.append({'name' : self.Species_2_Name,
925
+ 't_start': self.Species_2_Start,
926
+ 't_end' : self.Species_2_Stop})
927
+
928
+ if self.Species_3_Name:
929
+ ts.append({'name' : self.Species_3_Name,
930
+ 't_start': self.Species_3_Start,
931
+ 't_end' : self.Species_3_Stop})
932
+
933
+ return ts
934
+
935
+ import wx
936
+ def populate_fresults(fitMod,inp,bgzero=True):
937
+ r = np.zeros(inp['x'].size, fitMod.fresultdtype)
938
+ for k in inp.keys():
939
+ f = k.split('_')
940
+ if len(f) == 1:
941
+ try:
942
+ r[f[0]] = inp[k]
943
+ except ValueError:
944
+ pass
945
+ elif len(f) == 2:
946
+ try:
947
+ r[f[0]][f[1]] = inp[k]
948
+ except ValueError:
949
+ pass
950
+ elif len(f) == 3:
951
+ try:
952
+ r[f[0]][f[1]][f[2]] = inp[k]
953
+ except ValueError:
954
+ pass
955
+ else:
956
+ raise RuntimeError('more fields than expected: %d' % len(f))
957
+
958
+ if bgzero:
959
+ r['fitResults']['bg'] = 0
960
+ r['fitResults']['br'] = 0
961
+
962
+ return r
963
+
964
+
965
+ from PYME.IO.MetaDataHandler import NestedClassMDHandler
966
+ def genFitImage(fitMod,fr,mdh,psfname=None):
967
+ mdh2 = NestedClassMDHandler(mdh)
968
+ if psfname is not None:
969
+ mdh2['PSFFile'] = psfname
970
+ fitim = fitMod.genFitImage(fr,mdh2)
971
+
972
+ return fitim
973
+
974
+ def get_photons(fitim,mdh):
975
+ nph = fitim.sum()*mdh.getEntry('Camera.ElectronsPerCount')/mdh.getEntry('Camera.TrueEMGain')
976
+ return nph
977
+
978
+
979
+ def nPhotons(fitMod,fr,mdh,psfname=None,nmax=100,progressBar=None,updateStep=100):
980
+ mdh2 = NestedClassMDHandler(mdh)
981
+ if psfname is not None:
982
+ mdh2['PSFFile'] = psfname
983
+ npoints = min(fr.shape[0],nmax)
984
+ nph = np.zeros((npoints))
985
+ us = int(updateStep)
986
+ for i in range(npoints):
987
+ nph[i] = get_photons(genFitImage(fitMod,fr[i],mdh2,psfname=None), mdh2)
988
+ if (progressBar is not None) and ((i % us) == 0):
989
+ progressBar.Update(100.0*i/float(npoints))
990
+ wx.Yield()
991
+ return nph
992
+
993
+
994
+ @register_module('BiplanePhotons')
995
+ class BiplanePhotons(ModuleBase):
996
+
997
+ inputName = Input('filtered')
998
+ outputName = Output('withPhotons')
999
+
1000
+ def execute(self, namespace):
1001
+ inp = namespace[self.inputName]
1002
+ mapped = tabular.MappingFilter(inp)
1003
+
1004
+ fdialog = wx.FileDialog(None, 'Please select PSF to use ...',
1005
+ #defaultDir=os.path.split(self.image.filename)[0],
1006
+ wildcard='PSF Files|*.psf|TIFF files|*.tif', style=wx.FD_OPEN)
1007
+ succ = fdialog.ShowModal()
1008
+ if (succ == wx.ID_OK):
1009
+ psfn = filename = fdialog.GetPath()
1010
+
1011
+ mdh = inp.mdh
1012
+ if mdh.getEntry('Analysis.FitModule') not in ['SplitterFitInterpBNR']:
1013
+ warn('Plugin works only for Biplane analysis')
1014
+ return
1015
+ fitMod = __import__('PYME.localization.FitFactories.' +
1016
+ mdh.getEntry('Analysis.FitModule'),
1017
+ fromlist=['PYME', 'localization', 'FitFactories'])
1018
+
1019
+ fr = populate_fresults(fitMod, inp)
1020
+ progress = wx.ProgressDialog("calculating photon numbers",
1021
+ "calculating...", maximum=100, parent=None,
1022
+ style=wx.PD_SMOOTH|wx.PD_AUTO_HIDE)
1023
+ nph = nPhotons(fitMod, fr, mdh, psfname=psfn, nmax=1e6,
1024
+ progressBar=progress, updateStep = 100)
1025
+ progress.Destroy()
1026
+ mapped.addColumn('nPhotons', nph)
1027
+ mapped.addColumn('fitResults_background', inp['fitResults_bg']+inp['fitResults_br'])
1028
+ #mapped.addColumn('sig',float(137.0)+np.zeros_like(inp['x'])) # this one is a straight kludge for mortensenError
1029
+
1030
+
1031
+ # propogate metadata, if present
1032
+ try:
1033
+ mapped.mdh = inp.mdh
1034
+ except AttributeError:
1035
+ pass
1036
+
1037
+ namespace[self.outputName] = mapped
1038
+
1039
+ @register_module('NPCAnalysisByID')
1040
+ class NPCAnalysisByID(ModuleBase):
1041
+
1042
+ inputName = Input('processed')
1043
+ outputName = Output('withNPCanalysis')
1044
+
1045
+ IDkey = CStr('objectID')
1046
+ SegmentThreshold = Int(10)
1047
+ SecondPass = Bool(False)
1048
+ FitMode = Enum(['abs','square'])
1049
+
1050
+ def execute(self, namespace):
1051
+ from PYMEcs.Analysis.NPC import estimate_nlabeled
1052
+ npcs = namespace[self.inputName]
1053
+ ids = npcs[self.IDkey]
1054
+ x = npcs['x']
1055
+ y = npcs['y']
1056
+
1057
+ uids,revids,idcounts = np.unique(ids,return_inverse=True,return_counts=True)
1058
+
1059
+ npcnlabeled = np.zeros_like(uids,dtype = 'int')
1060
+ npcradius = np.zeros_like(uids,dtype = 'float')
1061
+ for i, id in enumerate(uids):
1062
+ if (id > 0) and (idcounts[i] > 0):
1063
+ idx_thisid = ids == id
1064
+ xid = x[idx_thisid]
1065
+ yid = y[idx_thisid]
1066
+ npcnlabeled[i],npcradius[i] = estimate_nlabeled(xid,yid,nthresh=self.SegmentThreshold,
1067
+ do_plot=False,secondpass=self.SecondPass,
1068
+ fitmode=self.FitMode, return_radius=True)
1069
+
1070
+ npcnall = npcnlabeled[revids] # map back on all events
1071
+ npcradall = npcradius[revids] # map back on all events
1072
+ mapped = tabular.MappingFilter(npcs)
1073
+ mapped.addColumn('NPCnlabeled', npcnall)
1074
+ mapped.addColumn('NPCradius', npcradall)
1075
+
1076
+ try:
1077
+ mapped.mdh = NestedClassMDHandler(npcs.mdh)
1078
+ except AttributeError:
1079
+ mapped.mdh = NestedClassMDHandler() # make empty mdh
1080
+
1081
+ mapped.mdh['NPCAnalysis.EventThreshold'] = self.SegmentThreshold
1082
+ mapped.mdh['NPCAnalysis.RotationAlgorithm'] = self.FitMode
1083
+ mapped.mdh['NPCAnalysis.SecondPass'] = self.SecondPass
1084
+
1085
+ namespace[self.outputName] = mapped
1086
+
1087
+
1088
+ from scipy.stats import binned_statistic
1089
+ from scipy.signal import savgol_filter
1090
+ from scipy.interpolate import CubicSpline
1091
+
1092
+ # a function that is supposed to return a smoothed site trajectory as a function
1093
+ # as input we need site coordinates
1094
+ # the choice of savgol_filter for smoothing and CubicSpline for interpolation is a little arbitrary for now
1095
+ # and can be in future adjusted as needed
1096
+ # the bins need to be chosen in a robust manner - FIX
1097
+ def smoothed_site_func(t,coord_site,statistic='mean',bins=75,sgwindow_length=10,sgpolyorder=6,uselowess=False,lowessfrac=0.15):
1098
+ csitem, tbins, binnum = binned_statistic(t,coord_site,statistic=statistic,bins=bins)
1099
+ # replace NaNs with nearest neighbour values
1100
+ nanmask = np.isnan(csitem)
1101
+ csitem[nanmask] = np.interp(np.flatnonzero(nanmask), np.flatnonzero(~nanmask), csitem[~nanmask])
1102
+ # now we should have no NaNs left
1103
+ if np.any(np.isnan(csitem)):
1104
+ warn("csitem still contains NaNs, should not happen")
1105
+ tmid = 0.5*(tbins[0:-1]+tbins[1:])
1106
+ if uselowess:
1107
+ from statsmodels.nonparametric.smoothers_lowess import lowess
1108
+ filtered = lowess(csitem, tmid, frac=lowessfrac, return_sorted=False)
1109
+ else:
1110
+ filtered = savgol_filter(csitem,10,6)
1111
+ return CubicSpline(tmid,filtered)
1112
+
1113
+ from scipy.optimize import curve_fit
1114
+ def gfit_func(x, a0, a1, a2):
1115
+ z = (x - a1) / a2
1116
+ y = a0 * np.exp(-z**2 / 2)
1117
+ return y
1118
+
1119
+ def gaussfit(x,y,params=None):
1120
+ try:
1121
+ parameters, covar = curve_fit(gfit_func, x, y, params)
1122
+ except RuntimeError:
1123
+ return None, None
1124
+ else:
1125
+ return parameters, covar
1126
+
1127
+ def fit_binedges(tms,delta_s=100,width_s=500):
1128
+ tmin = tms.min()
1129
+ tmax = tms.max()
1130
+ delta_ms = 1e3*delta_s
1131
+ width_ms = 1e3*width_s
1132
+ numsamp = (tmax-tmin)/delta_ms
1133
+ t_samp = np.linspace(tmin,tmax,int(numsamp+0.5))
1134
+ edgeleft = np.maximum(t_samp - 0.5*width_ms, tmin)
1135
+ edgeright = np.minimum(t_samp + 0.5*width_ms, tmax)
1136
+ return (edgeleft, edgeright,t_samp)
1137
+
1138
+ def fit_sitedrift(tms,xsite,delta_s=100,width_s=500,bins=20):
1139
+ bel,ber,t_samp=fit_binedges(tms,delta_s=delta_s,width_s=width_s)
1140
+ pars = np.zeros((3,bel.size))
1141
+ errs = np.zeros_like(pars)
1142
+ for i in range(bel.size):
1143
+ tgood = (tms >= bel[i]) & (tms<=ber[i])
1144
+ hv, be = np.histogram(xsite[tgood],bins=bins)
1145
+ bctrs = 0.5*(be[:-1]+be[1:])
1146
+ par,cov = gaussfit(bctrs,hv,params=(10.,1.,3.)) # reasonable starting values
1147
+ pars[:,i] = par
1148
+ errs[:,i] = np.sqrt(np.diag(cov))
1149
+
1150
+ return t_samp, pars, errs
1151
+
1152
+ def site_fit_gaussian(tms,xsite,delta_s=100,width_s=500):
1153
+ t_samp, pars, errs = fit_sitedrift(tms,xsite,delta_s=delta_s,width_s=width_s)
1154
+ ifunc = CubicSpline(t_samp, pars[1,:])
1155
+ return ifunc
1156
+
1157
+ class ModuleBaseMDHmod(ModuleBase):
1158
+ """
1159
+ Exactly like ModuleBase but with a modified execute method
1160
+ that allows passing an 'mdh' key in the dict to modify mdh from run method
1161
+ """
1162
+ # NOTE: we override the 'execute' method here because we want to meddle with the metadata from within the run method;
1163
+ # a bit of a hack but mainly used
1164
+ # for experimentation in this case; we may solve this differently in future if really needed
1165
+ def execute(self, namespace):
1166
+ """
1167
+ takes a namespace (a dictionary like object) from which it reads its inputs and
1168
+ into which it writes outputs
1169
+
1170
+ NOTE: This was previously the function to define / override to make a module work. To support automatic metadata propagation
1171
+ and reduce the ammount of boiler plate, new modules should override the `run()` method instead.
1172
+ """
1173
+ from PYME.IO import MetaDataHandler
1174
+ inputs = {k: namespace[v] for k, v in self._input_traits.items()}
1175
+
1176
+ ret = self.run(**inputs)
1177
+ mdhret = None # the default unless 'mdh' is in returned dictironary
1178
+
1179
+ # convert output to a dictionary if needed
1180
+ if isinstance(ret, dict):
1181
+ out = {k : ret[v] for v, k in self._output_traits.items()}
1182
+ if 'mdh' in ret:
1183
+ mdhret = ret['mdh']
1184
+ elif isinstance(ret, List):
1185
+ out = {k : v for k, v in zip(self.outputs, ret)} #TODO - is this safe (is ordering consistent)
1186
+ else:
1187
+ # single output
1188
+ if len(self.outputs) > 1:
1189
+ raise RuntimeError('Module has multiple outputs, but .run() returns a single value')
1190
+
1191
+ out = {list(self.outputs)[0] : ret}
1192
+
1193
+ # complete metadata (injecting as appropriate)
1194
+ mdhin = MetaDataHandler.DictMDHandler(getattr(list(inputs.values())[0], 'mdh', None))
1195
+
1196
+ mdh = MetaDataHandler.DictMDHandler()
1197
+ self._params_to_metadata(mdh)
1198
+
1199
+ for v in out.values():
1200
+ if v is None:
1201
+ continue
1202
+ if getattr(v, 'mdh', None) is None:
1203
+ v.mdh = MetaDataHandler.DictMDHandler()
1204
+
1205
+ v.mdh.mergeEntriesFrom(mdhin) #merge, to allow e.g. voxel size overrides due to downsampling
1206
+ #print(v.mdh, mdh)
1207
+ v.mdh.copyEntriesFrom(mdh) # copy / overwrite with module processing parameters
1208
+ if mdhret is not None:
1209
+ v.mdh.copyEntriesFrom(mdhret)
1210
+
1211
+ namespace.update(out)
1212
+
1213
+
1214
+ @register_module('OrigamiSiteTrack')
1215
+ class OrigamiSiteTrack(ModuleBaseMDHmod):
1216
+ """
1217
+ Recipe module aimed at analysing oregami MINFLUX datasets. More docs to be added.
1218
+
1219
+ Inputs
1220
+ ------
1221
+ inputClusters: name of tabular input containing already
1222
+ "labeled" clusters used for lining up
1223
+ inputSites: name of tabular input containing already
1224
+ coalesced clusters that should match the inputClusters in terms of label etc
1225
+ inputAllPoints: [optional] tabular input that contains typically
1226
+ "all" data to which the calculated drift track should be applied
1227
+
1228
+ Outputs
1229
+ -------
1230
+ outputName: name of tabular output that will contain
1231
+ coordinate corrected
1232
+ version of inputClusters. Also contains a wealth of additional info, such
1233
+ as new error estimates based on site "scatter" and preserves the
1234
+ original and also precorrected versions of those properties.
1235
+ outputAllPoints: [optional] tabular output that contains the
1236
+ coordinate corrected (drift-corrected) version
1237
+ of inputAllPoints. Will only be generated if optional
1238
+ input inputAllPoints was supplied
1239
+
1240
+ """
1241
+ inputClusters = Input('siteclusters')
1242
+ inputSites = Input('sites')
1243
+ inputAllPoints = Input('') # optional input to apply the correction to
1244
+ outputName = Output('corrected_siteclusters')
1245
+ outputAllPoints = Output('') # optional output if our inputAllPoints was actually used
1246
+
1247
+ labelKey = CStr('dyeID',
1248
+ desc="property name of the ID indentifying site clusters; needs to match the ID name generated by DBSCAN clustering module") # should really be siteID
1249
+ smoothingBinWidthsSeconds = Float(200,label='temporal binning (s)',
1250
+ desc="parameter that sets the temporal binning (in units of s) when" +
1251
+ " estimating a smoothed drift trajectory from the data")
1252
+ savgolWindowLength = Int(10,label='savgol filter window length',
1253
+ desc="the window_length argument of the scipy.signal.savgol_filter function used in smoothing to obtain the drift trajectory")
1254
+ savgolPolyorder = Int(6,label='savgol filter polynomial order',
1255
+ desc="the polyorder argument of the scipy.signal.savgol_filter function used in smoothing to obtain the drift trajectory")
1256
+ binnedStatistic = Enum(['mean','median','Gaussian'],
1257
+ desc="statistic for smoothing when using binned_statistic function on data to obtain the drift trajectory by time windowing of the 'siteclouds'")
1258
+ gaussianBinSizeSeconds = Float(500)
1259
+ lowessFraction = Float(0.02)
1260
+ useLowess = Bool(False)
1261
+
1262
+ def run(self, inputClusters, inputSites,inputAllPoints=None):
1263
+ site_id = self.labelKey
1264
+ idsunique = inputSites[site_id].astype('i')
1265
+
1266
+ has_z = 'error_z' in inputClusters.keys()
1267
+ # centroid coordinates
1268
+ xc = inputSites['x']
1269
+ yc = inputSites['y']
1270
+ zc = inputSites['z']
1271
+
1272
+ # actual localisation coordinates
1273
+ x = inputClusters['x']
1274
+ y = inputClusters['y']
1275
+ z = inputClusters['z']
1276
+ t = inputClusters['t']
1277
+
1278
+ ids = inputClusters[site_id]
1279
+
1280
+ xsite = np.zeros_like(x)
1281
+ ysite = np.zeros_like(y)
1282
+ zsite = np.zeros_like(z)
1283
+ xerr = np.zeros_like(x)
1284
+ xerrnc = np.zeros_like(x)
1285
+ yerr = np.zeros_like(x)
1286
+ yerrnc = np.zeros_like(x)
1287
+ zerr = np.zeros_like(x)
1288
+ zerrnc = np.zeros_like(x)
1289
+
1290
+ for j,id in enumerate(idsunique):
1291
+ idx = ids == id
1292
+ xsite[idx] = x[idx]-xc[j]
1293
+ ysite[idx] = y[idx]-yc[j]
1294
+ zsite[idx] = z[idx]-zc[j]
1295
+ xerrnc[idx] = np.std(x[idx])
1296
+ yerrnc[idx] = np.std(y[idx])
1297
+ zerrnc[idx] = np.std(z[idx])
1298
+
1299
+ trange = t.max() - t.min()
1300
+ delta_ms = self.smoothingBinWidthsSeconds * 1e3 # 200 s
1301
+ nbins = int(trange/delta_ms)
1302
+
1303
+ if self.binnedStatistic in ['mean','median']:
1304
+ # we should check that with this choice of nbins we get no issues! (i.e. too few counts in some bins)
1305
+ c_xsite = smoothed_site_func(t,xsite,bins=nbins,statistic=self.binnedStatistic,
1306
+ sgwindow_length=self.savgolWindowLength,sgpolyorder=self.savgolPolyorder,
1307
+ uselowess=self.useLowess,lowessfrac=self.lowessFraction)
1308
+ c_ysite = smoothed_site_func(t,ysite,bins=nbins,statistic=self.binnedStatistic,
1309
+ sgwindow_length=self.savgolWindowLength,sgpolyorder=self.savgolPolyorder,
1310
+ uselowess=self.useLowess,lowessfrac=self.lowessFraction)
1311
+ if has_z:
1312
+ c_zsite = smoothed_site_func(t,zsite,bins=nbins,statistic=self.binnedStatistic,
1313
+ sgwindow_length=self.savgolWindowLength,sgpolyorder=self.savgolPolyorder,
1314
+ uselowess=self.useLowess,lowessfrac=self.lowessFraction)
1315
+ else:
1316
+ c_xsite = site_fit_gaussian(t,xsite,delta_s=self.smoothingBinWidthsSeconds,width_s=self.gaussianBinSizeSeconds)
1317
+ c_ysite = site_fit_gaussian(t,ysite,delta_s=self.smoothingBinWidthsSeconds,width_s=self.gaussianBinSizeSeconds)
1318
+ if has_z:
1319
+ c_zsite = site_fit_gaussian(t,zsite,delta_s=self.smoothingBinWidthsSeconds,width_s=self.gaussianBinSizeSeconds)
1320
+
1321
+ for j,id in enumerate(idsunique):
1322
+ idx = ids == id
1323
+ xerr[idx] = np.std(x[idx]-c_xsite(t[idx]))
1324
+ yerr[idx] = np.std(y[idx]-c_ysite(t[idx]))
1325
+ if has_z:
1326
+ zerr[idx] = np.std(z[idx]-c_zsite(t[idx]))
1327
+
1328
+ # note to self: we shall preserve the site coordinates in the new data source
1329
+ # new properties to create: [xyz]_site, [xyz]_ori, new [xyz]
1330
+
1331
+ mapped_ds = tabular.MappingFilter(inputClusters)
1332
+ mapped_ds.setMapping('x_ori', 'x')
1333
+ mapped_ds.setMapping('y_ori', 'y')
1334
+ if has_z:
1335
+ mapped_ds.setMapping('z_ori', 'z')
1336
+
1337
+ mapped_ds.setMapping('error_x_ori', 'error_x')
1338
+ mapped_ds.setMapping('error_y_ori', 'error_y')
1339
+ if has_z:
1340
+ mapped_ds.setMapping('error_z_ori', 'error_z')
1341
+
1342
+ # mapped_ds.addColumn('x_ori', x)
1343
+ # mapped_ds.addColumn('y_ori', y)
1344
+ # mapped_ds.addColumn('z_ori', z)
1345
+
1346
+ mapped_ds.addColumn('x_site_nc', xsite)
1347
+ mapped_ds.addColumn('y_site_nc', ysite)
1348
+ if has_z:
1349
+ mapped_ds.addColumn('z_site_nc', zsite)
1350
+
1351
+ mapped_ds.addColumn('x_site', xsite-c_xsite(t))
1352
+ mapped_ds.addColumn('y_site', ysite-c_ysite(t))
1353
+ if has_z:
1354
+ mapped_ds.addColumn('z_site', zsite-c_zsite(t))
1355
+
1356
+ mapped_ds.addColumn('error_x_nc', xerrnc)
1357
+ mapped_ds.addColumn('error_y_nc', yerrnc)
1358
+ if has_z:
1359
+ mapped_ds.addColumn('error_z_nc', zerrnc)
1360
+
1361
+ mapped_ds.addColumn('error_x', xerr)
1362
+ mapped_ds.addColumn('error_y', yerr)
1363
+ if has_z:
1364
+ mapped_ds.addColumn('error_z', zerr)
1365
+
1366
+ mapped_ds.addColumn('x', x-c_xsite(t))
1367
+ mapped_ds.addColumn('y', y-c_ysite(t))
1368
+ if has_z:
1369
+ mapped_ds.addColumn('z', z-c_zsite(t))
1370
+
1371
+ if 'driftx' in inputClusters.keys():
1372
+ mapped_ds.setMapping('driftx_ori', 'driftx')
1373
+ mapped_ds.setMapping('drifty_ori', 'drifty')
1374
+ if has_z:
1375
+ mapped_ds.setMapping('driftz_ori', 'driftz')
1376
+
1377
+ mapped_ds.addColumn('driftx', c_xsite(t))
1378
+ mapped_ds.addColumn('drifty', c_ysite(t))
1379
+ if has_z:
1380
+ mapped_ds.addColumn('driftz', c_zsite(t))
1381
+
1382
+ if inputAllPoints is not None:
1383
+ mapped_ap = tabular.MappingFilter(inputAllPoints)
1384
+ # actual localisation coordinates
1385
+ x = inputAllPoints['x']
1386
+ y = inputAllPoints['y']
1387
+ if has_z:
1388
+ z = inputAllPoints['z']
1389
+ t = inputAllPoints['t']
1390
+
1391
+ mapped_ap.addColumn('x', x-c_xsite(t))
1392
+ mapped_ap.addColumn('y', y-c_ysite(t))
1393
+ if has_z:
1394
+ mapped_ap.addColumn('z', z-c_zsite(t))
1395
+
1396
+ mapped_ap.addColumn('driftx', c_xsite(t))
1397
+ mapped_ap.addColumn('drifty', c_ysite(t))
1398
+ if has_z:
1399
+ mapped_ap.addColumn('driftz', c_zsite(t))
1400
+ else:
1401
+ # how to deal with an "optional" output
1402
+ # this would be a dummy assignment in the absence of inputAllPoints
1403
+ # mapped_ap = tabular.MappingFilter(inputClusters)
1404
+ mapped_ap = None # returning none in this case seems better and appears to work
1405
+
1406
+ return {'outputName': mapped_ds, 'outputAllPoints' : mapped_ap, 'mdh' : None } # pass proper mdh instead of None if metadata output needed
1407
+
1408
+
1409
+ @register_module('SiteErrors')
1410
+ class SiteErrors(ModuleBase):
1411
+ inputSites = Input('siteclumps')
1412
+ output = Output('with_site_errors') # localisations with site error info
1413
+
1414
+ labelKey = CStr('siteID',
1415
+ desc="property name of the ID identifying site clusters; needs to match the ID name generated by DBSCAN clustering module") # should really be siteID
1416
+
1417
+ def run(self, inputSites):
1418
+ site_id = self.labelKey
1419
+ ids = inputSites[site_id].astype('i')
1420
+ idsunique = np.unique(ids)
1421
+ has_z = 'error_z' in inputSites.keys()
1422
+
1423
+ x = inputSites['x']
1424
+ y = inputSites['y']
1425
+ z = inputSites['z']
1426
+ xerr = np.zeros_like(x)
1427
+ yerr = np.zeros_like(x)
1428
+ zerr = np.zeros_like(x)
1429
+
1430
+ for j,id in enumerate(idsunique):
1431
+ idx = ids == id
1432
+ xerr[idx] = np.std(x[idx])
1433
+ yerr[idx] = np.std(y[idx])
1434
+ if has_z:
1435
+ zerr[idx] = np.std(z[idx])
1436
+
1437
+ mapped_ds = tabular.MappingFilter(inputSites)
1438
+ mapped_ds.addColumn('site_error_x', xerr)
1439
+ mapped_ds.addColumn('site_error_y', yerr)
1440
+ if has_z:
1441
+ mapped_ds.addColumn('site_error_z', zerr)
1442
+ return mapped_ds
1443
+
1444
+
1445
+
1446
+ import numpy as np
1447
+ import scipy.special
1448
+
1449
+ @register_module('MINFLUXcolours')
1450
+ class MINFLUXcolours(ModuleBase):
1451
+
1452
+ inputLocalizations = Input('localizations')
1453
+ output = Output('localizations_mcolour') # localisations with MINFLUX colour info
1454
+
1455
+ dcrisgfrac = Bool(False)
1456
+
1457
+ def run(self,inputLocalizations):
1458
+
1459
+ mapped_ds = tabular.MappingFilter(inputLocalizations)
1460
+ mapped_ds.setMapping('A','nPhotons')
1461
+
1462
+ if self.dcrisgfrac:
1463
+ mapped_ds.setMapping('fitResults_Ag','dcr*nPhotons')
1464
+ mapped_ds.setMapping('fitResults_Ar','(1-dcr)*nPhotons')
1465
+ else:
1466
+ mapped_ds.setMapping('fitResults_Ag','nPhotons*dcr/(1+dcr)')
1467
+ mapped_ds.setMapping('fitResults_Ar','nPhotons/(1+dcr)')
1468
+
1469
+
1470
+ mapped_ds.setMapping('fitError_Ag','1*sqrt(fitResults_Ag/1)')
1471
+ mapped_ds.setMapping('fitError_Ar','1*sqrt(fitResults_Ar/1)')
1472
+
1473
+ mapped_ds.setMapping('gFrac', 'fitResults_Ag/(fitResults_Ag + fitResults_Ar)')
1474
+ mapped_ds.setMapping('error_gFrac',
1475
+ 'sqrt((fitError_Ag/fitResults_Ag)**2 + (fitError_Ag**2 + fitError_Ar**2)/(fitResults_Ag + fitResults_Ar)**2)' +
1476
+ '*fitResults_Ag/(fitResults_Ag + fitResults_Ar)')
1477
+
1478
+ sg = mapped_ds['fitError_Ag']
1479
+ sr = mapped_ds['fitError_Ar']
1480
+ g = mapped_ds['fitResults_Ag']
1481
+ r = mapped_ds['fitResults_Ar']
1482
+ I = mapped_ds['A']
1483
+
1484
+ colNorm = np.sqrt(2 * np.pi) * sg * sr / (2 * np.sqrt(sg ** 2 + sr ** 2) * I) * (
1485
+ scipy.special.erf((sg ** 2 * r + sr ** 2 * (I - g)) / (np.sqrt(2) * sg * sr * np.sqrt(sg ** 2 + sr ** 2)))
1486
+ - scipy.special.erf((sg ** 2 * (r - I) - sr ** 2 * g) / (np.sqrt(2) * sg * sr * np.sqrt(sg ** 2 + sr ** 2))))
1487
+
1488
+ mapped_ds.addColumn('ColourNorm', colNorm)
1489
+
1490
+ return mapped_ds
1491
+
1492
+ def get_clump_property(ids, prop, statistic='mean'):
1493
+ maxid = int(ids.max())
1494
+ edges = -0.5+np.arange(maxid+2)
1495
+ idrange = (0,maxid)
1496
+
1497
+ propclump, bin_edge, binno = binned_statistic(ids, prop, statistic=statistic,
1498
+ bins=edges, range=idrange)
1499
+ propclump[np.isnan(propclump)] = 1000.0 # (mark as huge value)
1500
+ mean_events = propclump[ids]
1501
+ return mean_events
1502
+
1503
+ @register_module("DcrColour")
1504
+ class DcrColour(ModuleBase):
1505
+ input = Input('localizations')
1506
+ output = Output('colour_mapped')
1507
+
1508
+ dcr_average_over_trace_threshold = Float(-1)
1509
+ dcr_majority_vote = Bool(False)
1510
+ dcr_majority_minimal_confidence = Float(0.1)
1511
+
1512
+ def run(self, input):
1513
+ mdh = input.mdh
1514
+
1515
+ output = tabular.MappingFilter(input)
1516
+ output.mdh = mdh
1517
+
1518
+ if self.dcr_average_over_trace_threshold > 0 and self.dcr_average_over_trace_threshold < 1 and 'dcr' in output.keys():
1519
+ #ratiometric
1520
+ dcr_trace = get_clump_property(input['clumpIndex'],input['dcr'])
1521
+ output.setMapping('ColourNorm', '1.0 + 0*dcr')
1522
+ output.addColumn('dcr_trace',dcr_trace)
1523
+ # the below calculates the "majority vote" (dcr_major = fraction of trace in short channel)
1524
+ # dcr_mconf is the "confidence" associated with the majority vote
1525
+ dcr_major = get_clump_property(input['clumpIndex'],1.0*(input['dcr'] < self.dcr_average_over_trace_threshold))
1526
+ dcr_mconf = 2.0*np.maximum(dcr_major,1.0-dcr_major)-1.0
1527
+ output.addColumn('dcr_major',dcr_major)
1528
+ output.addColumn('dcr_mconf',dcr_mconf)
1529
+ if self.dcr_majority_vote:
1530
+ output.addColumn('p_near', 1.0*(dcr_major > 0.5)*(dcr_mconf >= self.dcr_majority_minimal_confidence))
1531
+ output.addColumn('p_far', 1.0*(dcr_major <= 0.5)*(dcr_mconf >= self.dcr_majority_minimal_confidence))
1532
+ else:
1533
+ output.addColumn('p_near', 1.0*(dcr_trace < self.dcr_average_over_trace_threshold))
1534
+ output.addColumn('p_far', 1.0*(dcr_trace >= self.dcr_average_over_trace_threshold)*(dcr_trace <= 1.0))
1535
+
1536
+ #output.setMapping('p_near', '1.0*(dcr < %.2f)' % (self.dcr_threshold))
1537
+ #output.setMapping('p_far', '1.0*(dcr >= %.2f)' % (self.dcr_threshold))
1538
+
1539
+ cached_output = tabular.CachingResultsFilter(output)
1540
+ # cached_output.mdh = output.mdh
1541
+ return cached_output
1542
+
1543
+
1544
+ from pathlib import Path
1545
+ def check_mbm_name(mbmfilename,timestamp,endswith='__MBM-beads'):
1546
+ if timestamp is None:
1547
+ return True
1548
+ mbmp = Path(mbmfilename)
1549
+
1550
+ # should return False if warning is necessary
1551
+ return mbmp.stem.startswith(timestamp) and (mbmp.stem.endswith(endswith) or mbmp.suffix.endswith('.zip'))
1552
+
1553
+ def get_bead_dict_from_mbm(mbm):
1554
+ beads = {}
1555
+ raw_beads = mbm._raw_beads
1556
+ for bead in raw_beads:
1557
+ beads[bead] = {}
1558
+ beads[bead]['x'] = 1e9*raw_beads[bead]['pos'][:,0]
1559
+ beads[bead]['y'] = 1e9*raw_beads[bead]['pos'][:,1]
1560
+ beads[bead]['z'] = 1e9*raw_beads[bead]['pos'][:,2]
1561
+ beads[bead]['A'] = raw_beads[bead]['str']
1562
+ beads[bead]['t'] = np.asarray(1e3*raw_beads[bead]['tim'],dtype=int)
1563
+ beads[bead]['tim'] = raw_beads[bead]['tim']
1564
+ if 'tid' in raw_beads[bead].dtype.fields:
1565
+ beads[bead]['tid'] = raw_beads[bead]['tid']
1566
+ if 'gri' in raw_beads[bead].dtype.fields:
1567
+ beads[bead]['tid'] = raw_beads[bead]['gri']
1568
+
1569
+
1570
+ x = np.empty((0))
1571
+ y = np.empty((0))
1572
+ z = np.empty((0))
1573
+ t = np.empty((0),int)
1574
+ tid = np.empty((0),int)
1575
+ beadID = np.empty((0),int)
1576
+ objectID = np.empty((0),int)
1577
+ A = np.empty((0))
1578
+ tim = np.empty((0))
1579
+ good = np.empty((0),int)
1580
+
1581
+ for bead in beads:
1582
+ beadid = int(bead[1:])
1583
+ beadisgood = mbm.beadisgood[bead]
1584
+ # print('beadid %d' % beadid)
1585
+ x = np.append(x,beads[bead]['x'])
1586
+ y = np.append(y,beads[bead]['y'])
1587
+ z = np.append(z,beads[bead]['z'])
1588
+ t = np.append(t,beads[bead]['t'])
1589
+ tim = np.append(tim,beads[bead]['tim'])
1590
+ tid = np.append(tid,beads[bead]['tid'])
1591
+ A = np.append(A,beads[bead]['A'])
1592
+ beadID = np.append(beadID,np.full_like(beads[bead]['x'],beadid,dtype=int))
1593
+ # objectIDs start at 1, beadIDs can start at 0
1594
+ objectID = np.append(objectID,np.full_like(beads[bead]['x'],beadid+1,dtype=int))
1595
+ good = np.append(good,np.full_like(beads[bead]['x'],beadisgood,dtype=int))
1596
+
1597
+ return dict(x=x,y=y,z=z,t=t,tim=tim,beadID=beadID,tid=tid,
1598
+ A=A,good=good,objectID=objectID)
1599
+
1600
+ # remove this once sessionpath PR is accepted!!
1601
+ try:
1602
+ from PYME.LMVis.sessionpaths import register_path_modulechecks
1603
+ register_path_modulechecks('PYMEcs.MBMcorrection','mbmfile','mbmsettings')
1604
+ except ImportError:
1605
+ pass
1606
+
1607
+ # code on suggestion from https://blog.finxter.com/5-best-ways-to-compute-the-hash-of-a-python-tuple/
1608
+ import hashlib
1609
+ def tuple_hash(tuple_obj):
1610
+ hasher = hashlib.sha256()
1611
+ hasher.update(repr(tuple_obj).encode())
1612
+ return hasher.hexdigest()
1613
+
1614
+ @register_module('MBMcorrection')
1615
+ class MBMcorrection(ModuleBaseMDHmod):
1616
+ inputLocalizations = Input('localizations')
1617
+ output = Output('mbm_corrected')
1618
+ outputTracks = Output('mbm_tracks')
1619
+ outputTracksCorr = Output('mbm_tracks_corrected')
1620
+
1621
+ mbmfile = FileOrURI('',filter=['Npz (*.npz)|*.npz','Zip (*.zip)|*.zip'])
1622
+ mbmsettings = FileOrURI('',filter=['Json (*.json)|*.json'])
1623
+ mbmfilename_checks = Bool(True)
1624
+
1625
+ Median_window = Int(5)
1626
+ MBM_lowess_fraction = Float(0.1,label='lowess fraction for MBM smoothing',
1627
+ desc='lowess fraction used for smoothing of mean MBM trajectories (default 0.1); 0 = no smoothing')
1628
+ MBM_beads = List() # this is a dummy to make sure older style PVS files are read ok - TODO: get rid off at some stage!!!
1629
+ _MBM_beads = List() # this one does the real work and with the leading "_" is NOT treated as a parameter that "fires" the module!
1630
+ _mbm_allbeads = List()
1631
+ _initialized = Bool(False)
1632
+
1633
+ _mbm_cache = {}
1634
+ _lowess_cache = {}
1635
+
1636
+
1637
+ def lowess_cachetuple(self):
1638
+ mbm = self.getmbm()
1639
+ return (Path(self.mbmfile).name,self.MBM_lowess_fraction,self.Median_window,str(mbm.beadisgood))
1640
+
1641
+ def lowess_cachekey(self):
1642
+ return tuple_hash(self.lowess_cachetuple())
1643
+
1644
+ def lowess_cachehit(self):
1645
+ cachekey = self.lowess_cachekey()
1646
+ if cachekey in self._lowess_cache:
1647
+ logger.debug("CACHEHIT from in-memory-cache; cache tuple: %s" % str(self.lowess_cachetuple()))
1648
+ return self._lowess_cache[cachekey]
1649
+ elif self.lowess_chacheread():
1650
+ logger.debug("CACHEHIT from file-cache; cache tuple: %s" % str(self.lowess_cachetuple()))
1651
+ return self._lowess_cache[cachekey]
1652
+ else:
1653
+ logger.debug("NO CACHEHIT!!! cache tuple: %s" % str(self.lowess_cachetuple()))
1654
+ return None
1655
+
1656
+ def lowess_cachestore(self,axis,value):
1657
+ cachekey = self.lowess_cachekey()
1658
+ if cachekey not in self._lowess_cache:
1659
+ self._lowess_cache[cachekey] = {}
1660
+ self._lowess_cache[cachekey][axis] = value
1661
+
1662
+ def lowess_cachesave(self):
1663
+ cachehit = self.lowess_cachehit()
1664
+ if cachehit is not None:
1665
+ fpath = self.lowess_cachefilepath()
1666
+ np.savez(str(fpath),**cachehit)
1667
+
1668
+ def lowess_chacheread(self):
1669
+ cachekey = self.lowess_cachekey()
1670
+ fpath = self.lowess_cachefilepath()
1671
+ if fpath.exists():
1672
+ self._lowess_cache[cachekey] = np.load(str(fpath))
1673
+ return True
1674
+ else:
1675
+ return False
1676
+
1677
+ def lowess_cachefilepath(self):
1678
+ cachekey = self.lowess_cachekey()
1679
+ from pathlib import Path
1680
+ fdir = Path(self.mbmfile).parent
1681
+ fpath = fdir / (".mbm_lowess_%s.npz" % cachekey)
1682
+ return fpath
1683
+
1684
+ def getmbm(self):
1685
+ mbmkey = self.mbmfile
1686
+ if mbmkey in self._mbm_cache:
1687
+ return self._mbm_cache[mbmkey]
1688
+ else:
1689
+ return None
1690
+
1691
+ # cached lowess calc with time coordinate retrieval
1692
+ def lowess_calc(self,axis):
1693
+ mbm = self.getmbm()
1694
+ axismean = mbm.mean(axis)
1695
+ axismean_g = axismean[~np.isnan(axismean)]
1696
+ t_g = mbm.t[~np.isnan(axismean)]
1697
+ cachehit = self.lowess_cachehit() # LOWESS CACHE OP
1698
+ if self.MBM_lowess_fraction > 1e-5:
1699
+ from statsmodels.nonparametric.smoothers_lowess import lowess
1700
+ if cachehit is not None and axis in cachehit: # not all axes may have been calculated yet, so check for axis!
1701
+ axismean_sm = cachehit[axis] # LOWESS CACHE OP
1702
+ else:
1703
+ axismean_sm = lowess(axismean_g, t_g, frac=self.MBM_lowess_fraction,
1704
+ return_sorted=False)
1705
+ self.lowess_cachestore(axis,axismean_sm) # LOWESS CACHE OP
1706
+ return (t_g,axismean_sm)
1707
+ else:
1708
+ return (t_g,axismean_g)
1709
+
1710
+ def run(self,inputLocalizations):
1711
+ import json
1712
+ from PYME.IO import unifiedIO
1713
+ from pathlib import Path
1714
+ from PYMEcs.Analysis.MBMcollection import MBMCollectionDF
1715
+
1716
+ mapped_ds = tabular.MappingFilter(inputLocalizations)
1717
+
1718
+ if self.mbmfile != '':
1719
+ mbmkey = self.mbmfile
1720
+ if mbmkey not in self._mbm_cache.keys():
1721
+ from PYMEcs.IO.MINFLUX import foreshortening
1722
+ mbm = MBMCollectionDF(name=Path(self.mbmfile).stem,filename=self.mbmfile,
1723
+ foreshortening=foreshortening)
1724
+ logger.debug("reading in mbm data from file and made new mbm object")
1725
+ logger.debug("mbm beadisgood: %s" % mbm.beadisgood)
1726
+ self._mbm_cache[mbmkey] = mbm
1727
+ self._initialized = False # new file, mark as uninitialized
1728
+ else:
1729
+ mbm = self._mbm_cache[mbmkey]
1730
+
1731
+ if len(self._MBM_beads) == 0 or not self._initialized: # make sure we have a reasonable _MBM_beads list, i.e. re-initialiaise if not already done
1732
+ self._mbm_allbeads = [bead for bead in mbm.beadisgood if np.sum(np.logical_not(np.isnan(mbm.beads['x'][bead]))) > 1] # exclude "empty" trajectories
1733
+ self._MBM_beads = self._mbm_allbeads
1734
+
1735
+ mbmsettingskey = self.mbmsettings
1736
+ if mbmsettingskey != '':
1737
+ if mbmsettingskey not in self._mbm_cache.keys():
1738
+ s = unifiedIO.read(self.mbmsettings)
1739
+ mbmconf = json.loads(s)
1740
+ self._mbm_cache[mbmsettingskey] = mbmconf
1741
+ self._initialized = False # new file, mark as uninitialized
1742
+ # use of the _initialized flag should enable changes via the GUI tickboxes to work *after* a settings file has been read
1743
+ if not self._initialized:
1744
+ mbmconf = self._mbm_cache[mbmsettingskey]
1745
+ for bead in mbmconf['beads']:
1746
+ mbm.beadisgood[bead] = mbmconf['beads'][bead]
1747
+ self._MBM_beads = [bead for bead in mbmconf['beads'].keys() if mbmconf['beads'][bead] and bead in self._mbm_allbeads]
1748
+
1749
+ self._initialized = True
1750
+
1751
+ # NOTE: the bead bookkeeping logic needs a good double-check!!!
1752
+ # also add some comments to make logic clearer
1753
+ # in a second pass (if just loaded from mbmconf) set beadisgood to the useful self._MBM_beads subset
1754
+ # or, if _MBM_beads was changed via the GUI interface, propagate the choices into mbm.beadisgood here
1755
+ for bead in mbm.beadisgood:
1756
+ mbm.beadisgood[bead] = bead in self._MBM_beads
1757
+
1758
+ anygood = False
1759
+ for bead in mbm.beadisgood:
1760
+ if mbm.beadisgood[bead]:
1761
+ anygood = True
1762
+ if not anygood:
1763
+ raise RuntimeError("no 'good' bead selected, giving up...")
1764
+
1765
+ mbm.median_window = self.Median_window
1766
+
1767
+ bead_ds_dict = get_bead_dict_from_mbm(mbm)
1768
+ tnew = 1e-3*inputLocalizations['t']
1769
+ mbmcorr = {}
1770
+ mbmtrack_corr = {}
1771
+ for axis in ['x','y','z']:
1772
+ tsm, axismean_sm = self.lowess_calc(axis) # cache based lowess calculation
1773
+ axis_interp = np.interp(tnew,tsm,axismean_sm)
1774
+ mbmcorr[axis] = axis_interp
1775
+ mbmtrack_corr[axis] = np.interp(bead_ds_dict['tim'],tsm,axismean_sm)
1776
+ axis_nc = "%s_nc" % axis
1777
+ mapped_ds.addColumn('mbm%s' % axis, mbmcorr[axis])
1778
+ if axis_nc in inputLocalizations.keys():
1779
+ mapped_ds.addColumn(axis,inputLocalizations[axis_nc] - mbmcorr[axis])
1780
+
1781
+ if self.mbmfilename_checks:
1782
+ if not check_mbm_name(self.mbmfile,inputLocalizations.mdh.get('MINFLUX.TimeStamp')):
1783
+ warn("check MBM filename (%s) vs Series timestamp (%s)" %
1784
+ (Path(self.mbmfile).name,inputLocalizations.mdh.get('MINFLUX.TimeStamp')))
1785
+ if mbmsettingskey != '' and not check_mbm_name(self.mbmsettings,inputLocalizations.mdh.get('MINFLUX.TimeStamp'),endswith='npz-settings'):
1786
+ warn("check MBM settings filename (%s) vs Series timestamp (%s)" %
1787
+ (Path(self.mbmsettings).name,inputLocalizations.mdh.get('MINFLUX.TimeStamp')))
1788
+
1789
+ from PYME.IO import MetaDataHandler
1790
+ MBMmdh = MetaDataHandler.DictMDHandler()
1791
+ MBMmdh['Processing.MBMcorrection.mbm'] = mbm
1792
+
1793
+ from PYME.IO.tabular import DictSource
1794
+ mbmtrack_ds = DictSource(bead_ds_dict)
1795
+ mbmtrack_dscorr = tabular.MappingFilter(mbmtrack_ds)
1796
+ for axis in ['x','y','z']:
1797
+ mbmtrack_dscorr.addColumn(axis,mbmtrack_ds[axis] - mbmtrack_corr[axis])
1798
+
1799
+ return dict(output=mapped_ds, outputTracks=mbmtrack_ds,
1800
+ outputTracksCorr=mbmtrack_dscorr, mdh=MBMmdh)
1801
+ # mapped_ds.mbm = mbm # attach mbm object to the output
1802
+
1803
+ return dict(output=mapped_ds, outputTracks=None,
1804
+ outputTracksCorr=None, mdh=None) # return empty slots for outputs etc in this case
1805
+
1806
+ @property
1807
+ def default_view(self):
1808
+ from traitsui.api import View, Group, Item, CheckListEditor
1809
+ from PYME.ui.custom_traits_editors import CBEditor
1810
+
1811
+ return View(Item('inputLocalizations', editor=CBEditor(choices=self._namespace_keys)),
1812
+ Item('_'),
1813
+ Item('mbmfile'),
1814
+ Item('mbmsettings'),
1815
+ Item('mbmfilename_checks'),
1816
+ Item('_MBM_beads', editor=CheckListEditor(values=self._mbm_allbeads,cols=4),
1817
+ style='custom',
1818
+ ),
1819
+ Item('Median_window'),
1820
+ Item('MBM_lowess_fraction'),
1821
+ Item('_'),
1822
+ Item('output'),
1823
+ Item('outputTracks'),
1824
+ Item('outputTracksCorr'),
1825
+ buttons=['OK'])
1826
+
1827
+ from PYMEcs.Analysis.NPC import NPCSetContainer
1828
+
1829
+ @register_module('NPCAnalysisInput')
1830
+ class NPCAnalysisInput(ModuleBaseMDHmod):
1831
+ inputLocalizations = Input('selected_npcs')
1832
+ output = Output('with_npcs')
1833
+ outputGallery = Output('npc_gallery')
1834
+ outputSegments = Output('npc_segments')
1835
+ outputTemplates = Output('npc_templates')
1836
+
1837
+ NPC_analysis_file = FileOrURI('',filter = ['Pickle (*.pickle)|*.pickle'])
1838
+ NPC_Gallery_Arrangement = Enum(['SingleAverageSBS','TopOverBottom','TopBesideBottom','SingleAverage'],
1839
+ desc="how to arrange 3D NPC parts in NPC gallery; SBS = SideBySide top and bottom")
1840
+ NPC_hide = Bool(False,desc="if true hide this NPCset so you can fit again etc",label='hide NPCset')
1841
+ NPC_enforce_8foldsym = Bool(False,desc="if set enforce 8-fold symmetry by adding 8 rotated copies of each NPC",label='enforce 8-fold symmetry')
1842
+ NPCRotationAngle = Enum(['positive','negative','zero'],desc="way to treat rotation for NPC gallery")
1843
+ gallery_x_offset = Float(0)
1844
+ gallery_y_offset = Float(0)
1845
+ NPC_version_check = Enum(['minimum version','exact version','no check'])
1846
+ NPC_target_version = CStr('0.9')
1847
+
1848
+ filter_npcs = Bool(False)
1849
+ fitmin_max = Float(5.99)
1850
+ min_labeled = Int(1)
1851
+ labeling_threshold = Int(1)
1852
+ height_max = Float(90.0)
1853
+
1854
+ rotation_locked = Bool(True,label='NPC rotation estimate locked (3D)',
1855
+ desc="when estimating the NPC rotation (pizza slice boundaries), the top and bottom rings in 3D should be locked, "+
1856
+ "i.e. have the same rotation from the underlying structure")
1857
+ radius_uncertainty = Float(20.0,label="Radius scatter in nm",
1858
+ desc="a radius scatter that determines how much localisations can deviate from the mean ring radius "+
1859
+ "and still be accepted as part of the NPC; allows for distortions of NPCs and localisation errors")
1860
+ zclip = Float(55.0,label='Z-clip value from center of NPC',
1861
+ desc='the used zrange from the (estimated) center of the NPC, from (-zclip..+zclip) in 3D fitting')
1862
+
1863
+ _npc_cache = {}
1864
+
1865
+ def run(self,inputLocalizations):
1866
+ from PYME.IO import unifiedIO
1867
+ from pathlib import Path
1868
+
1869
+ mapped_ds = tabular.MappingFilter(inputLocalizations)
1870
+
1871
+ if self.NPC_analysis_file != '':
1872
+ npckey = self.NPC_analysis_file
1873
+ if npckey not in self._npc_cache.keys():
1874
+ from PYMEcs.IO.NPC import load_NPC_set, check_npcset_version
1875
+ mdh = inputLocalizations.mdh
1876
+ npcs = load_NPC_set(self.NPC_analysis_file,ts=mdh.get('MINFLUX.TimeStamp'),
1877
+ foreshortening=mdh.get('MINFLUX.Foreshortening',1.0))
1878
+ logger.debug("reading in npcset object from file %s" % self.NPC_analysis_file)
1879
+ if self.NPC_version_check != 'no check' and not check_npcset_version(npcs,self.NPC_target_version,mode=self.NPC_version_check):
1880
+ warn('requested npcset object version %s, got version %s' %
1881
+ (self.NPC_target_version,check_npcset_version(npcs,self.NPC_target_version,mode='return_version')))
1882
+ self._npc_cache[npckey] = npcs
1883
+ else:
1884
+ npcs = self._npc_cache[npckey]
1885
+
1886
+ from PYME.IO import MetaDataHandler
1887
+ NPCmdh = MetaDataHandler.DictMDHandler()
1888
+ NPCmdh['Processing.NPCAnalysisInput.npcs'] = NPCSetContainer(npcs)
1889
+ if self.filter_npcs:
1890
+ def npc_height(npc):
1891
+ height = npc.get_glyph_height() / (0.01*npc.opt_result.x[6])
1892
+ return height
1893
+
1894
+ if 'templatemode' in dir(npcs) and npcs.templatemode == 'detailed':
1895
+ rotation = 22.5 # this value may need adjustment
1896
+ else:
1897
+ rotation = None
1898
+ for npc in npcs.npcs:
1899
+ nt,nb = npc.nlabeled(nthresh=self.labeling_threshold,
1900
+ dr=self.radius_uncertainty,
1901
+ rotlocked=self.rotation_locked,
1902
+ zrange=self.zclip,
1903
+ rotation=rotation)
1904
+ npc.measures = [nt,nb]
1905
+ import copy
1906
+ npcs_filtered = copy.copy(npcs)
1907
+ vnpcs = [npc for npc in npcs.npcs if npc.opt_result.fun/npc.npts.shape[0] < self.fitmin_max]
1908
+ vnpcs = [npc for npc in vnpcs if np.sum(npc.measures) >= self.min_labeled]
1909
+ vnpcs = [npc for npc in vnpcs if npc_height(npc) <= self.height_max]
1910
+ npcs_filtered.npcs = vnpcs
1911
+ NPCmdh['Processing.NPCAnalysisInput.npcs_filtered'] = NPCSetContainer(npcs_filtered)
1912
+ else:
1913
+ # so we can pass this variable to a few calls and be sure to get the filtered version if filtering is requested
1914
+ # as not requested use the original npcs in place of filtered subset
1915
+ npcs_filtered = npcs
1916
+ from PYMEcs.Analysis.NPC import mk_NPC_gallery, mk_npctemplates
1917
+ outputGallery, outputSegments = mk_NPC_gallery(npcs_filtered,self.NPC_Gallery_Arrangement,
1918
+ self.zclip,self.NPCRotationAngle,
1919
+ xoffs=self.gallery_x_offset,
1920
+ yoffs=self.gallery_y_offset,
1921
+ enforce_8foldsym=self.NPC_enforce_8foldsym)
1922
+ outputTemplates = mk_npctemplates(npcs_filtered)
1923
+
1924
+ if npcs is not None and 'objectID' in inputLocalizations.keys():
1925
+ ids = inputLocalizations['objectID']
1926
+ fitminperloc = np.zeros_like(ids,dtype='f')
1927
+ if self.filter_npcs: # we only carry out the n_labeled measuring if we are asked to filter
1928
+ nlabeled = np.zeros_like(ids,dtype='i')
1929
+ for npc in npcs.npcs:
1930
+ if not npc.fitted:
1931
+ continue
1932
+ roi = ids==npc.objectID
1933
+ if np.any(roi):
1934
+ fitminperloc[roi] = npc.opt_result.fun/npc.npts.shape[0]
1935
+ if self.filter_npcs:
1936
+ nlabeled[roi] = np.sum(npc.measures)
1937
+ mapped_ds.addColumn('npc_fitminperloc',fitminperloc)
1938
+ if self.filter_npcs:
1939
+ mapped_ds.addColumn('npc_nlabeled',nlabeled)
1940
+
1941
+ return dict(output=mapped_ds, outputGallery=outputGallery,
1942
+ outputSegments=outputSegments, outputTemplates=outputTemplates, mdh=NPCmdh)
1943
+
1944
+ return dict(output=mapped_ds, outputGallery=None,
1945
+ outputSegments=None, outputTemplates=None, mdh=None) # return empty slots for outputs etc in this case
1946
+
1947
+
1948
+ @property
1949
+ def default_view(self):
1950
+ from traitsui.api import View, Group, Item, CheckListEditor
1951
+ from PYME.ui.custom_traits_editors import CBEditor
1952
+
1953
+ return View(Item('inputLocalizations', editor=CBEditor(choices=self._namespace_keys)),
1954
+ Item('_'),
1955
+ Item('NPC_analysis_file'),
1956
+ Item('NPC_version_check'),
1957
+ Item('NPC_target_version'),
1958
+ Item('NPC_hide'),
1959
+ Item('_'),
1960
+ Item('filter_npcs'),
1961
+ Item('fitmin_max'),
1962
+ Item('min_labeled'),
1963
+ Item('height_max'),
1964
+ Item('_'),
1965
+ Item('labeling_threshold'),
1966
+ Item('rotation_locked'),
1967
+ Item('radius_uncertainty'),
1968
+ Item('zclip'),
1969
+ Item('_'),
1970
+ Item('NPC_Gallery_Arrangement'),
1971
+ Item('NPC_enforce_8foldsym'),
1972
+ Item('NPCRotationAngle'),
1973
+ Item('gallery_x_offset'),
1974
+ Item('gallery_y_offset'),
1975
+ Item('_'),
1976
+ Item('output'),
1977
+ Item('outputGallery'),
1978
+ Item('outputSegments'),
1979
+ buttons=['OK'])
1980
+
1981
+ try:
1982
+ import alphashape
1983
+ except ImportError:
1984
+ has_ashape = False
1985
+ else:
1986
+ has_ashape = True
1987
+
1988
+ from scipy.spatial import ConvexHull
1989
+
1990
+ def shape_measure_alpha(points,alpha=0.01):
1991
+ alpha_shape = alphashape.alphashape(points[:,0:2], alpha)
1992
+ # alpha_vol = alphashape.alphashape(points, 1e-3) # errors with 3D shapes occasionally; try to fix this to convex_hull_like
1993
+
1994
+ area = alpha_shape.area
1995
+ # vol = alpha_vol.volume
1996
+ vol = ConvexHull(points[:,0:3]).volume # we fal back to convex hull to avoid issues
1997
+ nlabel = points.shape[0]
1998
+
1999
+ if alpha_shape.geom_type == 'MultiPolygon':
2000
+ polys = []
2001
+ for geom in alpha_shape.geoms:
2002
+ pc = []
2003
+ for point in geom.exterior.coords:
2004
+ pc.append(point)
2005
+ polys.append(np.array(pc))
2006
+ elif alpha_shape.geom_type == 'Polygon':
2007
+ polys = [np.array(alpha_shape.boundary.coords)]
2008
+ else:
2009
+ raise RuntimeError("Got alpha shape that is bounded by unknown geometry, got geom type %s with alpha = %.2f" %
2010
+ (alpha_shape.geom_type,alpha))
2011
+
2012
+ return (area,vol,nlabel,polys)
2013
+
2014
+ def shape_measure_convex_hull(points):
2015
+ shape2d = ConvexHull(points[:,0:2])
2016
+ area = shape2d.volume
2017
+ poly0 = points[shape2d.vertices,0:2]
2018
+ polys = [np.append(poly0,poly0[0,None,:],axis=0)] # close the polygon
2019
+ shape3d = ConvexHull(points[:,0:3])
2020
+ vol = shape3d.volume
2021
+ nlabel = points.shape[0]
2022
+
2023
+ return (area,vol,nlabel,polys)
2024
+
2025
+ import PYME.config
2026
+ def shape_measure(points,alpha=0.01,useAlphaShapes=True):
2027
+ if has_ashape and useAlphaShapes:
2028
+ return shape_measure_alpha(points,alpha=alpha)
2029
+ else:
2030
+ return shape_measure_convex_hull(points)
2031
+
2032
+ @register_module('SiteDensity')
2033
+ class SiteDensity(ModuleBase):
2034
+ """Documentation to be added."""
2035
+ inputLocalisations = Input('clustered')
2036
+ outputName = Output('cluster_density')
2037
+ outputShapes = Output('cluster_shapes')
2038
+
2039
+ IDName = CStr('dbscanClumpID')
2040
+ clusterSizeName = CStr('dbscanClumpSize')
2041
+ alpha = Float(0.01) # trying to ensure wqe stay with single polygon boundaries by default
2042
+ use_alpha_shapes = Bool(True,
2043
+ desc="use alpha shapes IF available, use convex hull if false (on systems without alphashape module falls back to convex hull in any case)")
2044
+
2045
+ def run(self, inputLocalisations):
2046
+
2047
+ ids = inputLocalisations[self.IDName]
2048
+ uids = np.unique(ids)
2049
+ sizes = inputLocalisations[self.clusterSizeName] # really needed?
2050
+
2051
+ # stddevs
2052
+ sx = np.zeros_like(ids,dtype=float)
2053
+ sy = np.zeros_like(ids,dtype=float)
2054
+ sz = np.zeros_like(ids,dtype=float)
2055
+
2056
+ # centroids
2057
+ cx = np.zeros_like(ids,dtype=float)
2058
+ cy = np.zeros_like(ids,dtype=float)
2059
+ cz = np.zeros_like(ids,dtype=float)
2060
+
2061
+ area = np.zeros_like(ids,dtype=float)
2062
+ vol = np.zeros_like(ids,dtype=float)
2063
+ dens = np.zeros_like(ids,dtype=float)
2064
+ nlabel = np.zeros_like(ids,dtype=float)
2065
+
2066
+ polx = np.empty((0))
2067
+ poly = np.empty((0))
2068
+ polz = np.empty((0))
2069
+ polarea = np.empty((0))
2070
+ clusterid = np.empty((0),int)
2071
+ polid = np.empty((0),int)
2072
+ polstdz = np.empty((0))
2073
+
2074
+ polidcur = 1 # we keep our own list of poly ids since we can get multiple polygons per cluster
2075
+ for id in uids:
2076
+ roi = ids==id
2077
+
2078
+ roix = inputLocalisations['x'][roi]
2079
+ roiy = inputLocalisations['y'][roi]
2080
+ roiz = inputLocalisations['z'][roi]
2081
+ roi3d=np.stack((roix,roiy,roiz),axis=1)
2082
+
2083
+ arear,volr,nlabelr,polys = shape_measure(roi3d, self.alpha, useAlphaShapes=self.use_alpha_shapes)
2084
+
2085
+ #alpha_shape = ashp.alphashape(roi3d[:,0:2], self.alpha)
2086
+ #alpha_vol = ashp.alphashape(roi3d, self.alpha)
2087
+
2088
+ sxr = np.std(roix)
2089
+ syr = np.std(roiy)
2090
+ szr = np.std(roiz)
2091
+ sx[roi] = sxr
2092
+ sy[roi] = syr
2093
+ sz[roi] = szr
2094
+
2095
+ cxr = np.mean(roix)
2096
+ cyr = np.mean(roiy)
2097
+ czr = np.mean(roiz)
2098
+ #stdy.append(syr)
2099
+ #stdz.append(szr)
2100
+ cx[roi] = cxr
2101
+ cy[roi] = cyr
2102
+ cz[roi] = czr
2103
+
2104
+ area[roi] = arear
2105
+ vol[roi] = volr
2106
+ nlabel[roi] = nlabelr
2107
+ dens[roi] = nlabelr/(arear/1e6)
2108
+
2109
+ # some code to process the polygons
2110
+ for pol in polys:
2111
+ polxr = pol[:,0]
2112
+ polx = np.append(polx,polxr)
2113
+ poly = np.append(poly,pol[:,1])
2114
+ polz = np.append(polz,np.full_like(polxr,czr))
2115
+ polid = np.append(polid,np.full_like(polxr,polidcur,dtype=int))
2116
+ clusterid = np.append(clusterid,np.full_like(polxr,id,dtype=int))
2117
+ polarea = np.append(polarea,np.full_like(polxr,arear))
2118
+ polstdz = np.append(polstdz,np.full_like(polxr,szr))
2119
+ polidcur += 1
2120
+
2121
+ mapped_ds = tabular.MappingFilter(inputLocalisations)
2122
+ mapped_ds.addColumn('clst_cx', cx)
2123
+ mapped_ds.addColumn('clst_cy', cy)
2124
+ mapped_ds.addColumn('clst_cz', cz)
2125
+
2126
+ mapped_ds.addColumn('clst_stdx', sx)
2127
+ mapped_ds.addColumn('clst_stdy', sy)
2128
+ mapped_ds.addColumn('clst_stdz', sz)
2129
+
2130
+ mapped_ds.addColumn('clst_area', area)
2131
+ mapped_ds.addColumn('clst_vol', vol)
2132
+ mapped_ds.addColumn('clst_size', nlabel)
2133
+ mapped_ds.addColumn('clst_density', dens)
2134
+
2135
+ from PYME.IO.tabular import DictSource
2136
+ dspoly = DictSource(dict(x=polx,
2137
+ y=poly,
2138
+ z=polz,
2139
+ polyIndex=polid,
2140
+ polyArea=polarea,
2141
+ clusterID=clusterid,
2142
+ stdz=polstdz))
2143
+
2144
+ return dict(outputName=mapped_ds,outputShapes=dspoly)
2145
+
2146
+ # this module regenerates the clump derived properties for MINFLUX after events were removed
2147
+ # this happens typically after filtering for posInClump
2148
+ @register_module('RecalcClumpProperties')
2149
+ class RecalcClumpProperties(ModuleBase):
2150
+ """Documentation to be added."""
2151
+ inputLocalisations = Input('Localizations')
2152
+ outputName = Output('Clumprecalc')
2153
+
2154
+ def run(self, inputLocalisations):
2155
+
2156
+ from PYMEcs.IO.MINFLUX import get_stddev_property
2157
+ ids = inputLocalisations['clumpIndex']
2158
+ counts = get_stddev_property(ids,inputLocalisations['clumpSize'],statistic='count')
2159
+ stdx = get_stddev_property(ids,inputLocalisations['x'])
2160
+ # we expect this to only happen when clumpSize == 1, because then std dev comes back as 0
2161
+ stdx[stdx < 1e-3] = 100.0 # if error estimate is too small, replace with 100 as "large" flag
2162
+ stdy = get_stddev_property(ids,inputLocalisations['y'])
2163
+ stdy[stdy < 1e-3] = 100.0
2164
+ if 'error_z' in inputLocalisations.keys():
2165
+ stdz = get_stddev_property(ids,inputLocalisations['z'])
2166
+ stdz[stdz < 1e-3] = 100.0
2167
+
2168
+ mapped_ds = tabular.MappingFilter(inputLocalisations)
2169
+ mapped_ds.addColumn('clumpSize', counts)
2170
+ mapped_ds.addColumn('error_x', stdx)
2171
+ mapped_ds.addColumn('error_y', stdy)
2172
+ if 'error_z' in inputLocalisations.keys():
2173
+ mapped_ds.addColumn('error_z', stdz)
2174
+
2175
+ return mapped_ds
2176
+
2177
+ # simple replacement module for full drift correction module
2178
+ # this one simply allows manually changing linear drift coefficients
2179
+ @register_module("LinearDrift")
2180
+ class LinearDrift(ModuleBase):
2181
+ input = Input('localizations')
2182
+ output = Output('driftcorr')
2183
+
2184
+ a_xlin = Float(0,label='x = x + a*t, a:')
2185
+ b_ylin = Float(0,label='y = y + b*t, b:')
2186
+ c_zlin = Float(0,label='z = z + c*t, c:')
2187
+
2188
+ def run(self, input):
2189
+ output = tabular.MappingFilter(input)
2190
+
2191
+ t = input['t']
2192
+ output.addColumn('x', input['x']+self.a_xlin*t)
2193
+ output.addColumn('y', input['y']+self.b_ylin*t)
2194
+ output.addColumn('z', input['z']+self.c_zlin*t)
2195
+
2196
+ return output
2197
+
2198
+
2199
+ @register_module("TrackProps")
2200
+ class TrackProps(ModuleBase):
2201
+ input = Input('localizations')
2202
+ output = Output('with_trackprops')
2203
+
2204
+ IDkey = CStr('clumpIndex')
2205
+
2206
+ def run(self, input):
2207
+ from PYMEcs.IO.MINFLUX import get_stddev_property
2208
+ ids = input[self.IDkey]
2209
+ tracexmin = get_stddev_property(ids,input['x'],statistic='min')
2210
+ tracexmax = get_stddev_property(ids,input['x'],statistic='max')
2211
+ traceymin = get_stddev_property(ids,input['y'],statistic='min')
2212
+ traceymax = get_stddev_property(ids,input['y'],statistic='max')
2213
+
2214
+ tracebbx = tracexmax - tracexmin
2215
+ tracebby = traceymax - traceymin
2216
+ tracebbdiag = np.sqrt(tracebbx**2 + tracebby**2)
2217
+
2218
+ mapped_ds = tabular.MappingFilter(input)
2219
+ mapped_ds.addColumn('trace_bbx',tracebbx)
2220
+ mapped_ds.addColumn('trace_bby',tracebby)
2221
+ mapped_ds.addColumn('trace_bbdiag',tracebbdiag)
2222
+ if 'error_z' in input.keys():
2223
+ tracezmin = get_stddev_property(ids,input['z'],statistic='min')
2224
+ tracezmax = get_stddev_property(ids,input['z'],statistic='max')
2225
+ tracebbz = tracezmax - tracezmin
2226
+ mapped_ds.addColumn('trace_bbz',tracebbz)
2227
+ tracebbspcdiag = np.sqrt(tracebbdiag**2 + tracebbz**2)
2228
+ mapped_ds.addColumn('trace_bbspcdiag',tracebbspcdiag)
2229
+ return mapped_ds
2230
+
2231
+ @register_module("SimulateSiteloss")
2232
+ class SimulateSiteloss(ModuleBase):
2233
+ input = Input('localizations')
2234
+ output = Output('with_retainprop')
2235
+
2236
+ Seed = Int(42)
2237
+ TimeConstant = Float(1000) # time course in seconds
2238
+
2239
+ def run(self, input):
2240
+ tim = 1e-3*input['t'] # t should be in ms and we use t rather than tim as it is guarnteed to be there (more or less)
2241
+ prob = np.exp(-tim/self.TimeConstant)
2242
+ rng = np.random.default_rng(seed=self.Seed)
2243
+ retained = rng.random(tim.shape) <= prob
2244
+
2245
+ mapped_ds = tabular.MappingFilter(input)
2246
+ mapped_ds.addColumn('p_simloss',prob)
2247
+ mapped_ds.addColumn('retain',retained)
2248
+ return mapped_ds
2249
+
2250
+ # two implementations to forward fill a 1D vector
2251
+ def ffill_pandas(ids):
2252
+ import pandas as pd
2253
+ idsf = ids.astype('f')
2254
+ idsf[ids==0] = np.nan
2255
+ df = pd.DataFrame.from_dict(dict(ids = idsf))
2256
+ dff = df.ffill()
2257
+ return dff['ids'].values.astype('i')
2258
+ # from https://stackoverflow.com/questions/41190852/most-efficient-way-to-forward-fill-nan-values-in-numpy-array
2259
+ # @user189035 replace mask.shape[1] with mask.size and remove axis=1 and replace the last line with out = arr[idx]
2260
+ def ffill_numpy(arr):
2261
+ '''Solution provided by Divakar.'''
2262
+ mask = arr == 0
2263
+ idx = np.where(~mask,np.arange(mask.size,dtype='i'),0)
2264
+ np.maximum.accumulate(idx,out=idx)
2265
+ out = arr[idx]
2266
+ return out
2267
+
2268
+ @register_module('SuperClumps')
2269
+ class SuperClumps(ModuleBase):
2270
+ """
2271
+ Generates 'super clumps' by grouping adjacent clumps that have a distance
2272
+ less than a selectable threshold distance - should be in the single digit nm range.
2273
+ Grouping is based on clumpIndex assuming that was originally set from trace ids (tid).
2274
+
2275
+ Parameters
2276
+ ----------
2277
+ inputL: string - name of the data source containing localizations with clumpIndex
2278
+ inputLM: string - name of the data source containing merged localizations based on same clumpIndex
2279
+ threshold_nm: float, distance threshold for grouping traces into larger "super traces"
2280
+
2281
+ Returns
2282
+ -------
2283
+ outputL : tabular.MappingFilter that contains locs with new clumpIndex and clumpSize fields (now marking super clumps)
2284
+ outputLM : tabular.MappingFilter that contains coalesced locs with new clumpIndex and clumpSize fields (now marking super clumps)
2285
+
2286
+ """
2287
+
2288
+ inputL = Input('Localizations')
2289
+ inputLM = Input('LocalizationsMerged')
2290
+ outputL = Output('with_superclumps')
2291
+ outputLM = Output('with_superclumps_merged')
2292
+
2293
+ threshold_nm = Float(5.0,label='threshold distance (nm)',
2294
+ desc='parameter that sets the distance threshold for grouping traces into larger "super traces"')
2295
+
2296
+ def run(self, inputL, inputLM):
2297
+
2298
+ from PYMEcs.IO.MINFLUX import get_stddev_property
2299
+ locs = inputL
2300
+ locsm = inputLM
2301
+
2302
+ x=locsm['x'];y=locsm['y'];z=locsm['z']
2303
+ dx = x[1:]-x[:-1]; dy = y[1:]-y[:-1]; dz = z[1:]-z[:-1]
2304
+ ds = np.sqrt(dx*dx+dy*dy+dz*dz)
2305
+
2306
+ # currently jumps are mereley flagged by a distance to previous gt threshold
2307
+ # one might also want to check that things are as expected, e.g. all trace sizes before a jump have clumpSize == loclimit
2308
+ # possibly other criteria
2309
+ jumps = np.append(np.array(1,'i'),(ds > self.threshold_nm))
2310
+
2311
+ newids = jumps.copy()
2312
+ njumps = int(jumps.sum())
2313
+ newids[jumps > 0] = np.arange(1,njumps+1)
2314
+
2315
+ superids = ffill_numpy(newids) # move the new superClumpIndex throughout each superclump by forward filling
2316
+ # superids2 = ffill_pandas(newids)
2317
+ # if not np.all(superids == superids2):
2318
+ # warn("difference between ffill1 and ffill2, unexpected, please check")
2319
+
2320
+ # now map the new superclump ids back onto the non-coalesced localizations
2321
+ cimerged = locsm['clumpIndex'].astype('i')
2322
+ ci = locs['clumpIndex'].astype('i')
2323
+ uid,idx,ridx = np.unique(ci,return_index=True,return_inverse=True)
2324
+ uidm,idxm,ridxm = np.unique(cimerged,return_index=True,return_inverse=True)
2325
+ if not np.all(uid == uidm):
2326
+ warn("issue in that ids between loc data and merged loc data do not agree, please check")
2327
+ sci = superids[idxm][ridx] # this should give us the superindices in the fully expanded events
2328
+ scisize = get_stddev_property(sci,sci,statistic='count').astype('i')
2329
+ superidsize = get_stddev_property(superids,superids,statistic='count').astype('i')
2330
+
2331
+ from PYMEcs.IO.MINFLUX import mk_posinid
2332
+ sci_pic = mk_posinid(sci)
2333
+ # now make the relevant mappings and
2334
+ # assign superids, superidsize for locsm mapping
2335
+ # assign sci, scisize for locs for locs mapping
2336
+ # existing clumpIndex, clumpSize are retained as subClumpIndex etc
2337
+
2338
+ mapped_l = tabular.MappingFilter(locs)
2339
+ mapped_lm = tabular.MappingFilter(locsm)
2340
+
2341
+ mapped_l.addColumn('clumpIndex',sci)
2342
+ mapped_l.addColumn('clumpSize',scisize)
2343
+ mapped_l.addColumn('posInClump',sci_pic)
2344
+ mapped_l.addColumn('subClumpIndex',locs['clumpIndex'])
2345
+ mapped_l.addColumn('subClumpSize',locs['clumpSize'])
2346
+ mapped_l.addColumn('subPosInClump',locs['posInClump'])
2347
+
2348
+ mapped_lm.addColumn('clumpIndex',superids)
2349
+ mapped_lm.addColumn('clumpSize',superidsize)
2350
+ mapped_lm.addColumn('subClumpIndex',locsm['clumpIndex'])
2351
+ mapped_lm.addColumn('subClumpSize',locsm['clumpSize'])
2352
+ mapped_lm.addColumn('tracedist',np.append([0],ds))
2353
+
2354
+ return dict(outputL=mapped_l,outputLM=mapped_lm)
2355
+
2356
+ @register_module('ErrorFromClumpIndex')
2357
+ class ErrorFromClumpIndex(ModuleBase):
2358
+ inputlocalizations = Input('with_clumps')
2359
+ outputlocalizations = Output('with_errors')
2360
+
2361
+ labelKey = CStr('clumpIndex')
2362
+
2363
+ def run(self, inputlocalizations):
2364
+ from PYMEcs.IO.MINFLUX import get_stddev_property
2365
+
2366
+ locs = inputlocalizations
2367
+ site_id = self.labelKey
2368
+ ids = locs[site_id].astype('i')
2369
+ stdx = get_stddev_property(ids,locs['x'])
2370
+ stdy = get_stddev_property(ids,locs['y'])
2371
+
2372
+ mapped = tabular.MappingFilter(locs)
2373
+ mapped.addColumn('error_x',stdx)
2374
+ mapped.addColumn('error_y',stdy)
2375
+ has_z = 'z' in locs.keys() and np.std(locs['z']) > 1.0
2376
+ if has_z:
2377
+ stdz = get_stddev_property(ids,locs['z'])
2378
+ mapped.addColumn('error_z',stdz)
2379
+
2380
+ return mapped