ptylab 0.2.5__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 (55) hide show
  1. PtyLab/Engines/BaseEngine.py +1859 -0
  2. PtyLab/Engines/OPR.py +279 -0
  3. PtyLab/Engines/__init__.py +33 -0
  4. PtyLab/Engines/aPIE.py +357 -0
  5. PtyLab/Engines/e3PIE.py +235 -0
  6. PtyLab/Engines/ePIE.py +157 -0
  7. PtyLab/Engines/ePIE_TV.py +191 -0
  8. PtyLab/Engines/mPIE.py +270 -0
  9. PtyLab/Engines/mPIE_tv.py +246 -0
  10. PtyLab/Engines/mqNewton.py +251 -0
  11. PtyLab/Engines/multiPIE.py +205 -0
  12. PtyLab/Engines/pcPIE.py +202 -0
  13. PtyLab/Engines/qNewton.py +140 -0
  14. PtyLab/Engines/zPIE.py +364 -0
  15. PtyLab/ExperimentalData/ExperimentalData.py +288 -0
  16. PtyLab/ExperimentalData/__init__.py +1 -0
  17. PtyLab/GridGeneration/FermatGridGenerator.py +130 -0
  18. PtyLab/GridGeneration/__init__.py +0 -0
  19. PtyLab/Monitor/Monitor.py +361 -0
  20. PtyLab/Monitor/Plots.py +334 -0
  21. PtyLab/Monitor/README.md +13 -0
  22. PtyLab/Monitor/TensorboardMonitor.py +558 -0
  23. PtyLab/Monitor/__init__.py +0 -0
  24. PtyLab/Operators/Operators.py +1099 -0
  25. PtyLab/Operators/README.md +8 -0
  26. PtyLab/Operators/__init__.py +1 -0
  27. PtyLab/Operators/_propagation_kernels.py +36 -0
  28. PtyLab/Params/Params.py +217 -0
  29. PtyLab/Params/__init__.py +0 -0
  30. PtyLab/ProbeEngines/OPRP.py +151 -0
  31. PtyLab/ProbeEngines/StandardProbe.py +105 -0
  32. PtyLab/ProbeEngines/__init__.py +1 -0
  33. PtyLab/Reconstruction/CalibrationFPM.py +919 -0
  34. PtyLab/Reconstruction/Reconstruction.py +875 -0
  35. PtyLab/Reconstruction/__init__.py +0 -0
  36. PtyLab/Regularizers/__init__.py +207 -0
  37. PtyLab/__init__.py +56 -0
  38. PtyLab/io/__init__.py +12 -0
  39. PtyLab/io/generateSimulationData.py +135 -0
  40. PtyLab/io/readExample.py +70 -0
  41. PtyLab/io/readHdf5.py +125 -0
  42. PtyLab/utils/__init__.py +0 -0
  43. PtyLab/utils/alignment.py +52 -0
  44. PtyLab/utils/cli.py +50 -0
  45. PtyLab/utils/downloader.py +39 -0
  46. PtyLab/utils/fsvd.py +105 -0
  47. PtyLab/utils/gpuUtils.py +103 -0
  48. PtyLab/utils/initializationFunctions.py +62 -0
  49. PtyLab/utils/scanGrids.py +313 -0
  50. PtyLab/utils/utils.py +312 -0
  51. PtyLab/utils/visualisation.py +340 -0
  52. ptylab-0.2.5.dist-info/METADATA +150 -0
  53. ptylab-0.2.5.dist-info/RECORD +55 -0
  54. ptylab-0.2.5.dist-info/WHEEL +4 -0
  55. ptylab-0.2.5.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1859 @@
1
+ import logging
2
+ import warnings
3
+ import os
4
+
5
+ import numpy as np
6
+ from matplotlib import pyplot as plt
7
+
8
+ from PtyLab import Operators
9
+ from PtyLab.ExperimentalData.ExperimentalData import ExperimentalData
10
+ from PtyLab.Monitor.Monitor import Monitor
11
+ from PtyLab.Params.Params import Params
12
+ from PtyLab.Reconstruction.Reconstruction import (
13
+ Reconstruction,
14
+ calculate_pixel_positions,
15
+ )
16
+ from PtyLab.Regularizers import grad_TV
17
+
18
+ # PtyLab imports
19
+ from PtyLab.utils.gpuUtils import (
20
+ asNumpyArray,
21
+ getArrayModule,
22
+ isGpuArray,
23
+ transfer_fields_to_cpu,
24
+ transfer_fields_to_gpu,
25
+ )
26
+ from PtyLab.utils.utils import circ, fft2c, ifft2c, orthogonalizeModes
27
+
28
+ try:
29
+ import cupy as cp
30
+ from cupyx.scipy.ndimage import fourier_gaussian as fourier_gaussian_gpu
31
+ except ImportError:
32
+ from scipy.ndimage import fourier_gaussian as fourier_gaussian_gpu
33
+
34
+ cp = None
35
+ from scipy.ndimage import fourier_gaussian as fourier_gaussian_cpu
36
+
37
+
38
+ def smooth_amplitude(
39
+ field: np.ndarray, width: float, aleph: float, amplitude_only: bool = True
40
+ ):
41
+ """
42
+ Smooth the amplitude of a field. Optional phase can be smoothed as well.
43
+ Parameters
44
+ ----------
45
+ field
46
+ width
47
+ aleph
48
+ amplitude_only
49
+
50
+ Returns
51
+ -------
52
+
53
+ """
54
+ xp = getArrayModule(field)
55
+ smooth_fun = isGpuArray(field) and fourier_gaussian_gpu or fourier_gaussian_cpu
56
+ gimmel = 1e-5
57
+ if amplitude_only:
58
+ ph_field = field / (xp.abs(field) + gimmel)
59
+ A_field = abs(field)
60
+ else:
61
+ ph_field = 1
62
+ A_field = field
63
+ F_field = xp.fft.fft2(A_field)
64
+ for ax in [-2, -1]:
65
+ F_field = smooth_fun(F_field, width, axis=ax)
66
+ field_smooth = xp.fft.ifft2(F_field)
67
+
68
+ if amplitude_only:
69
+ field_smooth = abs(field_smooth) * ph_field
70
+ return aleph * field_smooth + (1 - aleph) * field
71
+
72
+
73
+ class BaseEngine(object):
74
+ """
75
+ Common properties that are common for all reconstruction Engines are defined here.
76
+
77
+ Unless you are testing the code, there's hardly any need to create this object. For your own implementation,
78
+ inherit from this object
79
+
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ reconstruction: Reconstruction,
85
+ experimentalData: ExperimentalData,
86
+ params: Params,
87
+ monitor: Monitor,
88
+ ):
89
+ # These statements don't copy any data, they just keep a reference to the object
90
+ self.betaObject = 0.25
91
+ self.reconstruction: Reconstruction = reconstruction
92
+ self.experimentalData = experimentalData
93
+ self.params = params
94
+ self.monitor = monitor
95
+ self.monitor.reconstruction = reconstruction
96
+
97
+ # datalogger
98
+ self.logger = logging.getLogger("BaseEngine")
99
+
100
+ def _prepareReconstruction(self):
101
+ """
102
+ Initialize everything that depends on user changeable attributes.
103
+ :return:
104
+ """
105
+ # check miscellaneous quantities specific for certain Engines
106
+ self._checkMISC()
107
+ self._checkFFT()
108
+ # self._initializeQuadraticPhase()
109
+ self._initialProbePowerCorrection()
110
+ self._probeWindow()
111
+ self._initializeErrors()
112
+ self._setObjectProbeROI()
113
+ self._showInitialGuesses()
114
+ self._initializePCParameters()
115
+ self._checkGPU() # checkGPU needs to be the last
116
+
117
+ # self.reconstruction.probe_storage.push(self.reconstruction.probe, 0, self.experimentalData.ptychogram.shape[0])
118
+
119
+ def _setCPSC(self):
120
+ """
121
+ set constrained-pixel-sum constraint:
122
+ -save measured diffraction patterns into ptychograpmDownsampled
123
+ -pad the probe (useful when having a pre-calibrated probe)
124
+ -update the coordinates
125
+ """
126
+
127
+ # save the measured ptychogram into ptychograpmDownsampled
128
+ self.experimentalData.ptychogramDownsampled = self.experimentalData.ptychogram
129
+
130
+ # pad the probe
131
+ padNum_before = (
132
+ (self.params.CPSCupsamplingFactor - 1) * self.reconstruction.Np // 2
133
+ )
134
+ padNum_after = (
135
+ self.params.CPSCupsamplingFactor - 1
136
+ ) * self.reconstruction.Np - padNum_before
137
+ self.reconstruction.probe = np.pad(
138
+ self.reconstruction.probe,
139
+ (
140
+ (0, 0),
141
+ (0, 0),
142
+ (0, 0),
143
+ (0, 0),
144
+ (padNum_before, padNum_after),
145
+ (padNum_before, padNum_after),
146
+ ),
147
+ )
148
+
149
+ # pad the momentums, buffers
150
+ if hasattr(self.reconstruction, "probeBuffer"):
151
+ self.reconstruction.probeBuffer = self.reconstruction.probe.copy()
152
+ if hasattr(self.reconstruction, "probeMomentum"):
153
+ self.reconstruction.probeMomentum = np.pad(
154
+ self.reconstruction.probeMomentum,
155
+ (
156
+ (0, 0),
157
+ (0, 0),
158
+ (0, 0),
159
+ (0, 0),
160
+ (padNum_before, padNum_after),
161
+ (padNum_before, padNum_after),
162
+ ),
163
+ )
164
+
165
+ # update coordinates (only need to update the Nd and dxd, the rest updates automatically)
166
+ self.reconstruction.Nd = (
167
+ self.experimentalData.ptychogramDownsampled.shape[-1]
168
+ * self.params.CPSCupsamplingFactor
169
+ )
170
+ self.reconstruction.dxd = (
171
+ self.reconstruction.dxd / self.params.CPSCupsamplingFactor
172
+ )
173
+
174
+ self.logger.info("CPSCswitch is on, coordinates(dxd,dxp,dxo) have been updated")
175
+
176
+ def update_data(self, experimentalData, reconstruction=None):
177
+ """Update the experimentalData if necessary"""
178
+ self.experimentalData = experimentalData
179
+ if reconstruction is not None:
180
+ self.reconstruction = reconstruction
181
+
182
+ def _initializePCParameters(self):
183
+ if self.params.positionCorrectionSwitch:
184
+ # additional pcPIE parameters as they appear in Matlab
185
+ self.daleth = 0.5 # feedback
186
+ self.beth = 0.9 # friction
187
+ self.adaptStep = 1 # adaptive step size
188
+ self.D = np.zeros(
189
+ (self.experimentalData.numFrames, 2)
190
+ ) # position search direction
191
+ # predefine shifts
192
+ rmax = 2
193
+ dy, dx = np.mgrid[-rmax : rmax + 1, -rmax : rmax + 1]
194
+
195
+ # self.rowShifts = dy.flatten()#np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1])
196
+ self.rowShifts = np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1])
197
+ # self.colShifts = dx.flatten()#np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1])
198
+ self.colShifts = np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1])
199
+ self.startAtIteration = 1
200
+ self.meanEncoder00 = np.mean(self.experimentalData.encoder[:, 0]).copy()
201
+ self.meanEncoder01 = np.mean(self.experimentalData.encoder[:, 1]).copy()
202
+
203
+ def _initializeErrors(self):
204
+ """
205
+ initialize all kinds of errors:
206
+ detectorError is a matrix calculated at each iteration (numFrames,Nd,Nd);
207
+ errorAtPos sums over detectorError at each iteration, (numFrames,1);
208
+ reconstruction.error sums over errorAtPos, one number at each iteration;
209
+ """
210
+ # initialize detector error matrices
211
+ if self.params.saveMemory:
212
+ self.reconstruction.detectorError = 0
213
+ else:
214
+ if not hasattr(self.reconstruction, "detectorError"):
215
+ self.reconstruction.detectorError = np.zeros(
216
+ (
217
+ self.experimentalData.numFrames,
218
+ self.reconstruction.Nd,
219
+ self.reconstruction.Nd,
220
+ )
221
+ )
222
+ # initialize energy at each scan position
223
+ if not hasattr(self.reconstruction, "errorAtPos"):
224
+ self.reconstruction.errorAtPos = np.zeros(
225
+ (self.experimentalData.numFrames, 1), dtype=np.float32
226
+ )
227
+ # initialize final error
228
+ if not hasattr(self.reconstruction, "error"):
229
+ self.reconstruction.error = []
230
+
231
+ def _initialProbePowerCorrection(self):
232
+ if self.params.probePowerCorrectionSwitch:
233
+ self.reconstruction.probe = (
234
+ self.reconstruction.probe
235
+ / np.sqrt(
236
+ np.sum(self.reconstruction.probe * self.reconstruction.probe.conj())
237
+ )
238
+ * self.experimentalData.maxProbePower
239
+ )
240
+
241
+ def _probeWindow(self):
242
+ # absorbing probe boundary: filter probe with super-gaussian window function
243
+ if not self.params.saveMemory or self.params.absorbingProbeBoundary:
244
+ self.probeWindow = np.exp(
245
+ -(
246
+ (
247
+ (self.reconstruction.Xp**2 + self.reconstruction.Yp**2)
248
+ / (
249
+ 2
250
+ * (
251
+ 3
252
+ / 4
253
+ * self.reconstruction.Np
254
+ * self.reconstruction.dxp
255
+ / 2.355
256
+ )
257
+ ** 2
258
+ )
259
+ )
260
+ ** 10
261
+ )
262
+ )
263
+
264
+ if self.params.probeBoundary:
265
+ self.probeWindow = circ(
266
+ self.reconstruction.Xp,
267
+ self.reconstruction.Yp,
268
+ self.experimentalData.entrancePupilDiameter
269
+ + self.experimentalData.entrancePupilDiameter * 0.2,
270
+ )
271
+
272
+ def _setObjectProbeROI(self, update=False):
273
+ """
274
+ Set object/probe ROI for monitoring
275
+ """
276
+ if not hasattr(self.monitor, "objectROI") or update:
277
+ if self.monitor.objectZoom == "full" or self.monitor.objectZoom is None:
278
+ self.monitor.objectROI = [
279
+ slice(None, None, None),
280
+ slice(None, None, None),
281
+ ]
282
+ else:
283
+ rx, ry = (
284
+ (
285
+ np.max(self.reconstruction.positions, axis=0)
286
+ - np.min(self.reconstruction.positions, axis=0)
287
+ + self.reconstruction.Np
288
+ )
289
+ / self.monitor.objectZoom
290
+ ).astype(int)
291
+ xc, yc = (
292
+ (
293
+ np.max(self.reconstruction.positions, axis=0)
294
+ + np.min(self.reconstruction.positions, axis=0)
295
+ + self.reconstruction.Np
296
+ )
297
+ / 2
298
+ ).astype(int)
299
+
300
+ # self.monitor.objectROI = [
301
+ # slice(
302
+ # max(0, yc - ry // 2), min(self.reconstruction.No, yc + ry // 2)
303
+ # ),
304
+ # slice(
305
+ # max(0, xc - rx // 2), min(self.reconstruction.No, xc + rx // 2)
306
+ # ),
307
+ # ]
308
+ self.monitor.objectROI = [
309
+ slice(
310
+ max(0, xc - rx // 2), min(self.reconstruction.No, xc + rx // 2)
311
+ ),
312
+ slice(
313
+ max(0, yc - ry // 2), min(self.reconstruction.No, yc + ry // 2)
314
+ )
315
+ ]
316
+
317
+
318
+ if not hasattr(self.monitor, "probeROI") or update:
319
+ if self.monitor.probeZoom == "full" or self.monitor.probeZoom is None:
320
+ self.monitor.probeROI = [slice(None, None), slice(None, None)]
321
+ else:
322
+ r = int(
323
+ self.experimentalData.entrancePupilDiameter
324
+ / self.reconstruction.dxp
325
+ / self.monitor.probeZoom
326
+ )
327
+ self.monitor.probeROI = [
328
+ slice(
329
+ max(0, self.reconstruction.Np // 2 - r),
330
+ min(self.reconstruction.Np, self.reconstruction.Np // 2 + r),
331
+ ),
332
+ slice(
333
+ max(0, self.reconstruction.Np // 2 - r),
334
+ min(self.reconstruction.Np, self.reconstruction.Np // 2 + r),
335
+ ),
336
+ ]
337
+
338
+ def _showInitialGuesses(self):
339
+ self.monitor.initializeMonitors()
340
+ objectEstimate = np.squeeze(
341
+ self.reconstruction.object[
342
+ ..., self.monitor.objectROI[0], self.monitor.objectROI[1]
343
+ ]
344
+ )
345
+ probeEstimate = np.squeeze(
346
+ self.reconstruction.probe[
347
+ ..., self.monitor.probeROI[0], self.monitor.probeROI[1]
348
+ ]
349
+ )
350
+
351
+ self.monitor.updateObjectProbeErrorMonitor(
352
+ error=self.reconstruction.error,
353
+ object_estimate=objectEstimate,
354
+ probe_estimate=probeEstimate,
355
+ zo=self.reconstruction.zo,
356
+ purity_probe=self.reconstruction.purityProbe,
357
+ purity_object=self.reconstruction.purityObject,
358
+ encoder_positions=self.reconstruction.positions,
359
+ )
360
+
361
+ # self.monitor.updateObjectProbeErrorMonitor()
362
+
363
+ def _checkMISC(self):
364
+ """
365
+ checks miscellaneous quantities specific certain Engines
366
+ """
367
+ if self.params.backgroundModeSwitch:
368
+ self.reconstruction.background = 1e-1 * np.ones(
369
+ (self.reconstruction.Np, self.reconstruction.Np)
370
+ )
371
+
372
+ # preallocate intensity scaling vector
373
+ if self.params.intensityConstraint == "fluctuation":
374
+ self.intensityScaling = np.ones(self.experimentalData.numFrames)
375
+
376
+ if self.params.intensityConstraint == "interferometric":
377
+ self.reconstruction.reference = np.ones(
378
+ self.reconstruction.probe[0, 0, 0, 0, ...].shape
379
+ )
380
+
381
+ # check if both probePoprobePowerCorrectionSwitch and modulusEnforcedProbeSwitch are on.
382
+ # Since this can cause a contradiction, it raises an error
383
+ if (
384
+ self.params.probePowerCorrectionSwitch
385
+ and self.params.modulusEnforcedProbeSwitch
386
+ ):
387
+ raise ValueError(
388
+ "probePowerCorrectionSwitch and modulusEnforcedProbeSwitch "
389
+ "can not simultaneously be switched on!"
390
+ )
391
+
392
+ if self.params.propagatorType == "ASP" and self.params.fftshiftSwitch:
393
+ raise ValueError(
394
+ "ASP propagatorType works only with fftshiftSwitch = False"
395
+ )
396
+ if self.params.propagatorType == "scaledASP" and self.params.fftshiftSwitch:
397
+ raise ValueError(
398
+ "scaledASP propagatorType works only with fftshiftSwitch = False"
399
+ )
400
+
401
+ if self.params.CPSCswitch:
402
+ if not hasattr(self.experimentalData, "ptychogramDownsampled"):
403
+ if self.params.CPSCupsamplingFactor == None:
404
+ raise ValueError(
405
+ "CPSCswitch is on, CPSCupsamplingFactor need to be set"
406
+ )
407
+ else:
408
+ self._setCPSC()
409
+
410
+ def _checkFFT(self):
411
+ """
412
+ shift arrays to accelerate fft
413
+ """
414
+ if self.params.fftshiftSwitch:
415
+ if self.params.fftshiftFlag == 0:
416
+ print("check fftshift...")
417
+ print("fftshift data for fast far-field update")
418
+ # shift detector quantities
419
+ self.experimentalData.ptychogram = np.fft.ifftshift(
420
+ self.experimentalData.ptychogram, axes=(-1, -2)
421
+ )
422
+ if hasattr(self.experimentalData, "ptychogramDownsampled"):
423
+ self.experimentalData.ptychogramDownsampled = np.fft.ifftshift(
424
+ self.experimentalData.ptychogramDownsampled, axes=(-1, -2)
425
+ )
426
+ if hasattr(self.experimentalData, "w"):
427
+ if self.experimentalData.W is not None:
428
+ self.experimentalData.W = np.fft.ifftshift(
429
+ self.experimentalData.W, axes=(-1, -2)
430
+ )
431
+ if self.experimentalData.emptyBeam is not None:
432
+ self.experimentalData.emptyBeam = np.fft.ifftshift(
433
+ self.experimentalData.emptyBeam, axes=(-1, -2)
434
+ )
435
+ if hasattr(self.experimentalData, "PSD"):
436
+ if self.experimentalData.PSD is not None:
437
+ self.experimentalData.PSD = np.fft.ifftshift(
438
+ self.experimentalData.PSD, axes=(-1, -2)
439
+ )
440
+ self.params.fftshiftFlag = 1
441
+ else:
442
+ if self.params.fftshiftFlag == 1:
443
+ print("check fftshift...")
444
+ print("ifftshift data")
445
+ self.experimentalData.ptychogram = np.fft.fftshift(
446
+ self.experimentalData.ptychogram, axes=(-1, -2)
447
+ )
448
+ if hasattr(self.experimentalData, "ptychogramDownsampled"):
449
+ self.experimentalData.ptychogramDownsampled = np.fft.fftshift(
450
+ self.experimentalData.ptychogramDownsampled, axes=(-1, -2)
451
+ )
452
+ if self.experimentalData.W != None:
453
+ self.experimentalData.W = np.fft.fftshift(
454
+ self.experimentalData.W, axes=(-1, -2)
455
+ )
456
+ if self.experimentalData.emptyBeam != None:
457
+ self.experimentalData.emptyBeam = np.fft.fftshift(
458
+ self.experimentalData.emptyBeam, axes=(-1, -2)
459
+ )
460
+ self.params.fftshiftFlag = 0
461
+
462
+ def _move_data_to_gpu(self):
463
+ """
464
+ Move the data to the GPU, called when the gpuSwitch is on.
465
+ :return:
466
+ """
467
+
468
+ self.reconstruction._move_data_to_gpu()
469
+ self.experimentalData._move_data_to_gpu()
470
+
471
+ transfer_fields_to_gpu(
472
+ self,
473
+ [
474
+ "probeWindow",
475
+ ],
476
+ self.logger,
477
+ ) # '.probeWindow = cp.array(self.probeWindow)
478
+
479
+ # reconstruction parameters
480
+ # self.reconstruction.probe = cp.array(self.reconstruction.probe, cp.complex64)
481
+ # self.reconstruction.object = cp.array(self.reconstruction.object, cp.complex64)
482
+ # self.reconstruction.detectorError = cp.array(self.reconstruction.detectorError, cp.float32)
483
+
484
+ # if self.params.momentumAcceleration:
485
+ # self.reconstruction.probeBuffer = cp.array(self.reconstruction.probeBuffer, cp.complex64)
486
+ # self.reconstruction.objectBuffer = cp.array(self.reconstruction.objectBuffer, cp.complex64)
487
+ # self.reconstruction.probeMomentum = cp.array(self.reconstruction.probeMomentum, cp.complex64)
488
+ # self.reconstruction.objectMomentum = cp.array(self.reconstruction.objectMomentum, cp.complex64)
489
+
490
+ # for doing the coordinate transform and especially the otherwise slow interpolation of aPIE on the gpu
491
+ if hasattr(self.params, "aPIEflag"):
492
+ if self.params.aPIEflag == True:
493
+ fields_to_transfer = [
494
+ "ptychogramUntransformed",
495
+ "Uq",
496
+ "Vq",
497
+ "theta",
498
+ "wavelength",
499
+ "Xd",
500
+ "Yd" "dxd",
501
+ "zo",
502
+ ]
503
+ self.theta = self.reconstruction.theta
504
+ self.wavelength = self.reconstruction.wavelength
505
+
506
+ transfer_fields_to_gpu(self, fields_to_transfer, self.logger)
507
+ # self.ptychogramUntransformed = cp.array(self.ptychogramUntransformed)
508
+ # self.Uq = cp.array(self.Uq)
509
+ # self.Vq = cp.array(self.Vq)
510
+ # self.theta = cp.array(self.reconstruction.theta)
511
+ # self.wavelength = cp.array(self.reconstruction.wavelength)
512
+ # self.Xd = cp.array(self.Xd)
513
+ # self.Yd = cp.array(self.Yd)
514
+ # self.dxd = cp.array(self.dxd)
515
+ # self.zo = cp.array(self.zo)
516
+ # self.experimentalData.W = cp.array(self.experimentalData.W)
517
+
518
+ # non-reconstruction parameters
519
+ # if hasattr(self.experimentalData, 'ptychogramDownsampled'):
520
+ # self.experimentalData.ptychogramDownsampled = cp.array(self.experimentalData.ptychogramDownsampled,
521
+ # cp.float32)
522
+ # else:
523
+ # self.experimentalData.ptychogram = cp.array(self.experimentalData.ptychogram, cp.float32)
524
+
525
+ # propagators
526
+ # if self.params.propagatorType == 'Fresnel':
527
+ # self.reconstruction.quadraticPhase = cp.array(self.reconstruction.quadraticPhase)
528
+ # elif self.params.propagatorType == 'ASP' or self.params.propagatorType == 'polychromeASP':
529
+ # self.reconstruction.transferFunction = cp.array(self.reconstruction.transferFunction)
530
+ # elif self.params.propagatorType == 'scaledASP' or self.params.propagatorType == 'scaledPolychromeASP':
531
+ # self.reconstruction.Q1 = cp.array(self.reconstruction.Q1)
532
+ # self.reconstruction.Q2 = cp.array(self.reconstruction.Q2)
533
+ # elif self.params.propagatorType == 'twoStepPolychrome':
534
+ # self.reconstruction.quadraticPhase = cp.array(self.reconstruction.quadraticPhase)
535
+ # self.reconstruction.transferFunction = cp.array(self.reconstruction.transferFunction)
536
+
537
+ # other parameters
538
+ # if self.params.backgroundModeSwitch:
539
+ # self.reconstruction.background = cp.array(self.reconstruction.background)
540
+ # if self.params.absorbingProbeBoundary or self.params.probeBoundary:
541
+
542
+ # if self.params.modulusEnforcedProbeSwitch:
543
+ # self.experimentalData.emptyBeam = cp.array(self.experimentalData.emptyBeam)
544
+ # if self.params.intensityConstraint == 'interferometric':
545
+ # self.reconstruction.reference = cp.array(self.reconstruction.reference)
546
+
547
+ def _move_data_to_cpu(self):
548
+ """
549
+ Move the data to the CPU, called when the gpuSwitch is off.
550
+ :return:
551
+ """
552
+ # reconstruction parameters
553
+
554
+ self.reconstruction._move_data_to_cpu()
555
+ self.experimentalData._move_data_to_cpu()
556
+ transfer_fields_to_cpu(
557
+ self,
558
+ [
559
+ "probeWindow",
560
+ ],
561
+ self.logger,
562
+ )
563
+
564
+ # self.reconstruction.move_to_CPU()
565
+ # self.params.move_to_CPU()
566
+
567
+ # self.reconstruction.probe = asNumpyArray(self.reconstruction.probe)
568
+ # self.reconstruction.object = asNumpyArray(self.reconstruction.object)
569
+
570
+ # if self.params.momentumAcceleration:
571
+ # reconstruction_fields_to_transfer = ['probeBuffer', 'objectBuffer', 'probeMomentum',' objectMomentum']
572
+ # for field in reconstruction_fields_to_transfer:
573
+ #
574
+ # setattr(self.reconstruction, field,)
575
+ # self.reconstruction.probeBuffer = self.reconstruction.probeBuffer.get()
576
+ # self.reconstruction.objectBuffer = self.reconstruction.objectBuffer.get()
577
+ # self.reconstruction.probeMomentum = self.reconstruction.probeMomentum.get()
578
+ # self.reconstruction.objectMomentum = self.reconstruction.objectMomentum.get()
579
+
580
+ # for doing the coordinate transform and especially the otherwise slow interpolation of aPIE on the gpu
581
+ # if hasattr(self.params, 'aPIEflag'):
582
+ # if self.params.aPIEflag:
583
+ # self.theta = self.theta.get()
584
+ #
585
+ fields_to_transfer = ["theta", "probeWindow"]
586
+ # self.probeWindow = self.probeWindow.get()
587
+
588
+ # non-reconstruction parameters
589
+ # if hasattr(self.experimentalData, 'ptychogramDownsampled'):
590
+ # self.experimentalData.ptychogramDownsampled = self.experimentalData.ptychogramDownsampled.get()
591
+ # else:
592
+ # self.experimentalData.ptychogram = self.experimentalData.ptychogram.get()
593
+ # self.reconstruction.detectorError = self.reconstruction.detectorError.get()
594
+
595
+ # propagators
596
+ # if self.params.propagatorType == 'Fresnel':
597
+ # self.reconstruction.quadraticPhase = self.reconstruction.quadraticPhase.get()
598
+ # elif self.params.propagatorType == 'ASP' or self.params.propagatorType == 'polychromeASP':
599
+ # self.reconstruction.transferFunction = self.reconstruction.transferFunction.get()
600
+ # elif self.params.propagatorType == 'scaledASP' or self.params.propagatorType == 'scaledPolychromeASP':
601
+ # self.reconstruction.Q1 = self.reconstruction.Q1.get()
602
+ # self.reconstruction.Q2 = self.reconstruction.Q2.get()
603
+ # elif self.params.propagatorType == 'twoStepPolychrome':
604
+ # self.reconstruction.quadraticPhase = self.reconstruction.quadraticPhase.get()
605
+ # self.reconstruction.transferFunction = self.reconstruction.transferFunction.get()
606
+
607
+ # other parameters
608
+ # if self.params.backgroundModeSwitch:
609
+ # # self.reconstruction.background = self.reconstruction.background.get()
610
+ # if self.params.absorbingProbeBoundary or self.params.probeBoundary:
611
+
612
+ # if self.params.modulusEnforcedProbeSwitch:
613
+ # self.experimentalData.emptyBeam = self.experimentalData.emptyBeam.get()
614
+ # # if self.params.intensityConstraint == 'interferometric':
615
+ # self.reconstruction.reference = self.reconstruction.reference.get()
616
+
617
+ def _checkGPU(self):
618
+ if not hasattr(self.params, "gpuFlag"):
619
+ self.params.gpuFlag = 0
620
+
621
+ if self.params._gpuSwitch:
622
+ if cp is None:
623
+ raise ImportError(
624
+ "Could not import cupy, therefore no GPU reconstruction is possible. To reconstruct, set the params.gpuSwitch to False."
625
+ )
626
+ if not self.params.gpuFlag:
627
+ self.logger.info("switch to gpu")
628
+
629
+ # load data to gpu
630
+ self._move_data_to_gpu()
631
+ self.params.gpuFlag = 1
632
+ # always do this as it gets away with hard to debug errors
633
+ self._move_data_to_gpu()
634
+ else:
635
+ self._move_data_to_cpu()
636
+ if self.params.gpuFlag:
637
+ self.logger.info("switch to cpu")
638
+ self._move_data_to_cpu()
639
+ self.params.gpuFlag = 0
640
+
641
+ def setPositionOrder(self):
642
+ if self.params.positionOrder == "sequential":
643
+ self.positionIndices = np.arange(self.experimentalData.numFrames)
644
+
645
+ elif self.params.positionOrder == "random":
646
+ if len(self.reconstruction.error) == 0:
647
+ self.positionIndices = np.arange(self.experimentalData.numFrames)
648
+ else:
649
+ if len(self.reconstruction.error) < 2:
650
+ self.positionIndices = np.arange(self.experimentalData.numFrames)
651
+ else:
652
+ self.positionIndices = np.arange(self.experimentalData.numFrames)
653
+ np.random.shuffle(self.positionIndices)
654
+
655
+ # order by illumiantion angles. Use smallest angles first
656
+ # (i.e. start with brightfield data first, then add the low SNR
657
+ # darkfield)
658
+ # todo check this with Antonios
659
+ elif self.params.positionOrder == "NA":
660
+ rows = self.reconstruction.positions[:, 0] - np.mean(
661
+ self.reconstruction.positions[:, 0]
662
+ )
663
+ cols = self.reconstruction.positions[:, 1] - np.mean(
664
+ self.reconstruction.positions[:, 1]
665
+ )
666
+ dist = np.sqrt(rows**2 + cols**2)
667
+ self.positionIndices = np.argsort(dist)
668
+ else:
669
+ raise ValueError("position order not properly set")
670
+
671
+ def changeExperimentalData(self, experimentalData: ExperimentalData):
672
+
673
+ if experimentalData is not None:
674
+ if not isinstance(experimentalData, ExperimentalData):
675
+ raise TypeError("Experimental data should be of class ExperimentalData")
676
+ self.experimentalData = experimentalData
677
+
678
+ def changeOptimizable(self, optimizable: Reconstruction):
679
+
680
+ if optimizable is not None:
681
+ if not isinstance(optimizable, Reconstruction):
682
+ raise TypeError(
683
+ f"Argument should be an subclass of Reconstruction, but it is {type(optimizable)}"
684
+ )
685
+ self.reconstruction = optimizable
686
+
687
+ def convert2single(self):
688
+ """
689
+ Convert the datasets to single precision. Matches: convert2single.m
690
+ :return:
691
+ """
692
+ self.dtype_complex = np.complex64
693
+ self.dtype_real = np.float32
694
+ self._match_dtypes_complex()
695
+ self._match_dtypes_real()
696
+
697
+ def _match_dtypes_complex(self):
698
+ raise NotImplementedError()
699
+
700
+ def _match_dtypes_real(self):
701
+ raise NotImplementedError()
702
+
703
+ def object2detector(self, esw=None):
704
+ """
705
+ Implements object2detector.m. Modifies esw in-place
706
+ :return:
707
+ """
708
+ if esw is None:
709
+ # todo: check this, it seems weird to store it in self.esw
710
+ esw = self.reconstruction.esw
711
+ self.esw, self.reconstruction.ESW = Operators.Operators.object2detector(
712
+ esw, self.params, self.reconstruction
713
+ )
714
+
715
+ def detector2object(self, ESW=None):
716
+ """
717
+ Propagate the ESW to the object plane (in-place).
718
+
719
+ Matches: detector2object.m
720
+ :return:
721
+ """
722
+ if ESW is None:
723
+ ESW = self.reconstruction.ESW
724
+ esw, eswUpdate = Operators.Operators.detector2object(
725
+ ESW, self.params, self.reconstruction
726
+ )
727
+ # Dirk is not sure why this has to be changed at all but it sometimes is changed for some reason
728
+ self.reconstruction.esw = esw
729
+ # this is the new estimate which will be processed later
730
+ self.reconstruction.eswUpdate = eswUpdate
731
+
732
+ def fft2s(self):
733
+ """
734
+ Computes the fourier transform of the exit surface wave.
735
+ :return:
736
+ """
737
+ self.reconstruction.ESW = FT2(
738
+ self.reconstruction.esw, self.params.fftshiftSwitch
739
+ )
740
+
741
+ def ifft2s(self):
742
+ """Inverse FFT"""
743
+ # find out if this should be performed on the GPU
744
+ self.reconstruction.eswUpdate = IFT(
745
+ self.reconstruction.ESW, self.params.fftshiftSwitch
746
+ )
747
+
748
+ def getBeamWidth(self):
749
+ """
750
+ Calculate probe beam width (Full width half maximum)
751
+ :return:
752
+ """
753
+ xp = getArrayModule(self.reconstruction.probe)
754
+ P = xp.sum(
755
+ abs((self.reconstruction.probe[..., -1, :, :])) ** 2,
756
+ axis=(0, 1, 2),
757
+ )
758
+ P = P / xp.sum(P, axis=(-1, -2))
759
+ P = asNumpyArray(P)
760
+ xMean = np.sum(self.reconstruction.Xp * P, axis=(-1, -2))
761
+ yMean = np.sum(self.reconstruction.Yp * P, axis=(-1, -2))
762
+ xVariance = np.sum((self.reconstruction.Xp - xMean) ** 2 * P, axis=(-1, -2))
763
+ yVariance = np.sum((self.reconstruction.Yp - yMean) ** 2 * P, axis=(-1, -2))
764
+
765
+ c = 2 * xp.sqrt(
766
+ 2 * xp.log(2)
767
+ ) # constant for converting variance to FWHM (see e.g. https://en.wikipedia.org/wiki/Full_width_at_half_maximum)
768
+
769
+ self.reconstruction.beamWidthX = asNumpyArray(c * np.sqrt(xVariance))
770
+ self.reconstruction.beamWidthY = asNumpyArray(c * np.sqrt(yVariance))
771
+
772
+ return self.reconstruction.beamWidthY, self.reconstruction.beamWidthX
773
+
774
+ def getOverlap(self, ind1, ind2):
775
+ """
776
+ Calculate linear and area overlap between two scan positions indexed ind1 and ind2
777
+ """
778
+ sy = (
779
+ abs(
780
+ self.reconstruction.positions[ind2, 0]
781
+ - self.reconstruction.positions[ind1, 0]
782
+ )
783
+ * self.reconstruction.dxp
784
+ )
785
+ sx = (
786
+ abs(
787
+ self.reconstruction.positions[ind2, 1]
788
+ - self.reconstruction.positions[ind1, 1]
789
+ )
790
+ * self.reconstruction.dxp
791
+ )
792
+
793
+ # task 1: get linear overlap
794
+ self.getBeamWidth()
795
+ self.reconstruction.linearOverlap = 1 - np.sqrt(sx**2 + sy**2) / np.minimum(
796
+ self.reconstruction.beamWidthX, self.reconstruction.beamWidthY
797
+ )
798
+ self.reconstruction.linearOverlap = np.maximum(
799
+ self.reconstruction.linearOverlap, 0
800
+ )
801
+
802
+ # task 2: get area overlap
803
+ # spatial frequency pixel size
804
+ df = 1 / (self.reconstruction.Np * self.reconstruction.dxp)
805
+ # spatial frequency meshgrid
806
+ fx = np.arange(-self.reconstruction.Np // 2, self.reconstruction.Np // 2) * df
807
+ Fx, Fy = np.meshgrid(fx, fx)
808
+ # absolute value of probe and 2D fft
809
+ P = abs(asNumpyArray(self.reconstruction.probe[:, 0, 0, -1, ...]))
810
+ Q = fft2c(P)
811
+ # calculate overlap between positions
812
+ self.reconstruction.areaOverlap = np.mean(
813
+ abs(
814
+ np.sum(
815
+ Q**2 * np.exp(-1.0j * 2 * np.pi * (Fx * sx + Fy * sy)),
816
+ axis=(-1, -2),
817
+ )
818
+ )
819
+ / np.sum(abs(Q) ** 2, axis=(-1, -2)),
820
+ axis=0,
821
+ )
822
+
823
+ def getErrorMetrics(self):
824
+ """
825
+ matches getErrorMetrics.m
826
+ :return:
827
+ """
828
+ if not self.params.saveMemory:
829
+ # Calculate mean error for all positions (make separate function for all of that)
830
+ if self.params.FourierMaskSwitch:
831
+ self.reconstruction.errorAtPos = np.sum(
832
+ np.abs(self.reconstruction.detectorError) * self.experimentalData.W,
833
+ axis=(-1, -2),
834
+ )
835
+ else:
836
+ self.reconstruction.errorAtPos = np.sum(
837
+ np.abs(self.reconstruction.detectorError), axis=(-1, -2)
838
+ )
839
+ self.reconstruction.errorAtPos = asNumpyArray(
840
+ self.reconstruction.errorAtPos
841
+ ) / asNumpyArray(self.experimentalData.energyAtPos + 1e-20)
842
+ eAverage = np.sum(self.reconstruction.errorAtPos)
843
+
844
+ # append to error vector (for plotting error as function of iteration)
845
+ self.reconstruction.error = np.append(self.reconstruction.error, eAverage)
846
+
847
+ def getRMSD(self, positionIndex):
848
+ """
849
+ Root mean square deviation between ptychogram and intensity estimate
850
+ :param positionIndex:
851
+ :return:
852
+ """
853
+ # find out wether or not to use the GPU
854
+ xp = getArrayModule(self.reconstruction.Iestimated)
855
+ self.currentDetectorError = abs(
856
+ self.reconstruction.Imeasured - self.reconstruction.Iestimated
857
+ )
858
+
859
+ # todo saveMemory implementation
860
+ if self.params.saveMemory:
861
+ if self.params.FourierMaskSwitch and not self.params.CPSCswitch:
862
+ self.reconstruction.errorAtPos[positionIndex] = xp.sum(
863
+ self.currentDetectorError * self.experimentalData.W
864
+ )
865
+ elif self.params.FourierMaskSwitch and self.params.CPSCswitch:
866
+ raise NotImplementedError
867
+ else:
868
+
869
+ self.reconstruction.errorAtPos[positionIndex] = asNumpyArray(
870
+ xp.sum(self.currentDetectorError)
871
+ )
872
+ else:
873
+ self.reconstruction.detectorError[positionIndex] = self.currentDetectorError
874
+
875
+ def intensityProjection(self, positionIndex):
876
+ """Compute the projected intensity.
877
+ Barebones, need to implement other methods
878
+ """
879
+ # figure out whether or not to use the GPU
880
+ xp = getArrayModule(self.reconstruction.esw)
881
+ # zero division mitigator
882
+ gimmel = 1e-10
883
+
884
+ # propagate to detector
885
+ self.object2detector()
886
+
887
+ # get estimated intensity (2D array, in the case of multislice, only take the last slice)
888
+ if self.params.intensityConstraint == "interferometric":
889
+ self.reconstruction.Iestimated = xp.sum(
890
+ xp.abs(self.reconstruction.ESW + self.reconstruction.reference) ** 2,
891
+ axis=(0, 1, 2),
892
+ )[-1]
893
+ else:
894
+ self.reconstruction.Iestimated = xp.sum(
895
+ xp.abs(self.reconstruction.ESW) ** 2, axis=(0, 1, 2)
896
+ )[-1]
897
+ self.logger.debug(
898
+ f"Estimated intensity: {self.reconstruction.Iestimated.sum()}, Measured: {self.experimentalData.ptychogram[positionIndex].sum()}"
899
+ )
900
+ if self.params.backgroundModeSwitch:
901
+ self.reconstruction.Iestimated += self.reconstruction.background
902
+
903
+ # get measured intensity todo implement kPIE
904
+ if self.params.CPSCswitch:
905
+ self.decompressionProjection(positionIndex)
906
+ else:
907
+ self.reconstruction.Imeasured = self.experimentalData.ptychogram[
908
+ positionIndex
909
+ ]
910
+
911
+ self.getRMSD(positionIndex)
912
+
913
+ # adaptive denoising
914
+ if self.params.adaptiveDenoisingSwitch:
915
+ self.adaptiveDenoising()
916
+
917
+ # intensity projection constraints
918
+ if self.params.intensityConstraint == "fluctuation":
919
+ # scaling
920
+ if self.params.FourierMaskSwitch:
921
+ aleph = xp.sum(
922
+ self.reconstruction.Imeasured
923
+ * self.reconstruction.Iestimated
924
+ * self.experimentalData.W
925
+ ) / xp.sum(
926
+ self.reconstruction.Imeasured
927
+ * self.reconstruction.Imeasured
928
+ * self.experimentalData.W
929
+ )
930
+ else:
931
+ aleph = xp.sum(
932
+ self.reconstruction.Imeasured * self.reconstruction.Iestimated
933
+ ) / xp.sum(
934
+ self.reconstruction.Imeasured * self.reconstruction.Imeasured
935
+ )
936
+ self.params.intensityScaling[positionIndex] = aleph
937
+ # scaled projection
938
+ frac = (
939
+ (1 + aleph)
940
+ / 2
941
+ * self.reconstruction.Imeasured
942
+ / (self.reconstruction.Iestimated + gimmel)
943
+ )
944
+
945
+ elif self.params.intensityConstraint == "exponential":
946
+ x = self.currentDetectorError / (self.reconstruction.Iestimated + gimmel)
947
+ W = xp.exp(-0.05 * x)
948
+ frac = xp.sqrt(
949
+ self.reconstruction.Imeasured
950
+ / (self.reconstruction.Iestimated + gimmel)
951
+ )
952
+ frac = W * frac + (1 - W)
953
+
954
+ elif self.params.intensityConstraint == "poission":
955
+ frac = self.reconstruction.Imeasured / (
956
+ self.reconstruction.Iestimated + gimmel
957
+ )
958
+
959
+ elif (
960
+ self.params.intensityConstraint == "standard"
961
+ or self.params.intensityConstraint == "interferometric"
962
+ ):
963
+ frac = xp.sqrt(
964
+ self.reconstruction.Imeasured
965
+ / (self.reconstruction.Iestimated + gimmel)
966
+ )
967
+
968
+ else:
969
+ raise ValueError("intensity constraint not properly specified!")
970
+
971
+ # apply mask
972
+ if (
973
+ self.params.FourierMaskSwitch
974
+ and self.params.CPSCswitch
975
+ and len(self.reconstruction.error) > 5
976
+ ):
977
+ frac = self.experimentalData.W * frac + (1 - self.experimentalData.W)
978
+
979
+ # update ESW
980
+ if self.params.intensityConstraint == "interferometric":
981
+ temp = (
982
+ self.reconstruction.ESW + self.reconstruction.reference
983
+ ) * frac - self.reconstruction.ESW
984
+ self.reconstruction.ESW = (
985
+ self.reconstruction.ESW + self.reconstruction.reference
986
+ ) * frac - self.reconstruction.reference
987
+ self.reconstruction.reference = temp
988
+ else:
989
+ if hasattr(self.params, 'intensityMask'):
990
+ if self.params.intensityMask:
991
+ self.reconstruction.ESW = self.reconstruction.ESW * (frac * (self.reconstruction.intensity_mask) + (self.reconstruction.intensity_mask - 1))
992
+ else:
993
+ self.reconstruction.ESW = self.reconstruction.ESW * frac
994
+ else:
995
+ self.reconstruction.ESW = self.reconstruction.ESW * frac
996
+
997
+ # update background (see PhD thsis by Peng Li)
998
+ if self.params.backgroundModeSwitch:
999
+ if self.params.FourierMaskSwitch:
1000
+ self.reconstruction.background = (
1001
+ self.reconstruction.background
1002
+ * (1 + 1 / self.experimentalData.numFrames * (xp.sqrt(frac) - 1))
1003
+ ** 2
1004
+ * self.experimentalData.W
1005
+ )
1006
+ else:
1007
+ self.reconstruction.background = (
1008
+ self.reconstruction.background
1009
+ * (1 + 1 / self.experimentalData.numFrames * (xp.sqrt(frac) - 1))
1010
+ ** 2
1011
+ )
1012
+
1013
+ # back propagate to object plane
1014
+ self.detector2object()
1015
+
1016
+ def decompressionProjection(self, positionIndex):
1017
+ """
1018
+ calculate the upsampled Imeasured from downsampled Imeasured that is actually measured.
1019
+ :param positionIndex: index for scan positions
1020
+ :return:
1021
+ """
1022
+ # overwrite the measured intensity (just to have same dimensions as Iestimated)
1023
+ xp = getArrayModule(self.reconstruction.Iestimated)
1024
+
1025
+ # determine downsampled fraction (Sl)
1026
+ frac = self.experimentalData.ptychogramDownsampled[positionIndex] / (
1027
+ xp.sum(
1028
+ self.reconstruction.Iestimated.reshape(
1029
+ self.reconstruction.Nd // self.params.CPSCupsamplingFactor,
1030
+ self.params.CPSCupsamplingFactor,
1031
+ self.reconstruction.Nd // self.params.CPSCupsamplingFactor,
1032
+ self.params.CPSCupsamplingFactor,
1033
+ ),
1034
+ axis=(1, 3),
1035
+ )
1036
+ + np.finfo(np.float32).eps
1037
+ )
1038
+ if self.params.FourierMaskSwitch and len(self.reconstruction.error) > 5:
1039
+ frac = self.experimentalData.W * frac + (1 - self.experimentalData.W)
1040
+ # overwrite up-sampled measured intensity
1041
+ self.reconstruction.Imeasured = self.reconstruction.Iestimated * xp.repeat(
1042
+ xp.repeat(frac, self.params.CPSCupsamplingFactor, axis=-1),
1043
+ self.params.CPSCupsamplingFactor,
1044
+ axis=-2,
1045
+ )
1046
+
1047
+ def showReconstruction(self, loop):
1048
+ """
1049
+ Show the reconstruction process.
1050
+ :param loop: the iteration number
1051
+ :return:
1052
+ """
1053
+ if np.mod(loop, self.monitor.figureUpdateFrequency) == 0:
1054
+
1055
+ if self.experimentalData.operationMode == "FPM":
1056
+ object_estimate = np.squeeze(
1057
+ asNumpyArray(
1058
+ fft2c(self.reconstruction.object)[
1059
+ ..., self.monitor.objectROI[0], self.monitor.objectROI[1]
1060
+ ]
1061
+ )
1062
+ )
1063
+ probe_estimate = np.squeeze(
1064
+ asNumpyArray(
1065
+ self.reconstruction.probe[
1066
+ ..., self.monitor.probeROI[0], self.monitor.probeROI[1]
1067
+ ]
1068
+ )
1069
+ )
1070
+ else:
1071
+ object_estimate = np.squeeze(
1072
+ asNumpyArray(
1073
+ self.reconstruction.object[
1074
+ ..., self.monitor.objectROI[0], self.monitor.objectROI[1]
1075
+ ]
1076
+ )
1077
+ )
1078
+ probe_estimate = np.squeeze(
1079
+ asNumpyArray(
1080
+ self.reconstruction.probe[
1081
+ ..., self.monitor.probeROI[0], self.monitor.probeROI[1]
1082
+ ]
1083
+ )
1084
+ )
1085
+ self.monitor.updateObjectProbeErrorMonitor(
1086
+ error=self.reconstruction.error,
1087
+ object_estimate=object_estimate,
1088
+ probe_estimate=probe_estimate,
1089
+ zo=self.reconstruction.zo,
1090
+ purity_probe=self.reconstruction.purityProbe,
1091
+ purity_object=self.reconstruction.purityObject,
1092
+ encoder_positions=self.reconstruction.positions,
1093
+ )
1094
+
1095
+ self.monitor.writeEngineName(repr(type(self)))
1096
+
1097
+ self.monitor.update_encoder(
1098
+ corrected_positions=self.reconstruction.encoder_corrected,
1099
+ original_positions=self.experimentalData.encoder,
1100
+ )
1101
+
1102
+ self.monitor.updateBeamWidth(*self.getBeamWidth())
1103
+
1104
+ # self.monitor.visualize_probe_engine(self.reconstruction.probe_storage)
1105
+
1106
+ if self.monitor.verboseLevel == "high":
1107
+ if self.params.fftshiftSwitch:
1108
+ Iestimated = np.fft.fftshift(
1109
+ asNumpyArray(self.reconstruction.Iestimated)
1110
+ )
1111
+ Imeasured = np.fft.fftshift(
1112
+ asNumpyArray(self.reconstruction.Imeasured)
1113
+ )
1114
+ else:
1115
+ Iestimated = asNumpyArray(self.reconstruction.Iestimated)
1116
+ Imeasured = asNumpyArray(self.reconstruction.Imeasured)
1117
+
1118
+ self.monitor.updateDiffractionDataMonitor(
1119
+ Iestimated=Iestimated, Imeasured=Imeasured
1120
+ )
1121
+
1122
+ self.getOverlap(0, 1)
1123
+
1124
+ self.pbar.write("")
1125
+ self.pbar.write("iteration: %i" % loop)
1126
+ self.pbar.write("error: %.1f" % self.reconstruction.error[-1])
1127
+ self.pbar.write(
1128
+ "estimated linear overlap: %.1f %%"
1129
+ % (100 * self.reconstruction.linearOverlap)
1130
+ )
1131
+ self.pbar.write(
1132
+ "estimated area overlap: %.1f %%"
1133
+ % (100 * self.reconstruction.areaOverlap)
1134
+ )
1135
+
1136
+ self.monitor.update_overlap(
1137
+ self.reconstruction.areaOverlap, self.reconstruction.linearOverlap
1138
+ )
1139
+ # self.pbar.write('coherence structure:')
1140
+
1141
+ if self.params.positionCorrectionSwitch:
1142
+ # show reconstruction
1143
+ return
1144
+ if (
1145
+ len(self.reconstruction.error) > self.startAtIteration
1146
+ ): # & (np.mod(loop,
1147
+ # self.monitor.figureUpdateFrequency) == 0):
1148
+ figure, ax = plt.subplots(
1149
+ 1, 1, num=102, squeeze=True, clear=True, figsize=(5, 5)
1150
+ )
1151
+ ax.set_title("Estimated scan grid positions")
1152
+ ax.set_xlabel("(um)")
1153
+ ax.set_ylabel("(um)")
1154
+ # ax.set_xscale('symlog')
1155
+ (line1,) = plt.plot(
1156
+ (
1157
+ self.reconstruction.positions0[:, 1]
1158
+ - self.reconstruction.No // 2
1159
+ + self.reconstruction.Np // 2
1160
+ )
1161
+ * self.reconstruction.dxo
1162
+ * 1e6,
1163
+ (
1164
+ self.reconstruction.positions0[:, 0]
1165
+ - self.reconstruction.No // 2
1166
+ + self.reconstruction.Np // 2
1167
+ )
1168
+ * self.reconstruction.dxo
1169
+ * 1e6,
1170
+ "bo",
1171
+ label="before correction",
1172
+ )
1173
+ (line2,) = plt.plot(
1174
+ (
1175
+ self.reconstruction.positions[:, 1]
1176
+ - self.reconstruction.No // 2
1177
+ + self.reconstruction.Np // 2
1178
+ )
1179
+ * self.reconstruction.dxo
1180
+ * 1e6,
1181
+ (
1182
+ self.reconstruction.positions[:, 0]
1183
+ - self.reconstruction.No // 2
1184
+ + self.reconstruction.Np // 2
1185
+ )
1186
+ * self.reconstruction.dxo
1187
+ * 1e6,
1188
+ "yo",
1189
+ label="after correction",
1190
+ )
1191
+ # plt.xlabel('(um))')
1192
+ # plt.ylabel('(um))')
1193
+ # plt.show()
1194
+ plt.legend(handles=[line1, line2])
1195
+ plt.tight_layout()
1196
+ # plt.show(block=False)
1197
+
1198
+ figure2, ax2 = plt.subplots(
1199
+ 1, 1, num=103, squeeze=True, clear=True, figsize=(5, 5)
1200
+ )
1201
+ ax2.set_title("Displacement")
1202
+ ax2.set_xlabel("(um)")
1203
+ ax2.set_ylabel("(um)")
1204
+ plt.plot(
1205
+ self.D[:, 1] * self.reconstruction.dxo * 1e6,
1206
+ self.D[:, 0] * self.reconstruction.dxo * 1e6,
1207
+ "o",
1208
+ )
1209
+ # ax.set_xscale('symlog')
1210
+ plt.tight_layout()
1211
+ # plt.show(block=False)
1212
+
1213
+ # elif np.mod(loop, self.monitor.figureUpdateFrequency) == 0:
1214
+ figure.show()
1215
+ figure2.show()
1216
+ figure.canvas.draw()
1217
+ figure.canvas.flush_events()
1218
+ figure2.canvas.draw()
1219
+ figure2.canvas.flush_events()
1220
+ # self.showReconstruction(loop)
1221
+ # print('iteration:%i' %len(self.reconstruction.error))
1222
+ # print('runtime:')
1223
+ # print('error:')
1224
+
1225
+ # Dump each iteration the current object
1226
+ if self.params.dump_obj:
1227
+ folder_path = 'dumps'
1228
+ if loop == 0:
1229
+ if not os.path.exists(folder_path):
1230
+ # Create the folder
1231
+ os.makedirs(folder_path)
1232
+ print(f"Folder '{folder_path}' created.")
1233
+ else:
1234
+ print(f"Folder '{folder_path}' already exists.")
1235
+
1236
+ filename = 'obj_dump_' + str(loop) + '.h5py'
1237
+ import h5py
1238
+ file_path = os.path.join(folder_path, filename)
1239
+ with h5py.File(file_path, 'w') as hdf:
1240
+ obj = self.reconstruction.object.get()
1241
+ hdf.create_dataset('Object', data=obj)
1242
+
1243
+
1244
+ def positionCorrection(self, objectPatch, positionIndex, sy, sx):
1245
+ """
1246
+ Modified from pcPIE. Position correction is done by using positionCorrection and positionCorrectionUpdate
1247
+ :param objectPatch:
1248
+ :param positionIndex:
1249
+ :param sy:
1250
+ :param sx:
1251
+ :return:
1252
+ """
1253
+
1254
+ xp = getArrayModule(objectPatch)
1255
+ if len(self.reconstruction.error) > self.startAtIteration:
1256
+ self.logger.debug("Calculating position correction")
1257
+ # position gradients
1258
+ # shiftedImages = xp.zeros((self.rowShifts.shape + objectPatch.shape))
1259
+ cc = xp.zeros((len(self.rowShifts), 1))
1260
+
1261
+ # use the real-space object (FFT for FPM)
1262
+ O = self.reconstruction.object
1263
+ Opatch = objectPatch
1264
+ if self.experimentalData.operationMode == "FPM":
1265
+ O = fft2c(self.reconstruction.object)
1266
+ Opatch = fft2c(objectPatch)
1267
+
1268
+ if self.params.positionCorrectionSwitch_radius < 2:
1269
+ # do the direct one as it's a bit faster
1270
+
1271
+ for shifts in range(len(self.rowShifts)):
1272
+ tempShift = xp.roll(Opatch, self.rowShifts[shifts], axis=-2)
1273
+ # shiftedImages[shifts, ...] = xp.roll(tempShift, self.colShifts[shifts], axis=-1)
1274
+ shiftedImages = xp.roll(tempShift, self.colShifts[shifts], axis=-1)
1275
+ cc[shifts] = xp.squeeze(
1276
+ xp.sum(shiftedImages.conj() * O[..., sy, sx], axis=(-2, -1))
1277
+ )
1278
+ del tempShift, shiftedImages
1279
+ betaGrad = 1000
1280
+ r = 3
1281
+ else:
1282
+ # print('doing FT position correction')
1283
+ ss = slice(
1284
+ -self.params.positionCorrectionSwitch_radius,
1285
+ self.params.positionCorrectionSwitch_radius + 1,
1286
+ )
1287
+ rowShifts, colShifts = xp.mgrid[ss, ss]
1288
+ self.rowShifts = rowShifts.flatten()
1289
+ self.colShifts = colShifts.flatten()
1290
+ FT_O = xp.fft.fft2(O[..., sy, sx] - O[..., sy, sx].mean())
1291
+ FT_Op = xp.fft.fft2(Opatch - O.mean())
1292
+ xcor = xp.fft.ifft2(FT_O * FT_Op.conj())
1293
+ xcor = abs(xp.fft.fftshift(xcor))
1294
+ N = xcor.shape[-1]
1295
+ sy = slice(
1296
+ N // 2 - self.params.positionCorrectionSwitch_radius,
1297
+ N // 2 + self.params.positionCorrectionSwitch_radius + 1,
1298
+ )
1299
+ xcor = xcor[..., sy, sy]
1300
+ cc = xcor.flatten()
1301
+ betaGrad = 5
1302
+ r = 10
1303
+ # dy, dx = xp.unravel_index(xp.argmax(xcor), xcor.shape)
1304
+ # dx = dx.get()
1305
+ # truncated cross - correlation
1306
+ # cc = xp.squeeze(xp.sum(shiftedImages.conj() * self.reconstruction.object[..., sy, sx], axis=(-2, -1)))
1307
+ cc = abs(cc)
1308
+
1309
+ normFactor = xp.sum(Opatch.conj() * Opatch, axis=(-2, -1)).real
1310
+ grad_x = betaGrad * xp.sum(
1311
+ (cc.T - xp.mean(cc)) / normFactor * xp.array(self.colShifts)
1312
+ )
1313
+ grad_y = betaGrad * xp.sum(
1314
+ (cc.T - xp.mean(cc)) / normFactor * xp.array(self.rowShifts)
1315
+ )
1316
+ # r = np.clip(self.params.positionCorrectionSwitch_radius//5, 3, self.reconstruction.Np//10) # maximum shift in pixels?
1317
+
1318
+ if abs(grad_x) > r:
1319
+ grad_x = r * grad_x / abs(grad_x)
1320
+ if abs(grad_y) > r:
1321
+ grad_y = r * grad_y / abs(grad_y)
1322
+ grad_y = asNumpyArray(grad_y)
1323
+ grad_x = asNumpyArray(grad_x)
1324
+ delta_p = self.daleth * np.array([grad_y, grad_x])
1325
+ self.D[positionIndex, :] = delta_p + self.beth * self.D[positionIndex, :]
1326
+ return delta_p
1327
+ return np.zeros(2)
1328
+
1329
+ def position_update_to_change_in_z(self, loop):
1330
+ """
1331
+ Update the z based on the position updates.
1332
+ """
1333
+ import jax
1334
+ from jax.experimental import optimizers
1335
+
1336
+ if not hasattr(self, "optlib"):
1337
+ self.i_z_optimizer = 0
1338
+ # from itertools import count
1339
+ # count
1340
+ op_init, op_update, op_get = optimizers.adam(3e-3)
1341
+ state = op_init(self.reconstruction.zo)
1342
+ self.optlib = {"op_update": op_update, "op_get": op_get, "state": state}
1343
+ else:
1344
+ state = self.optlib["state"]
1345
+ op_get = self.optlib["op_get"]
1346
+ op_update = self.optlib["op_update"]
1347
+
1348
+ X0 = self.reconstruction.encoder_corrected
1349
+ Y0 = self.experimentalData.encoder
1350
+ msqdisplacement = np.linalg.norm(1e6 * X0 - 1e6 * Y0)
1351
+
1352
+ # center both
1353
+ X0 = X0 - X0.mean(axis=0, keepdims=True)
1354
+ Y0 = Y0 - Y0.mean(axis=0, keepdims=True)
1355
+
1356
+ # now, find the scaling with respect to the original one
1357
+ factor = np.std(X0) / np.std(Y0)
1358
+
1359
+ # update z
1360
+ new_z = self.reconstruction.zo / factor
1361
+ step = new_z - self.reconstruction.zo
1362
+ self.logger.info(f"Naive estimate of new z: {new_z:.3f}, stepsize {step:.3f}")
1363
+ step = 5 * step
1364
+ # check if the thing should be updated.
1365
+ if abs(step) < 1e-4: # if it's too small, just truncate it,
1366
+ # it may be that the distance changed due to some other update.
1367
+ # Take that into account as if we don't the steps will be super large.
1368
+ self.i_z_optimizer += 1
1369
+ step = self.reconstruction.zo - op_get(state)
1370
+ self.optlib["state"] = op_update(self.i_z_optimizer, -step, state)
1371
+
1372
+ self.logger.info("Skipping update as step is too small")
1373
+ # as we're only updating it for sake of good measure, we don't have to update anything else.
1374
+ return
1375
+ # now, as we're actually updating, we can increase the step
1376
+ self.i_z_optimizer += 1
1377
+ self.optlib["state"] = op_update(self.i_z_optimizer, -step, state)
1378
+ # get the new value
1379
+ z_new = float(jax.device_get(op_get(self.optlib["state"])))
1380
+
1381
+ self.logger.info(f"Loop: {loop} step: {step}")
1382
+ self.logger.info(
1383
+ f"old z: {self.reconstruction.zo:.3f}\n new z calculated: {z_new:.3f}\n diff: {self.reconstruction.zo - z_new}\n"
1384
+ )
1385
+ # scale the coordinates accordingly
1386
+ factor = self.reconstruction.zo / z_new
1387
+ self.reconstruction.zo = z_new
1388
+ new_encoder = self.reconstruction.encoder_corrected.copy()
1389
+ new_encoder -= new_encoder.mean(axis=0, keepdims=True)
1390
+ new_encoder /= factor # this should be the correct one!
1391
+
1392
+ new_encoder += self.experimentalData.encoder.mean(axis=0, keepdims=True)
1393
+
1394
+ msqdisplacement_a = np.linalg.norm(
1395
+ 1e6 * new_encoder - self.experimentalData.encoder * 1e6
1396
+ )
1397
+
1398
+ self.reconstruction.encoder_corrected = new_encoder
1399
+ self.logger.info(
1400
+ f"Mean square displacement: before: {msqdisplacement:.3f} after: {msqdisplacement_a:.3f}"
1401
+ )
1402
+
1403
+ def positionCorrectionUpdate(self):
1404
+ # fit the scaling out, to put in the z
1405
+ if len(self.reconstruction.error) > self.startAtIteration:
1406
+ self.logger.info("Updating positions")
1407
+
1408
+ # update positions
1409
+ if self.experimentalData.operationMode == "FPM":
1410
+ conv = (
1411
+ -(1 / self.reconstruction.wavelength)
1412
+ * self.reconstruction.dxo
1413
+ * self.reconstruction.Np
1414
+ )
1415
+ z = self.reconstruction.zled
1416
+ k = (
1417
+ self.reconstruction.positions
1418
+ - self.adaptStep * self.D
1419
+ - self.reconstruction.No // 2
1420
+ + self.reconstruction.Np // 2
1421
+ )
1422
+ self.reconstruction.encoder_corrected = (
1423
+ np.sign(conv)
1424
+ * k
1425
+ * z
1426
+ / (np.sqrt(conv**2 - k[:, 0] ** 2 - k[:, 1] ** 2))[..., None]
1427
+ )
1428
+ else:
1429
+
1430
+ new_encoder = (
1431
+ self.reconstruction.encoder_corrected
1432
+ - self.adaptStep * self.D * self.reconstruction.dxo
1433
+ )
1434
+ new_encoder = new_encoder - new_encoder.mean(axis=0, keepdims=True)
1435
+ new_encoder = new_encoder + self.experimentalData.encoder.mean(
1436
+ axis=0, keepdims=True
1437
+ )
1438
+
1439
+ self.reconstruction.encoder_corrected = new_encoder
1440
+ self.logger.info(f"Average update size: {abs(self.D).mean():.2f} pixels")
1441
+
1442
+ def applyConstraints(self, loop):
1443
+ """
1444
+ Apply constraints.
1445
+ :param loop: loop number
1446
+ :return:
1447
+ """
1448
+ # dirks additions, untested
1449
+ if self.params.l2reg:
1450
+ # turns down areas that are not updated. Similar to an
1451
+ # l2 regularizer
1452
+ self.reconstruction.object *= 1 - self.params.l2reg_object_aleph
1453
+ self.reconstruction.probe *= 1 - self.params.l2reg_probe_aleph
1454
+
1455
+ # enforce empty beam constraint
1456
+ if self.params.modulusEnforcedProbeSwitch:
1457
+ self.modulusEnforcedProbe()
1458
+
1459
+ if self.params.orthogonalizationSwitch:
1460
+ if np.mod(loop, self.params.orthogonalizationFrequency) == 0:
1461
+ self.orthogonalization()
1462
+
1463
+ # probe normalization to measured PSD todo: check for multiwave and multi object states
1464
+ if self.params.probePowerCorrectionSwitch:
1465
+ self.reconstruction.probe = (
1466
+ self.reconstruction.probe
1467
+ / np.sqrt(
1468
+ np.sum(self.reconstruction.probe * self.reconstruction.probe.conj())
1469
+ )
1470
+ * self.experimentalData.maxProbePower
1471
+ )
1472
+ if self.params.probeSpectralPowerCorrectionSwitch:
1473
+ for wl in range(self.reconstruction.probe.shape[0]):
1474
+ self.reconstruction.probe[wl, ...] *= self.experimentalData.maxProbePower * self.experimentalData.spectralPower[wl] \
1475
+ / np.sqrt(np.sum(self.reconstruction.probe[wl, ...] * self.reconstruction.probe[wl, ...].conj()))
1476
+
1477
+ if (
1478
+ self.params.comStabilizationSwitch is not None
1479
+ and self.params.comStabilizationSwitch is not False
1480
+ ):
1481
+ if loop % int(self.params.comStabilizationSwitch) == 0:
1482
+ self.comStabilization()
1483
+
1484
+ if self.params.PSDestimationSwitch:
1485
+ raise NotImplementedError()
1486
+
1487
+ if self.params.probeBoundary:
1488
+ self.reconstruction.probe *= self.probeWindow
1489
+
1490
+ if self.params.absorbingProbeBoundary:
1491
+ if self.experimentalData.operationMode == "FPM":
1492
+ self.absorbingProbeBoundaryAleph = 1
1493
+
1494
+ self.reconstruction.probe = (
1495
+ (1 - self.params.absorbingProbeBoundaryAleph)
1496
+ * self.reconstruction.probe
1497
+ + self.params.absorbingProbeBoundaryAleph
1498
+ * self.reconstruction.probe
1499
+ * self.probeWindow
1500
+ )
1501
+
1502
+ # experimental: also apply in fourier space
1503
+ # self.reconstruction.probe = ifft2c(fft2c(self.reconstruction.probe)*self.probeWindow)
1504
+
1505
+ # Todo: objectSmoothenessSwitch,probeSmoothenessSwitch,
1506
+ if self.params.probeSmoothenessSwitch:
1507
+
1508
+ self.reconstruction.probe = smooth_amplitude(
1509
+ self.reconstruction.probe,
1510
+ self.params.probeSmoothenessWidth,
1511
+ self.params.probeSmoothnessAleph,
1512
+ )
1513
+
1514
+ if self.params.objectSmoothenessSwitch:
1515
+ self.reconstruction.object = smooth_amplitude(
1516
+ self.reconstruction.object,
1517
+ self.params.objectSmoothenessWidth,
1518
+ self.params.objectSmoothnessAleph,
1519
+ )
1520
+
1521
+ if self.params.absObjectSwitch:
1522
+ self.reconstruction.object = (
1523
+ 1 - self.params.absObjectBeta
1524
+ ) * self.reconstruction.object + self.params.absObjectBeta * abs(
1525
+ self.reconstruction.object
1526
+ )
1527
+
1528
+ if self.params.absProbeSwitch:
1529
+ self.reconstruction.probe = (
1530
+ 1 - self.params.absProbeBeta
1531
+ ) * self.reconstruction.probe + self.params.absProbeBeta * abs(
1532
+ self.reconstruction.probe
1533
+ )
1534
+
1535
+ # this is intended to slowly push non-measured object region to abs value lower than
1536
+ # the max abs inside object ROI allowing for good contrast when monitoring object
1537
+ if self.params.objectContrastSwitch:
1538
+ self.reconstruction.object = (
1539
+ 0.995 * self.reconstruction.object
1540
+ + 0.005
1541
+ * np.mean(
1542
+ abs(
1543
+ self.reconstruction.object[
1544
+ ..., self.monitor.objectROI[0], self.monitor.objectROI[1]
1545
+ ]
1546
+ )
1547
+ )
1548
+ )
1549
+ if self.params.couplingSwitch and self.reconstruction.nlambda > 1:
1550
+ self.reconstruction.probe[0] = (
1551
+ 1 - self.params.couplingAleph
1552
+ ) * self.reconstruction.probe[
1553
+ 0
1554
+ ] + self.params.couplingAleph * self.reconstruction.probe[
1555
+ 1
1556
+ ]
1557
+ for lambdaLoop in np.arange(1, self.reconstruction.nlambda - 1):
1558
+ self.reconstruction.probe[lambdaLoop] = (
1559
+ 1 - self.params.couplingAleph
1560
+ ) * self.reconstruction.probe[
1561
+ lambdaLoop
1562
+ ] + self.params.couplingAleph * (
1563
+ self.reconstruction.probe[lambdaLoop + 1]
1564
+ + self.reconstruction.probe[lambdaLoop - 1]
1565
+ ) / 2
1566
+
1567
+ self.reconstruction.probe[-1] = (
1568
+ 1 - self.params.couplingAleph
1569
+ ) * self.reconstruction.probe[
1570
+ -1
1571
+ ] + self.params.couplingAleph * self.reconstruction.probe[
1572
+ -2
1573
+ ]
1574
+ if self.params.binaryProbeSwitch:
1575
+ probePeakAmplitude = np.max(abs(self.reconstruction.probe))
1576
+ probeThresholded = self.reconstruction.probe.copy()
1577
+ probeThresholded[
1578
+ (
1579
+ abs(probeThresholded)
1580
+ < self.params.binaryProbeThreshold * probePeakAmplitude
1581
+ )
1582
+ ] = 0
1583
+
1584
+ self.reconstruction.probe = (
1585
+ (1 - self.params.binaryProbeAleph) * self.reconstruction.probe
1586
+ + self.params.binaryProbeAleph * probeThresholded
1587
+ )
1588
+
1589
+ if self.params.positionCorrectionSwitch:
1590
+
1591
+ self.positionCorrectionUpdate()
1592
+
1593
+ if (
1594
+ self.params.map_position_to_z_change
1595
+ and (loop % 5 == 1)
1596
+ and self.params.positionCorrectionSwitch
1597
+ ):
1598
+ self.position_update_to_change_in_z(loop)
1599
+
1600
+ if self.params.TV_autofocus:
1601
+ merit, AOI_image, allmerits = self.reconstruction.TV_autofocus(
1602
+ self.params, loop=loop
1603
+ )
1604
+ self.monitor.update_focusing_metric(
1605
+ merit,
1606
+ AOI_image,
1607
+ metric_name=self.params.TV_autofocus_metric,
1608
+ allmerits=allmerits,
1609
+ )
1610
+
1611
+ # if self.params.OPRP and loop % self.params.OPRP_tsvd_interval == 0:
1612
+ # self.reconstruction.probe_storage.tsvd()
1613
+
1614
+ def orthogonalization(self):
1615
+ """
1616
+ Perform orthogonalization
1617
+ :return:
1618
+ """
1619
+ xp = getArrayModule(self.reconstruction.probe)
1620
+ if self.reconstruction.npsm > 1:
1621
+ # orthogonalize the probe for each wavelength and each slice
1622
+ for id_l in range(self.reconstruction.nlambda):
1623
+ for id_s in range(self.reconstruction.nslice):
1624
+ (
1625
+ self.reconstruction.probe[id_l, 0, :, id_s, :, :],
1626
+ self.normalizedEigenvaluesProbe,
1627
+ self.MSPVprobe,
1628
+ ) = orthogonalizeModes(
1629
+ self.reconstruction.probe[id_l, 0, :, id_s, :, :],
1630
+ method="snapShots",
1631
+ )
1632
+ self.reconstruction.purityProbe = np.sqrt(
1633
+ np.sum(self.normalizedEigenvaluesProbe**2)
1634
+ )
1635
+ self.reconstruction.purityProbeHist.append(self.reconstruction.purityProbe.get())
1636
+ # orthogonolize momentum operator
1637
+ if self.params.momentumAcceleration:
1638
+ # orthogonalize probe Buffer
1639
+ p = self.reconstruction.probeBuffer[
1640
+ id_l, 0, :, id_s, :, :
1641
+ ].reshape(
1642
+ (self.reconstruction.npsm, self.reconstruction.Np**2)
1643
+ )
1644
+ self.reconstruction.probeBuffer[id_l, 0, :, id_s, :, :] = (
1645
+ xp.array(self.MSPVprobe) @ p
1646
+ ).reshape(
1647
+ (
1648
+ self.reconstruction.npsm,
1649
+ self.reconstruction.Np,
1650
+ self.reconstruction.Np,
1651
+ )
1652
+ )
1653
+ # orthogonalize probe momentum
1654
+ p = self.reconstruction.probeMomentum[
1655
+ id_l, 0, :, id_s, :, :
1656
+ ].reshape(
1657
+ (self.reconstruction.npsm, self.reconstruction.Np**2)
1658
+ )
1659
+ self.reconstruction.probeMomentum[id_l, 0, :, id_s, :, :] = (
1660
+ xp.array(self.MSPVprobe) @ p
1661
+ ).reshape(
1662
+ (
1663
+ self.reconstruction.npsm,
1664
+ self.reconstruction.Np,
1665
+ self.reconstruction.Np,
1666
+ )
1667
+ )
1668
+
1669
+ # if self.comStabilizationSwitch:
1670
+ # self.comStabilization()
1671
+ # self.reconstruction.probe_storage.push(self.reconstruction.probe, None, len(self.experimentalData.ptychogram), force=True)
1672
+
1673
+ elif self.reconstruction.nosm > 1:
1674
+ # orthogonalize the object for each wavelength and each slice
1675
+ for id_l in range(self.reconstruction.nlambda):
1676
+ for id_s in range(self.reconstruction.nslice):
1677
+ (
1678
+ self.reconstruction.object[id_l, :, 0, id_s, :, :],
1679
+ self.normalizedEigenvaluesObject,
1680
+ self.MSPVobject,
1681
+ ) = orthogonalizeModes(
1682
+ self.reconstruction.object[id_l, :, 0, id_s, :, :],
1683
+ method="snapShots",
1684
+ )
1685
+ self.reconstruction.purityObject = np.sqrt(
1686
+ np.sum(self.normalizedEigenvaluesObject**2)
1687
+ )
1688
+
1689
+ # orthogonolize momentum operator
1690
+ if self.params.momentumAcceleration:
1691
+ # orthogonalize object Buffer
1692
+ p = self.reconstruction.objectBuffer[
1693
+ id_l, :, 0, id_s, :, :
1694
+ ].reshape(
1695
+ (self.reconstruction.nosm, self.reconstruction.No**2)
1696
+ )
1697
+ self.reconstruction.objectBuffer[id_l, :, 0, id_s, :, :] = (
1698
+ xp.array(self.MSPVobject) @ p
1699
+ ).reshape(
1700
+ (
1701
+ self.reconstruction.nosm,
1702
+ self.reconstruction.No,
1703
+ self.reconstruction.No,
1704
+ )
1705
+ )
1706
+ # orthogonalize object momentum
1707
+ p = self.reconstruction.objectMomentum[
1708
+ id_l, :, 0, id_s, :, :
1709
+ ].reshape(
1710
+ (self.reconstruction.nosm, self.reconstruction.No**2)
1711
+ )
1712
+ self.reconstruction.objectMomentum[id_l, :, 0, id_s, :, :] = (
1713
+ xp.array(self.MSPVobject) @ p
1714
+ ).reshape(
1715
+ (
1716
+ self.reconstruction.nosm,
1717
+ self.reconstruction.No,
1718
+ self.reconstruction.No,
1719
+ )
1720
+ )
1721
+
1722
+ else:
1723
+ pass
1724
+
1725
+ def comStabilization(self):
1726
+ """
1727
+ Perform center of mass stabilization (center the probe)
1728
+ :return:
1729
+ """
1730
+ self.logger.info("Doing probe com stabilization")
1731
+ xp = getArrayModule(self.reconstruction.probe)
1732
+ # calculate center of mass of the probe (for multislice cases, the probe for the last slice is used)
1733
+ P2 = xp.sum(
1734
+ abs(self.reconstruction.probe[:, :, :, -1, ...]) ** 2, axis=(0, 1, 2)
1735
+ )
1736
+ P2 = abs(self.reconstruction.probe[0, 0, 0, -1])
1737
+ demon = xp.sum(P2) * self.reconstruction.dxp
1738
+ xc = int(
1739
+ xp.around(xp.sum(xp.array(self.reconstruction.Xp, xp.float32) * P2) / demon)
1740
+ )
1741
+ yc = int(
1742
+ xp.around(xp.sum(xp.array(self.reconstruction.Yp, xp.float32) * P2) / demon)
1743
+ )
1744
+ # print('Center of mass:', yc, xc)
1745
+ # shift only if necessary
1746
+ if xc**2 + yc**2 > 1:
1747
+ # self.reconstruction.probe_storage._push_hard(self.reconstruction.probe, 100)
1748
+ # self.reconstruction.probe_storage.roll(-yc, -xc)
1749
+
1750
+ # shift probe
1751
+ self.reconstruction.probe = xp.roll(
1752
+ self.reconstruction.probe, (-yc, -xc), axis=(-2, -1)
1753
+ )
1754
+ # for k in xp.arange(self.reconstruction.npsm):
1755
+ # self.reconstruction.probe[:, :, k, -1, ...] = \
1756
+ # xp.roll(self.reconstruction.probe[:, :, k, -1, ...], (-yc, -xc), axis=(-2, -1))
1757
+ # # for mPIE
1758
+ if self.params.momentumAcceleration:
1759
+ self.reconstruction.probeMomentum = xp.roll(
1760
+ self.reconstruction.probeMomentum, (-yc, -xc), axis=(-2, -1)
1761
+ )
1762
+ self.reconstruction.probeBuffer = xp.roll(
1763
+ self.reconstruction.probeBuffer, (-yc, -xc), axis=(-2, -1)
1764
+ )
1765
+
1766
+ # shift object
1767
+ self.reconstruction.object = xp.roll(
1768
+ self.reconstruction.object, (-yc, -xc), axis=(-2, -1)
1769
+ )
1770
+ # for mPIE
1771
+ if self.params.momentumAcceleration:
1772
+ self.reconstruction.objectMomentum = xp.roll(
1773
+ self.reconstruction.objectMomentum, (-yc, -xc), axis=(-2, -1)
1774
+ )
1775
+ self.reconstruction.objectBuffer = xp.roll(
1776
+ self.reconstruction.objectBuffer, (-yc, -xc), axis=(-2, -1)
1777
+ )
1778
+
1779
+ def modulusEnforcedProbe(self):
1780
+ # propagate probe to detector
1781
+ xp = getArrayModule(self.reconstruction.esw)
1782
+ self.reconstruction.esw = self.reconstruction.probe
1783
+ self.object2detector()
1784
+
1785
+ if self.params.FourierMaskSwitch:
1786
+ self.reconstruction.ESW = self.reconstruction.ESW * xp.sqrt(
1787
+ self.experimentalData.emptyBeam / 1e-10
1788
+ + xp.sum(xp.abs(self.reconstruction.ESW) ** 2, axis=(0, 1, 2, 3))
1789
+ ) * self.experimentalData.W + self.reconstruction.ESW * (
1790
+ 1 - self.experimentalData.W
1791
+ )
1792
+ else:
1793
+ self.reconstruction.ESW = self.reconstruction.ESW * np.sqrt(
1794
+ self.experimentalData.emptyBeam
1795
+ / (1e-10 + xp.sum(abs(self.reconstruction.ESW) ** 2, axis=(0, 1, 2, 3)))
1796
+ )
1797
+
1798
+ self.detector2object()
1799
+
1800
+ if self.params.OPRP:
1801
+ pass
1802
+ # self.probes.append(self.reconstruction.esw.reshape(-))
1803
+
1804
+ self.reconstruction.probe = self.reconstruction.esw
1805
+
1806
+ def adaptiveDenoising(self):
1807
+ """
1808
+ Use the difference of mean intensities between the low-resolution
1809
+ object estimate and the low-resolution raw data to estimate the
1810
+ noise floor to be clipped.
1811
+ :return:
1812
+ """
1813
+ # figure out wether or not to use the GPU
1814
+ xp = getArrayModule(self.reconstruction.esw)
1815
+
1816
+ Ameasured = self.reconstruction.Imeasured**0.5
1817
+ Aestimated = xp.abs(self.reconstruction.Iestimated) ** 0.5
1818
+
1819
+ noise = xp.abs(xp.mean(Ameasured - Aestimated))
1820
+
1821
+ Ameasured = Ameasured - noise
1822
+ Ameasured[Ameasured < 0] = 0
1823
+ self.reconstruction.Imeasured = Ameasured**2
1824
+
1825
+ def z_update(self, stepsize=0.01, roi_bounds=[0.3, 0.7], d=10):
1826
+ """
1827
+ Update Z based on TV
1828
+ :param stepsize:
1829
+ :return:
1830
+ """
1831
+ self.reconstruction.TV_autofocus()
1832
+
1833
+ def objectPatchUpdate_TV(self, objectPatch: np.ndarray, DELTA: np.ndarray):
1834
+ """
1835
+ Update the object patch with a TV regularization.
1836
+
1837
+ :param objectPatch:
1838
+ :param DELTA:
1839
+ :return:
1840
+ """
1841
+
1842
+ xp = getArrayModule(objectPatch)
1843
+ frac = self.reconstruction.probe.conj() / xp.max(
1844
+ xp.sum(xp.abs(self.reconstruction.probe) ** 2, axis=(0, 1, 2, 3))
1845
+ )
1846
+
1847
+ # gradient = xp.gradient(objectPatch, axis=(4, 5))
1848
+ #
1849
+ # # norm = xp.abs(gradient[0] + gradient[1]) ** 2
1850
+ # norm = (gradient[0] + gradient[1]) ** 2
1851
+ # temp = [gradient[0] / xp.sqrt(norm + epsilon), gradient[1] / xp.sqrt(norm + epsilon)]
1852
+ # TV_update = divergence(temp)
1853
+ TV_update = grad_TV(objectPatch, epsilon=1e-2)
1854
+ lam = self.params.objectTVregStepSize
1855
+ return (
1856
+ objectPatch
1857
+ + self.betaObject * xp.sum(frac * DELTA, axis=(0, 2, 3), keepdims=True)
1858
+ + lam * self.betaObject * TV_update
1859
+ )