Bases: TSTransform
flowchart TD
sgnts.transforms.and_transform.ANDTransform[ANDTransform]
sgnts.base.base.TSTransform[TSTransform]
sgnts.base.base.TimeSeriesMixin[TimeSeriesMixin]
sgnts.base.base.TSTransform --> sgnts.transforms.and_transform.ANDTransform
sgnts.base.base.TimeSeriesMixin --> sgnts.base.base.TSTransform
click sgnts.transforms.and_transform.ANDTransform href "" "sgnts.transforms.and_transform.ANDTransform"
click sgnts.base.base.TSTransform href "" "sgnts.base.base.TSTransform"
click sgnts.base.base.TimeSeriesMixin href "" "sgnts.base.base.TimeSeriesMixin"
Perform logical AND operation across multiple input streams based on gap status.
This transform takes multiple input streams with arbitrary sample rates and
produces a single output stream at the maximum sample rate among all inputs.
Output behavior:
- Where ALL inputs have non-gap data: output is 1 (logical AND)
- Where ANY input has a gap: output is a gap buffer (data=None)
This approach is semantically correct as gaps represent "no valid data" rather
than zeros. Downstream transforms can decide how to handle gaps (e.g., convert
to zeros if needed).
The transform uses TSSlice set logic to determine overlapping regions and
efficiently creates output buffers using the split method with contiguous=True.
Parameters:
| Name |
Type |
Description |
Default |
output_shape
|
tuple[int, ...]
|
Optional[tuple[int, ...]], shape of output samples (excluding time
dimension).
If None, defaults to scalar output ()
|
tuple()
|
Notes
Thread safety:
Marked thread_safe = True. Pad layout: N sink pads +
1 source pad (@validator.many_to_one). The N sink pads'
pull callbacks CAN run concurrently in the same wave;
internal runs alone.
``pull`` (inherited ``TimeSeriesMixin.pull``):
per-pad-keyed dict writes; safe across pads. ``new``
(inherited): read-only ``self.outframes`` lookup.
``process``: TSSlice set-logic and buffer splitting on
local data only — no element-level mutation.
Where speedup comes from: the work is mostly Python-bound
(TSSlice ops) so direct compute-parallelism is modest, but
marking thread_safe lets the element run concurrently with
adjacent thread_safe elements without forcing a
serialization point in the wave.
**Future editors MUST preserve thread safety**: keep
``process`` purely functional on its inputs. Do NOT add
element-level state mutated from ``pull`` outside of
per-pad-keyed containers — multi-sink elements will race.
Source code in src/sgnts/transforms/and_transform.py
| @dataclass
class ANDTransform(TSTransform):
"""Perform logical AND operation across multiple input streams based on gap status.
This transform takes multiple input streams with arbitrary sample rates and
produces a single output stream at the maximum sample rate among all inputs.
Output behavior:
- Where ALL inputs have non-gap data: output is 1 (logical AND)
- Where ANY input has a gap: output is a gap buffer (data=None)
This approach is semantically correct as gaps represent "no valid data" rather
than zeros. Downstream transforms can decide how to handle gaps (e.g., convert
to zeros if needed).
The transform uses TSSlice set logic to determine overlapping regions and
efficiently creates output buffers using the split method with contiguous=True.
Args:
output_shape:
Optional[tuple[int, ...]], shape of output samples (excluding time
dimension).
If None, defaults to scalar output ()
Notes:
Thread safety:
Marked ``thread_safe = True``. Pad layout: N sink pads +
1 source pad (``@validator.many_to_one``). The N sink pads'
``pull`` callbacks CAN run concurrently in the same wave;
``internal`` runs alone.
``pull`` (inherited ``TimeSeriesMixin.pull``):
per-pad-keyed dict writes; safe across pads. ``new``
(inherited): read-only ``self.outframes`` lookup.
``process``: TSSlice set-logic and buffer splitting on
local data only — no element-level mutation.
Where speedup comes from: the work is mostly Python-bound
(TSSlice ops) so direct compute-parallelism is modest, but
marking thread_safe lets the element run concurrently with
adjacent thread_safe elements without forcing a
serialization point in the wave.
**Future editors MUST preserve thread safety**: keep
``process`` purely functional on its inputs. Do NOT add
element-level state mutated from ``pull`` outside of
per-pad-keyed containers — multi-sink elements will race.
"""
thread_safe = True
backends = frozenset({"numpy"})
output_shape: tuple[int, ...] = field(default_factory=tuple)
def configure(self) -> None:
# Explicitly disable adapter to prevent gap filling
self.adapter_config.enable = False
@validator.many_to_one
def validate(self) -> None:
pass
def output_prototype(self, pad):
# Emits a float64 presence mask regardless of input dtype (numpy-only).
return self.input_prototype(self.sink_pad_names[0], dtype=float)
@transform.many_to_one
def process(
self, input_frames: dict[SinkPad, TSFrame], output_frame: TSCollectFrame
) -> None:
"""Generate output frame with AND logic across all inputs.
Output contains buffers with 1s where all inputs have data,
and gap buffers where any input has gaps.
"""
frames = list(input_frames.values())
# Use the maximum sample rate among all inputs for output
max_rate = max(f.sample_rate for f in frames)
# Collect non-gap slices from each input
all_nongap_slices = [
TSSlices([buf.slice for buf in frame if not buf.is_gap]) for frame in frames
]
# Find intersection of all non-gap regions (where ALL inputs have data)
intersection_slices = TSSlices.intersection_of_multiple(all_nongap_slices)
# Create initial buffer spanning entire frame with 1s
frame_slice = TSSlice(frames[0].offset, frames[0].end_offset)
full_buffer = SeriesBuffer.fromoffsetslice(
frame_slice,
sample_rate=max_rate,
data=None,
channels=self.output_shape,
)
full_buffer.set_data(numpy.ones(full_buffer.shape)) # filled with ones
# Split buffer at intersection boundaries:
# - Keeps 1s where we have intersections (all inputs have data)
# - Creates gap buffers (data=None) between intersections
# If no intersections exist, create a single gap buffer
output_buffers = (
full_buffer.split(intersection_slices, contiguous=True)
if intersection_slices.slices
else [full_buffer.new()] # new() with no data creates a gap buffer
)
output_frame.extend(output_buffers)
|
Generate output frame with AND logic across all inputs.
Output contains buffers with 1s where all inputs have data,
and gap buffers where any input has gaps.
Source code in src/sgnts/transforms/and_transform.py
| @transform.many_to_one
def process(
self, input_frames: dict[SinkPad, TSFrame], output_frame: TSCollectFrame
) -> None:
"""Generate output frame with AND logic across all inputs.
Output contains buffers with 1s where all inputs have data,
and gap buffers where any input has gaps.
"""
frames = list(input_frames.values())
# Use the maximum sample rate among all inputs for output
max_rate = max(f.sample_rate for f in frames)
# Collect non-gap slices from each input
all_nongap_slices = [
TSSlices([buf.slice for buf in frame if not buf.is_gap]) for frame in frames
]
# Find intersection of all non-gap regions (where ALL inputs have data)
intersection_slices = TSSlices.intersection_of_multiple(all_nongap_slices)
# Create initial buffer spanning entire frame with 1s
frame_slice = TSSlice(frames[0].offset, frames[0].end_offset)
full_buffer = SeriesBuffer.fromoffsetslice(
frame_slice,
sample_rate=max_rate,
data=None,
channels=self.output_shape,
)
full_buffer.set_data(numpy.ones(full_buffer.shape)) # filled with ones
# Split buffer at intersection boundaries:
# - Keeps 1s where we have intersections (all inputs have data)
# - Creates gap buffers (data=None) between intersections
# If no intersections exist, create a single gap buffer
output_buffers = (
full_buffer.split(intersection_slices, contiguous=True)
if intersection_slices.slices
else [full_buffer.new()] # new() with no data creates a gap buffer
)
output_frame.extend(output_buffers)
|