feat: support custom rectangular sample formats with per-axis spacing#522
Open
hongquanli wants to merge 14 commits intomasterfrom
Open
feat: support custom rectangular sample formats with per-axis spacing#522hongquanli wants to merge 14 commits intomasterfrom
hongquanli wants to merge 14 commits intomasterfrom
Conversation
Add well_size_x_mm, well_size_y_mm, well_spacing_x_mm, well_spacing_y_mm, and well_shape columns. Backward-compat shim reads old CSV format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_effective_well_size() and calculate_well_coverage() now accept per-axis well dimensions. Round wells unchanged, rectangular wells return (x, y) tuples or use rectangular bounds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Position calculations now use well_spacing_x_mm for columns and well_spacing_y_mm for rows. Covers both GUI well selection and TCP/script-driven acquisition paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace single well_size_mm/well_spacing_mm attributes with per-axis variants (well_size_x_mm, well_size_y_mm, well_spacing_x_mm, well_spacing_y_mm) and add well_shape. Update update_wellplate_settings() signature to accept 13 params matching the updated signal and scan_coordinates.py interface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show entry_scan_size_y when well is rectangular with different X/Y dimensions. add_region() and set_well_coordinates() accept per-axis scan sizes. Coverage calculation updated accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_cmd_parse_wells() now uses well_spacing_x_mm for columns and well_spacing_y_mm for rows. Backward compat for old API clients. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signal now emits 13 params including well_size_x/y_mm, well_spacing_x/y_mm, and well_shape. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
onDoubleClick uses spacing_x_mm for columns, spacing_y_mm for rows. Well1536SelectionWidget also updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add X/Y spacing inputs, well shape selector, and 2-diagonal-corners calibration mode for rectangular wells. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add "square" option to well shape dropdown - Hide "3 Edge Points" for square/rectangular wells - Hide "2 Diagonal Corners" for circular wells - Auto-select appropriate calibration method on shape change - Use == "circular" checks instead of == "rectangular" throughout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After Accepted: read the combo box selection set by the dialog (new format or recalibrated existing format) and emit settings. After Rejected: revert to the format that was selected before opening the dialog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix TCP/script path: pass scan_size_y_mm through get_scan_coordinates_from_selected_wells and create_region_coordinates for rectangular wells with different X/Y dimensions - Add tests for "square" well_shape (CSV, effective size, coverage) - Add tests for per-axis add_region (asymmetric grid, equal sizes) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix 384/1536 well_shape from "rectangular" to "square" (semantically correct) - Extract _make_mm_spinbox() factory in WellplateCalibration (removes 8x7 lines of boilerplate) - Remove redundant double float() reads in CSV backward-compat shim - Remove unnecessary inline comments on width_mm/height_mm assignments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace raw "circular"/"square"/"rectangular" strings with WellShape enum throughout the codebase. Enum stored in dicts, .value string transported through Qt signals, converted back at receiving slots. Adds WellShape.is_round property and WellShape.from_str() factory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends Squid’s sample format model and scan planning pipeline to support non-square rectangular well geometries by introducing per-axis (X/Y) well size/spacing, a well_shape attribute, and corresponding UI/API updates, while maintaining backward compatibility for older CSVs and TCP clients.
Changes:
- Replaces scalar well size/spacing with per-axis
*_x_mm/*_y_mmfields and addswell_shapeacross CSV loading, globals, UI, and coordinate generation. - Adds UI support for asymmetric scan sizes (second spinbox) and a new “2 Diagonal Corners” calibration mode for non-circular wells.
- Updates geometry and scan-coordinate generation utilities to account for rectangular well bounds and per-axis spacing; adds unit tests for the new format behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| software/tests/control/test_scan_size_consistency.py | Updates tests to match the new get_effective_well_size() signature. |
| software/tests/control/test_rectangular_format.py | Adds coverage for CSV parsing (new/old), well shape parsing, rectangular effective sizing/coverage, and per-axis scanning. |
| software/objective_and_sample_formats/sample_formats.csv | Migrates built-in formats to the new per-axis columns and adds well_shape. |
| software/control/widgets.py | Updates sample-format plumbing, adds per-axis scan size UI, adds diagonal-corners calibration flow, expands format settings signal payload. |
| software/control/microscope_control_server.py | Updates TCP well parsing to use per-axis spacing with backward compatibility for old keys. |
| software/control/core/scan_coordinates.py | Updates well coordinate calculations for per-axis spacing and adds per-axis region generation support. |
| software/control/core/geometry_utils.py | Updates effective-well-size/coverage calculations to accept per-axis well dimensions. |
| software/control/core/core.py | Updates NavigationViewer to store per-axis well geometry and well shape. |
| software/control/_def.py | Adds WellShape, updates CSV loader + globals + settings defaults for per-axis fields and shape. |
Comments suppressed due to low confidence (1)
software/control/widgets.py:13664
load_existing_format_values()forcesedge_points_radiofor all formats except 384/1536. For non-circular well shapes (square/rectangular),_on_well_shape_changed()hides the edge-points option; re-checking it here can leave a hidden radio button selected and show the wrong calibration UI. Consider selecting the default calibration method based on the loadedwell_shape(e.g., diagonal corners or center point for non-circular) rather than plate name.
# Auto-select center point method for 384 and 1536 well plates because their
# small well diameters make it difficult to reliably set 3 distinct points
# on the well edge under a microscope
if selected_format in ("384 well plate", "1536 well plate"):
self.center_point_radio.setChecked(True)
else:
self.edge_points_radio.setChecked(True)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
8269
to
8283
| def update_coverage_from_scan_size(self): | ||
| self.entry_well_coverage.blockSignals(True) | ||
| if "glass slide" not in self.navigationViewer.sample: | ||
| well_size_mm = self.scanCoordinates.well_size_mm | ||
| well_size_x_mm = self.scanCoordinates.well_size_x_mm | ||
| well_size_y_mm = self.scanCoordinates.well_size_y_mm | ||
| scan_size = self.entry_scan_size.value() | ||
| overlap_percent = self.entry_overlap.value() | ||
| fov_size_mm = self.navigationViewer.camera.get_fov_size_mm() * self.objectiveStore.get_pixel_size_factor() | ||
| shape = self.combobox_shape.currentText() | ||
| is_round_well = self.scanCoordinates.format not in ["384 well plate", "1536 well plate"] | ||
| is_round_well = self.scanCoordinates.well_shape.is_round | ||
|
|
||
| coverage = calculate_well_coverage( | ||
| scan_size, fov_size_mm, overlap_percent, shape, well_size_mm, is_round_well | ||
| scan_size, fov_size_mm, overlap_percent, shape, well_size_x_mm, well_size_y_mm, is_round_well | ||
| ) | ||
|
|
Comment on lines
+208
to
+229
| if scan_size_y_mm is not None and scan_size_y_mm != scan_size_mm: | ||
| # Per-axis scan: X and Y have different sizes (rectangular wells) | ||
| width_mm = scan_size_mm | ||
| height_mm = scan_size_y_mm | ||
|
|
||
| steps_width = max(1, math.floor(width_mm / step_size_mm)) | ||
| steps_height = max(1, math.floor(height_mm / step_size_mm)) | ||
|
|
||
| half_steps_width = (steps_width - 1) / 2 | ||
| half_steps_height = (steps_height - 1) / 2 | ||
|
|
||
| for i in range(steps_height): | ||
| row = [] | ||
| y = center_y + (i - half_steps_height) * step_size_mm | ||
| for j in range(steps_width): | ||
| x = center_x + (j - half_steps_width) * step_size_mm | ||
| if self.validate_coordinates(x, y): | ||
| row.append((x, y)) | ||
| if self.fov_pattern == "S-Pattern" and i % 2 == 1: | ||
| row.reverse() | ||
| scan_coordinates.extend(row) | ||
| elif shape == "Rectangle": |
Comment on lines
+3
to
+6
| import csv | ||
| import os | ||
| import tempfile | ||
|
|
Comment on lines
102
to
129
| @@ -102,40 +111,46 @@ def calculate_well_coverage(scan_size_mm, fov_size_mm, overlap_percent, shape, w | |||
| fov_size_mm: Field of view size in mm | |||
| overlap_percent: Overlap between adjacent tiles (%) | |||
| shape: Scan shape ("Circle", "Square", or "Rectangle") | |||
| well_size_mm: Well diameter (round) or side length (square) | |||
| is_round_well: True for round wells, False for square wells | |||
| well_size_x_mm: Well X dimension (or diameter for round wells) | |||
| well_size_y_mm: Well Y dimension (defaults to well_size_x_mm for backward compat) | |||
| is_round_well: True for round wells, False for rectangular wells | |||
|
|
|||
| Returns: | |||
| Coverage percentage (0-100) | |||
| """ | |||
| if well_size_y_mm is None: | |||
| well_size_y_mm = well_size_x_mm | |||
|
|
|||
| step_size = fov_size_mm * (1 - overlap_percent / 100) | |||
| if step_size <= 0 or scan_size_mm <= 0 or well_size_mm <= 0: | |||
| if step_size <= 0 or scan_size_mm <= 0 or well_size_x_mm <= 0 or well_size_y_mm <= 0: | |||
| return 0 | |||
|
|
|||
| tiles = get_tile_positions(scan_size_mm, fov_size_mm, overlap_percent, shape) | |||
| if not tiles: | |||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
well_spacing_mm/well_size_mmwith per-axis X/Y pairs (well_spacing_x_mm,well_spacing_y_mm,well_size_x_mm,well_size_y_mm)well_shapefield (circular,square,rectangular) to sample format definitionsChanges
sample_formats.csvwell_size_x_mm,well_size_y_mm,well_spacing_x_mm,well_spacing_y_mm,well_shape_def.pyget_wellplate_settings()geometry_utils.pyget_effective_well_size()returns(x, y)tuple for rectangular wells;calculate_well_coverage()supports rectangular boundsscan_coordinates.pyadd_region()core.pyNavigationViewerstores per-axis dimensionswidgets.pymicroscope_control_server.pyTest plan
🤖 Generated with Claude Code