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
2 changes: 1 addition & 1 deletion app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def platewells_by_sample_id(self) -> Dict[str, List[PlateWell]]:
result = {}
for plate in self.plate_registry:
for well in plate.plate_wells:
sid = well.sample.sample_id
sid = well.get_sample(self).file_sample_identifier
if sid:
result.setdefault(sid, []).append(well)
return result
Expand Down
71 changes: 67 additions & 4 deletions index.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
EMPTY_BORDER_PX,
make_tab,
get_tab_id,
build_standard_curve_figure,
build_computed_molarity
)

pd.set_option('future.no_silent_downcasting', True)
Expand Down Expand Up @@ -529,6 +531,54 @@ def plate_mapper(plate_ids, objects):
raise Exception("ERROR RAISED INTENTIONALLY - Reached end of manipulate_plates without handling trigger... major underlying issue to resolve.")


@app.callback(
Output({"type": "pcr-curve-graph", "index": MATCH}, "figure"),
Output({"type": "pcr-curve-table", "index": MATCH}, "children"),
Output({"type": "pcr-curve-store", "index": MATCH}, "data"),
Input("app_data_state", "data"),
State({"type": "datatable-plate", "index": MATCH}, "rowData"),
State({"type": "datatable-plate", "index": MATCH}, "id"),
prevent_initial_call=True,
)
def update_standard_curve(app_state, row_data, plate_id):

if not row_data:
return no_update, no_update, no_update

else:
app_state = AppState.from_json(app_state)
plate = app_state.plate_by_id.get(plate_id.get("index"))
df = plate.dataset(app_state, tidy=True).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False)

# grab only samples which are defined as standards
standards_df = df[df['sample_type'] == 'Standard'].copy()

# ensure numeric and drop rows with missing/invalid Standard or Cq
standards_df["Standard"] = pd.to_numeric(standards_df["Standard"], errors="coerce")
standards_df = standards_df.loc[standards_df["Standard"].notna() & standards_df["Cq"].notna()]

pcr_fig, slope, intercept, efficiency, r_squared = build_standard_curve_figure(standards_df)
store_data = {
"slope": slope,
"intercept": intercept,
"efficiency": efficiency,
"r_squared": r_squared
}

pcr_table_children = [
html.Tr([html.Th("PCR Efficiency"), html.Th("R-Squared"), html.Th("Slope"), html.Th("Intercept")]),
html.Tr([
html.Td(f"{efficiency:.1f}%"),
html.Td(f"{r_squared:.3f}"),
html.Td(f"{slope:.3f}"),
html.Td(f"{intercept:.3f}")
])
]
return pcr_fig, pcr_table_children, store_data




@app.callback(
[
Output("dynamic-tabs", "children", allow_duplicate=True),
Expand Down Expand Up @@ -766,10 +816,14 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig):
Output({"type": "datatable-plate", "index": MATCH}, "rowData"),
Input("update-rows-trigger-div", "children"), # broadcast trigger
Input("app_data_state", "data"), # source of truth
Input({"type": "pcr-curve-store", "index": MATCH}, "data"), # for qPCR plates, to show efficiency etc
# Input({"type": "phix-control-dropdown", "index": MATCH}, "value"), # to highlight the selected phix control value
State({"type": "datatable-plate", "index": MATCH}, "id"), # gives us the plate_id (via id["index"])
prevent_initial_call=True
)
def update_rows_callback(_, app_data_state_json, table_id):
# def update_rows_callback(_, app_data_state_json, pcr_store, phix_control_value, table_id):
def update_rows_callback(_, app_data_state_json, pcr_store, table_id):

if not app_data_state_json or not table_id:
raise PreventUpdate

Expand All @@ -778,14 +832,23 @@ def update_rows_callback(_, app_data_state_json, table_id):

# find the plate by invariant key
plate = app_state.plate_by_id.get(plate_id)

slope, intercept = pcr_store.get("slope"), pcr_store.get("intercept") if pcr_store else (None, None)

phix_correction_factor = 1.0

# if phix_control_value is None:
# phix_correction_factor = 1.0
# else:
# phix_correction_factor = 10 / float(phix_control_value)

if not plate:
return None

print("Processing Plate ID:", plate_id)

is_qpcr = (plate.plate_type == "qPCR")

df = plate.dataset(app_state, tidy=False).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False)
df = build_computed_molarity(app_state, plate, slope, intercept, phix_correction_factor)

# Keep columns tidy, but do not drop keys AG Grid needs (e.g., well_position_id if you use getRowId)
if is_qpcr:
Expand Down Expand Up @@ -842,7 +905,7 @@ def update_plate_buttons(app_data_state_json, selected_tab):
colum_value = row.get(region_molarity_col)
plate_type_display = "TS"
label = f"{plate_type_display} - PhiX - {well} - {colum_value}"
value = f"{plate_type_display} - {well} - {colum_value}"
value = str(colum_value)
dropdown_options.append({"label": label, "value": value})

return html.Div(
Expand Down
10 changes: 8 additions & 2 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ class Sample(BaseModel):
container: Optional[str] = ""
file_sample_identifier: Optional[str] = "" # File-specific column for identifying the sample
sample_type: Optional[SampleType] = SampleType.LIBRARY
computed_molarity: Optional[float | str] = " - " # Computed molarity for qPCR samples
computed_standard_error: Optional[float | str] = " - " # Standard error for qPCR samples

def get_plate_wells(self, app_state):
return app_state.platewells_by_sample_id.get(self.sample_id, [])
return app_state.platewells_by_sample_id.get(self.file_sample_identifier, [])


#####################################################
Expand Down Expand Up @@ -157,6 +159,10 @@ def get_well_by_position(self, position: str) -> Optional["PlateWell"]:

def to_json(self) -> str:
return self.model_dump_json()

def get_samples(self, app_state) -> List[Sample]:
sample_ids = {well.sample_identifier for well in self.plate_wells}
return [app_state.sample_by_id[sid] for sid in sample_ids if sid in app_state.sample_by_id]

@classmethod
def from_json(cls, json_str: str) -> "Plate":
Expand Down Expand Up @@ -260,7 +266,7 @@ def from_file(cls, input_file: "File", app_state) -> "Plate":

app_state.sample_registry.append(sample)

print("created sample", sample)
# print("created sample", sample)
for well_position, well_df in df_slice.groupby(input_file.well_position_id):
well_kwargs = {}
if input_file.plate_specific_columns:
Expand Down
Loading
Loading