PYME-extra 1.0.4.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. PYMEcs/Acquire/Actions/__init__.py +0 -0
  2. PYMEcs/Acquire/Actions/custom.py +167 -0
  3. PYMEcs/Acquire/Hardware/LPthreadedSimple.py +248 -0
  4. PYMEcs/Acquire/Hardware/LPthreadedSimpleSim.py +246 -0
  5. PYMEcs/Acquire/Hardware/NikonTiFlaskServer.py +45 -0
  6. PYMEcs/Acquire/Hardware/NikonTiFlaskServerT.py +59 -0
  7. PYMEcs/Acquire/Hardware/NikonTiRESTClient.py +73 -0
  8. PYMEcs/Acquire/Hardware/NikonTiSim.py +35 -0
  9. PYMEcs/Acquire/Hardware/__init__.py +0 -0
  10. PYMEcs/Acquire/Hardware/driftTrackGUI.py +329 -0
  11. PYMEcs/Acquire/Hardware/driftTrackGUI_n.py +472 -0
  12. PYMEcs/Acquire/Hardware/driftTracking.py +424 -0
  13. PYMEcs/Acquire/Hardware/driftTracking_n.py +433 -0
  14. PYMEcs/Acquire/Hardware/fakeCamX.py +15 -0
  15. PYMEcs/Acquire/Hardware/offsetPiezoRESTCorrelLog.py +38 -0
  16. PYMEcs/Acquire/__init__.py +0 -0
  17. PYMEcs/Analysis/MBMcollection.py +552 -0
  18. PYMEcs/Analysis/MINFLUX.py +280 -0
  19. PYMEcs/Analysis/MapUtils.py +77 -0
  20. PYMEcs/Analysis/NPC.py +1176 -0
  21. PYMEcs/Analysis/Paraflux.py +218 -0
  22. PYMEcs/Analysis/Simpler.py +81 -0
  23. PYMEcs/Analysis/Sofi.py +140 -0
  24. PYMEcs/Analysis/__init__.py +0 -0
  25. PYMEcs/Analysis/decSofi.py +211 -0
  26. PYMEcs/Analysis/eventProperties.py +50 -0
  27. PYMEcs/Analysis/fitDarkTimes.py +569 -0
  28. PYMEcs/Analysis/objectVolumes.py +20 -0
  29. PYMEcs/Analysis/offlineTracker.py +130 -0
  30. PYMEcs/Analysis/stackTracker.py +180 -0
  31. PYMEcs/Analysis/timeSeries.py +63 -0
  32. PYMEcs/Analysis/trackFiducials.py +186 -0
  33. PYMEcs/Analysis/zerocross.py +91 -0
  34. PYMEcs/IO/MINFLUX.py +851 -0
  35. PYMEcs/IO/NPC.py +117 -0
  36. PYMEcs/IO/__init__.py +0 -0
  37. PYMEcs/IO/darkTimes.py +19 -0
  38. PYMEcs/IO/picasso.py +219 -0
  39. PYMEcs/IO/tabular.py +11 -0
  40. PYMEcs/__init__.py +0 -0
  41. PYMEcs/experimental/CalcZfactor.py +51 -0
  42. PYMEcs/experimental/FRC.py +338 -0
  43. PYMEcs/experimental/ImageJROItools.py +49 -0
  44. PYMEcs/experimental/MINFLUX.py +1537 -0
  45. PYMEcs/experimental/NPCcalcLM.py +560 -0
  46. PYMEcs/experimental/Simpler.py +369 -0
  47. PYMEcs/experimental/Sofi.py +78 -0
  48. PYMEcs/experimental/__init__.py +0 -0
  49. PYMEcs/experimental/binEventProperty.py +187 -0
  50. PYMEcs/experimental/chaining.py +23 -0
  51. PYMEcs/experimental/clusterTrack.py +179 -0
  52. PYMEcs/experimental/combine_maps.py +104 -0
  53. PYMEcs/experimental/eventProcessing.py +93 -0
  54. PYMEcs/experimental/fiducials.py +323 -0
  55. PYMEcs/experimental/fiducialsNew.py +402 -0
  56. PYMEcs/experimental/mapTools.py +271 -0
  57. PYMEcs/experimental/meas2DplotDh5view.py +107 -0
  58. PYMEcs/experimental/mortensen.py +131 -0
  59. PYMEcs/experimental/ncsDenoise.py +158 -0
  60. PYMEcs/experimental/onTimes.py +295 -0
  61. PYMEcs/experimental/procPoints.py +77 -0
  62. PYMEcs/experimental/pyme2caml.py +73 -0
  63. PYMEcs/experimental/qPAINT.py +965 -0
  64. PYMEcs/experimental/randMap.py +188 -0
  65. PYMEcs/experimental/regExtraCmaps.py +11 -0
  66. PYMEcs/experimental/selectROIfilterTable.py +72 -0
  67. PYMEcs/experimental/showErrs.py +51 -0
  68. PYMEcs/experimental/showErrsDh5view.py +58 -0
  69. PYMEcs/experimental/showShiftMap.py +56 -0
  70. PYMEcs/experimental/snrEvents.py +188 -0
  71. PYMEcs/experimental/specLabeling.py +51 -0
  72. PYMEcs/experimental/splitRender.py +246 -0
  73. PYMEcs/experimental/testChannelByName.py +36 -0
  74. PYMEcs/experimental/timedSpecies.py +28 -0
  75. PYMEcs/experimental/utils.py +31 -0
  76. PYMEcs/misc/ExtraCmaps.py +177 -0
  77. PYMEcs/misc/__init__.py +0 -0
  78. PYMEcs/misc/configUtils.py +169 -0
  79. PYMEcs/misc/guiMsgBoxes.py +27 -0
  80. PYMEcs/misc/mapUtils.py +230 -0
  81. PYMEcs/misc/matplotlib.py +136 -0
  82. PYMEcs/misc/rectsFromSVG.py +182 -0
  83. PYMEcs/misc/shellutils.py +1110 -0
  84. PYMEcs/misc/utils.py +205 -0
  85. PYMEcs/misc/versionCheck.py +20 -0
  86. PYMEcs/misc/zcInfo.py +90 -0
  87. PYMEcs/pyme_warnings.py +4 -0
  88. PYMEcs/recipes/__init__.py +0 -0
  89. PYMEcs/recipes/base.py +75 -0
  90. PYMEcs/recipes/localisations.py +2380 -0
  91. PYMEcs/recipes/manipulate_yaml.py +83 -0
  92. PYMEcs/recipes/output.py +177 -0
  93. PYMEcs/recipes/processing.py +247 -0
  94. PYMEcs/recipes/simpler.py +290 -0
  95. PYMEcs/version.py +2 -0
  96. pyme_extra-1.0.4.post0.dist-info/METADATA +114 -0
  97. pyme_extra-1.0.4.post0.dist-info/RECORD +101 -0
  98. pyme_extra-1.0.4.post0.dist-info/WHEEL +5 -0
  99. pyme_extra-1.0.4.post0.dist-info/entry_points.txt +3 -0
  100. pyme_extra-1.0.4.post0.dist-info/licenses/LICENSE +674 -0
  101. pyme_extra-1.0.4.post0.dist-info/top_level.txt +1 -0
PYMEcs/Analysis/NPC.py ADDED
@@ -0,0 +1,1176 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from PYMEcs.pyme_warnings import warn
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ piover4 = np.pi/4.0
9
+
10
+ # from circle_fit import taubinSVD
11
+ # def fitcirc(x,y,sigma=None):
12
+ # pcs = np.vstack((x,y)).T
13
+ # xc, yc, r, sigma = taubinSVD(pcs)
14
+ # return (xc, yc, r, sigma)
15
+
16
+ # def centreshift(x,y,xc,yc):
17
+ # return (x-xc,y-yc)
18
+
19
+ # def plot_segments(rad,rotang=0):
20
+ # for i in range(8):
21
+ # ang = i * np.pi / 4.0
22
+ # plt.plot([0,rad*np.cos(ang+rotang)],[0,rad*np.sin(ang+rotang)],'b')
23
+
24
+ # def phi_from_coords(xn,yn):
25
+ # phis = np.arctan2(yn,xn)
26
+ # return phis
27
+
28
+ # def rot_coords(xn,yn,ang):
29
+ # c, s = np.cos(ang), np.sin(ang)
30
+ # R = np.array(((c, -s), (s, c)))
31
+ # pcs = np.vstack((xn,yn))
32
+ # crot = R @ pcs
33
+
34
+ # return (crot.T[:,0],crot.T[:,1])
35
+
36
+ from circle_fit import taubinSVD
37
+ def fitcirc(x,y,sigma=None):
38
+ pcs = np.vstack((x,y)).T
39
+ xc, yc, r, sigma = taubinSVD(pcs)
40
+ return (xc, yc, r, sigma)
41
+
42
+ def centreshift(x,y,xc,yc):
43
+ return (x-xc,y-yc)
44
+
45
+ def plot_segments(rad,rotang=0,ax=None):
46
+ if ax is None:
47
+ ax = plt.gca()
48
+ for i in range(8):
49
+ ang = i * np.pi / 4.0
50
+ ax.plot([0,rad*np.cos(ang+rotang)],[0,rad*np.sin(ang+rotang)],'b')
51
+
52
+ def phi_from_coords(xn,yn):
53
+ phis = np.arctan2(yn,xn)
54
+ return phis
55
+
56
+ def r_from_coords(xn,yn):
57
+ r = np.sqrt(yn*yn+xn*xn)
58
+ return r
59
+
60
+ # this implementation seems broken; retire for now
61
+ def estimate_rotation(xn,yn,spacing=1.0,mode='abs',do_plot=False,secondpass=False):
62
+ n_ang = int(45.0/spacing)
63
+ rotrad = np.arange(n_ang)*piover4/n_ang
64
+ phis = phi_from_coords(xn,yn)
65
+ r = r_from_coords(xn,yn)
66
+ frac, integ = np.modf((np.pi+phis)/piover4)
67
+ if mode == 'abs':
68
+ sqdiff = [ np.sum(np.abs(rd - frac*piover4)) for rd in rotrad]
69
+ elif mode == 'square':
70
+ sqdiff = [ np.sum((rd - frac*piover4)**2) for rd in rotrad]
71
+ else:
72
+ raise RuntimeError("unknown estimation mode %s" % mode)
73
+
74
+ if do_plot:
75
+ fig,ax = plt.subplots(2,1)
76
+ ax[0].scatter(np.degrees(rotrad),sqdiff)
77
+ ax[1].scatter(np.degrees(phis),r,alpha=0.4)
78
+
79
+ indmin = np.argmin(sqdiff)
80
+ radmin = rotrad[indmin]
81
+ radrot = piover4/2.0-radmin
82
+
83
+ if secondpass:
84
+ xn2, yn2 = rot_coords(xn,yn,radrot)
85
+ radrot2 = estimate_rotation(xn2,yn2,spacing=spacing,mode=mode,do_plot=do_plot,secondpass=False)
86
+ radrot = radrot+radrot2
87
+
88
+ return radrot
89
+
90
+ # FIXED up optimal rotation estimator
91
+ # we calculate a metric that looks at angles of events and is designed to have the smallest penalty (zero)
92
+ # at the center of a pi/4 (45 deg) segment and increases linearly or squarely towards the edges of the
93
+ # segments
94
+ # we do this by calculating the fractional part of the angle modulo pi/4 and subtract 0.5 so that the center
95
+ # gets a value of 0 and edges go to +- 0.5; we then take abs or squares of this "angle penalty" and sum these up
96
+ # we do this by rotating angles by a range of 0..pi/4 and then find the rotation angle that minimises the total penalty
97
+ # NOTE: important property of a well working routine: if we rotate the data the determined "optimal rotation"
98
+ # should shift linearly with this external rotation
99
+ def estimate_rotation2(xn,yn,spacing=1.0,mode='abs',do_plot=False):
100
+ n_ang = int(45.0/spacing)
101
+ rotrad = np.arange(n_ang)*piover4/n_ang
102
+ phis = phi_from_coords(xn,yn)
103
+ r = r_from_coords(xn,yn)
104
+
105
+ sqdiff = []
106
+ if mode == 'abs':
107
+ for rd in rotrad:
108
+ frac, integ = np.modf((np.pi+phis+rd)/piover4)
109
+ sqdiff.append(np.sum(np.abs(frac-0.5)))
110
+ elif mode == 'square':
111
+ for rd in rotrad:
112
+ frac, integ = np.modf((np.pi+phis+rd)/piover4)
113
+ sqdiff.append(np.sum((frac-0.5)**2))
114
+ else:
115
+ raise RuntimeError("unknown estimation mode %s" % mode)
116
+
117
+ if do_plot:
118
+ fig,ax = plt.subplots(2,1)
119
+ ax[0].scatter(np.degrees(rotrad),sqdiff)
120
+ ax[1].scatter(np.degrees(phis),r,alpha=0.4)
121
+ ax[1].set_ylim(0,80)
122
+
123
+ indmin = np.argmin(sqdiff)
124
+ radmin = rotrad[indmin]
125
+
126
+ return radmin
127
+
128
+ def rot_coords(xn,yn,ang):
129
+ c, s = np.cos(ang), np.sin(ang)
130
+ R = np.array(((c, -s), (s, c)))
131
+ pcs = np.vstack((xn,yn))
132
+ crot = R @ pcs
133
+
134
+ return (crot.T[:,0],crot.T[:,1])
135
+
136
+ def rfilt(xn,yn,r0,dr=25.0):
137
+
138
+ ri = np.sqrt(xn*xn+yn*yn)
139
+ rimask = (ri >r0-dr)*(ri < r0+dr)
140
+ return (xn[rimask],yn[rimask])
141
+
142
+ def estimate_nlabeled(x,y,r0=None,nthresh=10,dr=30.0,rotation=None,
143
+ do_plot=False,secondpass=False,fitmode='abs',return_radius=False,return_bysegments=False):
144
+ if r0 is None:
145
+ xc, yc, r0, sigma = fitcirc(x,y)
146
+ xn, yn = centreshift(x, y, xc, yc)
147
+ if rotation is None:
148
+ radrot = estimate_rotation2(xn,yn,mode=fitmode)
149
+ else:
150
+ radrot = rotation
151
+ xr1,yr1 = rot_coords(xn,yn,radrot)
152
+ xr, yr = rfilt(xr1,yr1,r0,dr=dr)
153
+ if secondpass:
154
+ xc2, yc2, r0, sigma = fitcirc(xr,yr)
155
+ xn, yn = centreshift(xr, yr, xc2, yc2)
156
+ if rotation is None:
157
+ radrot = estimate_rotation2(xn,yn,mode=fitmode)
158
+ else:
159
+ radrot = rotation
160
+ xr,yr = rot_coords(xn,yn,radrot)
161
+ else:
162
+ if rotation is None:
163
+ radrot = estimate_rotation2(x,y,mode=fitmode)
164
+ else:
165
+ radrot = rotation
166
+ xr1,yr1 = rot_coords(x,y,radrot)
167
+ xr, yr = rfilt(xr1,yr1,r0,dr=dr)
168
+
169
+ phis = phi_from_coords(xr,yr)
170
+ phibinedges = -np.pi + piover4*np.arange(9)
171
+ nhist,be = np.histogram(phis,bins=phibinedges)
172
+ Nlabeled = np.sum(nhist>=nthresh)
173
+
174
+ if do_plot:
175
+ segment_radius = 80.0
176
+ fig, axs = plt.subplots(2)
177
+ # first subplot
178
+ axs[0].set_aspect('equal')
179
+ axs[0].scatter(xr,yr,s=10,alpha=0.4,edgecolors='none')
180
+ axs[0].scatter([0],[0],marker='+')
181
+ plot_segments(segment_radius,ax=axs[0])
182
+ cir2 = plt.Circle((0, 0), r0, color='r',fill=False)
183
+ axs[0].add_patch(cir2)
184
+ from matplotlib.patches import Wedge
185
+ phibinedges_deg = np.degrees(phibinedges)
186
+
187
+ phibincenters = 0.5*(phibinedges[0:-1]+phibinedges[1:])
188
+ textradius = segment_radius + 13
189
+ for i,phic in enumerate(phibincenters):
190
+ xt,yt = (textradius*np.cos(phic),textradius*np.sin(phic))
191
+ axs[0].text(xt,yt,str(i+1),horizontalalignment='center', # we number segments from 1 to 8
192
+ verticalalignment='center',alpha=0.4)
193
+ axs[0].set_xlim(-100,100)
194
+ axs[0].set_ylim(-100,100)
195
+ for i in range(nhist.size):
196
+ if nhist[i] >= nthresh:
197
+ axs[0].add_patch(Wedge(
198
+ (0, 0), # (x,y)
199
+ segment_radius, # radius
200
+ phibinedges_deg[i], # theta1 (in degrees)
201
+ phibinedges_deg[i+1], # theta2
202
+ color="r", alpha=0.1))
203
+ axs[0].set_title('NPC Segments = %d, r0 = %.1f nm\nEvent threshold = %d, mode = %s, rot= %.2f rad' % (Nlabeled,r0,nthresh,fitmode,radrot))
204
+ axs[0].invert_yaxis() # the y axis direction seems inverted WRT PYMEVisualise, so try to make equal
205
+
206
+ # second suplot
207
+ def radtosegno(rad):
208
+ return (rad + np.pi + 0.5*piover4) / piover4
209
+
210
+ def segnotorad(sec):
211
+ return -np.pi + 0.5*piover4 + sec * piover4
212
+
213
+ axs[1].hist(phis,bins=phibinedges)
214
+ axs[1].plot([phibinedges[0],phibinedges[-1]],[nthresh,nthresh],'r--')
215
+ axs[1].set_xlabel('Angle range $\phi$, $\pi/4$ per segment (radians -$\pi,\cdots,\pi$)')
216
+ axs[1].set_ylabel('Events in segment')
217
+ secax = axs[1].secondary_xaxis('top', functions=(radtosegno, segnotorad))
218
+ secax.set_xlabel('segment number')
219
+ plt.tight_layout()
220
+
221
+ if return_radius:
222
+ return (Nlabeled,r0)
223
+ elif return_bysegments:
224
+ return (Nlabeled,nhist)
225
+ else:
226
+ return Nlabeled
227
+
228
+ from scipy.special import binom
229
+ from scipy.optimize import curve_fit
230
+
231
+ def pn(k,n,p):
232
+ return (binom(n,k)*(np.power(p,k)*np.power((1-p),(n-k))))
233
+
234
+ def pnpc(k,plabel):
235
+ pbright = 1-pn(0,4,plabel)
236
+ p_k_npc = pn(k,8,pbright)
237
+
238
+ return p_k_npc
239
+
240
+ # this is the formula for the 16-spot 3D arrangement, with 2 chances to label per spot
241
+ def pnpc3d(k,plabel):
242
+ pbright = 1-pn(0,2,plabel)
243
+ p_k_npc = pn(k,16,pbright)
244
+
245
+ return p_k_npc
246
+
247
+ def pnpc3dc(kfit,plabel):
248
+ krange = np.arange(17,dtype='i')
249
+ # important point: we must always first evealuate the probability expressions at the
250
+ # canonical points (0-16), form the cumluative sum and only then
251
+ # interpolate onto the coordinates where the fit is tested in a second step
252
+ pc = np.cumsum(pnpc3d(krange,plabel))
253
+ return np.interp(kfit,krange,pc)
254
+
255
+ def prangeNPC3D():
256
+ krange = np.arange(17,dtype='i')
257
+ prange=0.1*np.arange(1,10)
258
+
259
+ probs = {}
260
+ probs['krange'] = krange
261
+ for p in prange:
262
+ probs[p] = pnpc3d(krange,p)
263
+
264
+ return probs
265
+
266
+
267
+ def npclabel_fit(nphist,sigma=None):
268
+ npnormed = nphist/nphist.sum()
269
+ ks = np.arange(9)
270
+ popt, pcov = curve_fit(pnpc, ks, npnormed, sigma=sigma, method='lm', p0=[0.3])
271
+ perr = np.sqrt(np.diag(pcov))
272
+ nlabels_fit = pnpc(ks,popt[0])
273
+ n_labels_scaled = nphist.sum()*nlabels_fit
274
+
275
+ return (popt[0],n_labels_scaled,perr[0])
276
+
277
+ from PYMEcs.misc.utils import get_timestamp_from_filename
278
+ def plotcdf_npc3d(nlab,plot_as_points=True,timestamp=None,thresh=None,return_data=False):
279
+ pr = prangeNPC3D()
280
+ for p in pr.keys():
281
+ if p != 'krange':
282
+ plt.plot(pr['krange'],np.cumsum(pr[p]),label="p=%.1f" % p,alpha=0.5)
283
+ if timestamp is None:
284
+ labelexp = 'experiment'
285
+ else:
286
+ labelexp = 'exp %s' % timestamp
287
+
288
+ if thresh is not None:
289
+ labelexp = "thresh %d, %s" % (thresh,labelexp)
290
+
291
+ # make sure our bin centers are integer spaced from 0 to 16
292
+ histret = plt.hist(nlab,bins=np.arange(17)+0.5,density=True,
293
+ histtype="step", cumulative=1, label=labelexp, alpha=0.3)
294
+ if plot_as_points:
295
+ histn = histret[0]
296
+ histctr = 0.5*(histret[1][1:]+histret[1][0:-1])
297
+ plt.scatter(histctr,histn)
298
+
299
+ # ensure we only have integer major ticks in the plot
300
+ ax = plt.gca()
301
+ ax.xaxis.get_major_locator().set_params(integer=True)
302
+
303
+ popt,perr, pcbfx, pcbestfit = npclabel_fit3D(histctr,histn)
304
+ plt.plot(pcbfx,pcbestfit,'--')
305
+ plt.legend()
306
+
307
+ plt.title("NPC 3D analysis using %d NPCs, LE = %.1f %% +- %.1f %%" %
308
+ (nlab.size,100.0*popt,100.0*perr))
309
+ plt.xlabel("N labeled")
310
+ plt.ylabel("CDF")
311
+
312
+ if return_data:
313
+ return (histctr,histn)
314
+
315
+ def npclabel_fit3D(histx,histv,sigma=0.1):
316
+ popt, pcov = curve_fit(pnpc3dc, histx, histv, sigma=sigma, method='lm', p0=[0.4])
317
+ perr = np.sqrt(np.diag(pcov))
318
+ krange = np.arange(17,dtype='i')
319
+ pcumulative = pnpc3dc(krange,popt[0])
320
+
321
+ return (popt[0],perr[0], krange, pcumulative)
322
+
323
+
324
+ #################
325
+ # NPC 3D Analysis
326
+ #################
327
+
328
+ def to3vecs(x,y,z):
329
+ return np.stack((x,y,z),axis=1)
330
+
331
+ def xyzfrom3vec(v):
332
+ return (v[:,0],v[:,1],v[:,2])
333
+
334
+ from scipy.spatial.transform import Rotation as R
335
+ from scipy.interpolate import RegularGridInterpolator
336
+ from scipy.signal import fftconvolve
337
+
338
+ def fpinterpolate(fp3d,x,y,z,method='linear', bounds_error=True, fill_value=np.nan):
339
+ # V[i,j,k] = 100*x[i] + 10*y[j] + z[k]
340
+ fpinterp = RegularGridInterpolator((x,y,z), fp3d, method=method, bounds_error=bounds_error, fill_value=fill_value)
341
+ return fpinterp
342
+
343
+ # variation on makeNPC function in SimuFLUX by Marin & Ries (https://github.com/ries-lab/SimuFLUX)
344
+ def makeNPC(center=[0,0,0], R=50, copynumber=32, dz=50, rotation=0, twistangle=np.pi/32, shiftangle=np.pi/16, dR=3, dzpair=3.2):
345
+ if not isinstance(center, np.ndarray):
346
+ center = np.array(center, dtype=float)
347
+
348
+ dphi = np.pi/4
349
+ maxphi = 2 * np.pi - dphi - rotation
350
+ nphi = int(maxphi / dphi)
351
+ phi = np.arange(nphi+1)*dphi
352
+ v0 = np.zeros_like(phi)
353
+
354
+ if copynumber == 8:
355
+ phiall = phi
356
+ zall = v0
357
+ Rall = v0 + R
358
+ elif copynumber == 16:
359
+ phiall = np.hstack([phi, -phi+twistangle])
360
+ zall = np.hstack([v0+dz/2, v0-dz/2])
361
+ Rall = np.hstack([v0,v0]) + R
362
+ elif copynumber == 32:
363
+ phiall = np.hstack([phi, phi+shiftangle, -phi+twistangle, -phi + twistangle - shiftangle])
364
+ zall = np.hstack([v0+dz/2+dzpair/2, v0+dz/2-dzpair/2, v0-dz/2-dzpair/2, v0-dz/2+dzpair/2])
365
+ Rall = np.hstack([v0+R+dR, v0+R, v0+R+dR, v0+R])
366
+ else:
367
+ raise ValueError("NPC copy number: must be 8, 16 or 32, is: %d" % copynumber)
368
+
369
+ posnpc = np.vstack([Rall*np.cos(phiall), Rall*np.sin(phiall), zall]).T
370
+ posnpc += center
371
+
372
+ return posnpc
373
+
374
+ def npctemplate_detailed(x3d,y3d,z3d,npcgeometry,sigma=5.0):
375
+ vol = np.zeros_like(x3d)
376
+ d0, h0 = npcgeometry
377
+ npcpts = makeNPC(R=d0/2, dz=h0)
378
+ x=npcpts[:,0]
379
+ y=npcpts[:,1]
380
+ z=npcpts[:,2]
381
+ for i in range(x.size):
382
+ vol += np.exp(-((x3d-x[i])**2+(y3d-y[i])**2+(z3d-z[i])**2)/(2*sigma*sigma))
383
+
384
+ return vol
385
+
386
+ # we may rewrite this for our purpose if bounds violations become a problem
387
+ # code from https://stackoverflow.com/questions/21670080/how-to-find-global-minimum-in-python-optimization-with-bounds
388
+ class RandomDisplacementBounds(object):
389
+ """random displacement with bounds"""
390
+ def __init__(self, xmin, xmax, stepsize=0.5):
391
+ self.xmin = xmin
392
+ self.xmax = xmax
393
+ self.stepsize = stepsize
394
+
395
+ def __call__(self, x):
396
+ """take a random step but ensure the new position is within the bounds"""
397
+ while True:
398
+ # this could be done in a much more clever way, but it will work for example purposes
399
+ xnew = x + np.random.uniform(-self.stepsize, self.stepsize, np.shape(x))
400
+ if np.all(xnew < self.xmax) and np.all(xnew > self.xmin):
401
+ break
402
+ return xnew
403
+
404
+ # # define the new step taking routine and pass it to basinhopping
405
+ # take_step = RandomDisplacementBounds(xmin, xmax)
406
+ # result = basinhopping(f, x0, niter=100, minimizer_kwargs=minimizer_kwargs,
407
+ # take_step=take_step)
408
+
409
+ maxshift = 50.0
410
+ class LLmaximizerNPC3D(object):
411
+ # the bgprop value needs a little more thought, it could be specific for this set of parameters
412
+ def __init__(self, npcgeometry, extent_nm=150.0, voxelsize_nm=2.0, sigma=5.0, bgprob=1e-9,volcallback=None):
413
+ self.x = np.arange(-extent_nm/2.0, extent_nm/2.0+1.0, voxelsize_nm, dtype='f')
414
+ self.y = self.x.copy()
415
+ self.z = self.x.copy()
416
+ self.npcgeometry = npcgeometry
417
+ self.sigma = sigma
418
+ self.bgprob = bgprob
419
+ self.volcallback = volcallback
420
+
421
+ x2d,y2d = np.meshgrid(self.x,self.y)
422
+ x3d,y3d, z3d = np.meshgrid(self.x,self.y,self.z)
423
+
424
+ if volcallback is not None:
425
+ if not callable(volcallback):
426
+ raise RuntimeError("volcallback option is not callable")
427
+ self.fpg3d = volcallback(x3d,y3d,z3d,npcgeometry,sigma=sigma)
428
+ else:
429
+ eps=15.0 # we have removed the eps variable as input, set it here
430
+ d0, h0 = npcgeometry # diameter of ring and ring spacing
431
+ self.circ2d = (x2d**2 + y2d**2 -0.25*d0**2 <= eps**2) & (x2d**2 + y2d**2 -0.25*d0**2 >= -eps**2)
432
+ self.g3d = np.exp(-(x3d**2+y3d**2+z3d**2)/2.0/(sigma**2))
433
+ self.fp3d = np.zeros_like(x3d)
434
+ idz = np.argmin(np.abs(self.z-h0/2))
435
+ self.fp3d[:,:,idz] = self.circ2d
436
+ idz = np.argmin(np.abs(self.z-(-h0/2)))
437
+ self.fp3d[:,:,idz] = self.circ2d
438
+ self.fpg3d = np.clip(fftconvolve(self.fp3d,self.g3d,mode='same'),0,None)
439
+
440
+ self.fpg3d += bgprob # add background probability
441
+ self.fpg3d /= self.fpg3d.sum()
442
+ self.nllfp = -np.log10(self.fpg3d)
443
+ self.nllfpi = None
444
+ self.interpolator()
445
+
446
+ self.points = None
447
+ self.pars0 = (0.,0.,0., 0., 0., 100.0, 100.0) # shift_x, shift_y, shift_z, angle_around_z, angle_around_y, scale_xy, scale_z
448
+ self.bounds = (
449
+ (-maxshift,maxshift), # p[0]
450
+ (-maxshift,maxshift), # p[1]
451
+ (-maxshift,maxshift), # p[2]
452
+ (-90.0,90.0), # p[3]
453
+ (-35.0,35.0), # p[4]
454
+ (80.0,120.0), # p[5] - limit to 20% variation to avoid misfits
455
+ (50.0,150.0) # p[6] - limit to 50% variation to avoid misfits
456
+ )
457
+
458
+ def registerPoints(self,pts): # register candidate points for fitting
459
+ self.points = pts
460
+ self.transform_coords(self.pars0)
461
+
462
+ def fit(self,method='XXX'): # run the maximumLL fit
463
+ pass
464
+
465
+ def fitpars(self): # get the best fit parameters
466
+ pass
467
+
468
+ def LLcalc(self,params=None): # return log-likelihood for given parameter set; if None use best fit params
469
+ pass
470
+
471
+ def interpolator(self):
472
+ if self.nllfpi is None:
473
+ self.nllfpi = fpinterpolate(self.nllfp, self.x, self.y, self.z,bounds_error=False,fill_value=15.0) # need to think about the fill_value, it could need tweaking
474
+
475
+ return self.nllfpi
476
+
477
+ def lleval(self,pars):
478
+ c3d = self.transform_coords(pars)
479
+ llvals = self.nllfpi((c3d[:,0],c3d[:,1],c3d[:,2]))
480
+ return llvals.sum()
481
+
482
+ def transform_coords(self,pars):
483
+ if self.points is None:
484
+ raise RuntimeError("no valid points, please register points first")
485
+ c3d = self.points + [pars[0],pars[1],pars[2]] # pars[0:3] should be vector offset
486
+ self.c3dr = R.from_euler('zy', [pars[3],pars[4]], degrees=True).apply(c3d)
487
+ self.c3dr[:,0:2] *= 0.01*pars[5]
488
+ self.c3dr[:,2] *= 0.01*pars[6]
489
+ self._lastpars = pars
490
+ return self.c3dr
491
+
492
+ def transform_coords_inv(self,pars): # this is apparently currently not used and needs to be debugged before use
493
+ if 'c3dr' not in dir(self) or self.c3dr is None:
494
+ raise RuntimeError("need transformed points to start with")
495
+ c3dr = self.c3dr.copy()
496
+ c3dr[:,0:2] /= 0.01*pars[5]
497
+ c3dr[:,2] /= 0.01*pars[6]
498
+ c3di = R.from_euler('zy', [pars[3],pars[4]], degrees=True).inv().apply(c3dr)
499
+ c3di -= [pars[0],pars[1],pars[2]]
500
+ self.c3di = c3di
501
+ return self.c3di
502
+
503
+ def plot_points(self,mode='transformed',external_pts=None,axes=None,p0=None): # supported modes should be 'original', 'transformed', 'both', external
504
+ if mode == 'transformed':
505
+ x,y,z = xyzfrom3vec(self.c3dr)
506
+ elif mode == 'original':
507
+ x,y,z = xyzfrom3vec(self.points)
508
+ elif mode == 'both':
509
+ if p0 is not None: # in this mode we consider a p0 as initial transform if provided
510
+ plast = self._lastpars # perhaps better to use opt_result.x?
511
+ self.transform_coords(p0)
512
+ x,y,z = xyzfrom3vec(self.c3dr)
513
+ self.transform_coords(plast) # restore transformed coords to what they were at the start
514
+ else:
515
+ x,y,z = xyzfrom3vec(self.points)
516
+ x1,y1,z1 = xyzfrom3vec(self.c3dr)
517
+ elif mode == 'external':
518
+ if external_pts is None:
519
+ raise RuntimeError("with mode='external' need external points but none supplied")
520
+ x,y,z = xyzfrom3vec(external_pts)
521
+ else:
522
+ raise RuntimeError("unknown mode %s" % mode)
523
+
524
+ if mode == 'both':
525
+ if axes is None:
526
+ fig, (axt,axb) = plt.subplots(2,3)
527
+ else:
528
+ (axt,axb) = axes
529
+ else:
530
+ if axes is None:
531
+ fig, axt = plt.subplots(1,3,figsize=(6.4,2.4))
532
+ else:
533
+ axt = axes
534
+
535
+ axt[0].cla()
536
+ axt[0].imshow(self.fpg3d.sum(axis=2).T,extent=[self.x.min(), self.x.max(), self.y.min(), self.y.max()])
537
+ axt[0].scatter(x,y,c='orange',s=10)
538
+ axt[0].set_aspect('equal')
539
+ axt[0].set_title('x-y')
540
+ axt[1].cla()
541
+ axt[1].imshow(self.fpg3d.sum(axis=1).T,extent=[self.x.min(), self.x.max(), self.z.min(), self.z.max()])
542
+ axt[1].scatter(x,z,c='orange',s=10)
543
+ axt[1].set_aspect('equal')
544
+ axt[1].set_title('x-z')
545
+ axt[2].cla()
546
+ axt[2].imshow(self.fpg3d.sum(axis=0).T,extent=[self.y.min(), self.y.max(), self.z.min(), self.z.max()])
547
+ axt[2].scatter(y,z,c='orange',s=10)
548
+ axt[2].set_aspect('equal')
549
+ axt[2].set_title('y-z')
550
+
551
+ if mode == 'both':
552
+ axb[0].cla()
553
+ axb[0].imshow(self.fpg3d.sum(axis=2).T,extent=[self.x.min(), self.x.max(), self.y.min(), self.y.max()])
554
+ axb[0].scatter(x1,y1,c='orange',s=10)
555
+ axb[0].set_aspect('equal')
556
+ axb[0].set_title('x-y')
557
+ axb[1].cla()
558
+ axb[1].imshow(self.fpg3d.sum(axis=1).T,extent=[self.x.min(), self.x.max(), self.z.min(), self.z.max()])
559
+ axb[1].scatter(x1,z1,c='orange',s=10)
560
+ axb[1].set_aspect('equal')
561
+ axb[1].set_title('x-z')
562
+ axb[2].cla()
563
+ axb[2].imshow(self.fpg3d.sum(axis=0).T,extent=[self.y.min(), self.y.max(), self.z.min(), self.z.max()])
564
+ axb[2].scatter(y1,z1,c='orange',s=10)
565
+ axb[2].set_aspect('equal')
566
+ axb[2].set_title('y-z')
567
+
568
+
569
+ def function_to_minimize(self):
570
+ def minfunc(p):
571
+ return self.lleval(p)
572
+
573
+ return minfunc
574
+
575
+ # minimize the negative log likelihood
576
+ def nllminimize(self,p0=(0,0,0,0,0,100.0,100.0),method='L-BFGS-B'):
577
+ from scipy.optimize import minimize
578
+ self.p0 = p0
579
+ self.minmethod = method
580
+ self.opt_result = minimize(self.function_to_minimize(),p0,method=method,bounds=self.bounds)
581
+
582
+ def nll_basin_hopping(self,p0,method='L-BFGS-B',bounds=None):
583
+ from scipy.optimize import basinhopping
584
+ self.p0 = p0 # we record as p0 since pars0 is used at the time of "registerPoints"; will cause issues as nllm object is reused
585
+ self.minmethod = "basinhopping with %s" % method
586
+ if bounds is None:
587
+ bounds=self.bounds # default bounds
588
+ minimizer_kwargs = dict(method=method, bounds=bounds)
589
+ self.opt_result = basinhopping(self.function_to_minimize(), p0, minimizer_kwargs=minimizer_kwargs)
590
+
591
+ def pprint_lastpars(self):
592
+ print("Origin: %s" % self._lastpars[0:3])
593
+ print("Angles: %d rot-z, %d rot-y" % tuple(np.round(self._lastpars[3:5])))
594
+ print("Ring diam: %d, ring spacing: %d" % tuple(np.round(np.array(self.npcgeometry)*100.0/np.array(self._lastpars[5:]))))
595
+
596
+ class NPC3D(object):
597
+ def __init__(self, points=None, pipeline=None, objectID=None, zclip=None, offset_mode='mean'):
598
+ self.points = points
599
+ if pipeline is not None:
600
+ if objectID is None:
601
+ raise RuntimeError("need an objectID to set points from pipeline, None was given")
602
+ npcidx = pipeline['objectID'] == objectID
603
+ self.points = to3vecs(pipeline['x'][npcidx],pipeline['y'][npcidx],pipeline['z'][npcidx])
604
+ self.t = pipeline['t'][npcidx]
605
+ self.objectID = objectID
606
+ self.npts = None
607
+ if self.points is not None:
608
+ self.normalize_points(zclip=zclip, mode=offset_mode)
609
+ self.transformed_pts = None
610
+ self.opt_result = None
611
+ self.filtered_pts = None
612
+ self.fitted = False
613
+
614
+ def normalize_points(self,zclip=None,mode='mean'):
615
+ if mode == 'mean':
616
+ self.offset = self.points.mean(axis=0)[None,:]
617
+ elif mode == 'median':
618
+ self.offset = np.median(self.points,axis=0)[None,:]
619
+ else:
620
+ raise RuntimeError("unknown mode '%s', should be mean or median" % mode)
621
+ npts = self.points - self.offset
622
+ nt = self.t
623
+ if not zclip is None:
624
+ zgood = (npts[:,2] > -zclip)*(npts[:,2] < zclip)
625
+ npts = npts[zgood,:]
626
+ nt = nt[zgood]
627
+ self.npts = npts
628
+ self.nt = nt
629
+
630
+ def fitbymll(self,nllminimizer,plot=True,printpars=True,axes=None,preminimizer=None,axespre=None):
631
+ nllm = nllminimizer
632
+ self.nllminimizer = nllm
633
+ self.preminimizer = preminimizer
634
+
635
+ if preminimizer is not None:
636
+ preminimizer.registerPoints(self.npts)
637
+ preminimizer.nll_basin_hopping(p0=(0,0,0,0,0,100.0,100.0))
638
+ self.opt_result_pre = preminimizer.opt_result
639
+ self.transformed_pts_pre = preminimizer.c3dr
640
+ self.bounds_pre = preminimizer.bounds
641
+ # in the second stage llm minimizing stage start with best fit from previous fit as p0
642
+ # and allow mainly variation in rotation angles
643
+ # for other parameters only allow deviation from robust fitting in quite narrow range
644
+ p0 = self.opt_result_pre.x
645
+ dc = 3.0 # max deviation in coordinates (in nm)
646
+ dperc = 5.0 # max deviation in scaling percentage
647
+ bounds = (
648
+ (p0[0]-dc,p0[0]+dc), # p[0]
649
+ (p0[1]-dc,p0[1]+dc), # p[1]
650
+ (p0[2]-dc,p0[2]+dc), # p[2]
651
+ (-90.0,90.0), # p[3]
652
+ (-35.0,35.0), # p[4]
653
+ (p0[5]-dperc,p0[5]+dperc), # p[5] - limit to 20% variation to avoid misfits
654
+ (p0[6]-dperc,p0[6]+dperc) # p[6] - limit to 20% variation to avoid misfits
655
+ )
656
+ logger.info("p0 %s" % p0)
657
+ logger.info("bounds %s" % repr(bounds))
658
+
659
+ nllm.registerPoints(self.npts)
660
+ nllm.nll_basin_hopping(p0=p0,bounds=bounds)
661
+ self.opt_result = nllm.opt_result
662
+ self.transformed_pts = nllm.c3dr
663
+ self.bounds = bounds
664
+ else:
665
+ nllm.registerPoints(self.npts)
666
+ nllm.nll_basin_hopping(p0=(0,0,0,0,0,100.0,100.0)) # no bound keyword implies default bounds
667
+ self.opt_result = nllm.opt_result
668
+ self.transformed_pts = nllm.c3dr
669
+ self.bounds = nllm.bounds
670
+ self.fitted = True
671
+ if printpars:
672
+ nllm.pprint_lastpars()
673
+ if plot:
674
+ if preminimizer is not None:
675
+ preminimizer.plot_points(mode='both',axes=axespre)
676
+ p0 = nllm.p0
677
+ else:
678
+ p0 = None
679
+ nllm.plot_points(mode='both',axes=axes,p0=p0) # if a prefit was done we use its p0
680
+
681
+ def filter(self,axis='z',minval=0, maxval=100):
682
+ if axis == 'x':
683
+ coords = self.transformed_pts[:,0]
684
+ elif axis == 'y':
685
+ coords = self.transformed_pts[:,1]
686
+ elif axis == 'z':
687
+ coords = self.transformed_pts[:,2]
688
+ else:
689
+ raise RuntimeError("unknow axis %s requested (must be x, y or z)" % axis)
690
+
691
+ goodidx = (coords >= minval)*(coords <= maxval)
692
+ self.filtered_pts = self.transformed_pts[goodidx,:]
693
+ try:
694
+ self.filtered_t = self.nt[goodidx]
695
+ except AttributeError: # ignore if we do not have the 'nt' attribute
696
+ pass
697
+
698
+ def plot_points(self,mode='transformed'):
699
+ if mode == 'normalized':
700
+ pts = self.npts
701
+ elif mode == 'transformed':
702
+ pts = self.transformed_pts
703
+ elif mode == 'filtered':
704
+ pts = self.filtered_pts
705
+ else:
706
+ raise RuntimeError("unknown mode %s" % mode)
707
+
708
+ self.nllminimizer.plot_points(mode='external',external_pts=pts)
709
+
710
+
711
+ def plot_points3D(self,mode='transformed',ax=None,with_offset=False,s=10):
712
+ if mode == 'normalized':
713
+ pts = self.npts
714
+ if with_offset:
715
+ pts = pts + self.offset
716
+ elif mode == 'transformed':
717
+ pts = self.transformed_pts
718
+ elif mode == 'filtered':
719
+ pts = self.filtered_pts
720
+ else:
721
+ raise RuntimeError("unknown mode %s" % mode)
722
+
723
+ if ax is None:
724
+ fig = plt.figure()
725
+ ax = fig.add_subplot(projection='3d')
726
+ ax.scatter(pts[:,0], pts[:,1], pts[:,2], 'o', s=s)
727
+ return ax
728
+
729
+ # the nominal glyph diam and height, set by the LLM fitter npcgeometry
730
+ # note access will only work after llm fit has taklen place!
731
+ def get_glyph_diam(self):
732
+ return self.nllminimizer.npcgeometry[0]
733
+
734
+ def get_glyph_height(self):
735
+ return self.nllminimizer.npcgeometry[1]
736
+
737
+ def get_glyph(self, with_offset=True):
738
+
739
+ def get_circ_coords(radius=50.0, npoints=25):
740
+ angle = np.linspace( 0 , 2 * np.pi , npoints)
741
+ xc = radius * np.cos( angle )
742
+ yc = radius * np.sin( angle )
743
+ return (xc,yc)
744
+
745
+ def transform_coords_invs(x,y,z,pars,offset):
746
+ xr = x.copy()
747
+ yr = y.copy()
748
+ zr = z.copy()
749
+ zr /= 0.01*pars[6]
750
+ xr /= 0.01*pars[5]
751
+ yr /= 0.01*pars[5]
752
+ c3d = np.stack([xr,yr,zr],axis=1)
753
+ c3di = R.from_euler('zy', [pars[3],pars[4]], degrees=True).inv().apply(c3d)
754
+ c3di -= [pars[0],pars[1],pars[2]]
755
+ c3di += offset
756
+
757
+ return (c3di[:,0],c3di[:,1],c3di[:,2])
758
+
759
+ xc, yc = get_circ_coords(radius=0.5*self.get_glyph_diam(), npoints=25)
760
+
761
+ if with_offset:
762
+ offset = self.offset
763
+ else:
764
+ offset = 0
765
+ pars = self.opt_result.x
766
+ glyph = {}
767
+ x1,y1,z1 = transform_coords_invs(xc,yc,np.zeros_like(xc)-0.5*self.get_glyph_height(),pars,offset)
768
+ glyph['circ_bot'] = to3vecs(x1,y1,z1)
769
+ x2,y2,z2 = transform_coords_invs(xc,yc,np.zeros_like(xc)+0.5*self.get_glyph_height(),pars,offset)
770
+ glyph['circ_top'] = to3vecs(x2,y2,z2)
771
+ xa,ya,za = transform_coords_invs([0,0],[0,0],[-75.0,+75.0],pars,offset)
772
+ glyph['axis'] = to3vecs(xa,ya,za)
773
+
774
+ return glyph
775
+
776
+ def plot_points3D_with_glyph(self, ax=None, with_offset=False, s=10):
777
+
778
+ if ax is None:
779
+ fig = plt.figure()
780
+ ax = fig.add_subplot(projection='3d')
781
+
782
+ self.plot_points3D(mode='normalized',ax=ax,with_offset=with_offset,s=s)
783
+ glyph = self.get_glyph(with_offset=with_offset)
784
+ x1,y1,z1 = xyzfrom3vec(glyph['circ_bot'])
785
+ ax.plot(x1,y1,z1,'r')
786
+ x2,y2,z2 = xyzfrom3vec(glyph['circ_top'])
787
+ ax.plot(x2,y2,z2,'r')
788
+ xa,ya,za = xyzfrom3vec(glyph['axis'])
789
+ ax.plot(xa,ya,za,'r')
790
+
791
+ return ax
792
+
793
+
794
+ def nlabeled(self,nthresh=1,r0=50.0,dr=25.0,do_plot=False,rotlocked=True,zrange=150.0,analysis2d=False,rotation=None):
795
+ zrangeabs = abs(zrange)
796
+ if rotlocked and rotation is None:
797
+ self.filter('z',-zrangeabs,zrangeabs)
798
+ if self.filtered_pts.size > 0:
799
+ rotation = estimate_rotation2(self.filtered_pts[:,0],self.filtered_pts[:,1])
800
+ else:
801
+ rotation=None
802
+ self.rotation = rotation # remember rotation
803
+
804
+ if analysis2d:
805
+ self.filter('z',-zrangeabs,zrangeabs)
806
+ x=self.filtered_pts[:,0]
807
+ y=self.filtered_pts[:,1]
808
+ if self.filtered_pts.size > 0:
809
+ self.n = estimate_nlabeled(x,y,r0=r0,dr=dr,nthresh=nthresh,do_plot=do_plot,rotation=rotation)
810
+ else:
811
+ self.n = 0
812
+ return self.n
813
+ else:
814
+ self.filter('z',0,zrangeabs)
815
+ if self.filtered_pts.size > 0:
816
+ # self.plot_points('filtered')
817
+ x=self.filtered_pts[:,0]
818
+ y=self.filtered_pts[:,1]
819
+ (self.n_top,self.n_top_bysegments) = estimate_nlabeled(x,y,r0=r0,dr=dr,nthresh=nthresh,do_plot=do_plot,
820
+ rotation=rotation,return_bysegments=True)
821
+ else:
822
+ (self.n_top,self.n_top_bysegments) = (0,np.zeros((8)))
823
+ self.filter('z',-zrangeabs,0)
824
+ if self.filtered_pts.size > 0:
825
+ # self.plot_points('filtered')
826
+ x=self.filtered_pts[:,0]
827
+ y=self.filtered_pts[:,1]
828
+ (self.n_bot,self.n_bot_bysegments) = estimate_nlabeled(x,y,r0=r0,dr=dr,nthresh=nthresh,do_plot=do_plot,
829
+ rotation=rotation,return_bysegments=True)
830
+ else:
831
+ (self.n_bot,self.n_bot_bysegments) = (0,np.zeros((8)))
832
+ return (self.n_top,self.n_bot)
833
+
834
+ class NPC3DSet(object):
835
+ def __init__(self,filename=None,zclip=75.0,offset_mode='median',NPCdiam=100.0,NPCheight=70.0,
836
+ foreshortening=1.0,known_number=-1,templatemode='standard',sigma=7.0):
837
+ self.filename=filename
838
+ self.zclip = zclip
839
+ self.offset_mode = offset_mode
840
+ self.npcdiam = NPCdiam
841
+ self.npcheight = NPCheight
842
+ self.npcs = []
843
+ self.foreshortening=foreshortening # we just record this for reference
844
+ # TODO: expose llm parameters to this init method as needed in practice!
845
+ self.templatemode = templatemode
846
+ if templatemode == 'standard':
847
+ volcallback=None
848
+ # sigma = 7.0 # we set this via sigma keyword now
849
+ elif templatemode == 'detailed' or templatemode == 'twostage':
850
+ volcallback=npctemplate_detailed
851
+ # sigma = 7.0 # we set this via sigma keyword now
852
+ self.llm = LLmaximizerNPC3D([self.npcdiam,self.npcheight],sigma=sigma,bgprob=1e-9,extent_nm=300.0,volcallback=volcallback)
853
+ if templatemode == 'twostage':
854
+ self.llmpre = LLmaximizerNPC3D([self.npcdiam,self.npcheight],sigma=sigma,bgprob=1e-9,extent_nm=300.0,volcallback=None)
855
+ else:
856
+ self.llmpre = None
857
+ self.measurements = []
858
+ self.known_number = known_number # only considered if > 0
859
+
860
+ # v1.1
861
+ # - has templatemode added
862
+ # - has more complete recording of llm initialization parameters
863
+ # v1.2
864
+ # - twostage mode introduced with prefitting with robust template followed by detailed template
865
+ # v1.3
866
+ # - add time from original points
867
+ # v1.4
868
+ # - add bounds info to npc object when fitting
869
+ self._version='1.4' # REMEMBER to increment version when changing this object or the underlying npc object definitions
870
+
871
+ def registerNPC(self,npc):
872
+ self.npcs.append(npc)
873
+
874
+ def addNPCfromPipeline(self,pipeline,oid):
875
+ self.registerNPC(NPC3D(pipeline=pipeline,objectID=oid,zclip=self.zclip,offset_mode=self.offset_mode))
876
+
877
+ def measure_labeleff(self,nthresh=1,do_plot=False,printpars=False,refit=False):
878
+ self.measurements = []
879
+ if do_plot:
880
+ fig, axes = plt.subplots(2,3)
881
+ else:
882
+ axes = None
883
+ for npc in self.npcs:
884
+ if not npc.fitted or refit:
885
+ npc.fitbymll(self.llm,plot=do_plot,axes=axes,printpars=printpars)
886
+ nt,nb = npc.nlabeled(nthresh=nthresh,dr=20.0)
887
+ self.measurements.append([nt,nb])
888
+
889
+ def plot_labeleff(self,thresh=None):
890
+ from PYMEcs.misc.utils import get_timestamp_from_filename
891
+ if len(self.measurements) < 10:
892
+ raise RuntimeError("not enough measurements, need at least 10, got %d" %
893
+ len(self.measurements))
894
+ meas = np.array(self.measurements)
895
+ nlab = meas.sum(axis=1)
896
+ # fill with trailing zeros if we have a known number of NPCs but have fewer measurements
897
+ # the "missing NPCs" typically represent NPCs with no events
898
+ if int(self.known_number) > 0 and nlab.shape[0] < int(self.known_number):
899
+ nlab = np.pad(nlab,((0,int(self.known_number)-nlab.shape[0])))
900
+
901
+ plt.figure()
902
+ plotcdf_npc3d(nlab,timestamp=get_timestamp_from_filename(self.filename),thresh=thresh)
903
+
904
+ def diam(self):
905
+ diams = []
906
+ for npc in self.npcs:
907
+ diams.append(npc.get_glyph_diam()/(0.01*npc.opt_result.x[5]))
908
+ return diams
909
+
910
+ def height(self):
911
+ heights = []
912
+ for npc in self.npcs:
913
+ heights.append(npc.get_glyph_height()/(0.01*npc.opt_result.x[6]))
914
+ return heights
915
+
916
+ def n_bysegments(self):
917
+ nbs_top = []
918
+ nbs_bot = []
919
+ for npc in self.npcs:
920
+ if npc.fitted:
921
+ try:
922
+ nbs_top.append(npc.n_top_bysegments)
923
+ except AttributeError:
924
+ pass
925
+ try:
926
+ nbs_bot.append(npc.n_bot_bysegments)
927
+ except AttributeError:
928
+ pass
929
+ if len(nbs_top) == 0 and len(nbs_bot) == 0:
930
+ return None
931
+ else:
932
+ return dict(top=np.array(nbs_top),bottom=np.array(nbs_bot))
933
+
934
+ def version(self):
935
+ return self._version
936
+
937
+ class NPCSetContainer(object):
938
+ def __init__(self,npcs):
939
+ self.npcs=npcs
940
+
941
+ def get_npcset(self):
942
+ if '_unpickled' in dir(self):
943
+ warn(self._unpickled)
944
+ return None
945
+ return self.npcs
946
+
947
+ # we add custom pickling/unpickling methods so that an npcset instance in the
948
+ # PYME metadata won't greatly inflate the image saving footprint
949
+ # the pickled version of the npcset should be on disk already, no need to "properly" pickle/unpickle the NPCSetContainer
950
+ # which was created for this very purpose, i.e. the NPCSetContainer object needs to only persist during the lifetime of the pymevis viewer
951
+ def __getstate__(self):
952
+ warn("NPCset is being pickled - just a dummy mostly for PYME metadata - won't be usable after unpickling")
953
+ return 'not a valid npcset object after pickling/unpickling'
954
+
955
+ def __setstate__(self, d):
956
+ warn("NPCset is being unpickled - this is just a dummy unpickle, won't be usable after unpickling")
957
+ self._unpickled = d
958
+
959
+ def mk_NPC_gallery(npcs,mode,zclip3d,NPCRotationAngle,xoffs=0,yoffs=0,enforce_8foldsym=False):
960
+ x = np.empty((0))
961
+ y = np.empty((0))
962
+ z = np.empty((0))
963
+ t = np.empty((0),int)
964
+ objectID = np.empty((0),int)
965
+ is_top = np.empty((0),int)
966
+ segmentID = np.empty((0),int)
967
+ phi = np.empty((0))
968
+
969
+ if mode == 'TopOverBottom':
970
+ gspx = 180
971
+ gspy = 180
972
+ gsdx = 0
973
+ rowlength = 10
974
+ elif mode == 'TopBesideBottom':
975
+ gspx = 400
976
+ gspy = 180
977
+ gsdx = 180
978
+ rowlength = 5
979
+
980
+ elif mode == 'SingleAverageSBS':
981
+ gspx = 0
982
+ gspy = 0
983
+ gsdx = 180
984
+ rowlength = 5
985
+
986
+ else:
987
+ gspx = 0
988
+ gspy = 0
989
+ gsdx = 0
990
+ rowlength = 5
991
+
992
+ xtr = np.empty((0))
993
+ ytr = np.empty((0))
994
+ ztr = np.empty((0))
995
+ objectIDtr = np.empty((0),int)
996
+ polyidtr = np.empty((0),int)
997
+
998
+ xg = []
999
+ yg = []
1000
+ dphi = np.pi/4
1001
+ radius = 65.0
1002
+ pi_base = []
1003
+
1004
+ for i in range(8):
1005
+ xg.extend([0,radius*np.sin(i*dphi)])
1006
+ yg.extend([0,radius*np.cos(i*dphi)])
1007
+ pi_base.extend([i+1,i+1])
1008
+
1009
+ zt = np.full((16),25.0)
1010
+ zb = -np.full((16),25.0)
1011
+
1012
+ polyidx = np.array(pi_base,dtype='i')
1013
+ xga = np.array(xg)
1014
+ yga = np.array(yg)
1015
+
1016
+ def filtered_t(npc):
1017
+ if 'filtered_t' in dir(npc):
1018
+ return npc.filtered_t
1019
+ else:
1020
+ return range(npc.filtered_pts.shape[0])
1021
+
1022
+ if enforce_8foldsym:
1023
+ angled_repeats = 8
1024
+ else:
1025
+ angled_repeats = 1
1026
+
1027
+ for i,npc in enumerate(npcs.npcs):
1028
+ if not npc.fitted:
1029
+ warn("NPC not yet fitted, please call only after fitting")
1030
+ return
1031
+
1032
+ gxt = (i % rowlength) * gspx
1033
+ gxb = gxt + gsdx
1034
+ gy = (i // rowlength) * gspy
1035
+
1036
+ npc.filter('z',0,zclip3d)
1037
+ ptst = npc.filtered_pts
1038
+ tt = filtered_t(npc)
1039
+
1040
+ npc.filter('z',-zclip3d,0)
1041
+ ptsb = npc.filtered_pts
1042
+ tb = filtered_t(npc)
1043
+
1044
+ from scipy.spatial.transform import Rotation as R
1045
+ for i in range(angled_repeats):
1046
+ if npc.rotation is not None:
1047
+ if NPCRotationAngle == 'negative':
1048
+ factor = -1.0
1049
+ elif NPCRotationAngle == 'positive':
1050
+ factor = 1.0
1051
+ else:
1052
+ factor = 0.0
1053
+ ptst_t = R.from_euler('z', factor*npc.rotation + i*piover4, degrees=False).apply(ptst)
1054
+ ptsb_t = R.from_euler('z', factor*npc.rotation + i*piover4, degrees=False).apply(ptsb)
1055
+
1056
+ phit = phi_from_coords(ptst_t[:,0],ptst_t[:,1])
1057
+ phib = phi_from_coords(ptsb_t[:,0],ptsb_t[:,1])
1058
+
1059
+ x = np.append(x,ptst_t[:,0] + gxt)
1060
+ y = np.append(y,ptst_t[:,1] + gy)
1061
+ z = np.append(z,ptst_t[:,2])
1062
+ phi = np.append(phi, phit)
1063
+ segmentID = np.append(segmentID, ((phit+np.pi)/piover4).astype(int))
1064
+ t = np.append(t,tt)
1065
+
1066
+ x = np.append(x,ptsb_t[:,0] + gxb)
1067
+ y = np.append(y,ptsb_t[:,1] + gy)
1068
+ z = np.append(z,ptsb_t[:,2])
1069
+ phi = np.append(phi, phib)
1070
+ segmentID = np.append(segmentID, ((phib+np.pi)/piover4).astype(int))
1071
+ t = np.append(t,tb)
1072
+
1073
+ objectID = np.append(objectID,np.full_like(ptst[:,0],npc.objectID,dtype=int))
1074
+ objectID = np.append(objectID,np.full_like(ptsb[:,0],npc.objectID,dtype=int))
1075
+
1076
+ is_top = np.append(is_top,np.ones_like(phit,dtype=int))
1077
+ is_top = np.append(is_top,np.zeros_like(phib,dtype=int))
1078
+
1079
+ # remaining stuff for trace dict which shows segment boundaries
1080
+ xtr = np.append(xtr,xga + gxt)
1081
+ ytr = np.append(ytr,yga + gy)
1082
+ ztr = np.append(ztr,zt)
1083
+
1084
+ objectIDtr = np.append(objectIDtr, np.full_like(xga,npc.objectID,dtype=int))
1085
+ polyidtr = np.append(polyidtr, polyidx)
1086
+ polyidx += 8
1087
+
1088
+ xtr = np.append(xtr,xga + gxb)
1089
+ ytr = np.append(ytr,yga + gy)
1090
+ ztr = np.append(ztr,zb)
1091
+
1092
+ objectIDtr = np.append(objectIDtr, np.full_like(xga,npc.objectID,dtype=int))
1093
+ polyidtr = np.append(polyidtr, polyidx)
1094
+ polyidx += 8
1095
+
1096
+ # t = np.arange(x.size)
1097
+ A = np.full_like(x,10.0,dtype='f')
1098
+ error_x = np.full_like(x,1.0,dtype='f')
1099
+ error_y = np.full_like(x,1.0,dtype='f')
1100
+ error_z = np.full_like(x,1.0,dtype='f')
1101
+
1102
+ dsdict = dict(x=x+xoffs,y=y+yoffs,z=z,
1103
+ objectID=objectID,t=t,A=A,
1104
+ error_x=error_x,error_y=error_y,error_z=error_z,
1105
+ is_top=is_top,segmentID=segmentID,phi=phi)
1106
+
1107
+ trdict = dict(x=xtr+xoffs,y=ytr+yoffs,z=ztr,
1108
+ objectID=objectIDtr,polyIndex=polyidtr)
1109
+
1110
+ from PYME.IO.tabular import DictSource
1111
+ gallery = DictSource(dsdict)
1112
+ segments = DictSource(trdict)
1113
+
1114
+ return gallery,segments
1115
+
1116
+ def mk_npctemplates(npcs):
1117
+ x = np.empty((0))
1118
+ y = np.empty((0))
1119
+ z = np.empty((0))
1120
+ polyIndex = np.empty((0),int)
1121
+ polySize = np.empty((0),int)
1122
+ objectID = np.empty((0),int)
1123
+ NtopLabelled = np.empty((0),int)
1124
+ NbotLabelled = np.empty((0),int)
1125
+ NLabelled = np.empty((0),int)
1126
+ diams = np.empty((0),float)
1127
+ heights = np.empty((0),float)
1128
+ fitquals = np.empty((0),float)
1129
+ cx = np.empty((0),float)
1130
+ cy = np.empty((0),float)
1131
+ cz = np.empty((0),float)
1132
+ ci = 1
1133
+ for npc in npcs.npcs:
1134
+ nt, nb = (npc.n_top,npc.n_bot)
1135
+ glyph = npc.get_glyph()
1136
+ pars = npc.opt_result.x
1137
+ diam = npc.get_glyph_diam() / (0.01*pars[5])
1138
+ height = npc.get_glyph_height() / (0.01*pars[6])
1139
+ fitqual = npc.opt_result.fun/npc.npts.shape[0]
1140
+ offset = npc.offset[0]
1141
+ for poly in ['circ_bot','circ_top','axis']:
1142
+ c3 = glyph[poly]
1143
+ xg = c3[:,0]
1144
+ yg = c3[:,1]
1145
+ zg = c3[:,2]
1146
+ x = np.append(x,xg)
1147
+ y = np.append(y,yg)
1148
+ z = np.append(z,zg)
1149
+ polyIndex = np.append(polyIndex,np.full_like(xg,ci,dtype=int))
1150
+ polySize = np.append(polySize,np.full_like(xg,xg.size,dtype=int))
1151
+ ci += 1
1152
+ objectID = np.append(objectID,np.full_like(xg,npc.objectID,dtype=int))
1153
+ NtopLabelled = np.append(NtopLabelled,np.full_like(xg,nt,dtype=int))
1154
+ NbotLabelled = np.append(NbotLabelled,np.full_like(xg,nb,dtype=int))
1155
+ NLabelled = np.append(NLabelled,np.full_like(xg,nt+nb,dtype=int))
1156
+ diams = np.append(diams,np.full_like(xg,diam,dtype=float))
1157
+ heights = np.append(heights,np.full_like(xg,height,dtype=float))
1158
+ fitquals = np.append(fitquals,np.full_like(xg,fitqual,dtype=float))
1159
+ cx = np.append(cx,np.full_like(xg,offset[0]-pars[0],dtype=float))
1160
+ cy = np.append(cy,np.full_like(xg,offset[1]-pars[1],dtype=float))
1161
+ cz = np.append(cz,np.full_like(xg,offset[2]-pars[2],dtype=float))
1162
+ t = np.arange(x.size)
1163
+ A = np.full_like(x,10.0,dtype='f')
1164
+ error_x = np.full_like(x,1.0,dtype='f')
1165
+ error_y = np.full_like(x,1.0,dtype='f')
1166
+ error_z = np.full_like(x,1.0,dtype='f')
1167
+
1168
+ dsdict = dict(x=x,y=y,z=z,polyIndex=polyIndex,polySize=polySize,
1169
+ NtopLabelled=NtopLabelled,NbotLabelled=NbotLabelled,NLabelled=NLabelled,
1170
+ objectID=objectID,t=t,A=A,
1171
+ error_x=error_x,error_y=error_y,error_z=error_z,
1172
+ npc_height=heights,npc_diam=diams,npc_fitqual=fitquals,
1173
+ npc_ctrx=cx,npc_ctry=cy,npc_ctrz=cz)
1174
+
1175
+ from PYME.IO.tabular import DictSource
1176
+ return DictSource(dsdict)