Skip to content

Commit 07e8c21

Browse files
feat: Agilent Gen5 - Add Conc/Dil to Sample document custom fields (#1030)
1 parent ef8a064 commit 07e8c21

11 files changed

Lines changed: 1765 additions & 437 deletions

src/allotropy/parsers/agilent_gen5/agilent_gen5_parser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
create_metadata,
1414
create_results,
1515
create_spectrum_results,
16+
get_concentrations,
1617
get_identifiers,
1718
get_kinetic_measurements,
1819
get_results_section,
@@ -49,6 +50,7 @@ def create_data(self, named_file_contents: NamedFileContents) -> Data:
4950
kinetic_data = KineticData.create(reader.sections["Procedure Details"])
5051

5152
sample_identifiers = get_identifiers(reader.sections.get("Layout"))
53+
concentration_values = get_concentrations(reader.sections.get("Layout"))
5254
actual_temperature = get_temperature(reader.sections.get("Actual Temperature"))
5355
kinetic_result = get_kinetic_measurements(reader.sections.get("Time"))
5456
kinetic_measurements, kinetic_elapsed_time, kinetic_errors = kinetic_result or (
@@ -73,6 +75,7 @@ def create_data(self, named_file_contents: NamedFileContents) -> Data:
7375
sample_identifiers=sample_identifiers,
7476
actual_temperature=actual_temperature,
7577
results_section=results_section,
78+
concentration_values=concentration_values,
7679
)
7780

7881
if not wavelength_measurements:
@@ -95,6 +98,7 @@ def create_data(self, named_file_contents: NamedFileContents) -> Data:
9598
kinetic_measurements,
9699
kinetic_elapsed_time,
97100
kinetic_errors,
101+
concentration_values,
98102
)
99103
else:
100104
measurement_groups, calculated_data = create_results(
@@ -103,6 +107,7 @@ def create_data(self, named_file_contents: NamedFileContents) -> Data:
103107
read_data,
104108
sample_identifiers,
105109
actual_temperature,
110+
concentration_values,
106111
)
107112

108113
if not measurement_groups:

src/allotropy/parsers/agilent_gen5/agilent_gen5_structure.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,34 @@ def is_results(section: list[str]) -> bool:
650650
return None
651651

652652

653+
def get_concentrations(layout_lines: list[str] | None) -> dict[str, float | None]:
654+
"""Extract concentration/dilution values from the Layout section.
655+
656+
Returns a dictionary mapping well positions to their concentration/dilution values.
657+
"""
658+
if not layout_lines:
659+
return {}
660+
661+
# Create dataframe from tabular data and forward fill empty values in index
662+
data = read_csv(StringIO("\n".join(layout_lines[1:])), sep="\t")
663+
data = data.set_index(data.index.to_series().ffill(axis="index").values)
664+
665+
concentrations = {}
666+
for row_name, row in data.iterrows():
667+
label = row.iloc[-1]
668+
if label == "Conc/Dil":
669+
for col_index, col in enumerate(row.iloc[:-1]):
670+
well_pos = f"{row_name}{col_index + 1}"
671+
# Convert to float if possible
672+
if not pd.isna(col):
673+
concentration_value = try_float_or_none(col)
674+
concentrations[well_pos] = concentration_value
675+
else:
676+
concentrations[well_pos] = None
677+
678+
return concentrations
679+
680+
653681
def get_identifiers(layout_lines: list[str] | None) -> dict[str, str]:
654682
if not layout_lines:
655683
return {}
@@ -759,6 +787,7 @@ def create_results(
759787
read_data: list[ReadData],
760788
sample_identifiers: dict[str, str],
761789
actual_temperature: float | None,
790+
concentration_values: dict[str, float | None] | None = None,
762791
) -> tuple[list[MeasurementGroup], list[CalculatedDocument]]:
763792
if result_lines[0].strip() != "Results":
764793
msg = f"Expected the first line of the results section '{result_lines[0]}' to be 'Results'."
@@ -827,6 +856,9 @@ def create_results(
827856
sample_identifiers.get(well_position),
828857
actual_temperature,
829858
error_documents=error_documents_per_well.get(well_position),
859+
concentration_value=concentration_values.get(well_position)
860+
if concentration_values
861+
else None,
830862
)
831863
for measurement in measurements
832864
],
@@ -868,6 +900,7 @@ def create_kinetic_results(
868900
kinetic_measurements: dict[str, list[float | None]],
869901
kinetic_elapsed_time: list[float],
870902
kinetic_errors: dict[str, list[ErrorDocument]] | None = None,
903+
concentration_values: dict[str, float | None] | None = None,
871904
) -> tuple[list[MeasurementGroup], list[CalculatedDocument]]:
872905
if result_lines[0].strip() != "Results":
873906
msg = f"Expected the first line of the results section '{result_lines[0]}' to be 'Results'."
@@ -919,6 +952,9 @@ def create_kinetic_results(
919952
kinetic_measurements[well_position],
920953
kinetic_elapsed_time,
921954
error_documents_per_well.get(well_position, []),
955+
concentration_value=concentration_values.get(well_position)
956+
if concentration_values
957+
else None,
922958
)
923959
],
924960
)
@@ -1080,6 +1116,7 @@ def create_spectrum_results(
10801116
sample_identifiers: dict[str, str],
10811117
actual_temperature: float | None,
10821118
results_section: list[str] | None = None,
1119+
concentration_values: dict[str, float | None] | None = None,
10831120
) -> tuple[list[MeasurementGroup], list[CalculatedDocument]]:
10841121
if not wavelengths_sections:
10851122
return [], []
@@ -1240,6 +1277,11 @@ def create_spectrum_results(
12401277
else None,
12411278
detector_gain_setting=filter_set.gain if filter_set else None,
12421279
error_document=error_documents_by_well.get(well_position),
1280+
sample_custom_info={
1281+
"Conc/Dil": concentration_values.get(well_position)
1282+
if concentration_values
1283+
else None,
1284+
},
12431285
analytical_method_identifier=header_data.protocol_file_path
12441286
if header_data.protocol_file_path
12451287
else None,
@@ -1298,6 +1340,7 @@ def _create_measurement(
12981340
kinetic_measurements: list[float | None] | None = None,
12991341
kinetic_elapsed_time: list[float] | None = None,
13001342
error_documents: list[ErrorDocument] | None = None,
1343+
concentration_value: float | None = None,
13011344
) -> Measurement:
13021345
if read_data.read_mode == ReadMode.ABSORBANCE and not kinetic_data:
13031346
measurement_type = MeasurementType.ULTRAVIOLET_ABSORBANCE
@@ -1408,7 +1451,8 @@ def _create_measurement(
14081451
),
14091452
},
14101453
sample_custom_info={
1411-
"Plate Number": header_data.additional_data.pop("Plate Number", None)
1454+
"Plate Number": header_data.additional_data.pop("Plate Number", None),
1455+
"Conc/Dil": concentration_value,
14121456
},
14131457
measurement_custom_info=header_data.additional_data,
14141458
analytical_method_identifier=header_data.protocol_file_path

0 commit comments

Comments
 (0)