Source code for piblin_jax.data.collections.tabular_measurement_set

"""
TabularMeasurementSet class for piblin-jax.

MeasurementSet variant with tabular access patterns (rows and columns).
"""

from typing import Any

from .measurement import Measurement
from .measurement_set import MeasurementSet


[docs] class TabularMeasurementSet(MeasurementSet): """ MeasurementSet with measurements arranged in tabular format. This specialized variant organizes measurements in a logical table structure with row and column labels. This is useful for: - Experimental design matrices (e.g., multi-factor designs) - Microplate/well plate layouts - Spatial arrangements of measurements - Grid-based sampling patterns The tabular structure enables intuitive access patterns and natural visualization as tables or heatmaps. Parameters ---------- measurements : list[Measurement] List of Measurement objects. The order corresponds to row-major ordering in the table (row1-col1, row1-col2, ..., row2-col1, ...). row_labels : list[str] | None, optional Labels for table rows. If provided, must satisfy: len(row_labels) * len(col_labels) == len(measurements) col_labels : list[str] | None, optional Labels for table columns. If provided, must satisfy: len(row_labels) * len(col_labels) == len(measurements) conditions : dict[str, Any] | None, optional Experimental conditions for the measurement series. details : dict[str, Any] | None, optional Additional context for the measurement series. Attributes ---------- row_labels : list[str] | None Labels for table rows. col_labels : list[str] | None Labels for table columns. Notes ----- Measurements are stored in row-major order. For a 2x3 table: - measurements[0] = row 0, col 0 - measurements[1] = row 0, col 1 - measurements[2] = row 0, col 2 - measurements[3] = row 1, col 0 - measurements[4] = row 1, col 1 - measurements[5] = row 1, col 2 Examples -------- >>> import numpy as np >>> from piblin_jax.data.datasets import OneDimensionalDataset >>> from piblin_jax.data.collections import Measurement, TabularMeasurementSet >>> >>> # Create a 2x3 grid of measurements >>> x = np.linspace(0, 10, 50) >>> measurements = [] >>> >>> for i in range(2): # rows ... for j in range(3): # columns ... y = np.sin(x * (i + 1)) * (j + 1) ... ds = OneDimensionalDataset(x, y) ... m = Measurement( ... [ds], ... conditions={"row": i, "col": j} ... ) ... measurements.append(m) >>> >>> # Create tabular measurement set >>> tms = TabularMeasurementSet( ... measurements=measurements, ... row_labels=["row_A", "row_B"], ... col_labels=["col_1", "col_2", "col_3"], ... conditions={"plate": "plate_001"}, ... details={"date": "2025-10-18"} ... ) >>> >>> len(tms) 6 >>> tms.row_labels ['row_A', 'row_B'] >>> tms.col_labels ['col_1', 'col_2', 'col_3'] >>> >>> # Access measurement at row 1, col 2 >>> m = tms.get_measurement(1, 2) >>> m.conditions["row"] 1 >>> m.conditions["col"] 2 """
[docs] def __init__( self, measurements: list[Measurement], row_labels: list[str] | None = None, col_labels: list[str] | None = None, conditions: dict[str, Any] | None = None, details: dict[str, Any] | None = None, ): """ Initialize TabularMeasurementSet with optional row/column labels. Parameters ---------- measurements : list[Measurement] List of Measurement objects in row-major order. row_labels : list[str] | None, optional Labels for table rows. col_labels : list[str] | None, optional Labels for table columns. conditions : dict[str, Any] | None, optional Experimental conditions for this measurement series. details : dict[str, Any] | None, optional Additional context for this measurement series. Raises ------ ValueError If row_labels and col_labels are provided but their product doesn't match the number of measurements. """ # Validate dimensions if labels are provided if row_labels is not None and col_labels is not None: expected_count = len(row_labels) * len(col_labels) if len(measurements) != expected_count: raise ValueError( f"Number of measurements ({len(measurements)}) must equal " f"len(row_labels) * len(col_labels) ({expected_count}). " f"Got {len(row_labels)} rows and {len(col_labels)} columns." ) self._row_labels = row_labels self._col_labels = col_labels # Call parent constructor super().__init__(measurements, conditions, details)
@property def row_labels(self) -> list[str] | None: """ Get row labels for the table. Returns ------- list[str] | None List of row labels, or None if not provided. Examples -------- >>> tms.row_labels ['row_A', 'row_B', 'row_C'] """ return self._row_labels @property def col_labels(self) -> list[str] | None: """ Get column labels for the table. Returns ------- list[str] | None List of column labels, or None if not provided. Examples -------- >>> tms.col_labels ['col_1', 'col_2', 'col_3', 'col_4'] """ return self._col_labels @property def shape(self) -> tuple[int, int] | None: """ Get the shape of the table (rows, columns). Returns ------- tuple[int, int] | None (n_rows, n_cols) if labels are provided, None otherwise. Examples -------- >>> tms.shape (2, 3) """ if self._row_labels is not None and self._col_labels is not None: return (len(self._row_labels), len(self._col_labels)) return None
[docs] def get_measurement(self, row: int, col: int) -> Measurement: """ Get measurement at specified row and column indices. Uses row-major ordering: index = row * n_cols + col Parameters ---------- row : int Row index (0-based). col : int Column index (0-based). Returns ------- Measurement Measurement at the specified position. Raises ------ ValueError If row_labels and col_labels were not provided. IndexError If row or col indices are out of bounds. Examples -------- >>> m = tms.get_measurement(1, 2) >>> m.conditions["row"] 1 >>> m.conditions["col"] 2 """ if self._row_labels is None or self._col_labels is None: raise ValueError( "get_measurement() requires row_labels and col_labels. " "Use direct indexing instead: tms[index]" ) n_rows = len(self._row_labels) n_cols = len(self._col_labels) if not (0 <= row < n_rows): raise IndexError(f"Row index {row} out of bounds [0, {n_rows})") if not (0 <= col < n_cols): raise IndexError(f"Column index {col} out of bounds [0, {n_cols})") index = row * n_cols + col return self._measurements[index]
[docs] def get_row(self, row: int) -> list[Measurement]: """ Get all measurements in a specified row. Parameters ---------- row : int Row index (0-based). Returns ------- list[Measurement] List of measurements in the row. Raises ------ ValueError If row_labels and col_labels were not provided. IndexError If row index is out of bounds. Examples -------- >>> row_measurements = tms.get_row(0) >>> len(row_measurements) 3 >>> [m.conditions["col"] for m in row_measurements] [0, 1, 2] """ if self._row_labels is None or self._col_labels is None: raise ValueError("get_row() requires row_labels and col_labels.") n_rows = len(self._row_labels) n_cols = len(self._col_labels) if not (0 <= row < n_rows): raise IndexError(f"Row index {row} out of bounds [0, {n_rows})") start_idx = row * n_cols end_idx = start_idx + n_cols return list(self._measurements[start_idx:end_idx])
[docs] def get_column(self, col: int) -> list[Measurement]: """ Get all measurements in a specified column. Parameters ---------- col : int Column index (0-based). Returns ------- list[Measurement] List of measurements in the column. Raises ------ ValueError If row_labels and col_labels were not provided. IndexError If col index is out of bounds. Examples -------- >>> col_measurements = tms.get_column(1) >>> len(col_measurements) 2 >>> [m.conditions["row"] for m in col_measurements] [0, 1] """ if self._row_labels is None or self._col_labels is None: raise ValueError("get_column() requires row_labels and col_labels.") n_rows = len(self._row_labels) n_cols = len(self._col_labels) if not (0 <= col < n_cols): raise IndexError(f"Column index {col} out of bounds [0, {n_cols})") return [self._measurements[row * n_cols + col] for row in range(n_rows)]