From b78b8bafdae52e086762d003b462a6467f445aca Mon Sep 17 00:00:00 2001 From: Camilo Diaz Date: Fri, 3 May 2024 09:52:26 -0400 Subject: [PATCH] ENH: Added feature to load single .csv files from the gui STY: Fixed edge case on dropdown width. Fixed flake8 errors MAINT: Moving save simulation button initialization logic to a private funciton. Adding upload csv file test DOC: Added entry in whats_new.rst DOC: Update doc/whats_new.rst Co-authored-by: George Dang <53052793+gtdang@users.noreply.github.com> TEST: Fixed csv file upload test on windows MAINT: Removed commented code STY: Fixed docustring of the public funciton serialize_simulation STY: Applied changes in docustirng of serialize_simulation TST: Added new test cases on test_gui_upload_csv_simulation. Fixed flake8 errors --- doc/whats_new.rst | 3 + hnn_core/__init__.py | 2 +- hnn_core/dipole.py | 19 +-- hnn_core/gui/gui.py | 75 +++++---- hnn_core/tests/assets/test_default.csv | 202 +++++++++++++++++++++++++ hnn_core/tests/test_gui.py | 38 +++++ 6 files changed, 289 insertions(+), 50 deletions(-) create mode 100644 hnn_core/tests/assets/test_default.csv diff --git a/doc/whats_new.rst b/doc/whats_new.rst index f43c55680..a61a3e6ab 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -55,6 +55,9 @@ Changelog - Added gui widget to enable/disable synchronous input in simulations, by `Camilo Diaz`_ in :gh:`750` +- Added gui widgets to save simulation as csv and updated the file upload to support csv data, + by `Camilo Diaz`_ in :gh:`753` + Bug ~~~ - Fix inconsistent connection mapping from drive gids to cell gids, by diff --git a/hnn_core/__init__.py b/hnn_core/__init__.py index 89e430a63..40bf1bbbf 100644 --- a/hnn_core/__init__.py +++ b/hnn_core/__init__.py @@ -1,4 +1,4 @@ -from .dipole import simulate_dipole, read_dipole, average_dipoles, Dipole +from .dipole import simulate_dipole, read_dipole, average_dipoles, Dipole,_read_dipole_txt from .params import Params, read_params, convert_to_hdf5 from .network import Network, pick_connection from .network_models import jones_2009_model, law_2021_model, calcium_model diff --git a/hnn_core/dipole.py b/hnn_core/dipole.py index 28fe515a4..2ae172c5e 100644 --- a/hnn_core/dipole.py +++ b/hnn_core/dipole.py @@ -106,20 +106,25 @@ def simulate_dipole(net, tstop, dt=0.025, n_trials=None, record_vsec=False, return dpls -def _read_dipole_txt(fname): +def _read_dipole_txt(fname, extension='.txt'): """Read dipole values from a txt file and create a Dipole instance. Parameters ---------- - fname : str - Full path to the input file (.txt) - + fname : str or io.StringIO + Full path to the input file (.txt or .csv) or + Content of file in memory as a StringIO Returns ------- dpl : Dipole The instance of Dipole class """ - dpl_data = np.loadtxt(fname, dtype=float) + if extension == '.csv': + # read from a csv file ignoring the headers + dpl_data = np.genfromtxt(fname, delimiter=',', + skip_header=1, dtype=float) + else: + dpl_data = np.loadtxt(fname, dtype=float) ncols = dpl_data.shape[1] if ncols not in (2, 4): raise ValueError( @@ -174,10 +179,6 @@ def read_dipole(fname): The instance of Dipole class """ - # For supporting tests in test_gui.py - if isinstance(fname, StringIO): - return _read_dipole_txt(fname) - fname = str(fname) if not os.path.exists(fname): raise FileNotFoundError('File not found at path %s.' % (fname,)) diff --git a/hnn_core/gui/gui.py b/hnn_core/gui/gui.py index 4e39b06a7..6d4975ef2 100644 --- a/hnn_core/gui/gui.py +++ b/hnn_core/gui/gui.py @@ -234,35 +234,18 @@ def __init__(self, theme_color="#8A2BE2", description='Cores:', disabled=False) self.load_data_button = FileUpload( - accept='.txt', multiple=False, + accept='.txt,.csv', multiple=False, style={'button_color': self.layout['theme_color']}, description='Load data', button_style='success') - b64 = base64.b64encode("".encode()) - payload = b64.decode() - # Initialliting HTML code for download button - self.html_download_button = ''' - - - - ''' - # Create widget wrapper - self.save_simuation_button = ( - HTML(self.html_download_button. - format(payload=payload, - filename={""}, - is_disabled="disabled", - btn_height=self.layout['run_btn'].height, - color_theme=self.layout['theme_color']))) + # Create save simulation widget wrapper + self.save_simuation_button = self._init_html_download_button() self.simulation_list_widget = Dropdown(options=[], value=None, description='', - layout={'width': 'max-content'}) + layout={'width': '15%'}) # Drive selection self.widget_drive_type_selection = RadioButtons( options=['Evoked', 'Poisson', 'Rhythmic'], @@ -315,6 +298,27 @@ def __init__(self, theme_color="#8A2BE2", self._init_ui_components() self.add_logging_window_logger() + def _init_html_download_button(self): + b64 = base64.b64encode("".encode()) + payload = b64.decode() + # Initialliting HTML code for download button + self.html_download_button = ''' + + + + ''' + # Create widget wrapper + return ( + HTML(self.html_download_button. + format(payload=payload, + filename={""}, + is_disabled="disabled", + btn_height=self.layout['run_btn'].height, + color_theme=self.layout['theme_color']))) + def add_logging_window_logger(self): handler = _OutputWidgetHandler(self._log_out) handler.setFormatter( @@ -1211,7 +1215,10 @@ def on_upload_data_change(change, data, viz_manager, log_out): data_dict = change['new'][0] - data_fname = data_dict['name'].rstrip('.txt') + dict_name = data_dict['name'].rsplit('.', 1) + data_fname = dict_name[0] + file_extension = f".{dict_name[1]}" + if data_fname in data['simulation_data'].keys(): logger.error(f"Found existing data: {data_fname}.") return @@ -1220,7 +1227,7 @@ def on_upload_data_change(change, data, viz_manager, log_out): ext_content = codecs.decode(ext_content, encoding="utf-8") with log_out: data['simulation_data'][data_fname] = {'net': None, 'dpls': [ - hnn_core.read_dipole(io.StringIO(ext_content)) + hnn_core._read_dipole_txt(io.StringIO(ext_content), file_extension) ]} logger.info(f'External data {data_fname} loaded.') _template_name = "[Blank] single figure" @@ -1444,23 +1451,11 @@ def _serialize_simulation(log_out, sim_data, simulation_list_widget): def serialize_simulation(simulations_data, simulation_name): - """ - Serializes the simulation data for a given simulation name - into either a single CSV file - or a ZIP file containing multiple CSVs, depending on the number - of trials in the simulation. - Parameters: - - simulation_name (str): The name of the simulation to serialize. - This name is used to access the corresponding - data in the 'simulations' dictionary - of the instance. - - Returns: - - tuple: A tuple where the first element is either - the CSV content (str) of a single trial - or the binary data of a ZIP file (bytes) - containing multiple CSV files, and the - second element is the file extension (either ".csv" or ".zip"). + """Serializes simulation data to CSV. + + Creates a single CSV file or a ZIP file containing multiple CSVs, + depending on the number of trials in the simulation. + """ simulation_data = simulations_data["simulation_data"] csv_trials_output = [] diff --git a/hnn_core/tests/assets/test_default.csv b/hnn_core/tests/assets/test_default.csv new file mode 100644 index 000000000..0ec17ce62 --- /dev/null +++ b/hnn_core/tests/assets/test_default.csv @@ -0,0 +1,202 @@ +# times,agg,L2,L5 +0.000000, 0.004901, -0.000004, 0.004905 +0.850000, -0.001031, -0.000002, -0.001028 +1.700000, -0.000821, -0.000001, -0.000820 +2.550000, -0.000643, -0.000000, -0.000642 +3.400000, -0.000500, -0.000000, -0.000500 +4.250000, -0.000389, 0.000000, -0.000389 +5.100000, -0.000304, 0.000000, -0.000305 +5.950000, -0.000240, 0.000000, -0.000240 +6.800000, -0.000190, 0.000000, -0.000191 +7.650000, -0.000153, 0.000000, -0.000153 +8.500000, -0.000125, 0.000000, -0.000125 +9.350000, -0.000105, 0.000000, -0.000105 +10.200000, -0.000091, 0.000000, -0.000091 +11.050000, -0.000081, 0.000000, -0.000082 +11.900000, -0.000076, 0.000000, -0.000077 +12.750000, -0.000074, 0.000000, -0.000075 +13.600000, -0.000075, 0.000000, -0.000076 +14.450000, -0.000078, 0.000000, -0.000078 +15.300000, -0.000083, 0.000000, -0.000083 +16.150000, -0.000089, 0.000000, -0.000089 +17.000000, -0.000095, 0.000000, -0.000096 +17.850000, -0.000103, 0.000000, -0.000103 +18.700000, -0.000111, 0.000000, -0.000111 +19.550000, -0.000119, 0.000000, -0.000119 +20.400000, -0.000036, 0.000091, -0.000127 +21.250000, 0.000111, 0.000246, -0.000135 +22.100000, 0.000440, 0.000583, -0.000143 +22.950000, 0.000023, 0.000100, -0.000076 +23.800000, 0.000069, -0.000005, 0.000074 +24.650000, 0.001068, 0.000864, 0.000204 +25.500000, 0.002378, 0.002052, 0.000326 +26.350000, 0.003182, 0.002578, 0.000604 +27.200000, 0.003840, 0.002685, 0.001155 +28.050000, 0.007157, 0.003880, 0.003277 +28.900000, 0.010487, 0.005213, 0.005274 +29.750000, 0.011173, 0.006169, 0.005004 +30.600000, 0.011241, 0.006422, 0.004819 +31.450000, 0.009575, 0.006133, 0.003442 +32.300000, 0.009335, 0.005203, 0.004132 +33.150000, 0.006574, 0.003111, 0.003463 +34.000000, 0.002448, 0.000363, 0.002086 +34.850000, -0.000186, -0.001311, 0.001125 +35.700000, -0.003248, -0.000321, -0.002928 +36.550000, -0.005199, 0.001513, -0.006712 +37.400000, -0.005465, 0.002522, -0.007986 +38.250000, -0.004499, 0.003356, -0.007855 +39.100000, -0.003944, 0.002935, -0.006879 +39.950000, -0.003525, 0.001841, -0.005365 +40.800000, -0.003617, 0.000354, -0.003971 +41.650000, -0.003927, -0.000611, -0.003316 +42.500000, -0.004465, -0.001302, -0.003162 +43.350000, -0.004995, -0.001412, -0.003584 +44.200000, -0.005744, -0.000842, -0.004902 +45.050000, -0.007144, -0.000433, -0.006711 +45.900000, -0.008093, -0.000461, -0.007633 +46.750000, -0.008008, 0.000093, -0.008101 +47.600000, -0.007503, 0.000971, -0.008474 +48.450000, -0.005630, 0.001635, -0.007265 +49.300000, -0.003517, 0.001869, -0.005386 +50.150000, -0.002863, 0.001902, -0.004765 +51.000000, -0.005328, 0.001636, -0.006964 +51.850000, -0.007813, 0.001029, -0.008842 +52.700000, -0.005764, 0.000967, -0.006731 +53.550000, -0.001065, 0.001102, -0.002166 +54.400000, 0.000317, 0.001231, -0.000914 +55.250000, 0.001767, 0.001268, 0.000500 +56.100000, 0.004760, 0.001304, 0.003456 +56.950000, 0.005124, 0.001289, 0.003835 +57.800000, 0.002286, 0.001216, 0.001070 +58.650000, -0.000461, 0.001093, -0.001554 +59.500000, -0.002546, 0.000963, -0.003508 +60.350000, -0.004627, 0.000811, -0.005438 +61.200000, -0.006834, 0.000671, -0.007505 +62.050000, -0.008932, 0.000516, -0.009449 +62.900000, -0.013852, 0.000352, -0.014203 +63.750000, -0.018401, 0.000157, -0.018559 +64.600000, -0.024898, -0.000071, -0.024827 +65.450000, -0.032659, -0.000282, -0.032377 +66.300000, -0.039890, -0.000617, -0.039273 +67.150000, -0.048405, -0.000906, -0.047499 +68.000000, -0.057989, -0.001351, -0.056637 +68.850000, -0.059963, -0.001943, -0.058020 +69.700000, -0.048681, -0.002180, -0.046500 +70.550000, -0.031704, -0.002144, -0.029560 +71.400000, -0.017230, -0.002095, -0.015135 +72.250000, -0.020491, -0.002083, -0.018408 +73.100000, -0.010235, -0.001987, -0.008248 +73.950000, -0.000233, -0.001868, 0.001635 +74.800000, 0.006187, -0.001794, 0.007981 +75.650000, 0.004787, -0.001772, 0.006559 +76.500000, -0.000787, -0.001764, 0.000977 +77.350000, -0.003187, -0.001749, -0.001437 +78.200000, 0.006586, -0.001727, 0.008312 +79.050000, 0.004913, -0.001699, 0.006612 +79.900000, 0.002309, -0.001669, 0.003978 +80.750000, -0.003490, -0.001637, -0.001853 +81.600000, -0.002349, -0.001604, -0.000745 +82.450000, 0.007867, -0.001597, 0.009465 +83.300000, 0.013894, -0.001576, 0.015470 +84.150000, 0.009662, -0.001546, 0.011208 +85.000000, -0.004782, -0.001515, -0.003267 +85.850000, -0.015327, -0.001483, -0.013844 +86.700000, -0.019231, -0.001493, -0.017738 +87.550000, -0.023699, -0.001479, -0.022220 +88.400000, -0.027380, -0.001454, -0.025926 +89.250000, -0.029593, -0.001427, -0.028167 +90.100000, -0.030457, -0.001399, -0.029058 +90.950000, -0.027972, -0.001373, -0.026599 +91.800000, -0.023526, -0.001347, -0.022180 +92.650000, -0.027878, -0.001320, -0.026558 +93.500000, -0.034506, -0.001295, -0.033210 +94.350000, -0.037153, -0.001273, -0.035881 +95.200000, -0.034914, -0.001253, -0.033661 +96.050000, -0.028039, -0.001235, -0.026804 +96.900000, -0.020244, -0.001218, -0.019026 +97.750000, -0.009078, -0.001202, -0.007876 +98.600000, -0.002269, -0.001186, -0.001083 +99.450000, 0.002204, -0.001170, 0.003374 +100.300000, 0.005398, -0.001155, 0.006553 +101.150000, 0.005492, -0.001140, 0.006632 +102.000000, 0.003259, -0.001125, 0.004384 +102.850000, 0.015135, -0.001110, 0.016245 +103.700000, 0.017619, -0.001096, 0.018715 +104.550000, 0.018101, -0.001082, 0.019183 +105.400000, 0.018124, -0.001155, 0.019280 +106.250000, 0.020798, -0.001170, 0.021968 +107.100000, 0.023153, -0.001178, 0.024331 +107.950000, 0.022969, -0.001165, 0.024134 +108.800000, 0.020145, -0.001160, 0.021305 +109.650000, 0.018732, -0.001146, 0.019877 +110.500000, 0.016862, -0.001128, 0.017990 +111.350000, 0.011694, -0.000813, 0.012507 +112.200000, 0.007697, -0.000498, 0.008195 +113.050000, 0.003436, -0.000598, 0.004034 +113.900000, 0.001065, -0.001218, 0.002283 +114.750000, 0.001018, -0.001386, 0.002405 +115.600000, 0.001123, -0.001332, 0.002455 +116.450000, 0.000912, -0.001187, 0.002099 +117.300000, 0.001343, -0.000987, 0.002330 +118.150000, 0.003930, -0.000771, 0.004701 +119.000000, 0.007365, -0.000556, 0.007921 +119.850000, 0.008765, -0.000453, 0.009217 +120.700000, 0.010135, -0.000404, 0.010539 +121.550000, 0.012114, -0.000089, 0.012203 +122.400000, 0.014030, 0.000210, 0.013820 +123.250000, 0.012956, 0.000012, 0.012944 +124.100000, 0.009596, -0.000030, 0.009625 +124.950000, 0.009737, 0.001321, 0.008416 +125.800000, 0.011428, 0.002215, 0.009213 +126.650000, 0.009874, 0.001135, 0.008738 +127.500000, 0.007932, 0.000147, 0.007785 +128.350000, 0.008005, 0.000672, 0.007333 +129.200000, 0.008103, 0.000770, 0.007334 +130.050000, 0.007523, -0.000085, 0.007608 +130.900000, 0.011667, 0.001946, 0.009721 +131.750000, 0.016976, 0.004917, 0.012059 +132.600000, 0.018978, 0.004972, 0.014007 +133.450000, 0.018376, 0.003383, 0.014993 +134.300000, 0.018065, 0.004381, 0.013683 +135.150000, 0.018451, 0.005940, 0.012511 +136.000000, 0.017998, 0.006308, 0.011690 +136.850000, 0.016924, 0.005754, 0.011170 +137.700000, 0.022504, 0.008672, 0.013832 +138.550000, 0.025043, 0.011590, 0.013453 +139.400000, 0.029372, 0.011424, 0.017948 +140.250000, 0.032035, 0.009205, 0.022830 +141.100000, 0.034229, 0.009407, 0.024822 +141.950000, 0.033103, 0.011782, 0.021321 +142.800000, 0.030434, 0.012227, 0.018208 +143.650000, 0.027193, 0.009627, 0.017566 +144.500000, 0.018579, 0.006458, 0.012121 +145.350000, 0.006030, 0.001837, 0.004193 +146.200000, 0.000821, -0.000384, 0.001204 +147.050000, 0.003639, 0.001047, 0.002592 +147.900000, 0.010721, 0.004641, 0.006080 +148.750000, 0.020417, 0.009757, 0.010660 +149.600000, 0.024747, 0.013637, 0.011110 +150.450000, 0.026965, 0.014998, 0.011967 +151.300000, 0.030735, 0.014423, 0.016312 +152.150000, 0.035009, 0.014149, 0.020860 +153.000000, 0.036671, 0.011801, 0.024870 +153.850000, 0.035689, 0.009358, 0.026331 +154.700000, 0.031327, 0.008404, 0.022923 +155.550000, 0.026491, 0.008488, 0.018003 +156.400000, 0.024704, 0.007879, 0.016825 +157.250000, 0.023513, 0.006935, 0.016578 +158.100000, 0.021296, 0.005790, 0.015506 +158.950000, 0.022641, 0.005599, 0.017042 +159.800000, 0.022466, 0.006542, 0.015924 +160.650000, 0.018331, 0.007326, 0.011005 +161.500000, 0.014261, 0.007754, 0.006507 +162.350000, 0.013925, 0.007803, 0.006122 +163.200000, 0.013594, 0.007289, 0.006305 +164.050000, 0.010272, 0.006451, 0.003822 +164.900000, 0.007026, 0.005557, 0.001468 +165.750000, 0.002971, 0.005171, -0.002201 +166.600000, 0.002061, 0.005246, -0.003184 +167.450000, 0.003700, 0.005597, -0.001897 +168.300000, 0.007350, 0.005701, 0.001649 +169.150000, 0.010697, 0.005525, 0.005172 +170.000000, 0.013794, 0.005309, 0.008485 diff --git a/hnn_core/tests/test_gui.py b/hnn_core/tests/test_gui.py index 6f1822f89..ff00f72ff 100644 --- a/hnn_core/tests/test_gui.py +++ b/hnn_core/tests/test_gui.py @@ -4,6 +4,7 @@ import numpy as np import pytest import traitlets +import os from hnn_core import Dipole, Network, Params from hnn_core.gui import HNNGUI @@ -603,3 +604,40 @@ def test_gui_download_simulation(): serialize_simulation(gui.data, sim_name2)) # result is a single csv file assert file_extension == ".csv" + + +def test_gui_upload_csv_simulation(): + """Test if gui handles uploaded csv data""" + gui = HNNGUI() + _ = gui.compose() + + assert len(gui.viz_manager.data['figs']) == 0 + assert len(gui.data['simulation_data']) == 0 + + # Relative path to the file + file_relative_path = "./hnn_core/tests/assets/test_default.csv" + absolute_path = os.path.abspath(file_relative_path) + if os.name == 'nt': # Windows + # Convert backslashes to forward slashes and + # ensure we have three slashes after 'file:' + file_url = 'file:///' + absolute_path.replace('\\', '/') + else: # UNIX-like systems + file_url = 'file://' + absolute_path + _ = gui._simulate_upload_data(file_url) + + # we are loading only 1 trial, + # assume all the data we need is in the [0] position + data_lengh = ( + len(gui.data['simulation_data']['test_default']['dpls'][0].times)) + + assert len(gui.data['simulation_data']) == 1 + assert 'test_default' in gui.data['simulation_data'].keys() + assert gui.data['simulation_data']['test_default']['net'] is None + assert type(gui.data['simulation_data']['test_default']['dpls']) is list + assert len(gui.viz_manager.data['figs']) == 1 + assert (len(gui.data['simulation_data']['test_default'] + ['dpls'][0].data['agg']) == data_lengh) + assert (len(gui.data['simulation_data']['test_default'] + ['dpls'][0].data['L2']) == data_lengh) + assert (len(gui.data['simulation_data']['test_default'] + ['dpls'][0].data['L5']) == data_lengh)