Skip to content

Commit da785a7

Browse files
authored
feat: Qiacuity dPCR - Added unread data (#1103)
1 parent cca387f commit da785a7

8 files changed

Lines changed: 1994 additions & 19 deletions

File tree

src/allotropy/allotrope/schema_mappers/adm/pcr/BENCHLING/_2023/_09/dpcr.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class Measurement:
9090
errors: list[Error] | None = None
9191

9292
# custom
93+
sample_custom_info: dict[str, Any] | None = None
9394
custom_info: dict[str, Any] | None = None
9495

9596

@@ -182,6 +183,15 @@ def _get_technique_document(
182183
def _get_measurement_document(
183184
self, measurement: Measurement, metadata: Metadata
184185
) -> MeasurementDocumentItem:
186+
sample_document = SampleDocument(
187+
sample_identifier=measurement.sample_identifier,
188+
well_location_identifier=measurement.location_identifier,
189+
well_plate_identifier=measurement.plate_identifier,
190+
sample_role_type=measurement.sample_role_type,
191+
)
192+
sample_document = add_custom_information_document(
193+
sample_document, measurement.sample_custom_info
194+
)
185195
measurement_doc = MeasurementDocumentItem(
186196
measurement_identifier=measurement.identifier,
187197
measurement_time=self.get_date_time(measurement.measurement_time),
@@ -198,12 +208,7 @@ def _get_measurement_document(
198208
error_aggregate_document=self._get_error_aggregate_document(
199209
measurement.errors
200210
),
201-
sample_document=SampleDocument(
202-
sample_identifier=measurement.sample_identifier,
203-
well_location_identifier=measurement.location_identifier,
204-
well_plate_identifier=measurement.plate_identifier,
205-
sample_role_type=measurement.sample_role_type,
206-
),
211+
sample_document=sample_document,
207212
device_control_aggregate_document=DeviceControlAggregateDocument(
208213
device_control_document=[
209214
DeviceControlDocumentItem(
@@ -244,6 +249,7 @@ def _get_measurement_document(
244249
]
245250
),
246251
)
252+
247253
return add_custom_information_document(measurement_doc, measurement.custom_info)
248254

249255
def _get_calculated_data_aggregate_document(

src/allotropy/parsers/qiacuity_dpcr/constants.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
CALCULATED_DATA_CONFIGS: list[dict[str, str]] = [
2+
{
3+
"name": "CI (95%)",
4+
"keys": "CI (95%)",
5+
"unit": "%",
6+
"feature": "Positive Partition Count",
7+
},
8+
{
9+
"name": "SD",
10+
"keys": "SD",
11+
"unit": "(unitless)",
12+
"feature": "Number Concentration",
13+
},
14+
{
15+
"name": "CV%",
16+
"keys": "CV%",
17+
"unit": "%",
18+
"feature": "Number Concentration (#/μL)",
19+
},
20+
{
21+
"name": "Mean Concentration",
22+
"keys": "Mean conc. [copies/μL]",
23+
"unit": "#/μL",
24+
"feature": "Mean Concentration (#/μL)",
25+
},
26+
]
27+
128
BRAND_NAME = "Qiacuity Digital PCR System"
229
PRODUCT_MANUFACTURER = "Qiagen"
330
SOFTWARE_NAME = "Qiacuity Software Suite"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
4+
5+
from allotropy.allotrope.schema_mappers.adm.pcr.BENCHLING._2023._09.dpcr import (
6+
CalculatedDataItem,
7+
DataSource,
8+
)
9+
from allotropy.parsers.qiacuity_dpcr.constants import CALCULATED_DATA_CONFIGS
10+
from allotropy.parsers.utils.calculated_data_documents.definition import (
11+
CalculatedDocument as UtilsCalculatedDocument,
12+
DataSource as UtilsDataSource,
13+
Referenceable as UtilsReferenceable,
14+
)
15+
from allotropy.parsers.utils.pandas import SeriesData
16+
from allotropy.parsers.utils.uuids import random_uuid_str
17+
18+
19+
def _iter_row_calculated_docs(row: SeriesData) -> Iterable[UtilsCalculatedDocument]:
20+
measurement_identifier = row.get(str, "_measurement_identifier")
21+
if not measurement_identifier:
22+
return []
23+
24+
docs: list[UtilsCalculatedDocument] = []
25+
26+
measurement_ref = UtilsReferenceable(uuid=measurement_identifier)
27+
28+
for conf in CALCULATED_DATA_CONFIGS:
29+
value = row.get(float, conf["keys"])
30+
if value is None:
31+
continue
32+
docs.append(
33+
UtilsCalculatedDocument(
34+
uuid=random_uuid_str(),
35+
name=conf["name"],
36+
value=float(value),
37+
unit=conf["unit"],
38+
data_sources=[
39+
UtilsDataSource(
40+
feature=conf.get("feature", conf["name"]),
41+
reference=measurement_ref,
42+
)
43+
],
44+
)
45+
)
46+
47+
return docs
48+
49+
50+
def _docs_to_benchling_items(
51+
docs: Iterable[UtilsCalculatedDocument],
52+
) -> list[CalculatedDataItem]:
53+
items: list[CalculatedDataItem] = []
54+
for doc in docs:
55+
for flat_doc in doc.iter_struct():
56+
items.append(
57+
CalculatedDataItem(
58+
identifier=flat_doc.uuid,
59+
name=flat_doc.name,
60+
value=flat_doc.value,
61+
unit=flat_doc.unit or "",
62+
data_sources=[
63+
DataSource(
64+
identifier=ds.reference.uuid,
65+
feature=ds.feature,
66+
)
67+
for ds in flat_doc.data_sources
68+
],
69+
)
70+
)
71+
return items
72+
73+
74+
def create_calculated_data(rows: list[SeriesData]) -> list[CalculatedDataItem]:
75+
docs: list[UtilsCalculatedDocument] = []
76+
for row in rows:
77+
docs.extend(list(_iter_row_calculated_docs(row)))
78+
return _docs_to_benchling_items(docs)

src/allotropy/parsers/qiacuity_dpcr/qiacuity_dpcr_parser.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
MeasurementGroup,
88
)
99
from allotropy.named_file_contents import NamedFileContents
10+
from allotropy.parsers.qiacuity_dpcr.qiacuity_dpcr_calculated_data import (
11+
create_calculated_data as create_qiacuity_calculated_data,
12+
)
1013
from allotropy.parsers.qiacuity_dpcr.qiacuity_dpcr_reader import QiacuitydPCRReader
1114
from allotropy.parsers.qiacuity_dpcr.qiacuity_dpcr_structure import (
1215
create_measurements,
1316
create_metadata,
1417
)
1518
from allotropy.parsers.release_state import ReleaseState
16-
from allotropy.parsers.utils.pandas import map_rows
19+
from allotropy.parsers.utils.pandas import SeriesData
20+
from allotropy.parsers.utils.uuids import random_uuid_str
1721
from allotropy.parsers.vendor_parser import VendorParser
1822

1923

@@ -25,14 +29,25 @@ class QiacuitydPCRParser(VendorParser[Data, Model]):
2529

2630
def create_data(self, named_file_contents: NamedFileContents) -> Data:
2731
reader = QiacuitydPCRReader(named_file_contents)
32+
# Assign stable measurement identifiers per row for data source linkage
33+
reader.well_data["_measurement_identifier"] = [
34+
random_uuid_str() for _ in range(len(reader.well_data))
35+
]
36+
# Build SeriesData list once and reuse
37+
series_rows: list[SeriesData] = [
38+
SeriesData(row) for _, row in reader.well_data.iterrows()
39+
]
40+
calculated_data = create_qiacuity_calculated_data(series_rows)
41+
measurement_groups = [
42+
MeasurementGroup(
43+
measurements=[create_measurements(row) for row in series_rows],
44+
# TODO: Hardcoded plate well count to 0 since it's a required field
45+
# ASM will be modified to optional in future version
46+
plate_well_count=0,
47+
)
48+
]
2849
return Data(
2950
create_metadata(named_file_contents.original_file_path),
30-
measurement_groups=[
31-
MeasurementGroup(
32-
measurements=map_rows(reader.well_data, create_measurements),
33-
# TODO: Hardcoded plate well count to 0 since it's a required field
34-
# ASM will be modified to optional in future version
35-
plate_well_count=0,
36-
)
37-
],
51+
measurement_groups=measurement_groups,
52+
calculated_data=calculated_data,
3853
)

src/allotropy/parsers/qiacuity_dpcr/qiacuity_dpcr_structure.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@ def create_measurements(data: SeriesData) -> Measurement:
2929
"sample type", sample_role_type, SAMPLE_ROLE_TYPE_MAPPING
3030
)
3131

32+
identifier = data.get(str, "_measurement_identifier") or random_uuid_str()
33+
34+
sample_custom_info = data.get_custom_keys({"IC", "Control type"})
35+
for key in sample_custom_info:
36+
if sample_custom_info[key] in ("", "-", "-", "--"):
37+
sample_custom_info[key] = None
38+
3239
return Measurement(
33-
identifier=random_uuid_str(),
40+
identifier=identifier,
3441
measurement_time=DEFAULT_EPOCH_TIMESTAMP,
3542
sample_identifier=data[str, "Sample/NTC/Control"],
3643
sample_role_type=sample_role_type,
@@ -42,6 +49,8 @@ def create_measurements(data: SeriesData) -> Measurement:
4249
positive_partition_count=data[int, "Partitions (positive)"],
4350
negative_partition_count=data.get(int, "Partitions (negative)"),
4451
fluorescence_intensity_threshold_setting=data.get(float, "Threshold"),
52+
sample_custom_info=sample_custom_info,
53+
custom_info=data.get_unread(),
4554
)
4655

4756

0 commit comments

Comments
 (0)