Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions lambda/src/data_hub_lambda/spectramax_plate_reader/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,13 @@ def parse_raw_well_data(file_path: Path) -> pd.DataFrame:
for row_idx in range(num_rows):
row_fields = lines[i].split("\t")

if row_idx == 0:
time_str = row_fields[0].strip()
time_val = time_str if time_str else None
temp_str = row_fields[1].strip()
temp_val = float(temp_str) if temp_str else None
# SoftMax Pro writes elapsed time / temperature on the first
# populated row of each reading group. When leading rows are
# unselected (e.g. edge wells skipped), that is not row A.
if time_val is None and row_fields and row_fields[0].strip():
time_val = row_fields[0].strip()
if temp_val is None and len(row_fields) > 1 and row_fields[1].strip():
temp_val = float(row_fields[1].strip())

row_label = _ROW_LABELS[row_idx]

Expand Down
Binary file not shown.
10 changes: 10 additions & 0 deletions lambda/tests/spectramax_plate_reader/test_parse_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ def test_kinetic_absorbance(self) -> None:
"wavelengths": ["595"],
}

def test_kinetic_edge_wells_skipped(self) -> None:
result = parse_metadata(
_FIXTURES_DIR / "spectramax_plate_reader_kinetic_edge_wells_skipped.xls"
)
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Kinetic",
"wavelengths": ["595"],
}

def test_endpoint_flat(self) -> None:
result = parse_metadata(_FIXTURES_DIR / "spectramax_plate_reader_endpoint_flat.xls")
assert result == {
Expand Down
51 changes: 51 additions & 0 deletions lambda/tests/spectramax_plate_reader/test_parse_raw_well_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,57 @@ def test_plate5_row_count(self) -> None:
assert len(self.df[self.df["plate_name"] == "Plate5"]) == 241 * 96


class TestKineticEdgeWellsSkipped:
"""Kinetic / Absorbance / 595 nm with edge wells unselected.

SoftMax Pro leaves rows A/H and columns 1/12 empty and writes elapsed
time + temperature on the first populated row (B), not row A. The parser
must still attach those fields to every well in the reading group.
"""

@pytest.fixture(autouse=True)
def _load(self) -> None:
self.df = parse_raw_well_data(
_FIXTURES_DIR / "spectramax_plate_reader_kinetic_edge_wells_skipped.xls"
)

def test_columns(self) -> None:
assert list(self.df.columns) == _EXPECTED_COLUMNS

def test_shape(self) -> None:
# 2 time points × 6 rows (B–G) × 9 cols (2–10)
assert self.df.shape == (108, 8)

def test_time_points(self) -> None:
times = self.df["time"].unique().tolist()
assert times == ["00:00:00", "00:15:01"]

def test_time_and_temperature_not_null(self) -> None:
assert bool(self.df["time"].notna().all())
assert bool(self.df["temperature_c"].notna().all())

def test_wells_exclude_edges(self) -> None:
wells = set(self.df["well_position"])
assert "A1" not in wells
assert "H12" not in wells
assert wells == {f"{r}{c}" for r in "BCDEFG" for c in range(2, 11)}

def test_first_well(self) -> None:
first = self.df.iloc[0]
assert first["time"] == "00:00:00"
assert first["well_position"] == "B2"
assert first["temperature_c"] == pytest.approx(30.2)
assert first["value"] == pytest.approx(0.1020)

def test_second_time_point(self) -> None:
t1 = self.df[self.df["time"] == "00:15:01"]
assert len(t1) == 54
first = t1.iloc[0]
assert first["well_position"] == "B2"
assert first["temperature_c"] == pytest.approx(30.0)
assert first["value"] == pytest.approx(0.1120)


class TestEndpointFlat:
"""Endpoint / Absorbance / 595 nm — flat layout where all 96 wells appear
on a single data line with well-position column headers (A1, A2, …, H12).
Expand Down