Skip to content
Snippets Groups Projects
Commit 9c7bd30e authored by Andreas Hellerschmied's avatar Andreas Hellerschmied
Browse files

Merge branch 'develop' into 'master'

Cleaned and refactored code and set up a clean distributon (excluded...

See merge request hellerdev/lifescale_utils!4
parents f510d4fc 83e5b2f7
No related branches found
No related tags found
No related merge requests found
Showing
with 1339 additions and 91 deletions
......@@ -30,6 +30,8 @@ venv/
ENV/
env/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
......
exclude lifescale/models/ls_run.py lifescale/scripts/run_gui.py
\ No newline at end of file
# lifescale_gui
# lifescale_utils
Data analysis tools for lifescale with GUI.
# Installation and setup
* **1. Create virtual environment (venv)**
* `python3 -m venv env`
* **2. Activate virtual environment**
* `source env/bin/activate`
* **3. Clone git repository to local machine**
* `git clone git@gitlab.com:hellerdev/lifescale_gui.git`
* `cd lifescale_gui`
* **4. Install required python packages using pip**
* `pip install -r requirements.txt`
## Installation issues on Ubuntu (20.04):
After just installing PyQt5 with pip3 the following error occurred when trying to actually run a PyQt GUI: qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.
This issue was resolved by installing the QT dev tools (Designer, etc.):
sudo apt-get install qttools5-dev-tools
# Test installation with setuptools
With command line interface.
* **1. Configure setup.py**
* Define entry points (*console_scripts*)
* **2. Activate virtual environment**
* e.g. `source env/bin/activate`
* **3. Run setup.py**
* `python3 setup.py develop`
## Using make
`python3 setup.py develop`
# Run application on Windows and create a stand-alone Windows executable file:
TODO
# Comments on requirements.txt file:
* Two entries can be deleted:
* -e git+git@gitlab.com:Heller182/grav.git@fe528c0769502e84a06be67a742032cacfd386df#egg=gravtools
* pkg-resources==0.0.0 (created due a bug when using Linux, see: https://stackoverflow.com/questions/39577984/what-is-pkg-resources-0-0-0-in-output-of-pip-freeze-command)
# Create HTML documentation with sphinx:
Run make in the gravtools/doc directory:
* `>>>make html_doc`
# Guidelines and conventions
## Code style:
* Respect the PEP conventions on python coding!
* PEP 8 -- Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008/
* The maximum line length is 120 characters
* Use **type hints**: https://www.python.org/dev/peps/pep-0484/
* Use docstrings according to the numpy standard: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard
* They are useful to generate the documentation automatically
* Example: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html
* Comment code, if necessary!
* Use English language for the code, docstrings and comments
* German is allowed for user interfaces (GUI, command line), although English is preferred
## Documentation and docstring style
* The API reference is created with sphinx (https://www.sphinx-doc.org/).
* Docstrings have to follow the numpy standard, see: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard
* Examples: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html
* Package documentation via docstring in __ini__.py files
* Module documentation via docstring at first lines of py-file
* Documentation of classes, class methods and functions via docstrings
## Command line interface and executable scripts
* The command line interface is realized via entry points (console_scripts) in setuptools (python packaging tool)
* Input arguments are handled with argparse
* The code is located in the command_line module (gravtools/command_line.py)
* Executable scripts are located in gravtools/scripts
## Dependancies
* Required python packages are listed in requirements.txt
* created with `>>>pip freeze > requirements.txt`
## Version control with GIT
* Gitlab repository: https://gitlab.com/Heller182/grav
* Branching model:
* **master** branch: Current release version
* **develop** branch: Current working version.
* All team members merge their feature branches into develop (merge request via gitlab)
* Make sure that the develop branch contains a fully functional version of the code!
* **feature** branches: Branches of develop for the implementation of new features and other changes.
* Code changes only in feature branches!
* Naming convention: feature_<description of change/feature>, e.g. feature_new_tide_model
* Use gitignore files to prevent any data files (except example files), IDE control files, compiled python code, etc. from being stored in the GIT repository
* Generally rule: Ignore everything in a directory and define explicit exceptions!
## Packaging and distribution
* With setuptools
# Command line programs:
## ls2csv
The program *ls2csv* reads the content of the xlsm files written by lifescale units, parses the data and writes them to three csv
files (where `[run-name]` is the name from the settings sheet):
* `Masses_Vibrio_[run-name].csv`: Data series from the sheet AcquisitionIntervals.
* `Metadata_[run-name].csv`: Data from the sheet PanelData.
* `SampleSummary_[run-name].csv`: Data from the sheet IntervalAnalysis plus sample related data from AcquisitionIntervals.
### Usage:
```
ls2csv -i [path and nale of xlsm file] -o [outpur directory] [-s] [-nv]
options:
-h, --help show this help message and exit
-i INPUT_XLSM, --input-xlsm INPUT_XLSM
Path and name of the input xlsm file created by
lifescale. (default: None)
-o OUT_DIR, --out-dir OUT_DIR
Output directory for the CSV files. (default: None)
-nv, --not-verbose Disable command line status messages. (default: False)
-s, --sample-stats Calculate sample statistics of masses (median, std.
deviation, quartiles, interquartile range) and add
them to the SampleSummary output CSV file (columns:
Mass_median, Mass_std, Mass_q25, Mass_q75,Mass_iqr).
(default: False)
-t, --sort-masses-by-time
Sort data in the Masses CSV file by acquisition time.
(default: False)
```
# License and copyright
Copyright (C) 2022 Andreas Hellerschmied (<heller182@gmx.at>)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# Current pgm:
## Input data (naive_peaks.py):
- raw data files: binary data (required)
- metadata: csv file that holds the information of the PanelData tab in the according .xlsm file
- jason file with settings:
- raw data folder
- processing settings
# Questions:
- Which license?
- Is it OK for Joseph Elsherbini to use/change his code?
- Is a "raw data viewer" needed to show the content of the binary raw data files?
- Individual files, or from one experiment?
- Load to pandas DF and show in table in GUI?
- Is editing required?
- Are any search options needed?
- Format conversion, e.g. to csv files needed?
- Should there be the possibility to store the current GUI settings (data and processing parameters, etc.)
- Should there be the possibility to load default parameters and/or previously saved parameters (e.g. from json file)?
- Is the json file required in the current version, e.g. for documentation (processing parameters, etc.) or further analysis?
- Are there any default values for the processing parameters? Should they be stored anywhere (e.g. json file)?
- Standard values defined in pgm code (e.g. parameters.py file). These parameters are loaded on the initial start
- Save/Load parameters from GUI to json file => Define "standard parameter set" that way
## Processing:
- Is the input data (raw data) always organized in the same way, i.e.: Raw data folder that contains data files with the
file names containing the exper. name date and time?
- Where do we get the following parameters (listed in drop down menu) - from the filenames?
- Experiment name
- Date
- Time
- should directly the .xlsm file (PanelData tab) be used instead of an .csv file that holds identical information?
- What is the actual output of the lifescale devise?
- raw data?
- xlsm files with metadata?
## For plotting and analysis:
- Is additional data required from other sources, e.g. the .xlsm files?
- What should be plotted/analyzed/calculated?
"""LifeScale utils is a utility program for handling data output.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
:Authors:
Andreas Hellerschmied (heller182@gmx.at)
"""
__version__ = '0.0.3'
__author__ = 'Andreas Hellerschmied'
__git_repo__ = 'tba'
__email__ = 'heller182@gmx.at'
__copyright__ = '(c) 2022 Andreas Hellerschmied'
"""LifeScale utils command line interface module.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
:Authors:
Andreas Hellerschmied (heller182@gmx.at)
"""
\ No newline at end of file
"""Command line interface of lifescale utils.
Copyright (C) 2022 Andreas Hellerschmied <heller182@gmx.at>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from lifescale.scripts.ls2csv import ls2csv as ls2csv_main
import argparse
import os
def is_file(filename):
"""Check, whether the input string is the path to an existing file."""
if os.path.isfile(filename):
return filename
raise argparse.ArgumentTypeError("'{}' is not a valid file.".format(filename))
def is_dir(pathname):
"""Check, whether the input string is a valid and existing filepath."""
if os.path.exists(pathname):
return pathname
raise argparse.ArgumentTypeError("'{}' is not a valid directory.".format(pathname))
def ls2csv():
"""Command line interface including argument parser for the lifescale2csv converter."""
parser = argparse.ArgumentParser(prog="ls2csv",
description="Conversion from lifescale xlsm output to csv files",
epilog="The ls2csv converter loads and parses xlsm files created by the lifescale "
"unit. It writes several csv files to the output directory that contain "
"extracted data from the input xlsm file in an easily readable way.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-i", "--input-xlsm", type=is_file, required=True, help="Path and name of the input xlsm file "
"created by "
"lifescale.")
parser.add_argument("-o", "--out-dir", type=is_dir, required=True, help="Output directory for the CSV files.")
parser.add_argument("-nv", "--not-verbose", required=False, help="Disable command line status messages.",
action='store_true')
parser.add_argument("-s", "--sample-stats", required=False, help="Calculate sample statistics of masses (median, "
"std. deviation, quartiles, interquartile range) "
"and add them to the "
"SampleSummary output CSV file (columns: "
"Mass_median, Mass_std, Mass_q25, Mass_q75,"
"Mass_iqr).",
action='store_true')
parser.add_argument("-t", "--sort-masses-by-time", required=False, help="Sort data in the Masses CSV file by "
"acquisition time.",
action='store_true')
args = parser.parse_args()
verbose = not args.not_verbose
return ls2csv_main(xlsm_filename=args.input_xlsm,
output_dir=args.out_dir,
sample_stats=args.sample_stats,
sort_by_time=args.sort_masses_by_time,
verbose=verbose)
if __name__ == '__main__':
"""Main function for debugging and testing."""
ls2csv()
# Form implementation generated from reading ui file 'lifescale/gui/MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.2.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(992, 616)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.tabWidget_Main = QtWidgets.QTabWidget(self.centralwidget)
self.tabWidget_Main.setObjectName("tabWidget_Main")
self.tab_peaks = QtWidgets.QWidget()
self.tab_peaks.setObjectName("tab_peaks")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab_peaks)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.groupBox_data = QtWidgets.QGroupBox(self.tab_peaks)
self.groupBox_data.setObjectName("groupBox_data")
self.formLayout = QtWidgets.QFormLayout(self.groupBox_data)
self.formLayout.setObjectName("formLayout")
self.label_data_rawData = QtWidgets.QLabel(self.groupBox_data)
self.label_data_rawData.setObjectName("label_data_rawData")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_data_rawData)
self.horizontalLayout_data_rawData = QtWidgets.QHBoxLayout()
self.horizontalLayout_data_rawData.setObjectName("horizontalLayout_data_rawData")
self.lineEdit_data_rawData = QtWidgets.QLineEdit(self.groupBox_data)
self.lineEdit_data_rawData.setObjectName("lineEdit_data_rawData")
self.horizontalLayout_data_rawData.addWidget(self.lineEdit_data_rawData)
self.pushButton_data_rawData = QtWidgets.QPushButton(self.groupBox_data)
self.pushButton_data_rawData.setObjectName("pushButton_data_rawData")
self.horizontalLayout_data_rawData.addWidget(self.pushButton_data_rawData)
self.formLayout.setLayout(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_data_rawData)
self.label_data_metadataFile = QtWidgets.QLabel(self.groupBox_data)
self.label_data_metadataFile.setObjectName("label_data_metadataFile")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_data_metadataFile)
self.horizontalLayout_data_metadataFile = QtWidgets.QHBoxLayout()
self.horizontalLayout_data_metadataFile.setObjectName("horizontalLayout_data_metadataFile")
self.lineEdit_data_metadataFile = QtWidgets.QLineEdit(self.groupBox_data)
self.lineEdit_data_metadataFile.setObjectName("lineEdit_data_metadataFile")
self.horizontalLayout_data_metadataFile.addWidget(self.lineEdit_data_metadataFile)
self.pushButton_data_metadataFile = QtWidgets.QPushButton(self.groupBox_data)
self.pushButton_data_metadataFile.setObjectName("pushButton_data_metadataFile")
self.horizontalLayout_data_metadataFile.addWidget(self.pushButton_data_metadataFile)
self.formLayout.setLayout(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_data_metadataFile)
self.label_data_outputFolder = QtWidgets.QLabel(self.groupBox_data)
self.label_data_outputFolder.setObjectName("label_data_outputFolder")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_data_outputFolder)
self.horizontalLayout_data_outputFolder = QtWidgets.QHBoxLayout()
self.horizontalLayout_data_outputFolder.setObjectName("horizontalLayout_data_outputFolder")
self.lineEdit_data_outputFolder = QtWidgets.QLineEdit(self.groupBox_data)
self.lineEdit_data_outputFolder.setObjectName("lineEdit_data_outputFolder")
self.horizontalLayout_data_outputFolder.addWidget(self.lineEdit_data_outputFolder)
self.pushButton_data_outputFolder = QtWidgets.QPushButton(self.groupBox_data)
self.pushButton_data_outputFolder.setObjectName("pushButton_data_outputFolder")
self.horizontalLayout_data_outputFolder.addWidget(self.pushButton_data_outputFolder)
self.formLayout.setLayout(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_data_outputFolder)
self.label_data_selectExperiment = QtWidgets.QLabel(self.groupBox_data)
self.label_data_selectExperiment.setObjectName("label_data_selectExperiment")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_data_selectExperiment)
self.comboBox_data_selectExperiment = QtWidgets.QComboBox(self.groupBox_data)
self.comboBox_data_selectExperiment.setObjectName("comboBox_data_selectExperiment")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.comboBox_data_selectExperiment)
self.verticalLayout_2.addWidget(self.groupBox_data)
self.groupBox_processingParameters = QtWidgets.QGroupBox(self.tab_peaks)
self.groupBox_processingParameters.setObjectName("groupBox_processingParameters")
self.formLayout_2 = QtWidgets.QFormLayout(self.groupBox_processingParameters)
self.formLayout_2.setObjectName("formLayout_2")
self.label_massTransformation = QtWidgets.QLabel(self.groupBox_processingParameters)
self.label_massTransformation.setObjectName("label_massTransformation")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_massTransformation)
self.doubleSpinBox_massTransformation = QtWidgets.QDoubleSpinBox(self.groupBox_processingParameters)
self.doubleSpinBox_massTransformation.setDecimals(6)
self.doubleSpinBox_massTransformation.setObjectName("doubleSpinBox_massTransformation")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.doubleSpinBox_massTransformation)
self.label_massCutoff = QtWidgets.QLabel(self.groupBox_processingParameters)
self.label_massCutoff.setObjectName("label_massCutoff")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_massCutoff)
self.doubleSpinBox_massCutoff = QtWidgets.QDoubleSpinBox(self.groupBox_processingParameters)
self.doubleSpinBox_massCutoff.setDecimals(1)
self.doubleSpinBox_massCutoff.setObjectName("doubleSpinBox_massCutoff")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.doubleSpinBox_massCutoff)
self.label_peakWidthCutoff = QtWidgets.QLabel(self.groupBox_processingParameters)
self.label_peakWidthCutoff.setObjectName("label_peakWidthCutoff")
self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_peakWidthCutoff)
self.doubleSpinBox_peakWidthCutoff = QtWidgets.QDoubleSpinBox(self.groupBox_processingParameters)
self.doubleSpinBox_peakWidthCutoff.setDecimals(1)
self.doubleSpinBox_peakWidthCutoff.setObjectName("doubleSpinBox_peakWidthCutoff")
self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.doubleSpinBox_peakWidthCutoff)
self.label_peakDistanceCutoff = QtWidgets.QLabel(self.groupBox_processingParameters)
self.label_peakDistanceCutoff.setObjectName("label_peakDistanceCutoff")
self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_peakDistanceCutoff)
self.doubleSpinBox_peakDistanceCutoff = QtWidgets.QDoubleSpinBox(self.groupBox_processingParameters)
self.doubleSpinBox_peakDistanceCutoff.setDecimals(1)
self.doubleSpinBox_peakDistanceCutoff.setObjectName("doubleSpinBox_peakDistanceCutoff")
self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.doubleSpinBox_peakDistanceCutoff)
self.verticalLayout_2.addWidget(self.groupBox_processingParameters)
self.pushButton_run = QtWidgets.QPushButton(self.tab_peaks)
self.pushButton_run.setObjectName("pushButton_run")
self.verticalLayout_2.addWidget(self.pushButton_run)
self.tabWidget_Main.addTab(self.tab_peaks, "")
self.tab_analysis = QtWidgets.QWidget()
self.tab_analysis.setObjectName("tab_analysis")
self.tabWidget_Main.addTab(self.tab_analysis, "")
self.verticalLayout.addWidget(self.tabWidget_Main)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 992, 22))
self.menubar.setObjectName("menubar")
self.menuOptions = QtWidgets.QMenu(self.menubar)
self.menuOptions.setObjectName("menuOptions")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionSave_current_GUI_parameters_json = QtGui.QAction(MainWindow)
self.actionSave_current_GUI_parameters_json.setObjectName("actionSave_current_GUI_parameters_json")
self.actionLoad_GUI_parameters_json = QtGui.QAction(MainWindow)
self.actionLoad_GUI_parameters_json.setObjectName("actionLoad_GUI_parameters_json")
self.actionLoad_default_parameters = QtGui.QAction(MainWindow)
self.actionLoad_default_parameters.setObjectName("actionLoad_default_parameters")
self.menuOptions.addAction(self.actionSave_current_GUI_parameters_json)
self.menuOptions.addAction(self.actionLoad_GUI_parameters_json)
self.menuOptions.addAction(self.actionLoad_default_parameters)
self.menubar.addAction(self.menuOptions.menuAction())
self.retranslateUi(MainWindow)
self.tabWidget_Main.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "lifescale tools"))
self.groupBox_data.setTitle(_translate("MainWindow", "Data"))
self.label_data_rawData.setText(_translate("MainWindow", "Raw data folder"))
self.pushButton_data_rawData.setText(_translate("MainWindow", "Browse"))
self.label_data_metadataFile.setText(_translate("MainWindow", "Metadata file (optional)"))
self.pushButton_data_metadataFile.setText(_translate("MainWindow", "Browse"))
self.label_data_outputFolder.setText(_translate("MainWindow", "Output folder"))
self.pushButton_data_outputFolder.setText(_translate("MainWindow", "Browse"))
self.label_data_selectExperiment.setText(_translate("MainWindow", "Select Experiment"))
self.groupBox_processingParameters.setTitle(_translate("MainWindow", "Processing parameters"))
self.label_massTransformation.setText(_translate("MainWindow", "Mass transformation [fg/Hz]"))
self.label_massCutoff.setText(_translate("MainWindow", "Mass cutoff [fg]"))
self.label_peakWidthCutoff.setText(_translate("MainWindow", "Peak width cutoff [???]"))
self.label_peakDistanceCutoff.setText(_translate("MainWindow", "Peak distance cutoff [??]"))
self.pushButton_run.setText(_translate("MainWindow", "Run"))
self.tabWidget_Main.setTabText(self.tabWidget_Main.indexOf(self.tab_peaks), _translate("MainWindow", "Mass Peaks"))
self.tabWidget_Main.setTabText(self.tabWidget_Main.indexOf(self.tab_analysis), _translate("MainWindow", "Analysis"))
self.menuOptions.setTitle(_translate("MainWindow", "Options"))
self.actionSave_current_GUI_parameters_json.setText(_translate("MainWindow", "Save current GUI parameters (json)"))
self.actionLoad_GUI_parameters_json.setText(_translate("MainWindow", "Load GUI parameters (json)"))
self.actionLoad_default_parameters.setText(_translate("MainWindow", "Load default parameters"))
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>992</width>
<height>616</height>
</rect>
</property>
<property name="windowTitle">
<string>lifescale tools</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget_Main">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_peaks">
<attribute name="title">
<string>Mass Peaks</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_data">
<property name="title">
<string>Data</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_data_rawData">
<property name="text">
<string>Raw data folder</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_data_rawData">
<item>
<widget class="QLineEdit" name="lineEdit_data_rawData"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_data_rawData">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_data_metadataFile">
<property name="text">
<string>Metadata file (optional)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_data_metadataFile">
<item>
<widget class="QLineEdit" name="lineEdit_data_metadataFile"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_data_metadataFile">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_data_outputFolder">
<property name="text">
<string>Output folder</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_data_outputFolder">
<item>
<widget class="QLineEdit" name="lineEdit_data_outputFolder"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_data_outputFolder">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_data_selectExperiment">
<property name="text">
<string>Select Experiment</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_data_selectExperiment"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_processingParameters">
<property name="title">
<string>Processing parameters</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_massTransformation">
<property name="text">
<string>Mass transformation [fg/Hz]</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_massTransformation">
<property name="decimals">
<number>6</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_massCutoff">
<property name="text">
<string>Mass cutoff [fg]</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_massCutoff">
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_peakWidthCutoff">
<property name="text">
<string>Peak width cutoff [???]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_peakWidthCutoff">
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_peakDistanceCutoff">
<property name="text">
<string>Peak distance cutoff [??]</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_peakDistanceCutoff">
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_run">
<property name="text">
<string>Run</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_analysis">
<attribute name="title">
<string>Analysis</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>992</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuOptions">
<property name="title">
<string>Options</string>
</property>
<addaction name="actionSave_current_GUI_parameters_json"/>
<addaction name="actionLoad_GUI_parameters_json"/>
<addaction name="actionLoad_default_parameters"/>
</widget>
<addaction name="menuOptions"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionSave_current_GUI_parameters_json">
<property name="text">
<string>Save current GUI parameters (json)</string>
</property>
</action>
<action name="actionLoad_GUI_parameters_json">
<property name="text">
<string>Load GUI parameters (json)</string>
</property>
</action>
<action name="actionLoad_default_parameters">
<property name="text">
<string>Load default parameters</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
File moved
import sys
import os
from PyQt6.QtWidgets import QApplication, QMainWindow
from lifescale.gui.MainWindow import Ui_MainWindow
class MainWindow(QMainWindow, Ui_MainWindow):
"""Main Window of the application."""
def __init__(self):
"""Initializer."""
# GUI:
super().__init__()
self.setupUi(self)
# Connect signals and slots:
# Set up GUI items and widgets:
# Init models:
def main():
"""Main program to start the GUI."""
# Create the application
app = QApplication(sys.argv)
# Create and show the application's main window
main_window = MainWindow()
main_window.show()
# Run the application's main loop:
sys.exit(
app.exec()) # exit or error code of Qt (app.exec_) is passed to sys.exit. Terminates pgm with standard python method
if __name__ == "__main__":
"""Main Program."""
main()
"""LifeScale utils objects module.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
:Authors:
Andreas Hellerschmied (heller182@gmx.at)
"""
\ No newline at end of file
"""Modelling the data output of a LifeScale run.
Copyright (C) 2022 Andreas Hellerschmied <heller182@gmx.at>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime as dt
import numpy as np
import os
import pandas as pd
class LSData:
"""Modelling the data output of a LifeScale run.
Attributes
----------
run_name : str
Name of the LifeScale run.
input_xlsm_filename : str
Filename and path of the xlsm file written by LifeScale.
output_dir_path : str
Output directory filepath.
start_time_dt : datetime object
Start time of run.
end_time_dt : datetime object
End time of the run.
settings_dict : dict
Contains all settings from the Settings sheet of the input xlsm file. If more than one attributes are provides
for a parameter (dictionary key), the dictionary item is a list. If no attribute is provided, the item is `None`
df_panel_data : pandas dataframe
Pandas dataframe that holds the data of the PanelData sheet of the input xlsm file.
df_interval_analysis : pandas dataframe
Pandas dataframe that holds the data of the IntervalAnalysis sheet plus additional data of the input xlsm file.
df_masses : pandas dataframe
Pandas dataframe that holds the data derived from the AcquisitionIntervals sheet of the input xlsm file.
"""
def __init__(self,
run_name='',
guid='',
input_xlsm_filename='',
output_dir_path='',
start_time_dt=None,
end_time_dt=None,
settings_dict=None,
df_panel_data=None,
df_interval_analysis=None,
df_masses=None,
):
"""Default constructor of class LSData."""
# Check input arguments:
# run_name and accession number:
if isinstance(run_name, str):
self.run_name = run_name
else:
raise TypeError('"run_name" needs to be a string')
if isinstance(guid, str):
self.guid = guid
else:
raise TypeError('"guid" needs to be a string')
# output_dir_path:
if isinstance(output_dir_path, str):
self.output_dir_path = output_dir_path
else:
raise TypeError('"data_dir_path" needs to be a string')
# xlsm_filename:
if isinstance(input_xlsm_filename, str):
self.input_xlsm_filename = input_xlsm_filename
else:
raise TypeError('"xlsm_filename" needs to be a string')
# Start and end time of the run:
if isinstance(start_time_dt, dt.datetime):
self.start_time_dt = start_time_dt
else:
raise TypeError('"start_time_dt" needs to be a datetime object')
if isinstance(end_time_dt, dt.datetime):
self.end_time_dt = end_time_dt
else:
raise TypeError('"end_time_dt" needs to be a datetime object')
if isinstance(end_time_dt, dt.datetime):
self.end_time_dt = end_time_dt
else:
raise TypeError('"end_time_dt" needs to be a datetime object')
# Settings dict:
if isinstance(settings_dict, dict):
self.settings_dict = settings_dict
else:
raise TypeError('"settings_dict" needs to be a dict.')
# Dataframes:
if isinstance(df_panel_data, pd.DataFrame):
self.df_panel_data = df_panel_data
else:
raise TypeError('"df_panel_data" needs to be a pandas dataframe.')
if isinstance(df_interval_analysis, pd.DataFrame):
self.df_interval_analysis = df_interval_analysis
else:
raise TypeError('"df_interval_analysis" needs to be a pandas dataframe.')
if isinstance(df_masses, pd.DataFrame):
self.df_masses = df_masses
else:
raise TypeError('"df_masses" needs to be a pandas dataframe.')
# Initialize additional attributes:
pass
@classmethod
def from_xlsm_file(cls, input_xlsm_filename, verbose=True):
"""Constructor that generates and populates the LSData object from an xlsm LS output file.
Parameters
----------
input_xlsm_filename : str
Filename and path of the xlsm file written by LifeScale.
verbose : bool, optional (default = `True`)
If `True`, status messages are written to the command line.
Returns
-------
:py:obj:`.LSData`
Contains all LS output data loaded from the given xlsm file.
"""
REQUIRED_XLSM_SHEET_NAMES = [ # Raise an exception if they are not present in the input xlsm file.
'AcquisitionIntervals',
'IntervalAnalysis',
'PanelData',
'Settings',
]
# Check input data:
# input_xlsm_filename:
if not input_xlsm_filename:
raise AssertionError(f'The parameter "input_xlsm_filename" must not be empty!')
if not isinstance(input_xlsm_filename, str):
raise TypeError('"input_xlsm_filename" needs to be a string')
if not os.path.isfile(input_xlsm_filename):
raise AssertionError(f'XLSM file {input_xlsm_filename} does not exist!')
# Load all needed sheets of the xlsm file to pandas dataframes:
if verbose:
print(f'Load data from xlsm file: {input_xlsm_filename}')
# Get sheet names:
xl_file = pd.ExcelFile(input_xlsm_filename)
sheet_names = xl_file.sheet_names
# Check, if all required sheets are available:
if set(REQUIRED_XLSM_SHEET_NAMES) - set(sheet_names):
missing_sheets = list(set(REQUIRED_XLSM_SHEET_NAMES) - set(sheet_names))
raise AssertionError(f'The following sheets are missing the file {input_xlsm_filename}: {missing_sheets}')
# PanelData:
if verbose:
print(f' - Parse PanelData')
df_panel_data = xl_file.parse('PanelData')
df_panel_data = remove_space_from_column_names(df_panel_data)
df_panel_data['NumberOfIntervals'] = None
if not (df_panel_data[['Id']].value_counts().count() == len(df_panel_data)):
raise AssertionError(
f'The values in the column "Id" in PanelData is not unique!')
# IntervalAnalysis:
if verbose:
print(f' - Parse IntervalAnalysis')
df_interval_analysis = xl_file.parse('IntervalAnalysis')
df_interval_analysis = remove_space_from_column_names(df_interval_analysis)
df_interval_analysis['Status'] = None # From AcquisitionIntervals
df_interval_analysis['DetectedParticles'] = None # From AcquisitionIntervals
df_interval_analysis['MeasuredVolume'] = None # From AcquisitionIntervals
df_interval_analysis['ResonantFrequency'] = None # From AcquisitionIntervals
if not (df_interval_analysis[['Id', 'IntervalNumber']].value_counts().count() == len(df_interval_analysis)):
raise AssertionError(
f'The combination if the values in the columns "Id" and "IntervalNumber" in IntervalAnalysis is not '
f'unique!')
# Settings:
if verbose:
print(f' - Parse Settings')
settings_dict = {}
df_tmp = xl_file.parse('Settings', header=None)
for idx, row in df_tmp.iterrows():
short_row = row[1:]
item_not_nan_max_idx = short_row.loc[~short_row.isna()].index.max()
if item_not_nan_max_idx is np.nan: # No items that are not NaN!
settings_dict[row[0]] = None
else:
tmp_list = short_row.loc[:item_not_nan_max_idx].to_list()
num_items = len(tmp_list)
if num_items == 1:
settings_dict[row[0]] = tmp_list[0]
else:
settings_dict[row[0]] = tmp_list
run_name = settings_dict['Name']
if settings_dict['Guid'] is None:
guid = ''
else:
guid = str(settings_dict['Guid'])
start_time_dt = settings_dict['StartTime']
end_time_dt = start_time_dt + dt.timedelta(settings_dict['ElapsedTime'] / (24 * 60))
# # Settings (dataframe):
# df_settings = xl_file.parse('Settings', header=None).transpose()
# df_settings.columns = df_settings.iloc[0]
# df_settings = df_settings[1:]
# df_settings.reset_index(drop=True, inplace=True)
# Masses (from sheet AcquisitionIntervals):
if verbose:
print(f' - Parse Masses')
id_list = []
well_list = []
interval_num_list = []
time_list = []
masses_list = []
volume_list = []
total_num_particles_list = []
transit_time_list = []
pressure_drop_list = []
time_list_length_old = 0
# masses_list_length_old = 0
# volume_list_length_old = 0
# total_num_particles_list_length_old = 0
# transit_time_list_length_old = 0
# pressure_drop_list_length_old = 0
current_id = None
current_well = None
current_interval_num = None
current_detected_particles = None
df_tmp = xl_file.parse('AcquisitionIntervals', header=None)
for ids, row in df_tmp.iterrows():
if row[0] == 'Id':
current_id = row[1]
if ~(df_interval_analysis['Id'] == current_id).any():
raise AssertionError(f'"ID="{current_id} is not available in IntervalAnalysis!')
if ~(df_panel_data['Id'] == current_id).any():
raise AssertionError(f'"ID="{current_id} is not available in PanelData!')
continue
if row[0] == 'Well':
current_well = row[1]
if ~(df_interval_analysis['Well'] == current_well).any():
raise AssertionError(f'"Well="{current_well} is not available in IntervalAnalysis!')
if ~(df_panel_data['Well'] == current_well).any():
raise AssertionError(f'"Well="{current_well} is not available in PanelData!')
continue
if row[0] == 'Antibiotic':
continue
if row[0] == 'AntibioticConcentration':
continue
if row[0] == 'NumberOfIntervals':
df_panel_data.loc[df_panel_data['Id'] == current_id, 'NumberOfIntervals'] = row[1]
continue
if row[0] == 'IntervalNumber':
current_interval_num = row[1]
if ~(df_interval_analysis['IntervalNumber'] == current_interval_num).any():
raise AssertionError(
f'"IntervalNumber="{current_interval_num} is not available in IntervalAnalysis!')
continue
if row[0] == 'StartTime':
continue
if row[0] == 'EndTime':
continue
if row[0] == 'DilutionFactor':
continue
if row[0] == 'Status':
tmp_filter = (df_interval_analysis['Id'] == current_id) & (
df_interval_analysis['IntervalNumber'] == current_interval_num)
if len(tmp_filter[tmp_filter]) != 1:
raise AssertionError(
f'Invalid number of matches of "Id={current_id}" and "IntervalNumber={current_interval_num}" '
f'in IntervalAnalysis: {len(tmp_filter[tmp_filter])}')
df_interval_analysis.loc[tmp_filter, 'Status'] = row[1]
continue
if row[0] == 'DetectedParticles':
tmp_filter = (df_interval_analysis['Id'] == current_id) & (
df_interval_analysis['IntervalNumber'] == current_interval_num)
if len(tmp_filter[tmp_filter]) != 1:
raise AssertionError(
f'Invalid number of matches of "Id={current_id}" and "IntervalNumber={current_interval_num}" '
f'in IntervalAnalysis: {len(tmp_filter[tmp_filter])}')
df_interval_analysis.loc[tmp_filter, 'DetectedParticles'] = row[1]
current_detected_particles = row[1] # For cross-checks
continue
if row[0] == 'MeasuredVolume':
tmp_filter = (df_interval_analysis['Id'] == current_id) & (
df_interval_analysis['IntervalNumber'] == current_interval_num)
if len(tmp_filter[tmp_filter]) != 1:
raise AssertionError(
f'Invalid number of matches of "Id={current_id}" and "IntervalNumber={current_interval_num}" '
f'in IntervalAnalysis: {len(tmp_filter[tmp_filter])}')
df_interval_analysis.loc[tmp_filter, 'MeasuredVolume'] = row[1]
continue
if row[0] == 'ResonantFrequency':
tmp_filter = (df_interval_analysis['Id'] == current_id) & (
df_interval_analysis['IntervalNumber'] == current_interval_num)
if len(tmp_filter[tmp_filter]) != 1:
raise AssertionError(
f'Invalid number of matches of "Id={current_id}" and "IntervalNumber={current_interval_num}" '
f'in IntervalAnalysis: {len(tmp_filter[tmp_filter])}')
df_interval_analysis.loc[tmp_filter, 'ResonantFrequency'] = row[1]
continue
if row[0] == 'Time':
tmp_list = row_to_list(row[1:])
if (len(tmp_list) == 0) and (current_detected_particles != 0):
raise AssertionError(
f'Number of "DetectedParticles={current_detected_particles}" does not match length of "Time" '
f'series (= {len(tmp_list)}) ')
time_list += tmp_list
continue
if row[0] == 'Mass':
tmp_list = row_to_list(row[1:])
masses_list += tmp_list
continue
if row[0] == 'Volume':
tmp_list = row_to_list(row[1:])
volume_list += tmp_list
continue
if row[0] == 'TotalNumberOfParticlesThroughSensor':
tmp_list = row_to_list(row[1:])
total_num_particles_list += tmp_list
continue
if row[0] == 'TransitTime':
tmp_list = row_to_list(row[1:])
transit_time_list += tmp_list
continue
if row[0] == 'PressureDrop':
tmp_list = row_to_list(row[1:])
pressure_drop_list += tmp_list
# Finish data collection for the current Interval (sample):
# Check if the length of all data series of the current interval number match:
if not (len(time_list) == len(masses_list) == len(volume_list) == len(total_num_particles_list) == len(
transit_time_list) == len(pressure_drop_list)):
raise AssertionError(
f'The lengths of the data series in AcquisitionIntervals of "Well={current_well}" and '
f'"IntervalNumber={current_interval_num}" do not match!')
# Set up lists for well, id and interval number:
num_additional_items_in_data_series = len(time_list) - time_list_length_old
tmp_list = [current_id] * num_additional_items_in_data_series
id_list += tmp_list
tmp_list = [current_well] * num_additional_items_in_data_series
well_list += tmp_list
tmp_list = [current_interval_num] * num_additional_items_in_data_series
interval_num_list += tmp_list
# Reset counters:
time_list_length_old = len(time_list)
# masses_list_length_old = len(masses_list)
# volume_list_length_old = len(volume_list)
# total_num_particles_list_length_old = len(total_num_particles_list)
# transit_time_list_length_old = len(transit_time_list)
# pressure_drop_list_length_old = len(pressure_drop_list)
continue
# Check if the length of all data series lists match:
if not (len(time_list) == len(masses_list) == len(volume_list) == len(total_num_particles_list) == len(
transit_time_list) == len(pressure_drop_list) == len(id_list) == len(well_list) == len(
interval_num_list)):
raise AssertionError(
f'The lengths of the data series in AcquisitionIntervals do not match!')
# Create dataframe:
df_masses_columns = ['Id', 'Well', 'IntervalNumber', 'Time', 'Mass', 'Volume',
'TotalNumberOfParticlesThroughSensor', 'TransitTime', 'PressureDrop']
df_masses = pd.DataFrame(list(
zip(id_list, well_list, interval_num_list, time_list, masses_list, volume_list, total_num_particles_list,
transit_time_list, pressure_drop_list)),
columns=df_masses_columns)
df_masses['Id'] = df_masses['Id'].astype(int)
df_masses['IntervalNumber'] = df_masses['IntervalNumber'].astype(int)
# Sensor:
# sensor_dict = {}
# df_sensor = xl_file.parse('Sensor', header=None).transpose()
# # - The df needs to have two rows => key and value for the dict!
# if df_sensor.shape[0] != 2:
# raise AssertionError(f'More than one column of parameters in the sheet "Sensor!"')
# for col_idx in df_sensor.columns:
# sensor_dict[df_sensor.loc[0, col_idx]] = df_sensor.loc[1, col_idx]
if verbose:
print(f'...finished loading and parsing data!')
return cls(run_name=run_name,
guid=guid,
input_xlsm_filename=input_xlsm_filename,
start_time_dt=start_time_dt,
end_time_dt=end_time_dt,
settings_dict=settings_dict,
df_panel_data=df_panel_data,
df_interval_analysis=df_interval_analysis,
df_masses=df_masses,
)
def export_csv_files(self, output_filepath, sort_by_time=False, verbose=True):
"""Write CSV files to output directory
Parameters
----------
output_filepath : str
Path to the output directory.
sort_by_time : bool, optional (default=`False`)
Sort data in the Masses CSV file by the observation time.
verbose : bool, optional (default = `True`)
If `True`, status messages are written to the command line.
Returns
-------
:py:obj:`.LSData`
Contains all LS output data loaded from the given xlsm file.
"""
if verbose:
print('Write output')
# Checks:
if not os.path.exists(output_filepath):
raise AssertionError(f'The output path does not exist: {output_filepath}')
self.output_dir_path = output_filepath
if self.guid:
filename_ending = f'{self.run_name}_{self.guid}.csv'
else:
filename_ending = f'{self.run_name}.csv'
# Write PanelData:
filename = os.path.join(output_filepath, f'Metadata_{filename_ending}')
if verbose:
print(f'Write PanelData to: {filename}')
self.df_panel_data.to_csv(filename, index=False)
# Write Masses:
filename = os.path.join(output_filepath, f'Masses_{filename_ending}')
if verbose:
print(f'Write Masses to: {filename}')
if sort_by_time:
if verbose:
print(f' - Sort Masses by Time.')
self.df_masses.sort_values(by=['Time']).to_csv(filename, index=False)
else:
self.df_masses.to_csv(filename, index=False)
# Write IntervalAnalysis:
filename = os.path.join(output_filepath, f'SamplesSummary_{filename_ending}')
if verbose:
print(f'Write IntervalAnalysis to: {filename}')
self.df_interval_analysis.to_csv(filename, index=False)
# TODO: Output format (number of digits)
# TODO: Select columns for output (settable as parameter + default settings for each csv file)
def calc_sample_statistics(self, verbose=True):
"""Calculate statistical values for each sample and add it to the self.df_interval_analysis."""
if verbose:
print('Calculate sample statistics.')
for idx, row in self.df_interval_analysis.iterrows():
tmp_filter = (self.df_masses['Id'] == row.Id) & (self.df_masses['IntervalNumber'] == row.IntervalNumber)
tmp_filter_1 = (self.df_interval_analysis['Id'] == row.Id) & \
(self.df_interval_analysis['IntervalNumber'] == row.IntervalNumber)
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_median'] = self.df_masses.loc[tmp_filter, 'Mass'].median()
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_std'] = self.df_masses.loc[tmp_filter, 'Mass'].std()
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_q25'] = \
self.df_masses.loc[tmp_filter, 'Mass'].quantile(0.25)
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_q75'] = \
self.df_masses.loc[tmp_filter, 'Mass'].quantile(0.75)
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_iqr'] = \
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_q75'] - \
self.df_interval_analysis.loc[tmp_filter_1, 'Mass_q25']
@property
def get_number_of_observations(self):
"""Return the number of observations (items in the data series)."""
if self.df_masses is not None:
return len(self.df_masses)
else:
return 0
@property
def get_number_of_wells(self):
"""Return the number wells."""
if self.df_panel_data is not None:
return len(self.df_panel_data)
else:
return 0
@property
def get_number_of_intervals(self):
"""Return the number intervals."""
if self.df_interval_analysis is not None:
return len(self.df_interval_analysis)
else:
return 0
def __str__(self):
if self.run_name is not None:
return f'Run "{self.run_name}" with {self.get_number_of_observations} observations in ' \
f'{self.get_number_of_intervals} intervals and {self.get_number_of_wells} wells. '
else:
return f'Not data available yet.'
def remove_space_from_column_names(df):
"""Removes white space from column names of input dataframe."""
col_names = df.columns
col_names_corrected = []
for col_name in col_names:
col_names_corrected.append(col_name.strip())
df.columns = col_names_corrected
return df
def row_to_list(row) -> list:
"""Convert dataframe row to list and remove all trailing NaN values."""
item_not_nan_max_idx = row.loc[~row.isna()].index.max()
if item_not_nan_max_idx is np.nan: # No items that are not NaN!
out_list = []
else:
out_list = row.loc[:item_not_nan_max_idx].to_list()
return out_list
if __name__ == '__main__':
"""Main function, primarily for debugging and testing."""
xlsm_filename = '../../data/Example_several_wells/Vibrio_starvation_24.11.22_221125_163425.xlsm'
output_directory = '../../output/'
ls_data = LSData.from_xlsm_file(input_xlsm_filename=xlsm_filename)
ls_data.export_csv_files(output_directory)
print('End')
"""Modelling a LifeScale run
Copyright (C) 2022 Andreas Hellerschmied <heller182@gmx.at>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime as dt
import pytz
import numpy as np
import pickle
import os
import sys
import pandas as pd
# from gravtools.models.lsm import LSM
class LSRun:
"""LifeScale run object.
A LS run contains:
- run name and description
- input data
- settings
Attributes
----------
run_name : str
Name of the lsm run.
output_directory : str
Path to output directory (all output files are stored there).
pgm_version : str
Version of the program.
"""
def __init__(self,
campaign_name,
output_directory,
surveys=None, # Always use non-mutable default arguments!
stations=None, # Always use non-mutable default arguments!
lsm_runs=None, # Always use non-mutable default arguments!
ref_delta_t_dt=None # Reference time for drift determination
):
"""
Parameters
"""
\ No newline at end of file
"""LifeScale utils scripts module.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
:Authors:
Andreas Hellerschmied (heller182@gmx.at)
"""
\ No newline at end of file
"""Conversion program from xlsm to csv.
Copyright (C) 2022 Andreas Hellerschmied <heller182@gmx.at>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from lifescale.models.ls_data import LSData
def ls2csv(xlsm_filename, output_dir, sample_stats=True, sort_by_time=False, verbose=True):
"""Convert lifescale output file (xlsm) to csv files."""
ls_data = LSData.from_xlsm_file(input_xlsm_filename=xlsm_filename, verbose=verbose)
if sample_stats:
ls_data.calc_sample_statistics(verbose=verbose)
ls_data.export_csv_files(output_dir, sort_by_time=sort_by_time, verbose=verbose)
"""Start the lifescale GUI from here!
Copyright (C) 2022 Andreas Hellerschmied <heller182@gmx.at>
"""
from lifescale.gui.gui_main import main
def run_gui():
"""Start the GUI."""
main()
if __name__ == "__main__":
"""Main Program."""
run_gui()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment