spacr.ops_layout

Where every tile of a round well is, in closed form.

THE ADJACENCY WAS THE OPEN PROBLEM AND THIS IS THE ANSWER. Three earlier prototypes tried to RECONSTRUCT the grid from the links that register – a serpentine with a constant stride, then runs delimited by a failed link, then columns as maximal runs of vertical links. All three inferred the grid from the very edges that were missing, and all three failed on the same acquisition: 27 of 333 tiles placed.

The well is ROUND. Each column holds as many fields as fit inside the circle at that x, so the columns have DIFFERENT HEIGHTS and the index offset to the next column is that column’s height – which is why the measured horizontal offsets were +11, +4, -11, -9, -4, -13 and -5 rather than one number. Fitting the circle takes four parameters and answers every tile at once:

columns   21, snaked down (even columns top to bottom)
circle    radius 10.25 grid units, centre at column 10, row -10
heights   5, 9, 13, 15, 17, 17, 19, 19, 21, 21, 21, 21, 21,
          19, 19, 17, 17, 15, 13, 9, 5                SUM = 333

IT IS A FACT ABOUT THE ACQUISITION, NOT A GOOD FIT. Seven measured offsets against four free parameters would only be a fit; the model was then asked to predict the four neighbours of six sites it had never seen – 20, 60, 140, 200, 260, 300 – and registration confirmed 24 of 24, at peak/mean 23.6 to 53.1 against control pairs at 9.0 to 15.6. The controls came back at shift (0,0), which is the no-overlap signature behaving as it should.

THE MISTAKE THAT COST THE FIRST MODEL FOUR OF SEVEN, recorded because it is the same class of error as the wrapped shifts in 372’s PART 6-A: rows were indexed from each column’s own top. In a round well every column starts at a different row, so “row 3 of column c” and “row 3 of column c+1” are not side by side. THE NEIGHBOUR IS AT THE SAME ABSOLUTE GRID ROW, which is why WellLayout.position() returns absolute rows and WellLayout.neighbours() looks them up directly.

Typical use:

from spacr.ops_layout import round_well_layout

layout = round_well_layout(333)
for a, b, axis in layout.pairs():
    shift = register(tile(a), tile(b))    # phase correlation, per 372

Classes

WellLayout

A round well's tiles, addressed by site index and by grid position.

Functions

round_well_layout(→ WellLayout)

The layout whose circle holds exactly site_count fields.

Module Contents

class spacr.ops_layout.WellLayout[source]

A round well’s tiles, addressed by site index and by grid position.

Variables:
  • columns – how many columns of fields the acquisition raster has.

  • radius – the well’s radius, in grid units (one tile pitch = 1).

  • centre – (column, row) of the circle’s centre. The row is negative for the measured well because site 0 is at the TOP and rows increase downwards.

  • snake – whether the acquisition snakes – odd columns collected bottom to top. Micro-Manager’s HCS plugin does, which is why the reference implementation carries a remap_snake at all.

  • half_tile – half a tile’s side, in grid units. A site is kept when its whole tile lies inside the circle: at (dx, dy) from the centre, when (|dx| + half_tile)**2 + (|dy| + half_tile)**2 <= radius**2. Zero keeps every site whose centre is inside, which is the rule every layout followed before this field existed.

The tile has to fit, not its centre. The centre rule holds the 333-field sequencing well and cannot produce the column heights the 1,281-field phenotype well was measured at, for any radius. With the footprint term both come out as measured, and every column is centred on the same row.

micron_position(site: int, pitch: float = TILE_PITCH_UM) → Tuple[float, float][source]

Where the reference implementation says a tile is, in microns.

The honest score for a stitch is the residual against THIS, not a count of how many tiles were placed – 372’s PART 6-A, and the reason plate_coordinate was read from the source rather than described.

Parameters:
  • site – the site index.

  • pitch – microns between tile centres.

Returns:

(x, y) in microns, relative to the circle’s centre.

neighbours(site: int) → Dict[str, int][source]

The sites physically adjacent to one site.

FOUR CANDIDATES, NOT 128. Every one of them is a real adjacency: this is what replaces max_site_gap’s window, which offered a band of index neighbours of which at most four could be touching.

Parameters:

site – the site index.

Returns:

{direction: site} for the neighbours that exist.

pairs() → List[Tuple[int, int, str]][source]

Every adjacent pair once, as (first, second, axis).

ONCE, not twice: registering a pair in both directions doubles the work and asks the same question. The axis is “vertical” or “horizontal”, which the caller wants because a vertical link continues a column and a horizontal one crosses a snake turn – and because the two carry different expected shifts.

ORDERED BY GEOMETRY, NOT BY SITE INDEX, and this is the one that will not fail loudly. The SECOND tile is always the one BELOW for a vertical pair and the one to the RIGHT for a horizontal one, because that is what a caller cropping an overlap band has to assume – register_edge takes the bottom of the first and the top of the second. Ordering by index instead looks identical and is wrong on every odd column: the raster snakes, so in a bottom-to-top column the tile ABOVE carries the higher index. It cost half the edges of a toy well and reported itself as a residual of 0.00 px, because each surviving component still solved perfectly.

Returns:

the pairs, ordered by their first site.

position(site: int) → Tuple[int, int][source]

The (column, row) of one site.

Parameters:

site – the site index.

Returns:

the column and the ABSOLUTE grid row – not the row counted from this column’s own top. See the module note.

Raises:

IndexError – when the well holds no such site.

positions() → List[Tuple[int, int]][source]

site -> (column, row) for every site, in index order.

site(column: int, row: int) → int | None[source]

The site index at one grid position, or None when it is empty.

Parameters:
  • column – the column index.

  • row – the absolute grid row.

Returns:

the site index, or None outside the circle.

span(column: int) → Tuple[int, int] | None[source]

(first row, last row) of one column, or None if it is empty.

Parameters:

column – the column index.

Returns:

the inclusive absolute row range of the sites whose whole tile lies inside the circle – see half_tile.

property heights: List[int][source]

How many fields each column holds, left to right.

property site_count: int[source]

How many tiles the well holds.

spacr.ops_layout.round_well_layout(site_count: int = 333, columns: int | None = None, half_tile: float = 0.5857) → WellLayout[source]

The layout whose circle holds exactly site_count fields.

The measured well is returned unchanged whenever the search lands on its 333 sites, so the confirmed model is never re-derived. For any other count the radius is searched – a well imaged at a different magnification or a different plate format is the same circle with a different number of fields in it.

Parameters:
  • site_count – how many tiles the acquisition holds.

  • columns – the column count, when it is known. Derived from the circle otherwise.

  • half_tile – half a tile’s side in tile pitches – the tile size over twice the registered pitch. A field is kept when its whole tile lies inside the circle; zero keeps it when its centre does. The default is the phenotype acquisition’s measured 0.5857. The sequencing acquisition measured 0.5840, and every value strictly between 0.5 and 2.0 gives both wells – 333 and 1,281 fields – the column heights they were measured at.

Returns:

the layout.

Raises:

ValueError – when no circle holds exactly that many fields, which is the honest answer – a count that no round well produces means the acquisition is not one, and guessing the nearest would place every tile slightly wrong. Also when half_tile is negative or not a number.

For one half tile, the count fixes the fields. The number of fields inside the circle only grows with the radius, so every radius that holds exactly site_count holds the same ones. The radius the layout carries is one of those, not a measurement.