4848from allotropy .parsers .utils .uuids import random_uuid_str
4949from allotropy .parsers .utils .values import quantity_or_none , try_float_or_none
5050
51+ # Precompiled patterns for faster flow cell id normalization
52+ _FLOWCELL_ID_RE = re .compile (r"\d+" )
53+
5154
5255def _get_sensorgram_datacube (
5356 sensorgram_df : pd .DataFrame , * , cycle : int , flow_cell : str
5457) -> DataCube :
5558 # Extract all sensorgram data points
56- time_vals = sensorgram_df ["Time (s)" ].astype ( float ). to_list ( )
57- resp_vals = sensorgram_df ["Sensorgram (RU)" ].astype ( float ). to_list ( )
59+ time_vals = sensorgram_df ["Time (s)" ].to_numpy ( copy = False )
60+ resp_vals = sensorgram_df ["Sensorgram (RU)" ].to_numpy ( copy = False )
5861 return DataCube (
5962 label = f"Cycle{ cycle } _FlowCell{ flow_cell } " ,
6063 structure_dimensions = [
@@ -63,8 +66,8 @@ def _get_sensorgram_datacube(
6366 structure_measures = [
6467 DataCubeComponent (FieldComponentDatatype .double , "resonance" , "RU" )
6568 ],
66- dimensions = [time_vals ],
67- measures = [resp_vals ],
69+ dimensions = [time_vals . tolist () ],
70+ measures = [resp_vals . tolist () ],
6871 )
6972
7073
@@ -261,10 +264,22 @@ def _normalize_flow_cell_id(value: Any) -> str:
261264 # Don't normalize reference-subtracted flow cell IDs (e.g., "2-1", "3-1", "4-1")
262265 if "-" in s :
263266 return s
264- # Only normalize pure numeric flow cell IDs
265- m = re .match (r"\d+" , s )
267+ # Fast path for pure digits
268+ if s .isdigit ():
269+ return s
270+ # Only normalize pure numeric prefix if present
271+ m = _FLOWCELL_ID_RE .match (s )
266272 return m .group (0 ) if m else s
267273
274+ # Precompute kinetic analysis mapping from identifiers to flow cell ids (e.g., EvaluationItem2 -> "2")
275+ kinetic_map : dict [str , KineticResult ] | None = None
276+ if data .kinetic_analysis and data .kinetic_analysis .results_by_identifier :
277+ kinetic_map = {}
278+ for key , result in data .kinetic_analysis .results_by_identifier .items ():
279+ m = _FLOWCELL_ID_RE .search (str (key ))
280+ if m :
281+ kinetic_map [m .group (0 )] = result
282+
268283 # Process all flow cells (including reference-subtracted ones like "2-1", "3-1", "4-1")
269284 for flow_cell , df_fc in sensorgram_df .groupby ("Flow Cell Number" ):
270285 fc_id = _normalize_flow_cell_id (flow_cell )
@@ -357,33 +372,8 @@ def _normalize_flow_cell_id(value: Any) -> str:
357372 }
358373 break
359374
360- # Extract kinetic analysis data for this specific flow cell
361- # Match EvaluationItem identifier to flow cell identifier
362- combined_kinetic_data = None
363- if data .kinetic_analysis and data .kinetic_analysis .results_by_identifier :
364- # Try to find the specific EvaluationItem for this flow cell
365- # Flow cell IDs are typically "1", "2", "3", "4"
366- # EvaluationItem IDs are typically "EvaluationItem1", "EvaluationItem2", etc.
367- matching_eval_item = None
368-
369- # First, try direct mapping: flow cell "1" -> "EvaluationItem1"
370- eval_item_key = f"EvaluationItem{ fc_id } "
371- if eval_item_key in data .kinetic_analysis .results_by_identifier :
372- matching_eval_item = eval_item_key
373- else :
374- # If direct mapping fails, look for any EvaluationItem that might correspond to this flow cell
375- # This could be enhanced with more sophisticated matching logic if needed
376- for eval_key in data .kinetic_analysis .results_by_identifier .keys ():
377- if fc_id in eval_key or eval_key .endswith (fc_id ):
378- matching_eval_item = eval_key
379- break
380-
381- # Use only the matching EvaluationItem data for this flow cell
382- if matching_eval_item :
383- result = data .kinetic_analysis .results_by_identifier [matching_eval_item ]
384- combined_kinetic_data = result
385-
386- kinetic_data = combined_kinetic_data
375+ # Extract kinetic analysis data using precomputed mapping (if present)
376+ kinetic_data = kinetic_map .get (fc_id ) if kinetic_map is not None else None
387377
388378 measurements .append (
389379 Measurement (
@@ -458,37 +448,37 @@ def _normalize_flow_cell_id(value: Any) -> str:
458448
459449
460450def create_measurement_groups (data : Data ) -> list [MeasurementGroup ]:
461- sys = data .system_information
451+ system_info = data .system_information
462452 # Prefer application template timestamp if present in run metadata
463- if data .run_metadata .timestamp and not sys .measurement_time :
464- sys = SystemInformation (
465- application_name = sys .application_name ,
466- application_version = sys .application_version ,
467- user_name = sys .user_name ,
468- system_controller_identifier = sys .system_controller_identifier ,
469- os_type = sys .os_type ,
470- os_version = sys .os_version ,
453+ if data .run_metadata .timestamp and not system_info .measurement_time :
454+ system_info = SystemInformation (
455+ application_name = system_info .application_name ,
456+ application_version = system_info .application_version ,
457+ user_name = system_info .user_name ,
458+ system_controller_identifier = system_info .system_controller_identifier ,
459+ os_type = system_info .os_type ,
460+ os_version = system_info .os_version ,
471461 measurement_time = data .run_metadata .timestamp ,
472- unread_application_properties = sys .unread_application_properties ,
473- measurement_aggregate_fields = sys .measurement_aggregate_fields ,
462+ unread_application_properties = system_info .unread_application_properties ,
463+ measurement_aggregate_fields = system_info .measurement_aggregate_fields ,
474464 )
475465 # As a final fallback, look directly in application_template_details.properties
476- if not sys .measurement_time and data .application_template_details :
466+ if not system_info .measurement_time and data .application_template_details :
477467 props = data .application_template_details .get ("properties" , {})
478468 ts = props .get ("Timestamp" )
479469 if ts :
480- sys = SystemInformation (
481- application_name = sys .application_name ,
482- application_version = sys .application_version ,
483- user_name = sys .user_name ,
484- system_controller_identifier = sys .system_controller_identifier ,
485- os_type = sys .os_type ,
486- os_version = sys .os_version ,
470+ system_info = SystemInformation (
471+ application_name = system_info .application_name ,
472+ application_version = system_info .application_version ,
473+ user_name = system_info .user_name ,
474+ system_controller_identifier = system_info .system_controller_identifier ,
475+ os_type = system_info .os_type ,
476+ os_version = system_info .os_version ,
487477 measurement_time = ts ,
488- unread_application_properties = sys .unread_application_properties ,
489- measurement_aggregate_fields = sys .measurement_aggregate_fields ,
478+ unread_application_properties = system_info .unread_application_properties ,
479+ measurement_aggregate_fields = system_info .measurement_aggregate_fields ,
490480 )
491- if not sys .measurement_time :
481+ if not system_info .measurement_time :
492482 msg = "Missing measurement time. Expected application_template_details.properties.Timestamp."
493483 raise AllotropeParsingError (msg )
494484 groups : list [MeasurementGroup ] = []
@@ -499,7 +489,7 @@ def create_measurement_groups(data: Data) -> list[MeasurementGroup]:
499489 "data collection rate" : quantity_or_none (
500490 TQuantityValueHertz , data .run_metadata .data_collection_rate
501491 ),
502- ** sys .measurement_aggregate_fields ,
492+ ** system_info .measurement_aggregate_fields ,
503493 }
504494 # Add aggregate-level experimental data identifier for convenience (first measurement's FC)
505495 if measurements :
@@ -519,7 +509,7 @@ def create_measurement_groups(data: Data) -> list[MeasurementGroup]:
519509
520510 groups .append (
521511 MeasurementGroup (
522- measurement_time = sys .measurement_time ,
512+ measurement_time = system_info .measurement_time ,
523513 measurements = measurements ,
524514 experiment_type = None ,
525515 analytical_method_identifier = None ,
0 commit comments