@@ -207,6 +207,33 @@ def _get_regeneration_custom_info(
207207 return regen_info
208208
209209
210+ def _find_sheet (reader_data : dict [str , Any ], name : str ) -> str | None :
211+ """Find a sheet by name, tolerating variations in dash/space separators.
212+
213+ Biacore exports use inconsistent separators: "QC - X", "QC-X", "QC- X".
214+ This normalizes by collapsing whitespace around dashes before comparing.
215+ """
216+
217+ def _normalize (s : str ) -> str :
218+ return re .sub (r"\s*-\s*" , "-" , s ).strip ()
219+
220+ target = _normalize (name )
221+ for sheet_name in reader_data :
222+ if _normalize (sheet_name ) == target :
223+ return sheet_name
224+ return None
225+
226+
227+ def _find_sheets_with_prefix (reader_data : dict [str , Any ], prefix : str ) -> list [str ]:
228+ """Find all sheets whose names start with prefix after normalizing separators."""
229+
230+ def _normalize (s : str ) -> str :
231+ return re .sub (r"\s*-\s*" , "-" , s ).strip ()
232+
233+ target = _normalize (prefix )
234+ return [s for s in reader_data if _normalize (s ).startswith (target )]
235+
236+
210237def _get_table_from_dataframe (df : pd .DataFrame , split_on : str ) -> pd .DataFrame :
211238 data_table = drop_df_rows_while (df , lambda row : row [0 ] != split_on )
212239 return parse_header_row (
@@ -422,7 +449,11 @@ def get_data_processing_doc(reader: CytivaBiacoreInsightReader) -> DictType | No
422449 "QC - Capture level" ,
423450 "QC - Binding to reference" ,
424451 ]
425- qc_tables = [table for table in table_names if table in reader .data ]
452+ qc_tables = [
453+ sheet
454+ for name in table_names
455+ if (sheet := _find_sheet (reader .data , name )) is not None
456+ ]
426457 qc_df = None
427458 while qc_df is None and qc_tables :
428459 qc_table = qc_tables .pop (0 )
@@ -518,17 +549,20 @@ class EvaluationKinetics:
518549
519550 def __init__ (self , kinetics_table : pd .DataFrame ) -> None :
520551 def _get_key (row : pd .Series [Any ]) -> str :
521- # Handle both "Channel" and "Flow cell" columns
522552 channel_or_flowcell = row .get ("Channel" , row .get ("Flow cell" , "" ))
523- # For affinity data, we might have multiple capture solutions
524- # Build key with all available capture solutions
525553 capture_solution = row .get ("Capture 1 Solution" , "" )
526- # Handle None, NaN, or pd.NA values for capture_solution
527554 if capture_solution is None or (
528555 isinstance (capture_solution , float ) and np .isnan (capture_solution )
529556 ):
530557 capture_solution = ""
531- analyte_solution = row .get ("Analyte 1 Solution" , "" )
558+ analyte_solution = row .get (
559+ "Single cycle kinetics 1 Solution" ,
560+ row .get ("Analyte 1 Solution" , "" ),
561+ )
562+ if analyte_solution is None or (
563+ isinstance (analyte_solution , float ) and np .isnan (analyte_solution )
564+ ):
565+ analyte_solution = ""
532566 return f"{ channel_or_flowcell } { capture_solution } { analyte_solution } "
533567
534568 self ._data = {}
@@ -831,7 +865,9 @@ def create(
831865 capture_solution = _first_not_null_or_none (
832866 channel_data ["Capture 1 Solution" ]
833867 )
834- analyte_solution = first_row_data .get (str , "Analyte 1 Solution" )
868+ analyte_solution = first_row_data .get (
869+ str , "Single cycle kinetics 1 Solution"
870+ ) or first_row_data .get (str , "Analyte 1 Solution" )
835871
836872 # Get calculated concentration from Evaluation sheets if available
837873 # Use the first flow cell from channel_data to look up the concentration
@@ -896,47 +932,38 @@ def create(reader: CytivaBiacoreInsightReader) -> Data:
896932
897933 # OPTIMIZATION: Parse evaluation/affinity data once, not per cycle
898934 evaluation_kinetics = None
899- if "Evaluation - Kinetics" in reader .data :
935+ kinetics_sheet = _find_sheet (reader .data , "Evaluation - Kinetics" )
936+ if kinetics_sheet is not None :
900937 evaluation_kinetics_table = _get_table_from_dataframe (
901- reader .data ["Evaluation - Kinetics" ], split_on = "Group"
938+ reader .data [kinetics_sheet ], split_on = "Group"
902939 )
903940 evaluation_kinetics = EvaluationKinetics (evaluation_kinetics_table )
904941 else :
905- # Look for Affinity sheets (e.g., "Affinity 1", "Affinity 2", etc.)
906942 affinity_sheets = [
907943 sheet for sheet in reader .data .keys () if sheet .startswith ("Affinity" )
908944 ]
909945 if affinity_sheets :
910- # Use the first Affinity sheet found
911946 affinity_table = _get_table_from_dataframe (
912947 reader .data [affinity_sheets [0 ]], split_on = "Group"
913948 )
914949 evaluation_kinetics = EvaluationKinetics (affinity_table )
915950
916- # If no kinetics or affinity data found, create empty
917951 if evaluation_kinetics is None :
918952 evaluation_kinetics = EvaluationKinetics (pd .DataFrame ())
919953
920954 # OPTIMIZATION: Parse evaluation concentration data once, not per cycle
921- # Look for Evaluation sheets with concentration data
922955 evaluation_concentration_tables = []
923- evaluation_sheet_prefixes = [
924- "Evaluation - Trend_" ,
925- "Evaluation - Preced_" ,
926- ]
927- for sheet_name in reader .data .keys ():
928- if any (
929- sheet_name .startswith (prefix ) for prefix in evaluation_sheet_prefixes
930- ):
931- try :
932- eval_table = _get_table_from_dataframe (
933- reader .data [sheet_name ], split_on = "Cycle"
934- )
935- evaluation_concentration_tables .append (eval_table )
936- except (KeyError , ValueError , AssertionError ):
937- # If parsing fails (missing columns, malformed data), skip this sheet
938- # Evaluation sheets are optional, so we continue without them
939- continue
956+ eval_conc_sheets = _find_sheets_with_prefix (
957+ reader .data , "Evaluation - Trend_"
958+ ) + _find_sheets_with_prefix (reader .data , "Evaluation - Preced_" )
959+ for sheet_name in eval_conc_sheets :
960+ try :
961+ eval_table = _get_table_from_dataframe (
962+ reader .data [sheet_name ], split_on = "Cycle"
963+ )
964+ evaluation_concentration_tables .append (eval_table )
965+ except (KeyError , ValueError , AssertionError ):
966+ continue
940967
941968 evaluation_concentration = EvaluationConcentration (
942969 evaluation_concentration_tables
@@ -986,48 +1013,38 @@ def create_measurements_for_cycle(
9861013 cycle_data = report_point_table [report_point_table ["Cycle" ] == cycle_number ]
9871014
9881015 # Handle optional kinetics/affinity data
989- # Try "Evaluation - Kinetics" first, then look for "Affinity" sheets
9901016 evaluation_kinetics = None
991- if "Evaluation - Kinetics" in reader .data :
1017+ kinetics_sheet = _find_sheet (reader .data , "Evaluation - Kinetics" )
1018+ if kinetics_sheet is not None :
9921019 evaluation_kinetics_table = _get_table_from_dataframe (
993- reader .data ["Evaluation - Kinetics" ], split_on = "Group"
1020+ reader .data [kinetics_sheet ], split_on = "Group"
9941021 )
9951022 evaluation_kinetics = EvaluationKinetics (evaluation_kinetics_table )
9961023 else :
997- # Look for Affinity sheets (e.g., "Affinity 1", "Affinity 2", etc.)
9981024 affinity_sheets = [
9991025 sheet for sheet in reader .data .keys () if sheet .startswith ("Affinity" )
10001026 ]
10011027 if affinity_sheets :
1002- # Use the first Affinity sheet found
10031028 affinity_table = _get_table_from_dataframe (
10041029 reader .data [affinity_sheets [0 ]], split_on = "Group"
10051030 )
10061031 evaluation_kinetics = EvaluationKinetics (affinity_table )
10071032
1008- # If no kinetics or affinity data found, create empty
10091033 if evaluation_kinetics is None :
10101034 evaluation_kinetics = EvaluationKinetics (pd .DataFrame ())
10111035
1012- # Parse evaluation concentration data
10131036 evaluation_concentration_tables = []
1014- evaluation_sheet_prefixes = [
1015- "Evaluation - Trend_" ,
1016- "Evaluation - Preced_" ,
1017- ]
1018- for sheet_name in reader .data .keys ():
1019- if any (
1020- sheet_name .startswith (prefix ) for prefix in evaluation_sheet_prefixes
1021- ):
1022- try :
1023- eval_table = _get_table_from_dataframe (
1024- reader .data [sheet_name ], split_on = "Cycle"
1025- )
1026- evaluation_concentration_tables .append (eval_table )
1027- except (KeyError , ValueError , AssertionError ):
1028- # If parsing fails (missing columns, malformed data), skip this sheet
1029- # Evaluation sheets are optional, so we continue without them
1030- continue
1037+ eval_conc_sheets = _find_sheets_with_prefix (
1038+ reader .data , "Evaluation - Trend_"
1039+ ) + _find_sheets_with_prefix (reader .data , "Evaluation - Preced_" )
1040+ for sheet_name in eval_conc_sheets :
1041+ try :
1042+ eval_table = _get_table_from_dataframe (
1043+ reader .data [sheet_name ], split_on = "Cycle"
1044+ )
1045+ evaluation_concentration_tables .append (eval_table )
1046+ except (KeyError , ValueError , AssertionError ):
1047+ continue
10311048
10321049 evaluation_concentration = EvaluationConcentration (
10331050 evaluation_concentration_tables
0 commit comments