diff --git a/dashboard/data/bmg_plate.py b/dashboard/data/bmg_plate.py index 82662408..e0e03216 100644 --- a/dashboard/data/bmg_plate.py +++ b/dashboard/data/bmg_plate.py @@ -5,12 +5,6 @@ from enum import Enum, auto -class Mode(Enum): - ACTIVATION = auto() - INHIBITION = auto() - ALL = auto() - - PlateSummary = namedtuple( "PlateSummary", [ @@ -194,7 +188,7 @@ def parse_bmg_files(files: tuple[str, io.StringIO]) -> tuple[pd.DataFrame, np.nd def calculate_activation_inhibition_zscore( values: np.ndarray, stats: dict, - mode: Mode = Mode.ALL, + mode: str, without_pos: bool = False, ) -> tuple[np.ndarray]: """ @@ -207,8 +201,7 @@ def calculate_activation_inhibition_zscore( :return: activation, inhibition and z-score values """ activation, inhibition = None, None - if mode == Mode.ACTIVATION or mode == Mode.ALL: - # NOTE: for now `without_pos` is not used + if mode == "activation": if without_pos: activation = (values - stats["mean_neg"]) / (stats["mean_neg"]) * 100 else: @@ -218,10 +211,10 @@ def calculate_activation_inhibition_zscore( * 100 ) - if mode == Mode.INHIBITION or mode == Mode.ALL: + if mode == "inhibition": inhibition = ( - 1 - ((values - stats["mean_pos"])) / (stats["mean_neg"] - stats["mean_pos"]) - ) * 100 + (values - stats["mean_neg"]) / (stats["mean_pos"] - stats["mean_neg"]) * 100 + ) z_score = (values - stats["mean_cmpd"]) / stats["std_cmpd"] @@ -229,14 +222,17 @@ def calculate_activation_inhibition_zscore( def get_activation_inhibition_zscore_dict( - df_stats: pd.DataFrame, plate_values: np.ndarray, modes: dict[Mode] + df_stats: pd.DataFrame, + plate_values: np.ndarray, + mode: str, + without_pos: bool = False, ) -> dict[str, dict[str, float]]: """ Calculates activation and inhibition for each compound in the plates. :param df_stats: dataframe with statistics for each plate :param plate_values: array with values in the plate - :param mode: list of modes to calculate activation and inhibition + :param mode: mode to calculate activation and inhibition :return: dictionary with activation and inhibition values for each compound in the plate """ stats = {} @@ -251,11 +247,8 @@ def get_activation_inhibition_zscore_dict( act_inh_dict = {} for (_, row_stats), v in zip(df_stats.iterrows(), plate_values): - mode = ( - modes[row_stats["barcode"]] if row_stats["barcode"] in modes else Mode.ALL - ) activation, inhibition, z_score = calculate_activation_inhibition_zscore( - v[0], stats, mode=mode + v[0], stats, mode, without_pos ) act_inh_dict[row_stats["barcode"]] = { "activation": activation, diff --git a/dashboard/data/combine.py b/dashboard/data/combine.py index bbd7be75..610f6abb 100644 --- a/dashboard/data/combine.py +++ b/dashboard/data/combine.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from dashboard.data.bmg_plate import Mode, get_activation_inhibition_zscore_dict +from dashboard.data.bmg_plate import get_activation_inhibition_zscore_dict def values_array_to_column( @@ -93,7 +93,8 @@ def combine_bmg_echo_data( echo_df: pd.DataFrame, df_stats: pd.DataFrame, plate_values: np.ndarray, - modes: dict[Mode] = None, + mode: str, + without_pos: bool = False, ) -> pd.DataFrame: """ Combine Echo data with activation and inhibition values. @@ -101,15 +102,15 @@ def combine_bmg_echo_data( :param echo_df: dataframe with Echo data :param df_stats: dataframe containing statistics for each plate :param plate_values: numpy array with activation and inhibition values - shape: (#plates, 2, 16, 24) - :param modes: dictionary with modes for each plate + :param mode: mode to calculate activation and inhibition :return: dataframe with Echo data and activation and inhibition values """ PLATE = "Destination Plate Barcode" WELL = "Destination Well" - if modes is None: - modes = dict() - act_inh_dict = get_activation_inhibition_zscore_dict(df_stats, plate_values, modes) + act_inh_dict = get_activation_inhibition_zscore_dict( + df_stats, plate_values, mode, without_pos + ) dfs = [] for barcode, values_dict in act_inh_dict.items(): activation_inhibition_df = get_activation_inhibition_zscore_df( @@ -150,7 +151,7 @@ def split_compounds_controls(df: pd.DataFrame) -> tuple[pd.DataFrame]: def aggregate_well_plate_stats( - df: pd.DataFrame, assign_x_coords: bool = False + df: pd.DataFrame, key: str, assign_x_coords: bool = False ) -> tuple[pd.DataFrame]: """ Aggregates the statistics (mean and std) per plate needed for the plots. @@ -163,25 +164,19 @@ def aggregate_well_plate_stats( """ PLATE = "Destination Plate Barcode" - ACTIVATION = "% ACTIVATION" - INHIBITION = "% INHIBITION" Z_SCORE = "Z-SCORE" stats_df = ( - df.groupby(PLATE)[[ACTIVATION, INHIBITION, Z_SCORE]] + df.groupby(PLATE)[[key, Z_SCORE]] .agg(["mean", "std", "min", "max"]) .reset_index() ) stats_df.columns = [ PLATE, - f"{ACTIVATION}_mean", - f"{ACTIVATION}_std", - f"{ACTIVATION}_min", - f"{ACTIVATION}_max", - f"{INHIBITION}_mean", - f"{INHIBITION}_std", - f"{INHIBITION}_min", - f"{INHIBITION}_max", + f"{key}_mean", + f"{key}_std", + f"{key}_min", + f"{key}_max", f"{Z_SCORE}_mean", f"{Z_SCORE}_std", f"{Z_SCORE}_min", @@ -189,7 +184,7 @@ def aggregate_well_plate_stats( ] if assign_x_coords: - for col in [ACTIVATION, INHIBITION, Z_SCORE]: + for col in [key, Z_SCORE]: stats_df = stats_df.sort_values(by=f"{col}_mean") # sort by mean stats_df[f"{col}_x"] = range(len(stats_df)) # get the x coordinates diff --git a/dashboard/pages/components.py b/dashboard/pages/components.py index 640f9a89..9d534c1e 100644 --- a/dashboard/pages/components.py +++ b/dashboard/pages/components.py @@ -22,6 +22,7 @@ dcc.Store(id="z-slider-value", storage_type="local"), dcc.Store(id="report-data-correlation-plots", storage_type="local"), dcc.Store(id="report-data-hit-validation-input", storage_type="local"), + dcc.Store(id="activation-inhibition-screening-options", storage_type="local"), ], ) diff --git a/dashboard/pages/screening/callbacks.py b/dashboard/pages/screening/callbacks.py index 15e837d3..878e5558 100644 --- a/dashboard/pages/screening/callbacks.py +++ b/dashboard/pages/screening/callbacks.py @@ -1,6 +1,7 @@ import base64 import functools import io +import json import uuid from datetime import datetime import json @@ -25,7 +26,6 @@ from dashboard.data.combine import ( aggregate_well_plate_stats, combine_bmg_echo_data, - reorder_bmg_echo_columns, split_compounds_controls, ) from dashboard.data.file_preprocessing.echo_files_parser import EchoFilesParser @@ -41,6 +41,10 @@ plot_z_per_plate, visualize_multiple_plates, ) +from dashboard.visualization.text_tables import ( + make_filter_radio_options, + make_summary_stage_datatable, +) # === STAGE 1 === @@ -288,12 +292,37 @@ def upload_echo_data( ) +def on_additional_options_change( + key: str, + formula: str, +) -> dict[str, str]: + """ + Update the additional screening options dictionary + + :param screening_feature: screening feature + :param formula: formula + :return: updated dictionary + """ + disabled = key != "activation" + options_dict = {} + options_dict["key"] = key + options_dict["feature_column"] = "% " + key.upper() + options_dict["without_pos"] = formula + return options_dict, disabled + + # === STAGE 5 === def on_summary_entry( - current_stage: int, stored_uuid: str, z_slider: float, file_storage: FileStorage -) -> tuple[pd.DataFrame, go.Figure, go.Figure, go.Figure]: + current_stage: int, + stored_uuid: str, + z_slider: float, + screening_options: dict, + file_storage: FileStorage, +) -> tuple[ + pd.DataFrame, go.Figure, go.Figure, float, float, float, float, str, html.Div +]: """ Callback for the stage 5 entry Loads the data from storage and prepares visualizations @@ -320,37 +349,41 @@ def on_summary_entry( bmg_df, bmg_vals, z_slider["z_slider_value"] ) - echo_bmg_combined = combine_bmg_echo_data(echo_df, filtered_df, filtered_vals) - drop_duplicates = ( - True # TODO: inform the user about it/allow for deciding what to do + echo_bmg_combined = combine_bmg_echo_data( + echo_df, + filtered_df, + filtered_vals, + screening_options["key"], + screening_options["without_pos"], ) - - if drop_duplicates: - echo_bmg_combined = echo_bmg_combined.drop_duplicates() + echo_bmg_combined = echo_bmg_combined.drop_duplicates() compounds_df, control_pos_df, control_neg_df = split_compounds_controls( echo_bmg_combined ) compounds_df = compounds_df.dropna() - - activation_min = round(compounds_df["% ACTIVATION"].min()) - activation_max = round(compounds_df["% ACTIVATION"].max()) - inhibition_min = round(compounds_df["% INHIBITION"].min()) - inhibition_max = round(compounds_df["% INHIBITION"].max()) - file_storage.save_file( f"{stored_uuid}_echo_bmg_combined_df.pq", compounds_df.to_parquet() ) - cmpd_plate_stats_df = aggregate_well_plate_stats(compounds_df, assign_x_coords=True) - pos_plate_stats_df = aggregate_well_plate_stats(control_pos_df) - neg_plate_stats_df = aggregate_well_plate_stats(control_neg_df) + cmpd_plate_stats_df = aggregate_well_plate_stats( + compounds_df, screening_options["feature_column"], assign_x_coords=True + ) + pos_plate_stats_df = aggregate_well_plate_stats( + control_pos_df, screening_options["feature_column"] + ) + neg_plate_stats_df = aggregate_well_plate_stats( + control_neg_df, screening_options["feature_column"] + ) plate_stats_dfs = [cmpd_plate_stats_df, pos_plate_stats_df, neg_plate_stats_df] file_storage.save_file( f"{stored_uuid}_plate_stats_df.pq", cmpd_plate_stats_df.to_parquet() ) + feature_min = round(compounds_df[screening_options["feature_column"]].min()) + feature_max = round(compounds_df[screening_options["feature_column"]].max()) + fig_z_score = plot_activation_inhibition_zscore( compounds_df, plate_stats_dfs, @@ -358,51 +391,36 @@ def on_summary_entry( (-3, 3), # z-score min and max ) - fig_activation = plot_activation_inhibition_zscore( + fig_feature = plot_activation_inhibition_zscore( compounds_df, plate_stats_dfs, - "% ACTIVATION", - (activation_min, activation_max), + screening_options["feature_column"], + (feature_min, feature_max), ) - fig_inhibition = plot_activation_inhibition_zscore( - compounds_df, - plate_stats_dfs, - "% INHIBITION", - (inhibition_min, inhibition_max), + compounds_url_df = eos_to_ecbd_link(compounds_df) + summary_stage_datatable = make_summary_stage_datatable( + compounds_url_df, screening_options["feature_column"] ) - compounds_url_df = eos_to_ecbd_link(compounds_df) + radio_options = make_filter_radio_options(screening_options["key"]) report_data = { "fig_z_score": fig_z_score.to_html(full_html=False, include_plotlyjs="cdn"), - "fig_activation": fig_activation.to_html( - full_html=False, include_plotlyjs="cdn" - ), - "fig_inhibition": fig_inhibition.to_html( - full_html=False, include_plotlyjs="cdn" - ), + "fig_feature": fig_feature.to_html(full_html=False, include_plotlyjs="cdn"), } return ( - compounds_url_df.to_dict("records"), + summary_stage_datatable, fig_z_score, - fig_activation, - fig_inhibition, + fig_feature, -3, # z_score_min, 3, # z_score_max, - activation_min, - activation_max, - inhibition_min, - inhibition_max, - # NOTE: this will be cleared in the next PR (ACT/INH into one) - False, - False, - False, - False, - False, - False, + feature_min, + feature_max, f"number of compounds: {len(compounds_df)}", + f"{screening_options['feature_column']} range:", + radio_options, report_data, ) @@ -411,10 +429,8 @@ def on_filter_radio_or_range_update( key: str, z_score_min: float, z_score_max: float, - activation_min: float, - activation_max: float, - inhibition_min: float, - inhibition_max: float, + feature_min: float, + feature_max: float, ) -> dict: """ Callback for the filter radio button update @@ -422,10 +438,8 @@ def on_filter_radio_or_range_update( :param key: key to filter by :param z_score_min: min z-score value :param z_score_max: max z-score value - :param activation_min: min activation value - :param activation_max: max activation value - :param inhibition_min: min inhibition value - :param inhibition_max: max inhibition value + :param feature_min: min feature value + :param feature_max: max feature value :return: dictionary storing the ranges of interest """ @@ -434,13 +448,9 @@ def on_filter_radio_or_range_update( if key == "z_score": report_data_csv["key_min"] = z_score_min report_data_csv["key_max"] = z_score_max - elif key == "activation": - report_data_csv["key_min"] = activation_min - report_data_csv["key_max"] = activation_max - elif key == "inhibition": - report_data_csv["key_min"] = inhibition_min - report_data_csv["key_max"] = inhibition_max - + elif key == "activation" or key == "inhibition": + report_data_csv["key_min"] = feature_min + report_data_csv["key_max"] = feature_max return report_data_csv @@ -449,8 +459,8 @@ def on_range_update( max_value: float, figure: go.Figure, stored_uuid: str, + key: dict, file_storage: FileStorage, - key: str, ) -> go.Figure: """ Callback for the z-score range update button @@ -464,8 +474,10 @@ def on_range_update( :param key: key to filter by :return: updated figure """ + if key != "Z-SCORE": + key = key["feature_column"] - if max_value is None or max_value is None or max_value < min_value: + if min_value is None or max_value is None or max_value < min_value: return figure new_figure = go.Figure(figure) @@ -509,6 +521,7 @@ def on_range_update( x=outside_range_df[f"{key}_x"], y=outside_range_df[key], customdata=np.stack((outside_range_df[PLATE], outside_range_df[WELL]), axis=-1), + text=outside_range_df["EOS"], selector=dict(name="COMPOUNDS OUTSIDE"), ) @@ -591,7 +604,6 @@ def on_report_generate_button_click( report_data_second_stage: dict, report_data_third_stage: dict, report_data_screening_summary_plots: dict, - file_storage: FileStorage, ): filename = f"screening_report_{datetime.now().strftime('%Y-%m-%d')}.html" report_data_second_stage.update(report_data_third_stage) @@ -613,7 +625,6 @@ def on_json_generate_button_click( report_data_second_stage: dict, report_data_third_stage: dict, report_data_csv: dict, - file_storage: FileStorage, ): filename = f"screening_settings_{datetime.now().strftime('%Y-%m-%d')}.json" process_settings = read_stages_stats( @@ -682,29 +693,30 @@ def register_callbacks(elements, file_storage): )(functools.partial(upload_echo_data, file_storage=file_storage)) callback( - Output("echo-bmg-combined", "data"), + Output("activation-inhibition-screening-options", "data"), + Output("activation-formula-dropdown", "disabled"), + Input("screening-feature-dropdown", "value"), + Input("activation-formula-dropdown", "value"), + )(on_additional_options_change) + + callback( + Output("compounds-data-table", "children"), Output("z-score-plot", "figure"), - Output("activation-plot", "figure"), - Output("inhibition-plot", "figure"), + Output("feature-plot", "figure"), Output("z-score-min-input", "value"), Output("z-score-max-input", "value"), - Output("activation-min-input", "value"), - Output("activation-max-input", "value"), - Output("inhibition-min-input", "value"), - Output("inhibition-max-input", "value"), - Output("z-score-min-input", "disabled"), - Output("z-score-max-input", "disabled"), - Output("activation-min-input", "disabled"), - Output("activation-max-input", "disabled"), - Output("inhibition-min-input", "disabled"), - Output("inhibition-max-input", "disabled"), + Output("feature-min-input", "value"), + Output("feature-max-input", "value"), Output("compounds-data-subtitle", "children"), + Output("tab-feature-header", "children"), + Output("filter-radio", "options"), Output("report-data-screening-summary-plots", "data"), Input(elements["STAGES_STORE"], "data"), State("user-uuid", "data"), State("z-slider-value", "data"), + State("activation-inhibition-screening-options", "data"), )(functools.partial(on_summary_entry, file_storage=file_storage)) - + # Z-SCORE callback( Output("z-score-plot", "figure", allow_duplicate=True), Input("z-score-min-input", "value"), @@ -712,33 +724,25 @@ def register_callbacks(elements, file_storage): State("z-score-plot", "figure"), State("user-uuid", "data"), prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="Z-SCORE")) - callback( - Output("activation-plot", "figure", allow_duplicate=True), - Input("activation-min-input", "value"), - Input("activation-max-input", "value"), - State("activation-plot", "figure"), - State("user-uuid", "data"), - prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="% ACTIVATION")) + )(functools.partial(on_range_update, key="Z-SCORE", file_storage=file_storage)) + # ACTIVATION/INHIBITION callback( - Output("inhibition-plot", "figure", allow_duplicate=True), - Input("inhibition-min-input", "value"), - Input("inhibition-max-input", "value"), - State("inhibition-plot", "figure"), + Output("feature-plot", "figure", allow_duplicate=True), + Input("feature-min-input", "value"), + Input("feature-max-input", "value"), + State("feature-plot", "figure"), State("user-uuid", "data"), + State("activation-inhibition-screening-options", "data"), prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="% INHIBITION")) + )(functools.partial(on_range_update, file_storage=file_storage)) callback( Output("report-data-csv", "data"), Input("filter-radio", "value"), Input("z-score-min-input", "value"), Input("z-score-max-input", "value"), - Input("activation-min-input", "value"), - Input("activation-max-input", "value"), - Input("inhibition-min-input", "value"), - Input("inhibition-max-input", "value"), - )(functools.partial(on_filter_radio_or_range_update)) + Input("feature-min-input", "value"), + Input("feature-max-input", "value"), + )(on_filter_radio_or_range_update) callback( Output("download-echo-bmg-combined", "data"), Input("save-results-button", "n_clicks"), @@ -760,7 +764,7 @@ def register_callbacks(elements, file_storage): State("report-data-third-stage", "data"), State("report-data-screening-summary-plots", "data"), prevent_initial_call=True, - )(functools.partial(on_report_generate_button_click, file_storage=file_storage)) + )(on_report_generate_button_click) callback( Output("download-json-settings-screening", "data"), Input("generate-json-button", "n_clicks"), @@ -768,4 +772,4 @@ def register_callbacks(elements, file_storage): State("report-data-third-stage", "data"), State("report-data-csv", "data"), prevent_initial_call=True, - )(functools.partial(on_json_generate_button_click, file_storage=file_storage)) + )(on_json_generate_button_click) diff --git a/dashboard/pages/screening/report/report.html b/dashboard/pages/screening/report/report.html index c2edc2e1..817dfa76 100644 --- a/dashboard/pages/screening/report/report.html +++ b/dashboard/pages/screening/report/report.html @@ -75,10 +75,7 @@
- {{fig_activation|safe}} -
-- {{fig_inhibition|safe}} + {{fig_feature|safe}}