Skip to content

Commit 27aee9c

Browse files
feat: Add Diomni v4.2/4.3 file support to QuantStudio Design & Analysis parser (#1224)
## Summary - Adds support for files exported by Diomni software (v4.2/4.3), which use `Primary_result` sheet instead of `Results`, alphanumeric well identifiers (A01 format), separate `Software name`/`Software version` header fields, and multicomponent data with partial dye columns - Normalizes alphanumeric wells to numeric IDs with a stable sorted mapping across all sheets, preserving well position as a separate column - Gracefully handles missing reporter/passive dye columns in multicomponent data instead of raising an error ## Test plan - [x] New test file: `appbio_quantstudio_designandanalysis_Diomni_v4.3_Presence_Absence.xlsx` with expected JSON output - [x] All 29 parser tests pass (28 existing + 1 new) - [x] All 316 discover_vendor tests pass (no sniff conflicts) - [x] Lint clean (ruff, black, mypy) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 736fd27 commit 27aee9c

6 files changed

Lines changed: 1866 additions & 6 deletions

File tree

src/allotropy/parsers/appbio_quantstudio_designandanalysis/appbio_quantstudio_designandanalysis_data_creator.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,22 @@ def _create_melt_curve_data_cube(
166166

167167
def _create_measurement(well: Well, well_item: WellItem, data: Data) -> Measurement:
168168
header = data.header
169+
multicomponent = well.multicomponent_data
170+
reporter_dye = well_item.reporter_dye_setting
171+
passive_dye = header.passive_reference_dye_setting
172+
# Only request dye columns that actually exist in multicomponent data
173+
if multicomponent is not None:
174+
if reporter_dye is not None and reporter_dye not in multicomponent.columns:
175+
reporter_dye = None
176+
if passive_dye is not None and passive_dye not in multicomponent.columns:
177+
passive_dye = None
169178
(
170179
reporter_dye_data_cube,
171180
passive_reference_dye_data_cube,
172181
) = _create_multicomponent_data_cubes(
173-
well.multicomponent_data,
174-
well_item.reporter_dye_setting,
175-
header.passive_reference_dye_setting,
182+
multicomponent,
183+
reporter_dye,
184+
passive_dye,
176185
)
177186
return Measurement(
178187
identifier=well_item.uuid,

src/allotropy/parsers/appbio_quantstudio_designandanalysis/appbio_quantstudio_designandanalysis_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def sniff(cls, named_file_contents: NamedFileContents) -> bool:
4141
)
4242
sheet_names = set(wb.sheetnames)
4343
wb.close()
44-
return "Results" in sheet_names
44+
return "Results" in sheet_names or "Primary_result" in sheet_names
4545
except Exception:
4646
return False
4747

src/allotropy/parsers/appbio_quantstudio_designandanalysis/appbio_quantstudio_designandanalysis_reader.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
assert_not_none,
1717
)
1818

19+
RESULTS_SHEET = "Results"
20+
PRIMARY_RESULT_SHEET = "Primary_result"
21+
1922

2023
class DesignQuantstudioReader:
2124
SUPPORTED_EXTENSIONS = "xlsx,xls"
@@ -36,12 +39,14 @@ def create(named_file_contents: NamedFileContents) -> DesignQuantstudioReader:
3639

3740
def __init__(self, contents: dict[str, pd.DataFrame]) -> None:
3841
self.contents = contents
42+
if PRIMARY_RESULT_SHEET in contents and RESULTS_SHEET not in contents:
43+
contents[RESULTS_SHEET] = contents.pop(PRIMARY_RESULT_SHEET)
3944
self.header = self._get_header(contents)
4045
self.data = self._get_data(contents)
4146

4247
def _get_header(self, contents: dict[str, pd.DataFrame]) -> SeriesData:
4348
sheet = assert_not_none(
44-
contents.get("Results"),
49+
contents.get(RESULTS_SHEET),
4550
msg="Unable to find 'Results' sheet.",
4651
)
4752
df, _ = split_header_and_data(sheet, lambda row: row[0] is None)
@@ -52,8 +57,29 @@ def _get_data(self, contents: dict[str, pd.DataFrame]) -> dict[str, pd.DataFrame
5257
for name, sheet in contents.items():
5358
_, data = split_header_and_data(sheet, lambda row: row[0] is None)
5459
data_structure[name] = parse_header_row(data.replace(np.nan, None))
60+
self._normalize_well_columns(data_structure)
5561
return data_structure
5662

63+
def _normalize_well_columns(self, data_structure: dict[str, pd.DataFrame]) -> None:
64+
results = data_structure.get(RESULTS_SHEET)
65+
if results is None or "Well" not in results.columns:
66+
return
67+
first_well = results["Well"].iloc[0] if not results.empty else None
68+
if first_well is None or isinstance(first_well, int | float):
69+
return
70+
# Alphanumeric wells — build a stable numeric mapping and add Well Position
71+
all_wells: list[str] = []
72+
for df in data_structure.values():
73+
if "Well" in df.columns:
74+
all_wells.extend(df["Well"].dropna().unique().tolist())
75+
unique_wells = sorted({str(w) for w in all_wells})
76+
well_to_id = {well: idx + 1 for idx, well in enumerate(unique_wells)}
77+
for df in data_structure.values():
78+
if "Well" in df.columns:
79+
if "Well Position" not in df.columns:
80+
df.insert(1, "Well Position", df["Well"].astype(str))
81+
df["Well"] = df["Well"].map(well_to_id)
82+
5783
def has_sheet(self, sheet_name: str) -> bool:
5884
return sheet_name in self.data
5985

src/allotropy/parsers/appbio_quantstudio_designandanalysis/structure/generic/structure.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ def create(header: SeriesData) -> Header:
107107
software_name, software_version = (
108108
(software_info.group(1), software_info.group(2))
109109
if software_info
110-
else (None, None)
110+
else (
111+
header.get(str, "Software name"),
112+
header.get(str, "Software version"),
113+
)
111114
)
112115

113116
stage_number_raw = header.get(str, "PCR Stage/Step Number", "")

0 commit comments

Comments
 (0)