@@ -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+
653681def 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