spacr.ops_sample

Every other channel, sampled at the coordinates the numbering assigned.

SAMPLING EVERY OTHER CHANNEL at the object coordinates the numbering step assigned – the part that can be built and checked without a real cycle stack. The numbering step produced a plate-level object id and a well-frame centroid for every nucleus; this is what reads the remaining channels AT those coordinates and keys the result to those ids.

C1 SEQUENCING CHANNELS, PER CYCLE … NEVER averaged across cycles –

that is the one operation that destroys a barcode while leaving it decodable.

THAT SENTENCE IS THE WHOLE DESIGN CONSTRAINT AND IT IS ENFORCED, NOT DOCUMENTED. A mean across cycles of a four-channel readout still looks like a four-channel readout: call_reads accepts it, returns a barcode of the right length, and every downstream table fills in. The run completes, the numbers are plausible, and the barcodes are noise. There is no later check that can catch it, because nothing downstream knows what the per-cycle values were. So decode_input() refuses an array whose cycle axis has been collapsed, and this module offers no way to average one.

THE PHENOTYPE PATH IS THE OPPOSITE CASE, which is why the two are separate functions rather than one with a flag. A phenotype channel measured in several cycles IS several samples of one quantity and averaging it is the right thing, and what it emits is the schema measure already emits. Same plate, same objects, opposite correct answer; a shared flag would make that a caller’s choice, and it is not one.

The samplers are injected, as they are in spacr.ops_compose and spacr.ops_objects: nothing here opens a file or knows a transform’s provenance, so all of it is testable on planted truth.

Exceptions

SampleError

A sampling that cannot mean anything, with the way out in the text.

Functions

average_over_cycles(→ numpy.ndarray)

Mean of one PHENOTYPE channel across cycles. Correct here, only here.

barcode_rows(→ List[Dict[str, object]])

ops_barcodes rows: one per object, assembled across cycles.

decode_input(→ numpy.ndarray)

Stack per-cycle, per-channel samples into call_reads' input.

reads_rows(→ List[Dict[str, object]])

ops_reads rows: one per object per cycle, per the storage contract's columns.

sample_objects(→ numpy.ndarray)

Read image at each object's centroid, through transform.

Module Contents

exception spacr.ops_sample.SampleError[source]

Bases: ValueError

A sampling that cannot mean anything, with the way out in the text.

Initialize self. See help(type(self)) for accurate signature.

spacr.ops_sample.average_over_cycles(per_cycle: Sequence[numpy.ndarray]) → numpy.ndarray[source]

Mean of one PHENOTYPE channel across cycles. Correct here, only here.

Phenotype channels are sampled “at the same object ids”, and a phenotype measured in several cycles is several samples of one quantity – so averaging raises its precision, exactly as averaging the Hoechst across tiles does when they overlap.

SEPARATE FROM decode_input() ON PURPOSE. The same operation is right for a phenotype channel and catastrophic for a sequencing one, so it is two functions and not one with a flag: a flag would make that a caller’s decision, and it is a property of the channel.

NaN-aware, because sample_objects() returns NaN for an object this cycle did not cover, and an object measured in nine cycles of eleven should get the mean of the nine rather than NaN.

Parameters:

per_cycle – one array of per-object values per cycle, every array the same length and in the same object order. The order is the caller’s to keep: nothing here can detect two cycles sampled against different object tables.

spacr.ops_sample.barcode_rows(objects: Sequence, values: numpy.ndarray, *, bases: Sequence[str] | None = None, library: Sequence[str] | None = None) → List[Dict[str, object]][source]

ops_barcodes rows: one per object, assembled across cycles.

Delegates the call itself to spacr.ops_sbs.call_reads(), which already normalises each cycle’s channels and reports quality as the MINIMUM over cycles – “a barcode is only as trustworthy as its worst base”. Repeating that here would be a second decoder to keep in step.

Parameters:
  • objects – the numbered objects, in the same order as values’ first axis.

  • values – (objects, cycles, channels) intensities, NEVER averaged over cycles – see decode_input(), which refuses a single cycle for the same reason.

  • library – when given, each barcode is corrected to the nearest library member through spacr.ops_sbs.correct_to_library(), and the row carries what it mapped to.

spacr.ops_sample.decode_input(per_cycle: Sequence[Sequence[numpy.ndarray]]) → numpy.ndarray[source]

Stack per-cycle, per-channel samples into call_reads’ input.

Parameters:

per_cycle – per_cycle[c][k] is sample_objects()’ output for cycle c, channel k. Every entry must be the same length.

Returns:

(objects, cycles, channels) – the shape spacr.ops_sbs.call_reads() expects.

Raises:

SampleError – when a cycle is missing channels, when the lengths disagree, or – the one that matters – when only ONE cycle is offered, which is what a caller who has already averaged them hands over.

ONE CYCLE IS REFUSED AND THE REFUSAL IS THE POINT. A barcode is a sequence across cycles; a single “cycle” reaching here is either a run that lost its cycle axis or a caller who collapsed it. Both produce a decodable barcode made of nothing, and neither is visible downstream. If a single-cycle experiment ever needs decoding it should say so explicitly rather than arrive looking like this mistake.

spacr.ops_sample.reads_rows(objects: Sequence, values: numpy.ndarray, *, channels: Sequence[str], bases: Sequence[str] | None = None) → List[Dict[str, object]][source]

ops_reads rows: one per object per cycle, per the storage contract’s columns.

Parameters:
  • objects – the numbered objects the values were sampled at, in the same order as values’ first axis.

  • values – (objects, cycles, channels) from decode_input().

  • channels – a name per channel, for the column names.

  • bases – the letter each channel votes for; when given, each row carries the base this cycle called and its margin.

spacr.ops_sample.sample_objects(objects: Sequence, image: numpy.ndarray, *, transform: Callable[[float, float], Tuple[float, float]] | None = None, radius: int = 1) → numpy.ndarray[source]

Read image at each object’s centroid, through transform.

Parameters:
  • objects – spacr.ops_objects.PlateObject values, or anything with centroid_y and centroid_x in the WELL frame.

  • image – one channel of one cycle, in that cycle’s own frame.

  • transform – maps a well-frame (y, x) into this image’s frame – the A2/A3 transforms of PART 7. None means the image is already in the well frame.

  • radius – half-width of the square averaged around each point. The default 1 gives a 3x3, which is what a diffraction-limited spot occupies; 0 reads the single pixel.

Returns:

one float per object, NaN where the point falls outside.

NaN RATHER THAN A CLAMPED EDGE PIXEL. An object whose coordinates land off this cycle’s field was not measured in it, and the honest value for “not measured” is not the brightness of the nearest border pixel – that would be a number, and a number gets averaged into a barcode.