Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion src/allotropy/parsers/bmg_mars/bmg_mars_reader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import csv
from io import StringIO

import pandas as pd
Expand All @@ -16,6 +17,15 @@ class BmgMarsReader:
header_content: str

def __init__(self, named_file_contents: NamedFileContents) -> None:
try:
self._parse_file(named_file_contents)
except pd.errors.ParserError:
# Fix malformed CSV files with uneven columns and retry
named_file_contents = self._normalize_csv_columns(named_file_contents)
self._parse_file(named_file_contents)

def _parse_file(self, named_file_contents: NamedFileContents) -> None:
"""Parse the BMG MARS file and populate header and data attributes."""
reader = CsvReader(read_to_lines(named_file_contents))
lines = list(reader.pop_until_inclusive("^,?Raw Data"))
# Store the header contents so we can parse some values that don't have key/value
Expand All @@ -28,7 +38,6 @@ def __init__(self, named_file_contents: NamedFileContents) -> None:
axis="index"
)
new = df["value"].str.split(": ", expand=True, n=1)

# Handle the case where no ": " delimiter is found, resulting in a DataFrame with only one column
if new.shape[1] < 2:
msg = "Unable to parse header data: no key-value pairs found with expected format."
Expand All @@ -41,3 +50,67 @@ def __init__(self, named_file_contents: NamedFileContents) -> None:
reader.pop_csv_block_as_df(header=0, index_col=0),
msg="Unable to parse dataset from file.",
)

def _normalize_csv_columns(
self, named_file_contents: NamedFileContents
) -> NamedFileContents:
"""Fix malformed CSV files with uneven number of columns per row.

BMG MARS software <= v4.0 can produce CSV files where some rows have
more or fewer columns than others. This method normalizes all rows
to have the same number of columns by padding with empty strings,
allowing the csv reader to parse the file correctly.

Args:
named_file_contents: The named file contents to fix

Returns:
Fixed NamedFileContents with consistent column counts
"""
lines = read_to_lines(named_file_contents)

if not lines:
return named_file_contents

# Parse all rows and find the maximum number of columns
csv_rows = []
max_columns = 0

for line in lines:
if line.strip():
reader = csv.reader([line])
try:
row = next(reader)
csv_rows.append(row)
max_columns = max(max_columns, len(row))
except (csv.Error, StopIteration):
# If parsing fails, keep the original line
csv_rows.append([line])
max_columns = max(max_columns, 1)
else:
pass # skip empty lines

# Normalize all rows to have the same number of columns
normalized_rows = []
for row in csv_rows:
normalized_row = row.copy()
if len(normalized_row) < max_columns:
# Pad with empty strings
normalized_row.extend([""] * (max_columns - len(normalized_row)))
elif len(normalized_row) > max_columns:
# Truncate if somehow longer (shouldn't happen with max calculation)
normalized_row = normalized_row[:max_columns]
normalized_rows.append(normalized_row)

# Convert back to CSV string
output = StringIO()
writer = csv.writer(output)
writer.writerows(normalized_rows)
output.seek(0)

# Create a new NamedFileContents with the fixed content
return NamedFileContents(
contents=output,
original_file_path=named_file_contents.original_file_path,
encoding="utf-8",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
User: USER,Path: C:\Program Files (x86)\BMG\PHERAstar\User\Data,Test run no.: 4
Test name: Transcreener ADP2 FI,Date: 29/02/2016,Time: 14:34:46

ID1: black 384w small volume,ID2: 20 ul
Fluorescence (FI)

Raw Data (580/620)

,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24
A,25104,25203,24886,24098,24466,26571,24056,24156,24130,24089,21634,21969,22160,23823,23019,22230,22992,23852,23657,24361
B,33863,33227,34758,33603,34186,34318,34196,35279,35608,37342,35468,34056,34031,33235,33242,33304,35286,34482,34378,33774
C,45480,42551,41891,44773,43821,45338,43328,42901,44137,46425,39341,39162,39791,41566,41265,43889,40461,41195,40761,40262
D,50970,50862,50279,50797,51063,50202,50808,50966,50097,49497,50325,48821,48954,49208,49181,49016,49404,49271,48856,47830
E,53997,52824,53844,50679,55121,54280,53994,54413,54396,53971,53444,50748,53143,53916,53624,53156,53531,53493,53216,53154
F,55774,56191,56164,56462,55583,56253,56300,55464,56719,54927,56253,54441,58358,56279,55870,55886,55465,55897,55737,53798
G,57378,57180,57681,58158,57949,57397,57546,57895,58077,57617,57827,57072,58385,58021,58624,57891,57835,58321,58046,55748
H,59660,58847,59844,60655,61534,60652,60090,60958,59683,59631,59656,58141,59493,59990,59664,59956,59928,60272,59780,58810
I,63183,57522,58573,60240,60618,60777,61149,61504,61682,60194,61840,61912,61256,63237,61643,60385,60806,60566,60231,58425
J,62375,61292,62851,65025,64750,65573,58147,64039,63962,64279,64763,60451,64021,64272,65055,63228,61028,61503,61899,60751
K,63255,63635,63048,63799,65845,66218,65161,63773,64427,62842,65316,64136,65885,65606,65812,66198,62934,63653,64950,62926
L,66220,62882,65007,64797,65385,66359,66227,67120,65709,64827,65253,61630,66214,65540,65774,67329,65779,65519,65672,63404
M,63792,64859,66378,66579,67777,67381,67099,67971,66080,64760,67145,65628,66682,66075,66363,65642,67384,65322,63597,64895
N,64497,64888,64056,65284,66138,66751,66896,66044,66878,62160,64748,64183,65883,65153,66413,63789,63507,62883,63665,63882
O,68426,60666,65656,66138,63289,107,103,98,100,94,,59
P,63471,62852,62886,63555,63035,79,90,100,102,103,,62
Loading
Loading