Source code for piblin_jax.data.collections.experiment

"""
Experiment class for piblin-jax.

Container for multiple MeasurementSet objects representing a single experiment.
"""

from collections.abc import Iterator
from typing import Any

from .measurement_set import MeasurementSet


[docs] class Experiment: """ Container for MeasurementSet objects from a single experiment. An Experiment represents a complete experimental run or sample, which may contain multiple series of measurements (MeasurementSets). This is useful for: - Single sample with multiple measurement types - Complete experimental protocol with multiple phases - Single experimental run with multiple observables - One sample measured under different conditions Parameters ---------- measurement_sets : list[MeasurementSet] List of MeasurementSet objects from this experiment. conditions : dict[str, Any] | None, optional Experimental conditions for the entire experiment (e.g., sample ID, experimental date, operator). details : dict[str, Any] | None, optional Additional context for this experiment (e.g., sample description, experimental notes, quality flags). Attributes ---------- measurement_sets : tuple[MeasurementSet, ...] Immutable tuple of measurement sets in this experiment. conditions : dict[str, Any] Experimental conditions for this experiment. details : dict[str, Any] Additional metadata for this experiment. Notes ----- The measurement sets are stored as a tuple to ensure immutability, which is required for JAX transformations. Individual measurement sets can be accessed by indexing or iteration. Hierarchy level: ExperimentSet → **Experiment** → MeasurementSet → Measurement → Dataset Examples -------- >>> import numpy as np >>> from piblin_jax.data.datasets import OneDimensionalDataset >>> from piblin_jax.data.collections import ( ... Measurement, MeasurementSet, Experiment ... ) >>> >>> # Create first measurement set (absorption spectra) >>> x_abs = np.linspace(400, 800, 200) >>> measurements_abs = [] >>> for i in range(3): ... y = np.exp(-(x_abs - 550)**2 / 1000) * (1 + 0.1 * i) ... ds = OneDimensionalDataset(x_abs, y) ... m = Measurement([ds], conditions={"replicate": i+1}) ... measurements_abs.append(m) >>> ms_abs = MeasurementSet( ... measurements_abs, ... conditions={"measurement_type": "absorption"} ... ) >>> >>> # Create second measurement set (fluorescence spectra) >>> x_fl = np.linspace(500, 900, 200) >>> measurements_fl = [] >>> for i in range(3): ... y = np.exp(-(x_fl - 650)**2 / 1500) * (0.8 + 0.1 * i) ... ds = OneDimensionalDataset(x_fl, y) ... m = Measurement([ds], conditions={"replicate": i+1}) ... measurements_fl.append(m) >>> ms_fl = MeasurementSet( ... measurements_fl, ... conditions={"measurement_type": "fluorescence"} ... ) >>> >>> # Create experiment combining both measurement types >>> exp = Experiment( ... measurement_sets=[ms_abs, ms_fl], ... conditions={"sample": "S001", "date": "2025-10-18"}, ... details={"operator": "Jane Doe", "instrument": "Spec-X"} ... ) >>> >>> len(exp) 2 >>> exp.conditions["sample"] 'S001' >>> exp[0].conditions["measurement_type"] 'absorption' >>> exp[1].conditions["measurement_type"] 'fluorescence' """
[docs] def __init__( self, measurement_sets: list[MeasurementSet], conditions: dict[str, Any] | None = None, details: dict[str, Any] | None = None, ): """ Initialize Experiment with measurement sets and metadata. Parameters ---------- measurement_sets : list[MeasurementSet] List of MeasurementSet objects from this experiment. conditions : dict[str, Any] | None, optional Experimental conditions for this experiment. details : dict[str, Any] | None, optional Additional context for this experiment. """ self._measurement_sets = tuple(measurement_sets) # Immutable for JAX self._conditions = conditions if conditions is not None else {} self._details = details if details is not None else {}
@property def measurement_sets(self) -> tuple[MeasurementSet, ...]: """ Get all measurement sets in this experiment. :no-index: Returns ------- tuple[MeasurementSet, ...] Immutable tuple of MeasurementSet objects. Examples -------- >>> exp.measurement_sets (<MeasurementSet at 0x...>, <MeasurementSet at 0x...>) """ return self._measurement_sets @property def conditions(self) -> dict[str, Any]: """ Get experimental conditions for this experiment. :no-index: Returns ------- dict[str, Any] Dictionary of experimental conditions (sample, date, operator, etc.). Examples -------- >>> exp.conditions {'sample': 'S001', 'date': '2025-10-18', 'temperature': 25.0} """ return self._conditions @property def details(self) -> dict[str, Any]: """ Get additional details for this experiment. :no-index: Returns ------- dict[str, Any] Dictionary of additional context (notes, quality, instrument, etc.). Examples -------- >>> exp.details {'operator': 'Jane Doe', 'instrument': 'Spec-X', 'notes': 'Good quality'} """ return self._details
[docs] def __len__(self) -> int: """ Get number of measurement sets in this experiment. Returns ------- int Number of measurement sets. Examples -------- >>> len(exp) 2 """ return len(self._measurement_sets)
[docs] def __iter__(self) -> Iterator[MeasurementSet]: """ Iterate over measurement sets in this experiment. Yields ------ MeasurementSet Each measurement set in order. Examples -------- >>> for ms in exp: ... print(ms.conditions["measurement_type"]) absorption fluorescence """ return iter(self._measurement_sets)
[docs] def __getitem__(self, index: int | slice) -> MeasurementSet | tuple[MeasurementSet, ...]: """ Get measurement set by index. Parameters ---------- index : int or slice Index or slice to access measurement sets. Returns ------- MeasurementSet or tuple[MeasurementSet, ...] MeasurementSet at the given index, or tuple for slice. Examples -------- >>> exp[0] <MeasurementSet at 0x...> >>> exp[0:2] (<MeasurementSet at 0x...>, <MeasurementSet at 0x...>) """ return self._measurement_sets[index]