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
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,78 @@ def get_data(
return self._data.get(f"{channel} {capture_solution} {analyte_solution}", empty)


class EvaluationConcentration:
"""Stores calculated concentration data from Evaluation sheets.

Evaluation sheets (e.g., "Evaluation - Trend_Active_Stab") contain both
nominal concentrations and instrument-calculated concentrations based on
calibration curves. This class provides lookup by cycle, flow cell, and solution.
"""

_data: dict[str, float]

def __init__(self, evaluation_tables: list[pd.DataFrame]) -> None:
"""Parse evaluation tables and build concentration lookup.

Args:
evaluation_tables: List of DataFrames from Evaluation sheets,
each containing concentration data
"""
self._data = {}

for table in evaluation_tables:
if table.empty or len(table.columns) == 0:
continue

# Check if this table has the calculated concentration column
if "Calculated conc. (µg/ml)" not in table.columns:
continue

# Check if we have the necessary columns for keying
required_cols = ["Cycle", "Flow cell", "Solution"]
if not all(col in table.columns for col in required_cols):
continue

# Build lookup dictionary: key = "cycle flowcell solution"
for _, row in table.iterrows():
row_data = SeriesData(row)
cycle = row_data.get(int, "Cycle")
flow_cell = row_data.get(str, "Flow cell")
solution = row_data.get(str, "Solution")
calc_conc = row_data.get(float, "Calculated conc. (µg/ml)")

# Skip rows with missing key data or None calculated concentration
if cycle is None or flow_cell is None or solution is None:
continue
if calc_conc is None:
continue

# Build key: "cycle flowcell solution"
key = f"{cycle} {flow_cell} {solution}"
self._data[key] = calc_conc

def get_calculated_concentration(
self,
cycle: int,
flow_cell: str | int,
solution: str | None,
) -> float | None:
"""Get calculated concentration for a given cycle, flow cell, and solution.

Args:
cycle: Cycle number
flow_cell: Flow cell identifier (int or string like "2-1")
solution: Solution name (e.g., "Calib 1", "Sample 1")

Returns:
Calculated concentration in µg/ml, or None if not found
"""
if solution is None:
return None
key = f"{cycle} {flow_cell} {solution}"
return self._data.get(key)


@dataclass(frozen=True)
class ReportPointData:
identifier: str
Expand Down Expand Up @@ -395,6 +467,7 @@ def create(
channel_data: pd.DataFrame,
metadata: BiacoreInsightMetadata,
evaluation_kinetics: EvaluationKinetics,
evaluation_concentration: EvaluationConcentration,
grouping_column: str = "Channel",
) -> MeasurementData:
identifier = random_uuid_str()
Expand Down Expand Up @@ -489,6 +562,17 @@ def create(
)
analyte_solution = first_row_data.get(str, "Analyte 1 Solution")

# Get calculated concentration from Evaluation sheets if available
# Use the first flow cell from channel_data to look up the concentration
first_flow_cell = str(channel_data["Flow cell"].iloc[0])
calculated_concentration = (
evaluation_concentration.get_calculated_concentration(
cycle=cycle_number,
flow_cell=first_flow_cell,
solution=analyte_solution,
)
)

return MeasurementData(
identifier=identifier,
sample_identifier=f"Run{run}_Cycle{cycle_number}",
Expand Down Expand Up @@ -534,6 +618,10 @@ def create(
),
)
),
"Analyte 1 Calculated Concentration": quantity_or_none(
TQuantityValueMicrogramPerMilliliter,
calculated_concentration,
),
"Analyte 1 Molecular weight": quantity_or_none(
TQuantityValueDalton,
first_row_data.get(float, "Analyte 1 Molecular weight (Da)"),
Expand Down Expand Up @@ -589,6 +677,31 @@ def create(reader: CytivaBiacoreInsightReader) -> Data:
if evaluation_kinetics is None:
evaluation_kinetics = EvaluationKinetics(pd.DataFrame())

# OPTIMIZATION: Parse evaluation concentration data once, not per cycle
# Look for Evaluation sheets with concentration data
evaluation_concentration_tables = []
evaluation_sheet_prefixes = [
"Evaluation - Trend_",
"Evaluation - Preced_",
]
for sheet_name in reader.data.keys():
if any(
sheet_name.startswith(prefix) for prefix in evaluation_sheet_prefixes
):
try:
eval_table = _get_table_from_dataframe(
reader.data[sheet_name], split_on="Cycle"
)
evaluation_concentration_tables.append(eval_table)
except (KeyError, ValueError, AssertionError):
# If parsing fails (missing columns, malformed data), skip this sheet
# Evaluation sheets are optional, so we continue without them
continue

evaluation_concentration = EvaluationConcentration(
evaluation_concentration_tables
)

# OPTIMIZATION: Group all data by cycle once using pandas groupby
# This is much faster than filtering per cycle
cycles_dict = {}
Expand All @@ -601,7 +714,11 @@ def create(reader: CytivaBiacoreInsightReader) -> Data:
cycle_int = int(float(str(cycle_number)))
cycles_dict[cycle_int] = [
MeasurementData.create(
channel_data, metadata, evaluation_kinetics, grouping_column
channel_data,
metadata,
evaluation_kinetics,
evaluation_concentration,
grouping_column,
)
for _, channel_data in cycle_group.groupby(grouping_column)
]
Expand Down Expand Up @@ -652,12 +769,40 @@ def create_measurements_for_cycle(
if evaluation_kinetics is None:
evaluation_kinetics = EvaluationKinetics(pd.DataFrame())

# Parse evaluation concentration data
evaluation_concentration_tables = []
evaluation_sheet_prefixes = [
"Evaluation - Trend_",
"Evaluation - Preced_",
]
for sheet_name in reader.data.keys():
if any(
sheet_name.startswith(prefix) for prefix in evaluation_sheet_prefixes
):
try:
eval_table = _get_table_from_dataframe(
reader.data[sheet_name], split_on="Cycle"
)
evaluation_concentration_tables.append(eval_table)
except (KeyError, ValueError, AssertionError):
# If parsing fails (missing columns, malformed data), skip this sheet
# Evaluation sheets are optional, so we continue without them
continue

evaluation_concentration = EvaluationConcentration(
evaluation_concentration_tables
)

# Determine grouping column: use "Channel" if available, otherwise "Flow cell"
grouping_column = "Channel" if "Channel" in cycle_data.columns else "Flow cell"

return [
MeasurementData.create(
channel_data, metadata, evaluation_kinetics, grouping_column
channel_data,
metadata,
evaluation_kinetics,
evaluation_concentration,
grouping_column,
)
for _, channel_data in cycle_data.groupby(grouping_column)
]
Original file line number Diff line number Diff line change
Expand Up @@ -9932,6 +9932,10 @@
"Analyte 1 Concentration": {
"value": 0.215,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.2221081,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -10177,6 +10181,10 @@
"Analyte 1 Concentration": {
"value": 0.215,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.223146185,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -11685,6 +11693,10 @@
"Analyte 1 Concentration": {
"value": 0.215,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.22221145,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -11930,6 +11942,10 @@
"Analyte 1 Concentration": {
"value": 0.215,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.223151684,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -13438,6 +13454,10 @@
"Analyte 1 Concentration": {
"value": 0.322,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.328146726,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -13683,6 +13703,10 @@
"Analyte 1 Concentration": {
"value": 0.322,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.329638571,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -15191,6 +15215,10 @@
"Analyte 1 Concentration": {
"value": 0.322,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.327615142,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -15436,6 +15464,10 @@
"Analyte 1 Concentration": {
"value": 0.322,
"unit": "ug/mL"
},
"Analyte 1 Calculated Concentration": {
"value": 0.330206424,
"unit": "ug/mL"
}
}
},
Expand Down Expand Up @@ -38823,7 +38855,7 @@
"file name": "Name",
"UNC path": "Root/",
"ASM converter name": "allotropy_cytiva_biacore_insight",
"ASM converter version": "0.1.113",
"ASM converter version": "0.1.117",
"software name": "Biacore Insight Evaluation",
"software version": "6.0.7.1750"
},
Expand Down
Loading