From c0e05057c840867509641b1243fbed3f9123154a Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Mon, 15 Apr 2024 15:07:44 +0200 Subject: [PATCH 01/26] create analyze page --- eit_dash/callbacks/analyze_callbacks.py | 31 ++++++++++++ eit_dash/callbacks/preprocessing_callbacks.py | 47 ++++++++++++++----- eit_dash/definitions/element_ids.py | 3 ++ eit_dash/main.py | 12 +++-- eit_dash/pages/analyze.py | 34 ++++++++++++++ 5 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 eit_dash/callbacks/analyze_callbacks.py create mode 100644 eit_dash/pages/analyze.py diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py new file mode 100644 index 0000000..59a2377 --- /dev/null +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -0,0 +1,31 @@ +from dash import Input, Output, State, callback, ctx + +import eit_dash.definitions.element_ids as ids + + +@callback( + [Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True)], + [ + Input(ids.ANALYZE_RESULTS_TITLE, "children"), + ], + [ + State(ids.SUMMARY_COLUMN, "children"), + ], + # this allows duplicate outputs with initial call + prevent_initial_call="initial_duplicate", +) +def update_summary(start, summary): + """Updates summary. + + When the page is loaded, it populates the summary column + with the info about the loaded datasets and the preprocessing steps + """ + trigger = ctx.triggered_id + + results = [] + + if trigger is None: + data = [] + summary += data + + return results diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 051cfc5..d4c6579 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -61,7 +61,10 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] + options = [ + {"label": f'{data["Name"]}', "value": str(i)} + for i, data in enumerate(loaded_data) + ] return row, options @@ -69,7 +72,10 @@ def create_resampling_card(loaded_data): def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() - return [dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) for dataset in loaded_data] + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: @@ -91,7 +97,10 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d card_list = [ html.H4(period.label, className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] card_list += [ dbc.Button( "Remove", @@ -115,7 +124,10 @@ def create_filter_results_card(parameters: dict) -> dbc.Card: card_list = [ html.H4("Data filtered", className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in parameters.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in parameters.items() + ] return dbc.Card( dbc.CardBody(card_list), @@ -128,7 +140,10 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] + data += [ + {"Name": name, "Data type": channel} + for channel in dataset.continuous_data + ] if dataset.eit_data: data.append( { @@ -188,7 +203,7 @@ def load_datasets(title): [ Output(ids.OPEN_SYNCH_BUTTON, "disabled"), Output(ids.OPEN_SELECT_PERIODS_BUTTON, "disabled"), - Output(ids.SUMMARY_COLUMN, "children"), + Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True), Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), ], [ @@ -288,7 +303,10 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] + options = [ + {"label": sequence.label, "value": index} + for index, sequence in enumerate(signals) + ] body = [ html.H6("Select one dataset"), @@ -522,7 +540,9 @@ def remove_period(n_clicks, container, figure): try: figure["data"] = [ - trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != int(input_id) + trace + for trace in figure["data"] + if "meta" not in trace or trace["meta"]["uid"] != int(input_id) ] except TypeError: contextlib.suppress(Exception) @@ -597,9 +617,12 @@ def enable_apply_button( """Enable the apply button.""" if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) - or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 + ) + or ( + int(filter_selected) + in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] and co_low > 0 and co_low > 0 ) @@ -734,7 +757,9 @@ def save_filtered_signal(confirm, results): data.update_data(tmp_data) if not params: - params = tmp_data.continuous_data.data["global_impedance_filtered"].parameters + params = tmp_data.continuous_data.data[ + "global_impedance_filtered" + ].parameters # show info card results += [create_filter_results_card(params)] diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 7545a3e..ecaca3b 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -65,3 +65,6 @@ RESAMPLING_CARD_BODY = "resampling-card-body" RESAMPLING_FREQUENCY_INPUT = "resampling-frequency-input" SUMMARY_COLUMN = "summary-column" + +# analyze +ANALYZE_RESULTS_TITLE = "analyze-reults-title" diff --git a/eit_dash/main.py b/eit_dash/main.py index 169e9c9..2496239 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -2,16 +2,22 @@ from dash import html, page_container from eit_dash.app import app -from eit_dash.callbacks import load_callbacks, preprocessing_callbacks # noqa: F401 +from eit_dash.callbacks import ( + load_callbacks, + preprocessing_callbacks, + analyze_callbacks, +) # noqa: F401 app.layout = html.Div( [ - html.H1(id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}), + html.H1( + id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"} + ), dbc.Row( [ dbc.Col(dbc.NavLink("Load", href="/load")), dbc.Col(dbc.NavLink("Pre-processing", href="/preprocessing")), - dbc.Col(dbc.NavLink("Analyze", href="/dummy")), + dbc.Col(dbc.NavLink("Analyze", href="/analyze")), dbc.Col(dbc.NavLink("Summarize", href="/dummy")), ], style={"textAlign": "center"}, diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py new file mode 100644 index 0000000..f79d5b1 --- /dev/null +++ b/eit_dash/pages/analyze.py @@ -0,0 +1,34 @@ +from pathlib import Path + +import dash_bootstrap_components as dbc +from dash import dcc, html, register_page + +import eit_dash.definitions.element_ids as ids +import eit_dash.definitions.layout_styles as styles +from eit_dash.definitions.option_lists import InputFiletypes + +register_page(__name__, path="/analyze") + +summary = dbc.Col([html.H2("Summary", style=styles.COLUMN_TITLE)]) + +results = dbc.Col( + [ + html.H2("Results", id=ids.ANALYZE_RESULTS_TITLE, style=styles.COLUMN_TITLE), + html.Div(id=ids.DATASET_CONTAINER, style=styles.LOAD_RESULTS), + ], +) + +actions = dbc.Col( + [ + html.H2("Data analysis", style=styles.COLUMN_TITLE), + ], +) + +layout = dbc.Row( + [ + html.H1("ANALYZE DATA", style=styles.COLUMN_TITLE), + summary, + actions, + results, + ], +) From a3386915d10513b45d591eccd8852086aeabac8c Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Tue, 16 Apr 2024 10:36:38 +0200 Subject: [PATCH 02/26] refactored to use eit as continuous data --- eit_dash/callbacks/load_callbacks.py | 29 ++-- eit_dash/callbacks/preprocessing_callbacks.py | 22 +-- eit_dash/definitions/constants.py | 1 + eit_dash/utils/common.py | 155 ++++++++++-------- 4 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 eit_dash/definitions/constants.py diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 834b804..4278919 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -16,6 +16,7 @@ import eit_dash.definitions.element_ids as ids from eit_dash.app import data_object from eit_dash.definitions import layout_styles as styles +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import InputFiletypes from eit_dash.utils.common import ( create_slider_figure, @@ -47,7 +48,10 @@ def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: html.H4(dataset.label, className="card-title"), html.H6(InputFiletypes(file_type).name, className="card-subtitle"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] return dbc.Card(dbc.CardBody(card_list), id="card-1") @@ -131,13 +135,13 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): trigger = ctx.triggered_id - # cancelled selection. Reset the data and turn of the data selector + # cancelled selection. Reset the data and turn off the data selector if trigger == ids.LOAD_CANCEL_BUTTON: data = None file_data = None if not data: - # this is needed, because a figure object must be returned for the graph, evn if empty + # this is needed, because a figure object must be returned for the graph, even if empty figure = go.Figure() return True, [], figure @@ -157,12 +161,11 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): figure = create_slider_figure( file_data, - ["raw"], - list(file_data.continuous_data), - True, + continuous_data=list(file_data.continuous_data), + clickable_legend=True, ) - ok = ["raw"] + ok = [RAW_EIT_LABEL] if sig: ok += [options[s]["label"] for s in sig] @@ -202,12 +205,12 @@ def show_info( start_sample, stop_sample = get_selections_slidebar(slidebar_stat) if not start_sample: - start_sample = file_data.eit_data["raw"].time[0] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] if not stop_sample: - stop_sample = file_data.eit_data["raw"].time[-1] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] else: - start_sample = file_data.eit_data["raw"].time[0] - stop_sample = file_data.eit_data["raw"].time[-1] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] dataset_name = f"Dataset {data_object.get_sequence_list_length()}" @@ -224,8 +227,8 @@ def show_info( eit_data_cut.add(data[data_type].select_by_time(start_sample, stop_sample)) for data_type in (data := file_data.continuous_data): - # add just the selected signals - if data_type in selected: + # add just the selected signals and the raw EIT + if data_type in selected or data_type == RAW_EIT_LABEL: continuous_data_cut.add( data[data_type].select_by_time(start_sample, stop_sample), ) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index d4c6579..fb0e771 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -15,7 +15,9 @@ import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.utils.common import ( + create_loaded_data_summary, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -69,15 +71,6 @@ def create_resampling_card(loaded_data): return row, options -def create_loaded_data_summary(): - loaded_data = data_object.get_all_sequences() - - return [ - dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) - for dataset in loaded_data - ] - - def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: """ Create the card with the information on the selected period to be displayed in the Results section. @@ -89,7 +82,7 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d """ info_data = { "n_frames": period.eit_data["raw"].nframes, - "start_time": period.time[0], + "start_time": period.eit_data["raw"].time[0], "end_time": period.eit_data["raw"].time[-1], "dataset": dataset, } @@ -383,7 +376,6 @@ def initialize_figure( current_figure = create_slider_figure( data, - ["raw"], list(data.continuous_data), ) @@ -717,8 +709,8 @@ def show_filtered_results(selected, _): fig.add_trace( go.Scatter( - x=data.eit_data.data["raw"].time, - y=data.eit_data.data["raw"].global_impedance, + x=data.continuous_data[RAW_EIT_LABEL].time, + y=data.continuous_data[RAW_EIT_LABEL].values, name="Original signal", ), ) @@ -811,6 +803,6 @@ def filter_data(data: Sequence, filter_params: dict) -> ContinuousData | None: "a.u.", "impedance", parameters=filter_params, - time=data.eit_data.data["raw"].time, - values=filt.apply_filter(data.eit_data.data["raw"].global_impedance), + time=data.continuous_data[RAW_EIT_LABEL].time, + values=filt.apply_filter(data.continuous_data[RAW_EIT_LABEL].values), ) diff --git a/eit_dash/definitions/constants.py b/eit_dash/definitions/constants.py new file mode 100644 index 0000000..241c1b2 --- /dev/null +++ b/eit_dash/definitions/constants.py @@ -0,0 +1 @@ +RAW_EIT_LABEL = "global_impedance_(raw)" diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 874f8d0..9ad66ad 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -2,7 +2,12 @@ from typing import TYPE_CHECKING +import dash_bootstrap_components as dbc import plotly.graph_objects as go +from dash import html + +from eit_dash.app import data_object +from eit_dash.definitions.constants import RAW_EIT_LABEL if TYPE_CHECKING: from eitprocessing.sequence import Sequence @@ -20,17 +25,24 @@ def blank_fig(): return fig +def create_loaded_data_summary(): + loaded_data = data_object.get_all_sequences() + + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] + + def create_slider_figure( dataset: Sequence, - eit_variants: list[str] | None = None, continuous_data: list[str] | None = None, clickable_legend: bool = False, ) -> go.Figure: - """Create the figure for the selection of range. + """Create the figure for the selection of range. The raw global impedance is plotted by default. Args: dataset: Sequence object containing the selected dataset - eit_variants: list of the eit variants to be plotted continuous_data: list of the continuous data signals to be plotted clickable_legend: if True, the user can hide a signal by clicking on the legend """ @@ -40,45 +52,43 @@ def create_slider_figure( if continuous_data is None: continuous_data = [] - if eit_variants is None: - eit_variants = ["raw"] - - for eit_variant in eit_variants: - figure.add_trace( - go.Scatter( - x=dataset.eit_data[eit_variant].time, - y=dataset.eit_data[eit_variant].global_impedance, - name=eit_variant, - ), - ) + + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[RAW_EIT_LABEL].time, + y=dataset.continuous_data[RAW_EIT_LABEL].values, + name=RAW_EIT_LABEL, + ), + ) for n, cont_signal in enumerate(continuous_data): - figure.add_trace( - go.Scatter( - x=dataset.continuous_data[cont_signal].time, - y=dataset.continuous_data[cont_signal].values, - name=cont_signal, - opacity=0.5, - yaxis=f"y{n + 2}", - ), - ) - # decide whether to put the axis left or right - side = "right" if n % 2 == 0 else "left" - - y_position += 0.1 - new_y = { - "title": cont_signal, - "anchor": "free", - "overlaying": "y", - "side": side, - "autoshift": True, - } - - # layout parameters for multiple y axis - param_name = f"yaxis{n + 2}" - params.update({param_name: new_y}) - - for event in dataset.eit_data[eit_variants[0]].events: + if cont_signal != RAW_EIT_LABEL: + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[cont_signal].time, + y=dataset.continuous_data[cont_signal].values, + name=cont_signal, + opacity=0.5, + yaxis=f"y{n + 2}", + ), + ) + # decide whether to put the axis left or right + side = "right" if n % 2 == 0 else "left" + + y_position += 0.1 + new_y = { + "title": cont_signal, + "anchor": "free", + "overlaying": "y", + "side": side, + "autoshift": True, + } + + # layout parameters for multiple y axis + param_name = f"yaxis{n + 2}" + params.update({param_name: new_y}) + + for event in dataset.eit_data["raw"].events: annotation = {"text": f"{event.text}", "textangle": -90} figure.add_vline( x=event.time, @@ -117,32 +127,38 @@ def mark_selected_periods( """ for period in periods: seq = period.get_data() - for eit_variant in seq.eit_data: - selected_impedance = go.Scatter( - x=seq.eit_data[eit_variant].time, - y=seq.eit_data[eit_variant].global_impedance, - name=eit_variant, - meta={"uid": period.get_period_index()}, - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() - - if type(original_figure) == go.Figure: - original_figure.add_trace(selected_impedance) - else: - original_figure["data"].append(selected_impedance) + # for eit_variant in seq.eit_data: + # selected_impedance = go.Scatter( + # x=seq.eit_data[eit_variant].time, + # y=seq.eit_data[eit_variant].global_impedance, + # name=eit_variant, + # meta={"uid": period.get_period_index()}, + # line={"color": "black"}, + # showlegend=False, + # ).to_plotly_json() + # + # if type(original_figure) == go.Figure: + # original_figure.add_trace(selected_impedance) + # else: + # original_figure["data"].append(selected_impedance) for n, cont_signal in enumerate(seq.continuous_data): - selected_signal = go.Scatter( - x=seq.continuous_data[cont_signal].time, - y=seq.continuous_data[cont_signal].values, - name=cont_signal, - meta={"uid": period.get_period_index()}, - opacity=0.5, - yaxis=f"y{n + 2}", - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() + params = { + "x": seq.continuous_data[cont_signal].time, + "y": seq.continuous_data[cont_signal].values, + "name": cont_signal, + "meta": {"uid": period.get_period_index()}, + "line": {"color": "black"}, + "showlegend": False, + } + if cont_signal != RAW_EIT_LABEL: + params.update( + { + "opacity": 0.5, + "yaxis": f"y{n + 2}", + } + ) + selected_signal = go.Scatter(**params).to_plotly_json() if type(original_figure) == go.Figure: original_figure.add_trace(selected_signal) @@ -165,15 +181,16 @@ def get_signal_options( A list of label - value options for populating the options list """ options = [] - if show_eit: - # iterate over eit data - for eit in dataset.eit_data: - options.append({"label": eit, "value": len(options)}) + # if show_eit: + # # iterate over eit data + # for eit in dataset.eit_data: + # options.append({"label": eit, "value": len(options)}) if dataset.continuous_data: # iterate over continuous data for cont in dataset.continuous_data: - options.append({"label": cont, "value": len(options)}) + if (cont == RAW_EIT_LABEL and show_eit) or cont != RAW_EIT_LABEL: + options.append({"label": cont, "value": len(options)}) return options From cd0936fe0ffc4174da62ef29a29b395331696eb0 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Mon, 15 Apr 2024 15:07:44 +0200 Subject: [PATCH 03/26] create analyze page --- eit_dash/callbacks/analyze_callbacks.py | 31 +++++++++++++ eit_dash/callbacks/preprocessing_callbacks.py | 43 ++++++++++++++----- eit_dash/definitions/element_ids.py | 3 ++ eit_dash/main.py | 12 ++++-- eit_dash/pages/analyze.py | 34 +++++++++++++++ 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 eit_dash/callbacks/analyze_callbacks.py create mode 100644 eit_dash/pages/analyze.py diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py new file mode 100644 index 0000000..59a2377 --- /dev/null +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -0,0 +1,31 @@ +from dash import Input, Output, State, callback, ctx + +import eit_dash.definitions.element_ids as ids + + +@callback( + [Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True)], + [ + Input(ids.ANALYZE_RESULTS_TITLE, "children"), + ], + [ + State(ids.SUMMARY_COLUMN, "children"), + ], + # this allows duplicate outputs with initial call + prevent_initial_call="initial_duplicate", +) +def update_summary(start, summary): + """Updates summary. + + When the page is loaded, it populates the summary column + with the info about the loaded datasets and the preprocessing steps + """ + trigger = ctx.triggered_id + + results = [] + + if trigger is None: + data = [] + summary += data + + return results diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index d44fc75..b475f64 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -61,7 +61,10 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] + options = [ + {"label": f'{data["Name"]}', "value": str(i)} + for i, data in enumerate(loaded_data) + ] return row, options @@ -69,7 +72,10 @@ def create_resampling_card(loaded_data): def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() - return [dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) for dataset in loaded_data] + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: @@ -91,7 +97,10 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d card_list = [ html.H4(period.label, className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] card_list += [ dbc.Button( "Remove", @@ -115,7 +124,10 @@ def create_filter_results_card(parameters: dict) -> dbc.Card: card_list = [ html.H4("Data filtered", className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in parameters.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in parameters.items() + ] return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) @@ -126,7 +138,10 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] + data += [ + {"Name": name, "Data type": channel} + for channel in dataset.continuous_data + ] if dataset.eit_data: data.append( { @@ -186,7 +201,7 @@ def load_datasets(title): [ Output(ids.OPEN_SYNCH_BUTTON, "disabled"), Output(ids.OPEN_SELECT_PERIODS_BUTTON, "disabled"), - Output(ids.SUMMARY_COLUMN, "children"), + Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True), Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), ], [ @@ -286,7 +301,10 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] + options = [ + {"label": sequence.label, "value": index} + for index, sequence in enumerate(signals) + ] body = [ html.H6("Select one dataset"), @@ -608,9 +626,12 @@ def enable_apply_button( """Enable the apply button.""" if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) - or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 + ) + or ( + int(filter_selected) + in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] and co_low > 0 and co_low > 0 ) @@ -745,7 +766,9 @@ def save_filtered_signal(confirm, results: list): data.update_data(tmp_data) if not params: - params = tmp_data.continuous_data.data["global_impedance_filtered"].parameters + params = tmp_data.continuous_data.data[ + "global_impedance_filtered" + ].parameters # show info card for element in results: diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index ae9f7f1..5b48363 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -66,3 +66,6 @@ RESAMPLING_CARD_BODY = "resampling-card-body" RESAMPLING_FREQUENCY_INPUT = "resampling-frequency-input" SUMMARY_COLUMN = "summary-column" + +# analyze +ANALYZE_RESULTS_TITLE = "analyze-reults-title" diff --git a/eit_dash/main.py b/eit_dash/main.py index 169e9c9..2496239 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -2,16 +2,22 @@ from dash import html, page_container from eit_dash.app import app -from eit_dash.callbacks import load_callbacks, preprocessing_callbacks # noqa: F401 +from eit_dash.callbacks import ( + load_callbacks, + preprocessing_callbacks, + analyze_callbacks, +) # noqa: F401 app.layout = html.Div( [ - html.H1(id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}), + html.H1( + id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"} + ), dbc.Row( [ dbc.Col(dbc.NavLink("Load", href="/load")), dbc.Col(dbc.NavLink("Pre-processing", href="/preprocessing")), - dbc.Col(dbc.NavLink("Analyze", href="/dummy")), + dbc.Col(dbc.NavLink("Analyze", href="/analyze")), dbc.Col(dbc.NavLink("Summarize", href="/dummy")), ], style={"textAlign": "center"}, diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py new file mode 100644 index 0000000..f79d5b1 --- /dev/null +++ b/eit_dash/pages/analyze.py @@ -0,0 +1,34 @@ +from pathlib import Path + +import dash_bootstrap_components as dbc +from dash import dcc, html, register_page + +import eit_dash.definitions.element_ids as ids +import eit_dash.definitions.layout_styles as styles +from eit_dash.definitions.option_lists import InputFiletypes + +register_page(__name__, path="/analyze") + +summary = dbc.Col([html.H2("Summary", style=styles.COLUMN_TITLE)]) + +results = dbc.Col( + [ + html.H2("Results", id=ids.ANALYZE_RESULTS_TITLE, style=styles.COLUMN_TITLE), + html.Div(id=ids.DATASET_CONTAINER, style=styles.LOAD_RESULTS), + ], +) + +actions = dbc.Col( + [ + html.H2("Data analysis", style=styles.COLUMN_TITLE), + ], +) + +layout = dbc.Row( + [ + html.H1("ANALYZE DATA", style=styles.COLUMN_TITLE), + summary, + actions, + results, + ], +) From 73a7df02eed7d50c805dade2e36a563d81f446d3 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Tue, 16 Apr 2024 10:36:38 +0200 Subject: [PATCH 04/26] refactored to use eit as continuous data --- eit_dash/callbacks/load_callbacks.py | 29 ++-- eit_dash/callbacks/preprocessing_callbacks.py | 22 +-- eit_dash/definitions/constants.py | 1 + eit_dash/utils/common.py | 155 ++++++++++-------- 4 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 eit_dash/definitions/constants.py diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 834b804..4278919 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -16,6 +16,7 @@ import eit_dash.definitions.element_ids as ids from eit_dash.app import data_object from eit_dash.definitions import layout_styles as styles +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import InputFiletypes from eit_dash.utils.common import ( create_slider_figure, @@ -47,7 +48,10 @@ def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: html.H4(dataset.label, className="card-title"), html.H6(InputFiletypes(file_type).name, className="card-subtitle"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] return dbc.Card(dbc.CardBody(card_list), id="card-1") @@ -131,13 +135,13 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): trigger = ctx.triggered_id - # cancelled selection. Reset the data and turn of the data selector + # cancelled selection. Reset the data and turn off the data selector if trigger == ids.LOAD_CANCEL_BUTTON: data = None file_data = None if not data: - # this is needed, because a figure object must be returned for the graph, evn if empty + # this is needed, because a figure object must be returned for the graph, even if empty figure = go.Figure() return True, [], figure @@ -157,12 +161,11 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): figure = create_slider_figure( file_data, - ["raw"], - list(file_data.continuous_data), - True, + continuous_data=list(file_data.continuous_data), + clickable_legend=True, ) - ok = ["raw"] + ok = [RAW_EIT_LABEL] if sig: ok += [options[s]["label"] for s in sig] @@ -202,12 +205,12 @@ def show_info( start_sample, stop_sample = get_selections_slidebar(slidebar_stat) if not start_sample: - start_sample = file_data.eit_data["raw"].time[0] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] if not stop_sample: - stop_sample = file_data.eit_data["raw"].time[-1] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] else: - start_sample = file_data.eit_data["raw"].time[0] - stop_sample = file_data.eit_data["raw"].time[-1] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] dataset_name = f"Dataset {data_object.get_sequence_list_length()}" @@ -224,8 +227,8 @@ def show_info( eit_data_cut.add(data[data_type].select_by_time(start_sample, stop_sample)) for data_type in (data := file_data.continuous_data): - # add just the selected signals - if data_type in selected: + # add just the selected signals and the raw EIT + if data_type in selected or data_type == RAW_EIT_LABEL: continuous_data_cut.add( data[data_type].select_by_time(start_sample, stop_sample), ) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index b475f64..32a93a6 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -15,7 +15,9 @@ import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.utils.common import ( + create_loaded_data_summary, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -69,15 +71,6 @@ def create_resampling_card(loaded_data): return row, options -def create_loaded_data_summary(): - loaded_data = data_object.get_all_sequences() - - return [ - dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) - for dataset in loaded_data - ] - - def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: """ Create the card with the information on the selected period to be displayed in the Results section. @@ -89,7 +82,7 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d """ info_data = { "n_frames": period.eit_data["raw"].nframes, - "start_time": period.time[0], + "start_time": period.eit_data["raw"].time[0], "end_time": period.eit_data["raw"].time[-1], "dataset": dataset, } @@ -381,7 +374,6 @@ def initialize_figure( current_figure = create_slider_figure( data, - ["raw"], list(data.continuous_data), ) @@ -726,8 +718,8 @@ def show_filtered_results(selected, _): fig.add_trace( go.Scatter( - x=data.eit_data.data["raw"].time, - y=data.eit_data.data["raw"].global_impedance, + x=data.continuous_data[RAW_EIT_LABEL].time, + y=data.continuous_data[RAW_EIT_LABEL].values, name="Original signal", ), ) @@ -823,6 +815,6 @@ def filter_data(data: Sequence, filter_params: dict) -> ContinuousData | None: "a.u.", "impedance", parameters=filter_params, - time=data.eit_data.data["raw"].time, - values=filt.apply_filter(data.eit_data.data["raw"].global_impedance), + time=data.continuous_data[RAW_EIT_LABEL].time, + values=filt.apply_filter(data.continuous_data[RAW_EIT_LABEL].values), ) diff --git a/eit_dash/definitions/constants.py b/eit_dash/definitions/constants.py new file mode 100644 index 0000000..241c1b2 --- /dev/null +++ b/eit_dash/definitions/constants.py @@ -0,0 +1 @@ +RAW_EIT_LABEL = "global_impedance_(raw)" diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 874f8d0..9ad66ad 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -2,7 +2,12 @@ from typing import TYPE_CHECKING +import dash_bootstrap_components as dbc import plotly.graph_objects as go +from dash import html + +from eit_dash.app import data_object +from eit_dash.definitions.constants import RAW_EIT_LABEL if TYPE_CHECKING: from eitprocessing.sequence import Sequence @@ -20,17 +25,24 @@ def blank_fig(): return fig +def create_loaded_data_summary(): + loaded_data = data_object.get_all_sequences() + + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] + + def create_slider_figure( dataset: Sequence, - eit_variants: list[str] | None = None, continuous_data: list[str] | None = None, clickable_legend: bool = False, ) -> go.Figure: - """Create the figure for the selection of range. + """Create the figure for the selection of range. The raw global impedance is plotted by default. Args: dataset: Sequence object containing the selected dataset - eit_variants: list of the eit variants to be plotted continuous_data: list of the continuous data signals to be plotted clickable_legend: if True, the user can hide a signal by clicking on the legend """ @@ -40,45 +52,43 @@ def create_slider_figure( if continuous_data is None: continuous_data = [] - if eit_variants is None: - eit_variants = ["raw"] - - for eit_variant in eit_variants: - figure.add_trace( - go.Scatter( - x=dataset.eit_data[eit_variant].time, - y=dataset.eit_data[eit_variant].global_impedance, - name=eit_variant, - ), - ) + + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[RAW_EIT_LABEL].time, + y=dataset.continuous_data[RAW_EIT_LABEL].values, + name=RAW_EIT_LABEL, + ), + ) for n, cont_signal in enumerate(continuous_data): - figure.add_trace( - go.Scatter( - x=dataset.continuous_data[cont_signal].time, - y=dataset.continuous_data[cont_signal].values, - name=cont_signal, - opacity=0.5, - yaxis=f"y{n + 2}", - ), - ) - # decide whether to put the axis left or right - side = "right" if n % 2 == 0 else "left" - - y_position += 0.1 - new_y = { - "title": cont_signal, - "anchor": "free", - "overlaying": "y", - "side": side, - "autoshift": True, - } - - # layout parameters for multiple y axis - param_name = f"yaxis{n + 2}" - params.update({param_name: new_y}) - - for event in dataset.eit_data[eit_variants[0]].events: + if cont_signal != RAW_EIT_LABEL: + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[cont_signal].time, + y=dataset.continuous_data[cont_signal].values, + name=cont_signal, + opacity=0.5, + yaxis=f"y{n + 2}", + ), + ) + # decide whether to put the axis left or right + side = "right" if n % 2 == 0 else "left" + + y_position += 0.1 + new_y = { + "title": cont_signal, + "anchor": "free", + "overlaying": "y", + "side": side, + "autoshift": True, + } + + # layout parameters for multiple y axis + param_name = f"yaxis{n + 2}" + params.update({param_name: new_y}) + + for event in dataset.eit_data["raw"].events: annotation = {"text": f"{event.text}", "textangle": -90} figure.add_vline( x=event.time, @@ -117,32 +127,38 @@ def mark_selected_periods( """ for period in periods: seq = period.get_data() - for eit_variant in seq.eit_data: - selected_impedance = go.Scatter( - x=seq.eit_data[eit_variant].time, - y=seq.eit_data[eit_variant].global_impedance, - name=eit_variant, - meta={"uid": period.get_period_index()}, - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() - - if type(original_figure) == go.Figure: - original_figure.add_trace(selected_impedance) - else: - original_figure["data"].append(selected_impedance) + # for eit_variant in seq.eit_data: + # selected_impedance = go.Scatter( + # x=seq.eit_data[eit_variant].time, + # y=seq.eit_data[eit_variant].global_impedance, + # name=eit_variant, + # meta={"uid": period.get_period_index()}, + # line={"color": "black"}, + # showlegend=False, + # ).to_plotly_json() + # + # if type(original_figure) == go.Figure: + # original_figure.add_trace(selected_impedance) + # else: + # original_figure["data"].append(selected_impedance) for n, cont_signal in enumerate(seq.continuous_data): - selected_signal = go.Scatter( - x=seq.continuous_data[cont_signal].time, - y=seq.continuous_data[cont_signal].values, - name=cont_signal, - meta={"uid": period.get_period_index()}, - opacity=0.5, - yaxis=f"y{n + 2}", - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() + params = { + "x": seq.continuous_data[cont_signal].time, + "y": seq.continuous_data[cont_signal].values, + "name": cont_signal, + "meta": {"uid": period.get_period_index()}, + "line": {"color": "black"}, + "showlegend": False, + } + if cont_signal != RAW_EIT_LABEL: + params.update( + { + "opacity": 0.5, + "yaxis": f"y{n + 2}", + } + ) + selected_signal = go.Scatter(**params).to_plotly_json() if type(original_figure) == go.Figure: original_figure.add_trace(selected_signal) @@ -165,15 +181,16 @@ def get_signal_options( A list of label - value options for populating the options list """ options = [] - if show_eit: - # iterate over eit data - for eit in dataset.eit_data: - options.append({"label": eit, "value": len(options)}) + # if show_eit: + # # iterate over eit data + # for eit in dataset.eit_data: + # options.append({"label": eit, "value": len(options)}) if dataset.continuous_data: # iterate over continuous data for cont in dataset.continuous_data: - options.append({"label": cont, "value": len(options)}) + if (cont == RAW_EIT_LABEL and show_eit) or cont != RAW_EIT_LABEL: + options.append({"label": cont, "value": len(options)}) return options From fd78ecc41312e0c5283ff375dd2007f49722fad1 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Tue, 16 Apr 2024 16:10:20 +0200 Subject: [PATCH 05/26] small changes --- eit_dash/callbacks/preprocessing_callbacks.py | 2 +- eit_dash/main.py | 6 +++--- eit_dash/utils/common.py | 20 +------------------ 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 32a93a6..ccaf014 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -14,8 +14,8 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object -from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.definitions.constants import RAW_EIT_LABEL +from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( create_loaded_data_summary, create_slider_figure, diff --git a/eit_dash/main.py b/eit_dash/main.py index 2496239..f35c620 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -3,15 +3,15 @@ from eit_dash.app import app from eit_dash.callbacks import ( + analyze_callbacks, load_callbacks, preprocessing_callbacks, - analyze_callbacks, -) # noqa: F401 +) app.layout = html.Div( [ html.H1( - id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"} + id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}, ), dbc.Row( [ diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 9ad66ad..b8fd78b 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -127,20 +127,6 @@ def mark_selected_periods( """ for period in periods: seq = period.get_data() - # for eit_variant in seq.eit_data: - # selected_impedance = go.Scatter( - # x=seq.eit_data[eit_variant].time, - # y=seq.eit_data[eit_variant].global_impedance, - # name=eit_variant, - # meta={"uid": period.get_period_index()}, - # line={"color": "black"}, - # showlegend=False, - # ).to_plotly_json() - # - # if type(original_figure) == go.Figure: - # original_figure.add_trace(selected_impedance) - # else: - # original_figure["data"].append(selected_impedance) for n, cont_signal in enumerate(seq.continuous_data): params = { @@ -156,7 +142,7 @@ def mark_selected_periods( { "opacity": 0.5, "yaxis": f"y{n + 2}", - } + }, ) selected_signal = go.Scatter(**params).to_plotly_json() @@ -181,10 +167,6 @@ def get_signal_options( A list of label - value options for populating the options list """ options = [] - # if show_eit: - # # iterate over eit data - # for eit in dataset.eit_data: - # options.append({"label": eit, "value": len(options)}) if dataset.continuous_data: # iterate over continuous data From d53379f382af80dad6e62fcacdc920e3de2140a4 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 10:33:12 +0200 Subject: [PATCH 06/26] summary cards --- eit_dash/callbacks/analyze_callbacks.py | 42 ++++++++++--- eit_dash/callbacks/load_callbacks.py | 5 +- eit_dash/callbacks/preprocessing_callbacks.py | 62 +++---------------- eit_dash/definitions/element_ids.py | 5 +- eit_dash/main.py | 4 +- eit_dash/pages/analyze.py | 17 ++++- eit_dash/utils/common.py | 60 ++++++++++++++++++ 7 files changed, 124 insertions(+), 71 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 59a2377..db77a63 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,20 +1,27 @@ +import dash_bootstrap_components as dbc from dash import Input, Output, State, callback, ctx import eit_dash.definitions.element_ids as ids +from eit_dash.app import data_object +from eit_dash.utils.common import ( + create_filter_results_card, + create_loaded_data_summary, + create_selected_period_card, +) @callback( - [Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True)], + Output(ids.SUMMARY_COLUMN_ANALYZE, "children", allow_duplicate=True), [ Input(ids.ANALYZE_RESULTS_TITLE, "children"), ], [ - State(ids.SUMMARY_COLUMN, "children"), + State(ids.SUMMARY_COLUMN_ANALYZE, "children"), ], # this allows duplicate outputs with initial call prevent_initial_call="initial_duplicate", ) -def update_summary(start, summary): +def update_summary(_, summary): """Updates summary. When the page is loaded, it populates the summary column @@ -22,10 +29,29 @@ def update_summary(start, summary): """ trigger = ctx.triggered_id - results = [] - if trigger is None: - data = [] - summary += data + loaded_data = create_loaded_data_summary() + summary += loaded_data + + filter_params = {} + + for period in data_object.get_all_stable_periods(): + if not filter_params: + filter_params = ( + period.get_data() + .continuous_data.data["global_impedance_filtered"] + .parameters + ) + + summary += [ + create_selected_period_card( + period.get_data(), + period.get_dataset_index(), + period.get_period_index(), + False, + ) + ] + + summary += [create_filter_results_card(filter_params)] - return results + return summary diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 4278919..7c29c37 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -48,10 +48,7 @@ def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: html.H4(dataset.label, className="card-title"), html.H6(InputFiletypes(file_type).name, className="card-subtitle"), ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in info_data.items() - ] + card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] return dbc.Card(dbc.CardBody(card_list), id="card-1") diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index ccaf014..7aa7d04 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -17,7 +17,9 @@ from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( + create_filter_results_card, create_loaded_data_summary, + create_selected_period_card, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -71,60 +73,6 @@ def create_resampling_card(loaded_data): return row, options -def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: - """ - Create the card with the information on the selected period to be displayed in the Results section. - - Args: - period: Sequence object containing the selected period - dataset: The original dataset from which the period has been selected - index: of the period - """ - info_data = { - "n_frames": period.eit_data["raw"].nframes, - "start_time": period.eit_data["raw"].time[0], - "end_time": period.eit_data["raw"].time[-1], - "dataset": dataset, - } - - card_list = [ - html.H4(period.label, className="card-title"), - ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in info_data.items() - ] - card_list += [ - dbc.Button( - "Remove", - id={"type": ids.REMOVE_PERIOD_BUTTON, "index": str(index)}, - ), - ] - - return dbc.Card( - dbc.CardBody(card_list), - id={"type": ids.PERIOD_CARD, "index": str(index)}, - ) - - -def create_filter_results_card(parameters: dict) -> dbc.Card: - """ - Create the card with the information on the parameters used for filtering the data. - - Args: - parameters: dictionary containing the filter information - """ - card_list = [ - html.H4("Data filtered", className="card-title"), - ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in parameters.items() - ] - - return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) - - def get_loaded_data(): loaded_data = data_object.get_all_sequences() data = [] @@ -535,7 +483,11 @@ def remove_period(n_clicks, container, figure): # remove from the figure (if the figure exists) try: - figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != input_id] + figure["data"] = [ + trace + for trace in figure["data"] + if "meta" not in trace or trace["meta"]["uid"] != input_id + ] except TypeError: contextlib.suppress(Exception) diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 5b48363..9501534 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -68,4 +68,7 @@ SUMMARY_COLUMN = "summary-column" # analyze -ANALYZE_RESULTS_TITLE = "analyze-reults-title" +ANALYZE_RESULTS_TITLE = "analyze-results-title" +ANALYZE_TITLE = "analyze-title" +EELI_APPLY = "eeli-apply" +SUMMARY_COLUMN_ANALYZE = "summary-column-analyze" diff --git a/eit_dash/main.py b/eit_dash/main.py index f35c620..2c7a216 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -11,7 +11,9 @@ app.layout = html.Div( [ html.H1( - id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}, + id="test-id", + children="EIT-ALIVE dashboard", + style={"textAlign": "center"}, ), dbc.Row( [ diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index f79d5b1..c117a17 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -9,7 +9,12 @@ register_page(__name__, path="/analyze") -summary = dbc.Col([html.H2("Summary", style=styles.COLUMN_TITLE)]) +summary = dbc.Col( + [ + html.H2("Summary", style=styles.COLUMN_TITLE), + html.Div([], id=ids.SUMMARY_COLUMN_ANALYZE, style=styles.LOAD_RESULTS), + ] +) results = dbc.Col( [ @@ -20,7 +25,15 @@ actions = dbc.Col( [ - html.H2("Data analysis", style=styles.COLUMN_TITLE), + html.H2("Data analysis", id=ids.ANALYZE_TITLE, style=styles.COLUMN_TITLE), + html.P(), + html.Div( + dbc.Row( + dbc.Button("Apply EELI", id=ids.EELI_APPLY, disabled=False), + ), + hidden=False, + ), + html.P(), ], ) diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index b8fd78b..c3cbd7c 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -7,6 +7,8 @@ from dash import html from eit_dash.app import data_object +from eit_dash.definitions import element_ids as ids +from eit_dash.definitions import layout_styles as styles from eit_dash.definitions.constants import RAW_EIT_LABEL if TYPE_CHECKING: @@ -25,6 +27,24 @@ def blank_fig(): return fig +def create_filter_results_card(parameters: dict) -> dbc.Card: + """ + Create the card with the information on the parameters used for filtering the data. + + Args: + parameters: dictionary containing the filter information + """ + card_list = [ + html.H4("Data filtered", className="card-title"), + ] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in parameters.items() + ] + + return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) + + def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() @@ -34,6 +54,46 @@ def create_loaded_data_summary(): ] +def create_selected_period_card( + period: Sequence, dataset: str, index: int, remove_button: bool = True +) -> dbc.Card: + """ + Create the card with the information on the selected period to be displayed in the Results section. + + Args: + period: Sequence object containing the selected period + dataset: The original dataset from which the period has been selected + index: of the period + remove_button: add the remove button if set to True + """ + info_data = { + "n_frames": period.eit_data["raw"].nframes, + "start_time": period.eit_data["raw"].time[0], + "end_time": period.eit_data["raw"].time[-1], + "dataset": dataset, + } + + card_list = [ + html.H4(period.label, className="card-title"), + ] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] + if remove_button: + card_list += [ + dbc.Button( + "Remove", + id={"type": ids.REMOVE_PERIOD_BUTTON, "index": str(index)}, + ), + ] + + return dbc.Card( + dbc.CardBody(card_list), + id={"type": ids.PERIOD_CARD, "index": str(index)}, + ) + + def create_slider_figure( dataset: Sequence, continuous_data: list[str] | None = None, From b1f9ae1de5bf7412139c14b54936f1fc82ec77c9 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 11:39:53 +0200 Subject: [PATCH 07/26] adding graph container --- eit_dash/definitions/element_ids.py | 1 + eit_dash/pages/analyze.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 9501534..df99ad8 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -71,4 +71,5 @@ ANALYZE_RESULTS_TITLE = "analyze-results-title" ANALYZE_TITLE = "analyze-title" EELI_APPLY = "eeli-apply" +EELI_RESULTS_GRAPH = "eeli-results-graph" SUMMARY_COLUMN_ANALYZE = "summary-column-analyze" diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index c117a17..38ffecc 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -33,6 +33,12 @@ ), hidden=False, ), + html.Div( + dbc.Row( + dcc.Graph(id=ids.EELI_RESULTS_GRAPH), + ), + hidden=True, + ), html.P(), ], ) From 6d17aaf318ac4c4274a0f5283e855ae3f7adef18 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Mon, 15 Apr 2024 15:07:44 +0200 Subject: [PATCH 08/26] create analyze page --- eit_dash/callbacks/analyze_callbacks.py | 31 +++++++++++++ eit_dash/callbacks/preprocessing_callbacks.py | 43 ++++++++++++++----- eit_dash/definitions/element_ids.py | 3 ++ eit_dash/main.py | 12 ++++-- eit_dash/pages/analyze.py | 34 +++++++++++++++ 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 eit_dash/callbacks/analyze_callbacks.py create mode 100644 eit_dash/pages/analyze.py diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py new file mode 100644 index 0000000..59a2377 --- /dev/null +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -0,0 +1,31 @@ +from dash import Input, Output, State, callback, ctx + +import eit_dash.definitions.element_ids as ids + + +@callback( + [Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True)], + [ + Input(ids.ANALYZE_RESULTS_TITLE, "children"), + ], + [ + State(ids.SUMMARY_COLUMN, "children"), + ], + # this allows duplicate outputs with initial call + prevent_initial_call="initial_duplicate", +) +def update_summary(start, summary): + """Updates summary. + + When the page is loaded, it populates the summary column + with the info about the loaded datasets and the preprocessing steps + """ + trigger = ctx.triggered_id + + results = [] + + if trigger is None: + data = [] + summary += data + + return results diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 8bad26b..14c22e0 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -61,7 +61,10 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] + options = [ + {"label": f'{data["Name"]}', "value": str(i)} + for i, data in enumerate(loaded_data) + ] return row, options @@ -69,7 +72,10 @@ def create_resampling_card(loaded_data): def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() - return [dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) for dataset in loaded_data] + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: @@ -91,7 +97,10 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d card_list = [ html.H4(period.label, className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] card_list += [ dbc.Button( "Remove", @@ -115,7 +124,10 @@ def create_filter_results_card(parameters: dict) -> dbc.Card: card_list = [ html.H4("Data filtered", className="card-title"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in parameters.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in parameters.items() + ] return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) @@ -126,7 +138,10 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] + data += [ + {"Name": name, "Data type": channel} + for channel in dataset.continuous_data + ] if dataset.eit_data: data.append( { @@ -186,7 +201,7 @@ def load_datasets(title): [ Output(ids.OPEN_SYNCH_BUTTON, "disabled"), Output(ids.OPEN_SELECT_PERIODS_BUTTON, "disabled"), - Output(ids.SUMMARY_COLUMN, "children"), + Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True), Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), ], [ @@ -268,7 +283,10 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] + options = [ + {"label": sequence.label, "value": index} + for index, sequence in enumerate(signals) + ] body = [ html.H6("Select one dataset"), @@ -613,9 +631,12 @@ def enable_apply_button( if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) - or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 + ) + or ( + int(filter_selected) + in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] and co_low > 0 and co_low > 0 ) @@ -791,7 +812,9 @@ def save_filtered_signal(confirm, results: list): data.update_data(tmp_data) if not params: - params = tmp_data.continuous_data.data["global_impedance_filtered"].parameters + params = tmp_data.continuous_data.data[ + "global_impedance_filtered" + ].parameters # show info card for element in results: diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 6e2b168..276bea8 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -66,3 +66,6 @@ RESAMPLING_CARD_BODY = "resampling-card-body" RESAMPLING_FREQUENCY_INPUT = "resampling-frequency-input" SUMMARY_COLUMN = "summary-column" + +# analyze +ANALYZE_RESULTS_TITLE = "analyze-reults-title" diff --git a/eit_dash/main.py b/eit_dash/main.py index 169e9c9..2496239 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -2,16 +2,22 @@ from dash import html, page_container from eit_dash.app import app -from eit_dash.callbacks import load_callbacks, preprocessing_callbacks # noqa: F401 +from eit_dash.callbacks import ( + load_callbacks, + preprocessing_callbacks, + analyze_callbacks, +) # noqa: F401 app.layout = html.Div( [ - html.H1(id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}), + html.H1( + id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"} + ), dbc.Row( [ dbc.Col(dbc.NavLink("Load", href="/load")), dbc.Col(dbc.NavLink("Pre-processing", href="/preprocessing")), - dbc.Col(dbc.NavLink("Analyze", href="/dummy")), + dbc.Col(dbc.NavLink("Analyze", href="/analyze")), dbc.Col(dbc.NavLink("Summarize", href="/dummy")), ], style={"textAlign": "center"}, diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py new file mode 100644 index 0000000..f79d5b1 --- /dev/null +++ b/eit_dash/pages/analyze.py @@ -0,0 +1,34 @@ +from pathlib import Path + +import dash_bootstrap_components as dbc +from dash import dcc, html, register_page + +import eit_dash.definitions.element_ids as ids +import eit_dash.definitions.layout_styles as styles +from eit_dash.definitions.option_lists import InputFiletypes + +register_page(__name__, path="/analyze") + +summary = dbc.Col([html.H2("Summary", style=styles.COLUMN_TITLE)]) + +results = dbc.Col( + [ + html.H2("Results", id=ids.ANALYZE_RESULTS_TITLE, style=styles.COLUMN_TITLE), + html.Div(id=ids.DATASET_CONTAINER, style=styles.LOAD_RESULTS), + ], +) + +actions = dbc.Col( + [ + html.H2("Data analysis", style=styles.COLUMN_TITLE), + ], +) + +layout = dbc.Row( + [ + html.H1("ANALYZE DATA", style=styles.COLUMN_TITLE), + summary, + actions, + results, + ], +) From 5380fc649e55a320ce4827573bfe674cf01b8d3c Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Tue, 16 Apr 2024 10:36:38 +0200 Subject: [PATCH 09/26] refactored to use eit as continuous data --- eit_dash/callbacks/load_callbacks.py | 29 ++-- eit_dash/callbacks/preprocessing_callbacks.py | 22 +-- eit_dash/definitions/constants.py | 1 + eit_dash/utils/common.py | 155 ++++++++++-------- 4 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 eit_dash/definitions/constants.py diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 834b804..4278919 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -16,6 +16,7 @@ import eit_dash.definitions.element_ids as ids from eit_dash.app import data_object from eit_dash.definitions import layout_styles as styles +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import InputFiletypes from eit_dash.utils.common import ( create_slider_figure, @@ -47,7 +48,10 @@ def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: html.H4(dataset.label, className="card-title"), html.H6(InputFiletypes(file_type).name, className="card-subtitle"), ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] return dbc.Card(dbc.CardBody(card_list), id="card-1") @@ -131,13 +135,13 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): trigger = ctx.triggered_id - # cancelled selection. Reset the data and turn of the data selector + # cancelled selection. Reset the data and turn off the data selector if trigger == ids.LOAD_CANCEL_BUTTON: data = None file_data = None if not data: - # this is needed, because a figure object must be returned for the graph, evn if empty + # this is needed, because a figure object must be returned for the graph, even if empty figure = go.Figure() return True, [], figure @@ -157,12 +161,11 @@ def open_data_selector(data, cancel_load, sig, file_type, fig): figure = create_slider_figure( file_data, - ["raw"], - list(file_data.continuous_data), - True, + continuous_data=list(file_data.continuous_data), + clickable_legend=True, ) - ok = ["raw"] + ok = [RAW_EIT_LABEL] if sig: ok += [options[s]["label"] for s in sig] @@ -202,12 +205,12 @@ def show_info( start_sample, stop_sample = get_selections_slidebar(slidebar_stat) if not start_sample: - start_sample = file_data.eit_data["raw"].time[0] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] if not stop_sample: - stop_sample = file_data.eit_data["raw"].time[-1] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] else: - start_sample = file_data.eit_data["raw"].time[0] - stop_sample = file_data.eit_data["raw"].time[-1] + start_sample = file_data.continuous_data[RAW_EIT_LABEL].time[0] + stop_sample = file_data.continuous_data[RAW_EIT_LABEL].time[-1] dataset_name = f"Dataset {data_object.get_sequence_list_length()}" @@ -224,8 +227,8 @@ def show_info( eit_data_cut.add(data[data_type].select_by_time(start_sample, stop_sample)) for data_type in (data := file_data.continuous_data): - # add just the selected signals - if data_type in selected: + # add just the selected signals and the raw EIT + if data_type in selected or data_type == RAW_EIT_LABEL: continuous_data_cut.add( data[data_type].select_by_time(start_sample, stop_sample), ) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 14c22e0..5abd656 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -15,7 +15,9 @@ import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods +from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.utils.common import ( + create_loaded_data_summary, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -69,15 +71,6 @@ def create_resampling_card(loaded_data): return row, options -def create_loaded_data_summary(): - loaded_data = data_object.get_all_sequences() - - return [ - dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) - for dataset in loaded_data - ] - - def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: """ Create the card with the information on the selected period to be displayed in the Results section. @@ -89,7 +82,7 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d """ info_data = { "n_frames": period.eit_data["raw"].nframes, - "start_time": period.time[0], + "start_time": period.eit_data["raw"].time[0], "end_time": period.eit_data["raw"].time[-1], "dataset": dataset, } @@ -363,7 +356,6 @@ def initialize_figure( current_figure = create_slider_figure( data, - ["raw"], list(data.continuous_data), ) @@ -774,8 +766,8 @@ def show_filtered_results(_, update, selected): fig.add_trace( go.Scatter( - x=data.eit_data.data["raw"].time, - y=data.eit_data.data["raw"].global_impedance, + x=data.continuous_data[RAW_EIT_LABEL].time, + y=data.continuous_data[RAW_EIT_LABEL].values, name="Original signal", ), ) @@ -869,6 +861,6 @@ def filter_data(data: Sequence, filter_params: dict) -> ContinuousData | None: "a.u.", "impedance", parameters=filter_params, - time=data.eit_data.data["raw"].time, - values=filt.apply_filter(data.eit_data.data["raw"].global_impedance), + time=data.continuous_data[RAW_EIT_LABEL].time, + values=filt.apply_filter(data.continuous_data[RAW_EIT_LABEL].values), ) diff --git a/eit_dash/definitions/constants.py b/eit_dash/definitions/constants.py new file mode 100644 index 0000000..241c1b2 --- /dev/null +++ b/eit_dash/definitions/constants.py @@ -0,0 +1 @@ +RAW_EIT_LABEL = "global_impedance_(raw)" diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 874f8d0..9ad66ad 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -2,7 +2,12 @@ from typing import TYPE_CHECKING +import dash_bootstrap_components as dbc import plotly.graph_objects as go +from dash import html + +from eit_dash.app import data_object +from eit_dash.definitions.constants import RAW_EIT_LABEL if TYPE_CHECKING: from eitprocessing.sequence import Sequence @@ -20,17 +25,24 @@ def blank_fig(): return fig +def create_loaded_data_summary(): + loaded_data = data_object.get_all_sequences() + + return [ + dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) + for dataset in loaded_data + ] + + def create_slider_figure( dataset: Sequence, - eit_variants: list[str] | None = None, continuous_data: list[str] | None = None, clickable_legend: bool = False, ) -> go.Figure: - """Create the figure for the selection of range. + """Create the figure for the selection of range. The raw global impedance is plotted by default. Args: dataset: Sequence object containing the selected dataset - eit_variants: list of the eit variants to be plotted continuous_data: list of the continuous data signals to be plotted clickable_legend: if True, the user can hide a signal by clicking on the legend """ @@ -40,45 +52,43 @@ def create_slider_figure( if continuous_data is None: continuous_data = [] - if eit_variants is None: - eit_variants = ["raw"] - - for eit_variant in eit_variants: - figure.add_trace( - go.Scatter( - x=dataset.eit_data[eit_variant].time, - y=dataset.eit_data[eit_variant].global_impedance, - name=eit_variant, - ), - ) + + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[RAW_EIT_LABEL].time, + y=dataset.continuous_data[RAW_EIT_LABEL].values, + name=RAW_EIT_LABEL, + ), + ) for n, cont_signal in enumerate(continuous_data): - figure.add_trace( - go.Scatter( - x=dataset.continuous_data[cont_signal].time, - y=dataset.continuous_data[cont_signal].values, - name=cont_signal, - opacity=0.5, - yaxis=f"y{n + 2}", - ), - ) - # decide whether to put the axis left or right - side = "right" if n % 2 == 0 else "left" - - y_position += 0.1 - new_y = { - "title": cont_signal, - "anchor": "free", - "overlaying": "y", - "side": side, - "autoshift": True, - } - - # layout parameters for multiple y axis - param_name = f"yaxis{n + 2}" - params.update({param_name: new_y}) - - for event in dataset.eit_data[eit_variants[0]].events: + if cont_signal != RAW_EIT_LABEL: + figure.add_trace( + go.Scatter( + x=dataset.continuous_data[cont_signal].time, + y=dataset.continuous_data[cont_signal].values, + name=cont_signal, + opacity=0.5, + yaxis=f"y{n + 2}", + ), + ) + # decide whether to put the axis left or right + side = "right" if n % 2 == 0 else "left" + + y_position += 0.1 + new_y = { + "title": cont_signal, + "anchor": "free", + "overlaying": "y", + "side": side, + "autoshift": True, + } + + # layout parameters for multiple y axis + param_name = f"yaxis{n + 2}" + params.update({param_name: new_y}) + + for event in dataset.eit_data["raw"].events: annotation = {"text": f"{event.text}", "textangle": -90} figure.add_vline( x=event.time, @@ -117,32 +127,38 @@ def mark_selected_periods( """ for period in periods: seq = period.get_data() - for eit_variant in seq.eit_data: - selected_impedance = go.Scatter( - x=seq.eit_data[eit_variant].time, - y=seq.eit_data[eit_variant].global_impedance, - name=eit_variant, - meta={"uid": period.get_period_index()}, - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() - - if type(original_figure) == go.Figure: - original_figure.add_trace(selected_impedance) - else: - original_figure["data"].append(selected_impedance) + # for eit_variant in seq.eit_data: + # selected_impedance = go.Scatter( + # x=seq.eit_data[eit_variant].time, + # y=seq.eit_data[eit_variant].global_impedance, + # name=eit_variant, + # meta={"uid": period.get_period_index()}, + # line={"color": "black"}, + # showlegend=False, + # ).to_plotly_json() + # + # if type(original_figure) == go.Figure: + # original_figure.add_trace(selected_impedance) + # else: + # original_figure["data"].append(selected_impedance) for n, cont_signal in enumerate(seq.continuous_data): - selected_signal = go.Scatter( - x=seq.continuous_data[cont_signal].time, - y=seq.continuous_data[cont_signal].values, - name=cont_signal, - meta={"uid": period.get_period_index()}, - opacity=0.5, - yaxis=f"y{n + 2}", - line={"color": "black"}, - showlegend=False, - ).to_plotly_json() + params = { + "x": seq.continuous_data[cont_signal].time, + "y": seq.continuous_data[cont_signal].values, + "name": cont_signal, + "meta": {"uid": period.get_period_index()}, + "line": {"color": "black"}, + "showlegend": False, + } + if cont_signal != RAW_EIT_LABEL: + params.update( + { + "opacity": 0.5, + "yaxis": f"y{n + 2}", + } + ) + selected_signal = go.Scatter(**params).to_plotly_json() if type(original_figure) == go.Figure: original_figure.add_trace(selected_signal) @@ -165,15 +181,16 @@ def get_signal_options( A list of label - value options for populating the options list """ options = [] - if show_eit: - # iterate over eit data - for eit in dataset.eit_data: - options.append({"label": eit, "value": len(options)}) + # if show_eit: + # # iterate over eit data + # for eit in dataset.eit_data: + # options.append({"label": eit, "value": len(options)}) if dataset.continuous_data: # iterate over continuous data for cont in dataset.continuous_data: - options.append({"label": cont, "value": len(options)}) + if (cont == RAW_EIT_LABEL and show_eit) or cont != RAW_EIT_LABEL: + options.append({"label": cont, "value": len(options)}) return options From 3f42db587e31eccd554e5ad25daaf938463378df Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Tue, 16 Apr 2024 16:10:20 +0200 Subject: [PATCH 10/26] small changes --- eit_dash/callbacks/preprocessing_callbacks.py | 2 +- eit_dash/main.py | 6 +++--- eit_dash/utils/common.py | 20 +------------------ 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 5abd656..b772a72 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -14,8 +14,8 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object -from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.definitions.constants import RAW_EIT_LABEL +from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( create_loaded_data_summary, create_slider_figure, diff --git a/eit_dash/main.py b/eit_dash/main.py index 2496239..f35c620 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -3,15 +3,15 @@ from eit_dash.app import app from eit_dash.callbacks import ( + analyze_callbacks, load_callbacks, preprocessing_callbacks, - analyze_callbacks, -) # noqa: F401 +) app.layout = html.Div( [ html.H1( - id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"} + id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}, ), dbc.Row( [ diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 9ad66ad..b8fd78b 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -127,20 +127,6 @@ def mark_selected_periods( """ for period in periods: seq = period.get_data() - # for eit_variant in seq.eit_data: - # selected_impedance = go.Scatter( - # x=seq.eit_data[eit_variant].time, - # y=seq.eit_data[eit_variant].global_impedance, - # name=eit_variant, - # meta={"uid": period.get_period_index()}, - # line={"color": "black"}, - # showlegend=False, - # ).to_plotly_json() - # - # if type(original_figure) == go.Figure: - # original_figure.add_trace(selected_impedance) - # else: - # original_figure["data"].append(selected_impedance) for n, cont_signal in enumerate(seq.continuous_data): params = { @@ -156,7 +142,7 @@ def mark_selected_periods( { "opacity": 0.5, "yaxis": f"y{n + 2}", - } + }, ) selected_signal = go.Scatter(**params).to_plotly_json() @@ -181,10 +167,6 @@ def get_signal_options( A list of label - value options for populating the options list """ options = [] - # if show_eit: - # # iterate over eit data - # for eit in dataset.eit_data: - # options.append({"label": eit, "value": len(options)}) if dataset.continuous_data: # iterate over continuous data From 3dd649426674af28f86bac77b90fb339627e88cd Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 10:33:12 +0200 Subject: [PATCH 11/26] summary cards --- eit_dash/callbacks/analyze_callbacks.py | 42 ++++++++++--- eit_dash/callbacks/load_callbacks.py | 5 +- eit_dash/callbacks/preprocessing_callbacks.py | 62 +++---------------- eit_dash/definitions/element_ids.py | 5 +- eit_dash/main.py | 4 +- eit_dash/pages/analyze.py | 17 ++++- eit_dash/utils/common.py | 60 ++++++++++++++++++ 7 files changed, 124 insertions(+), 71 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 59a2377..db77a63 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,20 +1,27 @@ +import dash_bootstrap_components as dbc from dash import Input, Output, State, callback, ctx import eit_dash.definitions.element_ids as ids +from eit_dash.app import data_object +from eit_dash.utils.common import ( + create_filter_results_card, + create_loaded_data_summary, + create_selected_period_card, +) @callback( - [Output(ids.SUMMARY_COLUMN, "children", allow_duplicate=True)], + Output(ids.SUMMARY_COLUMN_ANALYZE, "children", allow_duplicate=True), [ Input(ids.ANALYZE_RESULTS_TITLE, "children"), ], [ - State(ids.SUMMARY_COLUMN, "children"), + State(ids.SUMMARY_COLUMN_ANALYZE, "children"), ], # this allows duplicate outputs with initial call prevent_initial_call="initial_duplicate", ) -def update_summary(start, summary): +def update_summary(_, summary): """Updates summary. When the page is loaded, it populates the summary column @@ -22,10 +29,29 @@ def update_summary(start, summary): """ trigger = ctx.triggered_id - results = [] - if trigger is None: - data = [] - summary += data + loaded_data = create_loaded_data_summary() + summary += loaded_data + + filter_params = {} + + for period in data_object.get_all_stable_periods(): + if not filter_params: + filter_params = ( + period.get_data() + .continuous_data.data["global_impedance_filtered"] + .parameters + ) + + summary += [ + create_selected_period_card( + period.get_data(), + period.get_dataset_index(), + period.get_period_index(), + False, + ) + ] + + summary += [create_filter_results_card(filter_params)] - return results + return summary diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 4278919..7c29c37 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -48,10 +48,7 @@ def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: html.H4(dataset.label, className="card-title"), html.H6(InputFiletypes(file_type).name, className="card-subtitle"), ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in info_data.items() - ] + card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] return dbc.Card(dbc.CardBody(card_list), id="card-1") diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index b772a72..e915811 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -17,7 +17,9 @@ from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( + create_filter_results_card, create_loaded_data_summary, + create_selected_period_card, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -71,60 +73,6 @@ def create_resampling_card(loaded_data): return row, options -def create_selected_period_card(period: Sequence, dataset: str, index: int) -> dbc.Card: - """ - Create the card with the information on the selected period to be displayed in the Results section. - - Args: - period: Sequence object containing the selected period - dataset: The original dataset from which the period has been selected - index: of the period - """ - info_data = { - "n_frames": period.eit_data["raw"].nframes, - "start_time": period.eit_data["raw"].time[0], - "end_time": period.eit_data["raw"].time[-1], - "dataset": dataset, - } - - card_list = [ - html.H4(period.label, className="card-title"), - ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in info_data.items() - ] - card_list += [ - dbc.Button( - "Remove", - id={"type": ids.REMOVE_PERIOD_BUTTON, "index": str(index)}, - ), - ] - - return dbc.Card( - dbc.CardBody(card_list), - id={"type": ids.PERIOD_CARD, "index": str(index)}, - ) - - -def create_filter_results_card(parameters: dict) -> dbc.Card: - """ - Create the card with the information on the parameters used for filtering the data. - - Args: - parameters: dictionary containing the filter information - """ - card_list = [ - html.H4("Data filtered", className="card-title"), - ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in parameters.items() - ] - - return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) - - def get_loaded_data(): loaded_data = data_object.get_all_sequences() data = [] @@ -517,7 +465,11 @@ def remove_period(n_clicks, container, figure): # remove from the figure (if the figure exists) try: - figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != input_id] + figure["data"] = [ + trace + for trace in figure["data"] + if "meta" not in trace or trace["meta"]["uid"] != input_id + ] except TypeError: contextlib.suppress(Exception) diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 276bea8..27af46c 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -68,4 +68,7 @@ SUMMARY_COLUMN = "summary-column" # analyze -ANALYZE_RESULTS_TITLE = "analyze-reults-title" +ANALYZE_RESULTS_TITLE = "analyze-results-title" +ANALYZE_TITLE = "analyze-title" +EELI_APPLY = "eeli-apply" +SUMMARY_COLUMN_ANALYZE = "summary-column-analyze" diff --git a/eit_dash/main.py b/eit_dash/main.py index f35c620..2c7a216 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -11,7 +11,9 @@ app.layout = html.Div( [ html.H1( - id="test-id", children="EIT-ALIVE dashboard", style={"textAlign": "center"}, + id="test-id", + children="EIT-ALIVE dashboard", + style={"textAlign": "center"}, ), dbc.Row( [ diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index f79d5b1..c117a17 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -9,7 +9,12 @@ register_page(__name__, path="/analyze") -summary = dbc.Col([html.H2("Summary", style=styles.COLUMN_TITLE)]) +summary = dbc.Col( + [ + html.H2("Summary", style=styles.COLUMN_TITLE), + html.Div([], id=ids.SUMMARY_COLUMN_ANALYZE, style=styles.LOAD_RESULTS), + ] +) results = dbc.Col( [ @@ -20,7 +25,15 @@ actions = dbc.Col( [ - html.H2("Data analysis", style=styles.COLUMN_TITLE), + html.H2("Data analysis", id=ids.ANALYZE_TITLE, style=styles.COLUMN_TITLE), + html.P(), + html.Div( + dbc.Row( + dbc.Button("Apply EELI", id=ids.EELI_APPLY, disabled=False), + ), + hidden=False, + ), + html.P(), ], ) diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index b8fd78b..c3cbd7c 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -7,6 +7,8 @@ from dash import html from eit_dash.app import data_object +from eit_dash.definitions import element_ids as ids +from eit_dash.definitions import layout_styles as styles from eit_dash.definitions.constants import RAW_EIT_LABEL if TYPE_CHECKING: @@ -25,6 +27,24 @@ def blank_fig(): return fig +def create_filter_results_card(parameters: dict) -> dbc.Card: + """ + Create the card with the information on the parameters used for filtering the data. + + Args: + parameters: dictionary containing the filter information + """ + card_list = [ + html.H4("Data filtered", className="card-title"), + ] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in parameters.items() + ] + + return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) + + def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() @@ -34,6 +54,46 @@ def create_loaded_data_summary(): ] +def create_selected_period_card( + period: Sequence, dataset: str, index: int, remove_button: bool = True +) -> dbc.Card: + """ + Create the card with the information on the selected period to be displayed in the Results section. + + Args: + period: Sequence object containing the selected period + dataset: The original dataset from which the period has been selected + index: of the period + remove_button: add the remove button if set to True + """ + info_data = { + "n_frames": period.eit_data["raw"].nframes, + "start_time": period.eit_data["raw"].time[0], + "end_time": period.eit_data["raw"].time[-1], + "dataset": dataset, + } + + card_list = [ + html.H4(period.label, className="card-title"), + ] + card_list += [ + dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) + for data, value in info_data.items() + ] + if remove_button: + card_list += [ + dbc.Button( + "Remove", + id={"type": ids.REMOVE_PERIOD_BUTTON, "index": str(index)}, + ), + ] + + return dbc.Card( + dbc.CardBody(card_list), + id={"type": ids.PERIOD_CARD, "index": str(index)}, + ) + + def create_slider_figure( dataset: Sequence, continuous_data: list[str] | None = None, From 67141d82e8479ec92eb123a14da08e97b23ab914 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 11:39:53 +0200 Subject: [PATCH 12/26] adding graph container --- eit_dash/definitions/element_ids.py | 1 + eit_dash/pages/analyze.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 27af46c..f824975 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -71,4 +71,5 @@ ANALYZE_RESULTS_TITLE = "analyze-results-title" ANALYZE_TITLE = "analyze-title" EELI_APPLY = "eeli-apply" +EELI_RESULTS_GRAPH = "eeli-results-graph" SUMMARY_COLUMN_ANALYZE = "summary-column-analyze" diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index c117a17..38ffecc 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -33,6 +33,12 @@ ), hidden=False, ), + html.Div( + dbc.Row( + dcc.Graph(id=ids.EELI_RESULTS_GRAPH), + ), + hidden=True, + ), html.P(), ], ) From 1aa4bff68d299087f68ceb4b02d6e02ffb5e9a21 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 14:45:00 +0200 Subject: [PATCH 13/26] saving derived from and running eeli --- eit_dash/callbacks/analyze_callbacks.py | 27 ++++++++++++++++++- eit_dash/callbacks/preprocessing_callbacks.py | 13 +++++---- eit_dash/definitions/constants.py | 1 + eit_dash/definitions/element_ids.py | 1 + eit_dash/pages/analyze.py | 1 + 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index db77a63..bbaea20 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,7 +1,9 @@ -import dash_bootstrap_components as dbc from dash import Input, Output, State, callback, ctx +from eitprocessing.parameters.eeli import EELI + import eit_dash.definitions.element_ids as ids +from eit_dash.definitions.constants import FILTERED_EIT_LABEL from eit_dash.app import data_object from eit_dash.utils.common import ( create_filter_results_card, @@ -9,6 +11,8 @@ create_selected_period_card, ) +import plotly.graph_objects as go + @callback( Output(ids.SUMMARY_COLUMN_ANALYZE, "children", allow_duplicate=True), @@ -55,3 +59,24 @@ def update_summary(_, summary): summary += [create_filter_results_card(filter_params)] return summary + + +@callback( + Output(ids.EELI_RESULTS_GRAPH, "figure"), + Output(ids.EELI_RESULTS_GRAPH_DIV, "hidden"), + Input(ids.EELI_APPLY, "n_clicks"), + prevent_initial_call=True, +) +def apply_eeli(_): + periods = data_object.get_all_stable_periods() + eeli = [] + for period in periods: + sequence = period.get_data() + eeli_result_filtered = EELI().compute_parameter( + sequence, FILTERED_EIT_LABEL + ) + + eeli.append(eeli_result_filtered) + + fig = go.Figure() + return fig, False diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index e915811..160d5be 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -14,7 +14,7 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object -from eit_dash.definitions.constants import RAW_EIT_LABEL +from eit_dash.definitions.constants import RAW_EIT_LABEL, FILTERED_EIT_LABEL from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( create_filter_results_card, @@ -726,8 +726,8 @@ def show_filtered_results(_, update, selected): fig.add_trace( go.Scatter( - x=filtered_data.continuous_data.data["global_impedance_filtered"].time, - y=filtered_data.continuous_data.data["global_impedance_filtered"].values, + x=filtered_data.continuous_data.data[FILTERED_EIT_LABEL].time, + y=filtered_data.continuous_data.data[FILTERED_EIT_LABEL].values, name="Filtered signal", ), ) @@ -757,7 +757,7 @@ def save_filtered_signal(confirm, results: list): if not params: params = tmp_data.continuous_data.data[ - "global_impedance_filtered" + FILTERED_EIT_LABEL ].parameters # show info card @@ -807,11 +807,14 @@ def filter_data(data: Sequence, filter_params: dict) -> ContinuousData | None: filt = ButterworthFilter(**filter_params) + gi = data.continuous_data[RAW_EIT_LABEL] + return ContinuousData( - "global_impedance_filtered", + FILTERED_EIT_LABEL, f"global_impedance filtered with {filter_params['filter_type']}", "a.u.", "impedance", + derived_from=[*gi.derived_from, gi], parameters=filter_params, time=data.continuous_data[RAW_EIT_LABEL].time, values=filt.apply_filter(data.continuous_data[RAW_EIT_LABEL].values), diff --git a/eit_dash/definitions/constants.py b/eit_dash/definitions/constants.py index 241c1b2..380f6ca 100644 --- a/eit_dash/definitions/constants.py +++ b/eit_dash/definitions/constants.py @@ -1 +1,2 @@ RAW_EIT_LABEL = "global_impedance_(raw)" +FILTERED_EIT_LABEL = "global_impedance_(filtered)" diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index f824975..6f441c3 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -72,4 +72,5 @@ ANALYZE_TITLE = "analyze-title" EELI_APPLY = "eeli-apply" EELI_RESULTS_GRAPH = "eeli-results-graph" +EELI_RESULTS_GRAPH_DIV = "eeli-results-graph-div" SUMMARY_COLUMN_ANALYZE = "summary-column-analyze" diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index 38ffecc..0b443e0 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -37,6 +37,7 @@ dbc.Row( dcc.Graph(id=ids.EELI_RESULTS_GRAPH), ), + id=ids.EELI_RESULTS_GRAPH_DIV, hidden=True, ), html.P(), From 9369cc33b247eb8a3b07154da75ee5de825e1b6e Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 14:45:14 +0200 Subject: [PATCH 14/26] saving derived from and running eeli --- eit_dash/callbacks/analyze_callbacks.py | 4 +--- eit_dash/callbacks/preprocessing_callbacks.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index bbaea20..d2bab19 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -72,9 +72,7 @@ def apply_eeli(_): eeli = [] for period in periods: sequence = period.get_data() - eeli_result_filtered = EELI().compute_parameter( - sequence, FILTERED_EIT_LABEL - ) + eeli_result_filtered = EELI().compute_parameter(sequence, FILTERED_EIT_LABEL) eeli.append(eeli_result_filtered) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 160d5be..5fe6547 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -756,9 +756,7 @@ def save_filtered_signal(confirm, results: list): data.update_data(tmp_data) if not params: - params = tmp_data.continuous_data.data[ - FILTERED_EIT_LABEL - ].parameters + params = tmp_data.continuous_data.data[FILTERED_EIT_LABEL].parameters # show info card for element in results: From 00cb32364a81250bc3310a9a90d8a38de0bc64a1 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 16:49:45 +0200 Subject: [PATCH 15/26] plotting results --- eit_dash/callbacks/analyze_callbacks.py | 105 ++++++++++++++++++++++-- eit_dash/definitions/element_ids.py | 1 + eit_dash/pages/analyze.py | 10 ++- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index d2bab19..6456978 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,8 +1,10 @@ from dash import Input, Output, State, callback, ctx +from matplotlib import pyplot as plt from eitprocessing.parameters.eeli import EELI import eit_dash.definitions.element_ids as ids +import eit_dash.definitions.layout_styles as styles from eit_dash.definitions.constants import FILTERED_EIT_LABEL from eit_dash.app import data_object from eit_dash.utils.common import ( @@ -13,9 +15,12 @@ import plotly.graph_objects as go +eeli = [] + @callback( Output(ids.SUMMARY_COLUMN_ANALYZE, "children", allow_duplicate=True), + Output(ids.ANALYZE_SELECT_PERIOD_VIEW, "options"), [ Input(ids.ANALYZE_RESULTS_TITLE, "children"), ], @@ -25,13 +30,15 @@ # this allows duplicate outputs with initial call prevent_initial_call="initial_duplicate", ) -def update_summary(_, summary): - """Updates summary. +def page_setup(_, summary): + """Setups the page elements when it starts up. When the page is loaded, it populates the summary column - with the info about the loaded datasets and the preprocessing steps + with the info about the loaded datasets and the preprocessing steps. + Populates the periods selections element with the loaded periods. """ trigger = ctx.triggered_id + options = [] if trigger is None: loaded_data = create_loaded_data_summary() @@ -56,25 +63,107 @@ def update_summary(_, summary): ) ] + # populate period selection + options.append( + { + "label": f"Period {period.get_period_index()}", + "value": period.get_period_index(), + } + ) + summary += [create_filter_results_card(filter_params)] - return summary + return summary, options @callback( - Output(ids.EELI_RESULTS_GRAPH, "figure"), Output(ids.EELI_RESULTS_GRAPH_DIV, "hidden"), Input(ids.EELI_APPLY, "n_clicks"), prevent_initial_call=True, ) def apply_eeli(_): + """Apply EELI and store results""" + + global eeli + + eeli.clear() + periods = data_object.get_all_stable_periods() - eeli = [] + for period in periods: sequence = period.get_data() eeli_result_filtered = EELI().compute_parameter(sequence, FILTERED_EIT_LABEL) + # TODO: the results should be stored in the sequence object + eeli_result_filtered["index"] = period.get_period_index() + eeli.append(eeli_result_filtered) - fig = go.Figure() - return fig, False + return False + + +@callback( + [ + Output(ids.EELI_RESULTS_GRAPH, "figure"), + Output(ids.EELI_RESULTS_GRAPH, "style"), + ], + Input(ids.ANALYZE_SELECT_PERIOD_VIEW, "value"), + prevent_initial_call=True, +) +def show_eeli(selected): + """ + Show the results of the EELI for the selected period. + """ + figure = go.Figure() + + sequence = data_object.get_stable_period(int(selected)).get_data() + for e in eeli: + if e["index"] == int(selected): + result = e + + figure.add_trace( + go.Scatter( + x=sequence.continuous_data[FILTERED_EIT_LABEL].time, + y=sequence.continuous_data[FILTERED_EIT_LABEL].values, + name=FILTERED_EIT_LABEL, + ) + ) + + figure.add_hline(y=result["mean"], line_color="red", name="Mean") + figure.add_hline(y=result["median"], line_color="red", name="Median") + + figure.add_scatter( + x=sequence.continuous_data[FILTERED_EIT_LABEL].time[result["indices"]], + y=result["values"], + line_color="black", + name="EELIs", + mode="markers", + ) + + sd_upper = result["mean"] + result["standard deviation"] + sd_lower = result["mean"] - result["standard deviation"] + + figure.add_trace( + go.Scatter( + x=sequence.continuous_data[FILTERED_EIT_LABEL].time, + y=[sd_upper] * len(sequence.continuous_data[FILTERED_EIT_LABEL].time), + fill=None, + mode="lines", + line_color="rgba(0,0,255,0)", # Set to transparent blue + name="Standard deviation", + ) + ) + + # Add the lower bound line + figure.add_trace( + go.Scatter( + x=sequence.continuous_data[FILTERED_EIT_LABEL].time, + y=[sd_lower] * len(sequence.continuous_data[FILTERED_EIT_LABEL].time), + fill="tonexty", # Fill area below this line + mode="lines", + line_color="rgba(0,0,255,0.3)", # Set to semi-transparent blue + name="Standard deviation", + ) + ) + + return figure, styles.GRAPH diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 6f441c3..36955fe 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -69,6 +69,7 @@ # analyze ANALYZE_RESULTS_TITLE = "analyze-results-title" +ANALYZE_SELECT_PERIOD_VIEW = "analyze-select-period-view" ANALYZE_TITLE = "analyze-title" EELI_APPLY = "eeli-apply" EELI_RESULTS_GRAPH = "eeli-results-graph" diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index 0b443e0..bdf4aca 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -33,10 +33,14 @@ ), hidden=False, ), + html.P(), html.Div( - dbc.Row( - dcc.Graph(id=ids.EELI_RESULTS_GRAPH), - ), + [ + dbc.Row(dbc.Select(id=ids.ANALYZE_SELECT_PERIOD_VIEW)), + dbc.Row( + dcc.Graph(id=ids.EELI_RESULTS_GRAPH, style=styles.EMPTY_ELEMENT), + ), + ], id=ids.EELI_RESULTS_GRAPH_DIV, hidden=True, ), From 43fbaf240e94bc736d6123512f9c4b73f07bbd1f Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Wed, 17 Apr 2024 17:01:09 +0200 Subject: [PATCH 16/26] updating data loaded cards --- eit_dash/callbacks/analyze_callbacks.py | 36 ++++++--------- eit_dash/callbacks/load_callbacks.py | 31 +------------ eit_dash/callbacks/preprocessing_callbacks.py | 37 +++++---------- eit_dash/pages/analyze.py | 2 +- eit_dash/utils/common.py | 45 +++++++++++++------ 5 files changed, 61 insertions(+), 90 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 6456978..aa55baa 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,20 +1,18 @@ +import plotly.graph_objects as go from dash import Input, Output, State, callback, ctx -from matplotlib import pyplot as plt - from eitprocessing.parameters.eeli import EELI import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles -from eit_dash.definitions.constants import FILTERED_EIT_LABEL from eit_dash.app import data_object +from eit_dash.definitions.constants import FILTERED_EIT_LABEL from eit_dash.utils.common import ( create_filter_results_card, + create_info_card, create_loaded_data_summary, create_selected_period_card, ) -import plotly.graph_objects as go - eeli = [] @@ -41,18 +39,15 @@ def page_setup(_, summary): options = [] if trigger is None: - loaded_data = create_loaded_data_summary() - summary += loaded_data + for d in data_object.get_all_sequences(): + card = create_info_card(d) + summary += [card] filter_params = {} for period in data_object.get_all_stable_periods(): if not filter_params: - filter_params = ( - period.get_data() - .continuous_data.data["global_impedance_filtered"] - .parameters - ) + filter_params = period.get_data().continuous_data.data["global_impedance_filtered"].parameters summary += [ create_selected_period_card( @@ -60,7 +55,7 @@ def page_setup(_, summary): period.get_dataset_index(), period.get_period_index(), False, - ) + ), ] # populate period selection @@ -68,7 +63,7 @@ def page_setup(_, summary): { "label": f"Period {period.get_period_index()}", "value": period.get_period_index(), - } + }, ) summary += [create_filter_results_card(filter_params)] @@ -82,8 +77,7 @@ def page_setup(_, summary): prevent_initial_call=True, ) def apply_eeli(_): - """Apply EELI and store results""" - + """Apply EELI and store results.""" global eeli eeli.clear() @@ -111,9 +105,7 @@ def apply_eeli(_): prevent_initial_call=True, ) def show_eeli(selected): - """ - Show the results of the EELI for the selected period. - """ + """Show the results of the EELI for the selected period.""" figure = go.Figure() sequence = data_object.get_stable_period(int(selected)).get_data() @@ -126,7 +118,7 @@ def show_eeli(selected): x=sequence.continuous_data[FILTERED_EIT_LABEL].time, y=sequence.continuous_data[FILTERED_EIT_LABEL].values, name=FILTERED_EIT_LABEL, - ) + ), ) figure.add_hline(y=result["mean"], line_color="red", name="Mean") @@ -151,7 +143,7 @@ def show_eeli(selected): mode="lines", line_color="rgba(0,0,255,0)", # Set to transparent blue name="Standard deviation", - ) + ), ) # Add the lower bound line @@ -163,7 +155,7 @@ def show_eeli(selected): mode="lines", line_color="rgba(0,0,255,0.3)", # Set to semi-transparent blue name="Standard deviation", - ) + ), ) return figure, styles.GRAPH diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 7c29c37..ded99ae 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -3,7 +3,6 @@ import os from pathlib import Path -import dash_bootstrap_components as dbc import plotly.graph_objects as go from dash import ALL, Input, Output, State, callback, ctx, html from dash.exceptions import PreventUpdate @@ -15,10 +14,10 @@ import eit_dash.definitions.element_ids as ids from eit_dash.app import data_object -from eit_dash.definitions import layout_styles as styles from eit_dash.definitions.constants import RAW_EIT_LABEL from eit_dash.definitions.option_lists import InputFiletypes from eit_dash.utils.common import ( + create_info_card, create_slider_figure, get_selections_slidebar, get_signal_options, @@ -27,32 +26,6 @@ file_data: Sequence | None = None -def create_info_card(dataset: Sequence, file_type: int) -> dbc.Card: - """Create the card with the information on the loaded dataset to be displayed in the Results section. - - Args: - dataset: Sequence object containing the selected dataset - file_type: Index of the selected type of selected - """ - info_data = { - "Name": dataset.eit_data["raw"].path.name, - "n_frames": dataset.eit_data["raw"].nframes, - "start_time": dataset.eit_data["raw"].time[0], - "end_time": dataset.eit_data["raw"].time[-1], - "vendor": dataset.eit_data["raw"].vendor, - "continuous signals": str(list(dataset.continuous_data)), - "path": str(dataset.eit_data["raw"].path), - } - - card_list = [ - html.H4(dataset.label, className="card-title"), - html.H6(InputFiletypes(file_type).name, className="card-subtitle"), - ] - card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] - - return dbc.Card(dbc.CardBody(card_list), id="card-1") - - # managing the file selection. Confirm button clicked @callback( Output(ids.CHOOSE_DATA_POPUP, "is_open"), @@ -241,7 +214,7 @@ def show_info( data_object.add_sequence(cut_data) # create the info summary card - card = create_info_card(cut_data, int(filetype)) + card = create_info_card(cut_data) # add the card to the current results if container_state: diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 5fe6547..41c8597 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -14,10 +14,11 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object -from eit_dash.definitions.constants import RAW_EIT_LABEL, FILTERED_EIT_LABEL +from eit_dash.definitions.constants import FILTERED_EIT_LABEL, RAW_EIT_LABEL from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( create_filter_results_card, + create_info_card, create_loaded_data_summary, create_selected_period_card, create_slider_figure, @@ -65,10 +66,7 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [ - {"label": f'{data["Name"]}', "value": str(i)} - for i, data in enumerate(loaded_data) - ] + options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] return row, options @@ -79,10 +77,7 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [ - {"Name": name, "Data type": channel} - for channel in dataset.continuous_data - ] + data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] if dataset.eit_data: data.append( { @@ -166,8 +161,10 @@ def update_summary(start, summary): results = [] if trigger is None: - data = create_loaded_data_summary() - summary += data + for d in data_object.get_all_sequences(): + card = create_info_card(d) + summary += [card] + for p in data_object.get_all_stable_periods(): data = p.get_data() results.append( @@ -224,10 +221,7 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [ - {"label": sequence.label, "value": index} - for index, sequence in enumerate(signals) - ] + options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] body = [ html.H6("Select one dataset"), @@ -465,11 +459,7 @@ def remove_period(n_clicks, container, figure): # remove from the figure (if the figure exists) try: - figure["data"] = [ - trace - for trace in figure["data"] - if "meta" not in trace or trace["meta"]["uid"] != input_id - ] + figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != input_id] except TypeError: contextlib.suppress(Exception) @@ -575,12 +565,9 @@ def enable_apply_button( if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) + or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 - ) - or ( - int(filter_selected) - in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] and co_low > 0 and co_low > 0 ) diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index bdf4aca..d67c42a 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -13,7 +13,7 @@ [ html.H2("Summary", style=styles.COLUMN_TITLE), html.Div([], id=ids.SUMMARY_COLUMN_ANALYZE, style=styles.LOAD_RESULTS), - ] + ], ) results = dbc.Col( diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index c3cbd7c..e5ba556 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -37,25 +37,47 @@ def create_filter_results_card(parameters: dict) -> dbc.Card: card_list = [ html.H4("Data filtered", className="card-title"), ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in parameters.items() - ] + card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in parameters.items()] return dbc.Card(dbc.CardBody(card_list), id=ids.FILTERING_SAVED_CARD) +def create_info_card(dataset: Sequence) -> dbc.Card: + """Create the card with the information on the loaded dataset to be displayed in the Results section. + + Args: + dataset: Sequence object containing the selected dataset + """ + info_data = { + "Name": dataset.eit_data["raw"].path.name, + "n_frames": dataset.eit_data["raw"].nframes, + "start_time": dataset.eit_data["raw"].time[0], + "end_time": dataset.eit_data["raw"].time[-1], + "vendor": dataset.eit_data["raw"].vendor, + "continuous signals": str(list(dataset.continuous_data)), + "path": str(dataset.eit_data["raw"].path), + } + + card_list = [ + html.H4(dataset.label, className="card-title"), + html.H6(dataset.eit_data["raw"].vendor, className="card-subtitle"), + ] + card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] + + return dbc.Card(dbc.CardBody(card_list), id="card-1") + + def create_loaded_data_summary(): loaded_data = data_object.get_all_sequences() - return [ - dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) - for dataset in loaded_data - ] + return [dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) for dataset in loaded_data] def create_selected_period_card( - period: Sequence, dataset: str, index: int, remove_button: bool = True + period: Sequence, + dataset: str, + index: int, + remove_button: bool = True, ) -> dbc.Card: """ Create the card with the information on the selected period to be displayed in the Results section. @@ -76,10 +98,7 @@ def create_selected_period_card( card_list = [ html.H4(period.label, className="card-title"), ] - card_list += [ - dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) - for data, value in info_data.items() - ] + card_list += [dbc.Row(f"{data}: {value}", style=styles.INFO_CARD) for data, value in info_data.items()] if remove_button: card_list += [ dbc.Button( From 4a5d9fe89b63b7ec28fd4aaddf40672cd7031379 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 10:03:10 +0200 Subject: [PATCH 17/26] linting --- eit_dash/callbacks/analyze_callbacks.py | 3 +-- eit_dash/callbacks/preprocessing_callbacks.py | 3 +-- eit_dash/main.py | 2 +- eit_dash/pages/analyze.py | 3 --- eit_dash/utils/common.py | 10 +--------- 5 files changed, 4 insertions(+), 17 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index aa55baa..09f4afe 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -9,7 +9,6 @@ from eit_dash.utils.common import ( create_filter_results_card, create_info_card, - create_loaded_data_summary, create_selected_period_card, ) @@ -78,7 +77,7 @@ def page_setup(_, summary): ) def apply_eeli(_): """Apply EELI and store results.""" - global eeli + global eeli # noqa: PLW0602 eeli.clear() diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 41c8597..2135494 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -19,7 +19,6 @@ from eit_dash.utils.common import ( create_filter_results_card, create_info_card, - create_loaded_data_summary, create_selected_period_card, create_slider_figure, get_selections_slidebar, @@ -698,7 +697,7 @@ def show_filtered_results(_, update, selected): try: filtered_data = tmp_results.get_stable_period(int(selected)).get_data() - except Exception: + except ValueError: return fig, styles.EMPTY_ELEMENT data = data_object.get_stable_period(int(selected)).get_data() diff --git a/eit_dash/main.py b/eit_dash/main.py index 2c7a216..1ef1bca 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -6,7 +6,7 @@ analyze_callbacks, load_callbacks, preprocessing_callbacks, -) +) # noqa: F401 app.layout = html.Div( [ diff --git a/eit_dash/pages/analyze.py b/eit_dash/pages/analyze.py index d67c42a..9df3284 100644 --- a/eit_dash/pages/analyze.py +++ b/eit_dash/pages/analyze.py @@ -1,11 +1,8 @@ -from pathlib import Path - import dash_bootstrap_components as dbc from dash import dcc, html, register_page import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles -from eit_dash.definitions.option_lists import InputFiletypes register_page(__name__, path="/analyze") diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index e5ba556..4183d2e 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -6,7 +6,6 @@ import plotly.graph_objects as go from dash import html -from eit_dash.app import data_object from eit_dash.definitions import element_ids as ids from eit_dash.definitions import layout_styles as styles from eit_dash.definitions.constants import RAW_EIT_LABEL @@ -67,12 +66,6 @@ def create_info_card(dataset: Sequence) -> dbc.Card: return dbc.Card(dbc.CardBody(card_list), id="card-1") -def create_loaded_data_summary(): - loaded_data = data_object.get_all_sequences() - - return [dbc.Row([html.Div(f"Loaded {dataset.label}", style={"textAlign": "left"})]) for dataset in loaded_data] - - def create_selected_period_card( period: Sequence, dataset: str, @@ -202,7 +195,6 @@ def mark_selected_periods( original_figure: figure to update periods: list of Sequence object containing the selected dataset. These ranges, the signal is plotted in black - period_index: index of the selected period """ for period in periods: seq = period.get_data() @@ -210,7 +202,7 @@ def mark_selected_periods( for n, cont_signal in enumerate(seq.continuous_data): params = { "x": seq.continuous_data[cont_signal].time, - "y": seq.continuous_data[cont_signal].values, + "y": seq.continuous_data[cont_signal].values, # noqa: PD011 "name": cont_signal, "meta": {"uid": period.get_period_index()}, "line": {"color": "black"}, From 8dd20c60c74ed55eed8b1501e754e6bdda1bcd03 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 10:19:46 +0200 Subject: [PATCH 18/26] pointing to the eeli branch --- poetry.lock | 20 ++++++++++---------- pyproject.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 64cc097..7d2a287 100644 --- a/poetry.lock +++ b/poetry.lock @@ -528,8 +528,8 @@ publishing = ["build", "twine", "wheel"] [package.source] type = "git" url = "https://github.com/EIT-ALIVE/eitprocessing" -reference = "HEAD" -resolved_reference = "31398fb4e20edf5c97cb676e12f6e0817ac3ca4d" +reference = "186_parameter_eeli_psomhorst" +resolved_reference = "cd77958cab0dc6002aed58af8bad9490ba27a440" [[package]] name = "exceptiongroup" @@ -686,13 +686,13 @@ files = [ [[package]] name = "itsdangerous" -version = "2.1.2" +version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, ] [[package]] @@ -1391,13 +1391,13 @@ xmp = ["defusedxml"] [[package]] name = "plotly" -version = "5.20.0" +version = "5.21.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.20.0-py3-none-any.whl", hash = "sha256:837a9c8aa90f2c0a2f0d747b82544d014dc2a2bdde967b5bb1da25b53932d1a9"}, - {file = "plotly-5.20.0.tar.gz", hash = "sha256:bf901c805d22032cfa534b2ff7c5aa6b0659e037f19ec1e0cca7f585918b5c89"}, + {file = "plotly-5.21.0-py3-none-any.whl", hash = "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf"}, + {file = "plotly-5.21.0.tar.gz", hash = "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073"}, ] [package.dependencies] @@ -1945,4 +1945,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "de940f9ffaac9599d4f3d43c489be02b00bb8dd6c621a050f0d6a889e3ff8d3b" +content-hash = "92654ad4b87fcfd8ce315c0fc35c3dd466c12f08c29403110a8dcc798dbe7358" diff --git a/pyproject.toml b/pyproject.toml index 8e6b9c6..aed4de7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ repository = "git@github.com:EIT-ALIVE/eit_dash" python = ">=3.10,<3.13" dash = { extras = ["testing"], version = "^2.11.1" } dash-bootstrap-components = "^1.4.1" -eitprocessing = { git = "https://github.com/EIT-ALIVE/eitprocessing" } +eitprocessing = { git = "https://github.com/EIT-ALIVE/eitprocessing", rev = "186_parameter_eeli_psomhorst" } numpy = "^1.25.2" pandas = "^2.0.3" ruff = "^0.3" @@ -65,7 +65,7 @@ ignore = [ "ARG001", # Unused function argument "ANN", # Type hinting "PLR0913", # Too many arguments - "PLW0603", # Using globals + "PLW0603", # Using globals # Unwanted (potentially) "FBT", # Using boolean arguments "S105", # Possible hardcoded password From 9ee669d8879ae524d83cdaf85869f99ed5e47662 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 10:20:05 +0200 Subject: [PATCH 19/26] formatting --- eit_dash/callbacks/load_callbacks.py | 4 +--- eit_dash/main.py | 4 +++- eit_dash/pages/load.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/eit_dash/callbacks/load_callbacks.py b/eit_dash/callbacks/load_callbacks.py index 7547ee2..53b84d6 100644 --- a/eit_dash/callbacks/load_callbacks.py +++ b/eit_dash/callbacks/load_callbacks.py @@ -254,9 +254,7 @@ def list_cwd_files(cwd): full_path = Path(cwd) / filepath is_dir = Path(full_path).is_dir() - extension = ( - filepath.suffix if not filepath.name.startswith(".") else filepath.name - ) + extension = filepath.suffix if not filepath.name.startswith(".") else filepath.name if is_dir or extension in [".bin", ".txt", ".zri"]: link = html.A( diff --git a/eit_dash/main.py b/eit_dash/main.py index ef03fe6..424308d 100644 --- a/eit_dash/main.py +++ b/eit_dash/main.py @@ -33,7 +33,9 @@ dbc.Col( html.H2( dbc.NavLink( - "ANALYZE", href="/analyze", style=styles.PAGES_LINK, + "ANALYZE", + href="/analyze", + style=styles.PAGES_LINK, ), ), ), diff --git a/eit_dash/pages/load.py b/eit_dash/pages/load.py index e6f1a8e..f9dcf91 100644 --- a/eit_dash/pages/load.py +++ b/eit_dash/pages/load.py @@ -22,10 +22,7 @@ [ dbc.Select( id=ids.INPUT_TYPE_SELECTOR, - options=[ - {"label": filetype.name, "value": filetype.value} - for filetype in InputFiletypes - ], + options=[{"label": filetype.name, "value": filetype.value} for filetype in InputFiletypes], value=str(InputFiletypes.Sentec.value), ), html.P(), @@ -44,7 +41,8 @@ html.H5("Signal selections", style=styles.SECTION_TITLE), dbc.Row( dcc.Checklist( - id=ids.CHECKBOX_SIGNALS, inputStyle=styles.CHECKBOX_INPUT, + id=ids.CHECKBOX_SIGNALS, + inputStyle=styles.CHECKBOX_INPUT, ), ), html.H5("Pre selection", style=styles.SECTION_TITLE), From 96eae5b379d82969a7262d54dc42458a495aad88 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 14:10:06 +0200 Subject: [PATCH 20/26] fixing filter --- eit_dash/callbacks/preprocessing_callbacks.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 2135494..8e4c60e 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -65,7 +65,10 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] + options = [ + {"label": f'{data["Name"]}', "value": str(i)} + for i, data in enumerate(loaded_data) + ] return row, options @@ -76,7 +79,10 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] + data += [ + {"Name": name, "Data type": channel} + for channel in dataset.continuous_data + ] if dataset.eit_data: data.append( { @@ -220,7 +226,10 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] + options = [ + {"label": sequence.label, "value": index} + for index, sequence in enumerate(signals) + ] body = [ html.H6("Select one dataset"), @@ -458,7 +467,11 @@ def remove_period(n_clicks, container, figure): # remove from the figure (if the figure exists) try: - figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != input_id] + figure["data"] = [ + trace + for trace in figure["data"] + if "meta" not in trace or trace["meta"]["uid"] != input_id + ] except TypeError: contextlib.suppress(Exception) @@ -564,11 +577,16 @@ def enable_apply_button( if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) - or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] - and co_low > 0 + int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 + ) + or ( + int(filter_selected) + in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + and co_low and co_low > 0 + and co_high + and co_high > 0 ) ) and order: return False From 8f8fe4181901dab7b573d9590eacd1c5bcff7348 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 14:40:37 +0200 Subject: [PATCH 21/26] fixing filtered label --- eit_dash/callbacks/analyze_callbacks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 09f4afe..42bcfe5 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -46,7 +46,11 @@ def page_setup(_, summary): for period in data_object.get_all_stable_periods(): if not filter_params: - filter_params = period.get_data().continuous_data.data["global_impedance_filtered"].parameters + filter_params = ( + period.get_data() + .continuous_data.data[FILTERED_EIT_LABEL] + .parameters + ) summary += [ create_selected_period_card( From 242c2422119dda5c473270424c9198362a6889a8 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 14:58:14 +0200 Subject: [PATCH 22/26] updating the manual --- docs/images/apply_eeli.png | Bin 0 -> 58282 bytes docs/user_manual.md | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 docs/images/apply_eeli.png diff --git a/docs/images/apply_eeli.png b/docs/images/apply_eeli.png new file mode 100644 index 0000000000000000000000000000000000000000..ea130dce22e7af500eda5397b6e21d08dcea0bde GIT binary patch literal 58282 zcmeFZXIN8P+b$YK!9vkR?>0n4MSAEqL=b{Xlde)jD2C7xP*6ZAE=5EjfYgZe-V%yP z5FxYxfzXsdkP?Cf5+Rf`qrUH2>-*kspFd~sALrWhx|ErjImZ}JyPq2?*f(y0zEtn4T_y3WThfWUEH5{BtC1`$qS`kDJ^!Me8EDN4(LCgky z;N|f=SK9$eie{5DKxd-}HOJvC*<8U@tr>dLfypYLcC05Zp{nky za*$HgEN{;g_cwAN`NW!B5?+YWJqOo1F=@rv#HKYw9+@JGB=Xk|0o_u#oh_ z+$m|CRMbX9zO?4|hffR*f-VGHpuLRY;7%cY29F0hBkuU5+sTcOEH0%CR@A~*vLR6_ zNt?B!_II>Dl>OoH2h0O`$;A0g6@1?J0<9}sPTZ4x*&J?9L6BBS*He?!+6}9AK8lsV zPjG`kpYI?+ub+-YG5?zd2BSqZReJ=rh=8ISbM= z3Ae9NjZiF7P*H)oA=ACR5#z=Uyd3V_qye`kmy9tRtuZxYOI%1*NgO^RNn&0CKmU+i z3ya#$Wdk+4wSae>ISgN&($y2kYbpj{u5sUhUZ|ZcC~=ioBnns_Bq_P5#6#oNPd~t1 z(teY*~+2*-}Ww`C?f`kKietUsHr? zMyD8Ha0V<93k?_a{Y+nTXh^|lD2#@_h<@9SN!X5#Vb}EV*2+i7CBhE`x*H?Lp>jS4 zPH-x%@(#^5THQ5@!`o^&OGbYSG1{(nW@d)gp8_P$1xViMXm`&y_CVEiK3xfJj8E@F zZ_j2%zhWxhUOWx4!<$k7OJ6P_f^oC=jc^@o8mUQpMy>F};!kZy*}oy!8U?BDOBk3QZS(%<6%NmW=+}iqVBF085tB77++I1Z8lJd` zLVzLXC4CKMQa+j#4V@yc4%;GT2l+6!UQ{`$LU={h)!G%a5Qe7AKktmQChY>_bWdA! zjg4J?DY)pmLs+n~PGgqZnRq%lg0a5lKXj7lGnz~zf0Yt^6-MavLY^$ls{gt%ZXsKk z2v2G7|1cMUFxy2q^=v-~WX4kVhjp8=rvx2}t%e`k#Jz%hXNx|Oxm%zJp}`ir40XLv z9~d$=vpP;vx;ugXh;JaHo`}Ec(0T7fH9{#HAMBPHEUv1OiN!yzi|)KKv)QnQE-}9? zN>Xa_!*`_^bzlBt$Y(cb*dLfkMYH?DW|wQJ65~X@w#9KM@?){$Ec=47##x8m<_<`5 z@mIjgl5fOUM4I_(6Ntjna`Q@x7}%2fP;<_v&&qa&L^>jCOL`X51Q>ROkhItL*}A4D zZ+tPpguH;IOq~pC@0%$i9|Z_2YS%k~+|H&kw)~wTS@ocv=srvI$C0oWbfpk+)Mqq(7~;ousT`H_E_TggYMWrg6|&w0thQ0B7c8PnlRTPS%k6yHh(181YsaXxn`DN zJpfia*e2nZtY|ymBAe10u5!y3piYEEU7CWgv5l$28oN`Y`s7DGo-=I}uK9x1{xn=T z4A))ijO|$8a+xI6PXe-=0OJJtzGp)hEXWcd&!~h1z_sCK#K=iQ@yD>eXp)%;IT$e@Ps?_Sxayl zJmm}jsW+TfF7NBl-b0cFj-E9&7!5W;*bIa=W$m}(ASt!T@_1T#R4ID(5m0A^T8K*6 zjyj8%&aN3_0PjA7)nZ1q6}KKchSUCZj0?2@f&OF%tn4OyMR5fX970WH#wN}UP!npB zar$+`u#WC8n+JtZbz;RVFPrAe5uCaZ|D4lFJ_8mKYob1HTGM#rB~v_Nd9{P;^o38$ zVcQ>_({>a&_bxxbfJp`*O&JRf9WSwQtaIAlxXscO41o3SH^?aNv+phyV-Jy(#BH$r z6Lg*0x{vH%BjOTh(+@7r(CUUde)^SyXDa!Z?um^APbsFDc)k~F(Qp?aO(*l}JgY1L zfI|R4cbMwg_xGTFfPDA?sN#B>kWp5;QYX?1m|}*VqTv380<;=FyF8*Dpu+4_YEM-t z7TOmsq-}R@Bov1?S35ExhJ`lH+mxUBpxpq-V;IoW<#T7WPTVz;20ip;1Nd|JySJKo zrcE*S4oT@^U+GA`E=Q#?nL~ud7&93c_SdaoN*YqO*zU(;7JS*v+zC(=l3v3hg$-Ug zP5Lti55$N_Q)}CNn@<~RfIzRB0Tx^!(*(I)L+3_%VCm~)>RY#v{s|p}w?Bfb@G)fC z#o|VXhj4H2+Vtfdms$d8e$F^W%P-4EoA8GaD#LElJ!WV&nVU%Z{N!NiDz-z`@TFCB zV8(=n>=|by@)U8~m_C_=a?uG4jgw>H&TUneK54Xy~xA@!*z3^F#?NUfYMc^t|HjC^LzAA zKOqLg;@WvKO)-)uZ}Un}(-pc~tOKj7u1EuX1u6{vMG!BCTrS^5H&n8!(A?aBtkmr9obXk|5fL zu30#@2UM!f!m&ekq1X?wiU8xuG@CdxQPlnhIc>0-lUL~Sw;3nSG)Psr#YGQofO{gS zV7c3Infk6KA}DEPIrXg#XaDN@FG2w0T4NJx{P4$K%m`3j1O+?UK@VG4LvsQ{PetTU zuV>lB0q9-#sblO*pS6R_qd8NpiFI<`X^$(UY25=u7g9UGl$~j^l(a2z%v{j}{tAHm zi`Wk9258Htl~)7nKW*5EWy5^veSL21CV+@ndq-$Se6wzaPvL)j9!yp&m7&i885&%w|@({bxCx8g% zC0+v2^*kW$pVb|_U#bIJB9$S}#|bvx-x7$7wVa5Q3a_>cK)^Hw2>MQP_4IIwj5x$uh$)Ax1vSxws`yAe(5u0pLNYfq*EtcLYE2fyv=P+s=o8A3Oi1n>;0HPo61*F-0`e`iw zx<46pS*x}{t!D)iq?N5UlegBX(*iD%Ta^QW-1q)m9=nM2_u+K%JSkm-vs(7K^&xip z!ojVBAdvO0SYA-!%e}D)KjxHyl#!!d>w7+KuAx2z#>vK9d$PS3^sorTT5m@}7fxME2Zm8EftkYOf7u4Q?yUU?f5=cxiCsG;Hvw#(CBFXa&q5aC7wO^-`5z;f5 zNen|-bx9MYwlu>6>1j4|qV}9HD@pW5v~UI2M;4G;8j7LaqkaLjtXb{jpSINBuIIW) z4v6{Dm2qhu2JFEp2kqP_(&ey#MQL1`MH*g=Zf)#IU!aRG`=l*wI5QmZZ1}>dlmzTt zx;T!;r0|LdK^wuX&*ePq1S$dd`^^PX;?=&Yg=Mt-%uOeh8On;h+LFT~=o9|hq%sT+ zqk4*i`gR?r^KF^@Dy`wvX~1lNeEs&>7*UinAsP4RYZ=049?t51Ra<>32h>SEpc~%^$ zJsaAiT~ya+s{>mjAmdq2Pxs!R%*09QEcvNv2Fzu4R0n%$p#pE-d2Q3iu#1D&?>r;F zo1%uF5&?l&x;a?*0jw3Ei?~f69zhr%KiETAU9D~YXp-uoy5}S*53x~w*JoHweU1u@o~Q)Q{o1UGM3V=jBy3Qbeg4&(VvG zrWKp z!>%reYE>LgPGeb#_e3$d<;by0=KFVH=>eAzz8@YJky9$Rm@K$N?O~e0WN>egY$s3F zj%t6!;y*<9w%|q@#Avzsk%TKI^9Qg{MjHpmY=|3Tj6eQxI)n$gnO>g+0+sp#$lQF| z(wO99j~F$|-}hvA)F#Ac>Bs9J(rQ9SPiKjYa%z(AeMAe}c>K|^ih?@%B&7;Q3b}UM z#n7kUXq?+&0=(XrlUGMsEe-m*3n%kEf9Olf^(_si1h>~Y|Kbovck{E?^*TYD($8qRTJzN6-JD6YiAz8D=D4o|`O zv$)o>aqc502-L~-)3i^=nkbcqd}v9^d%gvPN5Nj0#=0fIzssPi_~~RCa&uqrB`lO` zoHFh|K49ZN*iBxA3Pq)}(`vXiC=1^dwGs>ONI2hK zPU+kd<$nrZN-_Ko8II{9NJ`Fu$^N7pE1*dzxp7I3mUfA3l;>$&Q_Sw2+GDNmo56bJh_l!PG~L zcJrppCcXcdY=D&^%mPt!BEbgVRME6BVp9<8kG}6@pXIoY5tN*_*dE>Kz+H#bSJvS) z-c1o16ZfrXhNwRypG9l#EF)4D1d>@9ZR#ICAyPLkVI|>1&JL@3L8QzNhqTGx%xO8j zAWjtXCqi*D8s|LDjhJ_B^|3<|T0C4L`A5y~B9|sd z(C@rx1Jf7QTRORUx|PSkI6n1jiDb{LKPDEOkVD^6U~zc&7twz8c3>lF^WL!zz#23i zyzRW<_d_nnnKVZvFy)l!%rq5?_(Q;?LO&-3;x_Ns@y`UrLx}pSIDvz2&Wq}=6wD~= zqzRSw-Z2%H_BxxrU-4$*l`3@mQmXP9UM@w*i?7KAuL{!yCTX zXVf{$e+8XR0LysthW3vI>S32PRq8l4GQ@4|Tfme?QgI#A^U1sNuQvJtAkeVe&&c5l zcCJk$Wi)g%f!uO~XKZjRU@o3q8$piI6ubW|7l(v&elSd+j6AJs|3xD;I=wgUXpE!RjLensJg_?&&DFYaiPS{Sf{8? zel?RH+X1-t)t}MCboHKK8*-?3hV9fikZ70Y{tSJD-H?^+JsmiyhR-o?CW1#Em|^T} z`KwZ<4&mpVIokX3A&=&Ug9~^mbAx6U)=uz!n56ahL9##wk!iwlwKcK)sim) zagE`N==?wM(x@whlwQC$*VU;nJ>G?t$(I&a7vu*(sA&IB291E{pp6C|85r6I8G=%< zi|3%$=czH5Bg_S!g!C(>2<}~202GWBYSmZpc-iq?;pJ-yO3t#gO_1Rk8?d?GpDEkN zshejfA%q!Zg=EIW_xCw0P4z;2ZXkw72>vEBbf7X({uw6WhMbZKXNre~oFuq@cusXI zo`7GXmCtH>uhE8)M-GwL83wc6oLvx=RW)PxK@upgaNuUvy1igLL>}H54(%lD0*`e zs2`jIqE1k$=U9Mq&F$}3JUZ3@J#?9G0+8Vh2Wthu@$mo5UdaFZT$ZFt(&xN&94VEY z!~;tS^F4KG-_gg1E`7?8FUNisQEVl*D?=Dms8={j;_KvzpWp4vG#62XEf&fvx!c*0 za_l#of@U_29v%Amq_>i=28@#n*aaZv+&KQ?Brq26P^7+tYaYeJjvR|uB2JI(`#lai zTAg)`_sNl84_g_ETyG^ic7p>|LBH1V4pOSt0%omf-_gp*a2%kDKR;wD^SMLLLw=1@ zlz9gc4*&IlE7#ss-dNsWA7H13k#~^4$G+S`-a`Tg61^#?jLR_ImYzJnKEI>~jC=R@ zBV0FKT(>tzyBn(pM1)WcsGY&%p|#~Z{T&R6q)282UGc;4(Z@G#{aR5ksKan`-QIB9 z60&;EWhVs+SkyRy$;>H~Pua(sCuYBjw2<`ULn23#4<|o)DPsvF%)n%ozlm^DmOm;0 z4IoWiB!PLZCGUoZ{;$6V!qbBli^kBk#agQO>? zj7|un>SX-B`nA955|V1a)^=6vW71LX@Xt^{-;PGpUHYYaal?^@4xX zYhWOp_H-&guo(W#=<3u1#j9k9cJ_4%{LoW!&uAi688eX-m5k1p_bz3;AnI(5JY_^3y7X?#et#+ti%~_HUEn}wP%?al&Ognca{&-i z*`8lYRFpY13N}lV1n7A8n~tFy3vNjByS0E;Q8a!t3HEMH0rI-!3gqymeI5^fUj%fs zSLDDi=CIUp=*B-)xdRE`m}%V<1^v?QS4r6ZQnh47NK+s>jQ0q@58#=V{@Wrd(g-ZH zJ<*6ZaC}pA*RPwWuRFNL<=#aoxFA0&&98o5PyN>yt_pr9iIsKmIQ&*H(O&Zfpn$&y zdSmI}5|??GHYFl`IrHlH9==$BP~b~fbCKh)!md?x(Z!+PwGvj!%R1if``5|8ul%1p zxiAFY8Zq8RuW$1(lo(@-3kU{A=hve5`SuH0Iyl88-=&d6PG z+7Hrm|KOh#TyTvoEGx%$iTuPQK?3+I@@F|{zAV|QG!S6+MsBAv0L&oJ1uc=wybp%a zUBc38PEW*s-t|@@F7xS6gi3fh_6Z6kb3lp`iS7LjQu&tvPXQd9QDv)SU>BElUV8QX z>j^Xv2eFQ!ec1j~L6+A59F0iJ?I0Sk3&{F#`?%0`FFTf-01nt5hdJEAU!1-ZcjpWy zaO4MeE;1n%*q!}C4p%<)G>oLrR5k|BgL{)k81FYGVMZg6qAr!8YD1s8s1_JLdxiH7{q3CkBn-OcvS|817H2__UV2zax zPg!p6CQHHi&RQKbz;<9Fp&u8f$=D?+);(t=8RI)!JOC z2K$m>*e`~od)RTw_f$i|(jKimHMBE!14`k*8hL{mOCEMX%SUldn=|&%;+=UyeGzUo z*n`=6nHwe%I{m_P;oi^Xx&q5Jb}{9nB*&1u-ee75^Sy$V;a5IT!|W^RH`V$nuV!ay z6=N7QWe*lgE3XD1O{VwR&2rb5C`*#5L$M8SwqKg1w-IsAFX3vg_D+y_wwDv}`GG20 z#*uDko53D-Lu&0EpM=uW%i>I?W)ptuN9o*~6jYLUeO{hEYbnf6;6%fick+-q+T&5w zWDh2ZzFEiklCE(c=;iTdyolwCQC%9Pw83p3828*?8n^bNtNhDwh`BAZp8R$*-*IlMYXC z%$fI^H`3#d0QdCmklc>1=>U$0M+o(3>;d*7?r0Ka$xdyo95WJzJ#`nU`5zbg-m%9? z8p8GBl@cTk`T?BypBZqZBO1RXn|hghY+_8)<<%yEnNbGY`1~Ih8fB&saworMqU!F( zpR3WbsaoR;nhmKj@u#{j<=<5%oEHy94%<|It^t93?;zQyL{Q0LHfp4S?F6oDm3ZbH<3Qj$-KZKoVbQUQLQHuOYLQ?$qTVB=cONdeD8p{wu3dB0?clz=gWwSEct^f#N#n^2BUiz^$laxGg!sd zB&b+YP?EV&@I_OR-TlKNGb5ylQa9CTqoxXa)dJ>nyh(B_aoNdF)Im=McON$)of!8S zB|lXkw9&BA0rc@1urSg5ov17s1XVqBav?!-ZsK87a@vLbo(oC$K{|pLp&UDYn@IR4 zu5Fj@8O-xfKMbRFvR$sBRW&A5mB0FqFp-$N4cIAR)cV5y9xZbG&o%Rh)^d60q}4tB`_8ZWjK~93%!GAr&XvMV_lFV*!Zg(8ekOu@zFTQJEGh%FY*std42Tj%qNmr`; zosL#vX|Ar;N9cWV7w(>y%VXORCjF@b`CjJnN*cacS%jee3tY>wMQ= zmA%FSv|qp#c1i(azrG1xfpR_7(JFFK|KY``I+-onJ6iqp#Wj(rVX%=a?pA@xN$iQu zz^{!!BbTjOk{8l>vh!&IO_zKuPw_fsy+#%F_^QDkg1VXGX%tmXJIob%GNX-8Pm# zp6@tAK?=QdhzE-9s?=t1=3vaf&HOK?bPFFS=^2NY$_T0r?3Ap+&cRe2kiGN&nN3KV z6)ES~VH7%B8DhV=Vpl^c&;Os9TF9swTrI8*@}ezO5UD!Mz*t@@%wMjm)WCCOVlnIs zolIVuaT`I5JjHG6*^DgC+0BlO+xUF^?S14vxfS1KB;t;8r2mEbEC;8Y7U~RpUA*yO zf;@I^WA&-A&SJH#N@B5iQORGEq=wIZ*?QaYMWt%NerMlmlF7au84&aC9CBeBwDZC9Ej?Gy-{Oa3Ce(cgpz~8 z8x>6gZkC3PbDnKJx%aoa7fF|x_BU*P#^9-mjOnWX#*XXHfkgT0MRYXY(YiNOy;rqD z!N2j_kNm)Wu^(dRhPCAP%I2Yzk5rJQRCFsm%)6d=A&L9&pMztLLHdGI(+gmtp>oc4 z7WS737A=8O`9T%bF#1;Y>@%kK%oykf`|nlYyr6)}c?Q2*-%PS1`4Hy-2nza3O;z_g z`|U0glIj8LK4$w-x6eT1M-6E<_Oj=)4&od&%S4?#aFYo0RhxhQ;9h*s*>LaTuSROWzz# zHMw+LDjb$}S|Jh75X{g~g7Eeej)ZRy1(G*p)wO@D|K%G8_(XFp8tX$sCNsKf{+j~- z72hT{GA_W7e7@x#D!$a>V969{L+ob&55xXaw7>B1bca~S3GvVY0|yFkxrHxrRVWVBzrcjTLWTH|Br))(Bg-{;!H9>sgjbTGPPQjDi3k> z{ma+>6~mj>&s77rHhHvx0jwy3*Dt~T=TRx5Mev}7QS(^5#>6;ekhz&e@2<4HIs+hE z(_hH;H%{=Gz0?mHh00>*Vk@jkK5c{ll0Ts&>N_GAc*VHWa}e75%j%l~l;0JyY&F2+5Og{U0LQ8v{L z!GmSY&*VR!j*LwDd`!fpdnR2U3w=+);qYlO!l=#?z+^3F3x!b6ZO24VJXD)5aQ>H% z;tcoL4}bh~Av-JhP%JKV+h`!fbE$6e<9Y~hRmM|RHgI1%Li$@potOOvCp&0Z&)t&5 zx2^bm2MV)y^~u|5?in7$c)t-r@utR*cNoUEyTGcjwgi{jj=7O$gs~8C$JTfj;QR>htLY z?*h@~uOQLUCRes4cOGBF<_D`2LWF?4tiLV8-KcwA(o$Y6W7}Qy2@rXqn-76_Pb&4bf6EiulXLH3R;1N*MzP_f=rM8F#?lrEGSc}^ zvmm6FJeZMH%Q(!Rh-~!9QNOLYaUul8o#E!+ilda}^@PC3s@{4N;)m@2oj>oaIVyTbk5RylL5OB4TrxH77l3^VkZ{0gN_K>qzpvxAFrA%r_dF-g|fN`X) zH|piF&ps=#ICgoG6&kfS2G+vs%R8SdP|$lwm5F~>t21F{82wSja2l(zU{7@++zn?e zy5z;b$!6|&YD{no|2)p5aceWm!1W`V=hmF9OC?qYd5b}501SJeuQe{=m_;W1K*3tv z(XSqQ%&UL#Vg|Jpo4U^Vs=lXoy@zN{ak-aN!$-|{lRGz?gIVfUuJFiuWyuD*Em!^E zBh;NW_JBVS?!^#F0Le8?nqz}0hg;+Dy0(Edg2c${6FN*bAymAlQtA zGWD$3a%sU7H^Gv>E38h^%Qme zV)#E2BUKQ}7-zb~by!ahdG5@sMhd?s)f5SxQ`E9+c{0G=-gur)m#8n8NeO zJ>agaeOx0>5&ad{{EaPAvbzs^>CA{zUNtNH2NHW4EF0JnQ)uy6hZn&LXis#XDM_TSi(EdR7-^h2$My+&6ldgGdu~<3^cfP0DHvV)_+ap~FEulS zm5~8etjV!O&vWWiZ^?gl`uUeAE;g2{O*aD|e>w&GvtqUFvN&%yBeWbklo+?7?OjQQ z?;J07r~6o^SXZkuCBY*qeKPj@!t5RFdy2_?7?d^90?+)~47euB^@qy8;^;a5B@bG( zX^KW7<#H#YefJ)n_`Wpsz?L@i)p;g7hL{mH_PuC5h+!F%cw%y`$dcr< zJ`22efx;AEB>0<`>4f>1gzehUm}%?WC9O&?pcKa{(WQ#UCiGxcA;IS~n$Wmig7nKZ~Jqk@4~BlKpL9D~(kboY)>80?WwBOw^FVhiYwnpcwcDeVnphxM$tc zcEIg$g?A`}W~_nHipsJ8G^X*vRw3NV=W~YR`kj|GP3Bas8qCSHvYIeOnSb!mU5QkA zbNCyu33Ot^)-g;+vi(KZU0%d+V8nTGQ|pHN_s6-qjMa`0OQvN0aM{BYhzI9}I-i&F*q*gf z)6#3uQTc-cnut%6{8X}-V7?(moL%kIOy&e zF<=x(r72IhtJ?er^gju**dQ^ zR9v-HCrgDl+MBcgmuv$u_iy7-0bJN7~6%x9EV>Br-*;-=6*NJ2?76z=OFX; zN4~p1apJ|@WM=Yr&;zN8&;Ejg&%-wG#epSguDi}<& z=NE`Kg@+>Ef01!9n3RqB3`@mZy3}Cj&^$FKmpr`e-Gh(Zfh~ZrztHJwA5uU44xvq}zn=x_=#bDUW+)uv+v% zkMLvgz20i97M>5@WjLe6PM3HG%+>vT~QJ~S-5HLOe$un~*yFN<-mOp@rqQ?ONKd}E0HkT|CIe_08*|moJ zzK>%P!G6?zk_*jrI5=yh5InZoBj$?yK$>Nz?Lz3Z2kx|#De6eW3kgj})uU=K?83=) z4kU&=Er=eH6N^Wd&0P~lO*4BY5W9q=Z*%-YJ*z96j;yydUCjZ=rdy?U5)UqxK z#XX$S#P~jX7T~3pFWwclH56rDB{47xfyO211{F?C-UtbB3HGRdvqWDKi82VuCh<%4 z6ArTTP3~J8>FK#z{E9kVC9AJ{?gMRXi>=8v&h*;!yKKocnNu(88P!>xrj)c)R32Rp)xl?IF)OARYFgx2 zx=R37W@j707&|ae2oPnx>&>d&ZOonXnqB1S;+gP5a(=8bWz*jhEBU!UH`J>hRHLWxWJlRMm*7j-rp%kztrcza`bHM0c;+ zs_%GhQjcwZKLu0|6Xkxbq1Z=Fb#?9)-Hz>8=h@%gU>s+%BXRN>2h*0BsPgFM zz7lG*z9bIAqmhTHwj!Nyp0rq2blGsvdawOQeY_=JfuZee*WNTs_uUM24rTCK1{1(f z$Cw!nv-c8mVEf`knu)*m$yWP2M_X#x-+yoPf&hjPnETnJ|AIyPgL~d~m~C@G+&+%} zD)o(FzZ)nSS7#MIr|(3v&tj_8<+n8SduyP!X7Msla()=!<31VVU5fUTvV3uXS$}Oi z$+1rhe=Ovck}Xya~h3;O^4U3q77DuS+9AmJ_A}M_)e02MX9; zH>_tw$r7rF3|h)@B7>y+%prGUr`oGzvvFd(dk~XRlScohq_8#URoH!d-oMFp+9&(; z;EsbUUS9l$SNAR$=Aw0Ip0o4Q60@cP{^J~_e!p$~TLR^abqo&6+-P&3ysdLvlNM6J zu9sWAX2sT1>!GhA1(|8+Ss3zGxU6zTudz{FY(h);6YNMaRZvDD^WyvXswaU_SEerI zABu{v**BDbklRXG9MYaDfJ=PVStx6a19%{y#@%zAR7A-SmXp$r1}vrG%fLn9M1t5v zx`Xn9(-W#*-?0*-xWe$<%5N!-{i^j+d-7)0svo4A?u;sp6KOeokTsjid$}E<_AwCq z@#ykxLOFNC)pUV6;Wu#sS6(~|=`fsjwkr_7=5>H%I<99s@F`}G$9*F6g)46%uFVSBM=6DS7m&k-&@_P2@{&&wB}77Os))ICe!v z*R!uBZw2Fn<}V0{sfaX>>oT-bx}tU)2#E?WMCXE61jqdOTC(9ZA1kPka@d;U~mz7eI;o+8LO}o?^`PoUuAbB&}f zznR~uttVv>oOnPdjGn9!2Tq}R%KtKNGltA&S)7*7r)9JjW@8uD zmvw}$^Re>Vz-QwSKe}c?{?ZSu=iP6*k|(d--D6niZI>!CUJY|PJTf@_sptvtM%BM| zK_b*dr?KVGJD(L-=i_Wtm8Jgq<9hpW_Kh1e9oGcZ-0sX(emzy3Sp_XRT`QjCN4I zo2yS`y_g0&M@+^n!U9Or2d9buB(8K| zjOkGS*)Q|(XZ=3jDxSEZvi6$MNW;D5j}fU@XFM_TWQO(3`2tSYlb6@Q@X?oxGS%$P z0wGq8r+W+R3EWw$juWx@&s+mM5arMp^c=gHQ4B!gKuJP9%xd6aawr^8nL8fERfwWf=yK^>nB5dQ4 z(7yMv=1%=St>JjmluUHY)n-1e8pJ6gr7HiX&@Sto+BoQ}E=AqD-z`s}_#bjB9l!_3 z=H2hCnP-<*xv;M$|Hq?hsGC)-7Ajy%`uYV)E0vKU&+^@zuVt&YT3b1qJ~#B1`G-<7 z$>LN`rqMd^zAVxQnL~ZPU^sKknBc)BQ8bxT62rX&z=G~a-J!9Ut4g-*)-U#@PP}5F z0W{fM-R^1^AXH&K-QHE-o2?E_3nmkNc9U7K>rW1Ag_u2V1HPOgCd26m8Xg<0HR-0> z@_b*!n=3+GoP05L6%x!YlfsXTF@VTD=70n+upB%q?V@2zxxs z!(`|d93h%sQ{0z}tTf%RK7s2B?s%B%*k{ooS@o_OK*A7H8q4P@5?uIz_9bHJ(U9mm0JMYomT5=*mMcVp= zO6+O?#h}0JWwK4O=TFis*VGip-aq~ZL^r;+88&0WWxBkZl&_DBcA4a`jMbTd%rirp#7keJ4dD&V~&R(lM~P4V}2a2+r(S-+HjBbrgms+SAj9|aVy9C=nY>t>2o@_blKPBMIGG9M6HXGCRmU)Bq)2A;Fh-hI0F zi-h9RT>4H&!vnWWO&fD+njD6}xe~W12Z47)em5QsoNj~m-Sldj!3-Ft-jsMn#OfY6 zlXR(L=qnxRejge+8(*xR&aMj|MJgpi+AS+v3?A}`$I8CUCKdYB>bgW;<#LBxh%TlR zlK1Ey1tQit@EkLEJ4__daQ?X*2*7HQ(45 zZ5CqfbJI**HrdjyQXysLq|B|iB{8p_Q#%)$isL5cnYx03Rg--LPtP8c`3xZVxXf$R zhbP>cJ}o7Czh95eZr-Rg65<*OX_Ez{I(NGx_Kie3pft}4_HqX<^Ugnqc*Z4|iP=`x zldOQA=YP5Jkep*>;CEWOuWv5Xqc%2*C>Od9$iJ!lmR-#w&+$6Oab0v06IOPy;Ot61 z`VUO?r^FSPscN-kc*Vu{Gv7bm#(E%bolA(XfVxXA9FXC}`IkMaok_5Cs?cS3`kK`& zV0#nJUE*P2zV|zq?FpmF>l@m>)LcjB@vi*5+CYkTIa3$E=Hs=%%u~v9WOK$x;!4lFj1npy zb>3;vhOd`4olOvyOI@*~>Ga}Ljoq2;#a^y<(^kMww6W-|uP9KPX^sGXJ!H=ADzIsx zg1c(+3B7e%rf5q!@F3mMzVG?{2CY8xa$f*{Nibf7ZnGyei@%Q zMKu9{cZ*_ehRx*RHKa>ya7as^^^<7eq3pCB3yj{X_h{E7&VD{?wff=M9f3r`&L03k zc?CF(>d-G-c%cSVq<>953)KwegCG4e=C`gTh;+@2)DX~JU zwHFoVCdtRgD^=F4$Er`ns84t^FGqM2FO5{niz->?s-*oSzwLnOaO=?Nm+Rk&d5(eR z&eN+HzM6uY0fk02BK6B`LXNz&a=4T^~%w9f(&DOhaVy)4_?ouxq2Y?cV=G)ptiVy)A9);fRVN7C=D-rAdMmsiFc$F-i+13B7j+1Ox;r z25cxzkrKKfC4taELO`k%fzUf76h#73BGM8ignU2FJ@>ok{g1T*YcYGz%=64MvnSu` zfp9Jf)deElJTl;-hsQN6E*{6r4y`&_Xd?^rH|#JsIw|1Vpili1w>A1>@0!B>>5-OPkVEeSCi#dA-JLVZei`pkJ zB?&I7Y0;%uaE{_kjT$$l_WvgCr!}YYBk*0zx%w48NZa%ZVC)M;dpD93L*Fumf2wC? zt22L_!|f^`zrU<{%tZ0BmDmWUi6+aK`$4Q zQ{p~6{guzTo9SSF;#CfR6xVWyz%?MumHh{CrjsINg>K3FEMw|?>KO$)&GFjGJ)@Do z%23dPj88wEAh)jyI))#6sqb9Hyg#VeQ2hr_+6_TsuQqnkKSRVP81+0vpuMI9G|IlA zKg5XEzg^{qNxh)pC~vgS=+vu36iA0R$yDCG8G=_vqixlZSCHndpZ9Dr_rDXmL5c}Q zO1>Wd+&Bm5eExJVz32= zX9M73N+ptB$qCa}ED*+Jz2(b{ecP9FCC~UFJ-E5eIrMfMOm9qa@tGZ}O$*P^bgzJ2 zbp5Rr4VWMQJ%F8}ny4K?S>QdZa!vG#BR^3oh$VdX>=N_w7v8(ktCPIYo3S`rI{=)_ z2TG^pm~{gjC%WrCa3yJC?_2>mcw18P^}8l0mBIr8Q7i>#yb@ z)X~|p>7Ri_th;`&WgV@NX;O{3zS%&#Mx3tp?OC_R6irKAG@UQ9^(gyVze({3oR6zs zvk3|OxOPiD$ebA}!+gjL4c*P6Y+mr2D&L$6gfl~D!n~NFtBkyri&1Y9+LB8>`ZvEG zd-<;8%}ur@%_rH4Y_sTETEMmzNYZ1w@+kMxFVcb&ZFI%8+63AZX(Zv9~KqZvyRRWDnT|Am038=bp^p`8n>Q$MC7M z=D)7@K&sS<&t#(A20@tpN3y^uHwXTjJ_@6zpbe}p6Y}e(OA0D!1TITCT#Vbh6q`Rc z@8tZ#I8t`1ROx9Ix-(G#>N>Mq`wz(3fo);f0#{psn! z=jU`4c@EF~4LVWd`IWN?e3}IqYLA(ESI+1k(mZX2m(q*|5_Q*Ry0tsBcV_x-R^>gM z(x|X6+5aV@ldQe7sjwWMhCBck@emGjHPhS3FN2uU$3Z@oKSpMnJjtY+WFUM!nY=OnNw-^=v4%X z!yRLfqsBo-1Z9*k4EL+V`k^)l55)y2X&^w3HGlRfjQqLf@%&|-^RgDjB7Lc~718C#t)n}1q%Bpq9z3=C`xMN1t71ovUj?B~$tw6( z5BwZ}4fq(Hv^?+DndTIK(UpgG^#&$`(+FXoURsHGiquCWv8h5|7Ju>)uCue5Q5dSH;kl@aI3vopa zcKwX}fSYxrP_v3uZrO;d^BE3QUp5$jWJaj9d-74Mf(zRcp^JT2Te9O7_XKJ8fJ(gZ2KB5n~GxZ9#^z88TrJOzU4f7 zxOC}^X=K&radV1ZRP+!kr&pcM1j24gclai#$wTh!Mzi$sfY?#akmdwz4 z{70qAy>jQ{#Z@eB6&?kBpFewWo!pT!y1;odEro-H+zHef@qeB%oC%BgJjiKK?WNbd zb()$`K+AtVG>gj?{zcEbepMLtSRx=7#4iHI zG0`;#rI{JUSaWqavPCY;!`D9sU{>7~mmY=to@yhN#!u|Uv!@@y`&<0q)rZJpDQn1B zI7FeLkuF8^C=xO&+Wfd(cU1-4hsGOQ>CF00zbw|4m>jtUaoY+W-29$7>Qjw-D=l+T z|MF`pAS|iR;BpFQf!GFZ;J)?LaGT5BOq_mYV$&3+HMc9<1yU}EzMCu_{e{fFSHyq) z;n#2dk4tHbi~z;+}!xWDvt6O)!{Ikgbqj)K~-E$5fFyr7pY z59_gjGwl-5KdS&4-NHd{I=|L<-c04-^_ksLZ(p9Km*F zinzB<`_ zGv;xMk%>NJLOH)?wT{|<(Z5ITn-hGuxcDO zg%jJN9j@4lj{yMgDzVp>Sp4{L-;*F#87SsPpNJ`Iq~R%Cq_;rw8E%|^627JCm7XyD z3M`Q`?&Du?(ax##CSnx)=1C9_80l2&Xu{hj)O@_;hGhHii0J=T!fnDi=%pq2rf^%* z^W5Dt-c4O#d-u13Yr}9Dtw=iWeKoA67k4Jy14MNSm!A2B`+mzhQ;QZPcK+8(JOQgJanEA1+LznNcJSh!5nrS^%Y zA2Baft^O&)M)H4oi*C?=%pg?>szdWiJ+9us!kwum(7b$o{XOlh6GY!y@Kzg)l=a9~ zaQR&EOb0rr(p{NeF%NgHzh)$t<8NONam9YIM>xdkVFusV^8CGzAOw9n?2}aI41je_)i_&TZ~uN>=3ka_jGf1LK^3DNcrH#c81`omRcoag z=N4f^qHgHd0!2d{AzRhd=g(r50gk;f?8Jk1#_-8)%*ndPZIJ(ks*#jHN&e^cMnmOu zMthOOSwQG^H$1oY0dGaR7{O>?&JTiM2{hK&Lh?jT`IW4V52-Z%(R1;(4z=2(+6vS< z`IZta1TB& z(!5^`f;`G^;=RI~lV;*1$GYv8NM|F=8Bv39eQbF zDy+S-r}F))&JrsSsP#6^nu@q@hvmjk*8#WNTN@=>!TM;&;a9rKaE8Ep%9kGlm60OU ztiwY5|5zLH(N^b3Y<^Q--Ys6jvBUimviBvi>PK$~woN+36P|;hMR4Y$40Vr;9d~Vx z4c)51vNuP|dUbA(m$v7|e@6r;EcN7$@T^$;$qqE{~+aMQJn8dsIb4!=Y zNyL3-HuOTRj#{xK068Z5p>9sHHk~Rm3AR$qn*_I|992^~5fwKPdU@fgs_FV{9yIR4 z4S&N*kE6L2nuq>r-2i!)OhEB|O%46^=s6+LxAvMQ7s!mz6nJA~!2K`VjN>LFdp(Rk z!F^*xL=(}HNUUQEC@h+{=;zpHSZGvrc=#2a{i}8H@S)AX zy)%ks5r2NFHdplM6ue<``^?IRa%=7NW>aIK?O?asfU++yZFtZc=(xzz1G@EwMOW6m zHQrcltGaWPbssPMfoiZ(tUNpWN?F_&(1rGHj;a-pYew5_VN`BDA2yX;9osB4&hQvd z_7FBq`Ruf2?>1xluh9H_bP@#0*P?m8#l%B?1_WLltr)SUg$xYjt4UsNiI<;ny8RwNNs|968@0 z+#OW6w%6fWz34PZe6W)XJ$O;xe?Tg`Noz2Arr@FKt$3S`MVYRf3!~Gs(fqO}h>N_* zr7EI~wASIZ4kjhG=!DMS044uiP)4p~e3=a#N`Nrw!gm7h{qnvGgZ6F`F34G$5lwu= z0tV%`(52HQDE4=6c$wdd7uKoW=cP>Ag@44jP9WtHB8Lec*#)oXX&(+rmwZI%<99hr zs>^5<>ss1DAY{|G)(@Y%L^L!vbVqEsh}?a(%Oci3Vo)4}(psOH0gE27I8=7I9lR6n z!I9t;q?VmZ0x2CD+#ok@z6<`wyEvnz{OkBxZW-;rffypoiy66QA38hGX$;o!cOrIf zzMCxy(?IW;8}i;Q^V>IZjoV(1H5N0ahVdxTp6RDoW1NgoMOQ(7MdR{y0Qk&hNq;CA z*Az9SYxIlQMVE5$FQP5%)3T2V>++WrTEF=OJU;(hEsxZ=i-G6zI9O&}!=@1oe=rjQ z27JF4u?`Nus@v8>2u)CgNnm6RdC; z2`3#qM$C7rHC`WHQ-73Q`7DKxKO)~1HH_ip+I zqn83~dQYt$$fI1ceR;z(Jsx^9I#(`pNwTZ(YxrO(*}qp0KbtI|N(F^e{5IYU&Sq2x z#o2T*DJq)h(u|@z(RIET}JHlJ@nsZNme*`Q`17DzrUZfM(x45FRUT{MSf@<}t*J|F(eEU%B0i)etY+uqcif z7`$ zq#H7sgIB5yV!Ue9u9V!*b8JDr(u_9FiwZKc4?pi+sG_e7mCN8q9y+CH4F;^Hvf9E= zD1ILeNMA6?RoCG=hp{@vYk=SD!mJr+FX(oBzx)iPAex-^>^qCbkdT*q(NT||#xc%U zLqV8+CslVBe&qc0t9^&}t!x?|cux+wAXr;@Bn9FCq~PUa=?^7__|^ML>M-$YkB8Vw z-eIr*X~gR7UM@sF*tv8vtaTQMh2ZU2z>2p+qU;2cXWe1(ZD?QqA;N$SUr2p9CW;y)!35s%+8Kf|!V-jeY z``ZHeV)NrQPRTu;+LY*YPdszT-H02))k;$h3NkX$ma+N(zOo{`?O!vka7-K&g>zJ7 z^;f6H=|XRB?a`Ubeors2zP6&?u##O5q~%yyJ9={OZHknr-j_by<&kGMrMfJoa|?DCG7-z3ZI> z&>KQKxOg?hbGMjXw)eQfr;<=mjLgVm>QkH=TdP6t2ki*4(Gh0q$<)D-&TeLqE^(k- zy3`U@80HCfghqxAOWBMz3PTeq**LJKUxrHi`L=VEHN8b0sG7WCs>wB+f@r(nv3xL+*mUoo zCnyqoCkbFrYz~jo(O&GQCaUx(FRUE&A}#Io+IfdSz?Zwn`+26E1FFh!hRxjc#6bB= z$BC4Y_vE2&7p=Jh%0T{P^%HL^hoCsX@FV_u*J(YLFO;uHF-!Bez6PXQY%O)K01+H7 z$M~FC;`yxRB+g^7v- zCd|OE4&7M=(k7R;hqIsEIgGA|a?`Qb!O!uh{ZU90X16_lW3HpzE7j9bk#m+1{gO;p z^N~q~G$W&JBn$EM^|f)_8Il~J+*a;I=DVd;m~MAG2+>I@JF|EyDJF_RETaQtea{B@ z+6ruY#U_tED zdK5Cn^@3hQ?e$S_g*-4u`sMG)?uElOCNqqrsQiJb&VmxtQ7ESY6RBfmX>^!78) zDs=iO%2L5b;B;^K+|&0}2nD6pJ9kYPq`!_0N_@EX9MqiovlUBf2Etwh`0J=K)x)l@`#71$uFa84;&RX6y+d%6#M z0`d7l#cW1#nuFC=ONBKY)OdCPYEqgKaFQf0bps|bW5Qy)Lqpb3u>0=J`|OI ziT<*DNHTAxAGqJbDZFEgR&mnpfogIm2I{=~4eq)YhDvBbDJ_deeU}DGTb0C1r5(OdcejqN z@_Edfy?L$jz)OcePA1-`7?d7nC6Qgg@ZkPP-%-FEYw{P$%` zRM;95z-B(SA)>8%^2zngv=kVrGxaH6IyieukYHL0t!c>GSiE~zE!93+cipnd9f_<_ z!y*QcDfoibPkX7fhcQjiGJPMeiJI1(ND;8$lRiRZsJdfDXJPP@uNIzQ3w>l9uGec7 zV?4@wYb)_FYmXVLEbl}2TSDsZk*0@yG)YSx|3YLdRj_!v2(LyEk2Bx&93feco>=GN z1q^l$zRe#1al9DoX+d76cxeuG0~JwMHjBsKvaPWs*vr)}vWOA^Li$vaM?l!kGGx8Oi@7@lyvdw!Z}CPGv}O*`{wXEAP^Z~ zPihVZKj6RpDt0=xo#tVXtFTH?w1qclYUb?k0;@%m2x5KbCcXS}rbSXbUqeK+a7tYC z>r~Jlg$Qz5AF)*M#W#I5o2drN; zKdarT{^Z&R5_;dWbLOZy$5U^lSYGMi&nW=}9FE)EJwOcjVqx&69$-E9FPIL7K{0I_JWDapu##`Nku}{6u+Qms?2iO&vVl#KeY!2_$4Z{-HAFnvBa^SYck%@r3H` zzrrcd_1RET#0l%etMxJ)_(PSgG(5-;gcS+V^lR>8Yf)_4@3zqB@3R9~xs6BeZ+&?$ zJ8mOTTN(Q6=b;abFm{+q!3A^Cq7!CtBgKD-u;dJ2fqsfS6qzpgcKlZOS`qA!M@L{K z*lecRK`S&eHKIc}t0~~TC-MRRS~N!Rl8z?uzSE=i`XK%?Uz_K09PW>HX3y1^+B6Ur z7n*AHCLfH=n-V3&zRr+7`FJ&bxz91^!TPfnldU_)8UWaVojSVznQ=f&nF=-$5U683 z02-Q;XQIwT?Yr;i&q5Y?zQnmEH23bPy3D-%L!G>BKhGo5LoknKSX@KsvYjHIoH#TH z!MuLDs57~Ge8I}x%kn}bA)^RLSdbZ+onOU1Lmy4aKh(~Vy2i@k+J66sUEXj)pD7AR z5`1fkxEJ|zCbUau7s(e2XjVW88$E2%s09s;Bx}zQEJxns+6ZbDbdqAadqL`rf~P6u z;NOOt$IWCW(Vg(6kr7$#VQQ5DHQ^h&0(P$tcPsqZlZCt~ZbGb-EUu7(O-xI3#$1do zD_E7>4hk{yS7((H8mi48VA*~VcRZor0V|^FGG9L@4tva|Fq5;75gBm>J(V2Ut242F z^qv$d07e*#N4lI^ly4%!pvjL9E1r%c?x6N_lUA{BS__R|wOD%GOM_8q(9fU0#11`t z_2gA^qsX+TuEhVv?2Y-OE=@mIoldUM`XSDG{9ixPD8I8@*Sd8xOJDO&K2$I++AOuk z(5h^YeqV1itHt}EALcs9VWeC=pAV$vPny_FhB!=@nZql}y^#%~?wt+U5s_r@v(o@l z6$LiDPuw9iiilnV1_)9ICOX|wlBhELs^ny|nlc#OhLe{cUR;H6P_wDuq@QIs5gye2 z5J1JgehX@3!|!=~PwY&b6h=0~YF$nG)QulVBpH(sd2&-;RHiXa7J#apMx^k432Cym zW$CntwP~o=*!69ei7K@}&r!_Vw&Z_STHN&4ANrz?f^+y{#K!ilgl^mNEvNtfR0FyB z!)L}6DudxQS>G&G+a8&Qmp%;2GJs#>mLxt|ILYC%pvAEkXxoQq|F~PY)r*zzz8nP8JP*lTqDGXbqhJQDo8lmoB zx~|CFwkG_=UH=W;;Qj!&-d+htuHU#d^!87>Rb1R<6RCx=iCeVziLaHU6^Gj#hyExP!5XoG5?vk z6BTcC`;3~^AbskicQ$OrYiSKiA(#R4LE=nyPA5>Ii}(`YQt-Hm#vU~9x^9=qs4a^O zXfyIFgkzGrtTYb{Lr3R-`FQkj1yKBr7 z3#xG|dqguicUfuT_$W`OS6Wkm@z0^gv_As~3H4|+h|0y0q#&v}R$Sp9*!O5Aq-nwh zGG8cLwtB8MG|CmyXIxrHISKp^!QN!=n17!4U@42>GGf?W_HqPxtkVdslLd?CI z2HvlcYwS>Kyri(t_b*oMH%8QDWE@xIM;G$88TN=Dwx3NY{<8Zt+ds*HqYVFoY;%)P z8QXTR7UlA@{xEkYLfVMt-RL}a-8^-hd?-{YMmtIRmV1yK_7g%w5}Nqy2e(=K zuxE6m+m3#{Y;r7Lk?pIF)Znd-$a-;u+Cyr9SDV`g58ky*=#H*zhb0=J8vK1n*yQ{R zHG#Ipp(SOV1rxkCSs4HhS@3Al)P`M0!40#{SEZ+ls(tcWNR9a-x7Jx%7N$ryC5UB} z`AR5Ie%~8{F|zqK3zQ1bC!DuJ*78kEN;#6`wOk5xcu;!u@+CU(bp&wRFA{LAD`Nw@ z_5VLb{W^F<@#_rdJbvyFi?>1#TQJfro5)#*Odv78pXom)p257s;l24ffeYalX5~AP zVBESHWicY-fb`(K_?wfxI9iH^lNc5CQE5sZiFLWv>mOC()ub6ZnH`lGimB^?SyU?w zzj;#6t>6{W$%iVkWl|=kH4)tOwFpjT=4$Bc@#FAM$595UOf%J7%R&T~LTa;Hx zU&IAWCU}YGhcK~wOF<&Hr*UhEI@cK<^LodvAJbqO+v4_4cd(hox`gNy@+CQ&+)hLt z9oqGCp~vtl^WEo_87=u|WYaUXu$x^J6)}J#@c@jVJ0A~V7dRBTBA}{ZM>Hw6b{RJK zCboCE7ABtS0T=K9u&8puW!|Z!g@*gJ*I#nq>0Y10yNaHZ!Kxvez=v#U&FG+lXE<_4 zyO*amj+}EgiFj_Y+Pu#gW%O1hYl2SY5dvOx?pup6yW26sKc%(*(BYM|cZQexw|`#V z-q>H>qjBwK!<(AQ$zhuBB-+_eYvP|B4?q>if4&5rW@&L(-tgv6x_w**?~p|=8nUjJO@b0O3csT93T|m+{^|FYrTPGs(fLwm!oqmSAZ<3#}a`zrrzt}$X-Ys8;MuZ*BpOaaWH6?cWlJK)o(*bFKxI?*+;3&5X)t+j45;awd4EE zb?3A8P-qwBEGaWH#9+)r)K1x23yW`Hv3T2BhCLPD;Abt7CBg>E7iDc2_m*G#UiLMP z;yT_{TNzKc8G7thF03=iNf>s>o(WYD_Bo0&XfX1@Wm&ifl{tkLz_2&c`}j-`Y$Zx5 zmC@oK#eQal2_Jg6F^&{^qQy_uVa>d{JG834MPe=HSY-Ef1}&`Dh3~eQfh|kvfZO|) zBH6Cw#6k8VJC(-HHr^An6#F$2naZG#|E*Vx=-ae`9FUd1M)|1SNRPZrzg}?DNGU$BK~8!;=0qqn*74OR9BEGl&J$J7yn_4OiSVWYB)bc5eA=qR~a%X6?|%zZBjaI3A7ET zK8rUvVub7_K6)#ux(5gcqX!I{ql5=Q?(k-q1~_R+&o zPty&R#cKkfMgN4o@wqDm(ESYZ_t8~FwNrxm%E5g1vRZfyr83_Nc6L-k#NeafR+kD@ z-t}%Rbv-ddC1(7fCAVnB5odw$$4#U@9cWf-41o83G38JuXj6H&`Np_LOFeaLkx;?z zR>=}HJW7u{DD9o4scz&BIa!{BgHaX4Cp^NCQLY76qmbx7zN^PD!k6>kON0tzMXqG` z6a+qJH$Jnxoa0an!sGL6+cAz^d4L*2pZ-Cuk$c8F%SI!*uG!Tpb>Ep>WBqDdn-+0b zg4_n~KgP)kB-uUWa=f+nLxhDbnTD*$bBM=ZpYkwqdvwCN_&~hy5D%2 z9qLs}Kx??Zqcvr*|1&#hwRej+J(jr`^I>;$qF;5hVQEEqCU~PChgz#+uX}ZKjfdjZ z`d`U*W-Zn|BGU9#d%Yh`QfYez)+V7EYwEdZjoAz<$DZ)BL}uyE)NbQj=9TfB0E+%b zSZ3*cW?6$wn2^s@I)(Y|ei#{bByeSZm&{(%#1m<{o;nYVCelk?JZV6gkHzWlNF6I_BAWM_0O;?;3A4 zGS5%H3!53a^|O_}wkYkrJsZ0{4eai>(aqS)q_b9c##Wg@^Kl>9YIE7I$UyH@Y1YJ; zUbU7;OlC{OtTavuZDz-g2m>3wfiL?3xn|&SK$Azd_3t$*4(g{zPpb#5o((U~P_Rw2 z$w9{x`<*to;KM(~EHxn>-vgQAA;B3^sh+;Z*Da*zSB8hox zaubsX%|3^T;g7>ygOn>8@a9f zCCo!4o}AoR8u7&aV-^`abaHL@H=#c|{sr*Z3#9>$|3)Jca_UJhD^?pB8--V+=1Y|L z0VrMxE|V7cqL+N!`P2yfOjx%jX>xHxR0(6otomV5FPlg`iNlpImi{9b3+gY;)YtXwZwjcYkVEl#~KCExQ9EmoaB3!a6iD?<{V5Lx^f4yZ1@ZICfW) z7JZiW+IWY3Eia>?q-h?SN@z0bZ+lT$=!c&T@d|b*ZY*)UVM*MBL;3CY-1Vc?aa>XuI@iqyo6{_zXI;@Nt0DP75XHJ^Gii> z;8Vt3mE@F*ItH&tWiV$iyK@T@y0|?t26@W0`5h)OpcU|o3V=%-1m^&kq#6HG}ht5%%Jp!gA$-YSs|_C+i`bwe{I|gRx5E5!cMEwhQZBG? z05IM5@nS5n%$2W3Tlyuf>v&kNJjdUMQttd34ou{Y>O6EuK6at^{s5NjcvL;BRxrYv ze-pzku905|zpqZxDmB!UEIP4(nIrha5YMj9oYmntMd(&@S1WTkIGzR^OfS@+FMRwn z{~y~?r&4qv7sZMs3qpU=b(Nf3P_**`z+)MwXjdF=)hzyTZ<R%I{$3MC=DodN<*n5J*uluq=uh3JY_^&gOSN8#Dd;N!`Qon!4O`f+J6n@tufK-=I%tug%L_IoET?~}N({7> zl;V3AGK742C3(}HudaUEQ_qBK0iU1qP;r+lQnvZ+kpLx-Fb7Q@-bX2_bx>r{TRHTP z>RT0C{*%vAYYE<-q`u#gPf_llMh%fyf|#D~%3!JfMb#|GSG{P z&oT18gj!3iWZ`r^HX;f_0_^?BsA}3e&^q>9M=fJu+pQ_avtG8It;<<#EC2pyShk|S z9^8r0QCn&K5XvZm-@I2rZiUF3@cTOsuYGv@<;qQm5ELpq|Uz%-K9NkSVZa zP~tzBC7Ak?<6&o_cvsZfKn|I14sgj)mBck#->)q1-%9vd52#G0(`QM4;m~@K7VsoGMcy~6%{}agH=?rhoygcss8nzqv&mt< z7|1_-fSivno(YAYJ9vyatC)A|qnKs^U7W)l_+s+EJMKp{{z83$lS<^K*;i;!S}c#4 zVA`w7T%eNsWph0B%{;Du^r{20_Ehl3dD}YeK_LE9Q(<=@%ZK3tqOKCq711E3xaDI- zQ+zLO^?PI!(YDeGZ#8;b;T|4@=Jx@Wn1A>nwGbC|3vl&PaCTn33_({KhGdwzFTdxW zfWv2ze#D-Fyqp4p>J{|7`1&Klb$jULlIbv`}kH>vH}c+@5LFc48X=1DlqJKkty zZ>nm_P~Hw%c35?0&*Hzm*qY?tpT#K$R$>||A*)r_W_ z3*gu0hcvkR0KtK~&5v3YBAN`6H&P%J;b4~RKh+fuRNXc&UAAlb;^h*I-M?_S_azC= zPZV$GR9&EF78Hnbh_bhBsg?5|;2glu;sxHqMnBCIh~*UUS3seuij&Q@YuyxExI3&e z01t|-)p81CFGkgCVKl+#nb33x6YyU{$=L>XpXtPjB~V?k$DyD{T~2ZL z1Ri)djad)vSz)!i_B8}}l3yO^#d_noaNZSwJt*KJ0 zpKqNUr56eu=koLD56$_?TWFa(u%^h{4V-VuS>0zV^QZBj3i7T@Aai$%0Q|znlj2K0 zB!s}M9p`&^skloPx1%#^j=!sCvO+){7)n*FfJOh+6B7W`24(vgCGQFLh*rTnN=Z7Kabxjo@A(3T6W2H-c$F- zGzrT#h2(ytw0N;eJFuzbmysx?T%G3{cnR>R>BXLT9rHxFWJ@^QW%@_16)?B$y5L(1 z+$iKh11l+Hjye$z-p=Mz1sPwwD!^Cw2*!lUn*f}qwHg;IQZW;nzS zm{uepu0>HkM(E-twE&N$I;FNO`_1ePt$jl5gqeVkc>~Qgyp|^@$DPaXRO_Bfmn5<& z#<9LX!FaP<&9Ly)zp_%Wc`s>Cf*St)9Jg;P4ScbbhFwyOhqmjgsnW(bYNgcvDu-zL z4SRX&yw`c(G43@~b(YXY^&2_YllCc5?OGpqqRvqUtT(5@#NW`v5Xe%|rx!T$(5c%o zktMaUe=ljpFQvvqgnSR(1G-g7p|*rmf-Gd-9>Gtc=RKj9s1t4DUOPyL(qHG_Qn`$_u+wM6^Tie3)#6oMj`;mX&~J7M$T6pp zG*R!Skuu@L)IXdJYQ1+CmP5yUI*3itR(-}Mgp zKcH`1_hB#Y>n1&2=TteYHY5SVlWYXwsaS3S3#55*rxrvzupS&HMD0z4h7=RkI4`0BD7r zhB?<^(>gPuheulvdxy~ywTje}KQ4MiyCxR;SQxj&AC!VP5Sq*B-S0z~Q2P^R6Ksj- zn6c9CJV@)P`dLEBzr2_E-N^oO2QV-jk>6_8X9T{ifoM8OHQsx2n+Xtl?dPWmgUofh z?$@Bd@ukGdAr$(eW5xh!k&M`EtJ&JJuDoy!vq-1wn*7py*+-EzPhGT1EdQ)%l3Fwj zEK(dzCyi%HRlg^^ACXY;9}qg?27X7hE&Oer$R9|^If32Adm@oCv#XK{FBo~0Emeic zoIh1X&(5%wx=)VWMVCbA_8s?Vnx`pXNI*Y9;WR{34?B01{#Fd5=?Z3XgLAMfEb^(n znDS~EJnLp^m+SG-`<4m|AOHU-&+Y+2yNME?#8hU2@sZ)2@~;6>wHZ?H?MUKl3hdmzIm^=BV>0sMoJKiwWAhG@X>$LpL7>ikBJGH-{ukS%S~R1>7I z;jyC+hB%3i4Uoy*E#N$)rdasFO=`_MUNOOZ*_&|p)0K;cUoCH zh{%m4359cb2T7Rsku@#iQjwiRPr0<>sz3}th7X36ieUK;bx{QxD?<8bd+Ha{Kyd-W z$h1Nto3if{Q8j@~sB($o_9F|+hJl<$yjHQuE3$1NzZ<6w+z`l5pt*cjZPTD(bpb)- z`~RU12m_n#+Zo-qkywbGe^2`$+#8xYOi$xNn;iHgwT22hh2?TgDtl}3V7$){p)wIw zHtqELg5Z}RCN}LzY?NHVYn(Iiv#CMPd-8ndIm=7~8J=7h(i@1bq9ia*ffjGpGpEWK zv{GcZoo5wTn~i=J-x9kQCnlknb*VqKlyIFfR#weoBfvh$>MlsLFQ@H}7Zp(@_&l4- z3z<-qGG@A{dlyN|WQ={{S_?}TNs@Y3rc?@eO9`S@7nxw$OmNY0a(`D8A`vD_E^89@ zZUR|_HdAbYW5CJdL@JWbrdm8AIWdsPgFn)?zv{{BBAKMt4Yc9EVw(KLO$&oVWQJ2m z5>1AaccKD?(}m^S9^lbYg>B3`)D6`h}R32SziQKp{kyrDmQR zpSPC;WHCX*V5Yd#SoUo^(N_LLx@G4F#R3eX+5(06Os2@9W(NhYV0wU1P!88ynF$2W zPsd65Ngwx)DgPO0R(=*NEwG<3`Wvb9as1pJ>Qn)g^q4+q;hOywZ~#e%AKnCsC^N?> zPwgjDv--Wy`pB|DOg;Le!cJiCVRdje^yU~4>`g)^-NM`tI!7Z`!Zd-fW}cTeUQFn@ zl_avR^gqaTdwn^=--ux^Y#RIFwzZXFO9C`}opU?sNT$@-rKPTVGWPVQbk?W7Gk~?B zapQNf%F#dPIU}mwTz;Vcj<(6|MXnPg867(M9)|mgn16_tZ{)mTkHlRHU2Pe=4xHFd zI7C6BM3(k0^~S{PI6$!(9;G=>_?Q|e_+UL6m-hHFpk`v|NjvgesmkQFhOD1^27G}> zjT6{zrw{_Cf&6%;qy+!WZ1Y3$g9zOI$ItyTX7RyVA+s@v#(7CBf;vX`)ofDJd#p-qC#;P^Ro_D&~4#J)cE zfd67-nOY)LRfoH#c+OnLpp_FI-137)bbG=%2v7s6y<2cH(ROHW2<}&8m|V1L44%CT zYPydKa2o5b+ymTJeZ6b!deE=}6nLbYej_FQH1eK|>%;y^HyCdD{q%4x@+rUn;MzDq z$LE@WS}OHb#^tI4MB%Ted90a!t~ItG9O37k7iuCY6%$uje`T_t1UaQdH9`3PX7T4$B}_$s*415U3NIAW1Xw*vxyaeZT@1&5iQ8%`ZPE7-8n`cyZBdzoHpn4a@wem^}9-_(2 zD7tg`DNN)s?LG9lAwjnUB5IzxIudLFgLe7Sl6~PPHT>GY4J)exo#)%sOh7Dx+H*wf z&vSNQcf~BTo} zOW((I@t=`_@)ZB;!acw??L%8aYppY!j^M}(UJR#0C+lycQFz;KQ!VbPRW$iO=IX78 zEl>_r@P3W+UCWmM*rY%9^j9VXa)l!R7^3iPR{X^P@XGj_$`g~j_fWpk^T;sw1FYY& zz-D5iItJibE3%+C8x)LhLTukBB?PV$02kOnMhWCP{6^q6zVE9UQKj?8vsuNK^4JOK zV8KOrID-vrq@c`$xe6<@$$2?pE?@jcCB>z>-bDiwODRPb?%|%kYvxRG?<`QvA3#PW z0Iw2M?e@)mAG1N5LsximmGyiehDH2~c5QwY)-?#H@GvKN9;EkYS=x3ao*%KiTU1q( zmlqIEEVFH^j7GMP3z0GdLn40%`<9`VY7f;i&MaC{6K}@yHAt3RbW*h|99sbN`U}Sa zF~@lp9{qIeUUTDPv`2>T3Vd?y^4WKWOD=@25;ZJKT-23sxBd@toM7%y`i~d2g}uO9U_$$f<)R6_fnDuwaMyJJ`D*v#^S58b)u zUaJs3gPfc58oA6}KDui7%TmsK@Th++%7KD&vBjR>V%mUgud*=|Ob1WG2p z+yvfUD(fL6Pz!kYrc?LiqkA&OeKWW>?rBB)EIdS~@S1NvKbPZ3k{}V{>o012*m;q3X^uC*3mC?j%x zDM%iE(&P9`Y2P&PrED0RY)gVx9OkU$@=$_%NXzFI0Au;}>{czU35i4uNuPBQNmHT8 z3IIbTnAFqm4Exe7UF?K0mo3vd0C;Y8nY!v1XBNGR&2d^s%#bxfRE(#d>) zgXcX>BnJnb1JA!lW9uUX9X;bprk>SfH-7RTl!D%_Ujgi;;>5RCZaZ%nC_XSbPwfk2 zmXIAl$vu4u(N9u8bz+*UFJvOj{p!Me;S%EMIfXgLXF|320~8%?N-TbZ? zK=z>-NbgE*8?>Kse(TZQF&h>JL^x-+$gSEVDaO_@eye&eTROv8o?H$xf8h4>c3x*P zIggZ<%#T#Y#1N=I6QlIgTzQgXi&;FkG{a*e7Atv{nGiPSblJ z3k5wb$X)xZ-}6LfJ;)gESt`!b!W0zK1gN&Kc^SPKH7Zvv^S{aHKGG|tp`;@_FqN{O zaOuU!EV7y`#B{tv;klrYh|cl8Y#T$m_j~Z%aq>6)_Hz)-c-gVa-_Ch|#vti6B363Y zy-&U98%m}hd#b(pyKP~q8_-77yK_Q##-9Dj$H))7m>af8p6$P?pH&?klt7otLOT;|H z;ht<^H6B>%cZCckI+5ky)Qiu`77nCOx}4PtQxDc61%WXUC0$Abg=4$->h|@kzaL(K zShy%~-^9xkL?vCBI`9v?K|STkn2eC4(T&`xqiezD-aSdS6;BASs`TgO?;KcjgsnZUz0m!qCaL%%c^4dD@r4bNn@A?n;b1_QsFT#&Rcne99@^ zs;{TkvzBhKk3_JK(@ND zZJ4h5v@cy`Qjd@8*?H$okvP@U0t#Q1AwyuWVMVfX@TY@RuJ*0i$!98?XF$SH`^*m) zFKs?Bq^-<)_hU&Vy-H!L;;K{4(ANZ)KYZo%-}qs)hX-IoGSgmg63!9cV>msGF};uI z-R83V4*Id0hFz8m$>v#10uegp&s={uCWgbjiswz{sZOQNDo7Mrc&^xb$vmM_Cv(72B6kz6IeuND(y=5*j$b z@F)uZn)mHWaeU(^G(EWBSeam z?zS)Hm6Q(dabFrR%BnlsNTVGkZ|*H=(PwNoR^7ceN?qO_SasJmoGW#KKGFI1&{jD9 zo;v8iEE5l|n`M>DKRGtrJQ*h=b$nR*I+!bb_;&oB9&kH|*m$76TzXshr*{{)o>iV3 zjnP~?qsYZ&+2^ZKtNrn1U)*iTDn6)wC^g4I!~TpPXSH&njy7MYS=lKyzK}et<7Rnr zv3kCo@m!AWus(lL8*mJ@XqZ}76I60ri2}djSw}*aqB5vT!O=@RU!WmjF_v~_RcGkV zcxti9BYXMm$#2aEEgW@d)S7W5x7jVJ;~9WPTCOZGuZO>uAnB3bbGB74}@`XSti# zBedeO_i#`bB0%IA9jH#)#!Fo9GnyXqSIbL539hrV0|m`${i`%%HEjtPXSlp!5@QhMhqen&w#R)md@SS9~BQO_lBxhsVaha zEskW}sKTboM6y=*9W(5EW&%Z>(s$CPZlY9PlRXL?&=LmU7#NM-;tc2_nO?uc1&Nq5 zEP0)V6@6&Gktn+tZrSDYIHt2`ENdt&ze}n7Ib&Wf@{))RoNuWW)WgITDkV->VeeZ$ zD56ePXaq{TkkRlFpQ5^SrB&lE!(-^_A)a&5OT)EYaH8WBFha8ass?NNx>DVg*h)m_ zLxaKlXHGS4JhGx}Dy<(;P_2`-mqZBwJ6q-X_CR?c0%x1Pic(jv=DJ`dfznziby?En zv^Q1VqFtECElOa%WpNC;X4!V_J3-(CQvy2TI({X#8w|r`N7#)ejM=g2*QrA)D@wQ@ z+8kUL=?3;~-t!C8qjvu!s|p|B)!77U%W4b2!s(CYp@Qh8>6QIyV( zuG+y9mk)Sux=0sX2iE>1N-Z{=(^j_Y;k`sVBrQ&dU|X(RblJfk8BnbsTC%&^#F`1u7bx3^^$&Yb@ZSC+3dssGnS*_lzp1Rr?j2Pj5dk9T zE0q+Qyf-hO&o9Z=zajA~>lbA3m_6Z9JJHcK=+VQhc3&Ep4x{8oNn3S0Yln<<(OPXN z)p1=!FNlVPQZ;kCbCkpFa?tipaL0{u4MQPYpH%t1hB1I@zy*DU(I*Ccyh+swB(Br& zRBi{2pNLr8KewL$#;z*^H$Y`Awm06PJCD~_4x^S4g3By)@8cLspoXe44#`@M(S4x1 zoGph?!+6kWkF((aW#k)s-D&N#>z4^gn#>HtozFH6s%m0abWQoj??x&ZIl5#Bf@XqQ zRV3N2EAtIt8HOS-nrEwF5Xv|t`xRi?*JG4Jl!BPJ^LHqUS*~J$Cc;- z8Lg?I>SF(i^oIaLReKhF`eTV)zQ|(6McviE-4bX>?0?Tt9ZVK_}Zc4Uq zOywV7=t_`-CJCcvg&Dh9ynFYuTwdbt_ zj_HL;K0eCAfxSH0#^|{0nnexC%`~P{cdRigG)|2`e7d<1lSSeVtf%8$jS`KQquj2} z4|lfU&H+6Dh|=Nuq&$a`j+7Qs0<7=_rw`fuVN-TLP+RowgDWlkRg`0YTV}S$Lw8hF zkNRRoYiskoAwj`*@$|5H>wfxKArk@s)Jp|WNLwqVoOiI+{lml-Tn5tS;`!S|LLK}&}p_|~fv!KdN-)*IfFFI0%$liQCr#Qn%r<-vQ~4X?U=5 z(2_d4`VH@;Dgww29Y~g>ja%ChrUO7TAb^e``2T!Bbcd0W4v^Mp^n_>s?e-YhMqHRUH3k#b|gUVOI_N(zrCb61sd4zwM3zPj~yEU zEZ~$eVrs><=Z~Q1K1ql&;&fAe#xATUUBm0^nPL=03Rf{vl|d@H-Fi*T4wzvlXxeP{ z`C=*Ww@d9=KO`1(N02|qr1qNFGkAuobH+w0v+7f8AdgYoM8FD)#-9FzSu6;o=_v$Wk)S+9|q4WFJzk;UUN-| zow`s^@_JeRzRQ7nDc-%fCNw7I$8eQ_W=JS6lsKSl{qClbq&|cKOk#ojYKTfv=fc5m z);K@@u4g3e+m{R3bj?i{LG+V1mNAxhFRN-Qj$rV6j`cF&)JaZdKEHUUDd?)Mbe)c) z9_M#)OQd;TOniinr66jPYKAd@GnOx7<^AUrr@ZeaIsWY$!q2w(!h(i3+lKf;sp6P= z^Ep1}7HQW?U1z`1gb7J`L?KsshVX5GKAk_iV)S{nNwv3V@pW_F!h!NC>A|RzuU#3B z7oC-Tr&+3?jIz~|quhR`m?CwEW&^>-NyJC^vvQYNS5S z<)lc()6gp4Rn^j+sAg)7f%%uCap;n}{ac*^J_wNak~@!;l#nXb{-6##jCrgzk+b8{ zL+^fDxjX<9!v1%*1QEEjRog_NP@zrIE641@mHY}Jr<<|qquKF80od$)I zttY*89-6vpChIeM9^5wczdY_w-rSd_4J((kr1emA++pVKWc2L@36% zUC#o#fRh|<;nb2N5>h9JC8R#mVHO9}x2P#ClYFwEB2;FWI2W0!wBm|_!}}cyCn~|N zwwxEaV#iK0QJKWvx~}?33xi7)g{O_2XcwojQ5DoN5{j`>RJgu)q~l@V?2d3S+zxbB z*4hOX**gk^d3Fc0D+cA~r3Y1ewXfH|s4dFd9cy1rsD4e;;y&#Dq{)3@=zG}$mBlIu z*Cr2AcfHr83vYGay}+iPP(g~!I+ZbI3l#x{BR+2#?Z0QPnx!SD+#cTd%Jts9_$1&a zN5MCg2~ORFpVFM0f63QEL$=4oR;&_WT*W37%qcuR@}>5SK?|wOwa z*`-E@kiwq)Jfi-Sv37(HgS6?8Fc_Wp`I`(n8ommwvqY3;-W$nz+Wn~GZk)$mxojoU z(bXAA8QYKn39*S7L{rJr1QiPJN?bM+RbPV7H#a9FN7aOb%r;KkA`nb9fNqp;;0VjMMMD8i}VRyv-hs{22JOLfrIrK$@DNx z>jD$|%4zg}M-b5046r1jvN1pubEwML%G&)x@6bKBGqeKoZ^ z(=;U$u|+xz#h8BfROL@xqYPhOOlN(oLVB->WX&@cEMJAGb!#cx3FS@8e}T1PRW#`; z-!T&^2oL%J(nzX2JoPQ_Tbz-6R`z-=Q@xH6qspl&%(0rGTz!kau*}hy_09UJ$v2)# zi)p3jncJ1{+HOfZQQIgRRy~2r1zZ8UO&6c7tITUYs(!rau=WR=svVa2)EA-!StZ;x z>;Nqq9>gl+JZ~fa^k(4_6^@kK&q`lafsKC6Wyh>IX%0pppm9Ab8=U`2p|V*lH-53Y z%)}odw(QFbV0rcKb`Q~Pr>f*+QbPg2po%TWMJP><} z1@A`Nz;+3Y+hJwuh@JI5}t`F_R@SVj50`FdQ zY-`jFAq5QTl$L;5b8{H2zI+z<_6zgdDBeqtSJ^6`9$cTDy$^x^&U`(HmvSPO|Olnd2s*-k^REPB~3{8+2`>iy-4!9X@L zZ9{VMb$4}>v-uu`>Ju*<^@^VB9hZZ7J5lxh`m44mvfMu+!(J@9nA&B@R&<-GlbOBQ z*wmTJC9js?b(IY}9<>BsPZ2tluZ8+*oarnE!<~0tcxaeG&wZLz<?3zxmGKEwy^ZUcN#;XL zmRBuLPwo@Bt0zNUa$%C<0MbSOfS@I>xUqM+0eL#v(0~xt$Un`s2WOse`Qy6n25)5~ z7VcEzs3w8H1!x}pN~YW||FA@H->TDPDUv7T7yJ(lH?Zq$VC!7)f5E!CM`@@P>og@02>9$}jlE1vz_!5R&t63mR`wyqw zS5*Z0HDLfga##sjFZ5FR^+K*JDAk^Dw=%taB18*}#D+z+KuDcmLxLfJ(1$DadV~Qv zijEZ;W*XDq&`9CZcLY6itU`5^lO{7=_4CXW9s@Ld+m{9A;?slnstdhqSHiaKG`oH+ zrD5kEj#TDY1jGuTldgJZuN>;43g73^^G9YVj%C}K9EtXv5mx+IY`UY3e`i8(iCtr% zC%Z2Q@?IcD!#t^ojLKl(rHGADcs9#xy@h{%J&QJ(wPWOKW~ObT9Vh$SkLR9*Nq-N9 z+T5bV)-&p6evdzQBQNrlLUi%?>(*4Qb00rKSWzMDG26!~Q4%NO??pa!f?gZi=9=S$ zd!P)nzaig#R=v@2dvNMim5d(L`P}b}FzkhiHN`s}72=P6^AC-bi|yjuV+$5bpBWDc z4b-x-fiZOmoNL<_?i4<9mYZGqQa*mJmAwy!fA57JkXtN zmaKsUdwQmLowu&gf_I&u(mF&w(2Ww4v3OujEcu3xI30@(VbYBH6Cv%xLvHVLpQS}0 zwgQ47f)bCJ5?RleY6Dz5iCx?R(b%j>*zT%m49WKH<(lThzR^R3}f)VH%l%sHHdhdtmuka0}bayk(pTQKyh5BZ`!4QMPfRr&oXxN`!Hvq==c zP7GeQ#esfX_-1{@ef7BH=-4yu4SsY&o>|ujQY*Z*Q1*L4DrF*FGc{UyymqODr3^7hYq<=W3nw27-o%2{84$wA^d`6%!-`Bq%OHTwqHbU?@xxIAP`O zp#@jhiI2p&7+wqOq)jxmjh138{tT6an@Z>i_?-gKt}l7<8RiZC5)_GF}~YPeRd%3rG)qOEW(Oy?D25ug?&_xf0CN;(@y0N1jU5Z8-iTSdUtm$i0Cq#<#unaHIaqP zLjjJfjrN#M2fkSr5PWkzn@JxGR|e=-wVtzYlue;RH;lDnh|pk~pOywr`Y)RkQ}O_Ky6@a_Iyr z?KV|_;(9Sj`k=e%{}u1?A3t*}x#3Nx#^^LgyS&d|0O2%L+CEX5(>Ie5Exx*A{<<#E zKT3x680OEbgR-63s}sKP1WLq)^7gi%Se6pz;f_`eLYSW4mNzsDYSvUA!?4hI%Pg+l zg&QQ@ghknk)dzW6^k_k;MX;UaCY#+|o}9W*B%)Wa^|?emqVJU;UT)je(i+xcf_AM)-V%`GYjsnA9lx~XP&d|^^w zTg_~v;6R~TW!OIpCQ})Wikrc4J>zn?po~L}tu6Ty5KFm4Ij*jM%dDk-$KC?br6QLh z&mFdG^U|B}bRym6!K-6!1)sC??Z%kYs4ncol#Wbc}~Hz^f+{#s3HkzNu9_Au0)@ z8#(sr`8i=$9n4SyERxBgt4&$CKr0Pe{_u*K!C{PKzf&;J&BhNgIyAJyl;?G8&>-Dh zX7fhnHME?HT8_3(^?-)v^75-dj77 zwAdQlsaJ!)TsjkAhk4cQ)hd@wIqRQ5r)}S!Ch9A#U?;0wdfL9)Vxsa=j~3@=>e^LX z1f=&`A~%OQ{6YhlsPn;k3WIz9t)zG>Oy?n;cY1mQ=RIzLmzS@mLq#aTtxIcR4ng>| zA;&2{hoj}D;|QO_Ck1#-Q!rQl)fDRN3#QY*#&& z@8;*kCgNj2r_CMrAD~ngJu(m{v$iChqX}XQ?=Y*7GQN*KU#I^Q{C`@Gk`A0(xoLi* z^W^6hO*y`dL@|_li=lY^)!N~9c$q{)y?WklX?^+u`Zj5}LYm`(q7>dZ{W$3DQ)H#%Ch+V~goh@w zE6g|~vm&lkD;a9*wglqZxhPD8BD>h%Brr^MG*AcGw401eU)T9#;1=Gf5{i0-t|!%q zEoip{(qCape7z``?qKEG%Ai}&?$t7n?L->ScauF;Jlu75s}!}{wAt~{{_c>cuiSDN%HZj?rxzUmt$?}o~ClNoeTp4Bz0tlT-&L9X zP1E2x3b`UFmn}BdLtj8KCU#vFN{fjHm^!`_3HkLXlH}xUUOeR1{ArY`IrXrEp~2TL zewWtZ9h}s|5@kHQa_xmiX3;Ok607|6l$egw@Rxa8eY{o>&ZPEfw|zEYV2OAV&{pDU zZ%I14Y3T>PXic(*=`>Fh-@l^InLx}ZJ{GM3s95+`lh0?%hjG^p#6}C4a$oa-sB|4J z{{;+md~NV&+J_aAKFV`7tT56V{6A8G7->OVa9ev~7wLbD9L2sBZC5;z8rpGc9LH1S ze40OYuzl!L*>}INH)n21#w<7*G(GekBNa{C`YMH{f*js*u++zrw+x8_dVDG=RZR}| zT=YX^U0I<*ee39`D|h*`!?}dEMX-flg}xDyT@STvnI%gGE4O`pgc=0mBvi z`8M>a=^KUc6dg-ie#ZKhzrf2x9d=O;i?TFG-BfwfoET}{5 zp~*y8&`Ie)7-vD;oOPIb$o18KR1>hbM_gBUCU*J9h4m{3BBc)L8M4)aByTtEmHfpq z=*#?9DK4D5sO_vetN92+#M%0>);E``G4lp0C$-A%206%*11Kd z=7$?QO+)^x4k~ z2HR2Kf9AoIboYci5oNfXDf5w`*gbRx++!~zq7y5iR$jsK+5!OK)2o4tSuQ(#RW>-HcUKK#r5XQu*GDP7 zg=sNn4`S|=FB8e!)k~&_vY+p*v`Nqt6fPXl*GhT9ZEz)g%zb|SV_>$Z4`GhXQ}8>9 zv?cwxB_{Ny&4ZtK^Gh2A#0lZ)yi%n#%(A{z*CuoNyY{4E<60@kVQmifY5NyTm0i64 z57z}F46pje6pMe@HJp0lT$^H1<`&+52P4sepx%*J?wi(avH1zb;O&Av5k2O_lH=JC z(RrU?Ve+92O1-y2=|+(X`IimbT25Cld!`%)Y)2wrLV4J8}#yWTu?jM$yP;JP~H_tEq(KLc(wrSxAsW*LR5Zu zC?AN9H-X@-gYx?-`Kfv4h3mEYnaY{j-&a@`>_;U~@eP2iaZp8}!2xrN=CI4x{!(Ho zi%5{T-^g2(oF5#||&n|X_48ycfa9LHSFC1nkqfu0G%IRGlFP!Ok!P7_GE>e~wbFe(VkN@=k zA1QB~l)FjS^aJ%Y_qtR?`^%ZwMmzhX)5%qry>75|{LBC_LI&LtDk%%jw!)i$oiUWb zMk9MUs-FJclOp`%Mo&(=O>F*2x($0#NuQdMVI1}nllNy*)m~mhYa%NJN(Q4FLw$Ui zW#RpZgb~{Q&8^SiE-Lv6E(0P1zR499K)IXD`ee@=_*m7o{##CdJM9E8Pv`HXU6aB5 zJev2DA-ifz>-cJAMwkx=+Z3C@CH41DtcJzE|Ma4{wN}Z1cURFr;TQcT3!zP4 zaMA~|c4UztJcEZ`)J?&Z!nuaL0H#vfCt(Sh=V!qwQT)dB?4ubSmaIRNBeK3F z-9b@FGW{2Ir<(w=Wr`56xa7aNWVIW7`w|vd`17mcr^-TY%mxL4b_<>}87!L3w~%94 zG#hz&>!QYRJ5%srdZ&x?Bpx|Kkt=N7(xH=&h=>(i-lKh_W=6(APVxkpguM)+p&Wh; zPOmj(mM z<}R*FR{9~6GBD@pV)!P>-}i%D&LOP5y%L^Iti9;NnWX&+(h@XyqDwd=Y&6~iLTa#k z<-;)*G6Sk!_>qk2j{+xF6h1Ni+baJ+$s8)}m>x2dn^K&88f5%3Fw#7XW$;)qVl-HUS9nAf$1Snv&64K#MM&rapLhyHzhN}=O5os-9i#5s^5gD&=cIC8 z@TE}hh8hkkUTPpEoc`g>Km-`))VzTkPqZeZB18ksN)SDaM+8_(B7x4|b976uR30b( zXic@iKAOT=ZmD1NxRl(U+raLFvR1bTER9d!35Zxg?h8_NxTM-y63weEU^4WFsE}Su z)a2m8q<3#pN*$NT5&SZsl)QMq#_knr>%(DD;6?*MCO*+$$c~A8Y53v4?6O?dI0<#P z{~GZ&e9ftP!<9NGO$jfZ>IiY&h(A^0uCJYHI7QD3DqDR6c)Po-NquvbT}r2a-qTgD zgtD3a3ary5*2rJj0hA)0;&}+c)&iTzGl4o|+l8avc3*S7}Y>!XpYYwr9=8m zw2z$E)I4#0b+YHdr#|}WS^yfTa0)CG#rx>v%)gJ7fymprT zm|*P@5d*W+ItP$wY8a<3HXp%=`jOsAX~Snc1LRj0V-D16(6)otp&EBy7E5(P`#d3BBvBwN~-O6_2B?_Q}^mAy&OQKlI#$Vaeh zC&ERk!~yid z(6%}R#3pKQwMic%%DFCDoi%T~I7v?Jth2}4eL*XET8 z>kn&4!M35!=sNjTV6SptQBLV@IJIUZar3JRy3Pt#81OSD{3y*IU(mTwfY_FGE1VoY z`E*d05e<0DGpRl9>Gp>Nt8vg0rn2ememX$1nOIrIUYZ0<)@SZ~JzimPPFOXp{FPlD zoxajm9CA?Qtyln`sMmqcgp}}SfqDgvLa!D8+w?AWvjtF3=ij*llZi#2oM^#I8+(0G z)NLNm{**Oj#NU>^FsA)Y;fy}#$~srr5zWVD{L>JH7a)E3NTZQ31+j^5+_36oGGMy$ zH%#qXckyBz1-+eGb(Syd%#pd~FDALTuHNsqncUhePZomhF;%I)Gx?RVlLRlD!b*^r z-wN5J?qk($0C$8;+Ndcw)piB}EJi!r{wIvDb~I|ZO9}qS$Lf4-06fY%ilC*FX`Qyf zbI{Hg#L&^g3WZEkk%DLQzvZ7J?`)+9n?sRmP0=mFB;+ zX@0R4SJ!r9ez$tUU$pvl=Xra1H_S~D#gz<+pwLHyzOd2L%pV|B@ zHZMt|T`Ur)4?PP=;;EUZQNLo+RMEp+J>bLQn6u2qQC-`c+FQNg1HYUtFRIEulscj} zzBCakD}eW{%JbH3>*lkgd-t*lq6@Tv{nt+tF7p&3q91k9vFgMvJmySQ$)B^w4hC}- zOJx^)V?~`(49i9BUyA&NrdLcJ^J!j5AT^_=T0hA%Te?_15lBjtla3>_aIN26HpO1T9 zyC^PxGXfj*e%{7~Gs{q)c`is2;21xZ#wCLHKNoOr5sdpU2hgz4`pSf6kKB_Di)*TS zlhWCIz-jj`77tc9miorBKBM;MZA=*Ld}GjizzF>&bXX!wI>F&N_1>62Rxamqv*og2 z-yN|N*}3Tv<$2ynv4<&7n83l`yc-)pjr zL>~o|N%ve!YKL4+W^(WLrpNX7f#kPAeLUrq$RGInp`U?7&SG`b~85HN&QM_fdV5K9UulwZQ zU0WZz!^^e`Ol$8&j)Iktnvh*0kF?ZbO~aon!JB4+iJLVRVKqh}PdbO(NV#Vdt;o-B z!k@gIlkymaJoO&&P1{++iAllj{PW!&Hv#FhE8<~p3;?c<7}Sv>^EokX2yE}mZXxRq;=2Bd0evpF>-2Y&!T8JRLvKu`tgFahqQ}lcZ82vsmibBkWk8B}n^o5=SgFy}CRTQu~48%Y2{5 zPuPL=C8x=);(dbC1j(zaDz(Q1kiJI3XRW(9)5cN(S{}Vj4^_8)B#KlJc{{B-LuyIf zl*K=lM#!<}(+TRF`cFjC{V7)eOJS?I=qbQlOB{C8KO`Cx4_cA|Ypg$f3f9=OlPZca z_k^u&Eq3uB(YT2XnYXfjyXG6-fca#uYXxhIMni{x;&vkXG3IF4(p*;?eB2-2Bh}dB z`b%@8@^Cjt6;R-g?HKPg%`Ej!=^dV~9x{ACxqphZMy|q0=R}$wPAqaBqR2ne=1Ry7HfXF8nDWL@NT}^Z>}!25WT2V;?T3Rq7DvZGc|!s!~!y3 zWb7qdI+%iQYL8#`rsPL8(x-h%>onryoyFTy_lTs!4e;i!_lO@rC!_cQepRn>>0lry z7@Y>?LALl$Q_$Zo!Q$}^G$Cn^uyQT+OS=eC^=# zTkU?4uME~d^X~KKb$i4X8RWFul0gkallvibzkZE- zlR`{KhElw~H#!nh{1?)uFo&kthi{4JO5qX!`TvD;r3nOBBY|t{Z(`nU!G`B0Ks-@oS`7Eii1UZE!okc^{h?Oe- zkA{{?l6JjEvUkakaH$c#O<~rNzLRL^)Ufjf3P#74JI9YXoDo)|VaY*qO||b!B|y`7 zX>WbVA7+HV?{Z#^IMWIFHB*bwHc#>2SsCtOO`Y=Up(@9#(MCMD2`N`pmR zTI$}y^nCdSHo+*uukfIEWtQfR%iFB%T4*5PKV$@jBKqyGK&x|2Qj zaQJ&?d?Pawwq4dH#o?$frT_I$(UFDD)E&`P)CIFa{cHF36m2H&s9sev!zAYR6*`|c zqtpId`pm1QNbBeE^>`5_7I_7Y|5WXI82grY zXDoi#i8P8!g5%M~6Nj@?1P9mY#Ve8wXQcsVGBa!tPHgTq(pQniJ zsbeIsMmnE3Hp2ESMUk6yY?5y*p;^giNC;khAVX>?Pl6wkVV*nlQx7f?QF+}F_%ZMu8H({wQ*&;~{tUwTpb9cjj3XI7-x+qETx6lXXn-_jVGBP}D zxHwbJ1Xu=zrxs3{SFItG^qb)$8PWX7Nw&SI3Rlz6mJra+6|JBd8Of!WMV4V2gzOF= zZCBHf-gn=!@Em(tbiCQtVwQfoC<6vf8F~o%u@ZU`KP(Yz?I*y7j4`G?y0?zobj9Sj zO_P{=KFsNn7Z@FnZXove=E(=RoA#+ctGbhuKEro<>TmTUb=gwvp4qaUKThL5Zf0-p zCH7Q6GLnl8XxDSOTwY7?z3|2xrMrsY2W{ailIkpstXyHniA>@y@p4xEvrO+^jtDOqx&?-HYGk(u4z8&aG5?~3)v zgpOopvMDBcinz3N`;p|Dt7CFZ>K^^n;5s?MR4OOdAFEr1%JHl}yx&D4FW4`3ZwZkX z*(df(8^Ax&t_|bfbiQLb)?)w`fV^C`I7OOpO@b4q(lB+)X-_widA*`(FT8gK$SYU7 z&}7q_4y0*Q_yiR4$&^T7zKuSds<(D|XDuc)PCgHa^{{>iPrJ&=y*gd}h*DQfWOSb{nbl`Nn@T99@jZ=$x|OLk$ur_xZSmaRF5i5aptGg{?sMLBWKW&S~lWs zLM+(WdIT)u6zAdE!4A;ql%E;`XhJ5!e+70rIpz@z7`L!YX*KZ7Ca618LkwfA#+w6{j%hNsz^iMq= z>2UR}A z8MyBY*GPSOV%H%zwMe*ct=C*4Xb&&3s_x^0WA%q%#K&u5q|BiS9WA#@ytXEQ$2HjvXSeFU z%CS3c1Wc9G3MUr$Uwk{b8~S_b_mSYgf~q8hi0__)mujoY_zA&rOX?0L2er5_AUX(v z6F5YdqDfU~f+JWtA>Url#qKM_a`?~VRWN1vPfkbAo=-s~sQtUPnybGg(R&4^A8a9b zvN^sY6}Y&X4ppP`hgszwF7Y5y6-7o;=6+54%i79mk{sUJ8UlT%k!Qofk8j+p);pE!WXd8*8047 zJ#{>nbjYp{uJ$W5Ee1b$KXS-)XDr8U>oCzHi!e1L;k$@7v9-4BqbElG zX-BF5wxe9({a-T3ErW_H5pFWTqr4RxSTxb3Ca#wAtzGr)R)|YCNyDX z(SW?IR`pa#U1~ah5q{XQgd4+R{mQ&oO2{DARwa|$k4N5CKWNJK9(6*Nz&DgO))J(x z>o-(yn;m`D3Gi75KdCNF#?<&N4WahmUB<(Wd&z4BxO!K0*M{CH2Q_JunkQV9%qezD zMaZjpu8S~9Y~9^^Vlq-Mz4l>aO{2DcFUmgD?zai3#|5BAVtl-?|9Q8${A_Y0Z0A!~ z2c36e^~_>Oc1h%&G8nPz{@8G-Bt-jn!v(0VIG%(q*iW4TJH$woDYnCti+E(lF1py* zr|jZ&O1vHL_*9+u*M1WuHWMySP>;0_*PoLoNAiOw2TO`Mx=*h;dgUIFQqk}I$)S2u zi^!+{>ItY}tU#2&uD&7txnKD!WY0Q6be=)YVux=sc9N{&I^P>95yH8bi}CoUFK?1+ zMfZm;NUQv{JVyde?Mx2cdFtLZxyW97DajwczV)Y`(E>fIqwgz9FY?=9mjwy|MA1WLG{GarhaYktOrB!@}&`lE@B;QsgTW`X}+R`}9);Wh!7#q;!=cnbotDdA7IEdrDQ z6!iYbo9FQxmhbL+O82Gg7b7*kFQF@jp|xzkyKq5Hncs%rl%RMP?AS8dS*M?DT36p? z?}M?8lxVp)T)g78wz2X2i#_juN@)N6JmmPdN-+i^{k!drm+UvEHmd&54fBQnV($7T zMBZ8SMmu+DKjI)>sG6|*O;`QDtv)$`)7GCOK<1}%f&EB<(R z{{tp*wSMy>78v+Rx|F6pY>p|_pJ}8yZcupWM)&Hh1|E!+k|FCiYhy^Sz_|iBw_rLt`r1?KZkkbSfU>boU zegwiHMOb)p&2fmXE3wBLN!eG69roR7v*ih46k!eoj~CAgh95?ZD|{`d;@Q8f_miBe z55f^dLRx77MwzT$89_vYOgdrXBVDx${Mas8d(DsBL!twy5r5vVErr!If+)|=JTtqO zt9B;aY4p+=iRa22t^xnwCa<+DxVinW{CxYCf8ww2{@=V}@7v}7kM8>R|I62J&z`+| z`sVf8o51vPp{cN`b%xzU;MvP<_qV?R?!G@XZ^HTDy!p~|j{LrL{hvbp=S}PWu5WyF z=AXc?_%+%8-FGa|K`8r_MQIu!2;W~*Wc_J z4ur8jX6ZJRy|R>zPb%X$^FEoS-##D9v^sI}*`u7IYa9PRXX~3=n6+Hx;{V`p2miiz z_kHnS^YxAYpZWQ2**ypPiJ?b0r0}!kX6A`67w8#g%#i=U+RWR&^V?)b&l%rrOQp6Zq|@^;iGe*8fMplt1A8_p*NDyubEuFJGAA`@a77&*;s- zgtY+Zrcxhv{>hAOno>GG?DBW_{bW4#=YZ<%ebPH_|GTMaxS3geqO?n{)VI|C-81Kl z^F_VP590o&6IEWptm298I-B2C~3utHGi5N2R-5Kw0rRXE6k=fPH4 Y1OL^ED%N|C0^^Rs)78&qol`;+01hKUQvd(} literal 0 HcmV?d00001 diff --git a/docs/user_manual.md b/docs/user_manual.md index 3a451a4..4ff18bf 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -72,3 +72,12 @@ the preview will update. When you are happy with a particular setting/selection, + +## ANALYZE page + +Click `Apply EELI` to run EELI analysis. When the analysis is completed, it is possible to see +the results by selecting the period to visualize using the dropdown menu. + + + + From 7cc071750b6934838ed08c3c6db0a26e3c66d894 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli <100706999+wbaccinelli@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:09:09 +0200 Subject: [PATCH 23/26] Update docs/user_manual.md Co-authored-by: Dani Bodor --- docs/user_manual.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user_manual.md b/docs/user_manual.md index 4ff18bf..798b6a2 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -73,6 +73,8 @@ the preview will update. When you are happy with a particular setting/selection, +When you have selected your period and filters, ensure they are both listed on the right side under "Results", and then click on **`ANALYZE`** at the top of the page. + ## ANALYZE page Click `Apply EELI` to run EELI analysis. When the analysis is completed, it is possible to see From 84cf4fda1eb3ce54084e16da7bc25da8273336b6 Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 15:11:48 +0200 Subject: [PATCH 24/26] adding note to manual --- docs/user_manual.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/user_manual.md b/docs/user_manual.md index 4ff18bf..31a3fb9 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -9,6 +9,8 @@ dashboard remains local on your machine. Note 3: The Dashboard, this manual, and the (back end) software it is built around are works in progress. The images and interaction procedures may be slightly off. Please bear with us. +Note 4: If you encounter any issue while using the dashboard, please report it by opening an issue [here](https://github.com/EIT-ALIVE/eit_dash/issues). + ## LOAD page The dashboard should open on the **LOAD** page, but in case this does not happen, navigate there by clicking **LOAD** at @@ -41,7 +43,7 @@ they _must_ be selected here already. Note that on the following page more precise time selections can be made. This page is just intended to make a rough pre-selection. Also, pre-selections cannot be undone at this point, but do not need to be used for further processing. -When all pre-selections are made, Click **`PRE-PROCESSING`** at the top of the page to proceed to the next stage. +When all pre-selections are made, click **`PRE-PROCESSING`** at the top of the page to proceed to the next stage. ## PRE-PROCESSING page From 3c0b3a7ad966f4c18859a7433760874749ad47ab Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 15:24:28 +0200 Subject: [PATCH 25/26] using available data --- eit_dash/callbacks/analyze_callbacks.py | 51 ++++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 42bcfe5..512d317 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -1,3 +1,5 @@ +import contextlib + import plotly.graph_objects as go from dash import Input, Output, State, callback, ctx from eitprocessing.parameters.eeli import EELI @@ -5,7 +7,7 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles from eit_dash.app import data_object -from eit_dash.definitions.constants import FILTERED_EIT_LABEL +from eit_dash.definitions.constants import FILTERED_EIT_LABEL, RAW_EIT_LABEL from eit_dash.utils.common import ( create_filter_results_card, create_info_card, @@ -45,13 +47,6 @@ def page_setup(_, summary): filter_params = {} for period in data_object.get_all_stable_periods(): - if not filter_params: - filter_params = ( - period.get_data() - .continuous_data.data[FILTERED_EIT_LABEL] - .parameters - ) - summary += [ create_selected_period_card( period.get_data(), @@ -69,7 +64,17 @@ def page_setup(_, summary): }, ) - summary += [create_filter_results_card(filter_params)] + if not filter_params: + try: + filter_params = ( + period.get_data() + .continuous_data.data[FILTERED_EIT_LABEL] + .parameters + ) + except KeyError: + contextlib.suppress(Exception) + if filter_params: + summary += [create_filter_results_card(filter_params)] return summary, options @@ -89,7 +94,12 @@ def apply_eeli(_): for period in periods: sequence = period.get_data() - eeli_result_filtered = EELI().compute_parameter(sequence, FILTERED_EIT_LABEL) + if sequence.continuous_data.get(FILTERED_EIT_LABEL): + eeli_result_filtered = EELI().compute_parameter( + sequence, FILTERED_EIT_LABEL + ) + else: + eeli_result_filtered = EELI().compute_parameter(sequence, RAW_EIT_LABEL) # TODO: the results should be stored in the sequence object eeli_result_filtered["index"] = period.get_period_index() @@ -116,11 +126,16 @@ def show_eeli(selected): if e["index"] == int(selected): result = e + if sequence.continuous_data.get(FILTERED_EIT_LABEL): + data = sequence.continuous_data.get(FILTERED_EIT_LABEL) + else: + data = sequence.continuous_data.get(RAW_EIT_LABEL) + figure.add_trace( go.Scatter( - x=sequence.continuous_data[FILTERED_EIT_LABEL].time, - y=sequence.continuous_data[FILTERED_EIT_LABEL].values, - name=FILTERED_EIT_LABEL, + x=data.time, + y=data.values, + name=data.label, ), ) @@ -128,7 +143,7 @@ def show_eeli(selected): figure.add_hline(y=result["median"], line_color="red", name="Median") figure.add_scatter( - x=sequence.continuous_data[FILTERED_EIT_LABEL].time[result["indices"]], + x=data.time[result["indices"]], y=result["values"], line_color="black", name="EELIs", @@ -140,8 +155,8 @@ def show_eeli(selected): figure.add_trace( go.Scatter( - x=sequence.continuous_data[FILTERED_EIT_LABEL].time, - y=[sd_upper] * len(sequence.continuous_data[FILTERED_EIT_LABEL].time), + x=data.time, + y=[sd_upper] * len(data.time), fill=None, mode="lines", line_color="rgba(0,0,255,0)", # Set to transparent blue @@ -152,8 +167,8 @@ def show_eeli(selected): # Add the lower bound line figure.add_trace( go.Scatter( - x=sequence.continuous_data[FILTERED_EIT_LABEL].time, - y=[sd_lower] * len(sequence.continuous_data[FILTERED_EIT_LABEL].time), + x=data.time, + y=[sd_lower] * len(data.time), fill="tonexty", # Fill area below this line mode="lines", line_color="rgba(0,0,255,0.3)", # Set to semi-transparent blue From d758b17ff30e12e0fdbfe10615eee789119a2e6a Mon Sep 17 00:00:00 2001 From: Walter Baccinelli Date: Thu, 18 Apr 2024 15:25:02 +0200 Subject: [PATCH 26/26] linting --- eit_dash/callbacks/analyze_callbacks.py | 9 ++---- eit_dash/callbacks/preprocessing_callbacks.py | 28 ++++--------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/eit_dash/callbacks/analyze_callbacks.py b/eit_dash/callbacks/analyze_callbacks.py index 512d317..bd6cfed 100644 --- a/eit_dash/callbacks/analyze_callbacks.py +++ b/eit_dash/callbacks/analyze_callbacks.py @@ -66,11 +66,7 @@ def page_setup(_, summary): if not filter_params: try: - filter_params = ( - period.get_data() - .continuous_data.data[FILTERED_EIT_LABEL] - .parameters - ) + filter_params = period.get_data().continuous_data.data[FILTERED_EIT_LABEL].parameters except KeyError: contextlib.suppress(Exception) if filter_params: @@ -96,7 +92,8 @@ def apply_eeli(_): sequence = period.get_data() if sequence.continuous_data.get(FILTERED_EIT_LABEL): eeli_result_filtered = EELI().compute_parameter( - sequence, FILTERED_EIT_LABEL + sequence, + FILTERED_EIT_LABEL, ) else: eeli_result_filtered = EELI().compute_parameter(sequence, RAW_EIT_LABEL) diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 8e4c60e..11c4e65 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -65,10 +65,7 @@ def create_resampling_card(loaded_data): for data in loaded_data ] - options = [ - {"label": f'{data["Name"]}', "value": str(i)} - for i, data in enumerate(loaded_data) - ] + options = [{"label": f'{data["Name"]}', "value": str(i)} for i, data in enumerate(loaded_data)] return row, options @@ -79,10 +76,7 @@ def get_loaded_data(): for dataset in loaded_data: name = dataset.label if dataset.continuous_data: - data += [ - {"Name": name, "Data type": channel} - for channel in dataset.continuous_data - ] + data += [{"Name": name, "Data type": channel} for channel in dataset.continuous_data] if dataset.eit_data: data.append( { @@ -226,10 +220,7 @@ def populate_periods_selection_modal(method): if int_value == PeriodsSelectMethods.Manual.value: signals = data_object.get_all_sequences() - options = [ - {"label": sequence.label, "value": index} - for index, sequence in enumerate(signals) - ] + options = [{"label": sequence.label, "value": index} for index, sequence in enumerate(signals)] body = [ html.H6("Select one dataset"), @@ -467,11 +458,7 @@ def remove_period(n_clicks, container, figure): # remove from the figure (if the figure exists) try: - figure["data"] = [ - trace - for trace in figure["data"] - if "meta" not in trace or trace["meta"]["uid"] != input_id - ] + figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != input_id] except TypeError: contextlib.suppress(Exception) @@ -577,12 +564,9 @@ def enable_apply_button( if ( (int(filter_selected) == FilterTypes.lowpass.value and co_high and co_high > 0) + or (int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0) or ( - int(filter_selected) == FilterTypes.highpass.value and co_low and co_low > 0 - ) - or ( - int(filter_selected) - in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] + int(filter_selected) in [FilterTypes.bandpass.value, FilterTypes.bandstop.value] and co_low and co_low > 0 and co_high