Skip to content

Commit 91c96e2

Browse files
authored
Merge branch 'main' into felipenarv/mass_spectrometry_mapper
2 parents e9d4b9c + 4aa8836 commit 91c96e2

26 files changed

Lines changed: 6644 additions & 699 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this packages will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.111] - 2025-11-19
9+
10+
### Added
11+
12+
- JSON-to-CSV - improve list parsing to better support datacubes (#1124)
13+
- Cytiva Biacore T200 control - add unread data (#1120)
14+
- Qiacuity dPCR - Added unread data (#1103)
15+
- Migrate biorad_bioplex_manager to use SeriesData.get_unread (#1100)
16+
- Mass spectrometer ASM schema (#1121)
17+
- Roche Cedex BioHT - Add support for mM unit (mmol/L) (#1119)
18+
819
## [0.1.110] - 2025-11-03
920

1021
### Added

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ download-schema = "scripts/download_schema.py {args:}"
151151
create-parser = "scripts/create_parser.py {args:}"
152152
update-instrument-table = "scripts/update_supported_instruments_table.py {args:}"
153153
update-version = "scripts/update_version.py {args:}"
154+
json-to-csv = "scripts/json_to_csv_convert.py {args:}"
154155

155156
[tool.hatch.envs.win-scripts]
156157
platforms = ["windows"]
@@ -161,6 +162,7 @@ download-schema = "python scripts\\download_schema.py {args:}"
161162
create-parser = "python scripts\\create_parser.py {args:}"
162163
update-instrument-table = "python scripts\\update_supported_instruments_table.py {args:}"
163164
update-version = "python scripts\\update_version.py {args:}"
165+
json-to-csv = "python scripts\\json_to_csv_convert.py {args:}"
164166

165167
[tool.hatch.envs.viz.scripts]
166168
run = "scripts/visualization.py {args:}"

src/allotropy/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.110"
1+
__version__ = "0.1.111"

src/allotropy/allotrope/schema_mappers/adm/binding_affinity_analyzer/benchling/_2024/_12/binding_affinity_analyzer.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class MeasurementType(Enum):
5656
class DeviceDocument:
5757
device_type: str
5858
device_identifier: str
59+
device_custom_info: DictType | None = None
5960

6061

6162
@dataclass(frozen=True)
@@ -79,6 +80,7 @@ class Metadata:
7980
lot_number: str | None = None
8081
sensor_chip_custom_info: DictType | None = None
8182
data_system_custom_info: DictType | None = None
83+
device_system_custom_info: DictType | None = None
8284

8385

8486
@dataclass(frozen=True)
@@ -172,23 +174,29 @@ def map_model(self, data: Data) -> Model:
172174
),
173175
data.metadata.data_system_custom_info,
174176
),
175-
device_system_document=DeviceSystemDocument(
176-
device_identifier=data.metadata.device_identifier,
177-
model_number=data.metadata.model_number,
178-
brand_name=data.metadata.brand_name,
179-
product_manufacturer=data.metadata.product_manufacturer,
180-
equipment_serial_number=data.metadata.equipment_serial_number,
181-
device_document=(
182-
[
183-
DeviceDocumentItem(
184-
device_type=device_document_item.device_type,
185-
device_identifier=device_document_item.device_identifier,
186-
)
187-
for device_document_item in data.metadata.device_document
188-
]
189-
if data.metadata.device_document
190-
else None
177+
device_system_document=add_custom_information_document(
178+
DeviceSystemDocument(
179+
device_identifier=data.metadata.device_identifier,
180+
model_number=data.metadata.model_number,
181+
brand_name=data.metadata.brand_name,
182+
product_manufacturer=data.metadata.product_manufacturer,
183+
equipment_serial_number=data.metadata.equipment_serial_number,
184+
device_document=(
185+
[
186+
add_custom_information_document(
187+
DeviceDocumentItem(
188+
device_type=device_document_item.device_type,
189+
device_identifier=device_document_item.device_identifier,
190+
),
191+
custom_info_doc=device_document_item.device_custom_info,
192+
)
193+
for device_document_item in data.metadata.device_document
194+
]
195+
if data.metadata.device_document
196+
else None
197+
),
191198
),
199+
data.metadata.device_system_custom_info,
192200
),
193201
binding_affinity_analyzer_document=[
194202
self._get_technique_document(measurement_group, data.metadata)

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/json_to_csv/json_to_csv.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,22 @@ def _map_dataset(
141141
msg = f"Invalid transform type in dataset config: {transform.type_}"
142142
raise ValueError(msg)
143143

144-
df = (
145-
df
146-
if path_df.empty
147-
else (path_df if df.empty else df.merge(path_df, how="cross"))
148-
)
144+
# Combine the current dataframe with the path dataframe
145+
if path_df.empty:
146+
# No new data to add
147+
pass
148+
elif df.empty:
149+
# First data, use it as the base
150+
df = path_df
151+
elif len(df) == len(path_df):
152+
# Same length: concatenate side-by-side (zip parallel arrays)
153+
# This handles datacube dimensions/measures that should be paired element-wise
154+
df = pd.concat(
155+
[df.reset_index(drop=True), path_df.reset_index(drop=True)], axis=1
156+
)
157+
else:
158+
# Different lengths: cross product (Cartesian product)
159+
df = df.merge(path_df, how="cross")
149160

150161
return df
151162

src/allotropy/parsers/cytiva_biacore_t200_control/cytiva_biacore_t200_control_data_creator.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,44 @@ def _get_sensorgram_datacube(sensorgram_data: pd.DataFrame) -> DataCube:
6868

6969

7070
def _get_device_control_custom_info(data: Data) -> DictType:
71-
custom_ifo: dict[str, Any] = {
71+
application_template_details = data.application_template_details
72+
73+
custom_info: dict[str, Any] = {
7274
"number of flow cells": data.chip_data.number_of_flow_cells,
7375
"number of spots": data.chip_data.number_of_spots,
7476
"buffer volume": quantity_or_none(
7577
TQuantityValueMilliliter, data.run_metadata.buffer_volume
7678
),
7779
}
7880
if detection_setting := data.run_metadata.detection_setting:
79-
custom_ifo.update({detection_setting.key: detection_setting.value})
80-
return custom_ifo
81+
custom_info.update({detection_setting.key: detection_setting.value})
82+
83+
detection_info = application_template_details.get_nested("detection")
84+
custom_info.update(
85+
detection_info.get(key={"FlowCellSingle", "FlowCellDual", "FlowCellMulti"})
86+
)
87+
88+
temp_info = application_template_details.get_nested("RackTemperature")
89+
custom_info.update(
90+
temp_info.get_keys_as_dict(
91+
{
92+
"minimum operating temperature": (float, "min", None),
93+
"maximum operating temperature": (float, "max", None),
94+
}
95+
)
96+
)
97+
98+
system_preparations = application_template_details.get_nested("system_preparations")
99+
custom_info.update(
100+
system_preparations.get_keys_as_dict(
101+
{
102+
"analysis temperature": (float, "AnalTemp", None),
103+
"prime": (bool, "Prime", None),
104+
"normalize": (bool, "Normalize", None),
105+
}
106+
)
107+
)
108+
return custom_info
81109

82110

83111
def create_metadata(data: Data, named_file_contents: NamedFileContents) -> Metadata:
@@ -103,15 +131,19 @@ def create_metadata(data: Data, named_file_contents: NamedFileContents) -> Metad
103131
lot_number=chip_data.lot_number,
104132
sensor_chip_identifier=chip_data.sensor_chip_identifier,
105133
device_document=[
106-
DeviceDocument(device.type_, device.identifier)
134+
DeviceDocument(device.type_, device.identifier, device.custom_info)
107135
for device in run_metadata.devices
108136
],
109137
sensor_chip_custom_info=chip_data.custom_info,
138+
data_system_custom_info=system_information.data_system_custom_info,
139+
device_system_custom_info=system_information.device_system_custom_info,
110140
)
111141

112142

113143
def create_measurements(
114-
measurements_data: list[MeasurementData], device_control_custom_info: DictType
144+
measurements_data: list[MeasurementData],
145+
device_control_custom_info: DictType,
146+
data: Data,
115147
) -> list[Measurement]:
116148
return [
117149
Measurement(
@@ -129,13 +161,25 @@ def create_measurements(
129161
flow_rate=measurement.flow_rate,
130162
contact_time=measurement.contact_time,
131163
dilution=measurement.dilution,
132-
device_control_custom_info=device_control_custom_info,
164+
device_control_custom_info=dict(
165+
device_control_custom_info
166+
), # copy to avoid modifying the original
133167
)
134168
],
135169
sample_custom_info={
170+
**data.application_template_details.get_nested(
171+
"racks"
172+
).get_keys_as_dict(
173+
{
174+
"Rack1": (str, "_Rack1", None),
175+
"Rack2": (str, "_Rack2", None),
176+
"Lock Positions": (bool, "_LockPositions", None),
177+
}
178+
),
136179
"molecular weight": quantity_or_none(
137180
TQuantityValueDalton, measurement.molecular_weight
138-
)
181+
),
182+
**measurement.sample_custom_info,
139183
},
140184
sensorgram_data_cube=_get_sensorgram_datacube(measurement.sensorgram_data),
141185
report_point_data=(
@@ -168,7 +212,7 @@ def create_measurement_groups(data: Data) -> list[MeasurementGroup]:
168212
MeasurementGroup(
169213
measurement_time=system_information.measurement_time,
170214
measurements=create_measurements(
171-
measurements_data, device_control_custom_info
215+
measurements_data, device_control_custom_info, data
172216
),
173217
experiment_type=system_information.experiment_type,
174218
analytical_method_identifier=system_information.analytical_method_identifier,
@@ -181,6 +225,10 @@ def create_measurement_groups(data: Data) -> list[MeasurementGroup]:
181225
TQuantityValueHertz,
182226
try_float_or_none(data.run_metadata.data_collection_rate),
183227
),
228+
**data.application_template_details.get_nested(
229+
"properties"
230+
).get_keys_as_dict({"Run Type": (str, "TypeName", None)}),
231+
**system_information.measurement_aggregate_custom_info,
184232
},
185233
)
186234
for measurements_data in data.sample_data.measurements.values()

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

0 commit comments

Comments
 (0)