@@ -362,19 +362,30 @@ def get_r_point_data(r_point_data: str) -> list[dict[str, Any]]:
362362 return r_data_list
363363
364364
365- def decode_data (named_file_contents : NamedFileContents ) -> dict [ str , Any ]:
365+ def decode_data_streaming (named_file_contents : NamedFileContents ) -> Any : # Generator
366366 """
367- Decodes the proprietary file into a structured dict
367+ Decodes the proprietary file into a structured dict, streaming cycle data.
368368 :param named_file_contents: The named file contents containing the input file details
369- :return: structured dictionary of decoded data
369+ :return: Iterator yielding metadata first, then cycle data dicts
370370 """
371371 intermediate_json : dict [str , Any ] = {}
372372 content = ole .OleFileIO (named_file_contents .get_bytes_stream ())
373373 streams = content .listdir ()
374- sensorgram_df_list = []
375- report_point_data = {}
374+
375+ # Group streams by cycle number
376+ cycle_streams : dict [int , list [Any ]] = {}
377+ report_point_df = None
376378
377379 for stream in streams :
380+ # Check if this is a cycle-specific stream first
381+ if cycle_match := cycle_pattern .search (stream [0 ]):
382+ cycle_number = int (cycle_match .group (1 ))
383+ if cycle_number not in cycle_streams :
384+ cycle_streams [cycle_number ] = []
385+ cycle_streams [cycle_number ].append (stream )
386+ continue
387+
388+ # Process metadata streams
378389 stream_content = content .openstream (stream ).read ()
379390 if stream == ["Environment" ]:
380391 intermediate_json ["system_information" ] = extract_stream_data (
@@ -384,7 +395,6 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
384395 intermediate_json ["chip" ] = extract_stream_data (
385396 stream_content .decode ("utf-8" )
386397 )
387-
388398 elif stream == ["AppData" , "Dip" ]:
389399 intermediate_json ["dip" ] = get_dip_data (stream_content .decode ("utf-8" ))
390400 elif stream == ["AppData" , "ApplicationTemplate" ]:
@@ -396,76 +406,11 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
396406 intermediate_json ["sample_data" ] = sample_data
397407 elif stream == ["RPoint Table" ]:
398408 r_point_data = get_r_point_data (stream_content .decode ("utf-8" ))
399- r_point_dataframe = pd .DataFrame (r_point_data )
400- report_point_data = {
401- group ["Cycle" ].iloc [0 ]: group
402- for _ , group in r_point_dataframe .groupby ("Cycle" )
403- }
409+ report_point_df = pd .DataFrame (r_point_data )
404410
405- if not (cycle_match := cycle_pattern .search (stream [0 ])):
406- continue
407- cycle_number = int (cycle_match .group (1 ))
408-
409- if (curve_match := curve_pattern .search (str (stream ))) and (
410- window_match := window_pattern .search (str (stream ))
411- ):
412- curve_number = curve_match .group (1 )
413- window_number = window_match .group (1 )
414-
415- # get the flow cell number
416- if "Labels" in str (stream ):
417- label_list = []
418- if len (stream_content ) != 0 :
419- decoded_stream_content = stream_content .decode ("utf-8" )
420- for line in decoded_stream_content .strip ().split ("\n " ):
421- label_list .append (line )
422- if "Fc" in line :
423- flow_cell = label_list [0 ].split ("=" )[1 ]
424-
425- # gets the xy data as data frame
426- elif "XYData" in str (stream ):
427- xy_data_list = list (
428- struct .unpack ("f" * (len (stream_content ) // 4 ), stream_content )
429- )
430- indexed_xy_data_list = xy_data_list [3 :]
431- length_of_xydata = len (xy_data_list [3 :])
432- xy_result_list = indexed_xy_data_list [int (length_of_xydata / 2 ) :]
433- time_values = indexed_xy_data_list [: int (length_of_xydata / 2 )]
434- length_of_xy = len (xy_result_list )
435-
436- xy_data_frame = pd .DataFrame (
437- {
438- "Flow Cell Number" : [flow_cell ] * length_of_xy ,
439- "Cycle Number" : [cycle_number ] * length_of_xy ,
440- "Curve Number" : [curve_number ] * length_of_xy ,
441- "Window Number" : [window_number ] * length_of_xy ,
442- "Sensorgram (RU)" : xy_result_list ,
443- "Time (s)" : time_values ,
444- }
445- )
446-
447- sensorgram_df_list .append (xy_data_frame )
448-
449- # gets the segment data
450- elif "Segment" in str (stream ):
451- segment_data_list = list (
452- struct .unpack ("f" * (len (stream_content ) // 4 ), stream_content )
453- )
454- length_of_segment = len (segment_data_list [11 :])
455- segment_data_frame = pd .DataFrame (
456- {
457- "Flow Cell Number" : [flow_cell ] * length_of_segment ,
458- "Cycle Number" : [cycle_number ] * length_of_segment ,
459- "Curve Number" : [curve_number ] * length_of_segment ,
460- "Window Number" : [window_number ] * length_of_segment ,
461- "Sensorgram (RU)" : segment_data_list [11 :],
462- }
463- )
464- sensorgram_df_list .append (segment_data_frame )
465-
466- combined_sensorgram_df = pd .concat (sensorgram_df_list , ignore_index = True )
467-
468- sensorgram_data = {}
411+ # Get total cycles and data collection rate
412+ total_cycles = max (cycle_streams .keys ()) if cycle_streams else 0
413+ intermediate_json ["total_cycles" ] = total_cycles
469414
470415 data_collection_rate = None
471416 if intermediate_json .get ("application_template_details" , {}).get (
@@ -475,10 +420,82 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
475420 "DataCollectionRate"
476421 ]["value" ]
477422
478- grouped_df = combined_sensorgram_df .groupby ("Cycle Number" )
479- for cycle_num , cycle_group in grouped_df :
480- group = cycle_group .copy ()
423+ # Yield metadata first
424+ yield intermediate_json
425+
426+ # Process cycles one at a time
427+ for cycle_num in sorted (cycle_streams .keys ()):
428+ sensorgram_df_list = []
429+ flow_cell = "1" # default
481430
431+ for stream in cycle_streams [cycle_num ]:
432+ stream_content = content .openstream (stream ).read ()
433+
434+ if (curve_match := curve_pattern .search (str (stream ))) and (
435+ window_match := window_pattern .search (str (stream ))
436+ ):
437+ curve_number = curve_match .group (1 )
438+ window_number = window_match .group (1 )
439+
440+ # get the flow cell number
441+ if "Labels" in str (stream ):
442+ label_list = []
443+ if len (stream_content ) != 0 :
444+ decoded_stream_content = stream_content .decode ("utf-8" )
445+ for line in decoded_stream_content .strip ().split ("\n " ):
446+ label_list .append (line )
447+ if "Fc" in line :
448+ flow_cell = label_list [0 ].split ("=" )[1 ]
449+
450+ # gets the xy data as data frame
451+ elif "XYData" in str (stream ):
452+ xy_data_list = list (
453+ struct .unpack ("f" * (len (stream_content ) // 4 ), stream_content )
454+ )
455+ indexed_xy_data_list = xy_data_list [3 :]
456+ length_of_xydata = len (xy_data_list [3 :])
457+ xy_result_list = indexed_xy_data_list [int (length_of_xydata / 2 ) :]
458+ time_values = indexed_xy_data_list [: int (length_of_xydata / 2 )]
459+ length_of_xy = len (xy_result_list )
460+
461+ xy_data_frame = pd .DataFrame (
462+ {
463+ "Flow Cell Number" : [flow_cell ] * length_of_xy ,
464+ "Cycle Number" : [cycle_num ] * length_of_xy ,
465+ "Curve Number" : [curve_number ] * length_of_xy ,
466+ "Window Number" : [window_number ] * length_of_xy ,
467+ "Sensorgram (RU)" : xy_result_list ,
468+ "Time (s)" : time_values ,
469+ }
470+ )
471+
472+ sensorgram_df_list .append (xy_data_frame )
473+
474+ # gets the segment data
475+ elif "Segment" in str (stream ):
476+ segment_data_list = list (
477+ struct .unpack ("f" * (len (stream_content ) // 4 ), stream_content )
478+ )
479+ length_of_segment = len (segment_data_list [11 :])
480+ segment_data_frame = pd .DataFrame (
481+ {
482+ "Flow Cell Number" : [flow_cell ] * length_of_segment ,
483+ "Cycle Number" : [cycle_num ] * length_of_segment ,
484+ "Curve Number" : [curve_number ] * length_of_segment ,
485+ "Window Number" : [window_number ] * length_of_segment ,
486+ "Sensorgram (RU)" : segment_data_list [11 :],
487+ }
488+ )
489+ sensorgram_df_list .append (segment_data_frame )
490+
491+ # Process this cycle's sensorgram data
492+ if not sensorgram_df_list :
493+ continue
494+
495+ combined_sensorgram_df = pd .concat (sensorgram_df_list , ignore_index = True )
496+ group = combined_sensorgram_df .copy ()
497+
498+ # Time normalization logic
482499 if "Time (s)" in group .columns :
483500 max_flow_cell = group ["Flow Cell Number" ].max ()
484501 max_flow_cell_mask = group ["Flow Cell Number" ] == max_flow_cell
@@ -495,9 +512,9 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
495512 )
496513 reference_times = max_flow_cell_data ["Time (s)" ].values
497514
498- for flow_cell in unique_flow_cells :
499- if flow_cell != max_flow_cell :
500- flow_cell_mask = group ["Flow Cell Number" ] == flow_cell
515+ for fc in unique_flow_cells :
516+ if fc != max_flow_cell :
517+ flow_cell_mask = group ["Flow Cell Number" ] == fc
501518 flow_cell_indices = group [flow_cell_mask ].index
502519
503520 num_points = len (flow_cell_indices )
@@ -516,7 +533,7 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
516533 else :
517534 group ["Time (s)" ] = group .groupby ("Flow Cell Number" ).cumcount () + 1
518535
519- sensorgram_data [ str ( cycle_num )] = group [
536+ sensorgram_data = group [
520537 [
521538 "Flow Cell Number" ,
522539 "Cycle Number" ,
@@ -527,16 +544,33 @@ def decode_data(named_file_contents: NamedFileContents) -> dict[str, Any]:
527544 ]
528545 ]
529546
530- # In some cases the "RPoint Table" stream may not have data for the first cycle,
531- # but we get streams with sensorgram data (XYData or Segment streams) for all the cycles.
532- intermediate_json ["cycle_data" ] = [
533- {
534- "cycle_number" : cycle ,
535- "report_point_data" : report_point_data .get (cycle ),
536- "sensorgram_data" : sensorgram_df ,
547+ # Get report point data for this cycle
548+ report_point_data = None
549+ if report_point_df is not None :
550+ cycle_report = report_point_df [report_point_df ["Cycle" ] == str (cycle_num )]
551+ if not cycle_report .empty :
552+ report_point_data = cycle_report
553+
554+ # Yield this cycle's data
555+ yield {
556+ "cycle_number" : str (cycle_num ),
557+ "report_point_data" : report_point_data ,
558+ "sensorgram_data" : sensorgram_data ,
537559 }
538- for cycle , sensorgram_df in sensorgram_data .items ()
539- ]
540- intermediate_json ["total_cycles" ] = cycle_number
560+
561+
562+ def decode_data (named_file_contents : NamedFileContents ) -> dict [str , Any ]:
563+ """
564+ Decodes the proprietary file into a structured dict
565+ :param named_file_contents: The named file contents containing the input file details
566+ :return: structured dictionary of decoded data
567+ """
568+ # Use streaming decoder but collect all data
569+ stream_gen = decode_data_streaming (named_file_contents )
570+ intermediate_json : dict [str , Any ] = next (stream_gen ) # Get metadata
571+
572+ # Collect all cycle data
573+ cycle_data = list (stream_gen )
574+ intermediate_json ["cycle_data" ] = cycle_data
541575
542576 return intermediate_json
0 commit comments