feat: add Position plate_row/plate_col and configurable name_pattern to grid plans#268
feat: add Position plate_row/plate_col and configurable name_pattern to grid plans#268ieivanov wants to merge 18 commits into
Conversation
Add serialized int fields to PositionBase for annotating which well a position belongs to, enabling HCS zarr output from plain stage positions without requiring WellPlatePlan. Also populate these fields in WellPlatePlan.all_well_positions and selected_well_positions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add serialized int fields to PositionBase for annotating which well a position belongs to, enabling HCS zarr output from plain stage positions without requiring WellPlatePlan. Also populate these fields in WellPlatePlan.all_well_positions and selected_well_positions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2a92639 to
fa4f009
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #268 +/- ##
==========================================
+ Coverage 93.62% 93.67% +0.04%
==========================================
Files 33 33
Lines 2590 2607 +17
==========================================
+ Hits 2425 2442 +17
Misses 165 165 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Add a `name_pattern` field to `_GridPlan` that controls how grid
positions are named. Supports {row}, {col}, {idx} format variables.
Default is "{idx:04d}" (preserves backward compat). Names are now
generated in useq-schema so downstream consumers don't need to
override them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- If name is None but plate_row/plate_col are set, auto-generate
a well name using existing _index_to_row_name (e.g., 0,0 -> "A1")
- When iterating with a grid plan, compose pos_name as
"{position_name}_{grid_name}" for plate positions (e.g., "A1_0000")
- Explicit names are never overwritten
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9770005 to
3ad1742
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When plate_row and plate_col are set, the position name is always derived from them (e.g., plate_row=0, plate_col=0 -> "A1"). Providing an explicit name that doesn't match raises ValueError. This ensures the position name and zarr well path are always coupled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
plate_row accepts 0-based int (0 -> "A") or str ("A").
plate_col accepts 0-based int (0 -> "1") or str ("1").
The well name is derived accordingly and validated against
any explicit name provided.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99a70ca to
b8dde70
Compare
…pos_name 26 new tests covering: - Name auto-generation from int and str plate_row/plate_col - Mismatched name validation (ValueError) - JSON and YAML serialization round-trips - Propagation through Position.__add__ - WellPlatePlan setting plate_row/plate_col on positions - Grid name_pattern (default, custom, serialization) - Composite MDAEvent.pos_name for plate positions with grid Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When plate_row and plate_col are both int (standard well-plate indices), the name is strictly derived (e.g., 0,0 -> "A1") and mismatches raise ValueError. When either is a str (custom naming like "fish0", "neuromast0"), any explicit name is accepted, allowing free-form naming for non-standard plate layouts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I think I'm happy with this PR, happy to discuss more! |
Use inline isinstance checks instead of a variable so mypy can narrow int|str to int within the branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…schema into add-plate-row-col
just so that I have the full picture ... can you elaborate on why you don't want to use WellPlatePlan for describing positions in a well plate? |
Ah, yes, sorry that wasn't clear. At a higher level, while a This feature is added in pymmcore-plus/ome-writers#124 |
ah, perfect. Yes, makes sense. thank you |
i don't think I want to go in that direction (making |
Yes, I agree, I misspoke there |
|
I'm also contemplating ieivanov#1, I'd be happy with or without it |
| plate_row: int | str | None = None | ||
| plate_col: int | str | None = None |
There was a problem hiding this comment.
I'm a bit uncertain about int | str here. I see in your tests that you've used this for things like Position(plate_row="fish0", plate_col="neuromast0")... but that feels like it's overloading one concept (which row/col) to do double duty to store metadata that should arguably go elsewhere (either in the position name itself (which is already a totally open string) or in the sequence metadata somewhere (where you could map your row/col indices to treatments/metadata). Also, thinking ahead to how this information would be used to write an OME-Zarr, we have to know the row/col index (https://imaging-formats.github.io/yaozarrs/API_Reference/yaozarrs.v05/?h=row#yaozarrs.v05._plate.PlateWell) ... so allowing the user to provide a string here means we would have difficulty guaranteeing the ability to write ome-zarr.
I'm inclined to reduce this back to just int for now.
| return type(self).model_construct(**kwargs) # type: ignore [return-value] | ||
|
|
||
| @model_validator(mode="after") | ||
| def _name_from_plate(self) -> "Self": |
There was a problem hiding this comment.
this all feels like application-level logic to me. I don't think we should auto-calculate names from other fields. Just store the data, and leave this sort of concern (autogeneration of naming) to whoever is using the data, like a writer, or a GUI.
tlambert03
left a comment
There was a problem hiding this comment.
After thinking about this a bit, I'm definitely supportive of adding
class PositionBase:
plate_row: int | None = None
plate_col: int | None = Nonebut I think most of the rest of this PR (naming related stuff) is too opinionated for this level. I think we should try to just store the data and leave the name autogeneration to downstream applications that might have different opinions on how it should be done.
Would you be ok if I heavily reduced this PR to just adding the plate_row/plate_col fields?
|
@ieivanov, I merged #269, which branched off of and simplified this PR to just add the plate_row/plate_col stuff, and I've merged main back into your branch here. So, this PR is now just about adding naming conveniences and formatting. This is all stuff that I'm not entirely sure about... but we can continue the discussion here when you like. I think the primary need for the purpose of ome-writers (and persistent plate metadata without using a WellPlate object) is accomplished by #269 |
|
Thanks for thinking through this! I agree with you that naming logic is better suited to There is a bug in the current implementation of naming in stage_positions:
- x: 14000
y: 11000
plate_row: 0
plate_col: 0
- x: 23500
y: 11000
plate_row: 0
plate_col: 1
grid_plan:
rows: 2
columns: 2
fov_width: 180
fov_height: 180
overlap: -10I get an error due to duplicate names in the zarr store. I'll follow up with an issue on that repo. |
Summary
Adds well-plate position support to
PositionBase, enabling HCS zarr output from plain stage positions + grid plan without requiringWellPlatePlan.New fields on
PositionBaseplate_row: int | str | None— Row for well plate positions. Accepts 0-based int (0 → "A") or string ("A") used as-is.plate_col: int | str | None— Column for well plate positions. Accepts 0-based int (0 → "1") or string ("1") used as-is. In YAML, unquoted numbers are parsed as int, so use quotes for string columns (plate_col: "1"vsplate_col: 1).Name generation and validation
plate_row/plate_colare set, the position name is always derived from them (e.g.,plate_row=0, plate_col=0→"A1").ValueError.WellPlatePlan.selected_well_positionsandall_well_positionsnow populateplate_row/plate_col.Configurable
name_patternon grid plansname_patternfield on_GridPlan(default:"{idx:04d}").{row},{col},{idx}format variables.name_pattern: "row_{row:03d}_col_{col:04d}"→"row_000_col_0000".Composite
MDAEvent.pos_namepos_nameis composed as"{position_name}_{grid_name}"(e.g.,"A1_0000").pos_name= position name only).Naming behavior
Position(plate_row=0, plate_col=0)"A1"(auto-generated)Position(name="A1", plate_row=0, plate_col=0)"A1"(matching, accepted)Position(name="B9", plate_row=0, plate_col=0)ValueErrorPosition(plate_row="A", plate_col="1")"A1"(string pass-through)"A1_0000"(composite)"MyPos"(unchanged)YAML example
Backward compatibility
All changes are backward compatible:
plate_row/plate_colonPositionBaseNone. Old data deserializes fine.name_patternon_GridPlan"{idx:04d}"produces identical output to the oldf"{str(idx).zfill(4)}".plate_row/plate_colare set — never for existing positions.pos_nameposition.plate_row is not None.WellPlatePlanpositions gainplate_row/plate_colx,y,nameis unaffected.Note: Serialization output now includes
plate_row,plate_col, andname_patternas new fields. Deserialization of old data is unaffected.Companion PR: pymmcore-plus/ome-writers#124
Test plan
🤖 Generated with Claude Code