Skip to content

Commit 0a79d9b

Browse files
fix: Support SpectraMax 340PC-384 kinetic files in SoftMax Pro parser
Multiple issues prevented parsing of SpectraMax 340PC-384 kinetic export files: 1. Encoding: use chardet detection instead of assuming UTF-8, since these files are typically ISO-8859-1 encoded 2. Empty groups: handle Group blocks with no data rows (e.g. empty Standards groups) gracefully instead of erroring 3. Partial plates: skip wells with no data in kinetic mode rather than erroring; wells populated at t=0 that go missing later still error 4. Time format: accept M:SS and H:MM in addition to HH:MM:SS 5. GroupColumns: make Formula Name/Formula section optional since some export formats don't include it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4307c74 commit 0a79d9b

4 files changed

Lines changed: 37 additions & 29 deletions

File tree

src/allotropy/parsers/moldev_softmax_pro/softmax_pro_parser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Data,
88
Mapper,
99
)
10+
from allotropy.constants import CHARDET_ENCODING
1011
from allotropy.named_file_contents import NamedFileContents
1112
from allotropy.parsers.lines_reader import CsvReader, read_to_lines
1213
from allotropy.parsers.moldev_softmax_pro.softmax_pro_data_creator import (
@@ -48,6 +49,12 @@ def sniff(cls, named_file_contents: NamedFileContents) -> bool:
4849
return False
4950

5051
def create_data(self, named_file_contents: NamedFileContents) -> Data:
52+
if named_file_contents.encoding is None:
53+
named_file_contents = NamedFileContents(
54+
named_file_contents.contents,
55+
named_file_contents.original_file_path,
56+
CHARDET_ENCODING,
57+
)
5158
lines = read_to_lines(named_file_contents)
5259
reader = CsvReader(lines)
5360
data = StructureData.create(reader)

src/allotropy/parsers/moldev_softmax_pro/softmax_pro_structure.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from abc import ABC
44
from collections.abc import Iterator
55
from dataclasses import dataclass, field
6-
from datetime import datetime
76
from enum import Enum
87
import math
98
import re
@@ -65,14 +64,25 @@ def num_wells_to_n_columns(well_count: int) -> int:
6564

6665

6766
def time_to_seconds(time: str) -> float:
68-
"""Transforms HH:MM:SS formatted time into seconds"""
69-
try:
70-
pt = datetime.strptime(time, "%H:%M:%S").astimezone()
71-
except ValueError as e:
72-
msg = "Bad time formatting, expected HH:MM:SS, got {time}"
73-
raise AllotropeConversionError(msg) from e
74-
75-
return pt.hour * 3600 + pt.minute * 60 + pt.second
67+
"""Transforms time string into seconds. Supports H:MM:SS, HH:MM:SS, H:MM, M:SS formats."""
68+
parts = time.split(":")
69+
if len(parts) == 3:
70+
try:
71+
h, m, s = int(parts[0]), int(parts[1]), int(parts[2])
72+
return h * 3600 + m * 60 + s
73+
except ValueError as e:
74+
msg = f"Bad time formatting, expected HH:MM:SS, got {time}"
75+
raise AllotropeConversionError(msg) from e
76+
elif len(parts) == 2:
77+
try:
78+
m, s = int(parts[0]), int(parts[1])
79+
return m * 60 + s
80+
except ValueError as e:
81+
msg = f"Bad time formatting, expected MM:SS, got {time}"
82+
raise AllotropeConversionError(msg) from e
83+
else:
84+
msg = f"Bad time formatting, expected HH:MM:SS or MM:SS, got {time}"
85+
raise AllotropeConversionError(msg)
7686

7787

7888
class ReadType(Enum):
@@ -256,10 +266,8 @@ def create(reader: CsvReader) -> GroupData:
256266
# We are doing 1 for now, but we should check
257267
data = data.dropna(axis=1, how="all")
258268

259-
assert_not_none(
260-
data.get("Sample"),
261-
msg=f"Unable to find sample identifier column in group data {name}",
262-
)
269+
if data.empty or data.get("Sample") is None:
270+
return GroupData(name=name, sample_data=[])
263271

264272
calc_data_cols = GroupData.get_calculated_data_columns(data)
265273

@@ -296,18 +304,10 @@ class GroupColumns:
296304

297305
@staticmethod
298306
def create(reader: CsvReader) -> GroupColumns:
299-
data = assert_not_none(
300-
reader.pop_csv_block_as_df(sep="\t", header=0),
301-
msg="Unable to find group block columns.",
302-
)
303-
304-
if "Formula Name" not in data:
305-
msg = "Unable to find 'Formula Name' in group block columns."
306-
raise AllotropeConversionError(msg)
307+
data = reader.pop_csv_block_as_df(sep="\t", header=0)
307308

308-
if "Formula" not in data:
309-
msg = "Unable to find 'Formula' in group block columns."
310-
raise AllotropeConversionError(msg)
309+
if data is None or "Formula Name" not in data or "Formula" not in data:
310+
return GroupColumns(data={})
311311

312312
return GroupColumns(
313313
data=dict(zip(data["Formula Name"], data["Formula"], strict=True)),
@@ -430,8 +430,7 @@ def create(
430430
for position, raw_value in data.items():
431431
value = try_non_nan_float_or_none(raw_value)
432432
if value is None and elapsed_time is not None:
433-
msg = f"Missing kinetic measurement for well position {position} at {elapsed_time}s."
434-
raise AllotropeConversionError(msg)
433+
continue
435434
data_elements[str(position)] = DataElement(
436435
uuid=random_uuid_str(),
437436
plate=header.name,
@@ -459,6 +458,8 @@ def update_kinetic_data_elements(
459458
for col, raw_value in zip(df_data.columns, row_data, strict=True)
460459
}
461460
for position, value in data.items():
461+
if position not in self.data_elements:
462+
continue
462463
if value is None:
463464
msg = f"Missing kinetic measurement for well position {position} at {elapsed_time}s."
464465
raise AllotropeConversionError(msg)

tests/parsers/moldev_softmax_pro/testdata/errors/missing_kinetic_measurement.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ foo bar baz
2222

2323
Plate: Plate1 1.3 PlateFormat Kinetic Fluorescence FALSE Raw FALSE 4 120 90 1 525 1 3 6 485 Automatic 515 6 Medium 1 2
2424
Temperature(¡C) 1 2 3
25-
00:00:00 35 NaN 0 0
25+
00:00:00 35 1.5 0 0
2626
0 0 0
2727

28-
00:01:30 35 39.50988 20.26845 10.26596
28+
00:01:30 35 NaN 20.26845 10.26596
2929
41.66635 21.40594 10.85013
3030

3131
1 2 3

tests/parsers/moldev_softmax_pro/to_allotrope_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_handles_unrecognized_read_mode() -> None:
4242
def test_missing_kinetic_measurement() -> None:
4343
with pytest.raises(
4444
AllotropeConversionError,
45-
match="Missing kinetic measurement for well position A1 at 0s.",
45+
match=re.escape("Missing kinetic measurement for well position A1 at 90s."),
4646
):
4747
from_file(f"{TESTDATA}/errors/missing_kinetic_measurement.txt", VENDOR_TYPE)
4848

0 commit comments

Comments
 (0)