@@ -90,6 +90,27 @@ def _extract_peak_data(well_plate_data: SeriesData) -> list[dict[str, Any]]:
9090 return peak_data
9191
9292
93+ def _get_calculated_value (series : SeriesData , key : str ) -> float | None :
94+ """Return numeric value for calculated data with special handling.
95+
96+ - If the raw cell value is the string literal "N/A"
97+ return NEGATIVE_ZERO.
98+ - If the cell is empty/NaN, return None so the caller can skip adding the key.
99+ - Otherwise, attempt to convert to float using the standard SeriesData conversion.
100+ """
101+ # Read as string first to inspect literal content without coercing to float/None
102+ raw_string = series .get (str , key , validate = SeriesData .NOT_NAN )
103+ if raw_string is None :
104+ return None
105+ stripped = str (raw_string ).strip ()
106+ if stripped == "" :
107+ return None
108+ if stripped .upper () == NOT_APPLICABLE :
109+ return NEGATIVE_ZERO
110+ # Otherwise convert using the standard float handling
111+ return series .get (float , key )
112+
113+
93114def _create_measurement (
94115 well_plate_data : SeriesData ,
95116 header : SeriesData ,
@@ -137,6 +158,13 @@ def _create_measurement(
137158 detection_type = DEFAULT_DETECTION_TYPE
138159 if application and "B22 & kD" in application :
139160 detection_type = DYNAMIC_LIGHT_SCATTERING_DETECTION_TYPE
161+ # Build calculated data values with special handling for N/A and empty cells
162+ calculated_data_values : dict [str , float ] = {}
163+ for item in CALCULATED_DATA_LOOKUP .get (wavelength_column , []):
164+ value = _get_calculated_value (well_plate_data , item ["column" ])
165+ if value is not None :
166+ calculated_data_values [item ["column" ]] = value
167+
140168 return Measurement (
141169 type_ = MeasurementType .ULTRAVIOLET_ABSORBANCE ,
142170 device_type = DEVICE_TYPE ,
@@ -188,39 +216,38 @@ def _create_measurement(
188216 else None ,
189217 wavelength_identifier = wavelength_column ,
190218 calc_docs_custom_info = {
191- ** {
192- item ["column" ]: well_plate_data .get (float , item ["column" ])
193- for item in CALCULATED_DATA_LOOKUP .get (wavelength_column , [])
194- },
219+ ** calculated_data_values ,
195220 ** {
196221 "b22 linear fit" : well_plate_data .get (str , "b22 linear fit" ),
197222 "kd linear fit" : well_plate_data .get (str , "kd linear fit" ),
198223 },
199224 },
200225 measurement_custom_info = {
201226 "electronic_absorbance_reference_absorbance" : background_absorbance ,
202- ** well_plate_data .get_unread (
203- # Skip already mapped columns from well plate data (repeated in CSV no header cases)
204- skip = {
205- "time" ,
206- "row" ,
207- "concentration factor (ng/ul)" ,
208- "application" ,
209- "concentration (ng/ul)" ,
210- "background wvl. (nm)" ,
211- "plate id" ,
212- "plate position" ,
213- "plate type" ,
214- "instrument id" ,
215- "column" ,
216- # Strings to skip since these are already captured as measurements/calculated data
217- # Skip absorbance measurements with a### (10mm) -- spectral scans
218- r"^a\d{3} \(10mm\)$" ,
219- # a###/a### -- calculated purity values
220- r"^a\d{3}/a\d{3}$" ,
221- # a### -- raw absorbance measurement
222- r"^a\d{3}$" ,
223- },
227+ ** _filter_empty_string_values (
228+ well_plate_data .get_unread (
229+ # Skip already mapped columns from well plate data (repeated in CSV no header cases)
230+ skip = {
231+ "time" ,
232+ "row" ,
233+ "concentration factor (ng/ul)" ,
234+ "application" ,
235+ "concentration (ng/ul)" ,
236+ "background wvl. (nm)" ,
237+ "plate id" ,
238+ "plate position" ,
239+ "plate type" ,
240+ "instrument id" ,
241+ "column" ,
242+ # Strings to skip since these are already captured as measurements/calculated data
243+ # Skip absorbance measurements with a### (10mm) -- spectral scans
244+ r"^a\d{3} \(10mm\)$" ,
245+ # a###/a### -- calculated purity values
246+ r"^a\d{3}/a\d{3}$" ,
247+ # a### -- raw absorbance measurement
248+ r"^a\d{3}$" ,
249+ },
250+ )
224251 ),
225252 },
226253 )
@@ -232,8 +259,8 @@ def _get_error_documents(
232259) -> list [ErrorDocument ]:
233260 error_documents = []
234261 for item in CALCULATED_DATA_LOOKUP .get (wavelength_column , []):
235- value = well_plate_data . get ( float , item ["column" ])
236- if value is None :
262+ value = _get_calculated_value ( well_plate_data , item ["column" ])
263+ if value == NEGATIVE_ZERO :
237264 error_documents .append (
238265 ErrorDocument (
239266 error = NOT_APPLICABLE ,
@@ -283,37 +310,49 @@ def create_metadata(header: SeriesData, file_path: str) -> Metadata:
283310 unc_path = file_path ,
284311 asm_file_identifier = asm_file_identifier .name ,
285312 data_system_instance_id = NOT_APPLICABLE ,
286- custom_info_doc = header .get_unread (
287- # Skip already mapped columns from well plate data (repeated in CSV no header cases)
288- skip = {
289- "time" ,
290- "row" ,
291- "path length mode" ,
292- "sample group" ,
293- "pump" ,
294- "concentration factor (ng/ul)" ,
295- "sample name" ,
296- "application" ,
297- "concentration (ng/ul)" ,
298- "background wvl. (nm)" ,
299- "plate id" ,
300- "plate position" ,
301- # Strings to skip since these are already captured as measurements/calculated data
302- # Skip absorbance spectrum measurements with a### (10mm)
303- r"^a\d{3} \(10mm\)$" ,
304- # a###/a###
305- r"^a\d{3}/a\d{3}$" ,
306- # a###
307- r"^a\d{3}$" ,
308- # background a###
309- r"^background \(a\d{3}\)$" ,
310- # a### concentration (ng/uL)
311- r"^a\d{3} concentration \(ng/ul\)$" ,
312- },
313+ custom_info_doc = _filter_empty_string_values (
314+ header .get_unread (
315+ # Skip already mapped columns from well plate data (repeated in CSV no header cases)
316+ skip = {
317+ "time" ,
318+ "row" ,
319+ "path length mode" ,
320+ "sample group" ,
321+ "pump" ,
322+ "concentration factor (ng/ul)" ,
323+ "sample name" ,
324+ "application" ,
325+ "concentration (ng/ul)" ,
326+ "e1%" ,
327+ "concentration (mg/ml)" ,
328+ "background wvl. (nm)" ,
329+ "plate id" ,
330+ "plate position" ,
331+ # Strings to skip since these are already captured as measurements/calculated data
332+ # Skip absorbance spectrum measurements with a### (10mm)
333+ r"^a\d{3} \(10mm\)$" ,
334+ # a###/a###
335+ r"^a\d{3}/a\d{3}$" ,
336+ # a###
337+ r"^a\d{3}$" ,
338+ # background a###
339+ r"^background \(a\d{3}\)$" ,
340+ # a### concentration (ng/uL)
341+ r"^a\d{3} concentration \(ng/ul\)$" ,
342+ },
343+ )
313344 ),
314345 )
315346
316347
348+ def _filter_empty_string_values (unread : dict [str , Any ]) -> dict [str , Any ]:
349+ return {
350+ key : value
351+ for key , value in unread .items ()
352+ if not (isinstance (value , str ) and value .strip () == "" )
353+ }
354+
355+
317356def create_measurement_groups (
318357 header : SeriesData , data : pd .DataFrame
319358) -> tuple [list [MeasurementGroup ], list [CalculatedDocument ]]:
0 commit comments