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.7";
public static final String version = "0.7.8";

private Studio studio_;
private LightSheetManager model_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.micromanager.lightsheetmanager.gui.components.Panel;
import org.micromanager.lightsheetmanager.LightSheetManager;
import org.micromanager.lightsheetmanager.api.data.ChannelMode;
import org.micromanager.lightsheetmanager.api.data.GeometryType;
import org.micromanager.lightsheetmanager.gui.components.SettingsListener;
import org.micromanager.lightsheetmanager.gui.utils.DialogUtils;
import org.micromanager.lightsheetmanager.model.channels.ChannelSpec;
Expand All @@ -31,6 +32,10 @@ public class ChannelTablePanel extends Panel implements SettingsListener {
private ComboBox<String> cmbChannelGroup_;
private ComboBox<ChannelMode> cmbChannelMode_;

// The last mode actually written to the settings, used to put the combo back when a mode is
// refused. Tracked here rather than read back from the settings, which lag the builder.
private ChannelMode lastChannelMode_;

private final ChannelTable table_;
private final LightSheetManager model_;

Expand All @@ -43,6 +48,16 @@ public ChannelTablePanel(final LightSheetManager model, final CheckBox checkBox)
model.userSettings().addChangeListener(this);
}

/**
* Returns true when the geometry cannot use per-volume hardware channel switching.
* <p>
* The controller clocks that channel counter from the view select signal, which does not
* alternate on a single view geometry, so the mode is only usable with two views.
*/
private boolean isVolumeHwRefused() {
return model_.devices().adapter().geometry() == GeometryType.SCAPE;
}

private void createUserInterface() {
lblChannelGroup_ = new JLabel("Channel Group:");
lblChangeChannel_ = new JLabel("Channel Mode:");
Expand All @@ -60,9 +75,12 @@ private void createUserInterface() {
model_.acquisitions().settings().channels().group(),
120, 22);

cmbChannelMode_ = new ComboBox<>(ChannelMode.values(),
model_.acquisitions().settings().channels().mode(),
140, 22);
lastChannelMode_ = model_.acquisitions().settings().channels().mode();
if (lastChannelMode_ == ChannelMode.VOLUME_HW && isVolumeHwRefused()) {
// Settings loaded from an earlier build could hold a mode that is refused below.
lastChannelMode_ = ChannelMode.VOLUME;
}
cmbChannelMode_ = new ComboBox<>(ChannelMode.values(), lastChannelMode_, 140, 22);

add(lblChannelGroup_, "split 2");
add(cmbChannelGroup_, "wrap");
Expand Down Expand Up @@ -128,12 +146,15 @@ private void createEventHandlers() {
// select channel mode
cmbChannelMode_.registerListener(() -> {
final ChannelMode selected = cmbChannelMode_.getSelected();
model_.acquisitions().settingsBuilder().channelBuilder().mode(selected);
if (selected == ChannelMode.VOLUME_HW) {
if (selected == ChannelMode.VOLUME_HW && isVolumeHwRefused()) {
SwingUtilities.invokeLater(() -> {
DialogUtils.showErrorMessage(cmbChannelMode_, "Not Implemented",
"Not implemented in SCAPE, please contact ASI to request this feature.");
cmbChannelMode_.setSelected(lastChannelMode_);
});
} else {
model_.acquisitions().settingsBuilder().channelBuilder().mode(selected);
lastChannelMode_ = selected;
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,12 +673,17 @@ public void close() {
channelIndex, used[channelIndex]));
}
} else {
// Hardware channel modes (SLICE_HW / VOLUME_HW with hardware timepoints
// off): the controller emits all channels in one trigger, so a single
// merged iterator is correct here, unchanged.
// SLICE_HW with hardware timepoints off: the controller emits all
// channels within one armed run, so this submits a single iterator
// carrying no per-channel presets. Stamping a preset per channel made
// AcqEngJ split the merge and start one more camera sequence than the
// controller was armed to deliver, which then waited for frames that
// never arrived. VOLUME_HW reaches this branch too, but needs the
// opposite channel axis order and is refused on SCAPE.
currentAcquisition_.submitEventIterator(
LightSheetEventAdapter.createChannelAcqEvents(
baseEvent.copy(), acqSettings_, cameraNames, null));
LightSheetEventAdapter.createChannelPerSliceAcqEvents(
baseEvent.copy(), acqSettings_, cameraNames,
acqSettings_.channels().used(), null));
}
} else {
currentAcquisition_.submitEventIterator(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,54 +95,6 @@ public static Iterator<AcquisitionEvent> createTimelapseVolumeAcqEvents(
return new AcquisitionEventIterator(baseEvent, acqFunctions, eventMonitor);
}

/**
*
* @param interleaved true: do we want to do every channel at each z slice before moving to
* the next z slice
* false: do an entire volume in one channel, then the next one
*/
public static Iterator<AcquisitionEvent> createMultiChannelVolumeAcqEvents(
AcquisitionEvent baseEvent, AcquisitionSettings settings,
String[] cameraDeviceNames,
Function<AcquisitionEvent, AcquisitionEvent> eventMonitor, boolean interleaved) {

Function<AcquisitionEvent, Iterator<AcquisitionEvent>> channels =
channels(settings.channels().used());

Function<AcquisitionEvent, Iterator<AcquisitionEvent>> zStack =
zStack(0, settings.volume().slicesPerView());

Function<AcquisitionEvent, Iterator<AcquisitionEvent>> cameras = cameras(cameraDeviceNames);

ArrayList<Function<AcquisitionEvent, Iterator<AcquisitionEvent>>> acqFunctions = new ArrayList<>();
if (interleaved) {
acqFunctions.add(cameras);
acqFunctions.add(zStack);
acqFunctions.add(channels);
} else {
acqFunctions.add(channels);
acqFunctions.add(cameras);
acqFunctions.add(zStack);
}
return new AcquisitionEventIterator(baseEvent, acqFunctions, eventMonitor);
}

public static Iterator<AcquisitionEvent> createVolumeAcqEvents(
AcquisitionEvent baseEvent, AcquisitionSettings settings,
String[] cameraDeviceNames,
Function<AcquisitionEvent, AcquisitionEvent> eventMonitor) {

Function<AcquisitionEvent, Iterator<AcquisitionEvent>> cameras = cameras(cameraDeviceNames);

Function<AcquisitionEvent, Iterator<AcquisitionEvent>> zStack =
zStack(0, settings.volume().slicesPerView());

ArrayList<Function<AcquisitionEvent, Iterator<AcquisitionEvent>>> acqFunctions = new ArrayList<>();
acqFunctions.add(cameras);
acqFunctions.add(zStack);
return new AcquisitionEventIterator(baseEvent, acqFunctions, eventMonitor);
}

public static Iterator<AcquisitionEvent> createChannelAcqEvents(
AcquisitionEvent baseEvent, AcquisitionSettings settings,
String[] cameraDeviceNames,
Expand Down Expand Up @@ -233,6 +185,148 @@ public static Iterator<AcquisitionEvent> createSingleChannelVolumeAcqEvents(
return new AcquisitionEventIterator(baseEvent, acqFunctions, eventMonitor);
}

/**
* Build events for one volume whose channels the controller switches slice by slice.
* <p>
* <b>Slice-by-slice switching only.</b> Volume-by-volume switching changes channel once per
* volume and needs the opposite axis order; see {@link #createChannelPerVolumeAcqEvents}. Using
* this factory for it would not fail, since the event count still matches; the images would
* simply be filed against the wrong coordinates.
* <p>
* The controller emits every channel within a single armed run, so the event stream has to
* describe one sequence rather than one per channel. Two things follow from that.
* <p>
* The events carry no per-channel config preset. A preset that differs between consecutive events
* makes AcqEngJ break the merge and start one more camera sequence than the controller was armed
* to deliver, so the extra sequence waits for frames that never arrive. Presets also move whatever
* devices they name, such as filter wheels, which cannot follow a controller switching channels
* at slice rate.
* <p>
* The channel axis is innermost because the controller interleaves channels within a slice, and
* AcqEngJ assigns each image to the first event matching that camera in sequence order, so the
* event order per camera has to match the order the camera delivers frames.
* <p>
* A single channel is the exception. Hardware channel switching is not set up at all for one
* channel, so that channel's own preset and offset apply, as they do on the software path.
* Baking them into the base event leaves every event carrying the same preset, which cannot
* split the merge, since a sequence only breaks where consecutive presets differ.
*
* @param usedChannels the channels the controller was programmed for
*/
public static Iterator<AcquisitionEvent> createChannelPerSliceAcqEvents(
AcquisitionEvent baseEvent, AcquisitionSettings settings,
String[] cameraDeviceNames, ChannelSpec[] usedChannels,
Function<AcquisitionEvent, AcquisitionEvent> eventMonitor) {

if (usedChannels.length == 1) {
final ChannelSpec channel = usedChannels[0];
baseEvent.setConfigGroup(channel.getGroup());
baseEvent.setConfigPreset(channel.getName());

// Channel z-offset: mirror channels(), apply the offset to the current stage/z position.
double zPos;
if (baseEvent.getZPosition() == null) {
try {
zPos = Engine.getCore().getPosition() + channel.getOffset();
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
zPos = baseEvent.getZPosition() + channel.getOffset();
}
baseEvent.setZ(baseEvent.getZIndex(), zPos);
}

// Base 0 so cameras() leaves the plain camera index on the axis; channelAxis() runs
// innermost and folds it into the combined slot.
Function<AcquisitionEvent, Iterator<AcquisitionEvent>> cameras =
cameras(cameraDeviceNames, 0);
Function<AcquisitionEvent, Iterator<AcquisitionEvent>> zStack =
zStack(0, settings.volume().slicesPerView());
Function<AcquisitionEvent, Iterator<AcquisitionEvent>> channels =
channelAxis(usedChannels.length, cameraDeviceNames.length);

ArrayList<Function<AcquisitionEvent, Iterator<AcquisitionEvent>>> acqFunctions = new ArrayList<>();
acqFunctions.add(cameras);
acqFunctions.add(zStack);
acqFunctions.add(channels);
return new AcquisitionEventIterator(baseEvent, acqFunctions, eventMonitor);
}

/**
* Build events for a volume whose channels the controller switches volume by volume.
* <p>
* <b>Not implemented.</b> Present so the gap is visible next to
* {@link #createChannelPerSliceAcqEvents} rather than being rediscovered.
* <p>
* This mode switches channel once per volume instead of once per slice, so the controller
* delivers every slice of one channel before it starts the next channel. The channel axis
* therefore has to sit <b>outside</b> the z axis, the opposite of the slice-by-slice factory,
* and the combined axis slot cannot be derived the way {@link #channelAxis} derives it, since
* the cameras fan out below the channels rather than above them. Getting the order wrong does
* not fail: the event count still matches, so the images are simply filed against the wrong
* coordinates.
* <p>
* A single view geometry cannot use this at all, because the controller clocks its channel
* counter from the view select signal, which never alternates with one view. The two view
* geometry is what this is for, and its event submission is currently disabled, so nothing
* calls this yet. Implement it against two view hardware rather than from this description.
*/
public static Iterator<AcquisitionEvent> createChannelPerVolumeAcqEvents(
AcquisitionEvent baseEvent, AcquisitionSettings settings,
String[] cameraDeviceNames, ChannelSpec[] usedChannels,
Function<AcquisitionEvent, AcquisitionEvent> eventMonitor) {
throw new UnsupportedOperationException(
"volume-by-volume hardware channel switching is not implemented; "
+ "it needs the channel axis outside the z axis, unlike "
+ "createChannelPerSliceAcqEvents");
}

/**
* Fan an event out over controller-switched channels, writing only the {@link #CAMERA_AXIS}
* coordinate.
* <p>
* Deliberately sets no config group or preset, and no channel z-offset: the controller owns
* channel switching here, and the offset stays wherever the user's own preset left it.
* Runs innermost, after {@link #cameras}, so the value already on the axis is the camera index;
* it is replaced with the combined slot {@code channelIndex * numCameras + cameraIndex} that
* sizes and names the datastore.
*
* @param numChannels the number of channels the controller was programmed for
* @param numCameras the number of cameras this event fans out over
*/
public static Function<AcquisitionEvent, Iterator<AcquisitionEvent>> channelAxis(
int numChannels, int numCameras) {
return (AcquisitionEvent event) -> new Iterator<>() {

private int channelIndex_ = 0;

@Override
public boolean hasNext() {
return channelIndex_ < numChannels;
}

@Override
public AcquisitionEvent next() {
AcquisitionEvent channelEvent = event.copy();

Object position = event.getAxisPosition(CAMERA_AXIS);
int cameraIndex = 0;
if (position != null) {
try {
cameraIndex = Integer.parseInt(position.toString());
} catch (NumberFormatException e) {
// ignore => number already assigned
}
}

channelEvent.setAxisPosition(CAMERA_AXIS, channelIndex_ * numCameras + cameraIndex);
channelIndex_++;
return channelEvent;
}
};
}

/**
* Fan an event out over the cameras, deriving the channel-axis base from the event.
* <p>
Expand Down
Loading