Skip to content

feat: add Position plate_row/plate_col and configurable name_pattern to grid plans#268

Closed
ieivanov wants to merge 18 commits into
pymmcore-plus:mainfrom
ieivanov:add-plate-row-col
Closed

feat: add Position plate_row/plate_col and configurable name_pattern to grid plans#268
ieivanov wants to merge 18 commits into
pymmcore-plus:mainfrom
ieivanov:add-plate-row-col

Conversation

@ieivanov
Copy link
Copy Markdown
Contributor

@ieivanov ieivanov commented Mar 24, 2026

Summary

Adds well-plate position support to PositionBase, enabling HCS zarr output from plain stage positions + grid plan without requiring WellPlatePlan.

New fields on PositionBase

  • plate_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" vs plate_col: 1).

Name generation and validation

  • When plate_row/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 matches is accepted; a mismatched name raises ValueError.
  • WellPlatePlan.selected_well_positions and all_well_positions now populate plate_row/plate_col.

Configurable name_pattern on grid plans

  • New name_pattern field on _GridPlan (default: "{idx:04d}").
  • Supports {row}, {col}, {idx} format variables.
  • Example: name_pattern: "row_{row:03d}_col_{col:04d}""row_000_col_0000".

Composite MDAEvent.pos_name

  • For plate positions with a grid plan, pos_name is composed as "{position_name}_{grid_name}" (e.g., "A1_0000").
  • Non-plate positions with grids are unchanged (pos_name = position name only).

Naming behavior

Input Result
Position(plate_row=0, plate_col=0) name="A1" (auto-generated)
Position(name="A1", plate_row=0, plate_col=0) name="A1" (matching, accepted)
Position(name="B9", plate_row=0, plate_col=0) ValueError
Position(plate_row="A", plate_col="1") name="A1" (string pass-through)
Plate position + grid pos_name="A1_0000" (composite)
Regular position + grid (no plate) pos_name="MyPos" (unchanged)

YAML example

stage_positions:
- x: 1000
  y: 1000
  plate_row: 0
  plate_col: 0
  # name auto-generated as "A1"

grid_plan:
  rows: 2
  columns: 2
  fov_width: 180
  fov_height: 180
  # name_pattern: "row_{row:03d}_col_{col:04d}"  # optional

Backward compatibility

All changes are backward compatible:

Change Why it's safe
plate_row/plate_col on PositionBase New optional fields, default None. Old data deserializes fine.
name_pattern on _GridPlan Default "{idx:04d}" produces identical output to the old f"{str(idx).zfill(4)}".
Name auto-generation Only fires when plate_row/plate_col are set — never for existing positions.
Composite pos_name Only fires when position.plate_row is not None.
WellPlatePlan positions gain plate_row/plate_col Additive — existing code reading x, y, name is unaffected.

Note: Serialization output now includes plate_row, plate_col, and name_pattern as new fields. Deserialization of old data is unaffected.

Companion PR: pymmcore-plus/ome-writers#124

Test plan

  • All 385 tests pass (360 existing + 25 new)
  • 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 populates plate_row/plate_col
  • Grid name_pattern (default, custom, serialization)
  • Composite MDAEvent.pos_name for plate positions with grid

🤖 Generated with Claude Code

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>
@ieivanov ieivanov force-pushed the add-plate-row-col branch from 2a92639 to fa4f009 Compare March 25, 2026 00:08
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.67%. Comparing base (2462136) to head (61efb76).
⚠️ Report is 1 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ieivanov and others added 2 commits March 24, 2026 17:37
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>
@ieivanov ieivanov changed the title feat: add plate_row and plate_col fields to PositionBase feat: add plate_row/plate_col and configurable name_pattern to grid plans Mar 25, 2026
- 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>
@ieivanov ieivanov force-pushed the add-plate-row-col branch from 9770005 to 3ad1742 Compare March 25, 2026 01:08
pre-commit-ci Bot and others added 4 commits March 25, 2026 01:08
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>
@ieivanov ieivanov force-pushed the add-plate-row-col branch from 99a70ca to b8dde70 Compare March 25, 2026 01:50
ieivanov and others added 4 commits March 24, 2026 19:07
…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>
@ieivanov ieivanov marked this pull request as ready for review March 25, 2026 02:23
@ieivanov
Copy link
Copy Markdown
Contributor Author

I think I'm happy with this PR, happy to discuss more!

ieivanov and others added 2 commits March 24, 2026 19:24
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>
@ieivanov ieivanov changed the title feat: add plate_row/plate_col and configurable name_pattern to grid plans feat: add Position plate_row/plate_col and configurable name_pattern to grid plans Mar 25, 2026
@tlambert03
Copy link
Copy Markdown
Member

without requiring WellPlatePlan

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?

@ieivanov
Copy link
Copy Markdown
Contributor Author

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. WellPlatePlan is great when you want to construct a set of positions with regular spacing - preset distance between wells. When we image well plates, we often tune the Z position for each well (we use high NA objectives what can be sensitive to small warps in the well bottom) and may occasionally tune the well center to account for uneven seeding of the cells. This is fairly manual, but worthwhile for some acquisitions.

At a higher level, while a WellPlatePlan is a Sequence[Position], not every Sequence[Position] can be a WellPlatePlan. I'd like WellPlatePlan to be convenience method for constructing a set of positions in a well plate, rather than a requirement. Currently, in ome-writers we require a useq.WellPlatePlan to write HCS zarr stores since useq.Positions themselves don't carry enough information to construct a well plate.

def _plate_from_useq(seq: useq.MDASequence) -> Plate | None:
    """Convert a useq WellPlatePlan to an ome-writers Plate."""
    import useq

    useq_plate = seq.stage_positions
    if not isinstance(useq_plate, useq.WellPlatePlan):
        return None

This feature is added in pymmcore-plus/ome-writers#124

@tlambert03
Copy link
Copy Markdown
Member

preset distance between wells

ah, perfect. Yes, makes sense. thank you

@tlambert03
Copy link
Copy Markdown
Member

I'd like WellPlatePlan to be convenience method for constructing a set of positions in a well plate, rather than a requirement.

i don't think I want to go in that direction (making WellPlatePlan a literal convenience method) ... since that loses serializability. I think having it be a model, that also happens to be a Sequence[Position] is the correct model here. but, I do agree that it lacks the flexibility for this case, and supporting any sequence[Position] is better. So, generally agree with the overall point here

@ieivanov
Copy link
Copy Markdown
Contributor Author

I think having it be a model, that also happens to be a Sequence[Position] is the correct model here.

Yes, I agree, I misspoke there

@ieivanov
Copy link
Copy Markdown
Contributor Author

I'm also contemplating ieivanov#1, I'd be happy with or without it

Comment thread src/useq/_grid.py Outdated
Comment thread src/useq/_position.py
Comment on lines +72 to +73
plate_row: int | str | None = None
plate_col: int | str | None = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread src/useq/_position.py
return type(self).model_construct(**kwargs) # type: ignore [return-value]

@model_validator(mode="after")
def _name_from_plate(self) -> "Self":
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

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

@ieivanov

After thinking about this a bit, I'm definitely supportive of adding

class PositionBase:
    plate_row: int | None = None
    plate_col: int | None = None

but 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?

@tlambert03
Copy link
Copy Markdown
Member

@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

@ieivanov
Copy link
Copy Markdown
Contributor Author

Thanks for thinking through this! I agree with you that naming logic is better suited to ome-writers. I think #269 accomplished what I needed from useq-schema.

There is a bug in the current implementation of naming in ome-writers, however. If I try an acquisition with

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: -10

I get an error due to duplicate names in the zarr store. I'll follow up with an issue on that repo.

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