From 0b3f76f580d7ca397e60365d6b42583ca9c4dfbe Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 19 Apr 2023 17:29:38 +0330 Subject: [PATCH 01/31] raw plot bog fixed, file dialog for server created --- ross_backend/resources/browse.py | 106 +++++++++++++++++++++ ross_backend/rutils/io.py | 0 ross_ui/controller/hdf5.py | 122 ++----------------------- ross_ui/controller/serverFileDialog.py | 34 +++++++ ross_ui/view/serverFileDialog.py | 35 +++++++ 5 files changed, 181 insertions(+), 116 deletions(-) create mode 100644 ross_backend/resources/browse.py create mode 100644 ross_backend/rutils/io.py create mode 100644 ross_ui/controller/serverFileDialog.py create mode 100644 ross_ui/view/serverFileDialog.py diff --git a/ross_backend/resources/browse.py b/ross_backend/resources/browse.py new file mode 100644 index 0000000..04f8934 --- /dev/null +++ b/ross_backend/resources/browse.py @@ -0,0 +1,106 @@ +from blacklist import BLACKLIST +from flask_jwt_extended import ( + create_access_token, + create_refresh_token, + jwt_refresh_token_required, + get_jwt_identity, + get_raw_jwt, + jwt_required +) +from flask_restful import Resource, reqparse +from models.config import ConfigDetectionModel, ConfigSortModel +from models.project import ProjectModel +from models.user import UserModel +from werkzeug.security import safe_str_cmp + +_user_parser = reqparse.RequestParser() +_user_parser.add_argument('username', + type=str, + required=True, + help="This field cannot be blank." + ) +_user_parser.add_argument('password', + type=str, + required=True, + help="This field cannot be blank." + ) + + +class UserRegister(Resource): + def post(self): + data = _user_parser.parse_args() + + if UserModel.find_by_username(data['username']): + return {"message": "A user with that username already exists"}, 400 + + user = UserModel(data['username'], data['password']) + user.save_to_db() + user_id = user.get_id() + proj = ProjectModel(user_id) # create a default project for user + proj.save_to_db() + user.project_default = proj.id + user.save_to_db() + config_detect = ConfigDetectionModel(user_id, + project_id=proj.id) # create a default detection config for the default project + config_detect.save_to_db() + config_sort = ConfigSortModel(user_id, project_id=proj.id) + config_sort.save_to_db() + + return {"message": "User created successfully."}, 201 + + +class UserLogin(Resource): + def post(self): + data = _user_parser.parse_args() + user = UserModel.find_by_username(data['username']) + + if user and safe_str_cmp(user.password, data['password']): + access_token = create_access_token(identity=user.id, fresh=True) + refresh_token = create_refresh_token(user.id) + Project_id = user.project_default + + # return { + # 'access_token': access_token, + # 'refresh_token': refresh_token + # }, 200 + + return { + 'access_token': access_token, + 'refresh_token': refresh_token, + 'project_id': Project_id + }, 200 + + return {"message": "Invalid Credentials!"}, 401 + + +class UserLogout(Resource): + @jwt_required + def post(self): + jti = get_raw_jwt()['jti'] + BLACKLIST.add(jti) + return {"message": "Successfully logged out"}, 200 + + +class User(Resource): + @classmethod + def get(cls, user_id: int): + user = UserModel.find_by_id(user_id) + if not user: + return {'message': 'User Not Found'}, 404 + return user.json(), 200 + + @classmethod + def delete(cls, user_id: int): + user = UserModel.find_by_id(user_id) + if not user: + return {'message': 'User Not Found'}, 404 + user.delete_from_db() + return {'message': 'User deleted.'}, 200 + + +class TokenRefresh(Resource): + @jwt_refresh_token_required + def post(self): + current_user = get_jwt_identity() + new_token = create_access_token(identity=current_user, fresh=False) + return {'access_token': new_token}, 200 diff --git a/ross_backend/rutils/io.py b/ross_backend/rutils/io.py new file mode 100644 index 0000000..e69de29 diff --git a/ross_ui/controller/hdf5.py b/ross_ui/controller/hdf5.py index e78ac69..f974023 100644 --- a/ross_ui/controller/hdf5.py +++ b/ross_ui/controller/hdf5.py @@ -1,35 +1,11 @@ -# -*- coding: utf-8 -*- -""" -In this example we create a subclass of PlotCurveItem for displaying a very large -data set from an HDF5 file that does not fit in memory. - -The basic approach is to override PlotCurveItem.viewRangeChanged such that it -reads only the portion of the HDF5 data that is necessary to display the visible -portion of the data. This is further downsampled to reduce the number of samples -being displayed. - -A more clever implementation of this class would employ some kind of caching -to avoid re-reading the entire visible waveform at every update. -""" - -# import initExample ## Add path to library (just for examples; you do not need this) - import numpy as np import pyqtgraph as pg -# pg.mkQApp() - - -# plt = pg.plot() -# plt.setWindowTitle('pyqtgraph example: HDF5 big data') -# plt.enableAutoRange(False, False) -# plt.setXRange(0, 500) - class HDF5Plot(pg.PlotCurveItem): def __init__(self, *args, **kwds): self.hdf5 = None - self.limit = 10000 # maximum number of samples to be plotted + self.limit = 10000 pg.PlotCurveItem.__init__(self, *args, **kwds) def setHDF5(self, data, pen=None): @@ -47,103 +23,17 @@ def updateHDF5Plot(self): vb = self.getViewBox() if vb is None: - return # no ViewBox yet + return - # Determine what data range must be read from HDF5 xrange = vb.viewRange()[0] start = max(0, int(xrange[0]) - 1) stop = min(len(self.hdf5), int(xrange[1] + 2)) - # Decide by how much we should downsample ds = int((stop - start) / self.limit) + 1 + visible = self.hdf5[start:stop:ds] + x = np.arange(start, stop, ds) - if ds == 1: - # Small enough to display with no intervention. - visible = self.hdf5[start:stop] - scale = 1 - else: - # Here convert data into a down-sampled array suitable for visualizing. - # Must do this piecewise to limit memory usage. - samples = 1 + ((stop - start) // ds) - visible = np.zeros(samples * 2, dtype=self.hdf5.dtype) - sourcePtr = start - targetPtr = 0 - - # read data in chunks of ~1M samples - chunkSize = (1000000 // ds) * ds - while sourcePtr < stop - 1: - chunk = self.hdf5[sourcePtr:min(stop, sourcePtr + chunkSize)] - sourcePtr += len(chunk) - - # reshape chunk to be integral multiple of ds - chunk = chunk[:(len(chunk) // ds) * ds].reshape(len(chunk) // ds, ds) - - # compute max and min - chunkMax = chunk.max(axis=1) - chunkMin = chunk.min(axis=1) - - # interleave min and max into plot data to preserve envelope shape - visible[targetPtr:targetPtr + chunk.shape[0] * 2:2] = chunkMin - visible[1 + targetPtr:1 + targetPtr + chunk.shape[0] * 2:2] = chunkMax - targetPtr += chunk.shape[0] * 2 - - visible = visible[:targetPtr] - scale = ds * 0.5 - - self.setData(visible) # update the plot - self.setPos(start, 0) # shift to match starting index + self.setData(x, visible) if self.pen is not None: self.setPen(self.pen) - self.resetTransform() - # self.scale(scale, 1) # scale to match downsampling - -# def createFile(finalSize=2000000000): -# """Create a large HDF5 data file for testing. -# Data consists of 1M random samples tiled through the end of the array. -# """ - -# chunk = np.random.normal(size=1000000).astype(np.float32) - -# f = h5py.File('test.hdf5', 'w') -# f.create_dataset('data', data=chunk, chunks=True, maxshape=(None,)) -# data = f['data'] - -# nChunks = finalSize // (chunk.size * chunk.itemsize) -# with pg.ProgressDialog("Generating test.hdf5...", 0, nChunks) as dlg: -# for i in range(nChunks): -# newshape = [data.shape[0] + chunk.shape[0]] -# data.resize(newshape) -# data[-chunk.shape[0]:] = chunk -# dlg += 1 -# if dlg.wasCanceled(): -# f.close() -# os.remove('test.hdf5') -# sys.exit() -# dlg += 1 -# f.close() - -# if len(sys.argv) > 1: -# fileName = sys.argv[1] -# else: -# fileName = 'test.hdf5' -# if not os.path.isfile(fileName): -# size, ok = QtGui.QInputDialog.getDouble(None, "Create HDF5 Dataset?", "This demo requires a large HDF5 array. To generate a file, enter the array size (in GB) and press OK.", 2.0) -# if not ok: -# sys.exit(0) -# else: -# createFile(int(size*1e9)) -# #raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") - -# f = h5py.File(fileName, 'r') -# curve = HDF5Plot() -# curve.setHDF5(f['data']) -# plt.addItem(curve) - - -# ## Start Qt event loop unless running in interactive mode or using pyside. -# if __name__ == '__main__': - - -# import sys -# if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): -# QtGui.QApplication.instance().exec_() + self.resetTransform() \ No newline at end of file diff --git a/ross_ui/controller/serverFileDialog.py b/ross_ui/controller/serverFileDialog.py new file mode 100644 index 0000000..293c80c --- /dev/null +++ b/ross_ui/controller/serverFileDialog.py @@ -0,0 +1,34 @@ +from PyQt5 import QtWidgets + +from model.api import UserAccount +from view.signin import Signin_Dialog + + +class SigninApp(Signin_Dialog): + def __init__(self, server): + super(SigninApp, self).__init__(server) + self.pushButton_in.pressed.connect(self.accept_in) + self.pushButton_up.pressed.connect(self.accept_up) + + def accept_in(self): + username = self.textEdit_username.text() + password = self.textEdit_password.text() + self.user = UserAccount(self.url) + res = self.user.sign_in(username, password) + if res['stat']: + super().accept() + else: + QtWidgets.QMessageBox.critical(self, "Error", res["message"]) + + def accept_up(self): + username = self.textEdit_username.text() + password = self.textEdit_password.text() + self.user = UserAccount(self.url) + res = self.user.sign_up(username, password) + if res['stat']: + self.label_res.setStyleSheet("color: green") + QtWidgets.QMessageBox.information(self, "Account Created", res["message"]) + else: + self.label_res.setStyleSheet("color: red") + # self.label_res.setText(res['message']) + QtWidgets.QMessageBox.critical(self, "Error", res["message"]) diff --git a/ross_ui/view/serverFileDialog.py b/ross_ui/view/serverFileDialog.py new file mode 100644 index 0000000..e355951 --- /dev/null +++ b/ross_ui/view/serverFileDialog.py @@ -0,0 +1,35 @@ +from PyQt5 import QtCore, QtWidgets + + +class Signin_Dialog(QtWidgets.QDialog): + def __init__(self, server): + super().__init__() + self.url = server + self.setFixedSize(400, 300) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(40, 60, 121, 21)) + self.label_2 = QtWidgets.QLabel(self) + self.label_2.setGeometry(QtCore.QRect(50, 130, 121, 20)) + + self.textEdit_username = QtWidgets.QLineEdit(self) + self.textEdit_username.setGeometry(QtCore.QRect(145, 55, 141, 31)) + + self.textEdit_password = QtWidgets.QLineEdit(self) + self.textEdit_password.setGeometry(QtCore.QRect(145, 125, 141, 31)) + self.textEdit_password.setEchoMode(QtWidgets.QLineEdit.Password) + + self.label_res = QtWidgets.QLabel(self) + self.label_res.setGeometry(QtCore.QRect(50, 180, 361, 16)) + + self.pushButton_in = QtWidgets.QPushButton(self) + self.pushButton_in.setGeometry(QtCore.QRect(130, 220, 121, 31)) + + self.pushButton_up = QtWidgets.QPushButton(self) + self.pushButton_up.setGeometry(QtCore.QRect(260, 220, 111, 31)) + + self.setWindowTitle("Sign In/Up") + self.label.setText("Username") + self.label_2.setText("Password") + self.label_res.setText("") + self.pushButton_in.setText("Sign In") + self.pushButton_up.setText("Sign Up") From 46461acc51d2ba3f3a0683d39b34897a6c806677 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 19 Apr 2023 17:32:12 +0330 Subject: [PATCH 02/31] app route optimized --- ross_backend/app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ross_backend/app.py b/ross_backend/app.py index f057d59..cd65986 100644 --- a/ross_backend/app.py +++ b/ross_backend/app.py @@ -7,13 +7,14 @@ from blacklist import BLACKLIST from flask import Flask, jsonify from flask_jwt_extended import JWTManager -from resources.sort import SortDefault, Sort -from resources.project import Project, Projects -from resources.data import RawData, RawDataDefault -from resources.detect import Detect, DetectDefault +from resources.sort import SortDefault +from resources.project import Projects +from resources.data import RawDataDefault +from resources.detect import DetectDefault from resources.sorting_result import SortingResultDefault from resources.detection_result import DetectionResultDefault from resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout +from resources.browse import Browse app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db' # 'sqlite:///:memory:' # 'sqlite:///data.db' @@ -100,6 +101,8 @@ def revoked_token_callback(): api.add_resource(Projects, '/projects') # api.add_resource(Project, '/project/') +api.add_resource(Browse, '/browse') + if __name__ == '__main__': db.init_app(app) - app.run(port=5000, debug=False) + app.run(host='0.0.0.0', port=5000, debug=False) From d4bd3b09785e00f714aa8d89d63e3122845cbd95 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 25 Apr 2023 15:53:01 +0330 Subject: [PATCH 03/31] code refactor, raw data handled in server, hdf5 plot handled to accept raw from server --- .gitignore | 1 + README.md | 63 ++-- environment.yml | 78 ++--- ross_backend/models/data.py | 13 +- ross_backend/resources/browse.py | 114 +------ ross_backend/resources/data.py | 280 +++++++++--------- ross_backend/resources/detect.py | 194 ++++++------ ross_backend/resources/detection_result.py | 3 +- ross_backend/resources/funcs/detection.py | 1 + ross_backend/resources/funcs/skew_t_sorter.py | 9 +- ross_backend/resources/funcs/sort_utils.py | 2 +- ross_backend/resources/funcs/sorting.py | 1 - ross_backend/resources/funcs/t_sorting.py | 3 +- ross_backend/resources/project.py | 1 + ross_backend/resources/sort.py | 5 +- ross_backend/resources/sorting_result.py | 1 + ross_backend/resources/user.py | 5 +- ross_backend/rutils/io.py | 44 +++ ross_ui/controller/hdf5.py | 29 +- ross_ui/controller/mainWindow.py | 25 +- ross_ui/controller/serverFileDialog.py | 73 +++-- ross_ui/controller/signin.py | 6 +- ross_ui/model/api.py | 135 ++++----- ross_ui/view/icons/ross.png | Bin 24600 -> 26539 bytes ross_ui/view/mainWindow.py | 11 +- ross_ui/view/serverFileDialog.py | 62 ++-- 26 files changed, 593 insertions(+), 566 deletions(-) diff --git a/.gitignore b/.gitignore index 8ac4db9..fc177d9 100644 --- a/.gitignore +++ b/.gitignore @@ -407,3 +407,4 @@ compile_commands.json data /ross_backend/data.db /ross_ui/ross_data/ +ross_backend/ross_data/ \ No newline at end of file diff --git a/README.md b/README.md index 8dc8058..fe2b0ca 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,28 @@ # ROSS v2 + ![image](./images/Ross_Color.png) -ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the paper entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). (Official Python Implementation) +ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the +paper +entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). ( +Official Python Implementation) ### Important Note on ROSS v2 -ROSS v2 is implemented based on the client-server architecture. In the beta version, the GUI and processing units are completely separated and their connection is based on Restful APIs. However, at this moment, it only works on one machine and we try to find a good way to optimize the data transfer between the client and the server. In our final release, you would be able to run the light GUI on a simple machine while the data and algorithms would be executed on a separate server in your lab. + +ROSS v2 is implemented based on the client-server architecture. In the beta version, the GUI and processing units are +completely separated and their connection is based on Restful APIs. However, at this moment, it only works on one +machine and we try to find a good way to optimize the data transfer between the client and the server. In our final +release, you would be able to run the light GUI on a simple machine while the data and algorithms would be executed on a +separate server in your lab. ## Requirements + - All the requirement packages are listed at enviroments.yml file in root path ## How to install + 1. Git Clone this repository to your local path ```git clone https://github.com/ramintoosi/ROSS``` -2. Checkout to v2 ```git checkout v2``` +2. Checkout to v2 ```git checkout v2``` 3. Create a conda enviroment by command : ```conda env create -f environment.yml``` 4. Activate conda environment ```conda activate ross``` @@ -21,49 +32,57 @@ ROSS v2 is implemented based on the client-server architecture. In the beta vers 2. Run the UI by typing ```python ./ross_ui/main.py``` in the terminal. 3. The first time you want to use the software, you must define a user as follows: - - In opened window, click on ```Options``` ---> ```Sign In/Up``` , enter the desired username and password, click on ```Sign Up```. +- In opened window, click on ```Options``` ---> ```Sign In/Up``` , enter the desired username and password, click + on ```Sign Up```. -4. The next time you want to use the software, just click on ```Options``` ---> ```Sign In/Up``` and enter your username and password, click on ```Sign In``` . +4. The next time you want to use the software, just click on ```Options``` ---> ```Sign In/Up``` and enter your username + and password, click on ```Sign In``` . -5. Import your "Raw Data" as follows : +5. Import your "Raw Data" as follows : - - In opened window, click on ```File``` ---> ```Import``` ---> ```Raw Data``` , select the data file from your system, then, select the variable containing raw-data ```(Spike)``` and click on ```OK```. +- In opened window, click on ```File``` ---> ```Import``` ---> ```Raw Data``` , select the data file from your system, + then, select the variable containing raw-data ```(Spike)``` and click on ```OK```. -6. Now you can Go on and enjoy the Software. +6. Now you can Go on and enjoy the Software. For more instructions and samples please visit ROSS documentation at (link), or demo video at (link). + ## Usage -ROSS v2, same as v1, provides useful tools for spike detection, automatic and manual sorting. +ROSS v2, same as v1, provides useful tools for spike detection, automatic and manual sorting. - Detection - You can load raw extracellular data and adjust the provided settings for filtering and thresholding. Then by pushing **Start Detection** button the detection results appear in a PCA plot: - + You can load raw extracellular data and adjust the provided settings for filtering and thresholding. Then by pushing * + *Start Detection** button the detection results appear in a PCA plot: + ![image](./images/detection.png) - Automatic Sorting - Automatic sorting is implemented based on five different methods: skew-t and t distributions, GMM, k-means and template matching. Several options are provided for configurations in the algorithm. Automatic sorting results will appear in PCA and waveform plots: - - ![image](./images/sort.png) + Automatic sorting is implemented based on five different methods: skew-t and t distributions, GMM, k-means and + template matching. Several options are provided for configurations in the algorithm. Automatic sorting results will + appear in PCA and waveform plots: + +![image](./images/sort.png) - Manual Sorting - Manual sorting tool is used for manual modifications on automatic results by the researcher. These tools include: Merge, Delete, Resort and Manual grouping or deleting samples in PCA domain: - + Manual sorting tool is used for manual modifications on automatic results by the researcher. These tools include: + Merge, Delete, Resort and Manual grouping or deleting samples in PCA domain: + ![image](./images/sort2.png) - Visualization - - - Several visualization tools are provided such as: 3D plot - + + - Several visualization tools are provided such as: 3D plot + ![image](./images/vis1.png) - - - Also, inter spike interval, neuron live time and Cluster Waveforms - + + - Also, inter spike interval, neuron live time and Cluster Waveforms + ![image](./images/vis2.png) diff --git a/environment.yml b/environment.yml index 7f25da1..87dd865 100644 --- a/environment.yml +++ b/environment.yml @@ -46,42 +46,42 @@ dependencies: - xz=5.2.5=h7b6447c_0 - zlib=1.2.11=h7f8727e_4 - pip: - - aniso8601==9.0.1 - - cffi==1.15.1 - - charset-normalizer==2.0.10 - - colour==0.1.5 - - cryptography==39.0.0 - - cycler==0.11.0 - - flask-jwt==0.3.2 - - flask-jwt-extended==3.0.0 - - flask-restful==0.3.8 - - flask-sqlalchemy==2.5.1 - - greenlet==1.1.2 - - h5py==3.6.0 - - idna==3.3 - - joblib==1.1.0 - - kiwisolver==1.3.2 - - matplotlib==3.4.3 - - nptdms==1.4.0 - - numpy==1.22.1 - - opencv-python==4.6.0.66 - - pillow==9.0.0 - - pycparser==2.21 - - pyjwt==1.4.2 - - pyopengl==3.1.5 - - pyopenssl==23.0.0 - - pyparsing==3.0.6 - - pyqt5==5.15.6 - - pyqt5-qt5==5.15.2 - - pyqt5-sip==12.9.0 - - pyqtgraph==0.13.1 - - pytz==2021.3 - - pywavelets==1.2.0 - - pyyawt==0.1.1 - - requests==2.26.0 - - scikit-learn==1.0.1 - - scipy==1.7.1 - - sip==6.5.0 - - sqlalchemy==1.4.29 - - threadpoolctl==3.0.0 - - urllib3==1.26.8 \ No newline at end of file + - aniso8601==9.0.1 + - cffi==1.15.1 + - charset-normalizer==2.0.10 + - colour==0.1.5 + - cryptography==39.0.0 + - cycler==0.11.0 + - flask-jwt==0.3.2 + - flask-jwt-extended==3.0.0 + - flask-restful==0.3.8 + - flask-sqlalchemy==2.5.1 + - greenlet==1.1.2 + - h5py==3.6.0 + - idna==3.3 + - joblib==1.1.0 + - kiwisolver==1.3.2 + - matplotlib==3.4.3 + - nptdms==1.4.0 + - numpy==1.22.1 + - opencv-python==4.6.0.66 + - pillow==9.0.0 + - pycparser==2.21 + - pyjwt==1.4.2 + - pyopengl==3.1.5 + - pyopenssl==23.0.0 + - pyparsing==3.0.6 + - pyqt5==5.15.6 + - pyqt5-qt5==5.15.2 + - pyqt5-sip==12.9.0 + - pyqtgraph==0.13.1 + - pytz==2021.3 + - pywavelets==1.2.0 + - pyyawt==0.1.1 + - requests==2.26.0 + - scikit-learn==1.0.1 + - scipy==1.7.1 + - sip==6.5.0 + - sqlalchemy==1.4.29 + - threadpoolctl==3.0.0 + - urllib3==1.26.8 \ No newline at end of file diff --git a/ross_backend/models/data.py b/ross_backend/models/data.py index 4dc5acf..10e137a 100644 --- a/ross_backend/models/data.py +++ b/ross_backend/models/data.py @@ -1,7 +1,6 @@ -from db import db - +from __future__ import annotations -# from sqlalchemy.dialects import postgresql +from db import db class RawModel(db.Model): @@ -9,14 +8,16 @@ class RawModel(db.Model): id = db.Column(db.Integer, primary_key=True) data = db.Column(db.String) + mode = db.Column(db.Integer, default=0) # 0: client, 1: server user_id = db.Column(db.Integer, db.ForeignKey('users.id')) project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete="CASCADE")) # user = db.relationship('UserModel') # project = db.relationship('ProjectModel', backref="raw", lazy=True) - def __init__(self, user_id, data, project_id): + def __init__(self, user_id, data, project_id, mode): self.data = data + self.mode = mode self.user_id = user_id self.project_id = project_id @@ -33,11 +34,11 @@ def get(cls): return cls.query.first() @classmethod - def find_by_user_id(cls, _id): + def find_by_user_id(cls, _id) -> RawModel: return cls.query.filter_by(user_id=_id, project_id=0).first() @classmethod - def find_by_project_id(cls, project_id): + def find_by_project_id(cls, project_id) -> RawModel: return cls.query.filter_by(project_id=project_id).first() def delete_from_db(self): diff --git a/ross_backend/resources/browse.py b/ross_backend/resources/browse.py index 04f8934..45cbd98 100644 --- a/ross_backend/resources/browse.py +++ b/ross_backend/resources/browse.py @@ -1,106 +1,24 @@ -from blacklist import BLACKLIST +import os +from pathlib import Path + from flask_jwt_extended import ( - create_access_token, - create_refresh_token, - jwt_refresh_token_required, - get_jwt_identity, - get_raw_jwt, - jwt_required + jwt_required, ) from flask_restful import Resource, reqparse -from models.config import ConfigDetectionModel, ConfigSortModel -from models.project import ProjectModel -from models.user import UserModel -from werkzeug.security import safe_str_cmp - -_user_parser = reqparse.RequestParser() -_user_parser.add_argument('username', - type=str, - required=True, - help="This field cannot be blank." - ) -_user_parser.add_argument('password', - type=str, - required=True, - help="This field cannot be blank." - ) - - -class UserRegister(Resource): - def post(self): - data = _user_parser.parse_args() - - if UserModel.find_by_username(data['username']): - return {"message": "A user with that username already exists"}, 400 - - user = UserModel(data['username'], data['password']) - user.save_to_db() - user_id = user.get_id() - proj = ProjectModel(user_id) # create a default project for user - proj.save_to_db() - user.project_default = proj.id - user.save_to_db() - config_detect = ConfigDetectionModel(user_id, - project_id=proj.id) # create a default detection config for the default project - config_detect.save_to_db() - config_sort = ConfigSortModel(user_id, project_id=proj.id) - config_sort.save_to_db() - - return {"message": "User created successfully."}, 201 - -class UserLogin(Resource): - def post(self): - data = _user_parser.parse_args() - user = UserModel.find_by_username(data['username']) +Raw_data_path = os.path.join(Path(__file__).parent, '../ross_data/Raw_Data') +Path(Raw_data_path).mkdir(parents=True, exist_ok=True) - if user and safe_str_cmp(user.password, data['password']): - access_token = create_access_token(identity=user.id, fresh=True) - refresh_token = create_refresh_token(user.id) - Project_id = user.project_default - # return { - # 'access_token': access_token, - # 'refresh_token': refresh_token - # }, 200 +class Browse(Resource): + parser = reqparse.RequestParser(bundle_errors=True) + parser.add_argument('root', type=str, required=False, default=str(Path.home())) - return { - 'access_token': access_token, - 'refresh_token': refresh_token, - 'project_id': Project_id - }, 200 - - return {"message": "Invalid Credentials!"}, 401 - - -class UserLogout(Resource): @jwt_required - def post(self): - jti = get_raw_jwt()['jti'] - BLACKLIST.add(jti) - return {"message": "Successfully logged out"}, 200 - - -class User(Resource): - @classmethod - def get(cls, user_id: int): - user = UserModel.find_by_id(user_id) - if not user: - return {'message': 'User Not Found'}, 404 - return user.json(), 200 - - @classmethod - def delete(cls, user_id: int): - user = UserModel.find_by_id(user_id) - if not user: - return {'message': 'User Not Found'}, 404 - user.delete_from_db() - return {'message': 'User deleted.'}, 200 - - -class TokenRefresh(Resource): - @jwt_refresh_token_required - def post(self): - current_user = get_jwt_identity() - new_token = create_access_token(identity=current_user, fresh=False) - return {'access_token': new_token}, 200 + def get(self): + root = Browse.parser.parse_args()['root'] + list_of_folders = [x for x in os.listdir(root) if + os.path.isdir(os.path.join(root, x)) and not x.startswith('.')] + list_of_files = [x for x in os.listdir(root) if os.path.isfile(os.path.join(root, x)) and not x.startswith('.') + and x.endswith(('.mat', '.csv', '.tdms'))] + return {'folders': list_of_folders, 'files': list_of_files, 'root': root} diff --git a/ross_backend/resources/data.py b/ross_backend/resources/data.py index 14c9dd6..e56002a 100644 --- a/ross_backend/resources/data.py +++ b/ross_backend/resources/data.py @@ -1,162 +1,176 @@ +import io +import pickle + import flask +import numpy as np from flask import request from flask_jwt_extended import jwt_required, get_jwt_identity -from flask_restful import Resource, reqparse -from models.data import RawModel -from models.project import ProjectModel - - -class RawData(Resource): - parser = reqparse.RequestParser() - parser.add_argument('raw', type=str, required=True, help="This field cannot be left blank!") - - @jwt_required - def get(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - raw = proj.raw - - # raw = RawModel.find_by_user_id(user_id) - if raw: - # b = io.BytesIO() - # b.write(raw.raw) - # b.seek(0) - - # d = np.load(b, allow_pickle=True) - # print(d['raw'].shape) - # b.close() - # print(user_id, raw.project_id) - return {'message': "Raw Data Exists."}, 201 - - return {'message': 'Raw Data does not exist'}, 404 - - @jwt_required - def post(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - raw = proj.raw - - if raw: - return {'message': "Raw Data already exists."}, 400 - filestr = request.data - # data = RawData.parser.parse_args() +from flask_restful import Resource - # print(eval(data['raw']).shape) - raw = RawModel(user_id=user_id, project_id=proj.id, data=filestr) # data['raw']) - - try: - raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 - - return "Success", 201 - - @jwt_required - def delete(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - raw = proj.raw - if raw: - raw.delete_from_db() - return {'message': 'Raw Data deleted.'} - return {'message': 'Raw Data does not exist.'}, 404 - - @jwt_required - def put(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - raw = proj.raw - filestr = request.data - if raw: - print('here') - raw.data = filestr - try: - raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 - return "Success", 201 - - else: - raw = RawModel(user_id, data=filestr, project_id=proj.id) - try: - print('now here') - raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 - - return "Success", 201 +from models.data import RawModel +from rutils.io import read_file_in_server + +# class RawData(Resource): +# parser = reqparse.RequestParser() +# parser.add_argument('raw', type=str, required=True, help="This field cannot be left blank!") +# +# @jwt_required +# def get(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# raw = proj.raw +# +# # raw = RawModel.find_by_user_id(user_id) +# if raw: +# # b = io.BytesIO() +# # b.write(raw.raw) +# # b.seek(0) +# +# # d = np.load(b, allow_pickle=True) +# # print(d['raw'].shape) +# # b.close() +# # print(user_id, raw.project_id) +# return {'message': "Raw Data Exists."}, 201 +# +# return {'message': 'Raw Data does not exist'}, 404 +# +# @jwt_required +# def post(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# raw = proj.raw +# +# if raw: +# return {'message': "Raw Data already exists."}, 400 +# filestr = request.data +# # data = RawData.parser.parse_args() +# +# # print(eval(data['raw']).shape) +# raw = RawModel(user_id=user_id, project_id=proj.id, data=filestr) # data['raw']) +# +# try: +# raw.save_to_db() +# except: +# return {"message": "An error occurred inserting raw data."}, 500 +# +# return "Success", 201 +# +# @jwt_required +# def delete(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# raw = proj.raw +# if raw: +# raw.delete_from_db() +# return {'message': 'Raw Data deleted.'} +# return {'message': 'Raw Data does not exist.'}, 404 +# +# @jwt_required +# def put(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# raw = proj.raw +# filestr = request.data +# if raw: +# print('here') +# raw.data = filestr +# try: +# raw.save_to_db() +# except: +# return {"message": "An error occurred inserting raw data."}, 500 +# return "Success", 201 +# +# else: +# raw = RawModel(user_id, data=filestr, project_id=proj.id) +# try: +# print('now here') +# raw.save_to_db() +# except: +# return {"message": "An error occurred inserting raw data."}, 500 +# +# return "Success", 201 + + +SESSION = dict() class RawDataDefault(Resource): - parser = reqparse.RequestParser() - parser.add_argument('raw', type=str, required=True, help="This field cannot be left blank!") @jwt_required def get(self): - user_id = get_jwt_identity() - # user = UserModel.find_by_id(user_id) - project_id = request.form['project_id'] + project_id = request.json['project_id'] raw = RawModel.find_by_project_id(project_id) if raw: - response = flask.make_response(raw.data) - response.headers.set('Content-Type', 'application/octet-stream') - return response + if raw.mode == 0: + response = flask.make_response(raw.data) + response.headers.set('Content-Type', 'application/octet-stream') + return response, 210 + else: + if request.json['start'] is None: + return {'message': 'SERVER MODE'}, 212 + if project_id in SESSION: + raw_data = SESSION[project_id] + else: + with open(raw.data, 'rb') as f: + raw_data = pickle.load(f) + start = request.json['start'] + stop = request.json['stop'] + limit = request.json['limit'] + stop = min(len(raw_data), stop) + + ds = int((stop - start) / limit) + 1 + visible = raw_data[start:stop:ds] + + buffer = io.BytesIO() + np.savez_compressed(buffer, visible=visible, stop=stop, ds=ds) + buffer.seek(0) + data = buffer.read() + buffer.close() + response = flask.make_response(data) + response.headers.set('Content-Type', 'application/octet-stream') + response.status_code = 211 + return response return {'message': 'Raw Data does not exist'}, 404 @jwt_required def post(self): - filestr = request.data - user_id = get_jwt_identity() - if RawModel.find_by_user_id(user_id): - return {'message': "Raw Data already exists."}, 400 - - # data = RawData.parser.parse_args() - - # print(eval(data['raw']).shape) - raw = RawModel(user_id=user_id, data=filestr) # data['raw'] - - try: - raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 - - return "Success", 201 - - @jwt_required - def delete(self): - user_id = get_jwt_identity() - raw = RawModel.find_by_user_id(user_id) - if raw: - raw.delete_from_db() - return {'message': 'Raw Data deleted.'} - return {'message': 'Raw Data does not exist.'}, 404 - - @jwt_required - def put(self): - filestr = request.form['raw_bytes'] + raw_data = request.json['raw_data'] user_id = get_jwt_identity() - project_id = request.form['project_id'] + project_id = request.json['project_id'] + mode = request.json['mode'] raw = RawModel.find_by_project_id(project_id) + if mode == 0: + raw_data_path = raw_data + elif mode == 1: + try: + raw_data_path, SESSION[project_id] = read_file_in_server(request.json) + except TypeError as e: + return {"message": str(e)}, 400 + except ValueError as e: + return {"message": str(e)}, 400 + except KeyError: + return {"message": 'Provided variable name is incorrect'}, 400 + else: + return {"message": f"Mode {mode} not supported"}, 400 if raw: - raw.data = filestr - print("raw data in if ", raw) + raw.data = raw_data_path + raw.mode = mode try: - print("raw data in try ", raw) raw.save_to_db() except: return {"message": "An error occurred inserting raw data."}, 500 return "Success", 201 else: - raw = RawModel(user_id, data=filestr, project_id=project_id) + raw = RawModel(user_id, data=raw_data_path, project_id=project_id, mode=mode) try: raw.save_to_db() diff --git a/ross_backend/resources/detect.py b/ross_backend/resources/detect.py index 75cdc90..cfaf75e 100644 --- a/ross_backend/resources/detect.py +++ b/ross_backend/resources/detect.py @@ -1,109 +1,109 @@ import pickle import traceback -from uuid import uuid4 from pathlib import Path +from uuid import uuid4 from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource, reqparse, request + from models.config import ConfigDetectionModel from models.data import DetectResultModel -from models.project import ProjectModel from models.data import RawModel from resources.funcs.detection import startDetection -class Detect(Resource): - parser = reqparse.RequestParser(bundle_errors=True) - parser.add_argument('filter_type', type=str, required=True, choices=('butter')) - parser.add_argument('filter_order', type=int, required=True) - parser.add_argument('pass_freq', type=int, required=True) - parser.add_argument('stop_freq', type=int, required=True) - parser.add_argument('sampling_rate', type=int, required=True) - parser.add_argument('thr_method', type=str, required=True, choices=('median', 'wavelet', 'plexon')) - parser.add_argument('side_thr', type=str, required=True, choices=('positive', 'negative', 'two')) - parser.add_argument('pre_thr', type=int, required=True) - parser.add_argument('post_thr', type=int, required=True) - parser.add_argument('dead_time', type=int, required=True) - parser.add_argument('run_detection', type=bool, default=False) - - @jwt_required - def get(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - return config.json() - return {'message': 'Detection config does not exist'}, 404 - - @jwt_required - def post(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - return {'message': "Config Detection already exists."}, 400 - - data = Detect.parser.parse_args() - - config = ConfigDetectionModel(user_id, **data, project_id=proj.id) - - try: - config.save_to_db() - except: - return {"message": "An error occurred inserting detection config."}, 500 - - # if data['run_detection']: - # try: - # print('starting Detection ...') - # startDetection() - # except: - # return {"message": "An error occurred in detection."}, 500 - - return config.json(), 201 - - @jwt_required - def delete(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - config.delete_from_db() - return {'message': 'Detection config deleted.'} - return {'message': 'Detection config does not exist.'}, 404 - - @jwt_required - def put(self, name): - data = Detect.parser.parse_args() - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - for key in data: - config.key = data[key] - try: - config.save_to_db() - except: - return {"message": "An error occurred inserting detection config."}, 500 - - return config.json(), 201 - - else: - config = ConfigDetectionModel(user_id, **data, project_id=proj.id) - - try: - config.save_to_db() - except: - return {"message": "An error occurred inserting detection config."}, 500 - - return config.json(), 201 +# class Detect(Resource): +# parser = reqparse.RequestParser(bundle_errors=True) +# parser.add_argument('filter_type', type=str, required=True, choices=('butter')) +# parser.add_argument('filter_order', type=int, required=True) +# parser.add_argument('pass_freq', type=int, required=True) +# parser.add_argument('stop_freq', type=int, required=True) +# parser.add_argument('sampling_rate', type=int, required=True) +# parser.add_argument('thr_method', type=str, required=True, choices=('median', 'wavelet', 'plexon')) +# parser.add_argument('side_thr', type=str, required=True, choices=('positive', 'negative', 'two')) +# parser.add_argument('pre_thr', type=int, required=True) +# parser.add_argument('post_thr', type=int, required=True) +# parser.add_argument('dead_time', type=int, required=True) +# parser.add_argument('run_detection', type=bool, default=False) +# +# @jwt_required +# def get(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# config = proj.config +# if config: +# return config.json() +# return {'message': 'Detection config does not exist'}, 404 +# +# @jwt_required +# def post(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# config = proj.config +# if config: +# return {'message': "Config Detection already exists."}, 400 +# +# data = Detect.parser.parse_args() +# +# config = ConfigDetectionModel(user_id, **data, project_id=proj.id) +# +# try: +# config.save_to_db() +# except: +# return {"message": "An error occurred inserting detection config."}, 500 +# +# # if data['run_detection']: +# # try: +# # print('starting Detection ...') +# # startDetection() +# # except: +# # return {"message": "An error occurred in detection."}, 500 +# +# return config.json(), 201 +# +# @jwt_required +# def delete(self, name): +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# config = proj.config +# if config: +# config.delete_from_db() +# return {'message': 'Detection config deleted.'} +# return {'message': 'Detection config does not exist.'}, 404 +# +# @jwt_required +# def put(self, name): +# data = Detect.parser.parse_args() +# user_id = get_jwt_identity() +# proj = ProjectModel.find_by_project_name(user_id, name) +# if not proj: +# return {'message': 'Project does not exist'}, 404 +# config = proj.config +# if config: +# for key in data: +# config.key = data[key] +# try: +# config.save_to_db() +# except: +# return {"message": "An error occurred inserting detection config."}, 500 +# +# return config.json(), 201 +# +# else: +# config = ConfigDetectionModel(user_id, **data, project_id=proj.id) +# +# try: +# config.save_to_db() +# except: +# return {"message": "An error occurred inserting detection config."}, 500 +# +# return config.json(), 201 class DetectDefault(Resource): @@ -124,7 +124,7 @@ class DetectDefault(Resource): @jwt_required def get(self): - user_id = get_jwt_identity() + # user_id = get_jwt_identity() # user = UserModel.find_by_id(user_id) project_id = request.form['project_id'] config = ConfigDetectionModel.find_by_project_id(project_id) @@ -138,7 +138,7 @@ def post(self): if ConfigDetectionModel.find_by_user_id(user_id): return {'message': "Detection config already exists."}, 400 - data = Detect.parser.parse_args() + data = DetectDefault.parser.parse_args() config = ConfigDetectionModel(user_id, **data) try: @@ -203,7 +203,7 @@ def put(self): print("inserting detection result to database") detection_result_path = str(Path(RawModel.find_by_project_id(project_id).data).parent / \ - (str(uuid4()) + '.pkl')) + (str(uuid4()) + '.pkl')) with open(detection_result_path, 'wb') as f: pickle.dump(data_file, f) diff --git a/ross_backend/resources/detection_result.py b/ross_backend/resources/detection_result.py index 099eab5..0010163 100644 --- a/ross_backend/resources/detection_result.py +++ b/ross_backend/resources/detection_result.py @@ -6,6 +6,7 @@ from flask import request from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource, reqparse + from models.data import DetectResultModel @@ -15,8 +16,6 @@ class DetectionResultDefault(Resource): @jwt_required def get(self): - user_id = get_jwt_identity() - # user = UserModel.find_by_id(user_id) project_id = request.form["project_id"] detect_result_model = DetectResultModel.find_by_project_id(project_id) diff --git a/ross_backend/resources/funcs/detection.py b/ross_backend/resources/funcs/detection.py index 1c80a05..ec6c355 100644 --- a/ross_backend/resources/funcs/detection.py +++ b/ross_backend/resources/funcs/detection.py @@ -3,6 +3,7 @@ import numpy as np import pyyawt import scipy.signal + from models.config import ConfigDetectionModel from models.data import RawModel diff --git a/ross_backend/resources/funcs/skew_t_sorter.py b/ross_backend/resources/funcs/skew_t_sorter.py index 07381ee..1fd049a 100644 --- a/ross_backend/resources/funcs/skew_t_sorter.py +++ b/ross_backend/resources/funcs/skew_t_sorter.py @@ -1,15 +1,16 @@ import numpy as np import sklearn.decomposition as decomp +from scipy import optimize +from scipy import stats +from scipy.spatial.distance import cdist +from scipy.special import gamma + from resources.funcs.fcm import FCM from resources.funcs.sort_utils import ( matrix_sqrt, dmvt_ls, d_mixedmvST, ) -from scipy import optimize -from scipy import stats -from scipy.spatial.distance import cdist -from scipy.special import gamma def skew_t_sorter(alignedSpikeMat, sdd, REM=np.array([]), INPCA=False): diff --git a/ross_backend/resources/funcs/sort_utils.py b/ross_backend/resources/funcs/sort_utils.py index 0a6da0d..6bb23bf 100644 --- a/ross_backend/resources/funcs/sort_utils.py +++ b/ross_backend/resources/funcs/sort_utils.py @@ -36,7 +36,7 @@ def dmvt_ls(y, mu, Sigma, landa, nu): t = scipy.stats.t(nu + p) dens = 2 * (denst) * t.cdf(np.sqrt((p + nu) / (mahalanobis_d + nu)) * np.expand_dims(np.sum( np.tile(np.expand_dims(np.linalg.lstsq(matrix_sqrt(Sigma).T, landa.T, rcond=None)[0], axis=0), (n, 1)) * ( - y - mu), axis=1), axis=1)) + y - mu), axis=1), axis=1)) return dens diff --git a/ross_backend/resources/funcs/sorting.py b/ross_backend/resources/funcs/sorting.py index 0829d6d..172ada3 100644 --- a/ross_backend/resources/funcs/sorting.py +++ b/ross_backend/resources/funcs/sorting.py @@ -1,6 +1,5 @@ import pickle -import numpy as np from models.config import ConfigSortModel from models.data import DetectResultModel from resources.funcs.gmm import * diff --git a/ross_backend/resources/funcs/t_sorting.py b/ross_backend/resources/funcs/t_sorting.py index fae81fc..f70cefc 100644 --- a/ross_backend/resources/funcs/t_sorting.py +++ b/ross_backend/resources/funcs/t_sorting.py @@ -1,9 +1,10 @@ import numpy as np import scipy.linalg import sklearn.decomposition as decomp -from resources.funcs.fcm import FCM from scipy.special import gamma +from resources.funcs.fcm import FCM + def t_dist_sorter(alignedSpikeMat, sdd): out = dict() diff --git a/ross_backend/resources/project.py b/ross_backend/resources/project.py index a5c0cfa..28eeddd 100644 --- a/ross_backend/resources/project.py +++ b/ross_backend/resources/project.py @@ -1,5 +1,6 @@ from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource + from models.config import ConfigDetectionModel, ConfigSortModel from models.data import RawModel from models.project import ProjectModel diff --git a/ross_backend/resources/sort.py b/ross_backend/resources/sort.py index d885be3..56a958e 100644 --- a/ross_backend/resources/sort.py +++ b/ross_backend/resources/sort.py @@ -1,14 +1,14 @@ import pickle import traceback -from uuid import uuid4 from pathlib import Path +from uuid import uuid4 from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource, reqparse, request + from models.config import ConfigSortModel from models.data import SortResultModel, RawModel from models.project import ProjectModel -from models.user import UserModel from resources.funcs.sorting import startSorting, startReSorting @@ -174,7 +174,6 @@ class SortDefault(Resource): def get(self): user_id = get_jwt_identity() project_id = request.form['project_id'] - user = UserModel.find_by_id(user_id) config = ConfigSortModel.find_by_project_id(project_id) if config: return config.json() diff --git a/ross_backend/resources/sorting_result.py b/ross_backend/resources/sorting_result.py index 7ab744f..bfae282 100644 --- a/ross_backend/resources/sorting_result.py +++ b/ross_backend/resources/sorting_result.py @@ -8,6 +8,7 @@ from flask import request from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource, reqparse + from models.data import SortResultModel diff --git a/ross_backend/resources/user.py b/ross_backend/resources/user.py index 04f8934..3c88cc7 100644 --- a/ross_backend/resources/user.py +++ b/ross_backend/resources/user.py @@ -1,4 +1,3 @@ -from blacklist import BLACKLIST from flask_jwt_extended import ( create_access_token, create_refresh_token, @@ -8,10 +7,12 @@ jwt_required ) from flask_restful import Resource, reqparse +from werkzeug.security import safe_str_cmp + +from blacklist import BLACKLIST from models.config import ConfigDetectionModel, ConfigSortModel from models.project import ProjectModel from models.user import UserModel -from werkzeug.security import safe_str_cmp _user_parser = reqparse.RequestParser() _user_parser.add_argument('username', diff --git a/ross_backend/rutils/io.py b/ross_backend/rutils/io.py index e69de29..4a00e4b 100644 --- a/ross_backend/rutils/io.py +++ b/ross_backend/rutils/io.py @@ -0,0 +1,44 @@ +import os +import pickle +from pathlib import Path +from uuid import uuid4 + +from scipy.io import loadmat + +Raw_data_path = os.path.join(Path(__file__).parent, '../ross_data/Raw_Data') +Path(Raw_data_path).mkdir(parents=True, exist_ok=True) + + +def read_file_in_server(request_data: dict): + print(request_data) + if 'raw_data' in request_data and 'project_id' in request_data: + filename = request_data['raw_data'] + file_extension = os.path.splitext(filename)[-1] + if file_extension == '.mat': + file_raw = loadmat(filename) + variables = list(file_raw.keys()) + if '__version__' in variables: variables.remove('__version__') + if '__header__' in variables: variables.remove('__header__') + if '__globals__' in variables: variables.remove('__globals__') + + if len(variables) > 1: + if 'varname' in request_data: + variable = request_data['varname'] + else: + raise ValueError("More than one variable exists ") + else: + variable = variables[0] + + temp = file_raw[variable].flatten() + + # ------------------ save raw data as pkl file in data_set folder ----------------------- + address = os.path.join(Raw_data_path, str(uuid4()) + '.pkl') + + with open(address, 'wb') as f: + pickle.dump(temp, f) + # ---------------------------------------------------------------------------------------- + return address, temp + else: + raise TypeError("File not supported") + else: + raise ValueError("request data is incorrect") diff --git a/ross_ui/controller/hdf5.py b/ross_ui/controller/hdf5.py index f974023..48dbfa9 100644 --- a/ross_ui/controller/hdf5.py +++ b/ross_ui/controller/hdf5.py @@ -1,9 +1,13 @@ import numpy as np import pyqtgraph as pg +from model.api import API + class HDF5Plot(pg.PlotCurveItem): def __init__(self, *args, **kwds): + self.pen = None + self.api = None self.hdf5 = None self.limit = 10000 pg.PlotCurveItem.__init__(self, *args, **kwds) @@ -13,13 +17,13 @@ def setHDF5(self, data, pen=None): self.pen = pen self.updateHDF5Plot() + def setAPI(self, api: API): + self.api = api + def viewRangeChanged(self): self.updateHDF5Plot() def updateHDF5Plot(self): - if self.hdf5 is None: - self.setData([]) - return vb = self.getViewBox() if vb is None: @@ -27,13 +31,24 @@ def updateHDF5Plot(self): xrange = vb.viewRange()[0] start = max(0, int(xrange[0]) - 1) - stop = min(len(self.hdf5), int(xrange[1] + 2)) - ds = int((stop - start) / self.limit) + 1 - visible = self.hdf5[start:stop:ds] + if self.hdf5 is None: + stop = int(xrange[1] + 2) + res = self.api.get_raw_data(start, stop, self.limit) + if not res['stat']: + self.setData([]) + return + stop = res['stop'] + visible = res['visible'] + ds = res['ds'] + else: + stop = min(len(self.hdf5), int(xrange[1] + 2)) + ds = int((stop - start) / self.limit) + 1 + visible = self.hdf5[start:stop:ds] + x = np.arange(start, stop, ds) self.setData(x, visible) if self.pen is not None: self.setPen(self.pen) - self.resetTransform() \ No newline at end of file + self.resetTransform() diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 5c441f9..3777721 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -1,14 +1,15 @@ +import enum import os +import pathlib import pickle import random import time import traceback from uuid import uuid4 -import enum -import pathlib import matplotlib.pyplot as plt import numpy as np +import pandas as pd import pyqtgraph import pyqtgraph.exporters import pyqtgraph.opengl as gl @@ -21,7 +22,6 @@ from nptdms import TdmsFile from shapely.geometry import Point, Polygon from sklearn.neighbors import NearestNeighbors -import pandas as pd from controller.detectedMatSelect import DetectedMatSelectApp as detected_mat_form from controller.detectedTimeSelect import DetectedTimeSelectApp as detected_time_form @@ -33,6 +33,7 @@ from controller.saveAs import SaveAsApp as save_as_form from controller.segmented_time import SegmentedTime from controller.serverAddress import ServerApp as server_form +from controller.serverFileDialog import ServerFileDialogApp as sever_dialog from controller.signin import SigninApp as signin_form from view.mainWindow import MainWindow @@ -308,6 +309,12 @@ def open_signin_dialog(self): self.statusBar().showMessage(self.tr("Loaded."), 2500) self.saveAct.setEnabled(True) + @QtCore.pyqtSlot() + def open_file_dialog_server(self): + dialog = sever_dialog(self.user) + if dialog.exec_() == QtWidgets.QDialog.Accepted: + pass + def open_server_dialog(self): dialog = server_form(server_text=self.url) if dialog.exec_() == QtWidgets.QDialog.Accepted: @@ -467,6 +474,7 @@ def onSort(self): def plotRaw(self): curve = HDF5Plot() + curve.setAPI(self.user) curve.setHDF5(self.raw) self.widget_raw.clear() self.widget_raw.addItem(curve) @@ -1389,12 +1397,13 @@ def loadDefaultProject(self): res = self.user.get_raw_data() if res['stat']: - with open(res['raw'], 'rb') as f: - new_data = pickle.load(f) + if 'raw' in res: + with open(res['raw'], 'rb') as f: + new_data = pickle.load(f) - self.raw = new_data - self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() + self.raw = new_data + self.statusBar().showMessage(self.tr("Plotting..."), 2500) + self.wait() flag_raw = True res = self.user.get_detection_result() diff --git a/ross_ui/controller/serverFileDialog.py b/ross_ui/controller/serverFileDialog.py index 293c80c..93ef9a6 100644 --- a/ross_ui/controller/serverFileDialog.py +++ b/ross_ui/controller/serverFileDialog.py @@ -1,34 +1,53 @@ +import os + from PyQt5 import QtWidgets -from model.api import UserAccount -from view.signin import Signin_Dialog +from model.api import API +from view.serverFileDialog import ServerFileDialog + + +class ServerFileDialogApp(ServerFileDialog): + def __init__(self, api: API): + super(ServerFileDialogApp, self).__init__() + self.api = api + self.root = None + self.list_folder.itemDoubleClicked.connect(self.itemDoubleClicked) -class SigninApp(Signin_Dialog): - def __init__(self, server): - super(SigninApp, self).__init__(server) - self.pushButton_in.pressed.connect(self.accept_in) - self.pushButton_up.pressed.connect(self.accept_up) + self.request_dir() - def accept_in(self): - username = self.textEdit_username.text() - password = self.textEdit_password.text() - self.user = UserAccount(self.url) - res = self.user.sign_in(username, password) - if res['stat']: - super().accept() + def request_dir(self): + self.list_folder.clear() + dir_dict = self.api.browse(self.root) + if dir_dict is not None: + self.line_address.setText(dir_dict['root']) + + item = QtWidgets.QListWidgetItem('..') + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) + self.list_folder.addItem(item) + + for folder_name in dir_dict['folders']: + item = QtWidgets.QListWidgetItem(folder_name) + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) + self.list_folder.addItem(item) + for filename in dir_dict['files']: + item = QtWidgets.QListWidgetItem(filename) + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_FileIcon)) + self.list_folder.addItem(item) else: - QtWidgets.QMessageBox.critical(self, "Error", res["message"]) - - def accept_up(self): - username = self.textEdit_username.text() - password = self.textEdit_password.text() - self.user = UserAccount(self.url) - res = self.user.sign_up(username, password) - if res['stat']: - self.label_res.setStyleSheet("color: green") - QtWidgets.QMessageBox.information(self, "Account Created", res["message"]) + QtWidgets.QMessageBox.critical(self, 'Error', 'Server Error') + + def itemDoubleClicked(self, item: QtWidgets.QListWidgetItem): + name = item.text() + isfolder = item.icon().name() == 'folder' + if isfolder: + self.root = os.path.join(self.line_address.text(), name) + self.request_dir() else: - self.label_res.setStyleSheet("color: red") - # self.label_res.setText(res['message']) - QtWidgets.QMessageBox.critical(self, "Error", res["message"]) + ret = self.api.post_raw_data(raw_data_path=os.path.join(self.line_address.text(), name), + mode=1, + varname=self.line_varname.text()) + if ret['stat']: + QtWidgets.QMessageBox.information(self, 'Info', 'File successfully added.') + else: + QtWidgets.QMessageBox.critical(self, 'Error', ret['message']) diff --git a/ross_ui/controller/signin.py b/ross_ui/controller/signin.py index 293c80c..2be3035 100644 --- a/ross_ui/controller/signin.py +++ b/ross_ui/controller/signin.py @@ -1,6 +1,6 @@ from PyQt5 import QtWidgets -from model.api import UserAccount +from model.api import API from view.signin import Signin_Dialog @@ -13,7 +13,7 @@ def __init__(self, server): def accept_in(self): username = self.textEdit_username.text() password = self.textEdit_password.text() - self.user = UserAccount(self.url) + self.user = API(self.url) res = self.user.sign_in(username, password) if res['stat']: super().accept() @@ -23,7 +23,7 @@ def accept_in(self): def accept_up(self): username = self.textEdit_username.text() password = self.textEdit_password.text() - self.user = UserAccount(self.url) + self.user = API(self.url) res = self.user.sign_up(username, password) if res['stat']: self.label_res.setStyleSheet("color: green") diff --git a/ross_ui/model/api.py b/ross_ui/model/api.py index c102299..7ad8d85 100644 --- a/ross_ui/model/api.py +++ b/ross_ui/model/api.py @@ -4,7 +4,7 @@ import requests -class UserAccount(): +class API(): def __init__(self, url): self.url = url self.refresh_token = None @@ -21,6 +21,9 @@ def refresh_jwt_token(self): if response.ok: self.access_token = response.json()["access_token"] + return True + else: + return False def sign_up(self, username, password): data = {"username": username, "password": password} @@ -56,104 +59,65 @@ def sign_out(self): else: return {'stat': False, 'message': response.json()["message"]} - def post_raw_data(self, raw_data): - if not (self.access_token is None): + def post_raw_data(self, raw_data_path, mode=0, varname=''): + if self.access_token is not None: + # buffer = io.BytesIO() # np.savez_compressed(buffer, raw=raw_data) # buffer.seek(0) # raw_bytes = buffer.read() # buffer.close() - response = requests.put(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, - data={"raw_bytes": raw_data, "project_id": self.project_id}) + response = requests.post(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, + json={"raw_data": raw_data_path, + "project_id": self.project_id, + "mode": mode, + "varname": varname}) if response.ok: return {'stat': True, 'message': 'success'} elif response.json()["message"] == 'The token has expired.': self.refresh_jwt_token() - response = requests.put(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, - data={"raw_bytes": raw_data, "project_id": self.project_id}) + response = requests.post(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, + json={"raw_data": raw_data_path, + "project_id": self.project_id, + "mode": mode, + "varname": varname}) if response.ok: return {'stat': True, 'message': 'success'} return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} - # def post_detected_data(self, spike_mat, spike_time): - # if not (self.access_token is None): - # buffer = io.BytesIO() - # np.savez_compressed(buffer, spike_mat=spike_mat, spike_time=spike_time) - # buffer.seek(0) - # detected_bytes = buffer.read() - # buffer.close() - # response = requests.put(self.url + '/detect', headers={'Authorization': 'Bearer ' + self.access_token}, - # data={"raw_bytes": detected_bytes, "project_id": self.project_id}) - # - # if response.ok: - # return {'stat': True, 'message': 'success'} - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.put(self.url + '/detect', headers={'Authorization': 'Bearer ' + self.access_token}, - # data=detected_bytes) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - - # def post_sorting_data(self, clusters): - # pass - - # def load_project(self, project_name): - # if not (self.access_token is None): - # response = requests.get(self.url + '/project/' + project_name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True} - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.get(self.url + '/project/' + project_name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - - # def get_projects(self): - # if not (self.access_token == None): - # response = requests.get(self.url + '/projects', headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'projects': response.json()['projects']} - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.get(self.url + '/projects', - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'projects': response.json()["projects"]} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - - def get_raw_data(self): + def get_raw_data(self, start=None, stop=None, limit=None): if not (self.access_token is None): response = requests.get(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) + json={'project_id': self.project_id, + 'start': start, + 'stop': stop, + 'limit': limit}) + if response.ok: - # b = io.BytesIO() - # b.write(response.content) - # b.seek(0) - # d = np.load(b, allow_pickle=True) - return {'stat': True, 'raw': response.content} + if response.status_code == 210: + return {'stat': True, 'raw': response.content} + elif response.status_code == 211: + iob = io.BytesIO() + iob.write(response.content) + iob.seek(0) + raw_data = np.load(iob, allow_pickle=True) + return {'stat': True, + 'visible': raw_data['visible'].flatten(), + 'stop': raw_data['stop'].flatten(), + 'ds': raw_data['ds'].flatten()} + elif response.status_code == 212: + return {'stat': True, 'message': 'SERVER MODE'} + else: + return {'stat': False, 'message': 'Status code not supported!'} elif response.status_code == 401: - self.refresh_jwt_token() - response = requests.get(self.url + '/raw', - headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) - if response.ok: - b = io.BytesIO() - b.write(response.content) - b.seek(0) - d = np.load(b, allow_pickle=True) - return {'stat': True, 'raw': d['raw'].flatten()} + ret = self.refresh_jwt_token() + if ret: + self.get_raw_data(start, stop, limit) return {'stat': False, 'message': response.json()['message']} return {'stat': False, 'message': 'Not Logged In!'} @@ -379,3 +343,20 @@ def save_sort_results(self, clusters): return {'stat': True, 'message': 'success'} return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} + + def browse(self, root: str): + if root is None: + response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}) + else: + response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}, + json={'root': root}) + if response.ok: + return response.json() + else: + return None + + def browse_send_filename(self, filename, varname): + response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}, + json={'filename': filename, 'varname': varname, 'project_id': self.project_id}) + if response.ok: + return response diff --git a/ross_ui/view/icons/ross.png b/ross_ui/view/icons/ross.png index 0d0afb7a165cd499ebd1f31a2661a0f9e1f0df73..457254d2a134520e86034eb0d4253b6984a31cf1 100644 GIT binary patch literal 26539 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 F{|^rjw(I}^ literal 24600 zcmeFYbyOT((=XaHxVzf`2@>4h-5r9v1$T!*0wlO35P~H*!6i5h2?-V?xI=Jv*Grz~ zdG&th-tSxMoPW-GnKiw-dsqE-Rqd+UHPy{4byYbGR8mv`05BBfr8NNn^mq!ukP#n` zJ9}+50D#@)ul?Lp)7%H*>h5A=?`RG2^mDa_So_*P4*Jd%WY~L?3sposco4L~CoeUU z$8WS!oF3o6z0-L*eJrv!nXDsaLzp5B0fXKSTwgpmov;N<{+Kva*Hq~}#(PoRe)Mtb zW^^#G_Lp#b@Z8DeUL#56Xtvas+PQ|OB02sPK8umn6-Up4Ns5JOo9^$VF@`SJ(n7Pz zUJdq~Sct-n+d_^mA_5TYeDrz8lRH>HRNe@iGBI8ZZGNR}->#lIXfj=lQa-g=6i2n(Sv<;s~e1_FCza^0b8)+tIkHJS<6zPgL8{lb`+U1$E9`P`pb*2{Q}RQ z+-yfN1WSa>pX-+@Gv@MR?n>MSudI@g6|jXsYj zV_IKpLws+~;C|g>4Ou?N8}kLJr%iDKgL4s?jNEwxa5ul`67r22)Afmh+~mnQH6a&ee3S!KT#=!z5fq z=8{!SMV9jGXVErgCF=$+%FEq`zW2nb@v+*K->{0D+WBucrI`efS1F1PHG4kTy=O8- z5`v?%dQU&6jJfD~@?g3eD|wODa~D6WRy^Px@f=gKPZFb)V5*yEfaqQyVg|`GnP1e)y%Et&s6pnIlVcRx7(hw6aIUCx zaxg4!8kX+M^x7jpAqA6s>>oh|r3VGPD+u}iz}c)IBga~OL3o_4JF$LU)j(@)dm^$o zaRtx|S+@-~Z}iPumu_Z`a_ud9XWQth1+Kqq-y>-})2a2_Z>l$L3Q6~Fs5lvo%q}Icjww=x_I7i~rYhozhLw_{q*LmgF z-i7s8debP&vYhVG=d3V^Ekfq97s%T*J*3i7?U5Iyc?%hm6ShaI_t7-7lhPjr4=pe? z4P1g+>fp9opN_vHBjHW{DRf*R*#=9t%&s19S0tYGp`+^CWpYc9&`^GNI{fZxjWg^6 ziC*C(ZRFNT@?6i?T4s!^*3iZ6K*j!Ey<)B-siwlUXS-uC&o~b?++3;oiPMv634ctx zlH4;LEW$6wJ3AFk*J}>m9EnQiu}#aYegnqDBPpBmPESvnIH|4#bBYDP^?sDC2CpT= z^a*57-T?6MY-7B6OraznxHz*6?-Y6&KI|~rD4kwpZKvG%7_C19t!_#x7w+4Up_3-* z=x~GL4!z7J7DvYvnuXxQ6_n;`ZL03z3$R8lWFHW(7L(x8%nI?i+5Ernuj--OCRN~o z3OR>H4y^X)oaEG^n)+P@ZygTfUDb4z&pA&i{5W@feW~=Une~$o$S3_W=^XbXa%)jL z@HkNMTS>K?sNYHyYVss9jIpI)m#gqG3Ov(g*}~qmG3&A!{jR*6V`y5GuJZ{K_> z_$n{PP|;b!6wyCi8MP%Pg2{D2-^3)+?4yY%uem#W0uq<2)nNXs_E`z?=klu}TeM7T z?~2(2%ByXyg1?aw{QCS_xo=g+D*Ka;Wz@scVoj6U)@5(8H3)BU6SOH@oWZa2N|&2g zmcECAi9gWr945sV)7Tc4ysrXg-=UmB zc(dH+OFHPMbUyujWj_~58tUq%JCi`vIye~`UIYp2yGJc1Yhub>C;P}Id?E;ck1>d( zJjjq~u(#lvA+}tC%&F??fIl(b%eRM$u<;Ri?(-3&Sgy9qH;j;ye-Ctlp%RZ&h35Bw zIYmIf|1CMF;1>tz*Lv{Aa}<_e8Wj6TEEojOGd~lOY*U2l9KAPXZST!Sm2`?+53U{J z4$Auh`}q>7FpJ_~rLi#e$oB_4#GIR?8Ll+q7wNT`pmNN-3IS>P*G+Zkypz_x>94W} zPu>W)qE6E*s-s(^u;3NjZ-M(W=%^f-yj@ie*AiIkd-0k`X)AIXkV9)f6s<od@F{mmz(QhI+V{LGEwRvS&)`u?7*JlPgiBqcw2yav*VRrTKjYwmp z`?2ipVQVlf2ZSVd+%N)K<|nRQpAdE52U^a*zfLMvuBI-IAT))D;^Vz@qvvTe=0#}b z`=V`FJ%oW+UlvmX(L^PTKPnDAKvNq3qJ&;R+<_J=Em_?WaAv$_F!cJRd{+Q{&(vB! z!BG>MmjDa9+=C2s8}8HIm}m;c=Mk@m-50`70=#ec}l zbtcmgc$F*h*Zth%Uf3N4=P+#abRQWkeW0<_R!3Rsk--5=J?lI-`^hn6R{k2}%8YUk z#EORWDlJR0y8!s9Au;kkG2@dR`dd7~dM~`2#4VlLZB5>+(%c}kh8XHkW*n1aK~;kE z_#YC#ERtRwcPn!NeWht z@%?GnPgRHix~s{P z_<@V@{3pxybp~B@czPV>$VY|`82k&G&}Och{K%&fA*m!Z%59WW2e;}&kfy4&fi`%4 zIrR>pfojQaik2rp>re@mz;YpryJNFGfGa{eCbBBAE!i35U>Q$ekC!3`hio5|z|CAQ zoSe^n(xx|7-abCidQ^XXZayuyS8>bedY)vYxT{T~`=u;lf#l}n5B^jVwc*JqZt>G+ zBT+u6%NoLh+ItpWrzYq_QQVhffxUIUEf#j#D6?6US&{4kVpP(7F*PW&YC+7OG)!sv z>!c+|5O(M&oSuLD6fVoH@-`Mjqi6$9eh$l zL=sJkAniq#MP1>#yjNMQ@%}=BC=t8KlQRONep8<03{Dx$wO=GmzJ8oX7$TQQ$}8rf`WC&YYxFpr;~PO#xYT$ zQ40BepQX%9Qm2_Vf5Q5jR_bmytB7d6?viLeS@%=Zb&PUdX2M!OJQz7a?|vNph*!`L zH;eOZ?65nlAI)V&LyFIH9eYVtx#eR-m%&>k>ZD6&z6fJT2izR3ZQZJ)eoH3ri_KDH zMvz~U*=f9~OZjRX{ox(L#jkm00i6KvYJ8anL-fHn(+tl`(Cgwu24B8M zsT3BUzhNziW^NG_wO8R%Jx)Si4Xr`{fNj7mXS}6PHVJGZlL^Sj*>m&1e&KiP?3^dN ztucW3vfHTW3P(P}jQ}pdL$Z8S7e3)fJJZ67&H7WG5wmTl;0oCvfr7b)bSqfaBUzjz z3G&0W&!4V&PLG=p8*whbc3VOm7PX{ca%a~Ln(bX2PNFISFDn}EnUJ}w)Im0%>^E;I zvF2qW7>3=oZX=p{ncCxK$~>j?bJotl7l(-$w+Z$iJ^{Z~_`*4C(omAF7hF%7&hL}S!TiZ!(=}8E;_Ho9m}d0y~wq|SKBg+-7;=CO25>3 z(adm`YZ?p9J_d`uDjSt^l#h+-ihx2=_O^r?w-MAwiRVkmx;aL#w00#&5lIL4GsitS znYRexmpN=uWqwxb+S_CayQB zCka`TIBL-V>}k8ko;tI)sd(gAd|{YECBy5YfVlw2Ge1P_R!_XuVjcSR`Isi|#?z^M zIG!$%ZyCumOkd9NcQQi0EHmNrJYcTH6qLWqYS^#RfBRA^kA^Z0>FkId5jF;Pd0CP#C2vBrzR3dxBOq&>l$Ij(>A>%rtVay{aDXZmDr@nW-z{s>|ZV?iN{%cMih#RvNk}Q#Xmn67LSV>)Z7ZdIOS+ndql*wffaA zj!UwGg~12VZ`v1Ca+4^wcDvk-Rd9ZJ-%J(QtZgLu7;^MUvL+hzU%o562pHbkOI;bx zl4C}8PR-Cvq=XH%;r>#t^T>+79%Ga@8Qdv@=$w;vR;tZRv6%LQj>@^frhN`aJpmtu3dn)NcFS z)@@wA`qk=5lkCJ#c>Xfyh5Qrs62G;dVMFpt6x?{V z&f)dD?9qM&)r{>JTYkJ9It%Obv*Y?b)Av88i`Z(1W(2EjJc!EQ=XcR3JD^h5B*-m_ zb)hoF6HTzGaUS;B3XiT!N1u{mI=}8)?mxSU6GNVctYJR#5O_?5I^av4HOf%**x-n;my%LfkdpdaKjX2Bkr|XEBL7vK{F~|M>yK~3 zys_PS)P?l13Eak>;eU`UCNS_~Z#qTG;C{l$8XxA+*_rUIG(4uZ0<$6vatT`AUe4d7 zd3w*rSQmY^)JS!(FS5S?l5G3I&BTsr*X&e?>&B5{OweMA0|TzHW#nz%a?mR%c#@l5 z93Gvqey3w9mU;2&YZ=3*u8l5BI^)F4q@CB@E}3Yo zXu@2Mg<2-Vr{y03?1G>=`OGp9ckHBOT2m2&iNF9ceQ3JO%R^uEfG8=t5)wowp zt7jwhg6q1FncWxdw8F2A=fdq8$S?V{is9;tERB}hw(x6SWUuWmpm2H-Oa8O_+o*3V z?T`JXclM9{rROTjf|f2$tmalO7S^o3POiV(P=K(QudBJGgS98b!rIo}S%l`GrGo}y zZzV#b$D_io;woirXD{#XZms37s%`1-U@2flBPNO}>?`;P;AHJ-4)JwzboLPR6`}b9 zSMc%t_iZ*B$R8x04k9$qRn#F;F7DP4ZdPtqb`}|5dv8t}QB;VqyOoWgrnKx|5RX?P zGE#qzN>`D92 zlUiB+o!`~V-SH1QSXr`JJ6bzEl6pMO%JDB#$|s#D6#ri+J?(A& zVda03?f1xkrTyP=|AqQ*n*1U4->oMo<>Kh#uHs^4EkeWfZ$ppiK>g3|rU;FPrko^1 zK}L#~Lx7i;g_D)z586LWEhz0`>GfMn1?j)Jl$DF6y_MiUCgJC_;N#=tw_q_B;NfNw z;O4ewG3OWHV_|3Ku;%5k<&u4{59{1#Mj1oy;G7!`{i<)|$=L+4hft z->WAmp{^i8!^z72k2~s)=AOS9L}--loxObjF`#YlWUb|C?)2!(9K8G-j~{j}c5ZeK z4!(cl*0px`c#MbNm>le^Tz`)IUSq*WWgg9I{ySP80seq})J0Ir-P+vKTKe(($ivRb z#lp_P!p^D9&MwH#CCDwn^mzOW)+|&HO3-{_`mIXkJ|l-;^AWB>0|D0En)lU0wOdLzqbO2`rmRRgp-q% z2g0Cd?`-AbzE#ly+N_pgieZ|f`fxDEUfm=K0Pa>)P3l5(?f@N0AM z2y(DLhWTH?{=e}1|Ce_Q{}CAfvO(wIub_W5;zFc+D6NSX~m7$ z9IP7LB;w(QbC)F=iP1l$@spm?V0%YOP>VhJ{^sfH#wyI&K>Com^6jyb>(YHssSD-t zlhe`pi-mU6i@ku)7p)s2;zE0MqpJUMsO2U9oMv{PgVw-*4g$P!Ezq#hfC}8@G=Od9 z%r(LdZ+9li2P{o=`F9ZA~Io@V+%$NA+FNZK;YI1YryooJ+TRL zaUm|fgM(+V3yCrz4tMxSA95C2)^{QxFuiU^+w%+-i~2?zV7-` ziiY+!bk4(WY{daRHrV3!{^IMRiwHLT?M*zraLwJtZ&l7t>f6Mf@uX)z{CuZsmYSS^ zL5S_@0J>mJxX)Qs?cq~KpMNNeU0{Oyju|$m4oj#DG4TNQ%09y_f#7J>WP)@OQY7-f zuio7Chl?$sy8c=(Xejhpjs^eM{c)+3hfUPE*I1weDBYjcA zY<7Ir`*c#+X?BGOrAaM)|LYIla*D`IWAxvReLFqN=6%Il2C&YMZn#f|K-`zVhCW=FQpAK5W!th3~>Q9u|I1_#Wc zERg~m;LD?UK62_&;Yw`yDwy5Dba{v8DN^*yrx*}wK8o}6yp7M5d6ambkjvroeLGCE z0iN4|w@w(H8bCIj8N~`U1vG|r#&P<_yZJu4wy^j?j>ixGEQ#-2K~Fqr(e%Ls)S6Ro z7_v}*>kgRETe#kYowM3wn#BpFF&oQ*7T`O70u%jj3?=u$%cn7v-ls(ofj5Jb?JN7s z=rA8}X}6f{)j)TCkD#?`k>IuEDe+GGxR7b!^#w0*$PV2>b0zE9F}b1&sBk>^vJUJw z(85{(@|Q-cBCi|a3_XEG_+>TLx!d%w-uL4|y5pplyfZ}Bpl>AN+u1^Y^wyRNnxpP?=Lg-Yfo0Ah?w;g(Th!%Jt2yq8m& z&<+=up>@FK#<3DZEOxl$)D+_fb_n%Sa*YyQG#_#$PKx&zZDV=Q3K#^AUiQ5~>}6B= zj-C~uhHsW*WlPN-EOrA8DgP>ZwtGUku<&sJ^n>tn1Gp`5VUWo0egbzWaD(IJbJ%zm zF;$EYhh2-d6q_n0F^y3JZO1jkU8991WDugKZ#7R6Ffe;z;v};xzGApjvxVP5brlaN z;zeQ?vc-Nq$B;sZ`S{3*0&DaksTxwakBR>MjQ~Ifj8cSQ=Y}VdB@=nDrNskVG&2MmE^SQW;tOJOQHzU?D?BcsruO(*p z>43>y->*1)YHxsHzH^N}D$=6z2}^+pPU+w!<|qBTwfnVW{)&~1KOIBOQ|b05IiJ66 z|GWcKpS-4sp*k}c2=jXHSGk6{v|P~t8S#)8-N2iyy9@WaoqH0v;6h}o*W7(P1SHfdDDikhsG z&8H&CGPKiPUNrubNXRfb>{gTt-gOCI>;fJzZSmczdAau8ZtIW{E&*Q9oayQ`+|qnI zNBl}clKjU)tL%QvsM|b9p8X!&jV%y+Ttmz59Wx5Nxr2giG}c@nx8J;v58huhLS|05 zfl5{$S+p@UKs4)k*5A+C7|;qO;^;+seE93HtZyoM>qm^g_?!;CRAfUa)&d>!dL21b z=B3)DzD8nFeeFajRPG0C@$@`~&(Qleev-;fO%x8=^om~b?94WSD&e((PIz1sVXilbC}Dd)I+$- zU$Acbc|T>qr5pYX>BSye@ppr)0k$8*h9^We15SWw4s4V z4Hq8SXV!{Ny{VobEH=j9Tvx3Il_Yi=7#2{~5o>q-Hb}#75N#}>dHXQE^l)cFT@g4$ zEhebhb3l1EUT!TqJCLCbc0@oYv{JRb4OU6W@?XsN7oJSs{%loS_95HCQn?o7!F83( z)2la)VD@ZHZVUlh&y^b)Xax$YgNia7j)tf(VT3#o+%DZSEub!wQ)koAuFyjU59Xkf zq7uLw|R z@H_M=-^gA4WX zx7Xr~^eL}Txx2Re8`O%;kaEy zgi|+{J-m(=ZWMrCd55{pyG9qW(KuL9M1s{6OZ%1ki#B`T(>9cfAeSxpI>Vk0tI|!P zqATVd9ozxb9hdJ!LaM60%GHE##Q5MYUuq!c(%*fbCwh5(Gxd7xbvF1C2GZlFj>mD& zs;)*7!gi8lcf-!demES|Gpa8Rg`RXBh9(ZNEvTz%thy4*kp=K0J)OA?UiI(Wnhm~h zZmaOEf5n`0-NmNaLjjbrO;cyaiWl9JFs8txCHckmU7&9iYf~&`gH4Xq0MRoN$cDFk8ZMs0TgvLC*cu!{xkO&;FQ8j6FZN z{gQ{v|Hx**X>c9RTgfGms^9;nNjKM<6c@MtI{eGZS1*VAhoAIr-xitTqWlFQWry1L4RJx$x56>g~vVR{iotm0J=Z45iy_eK7I(2c3iebZ@8;N}OP zlp+MqMdC%K%8?dNh-_)U^iYbK$$q=`7*>CFGA`^3UE!Y4}xvw8ujzT%ZqUU2ID&Je=Gj}oY;(3^M|7wsAw?ynziaJc0#8YK0M z2{Q2-iKRtkc8Padsc2=m8+uH<1jC&$Y7*>z}WebOIwZ z*`KGZIcqKkW>T!lp{3|R?+@Ao^@3=*j7OF5uc28$`jc=~C$tO#2Rl__J)FNQqPki-F+{{BF3?=%!S~I( z^NLirRXCN8zkM<7R9f&R({Y?7Yc@x?p_anIUH^hf7@&EbW&Ol665E(O6k?D?xH z|G1tv02XH~6>vD{B6fueu;JyNfZ8B;-hGz@9&Frw?wNTfqfWM@CfHkdo6MP*@WO}l zlg*fyt=R7`H99=-RjVIV=Ue)|`GoGKqL;Z0H%845*y zYmyGhrCd;$Gf5?tLlbm*YYI$Fi<=@Hl7d*_{IZ}S*dO5G`HxrKH z%NI;eDz6{Zo9hjUpmrZ(UsBMnU}nvnyBWhuSj!w%+^l_9LHSQsH%fD61UGL%J! zxEU#xfJkI_a|IM=U9_{{XDfmTli&YjQ2MC~gWrvAt0}0w*M;lmyR5rL-z8`%(c>Zo z&qvG}0V)J<#P)*$H)&xA7YJw9K9^lQX)B4+pqK?8SW z-cy|%X9b_dD{XT^AKx=Js_D0>KC>ayC#eXNpg3rMED}$zcC1?ZqC!W|JILuFN0-mo z`Nn4x+(2>bdbZ>O;Yg1^A@s~+=MN8%XQHEPLsiZ3vP?jHa3@H$s9=-UX-?EbEI`zS zv}2Jk$M?7!)%EVSeWu+$0EDYlNO4?Tl-)D zH@5vFt9bG+qGzPt0VsE~>Y`!0?yyA$P8xIKGbb4K<|vCqET}n=h#j;V2&r6eBMl9b z>Iyl|929>LQH8F={-FBdOeB720~~NmWiC6@rmmTo_JlN<^;_m>!+~wQj+5W};cCvd zd6mBH_fF#KaXL;a=@MW@*&q9YX-UDa&-hkCD^lp*#9BH-;CUMvyVO@NW$C@)%a(24 z-2H~`Ea;%sW*o)nH4>$t(@*EuI;;K^P4U}%2^76hBxoWg`7iiz=Wm%_01=ziT|_lS_B$TPybm?{MW(~{ zrE0>PN#vnRHir~0w7Qtq@vd_T9+EzrR*DWGI6Et?Fdy>{7Tn`NjNt(23|}qRZ_&v8 z04PN@>-FV3^eLND!hKcq3*d6z9jZmvVr;A;=%dyLBHva$?1OR zMk+h>1&~0OiW1Gu&d8@9#IyMLzRYTU)h`Y}-GuW`docm${?tDpQu?_Uq8Ze`$7QYF zq<^dNYS-y(8l1HT=oiVG8Kd6u$%fy}wsh)#;6rsg3_fW2)k5J{f<1}m7a->h>B>QA z496bAamTYD(uE)aA|QxVhw`(;<^ z92)8hU7m5DLkJ4V6H=e6tRUEwHuzA44lmu_kTbk_5=Y2GBCeakG>MTOpT;%_T-R)N z#7X%=7Q!Qhm7~9w-K>y+$2a6Mf{@w1GRBf!7@}0sLeJ>M7Y{L(az7vXN7se4pqbYuBSGJo^rP$BwG4o=L)N~1p3ujmM! zC{ha7eCFDUFhxg3y>u_P<*B1sf$@WM+SgN>9cIz4a@ z{_=_Z)d$Vs__z+l%~mmTSk6mvDocH51Jn1(`l?X1%$v;>N=woQOR+0;;NjWQ`cbgx zO~CU&f06f#4zcp+NBjX2w%QGOm;R#r*r9Jj!xhjmpEiREo{HBM<53UO3-)u`PZRJo zNr_?Ry7LIt}>qWAZF zdfyh@wV83QU{BndkSwVe8vBI^093!QwmTviHlCCZj`&RyEfqjawz=B!ol%M|WPiwe z5uA5x(PTnW{O?9lJ-;MRJkwmNe<{*$_^GtwAZZ)*>gv=1lma`T{69}c@foz|zHm9*}PDIHTN+TI|GDpr(JlED(B zxs#l)1+wv$xoVp$c3Ro;&XJxK zug?vzfwl+sbzGA`x7kmLTesj9Iib{1i<$KD1-FReQ`Rt;mCIsWBl3q2N{Rbi@(%*H z`h#n@kc{%7!n5x|xgtZRBzHX|mC1)05*0rhCMNCTz zJlkyHZO&QDwH)834QI_sW7BUJ7)2EU4fCn=4W$nn4#G1>Uz66wv{&{6>cV+qi~>a( zTX7g7k5#{_c+s}MLNTITD{_G+dG4dbit~N!aot7dmy*t%c7me|ZI87|v+pV0FPrO1 z*};ZLO|x*s*3~L7o{HmmqEd)cAwgDW-DsR-Q zE5*l3t)Go6J2r6~S!Qos+*ExdK{J@h7T!776BNs1q`QL^?Qm)zk{9Yh2qiA zyrb=SE2$P7-K~1{RQ`l#cw?~;BWGnO^D8l+5q(Sq4oaClKx~qPev)9tHZ!Dd09QeB zj+p?~?inU=y(a~=QQ#6*2OS?bHIo%I0ZTPdM2h35SY&O69{bb1@R`=r1J73^A!Cn@ z92Rf?k}}`WuXmx{-N@fbpPP2&>^95%Hm~I9$o5@Q+-mJxPy}Nz<)Kp1y+pzTF7iDp z>~&l4B`F9x`dKI#1hqxJA^|n*ns#U5pfm0QPde)v;RQe0HQDU25uKgH2q-1O>~Ps? zBblCo&^guIq$Aq`xttEwf3}cso|5=05ST!I_SI5IvJG#b%jT#Z6e!1r7(J-9Q`&;k zM@htu#B4UsPRM*hXqH3+?lyV&t|t8kU+a~FUB8#ihM2n~5}{Tl;pg8@#EUpMcOb)a z2izchYSu|*brY1)5KB2`>Q-L@V6f};h4lx>p zo8d|OSlIU*o!xLZU&ca`DSkx2rN6Nkci829yQ(>2q+SxjUvhEO7EDaq5pv#i`$mlL z@DuOa#J5=w^oeZMyX!a&HTY}OmW7uT(d}$ZAR1%wES>!PX334(Y-}V*NCmMp%j>2Ygv%W;afC^fZR0UiwESfJZ5mRxvGJxXpKFB|7_)xrn?6j8yAas$2n$P6*>G^ zxKPtV@Z36+($%PV&YN{4#*Q?MVVrz)^6!}gKr#K*sivw_93NK$)^PDc^;5QfsjVH;=baw7wmKj9$w7I$WQtigQi0SdoPXh(@Hr;)|^Q{6* zv6r7i#fZ#|L6is<8X7K(29qzjLpman$2eAB#txsQR_|bn^U0v7&&=|Lwuc^xSzhh% zHV!xbcn0u+_~BqAv)PTu(%6gPUnLnvhG31>;8PCnz(#6a{svo6>$%D2hRRqG^Hytx zC@q&@-QlqEp3IvYa&eRi*8)Ct0%YGYPf)r&0{F%E53z%2?s(L^)#yep%c0{5e8VB; z!o7NGKek*zg))MLO(wCI^Z_@{mp*EQ?~FpO98^{k7E93%g8;tp%ap1HvEqZf**S3e zR{$VS{rxUL9bEd><8I9jqGbt|Vq2U9aclU)*L(S)EFI6>u2dS=z(H11+G}na(yTs? z;UUaHrgpzR_r;TU+6C231Zza*HJqlOwarum4qC}gesYrGHIp7b1}S2p_fg<1@ix_v zZ8#8aPD^DY8usQ<)8Ul}%e{DR$}hq^Qin15`~1N)xAFtr;)l(zZkU#UmdY;2&;k<+ zXv9BIfTS`St_rP(D(K3-C|DsH4Ya_pBnY8oU0MLJ2rcI2y@f_M4)6{c21`%VUCjGj zM*NT}?+PICL`!mC=TTcwj|8dthGcx6T%R;OZUq*q=7iYMu3i1yO8w)(D#7_LRfD8E z_;=k8D_-@8d_{+;J!s{;YWe}str=tk{gf(h`&0w*sd&yyys7lgw~z-+T_vo?GQf&W zqX=^oT|zuS-&Sz#@wo~L&XOJWLC{0*?82V$=3Ix!say`hvNUzJQ{@lYCwUcaVT1km zrM95Nii4+*wMGp0GbN~MxzmY#Z!i+PVyCV2Qb{i$`|h}SeWQ`0r}b9=F-(~ss{aCR zJIJ>uIrOpMnzGb;4u;f6_N%+TDrm)fiW++t`nDpZ@`%1iylltauE?CEqiv#3`Kkls zc>eKME+7|lhSEWF{7v#+Sn(tayp>sxS8!?Wlco%&*J)2AG9tp{pmzMAjy%ZQV^e1*6VZ?=3aN4Ywv$4E(t}?!0 z{a~c;I`HXMMQFmLbqpMbja=3X&~?}rZ8jNKq2pK6x-K9!b=KbB7w&#uFup%}i{uqQ z8|^_oOvL<4U4M*Wy9MP_LcNk8VSZ;DZq!mHx=Tva7C`fqIP#|s?njEGd~XCjNK z>Nq|aLfAWm@WGgfP=_wl5{d#YB;wspR9AvNKlt~)#4+CKCufa%{na zf^G76>MsZW7{Qw!-@gz2n)LAFKrWnIZIE^7=L8}cg*j7vgFJ5N%zI>yM`9cSi~a7~UptZ} z3pfqPS7sm|9J3H=zRz!6zPuUQ8(l#d3xm?tRCbCUi1L9^qAoCOc{W~<4YMbwCacNT z)jsDF`}8FxW#==IQ!0wfdBaW33z1XJLQQnQzDD(7uHBs&;k#YlU8GG&C$S3)S7pLB z8OAm&b9T|Fr(uhlGJQ0TC*v0HNL95H?vMi7&krUN07ybaq6k9$X972;-)c1p3awMx zbtk{TDqKJ3I{u|VklWa>DAcLKvH{36l=akmA3 zDSdMFfJS`Gbi_V1;Hk&W2i1_C8L8qaLjy4S_HOTC`Sk8LgUAGHzcWn$@qLpKQ1KJDcKq++@;F_+X}u{`wi*M>Te9Uf0mx zzWqMIFmRUK3wYPeUmE%jgxVkJVaA`jnoos08Ysiu?#{TMn4dCo@(BQK`8@Z`!1{(W zT;FuvE}IG0^k(kWEL3`j=-WG0y}CfIg?f4&#*XSRCALNGk)WNO8Id5*%SaD?e!Y_$ zRS6*g+cXEoFe?a~9p#jo4oTl4Yt$2T4KL^+AFQ2sF>t*>9?I!Ilz8T8YOjmH)JKnQ zbbWgBtSWPcB*8!-6ergGEks7ouk)8A!vo$r1HicM~c0iT)SOph#XwHxkoF36mJkwwXap* zR!wIFPQ>}9+^rrhHGa*?)0sATz~rY=4^H48$VqUx7v~!ah_}FIpPx-Qy@@!Vq#($e z>`u?z-&51POjEZI9G2;eoxn52_SR8c>rj`$8&_NR4_1dat>0!<0CZk&& zK9mQGV#qG8S`xt2;HD6lq)bAtofF_2o3P)bUIqgFX9xB^MB1NNGvLIN&;_9C{-}&R zCdP0@8rK#0@SfG{Cj0=t1KgBg@R>4fE-Hs0y8p+X5KqIwJR)0G4)aQMUa;e7N_ROc z+rudZVNe!ki%`^BFDSROTTOm?V^xmreoA>}o7o(wkV7&fEvW!ckfO>3*R}-#ORqw_ zzVBCF~MQs$y=fYmoO(#R}yem*YmU z^F()NyyfgC$i*7>`Id)7CAxLGv`cC!LFw>FN=lfyW*l-~P@YR?8@$_xuh$#cBfE|; z*`pfv#&c*m)5L7~#)rF!t=9N8ZOi1Yx7Ed^Wzz&$BnzIiwLO0ueqOSF>JBei*H(C< zPPlc#(zc*UOEN98!pn0*T#8!D3n%ZiGCn zDJ7gz9D)@cAkK`7v>|s>M<@1Japh@33neQ*Wg?_2ZHSU0*1X~qD5-dc3HTR(tKv)u zlvT%%%i}GIzkUkNvW8~1#ioeh&Obhd?{F$b&vj%hqWIu23w?1#w-?tYLDE$B%w-gN(`4=AFdK}8@)FM@PJZwUckMWXbs1P}{V zf(nEllu#6;DkYRa=penfaKpWS;C?wD=9#@`_S)x}*=J_0!-49TDxWT)t=!}{`tqR) z5?rz>gP5MGdIP!N4ST!hoLhg9tD;Z=GMhGk;nop{IQA&e_1PV_HP&VuE3p=r`o9aO zuHy`OR+C)^?rp4vyoS!1SV8Bb;nx;Amlu6Y?KM=%*Nd6HEaS{Pyi=>m9zcD?NOWKr zB(^0o=-c0!>>FY1eF5e#mZ!f!(l6@g4Bgf5zs1+;+M>I@VFuc{hZ>Bi{K)EdO($_4 zcUuD;bbWZ#LGLrGi(<$-RJ!;P**Cx`w2Ru`qGv0l|Fo96^lE0SF1d~$@^XLwG5XdM zOK%l`?@xx`Mrmx zR&p#|8EZDHfFS?Fkw!bb^)pLU7j0;K1^tcKS*FKxO1Myqyv*RT)S^mGuCzF9^dDkA zL4T}NctHa?w2N?~#q8x0Umiu38Q<42GX)D@&?zh{l}RD!DYF#ap*V2YU0qFu=Mnm@ zrViN$D{m3rGe%a+X;r(zEYQ_AnZ2s5-m?nNx@*z2S(z@mrC_gjmUyA8vNBYwnv&-ENp2vMtRV zTO-Qte6t|@6H`~uaL4#jl8=)wFdE#w%R0)nW-CpIBdb3AI%LP<&4S%d4V2(DjJ8M^ zO`N)^*pf2|HX8NFW4|wLwN5p6vW`(;P@P~JKi#I zJ-_}!6N7kK&ph)L-7lbCi{}o!WI{ccd(`!ySZ1}aHxL- z?Iyqts!YR2Pn?(0f%g_JQ>vU(S^D~9E)uOMEM;3_8~12~vF|iRTZ{uMHdJXK(dW#6 zZa7fmYbJfTKkyyknxqma3G2r6`^mlvlyTm&1=rERw13}b3&$Nt>$HD(>C^9*8<5p% zw?mDr{aB{wGB%f{zR%RHkeG)bw7v(8imHQ7<p$;LQ zTx{p@UuS*3%E)9MCI0^-5}5K|2Z5%a0aOpkI=( z=X?*&39l5uPA!$s-cYYi=O%tg^*(PorkEJjJHC85uJgw}O(rUHJJJFqj-oA5)o)_a zAZ`6t12EF4;cb-2j|`wAY;Z8tNM&X0>Z(F_Y%x4f1`rAa397Na!EiXp^IWRaQ{peP zJS`4MzI3i_F!p`1ER$64!{L)g;cu+aXRd2%k4M$V=b8R1(45in?fzMVN6+}EhhfQd zKV)YUPHQ_621zDqG4=#A+pO&8+fz-}VIAvUKj1$nIj$ZK`&Wl4i2nKMEH-OV@4_<= z@Trn+W-Ar^q=t&g=G||Hz#k`(uHH;C?y-6z0z~HPTD)o|a3uQt6A#=Wf3wb5$3c1tJ*d;fEsdnT7Qy zD!Um8vrZ;87jPSt?wq(hNt{P+DF^oQh$`P4S42m$Wgtp!rd9!)waO}6b182-(toH3 z)q$DMwSmA~cD^)cskDG*^SzeXOCMrvWaEV9DihYkD4_wrbYkGnwc=$rlWcPRJyfun zo0{zR=+ezSpG{e87q7Klx`0}5i56tQbF?;WN;Y|{Hj9=(&p3S1awyj3l zp7fy2$ur}sfuG~qweY;n0UD_{X*yMkGQ+IS+5u+~$4&!%75qT-XPqqH^L2zv&?up) z@(4)xaQ$<>WP1SP$V#gUO%K_-=8a7CNB+pZ`^!JiZx_ea^FTlFLW=c&Z_spDiOyFB zftz9hM#^CxCg~(eZ&=Gqw+J}_CpHX|*$@kuDe61JPC zvfFg8B@jEI`u>TyD=4b7+o*k!BRbq|y3RBOC}_>Pwd@cLq&d zdyMku#f8q1)7%=)+xliIz1D5w*w{U`|KR*ZCDF)=BB?WRx%CV(aq=Ux08dgec`k9G zREVYPD$(Yhu9UzGtp|(`5g@coZ7c5=%B-!OQlxX@YyXQ?&HEGl$Acujdf*#iv+>)h z&rP09*aZal7?06cOY+@keBu7`6l0+>H@hhV@1)e@ zs|U@d_(VVP{HL2!j{|`FSKqi990FysSAF+Q9*t$6KoR2?&Z@5F%hj#sf0-ZRKy4}? z^&4i@%svJP)ZGmYs&3Tc?E~g$uFS@ZQYB|mO=>kt)!#DK9#ffT2g&batfja4&_pK4 zN;V`q#pDbPdZu|+ZzhR%o)*#Atg)+X8qon zPMu!%Tjw_egcjVDLJt$=b+wJeF=EBzK6WDTG5zmg_-)5yYZ%hx7B zS2^?&(&k0ga|>>)o7RRrvjGRu18iz1qV>eVQF-wLV6q;J&PO(pLNKG&aL-Osd)kZC zu=`6l_QLM6`8|wT{o!}Q+`&&S0#P1;yzSrgxzZ=1BUX?%@E%JT=jZyhs>2%W-1ZW;(Wy0q}uN~SMV2r)Sg&gko2eQp??-32@ zAwO|jeO}HnK0xE216jRW=TlPnR!UofbPl_Zh<~AI5$wG^yN!3|p-{U!ekoZUtH1FR zo42*F=MQ;I-KhZW_o4^2Lv$YSzG;w^d3yJT9BI0DLKGA$m*2FQRsSvI;2I*wAlR@| z#6Wu|{P{3Z)=S)AUvNT+SN?m9p5}6os$x}r-QtkBEVO~uh;kENYx>f#*IkF11<|u1 z#yosfIYFGqTd0DxePv@iM-1BgAkAQ?%1kjSxNAypX*gkKXDmyLXoQ8Cy|hvcZA@$% zvZ%+a^nrke2R7&*zxDpfk>##AbULnCG1FXH(DW}yCP-JZJLy)mS;1N*JF87wpuchP z7qxx-+k?BU&VFPJG`KLHxYMpD?IlF30k$Bk8*QVdlX;7aqp{j<|G*zfYX9z>8w2Ty zOOq?4@L-63s|{!;`M&n%m(HmL%u(&yMI9@EM$~i9w5ZSUgU@U8^5{&Vc@C%M59&0a zfeZH15-j%7@kDmi$VV^BuV3q$hRo%m4YRfDQ%CyJ5-O8lyw2tb(9ZJe2zoc6vcvz? zm3Qvq8pA69VKr$VBW1a_9kTV9JN%|&g9 zmIT$5WN|%xt1!f%RgY$NAP>6F1T!B#wDL&t&|r|5-y468>!f6farmVgMwa(7mLOj_ zNUMm(m5J&XvT#Fy!Z4Fi$NtOGF4`idguI13Wr#m^AAw;FHY! z2y?AVg}0Qk3{N|-G@#DfZXO<iiWxWhN$`8OYm#Nhra1EY2f9S=1 z?5$6MQ))+o?Hc5}AJd|W2Lp-V1;7U``A7XqzR-{n^b z4a{HO zsK%?Zz&T9A>i-f!w*yXal;+Y0cJPJUcp6X^-EbU_88;jl7U+89|+i*E|7Fe^KTvR{5%Mf%2{J4?FBk}?OQEte?I#2wiuhN6=w zuLh5k`j0L69W3i(=svA>D{FlKlO-SwW5RNi0F+*+%1{*v@hsbH!u@D)NRpYx&np%k zvEpmFY_zoQNjlZwtE6MQI=7hOp7#V;U$LZ9xG-RvJ}pR0Trb}1K-c61;EmL${rRqw_18T&w`UDl!p8wZ?u&7$R8AY792@ovmk7(g;1WQIzi{%hw*~>@;a?$LH zYkb_ZXN?~Od0_|qWHYv~uwD8qFINKcxLb5(L$o{|=FV*&%D8f^gm9bjS32#2UYV7f k<>Lb=E_wf-#~|#8RfTWKsGO2UIaW8{)<@hZ(X);GA9OkUg8%>k diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index cf7a077..2dc43d4 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -1,4 +1,3 @@ -from PyQt5 import QtCore import pyqtgraph as pg import pyqtgraph.opengl as gl from PyQt5 import QtCore @@ -12,7 +11,7 @@ icon_path = './view/icons/' -__version__ = "1.0.0" +__version__ = "2.0.0" # ----------------------------------------------------------------------------- @@ -91,6 +90,11 @@ def createActions(self): self.importRawAct.setIcon(QtGui.QIcon(icon_path + "Import.png")) self.importRawAct.triggered.connect(self.onImportRaw) + self.importRawActServer = QtWidgets.QAction(self.tr("&Raw Data From Server"), self) + self.importRawActServer.setStatusTip(self.tr("Import Raw Data From Server")) + self.importRawActServer.setIcon(QtGui.QIcon(icon_path + "Import.png")) + self.importRawActServer.triggered.connect(self.open_file_dialog_server) + self.importDetectedAct = QtWidgets.QAction(self.tr("&Detection Result"), self) self.importDetectedAct.setStatusTip(self.tr("Import Detection Result")) self.importDetectedAct.setIcon(QtGui.QIcon(icon_path + "Import.png")) @@ -342,7 +346,8 @@ def createMenubar(self): self.fileMenu.addAction(self.saveAsAct) self.fileMenu.addSeparator() self.importMenu = self.fileMenu.addMenu(self.tr("&Import")) - self.importMenu.addActions((self.importRawAct, self.importDetectedAct, self.importSortedAct)) + self.importMenu.addActions( + (self.importRawAct, self.importRawActServer, self.importDetectedAct, self.importSortedAct)) self.exportMenu = self.fileMenu.addMenu(self.tr("&Export")) self.exportMenu.addActions((self.exportRawAct, self.exportDetectedAct, self.exportSortedAct)) diff --git a/ross_ui/view/serverFileDialog.py b/ross_ui/view/serverFileDialog.py index e355951..54e2199 100644 --- a/ross_ui/view/serverFileDialog.py +++ b/ross_ui/view/serverFileDialog.py @@ -1,35 +1,33 @@ -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtWidgets -class Signin_Dialog(QtWidgets.QDialog): - def __init__(self, server): +class ServerFileDialog(QtWidgets.QDialog): + def __init__(self): super().__init__() - self.url = server - self.setFixedSize(400, 300) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 60, 121, 21)) - self.label_2 = QtWidgets.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(50, 130, 121, 20)) - - self.textEdit_username = QtWidgets.QLineEdit(self) - self.textEdit_username.setGeometry(QtCore.QRect(145, 55, 141, 31)) - - self.textEdit_password = QtWidgets.QLineEdit(self) - self.textEdit_password.setGeometry(QtCore.QRect(145, 125, 141, 31)) - self.textEdit_password.setEchoMode(QtWidgets.QLineEdit.Password) - - self.label_res = QtWidgets.QLabel(self) - self.label_res.setGeometry(QtCore.QRect(50, 180, 361, 16)) - - self.pushButton_in = QtWidgets.QPushButton(self) - self.pushButton_in.setGeometry(QtCore.QRect(130, 220, 121, 31)) - - self.pushButton_up = QtWidgets.QPushButton(self) - self.pushButton_up.setGeometry(QtCore.QRect(260, 220, 111, 31)) - - self.setWindowTitle("Sign In/Up") - self.label.setText("Username") - self.label_2.setText("Password") - self.label_res.setText("") - self.pushButton_in.setText("Sign In") - self.pushButton_up.setText("Sign Up") + self.setFixedSize(600, 300) + + self.layout_out = QtWidgets.QVBoxLayout() + self.layout_file_but = QtWidgets.QHBoxLayout() + + self.line_address = QtWidgets.QLineEdit() + self.line_address.setEnabled(False) + self.layout_out.addWidget(self.line_address) + + self.list_folder = QtWidgets.QListWidget() + self.layout_out.addWidget(self.list_folder) + + self.layout_out.addWidget(QtWidgets.QLabel('For mat files enter the variable name (if more than one variable ' + 'is stored).')) + + self.line_varname = QtWidgets.QLineEdit() + self.layout_out.addWidget(self.line_varname) + + self.push_open = QtWidgets.QPushButton('Open') + self.push_cancel = QtWidgets.QPushButton('Cancel') + self.layout_file_but.addWidget(self.push_open) + self.layout_file_but.addWidget(self.push_cancel) + self.layout_out.addLayout(self.layout_file_but) + + self.setLayout(self.layout_out) + + self.setWindowTitle("Server File Dialog") From 4aca7d8c0516fded19408dbcd39ba252b15ad2bd Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 25 Apr 2023 15:57:10 +0330 Subject: [PATCH 04/31] code refactor --- ross_backend/resources/data.py | 89 ---------------------------------- 1 file changed, 89 deletions(-) diff --git a/ross_backend/resources/data.py b/ross_backend/resources/data.py index e56002a..09d43f9 100644 --- a/ross_backend/resources/data.py +++ b/ross_backend/resources/data.py @@ -10,95 +10,6 @@ from models.data import RawModel from rutils.io import read_file_in_server -# class RawData(Resource): -# parser = reqparse.RequestParser() -# parser.add_argument('raw', type=str, required=True, help="This field cannot be left blank!") -# -# @jwt_required -# def get(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# raw = proj.raw -# -# # raw = RawModel.find_by_user_id(user_id) -# if raw: -# # b = io.BytesIO() -# # b.write(raw.raw) -# # b.seek(0) -# -# # d = np.load(b, allow_pickle=True) -# # print(d['raw'].shape) -# # b.close() -# # print(user_id, raw.project_id) -# return {'message': "Raw Data Exists."}, 201 -# -# return {'message': 'Raw Data does not exist'}, 404 -# -# @jwt_required -# def post(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# raw = proj.raw -# -# if raw: -# return {'message': "Raw Data already exists."}, 400 -# filestr = request.data -# # data = RawData.parser.parse_args() -# -# # print(eval(data['raw']).shape) -# raw = RawModel(user_id=user_id, project_id=proj.id, data=filestr) # data['raw']) -# -# try: -# raw.save_to_db() -# except: -# return {"message": "An error occurred inserting raw data."}, 500 -# -# return "Success", 201 -# -# @jwt_required -# def delete(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# raw = proj.raw -# if raw: -# raw.delete_from_db() -# return {'message': 'Raw Data deleted.'} -# return {'message': 'Raw Data does not exist.'}, 404 -# -# @jwt_required -# def put(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# raw = proj.raw -# filestr = request.data -# if raw: -# print('here') -# raw.data = filestr -# try: -# raw.save_to_db() -# except: -# return {"message": "An error occurred inserting raw data."}, 500 -# return "Success", 201 -# -# else: -# raw = RawModel(user_id, data=filestr, project_id=proj.id) -# try: -# print('now here') -# raw.save_to_db() -# except: -# return {"message": "An error occurred inserting raw data."}, 500 -# -# return "Success", 201 - - SESSION = dict() From 00af3efd94bc15ad6962b867df1c4849223ff7c5 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 26 Apr 2023 09:21:57 +0330 Subject: [PATCH 05/31] return exception in raw post --- ross_backend/resources/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ross_backend/resources/data.py b/ross_backend/resources/data.py index 09d43f9..968a1ee 100644 --- a/ross_backend/resources/data.py +++ b/ross_backend/resources/data.py @@ -77,8 +77,8 @@ def post(self): raw.mode = mode try: raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 + except Exception as e: + return {"message": str(e)}, 500 return "Success", 201 else: raw = RawModel(user_id, data=raw_data_path, project_id=project_id, mode=mode) From b42aff70b1e29e8a79df478c2cb7f39856f0153f Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 26 Apr 2023 11:09:31 +0330 Subject: [PATCH 06/31] change refresh token behaviour in ui - api --- ross_ui/model/api.py | 237 ++++++++++++++----------------------------- 1 file changed, 77 insertions(+), 160 deletions(-) diff --git a/ross_ui/model/api.py b/ross_ui/model/api.py index 7ad8d85..eb3cdbd 100644 --- a/ross_ui/model/api.py +++ b/ross_ui/model/api.py @@ -15,10 +15,6 @@ def refresh_jwt_token(self): response = requests.post(self.url + '/refresh', headers={'Authorization': 'Bearer ' + self.refresh_token}) - # response = requests.post(self.url + '/refresh', - # headers={'Authorization': 'Bearer ' + self.refresh_token}, - # data={'project_id': self.project_id}) - if response.ok: self.access_token = response.json()["access_token"] return True @@ -76,20 +72,15 @@ def post_raw_data(self, raw_data_path, mode=0, varname=''): if response.ok: return {'stat': True, 'message': 'success'} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.post(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, - json={"raw_data": raw_data_path, - "project_id": self.project_id, - "mode": mode, - "varname": varname}) - if response.ok: - return {'stat': True, 'message': 'success'} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.post_raw_data(raw_data_path, mode, varname) return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def get_raw_data(self, start=None, stop=None, limit=None): - if not (self.access_token is None): + if self.access_token is not None: response = requests.get(self.url + '/raw', headers={'Authorization': 'Bearer ' + self.access_token}, json={'project_id': self.project_id, @@ -122,7 +113,7 @@ def get_raw_data(self, start=None, stop=None, limit=None): return {'stat': False, 'message': 'Not Logged In!'} def get_detection_result(self): - if not (self.access_token is None): + if self.access_token is not None: response = requests.get(self.url + '/detection_result', headers={'Authorization': 'Bearer ' + self.access_token}, data={'project_id': self.project_id}) @@ -135,21 +126,15 @@ def get_detection_result(self): return {'stat': True, 'spike_mat': d['spike_mat'], 'spike_time': d['spike_time']} elif response.status_code == 401: - self.refresh_jwt_token() - response = requests.get(self.url + '/detection_result', - headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) - if response.ok: - b = io.BytesIO() - b.write(response.content) - b.seek(0) - d = np.load(b, allow_pickle=True) - return {'stat': True, 'spike_mat': d['spike_mat'], 'spike_time': d['spike_time']} + ret = self.refresh_jwt_token() + if ret: + self.get_detection_result() + return {'stat': False, 'message': response.json()['message']} return {'stat': False, 'message': 'Not Logged In!'} def get_sorting_result(self): - if not (self.access_token is None): + if self.access_token is not None: response = requests.get(self.url + '/sorting_result', headers={'Authorization': 'Bearer ' + self.access_token}, data={'project_id': self.project_id}) @@ -161,165 +146,101 @@ def get_sorting_result(self): d = np.load(b, allow_pickle=True) return {'stat': True, 'clusters': d['clusters']} elif response.status_code == 401: - self.refresh_jwt_token() - response = requests.get(self.url + '/sorting_result', - headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) - if response.ok: - b = io.BytesIO() - b.write(response.content) - b.seek(0) - d = np.load(b, allow_pickle=True) - print("d", d) - return {'stat': True, 'clusters': d['clusters']} + ret = self.refresh_jwt_token() + if ret: + self.get_sorting_result() + return {'stat': False, 'message': response.json()['message']} return {'stat': False, 'message': 'Not Logged In!'} def get_config_detect(self): - if not (self.access_token is None): + if self.access_token is not None: response = requests.get(self.url + '/detect', headers={'Authorization': 'Bearer ' + self.access_token}, data={'project_id': self.project_id}) if response.ok: return {'stat': True, 'config': response.json()} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.get(self.url + '/detect', - headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) - if response.ok: - return {'stat': True, 'config': response.json()} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.get_config_detect() return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def get_config_sort(self): - if not (self.access_token is None): + if self.access_token is not None: response = requests.get(self.url + '/sort', headers={'Authorization': 'Bearer ' + self.access_token}, data={'project_id': self.project_id}) if response.ok: return {'stat': True, 'config': response.json()} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.get(self.url + '/sort', - headers={'Authorization': 'Bearer ' + self.access_token}, - data={'project_id': self.project_id}) - if response.ok: - return {'stat': True, 'config': response.json()} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.get_config_sort() return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} - # def save_project_as(self, name): - # if not (self.access_token is None): - # response = requests.post(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}, - # data = {'project_id': self.project_id}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.post(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}, - # data = {'project_id': self.project_id}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - - # def save_project(self, name): - # if not (self.access_token is None): - # response = requests.put(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.post(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - # - # def delete_project(self, name): - # if not (self.access_token is None): - # response = requests.delete(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # - # elif response.json()["message"] == 'The token has expired.': - # self.refresh_jwt_token() - # response = requests.delete(self.url + '/project/' + name, - # headers={'Authorization': 'Bearer ' + self.access_token}) - # if response.ok: - # return {'stat': True, 'message': 'success'} - # return {'stat': False, 'message': response.json()["message"]} - # return {'stat': False, 'message': 'Not Logged In!'} - def start_detection(self, config): - data = config - data['run_detection'] = True - data['project_id'] = self.project_id - if not (self.access_token is None): + if self.access_token is not None: + + data = config + data['run_detection'] = True + data['project_id'] = self.project_id + response = requests.put(self.url + '/detect', headers={'Authorization': 'Bearer ' + self.access_token}, json=data) if response.ok: return {'stat': True, 'message': 'success'} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.put(self.url + '/detect', - headers={'Authorization': 'Bearer ' + self.access_token}, - json=data) - if response.ok: - return {'stat': True, 'message': 'success'} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.start_detection(config) return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def start_sorting(self, config): - data = config - data['run_sorting'] = True - data['project_id'] = self.project_id - if not (self.access_token is None): + if self.access_token is not None: + + data = config + data['run_sorting'] = True + data['project_id'] = self.project_id + response = requests.put(self.url + '/sort', headers={'Authorization': 'Bearer ' + self.access_token}, json=data) if response.ok: return {'stat': True, 'message': 'success'} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.put(self.url + '/sort', headers={'Authorization': 'Bearer ' + self.access_token}, - json=data) - if response.ok: - return {'stat': True, 'message': 'success'} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.start_sorting(config) return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def start_Resorting(self, config, clusters, selected_clusters): - data = config - data['clusters'] = [clusters.tolist()] - data['selected_clusters'] = [selected_clusters] - data['run_sorting'] = True - data['project_id'] = self.project_id - if not (self.access_token is None): + if self.access_token is not None: + + data = config + data['clusters'] = [clusters.tolist()] + data['selected_clusters'] = [selected_clusters] + data['run_sorting'] = True + data['project_id'] = self.project_id + response = requests.put(self.url + '/sort', headers={'Authorization': 'Bearer ' + self.access_token}, json=data) if response.ok: return {'stat': True, 'message': 'success', 'clusters': response.json()["clusters"]} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - response = requests.put(self.url + '/sort', headers={'Authorization': 'Bearer ' + self.access_token}, - json=data) - if response.ok: - return {'stat': True, 'message': 'success', 'clusters': response.json()["clusters"]} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.start_Resorting(config, clusters, selected_clusters) return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def save_sort_results(self, clusters): - if not (self.access_token is None): + if self.access_token is not None: buffer = io.BytesIO() np.savez_compressed(buffer, clusters=clusters, project_id=self.project_id) @@ -332,31 +253,27 @@ def save_sort_results(self, clusters): if response.ok: return {'stat': True, 'message': 'success'} - elif response.json()["message"] == 'The token has expired.': - self.refresh_jwt_token() - - response = requests.put(self.url + '/sorting_result', - headers={'Authorization': 'Bearer ' + self.access_token}, - data=clusters_bytes) - - if response.ok: - return {'stat': True, 'message': 'success'} + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.save_sort_results(clusters) return {'stat': False, 'message': response.json()["message"]} return {'stat': False, 'message': 'Not Logged In!'} def browse(self, root: str): - if root is None: - response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}) - else: - response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}, - json={'root': root}) - if response.ok: - return response.json() - else: - return None + if self.access_token is not None: + if root is None: + response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}) + else: + response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}, + json={'root': root}) + if response.ok: + return response.json() + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.browse(root) + else: + return None + - def browse_send_filename(self, filename, varname): - response = requests.get(self.url + '/browse', headers={'Authorization': 'Bearer ' + self.access_token}, - json={'filename': filename, 'varname': varname, 'project_id': self.project_id}) - if response.ok: - return response From eb74f7b7e3a60ee93d202b0f120e9ae34f4a7ea9 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 26 Apr 2023 11:24:59 +0330 Subject: [PATCH 07/31] a better signin view --- ross_ui/controller/signin.py | 4 +-- ross_ui/view/signin.py | 50 +++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/ross_ui/controller/signin.py b/ross_ui/controller/signin.py index 2be3035..4a662e5 100644 --- a/ross_ui/controller/signin.py +++ b/ross_ui/controller/signin.py @@ -7,6 +7,7 @@ class SigninApp(Signin_Dialog): def __init__(self, server): super(SigninApp, self).__init__(server) + self.user = None self.pushButton_in.pressed.connect(self.accept_in) self.pushButton_up.pressed.connect(self.accept_up) @@ -26,9 +27,6 @@ def accept_up(self): self.user = API(self.url) res = self.user.sign_up(username, password) if res['stat']: - self.label_res.setStyleSheet("color: green") QtWidgets.QMessageBox.information(self, "Account Created", res["message"]) else: - self.label_res.setStyleSheet("color: red") - # self.label_res.setText(res['message']) QtWidgets.QMessageBox.critical(self, "Error", res["message"]) diff --git a/ross_ui/view/signin.py b/ross_ui/view/signin.py index e355951..a86d450 100644 --- a/ross_ui/view/signin.py +++ b/ross_ui/view/signin.py @@ -5,31 +5,39 @@ class Signin_Dialog(QtWidgets.QDialog): def __init__(self, server): super().__init__() self.url = server - self.setFixedSize(400, 300) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 60, 121, 21)) - self.label_2 = QtWidgets.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(50, 130, 121, 20)) + self.setFixedSize(300, 150) - self.textEdit_username = QtWidgets.QLineEdit(self) - self.textEdit_username.setGeometry(QtCore.QRect(145, 55, 141, 31)) + l_label = QtWidgets.QVBoxLayout() + l_line = QtWidgets.QVBoxLayout() + l_push = QtWidgets.QHBoxLayout() + l1 = QtWidgets.QHBoxLayout() + l2 = QtWidgets.QVBoxLayout() - self.textEdit_password = QtWidgets.QLineEdit(self) - self.textEdit_password.setGeometry(QtCore.QRect(145, 125, 141, 31)) + self.label_u = QtWidgets.QLabel("Username") + self.label_p = QtWidgets.QLabel("Password") + + l_label.addWidget(self.label_u) + l_label.addWidget(self.label_p) + + self.textEdit_username = QtWidgets.QLineEdit() + self.textEdit_password = QtWidgets.QLineEdit() self.textEdit_password.setEchoMode(QtWidgets.QLineEdit.Password) - self.label_res = QtWidgets.QLabel(self) - self.label_res.setGeometry(QtCore.QRect(50, 180, 361, 16)) + l_line.addWidget(self.textEdit_username) + l_line.addWidget(self.textEdit_password) + + l1.addLayout(l_label) + l1.addLayout(l_line) + + self.pushButton_in = QtWidgets.QPushButton("Sign In") + self.pushButton_up = QtWidgets.QPushButton("Sign Up") + + l_push.addWidget(self.pushButton_in) + l_push.addWidget(self.pushButton_up) - self.pushButton_in = QtWidgets.QPushButton(self) - self.pushButton_in.setGeometry(QtCore.QRect(130, 220, 121, 31)) + l2.addLayout(l1) + l2.addLayout(l_push) - self.pushButton_up = QtWidgets.QPushButton(self) - self.pushButton_up.setGeometry(QtCore.QRect(260, 220, 111, 31)) + self.setLayout(l2) - self.setWindowTitle("Sign In/Up") - self.label.setText("Username") - self.label_2.setText("Password") - self.label_res.setText("") - self.pushButton_in.setText("Sign In") - self.pushButton_up.setText("Sign Up") + self.setWindowTitle('Authentication') From 095881c60d95d7de33b85312a25077a90bcda427 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 26 Apr 2023 12:02:11 +0330 Subject: [PATCH 08/31] enable disable behavior on signin/out --- ross_ui/controller/mainWindow.py | 16 ++++++++++++---- ross_ui/view/mainWindow.py | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 3777721..f41d54b 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -300,14 +300,18 @@ def open_signin_dialog(self): self.accountButton.setStatusTip("Signed In") self.logInAct.setEnabled(False) self.logOutAct.setEnabled(True) - self.saveAsAct.setEnabled(True) + # self.saveAct.setEnabled(True) + # self.saveAsAct.setEnabled(True) + self.importMenu.setEnabled(True) self.openAct.setEnabled(True) + self.exportMenu.setEnabled(True) + self.runMenu.setEnabled(True) + self.visMenu.setEnabled(True) self.statusBar().showMessage(self.tr("Loading Data...")) # self.wait() self.loadDefaultProject() self.statusBar().showMessage(self.tr("Loaded."), 2500) - self.saveAct.setEnabled(True) @QtCore.pyqtSlot() def open_file_dialog_server(self): @@ -375,8 +379,12 @@ def onSignOut(self): self.logInAct.setEnabled(True) self.logOutAct.setEnabled(False) self.user = None - self.saveAct.setEnabled(False) - self.saveAsAct.setEnabled(False) + # self.saveAct.setEnabled(False) + # self.saveAsAct.setEnabled(False) + self.importMenu.setEnabled(False) + self.exportMenu.setEnabled(False) + self.runMenu.setEnabled(False) + self.visMenu.setEnabled(False) self.widget_raw.clear() self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index 2dc43d4..49ed4f5 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -72,10 +72,10 @@ def createActions(self): self.closeAct.setEnabled(False) self.saveAct = QtWidgets.QAction(self.tr("&Save"), self) + self.saveAct.setEnabled(False) self.saveAct.setShortcut(QtGui.QKeySequence.Save) self.saveAct.setStatusTip(self.tr("Save")) self.saveAct.setIcon(QtGui.QIcon(icon_path + "Save.png")) - self.saveAct.setEnabled(False) self.saveAct.triggered.connect(self.onSave) self.saveAsAct = QtWidgets.QAction(self.tr("&Save As..."), self) @@ -346,9 +346,11 @@ def createMenubar(self): self.fileMenu.addAction(self.saveAsAct) self.fileMenu.addSeparator() self.importMenu = self.fileMenu.addMenu(self.tr("&Import")) + self.importMenu.setEnabled(False) self.importMenu.addActions( (self.importRawAct, self.importRawActServer, self.importDetectedAct, self.importSortedAct)) self.exportMenu = self.fileMenu.addMenu(self.tr("&Export")) + self.exportMenu.setEnabled(False) self.exportMenu.addActions((self.exportRawAct, self.exportDetectedAct, self.exportSortedAct)) # ------------------- tool from menu ------------------ @@ -359,6 +361,7 @@ def createMenubar(self): # run menu self.runMenu = self.menuBar().addMenu(self.tr("&Run")) + self.runMenu.setEnabled(False) self.runMenu.addAction(self.detectAct) self.runMenu.addAction(self.sortAct) # self.runMenu.addAction(self.batchAct) @@ -378,6 +381,7 @@ def createMenubar(self): # Menu entry for visualization self.visMenu = self.menuBar().addMenu(self.tr("&Visualization")) + self.visMenu.setEnabled(False) self.visMenu.addAction(self.detect3dAct) self.visMenu.addAction(self.clusterwaveAct) self.visMenu.addAction(self.livetimeAct) From 307a0ccb266d4bbf9f207175d1653e4587c4949e Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 26 Apr 2023 18:11:17 +0330 Subject: [PATCH 09/31] delete project materials after new raw. bug fixed --- ross_backend/models/data.py | 4 ++-- ross_backend/resources/data.py | 15 +++++++++++---- ross_backend/resources/funcs/t_sorting.py | 3 --- ross_backend/resources/user.py | 3 +-- ross_ui/controller/mainWindow.py | 22 ++++++++++++++++++++-- ross_ui/controller/serverFileDialog.py | 1 + 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/ross_backend/models/data.py b/ross_backend/models/data.py index 10e137a..ab84d1d 100644 --- a/ross_backend/models/data.py +++ b/ross_backend/models/data.py @@ -79,7 +79,7 @@ def get(cls): # return cls.query.filter_by(user_id=_id, project_id=0).first() @classmethod - def find_by_project_id(cls, project_id): + def find_by_project_id(cls, project_id) -> DetectResultModel: return cls.query.filter_by(project_id=project_id).first() def delete_from_db(self): @@ -119,7 +119,7 @@ def get(cls): # return cls.query.filter_by(user_id=_id, project_id=0).first() @classmethod - def find_by_project_id(cls, project_id): + def find_by_project_id(cls, project_id) -> SortResultModel: return cls.query.filter_by(project_id=project_id).first() def delete_from_db(self): diff --git a/ross_backend/resources/data.py b/ross_backend/resources/data.py index 968a1ee..95ac882 100644 --- a/ross_backend/resources/data.py +++ b/ross_backend/resources/data.py @@ -7,7 +7,7 @@ from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource -from models.data import RawModel +from models.data import RawModel, DetectResultModel, SortResultModel from rutils.io import read_file_in_server SESSION = dict() @@ -24,7 +24,8 @@ def get(self): if raw.mode == 0: response = flask.make_response(raw.data) response.headers.set('Content-Type', 'application/octet-stream') - return response, 210 + response.status_code = 210 + return response else: if request.json['start'] is None: return {'message': 'SERVER MODE'}, 212 @@ -85,7 +86,13 @@ def post(self): try: raw.save_to_db() - except: - return {"message": "An error occurred inserting raw data."}, 500 + detect_result = DetectResultModel.find_by_project_id(project_id) + if detect_result: + detect_result.delete_from_db() + sort_result = SortResultModel.find_by_project_id(project_id) + if sort_result: + sort_result.delete_from_db() + except Exception as e: + return {"message": str(e)}, 500 return "Success", 201 diff --git a/ross_backend/resources/funcs/t_sorting.py b/ross_backend/resources/funcs/t_sorting.py index f70cefc..669b606 100644 --- a/ross_backend/resources/funcs/t_sorting.py +++ b/ross_backend/resources/funcs/t_sorting.py @@ -77,10 +77,7 @@ def t_dist_sorter(alignedSpikeMat, sdd): while g >= g_min: itr = 0 # EM - print('#' * 10) - print('EM Alg. Started...') while ((delta_L > delta_L_limit) or (delta_v > delta_v_limit)) and itr < max_iter: - print('iteration number = ', itr) # print('g = ', g) # print('Pi = ', Pi) # print('#' * 5) diff --git a/ross_backend/resources/user.py b/ross_backend/resources/user.py index 3c88cc7..deff117 100644 --- a/ross_backend/resources/user.py +++ b/ross_backend/resources/user.py @@ -41,8 +41,7 @@ def post(self): proj.save_to_db() user.project_default = proj.id user.save_to_db() - config_detect = ConfigDetectionModel(user_id, - project_id=proj.id) # create a default detection config for the default project + config_detect = ConfigDetectionModel(user_id, project_id=proj.id) config_detect.save_to_db() config_sort = ConfigSortModel(user_id, project_id=proj.id) config_sort.save_to_db() diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index f41d54b..586a80f 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -115,7 +115,7 @@ def onImportRaw(self): ) if not filename: - return FileNotFoundError('you should select a file') + raise FileNotFoundError('you should select a file') if not os.path.isfile(filename): raise FileNotFoundError(filename) @@ -147,6 +147,7 @@ def onImportRaw(self): with open(address, 'wb') as f: pickle.dump(temp, f) + # ----------------------------------------------------------------------------------------------------- elif file_extension == '.csv': @@ -193,6 +194,9 @@ def onImportRaw(self): self.wait() self.plotRaw() + self.spike_mat = None + self.spike_time = None + self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() self.widget_waveform.clear() @@ -317,7 +321,21 @@ def open_signin_dialog(self): def open_file_dialog_server(self): dialog = sever_dialog(self.user) if dialog.exec_() == QtWidgets.QDialog.Accepted: - pass + self.refreshAct.setEnabled(True) + self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500) + self.wait() + + self.statusBar().showMessage(self.tr("Plotting..."), 2500) + self.wait() + self.plotRaw() + + self.spike_mat = None + self.spike_time = None + self.raw = None + + self.plot_histogram_pca.clear() + self.plot_clusters_pca.clear() + self.widget_waveform.clear() def open_server_dialog(self): dialog = server_form(server_text=self.url) diff --git a/ross_ui/controller/serverFileDialog.py b/ross_ui/controller/serverFileDialog.py index 93ef9a6..747590f 100644 --- a/ross_ui/controller/serverFileDialog.py +++ b/ross_ui/controller/serverFileDialog.py @@ -49,5 +49,6 @@ def itemDoubleClicked(self, item: QtWidgets.QListWidgetItem): varname=self.line_varname.text()) if ret['stat']: QtWidgets.QMessageBox.information(self, 'Info', 'File successfully added.') + super().accept() else: QtWidgets.QMessageBox.critical(self, 'Error', ret['message']) From a0a9f978a7565bdc87db63a8b1e9958187e45aca Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 2 May 2023 15:46:21 +0330 Subject: [PATCH 10/31] server mode works wit clusters --- ross_backend/resources/detect.py | 177 +++----------------- ross_backend/resources/detection_result.py | 75 ++------- ross_backend/resources/funcs/detection.py | 2 +- ross_backend/resources/funcs/sort_utils.py | 2 +- ross_backend/resources/funcs/sorting.py | 23 ++- ross_backend/resources/sort.py | 135 +-------------- ross_backend/resources/sorting_result.py | 79 +++++---- ross_ui/controller/hdf5.py | 38 ++++- ross_ui/controller/mainWindow.py | 186 ++++++++++++--------- ross_ui/controller/plot_time.py | 6 - ross_ui/controller/segmented_time.py | 9 +- ross_ui/model/api.py | 12 +- ross_ui/view/mainWindow.py | 2 +- 13 files changed, 266 insertions(+), 480 deletions(-) delete mode 100644 ross_ui/controller/plot_time.py diff --git a/ross_backend/resources/detect.py b/ross_backend/resources/detect.py index cfaf75e..b49a1a3 100644 --- a/ross_backend/resources/detect.py +++ b/ross_backend/resources/detect.py @@ -10,100 +10,7 @@ from models.data import DetectResultModel from models.data import RawModel from resources.funcs.detection import startDetection - - -# class Detect(Resource): -# parser = reqparse.RequestParser(bundle_errors=True) -# parser.add_argument('filter_type', type=str, required=True, choices=('butter')) -# parser.add_argument('filter_order', type=int, required=True) -# parser.add_argument('pass_freq', type=int, required=True) -# parser.add_argument('stop_freq', type=int, required=True) -# parser.add_argument('sampling_rate', type=int, required=True) -# parser.add_argument('thr_method', type=str, required=True, choices=('median', 'wavelet', 'plexon')) -# parser.add_argument('side_thr', type=str, required=True, choices=('positive', 'negative', 'two')) -# parser.add_argument('pre_thr', type=int, required=True) -# parser.add_argument('post_thr', type=int, required=True) -# parser.add_argument('dead_time', type=int, required=True) -# parser.add_argument('run_detection', type=bool, default=False) -# -# @jwt_required -# def get(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# config = proj.config -# if config: -# return config.json() -# return {'message': 'Detection config does not exist'}, 404 -# -# @jwt_required -# def post(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# config = proj.config -# if config: -# return {'message': "Config Detection already exists."}, 400 -# -# data = Detect.parser.parse_args() -# -# config = ConfigDetectionModel(user_id, **data, project_id=proj.id) -# -# try: -# config.save_to_db() -# except: -# return {"message": "An error occurred inserting detection config."}, 500 -# -# # if data['run_detection']: -# # try: -# # print('starting Detection ...') -# # startDetection() -# # except: -# # return {"message": "An error occurred in detection."}, 500 -# -# return config.json(), 201 -# -# @jwt_required -# def delete(self, name): -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# config = proj.config -# if config: -# config.delete_from_db() -# return {'message': 'Detection config deleted.'} -# return {'message': 'Detection config does not exist.'}, 404 -# -# @jwt_required -# def put(self, name): -# data = Detect.parser.parse_args() -# user_id = get_jwt_identity() -# proj = ProjectModel.find_by_project_name(user_id, name) -# if not proj: -# return {'message': 'Project does not exist'}, 404 -# config = proj.config -# if config: -# for key in data: -# config.key = data[key] -# try: -# config.save_to_db() -# except: -# return {"message": "An error occurred inserting detection config."}, 500 -# -# return config.json(), 201 -# -# else: -# config = ConfigDetectionModel(user_id, **data, project_id=proj.id) -# -# try: -# config.save_to_db() -# except: -# return {"message": "An error occurred inserting detection config."}, 500 -# -# return config.json(), 201 +from resources.detection_result import SESSION class DetectDefault(Resource): @@ -119,13 +26,10 @@ class DetectDefault(Resource): parser.add_argument('post_thr', type=int, required=True) parser.add_argument('dead_time', type=int, required=True) parser.add_argument('run_detection', type=bool, default=False) - # -------------------------------------------- parser.add_argument('project_id', type=int, default=False) @jwt_required def get(self): - # user_id = get_jwt_identity() - # user = UserModel.find_by_id(user_id) project_id = request.form['project_id'] config = ConfigDetectionModel.find_by_project_id(project_id) if config: @@ -134,38 +38,6 @@ def get(self): @jwt_required def post(self): - user_id = get_jwt_identity() - if ConfigDetectionModel.find_by_user_id(user_id): - return {'message': "Detection config already exists."}, 400 - - data = DetectDefault.parser.parse_args() - config = ConfigDetectionModel(user_id, **data) - - try: - config.save_to_db() - except: - return {"message": "An error occurred inserting detection config."}, 500 - - if data['run_detection']: - try: - print('starting Detection ...') - startDetection(user_id) - except: - return {"message": "An error occurred in detection."}, 500 - - return config.json(), 201 - - @jwt_required - def delete(self): - user_id = get_jwt_identity() - config = ConfigDetectionModel.find_by_user_id(user_id) - if config: - config.delete_from_db() - return {'message': 'Detection config deleted.'} - return {'message': 'Detection config does not exist.'}, 404 - - @jwt_required - def put(self): data = DetectDefault.parser.parse_args() project_id = data['project_id'] user_id = get_jwt_identity() @@ -195,34 +67,35 @@ def put(self): except: print(traceback.format_exc()) return {"message": "An error occurred inserting detection config."}, 500 - if data['run_detection']: - try: - spikeMat, spikeTime = startDetection(project_id) - data_file = {'spikeMat': spikeMat, 'spikeTime': spikeTime} - # ------------------------------------------------------- - print("inserting detection result to database") - - detection_result_path = str(Path(RawModel.find_by_project_id(project_id).data).parent / \ - (str(uuid4()) + '.pkl')) + # if data['run_detection']: + try: + spikeMat, spikeTime = startDetection(project_id) + data_file = {'spikeMat': spikeMat, 'spikeTime': spikeTime, 'config': config.json()} + # ------------------------------------------------------- + print("inserting detection result to database") - with open(detection_result_path, 'wb') as f: - pickle.dump(data_file, f) + detection_result_path = str(Path(RawModel.find_by_project_id(project_id).data).parent / + (str(uuid4()) + '.pkl')) - detectResult = DetectResultModel.find_by_project_id(project_id) + with open(detection_result_path, 'wb') as f: + pickle.dump(data_file, f) - if detectResult: - detectResult.data = detection_result_path - else: - detectResult = DetectResultModel(user_id, detection_result_path, project_id) + detectResult = DetectResultModel.find_by_project_id(project_id) - try: - detectResult.save_to_db() - except: - print(traceback.format_exc()) - return {"message": "An error occurred inserting detection result."}, 500 + if detectResult: + detectResult.data = detection_result_path + else: + detectResult = DetectResultModel(user_id, detection_result_path, project_id) + try: + detectResult.save_to_db() + SESSION[project_id] = data_file except: print(traceback.format_exc()) - return {"message": "An error occurred in detection."}, 500 + return {"message": "An error occurred inserting detection result."}, 500 + + except: + print(traceback.format_exc()) + return {"message": "An error occurred in detection."}, 500 - return config.json(), 201 + return "Success", 201 diff --git a/ross_backend/resources/detection_result.py b/ross_backend/resources/detection_result.py index 0010163..f9f422f 100644 --- a/ross_backend/resources/detection_result.py +++ b/ross_backend/resources/detection_result.py @@ -4,11 +4,13 @@ import flask import numpy as np from flask import request -from flask_jwt_extended import jwt_required, get_jwt_identity +from flask_jwt_extended import jwt_required from flask_restful import Resource, reqparse from models.data import DetectResultModel +SESSION = dict() + class DetectionResultDefault(Resource): parser = reqparse.RequestParser() @@ -16,15 +18,24 @@ class DetectionResultDefault(Resource): @jwt_required def get(self): - project_id = request.form["project_id"] - detect_result_model = DetectResultModel.find_by_project_id(project_id) + project_id = int(request.form["project_id"]) + detect_result = None + if project_id in SESSION: + detect_result = SESSION[project_id] + else: + detect_result_model = DetectResultModel.find_by_project_id(project_id) + if detect_result_model: + with open(detect_result_model.data, 'rb') as f: + detect_result = pickle.load(f) + SESSION[project_id] = detect_result - if detect_result_model: - with open(detect_result_model.data, 'rb') as f: - detect_result = pickle.load(f) + if detect_result is not None: buffer = io.BytesIO() - np.savez_compressed(buffer, spike_mat=detect_result['spikeMat'], spike_time=detect_result['spikeTime']) + np.savez_compressed(buffer, + spike_mat=detect_result['spikeMat'], + spike_time=detect_result['spikeTime'], + config=detect_result['config']) buffer.seek(0) raw_bytes = buffer.read() buffer.close() @@ -34,53 +45,3 @@ def get(self): return response return {'message': 'Detection Result Data does not exist'}, 404 - - @jwt_required - def post(self): - filestr = request.data - user_id = get_jwt_identity() - if DetectResultModel.find_by_user_id(user_id): - return {'message': "Detection Result already exists."}, 400 - # data = RawData.parser.parse_args() - # print(eval(data['raw']).shape) - data = DetectResultModel(user_id=user_id, data=filestr) # data['raw']) - - try: - data.save_to_db() - except: - return {"message": "An error occurred inserting sort result data."}, 500 - - return "Success", 201 - - @jwt_required - def delete(self): - user_id = get_jwt_identity() - data = DetectResultModel.find_by_user_id(user_id) - if data: - data.delete_from_db() - return {'message': 'Detection Result Data deleted.'} - return {'message': 'Detection Result Data does not exist.'}, 404 - - @jwt_required - def put(self): - filestr = request.data - user_id = get_jwt_identity() - data = DetectResultModel.find_by_user_id(user_id) - if data: - data.data = filestr - try: - data.save_to_db() - - except: - return {"message": "An error occurred inserting sort result data."}, 500 - return "Success", 201 - - else: - data = DetectResultModel(user_id, data=filestr) - - try: - data.save_to_db() - except: - return {"message": "An error occurred inserting sort result data."}, 500 - - return "Success", 201 diff --git a/ross_backend/resources/funcs/detection.py b/ross_backend/resources/funcs/detection.py index ec6c355..f82d730 100644 --- a/ross_backend/resources/funcs/detection.py +++ b/ross_backend/resources/funcs/detection.py @@ -122,7 +122,7 @@ def spike_detector(data, thr, pre_thresh, post_thresh, dead_time, side): indx_spikes = np.nonzero(spike_detected)[0] SpikeMat = np.zeros((len(indx_spikes), n_points_per_spike)) - SpikeTime = np.zeros((len(indx_spikes), 1)) + SpikeTime = np.zeros((len(indx_spikes), ), dtype=np.uint32) # assigning SpikeMat and SpikeTime matrices for i, curr_indx in enumerate(indx_spikes): diff --git a/ross_backend/resources/funcs/sort_utils.py b/ross_backend/resources/funcs/sort_utils.py index 6bb23bf..613a225 100644 --- a/ross_backend/resources/funcs/sort_utils.py +++ b/ross_backend/resources/funcs/sort_utils.py @@ -141,7 +141,7 @@ def spike_alignment(spike_mat, spike_time, ss): newSpikeMat = spike_mat[:, max_shift:-max_shift] # shifting time with the amount of "max_shift" because first "max_shift" samples are ignored. - time_shift = np.zeros((n_spike, 1)) + max_shift + time_shift = np.zeros((n_spike, )) + max_shift # indices of neg group ind_neg_find = np.nonzero(ind_neg)[0] diff --git a/ross_backend/resources/funcs/sorting.py b/ross_backend/resources/funcs/sorting.py index 172ada3..ec44f16 100644 --- a/ross_backend/resources/funcs/sorting.py +++ b/ross_backend/resources/funcs/sorting.py @@ -1,5 +1,7 @@ import pickle +import numpy as np + from models.config import ConfigSortModel from models.data import DetectResultModel from resources.funcs.gmm import * @@ -9,6 +11,15 @@ from resources.funcs.t_sorting import * +def create_cluster_time_vec(spike_time: np.ndarray, clusters: list, config: dict): + cluster_time_vec = np.zeros(spike_time[-1] + config['post_thr'], dtype=np.int8) + for i, t in enumerate(spike_time): + cluster_time_vec[t - config['pre_thr']: t + config['post_thr']] = clusters[i]+1 + return cluster_time_vec + + +# TODO: combine two sorting functions into one + def startSorting(project_id): detect_result = DetectResultModel.find_by_project_id(project_id) if not detect_result: @@ -26,6 +37,7 @@ def startSorting(project_id): spike_mat = d['spikeMat'] spike_time = d['spikeTime'] + config_det = d['config'] if config.alignment: spike_mat, spike_time = spike_alignment(spike_mat, spike_time, config) @@ -45,8 +57,10 @@ def startSorting(project_id): optimal_set = kmeans(spike_mat, config) elif config.sorting_type == 'GMM': optimal_set = gmm_sorter(spike_mat, config) + else: + raise NotImplementedError(f'{config.sorting_type} not implemented') - return optimal_set + return optimal_set, create_cluster_time_vec(spike_time=d['spikeTime'], clusters=optimal_set, config=config_det) def startReSorting(project_id, clusters, selected_clusters): @@ -66,11 +80,12 @@ def startReSorting(project_id, clusters, selected_clusters): spike_mat = d['spikeMat'] spike_time = d['spikeTime'] + config_det = d['config'] + # TODO : CHECK AND CORRECT # if config.alignment: # spike_mat, spike_time = spike_alignment(spike_mat, spike_time, config) - # TODO : CHECK AND CORRECT if config.filtering: pass # REM = spikeFiltering(spike_mat, config) @@ -87,7 +102,9 @@ def startReSorting(project_id, clusters, selected_clusters): optimal_set = kmeans(spike_mat, config) elif config.sorting_type == 'GMM': optimal_set = gmm_sorter(spike_mat, config) + else: + raise NotImplementedError(f'{config.sorting_type} not implemented') clusters = np.array(clusters) clusters[np.isin(clusters, selected_clusters)] = optimal_set + np.max(clusters) + 1 - return clusters.tolist() + return clusters.tolist(), create_cluster_time_vec(spike_time=d['spikeTime'], clusters=optimal_set, config=config_det) diff --git a/ross_backend/resources/sort.py b/ross_backend/resources/sort.py index 56a958e..2a15c25 100644 --- a/ross_backend/resources/sort.py +++ b/ross_backend/resources/sort.py @@ -10,123 +10,7 @@ from models.data import SortResultModel, RawModel from models.project import ProjectModel from resources.funcs.sorting import startSorting, startReSorting - - -class Sort(Resource): - parser = reqparse.RequestParser(bundle_errors=True) - - # alignment settings - parser.add_argument('max_shift', type=int, required=True) - parser.add_argument('histogram_bins', type=int, required=True) - parser.add_argument('num_peaks', type=int, required=True) - parser.add_argument('compare_mode', type=str, required=True, choices=('magnitude', 'index')) - - # filtering settings - parser.add_argument('max_std', type=float, required=True) - parser.add_argument('max_mean', type=float, required=True) - parser.add_argument('max_outliers', type=float, required=True) - - # sort settings - parser.add_argument('nu', type=float, required=True) - parser.add_argument('max_iter', type=int, required=True) - parser.add_argument('PCA_num', type=int, required=True) - parser.add_argument('g_max', type=int, required=True) - parser.add_argument('g_min', type=int, required=True) - parser.add_argument('u_lim', type=float, required=True) - parser.add_argument('error', type=float, required=True) - parser.add_argument('tol', type=float, required=True) - parser.add_argument('N', type=int, required=True) - parser.add_argument('matching_mode', type=str, required=True, choices=('Euclidean', 'Chi_squared', 'Correlation')) - parser.add_argument('alpha', type=float, required=True) - parser.add_argument('combination', type=bool, required=True) - parser.add_argument('custom_template', type=bool, required=True) - parser.add_argument('sorting_type', type=str, - choices=('t dist', 'skew-t dist', 'GMM', 'K-means', 'template matching'), required=True) - parser.add_argument('max_iter', type=int, required=True) - parser.add_argument('alignment', type=bool, required=True) - parser.add_argument('filtering', type=bool, required=True) - parser.add_argument('run_sorting', type=bool, default=False) - - @jwt_required - def get(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - return config.json() - return {'message': 'Detection config does not exist'}, 404 - - @jwt_required - def post(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - return {'message': "Config Detection already exists."}, 400 - - data = Sort.parser.parse_args() - - config = ConfigSortModel(user_id, **data, project_id=proj.id) - - try: - config.save_to_db() - except: - return {"message": "An error occurred inserting detection config."}, 500 - - # if data['run_detection']: - # try: - # print('starting Detection ...') - # startDetection() - # except: - # return {"message": "An error occurred in detection."}, 500 - - return config.json(), 201 - - @jwt_required - def delete(self, name): - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - config.delete_from_db() - return {'message': 'Detection config deleted.'} - return {'message': 'Detection config does not exist.'}, 404 - - @jwt_required - def put(self, name): - data = Sort.parser.parse_args() - user_id = get_jwt_identity() - proj = ProjectModel.find_by_project_name(user_id, name) - if not proj: - return {'message': 'Project does not exist'}, 404 - config = proj.config - if config: - for key in data: - config.key = data[key] - try: - config.save_to_db() - except: - print(traceback.format_exc()) - return {"message": "An error occurred inserting detection config."}, 500 - - return config.json(), 201 - - else: - config = ConfigSortModel(user_id, **data, project_id=proj.id) - - try: - config.save_to_db() - except: - print(traceback.format_exc()) - return {"message": "An error occurred inserting detection config."}, 500 - - return config.json(), 201 +from resources.sorting_result import SESSION class SortDefault(Resource): @@ -172,22 +56,12 @@ class SortDefault(Resource): @jwt_required def get(self): - user_id = get_jwt_identity() project_id = request.form['project_id'] config = ConfigSortModel.find_by_project_id(project_id) if config: return config.json() return {'message': 'Sort config does not exist'}, 404 - @jwt_required - def delete(self): - user_id = get_jwt_identity() - config = ConfigSortModel.find_by_user_id(user_id) - if config: - config.delete_from_db() - return {'message': 'Sorting config deleted.'} - return {'message': 'Sorting config does not exist.'}, 404 - @jwt_required def put(self): data = SortDefault.parser.parse_args() @@ -247,12 +121,12 @@ def put(self): print('Starting Sorting ...') if clusters is not None: - clusters_index = startReSorting(project_id, clusters, selected_clusters) + clusters_index, cluster_time_vec = startReSorting(project_id, clusters, selected_clusters) return {'clusters': clusters_index}, 201 else: - clusters_index = startSorting(project_id) + clusters_index, cluster_time_vec = startSorting(project_id) - data = {"clusters": clusters_index} + data = {"clusters": clusters_index, "cluster_time_vec": cluster_time_vec} sort_result_path = str(Path(RawModel.find_by_project_id(project_id).data).parent / (str(uuid4()) + '.pkl')) @@ -269,6 +143,7 @@ def put(self): try: sortResult.save_to_db() + SESSION[project_id] = data except: print(traceback.format_exc()) return {"message": "An error occurred inserting sorting result."}, 500 diff --git a/ross_backend/resources/sorting_result.py b/ross_backend/resources/sorting_result.py index bfae282..4ff00bc 100644 --- a/ross_backend/resources/sorting_result.py +++ b/ross_backend/resources/sorting_result.py @@ -11,6 +11,8 @@ from models.data import SortResultModel +SESSION = dict() + class SortingResultDefault(Resource): parser = reqparse.RequestParser() @@ -18,16 +20,20 @@ class SortingResultDefault(Resource): @jwt_required def get(self): - user_id = get_jwt_identity() project_id = request.form["project_id"] - sort_result = SortResultModel.find_by_project_id(project_id) + sort_dict = None + if project_id in SESSION: + sort_dict = SESSION[project_id] + else: - if sort_result: - with open(sort_result.data, 'rb') as f: - sort_dict = pickle.load(f) + sort_result = SortResultModel.find_by_project_id(project_id) + + if sort_result: + with open(sort_result.data, 'rb') as f: + sort_dict = pickle.load(f) + if sort_dict is not None: buffer = io.BytesIO() - print("sort_dict['clusters']", sort_dict['clusters']) - np.savez_compressed(buffer, clusters=sort_dict['clusters']) + np.savez_compressed(buffer, clusters=sort_dict['clusters'], cluster_time_vec=sort_dict["cluster_time_vec"]) buffer.seek(0) raw_bytes = buffer.read() buffer.close() @@ -38,38 +44,39 @@ def get(self): return {'message': 'Sort Result Data does not exist'}, 404 - @jwt_required - def post(self): - filestr = request.data - user_id = get_jwt_identity() - project_id = request.form["project_id"] - if SortResultModel.find_by_project_id(project_id): - return {'message': "Detection Result already exists."}, 400 - - # data = RawData.parser.parse_args() - - # print(eval(data['raw']).shape) - data = SortResultModel(user_id=user_id, data=filestr, project_id=project_id) # data['raw']) - - try: - data.save_to_db() - except: - return {"message": "An error occurred inserting sort result data."}, 500 - - return "Success", 201 - - @jwt_required - def delete(self): - user_id = get_jwt_identity() - project_id = request.form["project_id"] - data = SortResultModel.find_by_project_id(project_id) - if data: - data.delete_from_db() - return {'message': 'Sort Result Data deleted.'} - return {'message': 'Sort Result Data does not exist.'}, 404 + # @jwt_required + # def post(self): + # filestr = request.data + # user_id = get_jwt_identity() + # project_id = request.form["project_id"] + # if SortResultModel.find_by_project_id(project_id): + # return {'message': "Detection Result already exists."}, 400 + # + # # data = RawData.parser.parse_args() + # + # # print(eval(data['raw']).shape) + # data = SortResultModel(user_id=user_id, data=filestr, project_id=project_id) # data['raw']) + # + # try: + # data.save_to_db() + # except: + # return {"message": "An error occurred inserting sort result data."}, 500 + # + # return "Success", 201 + + # @jwt_required + # def delete(self): + # user_id = get_jwt_identity() + # project_id = request.form["project_id"] + # data = SortResultModel.find_by_project_id(project_id) + # if data: + # data.delete_from_db() + # return {'message': 'Sort Result Data deleted.'} + # return {'message': 'Sort Result Data does not exist.'}, 404 @jwt_required def put(self): + # TODO : changes must be applied to cluster_time_vec # filestr = request.data user_id = get_jwt_identity() diff --git a/ross_ui/controller/hdf5.py b/ross_ui/controller/hdf5.py index 48dbfa9..7ae3277 100644 --- a/ross_ui/controller/hdf5.py +++ b/ross_ui/controller/hdf5.py @@ -5,8 +5,12 @@ class HDF5Plot(pg.PlotCurveItem): + res = None + SS = None + def __init__(self, *args, **kwds): - self.pen = None + self.cluster = None + # self.pen = None self.api = None self.hdf5 = None self.limit = 10000 @@ -15,7 +19,9 @@ def __init__(self, *args, **kwds): def setHDF5(self, data, pen=None): self.hdf5 = data self.pen = pen - self.updateHDF5Plot() + if self.pen is not None: + self.setPen(self.pen) + # self.updateHDF5Plot() def setAPI(self, api: API): self.api = api @@ -23,6 +29,9 @@ def setAPI(self, api: API): def viewRangeChanged(self): self.updateHDF5Plot() + def setCluster(self, cluster): + self.cluster = cluster + def updateHDF5Plot(self): vb = self.getViewBox() @@ -30,25 +39,38 @@ def updateHDF5Plot(self): return xrange = vb.viewRange()[0] + if xrange[1] - xrange[0] < 10: + return + start = max(0, int(xrange[0]) - 1) if self.hdf5 is None: stop = int(xrange[1] + 2) - res = self.api.get_raw_data(start, stop, self.limit) + if (HDF5Plot.SS is None) or ([start, stop] != HDF5Plot.SS): + res = self.api.get_raw_data(start, stop, self.limit) + HDF5Plot.SS = [start, stop] + HDF5Plot.res = res + else: + res = HDF5Plot.res if not res['stat']: self.setData([]) return stop = res['stop'] - visible = res['visible'] + visible = res['visible'].copy() ds = res['ds'] else: stop = min(len(self.hdf5), int(xrange[1] + 2)) ds = int((stop - start) / self.limit) + 1 - visible = self.hdf5[start:stop:ds] + visible = self.hdf5[start:stop:ds].copy() x = np.arange(start, stop, ds) - self.setData(x, visible) - if self.pen is not None: - self.setPen(self.pen) + if self.cluster is not None: + # visible = visible[self.cluster[x]] + # x = x[self.cluster[x]] + visible[np.logical_not(self.cluster[x])] = np.nan + # x[not self.cluster[x]] = np.nan + self.setData(x, visible, connect='finite') + # if self.pen is not None: + # self.setPen(self.pen) self.resetTransform() diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 586a80f..0aca852 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -3,7 +3,6 @@ import pathlib import pickle import random -import time import traceback from uuid import uuid4 @@ -61,13 +60,14 @@ def __init__(self): self.clusters_tmp = None self.clusters_init = None self.clusters = None - self.colors = None + self.colors = self.distin_color(127) - self.url = 'http://127.0.0.1:5000' + self.url = 'http://localhost:5000' self.raw = None self.spike_mat = None self.spike_time = None + self.cluster_time_vec = None self.Raw_data_path = os.path.join(pathlib.Path(__file__).parent, '../ross_data/Raw_Data') self.pca_path = os.path.join(pathlib.Path(__file__).parent, '../ross_data/pca_images') @@ -196,6 +196,8 @@ def onImportRaw(self): self.spike_mat = None self.spike_time = None + self.clusters_tmp = None + self.clusters_init = None self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() @@ -213,74 +215,75 @@ def onImportRaw(self): self.wait() def onImportDetected(self): - filename, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Open file"), os.getcwd(), - self.tr("Detected Spikes Files(*.mat *.csv *.tdms)")) - - if not filename: - return FileNotFoundError('you should select a file') - - if not os.path.isfile(filename): - raise FileNotFoundError(filename) - - self.statusBar().showMessage(self.tr("Loading...")) - self.wait() - file_extension = os.path.splitext(filename)[-1] - if file_extension == '.mat': - file_raw = sio.loadmat(filename) - variables = list(file_raw.keys()) - if '__version__' in variables: - variables.remove('__version__') - if '__header__' in variables: - variables.remove('__header__') - if '__globals__' in variables: - variables.remove('__globals__') - - if len(variables) > 1: - variable1 = self.open_detected_mat_dialog(variables) - # self.wait() - if not variable1: - self.statusBar().showMessage(self.tr(" ")) - return - variable2 = self.open_detected_time_dialog(variables) - if not variable2: - self.statusBar().showMessage(self.tr(" ")) - return - else: - return - - temp = file_raw[variable1].flatten() - self.spike_mat = temp - - temp = file_raw[variable2].flatten() - self.spike_time = temp - - elif file_extension == '.csv': - pass - - else: - pass - - self.refreshAct.setEnabled(True) - self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500) - self.wait() - - self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() - self.plotWaveForms() - self.plotDetectionResult() - self.plotPcaResult() - - if self.user: - self.statusBar().showMessage(self.tr("Uploading to server...")) - self.wait() - - res = self.user.post_detected_data(self.spike_mat, self.spike_time) - - if res['stat']: - self.statusBar().showMessage(self.tr("Uploaded"), 2500) - self.wait() - else: - self.wait() + pass + # filename, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Open file"), os.getcwd(), + # self.tr("Detected Spikes Files(*.mat *.csv *.tdms)")) + # + # if not filename: + # return FileNotFoundError('you should select a file') + # + # if not os.path.isfile(filename): + # raise FileNotFoundError(filename) + # + # self.statusBar().showMessage(self.tr("Loading...")) + # self.wait() + # file_extension = os.path.splitext(filename)[-1] + # if file_extension == '.mat': + # file_raw = sio.loadmat(filename) + # variables = list(file_raw.keys()) + # if '__version__' in variables: + # variables.remove('__version__') + # if '__header__' in variables: + # variables.remove('__header__') + # if '__globals__' in variables: + # variables.remove('__globals__') + # + # if len(variables) > 1: + # variable1 = self.open_detected_mat_dialog(variables) + # # self.wait() + # if not variable1: + # self.statusBar().showMessage(self.tr(" ")) + # return + # variable2 = self.open_detected_time_dialog(variables) + # if not variable2: + # self.statusBar().showMessage(self.tr(" ")) + # return + # else: + # return + # + # temp = file_raw[variable1].flatten() + # self.spike_mat = temp + # + # temp = file_raw[variable2].flatten() + # self.spike_time = temp + # + # elif file_extension == '.csv': + # pass + # + # else: + # pass + # + # self.refreshAct.setEnabled(True) + # self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500) + # self.wait() + # + # self.statusBar().showMessage(self.tr("Plotting..."), 2500) + # self.wait() + # self.plotWaveForms() + # self.plotDetectionResult() + # self.plotPcaResult() + # + # if self.user: + # self.statusBar().showMessage(self.tr("Uploading to server...")) + # self.wait() + # + # res = self.user.post_detected_data(self.spike_mat, self.spike_time) + # + # if res['stat']: + # self.statusBar().showMessage(self.tr("Uploaded"), 2500) + # self.wait() + # else: + # self.wait() def onImportSorted(self): pass @@ -332,6 +335,8 @@ def open_file_dialog_server(self): self.spike_mat = None self.spike_time = None self.raw = None + self.clusters_tmp = None + self.clusters_init = None self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() @@ -390,6 +395,8 @@ def onSignOut(self): self.spike_mat = None self.spike_time = None self.user_name = None + self.clusters_tmp = None + self.clusters_init = None self.accountButton.setIcon(QtGui.QIcon(icon_path + "unverified.png")) self.accountButton.setMenu(None) self.accountButton.setText('') @@ -483,13 +490,14 @@ def onSort(self): res = self.user.get_sorting_result() if res['stat']: self.clusters = res['clusters'] + self.cluster_time_vec = res["cluster_time_vec"] self.clusters_init = self.clusters.copy() self.clusters_tmp = self.clusters.copy() self.statusBar().showMessage(self.tr("Clusters Waveforms..."), 2500) self.wait() self.updateplotWaveForms(self.clusters) self.wait() - self.update_plotRaw(self.clusters) + self.update_plotRaw() self.wait() self.updateManualClusterList(self.clusters_tmp) self.plotDetectionResult() @@ -508,7 +516,29 @@ def plotRaw(self): self.widget_raw.showGrid(x=True, y=True) self.widget_raw.setMouseEnabled(y=False) - def update_plotRaw(self, clusters): + def update_plotRaw(self): + curve = HDF5Plot() + curve.setAPI(self.user) + curve.setHDF5(self.raw) + self.widget_raw.clear() + self.widget_raw.showGrid(x=True, y=True) + self.widget_raw.setMouseEnabled(y=False) + self.widget_raw.addItem(curve) + + if self.cluster_time_vec is not None: + for i, i_cluster in enumerate(np.unique(self.cluster_time_vec)): + if i_cluster == 0: + continue + color = self.colors[i - 1] + pen = pyqtgraph.mkPen(color=color) + curve = HDF5Plot() + curve.setAPI(self.user) + curve.setHDF5(self.raw, pen) + curve.setCluster(self.cluster_time_vec == i_cluster) + self.widget_raw.addItem(curve) + self.widget_raw.setXRange(0, 10000) + + def _update_plotRaw(self, clusters): data = self.raw colors = self.colors num_of_clusters = self.number_of_clusters @@ -597,7 +627,6 @@ def updateplotWaveForms(self, clusters_=None): un = np.unique(clusters) self.number_of_clusters = len(un[un >= 0]) - self.colors = self.distin_color(self.number_of_clusters) spike_clustered = dict() for i in range(self.number_of_clusters): spike_clustered[i] = spike_mat[clusters == i] @@ -1012,9 +1041,8 @@ def onWatchdogEvent(self): """Perform checks in regular intervals.""" self.mdiArea.checkTimestamps() - def wait(self, duration=2.0): + def wait(self): QtWidgets.QApplication.processEvents() - time.sleep(duration) def UpdatedClusterIndex(self): cl_ind = np.unique(self.clusters_tmp) @@ -1412,6 +1440,7 @@ def PCAManualCloseButton(self): def loadDefaultProject(self): flag_raw = False flag_update_raw = False + SERVER_MODE = False res = self.user.get_config_detect() if res['stat']: @@ -1430,6 +1459,8 @@ def loadDefaultProject(self): self.raw = new_data self.statusBar().showMessage(self.tr("Plotting..."), 2500) self.wait() + else: + SERVER_MODE = True flag_raw = True res = self.user.get_detection_result() @@ -1444,6 +1475,7 @@ def loadDefaultProject(self): res = self.user.get_sorting_result() if res['stat']: self.clusters_init = res['clusters'] + self.cluster_time_vec = res['cluster_time_vec'] self.clusters = self.clusters_init.copy() self.clusters_tmp = self.clusters_init.copy() self.updateplotWaveForms(self.clusters_init.copy()) @@ -1454,7 +1486,7 @@ def loadDefaultProject(self): if flag_raw: if flag_update_raw: - self.update_plotRaw(self.clusters) + self.update_plotRaw() else: self.plotRaw() diff --git a/ross_ui/controller/plot_time.py b/ross_ui/controller/plot_time.py deleted file mode 100644 index b6ed4ed..0000000 --- a/ross_ui/controller/plot_time.py +++ /dev/null @@ -1,6 +0,0 @@ -class PlotTime: - def __init__(self, start, end, spike, color): - self.start = start - self.end = end - self.spike = spike - self.color = color diff --git a/ross_ui/controller/segmented_time.py b/ross_ui/controller/segmented_time.py index 4e4a53e..01ed19e 100644 --- a/ross_ui/controller/segmented_time.py +++ b/ross_ui/controller/segmented_time.py @@ -1,4 +1,11 @@ -from controller.plot_time import PlotTime + + +class PlotTime: + def __init__(self, start, end, spike, color): + self.start = start + self.end = end + self.spike = spike + self.color = color class SegmentedTime: diff --git a/ross_ui/model/api.py b/ross_ui/model/api.py index eb3cdbd..8bc650f 100644 --- a/ross_ui/model/api.py +++ b/ross_ui/model/api.py @@ -144,13 +144,13 @@ def get_sorting_result(self): b.seek(0) d = np.load(b, allow_pickle=True) - return {'stat': True, 'clusters': d['clusters']} + return {'stat': True, 'clusters': d['clusters'], "cluster_time_vec": d["cluster_time_vec"]} elif response.status_code == 401: ret = self.refresh_jwt_token() if ret: self.get_sorting_result() - return {'stat': False, 'message': response.json()['message']} + return {'stat': False, 'message': response.content} return {'stat': False, 'message': 'Not Logged In!'} def get_config_detect(self): @@ -188,9 +188,9 @@ def start_detection(self, config): data['run_detection'] = True data['project_id'] = self.project_id - response = requests.put(self.url + '/detect', - headers={'Authorization': 'Bearer ' + self.access_token}, - json=data) + response = requests.post(self.url + '/detect', + headers={'Authorization': 'Bearer ' + self.access_token}, + json=data) if response.ok: return {'stat': True, 'message': 'success'} @@ -275,5 +275,3 @@ def browse(self, root: str): self.browse(root) else: return None - - diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index 49ed4f5..15d7a4f 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -11,7 +11,7 @@ icon_path = './view/icons/' -__version__ = "2.0.0" +__version__ = "2.0.0-alpha" # ----------------------------------------------------------------------------- From 3a85e487fccc3f586e026212e3381be39630820b Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 2 May 2023 15:49:43 +0330 Subject: [PATCH 11/31] update_plotRow changed --- ross_ui/controller/mainWindow.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 0aca852..6c2d1b8 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -538,28 +538,6 @@ def update_plotRaw(self): self.widget_raw.addItem(curve) self.widget_raw.setXRange(0, 10000) - def _update_plotRaw(self, clusters): - data = self.raw - colors = self.colors - num_of_clusters = self.number_of_clusters - spike_time = self.spike_time - time = SegmentedTime(spike_time, clusters) - multi_curve = MultiColoredCurve(data, time, num_of_clusters, len(data)) - self.widget_raw.clear() - for i in range(num_of_clusters + 1): - curve = HDF5Plot() - if i == num_of_clusters: - color = (255, 255, 255) - pen = pyqtgraph.mkPen(color=color) - else: - color = colors[i] - pen = pyqtgraph.mkPen(color=color) - curve.setHDF5(multi_curve.curves[str(i)], pen) - self.widget_raw.addItem(curve) - self.widget_raw.setXRange(0, 10000) - self.widget_raw.showGrid(x=True, y=True) - self.widget_raw.setMouseEnabled(y=False) - def onVisPlot(self): self.widget_visualizations() From 15a3e069d638d64a51e2affe581c0a63194fb964 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 2 May 2023 16:01:19 +0330 Subject: [PATCH 12/31] function for var reset - import --- ross_ui/controller/mainWindow.py | 68 ++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 6c2d1b8..ccd737d 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -103,6 +103,54 @@ def __init__(self): self.closeBottonPCAManual.clicked.connect(self.PCAManualCloseButton) self.doneBottonPCAManual.clicked.connect(self.PCAManualDoneButton) + def resetOnSignOutVars(self): + + self.raw = None + self.spike_mat = None + self.spike_time = None + self.user_name = None + self.user = None + self.image = None + self.pca_spikes = None + self.number_of_clusters = None + self.clusters_tmp = None + self.clusters_init = None + self.clusters = None + self.cluster_time_vec = None + + self.user = None + self.user_name = None + self.current_project = None + + self.saveManualFlag = False + self.plotManualFlag = False + self.resetManualFlag = False + self.undoManualFlag = False + + self.tempList = [] + + def resetOnImportVars(self): + + # initial values for software options + self.pca_manual = None + self.image = None + self.pca_spikes = None + self.number_of_clusters = None + self.clusters_tmp = None + self.clusters_init = None + self.clusters = None + + self.spike_mat = None + self.spike_time = None + self.cluster_time_vec = None + + self.saveManualFlag = False + self.plotManualFlag = False + self.resetManualFlag = False + self.undoManualFlag = False + self.tempList = [] + + def onUserAccount(self): if self.user is None: self.open_signin_dialog() @@ -194,10 +242,7 @@ def onImportRaw(self): self.wait() self.plotRaw() - self.spike_mat = None - self.spike_time = None - self.clusters_tmp = None - self.clusters_init = None + self.resetOnImportVars() self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() @@ -332,11 +377,7 @@ def open_file_dialog_server(self): self.wait() self.plotRaw() - self.spike_mat = None - self.spike_time = None - self.raw = None - self.clusters_tmp = None - self.clusters_init = None + self.resetOnImportVars() self.plot_histogram_pca.clear() self.plot_clusters_pca.clear() @@ -391,19 +432,14 @@ def open_save_as_dialog(self): def onSignOut(self): res = self.user.sign_out() if res['stat']: - self.raw = None - self.spike_mat = None - self.spike_time = None - self.user_name = None - self.clusters_tmp = None - self.clusters_init = None + self.resetOnSignOutVars() + self.accountButton.setIcon(QtGui.QIcon(icon_path + "unverified.png")) self.accountButton.setMenu(None) self.accountButton.setText('') self.accountButton.setStatusTip("Sign In/Up") self.logInAct.setEnabled(True) self.logOutAct.setEnabled(False) - self.user = None # self.saveAct.setEnabled(False) # self.saveAsAct.setEnabled(False) self.importMenu.setEnabled(False) From 4b6bb89485504a4816cb2eb3996e21d716f3f2b0 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Mon, 8 May 2023 15:17:26 +0330 Subject: [PATCH 13/31] spike detection matched with new structure --- ross_backend/models/data.py | 12 ++- ross_backend/models/project.py | 18 ++-- ross_backend/resources/detect.py | 8 +- ross_backend/resources/detection_result.py | 10 +- ross_backend/resources/funcs/detection.py | 13 ++- ross_backend/resources/sort.py | 1 - ross_backend/resources/sorting_result.py | 58 +++++------ ross_ui/controller/mainWindow.py | 107 +++++++++------------ ross_ui/controller/multicolor_curve.py | 25 ----- ross_ui/controller/segmented_time.py | 24 ----- ross_ui/model/api.py | 8 +- 11 files changed, 110 insertions(+), 174 deletions(-) delete mode 100644 ross_ui/controller/multicolor_curve.py delete mode 100644 ross_ui/controller/segmented_time.py diff --git a/ross_backend/models/data.py b/ross_backend/models/data.py index ab84d1d..c868799 100644 --- a/ross_backend/models/data.py +++ b/ross_backend/models/data.py @@ -8,7 +8,7 @@ class RawModel(db.Model): id = db.Column(db.Integer, primary_key=True) data = db.Column(db.String) - mode = db.Column(db.Integer, default=0) # 0: client, 1: server + mode = db.Column(db.Integer, default=0) # 0: inplace, 1: server user_id = db.Column(db.Integer, db.ForeignKey('users.id')) project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete="CASCADE")) @@ -54,13 +54,16 @@ class DetectResultModel(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('users.id')) project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete="CASCADE")) + mode = db.Column(db.Integer, default=0) # 0: inplace, 1: server + # user = db.relationship('UserModel') # project = db.relationship('ProjectModel', backref="raw", lazy=True) - def __init__(self, user_id, data, project_id): + def __init__(self, user_id, data, project_id, mode=0): self.data = data self.user_id = user_id self.project_id = project_id + self.mode = mode def json(self): return {'data': self.data} @@ -94,13 +97,16 @@ class SortResultModel(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('users.id')) project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete="CASCADE")) + mode = db.Column(db.Integer, default=0) # 0: inplace, 1: server + # user = db.relationship('UserModel') # project = db.relationship('ProjectModel', backref="raw", lazy=True) - def __init__(self, user_id, data, project_id): + def __init__(self, user_id, data, project_id, mode=0): self.data = data self.user_id = user_id self.project_id = project_id + self.mode = mode def json(self): return {'data': self.data} diff --git a/ross_backend/models/project.py b/ross_backend/models/project.py index 0227ab4..06c06e2 100644 --- a/ross_backend/models/project.py +++ b/ross_backend/models/project.py @@ -7,15 +7,15 @@ class ProjectModel(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete="CASCADE")) - config_detect = db.relationship('ConfigDetectionModel', backref='project', uselist=False, - cascade="all,delete,delete-orphan") - config_sort = db.relationship('ConfigSortModel', backref='project', uselist=False, - cascade="all,delete,delete-orphan") - raw = db.relationship('RawModel', backref='project', uselist=False, cascade="all,delete,delete-orphan") - detection_result = db.relationship('DetectResultModel', backref='project', uselist=False, - cascade="all,delete,delete-orphan") - sorting_result = db.relationship('SortResultModel', backref='project', uselist=False, - cascade="all,delete,delete-orphan") + # config_detect = db.relationship('ConfigDetectionModel', backref='project', uselist=False, + # cascade="all,delete,delete-orphan") + # config_sort = db.relationship('ConfigSortModel', backref='project', uselist=False, + # cascade="all,delete,delete-orphan") + # raw = db.relationship('RawModel', backref='project', uselist=False, cascade="all,delete,delete-orphan") + # detection_result = db.relationship('DetectResultModel', backref='project', uselist=False, + # cascade="all,delete,delete-orphan") + # sorting_result = db.relationship('SortResultModel', backref='project', uselist=False, + # cascade="all,delete,delete-orphan") # user = db.relationship('UserModel', backref="projects", lazy=True) # raw = db.relationship('RawModel', back_populates="project") diff --git a/ross_backend/resources/detect.py b/ross_backend/resources/detect.py index b49a1a3..85b0d76 100644 --- a/ross_backend/resources/detect.py +++ b/ross_backend/resources/detect.py @@ -69,8 +69,12 @@ def post(self): return {"message": "An error occurred inserting detection config."}, 500 # if data['run_detection']: try: - spikeMat, spikeTime = startDetection(project_id) - data_file = {'spikeMat': spikeMat, 'spikeTime': spikeTime, 'config': config.json()} + + spikeMat, spikeTime, pca_spikes, inds = startDetection(project_id) + + data_file = {'spikeMat': spikeMat, 'spikeTime': spikeTime, + 'config': config.json(), 'pca_spikes': pca_spikes, + 'inds': inds} # ------------------------------------------------------- print("inserting detection result to database") diff --git a/ross_backend/resources/detection_result.py b/ross_backend/resources/detection_result.py index f9f422f..b90559f 100644 --- a/ross_backend/resources/detection_result.py +++ b/ross_backend/resources/detection_result.py @@ -30,12 +30,14 @@ def get(self): SESSION[project_id] = detect_result if detect_result is not None: - + inds = detect_result['inds'] buffer = io.BytesIO() np.savez_compressed(buffer, - spike_mat=detect_result['spikeMat'], - spike_time=detect_result['spikeTime'], - config=detect_result['config']) + spike_mat=detect_result['spikeMat'][inds[:100], :], + spike_time=detect_result['spikeTime'][inds[:100], ], + config=detect_result['config'], + pca_spikes=detect_result['pca_spikes'], + inds=inds[:100]) buffer.seek(0) raw_bytes = buffer.read() buffer.close() diff --git a/ross_backend/resources/funcs/detection.py b/ross_backend/resources/funcs/detection.py index f82d730..5a4bf2d 100644 --- a/ross_backend/resources/funcs/detection.py +++ b/ross_backend/resources/funcs/detection.py @@ -1,8 +1,10 @@ import pickle +import random import numpy as np import pyyawt import scipy.signal +import sklearn.decomposition as decom from models.config import ConfigDetectionModel from models.data import RawModel @@ -52,7 +54,13 @@ def startDetection(project_id): # spike detection SpikeMat, SpikeTime = spike_detector(data_filtered, thr, pre_thr, post_thr, dead_time, thr_side) # print("SpikeMat shape and SpikeTime shape : ", SpikeMat.shape, SpikeTime.shape) - return SpikeMat, SpikeTime + + pca = decom.PCA(n_components=3) + pca_spikes = pca.fit_transform(SpikeMat) + inds = list(range(pca_spikes.shape[0])) + random.shuffle(inds) + + return SpikeMat, SpikeTime, pca_spikes, tuple(inds) # Threshold @@ -75,11 +83,8 @@ def threshold_calculator(method, thr_factor, data): # Filtering def filtering(data, forder, fRp, fRs, sr): - # print('inside filtering!') b, a = scipy.signal.butter(forder, (fRp / (sr / 2), fRs / (sr / 2)), btype='bandpass') - # print('after b, a') data_filtered = scipy.signal.filtfilt(b, a, data) - # print('after filtfilt!') return data_filtered diff --git a/ross_backend/resources/sort.py b/ross_backend/resources/sort.py index 2a15c25..7a782ab 100644 --- a/ross_backend/resources/sort.py +++ b/ross_backend/resources/sort.py @@ -8,7 +8,6 @@ from models.config import ConfigSortModel from models.data import SortResultModel, RawModel -from models.project import ProjectModel from resources.funcs.sorting import startSorting, startReSorting from resources.sorting_result import SESSION diff --git a/ross_backend/resources/sorting_result.py b/ross_backend/resources/sorting_result.py index 4ff00bc..9170a15 100644 --- a/ross_backend/resources/sorting_result.py +++ b/ross_backend/resources/sorting_result.py @@ -2,6 +2,7 @@ import os.path import pickle from uuid import uuid4 +from pathlib import Path import flask import numpy as np @@ -9,7 +10,9 @@ from flask_jwt_extended import jwt_required, get_jwt_identity from flask_restful import Resource, reqparse -from models.data import SortResultModel +from models.data import SortResultModel, RawModel, DetectResultModel +from resources.detection_result import SESSION as DET_SESSION +from resources.funcs.sorting import create_cluster_time_vec SESSION = dict() @@ -44,39 +47,8 @@ def get(self): return {'message': 'Sort Result Data does not exist'}, 404 - # @jwt_required - # def post(self): - # filestr = request.data - # user_id = get_jwt_identity() - # project_id = request.form["project_id"] - # if SortResultModel.find_by_project_id(project_id): - # return {'message': "Detection Result already exists."}, 400 - # - # # data = RawData.parser.parse_args() - # - # # print(eval(data['raw']).shape) - # data = SortResultModel(user_id=user_id, data=filestr, project_id=project_id) # data['raw']) - # - # try: - # data.save_to_db() - # except: - # return {"message": "An error occurred inserting sort result data."}, 500 - # - # return "Success", 201 - - # @jwt_required - # def delete(self): - # user_id = get_jwt_identity() - # project_id = request.form["project_id"] - # data = SortResultModel.find_by_project_id(project_id) - # if data: - # data.delete_from_db() - # return {'message': 'Sort Result Data deleted.'} - # return {'message': 'Sort Result Data does not exist.'}, 404 - @jwt_required def put(self): - # TODO : changes must be applied to cluster_time_vec # filestr = request.data user_id = get_jwt_identity() @@ -85,13 +57,29 @@ def put(self): b.seek(0) d = np.load(b, allow_pickle=True) - project_id = d["project_id"] + project_id = int(d["project_id"]) clusters = d["clusters"] b.close() - save_sort_result_path = '../ross_data/Sort_Result/' + str(uuid4()) + '.pkl' + detect_result = None + if project_id in DET_SESSION: + detect_result = DET_SESSION[project_id] + else: + detect_result_model = DetectResultModel.find_by_project_id(project_id) + if detect_result_model: + with open(detect_result_model.data, 'rb') as f: + detect_result = pickle.load(f) + DET_SESSION[project_id] = detect_result + if detect_result is None: + return {"message": "No Detection"}, 400 + + save_sort_result_path = str(Path(RawModel.find_by_project_id(project_id).data).parent / + (str(uuid4()) + '.pkl')) + + cluster_time_vec = create_cluster_time_vec(detect_result['spikeTime'], clusters, detect_result['config']) + with open(save_sort_result_path, 'wb') as f: - pickle.dump({"clusters": clusters}, f) + pickle.dump({"clusters": clusters, "cluster_time_vec": cluster_time_vec}, f) data = SortResultModel.find_by_project_id(int(project_id)) if data: if os.path.isfile(data.data): diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index ccd737d..ffe26eb 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -1,4 +1,3 @@ -import enum import os import pathlib import pickle @@ -14,7 +13,6 @@ import pyqtgraph.opengl as gl import scipy.io as sio import scipy.stats as stats -import sklearn.decomposition as decom from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5.QtGui import QPixmap, QTransform, QColor, QIcon from colour import Color @@ -26,11 +24,9 @@ from controller.detectedTimeSelect import DetectedTimeSelectApp as detected_time_form from controller.hdf5 import HDF5Plot from controller.matplot_figures import MatPlotFigures -from controller.multicolor_curve import MultiColoredCurve from controller.projectSelect import projectSelectApp as project_form from controller.rawSelect import RawSelectApp as raw_form from controller.saveAs import SaveAsApp as save_as_form -from controller.segmented_time import SegmentedTime from controller.serverAddress import ServerApp as server_form from controller.serverFileDialog import ServerFileDialogApp as sever_dialog from controller.signin import SigninApp as signin_form @@ -38,11 +34,7 @@ icon_path = './view/icons/' - -class softwareMode(enum.Enum): - SAME_PALACE = 0 - CLIENT_SIDE = 1 - SERVER_SIDE = 2 +os.makedirs('.tmp', exist_ok=True) class MainApp(MainWindow): @@ -60,6 +52,7 @@ def __init__(self): self.clusters_tmp = None self.clusters_init = None self.clusters = None + self.inds = None self.colors = self.distin_color(127) self.url = 'http://localhost:5000' @@ -82,7 +75,6 @@ def __init__(self): self.plotManualFlag = False self.resetManualFlag = False self.undoManualFlag = False - self.mode = softwareMode.SAME_PALACE self.tempList = [] self.startDetection.pressed.connect(self.onDetect) @@ -150,7 +142,6 @@ def resetOnImportVars(self): self.undoManualFlag = False self.tempList = [] - def onUserAccount(self): if self.user is None: self.open_signin_dialog() @@ -486,6 +477,8 @@ def onDetect(self): if res['stat']: self.spike_mat = res['spike_mat'] self.spike_time = res['spike_time'] + self.pca_spikes = res['pca_spikes'] + self.inds = res['inds'] self.statusBar().showMessage(self.tr("Plotting..."), 2500) self.wait() self.plotDetectionResult() @@ -632,18 +625,18 @@ def updateplotWaveForms(self, clusters_=None): clusters_ = self.clusters_tmp.copy() if self.clusters_tmp is not None: - spike_mat = self.spike_mat[self.clusters_tmp != -1, :] + spike_mat = self.spike_mat[self.clusters_tmp[self.inds,] != -1, :] else: spike_mat = self.spike_mat - clusters = clusters_[clusters_ != -1] + clusters = clusters_[self.inds][clusters_[self.inds] != -1] un = np.unique(clusters) self.number_of_clusters = len(un[un >= 0]) spike_clustered = dict() for i in range(self.number_of_clusters): - spike_clustered[i] = spike_mat[clusters == i] + spike_clustered[i] = spike_mat[clusters == i, :] self.widget_waveform.clear() self.widget_waveform.showGrid(x=True, y=True) @@ -788,8 +781,8 @@ def Plot3d(self): number_of_clusters = len(np.unique(self.clusters)) # Prepration - pca = decom.PCA(n_components=3) - pca_spikes = pca.fit_transform(self.spike_mat) + + pca_spikes = self.pca_spikes pca1 = pca_spikes[:, 0] pca2 = pca_spikes[:, 1] pca3 = pca_spikes[:, 2] @@ -857,8 +850,7 @@ def plotDetectionResult(self): spike_mat = self.spike_mat self.plot_histogram_pca.clear() - pca = decom.PCA(n_components=2) - pca_spikes = pca.fit_transform(spike_mat) + pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(pca_spikes[:, 0], pca_spikes[:, 1], bins=512) x_range = xedges[-1] - xedges[0] @@ -895,16 +887,13 @@ def plotPcaResult(self): self.plot_clusters_pca.clear() if self.clusters_tmp is not None: - spike_mat = self.spike_mat[self.clusters_tmp != -1, :] clusters_ = self.clusters_tmp[self.clusters_tmp != -1] + x_data = self.pca_spikes[self.clusters_tmp != -1, 0] + y_data = self.pca_spikes[self.clusters_tmp != -1, 1] else: - spike_mat = self.spike_mat clusters_ = None - - pca = decom.PCA(n_components=2) - pca_spikes = pca.fit_transform(spike_mat) - x_data = pca_spikes[:, 0] - y_data = pca_spikes[:, 1] + x_data = self.pca_spikes[:, 0] + y_data = self.pca_spikes[:, 1] scatter = pyqtgraph.ScatterPlotItem() @@ -953,8 +942,7 @@ def onDetect3D(self): self.subwindow_detect3d.setVisible(True) try: - pca = decom.PCA(n_components=2) - pca_spikes = pca.fit_transform(self.spike_mat) + pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(pca_spikes[:, 0], pca_spikes[:, 1], bins=512) gx = gl.GLGridItem() @@ -993,8 +981,7 @@ def closeDetect3D(self): self.subwindow_detect3d.setVisible(False) def onDetect3D1(self): - pca = decom.PCA(n_components=2) - pca_spikes = pca.fit_transform(self.spike_mat) + pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(pca_spikes[:, 0], pca_spikes[:, 1], bins=512) xpos, ypos = np.meshgrid(xedges[:-1] + xedges[1:], yedges[:-1] + yedges[1:]) - (xedges[1] - xedges[0]) xpos = xpos.flatten() * 1. / 2 @@ -1164,7 +1151,7 @@ def onPlotManualSorting(self): self.updateplotWaveForms(self.clusters_tmp.copy()) self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) self.wait() - self.update_plotRaw(self.clusters_tmp.copy()) + self.update_plotRaw() self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) self.plotManualFlag = False @@ -1191,28 +1178,28 @@ def onResetManualSorting(self): self.saveManualFlag = False def onSaveManualSorting(self): - if self.saveManualFlag: - self.clusters = self.clusters_tmp.copy() - # self.number_of_clusters = np.shape(np.unique(self.clusters))[0] - self.statusBar().showMessage(self.tr("Save Clustering Results...")) - self.wait() + # if self.saveManualFlag: + self.clusters = self.clusters_tmp.copy() + # self.number_of_clusters = np.shape(np.unique(self.clusters))[0] + self.statusBar().showMessage(self.tr("Save Clustering Results...")) + self.wait() - res = self.user.save_sort_results(self.clusters) - if res['stat']: - self.wait() - self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) - self.updateplotWaveForms(self.clusters) - self.wait() - self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) - self.update_plotRaw(self.clusters) - self.wait() - self.statusBar().showMessage(self.tr("Saving Done.")) - self.updateManualClusterList(self.clusters_tmp) - # self.updateManualSortingView() - else: - self.statusBar().showMessage(self.tr("An error occurred in saving!..."), 2000) + res = self.user.save_sort_results(self.clusters) + if res['stat']: + self.wait() + self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) + self.updateplotWaveForms(self.clusters) + self.wait() + self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) + self.update_plotRaw() + self.wait() + self.statusBar().showMessage(self.tr("Saving Done.")) + self.updateManualClusterList(self.clusters_tmp) + # self.updateManualSortingView() + else: + self.statusBar().showMessage(self.tr("An error occurred in saving!..."), 2000) - self.saveManualFlag = False + self.saveManualFlag = False def onUndoManualSorting(self): try: @@ -1349,13 +1336,8 @@ def closeAssign(self): self.subwindow_assign.setVisible(False) def OnPcaRemove(self): - if self.clusters_tmp is not None: - spike_mat = self.spike_mat[self.clusters_tmp != -1, :] - else: - spike_mat = self.spike_mat - pca = decom.PCA(n_components=2) - self.pca_spikes = pca.fit_transform(spike_mat) + self.pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(self.pca_spikes[:, 0], self.pca_spikes[:, 1], bins=512) x_range = xedges[-1] - xedges[0] @@ -1372,7 +1354,7 @@ def OnPcaRemove(self): if image._renderRequired: image.render() image.qimage = image.qimage.transformed(QTransform().scale(1, -1)) - image.save('test.png') + image.save('.tmp/test.png') self.PCAManualResetButton() self.subwindow_pca_manual.setVisible(True) @@ -1381,13 +1363,8 @@ def OnPcaRemove(self): def PCAManualDoneButton(self): points = self.subwindow_pca_manual.widget().points.copy() - if self.clusters_tmp is not None: - spike_mat = self.spike_mat[self.clusters_tmp != -1, :] - else: - spike_mat = self.spike_mat - pca = decom.PCA(n_components=2) - self.pca_spikes = pca.fit_transform(spike_mat) + self.pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(self.pca_spikes[:, 0], self.pca_spikes[:, 1], bins=512) x_range = xedges[-1] - xedges[0] y_range = yedges[-1] - yedges[0] @@ -1433,7 +1410,7 @@ def PCAManualDoneButton(self): def PCAManualResetButton(self): self.subwindow_pca_manual.widget().reset() - self.image = QPixmap('test.png') + self.image = QPixmap('.tmp/test.png') self.label_pca_manual.setMaximumWidth(self.image.width()) self.label_pca_manual.setMaximumHeight(self.image.height()) self.label_pca_manual.resize(self.image.width(), self.image.height()) @@ -1481,6 +1458,8 @@ def loadDefaultProject(self): if res['stat']: self.spike_mat = res['spike_mat'] self.spike_time = res['spike_time'] + self.pca_spikes = res['pca_spikes'] + self.inds = res['inds'] self.plotWaveForms() self.plotDetectionResult() self.plotPcaResult() diff --git a/ross_ui/controller/multicolor_curve.py b/ross_ui/controller/multicolor_curve.py deleted file mode 100644 index 95ef681..0000000 --- a/ross_ui/controller/multicolor_curve.py +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np - - -class MultiColoredCurve: - def __init__(self, data, segmented_time, num_of_clusters, raw_len): - self.data = data - self.segmented = segmented_time - self.curves = dict() - - for i in range(num_of_clusters + 1): - self.curves['{}'.format(i)] = np.zeros(raw_len) - - for seg in self.segmented.segmented: - if seg.spike: - start = int(seg.start) - end = int(seg.end) - if int(seg.color) >= 0: - self.curves[str(int(seg.color))][start: end] = self.data[start: end].copy() - else: - self.curves[str(num_of_clusters)][start: end] = self.data[start: end].copy() - - else: - start = int(seg.start) - end = int(seg.end) - self.curves[str(num_of_clusters)][start: end] = self.data[start: end].copy() diff --git a/ross_ui/controller/segmented_time.py b/ross_ui/controller/segmented_time.py deleted file mode 100644 index 01ed19e..0000000 --- a/ross_ui/controller/segmented_time.py +++ /dev/null @@ -1,24 +0,0 @@ - - -class PlotTime: - def __init__(self, start, end, spike, color): - self.start = start - self.end = end - self.spike = spike - self.color = color - - -class SegmentedTime: - def __init__(self, spike_time, clusters, sr=40000): - self.segmented = [] - self.segmented.append(PlotTime(0, spike_time[0] - sr * 0.001, spike=False, color=-1)) - for i in range(1, spike_time.shape[0]): - try: - self.segmented.append( - PlotTime(spike_time[i - 1] - sr * 0.001, spike_time[i - 1] + sr * 0.0015, spike=True, - color=clusters[i - 1] if clusters[i - 1] >= 0 else -1)) - - self.segmented.append(PlotTime(spike_time[i - 1] + sr * 0.0015, spike_time[i] - sr * 0.001, spike=False, - color=-1)) - except: - break diff --git a/ross_ui/model/api.py b/ross_ui/model/api.py index 8bc650f..68b20bd 100644 --- a/ross_ui/model/api.py +++ b/ross_ui/model/api.py @@ -123,14 +123,16 @@ def get_detection_result(self): b.write(response.content) b.seek(0) d = np.load(b, allow_pickle=True) - return {'stat': True, 'spike_mat': d['spike_mat'], 'spike_time': d['spike_time']} + return {'stat': True, + 'spike_mat': d['spike_mat'], 'spike_time': d['spike_time'], + 'pca_spikes': d['pca_spikes'], 'inds': d['inds']} elif response.status_code == 401: ret = self.refresh_jwt_token() if ret: self.get_detection_result() - return {'stat': False, 'message': response.json()['message']} + return {'stat': False, 'message': response.content} return {'stat': False, 'message': 'Not Logged In!'} def get_sorting_result(self): @@ -257,7 +259,7 @@ def save_sort_results(self, clusters): ret = self.refresh_jwt_token() if ret: self.save_sort_results(clusters) - return {'stat': False, 'message': response.json()["message"]} + return {'stat': False, 'message': response.content} return {'stat': False, 'message': 'Not Logged In!'} def browse(self, root: str): From 4918b792a33d0c58d7230594879589d15fdc1808 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Mon, 8 May 2023 18:35:27 +0330 Subject: [PATCH 14/31] 3d plot functionality changed --- .gitignore | 3 +- ross_backend/resources/detection_result.py | 7 +- ross_ui/controller/mainWindow.py | 122 ++++++--------------- ross_ui/controller/matplot_figures.py | 16 +-- ross_ui/view/mainWindow.py | 80 ++++++-------- 5 files changed, 79 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index fc177d9..b71c9e8 100644 --- a/.gitignore +++ b/.gitignore @@ -407,4 +407,5 @@ compile_commands.json data /ross_backend/data.db /ross_ui/ross_data/ -ross_backend/ross_data/ \ No newline at end of file +ross_backend/ross_data/ +ross_ui/.tmp \ No newline at end of file diff --git a/ross_backend/resources/detection_result.py b/ross_backend/resources/detection_result.py index b90559f..129bac9 100644 --- a/ross_backend/resources/detection_result.py +++ b/ross_backend/resources/detection_result.py @@ -10,6 +10,7 @@ from models.data import DetectResultModel SESSION = dict() +DATA_NUM_TO_SEND = 1000 class DetectionResultDefault(Resource): @@ -33,11 +34,11 @@ def get(self): inds = detect_result['inds'] buffer = io.BytesIO() np.savez_compressed(buffer, - spike_mat=detect_result['spikeMat'][inds[:100], :], - spike_time=detect_result['spikeTime'][inds[:100], ], + spike_mat=detect_result['spikeMat'][inds[:DATA_NUM_TO_SEND], :], + spike_time=detect_result['spikeTime'], config=detect_result['config'], pca_spikes=detect_result['pca_spikes'], - inds=inds[:100]) + inds=inds[:DATA_NUM_TO_SEND]) buffer.seek(0) raw_bytes = buffer.read() buffer.close() diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index ffe26eb..4d577ce 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -618,7 +618,7 @@ def distin_color(self, number_of_colors): h = h % 1 colors.append(self.hsv_to_rgb(h, 0.99, 0.99)) - return np.array(colors) + return np.array(colors, dtype=int) def updateplotWaveForms(self, clusters_=None): if clusters_ is None: @@ -677,55 +677,38 @@ def updateplotWaveForms(self, clusters_=None): self.widget_waveform.autoRange() - def create_sub_base(self): - self.number_of_clusters = np.shape(np.unique(self.clusters))[0] - # self.number_of_clusters = len(np.unique(self.clusters)) - if self.number_of_clusters % 3 != 0: - nrow = int(self.number_of_clusters / 3) + 1 - else: - nrow = int(self.number_of_clusters / 3) - - self.sub_base = str(nrow) + str(3) - def onPlotClusterWave(self): try: - self.create_sub_base() + # self.create_sub_base() number_of_clusters = self.number_of_clusters colors = self.colors spike_clustered = dict() for i in range(number_of_clusters): - spike_clustered[i] = self.spike_mat[self.clusters_tmp == i] + spike_clustered[i] = self.spike_mat[self.clusters_tmp[self.inds] == i] - figure = MatPlotFigures('Clusters Waveform', number_of_clusters, width=10, height=6, dpi=100, - subplot_base=self.sub_base) + figure = MatPlotFigures('Clusters Waveform', number_of_clusters, width=10, height=6, dpi=100) for i, ax in enumerate(figure.axes): + for spike in spike_clustered[i]: + ax.plot(spike, color=tuple(colors[i] / 255), linewidth=1, alpha=0.25) avg = np.average(spike_clustered[i], axis=0) - selected_spike = spike_clustered[i][np.sum(np.power(spike_clustered[i] - avg, 2), axis=1) < - np.sum(np.power(avg, 2)) * 0.2] - - # selected_spike = spike_clustered[i][:5] - ax.plot(avg, color='red', linewidth=3) - for spike in selected_spike[:100]: - ax.plot(spike, color=tuple(colors[i] / 255), linewidth=1, alpha=0.125) + ax.plot(avg, color='red', linewidth=2) ax.set_title('Cluster {}'.format(i + 1)) plt.tight_layout() plt.show() except: - pass + print(traceback.format_exc()) def onPlotLiveTime(self): try: - self.create_sub_base() number_of_clusters = self.number_of_clusters colors = self.colors spike_clustered_time = dict() for i in range(number_of_clusters): spike_clustered_time[i] = self.spike_time[self.clusters == i] - figure = MatPlotFigures('LiveTime', number_of_clusters, width=10, height=6, dpi=100, - subplot_base=self.sub_base) + figure = MatPlotFigures('LiveTime', number_of_clusters, width=10, height=6, dpi=100) for i, ax in enumerate(figure.axes): ax.hist(spike_clustered_time[i], bins=100, color=tuple(colors[i] / 255)) ax.set_title('Cluster {}'.format(i + 1)) @@ -733,11 +716,10 @@ def onPlotLiveTime(self): plt.show() except: - pass + print(traceback.format_exc()) def onPlotIsi(self): try: - self.create_sub_base() number_of_clusters = self.number_of_clusters colors = self.colors spike_clustered_time = dict() @@ -748,7 +730,7 @@ def onPlotIsi(self): tmp1 = spike_clustered_time[i][1:].copy() spike_clustered_delta[i] = tmp1 - tmp2 - figure = MatPlotFigures('ISI', number_of_clusters, width=10, height=6, dpi=100, subplot_base=self.sub_base) + figure = MatPlotFigures('ISI', number_of_clusters, width=10, height=6, dpi=100) for i, ax in enumerate(figure.axes): gamma = stats.gamma @@ -774,7 +756,6 @@ def onPlot3d(self): def Plot3d(self): try: - self.plot_3d.setCameraPosition(distance=30) axis1 = self.axis1ComboBox.currentIndex() axis2 = self.axis2ComboBox.currentIndex() axis3 = self.axis3ComboBox.currentIndex() @@ -783,71 +764,34 @@ def Plot3d(self): # Prepration pca_spikes = self.pca_spikes - pca1 = pca_spikes[:, 0] - pca2 = pca_spikes[:, 1] - pca3 = pca_spikes[:, 2] - spike_time = np.squeeze(self.spike_time / 100000) + pca1 = pca_spikes[self.inds, 0] + pca2 = pca_spikes[self.inds, 1] + pca3 = pca_spikes[self.inds, 2] + spike_time = np.squeeze(self.spike_time[self.inds]) p2p = np.squeeze(np.abs(np.amax(self.spike_mat, axis=1) - np.amin(self.spike_mat, axis=1))) duty = np.squeeze(np.abs(np.argmax(self.spike_mat, axis=1) - np.argmin(self.spike_mat, axis=1)) / 5) - gx = gl.GLGridItem() - gx.rotate(90, 0, 1, 0) - gx.setSize(15, 15) - gx.translate(-7.5, 0, 0) - self.plot_3d.addItem(gx) - gy = gl.GLGridItem() - gy.rotate(90, 1, 0, 0) - gy.setSize(15, 15) - gy.translate(0, -7.5, 0) - self.plot_3d.addItem(gy) - gz = gl.GLGridItem() - gz.setSize(15, 15) - gz.translate(0, 0, -7.5) - self.plot_3d.addItem(gz) - - mode_flag = False mode_list = [pca1, pca2, pca3, spike_time, p2p, duty] - if (axis1 != axis2) and (axis1 != axis3) and (axis2 != axis3): - pos = np.array((mode_list[axis1], mode_list[axis2], mode_list[axis3])).T - mode_flag = True - - if mode_flag: - colors = self.colors - items = self.plot_3d.items.copy() - for it in items: - if type(it) == pyqtgraph.opengl.items.GLScatterPlotItem.GLScatterPlotItem: - self.plot_3d.removeItem(it) - - for i in range(number_of_clusters): - pos_cluster = pos[self.clusters == i, :] - avg = np.average(pos_cluster, axis=0) - selected_pos = pos_cluster[np.sum(np.power((pos[self.clusters == i, :] - avg), 2), axis=1) < - 0.05 * np.amax(np.sum(np.power((pos[self.clusters == i, :] - avg), 2), - axis=1)), :] - ind = np.arange(selected_pos.shape[0]) - np.random.shuffle(ind) - scattered_pos = selected_pos[ind[:300], :] - color = np.zeros([np.shape(scattered_pos)[0], 4]) - color[:, 0] = colors[i][0] / 255 - color[:, 1] = colors[i][1] / 255 - color[:, 2] = colors[i][2] / 255 - color[:, 3] = 1 - self.plot_3d.addItem(gl.GLScatterPlotItem(pos=scattered_pos, size=3, color=color)) - else: - print('Same Axis Error') + a1, a2, a3 = mode_list[axis1], mode_list[axis2], mode_list[axis3] + + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + for i in range(number_of_clusters): + ax.scatter(a1[self.clusters_tmp[self.inds] == i], a2[self.clusters_tmp[self.inds] == i], + a3[self.clusters_tmp[self.inds] == i], c='#%02x%02x%02x' % tuple(self.colors[i])) + ax.set_xlabel(self.axis1ComboBox.currentText()) + ax.set_ylabel(self.axis2ComboBox.currentText()) + ax.set_zlabel(self.axis3ComboBox.currentText()) + plt.show() except: - pass + print(traceback.format_exc()) def close3D(self): self.subwindow_3d.setVisible(False) def plotDetectionResult(self): try: - if self.clusters_tmp is not None: - spike_mat = self.spike_mat[self.clusters_tmp != -1, :] - else: - spike_mat = self.spike_mat self.plot_histogram_pca.clear() pca_spikes = self.pca_spikes @@ -881,7 +825,7 @@ def plotDetectionResult(self): self.plot_histogram_pca.show() except: - pass + print(traceback.format_exc()) def plotPcaResult(self): self.plot_clusters_pca.clear() @@ -975,7 +919,7 @@ def onDetect3D(self): self.plot_detect3d.addItem(bg) except: - pass + print(traceback.format_exc()) def closeDetect3D(self): self.subwindow_detect3d.setVisible(False) @@ -1112,9 +1056,7 @@ def onActManualSorting(self): self.plotHistFlag = True except: - print("an error accrued in manual pcaRemove") print(traceback.format_exc()) - pass elif act.text() == "PCA Group": try: self.pca_manual = "Group" @@ -1122,9 +1064,7 @@ def onActManualSorting(self): self.manualPreparingSorting(self.clusters_tmp.copy()) except: - print("an error accrued in manual pcaGroup") print(traceback.format_exc()) - pass elif act.text() == "Resort": try: @@ -1282,7 +1222,7 @@ def assignManual(self): self.listTargetsWidget.addItem(item_target) self.listTargetsWidget.setCurrentItem(item_target) except: - pass + print(traceback.format_exc()) def onAssignManualSorting(self): source = self.listSourceWidget.currentItem() @@ -1330,7 +1270,7 @@ def onAssignManualSorting(self): self.updateManualClusterList(self.clusters_tmp) except: - pass + print(traceback.format_exc()) def closeAssign(self): self.subwindow_assign.setVisible(False) diff --git a/ross_ui/controller/matplot_figures.py b/ross_ui/controller/matplot_figures.py index b9d8c02..454dfd5 100644 --- a/ross_ui/controller/matplot_figures.py +++ b/ross_ui/controller/matplot_figures.py @@ -1,17 +1,13 @@ +import math + import matplotlib.pyplot as plt class MatPlotFigures: - def __init__(self, fig_title, number_of_clusters, width=10, height=6, dpi=100, subplot_base='13'): + def __init__(self, fig_title, number_of_clusters, width=10, height=6, dpi=100): fig = plt.figure(figsize=(width, height), dpi=dpi) fig.canvas.set_window_title(fig_title) self.axes = [] - for i in range(int(subplot_base) // 10): - if i == (int(subplot_base) // 10) - 1: - for j in range(number_of_clusters % 3): - sub = int(subplot_base + str((i * 3) + j + 1)) - self.axes.append(fig.add_subplot(sub)) - else: - for j in range(3): - sub = int(subplot_base + str((i * 3) + j + 1)) - self.axes.append(fig.add_subplot(sub)) + nrows = math.ceil(number_of_clusters / 3) + for i in range(number_of_clusters): + self.axes.append(fig.add_subplot(nrows, 3, i+1)) diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index 15d7a4f..25465f7 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -255,11 +255,11 @@ def createActions(self): self.plotAct.setStatusTip(self.tr("Plotting Clusters")) self.plotAct.triggered.connect(self.onVisPlot) - self.detect3dAct = QtWidgets.QAction(self.tr("&Detection Histogram 3D")) + # self.detect3dAct = QtWidgets.QAction(self.tr("&Detection Histogram 3D")) # self.detect3dAct.setCheckable(True) # self.detect3dAct.setChecked(False) - self.detect3dAct.setStatusTip(self.tr("Plotting 3D histogram of detection result")) - self.detect3dAct.triggered.connect(self.onDetect3D) + # self.detect3dAct.setStatusTip(self.tr("Plotting 3D histogram of detection result")) + # self.detect3dAct.triggered.connect(self.onDetect3D) self.clusterwaveAct = QtWidgets.QAction(self.tr("&Plotting Clusters Waveforms")) # self.clusterwaveAct.setCheckable(True) @@ -382,7 +382,7 @@ def createMenubar(self): # Menu entry for visualization self.visMenu = self.menuBar().addMenu(self.tr("&Visualization")) self.visMenu.setEnabled(False) - self.visMenu.addAction(self.detect3dAct) + # self.visMenu.addAction(self.detect3dAct) self.visMenu.addAction(self.clusterwaveAct) self.visMenu.addAction(self.livetimeAct) self.visMenu.addAction(self.isiAct) @@ -434,7 +434,7 @@ def align_subwindows(self): self.subwindow_detect3d.setGeometry(0, 0, int(self.w_mdi / 2), int(self.h_mdi * 2 / 3)) self.subwindow_detect3d.setVisible(False) - self.subwindow_3d.setGeometry(0, 0, int(self.w_mdi / 2), int(self.h_mdi * 2 / 3)) + # self.subwindow_3d.setGeometry(0, 0, int(self.w_mdi / 2), int(self.h_mdi * 2 / 3)) self.subwindow_3d.setVisible(False) self.subwindow_pca_manual.setVisible(False) @@ -483,20 +483,13 @@ def createSubWindows(self): layout_detect3d.addWidget(self.plot_detect3d) self.widget_detect3d.setLayout(layout_detect3d) - # Plot Clusters Waveform - # self.widget_clusterwaveform = pyqtgraph.widgets.MultiPlotWidget.MultiPlotWidget() - - # Plot Live Time - # self.widget_livetime = QtWidgets.QWidget() - # Plot ISI # self.widget_isi = QtWidgets.QWidget() # 3D Plot self.widget_3d = QtWidgets.QWidget() - self.plot_3d = gl.GLViewWidget() - self.plot_3d.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - layout_plot_3d = QtWidgets.QGridLayout() + self.widget_3d.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + layout_widget_3d = QtWidgets.QGridLayout() layout_plot_pca_manual = QtWidgets.QGridLayout() self.axis1ComboBox = QtWidgets.QComboBox() @@ -530,23 +523,35 @@ def createSubWindows(self): self.plotButton = QtWidgets.QPushButton(text='Plot') - axisWidget = QtWidgets.QWidget() + layout_widget_3d.addWidget(QtWidgets.QLabel("Axis 1"), 0, 0) + layout_widget_3d.addWidget(QtWidgets.QLabel("Axis 2"), 0, 1) + layout_widget_3d.addWidget(QtWidgets.QLabel("Axis 3"), 0, 2) + + layout_widget_3d.addWidget(self.axis1ComboBox, 1, 0) + layout_widget_3d.addWidget(self.axis2ComboBox, 1, 1) + layout_widget_3d.addWidget(self.axis3ComboBox, 1, 2) + layout_widget_3d.addWidget(self.plotButton, 1, 3) + layout_widget_3d.addWidget(self.closeButton3d, 1, 4) + + self.widget_3d.setLayout(layout_widget_3d) + + # axisWidget = QtWidgets.QWidget() axisWidget_pca_manual = QtWidgets.QWidget() - axisLayout = QtWidgets.QHBoxLayout() - label = QtWidgets.QLabel('axis 1') - axisLayout.addWidget(label) - axisLayout.addWidget(self.axis1ComboBox) - label = QtWidgets.QLabel('axis 2') - axisLayout.addWidget(label) - axisLayout.addWidget(self.axis2ComboBox) - label = QtWidgets.QLabel('axis 3') - axisLayout.addWidget(label) - axisLayout.addWidget(self.axis3ComboBox) - axisLayout.addStretch(5) - axisLayout.addWidget(self.plotButton) - axisLayout.addWidget(self.closeButton3d) - axisWidget.setLayout(axisLayout) + # axisLayout = QtWidgets.QHBoxLayout() + # label = QtWidgets.QLabel('axis 1') + # axisLayout.addWidget(label) + # axisLayout.addWidget(self.axis1ComboBox) + # label = QtWidgets.QLabel('axis 2') + # axisLayout.addWidget(label) + # axisLayout.addWidget(self.axis2ComboBox) + # label = QtWidgets.QLabel('axis 3') + # axisLayout.addWidget(label) + # axisLayout.addWidget(self.axis3ComboBox) + # axisLayout.addStretch(5) + # axisLayout.addWidget(self.plotButton) + # axisLayout.addWidget(self.closeButton3d) + # axisWidget.setLayout(axisLayout) axisLayout_pca_button = QtWidgets.QHBoxLayout() axisLayout_pca_main = QtWidgets.QVBoxLayout() @@ -565,10 +570,7 @@ def createSubWindows(self): axisLayout_pca_main.addLayout(axisLayout_pca_button) axisWidget_pca_manual.setLayout(axisLayout_pca_main) - - layout_plot_3d.addWidget(axisWidget) - layout_plot_3d.addWidget(self.plot_3d) - self.widget_3d.setLayout(layout_plot_3d) + # layout_plot_3d.addWidget(self.plot_3d) self.painter = QPainter() layout_plot_pca_manual.addWidget(axisWidget_pca_manual) @@ -953,18 +955,11 @@ def createSubWindows(self): self.mdiArea.addSubWindow(self.widget_waveform).setWindowTitle("Waveforms") self.mdiArea.addSubWindow(self.plot_histogram_pca).setWindowTitle("2D PCA Histogram") - # self.mdiArea.addSubWindow(self.widget_clusters).setWindowTitle("Clusters") self.mdiArea.addSubWindow(self.widget_raw).setWindowTitle("Raw Data") self.mdiArea.addSubWindow(self.widget_settings).setWindowTitle("Settings") - # self.mdiArea.addSubWindow(self.widget_visualizations).setWindowTitle("Visualizations") self.mdiArea.addSubWindow(self.widget_detect3d).setWindowTitle("DetectionResult 3D Hist") - # self.mdiArea.addSubWindow(self.widget_clusters).setWindowTitle("Waveforms") - # self.mdiArea.addSubWindow(self.widget_clusterwaveform).setWindowTitle("Plot Clusters Waveforms") - # self.mdiArea.addSubWindow(self.widget_livetime).setWindowTitle("Live Time") - # self.mdiArea.addSubWindow(self.widget_isi).setWindowTitle("ISI") self.mdiArea.addSubWindow(self.widget_3d).setWindowTitle("3D Plot") self.mdiArea.addSubWindow(self.widget_assign_manual).setWindowTitle("Assign to Nearest") - self.mdiArea.addSubWindow(self.widget_pca_manual).setWindowTitle('PCA MANUAL') self.mdiArea.addSubWindow(self.plot_clusters_pca).setWindowTitle("2D PCA Clusters") @@ -973,15 +968,12 @@ def createSubWindows(self): self.subwindow_pca_histograms = subwindow_list[1] self.subwindow_raw = subwindow_list[2] self.subwindow_settings = subwindow_list[3] - # self.subwindow_visualization = subwindow_list[4] self.subwindow_detect3d = subwindow_list[4] self.subwindow_3d = subwindow_list[5] + self.subwindow_3d.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self.subwindow_assign = subwindow_list[6] self.subwindow_pca_manual = subwindow_list[7] self.subwindow_pca_clusters = subwindow_list[8] - # self.subwindow_clusterwave = subwindow_list[5] - # self.subwindow_livetime = subwindow_list[6] - # self.subwindow_isi = subwindow_list[7] for subwindow in subwindow_list: subwindow.setWindowFlags( From 3a6c3d10125dfae4064c7478406cc2dcbed541bf Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Mon, 8 May 2023 19:24:56 +0330 Subject: [PATCH 15/31] options - server dial modified --- ross_ui/controller/matplot_figures.py | 2 +- ross_ui/view/serverAddress.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ross_ui/controller/matplot_figures.py b/ross_ui/controller/matplot_figures.py index 454dfd5..61d74fc 100644 --- a/ross_ui/controller/matplot_figures.py +++ b/ross_ui/controller/matplot_figures.py @@ -6,7 +6,7 @@ class MatPlotFigures: def __init__(self, fig_title, number_of_clusters, width=10, height=6, dpi=100): fig = plt.figure(figsize=(width, height), dpi=dpi) - fig.canvas.set_window_title(fig_title) + fig.canvas.manager.set_window_title(fig_title) self.axes = [] nrows = math.ceil(number_of_clusters / 3) for i in range(number_of_clusters): diff --git a/ross_ui/view/serverAddress.py b/ross_ui/view/serverAddress.py index 7b88739..042bcc6 100644 --- a/ross_ui/view/serverAddress.py +++ b/ross_ui/view/serverAddress.py @@ -4,18 +4,26 @@ class Server_Dialog(QtWidgets.QDialog): def __init__(self, server_text): super().__init__() - self.setFixedSize(400, 227) + self.setFixedSize(400, 150) self.buttonBox = QtWidgets.QDialogButtonBox(self) - self.buttonBox.setGeometry(QtCore.QRect(30, 160, 341, 32)) + layout_main = QtWidgets.QVBoxLayout() + # self.buttonBox.setGeometry(QtCore.QRect(30, 160, 341, 32)) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(80, 50, 141, 21)) + # self.label.setGeometry(QtCore.QRect(80, 50, 141, 21)) self.lineEdit = QtWidgets.QLineEdit(self) - self.lineEdit.setGeometry(QtCore.QRect(80, 100, 221, 31)) + # self.lineEdit.setGeometry(QtCore.QRect(80, 100, 221, 31)) self.setWindowTitle("Server Address") self.label.setText("Enter Server Address:") self.lineEdit.setText(server_text) + + layout_main.addWidget(self.label) + layout_main.addWidget(self.lineEdit) + layout_main.addWidget(QtWidgets.QLabel("")) + layout_main.addWidget(self.buttonBox) + + self.setLayout(layout_main) From 4ade3351c3c7e19b01ee7132d14e4f711d4778fe Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Mon, 8 May 2023 20:21:08 +0330 Subject: [PATCH 16/31] manual bug fixed --- ross_ui/controller/mainWindow.py | 39 +++++++++++++------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 4d577ce..e3d70bd 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -998,7 +998,7 @@ def UpdatedClusterIndex(self): cnt += 1 def manualPreparingSorting(self, temp): - if len(self.tempList) == 5: + if len(self.tempList) == 10: self.tempList.pop(0) self.tempList.append(temp) self.plotManualFlag = True @@ -1023,10 +1023,10 @@ def onActManualSorting(self): act = self.manualActWidget.currentItem() clusters = self.listWidget.selectedIndexes() selected_clusters = [cl.row() for cl in clusters] + self.manualPreparingSorting(self.clusters_tmp.copy()) if act.text() == 'Merge': try: self.mergeManual(selected_clusters) - self.manualPreparingSorting(self.clusters_tmp.copy()) self.updateManualClusterList(self.clusters_tmp.copy()) except: print("an error accrued in manual Merge") @@ -1034,7 +1034,6 @@ def onActManualSorting(self): elif act.text() == 'Remove': try: self.removeManual(selected_clusters) - self.manualPreparingSorting(self.clusters_tmp.copy()) self.updateManualClusterList(self.clusters_tmp.copy()) self.plotHistFlag = True except: @@ -1043,7 +1042,6 @@ def onActManualSorting(self): elif act.text() == 'Assign to nearest': try: self.assignManual() - self.manualPreparingSorting(self.clusters_tmp.copy()) except: print("an error accrued in manual Assign to nearest") @@ -1052,7 +1050,6 @@ def onActManualSorting(self): try: self.pca_manual = "Remove" self.OnPcaRemove() - self.manualPreparingSorting(self.clusters_tmp.copy()) self.plotHistFlag = True except: @@ -1061,7 +1058,6 @@ def onActManualSorting(self): try: self.pca_manual = "Group" self.OnPcaRemove() - self.manualPreparingSorting(self.clusters_tmp.copy()) except: print(traceback.format_exc()) @@ -1069,7 +1065,6 @@ def onActManualSorting(self): elif act.text() == "Resort": try: self.manualResort(selected_clusters) - self.manualPreparingSorting(self.clusters_tmp.copy()) self.updateManualClusterList(self.clusters_tmp.copy()) except: print("an error accrued in manual resort") @@ -1110,7 +1105,7 @@ def onResetManualSorting(self): self.statusBar().showMessage(self.tr("Resetting Spikes Waveforms...")) self.wait() - self.update_plotRaw(self.clusters_init.copy()) + self.update_plotRaw() self.statusBar().showMessage(self.tr("Resetting Raw Data Waveforms..."), 2000) self.resetManualFlag = False @@ -1143,11 +1138,11 @@ def onSaveManualSorting(self): def onUndoManualSorting(self): try: - self.tempList.pop() - if len(self.tempList) != 0: - self.clusters_tmp = self.tempList[-1] + + if len(self.tempList) > 0: + self.clusters_tmp = self.tempList.pop() else: - self.clusters_tmp = self.clusters_init.copy() + QtWidgets.QMessageBox.information(self, "ROSS", "No More Undo :(") # update cluster list self.updateManualClusterList(self.clusters_tmp) @@ -1237,12 +1232,12 @@ def onAssignManualSorting(self): self.statusBar().showMessage(self.tr("Assigning Source Cluster to Targets...")) self.wait() - source_spikes = self.spike_mat[self.clusters_tmp == source_cluster] + source_spikes = self.pca_spikes[self.clusters_tmp == source_cluster] source_ind = np.nonzero(self.clusters_tmp == source_cluster) target_avg = np.zeros((len(target_clusters), source_spikes.shape[1])) for it, target in enumerate(target_clusters): - target_avg[it, :] = np.average(self.spike_mat[self.clusters_tmp == target], axis=0) + target_avg[it, :] = np.average(self.pca_spikes[self.clusters_tmp == target], axis=0) # TODO: check different nearest_neighbors algorithms nbrs = NearestNeighbors(n_neighbors=1).fit(target_avg) indices = nbrs.kneighbors(source_spikes, return_distance=False) @@ -1255,16 +1250,14 @@ def onAssignManualSorting(self): self.listSourceWidget.clear() self.listTargetsWidget.clear() - - for i in range(len(np.unique(self.clusters_tmp))): + # TODO: check for all bugs like this + # for i in range(len(np.unique(self.clusters_tmp))): + cu = np.unique(self.clusters_tmp) + for i in range(len(cu[cu != -1])): item = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) self.listSourceWidget.addItem(item) + self.listTargetsWidget.addItem(item) - for i in range(len(np.unique(self.clusters_tmp))): - item_target = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) - self.listTargetsWidget.addItem(item_target) - - self.manualPreparingSorting(self.clusters_tmp.copy()) else: self.statusBar().showMessage(self.tr("You Should Choose One Source and at least One Target..."), 2000) @@ -1345,8 +1338,8 @@ def PCAManualDoneButton(self): self.subwindow_pca_manual.setVisible(False) self.updateplotWaveForms(self.clusters_tmp.copy()) self.updateManualClusterList(self.clusters_tmp.copy()) - self.plotDetectionResult() - self.plotPcaResult() + # self.plotDetectionResult() + # self.plotPcaResult() def PCAManualResetButton(self): self.subwindow_pca_manual.widget().reset() From 1b4e3ba79ef798e13a29f956c5979e3779e06088 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Sun, 21 May 2023 19:04:47 +0330 Subject: [PATCH 17/31] bug fixed --- ross_backend/resources/data.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ross_backend/resources/data.py b/ross_backend/resources/data.py index 95ac882..accfb92 100644 --- a/ross_backend/resources/data.py +++ b/ross_backend/resources/data.py @@ -76,11 +76,6 @@ def post(self): if raw: raw.data = raw_data_path raw.mode = mode - try: - raw.save_to_db() - except Exception as e: - return {"message": str(e)}, 500 - return "Success", 201 else: raw = RawModel(user_id, data=raw_data_path, project_id=project_id, mode=mode) From 2fcfca7b0e80abde7e840751fdd4a201e2ed3d82 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Sun, 21 May 2023 19:25:51 +0330 Subject: [PATCH 18/31] server file dialog buttons connected --- ross_ui/controller/serverFileDialog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ross_ui/controller/serverFileDialog.py b/ross_ui/controller/serverFileDialog.py index 747590f..95cedfd 100644 --- a/ross_ui/controller/serverFileDialog.py +++ b/ross_ui/controller/serverFileDialog.py @@ -13,6 +13,8 @@ def __init__(self, api: API): self.root = None self.list_folder.itemDoubleClicked.connect(self.itemDoubleClicked) + self.push_open.clicked.connect(self.pushOpenClicked) + self.push_cancel.clicked.connect(self.reject) self.request_dir() @@ -49,6 +51,13 @@ def itemDoubleClicked(self, item: QtWidgets.QListWidgetItem): varname=self.line_varname.text()) if ret['stat']: QtWidgets.QMessageBox.information(self, 'Info', 'File successfully added.') - super().accept() + self.accept() else: QtWidgets.QMessageBox.critical(self, 'Error', ret['message']) + + def pushOpenClicked(self): + try: + self.itemDoubleClicked(self.list_folder.selectedItems()[0]) + except IndexError: + pass + From a3a65a890559b74e86fe3fada93234968e85bc44 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Sun, 21 May 2023 20:14:17 +0330 Subject: [PATCH 19/31] resort bug fixed --- ross_backend/resources/funcs/sorting.py | 7 ++-- ross_backend/resources/funcs/t_sorting.py | 27 +++------------ ross_ui/controller/mainWindow.py | 41 +++-------------------- 3 files changed, 13 insertions(+), 62 deletions(-) diff --git a/ross_backend/resources/funcs/sorting.py b/ross_backend/resources/funcs/sorting.py index ec44f16..480c60a 100644 --- a/ross_backend/resources/funcs/sorting.py +++ b/ross_backend/resources/funcs/sorting.py @@ -1,6 +1,5 @@ import pickle - -import numpy as np +from typing import Iterable from models.config import ConfigSortModel from models.data import DetectResultModel @@ -11,7 +10,7 @@ from resources.funcs.t_sorting import * -def create_cluster_time_vec(spike_time: np.ndarray, clusters: list, config: dict): +def create_cluster_time_vec(spike_time: np.ndarray, clusters, config: dict): cluster_time_vec = np.zeros(spike_time[-1] + config['post_thr'], dtype=np.int8) for i, t in enumerate(spike_time): cluster_time_vec[t - config['pre_thr']: t + config['post_thr']] = clusters[i]+1 @@ -107,4 +106,4 @@ def startReSorting(project_id, clusters, selected_clusters): clusters = np.array(clusters) clusters[np.isin(clusters, selected_clusters)] = optimal_set + np.max(clusters) + 1 - return clusters.tolist(), create_cluster_time_vec(spike_time=d['spikeTime'], clusters=optimal_set, config=config_det) + return clusters.tolist(), create_cluster_time_vec(spike_time=d['spikeTime'], clusters=clusters, config=config_det) diff --git a/ross_backend/resources/funcs/t_sorting.py b/ross_backend/resources/funcs/t_sorting.py index 669b606..c86d7f4 100644 --- a/ross_backend/resources/funcs/t_sorting.py +++ b/ross_backend/resources/funcs/t_sorting.py @@ -8,7 +8,6 @@ def t_dist_sorter(alignedSpikeMat, sdd): out = dict() - print('t-sorting started!') g_max = sdd.g_max g_min = sdd.g_min @@ -21,8 +20,6 @@ def t_dist_sorter(alignedSpikeMat, sdd): u_limit = sdd.u_lim # Initialization - print('-*-' * 20) - print('Initialization Started...') nrow = lambda x: x.shape[0] ncol = lambda x: x.shape[1] n_feat = ncol(SpikeMat) @@ -39,18 +36,12 @@ def t_dist_sorter(alignedSpikeMat, sdd): delta_L = 100 delta_v = 100 max_iter = sdd.max_iter - print('...Initialization Done!') # FCM - print('-*-' * 20) - print('FCM started...') mu, U, _ = FCM(SpikeMat, g, [2, 20, 1, 0]) - print('...FCM Done!') # Estimate starting point for Sigma and Pi from simple clustering method # performed before - print('-*-' * 20) - print('Estimating Sigma and Pi...') rep = np.reshape(np.tile(np.expand_dims(np.arange(g), 1), (1, n_spike)), (g * n_spike, 1)) rep = np.squeeze(rep) rep_data = np.tile(SpikeMat, (g, 1)) @@ -69,18 +60,11 @@ def t_dist_sorter(alignedSpikeMat, sdd): v_old = v Ltt = [] - print('...Estimation Done!') - # Running clustering algorithm for g in [g_max, ...,g_min] - print('-*-' * 20) - print('Clustering Started...') while g >= g_min: itr = 0 # EM - while ((delta_L > delta_L_limit) or (delta_v > delta_v_limit)) and itr < max_iter: - # print('g = ', g) - # print('Pi = ', Pi) - # print('#' * 5) + while ((delta_L > delta_L_limit) or (delta_v > delta_v_limit)) and itr < max_iter and len(Pi) > 1: n_sigma = Sigma.shape[2] detSigma = np.zeros((1, n_sigma)) rep = np.reshape(np.tile(np.expand_dims(np.arange(g), 1), (1, n_spike)), (g * n_spike, 1)) @@ -174,7 +158,6 @@ def t_dist_sorter(alignedSpikeMat, sdd): if n_sigma > 1: P = c * np.exp(-(v + n_feat) * np.log(1 + M / v) / 2) @ np.diag(np.squeeze(detSigma)) else: - print("we are here") P = c * np.exp(-(v + n_feat) * np.log(1 + M / v) / 2) @ np.diag(detSigma[0]) # P = c * np.exp(-(v + n_feat) * np.log(1 + M / v) / 2) @ np.diag(np.squeeze(detSigma)) @@ -200,12 +183,14 @@ def t_dist_sorter(alignedSpikeMat, sdd): indx_remove = np.squeeze((Pi == 0)) mu = mu[np.logical_not(indx_remove)] + if len(mu.shape) == 3: + mu = mu[0] Sigma = Sigma[:, :, indx_remove == False] if len(Sigma.shape) > 3: Sigma = Sigma[:, :, :, 0] Pi = Pi[indx_remove == False] - if len(Pi.shape) > 2: - Pi = Pi[:, :, 0] + if len(Pi.shape) > 1: + Pi = Pi[:, 0] # Pi = np.array([d for (d, remove) in zip(Pi, indx_remove) if not remove]) z = z[:, indx_remove == False] if len(z.shape) > 2: @@ -233,8 +218,6 @@ def t_dist_sorter(alignedSpikeMat, sdd): # print('P shape : ', P.shape) # print('delta distance shape : ', delta_distance.shape) - print('...Em Done!') - if L > L_max: L_max = L out['cluster_index'] = np.argmax(z, axis=1) diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index e3d70bd..e37b202 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -494,15 +494,6 @@ def manualResort(self, selected_clusters): if res['stat']: self.statusBar().showMessage(self.tr("Manual ReSorting Done."), 2500) self.clusters_tmp = np.array(res['clusters']) - - self.UpdatedClusterIndex() - self.statusBar().showMessage(self.tr("Clusters Waveforms Updated..."), 2500) - self.wait() - self.updateplotWaveForms(self.clusters_tmp) - self.wait() - self.update_plotRaw(self.clusters_tmp) - self.wait() - self.updateManualClusterList(self.clusters_tmp) else: self.statusBar().showMessage(self.tr("Manual ReSorting got error")) @@ -645,25 +636,15 @@ def updateplotWaveForms(self, clusters_=None): for i in range(self.number_of_clusters): avg = np.average(spike_clustered[i], axis=0) color = self.colors[i] - # selected_spike = spike_clustered[i][np.sum(np.power(spike_clustered[i] - avg, 2), axis=1) < - # np.sum(np.power(avg, 2)) * 0.2] - # if self.saveManualFlag or self.plotManualFlag: if len(spike_clustered[i]) > 100: ind = np.arange(spike_clustered[i].shape[0]) np.random.shuffle(ind) spike = spike_clustered[i][ind[:100], :] else: spike = spike_clustered[i] - - # else: - # if len(selected_spike) > 100: - # ind = np.arange(selected_spike.shape[0]) - # np.random.shuffle(ind) - # spike = selected_spike[ind[:100], :] - # else: - # spike = selected_spike - + if spike.shape[0] == 0: + continue x = np.empty(np.shape(spike)) x[:] = np.arange(np.shape(spike)[1])[np.newaxis] @@ -1051,14 +1032,12 @@ def onActManualSorting(self): self.pca_manual = "Remove" self.OnPcaRemove() self.plotHistFlag = True - except: print(traceback.format_exc()) elif act.text() == "PCA Group": try: self.pca_manual = "Group" self.OnPcaRemove() - except: print(traceback.format_exc()) @@ -1069,6 +1048,7 @@ def onActManualSorting(self): except: print("an error accrued in manual resort") print(traceback.format_exc()) + self.UpdatedClusterIndex() if self.autoPlotManualCheck.isChecked(): self.onPlotManualSorting() except: @@ -1172,8 +1152,6 @@ def mergeManual(self, selected_clusters): for ind in selected_clusters: self.clusters_tmp[self.clusters_tmp == ind] = sel_cl - self.UpdatedClusterIndex() - self.statusBar().showMessage(self.tr("...Merging Done!"), 2000) self.wait() else: @@ -1185,7 +1163,6 @@ def removeManual(self, selected_clusters): self.wait() for sel_cl in selected_clusters: self.clusters_tmp[self.clusters_tmp == sel_cl] = - 1 - self.UpdatedClusterIndex() self.statusBar().showMessage(self.tr("...Removing Done!"), 2000) else: self.statusBar().showMessage(self.tr("For Removing you should select at least one clusters..."), 2000) @@ -1206,16 +1183,11 @@ def assignManual(self): # pass try: self.listSourceWidget.clear() + self.listTargetsWidget.clear() for i in range(n_clusters): item = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) self.listSourceWidget.addItem(item) - self.listSourceWidget.setCurrentItem(item) - - self.listTargetsWidget.clear() - for i in range(n_clusters): - item_target = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) - self.listTargetsWidget.addItem(item_target) - self.listTargetsWidget.setCurrentItem(item_target) + self.listTargetsWidget.addItem(item) except: print(traceback.format_exc()) @@ -1243,8 +1215,6 @@ def onAssignManualSorting(self): indices = nbrs.kneighbors(source_spikes, return_distance=False) self.clusters_tmp[source_ind] = np.array(target_clusters)[indices.squeeze()] - self.UpdatedClusterIndex() - self.statusBar().showMessage(self.tr("...Assigning to Nearest Clusters Done!"), 2000) self.wait() @@ -1333,7 +1303,6 @@ def PCAManualDoneButton(self): else: print("pca manual flag is not define") - self.UpdatedClusterIndex() self.subwindow_pca_manual.widget().reset() self.subwindow_pca_manual.setVisible(False) self.updateplotWaveForms(self.clusters_tmp.copy()) From e9f8bad4177f2e1c20473130c337e7c76e6efc9e Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Mon, 22 May 2023 19:09:06 +0330 Subject: [PATCH 20/31] manual sorting bug fixed --- ross_backend/resources/funcs/t_sorting.py | 2 - ross_ui/controller/exportResults.py | 63 ++++++ ross_ui/controller/mainWindow.py | 255 +++++++++++----------- ross_ui/controller/plot_curve.py | 9 - ross_ui/view/exportResults.py | 39 ++++ 5 files changed, 233 insertions(+), 135 deletions(-) create mode 100644 ross_ui/controller/exportResults.py create mode 100644 ross_ui/view/exportResults.py diff --git a/ross_backend/resources/funcs/t_sorting.py b/ross_backend/resources/funcs/t_sorting.py index c86d7f4..287ae00 100644 --- a/ross_backend/resources/funcs/t_sorting.py +++ b/ross_backend/resources/funcs/t_sorting.py @@ -221,8 +221,6 @@ def t_dist_sorter(alignedSpikeMat, sdd): if L > L_max: L_max = L out['cluster_index'] = np.argmax(z, axis=1) - - out['set.u'] = u else: break diff --git a/ross_ui/controller/exportResults.py b/ross_ui/controller/exportResults.py new file mode 100644 index 0000000..95cedfd --- /dev/null +++ b/ross_ui/controller/exportResults.py @@ -0,0 +1,63 @@ +import os + +from PyQt5 import QtWidgets + +from model.api import API +from view.serverFileDialog import ServerFileDialog + + +class ServerFileDialogApp(ServerFileDialog): + def __init__(self, api: API): + super(ServerFileDialogApp, self).__init__() + self.api = api + self.root = None + + self.list_folder.itemDoubleClicked.connect(self.itemDoubleClicked) + self.push_open.clicked.connect(self.pushOpenClicked) + self.push_cancel.clicked.connect(self.reject) + + self.request_dir() + + def request_dir(self): + self.list_folder.clear() + dir_dict = self.api.browse(self.root) + if dir_dict is not None: + self.line_address.setText(dir_dict['root']) + + item = QtWidgets.QListWidgetItem('..') + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) + self.list_folder.addItem(item) + + for folder_name in dir_dict['folders']: + item = QtWidgets.QListWidgetItem(folder_name) + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) + self.list_folder.addItem(item) + for filename in dir_dict['files']: + item = QtWidgets.QListWidgetItem(filename) + item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_FileIcon)) + self.list_folder.addItem(item) + else: + QtWidgets.QMessageBox.critical(self, 'Error', 'Server Error') + + def itemDoubleClicked(self, item: QtWidgets.QListWidgetItem): + name = item.text() + isfolder = item.icon().name() == 'folder' + if isfolder: + self.root = os.path.join(self.line_address.text(), name) + self.request_dir() + else: + ret = self.api.post_raw_data(raw_data_path=os.path.join(self.line_address.text(), name), + mode=1, + varname=self.line_varname.text()) + if ret['stat']: + QtWidgets.QMessageBox.information(self, 'Info', 'File successfully added.') + self.accept() + else: + QtWidgets.QMessageBox.critical(self, 'Error', ret['message']) + + def pushOpenClicked(self): + try: + self.itemDoubleClicked(self.list_folder.selectedItems()[0]) + except IndexError: + pass + diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index e37b202..e4d4743 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -44,7 +44,9 @@ def __init__(self): self.setWindowIcon(QtGui.QIcon('view/icons/ross.png')) # initial values for software options - self.plotHistFlag = False + # self.plotHistFlag = False + self.plotFlagHist = False + self.plotFlagClusterBased = False self.pca_manual = None self.image = None self.pca_spikes = None @@ -53,7 +55,7 @@ def __init__(self): self.clusters_init = None self.clusters = None self.inds = None - self.colors = self.distin_color(127) + self.colors = self.distinctColors(127) self.url = 'http://localhost:5000' @@ -77,11 +79,13 @@ def __init__(self): self.undoManualFlag = False self.tempList = [] + self.processEvents = QtWidgets.QApplication.processEvents + self.startDetection.pressed.connect(self.onDetect) self.startSorting.pressed.connect(self.onSort) self.plotButton.pressed.connect(self.Plot3d) self.actManual.pressed.connect(self.onActManualSorting) - self.plotManual.pressed.connect(self.onPlotManualSorting) + self.plotManual.pressed.connect(self.updateFigures) self.undoManual.pressed.connect(self.onUndoManualSorting) self.resetManual.pressed.connect(self.onResetManualSorting) self.saveManual.pressed.connect(self.onSaveManualSorting) @@ -160,7 +164,7 @@ def onImportRaw(self): raise FileNotFoundError(filename) self.statusBar().showMessage(self.tr("Loading...")) - self.wait() + self.processEvents() file_extension = os.path.splitext(filename)[-1] if file_extension == '.mat': file_raw = sio.loadmat(filename) @@ -227,10 +231,10 @@ def onImportRaw(self): self.refreshAct.setEnabled(True) self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500) - self.wait() + self.processEvents() self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() + self.processEvents() self.plotRaw() self.resetOnImportVars() @@ -241,14 +245,14 @@ def onImportRaw(self): if self.user: self.statusBar().showMessage(self.tr("Uploading to server...")) - self.wait() + self.processEvents() res = self.user.post_raw_data(address) if res['stat']: self.statusBar().showMessage(self.tr("Uploaded"), 2500) - self.wait() + self.processEvents() else: - self.wait() + self.processEvents() def onImportDetected(self): pass @@ -362,10 +366,10 @@ def open_file_dialog_server(self): if dialog.exec_() == QtWidgets.QDialog.Accepted: self.refreshAct.setEnabled(True) self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500) - self.wait() + self.processEvents() self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() + self.processEvents() self.plotRaw() self.resetOnImportVars() @@ -402,7 +406,7 @@ def open_project_dialog(self, projects): if dialog.exec_() == QtWidgets.QDialog.Accepted: self.current_project = dialog.comboBox.currentText() self.statusBar().showMessage(self.tr("Loading project...")) - self.wait() + self.processEvents() self.user.load_project(self.current_project) self.loadDefaultProject() self.statusBar().showMessage(self.tr("Loaded."), 2500) @@ -454,7 +458,7 @@ def onSaveAs(self): def onSave(self): self.statusBar().showMessage(self.tr("Saving...")) - self.wait() + self.processEvents() res = self.user.save_project(self.current_project) if res['stat']: self.statusBar().showMessage(self.tr("Saved."), 2500) @@ -468,7 +472,7 @@ def onDetect(self): return config_detect = self.read_config_detect() self.statusBar().showMessage(self.tr("Detection Started...")) - self.wait() + self.processEvents() res = self.user.start_detection(config_detect) if res['stat']: @@ -480,15 +484,15 @@ def onDetect(self): self.pca_spikes = res['pca_spikes'] self.inds = res['inds'] self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() - self.plotDetectionResult() - self.plotPcaResult() + self.processEvents() + self.plotHistogramPCA() + self.plotPCA2D() self.plotWaveForms() def manualResort(self, selected_clusters): config_sort = self.read_config_sort() self.statusBar().showMessage(self.tr("Manual ReSorting Started...")) - self.wait() + self.processEvents() res = self.user.start_Resorting(config_sort, self.clusters_tmp, selected_clusters) if res['stat']: @@ -503,7 +507,7 @@ def onSort(self): return config_sort = self.read_config_sort() self.statusBar().showMessage(self.tr("Sorting Started...")) - self.wait() + self.processEvents() res = self.user.start_sorting(config_sort) if res['stat']: self.statusBar().showMessage(self.tr("Sorting Done."), 2500) @@ -514,14 +518,14 @@ def onSort(self): self.clusters_init = self.clusters.copy() self.clusters_tmp = self.clusters.copy() self.statusBar().showMessage(self.tr("Clusters Waveforms..."), 2500) - self.wait() + self.processEvents() self.updateplotWaveForms(self.clusters) - self.wait() - self.update_plotRaw() - self.wait() + self.processEvents() + self.updatePlotRaw() + self.processEvents() self.updateManualClusterList(self.clusters_tmp) - self.plotDetectionResult() - self.plotPcaResult() + self.plotHistogramPCA() + self.plotPCA2D() else: self.statusBar().showMessage(self.tr("Sorting got error")) @@ -536,7 +540,7 @@ def plotRaw(self): self.widget_raw.showGrid(x=True, y=True) self.widget_raw.setMouseEnabled(y=False) - def update_plotRaw(self): + def updatePlotRaw(self): curve = HDF5Plot() curve.setAPI(self.user) curve.setHDF5(self.raw) @@ -599,7 +603,7 @@ def hsv_to_rgb(self, h, s, v): if i == 4: return (t, p, v) if i == 5: return (v, p, q) - def distin_color(self, number_of_colors): + def distinctColors(self, number_of_colors): colors = [] golden_ratio_conjugate = 0.618033988749895 # h = np.random.rand(1)[0] @@ -771,11 +775,14 @@ def Plot3d(self): def close3D(self): self.subwindow_3d.setVisible(False) - def plotDetectionResult(self): + def plotHistogramPCA(self): try: - self.plot_histogram_pca.clear() - pca_spikes = self.pca_spikes + if self.clusters_tmp is None: + pca_spikes = self.pca_spikes + else: + pca_spikes = self.pca_spikes[self.clusters_tmp != -1] + hist, xedges, yedges = np.histogram2d(pca_spikes[:, 0], pca_spikes[:, 1], bins=512) x_range = xedges[-1] - xedges[0] @@ -808,7 +815,7 @@ def plotDetectionResult(self): except: print(traceback.format_exc()) - def plotPcaResult(self): + def plotPCA2D(self): self.plot_clusters_pca.clear() if self.clusters_tmp is not None: @@ -832,8 +839,7 @@ def plotPcaResult(self): else: un = np.unique(clusters_) n_clusters = len(un[un >= 0]) - print("clusters", n_clusters) - new_colors = self.distin_color(n_clusters) + new_colors = self.distinctColors(n_clusters) for i in range(n_clusters): xx = x_data[clusters_ == i] yy = y_data[clusters_ == i] @@ -967,8 +973,7 @@ def onWatchdogEvent(self): """Perform checks in regular intervals.""" self.mdiArea.checkTimestamps() - def wait(self): - QtWidgets.QApplication.processEvents() + def UpdatedClusterIndex(self): cl_ind = np.unique(self.clusters_tmp) @@ -990,7 +995,7 @@ def updateManualClusterList(self, cluster_temp): self.listWidget.clear() # n_clusters = np.shape(np.unique(temp))[0] n_clusters = len(np.unique(cluster_temp)[np.unique(cluster_temp) >= 0]) - colors = self.distin_color(n_clusters) + colors = self.distinctColors(n_clusters) for i in range(n_clusters): item = QtWidgets.QListWidgetItem("Cluster {} ({:4.2f} %)".format(i + 1, (cluster_temp == i).mean() * 100)) pixmap = QPixmap(50, 50) @@ -1008,67 +1013,76 @@ def onActManualSorting(self): if act.text() == 'Merge': try: self.mergeManual(selected_clusters) - self.updateManualClusterList(self.clusters_tmp.copy()) + self.plotFlagClusterBased = True except: - print("an error accrued in manual Merge") print(traceback.format_exc()) elif act.text() == 'Remove': try: self.removeManual(selected_clusters) - self.updateManualClusterList(self.clusters_tmp.copy()) - self.plotHistFlag = True + self.plotFlagHist = True + self.plotFlagClusterBased = True except: print("an error accrued in manual Remove") print(traceback.format_exc()) + elif act.text() == "Resort": + try: + self.manualResort(selected_clusters) + self.plotFlagHist = True + self.plotFlagClusterBased = True + except: + print("an error accrued in manual resort") + print(traceback.format_exc()) + elif act.text() == 'Assign to nearest': try: self.assignManual() - except: print("an error accrued in manual Assign to nearest") print(traceback.format_exc()) elif act.text() == "PCA Remove": try: self.pca_manual = "Remove" - self.OnPcaRemove() - self.plotHistFlag = True + self.OnPcaManualAct() except: print(traceback.format_exc()) elif act.text() == "PCA Group": try: self.pca_manual = "Group" - self.OnPcaRemove() + self.OnPcaManualAct() except: print(traceback.format_exc()) - elif act.text() == "Resort": - try: - self.manualResort(selected_clusters) - self.updateManualClusterList(self.clusters_tmp.copy()) - except: - print("an error accrued in manual resort") - print(traceback.format_exc()) + self.UpdatedClusterIndex() + self.updateManualClusterList(self.clusters_tmp) if self.autoPlotManualCheck.isChecked(): - self.onPlotManualSorting() + self.updateFigures() except: print(traceback.format_exc()) self.statusBar().showMessage(self.tr("an error accrued in manual act !"), 2000) - def onPlotManualSorting(self): - # update 2d pca plot - self.plotPcaResult() - if self.plotHistFlag: - self.plotDetectionResult() - self.plotHistFlag = False - - if self.plotManualFlag: - self.updateplotWaveForms(self.clusters_tmp.copy()) - self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) - self.wait() - self.update_plotRaw() - self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) - self.plotManualFlag = False + # def onPlotManualSorting(self): + # # update 2d pca plot + # self.plotPCA2D() + # if self.plotHistFlag: + # self.plotHistogramPCA() + # self.plotHistFlag = False + # + # if self.plotManualFlag: + # self.updateplotWaveForms(self.clusters_tmp.copy()) + # self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) + # self.wait() + # self.update_plotRaw() + # self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) + # self.plotManualFlag = False + + def updateFigures(self): + if self.plotFlagHist: + self.plotHistogramPCA() + if self.plotFlagClusterBased: + self.plotPCA2D() + self.updatePlotRaw() + self.updateplotWaveForms() def onResetManualSorting(self): @@ -1076,16 +1090,16 @@ def onResetManualSorting(self): self.tempList = [] # update 2d pca plot - self.plotDetectionResult() - self.plotPcaResult() + self.plotHistogramPCA() + self.plotPCA2D() # update cluster list - self.updateManualClusterList(self.clusters_tmp.copy()) + self.updateManualClusterList(self.clusters_tmp) self.updateplotWaveForms(self.clusters_init.copy()) self.statusBar().showMessage(self.tr("Resetting Spikes Waveforms...")) - self.wait() + self.processEvents() - self.update_plotRaw() + self.updatePlotRaw() self.statusBar().showMessage(self.tr("Resetting Raw Data Waveforms..."), 2000) self.resetManualFlag = False @@ -1097,20 +1111,18 @@ def onSaveManualSorting(self): self.clusters = self.clusters_tmp.copy() # self.number_of_clusters = np.shape(np.unique(self.clusters))[0] self.statusBar().showMessage(self.tr("Save Clustering Results...")) - self.wait() + self.processEvents() res = self.user.save_sort_results(self.clusters) if res['stat']: - self.wait() + self.processEvents() self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) self.updateplotWaveForms(self.clusters) - self.wait() + self.processEvents() self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) - self.update_plotRaw() - self.wait() + self.updatePlotRaw() + self.processEvents() self.statusBar().showMessage(self.tr("Saving Done.")) - self.updateManualClusterList(self.clusters_tmp) - # self.updateManualSortingView() else: self.statusBar().showMessage(self.tr("An error occurred in saving!..."), 2000) @@ -1129,16 +1141,16 @@ def onUndoManualSorting(self): # update 2d pca plot if self.autoPlotManualCheck.isChecked(): - self.plotDetectionResult() - self.plotPcaResult() + self.plotHistogramPCA() + self.plotPCA2D() self.updateplotWaveForms(self.clusters_tmp.copy()) self.statusBar().showMessage(self.tr("Undoing Spikes Waveforms..."), 2000) - self.wait() + self.processEvents() self.statusBar().showMessage(self.tr("Undoing Raw Data Waveforms..."), 2000) - self.update_plotRaw(self.clusters_tmp.copy()) + self.updatePlotRaw(self.clusters_tmp.copy()) self.statusBar().showMessage(self.tr("Undoing Done!"), 2000) - self.wait() + self.processEvents() except: self.statusBar().showMessage(self.tr("There is no manual act for undoing!"), 2000) @@ -1146,21 +1158,21 @@ def onUndoManualSorting(self): def mergeManual(self, selected_clusters): if len(selected_clusters) >= 2: self.statusBar().showMessage(self.tr("Merging...")) - self.wait() + self.processEvents() sel_cl = selected_clusters[0] for ind in selected_clusters: self.clusters_tmp[self.clusters_tmp == ind] = sel_cl self.statusBar().showMessage(self.tr("...Merging Done!"), 2000) - self.wait() + self.processEvents() else: self.statusBar().showMessage(self.tr("For Merging you should select at least two clusters..."), 2000) def removeManual(self, selected_clusters): if len(selected_clusters) != 0: self.statusBar().showMessage(self.tr("Removing...")) - self.wait() + self.processEvents() for sel_cl in selected_clusters: self.clusters_tmp[self.clusters_tmp == sel_cl] = - 1 self.statusBar().showMessage(self.tr("...Removing Done!"), 2000) @@ -1169,25 +1181,17 @@ def removeManual(self, selected_clusters): def assignManual(self): self.subwindow_assign.setVisible(True) - try: - n_clusters = self.number_of_clusters - except: - n_clusters = 0 - # try: - # self.listSourceWidget.close() - # self.listTargetsWidget.close() - # self.assign_button.close() - # self.assign_close_button.close() - # except: - # pass + n_clusters = len(np.unique(self.clusters_tmp)) + try: self.listSourceWidget.clear() self.listTargetsWidget.clear() for i in range(n_clusters): - item = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) - self.listSourceWidget.addItem(item) - self.listTargetsWidget.addItem(item) + item1 = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) + item2 = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) + self.listSourceWidget.addItem(item1) + self.listTargetsWidget.addItem(item2) except: print(traceback.format_exc()) @@ -1202,7 +1206,7 @@ def onAssignManualSorting(self): try: if len(target_clusters) >= 1: self.statusBar().showMessage(self.tr("Assigning Source Cluster to Targets...")) - self.wait() + self.processEvents() source_spikes = self.pca_spikes[self.clusters_tmp == source_cluster] source_ind = np.nonzero(self.clusters_tmp == source_cluster) @@ -1216,7 +1220,7 @@ def onAssignManualSorting(self): self.clusters_tmp[source_ind] = np.array(target_clusters)[indices.squeeze()] self.statusBar().showMessage(self.tr("...Assigning to Nearest Clusters Done!"), 2000) - self.wait() + self.processEvents() self.listSourceWidget.clear() self.listTargetsWidget.clear() @@ -1224,21 +1228,24 @@ def onAssignManualSorting(self): # for i in range(len(np.unique(self.clusters_tmp))): cu = np.unique(self.clusters_tmp) for i in range(len(cu[cu != -1])): - item = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) - self.listSourceWidget.addItem(item) - self.listTargetsWidget.addItem(item) - + item1 = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) + item2 = QtWidgets.QListWidgetItem("Cluster %i" % (i + 1)) + self.listSourceWidget.addItem(item1) + self.listTargetsWidget.addItem(item2) + self.UpdatedClusterIndex() + self.updateManualClusterList(self.clusters_tmp) + if self.autoPlotManualCheck.isChecked(): + self.plotFlagClusterBased = True + self.updateFigures() else: self.statusBar().showMessage(self.tr("You Should Choose One Source and at least One Target..."), 2000) - - self.updateManualClusterList(self.clusters_tmp) except: print(traceback.format_exc()) def closeAssign(self): self.subwindow_assign.setVisible(False) - def OnPcaRemove(self): + def OnPcaManualAct(self): self.pca_spikes = self.pca_spikes hist, xedges, yedges = np.histogram2d(self.pca_spikes[:, 0], self.pca_spikes[:, 1], bins=512) @@ -1284,12 +1291,15 @@ def PCAManualDoneButton(self): poly = Polygon(norm_points) if self.pca_manual == "Remove": + self.plotFlagClusterBased = True + self.plotFlagHist = True for i in range(len(self.pca_spikes)): p1 = Point(self.pca_spikes[i]) if not (p1.within(poly)): self.clusters_tmp[i] = -1 elif self.pca_manual == "Group": + self.plotFlagClusterBased = True clusters = self.clusters_tmp.copy() un = np.unique(clusters[clusters != -1]) num_of_clusters = len(un[un >= 0]) @@ -1305,10 +1315,11 @@ def PCAManualDoneButton(self): self.subwindow_pca_manual.widget().reset() self.subwindow_pca_manual.setVisible(False) - self.updateplotWaveForms(self.clusters_tmp.copy()) - self.updateManualClusterList(self.clusters_tmp.copy()) - # self.plotDetectionResult() - # self.plotPcaResult() + + self.UpdatedClusterIndex() + self.updateManualClusterList(self.clusters_tmp) + if self.autoPlotManualCheck.isChecked(): + self.updateFigures() def PCAManualResetButton(self): self.subwindow_pca_manual.widget().reset() @@ -1333,7 +1344,6 @@ def PCAManualCloseButton(self): def loadDefaultProject(self): flag_raw = False flag_update_raw = False - SERVER_MODE = False res = self.user.get_config_detect() if res['stat']: @@ -1351,9 +1361,7 @@ def loadDefaultProject(self): self.raw = new_data self.statusBar().showMessage(self.tr("Plotting..."), 2500) - self.wait() - else: - SERVER_MODE = True + self.processEvents() flag_raw = True res = self.user.get_detection_result() @@ -1362,9 +1370,8 @@ def loadDefaultProject(self): self.spike_time = res['spike_time'] self.pca_spikes = res['pca_spikes'] self.inds = res['inds'] - self.plotWaveForms() - self.plotDetectionResult() - self.plotPcaResult() + self.plotFlagHist = True + # done res = self.user.get_sorting_result() @@ -1373,18 +1380,18 @@ def loadDefaultProject(self): self.cluster_time_vec = res['cluster_time_vec'] self.clusters = self.clusters_init.copy() self.clusters_tmp = self.clusters_init.copy() - self.updateplotWaveForms(self.clusters_init.copy()) - self.wait() flag_update_raw = True self.updateManualClusterList(self.clusters_tmp) - self.plotPcaResult() + self.plotFlagClusterBased = True if flag_raw: if flag_update_raw: - self.update_plotRaw() + self.updatePlotRaw() else: self.plotRaw() + self.updateFigures() + def read_config_detect(self): config = dict() config['filter_type'] = self.filterType.currentText() diff --git a/ross_ui/controller/plot_curve.py b/ross_ui/controller/plot_curve.py index 4e3d367..371443d 100644 --- a/ross_ui/controller/plot_curve.py +++ b/ross_ui/controller/plot_curve.py @@ -13,7 +13,6 @@ def viewRangeChanged(self): self.updatePlot() def updatePlot(self): - print('in update plot') vb = self.getViewBox() if vb is None: xstart = 0 @@ -23,33 +22,25 @@ def updatePlot(self): xend = int(vb.viewRange()[0][1]) + 1 if self.curve.end <= xend and self.curve.start >= xstart: - print('here 1') visible = self.curve.pos_y - print(len(visible)) self.setData(visible) self.setPos(self.curve.start, 0) self.setPen(self.curve.pen) elif self.curve.end <= xend and self.curve.start < xstart: - print('here 2') visible = self.curve.pos_y[xstart:] - print(len(visible)) self.setData(visible) self.setPos(xstart, 0) self.setPen(self.curve.pen) elif self.curve.end > xend and self.curve.start >= xstart: - print('here 3') visible = self.curve.pos_y[:xend] - print(len(visible)) self.setData(visible) self.setPos(self.curve.start, 0) self.setPen(self.curve.pen) elif self.curve.end > xend and self.curve.start < xstart: - print('here 4') visible = self.curve.pos_y[xstart: xend] - print(len(visible)) self.setData(visible) self.setPos(xstart, 0) self.setPen(self.curve.pen) diff --git a/ross_ui/view/exportResults.py b/ross_ui/view/exportResults.py new file mode 100644 index 0000000..f71aed8 --- /dev/null +++ b/ross_ui/view/exportResults.py @@ -0,0 +1,39 @@ +from PyQt5 import QtWidgets + + +class ExportView(QtWidgets.QDialog): + def __init__(self): + super().__init__() + self.setFixedSize(600, 300) + + layout_out = QtWidgets.QVBoxLayout() + groupbox = QtWidgets.QGroupBox("Export variables") + groupbox.setCheckable(True) + + vbox = QtWidgets.QVBoxLayout() + groupbox.setLayout(vbox) + + self.checkSpikeMat = QtWidgets.QCheckBox("Spike Waveforms") + self.checkSpikeTime = QtWidgets.QCheckBox("Spike Times") + self.checkClusters = QtWidgets.QCheckBox("Sorting Results (Cluster Indexes)") + + self.checkSpikeTime.setChecked(True) + self.checkClusters.setChecked(True) + + vbox.addWidget(self.checkSpikeMat) + vbox.addWidget(self.checkSpikeTime) + vbox.addWidget(self.checkClusters) + + hbox = QtWidgets.QHBoxLayout() + self.pushExport = QtWidgets.QPushButton("Export") + self.pushClose = QtWidgets.QPushButton("Close") + + hbox.addWidget(self.pushExport) + hbox.addWidget(self.pushClose) + + layout_out.addLayout(vbox) + layout_out.addLayout(hbox) + + self.setLayout(layout_out) + + self.setWindowTitle("Export Dialog") From d822ec6c7e831adc9cdbb98687f61bf2050b2ec6 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 23 May 2023 12:12:49 +0330 Subject: [PATCH 21/31] export modified --- .gitignore | 4 +- ross_backend/app.py | 8 +-- ross_backend/resources/detection_result.py | 35 +++++++-- ross_ui/controller/exportResults.py | 82 +++++++--------------- ross_ui/controller/mainWindow.py | 47 +++++++------ ross_ui/model/api.py | 21 ++++++ ross_ui/view/exportResults.py | 32 +++++++-- ross_ui/view/mainWindow.py | 27 ++++--- 8 files changed, 156 insertions(+), 100 deletions(-) diff --git a/.gitignore b/.gitignore index b71c9e8..0570184 100644 --- a/.gitignore +++ b/.gitignore @@ -408,4 +408,6 @@ data /ross_backend/data.db /ross_ui/ross_data/ ross_backend/ross_data/ -ross_ui/.tmp \ No newline at end of file +ross_ui/.tmp +*.mat +*.pickle \ No newline at end of file diff --git a/ross_backend/app.py b/ross_backend/app.py index cd65986..4fd45f7 100644 --- a/ross_backend/app.py +++ b/ross_backend/app.py @@ -12,7 +12,7 @@ from resources.data import RawDataDefault from resources.detect import DetectDefault from resources.sorting_result import SortingResultDefault -from resources.detection_result import DetectionResultDefault +from resources.detection_result import DetectionResult, DetectionResultSpikeMat from resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout from resources.browse import Browse @@ -88,14 +88,12 @@ def revoked_token_callback(): api.add_resource(UserLogout, '/logout') api.add_resource(User, '/user/') -# api.add_resource(RawData, '/raw/') api.add_resource(RawDataDefault, '/raw') api.add_resource(DetectDefault, '/detect') -# api.add_resource(Detect, '/detect/') api.add_resource(SortDefault, '/sort') -# api.add_resource(Sort, '/sort/') -api.add_resource(DetectionResultDefault, '/detection_result') +api.add_resource(DetectionResult, '/detection_result') +api.add_resource(DetectionResultSpikeMat, '/detection_result_waveform') api.add_resource(SortingResultDefault, '/sorting_result') api.add_resource(Projects, '/projects') diff --git a/ross_backend/resources/detection_result.py b/ross_backend/resources/detection_result.py index 129bac9..f782fc8 100644 --- a/ross_backend/resources/detection_result.py +++ b/ross_backend/resources/detection_result.py @@ -5,7 +5,7 @@ import numpy as np from flask import request from flask_jwt_extended import jwt_required -from flask_restful import Resource, reqparse +from flask_restful import Resource from models.data import DetectResultModel @@ -13,9 +13,7 @@ DATA_NUM_TO_SEND = 1000 -class DetectionResultDefault(Resource): - parser = reqparse.RequestParser() - parser.add_argument('raw', type=str, required=True, help="This field cannot be left blank!") +class DetectionResult(Resource): @jwt_required def get(self): @@ -48,3 +46,32 @@ def get(self): return response return {'message': 'Detection Result Data does not exist'}, 404 + + +class DetectionResultSpikeMat(Resource): + + @jwt_required + def get(self): + project_id = int(request.json["project_id"]) + detect_result = None + if project_id in SESSION: + detect_result = SESSION[project_id] + else: + detect_result_model = DetectResultModel.find_by_project_id(project_id) + if detect_result_model: + with open(detect_result_model.data, 'rb') as f: + detect_result = pickle.load(f) + SESSION[project_id] = detect_result + + if detect_result is not None: + buffer = io.BytesIO() + np.savez_compressed(buffer, spike_mat=detect_result['spikeMat']) + buffer.seek(0) + raw_bytes = buffer.read() + buffer.close() + + response = flask.make_response(raw_bytes) + response.headers.set('Content-Type', 'application/octet-stream') + return response + + return {'message': 'Detection Result Data does not exist'}, 404 diff --git a/ross_ui/controller/exportResults.py b/ross_ui/controller/exportResults.py index 95cedfd..1be45da 100644 --- a/ross_ui/controller/exportResults.py +++ b/ross_ui/controller/exportResults.py @@ -1,63 +1,35 @@ -import os - +from scipy.io import savemat from PyQt5 import QtWidgets from model.api import API -from view.serverFileDialog import ServerFileDialog +from view.exportResults import ExportResults -class ServerFileDialogApp(ServerFileDialog): +class ExportResultsApp(ExportResults): def __init__(self, api: API): - super(ServerFileDialogApp, self).__init__() + super().__init__() self.api = api - self.root = None - - self.list_folder.itemDoubleClicked.connect(self.itemDoubleClicked) - self.push_open.clicked.connect(self.pushOpenClicked) - self.push_cancel.clicked.connect(self.reject) - - self.request_dir() - - def request_dir(self): - self.list_folder.clear() - dir_dict = self.api.browse(self.root) - if dir_dict is not None: - self.line_address.setText(dir_dict['root']) - - item = QtWidgets.QListWidgetItem('..') - item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) - self.list_folder.addItem(item) - - for folder_name in dir_dict['folders']: - item = QtWidgets.QListWidgetItem(folder_name) - item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)) - self.list_folder.addItem(item) - for filename in dir_dict['files']: - item = QtWidgets.QListWidgetItem(filename) - item.setIcon(QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_FileIcon)) - self.list_folder.addItem(item) - else: - QtWidgets.QMessageBox.critical(self, 'Error', 'Server Error') - - def itemDoubleClicked(self, item: QtWidgets.QListWidgetItem): - name = item.text() - isfolder = item.icon().name() == 'folder' - if isfolder: - self.root = os.path.join(self.line_address.text(), name) - self.request_dir() - else: - ret = self.api.post_raw_data(raw_data_path=os.path.join(self.line_address.text(), name), - mode=1, - varname=self.line_varname.text()) - if ret['stat']: - QtWidgets.QMessageBox.information(self, 'Info', 'File successfully added.') - self.accept() + self.data_dict = {} + self.type = 'mat' + + self.pushExport.clicked.connect(self.pushExportClicked) + self.pushClose.clicked.connect(self.reject) + + def pushExportClicked(self): + if self.checkSpikeMat.isChecked(): + self.labelDownload.setText('Download spike waveforms from server ...') + QtWidgets.QApplication.processEvents() + res = self.api.get_spike_mat() + if res['stat']: + self.labelDownload.setText('Download Done!') + QtWidgets.QApplication.processEvents() + self.data_dict['SpikeWaveform'] = res['spike_mat'] else: - QtWidgets.QMessageBox.critical(self, 'Error', ret['message']) - - def pushOpenClicked(self): - try: - self.itemDoubleClicked(self.list_folder.selectedItems()[0]) - except IndexError: - pass - + self.labelDownload.setText('Download Error!') + QtWidgets.QMessageBox.critical(self, 'Error in Download', res['message']) + if self.radioMat.isChecked(): + self.type = 'mat' + elif self.radioPickle.isChecked(): + self.type = 'pickle' + + self.accept() diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index e4d4743..82d53c6 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -29,6 +29,7 @@ from controller.saveAs import SaveAsApp as save_as_form from controller.serverAddress import ServerApp as server_form from controller.serverFileDialog import ServerFileDialogApp as sever_dialog +from controller.exportResults import ExportResultsApp as export_results from controller.signin import SigninApp as signin_form from view.mainWindow import MainWindow @@ -89,10 +90,12 @@ def __init__(self): self.undoManual.pressed.connect(self.onUndoManualSorting) self.resetManual.pressed.connect(self.onResetManualSorting) self.saveManual.pressed.connect(self.onSaveManualSorting) + self.saveManual.setEnabled(False) self.closeButton3d.pressed.connect(self.close3D) self.closeButton3dDet.pressed.connect(self.closeDetect3D) self.assign_close_button.pressed.connect(self.closeAssign) self.assign_button.pressed.connect(self.onAssignManualSorting) + self.exportAct.triggered.connect(self.open_export_dialog) # PCA MANUAL self.resetBottonPCAManual.clicked.connect(self.PCAManualResetButton) @@ -351,7 +354,7 @@ def open_signin_dialog(self): # self.saveAsAct.setEnabled(True) self.importMenu.setEnabled(True) self.openAct.setEnabled(True) - self.exportMenu.setEnabled(True) + self.exportAct.setEnabled(True) self.runMenu.setEnabled(True) self.visMenu.setEnabled(True) @@ -378,6 +381,26 @@ def open_file_dialog_server(self): self.plot_clusters_pca.clear() self.widget_waveform.clear() + @QtCore.pyqtSlot() + def open_export_dialog(self): + dialog = export_results(self.user) + if dialog.exec_() == QtWidgets.QDialog.Accepted: + data_dict = dialog.data_dict + if dialog.checkSpikeTime: + data_dict['SpikeTime'] = self.spike_time + if dialog.checkClusters: + data_dict['Cluster'] = self.clusters + + filename = QtWidgets.QFileDialog.getSaveFileName(self, "Select Directory", f'./ross_result.{dialog.type}')[0] + if dialog.type == 'mat': + sio.savemat(filename, data_dict) + elif dialog.type == 'pickle': + with open(filename, 'wb') as f: + pickle.dump(data_dict, f) + else: + QtWidgets.QMessageBox.critical(self, 'Error', 'Type not supported!') + QtWidgets.QMessageBox.information(self, 'Save', 'Saved Successfully!') + def open_server_dialog(self): dialog = server_form(server_text=self.url) if dialog.exec_() == QtWidgets.QDialog.Accepted: @@ -438,7 +461,7 @@ def onSignOut(self): # self.saveAct.setEnabled(False) # self.saveAsAct.setEnabled(False) self.importMenu.setEnabled(False) - self.exportMenu.setEnabled(False) + self.exportAct.setEnabled(False) self.runMenu.setEnabled(False) self.visMenu.setEnabled(False) self.widget_raw.clear() @@ -973,8 +996,6 @@ def onWatchdogEvent(self): """Perform checks in regular intervals.""" self.mdiArea.checkTimestamps() - - def UpdatedClusterIndex(self): cl_ind = np.unique(self.clusters_tmp) cnt = 0 @@ -982,6 +1003,7 @@ def UpdatedClusterIndex(self): if not ind == -1: self.clusters_tmp[self.clusters_tmp == ind] = cnt cnt += 1 + self.onSaveManualSorting() def manualPreparingSorting(self, temp): if len(self.tempList) == 10: @@ -1052,7 +1074,6 @@ def onActManualSorting(self): except: print(traceback.format_exc()) - self.UpdatedClusterIndex() self.updateManualClusterList(self.clusters_tmp) if self.autoPlotManualCheck.isChecked(): @@ -1107,27 +1128,14 @@ def onResetManualSorting(self): self.saveManualFlag = False def onSaveManualSorting(self): - # if self.saveManualFlag: self.clusters = self.clusters_tmp.copy() - # self.number_of_clusters = np.shape(np.unique(self.clusters))[0] self.statusBar().showMessage(self.tr("Save Clustering Results...")) self.processEvents() res = self.user.save_sort_results(self.clusters) - if res['stat']: - self.processEvents() - self.statusBar().showMessage(self.tr("Updating Spikes Waveforms...")) - self.updateplotWaveForms(self.clusters) - self.processEvents() - self.statusBar().showMessage(self.tr("Updating Raw Data Waveforms..."), 2000) - self.updatePlotRaw() - self.processEvents() - self.statusBar().showMessage(self.tr("Saving Done.")) - else: + if not res['stat']: self.statusBar().showMessage(self.tr("An error occurred in saving!..."), 2000) - self.saveManualFlag = False - def onUndoManualSorting(self): try: @@ -1372,7 +1380,6 @@ def loadDefaultProject(self): self.inds = res['inds'] self.plotFlagHist = True - # done res = self.user.get_sorting_result() if res['stat']: diff --git a/ross_ui/model/api.py b/ross_ui/model/api.py index 68b20bd..20ebc81 100644 --- a/ross_ui/model/api.py +++ b/ross_ui/model/api.py @@ -135,6 +135,27 @@ def get_detection_result(self): return {'stat': False, 'message': response.content} return {'stat': False, 'message': 'Not Logged In!'} + def get_spike_mat(self): + if self.access_token is not None: + response = requests.get(self.url + '/detection_result_waveform', + headers={'Authorization': 'Bearer ' + self.access_token}, + json={'project_id': self.project_id}) + + if response.ok: + b = io.BytesIO() + b.write(response.content) + b.seek(0) + d = np.load(b, allow_pickle=True) + return {'stat': True, 'spike_mat': d['spike_mat']} + + elif response.status_code == 401: + ret = self.refresh_jwt_token() + if ret: + self.get_spike_mat() + + return {'stat': False, 'message': response.content} + return {'stat': False, 'message': 'Not Logged In!'} + def get_sorting_result(self): if self.access_token is not None: response = requests.get(self.url + '/sorting_result', diff --git a/ross_ui/view/exportResults.py b/ross_ui/view/exportResults.py index f71aed8..dca3a65 100644 --- a/ross_ui/view/exportResults.py +++ b/ross_ui/view/exportResults.py @@ -1,14 +1,13 @@ from PyQt5 import QtWidgets -class ExportView(QtWidgets.QDialog): +class ExportResults(QtWidgets.QDialog): def __init__(self): super().__init__() self.setFixedSize(600, 300) layout_out = QtWidgets.QVBoxLayout() groupbox = QtWidgets.QGroupBox("Export variables") - groupbox.setCheckable(True) vbox = QtWidgets.QVBoxLayout() groupbox.setLayout(vbox) @@ -24,15 +23,34 @@ def __init__(self): vbox.addWidget(self.checkSpikeTime) vbox.addWidget(self.checkClusters) - hbox = QtWidgets.QHBoxLayout() + groupboxradio = QtWidgets.QGroupBox("Export type") + + hboxradio = QtWidgets.QHBoxLayout() + + groupboxradio.setLayout(hboxradio) + + self.radioPickle = QtWidgets.QRadioButton("pickle") + self.radioMat = QtWidgets.QRadioButton("mat") + + self.radioPickle.setChecked(True) + + hboxradio.addWidget(self.radioPickle) + hboxradio.addWidget(self.radioMat) + + hboxpush = QtWidgets.QHBoxLayout() self.pushExport = QtWidgets.QPushButton("Export") self.pushClose = QtWidgets.QPushButton("Close") - hbox.addWidget(self.pushExport) - hbox.addWidget(self.pushClose) + hboxpush.addWidget(self.pushExport) + hboxpush.addWidget(self.pushClose) + self.labelDownload = QtWidgets.QLabel() + self.progbar = QtWidgets.QProgressBar() - layout_out.addLayout(vbox) - layout_out.addLayout(hbox) + layout_out.addWidget(groupbox) + layout_out.addWidget(groupboxradio) + layout_out.addWidget(self.labelDownload) + # layout_out.addWidget(self.progbar) + layout_out.addLayout(hboxpush) self.setLayout(layout_out) diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index 25465f7..c4bbad0 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -71,6 +71,12 @@ def createActions(self): self.closeAct.setIcon(QtGui.QIcon(icon_path + "Close.png")) self.closeAct.setEnabled(False) + self.exportAct = QtWidgets.QAction(self.tr("&Export"), self) + self.exportAct.setStatusTip(self.tr("Export results")) + self.exportAct.setIcon(QtGui.QIcon(icon_path + "Export.png")) + self.exportAct.setEnabled(False) + + self.saveAct = QtWidgets.QAction(self.tr("&Save"), self) self.saveAct.setEnabled(False) self.saveAct.setShortcut(QtGui.QKeySequence.Save) @@ -340,18 +346,23 @@ def createMenubar(self): # Menu entry for file actions. self.fileMenu = self.menuBar().addMenu(self.tr("&File")) # self.fileMenu.addAction(self.openAct) - self.fileMenu.addAction(self.closeAct) - self.fileMenu.addSeparator() - self.fileMenu.addAction(self.saveAct) - self.fileMenu.addAction(self.saveAsAct) - self.fileMenu.addSeparator() + + # self.fileMenu.addSeparator() + # self.fileMenu.addAction(self.saveAct) + # self.fileMenu.addAction(self.saveAsAct) + # self.fileMenu.addSeparator() self.importMenu = self.fileMenu.addMenu(self.tr("&Import")) self.importMenu.setEnabled(False) self.importMenu.addActions( (self.importRawAct, self.importRawActServer, self.importDetectedAct, self.importSortedAct)) - self.exportMenu = self.fileMenu.addMenu(self.tr("&Export")) - self.exportMenu.setEnabled(False) - self.exportMenu.addActions((self.exportRawAct, self.exportDetectedAct, self.exportSortedAct)) + + self.fileMenu.addSeparator() + self.fileMenu.addAction(self.exportAct) + self.fileMenu.addAction(self.closeAct) + + # self.exportMenu = self.fileMenu.addMenu(self.tr("&Export")) + # self.exportMenu.setEnabled(False) + # self.exportMenu.addActions((self.exportRawAct, self.exportDetectedAct, self.exportSortedAct)) # ------------------- tool from menu ------------------ # Menu entry for tool acions From b6477681dc7c97c702582cae34aed27c462c8f02 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Tue, 23 May 2023 12:15:08 +0330 Subject: [PATCH 22/31] readme updated --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index fe2b0ca..6faa41e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the paper -entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). ( -Official Python Implementation) +entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). (Official Python Implementation) ### Important Note on ROSS v2 From ac349c466ea7f6ede6aa542958d314ee94adb894 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 08:52:08 +0330 Subject: [PATCH 23/31] raw data check removed from detection body --- ross_backend/resources/detect.py | 15 ++++++++++++- ross_backend/resources/funcs/detection.py | 27 ++--------------------- test/backend/test_detection.py | 0 3 files changed, 16 insertions(+), 26 deletions(-) create mode 100644 test/backend/test_detection.py diff --git a/ross_backend/resources/detect.py b/ross_backend/resources/detect.py index 85b0d76..8d9b4e4 100644 --- a/ross_backend/resources/detect.py +++ b/ross_backend/resources/detect.py @@ -70,7 +70,20 @@ def post(self): # if data['run_detection']: try: - spikeMat, spikeTime, pca_spikes, inds = startDetection(project_id) + raw = RawModel.find_by_project_id(project_id) + + if not raw: + return {'message': 'No raw file'}, 404 + config = ConfigDetectionModel.find_by_project_id(project_id) + if not config: + return {'message': 'Detection config does not exist'}, 404 + if not raw.data: + return {'message': 'raw file has no data'}, 404 + + with open(raw.data, 'rb') as f: + data = pickle.load(f) + + spikeMat, spikeTime, pca_spikes, inds = startDetection(data, config) data_file = {'spikeMat': spikeMat, 'spikeTime': spikeTime, 'config': config.json(), 'pca_spikes': pca_spikes, diff --git a/ross_backend/resources/funcs/detection.py b/ross_backend/resources/funcs/detection.py index 5a4bf2d..d28069f 100644 --- a/ross_backend/resources/funcs/detection.py +++ b/ross_backend/resources/funcs/detection.py @@ -10,30 +10,7 @@ from models.data import RawModel -def startDetection(project_id): - raw = RawModel.find_by_project_id(project_id) - - if not raw: - raise Exception - config = ConfigDetectionModel.find_by_project_id(project_id) - if not config: - raise Exception - if not raw.data: - raise Exception - - # print("raw.data", raw.data) - # b = io.BytesIO() - # b.write(raw.data) - # b.seek(0) - # d = np.load(b, allow_pickle=True) - # data_address = d['raw'] - - with open(raw.data, 'rb') as f: - new_data = pickle.load(f) - data = new_data - - # b.close() - +def startDetection(data, config): thr_method = config.thr_method fRp = config.pass_freq fRs = config.stop_freq @@ -127,7 +104,7 @@ def spike_detector(data, thr, pre_thresh, post_thresh, dead_time, side): indx_spikes = np.nonzero(spike_detected)[0] SpikeMat = np.zeros((len(indx_spikes), n_points_per_spike)) - SpikeTime = np.zeros((len(indx_spikes), ), dtype=np.uint32) + SpikeTime = np.zeros((len(indx_spikes),), dtype=np.uint32) # assigning SpikeMat and SpikeTime matrices for i, curr_indx in enumerate(indx_spikes): diff --git a/test/backend/test_detection.py b/test/backend/test_detection.py new file mode 100644 index 0000000..e69de29 From 9779adb10d51a1d998ddc17e61a63984f66f80a3 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 13:26:36 +0330 Subject: [PATCH 24/31] optimize imports --- README.md | 3 ++- ross_backend/models/project.py | 1 + ross_backend/resources/detect.py | 2 +- ross_backend/resources/funcs/detection.py | 4 ---- ross_backend/resources/funcs/sort_utils.py | 2 +- ross_backend/resources/funcs/sorting.py | 3 +-- ross_backend/resources/sorting_result.py | 2 +- ross_ui/controller/exportResults.py | 1 - ross_ui/controller/mainWindow.py | 5 +++-- ross_ui/controller/matplot_figures.py | 2 +- ross_ui/controller/serverFileDialog.py | 1 - ross_ui/view/mainWindow.py | 1 - ross_ui/view/signin.py | 2 +- 13 files changed, 12 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6faa41e..fe2b0ca 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the paper -entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). (Official Python Implementation) +entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). ( +Official Python Implementation) ### Important Note on ROSS v2 diff --git a/ross_backend/models/project.py b/ross_backend/models/project.py index 06c06e2..c75942a 100644 --- a/ross_backend/models/project.py +++ b/ross_backend/models/project.py @@ -7,6 +7,7 @@ class ProjectModel(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete="CASCADE")) + # config_detect = db.relationship('ConfigDetectionModel', backref='project', uselist=False, # cascade="all,delete,delete-orphan") # config_sort = db.relationship('ConfigSortModel', backref='project', uselist=False, diff --git a/ross_backend/resources/detect.py b/ross_backend/resources/detect.py index 8d9b4e4..5af8d36 100644 --- a/ross_backend/resources/detect.py +++ b/ross_backend/resources/detect.py @@ -9,8 +9,8 @@ from models.config import ConfigDetectionModel from models.data import DetectResultModel from models.data import RawModel -from resources.funcs.detection import startDetection from resources.detection_result import SESSION +from resources.funcs.detection import startDetection class DetectDefault(Resource): diff --git a/ross_backend/resources/funcs/detection.py b/ross_backend/resources/funcs/detection.py index d28069f..32ee5ed 100644 --- a/ross_backend/resources/funcs/detection.py +++ b/ross_backend/resources/funcs/detection.py @@ -1,4 +1,3 @@ -import pickle import random import numpy as np @@ -6,9 +5,6 @@ import scipy.signal import sklearn.decomposition as decom -from models.config import ConfigDetectionModel -from models.data import RawModel - def startDetection(data, config): thr_method = config.thr_method diff --git a/ross_backend/resources/funcs/sort_utils.py b/ross_backend/resources/funcs/sort_utils.py index 613a225..080bdac 100644 --- a/ross_backend/resources/funcs/sort_utils.py +++ b/ross_backend/resources/funcs/sort_utils.py @@ -141,7 +141,7 @@ def spike_alignment(spike_mat, spike_time, ss): newSpikeMat = spike_mat[:, max_shift:-max_shift] # shifting time with the amount of "max_shift" because first "max_shift" samples are ignored. - time_shift = np.zeros((n_spike, )) + max_shift + time_shift = np.zeros((n_spike,)) + max_shift # indices of neg group ind_neg_find = np.nonzero(ind_neg)[0] diff --git a/ross_backend/resources/funcs/sorting.py b/ross_backend/resources/funcs/sorting.py index 480c60a..de93412 100644 --- a/ross_backend/resources/funcs/sorting.py +++ b/ross_backend/resources/funcs/sorting.py @@ -1,5 +1,4 @@ import pickle -from typing import Iterable from models.config import ConfigSortModel from models.data import DetectResultModel @@ -13,7 +12,7 @@ def create_cluster_time_vec(spike_time: np.ndarray, clusters, config: dict): cluster_time_vec = np.zeros(spike_time[-1] + config['post_thr'], dtype=np.int8) for i, t in enumerate(spike_time): - cluster_time_vec[t - config['pre_thr']: t + config['post_thr']] = clusters[i]+1 + cluster_time_vec[t - config['pre_thr']: t + config['post_thr']] = clusters[i] + 1 return cluster_time_vec diff --git a/ross_backend/resources/sorting_result.py b/ross_backend/resources/sorting_result.py index 9170a15..19c9b78 100644 --- a/ross_backend/resources/sorting_result.py +++ b/ross_backend/resources/sorting_result.py @@ -1,8 +1,8 @@ import io import os.path import pickle -from uuid import uuid4 from pathlib import Path +from uuid import uuid4 import flask import numpy as np diff --git a/ross_ui/controller/exportResults.py b/ross_ui/controller/exportResults.py index 1be45da..a4fc4ae 100644 --- a/ross_ui/controller/exportResults.py +++ b/ross_ui/controller/exportResults.py @@ -1,4 +1,3 @@ -from scipy.io import savemat from PyQt5 import QtWidgets from model.api import API diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index 82d53c6..adcb157 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -22,6 +22,7 @@ from controller.detectedMatSelect import DetectedMatSelectApp as detected_mat_form from controller.detectedTimeSelect import DetectedTimeSelectApp as detected_time_form +from controller.exportResults import ExportResultsApp as export_results from controller.hdf5 import HDF5Plot from controller.matplot_figures import MatPlotFigures from controller.projectSelect import projectSelectApp as project_form @@ -29,7 +30,6 @@ from controller.saveAs import SaveAsApp as save_as_form from controller.serverAddress import ServerApp as server_form from controller.serverFileDialog import ServerFileDialogApp as sever_dialog -from controller.exportResults import ExportResultsApp as export_results from controller.signin import SigninApp as signin_form from view.mainWindow import MainWindow @@ -391,7 +391,8 @@ def open_export_dialog(self): if dialog.checkClusters: data_dict['Cluster'] = self.clusters - filename = QtWidgets.QFileDialog.getSaveFileName(self, "Select Directory", f'./ross_result.{dialog.type}')[0] + filename = QtWidgets.QFileDialog.getSaveFileName(self, "Select Directory", f'./ross_result.{dialog.type}')[ + 0] if dialog.type == 'mat': sio.savemat(filename, data_dict) elif dialog.type == 'pickle': diff --git a/ross_ui/controller/matplot_figures.py b/ross_ui/controller/matplot_figures.py index 61d74fc..68a56f4 100644 --- a/ross_ui/controller/matplot_figures.py +++ b/ross_ui/controller/matplot_figures.py @@ -10,4 +10,4 @@ def __init__(self, fig_title, number_of_clusters, width=10, height=6, dpi=100): self.axes = [] nrows = math.ceil(number_of_clusters / 3) for i in range(number_of_clusters): - self.axes.append(fig.add_subplot(nrows, 3, i+1)) + self.axes.append(fig.add_subplot(nrows, 3, i + 1)) diff --git a/ross_ui/controller/serverFileDialog.py b/ross_ui/controller/serverFileDialog.py index 95cedfd..cc80323 100644 --- a/ross_ui/controller/serverFileDialog.py +++ b/ross_ui/controller/serverFileDialog.py @@ -60,4 +60,3 @@ def pushOpenClicked(self): self.itemDoubleClicked(self.list_folder.selectedItems()[0]) except IndexError: pass - diff --git a/ross_ui/view/mainWindow.py b/ross_ui/view/mainWindow.py index c4bbad0..fc2e5d6 100644 --- a/ross_ui/view/mainWindow.py +++ b/ross_ui/view/mainWindow.py @@ -76,7 +76,6 @@ def createActions(self): self.exportAct.setIcon(QtGui.QIcon(icon_path + "Export.png")) self.exportAct.setEnabled(False) - self.saveAct = QtWidgets.QAction(self.tr("&Save"), self) self.saveAct.setEnabled(False) self.saveAct.setShortcut(QtGui.QKeySequence.Save) diff --git a/ross_ui/view/signin.py b/ross_ui/view/signin.py index a86d450..00d5bf0 100644 --- a/ross_ui/view/signin.py +++ b/ross_ui/view/signin.py @@ -1,4 +1,4 @@ -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtWidgets class Signin_Dialog(QtWidgets.QDialog): From 4c534cea7eda7682b7ba9bebb421588527e71bec Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 15:45:26 +0330 Subject: [PATCH 25/31] test for detection --- environment.yml | 18 ++++++++++++++++- test/backend/test_detection.py | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 87dd865..f9136b0 100644 --- a/environment.yml +++ b/environment.yml @@ -47,11 +47,15 @@ dependencies: - zlib=1.2.11=h7f8727e_4 - pip: - aniso8601==9.0.1 + - astroid==2.15.3 - cffi==1.15.1 - charset-normalizer==2.0.10 - colour==0.1.5 + - coverage==7.2.7 - cryptography==39.0.0 - cycler==0.11.0 + - dill==0.3.6 + - exceptiongroup==1.1.1 - flask-jwt==0.3.2 - flask-jwt-extended==3.0.0 - flask-restful==0.3.8 @@ -59,13 +63,19 @@ dependencies: - greenlet==1.1.2 - h5py==3.6.0 - idna==3.3 + - iniconfig==2.0.0 + - isort==5.12.0 - joblib==1.1.0 - kiwisolver==1.3.2 + - lazy-object-proxy==1.9.0 - matplotlib==3.4.3 + - mccabe==0.7.0 - nptdms==1.4.0 - numpy==1.22.1 - opencv-python==4.6.0.66 - pillow==9.0.0 + - platformdirs==3.2.0 + - pluggy==1.0.0 - pycparser==2.21 - pyjwt==1.4.2 - pyopengl==3.1.5 @@ -75,6 +85,8 @@ dependencies: - pyqt5-qt5==5.15.2 - pyqt5-sip==12.9.0 - pyqtgraph==0.13.1 + - pytest==7.3.2 + - pytest-cov==4.1.0 - pytz==2021.3 - pywavelets==1.2.0 - pyyawt==0.1.1 @@ -84,4 +96,8 @@ dependencies: - sip==6.5.0 - sqlalchemy==1.4.29 - threadpoolctl==3.0.0 - - urllib3==1.26.8 \ No newline at end of file + - tomli==2.0.1 + - tomlkit==0.11.7 + - typing-extensions==4.5.0 + - urllib3==1.26.8 + - wrapt==1.15.0 diff --git a/test/backend/test_detection.py b/test/backend/test_detection.py index e69de29..af2d754 100644 --- a/test/backend/test_detection.py +++ b/test/backend/test_detection.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + +import numpy as np + +from ross_backend.resources.funcs.detection import startDetection + + +@dataclass +class Config: + filter_type = "butter" + filter_order = 4 + pass_freq = 300 + stop_freq = 3000 + sampling_rate = 40000 + thr_method = "median" + side_thr = "negative" + pre_thr = 40 + post_thr = 59 + dead_time = 20 + + +def get_data(num_spikes=3, config=None): + if config is None: + config = Config() + x = np.arange(0, 0.001, 1 / config.sampling_rate) + y = 2 * np.sin(2 * np.pi * 1000 * x) + z = np.zeros((config.pre_thr + config.post_thr + config.dead_time)) + return np.tile(np.concatenate((z, y, z)), num_spikes) + + +def test_start_detection(): + config = Config() + num_spikes = 4 + data = get_data(num_spikes=num_spikes) + SpikeMat, _, _, _ = startDetection(data, config) + + assert SpikeMat.shape[0] == num_spikes From 8c9fecd7c990e1b0158e0ce142ae1e15c4d7a42d Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 15:57:54 +0330 Subject: [PATCH 26/31] ross test with github actions --- .github/workflows/ross-test.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/ross-test.yml diff --git a/.github/workflows/ross-test.yml b/.github/workflows/ross-test.yml new file mode 100644 index 0000000..d548a1d --- /dev/null +++ b/.github/workflows/ross-test.yml @@ -0,0 +1,31 @@ +name: ROSS Test + +on: + push: + branches-ignore: + - master + pull_request: + branches: + - v2 + +jobs: + build-linux: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: '3.9' + - name: Add conda to system path + run: | + # $CONDA is an environment variable pointing to the root of the miniconda directory + echo $CONDA/bin >> $GITHUB_PATH + - name: Install dependencies + run: | + conda env update --file environment.yml --name base + - name: Test with pytest + run: | + python -m pytest -q -rf --tb=short --cov-report term-missing --cov=./ From 6eca8068b2337beadb4cfde9e32359b8c350871f Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 16:09:10 +0330 Subject: [PATCH 27/31] test badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fe2b0ca..4047118 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ![image](./images/Ross_Color.png) +![example workflow](https://github.com/ramintoosi/ROSS/actions/workflows/ross-test.yml/badge.svg) + ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the paper entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). ( From 5fc65c2ef9a1ad3aa15e19aea642cd285266763a Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 14 Jun 2023 16:11:02 +0330 Subject: [PATCH 28/31] test badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4047118..7817833 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![image](./images/Ross_Color.png) -![example workflow](https://github.com/ramintoosi/ROSS/actions/workflows/ross-test.yml/badge.svg) +![ROSS Test](https://github.com/ramintoosi/ROSS/actions/workflows/ross-test.yml/badge.svg) ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the paper From 1d94500f163405d0177d1ee34bacfcea47294713 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Sun, 25 Jun 2023 14:38:54 +0330 Subject: [PATCH 29/31] readme updated --- README.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7817833..b351040 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,20 @@ ![ROSS Test](https://github.com/ramintoosi/ROSS/actions/workflows/ross-test.yml/badge.svg) -ROSS v2 (beta) is the Python version of offline spike sorting software implemented based on the methods described in the +ROSS v2 (alpha) is the Python version of offline spike sorting software implemented based on the methods described in the paper entitled [An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions](https://www.nature.com/articles/s41598-021-93088-w). ( Official Python Implementation) ### Important Note on ROSS v2 -ROSS v2 is implemented based on the client-server architecture. In the beta version, the GUI and processing units are -completely separated and their connection is based on Restful APIs. However, at this moment, it only works on one -machine and we try to find a good way to optimize the data transfer between the client and the server. In our final -release, you would be able to run the light GUI on a simple machine while the data and algorithms would be executed on a -separate server in your lab. +ROSS v2 is implemented based on the client-server architecture. In the alpha version, the GUI and processing units are +completely separated and their connection is based on Restful APIs. Now, you are able to run the light GUI on a simple machine while the data and algorithms would be executed on a +separate server in your lab. Please carefully read the docs and check our tutorial videos. ## Requirements -- All the requirement packages are listed at enviroments.yml file in root path +- All the requirement packages are listed at environment.yml file in root path ## How to install @@ -32,6 +30,8 @@ separate server in your lab. 1. Run the backend by typing ```python ./ross_backend/app.py``` in the terminal. 2. Run the UI by typing ```python ./ross_ui/main.py``` in the terminal. + +**Note:** If you have a separate server, run ```step 1``` in your server and ```step 2``` in your personal computer. 3. The first time you want to use the software, you must define a user as follows: - In opened window, click on ```Options``` ---> ```Sign In/Up``` , enter the desired username and password, click @@ -45,7 +45,7 @@ separate server in your lab. - In opened window, click on ```File``` ---> ```Import``` ---> ```Raw Data``` , select the data file from your system, then, select the variable containing raw-data ```(Spike)``` and click on ```OK```. -6. Now you can Go on and enjoy the Software. +6. Enjoy the Software! For more instructions and samples please visit ROSS documentation at (link), or demo video at (link). @@ -87,7 +87,21 @@ ROSS v2, same as v1, provides useful tools for spike detection, automatic and ma ![image](./images/vis2.png) - - - +# Citation +If ROSS helps your research, please cite our paper in your publications. + +``` +@article{Toosi_2021, + doi = {10.1038/s41598-021-93088-w}, + url = {https://doi.org/10.1038%2Fs41598-021-93088-w}, + year = 2021, + month = {jul}, + publisher = {Springer Science and Business Media {LLC}}, + volume = {11}, + number = {1}, + author = {Ramin Toosi and Mohammad Ali Akhaee and Mohammad-Reza A. Dehaqani}, + title = {An automatic spike sorting algorithm based on adaptive spike detection and a mixture of skew-t distributions}, + journal = {Scientific Reports} +} +``` From 2f2bc3f70e04d38936360810fe179da6c008074a Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Sun, 25 Jun 2023 18:04:50 +0330 Subject: [PATCH 30/31] gmm sorting - test --- ross_backend/resources/funcs/gmm.py | 22 +++++++++----------- test/backend/test_sorting.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 test/backend/test_sorting.py diff --git a/ross_backend/resources/funcs/gmm.py b/ross_backend/resources/funcs/gmm.py index f1fae28..2d5c9be 100644 --- a/ross_backend/resources/funcs/gmm.py +++ b/ross_backend/resources/funcs/gmm.py @@ -3,27 +3,23 @@ from sklearn.mixture import GaussianMixture as GMM -def gmm_sorter(alignedSpikeMat, ss): - out = dict() - print('GMM') - +def gmm_sorter(aligned_spikemat, ss): g_max = ss.g_max g_min = ss.g_min max_iter = ss.max_iter n_cluster_range = np.arange(g_min + 1, g_max + 1) - scores = [] error = ss.error + scores = [] + for n_cluster in n_cluster_range: - clusterer = GMM(n_components=n_cluster, random_state=5, tol=error, max_iter=max_iter) - cluster_labels = clusterer.fit_predict(alignedSpikeMat) - silhouette_avg = silhouette_score(alignedSpikeMat, cluster_labels) - print('For n_cluster={0} ,score={1}'.format(n_cluster, silhouette_avg)) + cluster = GMM(n_components=n_cluster, random_state=5, tol=error, max_iter=max_iter) + cluster_labels = cluster.fit_predict(aligned_spikemat) + silhouette_avg = silhouette_score(aligned_spikemat, cluster_labels) scores.append(silhouette_avg) k = n_cluster_range[np.argmax(scores)] - clusterer = GMM(n_components=k, random_state=5, tol=error, max_iter=max_iter) - out['cluster_index'] = clusterer.fit_predict(alignedSpikeMat) - print('clusters : ', out['cluster_index']) + cluster = GMM(n_components=k, random_state=5, tol=error, max_iter=max_iter) + cluster_index = cluster.fit_predict(aligned_spikemat) - return out['cluster_index'] + return cluster_index diff --git a/test/backend/test_sorting.py b/test/backend/test_sorting.py new file mode 100644 index 0000000..86a9bd3 --- /dev/null +++ b/test/backend/test_sorting.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +import numpy as np + +from ross_backend.resources.funcs.gmm import gmm_sorter + + +@dataclass +class Config: + g_max = 10 + g_min = 2 + max_iter = 1000 + error = 1e-5 + + +def generate_data(num_clusters=2, n_per_class=200): + data = np.zeros((num_clusters * n_per_class, 2)) + for i in range(num_clusters): + data[i * n_per_class: (i + 1) * n_per_class, :] = np.random.multivariate_normal( + [2 * i, 2 * i], [[0.01, 0], [0, 0.01]], size=(n_per_class,) + ) + return data + + +def test_gmm(): + num_clusters = 3 + config = Config() + data = generate_data(num_clusters) + cluster_index = gmm_sorter(data, config) + + assert len(np.unique(cluster_index)) == num_clusters From 2380c0a271846b6f0999c3cfe06f3ac9f71ebc58 Mon Sep 17 00:00:00 2001 From: Ramin Toosi Date: Wed, 5 Jul 2023 10:59:16 +0330 Subject: [PATCH 31/31] picke type as raw input --- ross_backend/resources/browse.py | 2 +- ross_backend/rutils/io.py | 32 +++++++++--- ross_ui/controller/mainWindow.py | 90 +++++++++++++++++++------------- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/ross_backend/resources/browse.py b/ross_backend/resources/browse.py index 45cbd98..7d33cbd 100644 --- a/ross_backend/resources/browse.py +++ b/ross_backend/resources/browse.py @@ -20,5 +20,5 @@ def get(self): list_of_folders = [x for x in os.listdir(root) if os.path.isdir(os.path.join(root, x)) and not x.startswith('.')] list_of_files = [x for x in os.listdir(root) if os.path.isfile(os.path.join(root, x)) and not x.startswith('.') - and x.endswith(('.mat', '.csv', '.tdms'))] + and x.endswith(('.mat', '.pkl'))] return {'folders': list_of_folders, 'files': list_of_files, 'root': root} diff --git a/ross_backend/rutils/io.py b/ross_backend/rutils/io.py index 4a00e4b..ede3fc0 100644 --- a/ross_backend/rutils/io.py +++ b/ross_backend/rutils/io.py @@ -4,6 +4,7 @@ from uuid import uuid4 from scipy.io import loadmat +import numpy as np Raw_data_path = os.path.join(Path(__file__).parent, '../ross_data/Raw_Data') Path(Raw_data_path).mkdir(parents=True, exist_ok=True) @@ -31,14 +32,31 @@ def read_file_in_server(request_data: dict): temp = file_raw[variable].flatten() - # ------------------ save raw data as pkl file in data_set folder ----------------------- - address = os.path.join(Raw_data_path, str(uuid4()) + '.pkl') + elif file_extension == '.pkl': + with open(filename, 'rb') as f: + file_raw = pickle.load(f) + variables = list(file_raw.keys()) + + if len(variables) > 1: + if 'varname' in request_data: + variable = request_data['varname'] + else: + raise ValueError("More than one variable exists") + else: + variable = variables[0] + + temp = np.array(file_raw[variable]).flatten() - with open(address, 'wb') as f: - pickle.dump(temp, f) - # ---------------------------------------------------------------------------------------- - return address, temp else: - raise TypeError("File not supported") + raise TypeError("Type not supported") + + # ------------------ save raw data as pkl file in data_set folder ----------------------- + address = os.path.join(Raw_data_path, str(uuid4()) + '.pkl') + + with open(address, 'wb') as f: + pickle.dump(temp, f) + # ---------------------------------------------------------------------------------------- + return address, temp + else: raise ValueError("request data is incorrect") diff --git a/ross_ui/controller/mainWindow.py b/ross_ui/controller/mainWindow.py index adcb157..67fbbbc 100644 --- a/ross_ui/controller/mainWindow.py +++ b/ross_ui/controller/mainWindow.py @@ -157,7 +157,7 @@ def onImportRaw(self): filename, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Open file"), os.getcwd(), - self.tr("Raw Files(*.mat *.csv *.tdms)") + self.tr("Raw Files(*.mat *.pkl)") ) if not filename: @@ -184,53 +184,73 @@ def onImportRaw(self): else: variable = variables[0] + # nd.array with shape (N,) temp = file_raw[variable].flatten() - self.raw = temp + elif file_extension == '.pkl': + with open(filename, 'rb') as f: + file_raw = pickle.load(f) - # ------------------ save raw data as pkl file in data_set folder --------------------------------------- - address = os.path.join(self.Raw_data_path, str(uuid4()) + '.pkl') - - with open(address, 'wb') as f: - pickle.dump(temp, f) - - # ----------------------------------------------------------------------------------------------------- - - elif file_extension == '.csv': - df = pd.read_csv(filename, skiprows=1) - temp = df.to_numpy() - address = os.path.join(self.Raw_data_path, str(uuid4()) + '.pkl') - self.raw = temp - with open(address, 'wb') as f: - pickle.dump(temp, f) - elif file_extension == '.tdms': - tdms_file = TdmsFile.read(filename) - i = 0 - for group in tdms_file.groups(): - df = tdms_file.object(group).as_dataframe() - variables = list(df.keys()) - i = i + 1 + variables = list(file_raw.keys()) if len(variables) > 1: variable = self.open_raw_dialog(variables) if not variable: - self.statusBar().showMessage('') + self.statusBar().showMessage(self.tr("Nothing selected")) return else: variable = variables[0] - # group = tdms_file['group name'] - # channel = group['channel name'] - # channel_data = channel[:] - # channel_properties = channel.properties - temp = np.array(df[variable]).flatten() - self.raw = temp - address = os.path.join(self.Raw_data_path, os.path.split(filename)[-1][:-5] + '.pkl') - with open(address, 'wb') as f: - pickle.dump(temp, f) + # nd.array with shape (N,) + temp = np.array(file_raw[variable]).flatten() + + # elif file_extension == '.csv': + # df = pd.read_csv(filename, skiprows=1) + # temp = df.to_numpy() + # address = os.path.join(self.Raw_data_path, str(uuid4()) + '.pkl') + # self.raw = temp + # with open(address, 'wb') as f: + # pickle.dump(temp, f) + # elif file_extension == '.tdms': + # tdms_file = TdmsFile.read(filename) + # i = 0 + # for group in tdms_file.groups(): + # df = tdms_file.object(group).as_dataframe() + # variables = list(df.keys()) + # i = i + 1 + # + # if len(variables) > 1: + # variable = self.open_raw_dialog(variables) + # if not variable: + # self.statusBar().showMessage('') + # return + # else: + # variable = variables[0] + # # group = tdms_file['group name'] + # # channel = group['channel name'] + # # channel_data = channel[:] + # # channel_properties = channel.properties + # temp = np.array(df[variable]).flatten() + # self.raw = temp + # + # address = os.path.join(self.Raw_data_path, os.path.split(filename)[-1][:-5] + '.pkl') + # with open(address, 'wb') as f: + # pickle.dump(temp, f) else: - raise TypeError(f'File type {file_extension} is not supported!') + QtWidgets.QMessageBox.critical(self, 'Error', 'Type is not supported') + return + + # check tmp + if temp.ndim != 1: + QtWidgets.QMessageBox.critical(self, 'Error', 'Variable must be a vector') + return + + self.raw = temp + address = os.path.join(self.Raw_data_path, str(uuid4()) + '.pkl') + + with open(address, 'wb') as f: + pickle.dump(temp, f) self.refreshAct.setEnabled(True) self.statusBar().showMessage(self.tr("Successfully loaded file"), 2500)