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
11 changes: 7 additions & 4 deletions lambda/src/data_hub_lambda/spectramax_plate_reader/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,20 @@ def _parse_column_layout(col_header_line: str) -> _ColumnLayout:
raise ValueError("Could not determine column layout from header row")


def parse_metadata(file_path: Path) -> dict[str, str]:
def parse_metadata(file_path: Path) -> dict[str, object]:
"""Extract measurement metadata from a SpectraMax `.xls` file.

Returns:
A dict with keys `measurement_mode`, `measurement_type`, and
`wavelength`. Example::
`wavelengths`. Wavelengths are returned as a list of numeric
strings (without the ``nm`` suffix) to mirror the shape used by
other multi-wavelength instruments (e.g. Azure 600 Gel Doc) and
let the UI layer own display formatting. Example::

{
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "750 nm",
"wavelengths": ["750", "700", "650", "600"],
}

Raises:
Expand Down Expand Up @@ -223,7 +226,7 @@ def parse_metadata(file_path: Path) -> dict[str, str]:
return {
"measurement_mode": header.measurement_mode,
"measurement_type": header.measurement_type,
"wavelength": ", ".join(f"{w} nm" for w in wavelengths),
"wavelengths": [str(w) for w in wavelengths],
}

raise ValueError(f"No 'Plate:' header line found in {file_path}")
Expand Down
8 changes: 4 additions & 4 deletions lambda/tests/integration/test_lambda_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class TestSpectraMaxHappyPath:
{
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "750 nm",
"wavelengths": ["750"],
},
id="endpoint",
),
Expand All @@ -121,7 +121,7 @@ class TestSpectraMaxHappyPath:
{
"measurement_mode": "Absorbance",
"measurement_type": "Well Scan",
"wavelength": "595 nm",
"wavelengths": ["595"],
},
id="well-scan",
),
Expand All @@ -131,7 +131,7 @@ class TestSpectraMaxHappyPath:
{
"measurement_mode": "Absorbance",
"measurement_type": "Kinetic",
"wavelength": "595 nm",
"wavelengths": ["595"],
},
id="kinetic",
),
Expand All @@ -147,7 +147,7 @@ def test_xls_completes_with_processed_csv(
mock_s3_upload: MagicMock,
fixture_file: str,
run_id: str,
expected_metadata: dict[str, str],
expected_metadata: dict[str, object],
) -> None:
filename = f"{run_id}.xls"
s3_key = f"spectramax-id3-plate-reader/{run_id}/{filename}"
Expand Down
29 changes: 19 additions & 10 deletions lambda/tests/spectramax_plate_reader/test_parse_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,47 +77,47 @@ def test_endpoint_absorbance(self) -> None:
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "750 nm",
"wavelengths": ["750"],
}

def test_well_scan_absorbance(self) -> None:
result = parse_metadata(_FIXTURES_DIR / "spectramax_plate_reader_well_scan.xls")
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Well Scan",
"wavelength": "595 nm",
"wavelengths": ["595"],
}

def test_endpoint_fluorescence(self) -> None:
result = parse_metadata(_FIXTURES_DIR / "spectramax_plate_reader_fluorescence.xls")
assert result == {
"measurement_mode": "Fluorescence",
"measurement_type": "Endpoint",
"wavelength": "512 nm",
"wavelengths": ["512"],
}

def test_endpoint_sparse_absorbance(self) -> None:
result = parse_metadata(_FIXTURES_DIR / "spectramax_plate_reader_endpoint_sparse.xls")
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "600 nm",
"wavelengths": ["600"],
}

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

def test_endpoint_flat(self) -> None:
result = parse_metadata(_FIXTURES_DIR / "spectramax_plate_reader_endpoint_flat.xls")
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "595 nm",
"wavelengths": ["595"],
}


Expand All @@ -138,7 +138,7 @@ def test_absorbance_endpoint(self, tmp_path: Path) -> None:
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "750 nm",
"wavelengths": ["750"],
}

def test_fluorescence_kinetic(self, tmp_path: Path) -> None:
Expand All @@ -152,7 +152,7 @@ def test_fluorescence_kinetic(self, tmp_path: Path) -> None:
assert result == {
"measurement_mode": "Fluorescence",
"measurement_type": "Kinetic",
"wavelength": "488 nm",
"wavelengths": ["488"],
}

def test_reduced_anchor(self, tmp_path: Path) -> None:
Expand All @@ -167,7 +167,7 @@ def test_reduced_anchor(self, tmp_path: Path) -> None:
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "600 nm",
"wavelengths": ["600"],
}


Expand Down Expand Up @@ -198,7 +198,16 @@ def test_dual_wavelength(self, tmp_path: Path) -> None:
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelength": "750 nm, 600 nm",
"wavelengths": ["750", "600"],
}

def test_four_wavelengths(self, tmp_path: Path) -> None:
path = _build_xls(tmp_path, wavelength="750 700 650 600")
result = parse_metadata(path)
assert result == {
"measurement_mode": "Absorbance",
"measurement_type": "Endpoint",
"wavelengths": ["750", "700", "650", "600"],
}

def test_missing_plate_header(self, tmp_path: Path) -> None:
Expand Down
16 changes: 11 additions & 5 deletions web-app/components/instruments/runs-table/gel-doc-runs-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import type { RunRow } from ".";
import { ClickableRow } from "./clickable-row";
import { FilterableColumnHeader } from "./filterable-column-header";
import {
MetadataArrayBadges,
MetadataFieldBadge,
TruncatedBadges,
getMetadataArray,
getMetadataField,
sortWavelengths,
} from "./metadata-utils";
import { RanByCell } from "./ran-by-cell";
import { RunSelectAllCheckbox, RunSelectCheckbox } from "./run-select-checkbox";
Expand All @@ -46,6 +47,7 @@ export function GelDocRunsTable({
getMetadataArray(row.metadata, "wavelengths")
);
const wavelengthColors = buildWavelengthColorMap(allWavelengths);
const sortedWavelengthOptions = sortWavelengths(filterOptions.wavelengths);
const runRefs: RunRef[] = data.map((row) => ({
id: row.id,
instrumentId: row.instrument_id,
Expand Down Expand Up @@ -80,7 +82,7 @@ export function GelDocRunsTable({
<FilterableColumnHeader
label="Wavelengths"
paramKey="gel_wavelength"
options={filterOptions.wavelengths}
options={sortedWavelengthOptions}
/>
</TableHead>
<TableHead>
Expand All @@ -105,7 +107,9 @@ export function GelDocRunsTable({
const isDeleted = row.deleted_at !== null;
const captureType = getMetadataField(row.metadata, "capture_type");
const imagingMode = getMetadataField(row.metadata, "imaging_mode");
const wavelengths = getMetadataArray(row.metadata, "wavelengths");
const wavelengths = sortWavelengths(
getMetadataArray(row.metadata, "wavelengths")
);
const wavelengthColorLabels = getMetadataArray(
row.metadata,
"colors"
Expand Down Expand Up @@ -166,15 +170,17 @@ export function GelDocRunsTable({
/>
</TableCell>
<TableCell>
<MetadataArrayBadges
<TruncatedBadges
values={wavelengths}
colorMap={wavelengthColors}
maxVisible={1}
/>
</TableCell>
<TableCell>
<MetadataArrayBadges
<TruncatedBadges
values={wavelengthColorLabels}
colorMap={CHANNEL_COLOR_STYLES}
maxVisible={2}
/>
</TableCell>
<TableCell>
Expand Down
2 changes: 2 additions & 0 deletions web-app/components/instruments/runs-table/metadata-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export {
MetadataArrayBadges,
MetadataFieldBadge,
TruncatedBadges,
getMetadataArray,
getMetadataField,
sortWavelengths,
} from "@/components/runs/metadata-badges";
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import { cn, formatBytes } from "@/lib/utils";
import type { RunRow } from ".";
import { ClickableRow } from "./clickable-row";
import { FilterableColumnHeader } from "./filterable-column-header";
import { MetadataFieldBadge, getMetadataField } from "./metadata-utils";
import {
MetadataFieldBadge,
TruncatedBadges,
getMetadataArray,
getMetadataField,
sortWavelengths,
} from "./metadata-utils";
import { RanByCell } from "./ran-by-cell";
import { RunSelectAllCheckbox, RunSelectCheckbox } from "./run-select-checkbox";
import type { RunRef } from "./run-selection-provider";
Expand All @@ -37,6 +43,7 @@ export function PlateReaderRunsTable({
ranByOptions: { value: string; label: string }[];
}) {
const wavelengthColors = buildWavelengthColorMap(filterOptions.wavelengths);
const sortedWavelengthOptions = sortWavelengths(filterOptions.wavelengths);
const runRefs: RunRef[] = data.map((row) => ({
id: row.id,
instrumentId: row.instrument_id,
Expand All @@ -55,9 +62,9 @@ export function PlateReaderRunsTable({
<TableHead className="text-right">Total Size</TableHead>
<TableHead>
<FilterableColumnHeader
label="Wavelength"
label="Wavelengths"
paramKey="wavelength"
options={filterOptions.wavelengths}
options={sortedWavelengthOptions}
/>
</TableHead>
<TableHead>
Expand Down Expand Up @@ -87,7 +94,9 @@ export function PlateReaderRunsTable({
<TableBody>
{data.map((row) => {
const isDeleted = row.deleted_at !== null;
const wavelength = getMetadataField(row.metadata, "wavelength");
const wavelengths = sortWavelengths(
getMetadataArray(row.metadata, "wavelengths")
);
const mode = getMetadataField(row.metadata, "measurement_mode");
const type = getMetadataField(row.metadata, "measurement_type");
return (
Expand Down Expand Up @@ -130,11 +139,10 @@ export function PlateReaderRunsTable({
{formatBytes(row.total_size_bytes)}
</TableCell>
<TableCell>
<MetadataFieldBadge
value={wavelength}
colorClass={
wavelength ? wavelengthColors[wavelength] : undefined
}
<TruncatedBadges
values={wavelengths}
colorMap={wavelengthColors}
maxVisible={1}
/>
</TableCell>
<TableCell>
Expand Down
Loading
Loading