From 39fa9b5122fdfa19b8eaa04be59abab5d7d6980e Mon Sep 17 00:00:00 2001
From: Christoph Knote <christoph.knote@med.uni-augsburg.de>
Date: Sun, 13 Mar 2022 15:53:59 +0100
Subject: [PATCH] Switch to Poetry, package clean up.

---
 .gitignore                                    | 159 ++++++-
 CHANGES.md                                    |  17 +
 CHANGES.rst                                   |  17 -
 INSTALL.md                                    |  14 +
 INSTALL.rst                                   |   9 -
 MANIFEST.in                                   |   9 +-
 Makefile                                      |   3 +
 README.rst => README.md                       |  12 +-
 boxmox/__init__.py                            |  25 -
 boxmox/_console.py                            |  46 --
 boxmox/_installation.py                       |  34 --
 boxmox/_site_specific.py                      |  14 -
 boxmox/hub.py                                 | 436 ------------------
 pyproject.toml                                |   5 +
 setup.cfg                                     |  38 ++
 setup.py                                      |  40 --
 {boxmox => src/boxmox}/.gitignore             |   0
 src/boxmox/__init__.py                        |  39 ++
 {boxmox => src/boxmox}/data.py                | 191 +-------
 {boxmox => src/boxmox}/experiment.py          |  19 +-
 {boxmox => src/boxmox}/fluxes.py              |   0
 {boxmox => src/boxmox}/plotter.py             |   0
 tests/.gitignore                              |   1 +
 tests/__init__.py                             |   0
 tests/test_dataset.py                         |  17 +
 .../usage_examples}/intermediate.py           |   0
 .../usage_examples}/run_tests.bash            |   0
 {examples => tests/usage_examples}/simple.py  |   0
 28 files changed, 324 insertions(+), 821 deletions(-)
 create mode 100644 CHANGES.md
 delete mode 100644 CHANGES.rst
 create mode 100644 INSTALL.md
 delete mode 100644 INSTALL.rst
 create mode 100644 Makefile
 rename README.rst => README.md (59%)
 delete mode 100644 boxmox/__init__.py
 delete mode 100644 boxmox/_console.py
 delete mode 100644 boxmox/_installation.py
 delete mode 100644 boxmox/_site_specific.py
 delete mode 100644 boxmox/hub.py
 create mode 100644 pyproject.toml
 create mode 100644 setup.cfg
 delete mode 100644 setup.py
 rename {boxmox => src/boxmox}/.gitignore (100%)
 create mode 100644 src/boxmox/__init__.py
 rename {boxmox => src/boxmox}/data.py (68%)
 rename {boxmox => src/boxmox}/experiment.py (96%)
 rename {boxmox => src/boxmox}/fluxes.py (100%)
 rename {boxmox => src/boxmox}/plotter.py (100%)
 create mode 100644 tests/.gitignore
 create mode 100644 tests/__init__.py
 create mode 100644 tests/test_dataset.py
 rename {examples => tests/usage_examples}/intermediate.py (100%)
 rename {examples => tests/usage_examples}/run_tests.bash (100%)
 rename {examples => tests/usage_examples}/simple.py (100%)

diff --git a/.gitignore b/.gitignore
index 881b75f..de2d5e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,152 @@
-boxmox.egg-info/dependency_links.txt
-boxmox.egg-info/entry_points.txt
-boxmox.egg-info/not-zip-safe
-boxmox.egg-info/PKG-INFO
-boxmox.egg-info/requires.txt
-boxmox.egg-info/SOURCES.txt
-boxmox.egg-info/top_level.txt
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..c491c49
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,17 @@
+# Changelog
+
+## 1.2.0 (2022-03-08)
+
+- Updates to be compatible with BOXMOX 1.8
+
+## 1.1.0 (2020-09-16)
+
+- Python 3 compatible 
+
+## 1.0.0 (2017-12-19)
+
+- Peer-reviewed version to be published in Knote et al., GMD
+
+## 0.1.0 (2017-08-12)
+
+- Initial release
diff --git a/CHANGES.rst b/CHANGES.rst
deleted file mode 100644
index 16b0e2c..0000000
--- a/CHANGES.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-Changelog
-=========
-
-1.1.0 (2020-09-16)
-------------------
-
-- Python 3 compatible 
-
-1.0.0 (2017-12-19)
-------------------
-
-- Peer-reviewed version to be published in Knote et al., GMD
-
-0.1.0 (2017-08-12)
-------------------
-
-- Initial release
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 0000000..cf7c065
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,14 @@
+```
+pip install boxmox
+```
+
+The BOXMOX chemical box model needs to be installed and usable, 
+and the KPP_HOME environment variable has to be set.
+
+Set the BOXMOX environmental variable in ~/.bashrc or similar for your shell:
+
+```
+export BOXMOX_WORK_PATH=/where/you/want/boxmox/to/write/stuff/to/
+```
+
+Remember to close the shell and log in again for these changes to take effect.
diff --git a/INSTALL.rst b/INSTALL.rst
deleted file mode 100644
index bf2bf48..0000000
--- a/INSTALL.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-::
-
-   pip install boxmox
-
-Set the BOXMOX environmental variable in ~/.bashrc::
-
-   export BOXMOX_WORK_PATH=/where/you/want/boxmox/to/write/stuff/to/
-
-Remember to close the shell and log in again for these changes to take effect.
diff --git a/MANIFEST.in b/MANIFEST.in
index 2256c45..5c96aca 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,6 @@
-exclude boxmox/hub.py
-exclude boxmox/_site_specific.py
+exclude src/boxmox/_site_specific.py
 recursive-include examples *
-include CHANGES.rst
-include INSTALL.rst
+include CHANGES.md
+include INSTALL.md
+include README.md
 include LICENSE.txt
-include README.rst
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..232d276
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+.Phony: coverage
+coverage:
+	coverage run --source=tests -m unittest discover
diff --git a/README.rst b/README.md
similarity index 59%
rename from README.rst
rename to README.md
index 9579651..d67c25c 100644
--- a/README.rst
+++ b/README.md
@@ -1,17 +1,13 @@
-======
-boxmox
-======
+# BOXMOX
 
 ``boxmox`` is the Python wrapper for the chemical box model BOXMOX (a standalone
 C/Fortran executable)
 
-Documentation
-=============
+## Documentation
 
-maintained at https://boxmodeling.meteo.physik.uni-muenchen.de/documentation
+maintained at http://mbees.med.uni-augsburg.de/boxmodeling/
 
-Installation
-============
+## Installation
 
 The chemical box model BOXMOX is required. See the documentation for
 detailed instructions on how to install it.
\ No newline at end of file
diff --git a/boxmox/__init__.py b/boxmox/__init__.py
deleted file mode 100644
index 2a50f12..0000000
--- a/boxmox/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-try:
-    from ._site_specific import *
-except:
-    pass
-
-from . import _installation
-
-from .data import InputFile, InputFileOrig, InputFile17, Output, ConcentrationOutput, RatesOutput, AdjointOutput, JacobianOutput, HessianOutput
-from .fluxes import FluxParser
-
-work_path = _installation.validate()
-if work_path is None:
-    import warnings
-    warnings.warn("BOXMOX unusable - experiment execution disabled.")
-else:
-    from .experiment import Experiment, ExperimentFromExample, ExperimentFromExistingRun, Namelist, examples, compiledMechs
-
-try:
-    import matplotlib
-    from .plotter import ExperimentPlotter
-except:
-    import warnings
-    warnings.warn('matplotlib not found - plotting disabled.')
-
-from . import _console
diff --git a/boxmox/_console.py b/boxmox/_console.py
deleted file mode 100644
index c734905..0000000
--- a/boxmox/_console.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import sys
-
-def _plotExperimentParser():
-    from argparse import ArgumentParser
-
-    parser = ArgumentParser(description='BOXMOX experiment plotter.')
-    parser.add_argument('species', type=str,
-                       help='One or several (comma separated) species names to be plotted')
-    parser.add_argument('-e', '--experimentPath', type=str, default="./",
-                       help='Path to the experiment to be used. Defaults to current directory.')
-    parser.add_argument('-f', '--outputFile', type=str, default='plot.png',
-                       help='Name (or full path) of the output (png) file')
-    parser.add_argument('--timeLimits', type=str, default=None,
-                       help='Time axis limits (as "min,max") in time units.')
-
-    return parser
-
-def plotExperiment(args=None):
-    if args is None:
-        args = sys.argv[1:]
-
-    parser = _plotExperimentParser()
-
-    args = parser.parse_args()
-
-    import boxmox
-
-    try:
-        from . import ExperimentPlotter as ep
-    except:
-        import warnings
-        warnings.warn("Plotting disabled - is matplotlib installed?")
-        return
-
-    import matplotlib
-    matplotlib.use('Agg')
-    import matplotlib.pyplot as plt
-
-    exp = boxmox.ExperimentFromExistingRun(args.experimentPath)
-    tmin = tmax = None
-    if not args.timeLimits is None:
-        tmin = float(args.timeLimits.split(",")[0])
-        tmax = float(args.timeLimits.split(",")[1])
-    exp.plot(args.species.split(","), tmin=tmin, tmax=tmax)
-    plt.savefig(args.outputFile)
-
diff --git a/boxmox/_installation.py b/boxmox/_installation.py
deleted file mode 100644
index 17ab1e3..0000000
--- a/boxmox/_installation.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import os
-import sys
-if os.name == 'posix' and sys.version_info[0] < 3:
-    import subprocess32 as s
-else:
-    import subprocess as s
-
-def validate():
-
-    # 1) BOXMOX is installed and working
-    if 'KPP_HOME' in os.environ.keys():
-        try:
-            res = s.check_call("validate_BOXMOX_installation")
-        except:
-            print("BOXMOX installation broken!")
-            return None
-    else:
-        print('$KPP_HOME not found in environment - is BOXMOX installed?')
-        return None
-
-    # 2) an work directory has been defined and is useable
-    if 'BOXMOX_WORK_PATH' in os.environ.keys():
-        work_path = os.environ['BOXMOX_WORK_PATH']
-        try:
-            if not os.path.isdir(work_path):
-                os.makedirs(work_path)
-        except:
-            print("Could not create work directory " + work_path)
-            return None
-    else:
-        print('BOXMOX_WORK_PATH not found in environment - set it to a path were BOXMOX can write stuff to.')
-        return None
-
-    return work_path
diff --git a/boxmox/_site_specific.py b/boxmox/_site_specific.py
deleted file mode 100644
index 8642750..0000000
--- a/boxmox/_site_specific.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#import os
-#import pwd
-
-#user=pwd.getpwuid(os.getuid()) # full /etc/passwd info
-
-# possibly, www-data needs to know where BOXMOX is
-#KPP_HOME="/var/www/inBOX/boxmox/bin/boxmox"
-#
-#if not 'KPP_HOME' in os.environ.keys() or user.pw_name == "www-data" or user.pw_name == "apache":
-#  os.environ["KPP_HOME"] = KPP_HOME
-#  os.environ["PATH"] += ":"+KPP_HOME+"/bin:"+KPP_HOME+"/boxmox/bin"
-#  os.environ["BOXMOX_WORK_PATH"] = "/var/www/inBOX/boxmox/tmp"
-#  
-                        
diff --git a/boxmox/hub.py b/boxmox/hub.py
deleted file mode 100644
index fab894e..0000000
--- a/boxmox/hub.py
+++ /dev/null
@@ -1,436 +0,0 @@
-# Author: Christoph Knote <christoph.knote@physik.uni-muenchen.de>
-# Copyright: GNU General Public License Version 3, 29 June 2007
-"""
-A python interface for the chemistry box model BOXMOX
-
-How to use this module
-======================
-
-1. Import it: ``import boxmox.model``
-
-2. Create a new experiment either from scratch or from
-   one of the examples provided
-
-3. Run the experiment
-
-4. Use the output
-
-Notes
-=====
-
-- BOXMOX needs to be installed and working
-  (http://boxmodeling.meteo.physik.uni-muenchen.de)
-
-- Any chemical mechanism you want to use must have been
-  prepared for BOXMOX (see BOXMOX documentation)
-"""
-
-__docformat__ = 'restructuredtext'
-
-import os
-import shutil
-import sys
-import tempfile
-if os.name == 'posix' and sys.version_info[0] < 3:
-    import subprocess32 as s
-else:
-    import subprocess as s
-import f90nml
-
-from . import experiment
-
-class Hub(object):
-
-    def __init__(self):
-
-        self.workPath = validateInstall()
-        if self.workPath is None:
-            raise
-
-        self.boxmoxPath = os.path.join(os.environ['KPP_HOME'], "boxmox")
-
-        self.jobs = {}
-
-    def _get_job_id(self):
-        import random
-        return random.randint(0, 2**16-1)
-
-    def list_examples(self):
-        '''
-        List examples that come with your BOXMOX installation.
-        '''
-        return experiment.examples.keys()
-
-    def list_compiled_mechs(self, includeAdjoints=False):
-        '''
-        List compiled BOXMOX mechanisms.
-
-        Parameters
-
-        - `includeAdjoints: a boolean, whether to include the adjoint versions
-           of mechanisms
-        '''
-        mechsPath=os.path.join(os.environ['KPP_HOME'], "boxmox", "compiled_mechs")
-        mechs=os.listdir(mechsPath)
-        if not includeAdjoints:
-            mechs = [ x for x in mechs if not "_adjoint" in x ]
-        mechs.sort()
-        return mechs
-
-    def list_experiment_input_files(self, id):
-        '''
-        List input files that exist for a given BOXMOX experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        return self.jobs[id].input.keys()
-
-    def list_experiment_output_files(self, id):
-        '''
-        List output files that exist for a given BOXMOX experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        return self.jobs[id].output.keys()
-
-    # Translation functions
-
-    def __translate_fromto_mechanism(self, mechanism, species, dir):
-        xfrom = 0
-        xto   = 1
-        if dir == 'from':
-            xfrom = 1
-            xto   = 0
-
-        equiv_path=os.path.join(os.environ['KPP_HOME'], "examples", mechanism + "_wrfkpp.equiv")
-        if isinstance(species, (str, unicode)):
-            species = [ species ]
-
-        species_out = species
-
-        if os.path.exists(equiv_path):
-            f = open(equiv_path, "r")
-            thes = [ i.split() for i in f.readlines() if not "!" in i ]
-            f.close()
-
-            species_out = []
-
-            for spec in species:
-                new_spec = spec
-                for pair in thes:
-                    if pair[xfrom].lower() == spec.lower():
-                        new_spec = pair[xto]
-                species_out.append(new_spec)
-
-        return species_out
-
-    def translate_to_mechanism(self, mechanism, species):
-        '''
-        Translate from WRF species name to mechanism species name
-
-        Parameters:
-
-        - `mechanism`: a string, the name of the chemical mechanism
-        - `species`: a string, the name of the species to translate
-        '''
-        return self.__translate_fromto_mechanism(mechanism, species, dir='to')
-
-    def translate_from_mechanism(self, mechanism, species):
-        '''
-        Translate from mechanism species name to WRF species name
-
-        Parameters:
-
-        - `mechanism`: a string, the name of the chemical mechanism
-        - `species`: a string, the name of the species to translate
-        '''
-        return self.__translate_fromto_mechanism(mechanism, species, dir='from')
-
-    # namelists
-
-    def new_namelist(self):
-        '''
-        Create a BOXMOX namelist object
-        '''
-        return experiment.Namelist()
-
-    def read_namelist(self, id):
-        '''
-        Read BOXMOX namelist for experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        return self.jobs[id].namelist
-
-    def write_namelist(self, id, input_dict):
-        '''
-        Write BOXMOX namelist object to experiment directory
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `input_dict`: a dictionary to be written to the namelist
-           file in the experiment directory
-        - `exp_path`: a string, the path to the experiment (optional)
-        '''
-        nml = self.jobs[id].namelist
-
-        input_dict_lower = dict((k.lower(), k) for k in input_dict.keys())
-        for key in nml.keys():
-            if key in input_dict_lower.keys():
-                val = input_dict[input_dict_lower[key]]
-                if isinstance(nml[key], float):
-                    val = float(val)
-                elif isinstance(nml[key], bool):
-                    val = val.lower() in ['t', 'true', '.true.'] # note that bools are instances of int as well!
-                elif isinstance(nml[key], int):
-                    val = int(val)
-                else:
-                    val = val.encode('ascii', 'ignore')
-
-                nml[key] = val
-
-    # Experiment creation
-
-    def new_experiment_from_example(self, example):
-        '''
-        Create new BOXMOX experiment based on example
-
-        Parameters:
-
-        - `example`: a string, the name of the example used to create the experiment
-        - `exp`: a string, the name of the experiment
-        '''
-
-        id = self._get_job_id()
-        self.jobs[id] = experiment.ExperimentFromExample(example)
-        return id
-
-    def new_experiment(self, mechanism):
-        '''
-        Create new BOXMOX experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `mechanism`: a string, the chemical mechanism to use
-        - `exp_path`: a string, the path where to create the experiment (optional)
-        '''
-
-        id = self._get_job_id()
-        self.jobs[id] = experiment.Experiment(mechanism)
-        return id
-
-    def get_file_contents(exp, filename, f=sys.stdout):
-        '''
-        Reads the contents of a text file in the experiment directory into the argument 'f'
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `filename`: a string, path to the file to read
-        - `f`: the output object (defaults to sys.stdout)
-        '''
-        file_path = os.path.join(make_exp_path(exp), filename)
-        if os.path.isfile(file_path):
-            with open(file_path, 'r') as e:
-                f.write(e.read())
-
-    def put_file_contents(exp, filename, input_file_path, overwrite=False):
-        '''
-        Saves the contents of an input (text) file into a file in the experiment directory
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `filename`: a string, the target filename
-        - `input_file_path`: a string, the path to the input file
-        - `overwrite`: a boolean, whether to overwrite the target file if it already exists
-        '''
-        file_path = os.path.join(make_exp_path(exp), filename)
-        if os.path.isfile(input_file_path):
-            if overwrite and os.path.exists(file_path):
-                os.remove(file_path)
-            if not os.path.exists(file_path):
-                shutil.copy(input_file_path, file_path)
-            else:
-                raise IOError('Destination file {:s} already exists.'.format(file_path))
-
-    # BOXMOX'in
-
-    def __get_mechanism(self, id):
-        return self.jobs[id].mechanism
-
-    def run_experiment(self, id):
-        '''
-        Run a previously prepared BOXMOX experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        return self.jobs[id].run()
-
-    def run_experiment_async(exp, id=None):
-        '''
-        Run a previously prepared BOXMOX experiment asynchronous
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `id`: an integer, use this ID to keep track of the experiment (optional)
-        '''
-        exp_path  = make_exp_path(exp)
-
-        if not os.path.isdir(exp_path):
-            raise IOError('BOXMOX experiment path does not exist: ' + exp_path)
-
-        mechanism = __get_mechanism(exp)
-
-        if id is None:
-            id = __get_job_id()
-
-        try:
-            command = "./" + mechanism + ".exe > " + mechanism + ".log"
-            job_roster[id] = s.Popen(command,
-                                     shell=True,
-                                     cwd=exp_path,
-                                     universal_newlines=True)
-            return id, job_roster[id]
-        except Exception as exxy:
-            raise exxy("Running BOXMOX failed.")
-
-    def poll_experiment(id):
-        '''
-        Check for the current state of a running BOXMOX experiment
-
-        Parameters:
-
-        - `id`: an integer, the id of the experiment run
-        '''
-        result = False
-        if id in job_roster.keys():
-            tmp = job_roster[id].poll()
-            result = tmp == None
-        return result
-
-    # Work with output
-
-    def get_experiment_log(exp):
-        '''
-        Returns the BOXMOX experiment log
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        exp_path  = make_exp_path(exp)
-        mechanism = __get_mechanism(exp)
-
-        log_file = os.path.join(exp_path, mechanism + ".log")
-        log      = ""
-        if os.path.exists(log_file):
-            with open(log_file, "r") as f:
-                log = f.read()
-        return log
-
-    def read_concentrations(exp, variables=None):
-        """
-        Return an np.array containing the concentration time series which are
-        the result of a BOXMOX experiment run
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `variables`: a list of strings, the variable names to return (defaults to all variables)
-        """
-        exp_path = make_exp_path(exp)
-        mechanism = __get_mechanism(exp)
-        return data.read_concentrations_from_file(os.path.join(exp_path, mechanism + ".conc"), variables)
-
-    def read_rates(exp):
-        """
-        Return a dict containing the reaction rate coefficient time series which are
-        the result of a BOXMOX experiment run
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        """
-        exp_path = make_exp_path(exp)
-        mechanism = __get_mechanism(exp)
-        return data.read_rates_from_file(os.path.join(exp_path, mechanism + ".rates"))
-
-    def read_adjoint(exp, variables):
-        """
-        Return an np.array containing the adjoint matrix which is
-        the result of a BOXMOX experiment run
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `variables`: a list of strings, the variables to return
-        """
-        exp_path = make_exp_path(exp)
-        mechanism = __get_mechanism(exp)
-        return data.read_adjoint_from_file(os.path.join(exp_path, mechanism + ".adjoint"), variables)
-
-    # Removing stuff
-
-    def remove_file(exp, filename):
-        '''
-        Remove a file in the experiment directory
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        - `filename`: the file to remove
-        '''
-        file_path = os.path.join(make_exp_path(exp), filename)
-        if os.path.isfile(file_path):
-            os.remove(file_path)
-
-    def remove_experiment(exp):
-        '''
-        Remove BOXMOX experiment
-
-        Parameters:
-
-        - `exp`: a string, the name of the experiment
-        '''
-        exp_path = make_exp_path(exp)
-        if os.path.isdir(exp_path):
-            shutil.rmtree(exp_path, ignore_errors=False)
-
-    def cleanup_experiment(self, id):
-        '''
-        Clean up an existing BOXMOX experiment, so it can be re-run
-        '''
-        self.jobs[id]
-        # output files
-        nul = [ remove_file(exp, f) for f in list_experiment_output_files(exp) ]
-        # log files
-        log_file = os.path.join(make_exp_path(exp), __get_mechanism(exp), ".log")
-        remove_file(exp, log_file)
-
-    # saving the results
-
-    def archive(exp, archive_path):
-        '''
-        Archive BOXMOX experiment
-        '''
-        exp_path    = make_exp_path(exp)
-        if os.path.isdir( archive_path ):
-            raise Exception('Target path {:s} already exists. Stopping.'.format(archive_path))
-        shutil.move( exp_path, archive_path )
-
-
-
-
-
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7c36e66
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[build-system]
+requires = [
+    "numpy", "f90nml", "pyparsing", "setuptools"
+]
+build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..dd7e8cd
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,38 @@
+[metadata]
+name = boxmox
+version = 1.2.0
+author = Christoph Knote
+author_email = christoph.knote@med.uni-augsburg.de
+description = BOXMOX python interface
+long_description = file: README.md, INSTALL.md, CHANGES.md
+long_description_content_type = text/markdown
+url = https://mbees.med.uni-augsburg.de
+project_urls = 
+    Bug Tracker = http://mbees.med.uni-augsburg.de/gitlab/mbees/boxmox_pypackage/issues
+classifiers = 
+    Programming Language :: Python :: 3
+    Development Status :: 5 - Production/Stable
+    Environment :: Console
+    Intended Audience :: Developers
+    Intended Audience :: Education
+    Intended Audience :: End Users/Desktop
+    Intended Audience :: Science/Research
+    License :: OSI Approved :: GNU General Public License v3 (GPLv3)
+    Operating System :: POSIX
+    Topic :: Education
+    Topic :: Scientific/Engineering
+    Topic :: Utilities
+
+[options]
+package_dir =
+    = src
+packages = find:
+python_requires = >=3.0
+include_package_data = True
+
+[options.packages.find]
+where = src
+
+[options.entry_points]
+console_scripts = 
+    plot_BOXMOX_experiment = boxmox._console:plotExperiment
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 03f5695..0000000
--- a/setup.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import os
-from setuptools import setup
-
-def read(filename):
-    with open(os.path.join(os.path.dirname(__file__), filename)) as f:
-        return f.read()
-
-setup(name='boxmox',
-      description='Python wrapper for the chemical box model BOXMOX',
-      long_description=read('README.rst') + '\n\n' + read('INSTALL.rst') + '\n\n' + read('CHANGES.rst'),
-      version='1.1.0',
-      url='https://boxmodeling.meteo.physik.uni-muenchen.de',
-      author='Christoph Knote',
-      author_email='christoph.knote@physik.uni-muenchen.de',
-      license='GPLv3',
-      classifiers=[
-            'Development Status :: 5 - Production/Stable',
-            'Environment :: Console',
-            'Intended Audience :: Developers',
-            'Intended Audience :: Education',
-            'Intended Audience :: End Users/Desktop',
-            'Intended Audience :: Science/Research',
-            'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
-            'Operating System :: POSIX',
-            'Programming Language :: Python',
-            'Topic :: Education',
-            'Topic :: Scientific/Engineering',
-            'Topic :: Utilities'
-        ],
-      keywords='',
-      packages=['boxmox'],
-      install_requires=['numpy', 'f90nml', 'pyparsing' ],
-      entry_points={
-          'console_scripts':
-              [
-                  'plot_BOXMOX_experiment=boxmox._console:plotExperiment'
-              ]
-          },
-      include_package_data=True,
-      zip_safe=False)
diff --git a/boxmox/.gitignore b/src/boxmox/.gitignore
similarity index 100%
rename from boxmox/.gitignore
rename to src/boxmox/.gitignore
diff --git a/src/boxmox/__init__.py b/src/boxmox/__init__.py
new file mode 100644
index 0000000..30e3076
--- /dev/null
+++ b/src/boxmox/__init__.py
@@ -0,0 +1,39 @@
+import os
+import subprocess as s
+
+# 1) BOXMOX is installed and working
+if 'KPP_HOME' in os.environ.keys():
+    try:
+        res = s.check_call("validate_BOXMOX_installation")
+    except:
+        raise OSError("Cannot validate BOXMOX installation using validate_BOXMOX_installation!")
+else:
+    raise OSError('$KPP_HOME not found in environment - is BOXMOX installed?')
+
+# 2) an work directory has been defined and is useable
+work_path = "/does/not/exist"
+if 'BOXMOX_WORK_PATH' in os.environ.keys():
+    work_path = os.environ['BOXMOX_WORK_PATH']
+    try:
+        if not os.path.isdir(work_path):
+            os.makedirs(work_path)
+    except:
+        raise OSError("Could not create work directory " + work_path)
+else:
+    raise OSError('$BOXMOX_WORK_PATH not found in environment - set it to a path were BOXMOX can write stuff to.')
+
+if not os.path.isdir(work_path):
+    import warnings
+    warnings.warn("BOXMOX model unusable - experiment execution disabled.")
+else:
+    from .experiment import Experiment, ExperimentFromExample, ExperimentFromExistingRun, Namelist, examples, compiledMechs
+
+from .data import InputFile, InputFileOrig, InputFile17, Output, ConcentrationOutput, RatesOutput, AdjointOutput, JacobianOutput, HessianOutput
+from .fluxes import FluxParser
+
+try:
+    import matplotlib
+    from .plotter import ExperimentPlotter
+except:
+    import warnings
+    warnings.warn('matplotlib not found - plotting disabled.')
diff --git a/boxmox/data.py b/src/boxmox/data.py
similarity index 68%
rename from boxmox/data.py
rename to src/boxmox/data.py
index 3fd18ea..9b8763b 100644
--- a/boxmox/data.py
+++ b/src/boxmox/data.py
@@ -1,75 +1,12 @@
 import os
 import sys
 import shutil
-try:
-    from StringIO import StringIO ## for Python 2
-except ImportError:
-    from io import StringIO ## for Python 3
+from io import StringIO
 import csv
 
 import numpy as np
 
-def _mygenfromtxt2(f):
-    curpos = f.tell()
-    try:
-        dialect = csv.Sniffer().sniff(f.read(1048576), delimiters=";, ")
-        dialect.skipinitialspace = True
-        f.seek(curpos)
-        spamreader = csv.reader(f, dialect)
-    except:
-        # could not determine dialect, falling back to default.
-        f.seek(curpos)
-        spamreader = csv.reader(f, skipinitialspace = True, delimiter=" ")
-    # twice as fast as np.genfromtxt(..., names=True)
-    hdr = next(spamreader)
-    dat = []
-    for row in spamreader:
-        dat.append( tuple(map(float, row)) )
-    return dat, hdr
-
-def _mygenfromtxt(f):
-    curpos = f.tell()
-    try:
-        dialect = csv.Sniffer().sniff(f.read(1048576), delimiters=";, ")
-        dialect.skipinitialspace = True
-        f.seek(curpos)
-        spamreader = csv.reader(f, dialect)
-    except:
-        # could not determine dialect, falling back to default.
-        f.seek(curpos)
-        spamreader = csv.reader(f, skipinitialspace = True, delimiter=" ")
-    # twice as fast as np.genfromtxt(..., names=True)
-    hdr = next(spamreader)
-    dat = []
-    for row in spamreader:
-        dat.append( tuple(map(float, row)) )
-    return np.array(dat, dtype=[(_, float) for _ in hdr])
-
-def InputFile(fpath=None, version=1.7):
-    '''
-    Returns an instance of a BOXMOX input file class, either old format if
-    version < 1.7 (class InputFileOrig), or the current file format
-    (class InputFile17).
-    '''
-    if not fpath is None:
-        # file format discovery: 3 lines with numbers ==> 1.7
-        with open(fpath, 'r') as f:
-            one = f.readline()
-            two = f.readline()
-            tre = f.readline()
-        try:
-            test    = int(tre)
-            version = 1.7
-        except:
-            version = 1.0
-            pass
-
-    if version >= 1.7:
-        return InputFile17(fpath=fpath, version=version)
-    else:
-        return InputFileOrig(fpath=fpath)
-
-class InputFile17:
+class InputFile:
     '''
     A generic BOXMOX input file (>= v 1.7). Getting and setting of values
     works like a dictionary::
@@ -109,7 +46,22 @@ class InputFile17:
             nvar         = int(f.readline().replace(',', ''))
             nanc         = int(f.readline().replace(',', ''))
             self.timeFormat   = int(f.readline().replace(',', ''))
-            dmp, hdr          = _mygenfromtxt2(f)
+
+            curpos = f.tell()
+            try:
+                dialect = csv.Sniffer().sniff(f.read(1048576), delimiters=";, ")
+                dialect.skipinitialspace = True
+                f.seek(curpos)
+                spamreader = csv.reader(f, dialect)
+            except:
+                # could not determine dialect, falling back to default.
+                f.seek(curpos)
+                spamreader = csv.reader(f, skipinitialspace = True, delimiter=" ")
+            # twice as fast as np.genfromtxt(..., names=True)
+            hdr = next(spamreader)
+            dmp = []
+            for row in spamreader:
+                dmp.append( tuple(map(float, row)) )
 
         ntime = 0
         if not self.timeFormat == 0:
@@ -124,16 +76,13 @@ class InputFile17:
         self.write(f)
         return(f.getvalue())
 
-    def write(self, f=sys.stdout, version=1.7):
+    def write(self, f=sys.stdout):
         '''
         Write to <f>. <f> can be file handle or other connection. Defaults to sys.stdout.
         '''
-        # possibility to create old version output from new version data
-        if version >= 1.7:
-            f.write('{0:1d}'.format(self.nvar)                  +'\n')
-            f.write('{0:1d}'.format(self.nanc)                  +'\n')
-        else:
-            f.write('{0:1d}'.format(self.nanc+self.nvar)        +'\n')
+
+        f.write('{0:1d}'.format(self.nvar)                  +'\n')
+        f.write('{0:1d}'.format(self.nanc)                  +'\n')
         f.write('{0:1d}'.format(self.timeFormat)            +'\n')
 
         data_names = [ key for key in self.keys()     ]
@@ -158,7 +107,7 @@ class InputFile17:
                 data_line = '{0:e}'.format(float(self.time)) + ' ' + data_line
             f.write(data_line)
 
-    def __init__(self, fpath=None, version=1.7):
+    def __init__(self, fpath=None, version=1.8):
         #: Time format (0: constant, 1: seconds since start, 2: hour of diurnal cycle)
         self.timeFormat = 0
         self.timeVar    = 'time'
@@ -173,95 +122,7 @@ class InputFile17:
         #: File path of the underlying file (if it exists (yet))
         self.fpath       = fpath
         if not self.fpath is None:
-#            try:
             self.read(self.fpath)
-#            except Exception as e:
-#                print("Reading input file {:s} failed: {:s}".format(self.fpath, str(e)))
-
-class InputFileOrig:
-    '''
-    A generic BOXMOX input file (< 1.7). Getting and setting of values
-    works like a dictionary::
-
-       print(inp['O3'])
-       inp['O3'] = 0.040
-
-    '''
-    @property
-    def nvar(self):
-        '''
-        Number of variables.
-        '''
-        return len( [ x for x in self.keys() if not x is self._timeVar ] )
-    def __getitem__(self, item):
-        return [ self._data[i] for i in item ] if isinstance(item, list) else self._data[item]
-    def __setitem__(self, item, values):
-        if isinstance(item, list):
-            for i, v in zip(item, values):
-                self._data[i] = v
-        else:
-            self._data[item] = values
-    def keys(self):
-        return self._data.keys()
-
-    def read(self, fpath):
-        '''
-        Read input file from path.
-        '''
-        with open(fpath, 'r') as f:
-            self.nvar         = int(f.readline().replace(',', ''))
-            self.timeFormat   = int(f.readline().replace(',', ''))
-            dmp               = _mygenfromtxt(f)
-
-        if not self.timeFormat == 0:
-            self._timeVar  = dmp.dtype.names[0]
-        self._data     = { x: dmp[x] for x in dmp.dtype.names }
-
-    def __str__(self):
-        f = StringIO()
-        self.write(f)
-        return(f.getvalue())
-
-    def write(self, f=sys.stdout, version=1.0):
-        '''
-        Write to <f>. <f> can be file handle or other connection. Defaults to sys.stdout.
-        '''
-        f.write('{0:1d}'.format(self.nvar)                  +'\n')
-        if version >= 1.7:
-            f.write('{0:1d}'.format(0)                  +'\n')
-        f.write('{0:1d}'.format(self.timeFormat)            +'\n')
-
-        column_names = [ key for key in self.keys() if key != self._timeVar ]
-        if self._timeVar in self.keys() and str(self.timeFormat) != "0" :
-            f.write('{0:s}' .format(' '.join([self._timeVar] + column_names)) + '\n')
-
-            for itime, xtime in enumerate(self._data[self._timeVar]):
-                line = [ xtime ] + [ self._data[key][itime] for key in column_names ]
-                f.write(' '.join('{0:e}'.format(x) for x in line) + '\n')
-        else:
-            f.write('{0:s}' .format(' '.join( column_names)) +'\n')
-            def _data_fix(key):
-                if isinstance(self._data[key], list):
-                    return self._data[key][0]
-                else:
-                    return self._data[key]
-            f.write(' '.join( '{0:e}'.format(float(_data_fix(x))) for x in self.keys() ) + '\n')
-
-    def __init__(self, fpath=None):
-        #: Time format (0: constant, 1: seconds since start, 2: hour of diurnal cycle)
-        self.timeFormat  = 0
-        self._timeVar   = 'time'
-        self._data      = {}
-
-        self.version     = 0.0
-
-        #: File path of the underlying file (if it exists (yet))
-        self.fpath       = fpath
-        if not self.fpath is None:
-            try:
-                self.read(self.fpath)
-            except Exception as e:
-                print("Reading input file {:s} failed: {:s}".format(self.fpath, str(e)))
 
 class Output(object):
     '''
@@ -318,8 +179,6 @@ class ConcentrationOutput(Output):
             out = np.zeros( ( len(self.times), len(items) ) )
             for i in range(len(items)):
                 out[:,i] = self.data[items[i]]
-# old code - raises warning that it will break in the future (of numpy)
-#            out = self.data[items].view(dtype=np.float, type=np.ndarray).reshape((len(self.times), len(items)))
         else:
             out = self.data
         return out
@@ -515,7 +374,3 @@ class AdjointOutput(Output):
                     print("Problem reading adjoint value <{:s}>: {:s}".format(data[xstate_ind][ystate_ind], str(e)))
 
         self.matrix = adj_mat
-
-
-
-
diff --git a/boxmox/experiment.py b/src/boxmox/experiment.py
similarity index 96%
rename from boxmox/experiment.py
rename to src/boxmox/experiment.py
index fb99fb4..1e1543a 100644
--- a/boxmox/experiment.py
+++ b/src/boxmox/experiment.py
@@ -1,9 +1,5 @@
 import os
-import sys
-if os.name == 'posix' and sys.version_info[0] < 3:
-    import subprocess32 as s
-else:
-    import subprocess as s
+import subprocess as s
 import threading
 import shutil
 import fnmatch
@@ -22,7 +18,7 @@ from . import FluxParser
 
 work_path = _installation.validate()
 
-examplesPath=os.path.join(os.environ['KPP_HOME'], "boxmox", "boxmox_examples")
+examplesPath=os.path.join(os.environ['KPP_HOME'], "case_studies")
 
 class ExampleData:
     def __getitem__(self, name):
@@ -40,7 +36,7 @@ class ExampleData:
         except:
             pass
 
-compiledMechsPath=os.path.join(os.environ['KPP_HOME'], "boxmox", "compiled_mechs")
+compiledMechsPath=os.path.join(os.environ['KPP_HOME'], "compiled_mechs")
 
 compiledMechs = []
 try:
@@ -90,8 +86,7 @@ class Namelist:
     def __init__(self, path = None):
         self._namelist = None
 
-        firstExample = examples[list(examples.keys())[0]]
-        self.read(os.path.join(firstExample.path, "BOXMOX.nml"))
+        self.read(os.path.join(examplesPath, "BOXMOX.nml"))
         if not path is None:
             if os.path.exists(path):
                 self.read(path)
@@ -135,7 +130,7 @@ class Experiment:
         self.version  = self._determineVersion(self.path)
 
     def _determineVersion(self, path):
-        version     = 1.0
+        version     = 1.8
         versionFile = os.path.join(path, 'VERSION')
         if os.path.exists(versionFile):
             try:
@@ -144,10 +139,10 @@ class Experiment:
                     line = ".".join(line.split(".")[0:min(2, len(line.split(".")))])
                     # for development only:
                     if line == "__BOXMOX_VERSION__":
-                        line = 1.7
+                        line = 1.8
                     version = float(line)
             except:
-                version = 1.0
+                version = 1.8
                 pass
         return version
 
diff --git a/boxmox/fluxes.py b/src/boxmox/fluxes.py
similarity index 100%
rename from boxmox/fluxes.py
rename to src/boxmox/fluxes.py
diff --git a/boxmox/plotter.py b/src/boxmox/plotter.py
similarity index 100%
rename from boxmox/plotter.py
rename to src/boxmox/plotter.py
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..1bc5aa1
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+/.coverage
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
new file mode 100644
index 0000000..41617cd
--- /dev/null
+++ b/tests/test_dataset.py
@@ -0,0 +1,17 @@
+import unittest
+import pathlib
+import boxmox
+
+# working directory, example files
+wd = pathlib.Path(__file__).parent
+create_scripts = (wd / "usage_examples").glob("*.py")
+
+class Dummy(unittest.TestCase):
+    def dummy(self):
+
+        self.assertTrue(True)
+
+        return True
+
+if __name__ == "__main__":  # pragma: no cover
+    unittest.main()
diff --git a/examples/intermediate.py b/tests/usage_examples/intermediate.py
similarity index 100%
rename from examples/intermediate.py
rename to tests/usage_examples/intermediate.py
diff --git a/examples/run_tests.bash b/tests/usage_examples/run_tests.bash
similarity index 100%
rename from examples/run_tests.bash
rename to tests/usage_examples/run_tests.bash
diff --git a/examples/simple.py b/tests/usage_examples/simple.py
similarity index 100%
rename from examples/simple.py
rename to tests/usage_examples/simple.py
-- 
GitLab