Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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.7.6";
public static final String version = "0.7.7";

private Studio studio_;
private LightSheetManager model_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.micromanager.data.internal.PropertyKey;
import org.micromanager.lightsheetmanager.LightSheetManager;
import org.micromanager.lightsheetmanager.api.AcquisitionManager;
import org.micromanager.lightsheetmanager.api.TimingSettings;
import org.micromanager.lightsheetmanager.api.data.AcquisitionMode;
import org.micromanager.lightsheetmanager.api.internal.ScapeAcquisitionSettings;
import org.micromanager.lightsheetmanager.gui.tabs.acquisition.DurationPanel;
Expand Down Expand Up @@ -145,6 +146,74 @@ protected boolean validateCameraFrameSizes() {
return false;
}

/**
* Refuses to arm when the computed slice timing is not physically realizable.
*
* <p>In EDGE mode the solver derives the camera exposure by subtracting the camera's reset and
* readout time from the sample exposure, and nothing stops the result going negative. A sample
* exposure shorter than the camera needs to reset and read out therefore yields a negative
* exposure, which is handed to {@code setExposure()} and accepted by the device without
* complaint, so the run proceeds and produces nothing usable.
*
* <p>Deliberately conservative: only the two values that are meaningless at or below zero are
* required to be positive. Delays and the remaining durations are allowed to be zero.
*
* <p>Note "minimize slice period" does not rescue this: it is only consulted on the galvo path
* ({@code getTimingFromPeriodAndLightExposure}), while the stage-scan path derives the exposure
* as sample exposure minus the camera's reset plus readout regardless.
*
* <p>Called from both geometry engines' {@code setup()} before any hardware is touched.
*
* @return true if the timing is usable; false to abort setup
*/
protected boolean validateSliceTiming() {
final String problems = describeUnusableTiming(acqSettings_.timing());
if (problems == null) {
return true;
}
model_.logging().reportError("The computed slice timing cannot be used: " + problems
+ ".\n\nThis happens when the sample exposure is shorter than the time the camera "
+ "needs to reset and read out, so the acquisition was not started.\n\nRaise the "
+ "sample exposure, or shorten the readout with a smaller camera ROI or higher "
+ "binning, then try again.");
return false;
}

/**
* Describes what is wrong with a computed timing schedule, or returns null when it is usable.
*
* @param timing the computed timing settings
* @return a comma-separated description with no trailing punctuation, or null if usable
*/
private static String describeUnusableTiming(final TimingSettings timing) {
if (timing == null) {
return "no timing has been computed";
}
final StringBuilder problems = new StringBuilder();
// must be strictly positive: a slice that exposes for zero time images nothing
appendTimingProblem(problems, "camera exposure", timing.cameraExposure(), true);
appendTimingProblem(problems, "slice duration", timing.sliceDuration(), true);
// may legitimately be zero, so only reject negatives
appendTimingProblem(problems, "scan duration", timing.scanDuration(), false);
appendTimingProblem(problems, "laser trigger duration", timing.laserTriggerDuration(), false);
appendTimingProblem(problems, "camera trigger duration", timing.cameraTriggerDuration(), false);
appendTimingProblem(problems, "delay before scan", timing.delayBeforeScan(), false);
appendTimingProblem(problems, "delay before laser", timing.delayBeforeLaser(), false);
appendTimingProblem(problems, "delay before camera", timing.delayBeforeCamera(), false);
return problems.length() == 0 ? null : problems.toString();
}

private static void appendTimingProblem(final StringBuilder problems, final String name,
final double valueMs, final boolean mustBePositive) {
if (mustBePositive ? valueMs > 0.0 : valueMs >= 0.0) {
return;
}
if (problems.length() > 0) {
problems.append(", ");
}
problems.append(name).append(" is ").append(valueMs).append(" ms");
}

public AcquisitionEngine(final LightSheetManager model) {
model_ = Objects.requireNonNull(model);
studio_ = model.studio();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ boolean setup() {
return false; // early exit => cameras disagree on frame size
}

// an impossible slice period yields a negative camera exposure, which the device accepts
// without complaint and then images nothing; refuse rather than run it
if (!validateSliceTiming()) {
return false; // early exit => computed timing is not realizable
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ boolean setup() {
// make settings current
updateSettings();

// initialize stage scanning so we can restore state
// set before any validation below can return early: finish() runs on every exit from setup(),
// including the refusals, and restores these unconditionally. Initialized further down they
// are still at their field defaults on those paths, so finish() writes 0.0. The stage rejects
// that for speed but ACCEPTS it for acceleration, leaving it unable to move properly.
xyPosUm_ = new Point2D.Double();
origSpeedX_ = 1.0; // don't want 0 in case something goes wrong
origAccelX_ = 1.0; // don't want 0 in case something goes wrong

// fail before touching any hardware: the datastore is written by finish(), so an unusable
// save location would otherwise cost a full acquisition before it is discovered
if (!validateSaveLocation()) {
Expand All @@ -99,6 +108,12 @@ boolean setup() {
return false; // early exit => cameras disagree on frame size
}

// an impossible slice period yields a negative camera exposure, which the device accepts
// without complaint and then images nothing; refuse rather than run it
if (!validateSliceTiming()) {
return false; // early exit => computed timing is not realizable
}

// // check pixel size
// if (core_.getPixelSizeUm() < 1e-6) {
// studio_.logs().showError(
Expand Down Expand Up @@ -151,11 +166,6 @@ boolean run() {
// used to detect if the plugin is using ASI hardware
final boolean isUsingPLC = model_.devices().isUsingPLogic();

// initialize stage scanning so we can restore state
xyPosUm_ = new Point2D.Double();
origSpeedX_ = 1.0; // don't want 0 in case something goes wrong
origAccelX_ = 1.0; // don't want 0 in case something goes wrong

// make sure stage scan is supported if selected
if (acqSettings_.stageScan().enabled()) {
final ASIXYStage xyStage = model_.devices().device("SampleXY");
Expand Down Expand Up @@ -1450,6 +1460,17 @@ public DefaultTimingSettings.Builder getTimingFromPeriodAndLightExposure() {

@Override
public void updateDurationLabels() {
// TODO(IMMUTABLE-RUN): sync the snapshot to the builder before recomputing; real fix = the
// derive/arm split so the timing math reads one source.
//
// Every caller has just written the user's edit to the BUILDER, but the timing math reads
// the frozen snapshot (getTimingFromExposure and getTimingFromPeriodAndLightExposure both
// take sampleExposure, cameraMode, period and stageScan.enabled off acqSettings_). Without
// this sync the recompute runs on the previous edit: for most controls that leaves the
// labels one edit stale, and for the acquisition-mode dropdown it selects the wrong branch
// outright, computing galvo timing for a stage-scan run. That pair is then frozen and the
// next Run fails validation comparing one mode's exposure against the other's duration.
model_.acquisitions().updateSettings();
model_.acquisitions().recalculateSliceTiming();
model_.acquisitions().updateSettings();
// update durations now that settings are current
Expand Down
Loading