Skip to content

Commit 24f3b11

Browse files
feat: Cytiva Biacore T200 - Refactor to use JsonData (#1095)
Basically replaced DictType with JsonData, which keeps track of which keys have been called and create a warning when the object is deleted without having read all keys --------- Co-authored-by: james-leinas <157071641+james-leinas@users.noreply.github.com>
1 parent f0b0562 commit 24f3b11

5 files changed

Lines changed: 471 additions & 78 deletions

File tree

src/allotropy/allotrope/converter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"=": "_EQUALS_",
113113
"@": "_AT_",
114114
"'": "_QUOTE_",
115+
",": "_COMMA_",
115116
# NOTE: this MUST be at the end, or it will break other key replacements.
116117
" ": "_",
117118
}

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Data,
2020
)
2121
from allotropy.parsers.release_state import ReleaseState
22+
from allotropy.parsers.utils.dict_data import DictData
2223
from allotropy.parsers.vendor_parser import VendorParser
2324

2425

@@ -29,7 +30,7 @@ class CytivaBiacoreT200ControlParser(VendorParser[MapperData, Model]):
2930
SCHEMA_MAPPER = Mapper
3031

3132
def create_data(self, named_file_contents: NamedFileContents) -> MapperData:
32-
data = Data.create(decode_data(named_file_contents))
33+
data = Data.create(DictData(decode_data(named_file_contents)))
3334
return MapperData(
3435
metadata=create_metadata(data, named_file_contents),
3536
measurement_groups=create_measurement_groups(data),

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_structure.py

Lines changed: 94 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from collections import defaultdict
44
from dataclasses import dataclass, field
5+
from typing import Any
56

67
import pandas as pd
78

@@ -13,12 +14,12 @@
1314
)
1415
from allotropy.parsers.constants import NOT_APPLICABLE
1516
from allotropy.parsers.cytiva_biacore_t200_control import constants
17+
from allotropy.parsers.utils.dict_data import DictData
1618
from allotropy.parsers.utils.pandas import map_rows, SeriesData
1719
from allotropy.parsers.utils.uuids import random_uuid_str
1820
from allotropy.parsers.utils.values import (
1921
assert_not_none,
2022
try_float_or_none,
21-
try_int_or_none,
2223
)
2324
from allotropy.types import DictType
2425

@@ -30,32 +31,32 @@ class ChipData:
3031
number_of_flow_cells: int | None
3132
number_of_spots: int | None
3233
lot_number: str | None
33-
custom_info: DictType
34+
custom_info: dict[str, Any]
3435

3536
@staticmethod
36-
def create(chip_data: DictType) -> ChipData:
37+
def create(chip_data: DictData) -> ChipData:
3738
return ChipData(
38-
sensor_chip_identifier=assert_not_none(chip_data.get("Id"), "Chip ID"),
39-
sensor_chip_type=chip_data.get("Name"),
40-
number_of_flow_cells=try_int_or_none(chip_data.get("NoFcs")),
41-
number_of_spots=try_int_or_none(chip_data.get("NoSpots")),
42-
lot_number=lot_no if (lot_no := chip_data.get("LotNo")) else None,
39+
sensor_chip_identifier=assert_not_none(chip_data.get(str, "Id"), "Chip ID"),
40+
sensor_chip_type=chip_data.get(str, "Name"),
41+
number_of_flow_cells=chip_data.get(int, "NoFcs"),
42+
number_of_spots=chip_data.get(int, "NoSpots"),
43+
lot_number=lot_no if (lot_no := chip_data.get(str, "LotNo")) else None,
4344
custom_info={
44-
"ifc identifier": chip_data.get("IFC"),
45-
"last modified time": chip_data.get("LastModTime"),
46-
"last use time": chip_data.get("LastUseTime"),
47-
"first dock date": chip_data.get("FirstDockDate"),
45+
"ifc identifier": chip_data.get(str, "IFC"),
46+
"last modified time": chip_data.get(str, "LastModTime"),
47+
"last use time": chip_data.get(str, "LastUseTime"),
48+
"first dock date": chip_data.get(str, "FirstDockDate"),
4849
},
4950
)
5051

5152

5253
@dataclass(frozen=True)
5354
class DetectionSetting:
5455
key: str
55-
value: float
56+
value: float | None
5657

5758
@staticmethod
58-
def create(detection_setting: DictType) -> DetectionSetting:
59+
def create(detection_setting: DictData) -> DetectionSetting:
5960
detection_key = f"Detection{detection_setting['Detection']}"
6061
return DetectionSetting(
6162
key=detection_key.lower(),
@@ -81,35 +82,48 @@ class RunMetadata:
8182

8283
@staticmethod
8384
def create(
84-
application_template_details: dict[str, DictType] | None,
85+
application_template_details: DictData | None,
8586
) -> RunMetadata:
8687
if application_template_details is None:
8788
return RunMetadata()
89+
baseline_flow = application_template_details.get_nested("BaselineFlow")
90+
baseline_flow_value = baseline_flow.get("value")
91+
data_collection_rate = application_template_details.get_nested(
92+
"DataCollectionRate"
93+
)
94+
data_collection_rate_value = data_collection_rate.get("value")
8895
return RunMetadata(
89-
analyst=application_template_details["properties"].get("User"),
96+
analyst=application_template_details.get(
97+
DictData, "properties", DictData({})
98+
).get(str, "User"),
9099
compartment_temperature=try_float_or_none(
91-
application_template_details["RackTemperature"].get("Value")
100+
application_template_details.get_nested("RackTemperature")["Value"]
92101
if "sample_data" in application_template_details
93-
else application_template_details["system_preparations"].get("RackTemp")
94-
),
95-
baseline_flow=try_float_or_none(
96-
application_template_details.get("BaselineFlow", {}).get("value")
97-
),
98-
data_collection_rate=try_float_or_none(
99-
application_template_details.get("DataCollectionRate", {}).get("value")
102+
else application_template_details.get(
103+
DictData,
104+
"system_preparations",
105+
DictData({}),
106+
).get(str, "RackTemp")
100107
),
108+
baseline_flow=try_float_or_none(baseline_flow_value),
109+
data_collection_rate=try_float_or_none(data_collection_rate_value),
101110
detection_setting=(
102-
DetectionSetting.create(detection_setting)
103-
if (detection_setting := application_template_details.get("detection"))
111+
DetectionSetting.create(
112+
application_template_details.get_nested("detection")
113+
)
114+
if application_template_details.get_nested("detection")
104115
else None
105116
),
106117
buffer_volume=try_float_or_none(
107118
next(
108-
value
109-
for key, value in application_template_details[
110-
"prepare_run"
111-
].items()
112-
if key.startswith("Buffer")
119+
(
120+
value
121+
for key, value in application_template_details.get_nested(
122+
"prepare_run"
123+
).items()
124+
if key.startswith("Buffer")
125+
),
126+
None,
113127
)
114128
),
115129
devices=[
@@ -131,21 +145,22 @@ class SystemInformation:
131145
software_version: str | None
132146

133147
@staticmethod
134-
def create(system_information: DictType) -> SystemInformation:
148+
def create(system_information: DictData) -> SystemInformation:
135149
return SystemInformation(
136150
device_identifier=assert_not_none(
137-
system_information.get("InstrumentId"), "InstrumentId"
151+
system_information.get(str, "InstrumentId"), "InstrumentId"
138152
),
139153
model_number=assert_not_none(
140-
system_information.get("ProcessingUnit"), "ProcessingUnit"
154+
system_information.get(str, "ProcessingUnit"),
155+
"ProcessingUnit",
141156
),
142157
measurement_time=assert_not_none(
143-
system_information.get("Timestamp"), "Timestamp"
158+
system_information.get(str, "Timestamp"), "Timestamp"
144159
),
145-
experiment_type=system_information.get("RunTypeId"),
146-
analytical_method_identifier=system_information.get("TemplateFile"),
147-
software_name=system_information.get("Application"),
148-
software_version=system_information.get("Version"),
160+
experiment_type=system_information.get(str, "RunTypeId"),
161+
analytical_method_identifier=system_information.get(str, "TemplateFile"),
162+
software_name=system_information.get(str, "Application"),
163+
software_version=system_information.get(str, "Version"),
149164
)
150165

151166

@@ -212,34 +227,39 @@ class MeasurementData:
212227
flow_rate: float | None
213228
contact_time: float | None
214229
dilution: float | None
230+
custom_info: dict[str, Any]
215231

216232

217233
@dataclass(frozen=True)
218234
class SampleData:
219235
measurements: dict[str, list[MeasurementData]]
236+
custom_info: dict[str, Any]
220237

221238
@staticmethod
222-
def create(intermediate_structured_data: DictType) -> SampleData:
223-
application_template_details: dict[
224-
str, DictType
225-
] = intermediate_structured_data.get("application_template_details", {})
239+
def create(intermediate_structured_data: DictData) -> SampleData:
240+
application_template_details = intermediate_structured_data.get_nested(
241+
"application_template_details"
242+
)
226243
measurements: dict[str, list[MeasurementData]] = defaultdict(list)
227-
for idx in range(intermediate_structured_data["total_cycles"]):
228-
flowcell_cycle_data: DictType = application_template_details.get(
229-
f"Flowcell {idx + 1}", {}
230-
)
231-
sample_data: DictType = (
232-
sd[idx]
233-
if (sd := intermediate_structured_data.get("sample_data"))
234-
else {}
244+
total_cycles = assert_not_none(
245+
intermediate_structured_data.get(int, "total_cycles"),
246+
"total_cycles",
247+
)
248+
for idx in range(total_cycles):
249+
flowcell_cycle_json = application_template_details.get_nested(
250+
f"Flowcell {idx + 1}"
235251
)
236-
cycle_data: DictType = intermediate_structured_data["cycle_data"][idx]
252+
sd_list = intermediate_structured_data.get(list, "sample_data", [])
253+
sample_data_json = sd_list[idx] if sd_list else DictData({})
254+
255+
cycle_data_list = intermediate_structured_data.get(list, "cycle_data", [])
256+
cycle_data: dict[str, pd.DataFrame] = cycle_data_list[idx]
237257
sensorgram_data: pd.DataFrame = cycle_data["sensorgram_data"]
238258
# some experiments don't have report point data for some cycles (apparently just the first one)
239259
report_point_data: pd.DataFrame | None = cycle_data["report_point_data"]
240260

241-
sample_identifier = sample_data.get("sample_name", NOT_APPLICABLE)
242-
location_identifier = sample_data.get("rack")
261+
sample_identifier = sample_data_json.get(str, "sample_name", NOT_APPLICABLE)
262+
location_identifier = sample_data_json.get(str, "rack")
243263
sample_location_key = f"{location_identifier}_{sample_identifier}"
244264

245265
# Measurements are grouped by sample and location identifiers
@@ -252,12 +272,10 @@ def create(intermediate_structured_data: DictType) -> SampleData:
252272
flow_cell_identifier=str(flow_cell),
253273
location_identifier=location_identifier,
254274
sample_role_type=constants.SAMPLE_ROLE_TYPE.get(
255-
sample_data.get("role", "__IVALID_KEY__")
256-
),
257-
concentration=try_float_or_none(sample_data.get("concentration")),
258-
molecular_weight=try_float_or_none(
259-
sample_data.get("molecular_weight")
275+
sample_data_json.get(str, "role", "__IVALID_KEY__")
260276
),
277+
concentration=sample_data_json.get(float, "concentration"),
278+
molecular_weight=sample_data_json.get(float, "molecular_weight"),
261279
sensorgram_data=sensorgram_df,
262280
report_point_data=(
263281
map_rows(
@@ -268,23 +286,21 @@ def create(intermediate_structured_data: DictType) -> SampleData:
268286
else None
269287
),
270288
# for Mobilization experiments
271-
method_name=flowcell_cycle_data.get("MethodName"),
272-
ligand_identifier=flowcell_cycle_data.get("Ligand"),
273-
flow_path=flowcell_cycle_data.get("DetectionText"),
274-
flow_rate=try_float_or_none(flowcell_cycle_data.get("Flow")),
275-
contact_time=try_float_or_none(
276-
flowcell_cycle_data.get("ContactTime")
277-
),
278-
dilution=try_float_or_none(
279-
flowcell_cycle_data.get("DilutePercent")
280-
),
289+
method_name=flowcell_cycle_json.get(str, "MethodName"),
290+
ligand_identifier=flowcell_cycle_json.get(str, "Ligand"),
291+
flow_path=flowcell_cycle_json.get(str, "DetectionText"),
292+
flow_rate=flowcell_cycle_json.get(float, "Flow"),
293+
contact_time=flowcell_cycle_json.get(float, "ContactTime"),
294+
dilution=flowcell_cycle_json.get(float, "DilutePercent"),
295+
custom_info={},
281296
)
282297
# group sensorgram data by Flow Cell Number (Fc in rpoint data)
283298
for flow_cell, sensorgram_df in sensorgram_data.groupby(
284299
"Flow Cell Number"
285300
)
286301
]
287-
return SampleData(measurements)
302+
custom_info: dict[str, Any] = {}
303+
return SampleData(measurements, custom_info)
288304

289305

290306
@dataclass(frozen=True)
@@ -295,14 +311,15 @@ class Data:
295311
sample_data: SampleData
296312

297313
@staticmethod
298-
def create(intermediate_structured_data: DictType) -> Data:
299-
application_template_details: dict[
300-
str, DictType
301-
] | None = intermediate_structured_data.get("application_template_details")
302-
chip_data: DictType = intermediate_structured_data["chip"]
303-
system_information: DictType = intermediate_structured_data[
314+
def create(intermediate_structured_data: DictData) -> Data:
315+
application_template_details = intermediate_structured_data.get_nested(
316+
"application_template_details"
317+
)
318+
chip_data = intermediate_structured_data.get_nested("chip")
319+
system_information = intermediate_structured_data.get_nested(
304320
"system_information"
305-
]
321+
)
322+
306323
return Data(
307324
run_metadata=RunMetadata.create(application_template_details),
308325
chip_data=ChipData.create(chip_data),

0 commit comments

Comments
 (0)