Skip to content

feat: support custom rectangular sample formats with per-axis spacing#522

Open
hongquanli wants to merge 14 commits intomasterfrom
feat/rectangular-sample-format
Open

feat: support custom rectangular sample formats with per-axis spacing#522
hongquanli wants to merge 14 commits intomasterfrom
feat/rectangular-sample-format

Conversation

@hongquanli
Copy link
Copy Markdown
Contributor

Summary

  • Replace single well_spacing_mm / well_size_mm with per-axis X/Y pairs (well_spacing_x_mm, well_spacing_y_mm, well_size_x_mm, well_size_y_mm)
  • Add well_shape field (circular, square, rectangular) to sample format definitions
  • Add per-axis scan size UI (second spinbox shown for rectangular wells with different X/Y dimensions)
  • Add "2 Diagonal Corners" calibration mode for rectangular/square wells
  • Backward compatible: old CSV files and TCP API clients auto-upgraded

Changes

Area What changed
sample_formats.csv New columns: well_size_x_mm, well_size_y_mm, well_spacing_x_mm, well_spacing_y_mm, well_shape
_def.py Loader with backward compat, new globals, updated get_wellplate_settings()
geometry_utils.py get_effective_well_size() returns (x, y) tuple for rectangular wells; calculate_well_coverage() supports rectangular bounds
scan_coordinates.py Per-axis position calculations, per-axis scan size in add_region()
core.py NavigationViewer stores per-axis dimensions
widgets.py 13-param signal, per-axis scan size spinbox, well shape selector, diagonal corners calibration, updated WellSelectionWidget/Well1536SelectionWidget
microscope_control_server.py TCP server per-axis spacing with backward compat

Test plan

  • 25 unit tests passing (CSV loading, geometry, position calculations)
  • Manual smoke test: switch between standard formats (96, 384, etc.)
  • Manual: open calibration dialog, verify shape-dependent UI
  • Manual: create custom rectangular format, verify well positions

🤖 Generated with Claude Code

hongquanli and others added 14 commits March 25, 2026 02:50
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_mm fields and adds well_shape across 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() forces edge_points_radio for 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 loaded well_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:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants