Skip to content

Commit da58545

Browse files
authored
feat: BMG Mars - Support for CSVs generated by Mars V3.40 R2 (#1105)
BMG Mars V3.40 R2 generates malformed CSVs that have missing end of line separators. This PR adds support to enable successful parsing of those files. A test case was added, adapted from another test case, more real tests be provided if required
1 parent 788f9d5 commit da58545

3 files changed

Lines changed: 14012 additions & 1 deletion

File tree

src/allotropy/parsers/bmg_mars/bmg_mars_reader.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import csv
12
from io import StringIO
23

34
import pandas as pd
@@ -16,6 +17,15 @@ class BmgMarsReader:
1617
header_content: str
1718

1819
def __init__(self, named_file_contents: NamedFileContents) -> None:
20+
try:
21+
self._parse_file(named_file_contents)
22+
except pd.errors.ParserError:
23+
# Fix malformed CSV files with uneven columns and retry
24+
named_file_contents = self._normalize_csv_columns(named_file_contents)
25+
self._parse_file(named_file_contents)
26+
27+
def _parse_file(self, named_file_contents: NamedFileContents) -> None:
28+
"""Parse the BMG MARS file and populate header and data attributes."""
1929
reader = CsvReader(read_to_lines(named_file_contents))
2030
lines = list(reader.pop_until_inclusive("^,?Raw Data"))
2131
# Store the header contents so we can parse some values that don't have key/value
@@ -28,7 +38,6 @@ def __init__(self, named_file_contents: NamedFileContents) -> None:
2838
axis="index"
2939
)
3040
new = df["value"].str.split(": ", expand=True, n=1)
31-
3241
# Handle the case where no ": " delimiter is found, resulting in a DataFrame with only one column
3342
if new.shape[1] < 2:
3443
msg = "Unable to parse header data: no key-value pairs found with expected format."
@@ -41,3 +50,67 @@ def __init__(self, named_file_contents: NamedFileContents) -> None:
4150
reader.pop_csv_block_as_df(header=0, index_col=0),
4251
msg="Unable to parse dataset from file.",
4352
)
53+
54+
def _normalize_csv_columns(
55+
self, named_file_contents: NamedFileContents
56+
) -> NamedFileContents:
57+
"""Fix malformed CSV files with uneven number of columns per row.
58+
59+
BMG MARS software <= v4.0 can produce CSV files where some rows have
60+
more or fewer columns than others. This method normalizes all rows
61+
to have the same number of columns by padding with empty strings,
62+
allowing the csv reader to parse the file correctly.
63+
64+
Args:
65+
named_file_contents: The named file contents to fix
66+
67+
Returns:
68+
Fixed NamedFileContents with consistent column counts
69+
"""
70+
lines = read_to_lines(named_file_contents)
71+
72+
if not lines:
73+
return named_file_contents
74+
75+
# Parse all rows and find the maximum number of columns
76+
csv_rows = []
77+
max_columns = 0
78+
79+
for line in lines:
80+
if line.strip():
81+
reader = csv.reader([line])
82+
try:
83+
row = next(reader)
84+
csv_rows.append(row)
85+
max_columns = max(max_columns, len(row))
86+
except (csv.Error, StopIteration):
87+
# If parsing fails, keep the original line
88+
csv_rows.append([line])
89+
max_columns = max(max_columns, 1)
90+
else:
91+
pass # skip empty lines
92+
93+
# Normalize all rows to have the same number of columns
94+
normalized_rows = []
95+
for row in csv_rows:
96+
normalized_row = row.copy()
97+
if len(normalized_row) < max_columns:
98+
# Pad with empty strings
99+
normalized_row.extend([""] * (max_columns - len(normalized_row)))
100+
elif len(normalized_row) > max_columns:
101+
# Truncate if somehow longer (shouldn't happen with max calculation)
102+
normalized_row = normalized_row[:max_columns]
103+
normalized_rows.append(normalized_row)
104+
105+
# Convert back to CSV string
106+
output = StringIO()
107+
writer = csv.writer(output)
108+
writer.writerows(normalized_rows)
109+
output.seek(0)
110+
111+
# Create a new NamedFileContents with the fixed content
112+
return NamedFileContents(
113+
contents=output,
114+
original_file_path=named_file_contents.original_file_path,
115+
encoding="utf-8",
116+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
User: USER,Path: C:\Program Files (x86)\BMG\PHERAstar\User\Data,Test run no.: 4
2+
Test name: Transcreener ADP2 FI,Date: 29/02/2016,Time: 14:34:46
3+
4+
ID1: black 384w small volume,ID2: 20 ul
5+
Fluorescence (FI)
6+
7+
Raw Data (580/620)
8+
9+
,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24
10+
A,25104,25203,24886,24098,24466,26571,24056,24156,24130,24089,21634,21969,22160,23823,23019,22230,22992,23852,23657,24361
11+
B,33863,33227,34758,33603,34186,34318,34196,35279,35608,37342,35468,34056,34031,33235,33242,33304,35286,34482,34378,33774
12+
C,45480,42551,41891,44773,43821,45338,43328,42901,44137,46425,39341,39162,39791,41566,41265,43889,40461,41195,40761,40262
13+
D,50970,50862,50279,50797,51063,50202,50808,50966,50097,49497,50325,48821,48954,49208,49181,49016,49404,49271,48856,47830
14+
E,53997,52824,53844,50679,55121,54280,53994,54413,54396,53971,53444,50748,53143,53916,53624,53156,53531,53493,53216,53154
15+
F,55774,56191,56164,56462,55583,56253,56300,55464,56719,54927,56253,54441,58358,56279,55870,55886,55465,55897,55737,53798
16+
G,57378,57180,57681,58158,57949,57397,57546,57895,58077,57617,57827,57072,58385,58021,58624,57891,57835,58321,58046,55748
17+
H,59660,58847,59844,60655,61534,60652,60090,60958,59683,59631,59656,58141,59493,59990,59664,59956,59928,60272,59780,58810
18+
I,63183,57522,58573,60240,60618,60777,61149,61504,61682,60194,61840,61912,61256,63237,61643,60385,60806,60566,60231,58425
19+
J,62375,61292,62851,65025,64750,65573,58147,64039,63962,64279,64763,60451,64021,64272,65055,63228,61028,61503,61899,60751
20+
K,63255,63635,63048,63799,65845,66218,65161,63773,64427,62842,65316,64136,65885,65606,65812,66198,62934,63653,64950,62926
21+
L,66220,62882,65007,64797,65385,66359,66227,67120,65709,64827,65253,61630,66214,65540,65774,67329,65779,65519,65672,63404
22+
M,63792,64859,66378,66579,67777,67381,67099,67971,66080,64760,67145,65628,66682,66075,66363,65642,67384,65322,63597,64895
23+
N,64497,64888,64056,65284,66138,66751,66896,66044,66878,62160,64748,64183,65883,65153,66413,63789,63507,62883,63665,63882
24+
O,68426,60666,65656,66138,63289,107,103,98,100,94,,59
25+
P,63471,62852,62886,63555,63035,79,90,100,102,103,,62

0 commit comments

Comments
 (0)