-
Notifications
You must be signed in to change notification settings - Fork 40
[CSDM 1162] extend tiled acquisition plugin to support wafer acquisition workflow #3326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughThese changes refactor the tile acquisition system to use a unified tiled-area approach instead of manual per-tile loops. The tile acquisition plugin now delegates to Sequence DiagramsequenceDiagram
participant Plugin as TileAcqPlugin
participant Acq as acquireTiledArea
participant Task as TiledAcquisitionTask
participant Registrar as Registrar/Weaver
participant AcqMgr as AcquisitionTask.adjust_metadata
participant Result as StitchedResult
Plugin->>Plugin: compute region from start_pos, FoV, overlap
Plugin->>Plugin: run memory/time estimates (weaver/registrar aware)
Plugin->>Acq: call acquireTiledArea(region, streams, overlay_stream, sfov, weaver, registrar)
Acq->>Task: instantiate TiledAcquisitionTask(with overlay_stream, sfov, weaver, registrar)
Task->>Task: acquire tiles across region (preserve order)
Task->>AcqMgr: call adjust_metadata(raw_images) if overlay_stream present
Task->>Registrar: register/weave tiles (if registrar/weaver configured)
Task->>Result: produce stitched DataArray
Acq->>Plugin: return stitched DataArray
Plugin->>Plugin: export/display result, restore external hardware state
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/odemis/acq/stitching/_tiledacq.py (1)
737-765: Critical: Data structure mismatch will causeadjust_metadatato fail.
AcquisitionTask.adjust_metadataexpectsraw_datato be adict[Stream -> list of DataArray], but this code stores singleDataArrayobjects:
- Line 747:
raw_images[self._overlay_stream] = dastores a single DA- Line 761:
raw_images[stream] = dastores a single DAAdditionally,
OverlayStreamproduces two DataArrays (optical and SEM correction metadata), but_acquireStreamTileonly returnsdas[0]. Theadjust_metadatamethod accesses bothdata[0].metadataanddata[1].metadatafor OverlayStream.🐛 Proposed fix
def _getTileDAs(self, i, ix, iy): """ Iterate over each tile stream and construct their data arrays list :return: list(DataArray) list of each stream DataArray """ # Keep order so that the DataArrays are returned in the order they were # acquired. Not absolutely needed, but nice for the user in some cases. raw_images = OrderedDict() # stream -> list of raw images if self._overlay_stream: - da = self._acquireStreamTile(i, ix, iy, self._overlay_stream) - raw_images[self._overlay_stream] = da + # OverlayStream returns multiple DAs (optical + SEM correction), acquire all of them + self._future.running_subf = acqmng.acquire([self._overlay_stream], self._settings_obs, adjust_md=False) + das, e = self._future.running_subf.result() + if e: + logging.warning(f"Acquisition for tile {ix}x{iy}, overlay stream partially failed: {e}") + if self._future._task_state == CANCELLED: + raise CancelledError() + raw_images[self._overlay_stream] = das # list of DAs for stream in self._streams: if stream.focuser is not None and len(self._zlevels) > 1: # Acquire zstack images based on the given zlevels, and compress them into a single da da = self._acquireStreamCompressedZStack(i, ix, iy, stream) elif stream.focuser and len(self._zlevels) == 1: z = self._zlevels[0] logging.debug(f"Moving focus for tile {ix}x{iy} to {z}.") stream.focuser.moveAbsSync({'z': z}) # Acquire a single image of the stream da = self._acquireStreamTile(i, ix, iy, stream) else: # Acquire a single image of the stream da = self._acquireStreamTile(i, ix, iy, stream) - raw_images[stream] = da + raw_images[stream] = [da] # wrap in list for adjust_metadata compatibility AcquisitionTask.adjust_metadata(raw_images) - return list(raw_images.values()) + # Flatten the lists back to a single list of DAs (excluding overlay which is removed by adjust_metadata) + result = [] + for das in raw_images.values(): + result.extend(das) + return result
🧹 Nitpick comments (1)
src/odemis/acq/acqmng.py (1)
623-623: Consider usingitertools.chain.from_iterablefor better performance.The
sum(raw_images.values(), [])pattern has O(n²) complexity for list concatenation. For large acquisitions with many streams, this could become a bottleneck.♻️ Proposed fix
Add import at the top of the file:
from itertools import chainThen replace line 623:
- ret = sum(raw_images.values(), []) + ret = list(chain.from_iterable(raw_images.values()))
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
plugins/tileacq.pysrc/odemis/acq/acqmng.pysrc/odemis/acq/stitching/_tiledacq.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Always use type hints for function parameters and return types in Python code
Include docstrings for all functions and classes, following the reStructuredText style guide (without type information)
Ensure code is valid for Python 3.10 and above
Clean up code at the end of a task using autopep8 with the command:autopep8 --in-place --select W291,W292,W293,W391
Files:
src/odemis/acq/acqmng.pysrc/odemis/acq/stitching/_tiledacq.pyplugins/tileacq.py
🧠 Learnings (2)
📚 Learning: 2026-01-12T12:37:35.155Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:35.155Z
Learning: Maintain the debugging pattern of importing odemis.gui.conf and exporting TIFF files within acquisition/localization loops across all odemis Python sources. Do not remove or restructure this behavior in refactors if it serves debugging consistency; document the debugging purpose with comments and ensure the behavior remains consistent across modules (e.g., acquisition/localization loops such as src/odemis/acq/align/z_localization.py).
Applied to files:
src/odemis/acq/acqmng.pysrc/odemis/acq/stitching/_tiledacq.py
📚 Learning: 2026-01-12T12:37:40.187Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:40.187Z
Learning: In the odemis codebase, the pattern of importing `odemis.gui.conf` and exporting TIFF files during acquisition/localization loops (e.g., in `src/odemis/acq/align/z_localization.py`) is intentionally used in multiple places across the codebase for debugging purposes and should be maintained for consistency.
Applied to files:
plugins/tileacq.py
🧬 Code graph analysis (2)
src/odemis/acq/stitching/_tiledacq.py (1)
src/odemis/acq/acqmng.py (2)
acquire(56-86)adjust_metadata(627-661)
plugins/tileacq.py (1)
src/odemis/acq/stitching/_tiledacq.py (4)
acquireTiledArea(1084-1114)estimateTiledAcquisitionTime(1062-1070)TiledAcquisitionTask(103-1059)guessSmallestFov(300-311)
🪛 Ruff (0.14.11)
src/odemis/acq/acqmng.py
623-623: Avoid quadratic list summation
Replace with functools.reduce
(RUF017)
plugins/tileacq.py
598-598: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build (ubuntu-22.04)
- GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (17)
src/odemis/acq/acqmng.py (3)
56-79: LGTM! The newadjust_mdparameter is well-documented and correctly wired through toAcquisitionTask. This allows tiled acquisition to defer metadata adjustment until after all streams for a tile are acquired.
487-490: LGTM! Constructor correctly stores the newadjust_mdparameter.
618-627: LGTM! The conditional metadata adjustment and promotion to@staticmethodenables reuse from_tiledacq.py. The public API change from_adjust_metadata(private) toadjust_metadata(public static) is intentional per the summary.src/odemis/acq/stitching/_tiledacq.py (6)
108-140: LGTM! Theoverlay_streamparameter is well-documented and correctly stored for later use in metadata adjustment.
282-311: LGTM! PromotinggetFovandguessSmallestFovto static/class methods enables reuse from the plugin without instantiatingTiledAcquisitionTask.
588-609: LGTM! Memory estimation is correctly skipped when no stitching is configured (registrar/weaver are None).
631-658: LGTM! Time estimation correctly accounts for overlay stream and conditional stitching.
1033-1040: LGTM! Conditional stitching based on registrar/weaver presence aligns with the new workflow where stitching can be disabled.
1084-1103: LGTM! Theoverlay_streamparameter is correctly propagated toTiledAcquisitionTask.plugins/tileacq.py (8)
138-168: LGTM! Good addition of weaving method choices with sensible role-based defaults. The SECOM/DELPHI systems benefit fromWEAVER_COLLAGE_REVERSEto handle carbon decomposition effects, while SPARC benefits fromWEAVER_MEANfor smoother transitions.
250-254: LGTM! Correctly associatesWEAVER_COLLAGEwithREGISTER_IDENTITYsince collage weaving doesn't require registration alignment.
265-304: LGTM! Refactored to useestimateTiledAcquisitionTimewhich provides consistent time estimation with the actual acquisition path.
377-377: LGTM! Correctly uses the promoted class methodTiledAcquisitionTask.guessSmallestFovfor FoV calculation.
513-536: LGTM! Renamed from_get_acq_streamsto_get_stitch_streamsfor clarity, reflecting that these are the streams used for stitching (excluding overlay stream).
538-576: LGTM! Memory check refactored to useestimateTiledAcquisitionMemoryfor consistency with the actual acquisition path.
587-613: LGTM! New_get_regionmethod with proper type hints calculates the acquisition bounding box. The logic correctly computes the region based on starting position and total area.
615-696: LGTM! Theacquiremethod is well-refactored to delegate toacquireTiledArea. The hardware VA restoration in thefinallyblock ensures clean state recovery on error or cancellation.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
2304062 to
9446db8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/odemis/acq/acqmng.py (1)
623-659: Add type hints and update docstring to reStructuredText style inadjust_metadata.This function needs type hints and docstring alignment per the coding guidelines. The function currently lacks type annotations and uses the old docstring format with inline type information.
✅ Proposed update
-from typing import Set, Dict +from typing import Set, Dict, List, Optional, Any @@ - `@staticmethod` - def adjust_metadata(raw_data): + `@staticmethod` + def adjust_metadata(raw_data: Dict[Any, List[model.DataArray]]) -> None: """ Update/adjust the metadata of the raw data received based on global information. - raw_data (dict Stream -> list of DataArray): the raw data for each stream. - The raw data is directly updated, and even removed if necessary. + :param raw_data: Raw data for each stream. Updated in place; entries may be removed. """src/odemis/gui/plugin/__init__.py (1)
508-540: Add type hints and reformat docstring to follow coding guidelines.The method requires type hints for all parameters and a return type, with the docstring converted to reStructuredText format (without embedded type information). Update the import to include
OptionalandAny, and reformat the parameter documentation using:paramand:return:tags.Proposed update
-from typing import Callable +from typing import Any, Callable, Optional @@ - def addStream(self, stream, index=0, sp_options=None): + def addStream(self, stream: Optional[Any], index: Optional[int] = 0, + sp_options: Optional[int] = None) -> None: """ Adds a stream to the viewport, and a stream entry to the stream panel. It also ensures the panel box and viewport are shown. Note: If this method is not called, the stream panel and viewports are hidden. - stream (Stream or None): Stream to be added. Use None to force a viewport - to be seen without adding a stream. - index (0, 1, 2, or None): Index of the viewport to add the stream. 0 = left, - 1 = right, 2 = spectrum viewport. If None, it will not show the stream - on any viewport (and it will be added to the .hidden_view) - sp_options: (int or None) combination of OPT_* values for the StreamPanel or None for default. + :param stream: Stream to be added. Use None to force a viewport to be seen without adding a stream. + :param index: Index of the viewport to add the stream. 0 = left, 1 = right, 2 = spectrum viewport. + If None, the stream is not shown on any viewport (it is added to .hidden_view). + :param sp_options: Combination of OPT_* values for the StreamPanel, or None for default. + :return: None. """Use
Optional[Any]rather thanOptional[object]for the stream parameter (more idiomatic in Python 3.10+).src/odemis/acq/stitching/_tiledacq.py (2)
752-780: Fixraw_imagesshape foradjust_metadata(current code breaks overlay metadata).
AcquisitionTask.adjust_metadataexpectsdict[Stream, list[DataArray]]. Right now it receivesDataArray, so it iterates into pixel rows and fails to find.metadata. Also, the overlay stream loses its second DataArray because_acquireStreamTilereturns only the first image. This will break fine-alignment metadata propagation.🐛 Proposed fix (keep lists + preserve overlay data)
- raw_images = OrderedDict() # stream -> list of raw images - if self._overlay_stream: - da = self._acquireStreamTile(i, ix, iy, self._overlay_stream) - raw_images[self._overlay_stream] = da + raw_images = OrderedDict() # stream -> list of raw images + if self._overlay_stream: + self._future.running_subf = acqmng.acquire([self._overlay_stream], self._settings_obs) + overlay_das, e = self._future.running_subf.result() + if e: + logging.warning("Overlay acquisition partially failed: %s", e) + if self._future._task_state == CANCELLED: + raise CancelledError() + raw_images[self._overlay_stream] = list(overlay_das) for stream in self._streams: if stream.focuser is not None and len(self._zlevels) > 1: # Acquire zstack images based on the given zlevels, and compress them into a single da da = self._acquireStreamCompressedZStack(i, ix, iy, stream) elif stream.focuser and len(self._zlevels) == 1: z = self._zlevels[0] logging.debug(f"Moving focus for tile {ix}x{iy} to {z}.") stream.focuser.moveAbsSync({'z': z}) # Acquire a single image of the stream da = self._acquireStreamTile(i, ix, iy, stream) else: # Acquire a single image of the stream da = self._acquireStreamTile(i, ix, iy, stream) - raw_images[stream] = da + raw_images[stream] = [da] AcquisitionTask.adjust_metadata(raw_images) - return list(raw_images.values()) + # return single DA per stream (overlay already removed by adjust_metadata) + return [das[0] if len(das) == 1 else das for das in raw_images.values()]
288-317: Add type hints togetFovandguessSmallestFov.Both methods must include type hints for parameters and return values per the project's Python typing requirements. For example:
getFov(sd: Union[model.DataArray, Stream]) -> Tuple[float, float]guessSmallestFov(cls, ss: Iterable[Stream]) -> Tuple[float, float]src/odemis/gui/cont/stream_bar.py (1)
485-575: Add type hints to_add_streamand_add_stream_contmethods.Both methods lack type hints on their parameters and return types. Per coding guidelines, all Python function parameters and return types must be explicitly typed. This is particularly important for the new
sp_optionsparameter to maintain API clarity.For
_add_stream: Add types forstream,add_to_view,visible,play,stream_cont_cls,sp_optionsparameters, and the return type (should returnStreamController | Stream).For
_add_stream_cont: Add types forstream,show_panel,locked,static,view,cls,sp_optionsparameters, and the return type (StreamController).
🤖 Fix all issues with AI agents
In `@plugins/tileacq.py`:
- Around line 251-256: Add explicit type annotations: annotate
_on_weaver_change(weaver) with a parameter type and return type (e.g., def
_on_weaver_change(self, weaver: Any) -> None:) and annotate _get_stitch_streams
with the concrete return type described in its docstring (for example def
_get_stitch_streams(self, ...) -> Sequence[Tuple[int, int]] or the precise
Sequence/Iterable/List of stream tuples the docstring documents). Import
required typing names (Any, Sequence, Tuple, etc.) at the top and ensure both
functions have the matching types consistent with their docstrings.
- Around line 592-614: The region is too large because totalArea.value (which
includes overlap) is being used directly to compute xmax/ymin while the FoV used
to calculate tiles should be the reliable (non-overlap) footprint; in
_get_region replace using self.totalArea.value[...] directly with a width/height
scaled to the reliable FoV (compute reliable_fov = (1-overlap_frac)*sfov as
already done, then width = self.totalArea.value[0] * (reliable_fov[0]/sfov[0])
and height = self.totalArea.value[1] * (reliable_fov[1]/sfov[1]) and use those
for xmax/xmin/ymin/ymax) so that TiledAcquisitionTask and its nx/ny calculation
produce the expected tile counts.
In `@src/odemis/gui/cont/stream.py`:
- Around line 69-78: Add missing type annotations to StreamController.__init__:
annotate all parameters (stream_bar, stream, tab_data_model, show_panel, view,
sb_ctrl, sp_options) and the return type as None. Use concrete types where
available (e.g., view: MicroscopeView | None, sb_ctrl: StreamBarController |
None) and annotate sp_options as int | None (or Optional[int]) per guidelines;
if some types are defined elsewhere, import or use forward references (quoted
names) to avoid circular imports. Ensure the signature in
src/odemis/gui/cont/stream.py for def __init__ includes these annotations and
keeps the existing docstring unchanged.
🧹 Nitpick comments (2)
src/odemis/acq/acqmng.py (1)
616-621: Avoid quadratic list concatenation forret.
sum(raw_images.values(), [])grows cost with each append; use a linear concat to reduce overhead for large acquisitions.♻️ Proposed fix
+import itertools @@ - ret = sum(raw_images.values(), []) + ret = list(itertools.chain.from_iterable(raw_images.values()))plugins/tileacq.py (1)
31-32: Avoid privateconcurrent.futures._baseimport.
Use the publicconcurrent.futures.CancelledErrorto prevent stdlib API breakage.🔧 Suggested change
-from concurrent.futures._base import CancelledError +from concurrent.futures import CancelledError
| def _on_weaver_change(self, weaver): | ||
| if weaver == WEAVER_COLLAGE: | ||
| self.register = REGISTER_IDENTITY | ||
| else: | ||
| self.register = REGISTER_GLOBAL_SHIFT | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and its size
wc -l plugins/tileacq.pyRepository: delmic/odemis
Length of output: 79
🏁 Script executed:
# View the first method around lines 251-256
sed -n '245,260p' plugins/tileacq.pyRepository: delmic/odemis
Length of output: 629
🏁 Script executed:
# View the second method around lines 517-540
sed -n '510,545p' plugins/tileacq.pyRepository: delmic/odemis
Length of output: 1353
Add type hints to new helper methods.
Both _on_weaver_change (line 251) and _get_stitch_streams (line 517) are missing type annotations. Per coding guidelines, add parameter and return type hints:
_on_weaver_change: typeweaverparameter and add return type_get_stitch_streams: add return type annotation (currently documented in docstring only)
🤖 Prompt for AI Agents
In `@plugins/tileacq.py` around lines 251 - 256, Add explicit type annotations:
annotate _on_weaver_change(weaver) with a parameter type and return type (e.g.,
def _on_weaver_change(self, weaver: Any) -> None:) and annotate
_get_stitch_streams with the concrete return type described in its docstring
(for example def _get_stitch_streams(self, ...) -> Sequence[Tuple[int, int]] or
the precise Sequence/Iterable/List of stream tuples the docstring documents).
Import required typing names (Any, Sequence, Tuple, etc.) at the top and ensure
both functions have the matching types consistent with their docstrings.
| def _get_region(self, start_pos: dict) -> Tuple[float, float, float, float]: | ||
| """ | ||
| Calculate the acquisition region. | ||
| ss, stitch_ss = self._get_acq_streams() | ||
| :param start_pos: dict with 'x' and 'y' keys for the starting position, | ||
| which is the center of the first tile. | ||
| :return: (xmin, ymin, xmax, ymax) defining the acquisition region in meters | ||
| """ | ||
| sfov = self._guess_smallest_fov() | ||
|
|
||
| da_list = [] # for each position, a list of DataArrays | ||
| i = 0 | ||
| prev_idx = [0, 0] | ||
| overlap_frac = self.overlap.value / 100.0 | ||
|
|
||
| orig_hw_values: Dict[model.VigilantAttribute, Any] = {} # VA -> value | ||
| try: | ||
| # force external to all streams with emitters | ||
| for s in ss: | ||
| if (s.emitter | ||
| and model.hasVA(s.emitter, "external") | ||
| and s.emitter.external.value is None | ||
| ): | ||
| orig_hw_values[s.emitter.external] = s.emitter.external.value | ||
| s.emitter.external.value = True | ||
| # Reliable FoV | ||
| # The size of the smallest tile, non-including the overlap, which will be | ||
| # lost (and also indirectly represents the precision of the stage) | ||
| reliable_fov = ((1 - overlap_frac) * sfov[0], (1 - overlap_frac) * sfov[1]) | ||
|
|
||
| for ix, iy in self._generate_scanning_indices(trep): | ||
| logging.debug("Acquiring tile %dx%d", ix, iy) | ||
| self._move_to_tile((ix, iy), orig_pos, sfov, prev_idx) | ||
| prev_idx = ix, iy | ||
| # Update the progress bar | ||
| ft.set_progress(end=self.estimate_time(nb - i) + time.time()) | ||
|
|
||
| ft.running_subf = acqmng.acquire(ss, self.main_app.main_data.settings_obs) | ||
| das, e = ft.running_subf.result() # blocks until all the acquisitions are finished | ||
| if e: | ||
| logging.warning("Acquisition for tile %dx%d partially failed: %s", | ||
| ix, iy, e) | ||
|
|
||
| if ft._task_state == CANCELLED: | ||
| raise CancelledError() | ||
|
|
||
| # TODO: do in a separate thread | ||
| fn_tile = "%s-%.5dx%.5d%s" % (fn_bs, ix, iy, fn_ext) | ||
| logging.debug("Will save data of tile %dx%d to %s", ix, iy, fn_tile) | ||
| exporter.export(fn_tile, das) | ||
|
|
||
| if ft._task_state == CANCELLED: | ||
| raise CancelledError() | ||
|
|
||
| if self.stitch.value: | ||
| # Sort tiles (typically, largest SEM on first position) | ||
| # Make it a tuple to indicate to the stitching that all the streams should be tied in position. | ||
| da_list.append(tuple(self.sort_das(das, stitch_ss))) | ||
|
|
||
| # Check the FoV is correct using the data, and if not update | ||
| if i == 0: | ||
| sfov = self._check_fov(das, sfov) | ||
| i += 1 | ||
| finally: | ||
| # reset all external values | ||
| for va, value in orig_hw_values.items(): | ||
| try: | ||
| va.value = value | ||
| except Exception: | ||
| logging.exception("Failed to restore VA %s to %s", va, value) | ||
| xmin = start_pos["x"] - reliable_fov[0] / 2 | ||
| ymax = start_pos["y"] + reliable_fov[1] / 2 | ||
| xmax = xmin + self.totalArea.value[0] | ||
| ymin = ymax - self.totalArea.value[1] | ||
|
|
||
| return da_list, fn_tile | ||
| return (xmin, ymin, xmax, ymax) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Region size is inflated, leading to extra tiles.
totalArea already accounts for overlap; passing it to TiledAcquisitionTask causes nx/ny to round up and over-acquire. Use the reliable FoV footprint ((1-overlap) * sfov) for width/height so tile counts match nx/ny.
🧭 Proposed fix
- xmax = xmin + self.totalArea.value[0]
- ymin = ymax - self.totalArea.value[1]
+ width = reliable_fov[0] * self.nx.value
+ height = reliable_fov[1] * self.ny.value
+ xmax = xmin + width
+ ymin = ymax - height📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _get_region(self, start_pos: dict) -> Tuple[float, float, float, float]: | |
| """ | |
| Calculate the acquisition region. | |
| ss, stitch_ss = self._get_acq_streams() | |
| :param start_pos: dict with 'x' and 'y' keys for the starting position, | |
| which is the center of the first tile. | |
| :return: (xmin, ymin, xmax, ymax) defining the acquisition region in meters | |
| """ | |
| sfov = self._guess_smallest_fov() | |
| da_list = [] # for each position, a list of DataArrays | |
| i = 0 | |
| prev_idx = [0, 0] | |
| overlap_frac = self.overlap.value / 100.0 | |
| orig_hw_values: Dict[model.VigilantAttribute, Any] = {} # VA -> value | |
| try: | |
| # force external to all streams with emitters | |
| for s in ss: | |
| if (s.emitter | |
| and model.hasVA(s.emitter, "external") | |
| and s.emitter.external.value is None | |
| ): | |
| orig_hw_values[s.emitter.external] = s.emitter.external.value | |
| s.emitter.external.value = True | |
| # Reliable FoV | |
| # The size of the smallest tile, non-including the overlap, which will be | |
| # lost (and also indirectly represents the precision of the stage) | |
| reliable_fov = ((1 - overlap_frac) * sfov[0], (1 - overlap_frac) * sfov[1]) | |
| for ix, iy in self._generate_scanning_indices(trep): | |
| logging.debug("Acquiring tile %dx%d", ix, iy) | |
| self._move_to_tile((ix, iy), orig_pos, sfov, prev_idx) | |
| prev_idx = ix, iy | |
| # Update the progress bar | |
| ft.set_progress(end=self.estimate_time(nb - i) + time.time()) | |
| ft.running_subf = acqmng.acquire(ss, self.main_app.main_data.settings_obs) | |
| das, e = ft.running_subf.result() # blocks until all the acquisitions are finished | |
| if e: | |
| logging.warning("Acquisition for tile %dx%d partially failed: %s", | |
| ix, iy, e) | |
| if ft._task_state == CANCELLED: | |
| raise CancelledError() | |
| # TODO: do in a separate thread | |
| fn_tile = "%s-%.5dx%.5d%s" % (fn_bs, ix, iy, fn_ext) | |
| logging.debug("Will save data of tile %dx%d to %s", ix, iy, fn_tile) | |
| exporter.export(fn_tile, das) | |
| if ft._task_state == CANCELLED: | |
| raise CancelledError() | |
| if self.stitch.value: | |
| # Sort tiles (typically, largest SEM on first position) | |
| # Make it a tuple to indicate to the stitching that all the streams should be tied in position. | |
| da_list.append(tuple(self.sort_das(das, stitch_ss))) | |
| # Check the FoV is correct using the data, and if not update | |
| if i == 0: | |
| sfov = self._check_fov(das, sfov) | |
| i += 1 | |
| finally: | |
| # reset all external values | |
| for va, value in orig_hw_values.items(): | |
| try: | |
| va.value = value | |
| except Exception: | |
| logging.exception("Failed to restore VA %s to %s", va, value) | |
| xmin = start_pos["x"] - reliable_fov[0] / 2 | |
| ymax = start_pos["y"] + reliable_fov[1] / 2 | |
| xmax = xmin + self.totalArea.value[0] | |
| ymin = ymax - self.totalArea.value[1] | |
| return da_list, fn_tile | |
| return (xmin, ymin, xmax, ymax) | |
| def _get_region(self, start_pos: dict) -> Tuple[float, float, float, float]: | |
| """ | |
| Calculate the acquisition region. | |
| :param start_pos: dict with 'x' and 'y' keys for the starting position, | |
| which is the center of the first tile. | |
| :return: (xmin, ymin, xmax, ymax) defining the acquisition region in meters | |
| """ | |
| sfov = self._guess_smallest_fov() | |
| overlap_frac = self.overlap.value / 100.0 | |
| # Reliable FoV | |
| # The size of the smallest tile, non-including the overlap, which will be | |
| # lost (and also indirectly represents the precision of the stage) | |
| reliable_fov = ((1 - overlap_frac) * sfov[0], (1 - overlap_frac) * sfov[1]) | |
| xmin = start_pos["x"] - reliable_fov[0] / 2 | |
| ymax = start_pos["y"] + reliable_fov[1] / 2 | |
| width = reliable_fov[0] * self.nx.value | |
| height = reliable_fov[1] * self.ny.value | |
| xmax = xmin + width | |
| ymin = ymax - height | |
| return (xmin, ymin, xmax, ymax) |
🤖 Prompt for AI Agents
In `@plugins/tileacq.py` around lines 592 - 614, The region is too large because
totalArea.value (which includes overlap) is being used directly to compute
xmax/ymin while the FoV used to calculate tiles should be the reliable
(non-overlap) footprint; in _get_region replace using self.totalArea.value[...]
directly with a width/height scaled to the reliable FoV (compute reliable_fov =
(1-overlap_frac)*sfov as already done, then width = self.totalArea.value[0] *
(reliable_fov[0]/sfov[0]) and height = self.totalArea.value[1] *
(reliable_fov[1]/sfov[1]) and use those for xmax/xmin/ymin/ymax) so that
TiledAcquisitionTask and its nx/ny calculation produce the expected tile counts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nandishjpatel can you check this comment, I think it makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| def __init__(self, stream_bar, stream, tab_data_model, show_panel=True, view=None, | ||
| sb_ctrl=None): | ||
| sb_ctrl=None, sp_options=None): | ||
| """ | ||
| view (MicroscopeView or None): Link stream to a view. If view is None, the stream | ||
| will be linked to the focused view. Passing a view to the controller ensures | ||
| that the visibility button functions correctly when multiple views are present. | ||
| sb_ctrl (StreamBarController or None): the StreamBarController which (typically) | ||
| created this StreamController. Only needed for ROA repetition display. | ||
| sp_options: (int or None) combination of OPT_* values for the StreamPanel or None for default. | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
cat -n src/odemis/gui/cont/stream.py | head -100Repository: delmic/odemis
Length of output: 4996
🏁 Script executed:
sed -n '69,200p' src/odemis/gui/cont/stream.pyRepository: delmic/odemis
Length of output: 5847
🏁 Script executed:
grep -n "def " src/odemis/gui/cont/stream.py | head -20Repository: delmic/odemis
Length of output: 940
🏁 Script executed:
sed -n '290,335p' src/odemis/gui/cont/stream.pyRepository: delmic/odemis
Length of output: 1912
🏁 Script executed:
grep -n ": " src/odemis/gui/cont/stream.py | grep "def " | head -10Repository: delmic/odemis
Length of output: 39
🏁 Script executed:
rg "->|: int|: str|: bool|: \w+\[" src/odemis/gui/cont/stream.py | head -20Repository: delmic/odemis
Length of output: 461
🏁 Script executed:
rg '\-\>|: [a-zA-Z]' src/odemis/gui/cont/stream.py | head -20Repository: delmic/odemis
Length of output: 140
🏁 Script executed:
rg 'def .* ->' src/odemis/gui/cont/stream.pyRepository: delmic/odemis
Length of output: 39
🏁 Script executed:
fd "\.py$" src/odemis/gui/cont | head -10Repository: delmic/odemis
Length of output: 512
🏁 Script executed:
rg 'def .+\(.*\):' src/odemis/gui/cont/acquisition/snapshot.py | head -5Repository: delmic/odemis
Length of output: 274
🏁 Script executed:
sed -n '1,100p' src/odemis/gui/cont/acquisition/snapshot.pyRepository: delmic/odemis
Length of output: 3626
🏁 Script executed:
sed -n '69,150p' src/odemis/gui/cont/stream.pyRepository: delmic/odemis
Length of output: 3539
🏁 Script executed:
sed -n '1,60p' src/odemis/gui/cont/stream.py | grep -E "import|from"Repository: delmic/odemis
Length of output: 1242
🏁 Script executed:
rg "class StreamBarController" src/Repository: delmic/odemis
Length of output: 125
🏁 Script executed:
grep -A 5 "class MicroscopeView" src/odemis/gui/util.py | head -10Repository: delmic/odemis
Length of output: 112
🏁 Script executed:
rg "class MicroscopeView" src/Repository: delmic/odemis
Length of output: 381
Add type hints to StreamController.__init__.
This method lacks type hints for all parameters and return type. Per coding guidelines, type hints are mandatory for Python files. The sp_options parameter should be annotated as int | None (or Optional[int]).
🤖 Prompt for AI Agents
In `@src/odemis/gui/cont/stream.py` around lines 69 - 78, Add missing type
annotations to StreamController.__init__: annotate all parameters (stream_bar,
stream, tab_data_model, show_panel, view, sb_ctrl, sp_options) and the return
type as None. Use concrete types where available (e.g., view: MicroscopeView |
None, sb_ctrl: StreamBarController | None) and annotate sp_options as int | None
(or Optional[int]) per guidelines; if some types are defined elsewhere, import
or use forward references (quoted names) to avoid circular imports. Ensure the
signature in src/odemis/gui/cont/stream.py for def __init__ includes these
annotations and keeps the existing docstring unchanged.
| "Collage: Pastes tiles at their center positions, assuming uniform pixel" | ||
| " sizes and ignoring rotation/skew.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could make it a bit more clear that newer tiles are shown on top of older tiles, something like this (feel free to adjust if you have a better idea):
| "Collage: Pastes tiles at their center positions, assuming uniform pixel" | |
| " sizes and ignoring rotation/skew.\n" | |
| "Collage: Shows tiles at their center positions, assuming uniform pixel" | |
| " sizes and ignoring rotation/skew, new tiles are shown on top of the previous tile.\n" |
| WEAVER_COLLAGE_REVERSE: "Collage (reverse order)", | ||
| } | ||
| self.weaver = model.VAEnumerated(WEAVER_MEAN, choices=weaver_choices) | ||
| self.register = REGISTER_GLOBAL_SHIFT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why use GLOBAL_SHIFT as the default?
| if weaver == WEAVER_COLLAGE: | ||
| self.register = REGISTER_IDENTITY | ||
| else: | ||
| self.register = REGISTER_GLOBAL_SHIFT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment to explain why you do this? And why do you do REGISTER_IDENTITY only for WEAVER_COLLAGE and not WEAVER_COLLAGE_REVERSE?
| sfov=self._guess_smallest_fov(), | ||
| ) | ||
| except (ValueError, AttributeError): | ||
| # No streams or cannot compute FoV |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it make sense to log something here?
| ) | ||
| except (ValueError, AttributeError): | ||
| # No streams or cannot compute FoV | ||
| tat = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this really a sensible default?
| self.nx = model.IntContinuous(5, (1, 1000), setter=self._set_nx) | ||
| self.ny = model.IntContinuous(5, (1, 1000), setter=self._set_ny) | ||
| self.overlap = model.FloatContinuous(20, (1, 80), unit="%") | ||
| self.overlap = model.FloatContinuous(20, (0, 80), unit="%") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you have to divide the overlap by 100 in a few places, would it make sense to already divide by 100 here?
| def _get_region(self, start_pos: dict) -> Tuple[float, float, float, float]: | ||
| """ | ||
| Calculate the acquisition region. | ||
| ss, stitch_ss = self._get_acq_streams() | ||
| :param start_pos: dict with 'x' and 'y' keys for the starting position, | ||
| which is the center of the first tile. | ||
| :return: (xmin, ymin, xmax, ymax) defining the acquisition region in meters | ||
| """ | ||
| sfov = self._guess_smallest_fov() | ||
|
|
||
| da_list = [] # for each position, a list of DataArrays | ||
| i = 0 | ||
| prev_idx = [0, 0] | ||
| overlap_frac = self.overlap.value / 100.0 | ||
|
|
||
| orig_hw_values: Dict[model.VigilantAttribute, Any] = {} # VA -> value | ||
| try: | ||
| # force external to all streams with emitters | ||
| for s in ss: | ||
| if (s.emitter | ||
| and model.hasVA(s.emitter, "external") | ||
| and s.emitter.external.value is None | ||
| ): | ||
| orig_hw_values[s.emitter.external] = s.emitter.external.value | ||
| s.emitter.external.value = True | ||
| # Reliable FoV | ||
| # The size of the smallest tile, non-including the overlap, which will be | ||
| # lost (and also indirectly represents the precision of the stage) | ||
| reliable_fov = ((1 - overlap_frac) * sfov[0], (1 - overlap_frac) * sfov[1]) | ||
|
|
||
| for ix, iy in self._generate_scanning_indices(trep): | ||
| logging.debug("Acquiring tile %dx%d", ix, iy) | ||
| self._move_to_tile((ix, iy), orig_pos, sfov, prev_idx) | ||
| prev_idx = ix, iy | ||
| # Update the progress bar | ||
| ft.set_progress(end=self.estimate_time(nb - i) + time.time()) | ||
|
|
||
| ft.running_subf = acqmng.acquire(ss, self.main_app.main_data.settings_obs) | ||
| das, e = ft.running_subf.result() # blocks until all the acquisitions are finished | ||
| if e: | ||
| logging.warning("Acquisition for tile %dx%d partially failed: %s", | ||
| ix, iy, e) | ||
|
|
||
| if ft._task_state == CANCELLED: | ||
| raise CancelledError() | ||
|
|
||
| # TODO: do in a separate thread | ||
| fn_tile = "%s-%.5dx%.5d%s" % (fn_bs, ix, iy, fn_ext) | ||
| logging.debug("Will save data of tile %dx%d to %s", ix, iy, fn_tile) | ||
| exporter.export(fn_tile, das) | ||
|
|
||
| if ft._task_state == CANCELLED: | ||
| raise CancelledError() | ||
|
|
||
| if self.stitch.value: | ||
| # Sort tiles (typically, largest SEM on first position) | ||
| # Make it a tuple to indicate to the stitching that all the streams should be tied in position. | ||
| da_list.append(tuple(self.sort_das(das, stitch_ss))) | ||
|
|
||
| # Check the FoV is correct using the data, and if not update | ||
| if i == 0: | ||
| sfov = self._check_fov(das, sfov) | ||
| i += 1 | ||
| finally: | ||
| # reset all external values | ||
| for va, value in orig_hw_values.items(): | ||
| try: | ||
| va.value = value | ||
| except Exception: | ||
| logging.exception("Failed to restore VA %s to %s", va, value) | ||
| xmin = start_pos["x"] - reliable_fov[0] / 2 | ||
| ymax = start_pos["y"] + reliable_fov[1] / 2 | ||
| xmax = xmin + self.totalArea.value[0] | ||
| ymin = ymax - self.totalArea.value[1] | ||
|
|
||
| return da_list, fn_tile | ||
| return (xmin, ymin, xmax, ymax) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nandishjpatel can you check this comment, I think it makes sense
| overlay_stream = None | ||
| overlap_frac = self.overlap.value / 100 | ||
| stitch_ss = self._get_stitch_streams() | ||
| ss += stitch_ss |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not user ss.append(stich_ss) here?
| If focus_points is defined, zlevels is adjusted relative to the focus_points. | ||
| :param registrar: (REGISTER_*) type of registration method | ||
| :param weaver: (WEAVER_*) type of weaving method | ||
| :param registrar: (REGISTER_* or None) type of registration method. If registrar and weaver are None, do not stitch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you mean with "do not stitch"? Is that different than the identity registration?
JIRA Tickets: