Skip to content

Commit 167a1eb

Browse files
feat: Thermo Fisher Scientific SkanIt - add unread data (#1104)
1 parent 5106136 commit 167a1eb

5 files changed

Lines changed: 2229 additions & 15 deletions

File tree

src/allotropy/allotrope/schema_mappers/adm/plate_reader/rec/_2025/_03/plate_reader.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ class Metadata:
220220
equipment_serial_number: str | None = None
221221
product_manufacturer: str | None = None
222222
file_name: str | None = None
223-
custom_info_doc: dict[str, Any] | None = None
223+
metadata_custom_info: dict[str, Any] | None = None
224+
device_control_custom_info: dict[str, Any] | None = None
224225

225226

226227
@dataclass(frozen=True)
@@ -253,7 +254,7 @@ def map_model(self, data: Data) -> Model:
253254
ASM_converter_name=self.converter_name,
254255
ASM_converter_version=ASM_CONVERTER_VERSION,
255256
),
256-
data.metadata.custom_info_doc,
257+
data.metadata.metadata_custom_info,
257258
),
258259
plate_reader_document=[
259260
self._get_technique_document(measurement_group)

src/allotropy/parsers/thermo_skanit/thermo_skanit_structure.py

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,66 @@ def create_metadata(
8080
)
8181
general_info_data = ThermoSkanItMetadata._get_general_info_data(general_info_df)
8282
path = Path(file_path)
83+
84+
device_identifier = str(instrument_info_data.pop("device_identifier"))
85+
model_number = str(instrument_info_data.pop("model_number"))
86+
87+
equipment_serial_number = None
88+
if "equipment_serial_number" in instrument_info_data:
89+
equipment_serial_number = str(
90+
instrument_info_data.pop("equipment_serial_number")
91+
)
92+
93+
software_name = None
94+
if general_info_data.get("software_name") is not None:
95+
software_name = str(general_info_data.pop("software_name"))
96+
else:
97+
general_info_data.pop("software_name", None)
98+
99+
software_version = None
100+
if general_info_data.get("software_version") is not None:
101+
software_version = str(general_info_data.pop("software_version"))
102+
else:
103+
general_info_data.pop("software_version", None)
104+
105+
# Merge custom_info dicts, ensuring we only unpack dict types
106+
instrument_custom_info = instrument_info_data.get("custom_info", {})
107+
device_control_custom_info_raw = instrument_info_data.get(
108+
"device_control_custom_info"
109+
)
110+
general_custom_info = general_info_data.get("custom_info", {})
111+
112+
custom_info: dict[str, Any] = {}
113+
if isinstance(instrument_custom_info, dict):
114+
custom_info.update(instrument_custom_info)
115+
if isinstance(general_custom_info, dict):
116+
custom_info.update(general_custom_info)
117+
118+
# Ensure device_control_custom_info is the correct type
119+
device_control_custom_info = (
120+
device_control_custom_info_raw
121+
if isinstance(device_control_custom_info_raw, dict)
122+
else None
123+
)
124+
83125
return Metadata(
84126
asm_file_identifier=path.with_suffix(".json").name,
85127
data_system_instance_id=NOT_APPLICABLE,
86128
file_name=path.name,
87129
unc_path=file_path,
88-
device_identifier=instrument_info_data["device_identifier"],
89-
model_number=instrument_info_data["model_number"],
90-
equipment_serial_number=instrument_info_data.get("equipment_serial_number"),
91-
software_name=general_info_data["software_name"],
92-
software_version=general_info_data["software_version"],
130+
device_identifier=device_identifier,
131+
model_number=model_number,
132+
equipment_serial_number=equipment_serial_number,
133+
software_name=software_name,
134+
software_version=software_version,
135+
metadata_custom_info=custom_info,
136+
device_control_custom_info=device_control_custom_info,
93137
)
94138

95139
@staticmethod
96140
def _get_general_info_data(
97141
general_info_df: pd.DataFrame | None,
98-
) -> dict[str, str | None]:
142+
) -> dict[str, str | None | dict[str, str]]:
99143
if general_info_df is None:
100144
return dict.fromkeys(GENERAL_INFO_KEYS, None)
101145

@@ -106,15 +150,21 @@ def _get_general_info_data(
106150
software_name, software_version_txt = software_info.split(",", 1)
107151
match = re.search(r"\d+(?:\.\d+)*", software_version_txt)
108152
software_version = match.group() if match else None
153+
unread_data = general_info_data.get_unread(skip={"Filter", "Position"})
154+
# Filter out "nan" keys from custom info and ensure string values
155+
custom_info = {
156+
k: str(v) for k, v in unread_data.items() if k != "nan" and v is not None
157+
}
109158
return {
110159
"software_name": software_name,
111160
"software_version": software_version,
161+
"custom_info": custom_info,
112162
}
113163

114164
@staticmethod
115165
def _get_instrument_data(
116166
instrument_info_df: pd.DataFrame | None,
117-
) -> dict[str, str]:
167+
) -> dict[str, str | None | dict[str, str] | dict[str, float | str | None]]:
118168
if instrument_info_df is None:
119169
return {
120170
"device_identifier": NOT_APPLICABLE,
@@ -139,11 +189,45 @@ def _get_instrument_data(
139189
"model_number": ("Name", NOT_APPLICABLE),
140190
"equipment_serial_number": ("Serial number", None),
141191
}
142-
return {
192+
# Get the raw data first
193+
raw_data = {
143194
key: value
144195
for key, (label, default) in lookups.items()
145196
if (value := instrument_info_data.get(str, label, default)) is not None
146197
}
198+
device_control_custom_info = instrument_info_data.get_custom_keys(
199+
{
200+
"Optical response compensation",
201+
"Plate adapter number",
202+
"Plate adapter name",
203+
}
204+
)
205+
unread_data = instrument_info_data.get_unread(
206+
skip={
207+
"Wavelength",
208+
"Incubator",
209+
"Date and time of definition",
210+
"Top optics",
211+
"Bottom optics",
212+
"Dispenser 1",
213+
"Dispenser 2",
214+
"Module's name",
215+
"Module's serial number",
216+
"Instrument modules",
217+
"Gas control",
218+
"Bandwidth",
219+
"Position",
220+
"Filter",
221+
}
222+
)
223+
custom_info = {
224+
k: str(v) for k, v in unread_data.items() if k != "nan" and v is not None
225+
}
226+
return {
227+
**raw_data,
228+
"custom_info": custom_info,
229+
"device_control_custom_info": device_control_custom_info,
230+
}
147231

148232

149233
@dataclass
@@ -158,6 +242,7 @@ def create(
158242
well_plate_identifier: str | None,
159243
error_documents: list[ErrorDocument] | None = None,
160244
experimental_data_identifier: str | None = None,
245+
device_control_custom_info: dict[str, Any] | None = None,
161246
) -> Measurement:
162247
measurement_type_str = MEASUREMENT_TYPES[type_]
163248
error_docs = error_documents or []
@@ -184,6 +269,7 @@ def create(
184269
else None,
185270
experimental_data_identifier=experimental_data_identifier,
186271
error_document=error_docs if error_docs else None,
272+
device_control_custom_info=device_control_custom_info,
187273
)
188274

189275
@staticmethod
@@ -202,14 +288,19 @@ def create(
202288
sheet_df: pd.DataFrame,
203289
type_: MeasurementType,
204290
session_info_df: pd.DataFrame | None,
291+
device_control_custom_info: dict[str, Any] | None = None,
205292
) -> list[MeasurementGroup]:
206293
plates = ThermoSkanItMeasurementGroups.identify_data_and_sample_dfs(sheet_df)
207294

208295
session_name = exec_time = None
296+
custom_info = {}
209297
if session_info_df is not None:
210298
session_info_data = df_to_series_data(parse_header_row(session_info_df.T))
211299
session_name = session_info_data.get(str, "Session name")
212300
exec_time = session_info_data.get(str, "Execution time")
301+
unread_data = session_info_data.get_unread(skip={"Executed with"})
302+
# Filter out "nan" keys from custom info
303+
custom_info = {k: v for k, v in unread_data.items() if k != "nan"}
213304

214305
if not exec_time:
215306
exec_time = sheet_df.iloc[1].iloc[0]
@@ -273,6 +364,7 @@ def create(
273364
experimental_data_identifier=session_name.replace(".skax", "")
274365
if session_name
275366
else None,
367+
device_control_custom_info=device_control_custom_info,
276368
)
277369

278370
if well_key not in well_measurements:
@@ -288,6 +380,7 @@ def create(
288380
measurements=measurements,
289381
plate_well_count=plate_well_counts[plate_id],
290382
measurement_time=exec_time,
383+
custom_info=custom_info,
291384
)
292385
)
293386

@@ -458,10 +551,15 @@ def create(sheet_data: dict[str, pd.DataFrame], file_path: str) -> Data:
458551
general_info_df=clean_data.get("General information"),
459552
file_path=file_path,
460553
)
554+
555+
# Extract device control custom info from metadata
556+
device_control_custom_info = metadata.device_control_custom_info
557+
461558
measurement_groups = ThermoSkanItMeasurementGroups.create(
462559
sheet_df=measurement_df,
463560
session_info_df=clean_data.get("Session information"),
464561
type_=_type,
562+
device_control_custom_info=device_control_custom_info,
465563
)
466564

467565
return Data(

src/allotropy/parsers/unchained_labs_lunatic_stunner/unchained_labs_lunatic_stunner_structure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def create_metadata(header: SeriesData, file_path: str) -> Metadata:
463463
unc_path=file_path,
464464
asm_file_identifier=asm_file_identifier.name,
465465
data_system_instance_id=NOT_APPLICABLE,
466-
custom_info_doc=_filter_empty_string_values(
466+
metadata_custom_info=_filter_empty_string_values(
467467
header.get_unread(
468468
# Skip already mapped columns from well plate data (repeated in CSV no header cases)
469469
skip={

0 commit comments

Comments
 (0)