diff --git a/.gitignore b/.gitignore
index ecf0a5b90c327685bef39b9fd76b075618687b9f..26331733cd987d6316851e853cd400c7631ecfba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..dc4c575fe1343ac222be57a9d056e0b766d01183
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+exclude lifescale/models/ls_run.py lifescale/scripts/run_gui.py
\ No newline at end of file
diff --git a/README.md b/README.md
index 49c7c8b3c85f91445e50bf9b241dd809f4d2c571..790f66901c4fb10be2aa7dbc48e4712db4409f8d 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,52 @@
-# 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/>.
 
diff --git a/info/dev_notes.txt b/info/dev_notes.txt
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3dac3d743daa7f04244042b0ad23544e4f0fe848 100644
--- a/info/dev_notes.txt
+++ b/info/dev_notes.txt
@@ -0,0 +1,47 @@
+# 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?
+
+
+
+
diff --git a/lifescale/__init__.py b/lifescale/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..af1d517ca89f1f94b3c97b63573cf5f95052ea4e
--- /dev/null
+++ b/lifescale/__init__.py
@@ -0,0 +1,24 @@
+"""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'
diff --git a/lifescale/command_line/__init__.py b/lifescale/command_line/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..522bb39b3759f7a7c8067bca09a9dc391d2aa9a4
--- /dev/null
+++ b/lifescale/command_line/__init__.py
@@ -0,0 +1,18 @@
+"""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
diff --git a/lifescale/command_line/command_line.py b/lifescale/command_line/command_line.py
new file mode 100644
index 0000000000000000000000000000000000000000..86eff8c7e93ef09e30808abb4131271f212586ac
--- /dev/null
+++ b/lifescale/command_line/command_line.py
@@ -0,0 +1,75 @@
+"""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()
diff --git a/lifescale/gui/MainWindow.py b/lifescale/gui/MainWindow.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1d6bff8ecfb591c724f5d042341995d0d985ac8
--- /dev/null
+++ b/lifescale/gui/MainWindow.py
@@ -0,0 +1,161 @@
+# 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"))
diff --git a/lifescale/gui/MainWindow.ui b/lifescale/gui/MainWindow.ui
new file mode 100644
index 0000000000000000000000000000000000000000..8b01b72c078c0c7b11c1ad10d6185da8c5a62ca0
--- /dev/null
+++ b/lifescale/gui/MainWindow.ui
@@ -0,0 +1,231 @@
+<?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>
diff --git a/lifescale_gui/__init__.py b/lifescale/gui/__init__.py
similarity index 100%
rename from lifescale_gui/__init__.py
rename to lifescale/gui/__init__.py
diff --git a/lifescale/gui/gui_main.py b/lifescale/gui/gui_main.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bfbecadb8f7a91011c94714f3e6c0adcc92de1f
--- /dev/null
+++ b/lifescale/gui/gui_main.py
@@ -0,0 +1,40 @@
+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()
diff --git a/lifescale/models/__init__.py b/lifescale/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c91b86962d614df51c822e711fc9453b6009996a
--- /dev/null
+++ b/lifescale/models/__init__.py
@@ -0,0 +1,18 @@
+"""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
diff --git a/lifescale/models/ls_data.py b/lifescale/models/ls_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..f60d18a735a5d31732a21cbc93f02f156ec09537
--- /dev/null
+++ b/lifescale/models/ls_data.py
@@ -0,0 +1,552 @@
+"""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')
diff --git a/lifescale/models/ls_run.py b/lifescale/models/ls_run.py
new file mode 100644
index 0000000000000000000000000000000000000000..a90fbe35cdc8241531aee01d12d230388a25db0f
--- /dev/null
+++ b/lifescale/models/ls_run.py
@@ -0,0 +1,61 @@
+"""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
diff --git a/lifescale/scripts/__init__.py b/lifescale/scripts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ba464a9704a1acaa495ff9aa486b11588fe1007
--- /dev/null
+++ b/lifescale/scripts/__init__.py
@@ -0,0 +1,18 @@
+"""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
diff --git a/lifescale/scripts/ls2csv.py b/lifescale/scripts/ls2csv.py
new file mode 100644
index 0000000000000000000000000000000000000000..55c902b816a4ff2e01e64c08e5655b964a6aa250
--- /dev/null
+++ b/lifescale/scripts/ls2csv.py
@@ -0,0 +1,27 @@
+"""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)
diff --git a/lifescale/scripts/run_gui.py b/lifescale/scripts/run_gui.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7b7fb62509c887fc4a315e287d932242937777f
--- /dev/null
+++ b/lifescale/scripts/run_gui.py
@@ -0,0 +1,15 @@
+"""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()
diff --git a/lifescale_gui/gui/__init__.py b/lifescale_gui/gui/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/lifescale_gui/models/__init__.py b/lifescale_gui/models/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/lifescale_gui/naive_peaks/__init__.py b/lifescale_gui/naive_peaks/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/makefile b/makefile
index df442737e8a54169f99fa88bac257d42fd159a0e..50604b59aac78b45ba9a97620a3214e50df003c5 100644
--- a/makefile
+++ b/makefile
@@ -5,10 +5,27 @@ test:
 	$(info Test-run for this makefile!)
 	$(info Yeah!!)
 
-# Project initaialization
+# Project initialization
 init:
 	pip install -r requirements.txt
 
 # Convert *.ui files from Qt Designer to Python files:
 py_gui:
-	pyuic5 -o lifescale_gui/gui/MainWindow.py lifescale_gui/gui/MainWindow.ui
+	pyuic6 -o lifescale/gui/MainWindow.py lifescale/gui/MainWindow.ui
+
+# Package test (install in current virtual environment, editable install with pip)
+test_pack:
+	pip install -e .
+
+# Uninstall test package
+test_pack_uninstall:
+	pip uninstall gravtools
+
+# Build package with setuptools (new version):
+build:
+	rm -rf lifescale_utils.egg-info/
+	python -m build
+
+# Upload package to pypi.org
+pypi_push:
+	twine upload --verbose dist/*
\ No newline at end of file
diff --git a/output/.gitignore b/output/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c96a04f008ee21e260b28f7701595ed59e2839e3
--- /dev/null
+++ b/output/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..07de284aa5c45f56b69ca6f605edf72a14785b99
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..5cb308073936bf00b7166d0d1f735ff6878304e2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,35 @@
+[metadata]
+name = lifescale-utils
+version = attr: lifescale.__version__
+author = Andreas Hellerschmied
+author_email = heller182@gmx.at
+url = https://gitlab.com/hellerdev/lifescale_utils
+description = Lifescale utility software.
+long_description = file: README.md
+long_description_content_type = text/markdown
+keywords = Lifescale
+license = GNU GPLv3
+license_files = LICENSE
+classifiers =
+    License :: OSI Approved :: GNU General Public License (GPL)
+    Programming Language :: Python :: 3
+
+[options]
+python_requires = >=3.6, <4
+packages = find:
+zip_safe = True
+include_package_data = True
+install_requires =
+    numpy
+    pandas
+    openpyxl
+
+[options.entry_points]
+console_scripts =
+    ls2csv = lifescale.command_line.command_line:ls2csv
+
+[options.packages.find]
+exclude =
+    lifescale.gui*
+    lifescale.scripts.run_gui.py
+    lifescale.models.ls_run.py