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 pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "deeranalysis"
version = "2026.0.3"
version = "2026.1.0b1"
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"}
Expand Down
34 changes: 34 additions & 0 deletions src/deeranalysis/assets/dashAgGridComponentFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,38 @@ dagcomponentfuncs.TagsCellRenderer = function (props) {
);
})
);
};

// Warning counts with icons. props.value is {critical, moderate}, where a
// count of null means the fit was not checked for warnings and shows as "–".
// Levels with no warnings are left out.
dagcomponentfuncs.WarningsCellRenderer = function (props) {
const value = props.value;
const unchecked = value === null || value === undefined;
const levels = [
{ key: "critical", icon: "mdi:alert-circle-outline", color: "red" },
{ key: "moderate", icon: "mdi:alert-outline", color: "orange" },
];
const children = [];
levels.forEach(function (level) {
const n = unchecked ? null : value[level.key];
if (!unchecked && !n) {
return;
}
children.push(
React.createElement(window.dash_iconify.DashIconify, {
key: level.key + "-icon", icon: level.icon, width: 16, color: level.color,
}),
React.createElement(
"span",
{ key: level.key + "-count", style: { color: level.color, fontWeight: 700 } },
unchecked ? "–" : n
)
);
});
return React.createElement(
"div",
{ style: { display: "flex", gap: "4px", alignItems: "center", height: "100%" } },
children
);
};
33 changes: 25 additions & 8 deletions src/deeranalysis/components/fit_page_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from deeranalysis.utils.deerlab_options import regparam_options, plotly_deerlab, plotly_goodness_of_fit,plotly_lcurve,plotly_dipolar_spectrum
from deeranalysis.utils.database import get_session, Dataset
from deeranalysis.utils import dataarray_from_database_entry
from deeranalysis.components.warnings import number_of_warnings_card, number_of_warnings_children
from deeranalysis.utils.deerlab_fitwarnings import count_by_level, warnings_from_dict

from dash import dcc, html, callback, Input, Output, State, ALL, MATCH, no_update
import deerlab as dl
Expand Down Expand Up @@ -526,25 +528,28 @@ def _overview_card_grids(page_id):
"""Returns the two SimpleGrid rows of overview cards (shared between overview_tab and overview_tab_global)."""
return [
dmc.SimpleGrid(
cols={"base": 1, "sm": 2, "lg": 4},
cols={"base": 1, "sm": 3, "lg": 4},
mt="md",
spacing="md",
children=[
overview_card("mnr", page_id),
overview_card("lambda", page_id),
overview_card("chi2", page_id),
overview_card("rmsd", page_id),
],
),
dmc.SimpleGrid(
cols={"base": 1, "sm": 2},
mb="md",
spacing="md",
children=[
overview_card("mean_dist", page_id),
overview_card("std_dist", page_id),
number_of_warnings_card(None, None, page_id),
],
),
# dmc.SimpleGrid(
# cols={"base": 1, "sm": 2, "lg": 4},
# mb="md",
# spacing="md",
# children=[
#

# ],
# ),
]


Expand Down Expand Up @@ -593,7 +598,14 @@ def _dist(key):
mean_val, mean_unc = _dist('mean')
std_val, std_unc = _dist('std')

stored_warnings = store_data.get('warnings')
if stored_warnings is None:
warning_counts = {'critical': None, 'moderate': None}
else:
warning_counts = count_by_level(warnings_from_dict(stored_warnings))

return {
'warnings': warning_counts,
'mnr': {'value': _gof('MNR', 'mnr'), 'uncertainty': None},
'chi2': {'value': _gof('chi2red', 'chi2'), 'uncertainty': None},
'rmsd': {'value': _gof('RMSD', 'rmsd'), 'uncertainty': None},
Expand All @@ -608,6 +620,11 @@ def _render_cards(outputs, metrics):
result = []
for out in outputs:
metric = out["id"]["metric"]
if metric == "warnings":
counts = metrics.get("warnings") or {}
result.append(number_of_warnings_children(
counts.get("critical"), counts.get("moderate"), out["id"]["page"]))
continue
config = METRIC_CONFIG.get(metric, {"title": metric, "description": None, "thresholds": None})
metric_data = metrics.get(metric, {})
result.append(_make_card_children(
Expand Down
6 changes: 4 additions & 2 deletions src/deeranalysis/components/fpc_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from deeranalysis.utils.database import get_session, Dataset
from deeranalysis.utils import dataarray_from_database_entry
from deeranalysis.utils.deerlab_population import determine_pop_P
from deeranalysis.components.warnings import number_of_warnings_card

from dash import dcc, html, callback, Input, Output, State, MATCH
import deerlab as dl
Expand Down Expand Up @@ -416,21 +417,22 @@ def overview_tab_population(page_id):
# Dummy store so the shared overview-card callback can find all its MATCH inputs
dcc.Store(id={"type": "fit-results-store", "page": page_id}),
dmc.SimpleGrid(
cols={"base": 1, "sm": 2, "lg": 4},
cols={"base": 1, "sm": 3, "lg": 5},
mt="md",
spacing="md",
children=[
overview_card("mnr", page_id),
overview_card("lambda", page_id),
overview_card("chi2", page_id),
overview_card("rmsd", page_id),
number_of_warnings_card(None, None, page_id),
],
),
dmc.Text("Population Metrics", size="lg", fw=700, mt="xl"),
dmc.Space(h='md'),
dmc.SimpleGrid(
id={"type": "population-cards-grid", "page": page_id},
cols={"base": 1, "sm": 2, "lg": 3},
cols={"base": 1, "md": 2, "lg": 3},
mb="md",
spacing="md",
children=[],
Expand Down
184 changes: 184 additions & 0 deletions src/deeranalysis/components/warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Elements for displaying fitwarnings in the GUI.

import dash_mantine_components as dmc
import dash
from dash import html, dcc, callback, Input, Output, State,clientside_callback, MATCH, ALL, ALLSMALLER
from dash_iconify import DashIconify
from deeranalysis.utils.deerlab_fitwarnings import check_fit_results, warnings_to_dict, warnings_from_dict,FitWarning

def warning_card(warning):
if issubclass(type(warning), FitWarning):
warning_title = warning.title
warning_message = warning.message
level = warning.level
elif isinstance(warning, dict):
warning_title = warning.get('title', 'Warning')
warning_message = warning.get('message', 'This is a warning message.')
level = warning.get('level', 'moderate')
else:
raise ValueError("Warning must be a FitWarning instance or a dictionary.")
if level == 'critical':
card_color = "#F8D7DA"
card_icon = DashIconify(icon="mdi:alert-circle-outline", width=36, color="#721C24")
else:
card_color = "#FFF3CD"
card_icon = DashIconify(icon="mdi:alert-circle-outline", width=36, color="#856404")
# warning_title = dmc.Text("Conc Parameter Warning", c="#856404",fw=700)
# warning_message = dmc.Text("This is a warning message.", c="#856404")

card = dmc.Card(
children=[
dmc.Group(
children=[
dmc.Box(card_icon, style={"flexShrink": 0}),
dmc.Stack(
children=[
dmc.Text(warning_title, c="#856404",fw=700),
dmc.Text(warning_message, c="#856404")
],
gap="xs",
style={"flex": 1, "minWidth": 0},
)
],
gap="xs",
align="flex-start",
wrap="nowrap",
)
],
shadow="sm",
p="md",
style={"backgroundColor": card_color, "borderRadius": "8px"}
)
return card


def number_of_warnings_children(critical, moderate, page_id):
"""
Children of the number of warnings card, so that ``update_overview_cards``
can re-render them when the fit results change.

``critical`` and ``moderate`` are the counts, or ``None`` when the fit has
not been checked for warnings, which is shown as "–".
"""
title = "Warnings"

critical_color = "red"
moderate_color = "orange"

expand_button = dmc.ActionIcon(
DashIconify(icon="mdi:arrow-expand", width=20, color="gray"),
variant="transparent",
size="lg",
id={"type": "n_warnings_overview-expand-button", "page": page_id},
)

def _count(n):
return "–" if n is None else f"{n}"

value_line = dmc.Group(
[
dmc.Tooltip(DashIconify(icon="mdi:alert-circle-outline", width=20, color=critical_color),label="Critical Warnings", position="top", withArrow=True),
dmc.Text(_count(critical), size="xl", fw=700, c=critical_color),
dmc.Tooltip(DashIconify(icon="mdi:alert-outline", width=20, color=moderate_color),label="Moderate Warnings", position="top", withArrow=True),
dmc.Text(_count(moderate), size="xl", fw=700, c=moderate_color),
],
align="center",
justify="center"
)

return [
dmc.Box([
dmc.Text(title, size="lg", fw=700, ta="center"),
dmc.Box(expand_button, style={"position": "absolute", "top": 0, "right": 0}),
],
style={"position": "relative"}
),
value_line,
]


def number_of_warnings_card(critical, moderate, page_id):
"""
based on fit_page_components overview_card

Shares the ``overview-card`` id type with the other overview cards, under
the metric ``"warnings"``, so that ``update_overview_cards`` updates it.
"""
return dmc.Paper(
number_of_warnings_children(critical, moderate, page_id),
id={"type": "overview-card", "metric": "warnings", "page": page_id},
withBorder=True,
p="md",
style={"textAlign": "center", "minWidth": 120},
)



def list_of_warnings_card(warnings):

scroll_area_children = []
for warning in warnings:
card = warning_card(warning)
scroll_area_children.append(card)


return dmc.ScrollArea(
dmc.Stack(scroll_area_children, gap="md"),
mah=400,
scrollbars="y",
type="auto",
)



def list_of_warnings_modal(page_id):

children = dmc.Text("No warnings to display.", c="gray", size="md")
modal = dmc.Modal(
title="Warnings",
id={"type": "n_warnings_overview-modal", "page": page_id},
size="80%",
children=[
html.Div(children, style={"maxHeight": "400px", "overflowY": "auto"},id={"type": "n_warnings_overview-modal-content", "page": page_id}),
],
centered=False,
overlayProps={"opacity": 0.55, "blur": 3},
)

return modal

@callback(
Output({"type": "n_warnings_overview-modal-content", "page": MATCH}, "children"),
Input({"type": "n_warnings_overview-expand-button", "page": MATCH}, "n_clicks"),
State({"type": "fit-results-store", "page": MATCH}, "data"),
State({"type": "fit-results-store-multi", "page": MATCH}, "data"),
prevent_initial_call=True,
)
def update_warnings_modal_content(n_, fit_results_store, fit_results_store_multi):
# Every fit page renders both stores (single-fit pages use a dummy multi store),
# so a single callback handles both and prefers the multi store when populated.
if fit_results_store_multi is not None:
fit_results_store = fit_results_store_multi
if fit_results_store is None:
return dash.no_update
warnings = warnings_from_dict(fit_results_store.get('warnings', []))

if len(warnings) == 0:
children = dmc.Text("No warnings to display.", c="gray", size="md")
else:
children = list_of_warnings_card(warnings)

return children


@callback(
Output({"type": "n_warnings_overview-modal", "page": MATCH}, "opened"),
Input({"type": "n_warnings_overview-expand-button", "page": MATCH}, "n_clicks"),
State({"type": "n_warnings_overview-modal", "page": MATCH}, "opened"),
prevent_initial_call=True,
)
def open_warnings_modal(n_, opened):

if n_:
return not opened
return opened
6 changes: 5 additions & 1 deletion src/deeranalysis/pages/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from deeranalysis.components.fit_page_components import fit_results_tabs, fit_results_tab, goodness_of_fit_tab
from deeranalysis.components.model_edit_modal import create_model_edit_modal
from deeranalysis.utils.deerlab_options import regparam_options,background_models, plotly_goodness_of_fit, dists_stats_to_list, fit_to_dict,name_dataset_from_dict, build_model_data, plotly_lcurve
from deeranalysis.components.warnings import list_of_warnings_modal
from deeranalysis.utils.deerlab_fitwarnings import check_fit_results, warnings_to_dict

import deeranalysis.components.fit_page_components as fpc

Expand All @@ -32,6 +34,7 @@
create_dataset_modal(page_id=page_id),
create_fit_download_modal(page_id=page_id),
create_model_edit_modal(page_id=page_id),
list_of_warnings_modal(page_id=page_id),
html.Div([
dmc.Select(id={'type': 'dataset-dropdown', 'page': page_id}, label="Select a dataset", style={'flex': '1 1 0'}),
dmc.ActionIcon(DashIconify(icon='material-symbols:search', width=20),
Expand Down Expand Up @@ -164,9 +167,10 @@ def run_fit(n_clicks, dataset_id, fit_options, model_params):
print(f"Error during fitting: {e}")
return dash.no_update, f"Error during fitting: {e}", True, True, False


warnings = check_fit_results(fit, fit.Bmodel)
fit_dict = fit_to_dict(fit,background_only=True)
fit_dict['gof'] = fit.stats
fit_dict['warnings'] = warnings_to_dict(warnings)
return fit_dict, fit.__str__(), False, False, False


Expand Down
Loading
Loading