Skip to content

Commit 18bc216

Browse files
fix: Molecular Devices SoftMax Pro - handle reduced data blocks with empty rows correctly (#1110)
Raw data blocks are read using plate dimensions, but reduced data blocks were being parsed with `pop_csv_block_as_df` which ends at an empty line. This does not always work for reduced blocks either, which may have empty rows (row a row of wells with no measurements). So use plate dimensions for reduced blocks as well. The function to read using plate dimensions is factored out to allow for code reuse.
1 parent c4f13eb commit 18bc216

4 files changed

Lines changed: 2686 additions & 388 deletions

File tree

src/allotropy/parsers/moldev_softmax_pro/softmax_pro_structure.py

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -467,37 +467,48 @@ def update_kinetic_data_elements(
467467
self.data_elements[position].kinetic_measures.append(value)
468468

469469

470-
@dataclass(frozen=True)
471-
class RawData:
472-
wavelength_data: list[PlateWavelengthData]
470+
def get_plate_dataframe(
471+
reader: CsvReader, header: PlateHeader, columns: pd.Series[str] | None = None
472+
) -> pd.DataFrame:
473+
if columns is None:
474+
columns = assert_not_none(
475+
reader.pop_as_series(sep="\t"),
476+
msg="unable to find data columns for plate block raw data.",
477+
)
478+
dimensions = assert_not_none(
479+
NUM_WELLS_TO_PLATE_DIMENSIONS.get(header.num_wells),
480+
msg="unable to determine plate dimensions",
481+
)
482+
rows = dimensions[1]
473483

474-
@staticmethod
475-
def get_measurement_section(
476-
reader: CsvReader, columns: pd.Series[str], rows: int
477-
) -> pd.DataFrame:
478-
lines = []
479-
# read number of rows in plate section
480-
for i in range(rows):
481-
if not (line := reader.pop()):
482-
msg = f"Expected {rows} rows in measurement table, got {i}."
483-
raise AllotropeConversionError(msg)
484-
lines.append(line)
485-
reader.drop_empty()
484+
lines = []
485+
# read number of rows in plate section
486+
for i in range(rows):
487+
if not (line := reader.pop()):
488+
msg = f"Expected {rows} rows in measurement table, got {i}."
489+
raise AllotropeConversionError(msg)
490+
lines.append(line)
491+
reader.drop_empty()
486492

487-
# convert rows to df
488-
data = assert_not_none(
489-
reader.lines_as_df(lines=lines, sep="\t"),
490-
msg="unable to find data from plate block.",
491-
)
493+
# convert rows to df
494+
data = assert_not_none(
495+
reader.lines_as_df(lines=lines, sep="\t"),
496+
msg="unable to find data from plate block.",
497+
)
492498

493-
# Truncate columns Series to match the actual number of data columns
494-
if len(columns) >= data.shape[1]:
495-
columns = columns.iloc[: data.shape[1]]
496-
elif len(columns) < data.shape[1]:
497-
data = data.loc[:, : len(columns) - 1]
499+
# Truncate columns Series to match the actual number of data columns
500+
if len(columns) >= data.shape[1]:
501+
columns = columns.iloc[: data.shape[1]]
502+
elif len(columns) < data.shape[1]:
503+
data = data.loc[:, : len(columns) - 1]
498504

499-
set_columns(data, columns)
500-
return data
505+
set_columns(data, columns)
506+
return data
507+
508+
509+
@dataclass(frozen=True)
510+
class RawData:
511+
wavelength_data: list[PlateWavelengthData]
501512

502513

503514
@dataclass(frozen=True)
@@ -508,13 +519,7 @@ def create(reader: CsvReader, header: PlateHeader) -> PlateRawData:
508519
reader.pop_as_series(sep="\t"),
509520
msg="unable to find data columns for plate block raw data.",
510521
)
511-
# use plate dimensions to determine how many rows of plate block to read
512-
dimensions = assert_not_none(
513-
NUM_WELLS_TO_PLATE_DIMENSIONS.get(header.num_wells),
514-
msg="unable to determine plate dimensions",
515-
)
516-
rows = dimensions[1]
517-
data = RawData.get_measurement_section(reader, columns, rows)
522+
data = get_plate_dataframe(reader, header, columns=columns)
518523

519524
# get temperature and elapsed time (kinetic) from the first column of the first row with value
520525
first_row_idx = int(pd.to_numeric(data.first_valid_index()))
@@ -534,7 +539,7 @@ def create(reader: CsvReader, header: PlateHeader) -> PlateRawData:
534539

535540
if elapsed_time is not None and header.kinetic_points > 1:
536541
for _ in range(header.kinetic_points - 1):
537-
data = RawData.get_measurement_section(reader, columns, rows)
542+
data = get_plate_dataframe(reader, header, columns=columns)
538543

539544
elapsed_time = time_to_seconds(str(data.iloc[0, 0]))
540545
plate_raw_data._update_kinetic_data(
@@ -584,15 +589,9 @@ def create(reader: CsvReader, header: PlateHeader) -> SpectrumPlateRawData:
584589
reader.pop_as_series(sep="\t"),
585590
msg="unable to find data columns for plate block raw data.",
586591
)
587-
dimensions = assert_not_none(
588-
NUM_WELLS_TO_PLATE_DIMENSIONS.get(header.num_wells),
589-
msg="unable to determine plate dimensions",
590-
)
591-
rows = dimensions[1]
592592
wavelength_data: list[PlateWavelengthData] = []
593-
594593
for wavelength in header.wavelengths:
595-
data = RawData.get_measurement_section(reader, columns, rows)
594+
data = get_plate_dataframe(reader, header, columns=columns)
596595
first_row_idx = int(pd.to_numeric(data.first_valid_index()))
597596
temperature = try_non_nan_float_or_none(str(data.iloc[first_row_idx, 1]))
598597
wavelength_data.append(
@@ -615,15 +614,10 @@ class PlateReducedData:
615614

616615
@staticmethod
617616
def create(reader: CsvReader, header: PlateHeader) -> PlateReducedData:
618-
619-
raw_data = assert_not_none(
620-
reader.pop_csv_block_as_df(sep="\t", header=0),
621-
msg="Unable to find reduced data for plate block.",
622-
)
623-
617+
raw_data: pd.DataFrame = get_plate_dataframe(reader, header)
618+
# Reduced data has 2 empty columns where
624619
start = 2
625620
df_data = raw_data.iloc[:, start : (start + header.num_columns)]
626-
627621
reduced_data_elements = []
628622
for row, *data in df_data.itertuples():
629623
for col, str_value in zip(df_data.columns, data, strict=True):

0 commit comments

Comments
 (0)