From 8562fe78b347d27c9b5db51a7af4743b22ad34af Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 20 Aug 2026 22:40:30 -0700 Subject: [PATCH 1/6] scan from the event coordinate instead of the live stage position --- .../acquisitions/AcquisitionEngineScape.java | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java index d5a8484..dc40a15 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java @@ -15,7 +15,6 @@ import org.micromanager.data.Datastore; import org.micromanager.data.internal.DefaultDatastore; import org.micromanager.data.internal.DefaultSummaryMetadata; -import org.micromanager.internal.MMStudio; import org.micromanager.lightsheetmanager.api.data.AcquisitionMode; import org.micromanager.lightsheetmanager.api.data.CameraLibrary; import org.micromanager.lightsheetmanager.api.data.CameraMode; @@ -43,6 +42,7 @@ import java.awt.geom.Point2D; import java.io.File; import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -53,6 +53,10 @@ public class AcquisitionEngineScape extends AcquisitionEngine { PLogicScape controller_; ArrayList savedExposures_ = new ArrayList<>(); Point2D.Double xyPosUm_; + // Snapshot taken when the run is armed. The position list is user-editable at any time, so + // a live read can give different answers to different parts of one run: the saved + // position_list.pos, the generated events, and the per-arm stage scan setup must agree. + private volatile PositionList positionList_; private double origSpeedX_; private double origAccelX_; private double scanSpeedX_; @@ -97,8 +101,9 @@ boolean setup() { // make sure that there are positions in the PositionList; a pure read, so it belongs // above the hardware changes below for the same reason as the save location check + positionList_ = studio_.positions().getPositionList(); if (acqSettings_.isUsingMultiplePositions()) { - final int numPositions = studio_.positions().getPositionList().getNumberOfPositions(); + final int numPositions = positionList_.getNumberOfPositions(); if (numPositions == 0) { studio_.logs().showError("XY positions expected but the position list is empty"); return false; @@ -268,7 +273,7 @@ boolean run() { // write the position list if we are using multiple positions if (model_.acquisitions().settings().isSavingImagesDuringAcquisition() && model_.acquisitions().settings().isUsingMultiplePositions()) { - final PositionList positionList = model_.studio().positions().getPositionList(); + final PositionList positionList = positionList_; if (positionList.getNumberOfPositions() > 0) { try { final String path = saveDir + File.separator + "position_list.pos"; @@ -508,10 +513,37 @@ public AcquisitionEvent run(AcquisitionEvent event) { if (isUsingPLC) { if (acqSettings_.stageScan().enabled() && acqSettings_.isUsingMultiplePositions()) { final ASIXYStage xyStage = model_.devices().device("SampleXY"); - final Point2D.Double pos = xyStage.getXYPosition(); + // Scan from the coordinate this event was generated for instead of reading + // the stage. The read only agrees with the target when a move was issued + // for this arm. When the same position repeats across time points no move + // is issued, the stage is still parked on the previous scan start, and + // centering on that subtracts half the scan distance again, so the window + // walks half a field every arm while plane counts stay exact. The 1.4 + // plugin takes this value from the position list for the same reason. + // A hardware sequence arrives here as a wrapper event whose own + // coordinates and axis positions are null by construction. The constituent + // events keep theirs, so read the coordinate off the first of them. + AcquisitionEvent coordEvent = event; + final List sequence = event.getSequence(); + if (sequence != null && !sequence.isEmpty()) { + coordEvent = sequence.get(0); + } + final Double eventX = coordEvent.getXPosition(); + final Double eventY = coordEvent.getYPosition(); + if (eventX == null || eventY == null) { + // An exception thrown here is invisible. AcqEngJ leaves both cameras + // armed and the run stops with nothing in the log, so refuse through + // abort() rather than let a dereference escape the hook. + studio_.logs().logError("stage scan: acquisition event carried no XY " + + "coordinate, aborting rather than scanning at an unknown " + + "position"); + currentAcquisition_.abort(); + return event; + } xyStage.setSpeedX(scanSpeedX_); xyStage.setAccelerationX(scanAccelX_); - controllerInstance.prepareStageScanForAcquisition(pos.x, pos.y, acqSettings_); + controllerInstance.prepareStageScanForAcquisition( + eventX, eventY, acqSettings_); controllerInstance.triggerControllerStartAcquisition(acqSettings_.acquisitionMode()); return event; } @@ -590,7 +622,7 @@ public void close() { // Loop 1: XY positions - PositionList pl = MMStudio.getInstance().positions().getPositionList(); + PositionList pl = positionList_; String[] cameraNames; if (demoMode) { From 8024cbcae97eb9039b4835486289494eea21cc27 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 20 Aug 2026 22:40:30 -0700 Subject: [PATCH 2/6] refuse a galvo sweep past the scanner travel limits, bump version --- .../LightSheetManagerPlugin.java | 2 +- .../gui/tabs/acquisition/VolumePanel.java | 6 ++++-- .../lightsheetmanager/model/PLogicDispim.java | 14 ++++++++++++++ .../lightsheetmanager/model/PLogicScape.java | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java index d84020c..80cc7b4 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java @@ -15,7 +15,7 @@ public class LightSheetManagerPlugin implements MenuPlugin, SciJavaPlugin { public static final String copyright = "Applied Scientific Instrumentation (ASI), 2022-2026"; public static final String description = "A plugin to control various types of light sheet microscopes."; public static final String menuName = "Light Sheet Manager"; - public static final String version = "0.8.5"; + public static final String version = "0.8.10"; private Studio studio_; diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/VolumePanel.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/VolumePanel.java index 7ca456c..51aa228 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/VolumePanel.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/VolumePanel.java @@ -77,12 +77,14 @@ private void createUserInterface() { spnViewDelay_ = Spinner.createDoubleSpinner( volumeSettings.delayBeforeView(), 0.0, Double.MAX_VALUE, 0.25); + // bounded to match 1.4; unbounded entry here is what turns a typo into a galvo command + // far outside the scanner's travel spnSliceStepSize_ = Spinner.createDoubleSpinner( volumeSettings.sliceStepSize(), - 0.0, Double.MAX_VALUE, 0.1); + 0.0, 100.0, 0.1); spnNumSlices_ = Spinner.createIntegerSpinner( volumeSettings.slicesPerView(), - 1, Integer.MAX_VALUE, 1); + 1, 65000, 1); switch (geometryType_) { case DISPIM: diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java index 40b6672..cc8e972 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java @@ -603,6 +603,20 @@ public boolean prepareControllerForAcquisitionSide( sliceAmplitude = NumberUtils.roundToPlace(sliceAmplitude, 4); sliceCenter = NumberUtils.roundToPlace(sliceCenter, 4); + // Refuse a sweep that would drive the galvo past its travel limits, mirroring the + // check in the SCAPE controller. It runs after the rounding and before either branch + // below writes to the card, and a failed property read returns zero for both limits, + // so an unreadable scanner refuses the sweep rather than passing it through. + final double halfSweep = Math.abs(sliceAmplitude) / 2; + if (sliceCenter + halfSweep > scanner.getMaxDeflectionY()) { + studio_.logs().showError("Scanner will exceed allowed range in positive direction."); + return false; + } + if (sliceCenter - halfSweep < scanner.getMinDeflectionY()) { + studio_.logs().showError("Scanner will exceed allowed range in negative direction."); + return false; + } + if (offsetOnly) { scanner.sa().setOffsetY(sliceCenter); } else { // normal case diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java index 2ebb7f5..9c8b1f4 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java @@ -504,6 +504,22 @@ public boolean prepareControllerForAcquisitionSide( sliceAmplitude = NumberUtils.roundToPlace(sliceAmplitude, 4); sliceCenter = NumberUtils.roundToPlace(sliceCenter, 4); + // Refuse a sweep that would drive the galvo past its travel limits. This has to run here, + // after the rounding and before either branch below writes to the card. The piezo check + // further down cannot stand in for it: GALVO_SCAN zeroes piezoAmplitude first, so that + // check degenerates to validating a center with no sweep around it, and galvo scan is the + // mode most runs use. A failed property read returns zero for both limits, so an + // unreadable scanner refuses the sweep rather than passing it through. + final double halfSweep = Math.abs(sliceAmplitude) / 2; + if (sliceCenter + halfSweep > scanner_.getMaxDeflectionY()) { + studio_.logs().showError("Scanner will exceed allowed range in positive direction."); + return false; + } + if (sliceCenter - halfSweep < scanner_.getMinDeflectionY()) { + studio_.logs().showError("Scanner will exceed allowed range in negative direction."); + return false; + } + if (offsetOnly) { scanner_.sa().setOffsetY(sliceCenter); } else { // normal case From 4d06cc06d36c9cddb7f9d6e23b1cd7dc352ed8ad Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 20 Aug 2026 22:45:59 -0700 Subject: [PATCH 3/6] log when a stop is requested --- .../model/acquisitions/AcquisitionEngine.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java index d6a095c..6c5dd1b 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java @@ -426,7 +426,11 @@ public void requestStop() { // and reporting "not running" there would leave the run going with the button reading // "Start Acquisition", which is also the way into a second, unwanted run. stopRequested_ = true; - if (currentAcquisition_ != null && !currentAcquisition_.getDataSink().isFinished()) { + final boolean isAcquisitionLive = currentAcquisition_ != null + && !currentAcquisition_.getDataSink().isFinished(); + studio_.logs().logMessage("stop requested during " + + (isAcquisitionLive ? "acquisition" : "setup")); + if (isAcquisitionLive) { currentAcquisition_.abort(); } } From 0ab60e8b589df3831a2b9b3847586bd208cb7aae Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 20 Aug 2026 23:00:23 -0700 Subject: [PATCH 4/6] restore the multi-position time point interval warning --- .../acquisitions/AcquisitionEngineScape.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java index dc40a15..ab6c017 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java @@ -1078,16 +1078,19 @@ private boolean doHardwareCalculations(PLogicScape plc) { } // TODO: implement multiple positions using hardware time points, currently - // set hardware time points to false if using multiple positions + // set hardware time points to false if using multiple positions. The 1.4 plugin does + // not support this combination either, so it is new capability rather than a gap in + // the port. if (acqSettings_.isUsingMultiplePositions()) { - if (isUsingHardwareTimePoints) { -// || acqSettings_.numTimePoints() > 1) -// && (timepointIntervalMs < timepointDuration*1.2)) { + if (isUsingHardwareTimePoints + || (acqSettings_.numTimePoints() > 1 + && timepointIntervalMs < timepointDuration * 1.2)) { + // warn the user but allow the acquisition to continue asb_.useHardwareTimePoints(false); isUsingHardwareTimePoints = false; -// studio_.logs().showError("Time point interval may not be sufficient " -// + "depending on actual time required to change positions. " -// + "Proceed at your own risk."); + model_.logging().reportError("Time point interval may not be sufficient " + + "depending on actual time required to change positions. " + + "Proceed at your own risk."); } } From 65b9c1ffe770cf7e86c28bfda853270a46b85047 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 20 Aug 2026 23:50:04 -0700 Subject: [PATCH 5/6] write the run settings into the dataset directory --- .../acquisitions/AcquisitionEngineScape.java | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java index ab6c017..ce4c679 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java @@ -13,6 +13,7 @@ import org.micromanager.acquisition.internal.acqengjcompat.AcqEngJAdapter; import org.micromanager.acquisition.internal.acqengjcompat.AcqEngJMDADataSink; import org.micromanager.data.Datastore; +import org.micromanager.data.SummaryMetadata; import org.micromanager.data.internal.DefaultDatastore; import org.micromanager.data.internal.DefaultSummaryMetadata; import org.micromanager.lightsheetmanager.api.data.AcquisitionMode; @@ -264,27 +265,6 @@ boolean run() { String saveDir = acqSettings_.saveDirectory(); String saveName = acqSettings_.saveNamePrefix(); - // TODO: put this in AcquisitionEngine base class, between setup and run once structure is better - // save settings as JSON to the save directory - if (model_.acquisitions().settings().isSavingImagesDuringAcquisition()) { - FileUtils.writeStringToFile(saveDir + File.separator + "acq_settings.json", settingsJson); - } - - // write the position list if we are using multiple positions - if (model_.acquisitions().settings().isSavingImagesDuringAcquisition() - && model_.acquisitions().settings().isUsingMultiplePositions()) { - final PositionList positionList = positionList_; - if (positionList.getNumberOfPositions() > 0) { - try { - final String path = saveDir + File.separator + "position_list.pos"; - positionList.save(path); - model_.studio().logs().logMessage("Position list saved to " + path); - } catch (Exception e) { - model_.studio().logs().logError(e, "Could not save position list."); - } - } - } - // Sets MM's persisted preferred save mode. MMAcquisition reads it when SequenceSettings // has save() and root() set, which is what the saving branch below does, so this is what // picks ND-TIFF over multipage TIFF or a single plane series for the images written during @@ -341,6 +321,38 @@ boolean run() { sink.setDatastore(datastore_); sink.setPipeline(curPipeline_); + // TODO: put this in AcquisitionEngine base class, between setup and run once structure is better + // Write the run settings and the position list into the dataset directory instead of the + // parent, so a dataset carries the record of what produced it. Written here rather than + // earlier because the directory name is chosen by MMAcquisition above: it creates the + // directory at run start and stamps the name into the summary metadata as the prefix. + if (acqSettings_.isSavingImagesDuringAcquisition()) { + String datasetDir = saveDir; + final SummaryMetadata summary = datastore_.getSummaryMetadata(); + final String datasetName = (summary == null) ? null : summary.getPrefix(); + if (datasetName != null && !datasetName.isEmpty() + && new File(saveDir + File.separator + datasetName).isDirectory()) { + datasetDir = saveDir + File.separator + datasetName; + } else { + // a configured processing pipeline can delay the summary metadata reaching the + // store, so fall back to the parent directory rather than dropping the files + studio_.logs().logError("Could not resolve the dataset directory, writing the run " + + "settings beside the dataset instead of inside it."); + } + FileUtils.writeStringToFile( + datasetDir + File.separator + "acq_settings.json", settingsJson); + if (acqSettings_.isUsingMultiplePositions() + && positionList_.getNumberOfPositions() > 0) { + try { + final String path = datasetDir + File.separator + "position_list.pos"; + positionList_.save(path); + studio_.logs().logMessage("Position list saved to " + path); + } catch (Exception e) { + studio_.logs().logError(e, "Could not save position list."); + } + } + } + studio_.events().registerForEvents(this); // commented because this is prob specific to MM MDAs // studio_.events().post(new DefaultAcquisitionStartedEvent(datastore_, this, From dfcb9a88ef3bd850c3796e038757135fd5cf3896 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Aug 2026 00:16:29 -0700 Subject: [PATCH 6/6] bump the version to 0.8.11 --- .../micromanager/lightsheetmanager/LightSheetManagerPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java index 80cc7b4..b6b807e 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java @@ -15,7 +15,7 @@ public class LightSheetManagerPlugin implements MenuPlugin, SciJavaPlugin { public static final String copyright = "Applied Scientific Instrumentation (ASI), 2022-2026"; public static final String description = "A plugin to control various types of light sheet microscopes."; public static final String menuName = "Light Sheet Manager"; - public static final String version = "0.8.10"; + public static final String version = "0.8.11"; private Studio studio_;