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