diff --git a/pyproject.toml b/pyproject.toml index e129d80..5b5b322 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "deeranalysis" -version = "2026.0.2" +version = "2026.0.3" description = "A graphical fitting and data managment tool for DEER spectroscopy, using DeerLab and DeerNet as fitting engines." authors = [ {name = "Hugo Karas",email = "hkaras@ethz.ch"} diff --git a/src/deeranalysis/components/dataset_search_model.py b/src/deeranalysis/components/dataset_search_model.py index 378296d..f89c716 100644 --- a/src/deeranalysis/components/dataset_search_model.py +++ b/src/deeranalysis/components/dataset_search_model.py @@ -32,7 +32,8 @@ def create_dataset_AGgrid(id="datasets_grid"): ),style={"height": "50vh", "marginBottom": "10px"}) return grid -def create_dataset_modal(page_id): +def create_dataset_modal(page_id, select_btn_id=None): + select_btn_id = select_btn_id or {'type': 'select-dataset-btn', 'page': page_id} return html.Div([ dmc.Modal( title="Search Datasets", @@ -47,7 +48,7 @@ def create_dataset_modal(page_id): ), create_dataset_AGgrid("dataset_table"), dmc.Group( - [dmc.Button("Select Dataset", id={'type': 'select-dataset-btn', 'page': page_id})], + [dmc.Button("Select Dataset", id=select_btn_id)], justify="flex-end", className="mt-2" ), diff --git a/src/deeranalysis/components/fit_page_components.py b/src/deeranalysis/components/fit_page_components.py index 07918cc..da1c3f5 100644 --- a/src/deeranalysis/components/fit_page_components.py +++ b/src/deeranalysis/components/fit_page_components.py @@ -117,7 +117,7 @@ def distance_slider(page_id): return dmc.Stack([dmc.Text("Distance Axis (nm): ", size="sm", fw=500, mb=4), dcc.RangeSlider( id= {"type": "distance-axis", "page": page_id}, - min=1.5, + min=1.25, max=12, step=0.25, value=[1.75, 6], @@ -358,7 +358,7 @@ def pathway_input(page_id): return dmc.Tooltip(dmc.CheckboxGroup( id={'type': 'pathways-options', 'page': page_id}, label="Pathways to include:", - description="These pathways will be applied to all datasets, if they are fesiable for the corresponding experiment.", + description="These pathways will be applied to all datasets, if they are feasible for the corresponding experiment.", children=dmc.Group([ dmc.Checkbox(value='1', label='1'), dmc.Checkbox(value='2', label='2'), diff --git a/src/deeranalysis/components/model_edit_modal.py b/src/deeranalysis/components/model_edit_modal.py index 085485b..b5aa6e9 100644 --- a/src/deeranalysis/components/model_edit_modal.py +++ b/src/deeranalysis/components/model_edit_modal.py @@ -12,8 +12,8 @@ '3pDEER': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_3pdeer.html', '4pDEER': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_4pdeer.html', '5pDEER': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_fwd5pdeer.html', - 'dqc': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_dqc.html', - 'ridme': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_ridme.html', + 'DQC': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_dqc.html', + 'RIDME': 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.ex_ridme.html', } b_model_links = { @@ -52,8 +52,8 @@ def make_btn(label, href): base_path = 'https://jeschkelab.github.io/DeerLab/_autosummary/deerlab.{model}.html' return [ - make_btn("Experiment Model", base_path.format(model=exp_type) if (exp_type and exp_type != 'single') else None), - make_btn("Background Model", base_path.format(model=bg_model) if bg_model else None), + make_btn("Experiment Model", exp_model_links.get(exp_type, None) if (exp_type and exp_type != 'single') else None), + make_btn("Background Model", b_model_links.get(bg_model, None)), make_btn("Distance Distribution Model", p_model_links.get(p_model)), ] diff --git a/src/deeranalysis/pages/comparison.py b/src/deeranalysis/pages/comparison.py index 070a4d5..fd2aee3 100644 --- a/src/deeranalysis/pages/comparison.py +++ b/src/deeranalysis/pages/comparison.py @@ -10,7 +10,7 @@ from deeranalysis.utils.deerlab_options import plotly_comparison, colour_scheme_dark, colour_scheme_light -from deerlab import UQResult +from deerlab import UQResult, noiselevel dash.register_page(__name__) PAGE_ID = 'comparison' @@ -22,9 +22,10 @@ layout = html.Div([ dcc.Store(id='comp-n-slots', data=N_SLOTS_DEFAULT), + dcc.Store(id='comp-search-target-slot'), # ── Hidden: modals & drawer ──────────────────────────────────────────── - create_dataset_modal(PAGE_ID), + create_dataset_modal(PAGE_ID, select_btn_id={'type': 'comp-select-dataset-btn', 'page': PAGE_ID}), search_fit_modal(), dmc.Drawer( @@ -243,24 +244,30 @@ def get_options(dataset_id): @callback( Output('comp-plot', 'figure'), Output('comp-stats-table', 'children'), + Input({'type': 'dataset-dropdown', 'page': PAGE_ID, 'index': ALL}, 'value'), Input({'type': 'fit-dropdown', 'page': PAGE_ID, 'index': ALL}, 'value'), Input('comp-n-slots', 'data'), Input('comp-voffset-slider', 'value'), Input('comp-ci-select', 'value'), Input('comp-show-ci-toggle', 'checked'), ) -def compare_fits(fit_ids, n_slots, offset, ci_str, show_ci): +def compare_fits(dataset_ids, fit_ids, n_slots, offset, ci_str, show_ci): ci = int(ci_str) if ci_str else 95 + dataset_ids = dataset_ids[:n_slots] fit_ids = fit_ids[:n_slots] session = get_session() loaded = [] - for fid in fit_ids: - if not fid: - continue - fit = session.query(Fit).filter_by(id=fid).first() - if fit: - loaded.append((fit.dataset, fit)) + for did, fid in zip(dataset_ids, fit_ids): + if fid: + fit = session.query(Fit).filter_by(id=fid).first() + if fit: + loaded.append((fit.dataset, fit)) + continue + if did: + dataset = session.query(Dataset).filter_by(id=did).first() + if dataset: + loaded.append((dataset, None)) session.close() data_dicts = [_fit_to_dict(ds, fit) for ds, fit in loaded] @@ -275,6 +282,47 @@ def compare_fits(fit_ids, n_slots, offset, ci_str, show_ci): return fig, stats +@callback( + Output({'type': 'dataset-search-modal', 'page': PAGE_ID}, 'opened', allow_duplicate=True), + Output('comp-search-target-slot', 'data'), + Input({'type': 'open-dataset-search-btn', 'page': PAGE_ID, 'index': ALL}, 'n_clicks'), + prevent_initial_call=True, +) +def comp_open_dataset_search(n_clicks_list): + if not any(n_clicks_list): + return dash.no_update, dash.no_update + return True, ctx.triggered_id['index'] + + +@callback( + Output({'type': 'dataset-search-modal', 'page': PAGE_ID}, 'opened', allow_duplicate=True), + Output({'type': 'dataset-dropdown', 'page': PAGE_ID, 'index': ALL}, 'value'), + Input({'type': 'comp-select-dataset-btn', 'page': PAGE_ID}, 'n_clicks'), + State('dataset_table', 'selectedRows'), + State('comp-search-target-slot', 'data'), + State({'type': 'dataset-dropdown', 'page': PAGE_ID, 'index': ALL}, 'id'), + State({'type': 'dataset-dropdown', 'page': PAGE_ID, 'index': ALL}, 'value'), + prevent_initial_call=True, +) +def comp_select_dataset(n_clicks, selected_rows, target_slot, ids, current_values): + no_change = [dash.no_update] * len(current_values) + if not (n_clicks and selected_rows and target_slot): + return dash.no_update, no_change + + dataset_title = selected_rows[0].get('Title') + session = get_session() + dataset = session.query(Dataset).filter_by(name=dataset_title).first() + session.close() + if dataset is None: + return dash.no_update, no_change + + values = list(current_values) + for i, slot_id in enumerate(ids): + if str(slot_id['index']) == str(target_slot): + values[i] = str(dataset.id) + return False, values + + # ── Helpers ────────────────────────────────────────────────────────────────── def _convert_lists_in_dicts_to_arrays(d): @@ -291,6 +339,21 @@ def _fit_to_dict(dataset, fit): out['t'] = np.array(dataset.t, dtype=float) out['V'] = np.array(dataset.V, dtype=float) out['V'] /= out['V'].max() + + if fit is None: + out['model_t'] = out['t'] + out['dist_stats'] = {} + try: + out['gof'] = {'SNR': float(1.0 / noiselevel(out['V']))} + except Exception: + out['gof'] = {} + out['model'] = None + out['r'] = None + out['P'] = None + out['PUncert'] = None + out['background'] = None + return out + out['model_t'] = np.array(fit.t, dtype=float) out['dist_stats'] = fit.dist_stats or {} out['gof'] = fit.gof or {} @@ -333,7 +396,8 @@ def _fit_to_dict(dataset, fit): 'SNR': 'SNR', 'MNR': 'MNR', 'rmsd': 'RMSD', - 'R2': 'R²',} + 'R2': 'R²', + 'lam': 'Mod. Depth.',} def _compute_stat(metric, dd): try: diff --git a/src/deeranalysis/pages/global.py b/src/deeranalysis/pages/global.py index be1a7e3..d5a4931 100644 --- a/src/deeranalysis/pages/global.py +++ b/src/deeranalysis/pages/global.py @@ -67,7 +67,7 @@ dmc.CheckboxGroup( id={'type': 'pathways-options', 'page': page_id}, label="Pathways to include:", - description="These pathways will be applied to all datasets, if they are fesiable for the corresponding experiment.", + description="These pathways will be applied to all datasets, if they are feasible for the corresponding experiment.", children=dmc.Group([ dmc.Checkbox(value='1', label='1'), dmc.Checkbox(value='2', label='2'), diff --git a/src/deeranalysis/pages/parametric.py b/src/deeranalysis/pages/parametric.py index 3e81c6d..f37e959 100644 --- a/src/deeranalysis/pages/parametric.py +++ b/src/deeranalysis/pages/parametric.py @@ -66,7 +66,7 @@ dmc.CheckboxGroup( id={'type': 'pathways-options', 'page': page_id}, label="Pathways to include:", - description="These pathways will be applied to all datasets, if they are fesiable for the corresponding experiment.", + description="These pathways will be applied to all datasets, if they are feasible for the corresponding experiment.", children=dmc.Group([ dmc.Checkbox(value='1', label='1'), dmc.Checkbox(value='2', label='2'), diff --git a/src/deeranalysis/pages/population.py b/src/deeranalysis/pages/population.py index db053eb..f18d823 100644 --- a/src/deeranalysis/pages/population.py +++ b/src/deeranalysis/pages/population.py @@ -77,7 +77,7 @@ dmc.CheckboxGroup( id={'type': 'pathways-options', 'page': page_id}, label="Pathways to include:", - description="These pathways will be applied to all datasets, if they are fesiable for the corresponding experiment.", + description="These pathways will be applied to all datasets, if they are feasible for the corresponding experiment.", children=dmc.Group([ dmc.Checkbox(value='1', label='1'), dmc.Checkbox(value='2', label='2'), diff --git a/src/deeranalysis/utils/deerlab_normal.py b/src/deeranalysis/utils/deerlab_normal.py index 33a2146..caa1ab8 100644 --- a/src/deeranalysis/utils/deerlab_normal.py +++ b/src/deeranalysis/utils/deerlab_normal.py @@ -240,7 +240,7 @@ def deerlab_fitting(dataset, compactness=True, model=None, exp_type='5pDEER', ve elif exp_type == "3pDEER": experimentInfo = dl.ex_3pdeer(tau=tau1,pathways=pathways,pulselength=pulselength) elif exp_type == "RIDME": - experimentInfo = dl.ex_ridme(tau=tau1,tau2=tau2,pathways=pathways,pulselength=pulselength) + experimentInfo = dl.ex_ridme(tau1=tau1,tau2=tau2,pathways=pathways,pulselength=pulselength) elif exp_type == "SIFTER": experimentInfo = dl.ex_sifter(tau1=tau1,tau2=tau2,pathways=pathways,pulselength=pulselength) elif exp_type == "DQC": @@ -248,7 +248,7 @@ def deerlab_fitting(dataset, compactness=True, model=None, exp_type='5pDEER', ve elif exp_type == "single": experimentInfo = None else: - raise ValueError(f"Experiment type {exp_type} not recognized. Please specify a valid experiment type (e.g., '4pDEER', '5pDEER', '3pDEER').") + raise ValueError(f"Experiment type {exp_type} not recognized. Please specify a valid experiment type (e.g., '4pDEER', '5pDEER', '3pDEER', 'RIDME', 'SIFTER', 'DQC', 'single').") if 'r' in kwargs: r = kwargs.pop('r') diff --git a/src/deeranalysis/utils/deerlab_options.py b/src/deeranalysis/utils/deerlab_options.py index 6157b9b..c8854f4 100644 --- a/src/deeranalysis/utils/deerlab_options.py +++ b/src/deeranalysis/utils/deerlab_options.py @@ -14,7 +14,7 @@ ] experiment_type_options = [ - {"label": "Single Pathway", "value": "single", "max_pathways": 1, "delays": []}, + {"label": "Single Pathway", "value": "single", "max_pathways": 1, "delays": [], 'link': None}, {"label": "4-pulse DEER", "value": "4pDEER", "max_pathways": 4, "delays": ['tau1', 'tau2']}, {"label": "3-pulse DEER", "value": "3pDEER", "max_pathways": 3, "delays": ['tau1']}, {"label": "5-pulse DEER", "value": "5pDEER", "max_pathways": 5, "delays": ['tau1', 'tau2', 'tau3']}, @@ -593,7 +593,7 @@ def build_model_data(dataset, bg_model_name, pathways, r_range, Vmodel = bg_model exp_type='background' else: - if seq_name == '5pDEER' or ('tau3' in attrs and seq_name != '4pDEER'): + if seq_name == '5pDEER': exp_type = '5pDEER' tau1 = attrs['tau1'] / 1e3 tau2 = attrs['tau2'] / 1e3 @@ -615,21 +615,21 @@ def build_model_data(dataset, bg_model_name, pathways, r_range, elif seq_name == 'single': exp_type = 'single' exp_info = None - elif seq_name == 'ridme': - exp_type = 'ridme' + elif seq_name == 'RIDME': + exp_type = 'RIDME' tau1 = attrs['tau1'] / 1e3 tau2 = attrs['tau2'] / 1e3 pathways = [p for p in pathways if p <= 2] - exp_info = dl.ex_ridme(tau=tau1, tau2=tau2, pathways=pathways) - elif seq_name == 'dqc': - exp_type = 'dqc' + exp_info = dl.ex_ridme(tau1=tau1, tau2=tau2, pathways=pathways) + elif seq_name == 'DQC': + exp_type = 'DQC' tau1 = attrs['tau1'] / 1e3 tau2 = attrs['tau2'] / 1e3 tau3 = attrs['tau3'] / 1e3 pathways = [p for p in pathways if p <= 2] - exp_info = dl.ex_dqc(tau=tau1, tau2=tau2, tau3=tau3, pathways=pathways) - elif seq_name == 'sifter': - exp_type = 'sifter' + exp_info = dl.ex_dqc(tau1=tau1, tau2=tau2, tau3=tau3, pathways=pathways) + elif seq_name == 'SIFTER': + exp_type = 'SIFTER' tau1 = attrs['tau1'] / 1e3 tau2 = attrs['tau2'] / 1e3 pathways = [p for p in pathways if p <= 2]