1+ import re
2+ from typing import ClassVar
3+
14import pandas as pd
25
36from allotropy .named_file_contents import NamedFileContents
@@ -11,18 +14,85 @@ class BeckmanCoulterBiomekReader:
1114 header : SeriesData
1215 data : pd .DataFrame
1316
17+ # Fixed column names based on headerless files
18+ FIXED_COLUMN_NAMES : ClassVar [list [str ]] = [
19+ "Time Stamp" ,
20+ "Pod" ,
21+ "Transfer Step" ,
22+ "Deck Position" ,
23+ "Labware Name" ,
24+ "Labware Barcode" ,
25+ "Well Index" ,
26+ "Sample Name" ,
27+ "Probe" ,
28+ "Amount" ,
29+ "Liquid Handling Technique" ,
30+ ]
31+
32+ def _looks_like_data_line (self , line : str ) -> bool :
33+ """
34+ Check if a line looks like data rather than metadata.
35+ Data lines should have a date/time pattern as the first field.
36+ """
37+ parts = line .split ("," )
38+ if len (parts ) < 3 :
39+ return False
40+
41+ first_part = parts [0 ].strip ()
42+ # Check if first part looks like a timestamp (contains numbers and slashes or dashes)
43+ # Examples: "08/15/2025 10:59:55", "9/4/24 14:15"
44+ timestamp_pattern = r"^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\s+\d{1,2}:\d{2}"
45+ return bool (re .match (timestamp_pattern , first_part ))
46+
1447 def __init__ (self , named_file_contents : NamedFileContents ) -> None :
1548 reader = CsvReader .create (named_file_contents )
1649
17- header_lines = list (reader .pop_until (".*Well Index.*" ))
18- header_dict = {}
19- for line in header_lines :
20- split = line .split ("," )[0 ].split ("=" , maxsplit = 1 )
21- if len (split ) != 2 :
22- continue
23- header_dict [split [0 ].strip ()] = split [1 ].strip ()
24-
25- self .header = SeriesData (pd .Series (header_dict ))
26- self .data = assert_not_none (
27- reader .pop_csv_block_as_df (header = "infer" ), "Cannot parse empty dataset"
28- )
50+ # Check if this file has a header line by looking for "Well Index"
51+ has_header_line = any ("Well Index" in line for line in reader .lines )
52+
53+ if has_header_line :
54+ header_lines = list (reader .pop_until (".*Well Index.*" ))
55+ header_dict = {}
56+ for line in header_lines :
57+ split = line .split ("," )[0 ].split ("=" , maxsplit = 1 )
58+ if len (split ) != 2 :
59+ continue
60+ header_dict [split [0 ].strip ()] = split [1 ].strip ()
61+
62+ self .header = SeriesData (pd .Series (header_dict ))
63+ self .data = assert_not_none (
64+ reader .pop_csv_block_as_df (header = "infer" ), "Cannot parse empty dataset"
65+ )
66+ else :
67+ # Handle headerless files
68+ header_lines = []
69+ data_lines = []
70+
71+ for raw_line in reader .lines :
72+ line = raw_line .strip ()
73+ if not line :
74+ continue
75+
76+ if self ._looks_like_data_line (line ):
77+ data_lines .append (line )
78+ else :
79+ header_lines .append (line )
80+
81+ # Parse header metadata
82+ header_dict = {}
83+ for line in header_lines :
84+ split = line .split ("," )[0 ].split ("=" , maxsplit = 1 )
85+ if len (split ) != 2 :
86+ continue
87+ header_dict [split [0 ].strip ()] = split [1 ].strip ()
88+
89+ self .header = SeriesData (pd .Series (header_dict ))
90+
91+ # Create a new reader with just the data lines
92+ data_reader = CsvReader (data_lines )
93+ self .data = assert_not_none (
94+ data_reader .pop_csv_block_as_df (
95+ header = None , names = self .FIXED_COLUMN_NAMES
96+ ),
97+ "Cannot parse empty dataset" ,
98+ )
0 commit comments