Skip to content

Commit b17ffc4

Browse files
merge main
2 parents 8902be5 + c1bcdc6 commit b17ffc4

64 files changed

Lines changed: 35339 additions & 3554 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,50 @@ 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.110] - 2025-11-03
9+
10+
### Added
11+
12+
- Thermo Fisher Scientific NanoDrop 8000 - add unread data to asm (#1094)
13+
- Thermo Fisher Scientific NanoDrop Eight - add unread data to asm (#1096)
14+
15+
### Fixed
16+
17+
- Thermo Fisher Scientific SkanIt - error detecing plate word matching (#1113)
18+
19+
## [0.1.109] - 2025-10-28
20+
21+
### Added
22+
23+
- Thermo Fisher Scientific VISIONlite - add get unread data (#1097)
24+
- Flowjo - Added new example (#1108)
25+
- Beckman Coulter Biomek - clear unread data warnings (#1092)
26+
- Cytiva Biacore T200 - Refactor to use JsonData (#1095)
27+
- CTL ImmunoSpot - use proper units in data processing document (#1098)
28+
- Thermo Fisher Scientific SkanIt - add unread data (#1104)
29+
30+
### Fixed
31+
32+
- Molecular Devices SoftMax Pro - handle reduced data blocks with empty rows correctly (#1110)
33+
34+
## [0.1.108] - 2025-10-23
35+
36+
### Added
37+
38+
- BMG Labtech MARS - Support for CSVs generated by Mars V3.40 R2 (#1105)
39+
40+
## [0.1.107] - 2025-10-09
41+
42+
### Added
43+
44+
- Thermo Fisher Scientific Genesys30 - add unread data (#1093)
45+
- Mabtech Apex - support files with different plate info (#1099)
46+
- Cytiva Biacore T200 Evaluation Module - initial implementation (#1075)
47+
48+
### Fixed
49+
50+
- Thermo Fisher Scientific Qubit Flex - added handling for sep header (#1101)
51+
852
## [0.1.106] - 2025-09-30
953

1054
### Added

src/allotropy/__about__.py

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

src/allotropy/allotrope/converter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
"=": "_EQUALS_",
113113
"@": "_AT_",
114114
"'": "_QUOTE_",
115+
"*": "_ASTERISK_",
116+
",": "_COMMA_",
117+
"&": "_AMPERSAND_",
115118
# NOTE: this MUST be at the end, or it will break other key replacements.
116119
" ": "_",
117120
}

src/allotropy/allotrope/models/shared/definitions/custom.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
PartsPerBillion,
5555
PartsPerMillion,
5656
Percent,
57+
PerMolarPerCentimeter,
5758
PerMolarPerSecond,
5859
PerSecond,
5960
PH,
@@ -72,6 +73,7 @@
7273
SeimensPerMeter,
7374
SquareCentimetersPerGram,
7475
SquareCentimetersPerMole,
76+
SquareMillimeter,
7577
SquareResponseUnit,
7678
TODO,
7779
Unitless,
@@ -609,6 +611,18 @@ class TNullableQuantityValuePartsPerMillion(PartsPerMillion, TNullableQuantityVa
609611
pass
610612

611613

614+
@dataclass(frozen=True, kw_only=True)
615+
class TQuantityValuePerMolarPerCentimeter(PerMolarPerCentimeter, TQuantityValue):
616+
pass
617+
618+
619+
@dataclass(frozen=True, kw_only=True)
620+
class TNullableQuantityValuePerMolarPerCentimeter(
621+
PerMolarPerCentimeter, TNullableQuantityValue
622+
):
623+
pass
624+
625+
612626
@dataclass(frozen=True, kw_only=True)
613627
class TQuantityValuePerMolarPerSecond(PerMolarPerSecond, TQuantityValue):
614628
pass
@@ -821,6 +835,16 @@ class TNullableQuantityValueSquareCentimetersPerMole(
821835
pass
822836

823837

838+
@dataclass(frozen=True, kw_only=True)
839+
class TQuantityValueSquareMillimeter(SquareMillimeter, TQuantityValue):
840+
pass
841+
842+
843+
@dataclass(frozen=True, kw_only=True)
844+
class TNullableQuantityValueSquareMillimeter(SquareMillimeter, TNullableQuantityValue):
845+
pass
846+
847+
824848
@dataclass(frozen=True, kw_only=True)
825849
class TQuantityValueSquareResponseUnit(SquareResponseUnit, TQuantityValue):
826850
pass

src/allotropy/allotrope/models/shared/definitions/units.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ class PartsPerMillion(HasUnit):
249249
unit: str = "ppm"
250250

251251

252+
@dataclass(frozen=True, kw_only=True)
253+
class PerMolarPerCentimeter(HasUnit):
254+
unit: str = "M-1cm-1"
255+
256+
252257
@dataclass(frozen=True, kw_only=True)
253258
class PerMolarPerSecond(HasUnit):
254259
unit: str = "M-1s-1"
@@ -339,6 +344,11 @@ class SquareCentimetersPerMole(HasUnit):
339344
unit: str = "cm^2/mol"
340345

341346

347+
@dataclass(frozen=True, kw_only=True)
348+
class SquareMillimeter(HasUnit):
349+
unit: str = "mm^2"
350+
351+
342352
@dataclass(frozen=True, kw_only=True)
343353
class SquareResponseUnit(HasUnit):
344354
unit: str = "RU^2"

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/allotrope/schema_mappers/adm/spectrophotometry/benchling/_2023/_12/spectrophotometry.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
UltravioletAbsorbanceSpectrumDetectionMeasurementDocumentItems,
3030
)
3131
from allotropy.allotrope.models.shared.definitions.custom import (
32+
TQuantityValueKiloDalton,
3233
TQuantityValueMicroliter,
3334
TQuantityValueMilliAbsorbanceUnit,
35+
TQuantityValueNanogramPerMicroliter,
3436
TQuantityValueNanometer,
37+
TQuantityValuePerMolarPerCentimeter,
3538
TQuantityValueRelativeFluorescenceUnit,
3639
TQuantityValueUnitless,
3740
)
@@ -77,6 +80,7 @@ class ProcessedDataFeature:
7780
class ProcessedData:
7881
features: list[ProcessedDataFeature]
7982
identifier: str | None = None
83+
custom_info: dict[str, Any] | None = None
8084

8185

8286
@dataclass
@@ -102,6 +106,8 @@ class Measurement:
102106
original_sample_concentration_unit: str | None = None
103107
baseline_absorbance: float | None = None
104108
electronic_absorbance_reference_wavelength_setting: float | None = None
109+
cursor_position: float | None = None
110+
cursor_absorbance: float | None = None
105111

106112
# Measurements
107113
absorbance: JsonFloat | None = None
@@ -127,6 +133,7 @@ class MeasurementGroup:
127133
container_type: str | None = None
128134

129135
processed_data: ProcessedData | None = None
136+
custom_info: dict[str, Any] | None = None
130137

131138

132139
@dataclass(frozen=True)
@@ -194,16 +201,19 @@ def _get_technique_document(
194201
) -> SpectrophotometryDocumentItem:
195202
return SpectrophotometryDocumentItem(
196203
analyst=measurement_group.analyst,
197-
measurement_aggregate_document=MeasurementAggregateDocument(
198-
measurement_time=self.get_date_time(
199-
assert_not_none(measurement_group.measurement_time)
204+
measurement_aggregate_document=add_custom_information_document(
205+
MeasurementAggregateDocument(
206+
measurement_time=self.get_date_time(
207+
assert_not_none(measurement_group.measurement_time)
208+
),
209+
experiment_type=measurement_group.experiment_type,
210+
container_type=metadata.container_type,
211+
measurement_document=[
212+
self._get_measurement_document_item(measurement, metadata)
213+
for measurement in measurement_group.measurements
214+
],
200215
),
201-
experiment_type=measurement_group.experiment_type,
202-
container_type=metadata.container_type,
203-
measurement_document=[
204-
self._get_measurement_document_item(measurement, metadata)
205-
for measurement in measurement_group.measurements
206-
],
216+
measurement_group.custom_info,
207217
),
208218
)
209219

@@ -265,6 +275,14 @@ def _get_ultraviolet_absorbance_measurement_document(
265275
TQuantityValueMilliAbsorbanceUnit,
266276
measurement.baseline_absorbance,
267277
),
278+
"340 raw": quantity_or_none(
279+
TQuantityValueMilliAbsorbanceUnit,
280+
(measurement.custom_info or {}).get("340 raw"),
281+
),
282+
"Cursor Abs.": quantity_or_none(
283+
TQuantityValueMilliAbsorbanceUnit,
284+
measurement.cursor_absorbance,
285+
),
268286
}
269287
return add_custom_information_document(doc, custom_info_doc)
270288

@@ -337,6 +355,9 @@ def _get_device_control_custom_document(
337355
"dilution factor": quantity_or_none(
338356
TQuantityValueUnitless, measurement.dilution_factor_setting
339357
),
358+
"Cursor Pos.": quantity_or_none(
359+
TQuantityValueNanometer, measurement.cursor_position
360+
),
340361
}
341362
return (measurement.device_control_custom_info or {}) | custom_info
342363

@@ -359,14 +380,19 @@ def _get_sample_document(self, measurement: Measurement) -> SampleDocument:
359380
measurement.sample_custom_info["last read standards"]
360381
)
361382

383+
sample_custom_info = measurement.sample_custom_info or {}
384+
sample_custom_info["Mol. Wt. kda"] = quantity_or_none(
385+
TQuantityValueKiloDalton, sample_custom_info.get("Mol. Wt. kda")
386+
)
387+
362388
return add_custom_information_document(
363389
SampleDocument(
364390
sample_identifier=measurement.sample_identifier,
365391
batch_identifier=measurement.batch_identifier,
366392
location_identifier=measurement.location_identifier,
367393
well_plate_identifier=measurement.well_plate_identifier,
368394
),
369-
(measurement.sample_custom_info or {}) | custom_info_doc,
395+
sample_custom_info | custom_info_doc,
370396
)
371397

372398
def _get_processed_data_aggregate_document(
@@ -375,14 +401,38 @@ def _get_processed_data_aggregate_document(
375401
if not data:
376402
return None
377403

404+
# Build custom info for processed data with proper TQuantityValue wrappers
405+
processed_data_custom_info = {}
406+
if data.custom_info:
407+
processed_data_custom_info = {
408+
"E1%": quantity_or_none(
409+
TQuantityValueUnitless, data.custom_info.get("E1%")
410+
),
411+
"ext. coeff x10e3": quantity_or_none(
412+
TQuantityValuePerMolarPerCentimeter,
413+
data.custom_info.get("ext. coeff x10e3"),
414+
),
415+
"ext.c.": quantity_or_none(
416+
TQuantityValuePerMolarPerCentimeter,
417+
data.custom_info.get("ext.c. (l/(mol*cm))"),
418+
),
419+
"conc. factor": quantity_or_none(
420+
TQuantityValueNanogramPerMicroliter,
421+
data.custom_info.get("conc. factor (ng/ul)"),
422+
),
423+
}
424+
378425
return ProcessedDataAggregateDocument(
379426
processed_data_document=[
380-
ProcessedDataDocumentItem(
381-
# TODO(nstender): figure out how to limit possible classes from get_quantity_class for typing.
382-
mass_concentration=quantity_or_none(
383-
get_quantity_class(feature.unit), feature.result # type: ignore[arg-type]
427+
add_custom_information_document(
428+
ProcessedDataDocumentItem(
429+
# TODO(nstender): figure out how to limit possible classes from get_quantity_class for typing.
430+
mass_concentration=quantity_or_none(
431+
get_quantity_class(feature.unit), feature.result # type: ignore[arg-type]
432+
),
433+
processed_data_identifier=data.identifier,
384434
),
385-
processed_data_identifier=data.identifier,
435+
processed_data_custom_info,
386436
)
387437
for feature in data.features
388438
]

src/allotropy/allotrope/schema_parser/model_class_editor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ def _get_class_lines(
577577
) -> ClassLines | None:
578578
# Reads lines for the next class and returns as a ClassLines object.
579579
lines: list[str] = existing_lines or []
580-
started = False
580+
started = bool(lines)
581581
while True:
582582
line = file.readline()
583583
# Skip empty lines at the start, return when we hit a newline after the class.

0 commit comments

Comments
 (0)