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.
- PYMEcs/Acquire/Actions/__init__.py +0 -0
- PYMEcs/Acquire/Actions/custom.py +167 -0
- PYMEcs/Acquire/Hardware/LPthreadedSimple.py +248 -0
- PYMEcs/Acquire/Hardware/LPthreadedSimpleSim.py +246 -0
- PYMEcs/Acquire/Hardware/NikonTiFlaskServer.py +45 -0
- PYMEcs/Acquire/Hardware/NikonTiFlaskServerT.py +59 -0
- PYMEcs/Acquire/Hardware/NikonTiRESTClient.py +73 -0
- PYMEcs/Acquire/Hardware/NikonTiSim.py +35 -0
- PYMEcs/Acquire/Hardware/__init__.py +0 -0
- PYMEcs/Acquire/Hardware/driftTrackGUI.py +329 -0
- PYMEcs/Acquire/Hardware/driftTrackGUI_n.py +472 -0
- PYMEcs/Acquire/Hardware/driftTracking.py +424 -0
- PYMEcs/Acquire/Hardware/driftTracking_n.py +433 -0
- PYMEcs/Acquire/Hardware/fakeCamX.py +15 -0
- PYMEcs/Acquire/Hardware/offsetPiezoRESTCorrelLog.py +38 -0
- PYMEcs/Acquire/__init__.py +0 -0
- PYMEcs/Analysis/MBMcollection.py +552 -0
- PYMEcs/Analysis/MINFLUX.py +280 -0
- PYMEcs/Analysis/MapUtils.py +77 -0
- PYMEcs/Analysis/NPC.py +1176 -0
- PYMEcs/Analysis/Paraflux.py +218 -0
- PYMEcs/Analysis/Simpler.py +81 -0
- PYMEcs/Analysis/Sofi.py +140 -0
- PYMEcs/Analysis/__init__.py +0 -0
- PYMEcs/Analysis/decSofi.py +211 -0
- PYMEcs/Analysis/eventProperties.py +50 -0
- PYMEcs/Analysis/fitDarkTimes.py +569 -0
- PYMEcs/Analysis/objectVolumes.py +20 -0
- PYMEcs/Analysis/offlineTracker.py +130 -0
- PYMEcs/Analysis/stackTracker.py +180 -0
- PYMEcs/Analysis/timeSeries.py +63 -0
- PYMEcs/Analysis/trackFiducials.py +186 -0
- PYMEcs/Analysis/zerocross.py +91 -0
- PYMEcs/IO/MINFLUX.py +851 -0
- PYMEcs/IO/NPC.py +117 -0
- PYMEcs/IO/__init__.py +0 -0
- PYMEcs/IO/darkTimes.py +19 -0
- PYMEcs/IO/picasso.py +219 -0
- PYMEcs/IO/tabular.py +11 -0
- PYMEcs/__init__.py +0 -0
- PYMEcs/experimental/CalcZfactor.py +51 -0
- PYMEcs/experimental/FRC.py +338 -0
- PYMEcs/experimental/ImageJROItools.py +49 -0
- PYMEcs/experimental/MINFLUX.py +1537 -0
- PYMEcs/experimental/NPCcalcLM.py +560 -0
- PYMEcs/experimental/Simpler.py +369 -0
- PYMEcs/experimental/Sofi.py +78 -0
- PYMEcs/experimental/__init__.py +0 -0
- PYMEcs/experimental/binEventProperty.py +187 -0
- PYMEcs/experimental/chaining.py +23 -0
- PYMEcs/experimental/clusterTrack.py +179 -0
- PYMEcs/experimental/combine_maps.py +104 -0
- PYMEcs/experimental/eventProcessing.py +93 -0
- PYMEcs/experimental/fiducials.py +323 -0
- PYMEcs/experimental/fiducialsNew.py +402 -0
- PYMEcs/experimental/mapTools.py +271 -0
- PYMEcs/experimental/meas2DplotDh5view.py +107 -0
- PYMEcs/experimental/mortensen.py +131 -0
- PYMEcs/experimental/ncsDenoise.py +158 -0
- PYMEcs/experimental/onTimes.py +295 -0
- PYMEcs/experimental/procPoints.py +77 -0
- PYMEcs/experimental/pyme2caml.py +73 -0
- PYMEcs/experimental/qPAINT.py +965 -0
- PYMEcs/experimental/randMap.py +188 -0
- PYMEcs/experimental/regExtraCmaps.py +11 -0
- PYMEcs/experimental/selectROIfilterTable.py +72 -0
- PYMEcs/experimental/showErrs.py +51 -0
- PYMEcs/experimental/showErrsDh5view.py +58 -0
- PYMEcs/experimental/showShiftMap.py +56 -0
- PYMEcs/experimental/snrEvents.py +188 -0
- PYMEcs/experimental/specLabeling.py +51 -0
- PYMEcs/experimental/splitRender.py +246 -0
- PYMEcs/experimental/testChannelByName.py +36 -0
- PYMEcs/experimental/timedSpecies.py +28 -0
- PYMEcs/experimental/utils.py +31 -0
- PYMEcs/misc/ExtraCmaps.py +177 -0
- PYMEcs/misc/__init__.py +0 -0
- PYMEcs/misc/configUtils.py +169 -0
- PYMEcs/misc/guiMsgBoxes.py +27 -0
- PYMEcs/misc/mapUtils.py +230 -0
- PYMEcs/misc/matplotlib.py +136 -0
- PYMEcs/misc/rectsFromSVG.py +182 -0
- PYMEcs/misc/shellutils.py +1110 -0
- PYMEcs/misc/utils.py +205 -0
- PYMEcs/misc/versionCheck.py +20 -0
- PYMEcs/misc/zcInfo.py +90 -0
- PYMEcs/pyme_warnings.py +4 -0
- PYMEcs/recipes/__init__.py +0 -0
- PYMEcs/recipes/base.py +75 -0
- PYMEcs/recipes/localisations.py +2380 -0
- PYMEcs/recipes/manipulate_yaml.py +83 -0
- PYMEcs/recipes/output.py +177 -0
- PYMEcs/recipes/processing.py +247 -0
- PYMEcs/recipes/simpler.py +290 -0
- PYMEcs/version.py +2 -0
- pyme_extra-1.0.4.post0.dist-info/METADATA +114 -0
- pyme_extra-1.0.4.post0.dist-info/RECORD +101 -0
- pyme_extra-1.0.4.post0.dist-info/WHEEL +5 -0
- pyme_extra-1.0.4.post0.dist-info/entry_points.txt +3 -0
- pyme_extra-1.0.4.post0.dist-info/licenses/LICENSE +674 -0
- 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
|