@@ -260,26 +260,110 @@ public double fullFocus() throws Exception {
260260 * @param desiredZPos the Z position in microns for the main zDrive
261261 * @return true on success, false on failure.
262262 */
263+ // Maximum number of offset-adjustment iterations before we give up. Without
264+ // a cap, a bad (e.g. non-finite) multiplier or an oscillating ZDrive can keep
265+ // this loop running forever, repeatedly commanding the PFS offset.
266+ private static final int MAX_ADJUST_ITERATIONS = 20 ;
267+
268+ // Set to true to emit verbose per-iteration diagnostics from adjustPFSOffset
269+ // (kept available for future investigation of offset-adjustment issues).
270+ private static final boolean DEBUG_LOGGING = false ;
271+
272+ private void debugLog (String message ) {
273+ if (DEBUG_LOGGING ) {
274+ studio_ .logs ().logMessage (message );
275+ }
276+ }
277+
263278 private boolean adjustPFSOffset (double desiredZPos ) {
264279 CMMCore core = studio_ .core ();
265280 ArrayList <Pair <Double , Double >> m = new ArrayList <>();
266281 try {
267282 String pixelSizeConfig = core .getCurrentPixelSizeConfig ();
268283 MutablePropertyMapView settings = studio_ .profile ().getSettings (this .getClass ());
269- double multiplier = settings .getDouble (pixelSizeConfig , 1.0 );
284+ final double defaultMultiplier = 1.0 ;
285+ double multiplier = settings .getDouble (pixelSizeConfig , defaultMultiplier );
286+ // A non-finite multiplier may have been persisted by an earlier run.
287+ // Reject it as early as possible and fall back to the default, so the
288+ // bad value can never propagate into the offset calculation.
289+ if (!Double .isFinite (multiplier )) {
290+ studio_ .logs ().logError ("PFSOffsetFocusser: stored multiplier for pixelSizeConfig '"
291+ + pixelSizeConfig + "' was non-finite (" + multiplier + "); reverting to default "
292+ + defaultMultiplier + "." );
293+ multiplier = defaultMultiplier ;
294+ settings .putDouble (pixelSizeConfig , defaultMultiplier );
295+ }
270296 double zPos = core .getPosition (zDrive_ );
297+ debugLog ("PFSOffsetFocusser.adjustPFSOffset: start. desiredZPos="
298+ + desiredZPos + ", initial zPos=" + zPos + ", precision=" + precision_
299+ + ", pixelSizeConfig=" + pixelSizeConfig + ", stored multiplier=" + multiplier );
300+ int iteration = 0 ;
271301 while (Math .abs (desiredZPos - zPos ) > precision_ ) {
302+ if (++iteration > MAX_ADJUST_ITERATIONS ) {
303+ studio_ .logs ().logError ("PFSOffsetFocusser: gave up adjusting PFS offset after "
304+ + MAX_ADJUST_ITERATIONS + " iterations; ZDrive did not reach the target "
305+ + "position (last deviation " + Math .abs (desiredZPos - zPos ) + " um)." );
306+ return false ;
307+ }
272308 double currentOffset = core .getAutoFocusOffset ();
273309 double offsetDiff = multiplier * (desiredZPos - zPos );
274- core .setAutoFocusOffset (currentOffset + offsetDiff );
310+ double newOffset = currentOffset + offsetDiff ;
311+ debugLog ("PFSOffsetFocusser iter " + iteration
312+ + ": zPos=" + zPos + ", desiredZPos=" + desiredZPos
313+ + ", (desiredZPos-zPos)=" + (desiredZPos - zPos )
314+ + ", multiplier=" + multiplier
315+ + ", currentOffset=" + currentOffset
316+ + ", offsetDiff=" + offsetDiff
317+ + ", newOffset=" + newOffset );
318+ // Never command a non-finite offset to the hardware. This can arise
319+ // if the multiplier became non-finite (see weightedAverage below) or
320+ // if getAutoFocusOffset() returned an unexpected value.
321+ if (!Double .isFinite (newOffset )) {
322+ studio_ .logs ().logError ("PFSOffsetFocusser: computed a non-finite PFS offset ("
323+ + newOffset + ") from multiplier=" + multiplier + ", currentOffset="
324+ + currentOffset + ", desiredZPos=" + desiredZPos + ", zPos=" + zPos
325+ + "; aborting offset adjustment." );
326+ return false ;
327+ }
328+ core .setAutoFocusOffset (newOffset );
275329 Thread .sleep (1000 );
276330 double newZPos = core .getPosition (zDrive_ );
277331 double zPosDiff = newZPos - zPos ;
278- double multiplierEstimate = offsetDiff / zPosDiff ;
279- m .add (new ImmutablePair <>(multiplierEstimate , zPosDiff ));
280- multiplier = weightedAverage (m );
332+ debugLog ("PFSOffsetFocusser iter " + iteration
333+ + ": after setAutoFocusOffset(" + newOffset + "): newZPos=" + newZPos
334+ + ", zPosDiff=" + zPosDiff );
335+ if (zPosDiff != 0 ) {
336+ double multiplierEstimate = offsetDiff / zPosDiff ;
337+ // Weight by the magnitude of the ZDrive move: larger, more
338+ // reliable moves get more weight. Using the unsigned magnitude
339+ // (rather than signed zPosDiff) prevents the weights from
340+ // cancelling to ~0 when the ZDrive oscillates.
341+ m .add (new ImmutablePair <>(multiplierEstimate , Math .abs (zPosDiff )));
342+ double newMultiplier = weightedAverage (m );
343+ debugLog ("PFSOffsetFocusser iter " + iteration
344+ + ": multiplierEstimate=" + multiplierEstimate
345+ + ", weight=" + Math .abs (zPosDiff )
346+ + ", weightedAverage=" + newMultiplier
347+ + " (sample count=" + m .size () + ")" );
348+ // Keep the previous (good) multiplier if the weighted average is
349+ // not usable, e.g. when the weights sum to ~0 and produce a
350+ // divide-by-zero NaN/Infinity.
351+ if (Double .isFinite (newMultiplier )) {
352+ multiplier = newMultiplier ;
353+ } else {
354+ studio_ .logs ().logMessage ("PFSOffsetFocusser: weighted-average multiplier was "
355+ + "non-finite (" + newMultiplier + "); keeping previous multiplier "
356+ + multiplier + "." );
357+ }
358+ } else {
359+ debugLog ("PFSOffsetFocusser iter " + iteration
360+ + ": zPosDiff is 0; not updating multiplier." );
361+ }
281362 zPos = newZPos ;
282363 }
364+ debugLog ("PFSOffsetFocusser.adjustPFSOffset: converged after "
365+ + iteration + " iteration(s); final zPos=" + zPos
366+ + ", final multiplier=" + multiplier );
283367 settings .putDouble (pixelSizeConfig , multiplier );
284368 } catch (Exception e ) {
285369 studio_ .logs ().logError (e );
@@ -302,6 +386,13 @@ private double weightedAverage(List<Pair<Double, Double>> values) {
302386 sumZPosDiffs += val .getValue ();
303387 weightedEstimateSum += val .getValue () * val .getKey ();
304388 }
389+ // Weights are non-negative magnitudes (|zPosDiff|), so they only sum to ~0
390+ // if the list is empty or every move was negligible. Guard the divide-by-
391+ // zero and return NaN so the caller can fall back to the previous
392+ // multiplier instead of poisoning the offset.
393+ if (Math .abs (sumZPosDiffs ) < 1e-9 ) {
394+ return Double .NaN ;
395+ }
305396 return weightedEstimateSum / sumZPosDiffs ;
306397 }
307398
0 commit comments