diff --git a/eit_dash/callbacks/preprocessing_callbacks.py b/eit_dash/callbacks/preprocessing_callbacks.py index 1fe1522..8bad26b 100644 --- a/eit_dash/callbacks/preprocessing_callbacks.py +++ b/eit_dash/callbacks/preprocessing_callbacks.py @@ -1,18 +1,32 @@ +from __future__ import annotations + +import contextlib +import time +from typing import TYPE_CHECKING + import dash_bootstrap_components as dbc +import plotly.graph_objects as go from dash import ALL, Input, Output, State, callback, ctx, dcc, html from dash.exceptions import PreventUpdate -from eitprocessing.datahandling.sequence import Sequence +from eitprocessing.datahandling.continuousdata import ContinuousData +from eitprocessing.filters.butterworth_filters import ButterworthFilter 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 PeriodsSelectMethods +from eit_dash.definitions.option_lists import FilterTypes, PeriodsSelectMethods from eit_dash.utils.common import ( create_slider_figure, get_selections_slidebar, get_signal_options, mark_selected_periods, ) +from eit_dash.utils.data_singleton import LoadedData + +if TYPE_CHECKING: + from eitprocessing.datahandling.sequence import Sequence + +tmp_results: LoadedData = LoadedData() # ruff: noqa: D103 #TODO remove this line when finalizing this module @@ -91,6 +105,21 @@ def create_selected_period_card(period: Sequence, dataset: str, index: int) -> d ) +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 = [] @@ -157,7 +186,6 @@ def load_datasets(title): [ Output(ids.OPEN_SYNCH_BUTTON, "disabled"), Output(ids.OPEN_SELECT_PERIODS_BUTTON, "disabled"), - Output(ids.OPEN_FILTER_DATA_BUTTON, "disabled"), Output(ids.SUMMARY_COLUMN, "children"), Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), ], @@ -190,7 +218,7 @@ def update_summary(start, summary): create_selected_period_card(data, data.label, p.get_period_index()), ) - return False, False, False, summary, results + return False, False, summary, results @callback( @@ -325,6 +353,9 @@ def initialize_figure( if saved_periods := data_object.get_dataset_stable_periods(int(dataset)): current_figure = mark_selected_periods(current_figure, saved_periods) + # THIS IS A TEMPORARY PATCH + time.sleep(2) + return current_figure, style @@ -369,7 +400,7 @@ def select_period( start_sample = data.time[0] stop_sample = data.time[-1] - period_index = data_object.get_stable_periods_list_length() + period_index = data_object.get_next_period_index() cut_data = data.select_by_time( start_time=start_sample, @@ -380,7 +411,10 @@ def select_period( data_object.add_stable_period(cut_data, int(dataset)) # TODO: explore Patch https://dash.plotly.com/partial-properties - current_figure = mark_selected_periods(current_figure, [cut_data], period_index) + current_figure = mark_selected_periods( + current_figure, + [data_object.get_stable_period(period_index)], + ) # TODO: refactor to avoid duplications ok = [options[s]["label"] for s in signals] @@ -459,14 +493,359 @@ def remove_period(n_clicks, container, figure): if all(element is None for element in n_clicks): raise PreventUpdate - input_id = ctx.triggered_id["index"] + input_id = int(ctx.triggered_id["index"]) # remove from the singleton - data_object.remove_stable_period(int(input_id)) + data_object.remove_stable_period(input_id) + + # remove from the temp data, if present + if tmp_results: + try: + tmp_results.remove_stable_period(input_id) + except ValueError: + contextlib.suppress(Exception) - # remove from the figure - figure["data"] = [trace for trace in figure["data"] if "meta" not in trace or trace["meta"]["uid"] != int(input_id)] + # 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] + except TypeError: + contextlib.suppress(Exception) results = [card for card in container if f"'index': '{input_id}'" not in str(card)] return results, figure + + +# filters +@callback( + Output(ids.OPEN_FILTER_DATA_BUTTON, "disabled"), + Input(ids.PREPROCESING_RESULTS_CONTAINER, "children"), + prevent_initial_call=True, +) +def enable_filter_button(results): + """Enable the button for opening the filter modal.""" + if results: + return False + return True + + +@callback( + [ + Output(ids.FILTERING_SELECTION_POPUP, "is_open"), + Output(ids.FILTER_SELECTOR, "value"), + ], + [ + Input(ids.OPEN_FILTER_DATA_BUTTON, "n_clicks"), + Input(ids.FILTERING_CLOSE_BUTTON, "n_clicks"), + ], + prevent_initial_call=True, +) +def open_filtering_modal(open_click, confirm_click) -> bool: + """open/close modal dialog for filtering data.""" + trigger = ctx.triggered_id + + if trigger == ids.OPEN_FILTER_DATA_BUTTON: + return True, None + + return False, None + + +@callback( + [ + Output(ids.FILTER_PARAMS, "hidden"), + Output(ids.FILTER_CUTOFF_LOW, "disabled"), + Output(ids.FILTER_CUTOFF_HIGH, "disabled"), + Output(ids.FILTER_CUTOFF_LOW, "value"), + Output(ids.FILTER_CUTOFF_HIGH, "value"), + ], + Input(ids.FILTER_SELECTOR, "value"), + prevent_initial_call=True, +) +def show_filters_params(selected): + """Make visible the div containing the filters params.""" + cutoff_low_enabled = cutoff_high_enabled = filter_params = False + cutoff_low_val = cutoff_high_val = None + + # if no filter has been selected, hide the params + if not selected: + filter_params = True + elif int(selected) == FilterTypes.lowpass.value: + cutoff_low_enabled = True + elif int(selected) == FilterTypes.highpass.value: + cutoff_high_enabled = True + + return ( + filter_params, + cutoff_low_enabled, + cutoff_high_enabled, + cutoff_low_val, + cutoff_high_val, + ) + + +@callback( + Output(ids.FILTER_APPLY, "disabled"), + [ + Input(ids.FILTER_CUTOFF_LOW, "value"), + Input(ids.FILTER_CUTOFF_HIGH, "value"), + Input(ids.FILTER_ORDER, "value"), + ], + [ + State(ids.FILTER_CUTOFF_LOW, "value"), + State(ids.FILTER_CUTOFF_HIGH, "value"), + State(ids.FILTER_ORDER, "value"), + State(ids.FILTER_SELECTOR, "value"), + ], + prevent_initial_call=True, +) +def enable_apply_button( + co_low_in, + co_high_in, + order_in, + co_low, + co_high, + order, + filter_selected, +): + """Enable the apply button.""" + if not filter_selected: + return True + + 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 + and co_low > 0 + ) + ) and order: + return False + + return True + + +@callback( + [ + Output(ids.FILTERING_RESULTS_DIV, "hidden", allow_duplicate=True), + Output(ids.FILTERING_CONFIRM_DIV, "hidden", allow_duplicate=True), + Output(ids.FILTERING_SELECT_PERIOD_VIEW, "options", allow_duplicate=True), + ], + [ + Input(ids.FILTER_APPLY, "disabled"), + ], + prevent_initial_call=True, +) +def disable_results(disabled): + """Hide and disable results if the apply button is disabled.""" + # flag for showing graphs and confirm button + if not disabled: + raise PreventUpdate + + hidden_div = True + + options = [] + + return hidden_div, hidden_div, options + + +@callback( + [ + Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), + Output(ids.FILTERING_RESULTS_DIV, "hidden", allow_duplicate=True), + Output(ids.FILTERING_CONFIRM_DIV, "hidden", allow_duplicate=True), + Output(ids.FILTERING_SELECT_PERIOD_VIEW, "options", allow_duplicate=True), + Output(ids.ALERT_FILTER, "is_open"), + Output(ids.ALERT_FILTER, "children"), + Output("update-filter-results", "children"), + ], + [ + Input(ids.FILTER_APPLY, "n_clicks"), + ], + [ + State(ids.FILTER_CUTOFF_LOW, "value"), + State(ids.FILTER_CUTOFF_HIGH, "value"), + State(ids.FILTER_ORDER, "value"), + State(ids.FILTER_SELECTOR, "value"), + State(ids.PREPROCESING_RESULTS_CONTAINER, "children"), + ], + prevent_initial_call=True, +) +def apply_filter(_, co_low, co_high, order, filter_selected, results): + """Apply the filter.""" + global tmp_results # noqa: PLW0602 + # flag for the alert message + show_alert = False + # alert message + alert_msg = "" + + # placeholder to allow results update + placeholder_div = "updated" + + # flag for showing graphs and confirm button + hidden_div = False + + # build filter params + filter_params = get_selected_parameters(co_high, co_low, order, filter_selected) + + options = [] + + tmp_results.clear_data() + + # filter all the periods + try: + for period in data_object.get_all_stable_periods(): + filtered_data = filter_data(period.get_data(), filter_params) + data = period.get_data() + data.continuous_data.add(filtered_data) + tmp_results.add_stable_period( + data, + 0, + period.get_period_index(), + ) + + options.append( + { + "label": f"Period {period.get_period_index()}", + "value": period.get_period_index(), + }, + ) + except ValueError as e: + show_alert = True + alert_msg = f"{e}" + hidden_div = True + placeholder_div = None + + return ( + results, + hidden_div, + hidden_div, + options, + show_alert, + alert_msg, + placeholder_div, + ) + + +@callback( + [ + Output(ids.FILTERING_RESULTS_GRAPH, "figure"), + Output(ids.FILTERING_RESULTS_GRAPH, "style"), + ], + Input(ids.FILTERING_SELECT_PERIOD_VIEW, "value"), + Input("update-filter-results", "children"), + State(ids.FILTERING_SELECT_PERIOD_VIEW, "value"), + prevent_initial_call=True, +) +def show_filtered_results(_, update, selected): + """When selecting a period, shows the original and the filtered signal.""" + if not selected or not update: + raise PreventUpdate + + fig = go.Figure() + + try: + filtered_data = tmp_results.get_stable_period(int(selected)).get_data() + except Exception: + return fig, styles.EMPTY_ELEMENT + + data = data_object.get_stable_period(int(selected)).get_data() + + fig.add_trace( + go.Scatter( + x=data.eit_data.data["raw"].time, + y=data.eit_data.data["raw"].global_impedance, + name="Original signal", + ), + ) + + 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, + name="Filtered signal", + ), + ) + + return fig, styles.GRAPH + + +@callback( + [ + Output(ids.PREPROCESING_RESULTS_CONTAINER, "children", allow_duplicate=True), + Output(ids.ALERT_SAVED_RESULTS, "is_open"), + Output(ids.ALERT_SAVED_RESULTS, "children"), + ], + Input(ids.FILTERING_CONFIRM_BUTTON, "n_clicks"), + State(ids.PREPROCESING_RESULTS_CONTAINER, "children"), + prevent_initial_call=True, +) +def save_filtered_signal(confirm, results: list): + """When clocking the confirm button, store the results in the singleton.""" + params = {} + + # save the filtered data + for res in tmp_results.get_all_stable_periods(): + data = data_object.get_stable_period(res.get_period_index()) + tmp_data = res.get_data() + data.update_data(tmp_data) + + if not params: + params = tmp_data.continuous_data.data["global_impedance_filtered"].parameters + + # show info card + for element in results: + if element["props"]["id"] == ids.FILTERING_SAVED_CARD: + results.remove(element) + results += [create_filter_results_card(params)] + + return results, True, "Results have been saved" + + +def get_selected_parameters(co_high, co_low, order, filter_selected) -> dict: + """Build the parameters dictionary for the filter. + + Args: + co_high: cut off upper limit + co_low: cut off lower limit + order: filter order + filter_selected: value coming from the filter selection dropbox + + Returns: dictionary containing parameters for the filter + """ + if co_high is None: + cutoff_frequency = co_low + elif co_low is None: + cutoff_frequency = co_high + else: + cutoff_frequency = [co_low, co_high] + + return { + "filter_type": FilterTypes(int(filter_selected)).name, + "cutoff_frequency": cutoff_frequency, + "order": order, + } + + +def filter_data(data: Sequence, filter_params: dict) -> ContinuousData | None: + """Filter the impedance data in a period. + + Args: + data: sequence containing the data + filter_params: parameters for the filter + + Returns: the data with the filtered version added + """ + filter_params["sample_frequency"] = data.eit_data.data["raw"].framerate + + filt = ButterworthFilter(**filter_params) + + return ContinuousData( + "global_impedance_filtered", + f"global_impedance filtered with {filter_params['filter_type']}", + "a.u.", + "impedance", + parameters=filter_params, + time=data.eit_data.data["raw"].time, + values=filt.apply_filter(data.eit_data.data["raw"].global_impedance), + ) diff --git a/eit_dash/definitions/element_ids.py b/eit_dash/definitions/element_ids.py index 110a3f0..6e2b168 100644 --- a/eit_dash/definitions/element_ids.py +++ b/eit_dash/definitions/element_ids.py @@ -19,9 +19,25 @@ SELECT_CONFIRM_BUTTON = "select-confirm-button" # preprocessing page +ALERT_FILTER = "alert-filter" +ALERT_SAVED_RESULTS = "alert-saved-results" CONFIRM_RESAMPLING_BUTTON = "confirm-resampling-button" CONFIRM_SYNCH_BUTTON = "confirm-synch-button" DATASET_SELECTION_CHECKBOX = "dataset-selection-checkbox" +FILTER_APPLY = "filter-apply" +FILTER_ORDER = "filter-order" +FILTER_CUTOFF_LOW = "filter-cutoff-low" +FILTER_CUTOFF_HIGH = "filter-cutoff-high" +FILTERING_CLOSE_BUTTON = "filtering-close-button" +FILTERING_CONFIRM_BUTTON = "filtering-confirm-button" +FILTERING_CONFIRM_DIV = "filtering-confirm-div" +FILTER_PARAMS = "filter-params" +FILTERING_SELECTION_POPUP = "filtering-selection-popup" +FILTER_SELECTOR = "filter-selector" +FILTERING_SELECT_PERIOD_VIEW = "filtering-select-period-view" +FILTERING_RESULTS_DIV = "filtering-results-div" +FILTERING_RESULTS_GRAPH = "filtering-results-graph" +FILTERING_SAVED_CARD = "filtering-saved-card" SYNCHRONIZATION_POPUP = "synchronization-popup" SYNCHRONIZATION_CONFIRM_BUTTON = "synchronization-confirm-button" SYNC_DATA_PREVIEW_CONTAINER = "sync-data-preview-container" diff --git a/eit_dash/definitions/option_lists.py b/eit_dash/definitions/option_lists.py index 5ffe9e5..5a15d4b 100644 --- a/eit_dash/definitions/option_lists.py +++ b/eit_dash/definitions/option_lists.py @@ -1,5 +1,7 @@ from enum import Enum +from eitprocessing.filters.butterworth_filters import FILTER_TYPES + class InputFiletypes(Enum): """One hot encoding of input file types (EIT vendors).""" @@ -11,6 +13,12 @@ class InputFiletypes(Enum): Poly5 = 4 +# create the filters enum, according to what has been defined in the filters module +filters = {value: idx for idx, value in enumerate(FILTER_TYPES) if FILTER_TYPES[value].available_in_gui} + +FilterTypes = Enum("FilterTypes", filters) + + class SignalSelections(Enum): """One hot encoding of selectable signals.""" diff --git a/eit_dash/pages/preprocessing.py b/eit_dash/pages/preprocessing.py index 652038c..b98beef 100644 --- a/eit_dash/pages/preprocessing.py +++ b/eit_dash/pages/preprocessing.py @@ -3,7 +3,11 @@ import eit_dash.definitions.element_ids as ids import eit_dash.definitions.layout_styles as styles -from eit_dash.definitions.option_lists import PeriodsSelectMethods, SynchMethods +from eit_dash.definitions.option_lists import ( + FilterTypes, + PeriodsSelectMethods, + SynchMethods, +) register_page(__name__, path="/preprocessing") @@ -71,7 +75,7 @@ ), html.P(), dbc.Row( - dbc.Button("Filter data", id=ids.OPEN_FILTER_DATA_BUTTON, disabled=False), + dbc.Button("Filter data", id=ids.OPEN_FILTER_DATA_BUTTON, disabled=True), ), ], ) @@ -178,7 +182,7 @@ ), dbc.ModalFooter( dbc.Button( - "Close", + "Confirm", id=ids.PERIODS_CONFIRM_BUTTON, className="ms-auto", n_clicks=0, @@ -195,6 +199,134 @@ ], ) +alert_filter = dbc.Alert( + [], + id=ids.ALERT_FILTER, + color="danger", + dismissable=True, + is_open=False, + duration=3000, +) + +alert_saved_results = dbc.Alert( + [], + id=ids.ALERT_SAVED_RESULTS, + color="success", + dismissable=True, + is_open=False, + duration=3000, +) + +filter_params = html.Div( + [ + dbc.Row(alert_filter), + dbc.Row( + [ + dbc.Col( + [ + html.P("Filter Order"), + dbc.Input(id=ids.FILTER_ORDER, type="number", min=0), + ], + ), + dbc.Col( + [ + html.P("Cut off frequency low"), + dbc.Input(id=ids.FILTER_CUTOFF_LOW, type="number", min=0), + ], + ), + dbc.Col( + [ + html.P("Cut off frequency high"), + dbc.Input(id=ids.FILTER_CUTOFF_HIGH, type="number", min=0), + ], + ), + ], + ), + dbc.Row( + [ + dbc.Col( + [ + dbc.Button( + "Apply", + id=ids.FILTER_APPLY, + disabled=True, + ), + ], + ), + ], + style=styles.BUTTONS_ROW, + ), + dbc.Row( + [ + html.Div( + [ + html.H6("Select a period to view the results"), + dbc.Select(id=ids.FILTERING_SELECT_PERIOD_VIEW), + dcc.Graph( + id=ids.FILTERING_RESULTS_GRAPH, + style=styles.EMPTY_ELEMENT, + ), + ], + id=ids.FILTERING_RESULTS_DIV, + hidden=True, + ), + ], + style=styles.BUTTONS_ROW, + ), + dbc.Row( + [ + html.Div( + [ + dbc.Button("Confirm", id=ids.FILTERING_CONFIRM_BUTTON), + ], + id=ids.FILTERING_CONFIRM_DIV, + hidden=True, + ), + ], + style=styles.BUTTONS_ROW, + ), + ], + id=ids.FILTER_PARAMS, + hidden=True, +) + +modal_filtering = html.Div( + [ + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Filter"), close_button=True), + dbc.ModalBody( + [ + alert_saved_results, + html.H6("Select a filter"), + dbc.Select( + id=ids.FILTER_SELECTOR, + options=[{"label": filt.name, "value": filt.value} for filt in FilterTypes], + ), + html.P(), + filter_params, + ], + ), + dbc.ModalFooter( + dbc.Button( + "Close", + id=ids.FILTERING_CLOSE_BUTTON, + className="ms-auto", + n_clicks=0, + ), + ), + html.Div(id="update-filter-results", hidden=True), + ], + id=ids.FILTERING_SELECTION_POPUP, + centered=True, + is_open=False, + backdrop=False, + scrollable=True, + size="xl", + ), + ], +) + layout = dbc.Row( [ html.H1("PRE-PROCESSING", style=styles.COLUMN_TITLE), @@ -203,5 +335,6 @@ results, modal_synchronization, modal_selection, + modal_filtering, ], ) diff --git a/eit_dash/utils/common.py b/eit_dash/utils/common.py index 4d13c41..874f8d0 100644 --- a/eit_dash/utils/common.py +++ b/eit_dash/utils/common.py @@ -7,6 +7,8 @@ if TYPE_CHECKING: from eitprocessing.sequence import Sequence + from eit_dash.utils.data_singleton import Period + def blank_fig(): """Create an empty figure.""" @@ -102,8 +104,7 @@ def create_slider_figure( def mark_selected_periods( original_figure: go.Figure | dict, - periods: list[Sequence], - period_index: int, + periods: list[Period], ) -> go.Figure: """ Create the figure for the selection of range. @@ -115,12 +116,13 @@ def mark_selected_periods( period_index: index of the selected period """ for period in periods: - for eit_variant in period.eit_data: + seq = period.get_data() + for eit_variant in seq.eit_data: selected_impedance = go.Scatter( - x=period.eit_data[eit_variant].time, - y=period.eit_data[eit_variant].global_impedance, + x=seq.eit_data[eit_variant].time, + y=seq.eit_data[eit_variant].global_impedance, name=eit_variant, - meta={"uid": period_index}, + meta={"uid": period.get_period_index()}, line={"color": "black"}, showlegend=False, ).to_plotly_json() @@ -130,12 +132,12 @@ def mark_selected_periods( else: original_figure["data"].append(selected_impedance) - for n, cont_signal in enumerate(period.continuous_data): + for n, cont_signal in enumerate(seq.continuous_data): selected_signal = go.Scatter( - x=period.continuous_data[cont_signal].time, - y=period.continuous_data[cont_signal].values, + x=seq.continuous_data[cont_signal].time, + y=seq.continuous_data[cont_signal].values, name=cont_signal, - meta={"uid": period_index}, + meta={"uid": period.get_period_index()}, opacity=0.5, yaxis=f"y{n + 2}", line={"color": "black"}, diff --git a/eit_dash/utils/data_singleton.py b/eit_dash/utils/data_singleton.py index 3bd426a..0536117 100644 --- a/eit_dash/utils/data_singleton.py +++ b/eit_dash/utils/data_singleton.py @@ -90,7 +90,7 @@ def add_stable_period( raise ValueError(msg) if not period_index: - period_index = self.get_stable_periods_list_length() + period_index = self.get_next_period_index() # check that the index doesn't exist for period in self._stable_periods: @@ -111,7 +111,7 @@ def remove_stable_period(self, index: int): self._stable_periods.remove(period) return - msg = f"Period with index {index}not found" + msg = f"Period with index {index} not found" raise ValueError(msg) def get_stable_periods_list_length(self): @@ -129,7 +129,7 @@ def get_dataset_stable_periods(self, dataset_index: int) -> list[Sequence]: msg = f"Index higher than list length {self.get_sequence_list_length()}" raise ValueError(msg) - return [period.get_data() for period in self._stable_periods if period.get_dataset_index() == dataset_index] + return [period for period in self._stable_periods if period.get_dataset_index() == dataset_index] def get_all_stable_periods(self) -> list[Period]: """Retrieve all the saved stable periods. @@ -138,6 +138,29 @@ def get_all_stable_periods(self) -> list[Period]: """ return self._stable_periods + def get_stable_period(self, index: int): + """Get a stable period from the singleton, using its index. + + Args: + index: index of the sable period to be retrieved. + """ + for period in self._stable_periods: + if period.get_period_index() == index: + return period + + msg = f"Period with index {index} not found" + raise ValueError(msg) + + def get_stable_periods_indexes(self) -> list[int]: + """Get a list of the indexes of the stable periods currently available.""" + return [period.get_period_index() for period in self._stable_periods] + + def get_next_period_index(self): + """Determines the index to be assigned to the next stable period.""" + available_indexes = self.get_stable_periods_indexes() + + return max(available_indexes) + 1 if available_indexes else 0 + @dataclass class Period: @@ -179,3 +202,11 @@ def set_data(self, data: Sequence, dataset_index: int, period_index: int): self._data = data self._dataset_index = dataset_index self._period_index = period_index + + def update_data(self, data: Sequence) -> Sequence: + """Update the data of a period. + + Args: + data: The sequence with the updated period data + """ + self._data = data