Skip to content

Commit 144b745

Browse files
committed
Added extra fields cytiva unread
1 parent 7d04947 commit 144b745

11 files changed

Lines changed: 1460 additions & 534 deletions

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_data_creator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def create_measurements(
179179
"molecular weight": quantity_or_none(
180180
TQuantityValueDalton, measurement.molecular_weight
181181
),
182+
**measurement.sample_custom_info,
182183
},
183184
sensorgram_data_cube=_get_sensorgram_datacube(measurement.sensorgram_data),
184185
report_point_data=(

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_parser.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import json
2+
from pathlib import Path, PureWindowsPath
3+
import warnings
4+
15
from allotropy.allotrope.models.adm.binding_affinity_analyzer.wd._2024._12.binding_affinity_analyzer import (
26
Model,
37
)
@@ -30,9 +34,26 @@ class CytivaBiacoreT200ControlParser(VendorParser[MapperData, Model]):
3034
SCHEMA_MAPPER = Mapper
3135

3236
def create_data(self, named_file_contents: NamedFileContents) -> MapperData:
33-
data = Data.create(DictData(decode_data(named_file_contents)))
34-
return MapperData(
37+
base_data = DictData(decode_data(named_file_contents))
38+
data = Data.create(base_data)
39+
mapper_data = MapperData(
3540
metadata=create_metadata(data, named_file_contents),
3641
measurement_groups=create_measurement_groups(data),
3742
calculated_data=create_calculated_data(data),
3843
)
44+
try:
45+
unread = base_data.get_unread_deep()
46+
original_path = named_file_contents.original_file_path
47+
# Derive file stem robustly for both POSIX and Windows-style paths
48+
stem = (
49+
PureWindowsPath(original_path).stem
50+
if "\\" in original_path and "/" not in original_path
51+
else Path(original_path).stem
52+
)
53+
out_name = f"{stem}data_unread_2.json"
54+
with open(out_name, "w", encoding="utf-8") as f:
55+
json.dump(unread, f, ensure_ascii=False, indent=2)
56+
except Exception as e:
57+
# Best-effort debug artifact; do not break parsing on failure
58+
warnings.warn(f"Failed to write unread debug file: {e!s}", stacklevel=1)
59+
return mapper_data

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_structure.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
assert_not_none,
2222
try_float_or_none,
2323
)
24-
from allotropy.types import DictType
2524

2625

2726
@dataclass(frozen=True)
@@ -35,6 +34,7 @@ class ChipData:
3534

3635
@staticmethod
3736
def create(chip_data: DictData) -> ChipData:
37+
chip_data.mark_read_deep({"IFCType", "OSType", "OSVersion"})
3838
return ChipData(
3939
sensor_chip_identifier=assert_not_none(chip_data.get(str, "Id"), "Chip ID"),
4040
sensor_chip_type=chip_data.get(str, "Name"),
@@ -46,7 +46,7 @@ def create(chip_data: DictData) -> ChipData:
4646
"last modified time": chip_data.get(str, "LastModTime"),
4747
"last use time": chip_data.get(str, "LastUseTime"),
4848
"first dock date": chip_data.get(str, "FirstDockDate"),
49-
**chip_data.get_unread(skip={"IFCType", "OSType", "OSVersion"}),
49+
**chip_data.get_unread_deep(),
5050
},
5151
)
5252

@@ -203,16 +203,31 @@ class ReportPointData:
203203
absolute_resonance: float
204204
relative_resonance: float | None
205205
time_setting: float
206-
custom_info: DictType
206+
custom_info: dict[str, Any]
207207
min_resonance: float
208208
max_resonance: float
209209
lrsd: float
210210
slope: float
211211
sd: float
212+
sample_custom_info: dict[str, Any]
213+
measurement_aggregate_custom_info: dict[str, Any]
212214

213215
@staticmethod
214216
def create(data: SeriesData) -> ReportPointData:
215-
return ReportPointData(
217+
# This is to mark the keys as read so they don't get added to the custom_info, which has to use a regex
218+
# because some keys look like 'Sample1_Ligand'
219+
for key in (
220+
"FlowRate",
221+
"ContactTime",
222+
"Chip",
223+
"Ligand",
224+
"Method",
225+
"Fc",
226+
"Sample",
227+
):
228+
data.get_unread(f".*{key}.*")
229+
230+
report_point_data = ReportPointData(
216231
identifier=random_uuid_str(),
217232
identifier_role=data[str, "Id"],
218233
absolute_resonance=data[float, "AbsResp"],
@@ -236,8 +251,15 @@ def create(data: SeriesData) -> ReportPointData:
236251
"assay_step_purpose": data.get(str, "AssayStepPurpose"),
237252
"buffer": data.get(str, "Buffer"),
238253
},
254+
sample_custom_info=data.get_custom_keys({"Cycle"}),
255+
measurement_aggregate_custom_info=data.get_custom_keys({"Procedure"}),
239256
)
240257

258+
unread_data = data.get_unread()
259+
for key in list(unread_data.keys()):
260+
report_point_data.custom_info[key] = unread_data[key]
261+
return report_point_data
262+
241263

242264
@dataclass(frozen=True)
243265
class MeasurementData:
@@ -259,7 +281,7 @@ class MeasurementData:
259281
flow_rate: float | None
260282
contact_time: float | None
261283
dilution: float | None
262-
custom_info: dict[str, Any]
284+
sample_custom_info: dict[str, Any]
263285

264286

265287
@dataclass(frozen=True)
@@ -268,15 +290,15 @@ class SampleData:
268290
custom_info: dict[str, Any]
269291

270292
@staticmethod
271-
def create(intermediate_structured_data: DictData) -> SampleData:
272-
application_template_details = intermediate_structured_data.get_nested(
273-
"application_template_details"
274-
)
293+
def create(
294+
intermediate_structured_data: DictData, application_template_details: DictData
295+
) -> SampleData:
275296
measurements: dict[str, list[MeasurementData]] = defaultdict(list)
276297
total_cycles = assert_not_none(
277298
intermediate_structured_data.get(int, "total_cycles"),
278299
"total_cycles",
279300
)
301+
280302
for idx in range(total_cycles):
281303
flowcell_cycle_json = application_template_details.get_nested(
282304
f"Flowcell {idx + 1}"
@@ -324,15 +346,33 @@ def create(intermediate_structured_data: DictData) -> SampleData:
324346
flow_rate=flowcell_cycle_json.get(float, "Flow"),
325347
contact_time=flowcell_cycle_json.get(float, "ContactTime"),
326348
dilution=flowcell_cycle_json.get(float, "DilutePercent"),
327-
custom_info={},
349+
sample_custom_info={},
328350
)
329351
# group sensorgram data by Flow Cell Number (Fc in rpoint data)
330352
for flow_cell, sensorgram_df in sensorgram_data.groupby(
331353
"Flow Cell Number"
332354
)
333355
]
334-
custom_info: dict[str, Any] = {}
335-
return SampleData(measurements, custom_info)
356+
# Add custom info from report point data
357+
for measurement_data in measurements[sample_location_key]:
358+
if measurement_data.report_point_data:
359+
for report_point_data_item in measurement_data.report_point_data:
360+
measurement_data.sample_custom_info.update(
361+
report_point_data_item.sample_custom_info
362+
)
363+
break
364+
return SampleData(measurements, {})
365+
366+
def get_measurement_aggregate_custom_info(self) -> dict[str, Any]:
367+
measurement_aggregate_custom_info: dict[str, Any] = {}
368+
for measurements_by_sample_location_key in self.measurements.values():
369+
for measurement_data in measurements_by_sample_location_key:
370+
if measurement_data.report_point_data:
371+
for report_point_data_item in measurement_data.report_point_data:
372+
measurement_aggregate_custom_info.update(
373+
report_point_data_item.measurement_aggregate_custom_info
374+
)
375+
return measurement_aggregate_custom_info
336376

337377

338378
@dataclass(frozen=True)
@@ -360,9 +400,15 @@ def create(intermediate_structured_data: DictData) -> Data:
360400
system_information = SystemInformation.create(system_information_dictdata)
361401
chip_data_dictdata = intermediate_structured_data.get_nested("chip")
362402
chip_data = ChipData.create(chip_data_dictdata)
363-
sample_data = SampleData.create(intermediate_structured_data)
403+
sample_data = SampleData.create(
404+
intermediate_structured_data, application_template_details
405+
)
406+
407+
system_information.measurement_aggregate_custom_info.update(
408+
sample_data.get_measurement_aggregate_custom_info()
409+
)
364410

365-
# This has to be called later because in SampleData.create some of the unread keys are called
411+
# This has to be called later because SampleData.create uses some of the unread keys
366412
run_metadata.set_device_custom_info(application_template_details)
367413

368414
return Data(

src/allotropy/parsers/utils/dict_data.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,11 @@ def get_keys_as_dict(
431431
"output_field2": (float, "input_field2", 0.0),
432432
"renamed_field": (int, "original_name", None),
433433
}
434-
result = dict_data.get_keys_as_dict(field_mappings)
435-
434+
output = {
435+
"output_field1": "value1",
436+
"output_field2": 1.0,
437+
"renamed_field": 1,
438+
}
436439
"""
437440
result: dict[str, Any] = {}
438441

tests/allotrope/schema_parser/generate_schemas_test.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pathlib import Path
2+
import re
23

34
import pytest
45

@@ -19,10 +20,33 @@ def _get_schema_paths() -> list[Path]:
1920
]
2021

2122

23+
def _pick_subset(paths: list[Path], count: int = 8) -> list[Path]:
24+
if not paths:
25+
return []
26+
if len(paths) <= count:
27+
return paths
28+
step = max(1, len(paths) // count)
29+
# Evenly sample across the set for broad coverage
30+
return [paths[i] for i in range(0, len(paths), step)][:count]
31+
32+
33+
def test_generate_schemas_smoke_subset() -> None:
34+
"""Fast smoke test over a representative subset of schemas."""
35+
paths = _get_schema_paths()
36+
subset = _pick_subset(paths, count=8)
37+
# Build a regex that matches exactly any of the chosen relative schema paths
38+
escaped = [re.escape(str(p)) for p in subset]
39+
pattern = rf"^({'|'.join(escaped)})$"
40+
models_changed = generate_schemas(dry_run=True, schema_regex=pattern)
41+
assert (
42+
not models_changed
43+
), f"Expected no models files to have changed by generate-schemas script, found changes in: {models_changed}.\nPlease run 'hatch run scripts:generate-schemas' and validate the changes."
44+
45+
2246
@pytest.mark.long
23-
@pytest.mark.parametrize("schema_path", _get_schema_paths())
24-
def test_generate_schemas_runs_to_completion(schema_path: Path) -> None:
25-
models_changed = generate_schemas(dry_run=True, schema_regex=str(schema_path))
47+
def test_generate_schemas_runs_to_completion() -> None:
48+
"""Full run once across all schemas. Marked long for optional execution."""
49+
models_changed = generate_schemas(dry_run=True)
2650
assert (
2751
not models_changed
2852
), f"Expected no models files to have changed by generate-schemas script, found changes in: {models_changed}.\nPlease run 'hatch run scripts:generate-schemas' and validate the changes."

0 commit comments

Comments
 (0)