@@ -64,35 +64,49 @@ class Header:
6464 detector_gain_setting : str | None
6565 minimum_assay_bead_count_setting : float | None
6666 analyst : str | None
67+ custom_info : dict [str , Any ]
6768
6869 @classmethod
6970 def create (
70- cls , header_data : pd .DataFrame , minimum_assay_bead_count_setting : float | None
71+ cls ,
72+ header_data : pd .DataFrame ,
73+ header_row : SeriesData ,
74+ minimum_assay_bead_count_setting : float | None ,
7175 ) -> Header :
72- info_row = SeriesData ( header_data . iloc [ 0 ])
73- raw_datetime = info_row [ str , "BatchStartTime" ]
74- sample_volume = info_row . get ( str , [ "SampleVolume " , "MaxSampleUptakeVolume" ] )
76+ sample_volume = header_row . get ( str , [ "SampleVolume" , "MaxSampleUptakeVolume" ])
77+
78+ header_row . mark_read ({ "Program" , "ProtocolPlate " , "Date" } )
7579
7680 return Header (
7781 model_number = cls ._get_model_number (header_data ),
78- software_version = info_row .get (str , "Build" ),
79- equipment_serial_number = info_row .get (str , "SN" ),
80- analytical_method_identifier = info_row .get (str , "ProtocolName" ),
81- method_version = info_row .get (str , "ProtocolVersion" ),
82- experimental_data_identifier = info_row .get (str , "Batch" ),
82+ software_version = header_row .get (str , "Build" ),
83+ equipment_serial_number = header_row .get (str , "SN" ),
84+ analytical_method_identifier = header_row .get (str , "ProtocolName" ),
85+ method_version = header_row .get (str , "ProtocolVersion" ),
86+ experimental_data_identifier = header_row .get (str , "Batch" ),
8387 sample_volume_setting = (
8488 try_float (sample_volume .split ()[0 ], "sample volume setting" )
8589 if sample_volume
8690 else None
8791 ),
8892 plate_well_count = cls ._get_plate_well_count (header_data ),
89- measurement_time = raw_datetime ,
90- detector_gain_setting = info_row .get (
93+ measurement_time = header_row [ str , "BatchStartTime" ] ,
94+ detector_gain_setting = header_row .get (
9195 str , ["ProtocolReporterGain" , "ProtocolOperatingMode" ]
9296 ),
93- data_system_instance_identifier = info_row [str , "ComputerName" ],
97+ data_system_instance_identifier = header_row [str , "ComputerName" ],
9498 minimum_assay_bead_count_setting = minimum_assay_bead_count_setting ,
95- analyst = info_row .get (str , "Operator" ),
99+ analyst = header_row .get (str , "Operator" ),
100+ custom_info = {
101+ "Country Code" : header_row .get (str , "Country Code" ),
102+ "ProtocolDevelopingCompany" : header_row .get (
103+ str , "ProtocolDevelopingCompany"
104+ ),
105+ "Version" : header_row .get (str , "Version" ),
106+ # Used for the measurement groups
107+ "BatchStopTime" : header_row .get (str , "BatchStopTime" ),
108+ "ProtocolDescription" : header_row .get (str , "ProtocolDescription" ),
109+ },
96110 )
97111
98112 @classmethod
@@ -138,7 +152,10 @@ def create_calibration(calibration_data: SeriesData) -> Calibration:
138152 msg = f"Expected at least two columns on the calibration line, got:\n { calibration_data .series } ."
139153 raise AllotropeConversionError (msg )
140154
155+ # Read the calibration data using SeriesData methods
156+ calibration_name = calibration_data .series .iloc [0 ]
141157 calibration_info = str (calibration_data .series .iloc [1 ]).strip ()
158+ calibration_data .get_unread ()
142159
143160 # Check if the calibration info starts with a known status value
144161 status_values = ["Passed" , "Failed" , "Calibrated" , "Verified" ]
@@ -164,7 +181,7 @@ def create_calibration(calibration_data: SeriesData) -> Calibration:
164181 raise AllotropeConversionError (msg )
165182
166183 return Calibration (
167- name = calibration_data . series . iloc [ 0 ] .replace ("Last" , "" ).strip (),
184+ name = calibration_name .replace ("Last" , "" ).strip (),
168185 time = time ,
169186 report = report ,
170187 )
@@ -180,13 +197,17 @@ class Measurement:
180197 analytes : list [Analyte ]
181198 calculated_data : list [CalculatedDocument ]
182199 errors : list [Error ] | None = None
200+ measurement_custom_info : dict [str , Any ] | None = None
201+ sample_custom_info : dict [str , Any ] | None = None
202+ device_control_custom_info : dict [str , Any ] | None = None
183203
184204 @classmethod
185205 def create (
186206 cls ,
187207 results_data : dict [str , pd .DataFrame ],
188208 count_data : SeriesData ,
189209 bead_ids_data : SeriesData ,
210+ header_row : SeriesData ,
190211 ) -> Measurement :
191212 location = str (count_data .series .name )
192213 dilution_factor_data = results_data ["Dilution Factor" ]
@@ -201,9 +222,12 @@ def create(
201222 metadata_keys = ["Sample" , "Total Events" ]
202223
203224 well_location , location_id = cls ._get_location_details (location )
204- dilution_factor_setting = SeriesData (dilution_factor_data .loc [location ]).get (
225+ dilution_factor_series = SeriesData (dilution_factor_data .loc [location ])
226+ dilution_factor_setting = dilution_factor_series .get (
205227 float , "Dilution Factor" , NEGATIVE_ZERO , validate = SeriesData .NOT_NAN
206228 )
229+ # Capture unread dilution factor data to prevent warnings
230+ dilution_factor_series .get_unread ()
207231 errors : list [Error ] = []
208232 data_errors = cls ._get_errors (errors_data , well_location ) or []
209233 for data_error in data_errors :
@@ -289,6 +313,20 @@ def get_statistic_dimensions(analyte: str) -> list[StatisticDimension]:
289313 )
290314 )
291315
316+ device_control_custom_info = {
317+ "ProtocolHeater" : header_row .get (str , "ProtocolHeater" ),
318+ "DDGate" : header_row .get (str , "DDGate" ),
319+ "SampleTimeout" : header_row .get (str , "SampleTimeout" ),
320+ "ProtocolAnalysis" : header_row .get (str , "ProtocolAnalysis" ),
321+ "ProtocolMicrosphere" : header_row .get (str , "ProtocolMicrosphere" ),
322+ "PlateReadDirection" : header_row .get (str , "PlateReadDirection" ),
323+ }
324+ sample_custom_info = {
325+ "BatchDescription" : header_row .get (str , "BatchDescription" ),
326+ "PanelName" : header_row .get (str , "PanelName" ),
327+ "BeadType" : header_row .get (str , "BeadType" ),
328+ }
329+
292330 return Measurement (
293331 identifier = measurement_identifier ,
294332 sample_identifier = count_data [str , "Sample" ],
@@ -298,6 +336,9 @@ def get_statistic_dimensions(analyte: str) -> list[StatisticDimension]:
298336 analytes = analytes ,
299337 errors = errors ,
300338 calculated_data = calculated_data ,
339+ device_control_custom_info = device_control_custom_info ,
340+ sample_custom_info = sample_custom_info ,
341+ measurement_custom_info = count_data .get_unread () | header_row .get_unread (),
301342 )
302343
303344 @classmethod
@@ -316,17 +357,24 @@ def _get_errors(
316357 ) -> list [str ] | None :
317358 if errors_data is None or well_location not in errors_data .index :
318359 return None
319- return map_rows (
320- errors_data .loc [[well_location ]], lambda data : data [str , "Message" ]
321- )
360+
361+ def extract_message (data : SeriesData ) -> str :
362+ message = data [str , "Message" ]
363+ # Capture unread error data to prevent warnings
364+ data .get_unread ()
365+ return message
366+
367+ return map_rows (errors_data .loc [[well_location ]], extract_message )
322368
323369
324370@dataclass (frozen = True )
325371class MeasurementList :
326372 measurements : list [Measurement ]
327373
328374 @classmethod
329- def create (cls , results_data : dict [str , pd .DataFrame ]) -> MeasurementList :
375+ def create (
376+ cls , results_data : dict [str , pd .DataFrame ], header_row : SeriesData
377+ ) -> MeasurementList :
330378 if missing_sections := [
331379 section for section in REQUIRED_SECTIONS if section not in results_data
332380 ]:
@@ -352,6 +400,7 @@ def create_measurement(count_data: SeriesData) -> Measurement:
352400 results_data = results_data ,
353401 count_data = count_data ,
354402 bead_ids_data = bead_ids_data ,
403+ header_row = header_row ,
355404 )
356405
357406 return MeasurementList (map_rows (results_data ["Count" ], create_measurement ))
@@ -365,12 +414,14 @@ class Data:
365414
366415 @classmethod
367416 def create (cls , reader : LuminexXponentReader ) -> Data :
417+ header_row = SeriesData (reader .header_data .iloc [0 ])
418+
368419 return Data (
369420 header = Header .create (
370- reader .header_data , reader .minimum_assay_bead_count_setting
421+ reader .header_data , header_row , reader .minimum_assay_bead_count_setting
371422 ),
372423 calibrations = map_rows (reader .calibration_data , create_calibration ),
373- measurement_list = MeasurementList .create (reader .results_data ),
424+ measurement_list = MeasurementList .create (reader .results_data , header_row ),
374425 )
375426
376427
@@ -389,6 +440,7 @@ def create_metadata(
389440 software_name = DEFAULT_SOFTWARE_NAME ,
390441 software_version = header .software_version ,
391442 device_type = DEFAULT_DEVICE_TYPE ,
443+ custom_info = header .custom_info ,
392444 )
393445
394446
@@ -416,11 +468,24 @@ def create_measurement_groups(
416468 assay_bead_count = measurement .assay_bead_count ,
417469 analytes = measurement .analytes ,
418470 errors = measurement .errors if measurement .errors else None ,
471+ measurement_custom_info = measurement .measurement_custom_info ,
472+ sample_custom_info = measurement .sample_custom_info ,
473+ device_control_custom_info = measurement .device_control_custom_info ,
419474 )
420475 ],
476+ custom_info = {
477+ "BatchStopTime" : header .custom_info .get ("BatchStopTime" , None ),
478+ "ProtocolDescription" : header .custom_info .get (
479+ "ProtocolDescription" , None
480+ ),
481+ },
421482 )
422483 for measurement in measurements
423484 ]
485+ # Remove the header custom info that was used to create the measurement groups.
486+ header .custom_info .pop ("BatchStopTime" , None )
487+ header .custom_info .pop ("ProtocolDescription" , None )
488+
424489 calculated_documents = list (
425490 itertools .chain (* [measurement .calculated_data for measurement in measurements ])
426491 )
0 commit comments