From f6e02a86781eee149db1c7beb128949f9d7c0594 Mon Sep 17 00:00:00 2001 From: mroncera Date: Fri, 3 May 2024 11:54:47 +0200 Subject: [PATCH] =?UTF-8?q?FIX=20-=20Export=20CLTF/NEMI=20feature,=20add?= =?UTF-8?q?=20some=20strategies=20to=20display=20PSD=20as=20a=20V=C2=B2/Hz?= =?UTF-8?q?=20instead=20of=20V/sqrt(Hz)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/JUICE.json | 4 +- src/controler/controller.py | 88 +++++- src/model/strategies/strategy_lib/CLTF.py | 6 +- src/model/strategies/strategy_lib/Noise.py | 325 +++++++++++++++++++++ src/view/gui.py | 14 + 5 files changed, 425 insertions(+), 12 deletions(-) diff --git a/data/JUICE.json b/data/JUICE.json index 72c8468..2893163 100644 --- a/data/JUICE.json +++ b/data/JUICE.json @@ -248,5 +248,7 @@ "input_unit": "", "target_unit": "" } - } + }, + "SPICE_circuit": "NO Circuit Selected" + } \ No newline at end of file diff --git a/src/controler/controller.py b/src/controler/controller.py index 6a48223..0229283 100644 --- a/src/controler/controller.py +++ b/src/controler/controller.py @@ -5,9 +5,13 @@ import importlib import json +import numpy as np + from src.model.strategies.strategy_lib.Noise import PSD_R_cr, PSD_R_cr_filtered, PSD_R_Coil, PSD_R_Coil_filtered, \ PSD_Flicker, PSD_e_en, PSD_e_en_filtered, PSD_e_in, PSD_e_in_filtered, PSD_Total, PSD_Total_filtered, \ - Display_all_PSD, NEMI, Display_all_PSD_filtered, NEMI_FIltered, NEMI_FIlteredv2, NEMI_FIlteredv3 + Display_all_PSD, NEMI, Display_all_PSD_filtered, NEMI_FIltered, NEMI_FIlteredv2, NEMI_FIlteredv3, PSD_R_cr_V2, \ + PSD_R_cr_filtered_V2, PSD_R_Coil_V2, PSD_R_Coil_filtered_V2, PSD_Flicker_V2, PSD_e_en_V2, PSD_e_en_filtered_V2, \ + PSD_e_in_V2, PSD_e_in_filtered_V2 from src.model.strategies.strategy_lib.CLTF import CLTF_Strategy_Filtered, \ CLTF_Strategy_Non_Filtered_legacy, Display_CLTF_OLTF from src.model.strategies.strategy_lib.OLTF import OLTF_Strategy_Non_Filtered, OLTF_Strategy_Filtered @@ -95,39 +99,39 @@ }, "PSD_R_cr": { "default": PSD_R_cr, - "strategies": [PSD_R_cr] + "strategies": [PSD_R_cr, PSD_R_cr_V2] }, "PSD_R_cr_filtered": { "default": PSD_R_cr_filtered, - "strategies": [PSD_R_cr_filtered] + "strategies": [PSD_R_cr_filtered,PSD_R_cr_filtered_V2] }, "PSD_R_Coil": { "default": PSD_R_Coil, - "strategies": [PSD_R_Coil] + "strategies": [PSD_R_Coil, PSD_R_Coil_V2] }, "PSD_R_Coil_filtered": { "default": PSD_R_Coil_filtered, - "strategies": [PSD_R_Coil_filtered] + "strategies": [PSD_R_Coil_filtered, PSD_R_Coil_filtered_V2] }, "PSD_Flicker": { "default": PSD_Flicker, - "strategies": [PSD_Flicker] + "strategies": [PSD_Flicker, PSD_Flicker_V2] }, "PSD_e_en": { "default": PSD_e_en, - "strategies": [PSD_e_en] + "strategies": [PSD_e_en, PSD_e_en_V2] }, "PSD_e_en_filtered": { "default": PSD_e_en_filtered, - "strategies": [PSD_e_en_filtered] + "strategies": [PSD_e_en_filtered, PSD_e_en_filtered_V2] }, "PSD_e_in": { "default": PSD_e_in, - "strategies": [PSD_e_in] + "strategies": [PSD_e_in, PSD_e_in_V2] }, "PSD_e_in_filtered": { "default": PSD_e_in_filtered, - "strategies": [PSD_e_in_filtered] + "strategies": [PSD_e_in_filtered, PSD_e_in_filtered_V2] }, "PSD_Total": { "default": PSD_Total, @@ -282,3 +286,67 @@ def set_node_strategy(self, node_name, strategy_class, params_dict): print(strategy_instance) self.engine.swap_strategy_for_node(node_name, strategy_instance, params_dict) + def export_CLTF_NEMI(self, path): + """ + IF the node is NEMI, and the node CLTF_filtered, plot both in the same graph and save it in the path + :param path: Full path to save the graph + :return: SUCCESS or ERROR + """ + + # Retrieve the results of the NEMI node if it exists + data = self.get_current_results() + if data is None: + raise "Error: No data available" + + # Retrieve the results of the CLTF_Filtered node if it exists + try: + cltf_data = data.get("CLTF_Filtered", None) + nemi_data = data.get("NEMI", None) + + except KeyError: + raise "Error: Missing data for CLTF_Filtered or NEMI" + + try : + # Plot the data + import matplotlib.pyplot as plt + freq_vector = cltf_data["data"][:,0] + cltf_vector = 20*np.log(cltf_data["data"][:,1]) + nemi_vector = nemi_data["data"][:,1] + + fig, ax1 = plt.subplots() + color = 'tab:red' + ax1.set_xlabel('Frequency (Hz)') + ax1.semilogx() + ax1.semilogy() + + ax1.set_ylabel('NEMI', color=color) + ax1.plot(freq_vector, nemi_vector, color=color) + ax1.tick_params(axis='y', labelcolor=color) + + ax2 = ax1.twinx() + ax2.semilogx() + color = 'tab:blue' + + ax2.set_ylabel('CLTF (dB)', color=color) + ax2.plot(freq_vector, cltf_vector, color=color) + ax2.tick_params(axis='y', labelcolor=color) + + # add grid + ax1.grid("both") + ax2.grid("both") + + path = str(path) + + #if the path does not end with .png + if not path.endswith(".png"): + path += ".png" + + print("Saving plot to : " + path) + + plt.savefig(path) + + + return "SUCCESS" + except Exception as e: + return "ERROR Plotting data : " + str(e) + diff --git a/src/model/strategies/strategy_lib/CLTF.py b/src/model/strategies/strategy_lib/CLTF.py index 298e325..41be047 100644 --- a/src/model/strategies/strategy_lib/CLTF.py +++ b/src/model/strategies/strategy_lib/CLTF.py @@ -25,7 +25,11 @@ def calculate(self, dependencies: dict, parameters: InputParameters): oltf_values = vectorized_oltf(nb_spire, ray_spire, mu_app, frequency_vector, TF_ASIC_Stage_1_linear, inductance, capacitance, resistance, mutual_inductance, feedback_resistance) frequency_oltf_tensor = np.column_stack((frequency_vector, oltf_values)) - return frequency_oltf_tensor + return { + "data": frequency_oltf_tensor, + "labels": ["Frequency", "Gain"], + "units": ["Hz", ""] + } def calculate_cltf(self, nb_spire, diff --git a/src/model/strategies/strategy_lib/Noise.py b/src/model/strategies/strategy_lib/Noise.py index 4289d51..005b951 100644 --- a/src/model/strategies/strategy_lib/Noise.py +++ b/src/model/strategies/strategy_lib/Noise.py @@ -31,6 +31,35 @@ def calculate_psd(self, temperature, feedback_resistance): def get_dependencies(): return ['temperature', "feedback_resistance", "frequency_vector"] +class PSD_R_cr_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + temperature = parameters.data['temperature'] + feedback_resistance = parameters.data['feedback_resistance'] + + frequency_vector = dependencies['frequency_vector']["data"] + result = self.calculate_psd(temperature, feedback_resistance) + + result = result**2 + + ones = np.ones(len(frequency_vector)) + result = result * ones + results = np.column_stack((frequency_vector, result)) + + return { + "data": results, + "labels": ["Frequency", "PSD_R_cr"], + "units": ["Hz", "V²/Hz"] + } + + def calculate_psd(self, temperature, feedback_resistance): + result = 4 * k * temperature * feedback_resistance + return result ** 0.5 + + @staticmethod + def get_dependencies(): + return ['temperature', "feedback_resistance", "frequency_vector"] + class PSD_R_cr_filtered(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): @@ -52,6 +81,28 @@ def calculate(self, dependencies: dict, parameters: InputParameters): def get_dependencies(): return ['TF_ASIC_Stage_2', "PSD_R_cr",] +class PSD_R_cr_filtered_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_R_cr_non_filtered = 20*np.log10(dependencies['PSD_R_cr']["data"][:,1]) + TF_ASIC_Stage_2 = 20*np.log10(dependencies['TF_ASIC_Stage_2']["data"][:,1]) + + result = (PSD_R_cr_non_filtered + TF_ASIC_Stage_2) + result = 10**(result/20) + result = result**2 + results = np.column_stack((dependencies['PSD_R_cr']["data"][:,0], result)) + + return { + "data": results, + "labels": ["Frequency", "PSD_R_cr"], + "units": ["Hz", "V²/Hz"] + } + + + @staticmethod + def get_dependencies(): + return ['TF_ASIC_Stage_2', "PSD_R_cr",] + class PSD_R_Coil(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): @@ -114,6 +165,72 @@ def get_dependencies(): return ['TF_ASIC_Stage_2', "PSD_R_Coil"] +class PSD_R_Coil_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + temperature = parameters.data['temperature'] + mutual_inductance = parameters.data['mutual_inductance'] + feedback_resistance = parameters.data['feedback_resistance'] + + resistance = dependencies['resistance']["data"] + frequency_vector = dependencies['frequency_vector']["data"] + TF_ASIC_Stage_1 = dependencies['TF_ASIC_Stage_1']["data"][:, 1] + inductance = dependencies['inductance']["data"] + capacitance = dependencies['capacitance']["data"] + + vectorized_psd_r_coil = np.vectorize(self.calculate_psd) + psd_r_coil_values = vectorized_psd_r_coil(temperature, resistance, k, frequency_vector, TF_ASIC_Stage_1, + inductance, capacitance, mutual_inductance, feedback_resistance) + psd_r_coil_values = psd_r_coil_values ** 2 + frequency_psd_r_coil_tensor = np.column_stack((frequency_vector, psd_r_coil_values)) + results = frequency_psd_r_coil_tensor + + return { + "data": results, + "labels": ["Frequency", "PSD_R_Coil"], + "units": ["Hz", "V²/Hz"] + } + + def calculate_psd(self, temperature, resistance, k, f, TF_ASIC_Stage_1_point, inductance, capacitance, + mutual_inductance, feedback_resistance): + psd_r_coil_num = (4 * k * temperature * resistance) * TF_ASIC_Stage_1_point ** 2 + psd_r_coil_den = ( + (1 - inductance * capacitance * (2 * np.pi * f) ** 2) ** 2 + + ((resistance * capacitance * 2 * np.pi * f) + ( + (TF_ASIC_Stage_1_point * mutual_inductance * 2 * np.pi * f) / feedback_resistance)) ** 2 + ) + + result = (psd_r_coil_num / psd_r_coil_den) ** 0.5 + return result + + @staticmethod + def get_dependencies(): + return ['temperature', "feedback_resistance", "frequency_vector", "TF_ASIC_Stage_1", "inductance", + "capacitance", "resistance", "mutual_inductance", "feedback_resistance", "frequency_vector"] + + +class PSD_R_Coil_filtered_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_R_Coil_non_filtered = 20 * np.log10(dependencies['PSD_R_Coil']["data"][:, 1]) + TF_ASIC_Stage_2 = 20 * np.log10(dependencies['TF_ASIC_Stage_2']["data"][:, 1]) + + result = (PSD_R_Coil_non_filtered + TF_ASIC_Stage_2) + result = 10 ** (result / 20) + result = result ** 2 + values = np.column_stack((dependencies['PSD_R_Coil']["data"][:, 0], result)) + + return { + "data": values, + "labels": ["Frequency", "PSD_R_Coil"], + "units": ["Hz", "V²/Hz"] + } + + @staticmethod + def get_dependencies(): + return ['TF_ASIC_Stage_2', "PSD_R_Coil"] + + class PSD_Flicker(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): @@ -141,6 +258,35 @@ def get_dependencies(): def calculate_psd_flicker(self, Para_A, Para_B, Alpha, e_en, f): return Para_A * (1 / (Para_B * 10**(9) * (f ** (Alpha/10)))) + (e_en * 10 ** (-9)) +class PSD_Flicker_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + Para_A = parameters.data['Para_A'] + Para_B = parameters.data['Para_B'] + Alpha = parameters.data['Alpha'] + e_en = parameters.data['e_en'] + frequency_vector = dependencies['frequency_vector']["data"] + + vectorized_psd_flicker = np.vectorize(self.calculate_psd_flicker) + psd_flicker_values = vectorized_psd_flicker(Para_A, Para_B, Alpha, e_en, frequency_vector) + psd_flicker_values = psd_flicker_values**2 + frequency_psd_flicker_tensor = np.column_stack((frequency_vector, psd_flicker_values)) + values = frequency_psd_flicker_tensor + + return { + "data": values, + "labels": ["Frequency", "PSD_Flicker"], + "units": ["Hz", "V²/Hz"] + } + + @staticmethod + def get_dependencies(): + return ['frequency_vector', "Para_A", "Para_B", "Alpha", "e_en"] + + def calculate_psd_flicker(self, Para_A, Para_B, Alpha, e_en, f): + return Para_A * (1 / (Para_B * 10**(9) * (f ** (Alpha/10)))) + (e_en * 10 ** (-9)) + + class PSD_e_en(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): @@ -206,6 +352,75 @@ def get_dependencies(): return ['TF_ASIC_Stage_2', "PSD_e_en"] +class PSD_e_en_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_Flicker = dependencies['PSD_Flicker']["data"][:,1] + TF_ASIC_Stage_1 = dependencies['TF_ASIC_Stage_1']["data"][:,1] + + inductance = dependencies['inductance']["data"] + capacitance = dependencies['capacitance']["data"] + frequency_vector = dependencies['frequency_vector']["data"] + resistance = dependencies['resistance']["data"] + + feedback_resistance = parameters.data['feedback_resistance'] + mutual_inductance = parameters.data['mutual_inductance'] + + vectorized_psd_e_en = np.vectorize(self.calculate_psd_e_en) + psd_e_en_values = vectorized_psd_e_en(PSD_Flicker, TF_ASIC_Stage_1, inductance, capacitance, frequency_vector, resistance, feedback_resistance, mutual_inductance) + psd_e_en_values = psd_e_en_values**2 + frequency_psd_e_en_tensor = np.column_stack((frequency_vector, psd_e_en_values)) + values = frequency_psd_e_en_tensor + + return { + "data": values, + "labels": ["Frequency", "PSD_e_en"], + "units": ["Hz", "V²/Hz"] + } + + def calculate_psd_e_en(self, PSD_Flicker_point, TF_ASIC_Stage_1_point, L, C, f, R, feedback_resistance, mutual_inductance): + PSD_e_en_Num = ( + (PSD_Flicker_point ** 2 * TF_ASIC_Stage_1_point ** 2) + * ((1 - L * C * (2 * np.pi * f) ** 2) ** 2 + ( + R * C * 2 * np.pi * f) ** 2) + ) + + PSD_e_en_Den = ( + (1 - L * C * (2 * np.pi * f) ** 2) ** 2 + + ((R * C * 2 * np.pi * f) + ( + (TF_ASIC_Stage_1_point * mutual_inductance * 2 * np.pi * f) / feedback_resistance)) ** 2 + ) + + return (PSD_e_en_Num / PSD_e_en_Den) ** 0.5 + + @staticmethod + def get_dependencies(): + return ['PSD_Flicker', 'TF_ASIC_Stage_1', 'inductance', 'capacitance', 'frequency_vector', 'resistance', 'feedback_resistance', 'mutual_inductance'] + +class PSD_e_en_filtered_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_e_en = 20*np.log10(dependencies['PSD_e_en']["data"][:,1]) + TF_ASIC_Stage_2 = 20*np.log10(dependencies['TF_ASIC_Stage_2']["data"][:,1]) + + result = (PSD_e_en + TF_ASIC_Stage_2) + result = 10**(result/20) + result = result**2 + values = np.column_stack((dependencies['PSD_e_en']["data"][:,0], result)) + + return { + "data": values, + "labels": ["Frequency", "PSD_e_en"], + "units": ["Hz", "V²/Hz"] + } + + @staticmethod + def get_dependencies(): + return ['TF_ASIC_Stage_2', "PSD_e_en"] + + + + class PSD_e_in(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): e_in = parameters.data['e_in'] @@ -270,6 +485,73 @@ def calculate(self, dependencies: dict, parameters: InputParameters): def get_dependencies(): return ['TF_ASIC_Stage_2', "PSD_e_in"] +class PSD_e_in_V2(CalculationStrategy): + def calculate(self, dependencies: dict, parameters: InputParameters): + e_in = parameters.data['e_in'] + feedback_resistance = parameters.data['feedback_resistance'] + mutual_inductance = parameters.data['mutual_inductance'] + + impedance = dependencies['impedance']["data"][:,1] + frequency_vector = dependencies['frequency_vector']["data"] + TF_ASIC_Stage_1 = dependencies['TF_ASIC_Stage_1']["data"][:,1] + capacitance = dependencies['capacitance']["data"] + resistance = dependencies['resistance']["data"] + inductance = dependencies['inductance']["data"] + + + vectorized_psd_e_in = np.vectorize(self.calculate_psd_e_in) + psd_e_in_values = vectorized_psd_e_in(impedance, e_in, frequency_vector, TF_ASIC_Stage_1, inductance, capacitance, resistance, feedback_resistance, mutual_inductance) + psd_e_in_values = psd_e_in_values**2 + frequency_psd_e_in_tensor = np.column_stack((frequency_vector, psd_e_in_values)) + values = frequency_psd_e_in_tensor + + return { + "data": values, + "labels": ["Frequency", "PSD_e_in"], + "units": ["Hz", "V²/Hz"] + } + + def calculate_psd_e_in(self, impedance_point, e_in, f, TF_ASIC_Stage_1_point, L, C, R, feedback_resistance, mutual_inductance): + PSD_e_in_Num = impedance_point ** 2 * (e_in * 1e-15) ** 2 * TF_ASIC_Stage_1_point ** 2 * ( + (1 - L * C * (2 * np.pi * f) ** 2) ** 2 + ( + R * C * 2 * np.pi * f) ** 2 + ) + + PSD_e_in_Den = ( + (1 - L * C * (2 * np.pi * f) ** 2) ** 2 + + ((R * C * 2 * np.pi * f) + ( + (TF_ASIC_Stage_1_point * mutual_inductance * 2 * np.pi * f) / feedback_resistance)) ** 2 + ) + + return (PSD_e_in_Num / PSD_e_in_Den) ** 0.5 + + @staticmethod + def get_dependencies(): + return ['impedance', 'e_in', 'frequency_vector', 'TF_ASIC_Stage_1', 'inductance', 'capacitance', 'resistance', 'feedback_resistance', 'mutual_inductance'] + +class PSD_e_in_filtered_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_e_in = 20*np.log10(dependencies['PSD_e_in']["data"][:,1]) + TF_ASIC_Stage_2 = 20*np.log10(dependencies['TF_ASIC_Stage_2']["data"][:,1]) + + result = (PSD_e_in + TF_ASIC_Stage_2) + result = 10**(result/20) + result = result**2 + values = np.column_stack((dependencies['PSD_e_in']["data"][:,0], result)) + + return { + "data": values, + "labels": ["Frequency", "PSD_e_in"], + "units": ["Hz", "V²/Hz"] + } + + + @staticmethod + def get_dependencies(): + return ['TF_ASIC_Stage_2', "PSD_e_in"] + + class PSD_Total(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): PSD_e_in = dependencies['PSD_e_in']["data"][:,1] @@ -356,6 +638,49 @@ def calculate(self, dependencies: dict, parameters: InputParameters): def get_dependencies(): return ['PSD_e_in_filtered', 'PSD_e_en_filtered', 'PSD_R_Coil_filtered', 'PSD_R_cr_filtered', 'frequency_vector', "PSD_Total_filtered"] +class PSD_Total_V2(CalculationStrategy): + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_e_in = dependencies['PSD_e_in']["data"][:,1] + PSD_e_en = dependencies['PSD_e_en']["data"][:,1] + PSD_R_Coil = dependencies['PSD_R_Coil']["data"][:,1] + PSD_R_cr = dependencies['PSD_R_cr']["data"][:,1] + frequency_vector = dependencies['frequency_vector']["data"] + + result = (PSD_e_in**2 + PSD_e_en**2 + PSD_R_Coil**2 + PSD_R_cr**2)**0.5 + result = result**2 + values = np.column_stack((frequency_vector, result)) + + return { + "data": values, + "labels": ["Frequency", "PSD_Total"], + "units": ["Hz", "V²/Hz"] + } + + @staticmethod + def get_dependencies(): + return ['PSD_e_in', 'PSD_e_en', 'PSD_R_Coil', 'PSD_R_cr', 'frequency_vector'] + +class PSD_Total_filtered_V2(CalculationStrategy): + + def calculate(self, dependencies: dict, parameters: InputParameters): + PSD_Total = 20 * np.log10(dependencies['PSD_Total']["data"][:, 1]) + TF_ASIC_Stage_2 = 20 * np.log10(dependencies['TF_ASIC_Stage_2']["data"][:, 1]) + + result = (PSD_Total + TF_ASIC_Stage_2) + result = 10 ** (result / 20) + result = result ** 2 + values = np.column_stack((dependencies['PSD_Total']["data"][:, 0], result)) + + return { + "data": values, + "labels": ["Frequency", "PSD_Total"], + "units": ["Hz", "V²/Hz"] + } + + @staticmethod + def get_dependencies(): + return ['TF_ASIC_Stage_2', "PSD_Total"] + class NEMI(CalculationStrategy): def calculate(self, dependencies: dict, parameters: InputParameters): diff --git a/src/view/gui.py b/src/view/gui.py index 6f11d15..5caf2ef 100644 --- a/src/view/gui.py +++ b/src/view/gui.py @@ -932,6 +932,10 @@ def init_menu(self): export_results_btn = file_menu.addAction('&Export results') export_results_btn.triggered.connect(self.export_results) + export_CLTF_NEMI_btn = file_menu.addAction('&Export CLTF NEMI') + export_CLTF_NEMI_btn.triggered.connect(self.export_CLTF_NEMI) + + options_menu = self.menuBar().addMenu('&Options') change_plot_count_action = options_menu.addAction('Change Plot Count') change_plot_count_action.triggered.connect(self.change_plot_count) @@ -961,6 +965,16 @@ def display_graph_distance(self): def display_graph_degree(self): self.display_graph(clustering_type="degree") + def export_CLTF_NEMI(self): + try: + # Get the path to save the dependency tree + path, _ = QFileDialog.getSaveFileName(self, "Export CLTF NEMI", "", "json Files (*.png)") + message = self.controller.export_CLTF_NEMI(path) + QMessageBox.information(self, "Export Successful", "The CLTF NEMI has been exported successfully.") + except Exception as e: + QMessageBox.critical(self, "Export Failed", + f"An error occurred while exporting the CLTF NEMI: {str(e)}") + def display_graph(self, clustering_type="degree"): """ Opens an HTML file in the default system web browser.