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.
- PtyLab/Engines/BaseEngine.py +1859 -0
- PtyLab/Engines/OPR.py +279 -0
- PtyLab/Engines/__init__.py +33 -0
- PtyLab/Engines/aPIE.py +357 -0
- PtyLab/Engines/e3PIE.py +235 -0
- PtyLab/Engines/ePIE.py +157 -0
- PtyLab/Engines/ePIE_TV.py +191 -0
- PtyLab/Engines/mPIE.py +270 -0
- PtyLab/Engines/mPIE_tv.py +246 -0
- PtyLab/Engines/mqNewton.py +251 -0
- PtyLab/Engines/multiPIE.py +205 -0
- PtyLab/Engines/pcPIE.py +202 -0
- PtyLab/Engines/qNewton.py +140 -0
- PtyLab/Engines/zPIE.py +364 -0
- PtyLab/ExperimentalData/ExperimentalData.py +288 -0
- PtyLab/ExperimentalData/__init__.py +1 -0
- PtyLab/GridGeneration/FermatGridGenerator.py +130 -0
- PtyLab/GridGeneration/__init__.py +0 -0
- PtyLab/Monitor/Monitor.py +361 -0
- PtyLab/Monitor/Plots.py +334 -0
- PtyLab/Monitor/README.md +13 -0
- PtyLab/Monitor/TensorboardMonitor.py +558 -0
- PtyLab/Monitor/__init__.py +0 -0
- PtyLab/Operators/Operators.py +1099 -0
- PtyLab/Operators/README.md +8 -0
- PtyLab/Operators/__init__.py +1 -0
- PtyLab/Operators/_propagation_kernels.py +36 -0
- PtyLab/Params/Params.py +217 -0
- PtyLab/Params/__init__.py +0 -0
- PtyLab/ProbeEngines/OPRP.py +151 -0
- PtyLab/ProbeEngines/StandardProbe.py +105 -0
- PtyLab/ProbeEngines/__init__.py +1 -0
- PtyLab/Reconstruction/CalibrationFPM.py +919 -0
- PtyLab/Reconstruction/Reconstruction.py +875 -0
- PtyLab/Reconstruction/__init__.py +0 -0
- PtyLab/Regularizers/__init__.py +207 -0
- PtyLab/__init__.py +56 -0
- PtyLab/io/__init__.py +12 -0
- PtyLab/io/generateSimulationData.py +135 -0
- PtyLab/io/readExample.py +70 -0
- PtyLab/io/readHdf5.py +125 -0
- PtyLab/utils/__init__.py +0 -0
- PtyLab/utils/alignment.py +52 -0
- PtyLab/utils/cli.py +50 -0
- PtyLab/utils/downloader.py +39 -0
- PtyLab/utils/fsvd.py +105 -0
- PtyLab/utils/gpuUtils.py +103 -0
- PtyLab/utils/initializationFunctions.py +62 -0
- PtyLab/utils/scanGrids.py +313 -0
- PtyLab/utils/utils.py +312 -0
- PtyLab/utils/visualisation.py +340 -0
- ptylab-0.2.5.dist-info/METADATA +150 -0
- ptylab-0.2.5.dist-info/RECORD +55 -0
- ptylab-0.2.5.dist-info/WHEEL +4 -0
- 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
|
+
)
|