@@ -77,6 +77,7 @@ def __init__(
7777
7878 self .object_momentum_params = {}
7979 self .probe_momentum_params = {}
80+ self .probe_position_momentum_params = {}
8081
8182
8283 # Fourier error for momentum acceleration.
@@ -353,7 +354,9 @@ def apply_reconstruction_parameter_updates(self, indices: torch.Tensor):
353354
354355 # Update probe positions.
355356 if self .parameter_group .probe_positions .optimization_enabled (self .current_epoch ):
356- self .parameter_group .probe_positions .step_optimizer ()
357+ self .parameter_group .probe_positions .step_optimizer (
358+ clip_update = self .parameter_group .probe_positions .options .momentum_acceleration_gain <= 0
359+ )
357360
358361 # Update OPR modes and weights.
359362 if self .parameter_group .opr_mode_weights .optimization_enabled (self .current_epoch ):
@@ -824,6 +827,139 @@ def _apply_probe_momentum(self, alpha_p_mean, delta_p_hat):
824827 self .probe_momentum_params ["velocity_map" ][i_mode ] / 2.0
825828 )
826829
830+ @timer ()
831+ def _clip_probe_position_update (self , delta_pos ):
832+ """
833+ Clip the probe-position update by the configured limits.
834+
835+ Parameters
836+ ----------
837+ delta_pos : torch.Tensor
838+ A (batch_size, 2) tensor giving the probe-position update.
839+ """
840+ probe_positions = self .parameter_group .probe_positions
841+ limit_user = probe_positions .options .correction_options .update_magnitude_limit
842+ if limit_user is not None and limit_user <= 0 :
843+ raise ValueError (
844+ "`probe_position_options.correction_options.update_magnitude_limit` should "
845+ "either be None or a positive number."
846+ )
847+ if limit_user == torch .inf :
848+ limit_user = None
849+
850+ if not probe_positions .options .correction_options .clip_update_magnitude_by_mad and limit_user is None :
851+ return delta_pos
852+
853+ update_mag = delta_pos .abs ()
854+ update_signs = delta_pos .sign ()
855+
856+ if probe_positions .options .correction_options .clip_update_magnitude_by_mad :
857+ limit_mad = pmath .mad (delta_pos , dim = 0 ) * 10
858+ else :
859+ limit_mad = torch .full (
860+ (delta_pos .shape [- 1 ],), torch .inf , device = delta_pos .device , dtype = delta_pos .dtype
861+ )
862+ if limit_user is not None :
863+ limit = torch .clip (limit_mad , max = limit_user )
864+ else :
865+ limit = limit_mad
866+ delta_pos = update_mag .clip (max = limit ) * update_signs
867+ return delta_pos
868+
869+ @timer ()
870+ def _calculate_probe_position_corrcoef (self , current_update , previous_update ):
871+ """
872+ Calculate the mean of the per-axis Pearson correlation coefficients
873+ between two batches of probe-position updates.
874+ """
875+ if len (current_update ) < 2 or len (previous_update ) < 2 :
876+ return torch .tensor (0.0 , device = current_update .device , dtype = current_update .dtype )
877+
878+ current_centered = current_update - current_update .mean (0 , keepdim = True )
879+ previous_centered = previous_update - previous_update .mean (0 , keepdim = True )
880+ denominator = torch .sqrt ((current_centered ** 2 ).sum (0 ) * (previous_centered ** 2 ).sum (0 ))
881+ corr = torch .where (
882+ denominator > 0 ,
883+ (current_centered * previous_centered ).sum (0 ) / denominator ,
884+ torch .zeros_like (denominator ),
885+ )
886+ return corr .mean ()
887+
888+ @timer ()
889+ def _apply_probe_position_momentum (self , indices , delta_pos ):
890+ """
891+ Apply foldslice-style momentum acceleration to the current probe-position
892+ update after clipping.
893+
894+ Parameters
895+ ----------
896+ indices : torch.Tensor
897+ Indices of diffraction patterns in the current minibatch.
898+ delta_pos : torch.Tensor
899+ A (batch_size, 2) tensor giving the clipped probe-position update
900+ for the current minibatch.
901+ """
902+ # Only do momentum for far-field.
903+ free_space_propagation_distance_m = self .forward_model .free_space_propagation_distance_m
904+ is_far_field = math .isinf (free_space_propagation_distance_m )
905+ if not is_far_field :
906+ return delta_pos
907+
908+ probe_positions = self .parameter_group .probe_positions
909+
910+ momentum_memory = probe_positions .options .momentum_acceleration_memory
911+ if momentum_memory < 1 :
912+ raise ValueError (
913+ "`probe_position_options.momentum_acceleration_memory` must be positive."
914+ )
915+
916+ if "position_update_history" not in self .probe_position_momentum_params .keys ():
917+ self .probe_position_momentum_params ["position_update_history" ] = []
918+ self .probe_position_momentum_params ["velocity_map" ] = torch .zeros_like (
919+ probe_positions .data
920+ )
921+
922+ history = self .probe_position_momentum_params ["position_update_history" ]
923+ history_epoch = self .probe_position_momentum_params .get ("position_update_history_epoch" )
924+ if len (history ) == 0 or history_epoch != self .current_epoch :
925+ # Match foldslice's `position_update_memory{iter}` behavior by storing
926+ # one full update map per epoch and filling it across minibatches.
927+ history .append (torch .zeros_like (probe_positions .data ))
928+ self .probe_position_momentum_params ["position_update_history_epoch" ] = self .current_epoch
929+ if len (history ) > momentum_memory + 1 :
930+ history .pop (0 )
931+ history [- 1 ][indices ] = delta_pos
932+
933+ if len (history ) < momentum_memory + 1 :
934+ return delta_pos
935+
936+ corr_level = []
937+ current_update = history [- 1 ][indices ]
938+ for i in range (1 , momentum_memory + 1 ):
939+ previous_update = history [- 1 - i ][indices ]
940+ corr_level .append (self ._calculate_probe_position_corrcoef (current_update , previous_update ))
941+ corr_level = torch .stack (corr_level )
942+
943+ gain = 0.0
944+ friction = torch .tensor (0.5 , device = delta_pos .device , dtype = delta_pos .dtype )
945+ if torch .all (corr_level > 0 ):
946+ p = pmath .polyfit (
947+ torch .arange (0.0 , momentum_memory + 1.0 , device = delta_pos .device ),
948+ torch .concat ([torch .zeros ([1 ], device = delta_pos .device ), torch .log (corr_level )]),
949+ deg = 1 ,
950+ )
951+ gain = probe_positions .options .momentum_acceleration_gain
952+ friction = 0.1 * (- p [0 ]).clip (0 , None )
953+
954+ m = probe_positions .options .momentum_acceleration_gradient_mixing_factor
955+ m = friction if m is None else m
956+ self .probe_position_momentum_params ["velocity_map" ][indices ] = (
957+ 1 - friction
958+ ) * self .probe_position_momentum_params ["velocity_map" ][indices ] + m * delta_pos
959+ if delta_pos .abs ().max () < 0.1 :
960+ delta_pos = delta_pos + gain * self .probe_position_momentum_params ["velocity_map" ][indices ]
961+ return delta_pos
962+
827963 @timer ()
828964 def _calculate_object_patch_update_direction (
829965 self , chi , incident_wavefields = None , probe_mode_index = None
@@ -1175,11 +1311,16 @@ def update_probe_positions(
11751311 unique_probes ,
11761312 self .parameter_group .object .step_size ,
11771313 )
1314+ if self .parameter_group .probe_positions .options .momentum_acceleration_gain > 0 :
1315+ delta_pos = self ._clip_probe_position_update (delta_pos )
1316+ delta_pos = self ._apply_probe_position_momentum (indices , delta_pos )
11781317 delta_pos_full = torch .zeros_like (self .parameter_group .probe_positions .tensor )
11791318 delta_pos_full [indices ] = delta_pos
11801319 self .parameter_group .probe_positions .set_grad (- delta_pos_full )
11811320 if apply_updates :
1182- self .parameter_group .probe_positions .step_optimizer ()
1321+ self .parameter_group .probe_positions .step_optimizer (
1322+ clip_update = self .parameter_group .probe_positions .options .momentum_acceleration_gain <= 0
1323+ )
11831324
11841325 @timer ()
11851326 def _calculate_final_object_update_step_size (self ):
0 commit comments