Skip to content

Commit e7e10de

Browse files
authored
FEAT: allow unconstraining pixels below a threshold (#64)
1 parent e7b7224 commit e7e10de

11 files changed

Lines changed: 107 additions & 42 deletions

File tree

.gitignore

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,7 @@ workspace/
157157
*.hdf5
158158
*.h5
159159

160-
# PyCharm
161-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
162-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
163-
# and can be added to the global gitignore or merged into this file. For a more nuclear
164-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
165-
#.idea/
160+
# IDE
161+
.idea/
162+
.vscode/
163+
.loglogin

src/ptychi/api/options/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,12 @@ class ReconstructorOptions(Options):
10641064
and is not involved in the reconstruction math.
10651065
"""
10661066

1067+
exclude_measured_pixels_below: Optional[float] = None
1068+
"""
1069+
If not None, gradients corresponding to measured diffraction pixels whose intensity
1070+
is less than or equal to this value are set to 0 in reconstructors that support it.
1071+
"""
1072+
10671073
forward_model_options: ForwardModelOptions = dataclasses.field(
10681074
default_factory=ForwardModelOptions
10691075
)

src/ptychi/api/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def build_data(self):
173173
free_space_propagation_distance_m=self.data_options.free_space_propagation_distance_m,
174174
fft_shift=self.data_options.fft_shift,
175175
save_data_on_device=save_on_device,
176-
valid_pixel_mask=self.data_options.valid_pixel_mask
176+
valid_pixel_mask=self.data_options.valid_pixel_mask,
177177
)
178178

179179
def build_object(self):

src/ptychi/forward_models.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def __init__(
229229
self.intermediate_variables = self.PlanarPtychographyIntermediateVariables()
230230

231231
self.diffraction_pattern_blur_sigma = diffraction_pattern_blur_sigma
232-
232+
233233
self.check_inputs()
234234

235235
def check_inputs(self):
@@ -760,12 +760,18 @@ def scale_gradients(self, patterns):
760760

761761

762762
class NoiseModel(torch.nn.Module):
763-
def __init__(self, eps=1e-6, valid_pixel_mask: Optional[Tensor] = None) -> None:
763+
def __init__(
764+
self,
765+
eps: float = 1e-6,
766+
valid_pixel_mask: Optional[Tensor] = None,
767+
exclude_measured_pixels_below: Optional[float] = None,
768+
) -> None:
764769
super().__init__()
765770
self.eps = eps
766771
self.noise_statistics = None
767772
self.valid_pixel_mask = valid_pixel_mask
768-
773+
self.exclude_measured_pixels_below = exclude_measured_pixels_below
774+
769775
def nll(self, y_pred: Tensor, y_true: Tensor) -> Tensor:
770776
"""
771777
Calculate the negative log-likelihood.
@@ -775,6 +781,25 @@ def nll(self, y_pred: Tensor, y_true: Tensor) -> Tensor:
775781
def backward(self, *args, **kwargs):
776782
raise NotImplementedError
777783

784+
@staticmethod
785+
def get_constrained_pixel_mask(
786+
valid_pixel_mask: Optional[Tensor],
787+
exclude_measured_pixels_below: Optional[float],
788+
y_true: Tensor,
789+
) -> Tensor:
790+
constrained_pixel_mask = torch.ones_like(y_true, dtype=torch.bool)
791+
if valid_pixel_mask is not None:
792+
constrained_pixel_mask = valid_pixel_mask.to(y_true.device)
793+
constrained_pixel_mask = constrained_pixel_mask.unsqueeze(0).expand(
794+
y_true.shape[0], -1, -1
795+
)
796+
if exclude_measured_pixels_below is not None:
797+
constrained_pixel_mask = torch.logical_and(
798+
constrained_pixel_mask,
799+
y_true > exclude_measured_pixels_below,
800+
)
801+
return constrained_pixel_mask
802+
778803
@timer()
779804
def conform_to_exit_wave_size(
780805
self,
@@ -822,17 +847,24 @@ def backward_to_psi_far(self, y_pred, y_true, psi_far):
822847
$g = \frac{\partial L}{\partial \psi_{far}}$.
823848
824849
When `self.valid_pixel_mask` is not None, pixels of the gradient `g` where the
825-
mask is False are set to 0. When `g` is used to update the far-field wavefield
826-
`psi_far`, the invalid pixels are kept unchanged.
850+
mask is False are set to 0. When `self.exclude_measured_pixels_below` is not
851+
None, gradients at pixels with measured intensities less than or equal to that
852+
threshold are also set to 0.
827853
"""
828854
# Shape of g: (batch_size, h, w)
829855
# Shape of psi_far: (batch_size, n_probe_modes, h, w)
830856
y_pred, y_true, valid_pixel_mask = self.conform_to_exit_wave_size(
831857
y_pred, y_true, self.valid_pixel_mask, psi_far.shape[-2:]
832858
)
859+
constrained_pixel_mask = self.get_constrained_pixel_mask(
860+
valid_pixel_mask,
861+
self.exclude_measured_pixels_below,
862+
y_true,
863+
)
833864
g = 1 - torch.sqrt(y_true) / (torch.sqrt(y_pred) + self.eps) # Eq. 12b
834-
if valid_pixel_mask is not None:
835-
g[:, torch.logical_not(valid_pixel_mask)] = 0
865+
866+
g[torch.logical_not(constrained_pixel_mask)] = 0
867+
836868
w = 1 / (2 * self.sigma) ** 2
837869
g = 2 * w * g[:, None, :, :] * psi_far
838870
return g
@@ -863,8 +895,12 @@ def backward_to_psi_far(self, y_pred: Tensor, y_true: Tensor, psi_far: Tensor):
863895
y_pred, y_true, valid_pixel_mask = self.conform_to_exit_wave_size(
864896
y_pred, y_true, self.valid_pixel_mask, psi_far.shape[-2:]
865897
)
898+
constrained_pixel_mask = self.get_constrained_pixel_mask(
899+
valid_pixel_mask,
900+
self.exclude_measured_pixels_below,
901+
y_true,
902+
)
866903
g = 1 - y_true / (y_pred + self.eps) # Eq. 12b
867-
if valid_pixel_mask is not None:
868-
g[:, torch.logical_not(valid_pixel_mask)] = 0
904+
g[torch.logical_not(constrained_pixel_mask)] = 0
869905
g = g[:, None, :, :] * psi_far
870906
return g

src/ptychi/io_handles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def __init__(
5252
self.free_space_propagation_distance_m = free_space_propagation_distance_m
5353

5454
self.save_data_on_device = save_data_on_device
55-
55+
5656
def __getitem__(self, index):
5757
if not isinstance(index, torch.Tensor):
5858
index = torch.tensor(index, device=self.patterns.device, dtype=torch.long)

src/ptychi/reconstructors/ad_ptychography.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ def get_retain_graph(self) -> bool:
137137

138138
def run_minibatch(self, input_data, y_true, *args, **kwargs):
139139
y_pred = self.forward_model(*input_data)
140+
constrained_pixel_mask = self.get_constrained_pixel_mask(y_true)
140141
batch_loss = self.loss_function(
141-
y_pred[:, self.dataset.valid_pixel_mask], y_true[:, self.dataset.valid_pixel_mask]
142+
y_pred[constrained_pixel_mask], y_true[constrained_pixel_mask]
142143
)
143144

144145
batch_loss.backward(retain_graph=self.get_retain_graph())
@@ -149,5 +150,9 @@ def run_minibatch(self, input_data, y_true, *args, **kwargs):
149150
self.forward_model.zero_grad()
150151
self.run_post_update_hooks()
151152

152-
self.loss_tracker.update_batch_loss(y_pred=y_pred, y_true=y_true, loss=batch_loss.item())
153+
self.loss_tracker.update_batch_loss(
154+
y_pred=y_pred[constrained_pixel_mask],
155+
y_true=y_true[constrained_pixel_mask],
156+
loss=batch_loss.item(),
157+
)
153158
self.loss_tracker.update_batch_regularization_loss(reg_loss.item())

src/ptychi/reconstructors/base.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,14 @@ def build_dataloader(self):
419419
)
420420
return super().build_dataloader(batch_sampler=batch_sampler)
421421

422+
def get_constrained_pixel_mask(self, y_true: Tensor) -> Tensor:
423+
"""Get the detector pixels that should constrain the update for a batch."""
424+
return fm.NoiseModel.get_constrained_pixel_mask(
425+
valid_pixel_mask=self.dataset.valid_pixel_mask,
426+
exclude_measured_pixels_below=self.options.exclude_measured_pixels_below,
427+
y_true=y_true,
428+
)
429+
422430
def update_preconditioners(self):
423431
# Update preconditioner of the object only if:
424432
# - the preconditioner does not exist, or
@@ -684,9 +692,11 @@ def prepare_data(self, *args, **kwargs):
684692
self.parameter_group.probe.normalize_eigenmodes()
685693
logger.info("Probe eigenmodes normalized.")
686694

687-
@staticmethod
688695
def replace_propagated_exit_wave_magnitude(
689-
psi: Tensor, actual_pattern_intensity: Tensor
696+
self,
697+
psi: Tensor,
698+
actual_pattern_intensity: Tensor,
699+
constrained_pixel_mask: Optional[Tensor] = None,
690700
) -> Tensor:
691701
"""
692702
Replace the propogated exit wave amplitude.
@@ -697,6 +707,9 @@ def replace_propagated_exit_wave_magnitude(
697707
Predicted exit wave propagated to the detector plane.
698708
actual_pattern_intensity : Tensor
699709
The measured diffraction pattern at the detector.
710+
constrained_pixel_mask : Tensor, optional
711+
If given, only pixels where this mask is True are constrained. Other pixels
712+
are left unchanged from `psi`.
700713
701714
Returns
702715
-------
@@ -705,11 +718,14 @@ def replace_propagated_exit_wave_magnitude(
705718
of `actual_pattern_intensity`.
706719
"""
707720

708-
return (
721+
psi_prime = (
709722
psi
710723
/ ((psi.abs() ** 2).sum(1, keepdims=True).sqrt() + 1e-7)
711724
* torch.sqrt(actual_pattern_intensity + 1e-7)[:, None]
712725
)
726+
if constrained_pixel_mask is not None:
727+
return torch.where(constrained_pixel_mask[:, None], psi_prime, psi)
728+
return psi_prime
713729

714730
@timer()
715731
def adjoint_shift_probe_update_direction(self, indices, delta_p, first_mode_only=False):

src/ptychi/reconstructors/bh.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def compute_updates(
137137
psi_far = self.forward_model.intermediate_variables["psi_far"]
138138
p = probe.get_opr_mode(0) # to do for multi modes
139139
pos = self.positions
140+
self.current_constrained_pixel_mask = self.get_constrained_pixel_mask(y_true)[:, None]
140141

141142
# sqrt of data
142143
d = torch.sqrt(y_true)[:, torch.newaxis]
@@ -294,6 +295,7 @@ def gradientF(self, psi_far, d):
294295

295296
td = d * (psi_far / (torch.abs(psi_far) + self.eps))
296297
td = psi_far - td
298+
td = td * self.current_constrained_pixel_mask
297299
# Compensate FFT normalization only for the far-field Fourier propagator.
298300
if isinstance(self.forward_model.free_space_propagator, fm.FourierPropagator):
299301
td *= psi_far.shape[-1] * psi_far.shape[-2]
@@ -305,8 +307,13 @@ def hessianF(self, psi_far, psi_far1, psi_far2, data):
305307

306308
l0 = psi_far / (torch.abs(psi_far) + self.eps)
307309
d0 = data / (torch.abs(psi_far) + self.eps)
308-
v1 = torch.sum((1 - d0) * reprod(psi_far1, psi_far2))
309-
v2 = torch.sum(d0 * reprod(l0, psi_far1) * reprod(l0, psi_far2))
310+
v1 = torch.sum(self.current_constrained_pixel_mask * (1 - d0) * reprod(psi_far1, psi_far2))
311+
v2 = torch.sum(
312+
self.current_constrained_pixel_mask
313+
* d0
314+
* reprod(l0, psi_far1)
315+
* reprod(l0, psi_far2)
316+
)
310317
return 2 * (v1 + v2)
311318

312319
def gradient_o(self, p, gradF):

src/ptychi/reconstructors/dm.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ def build_dataloader(self):
9898

9999
@timer()
100100
def run_minibatch(self, input_data, y_true, *args, **kwargs):
101-
dm_error_squared = self.compute_updates(y_true, self.dataset.valid_pixel_mask)
101+
dm_error_squared = self.compute_updates(y_true)
102102
self.loss_tracker.update_batch_loss(loss=dm_error_squared.sqrt())
103103

104104
@timer()
105-
def compute_updates(self, y_true: Tensor, valid_pixel_mask: Tensor) -> Tensor:
105+
def compute_updates(self, y_true: Tensor) -> Tensor:
106106
"""
107107
Compute the updates to the object, probe, and exit wave using the procedure
108108
described here: [Probe retrieval in ptychographic coherent diffractive imaging
@@ -147,7 +147,7 @@ def compute_updates(self, y_true: Tensor, valid_pixel_mask: Tensor) -> Tensor:
147147
dm_error_squared = 0
148148
for i in range(n_chunks):
149149
obj_patches, dm_error_squared, new_psi = self.apply_dm_update_to_exit_wave_chunk(
150-
start_pts[i], end_pts[i], y_true, valid_pixel_mask, dm_error_squared
150+
start_pts[i], end_pts[i], y_true, dm_error_squared
151151
)
152152
if probe.optimization_enabled(self.current_epoch):
153153
self.add_to_probe_update_terms(
@@ -223,7 +223,6 @@ def apply_dm_update_to_exit_wave_chunk(
223223
start_pt: int,
224224
end_pt: int,
225225
y_true: Tensor,
226-
valid_pixel_mask: Tensor,
227226
dm_error_squared: Tensor,
228227
) -> Tuple[Tensor, Tensor, Tensor]:
229228
"""
@@ -235,8 +234,6 @@ def apply_dm_update_to_exit_wave_chunk(
235234
# - revised_psi --> 2 * Pi_o(psi_n)- psi_n
236235
# - new_psi --> Pi_o(psi_n)
237236

238-
probe = self.parameter_group.probe
239-
240237
# Get the update exit wave
241238
new_psi, obj_patches = self.calculate_exit_wave_chunk(
242239
start_pt, end_pt, return_obj_patches=True
@@ -246,10 +243,10 @@ def apply_dm_update_to_exit_wave_chunk(
246243
2 * new_psi - self.psi[start_pt:end_pt]
247244
)
248245
# Replace intensities
249-
revised_psi = torch.where(
250-
valid_pixel_mask.repeat(revised_psi.shape[0], probe.n_modes, 1, 1),
251-
self.replace_propagated_exit_wave_magnitude(revised_psi, y_true[start_pt:end_pt]),
246+
revised_psi = self.replace_propagated_exit_wave_magnitude(
252247
revised_psi,
248+
y_true[start_pt:end_pt],
249+
constrained_pixel_mask=self.get_constrained_pixel_mask(y_true[start_pt:end_pt]),
253250
)
254251
# Propagate back to sample plane
255252
revised_psi = self.forward_model.free_space_propagator.propagate_backward(revised_psi)

src/ptychi/reconstructors/lsqml.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def __init__(
6565
"gaussian": fm.PtychographyGaussianNoiseModel,
6666
"poisson": fm.PtychographyPoissonNoiseModel,
6767
}[options.noise_model](
68-
**noise_model_params, valid_pixel_mask=self.dataset.valid_pixel_mask.clone()
68+
**noise_model_params,
69+
valid_pixel_mask=self.dataset.valid_pixel_mask.clone(),
70+
exclude_measured_pixels_below=self.options.exclude_measured_pixels_below,
6971
)
7072

7173
self.alpha_psi_far = 0.5

0 commit comments

Comments
 (0)