diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4d5b8d8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +source = welltestpy +omit = *docs*, *examples*, *tests* + +[report] +exclude_lines = + pragma: no cover + if __name__ == '__main__': + def __repr__ + def __str__ diff --git a/.gitignore b/.gitignore index af45410..7866074 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,125 @@ -*.pyc -*.orig -*~ -.spyproject/ +# Byte-compiled / optimized / DLL files __pycache__/ -docs/build/ -#_build -#_static -#_templates -#docs/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/output.txt + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +tags +/test_* + +# own stuff info/ + +# Cython generated C code +*.c +*.cpp + + +# generated docs +docs/source/examples/ +docs/source/generated/ +examples/Cmp_UFZ-campaign.cmp + +*.DS_Store + +*.zip + +*.vtu +*.vtr diff --git a/.travis.yml b/.travis.yml index 9158822..8ba4c1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,53 +1,82 @@ language: python +python: 3.8 -matrix: - include: - # use macOS for py2 since building pandas wheels takes ages on linux - # and matplotlib is not installing on linux - - name: "MacOS py27" - os: osx - language: generic - env: - - PIP=pip2 - - CIBW_BUILD="cp27-*" - - COVER="off" - - # use macOS for py3 since building matplotlib is not working on linux - - name: "MacOS py36" - os: osx - language: generic - env: - - PIP=pip2 - - CIBW_BUILD="cp36-*" - - COVER="on" +# setuptools-scm needs all tags in order to obtain a proper version +git: + depth: false env: global: + # Note: TWINE_PASSWORD is set in Travis settings - TWINE_USERNAME=geostatframework - -script: - # create wheels - - sudo $PIP install cibuildwheel==0.10.1 - - cibuildwheel --output-dir wheelhouse - # create source dist for pypi and create coverage (only once for linux py3.6) - - | - if [[ $COVER == "on" ]]; then - rm -rf dist - python setup.py sdist - fi - -after_success: - # pypi upload (test allways and official on TAG) - - python -m pip install twine - - python -m twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ wheelhouse/*.whl - - python -m twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/*.tar.gz - - | - if [[ $TRAVIS_TAG ]]; then - python -m twine upload --skip-existing wheelhouse/*.whl - python -m twine upload --skip-existing dist/*.tar.gz - fi + - CIBW_BUILD="cp35-* cp36-* cp37-* cp38-*" + - CIBW_SKIP="*_i686" # skip linux 32bit for matplotlib + # update setuptools to latest version + - CIBW_BEFORE_BUILD="pip install -U setuptools" + # testing with cibuildwheel + - CIBW_TEST_REQUIRES=pytest + - CIBW_TEST_COMMAND="pytest -v {project}/tests" notifications: email: recipients: - info@geostat-framework.org + +before_install: + - | + if [[ "$TRAVIS_OS_NAME" = windows ]]; then + choco install python --version 3.8.0 + export PATH="/c/Python38:/c/Python38/Scripts:$PATH" + # make sure it's on PATH as 'python3' + ln -s /c/Python38/python.exe /c/Python38/python3.exe + fi + +install: + - python3 -m pip install cibuildwheel==1.3.0 + +script: + - python3 -m cibuildwheel --output-dir tmp_dist + +stages: + - test + - coverage + - name: deploy + if: (NOT type IN (pull_request)) AND (repo = GeoStat-Framework/welltestpy) + +jobs: + include: + - stage: test + name: Test on Linux + services: docker + - stage: test + name: Test on MacOS + os: osx + language: generic + - stage: test + name: Test on Windows + os: windows + language: shell + + - stage: coverage + name: Coverage on Linux + services: docker + install: python3 -m pip install .[test] coveralls + script: + - python3 -m pytest --cov welltestpy --cov-report term-missing -v tests/ + - python3 -m coveralls + + # Test Deploy source distribution + - stage: deploy + name: Test Deploy + install: python3 -m pip install -U setuptools wheel twine + script: python3 setup.py sdist --formats=gztar bdist_wheel + after_success: + - python3 -m twine upload --verbose --skip-existing --repository-url https://test.pypi.org/legacy/ dist/* + + # Deploy source distribution + - stage: deploy + name: Deploy to PyPI + if: tag IS present + install: python3 -m pip install -U setuptools wheel twine + script: python3 setup.py sdist --formats=gztar bdist_wheel + after_success: python3 -m twine upload --verbose --skip-existing dist/* diff --git a/.zenodo.json b/.zenodo.json new file mode 100755 index 0000000..d145906 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,20 @@ +{ + "license": "MIT", + "language": "eng", + "keywords": [ + "Groundwater flow equation", + "Groundwater", + "Pumping test", + "Pump test", + "Aquifer analysis", + "Python", + "GeoStat-Framework" + ], + "creators": [ + { + "orcid": "0000-0001-9060-4008", + "affiliation": "Helmholtz Centre for Environmental Research - UFZ", + "name": "Sebastian M\u00fcller" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..5e0422a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +All notable changes to **welltestpy** will be documented in this file. + + +## [1.0.0] - 2020-04-09 + +### Enhancements +- new estimators + - ExtTheis3D + - ExtTheis2D + - Neuman2004 + - Theis + - ExtThiem3D + - ExtThiem2D + - Neuman2004Steady + - Thiem +- better plotting +- unit-tests run with py35-py38 on Linux/Win/Mac +- coverage calculation +- sphinx gallery for examples +- allow style setting in plotting routines + +### Bugfixes +- estimation results stored as dict (order could alter before) + +### Changes +- py2 support dropped +- `Fieldsite.coordinates` now returns a `Variable`; `Fieldsite.pos` as shortcut +- `Fieldsite.pumpingrate` now returns a `Variable`; `Fieldsite.rate` as shortcut +- `Fieldsite.auqiferradius` now returns a `Variable`; `Fieldsite.radius` as shortcut +- `Fieldsite.auqiferdepth` now returns a `Variable`; `Fieldsite.depth` as shortcut +- `Well.coordinates` now returns a `Variable`; `Well.pos` as shortcut +- `Well.welldepth` now returns a `Variable`; `Well.depth` as shortcut +- `Well.wellradius` added and returns the radius `Variable` +- `Well.aquiferdepth` now returns a `Variable` +- `Fieldsite.addobservations` renamed to `Fieldsite.add_observations` +- `Fieldsite.delobservations` renamed to `Fieldsite.del_observations` +- `Observation` has changed order of inputs/outputs. Now: `observation`, `time` + + +## [0.3.2] - 2019-03-08 + +### Bugfixes +- adopt AnaFlow API + + +## [0.3.1] - 2019-03-08 + +### Bugfixes +- update travis workflow + + +## [0.3.0] - 2019-02-28 + +### Enhancements +- added documentation + + +## [0.2.0] - 2018-04-25 + +### Enhancements +- added license + + +## [0.1.0] - 2018-04-25 + +First alpha release of welltespy. + +[1.0.0]: https://github.com/GeoStat-Framework/welltestpy/compare/v0.3.2...v1.0.0 +[0.3.2]: https://github.com/GeoStat-Framework/welltestpy/compare/v0.3.1...v0.3.2 +[0.3.1]: https://github.com/GeoStat-Framework/welltestpy/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/GeoStat-Framework/welltestpy/compare/v0.2...v0.3.0 +[0.2.0]: https://github.com/GeoStat-Framework/welltestpy/compare/v0.1...v0.2 +[0.1.0]: https://github.com/GeoStat-Framework/welltestpy/releases/tag/v0.1 diff --git a/LICENSE b/LICENSE index 16a02ab..8724602 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Sebastian Mueller +Copyright (c) 2020 Sebastian Mueller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index bd4b50a..4326bad 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,12 @@ +include README.md include MANIFEST.in include setup.py +include setup.cfg recursive-include welltestpy *.py +recursive-include tests *.py recursive-include docs/source * -include docs/Makefile docs/requirements.txt +include docs/Makefile docs/requirements.txt docs/requirements_doc.txt include LICENSE +include requirements.txt +include requirements_setup.txt +include requirements_test.txt diff --git a/README.md b/README.md index 22856e4..bad4bae 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ -# Welcome to WellTestPy +# Welcome to welltestpy [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1229051.svg)](https://doi.org/10.5281/zenodo.1229051) [![PyPI version](https://badge.fury.io/py/welltestpy.svg)](https://badge.fury.io/py/welltestpy) -[![Build Status](https://travis-ci.org/GeoStat-Framework/welltestpy.svg?branch=master)](https://travis-ci.org/GeoStat-Framework/welltestpy) -[![Documentation Status](https://readthedocs.org/projects/welltestpy/badge/?version=latest)](https://geostat-framework.readthedocs.io/projects/welltestpy/en/latest/?badge=latest) +[![Build Status](https://travis-ci.com/GeoStat-Framework/welltestpy.svg?branch=master)](https://travis-ci.com/GeoStat-Framework/welltestpy) +[![Coverage Status](https://coveralls.io/repos/github/GeoStat-Framework/welltestpy/badge.svg?branch=master)](https://coveralls.io/github/GeoStat-Framework/welltestpy?branch=master) +[![Documentation Status](https://readthedocs.org/projects/welltestpy/badge/?version=stable)](https://geostat-framework.readthedocs.io/projects/welltestpy/en/stable/?badge=stable) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)

-WellTestPy-LOGO +welltestpy-LOGO

## Purpose -WellTestPy provides a framework to handle and plot data from well based field campaigns as well as a data interpretation module. +welltestpy provides a framework to handle, process, plot and analyse data from well based field campaigns. ## Installation @@ -22,76 +23,25 @@ You can install the latest version with the following command: pip install welltestpy -## Documentation for WellTestPy +## Documentation for welltestpy You can find the documentation under [geostat-framework.readthedocs.io][doc_link]. -### Example 1: Create a Campaign containing a pumping test +### Example 1: A campaign containing a pumping test -In the following a simple pumping test is created with artificial drawdown data -generated by the Theis-solution. +In the following, we will take a look at an artificial pumping test campaign, +that is stored in a file called `Cmp_UFZ-campaign.cmp`. ```python -# -*- coding: utf-8 -*- -import numpy as np import welltestpy as wtp -import anaflow as ana - -### create the field-site and the campaign -field = wtp.data.FieldSite(name="UFZ", coordinates=[51.353839, 12.431385]) -campaign = wtp.data.Campaign(name="UFZ-campaign", fieldsite=field) - -### add 4 wells to the campaign -campaign.add_well(name="well_0", radius=0.1, coordinates=(0.0, 0.0)) -campaign.add_well(name="well_1", radius=0.1, coordinates=(1.0, -1.0)) -campaign.add_well(name="well_2", radius=0.1, coordinates=(2.0, 2.0)) -campaign.add_well(name="well_3", radius=0.1, coordinates=(-2.0, -1.0)) - -### generate artificial drawdown data with the Theis solution -rate = -1e-4 -time = np.geomspace(10, 7200, 10) -transmissivity = 1e-4 -storage = 1e-4 -rad = [ - campaign.wells["well_0"].radius, # well radius of well_0 - campaign.wells["well_0"] - campaign.wells["well_1"], # distance 0-1 - campaign.wells["well_0"] - campaign.wells["well_2"], # distance 0-2 - campaign.wells["well_0"] - campaign.wells["well_3"], # distance 0-3 -] -drawdown = ana.theis( - time=time, - rad=rad, - storage=storage, - transmissivity=transmissivity, - rate=rate, -) - -### create a pumping test at well_0 -pumptest = wtp.data.PumpingTest( - name="well_0", - pumpingwell="well_0", - pumpingrate=rate, - description="Artificial pump test with Theis", -) - -### add the drawdown observation at the 4 wells -pumptest.add_transient_obs("well_0", time, drawdown[:, 0]) -pumptest.add_transient_obs("well_1", time, drawdown[:, 1]) -pumptest.add_transient_obs("well_2", time, drawdown[:, 2]) -pumptest.add_transient_obs("well_3", time, drawdown[:, 3]) - -### add the pumping test to the campaign -campaign.addtests(pumptest) -### optionally make the test steady -# campaign.tests["well_0"].make_steady() - -### plot the well constellation and a test overview + +# load the campaign +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") + +# plot the well constellation and a test overview campaign.plot_wells() campaign.plot() - -### save the whole campaign -campaign.save() ``` #### This will give the following plots: @@ -104,8 +54,6 @@ campaign.save() Pumptest

-And the campaign is stored to a file called `Cmp_UFZ-campaign.cmp` - ### Example 2: Estimate transmissivity and storativity @@ -115,7 +63,7 @@ transmissivity and storativity. ```python import welltestpy as wtp -campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") estimation = wtp.estimate.Theis("Estimate_theis", campaign, generate=True) estimation.run() ``` @@ -152,18 +100,18 @@ The results are: welltestpy.data # Subpackage to handle data from field campaigns welltestpy.estimate # Subpackage to estimate field parameters welltestpy.process # Subpackage to pre- and post-process data -welltestpy.tools # Subpackage with miscellaneous tools +welltestpy.tools # Subpackage with tools for plotting and triagulation ``` ## Requirements -- [NumPy >= 1.13.0](https://www.numpy.org) -- [SciPy >= 0.19.1](https://www.scipy.org) -- [Pandas >= 0.20.3](https://pandas.pydata.org) -- [Matplotlib >= 2.0.2](https://matplotlib.org) -- [AnaFlow](https://github.com/GeoStat-Framework/AnaFlow) -- [SpotPy](https://github.com/thouska/spotpy) +- [NumPy >= 1.14.5](https://www.numpy.org) +- [SciPy >= 1.1.0](https://www.scipy.org) +- [Pandas >= 0.23.2](https://pandas.pydata.org) +- [AnaFlow >= 1.0.0](https://github.com/GeoStat-Framework/AnaFlow) +- [SpotPy >= 1.5.0](https://github.com/thouska/spotpy) +- [Matplotlib >= 3.0.0](https://matplotlib.org) ## Contact @@ -173,7 +121,7 @@ You can contact us via . ## License -[MIT][license_link] © 2018-2019 +[MIT][license_link] © 2018-2020 [license_link]: https://github.com/GeoStat-Framework/welltestpy/blob/master/LICENSE -[doc_link]: https://geostat-framework.readthedocs.io/projects/welltestpy/en/latest/ +[doc_link]: https://welltestpy.readthedocs.io diff --git a/docs/Makefile b/docs/Makefile index 39bd7f4..7ba4a6b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python3 -msphinx -SPHINXPROJ = GeoStatTools +SPHINXPROJ = welltestpy SOURCEDIR = source BUILDDIR = build diff --git a/docs/requirements.txt b/docs/requirements.txt index eaf2728..c5a6a23 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,3 @@ -#required for readthedocs.org -numpy>=1.14.5 -scipy>=1.1.0 -pandas>=0.23.0 -matplotlib>=2.0.2 -spotpy>=1.5.0 -anaflow -numpydoc +-r requirements_doc.txt +-r ../requirements_setup.txt +-r ../requirements.txt diff --git a/docs/requirements_doc.txt b/docs/requirements_doc.txt new file mode 100755 index 0000000..6679f79 --- /dev/null +++ b/docs/requirements_doc.txt @@ -0,0 +1,2 @@ +numpydoc +sphinx-gallery \ No newline at end of file diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index 06b11d2..33ed6be 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -3,10 +3,10 @@ {{ super() }}
- - WellTestPy GitHub - WellTestPy Zenodo DOI - WellTestPy PyPI + + welltestpy GitHub + welltestpy Zenodo DOI + welltestpy PyPI
GeoStat Website diff --git a/docs/source/conf.py b/docs/source/conf.py index 5483b10..1c23770 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,10 +20,15 @@ # NOTE: # pip install sphinx_rtd_theme # is needed in order to build the documentation -import os -import sys +import datetime +import warnings + +warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.", +) -sys.path.insert(0, os.path.abspath("../../")) from welltestpy import __version__ as ver @@ -57,6 +62,7 @@ def setup(app): "sphinx.ext.autosummary", "sphinx.ext.napoleon", # parameters look better than with numpydoc only "numpydoc", + "sphinx_gallery.gen_gallery", ] # autosummaries from source-files @@ -95,8 +101,9 @@ def setup(app): master_doc = "contents" # General information about the project. -project = "WellTestPy" -copyright = "2019, Sebastian Mueller" +curr_year = datetime.datetime.now().year +project = "welltestpy" +copyright = "2018 - {}, Sebastian Mueller".format(curr_year) author = "Sebastian Mueller" # The version info for the project you're documenting, acts as replacement for @@ -170,7 +177,10 @@ def setup(app): # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = "WellTestPydoc" +htmlhelp_basename = "welltestpydoc" +# logos for the page +html_logo = "pics/WTP_150.png" +html_favicon = "pics/WTP.ico" # -- Options for LaTeX output --------------------------------------------- @@ -198,7 +208,7 @@ def setup(app): ( master_doc, "welltestpy.tex", - "WellTestPy Documentation", + "welltestpy Documentation", "Sebastian Mueller", "manual", ) @@ -210,7 +220,7 @@ def setup(app): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, "WellTestPy", "WellTestPy Documentation", [author], 1) + (master_doc, "welltestpy", "welltestpy Documentation", [author], 1) ] @@ -222,10 +232,10 @@ def setup(app): texinfo_documents = [ ( master_doc, - "WellTestPy", - "WellTestPy Documentation", + "welltestpy", + "welltestpy Documentation", author, - "WellTestPy", + "welltestpy", "Analytical solutions for the groundwater flow equation", "Miscellaneous", ) @@ -242,4 +252,33 @@ def setup(app): "Python": ("https://docs.python.org/", None), "NumPy": ("http://docs.scipy.org/doc/numpy/", None), "SciPy": ("http://docs.scipy.org/doc/scipy/reference", None), + "matplotlib": ("http://matplotlib.org", None), + "Sphinx": ("http://www.sphinx-doc.org/en/stable/", None), +} + +# -- Sphinx Gallery Options +from sphinx_gallery.sorting import FileNameSortKey + +sphinx_gallery_conf = { + # only show "print" output as output + "capture_repr": (), + # path to your examples scripts + "examples_dirs": ["../../examples",], + # path where to save gallery generated examples + "gallery_dirs": ["examples",], + # Pattern to search for example files + "filename_pattern": "/.*.py", + "ignore_pattern": r"03_estimate_hetero\.py", + # Remove the "Download all examples" button from the top level gallery + "download_all_examples": False, + # Sort gallery example by file name instead of number of lines (default) + "within_subsection_order": FileNameSortKey, + # directory where function granular galleries are stored + "backreferences_dir": None, + # Modules for which function level galleries are created. In + "doc_module": "welltestpy", + # "image_scrapers": ('pyvista', 'matplotlib'), + # "first_notebook_cell": ("%matplotlib inline\n" + # "from pyvista import set_plot_theme\n" + # "set_plot_theme('document')"), } diff --git a/docs/source/contents.rst b/docs/source/contents.rst index a168096..9160e69 100644 --- a/docs/source/contents.rst +++ b/docs/source/contents.rst @@ -7,5 +7,5 @@ Contents :maxdepth: 3 index - tutorials + examples/index package diff --git a/docs/source/data.data_io.rst b/docs/source/data.data_io.rst new file mode 100755 index 0000000..38a6513 --- /dev/null +++ b/docs/source/data.data_io.rst @@ -0,0 +1,11 @@ +welltestpy.data.data_io +======================= + +.. automodule:: welltestpy.data.data_io + :members: + :undoc-members: + :show-inheritance: + +.. raw:: latex + + \clearpage \ No newline at end of file diff --git a/docs/source/data.rst b/docs/source/data.rst index 52b9126..29b521f 100644 --- a/docs/source/data.rst +++ b/docs/source/data.rst @@ -10,6 +10,7 @@ welltestpy.data .. toctree:: :hidden: - data.campaignlib.rst - data.testslib.rst + data.data_io.rst data.varlib.rst + data.testslib.rst + data.campaignlib.rst \ No newline at end of file diff --git a/docs/source/estimate.estimatelib.rst b/docs/source/estimate.estimatelib.rst deleted file mode 100644 index 613d0ea..0000000 --- a/docs/source/estimate.estimatelib.rst +++ /dev/null @@ -1,11 +0,0 @@ -welltestpy.estimate.estimatelib -=============================== - -.. automodule:: welltestpy.estimate.estimatelib - :members: - :undoc-members: - :show-inheritance: - -.. raw:: latex - - \clearpage \ No newline at end of file diff --git a/docs/source/estimate.rst b/docs/source/estimate.rst index 6c1e3ec..8b35d45 100644 --- a/docs/source/estimate.rst +++ b/docs/source/estimate.rst @@ -9,9 +9,3 @@ welltestpy.estimate .. raw:: latex \clearpage - -.. toctree:: - :hidden: - - estimate.estimatelib.rst - estimate.spotpy_classes.rst diff --git a/docs/source/estimate.spotpy_classes.rst b/docs/source/estimate.spotpy_classes.rst deleted file mode 100644 index 50c48bc..0000000 --- a/docs/source/estimate.spotpy_classes.rst +++ /dev/null @@ -1,11 +0,0 @@ -welltestpy.estimate.spotpy_classes -================================== - -.. automodule:: welltestpy.estimate.spotpy_classes - :members: - :undoc-members: - :show-inheritance: - -.. raw:: latex - - \clearpage \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 8710ca4..dd403fb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,12 +1,12 @@ ===================== -WellTestPy Quickstart +welltestpy Quickstart ===================== .. image:: pics/WTP.png :width: 150px :align: center -WellTestPy provides a framework to handle and plot data from well based field campaigns as well as a data interpretation module. +welltestpy provides a framework to handle, process, plot and analyse data from well based field campaigns. Installation @@ -31,22 +31,21 @@ The following functions are provided directly welltestpy.data # Subpackage to handle data from field campaigns welltestpy.estimate # Subpackage to estimate field parameters welltestpy.process # Subpackage to pre- and post-process data - welltestpy.tools # Subpackage with miscellaneous tools + welltestpy.tools # Subpackage with tools for plotting and triagulation Requirements ============ -- `NumPy >= 1.13.0 `_ -- `SciPy >= 0.19.1 `_ -- `AnaFlow `_ -- `Matplotlib `_ -- `Pandas `_ -- `SpotPy `_ - +- `NumPy >= 1.14.5 `_ +- `SciPy >= 1.1.0 `_ +- `Pandas >= 0.23.2 `_ +- `AnaFlow >= 1.0.0 `_ +- `SpotPy >= 1.5.0 `_ +- `Matplotlib >= 3.0.0 `_ License ======= -`GPL `_ © 2019 +`MIT `_ diff --git a/docs/source/package.rst b/docs/source/package.rst index 4bd1550..6bcb149 100644 --- a/docs/source/package.rst +++ b/docs/source/package.rst @@ -1,5 +1,5 @@ ============== -WellTestPy API +welltestpy API ============== .. automodule:: welltestpy diff --git a/docs/source/pics/WTP.ico b/docs/source/pics/WTP.ico new file mode 100644 index 0000000..857215a Binary files /dev/null and b/docs/source/pics/WTP.ico differ diff --git a/docs/source/tutorial_01_create.rst b/docs/source/tutorial_01_create.rst deleted file mode 100644 index eef331b..0000000 --- a/docs/source/tutorial_01_create.rst +++ /dev/null @@ -1,80 +0,0 @@ -Tutorial 1: Create a Campaign containing a pumping test -======================================================= - -In the following a simple pumping test is created with artificial drawdown data -generated by the Theis-solution. - - -.. code-block:: python - - import numpy as np - import welltestpy as wtp - import anaflow as ana - - ### create the field-site and the campaign - field = wtp.data.FieldSite(name="UFZ", coordinates=[51.353839, 12.431385]) - campaign = wtp.data.Campaign(name="UFZ-campaign", fieldsite=field) - - ### add 4 wells to the campaign - campaign.add_well(name="well_0", radius=0.1, coordinates=(0.0, 0.0)) - campaign.add_well(name="well_1", radius=0.1, coordinates=(1.0, -1.0)) - campaign.add_well(name="well_2", radius=0.1, coordinates=(2.0, 2.0)) - campaign.add_well(name="well_3", radius=0.1, coordinates=(-2.0, -1.0)) - - ### generate artificial drawdown data with the Theis solution - rate = -1e-4 - time = np.geomspace(10, 7200, 10) - transmissivity = 1e-4 - storage = 1e-4 - rad = [ - campaign.wells["well_0"].radius, # well radius of well_0 - campaign.wells["well_0"] - campaign.wells["well_1"], # distance 0-1 - campaign.wells["well_0"] - campaign.wells["well_2"], # distance 0-2 - campaign.wells["well_0"] - campaign.wells["well_3"], # distance 0-3 - ] - drawdown = ana.theis( - time=time, - rad=rad, - storage=storage, - transmissivity=transmissivity, - rate=rate, - ) - - ### create a pumping test at well_0 - pumptest = wtp.data.PumpingTest( - name="well_0", - pumpingwell="well_0", - pumpingrate=rate, - description="Artificial pump test with Theis", - ) - - ### add the drawdown observation at the 4 wells - pumptest.add_transient_obs("well_0", time, drawdown[:, 0]) - pumptest.add_transient_obs("well_1", time, drawdown[:, 1]) - pumptest.add_transient_obs("well_2", time, drawdown[:, 2]) - pumptest.add_transient_obs("well_3", time, drawdown[:, 3]) - - ### add the pumping test to the campaign - campaign.addtests(pumptest) - ### optionally make the test steady - # campaign.tests["well_0"].make_steady() - - ### plot the well constellation and a test overview - campaign.plot_wells() - campaign.plot() - - ### save the whole campaign - campaign.save() - - -This will give the following plots: - -.. image:: pics/01_wells.png - :width: 400px - :align: center - -.. image:: pics/01_pumptest.png - :width: 400px - :align: center - -And the campaign is stored to a file called `Cmp_UFZ-campaign.cmp` diff --git a/docs/source/tutorial_02_estimate.rst b/docs/source/tutorial_02_estimate.rst deleted file mode 100644 index cc050c0..0000000 --- a/docs/source/tutorial_02_estimate.rst +++ /dev/null @@ -1,33 +0,0 @@ -Tutorial 2: Estimate transmissivity and storativity -=================================================== - -The pumping test from example 1 can now be loaded and used to estimate the values for -transmissivity and storativity. - - -.. code-block:: python - - import welltestpy as wtp - - campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") - estimation = wtp.estimate.Theis("Estimate_theis", campaign, generate=True) - estimation.run() - -This will give the following plots: - -.. image:: pics/02_fit.png - :width: 400px - :align: center - -.. image:: pics/02_paratrace.png - :width: 400px - :align: center - -.. image:: pics/02_parainter.png - :width: 400px - :align: center - -The results are: - -* `ln(T) = -9.22` which is equivalent to `T = 0.99 * 10^-4 m^2/s` -* `ln(S) = -9.10` which is equivalent to `S = 1.11 * 10^-4` diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst deleted file mode 100644 index 43583d8..0000000 --- a/docs/source/tutorials.rst +++ /dev/null @@ -1,12 +0,0 @@ -=================== -WellTestPy Tutorial -=================== - -In the following you will find several Tutorials on how to use WellTestPy to -explore its whole beauty and power. - -.. toctree:: - :maxdepth: 1 - - tutorial_01_create.rst - tutorial_02_estimate.rst diff --git a/examples/01_create.py b/examples/01_create.py index e1a1776..702d4cd 100644 --- a/examples/01_create.py +++ b/examples/01_create.py @@ -1,19 +1,33 @@ -# -*- coding: utf-8 -*- +""" +Creating a pumping test campaign +-------------------------------- + +In the following we are going to create an artificial pumping test campaign +on a field site. +""" + import numpy as np import welltestpy as wtp import anaflow as ana -### create the field-site and the campaign -field = wtp.data.FieldSite(name="UFZ", coordinates=[51.353839, 12.431385]) -campaign = wtp.data.Campaign(name="UFZ-campaign", fieldsite=field) -### add 4 wells to the campaign +############################################################################### +# Create the field-site and the campaign + +field = wtp.FieldSite(name="UFZ", coordinates=[51.353839, 12.431385]) +campaign = wtp.Campaign(name="UFZ-campaign", fieldsite=field) + +############################################################################### +# Add 4 wells to the campaign + campaign.add_well(name="well_0", radius=0.1, coordinates=(0.0, 0.0)) campaign.add_well(name="well_1", radius=0.1, coordinates=(1.0, -1.0)) campaign.add_well(name="well_2", radius=0.1, coordinates=(2.0, 2.0)) campaign.add_well(name="well_3", radius=0.1, coordinates=(-2.0, -1.0)) -### generate artificial drawdown data with the Theis solution +############################################################################### +# Generate artificial drawdown data with the Theis solution + rate = -1e-4 time = np.geomspace(10, 7200, 10) transmissivity = 1e-4 @@ -32,28 +46,37 @@ rate=rate, ) -### create a pumping test at well_0 -pumptest = wtp.data.PumpingTest( +############################################################################### +# Create a pumping test at well_0 + +pumptest = wtp.PumpingTest( name="well_0", pumpingwell="well_0", pumpingrate=rate, description="Artificial pump test with Theis", ) -### add the drawdown observation at the 4 wells +############################################################################### +# Add the drawdown observation at the 4 wells + pumptest.add_transient_obs("well_0", time, drawdown[:, 0]) pumptest.add_transient_obs("well_1", time, drawdown[:, 1]) pumptest.add_transient_obs("well_2", time, drawdown[:, 2]) pumptest.add_transient_obs("well_3", time, drawdown[:, 3]) -### add the pumping test to the campaign +############################################################################### +# Add the pumping test to the campaign + campaign.addtests(pumptest) -### optionally make the test steady +# optionally make the test (quasi)steady # campaign.tests["well_0"].make_steady() -### plot the well constellation and a test overview +############################################################################### +# Plot the well constellation and a test overview campaign.plot_wells() campaign.plot() -### save the whole campaign +############################################################################### +# Save the whole campaign to a file + campaign.save() diff --git a/examples/02_estimate.py b/examples/02_estimate.py index 60deee6..c5835d4 100755 --- a/examples/02_estimate.py +++ b/examples/02_estimate.py @@ -1,7 +1,19 @@ -# -*- coding: utf-8 -*- +""" +Estimate homogeneous parameters +------------------------------- + +Here we estimate transmissivity and storage from a pumping test campaign +with the classical theis solution. +""" + import welltestpy as wtp -campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") estimation = wtp.estimate.Theis("Estimate_theis", campaign, generate=True) estimation.run() + +############################################################################### +# In addition, we run a sensitivity analysis, to get an impression +# of the impact of each parameter + estimation.sensitivity() diff --git a/examples/03_estimate_hetero.py b/examples/03_estimate_hetero.py index 5b50b06..0449018 100644 --- a/examples/03_estimate_hetero.py +++ b/examples/03_estimate_hetero.py @@ -1,7 +1,15 @@ -# -*- coding: utf-8 -*- +""" +Estimate heterogeneous parameters +--------------------------------- + +Here we demonstrate how to estimate parameters of heterogeneity, namely +mean, variance and correlation length of log-transmissivity, as well as the +storage with the aid the the extended Theis solution in 2D. +""" + import welltestpy as wtp -campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") estimation = wtp.estimate.ExtTheis2D("Estimate_het2D", campaign, generate=True) estimation.run() estimation.sensitivity() diff --git a/examples/04_estimate_steady.py b/examples/04_estimate_steady.py index b460f57..2f41c5e 100755 --- a/examples/04_estimate_steady.py +++ b/examples/04_estimate_steady.py @@ -1,10 +1,20 @@ -# -*- coding: utf-8 -*- +""" +Estimate steady homogeneous parameters +-------------------------------------- + +Here we estimate transmissivity from the quasi steady state of +a pumping test campaign with the classical thiem solution. +""" + import welltestpy as wtp -campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") estimation = wtp.estimate.Thiem("Estimate_thiem", campaign, generate=True) estimation.run() + +############################################################################### # since we only have one parameter, # we need a dummy parameter to estimate sensitivity + estimation.gen_setup(dummy=True) estimation.sensitivity() diff --git a/examples/05_estimate_steady_het.py b/examples/05_estimate_steady_het.py index acc8e92..85a1164 100755 --- a/examples/05_estimate_steady_het.py +++ b/examples/05_estimate_steady_het.py @@ -1,7 +1,15 @@ -# -*- coding: utf-8 -*- +""" +Estimate steady heterogeneous parameters +---------------------------------------- + +Here we demonstrate how to estimate parameters of heterogeneity, namely +mean, variance and correlation length of log-transmissivity, +with the aid the the extended Thiem solution in 2D. +""" + import welltestpy as wtp -campaign = wtp.data.load_campaign("Cmp_UFZ-campaign.cmp") +campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") estimation = wtp.estimate.ExtThiem2D("Est_steady_het", campaign, generate=True) estimation.run() estimation.sensitivity() diff --git a/examples/06_triangulate.py b/examples/06_triangulate.py index cb15017..93ef244 100755 --- a/examples/06_triangulate.py +++ b/examples/06_triangulate.py @@ -1,4 +1,14 @@ -# -*- coding: utf-8 -*- +""" +Point triangulation +------------------- + +Often, we only know the distances between wells within a well base field campaign. +To retrieve their spatial positions, we provide a routine, that triangulates +their positions from a given distance matrix. + +If the solution is not unique, all possible constellations will be returned. +""" + import numpy as np from welltestpy.tools import triangulate, sym, plot_well_pos @@ -12,5 +22,7 @@ dist_mat = sym(dist_mat) # make the distance matrix symmetric well_const = triangulate(dist_mat, prec=0.1) -# plot all possible well constellations +############################################################################### +# Now we can plot all possible well constellations + plot_well_pos(well_const) diff --git a/examples/README.rst b/examples/README.rst new file mode 100755 index 0000000..44b238e --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,9 @@ +=================== +welltestpy Tutorial +=================== + +In the following you will find several Tutorials on how to use welltestpy to +explore its whole beauty and power. + +Gallery +======= diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..29ac972 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +numpy>=1.14.5 +scipy>=1.1.0 +pandas>=0.23.2 +anaflow>=1.0.0 +spotpy>=1.5.0 +matplotlib>=3.0.0 \ No newline at end of file diff --git a/requirements_setup.txt b/requirements_setup.txt new file mode 100755 index 0000000..80e9200 --- /dev/null +++ b/requirements_setup.txt @@ -0,0 +1,2 @@ +setuptools>=41.0.1 +setuptools_scm>=3.5.0 diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100755 index 0000000..be10813 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,2 @@ +pytest-cov>=2.8.0 +pytest>=5.3.0 diff --git a/setup.cfg b/setup.cfg index c62da7f..f48fdad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ [metadata] description-file = README.md license_file = LICENSE - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index 020232b..d167f74 100644 --- a/setup.py +++ b/setup.py @@ -2,40 +2,29 @@ """welltestpy - package to handle well-based Field-campaigns.""" import os -import codecs -import re - from setuptools import setup, find_packages -# find __version__ ############################################################ - - -def read(*parts): - """Read file data.""" - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, *parts), "r") as fp: - return fp.read() - - -def find_version(*file_paths): - """Find version without importing module.""" - version_file = read(*file_paths) - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M - ) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - +HERE = os.path.abspath(os.path.dirname(__file__)) -############################################################################### +with open(os.path.join(HERE, "README.md"), encoding="utf-8") as f: + README = f.read() +with open(os.path.join(HERE, "requirements.txt"), encoding="utf-8") as f: + REQ = f.read().splitlines() +with open(os.path.join(HERE, "requirements_setup.txt"), encoding="utf-8") as f: + REQ_SETUP = f.read().splitlines() +with open(os.path.join(HERE, "requirements_test.txt"), encoding="utf-8") as f: + REQ_TEST = f.read().splitlines() +with open( + os.path.join(HERE, "docs", "requirements_doc.txt"), encoding="utf-8" +) as f: + REQ_DOC = f.read().splitlines() +REQ_DEV = REQ_SETUP + REQ_TEST + REQ_DOC -DOCLINES = __doc__.split("\n") -README = open("README.md").read() +DOCLINE = __doc__.split("\n")[0] CLASSIFIERS = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Science/Research", @@ -48,37 +37,37 @@ def find_version(*file_paths): "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", "Topic :: Software Development", "Topic :: Utilities", ] -VERSION = find_version("welltestpy", "_version.py") - setup( name="welltestpy", - version=VERSION, - maintainer="Sebastian Mueller", - maintainer_email="sebastian.mueller@ufz.de", - description=DOCLINES[0], + description=DOCLINE, long_description=README, long_description_content_type="text/markdown", + maintainer="Sebastian Mueller", + maintainer_email="sebastian.mueller@ufz.de", author="Sebastian Mueller", author_email="sebastian.mueller@ufz.de", - url="https://github.com/GeoStat-Framework/welltestpy", + url="https://github.com/GeoStat-Framework/AnaFlow", license="MIT", classifiers=CLASSIFIERS, platforms=["Windows", "Linux", "Mac OS-X"], include_package_data=True, - install_requires=[ - "numpy>=1.14.5", - "scipy>=1.1.0", - "pandas>=0.23.0", - "matplotlib>=2.0.2", - "spotpy>=1.5.0", - "anaflow", - ], + python_requires=">=3.5", + use_scm_version={ + "relative_to": __file__, + "write_to": "welltestpy/_version.py", + "write_to_template": "__version__ = '{version}'", + "local_scheme": "no-local-version", + "fallback_version": "0.0.0.dev0", + }, + install_requires=REQ, + setup_requires=REQ_SETUP, + extras_require={"doc": REQ_DOC, "test": REQ_TEST, "dev": REQ_DEV}, packages=find_packages(exclude=["tests*", "docs*"]), ) diff --git a/tests/test_welltestpy.py b/tests/test_welltestpy.py new file mode 100644 index 0000000..b7c25fd --- /dev/null +++ b/tests/test_welltestpy.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" +This is the unittest of AnaFlow. +""" + +import unittest +import numpy as np +import matplotlib as mpl + +mpl.use("Agg") + +import welltestpy as wtp +from welltestpy.tools import triangulate, sym, plot_well_pos + +import anaflow as ana + + +class TestWTP(unittest.TestCase): + def setUp(self): + self.rate = -1e-4 + self.time = np.geomspace(10, 7200, 10) + self.transmissivity = 1e-4 + self.storage = 1e-4 + self.s_types = ["ST", "S1"] + + def test_create(self): + # create the field-site and the campaign + field = wtp.FieldSite(name="UFZ", coordinates=[51.3538, 12.4313]) + campaign = wtp.Campaign(name="UFZ-campaign", fieldsite=field) + + # add 4 wells to the campaign + campaign.add_well(name="well_0", radius=0.1, coordinates=(0.0, 0.0)) + campaign.add_well(name="well_1", radius=0.1, coordinates=(1.0, -1.0)) + campaign.add_well(name="well_2", radius=0.1, coordinates=(2.0, 2.0)) + campaign.add_well(name="well_3", radius=0.1, coordinates=(-2.0, -1.0)) + + # generate artificial drawdown data with the Theis solution + self.rad = [ + campaign.wells["well_0"].radius, # well radius of well_0 + campaign.wells["well_0"] - campaign.wells["well_1"], # dist. 0-1 + campaign.wells["well_0"] - campaign.wells["well_2"], # dist. 0-2 + campaign.wells["well_0"] - campaign.wells["well_3"], # dist. 0-3 + ] + drawdown = ana.theis( + time=self.time, + rad=self.rad, + storage=self.storage, + transmissivity=self.transmissivity, + rate=self.rate, + ) + + # create a pumping test at well_0 + pumptest = wtp.PumpingTest( + name="well_0", + pumpingwell="well_0", + pumpingrate=self.rate, + description="Artificial pump test with Theis", + ) + + # add the drawdown observation at the 4 wells + pumptest.add_transient_obs("well_0", self.time, drawdown[:, 0]) + pumptest.add_transient_obs("well_1", self.time, drawdown[:, 1]) + pumptest.add_transient_obs("well_2", self.time, drawdown[:, 2]) + pumptest.add_transient_obs("well_3", self.time, drawdown[:, 3]) + + # add the pumping test to the campaign + campaign.addtests(pumptest) + # plot the well constellation and a test overview + campaign.plot_wells() + campaign.plot() + # save the whole campaign + campaign.save() + # test making steady + campaign.tests["well_0"].make_steady() + + def test_est_theis(self): + campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") + estimation = wtp.estimate.Theis("est_theis", campaign, generate=True) + estimation.run() + res = estimation.estimated_para + estimation.sensitivity() + self.assertAlmostEqual(np.exp(res["mu"]), self.transmissivity, 2) + self.assertAlmostEqual(np.exp(res["lnS"]), self.storage, 2) + sens = estimation.sens + for s_typ in self.s_types: + self.assertTrue(sens[s_typ]["mu"] > sens[s_typ]["lnS"]) + + def test_est_thiem(self): + campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") + estimation = wtp.estimate.Thiem("est_thiem", campaign, generate=True) + estimation.run() + res = estimation.estimated_para + # since we only have one parameter, + # we need a dummy parameter to estimate sensitivity + estimation.gen_setup(dummy=True) + estimation.sensitivity() + self.assertAlmostEqual(np.exp(res["mu"]), self.transmissivity, 2) + sens = estimation.sens + for s_typ in self.s_types: + self.assertTrue(sens[s_typ]["mu"] > sens[s_typ]["dummy"]) + + def test_est_ext_thiem2D(self): + campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") + estimation = wtp.estimate.ExtThiem2D( + "est_ext_thiem2D", campaign, generate=True + ) + estimation.run() + res = estimation.estimated_para + estimation.sensitivity() + self.assertAlmostEqual(np.exp(res["mu"]), self.transmissivity, 2) + self.assertAlmostEqual(res["var"], 0.0, 0) + sens = estimation.sens + for s_typ in self.s_types: + self.assertTrue(sens[s_typ]["mu"] > sens[s_typ]["var"]) + self.assertTrue(sens[s_typ]["var"] > sens[s_typ]["len_scale"]) + + # def test_est_ext_thiem3D(self): + # campaign = wtp.load_campaign("Cmp_UFZ-campaign.cmp") + # estimation = wtp.estimate.ExtThiem3D( + # "est_ext_thiem3D", campaign, generate=True + # ) + # estimation.run() + # res = estimation.estimated_para + # estimation.sensitivity() + # self.assertAlmostEqual(np.exp(res["mu"]), self.transmissivity, 2) + # self.assertAlmostEqual(res["var"], 0.0, 0) + + def test_triangulate(self): + dist_mat = np.zeros((4, 4), dtype=float) + dist_mat[0, 1] = 3 # distance between well 0 and 1 + dist_mat[0, 2] = 4 # distance between well 0 and 2 + dist_mat[1, 2] = 2 # distance between well 1 and 2 + dist_mat[0, 3] = 1 # distance between well 0 and 3 + dist_mat[1, 3] = 3 # distance between well 1 and 3 + dist_mat[2, 3] = -1 # unknown distance between well 2 and 3 + dist_mat = sym(dist_mat) # make the distance matrix symmetric + well_const = triangulate(dist_mat, prec=0.1) + self.assertEqual(len(well_const), 4) + # plot all possible well constellations + plot_well_pos(well_const) + + +if __name__ == "__main__": + unittest.main() diff --git a/welltestpy/__init__.py b/welltestpy/__init__.py index c419c71..54b7e36 100644 --- a/welltestpy/__init__.py +++ b/welltestpy/__init__.py @@ -3,22 +3,64 @@ Purpose ======= -WellTestPy provides a framework to handle and plot data from well based -field campaigns as well as a data interpretation module. +welltestpy provides a framework to handle and plot data from well based +field campaigns as well as a parameter estimation module. Subpackages -=========== +^^^^^^^^^^^ .. autosummary:: data estimate process tools + +Classes +^^^^^^^ + +Campaign classes +~~~~~~~~~~~~~~~~ + +.. currentmodule:: welltestpy.data.campaignlib + +The following classes can be used to handle field campaigns. + +.. autosummary:: + Campaign + FieldSite + +Field Test classes +~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: welltestpy.data.testslib + +The following classes can be used to handle field test within a campaign. + +.. autosummary:: + PumpingTest + +Loading routines +^^^^^^^^^^^^^^^^ + +.. currentmodule:: welltestpy.data.data_io + +Campaign related loading routines + +.. autosummary:: + load_campaign """ -from __future__ import absolute_import +from . import data, estimate, process, tools + +try: + from ._version import __version__ +except ImportError: # pragma: nocover + # package is not installed + __version__ = "0.0.0.dev0" -from welltestpy._version import __version__ -from welltestpy import data, estimate, process, tools +from .data.campaignlib import Campaign, FieldSite +from .data.testslib import PumpingTest +from .data.data_io import load_campaign __all__ = ["__version__"] __all__ += ["data", "estimate", "process", "tools"] +__all__ += ["Campaign", "FieldSite", "PumpingTest", "load_campaign"] diff --git a/welltestpy/_version.py b/welltestpy/_version.py deleted file mode 100644 index b4e6a97..0000000 --- a/welltestpy/_version.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -"""Provide a central version""" -__version__ = "0.4.0.dev0" diff --git a/welltestpy/data/__init__.py b/welltestpy/data/__init__.py index d89889e..51bcbff 100644 --- a/welltestpy/data/__init__.py +++ b/welltestpy/data/__init__.py @@ -8,6 +8,7 @@ .. currentmodule:: welltestpy.data .. autosummary:: + data_io varlib testslib campaignlib @@ -59,7 +60,7 @@ Loading routines ~~~~~~~~~~~~~~~~ -.. currentmodule:: welltestpy.data.campaignlib +.. currentmodule:: welltestpy.data.data_io Campaign related loading routines @@ -67,15 +68,11 @@ load_campaign load_fieldsite -.. currentmodule:: welltestpy.data.testslib - Field test related loading routines .. autosummary:: load_test -.. currentmodule:: welltestpy.data.varlib - Variable related loading routines .. autosummary:: @@ -83,11 +80,9 @@ load_obs load_well """ -from __future__ import absolute_import +from . import varlib, testslib, campaignlib, data_io -from welltestpy.data import varlib, testslib, campaignlib - -from welltestpy.data.varlib import ( +from .varlib import ( Variable, TimeVar, HeadVar, @@ -98,16 +93,19 @@ DrawdownObs, StdyHeadObs, Well, - load_var, - load_obs, - load_well, ) -from welltestpy.data.testslib import PumpingTest, load_test -from welltestpy.data.campaignlib import ( +from .testslib import PumpingTest +from .campaignlib import ( FieldSite, Campaign, - load_fieldsite, +) +from .data_io import ( + load_var, + load_obs, + load_well, load_campaign, + load_fieldsite, + load_test, ) __all__ = [ @@ -121,16 +119,25 @@ "DrawdownObs", "StdyHeadObs", "Well", +] +__all__ += [ "PumpingTest", +] +__all__ += [ "FieldSite", "Campaign", +] +__all__ += [ "load_var", "load_obs", "load_well", "load_test", "load_fieldsite", "load_campaign", +] +__all__ += [ "varlib", "testslib", "campaignlib", + "data_io", ] diff --git a/welltestpy/data/campaignlib.py b/welltestpy/data/campaignlib.py index 5a7860b..4c8f744 100644 --- a/welltestpy/data/campaignlib.py +++ b/welltestpy/data/campaignlib.py @@ -8,39 +8,16 @@ .. autosummary:: FieldSite Campaign - load_fieldsite - load_campaign """ -from __future__ import absolute_import, division, print_function - from copy import deepcopy as dcopy -import os -import csv -import shutil -import zipfile -import tempfile -from io import TextIOWrapper as TxtIO - -import numpy as np - -from welltestpy.tools import BytIO -from welltestpy.tools.plotter import campaign_plot, campaign_well_plot -from welltestpy.data.varlib import ( - Variable, - CoordinatesVar, - load_var, - Well, - load_well, - _nextr, - _formstr, - _formname, -) -from welltestpy.data.testslib import Test, load_test - -__all__ = ["FieldSite", "Campaign", "load_fieldsite", "load_campaign"] - - -class FieldSite(object): + +from ..tools import plotter +from . import data_io, varlib, testslib + +__all__ = ["FieldSite", "Campaign"] + + +class FieldSite: """Class for a field site. This is a class for a field site. @@ -59,7 +36,7 @@ class FieldSite(object): """ def __init__(self, name, description="Field site", coordinates=None): - self.name = _formstr(name) + self.name = data_io._formstr(name) self.description = str(description) self._coordinates = None self.coordinates = coordinates @@ -75,29 +52,29 @@ def info(self): if self._coordinates is not None: info += self._coordinates.info + "\n" info += "----" + "\n" - # print("----") - # print("Field-site: "+str(self.name)) - # print("Description: "+str(self.description)) - # print("--") - # if hasattr(self, '_coordinates'): - # self._coordinates.info - # print("----") return info + @property + def pos(self): + """:class:`numpy.ndarray`: Position of the field site.""" + if self._coordinates is not None: + return self._coordinates.value + return None + @property def coordinates(self): """:class:`numpy.ndarray`: Coordinates of the field site.""" if self._coordinates is not None: - return self._coordinates.value + return self._coordinates return None @coordinates.setter def coordinates(self, coordinates): if coordinates is not None: - if isinstance(coordinates, Variable): + if isinstance(coordinates, varlib.Variable): self._coordinates = dcopy(coordinates) else: - self._coordinates = CoordinatesVar( + self._coordinates = varlib.CoordinatesVar( coordinates[0], coordinates[1] ) else: @@ -124,48 +101,10 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".fds"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Field_" + self.name - # ensure the name ends with '.csv' - if name[-4:] != ".fds": - name += ".fds" - name = _formname(name) - # create temporal directory for the included files - patht = tempfile.mkdtemp(dir=path) - # write the csv-file - # with open(patht+name[:-4]+".csv", 'w') as csvf: - with open(os.path.join(patht, "info.csv"), "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Fieldsite"]) - writer.writerow(["name", self.name]) - writer.writerow(["description", self.description]) - # define names for the variable-files - if self._coordinates is not None: - coordname = name[:-4] + "_CooVar.var" - # save variable-files - writer.writerow(["coordinates", coordname]) - self._coordinates.save(patht, coordname) - else: - writer.writerow(["coordinates", "None"]) - # compress everything to one zip-file - file_path = os.path.join(path, name) - with zipfile.ZipFile(file_path, "w") as zfile: - zfile.write(os.path.join(patht, "info.csv"), "info.csv") - if self._coordinates is not None: - zfile.write(os.path.join(patht, coordname), coordname) - # delete the temporary directory - shutil.rmtree(patht, ignore_errors=True) - return file_path - - -class Campaign(object): + return data_io.save_fieldsite(self, path, name) + + +class Campaign: """Class for a well based campaign. This is a class for a well based test campaign on a field site. @@ -203,7 +142,7 @@ def __init__( timeframe=None, description="Welltest campaign", ): - self.name = _formstr(name) + self.name = data_io._formstr(name) self.description = str(description) self._fieldsite = None self.fieldsite = fieldsite @@ -238,7 +177,7 @@ def wells(self, wells): if wells is not None: if isinstance(wells, dict): for k in wells.keys(): - if not isinstance(wells[k], Well): + if not isinstance(wells[k], varlib.Well): raise ValueError( "Campaign: some 'wells' are not of " + "type Well" ) @@ -250,9 +189,9 @@ def wells(self, wells): self.__wells = dcopy(wells) elif isinstance(wells, (list, tuple)): for wel in wells: - if not isinstance(wel, Well): + if not isinstance(wel, varlib.Well): raise ValueError( - "Campaign: some 'wells' " + "are not of type Well" + "Campaign: some 'wells' " + "are not of type u" ) self.__wells = {} for wel in wells: @@ -284,7 +223,7 @@ def add_well( aquiferdepth : :class:`Variable` or :class:`float`, optional Depth of the aquifer at the well. Default: ``"None"`` """ - well = Well(name, radius, coordinates, welldepth, aquiferdepth) + well = varlib.Well(name, radius, coordinates, welldepth, aquiferdepth) self.addwells(well) def addwells(self, wells): @@ -299,7 +238,7 @@ def addwells(self, wells): """ if isinstance(wells, dict): for k in wells.keys(): - if not isinstance(wells[k], Well): + if not isinstance(wells[k], varlib.Well): raise ValueError( "Campaign_addwells: some 'wells' " + "are not of type Well" @@ -318,7 +257,7 @@ def addwells(self, wells): self.__wells[k] = dcopy(wells[k]) elif isinstance(wells, (list, tuple)): for wel in wells: - if not isinstance(wel, Well): + if not isinstance(wel, varlib.Well): raise ValueError( "Campaign_addwells: some 'wells' " + "are not of type Well" @@ -330,7 +269,7 @@ def addwells(self, wells): ) for wel in wells: self.__wells[wel.name] = dcopy(wel) - elif isinstance(wells, Well): + elif isinstance(wells, varlib.Well): self.__wells[wells.name] = dcopy(wells) else: raise ValueError( @@ -367,7 +306,7 @@ def tests(self, tests): if tests is not None: if isinstance(tests, dict): for k in tests.keys(): - if not isinstance(tests[k], Test): + if not isinstance(tests[k], testslib.Test): raise ValueError( "Campaign: 'tests' are not of " + "type Test" ) @@ -379,14 +318,14 @@ def tests(self, tests): self.__tests = dcopy(tests) elif isinstance(tests, (list, tuple)): for tes in tests: - if not isinstance(tes, Test): + if not isinstance(tes, testslib.Test): raise ValueError( "Campaign: some 'tests' are not of " + "type Test" ) self.__tests = {} for tes in tests: self.__tests[tes.name] = dcopy(tes) - elif isinstance(tests, Test): + elif isinstance(tests, testslib.Test): self.__tests[tests.name] = dcopy(tests) else: raise ValueError( @@ -408,7 +347,7 @@ def addtests(self, tests): """ if isinstance(tests, dict): for k in tests.keys(): - if not isinstance(tests[k], Test): + if not isinstance(tests[k], testslib.Test): raise ValueError( "Campaign_addtests: some 'tests' " + "are not of type Test" @@ -427,7 +366,7 @@ def addtests(self, tests): self.__tests[k] = dcopy(tests[k]) elif isinstance(tests, (list, tuple)): for tes in tests: - if not isinstance(tes, Test): + if not isinstance(tes, testslib.Test): raise ValueError( "Campaign_addtests: some 'tests' " + "are not of type Test" @@ -439,7 +378,7 @@ def addtests(self, tests): ) for tes in tests: self.__tests[tes.name] = dcopy(tes) - elif isinstance(tests, Test): + elif isinstance(tests, testslib.Test): if tests.name in tuple(self.__tests.keys()): raise ValueError("Campaign.addtests: 'test' already present") self.__tests[tests.name] = dcopy(tests) @@ -484,7 +423,7 @@ def plot(self, select_tests=None, **kwargs): **kwargs Keyword-arguments forwarded to :any:`campaign_plot` """ - campaign_plot(self, select_tests, **kwargs) + return plotter.campaign_plot(self, select_tests, **kwargs) def plot_wells(self, **kwargs): """Generate a plot of the wells within the campaign. @@ -496,7 +435,7 @@ def plot_wells(self, **kwargs): **kwargs Keyword-arguments forwarded to :any:`campaign_well_plot`. """ - return campaign_well_plot(self, **kwargs) + return plotter.campaign_well_plot(self, **kwargs) def save(self, path="", name=None): """Save the campaign to file. @@ -515,141 +454,4 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".cmp"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Cmp_" + self.name - # ensure the name ends with '.csv' - if name[-4:] != ".cmp": - name += ".cmp" - name = _formname(name) - # create temporal directory for the included files - patht = tempfile.mkdtemp(dir=path) - # write the csv-file - # with open(patht+name[:-4]+".csv", 'w') as csvf: - with open(os.path.join(patht, "info.csv"), "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Campaign"]) - writer.writerow(["name", self.name]) - writer.writerow(["description", self.description]) - writer.writerow(["timeframe", self.timeframe]) - # define names for the variable-files - if self._fieldsite is not None: - fieldsname = name[:-4] + "_Fieldsite.fds" - # save variable-files - writer.writerow(["fieldsite", fieldsname]) - self._fieldsite.save(patht, fieldsname) - else: - writer.writerow(["fieldsite", "None"]) - - wkeys = tuple(self.wells.keys()) - writer.writerow(["Wells", len(wkeys)]) - wellsname = {} - for k in wkeys: - wellsname[k] = name[:-4] + "_" + k + "_Well.wel" - writer.writerow([k, wellsname[k]]) - self.wells[k].save(patht, wellsname[k]) - - tkeys = tuple(self.tests.keys()) - writer.writerow(["Tests", len(tkeys)]) - testsname = {} - for k in tkeys: - testsname[k] = name[:-4] + "_" + k + "_Test.tst" - writer.writerow([k, testsname[k]]) - self.tests[k].save(patht, testsname[k]) - - # compress everything to one zip-file - file_path = os.path.join(path, name) - with zipfile.ZipFile(file_path, "w") as zfile: - zfile.write(os.path.join(patht, "info.csv"), "info.csv") - if self._fieldsite is not None: - zfile.write(os.path.join(patht, fieldsname), fieldsname) - for k in wkeys: - zfile.write(os.path.join(patht, wellsname[k]), wellsname[k]) - for k in tkeys: - zfile.write(os.path.join(patht, testsname[k]), testsname[k]) - # delete the temporary directory - shutil.rmtree(patht, ignore_errors=True) - return file_path - - -def load_fieldsite(fdsfile): - """Load a field site from file. - - This reads a field site from a csv file. - - Parameters - ---------- - fdsfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(fdsfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - if next(data)[0] != "Fieldsite": - raise Exception - name = next(data)[1] - description = next(data)[1] - coordinfo = next(data)[1] - if coordinfo == "None": - coordinates = None - else: - coordinates = load_var(TxtIO(zfile.open(coordinfo))) - fieldsite = FieldSite(name, description, coordinates) - except Exception: - raise Exception( - "loadFieldSite: loading the fieldsite " + "was not possible" - ) - return fieldsite - - -def load_campaign(cmpfile): - """Load a campaign from file. - - This reads a campaign from a csv file. - - Parameters - ---------- - cmpfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(cmpfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - if next(data)[0] != "Campaign": - raise Exception - name = next(data)[1] - description = next(data)[1] - timeframe = next(data)[1] - row = _nextr(data) - if row[1] == "None": - fieldsite = None - else: - fieldsite = load_fieldsite(BytIO(zfile.read(row[1]))) - wcnt = np.int(next(data)[1]) - wells = {} - for __ in range(wcnt): - row = _nextr(data) - wells[row[0]] = load_well(BytIO(zfile.read(row[1]))) - - tcnt = np.int(next(data)[1]) - tests = {} - for __ in range(tcnt): - row = _nextr(data) - tests[row[0]] = load_test(BytIO(zfile.read(row[1]))) - - campaign = Campaign( - name, fieldsite, wells, tests, timeframe, description - ) - except Exception: - raise Exception( - "loadPumpingTest: loading the pumpingtest " + "was not possible" - ) - return campaign + return data_io.save_campaign(self, path, name) diff --git a/welltestpy/data/data_io.py b/welltestpy/data/data_io.py new file mode 100755 index 0000000..fd72630 --- /dev/null +++ b/welltestpy/data/data_io.py @@ -0,0 +1,737 @@ +# -*- coding: utf-8 -*- +""" +welltestpy subpackage providing input-output routines. + +.. currentmodule:: welltestpy.data.data_io + +The following functions are provided + +.. autosummary:: +""" +import os +import csv +import shutil +import zipfile +import tempfile +from io import TextIOWrapper as TxtIO, BytesIO as BytIO + +import numpy as np + +from . import varlib, campaignlib, testslib + + +# TOOLS ### + + +def _formstr(string): + # remove spaces, tabs, linebreaks and other separators + return "".join(str(string).split()) + + +def _formname(string): + # remove slashes + string = "".join(str(string).split(os.path.sep)) + # remove spaces, tabs, linebreaks and other separators + return _formstr(string) + + +def _nextr(data): + return tuple(filter(None, next(data))) + + +# SAVE ### + + +def save_var(var, path="", name=None): + """Save a variable to file. + + This writes the variable to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Var_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".var"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Var_" + var.name + # ensure the name ends with '.var' + if name[-4:] != ".var": + name += ".var" + name = _formname(name) + file_path = os.path.join(path, name) + # write the csv-file + with open(file_path, "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Variable"]) + writer.writerow(["name", var.name]) + writer.writerow(["symbol", var.symbol]) + writer.writerow(["units", var.units]) + writer.writerow(["description", var.description]) + if np.asanyarray(var.value).dtype == np.int: + writer.writerow(["integer"]) + else: + writer.writerow(["float"]) + if var.scalar: + writer.writerow(["scalar"]) + writer.writerow(["value", var.value]) + else: + writer.writerow(["shape"] + list(np.shape(var.value))) + tmpvalue = np.reshape(var.value, -1) + writer.writerow(["values", len(tmpvalue)]) + for val in tmpvalue: + writer.writerow([val]) + return file_path + + +def save_obs(obs, path="", name=None): + """Save an observation to file. + + This writes the observation to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Obs_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".obs"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Obs_" + obs.name + # ensure the name ends with '.obs' + if name[-4:] != ".obs": + name += ".obs" + name = _formname(name) + # create temporal directory for the included files + patht = tempfile.mkdtemp(dir=path) + # write the csv-file + # with open(patht+name[:-4]+".csv", 'w') as csvf: + with open(os.path.join(patht, "info.csv"), "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Observation"]) + writer.writerow(["name", obs.name]) + writer.writerow(["state", obs.state]) + writer.writerow(["description", obs.description]) + if obs.state == "steady": + obsname = name[:-4] + "_ObsVar.var" + writer.writerow(["observation", obsname]) + obs._observation.save(patht, obsname) + else: + timname = name[:-4] + "_TimVar.var" + obsname = name[:-4] + "_ObsVar.var" + writer.writerow(["time", timname]) + writer.writerow(["observation", obsname]) + obs._time.save(patht, timname) + obs._observation.save(patht, obsname) + # compress everything to one zip-file + file_path = os.path.join(path, name) + with zipfile.ZipFile(file_path, "w") as zfile: + # zfile.write(patht+name[:-4]+".csv", name[:-4]+".csv") + zfile.write(os.path.join(patht, "info.csv"), "info.csv") + if obs.state == "transient": + zfile.write(os.path.join(patht, timname), timname) + zfile.write(os.path.join(patht, obsname), obsname) + shutil.rmtree(patht, ignore_errors=True) + return file_path + + +def save_well(well, path="", name=None): + """Save a well to file. + + This writes the variable to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Well_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".wel"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Well_" + well.name + # ensure the name ends with '.csv' + if name[-4:] != ".wel": + name += ".wel" + name = _formname(name) + # create temporal directory for the included files + patht = tempfile.mkdtemp(dir=path) + # write the csv-file + # with open(patht+name[:-4]+".csv", 'w') as csvf: + with open(os.path.join(patht, "info.csv"), "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Well"]) + writer.writerow(["name", well.name]) + # define names for the variable-files + radiuname = name[:-4] + "_RadVar.var" + coordname = name[:-4] + "_CooVar.var" + welldname = name[:-4] + "_WedVar.var" + aquifname = name[:-4] + "_AqdVar.var" + # save variable-files + writer.writerow(["radius", radiuname]) + well.wellradius.save(patht, radiuname) + writer.writerow(["coordinates", coordname]) + well.coordinates.save(patht, coordname) + writer.writerow(["welldepth", welldname]) + well.welldepth.save(patht, welldname) + writer.writerow(["aquiferdepth", aquifname]) + well.aquiferdepth.save(patht, aquifname) + # compress everything to one zip-file + file_path = os.path.join(path, name) + with zipfile.ZipFile(file_path, "w") as zfile: + # zfile.write(patht+name[:-4]+".csv", name[:-4]+".csv") + zfile.write(os.path.join(patht, "info.csv"), "info.csv") + zfile.write(os.path.join(patht, radiuname), radiuname) + zfile.write(os.path.join(patht, coordname), coordname) + zfile.write(os.path.join(patht, welldname), welldname) + zfile.write(os.path.join(patht, aquifname), aquifname) + # delete the temporary directory + shutil.rmtree(patht, ignore_errors=True) + return file_path + + +def save_campaign(campaign, path="", name=None): + """Save the campaign to file. + + This writes the campaign to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Cmp_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".cmp"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Cmp_" + campaign.name + # ensure the name ends with '.csv' + if name[-4:] != ".cmp": + name += ".cmp" + name = _formname(name) + # create temporal directory for the included files + patht = tempfile.mkdtemp(dir=path) + # write the csv-file + # with open(patht+name[:-4]+".csv", 'w') as csvf: + with open(os.path.join(patht, "info.csv"), "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Campaign"]) + writer.writerow(["name", campaign.name]) + writer.writerow(["description", campaign.description]) + writer.writerow(["timeframe", campaign.timeframe]) + # define names for the variable-files + if campaign.fieldsite is not None: + fieldsname = name[:-4] + "_Fieldsite.fds" + # save variable-files + writer.writerow(["fieldsite", fieldsname]) + campaign.fieldsite.save(patht, fieldsname) + else: + writer.writerow(["fieldsite", "None"]) + + wkeys = tuple(campaign.wells.keys()) + writer.writerow(["Wells", len(wkeys)]) + wellsname = {} + for k in wkeys: + wellsname[k] = name[:-4] + "_" + k + "_Well.wel" + writer.writerow([k, wellsname[k]]) + campaign.wells[k].save(patht, wellsname[k]) + + tkeys = tuple(campaign.tests.keys()) + writer.writerow(["Tests", len(tkeys)]) + testsname = {} + for k in tkeys: + testsname[k] = name[:-4] + "_" + k + "_Test.tst" + writer.writerow([k, testsname[k]]) + campaign.tests[k].save(patht, testsname[k]) + + # compress everything to one zip-file + file_path = os.path.join(path, name) + with zipfile.ZipFile(file_path, "w") as zfile: + zfile.write(os.path.join(patht, "info.csv"), "info.csv") + if campaign.fieldsite is not None: + zfile.write(os.path.join(patht, fieldsname), fieldsname) + for k in wkeys: + zfile.write(os.path.join(patht, wellsname[k]), wellsname[k]) + for k in tkeys: + zfile.write(os.path.join(patht, testsname[k]), testsname[k]) + # delete the temporary directory + shutil.rmtree(patht, ignore_errors=True) + return file_path + + +def save_fieldsite(fieldsite, path="", name=None): + """Save a field site to file. + + This writes the field site to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Field_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".fds"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Field_" + fieldsite.name + # ensure the name ends with '.csv' + if name[-4:] != ".fds": + name += ".fds" + name = _formname(name) + # create temporal directory for the included files + patht = tempfile.mkdtemp(dir=path) + # write the csv-file + # with open(patht+name[:-4]+".csv", 'w') as csvf: + with open(os.path.join(patht, "info.csv"), "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Fieldsite"]) + writer.writerow(["name", fieldsite.name]) + writer.writerow(["description", fieldsite.description]) + # define names for the variable-files + if fieldsite.coordinates is not None: + coordname = name[:-4] + "_CooVar.var" + # save variable-files + writer.writerow(["coordinates", coordname]) + fieldsite.coordinates.save(patht, coordname) + else: + writer.writerow(["coordinates", "None"]) + # compress everything to one zip-file + file_path = os.path.join(path, name) + with zipfile.ZipFile(file_path, "w") as zfile: + zfile.write(os.path.join(patht, "info.csv"), "info.csv") + if fieldsite.coordinates is not None: + zfile.write(os.path.join(patht, coordname), coordname) + # delete the temporary directory + shutil.rmtree(patht, ignore_errors=True) + return file_path + + +def save_pumping_test(pump_test, path="", name=None): + """Save a pumping test to file. + + This writes the variable to a csv file. + + Parameters + ---------- + path : :class:`str`, optional + Path where the variable should be saved. Default: ``""`` + name : :class:`str`, optional + Name of the file. If ``None``, the name will be generated by + ``"Test_"+name``. Default: ``None`` + + Notes + ----- + The file will get the suffix ``".tst"``. + """ + path = os.path.normpath(path) + # create the path if not existing + if not os.path.exists(path): + os.makedirs(path) + # create a standard name if None is given + if name is None: + name = "Test_" + pump_test.name + # ensure the name ends with '.csv' + if name[-4:] != ".tst": + name += ".tst" + name = _formname(name) + # create temporal directory for the included files + patht = tempfile.mkdtemp(dir=path) + # write the csv-file + # with open(patht+name[:-4]+".csv", 'w') as csvf: + with open(os.path.join(patht, "info.csv"), "w") as csvf: + writer = csv.writer( + csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" + ) + writer.writerow(["Testtype", "PumpingTest"]) + writer.writerow(["name", pump_test.name]) + writer.writerow(["description", pump_test.description]) + writer.writerow(["timeframe", pump_test.timeframe]) + writer.writerow(["pumpingwell", pump_test.pumpingwell]) + # define names for the variable-files (file extension added autom.) + pumprname = name[:-4] + "_PprVar" + aquidname = name[:-4] + "_AqdVar" + aquirname = name[:-4] + "_AqrVar" + # save variable-files + pumpr_path = pump_test.pumpingrate.save(patht, pumprname) + pumpr_base = os.path.basename(pumpr_path) + writer.writerow(["pumpingrate", pumpr_base]) + aquid_path = pump_test.aquiferdepth.save(patht, aquidname) + aquid_base = os.path.basename(aquid_path) + writer.writerow(["aquiferdepth", aquid_base]) + aquir_path = pump_test.aquiferradius.save(patht, aquirname) + aquir_base = os.path.basename(aquir_path) + writer.writerow(["aquiferradius", aquir_base]) + okeys = tuple(pump_test.observations.keys()) + writer.writerow(["Observations", len(okeys)]) + obsname = {} + for k in okeys: + obsname[k] = name[:-4] + "_" + k + "_Obs.obs" + writer.writerow([k, obsname[k]]) + pump_test.observations[k].save(patht, obsname[k]) + # compress everything to one zip-file + file_path = os.path.join(path, name) + with zipfile.ZipFile(file_path, "w") as zfile: + zfile.write(os.path.join(patht, "info.csv"), "info.csv") + zfile.write(pumpr_path, pumpr_base) + zfile.write(aquir_path, aquir_base) + zfile.write(aquid_path, aquid_base) + for k in okeys: + zfile.write(os.path.join(patht, obsname[k]), obsname[k]) + # delete the temporary directory + shutil.rmtree(patht, ignore_errors=True) + return file_path + + +# LOAD ### + + +def load_var(varfile): + """Load a variable from file. + + This reads a variable from a csv file. + + Parameters + ---------- + varfile : :class:`str` + Path to the file + """ + try: + with open(varfile, "r") as vfile: + data = csv.reader(vfile) + if next(data)[0] != "Variable": + raise Exception + name = next(data)[1] + symbol = next(data)[1] + units = next(data)[1] + description = next(data)[1] + integer = next(data)[0] == "integer" + shapenfo = _nextr(data) + if shapenfo[0] == "scalar": + if integer: + value = np.int(next(data)[1]) + else: + value = np.float(next(data)[1]) + else: + shape = tuple(np.array(shapenfo[1:], dtype=np.int)) + vcnt = np.int(next(data)[1]) + vlist = [] + for __ in range(vcnt): + vlist.append(next(data)[0]) + if integer: + value = np.array(vlist, dtype=np.int).reshape(shape) + else: + value = np.array(vlist, dtype=np.float).reshape(shape) + + var = varlib.Variable(name, value, symbol, units, description) + except Exception: + try: + data = csv.reader(varfile) + if next(data)[0] != "Variable": + raise Exception + name = next(data)[1] + symbol = next(data)[1] + units = next(data)[1] + description = next(data)[1] + integer = next(data)[0] == "integer" + shapenfo = _nextr(data) + if shapenfo[0] == "scalar": + if integer: + value = np.int(next(data)[1]) + else: + value = np.float(next(data)[1]) + else: + shape = tuple(np.array(shapenfo[1:], dtype=np.int)) + vcnt = np.int(next(data)[1]) + vlist = [] + for __ in range(vcnt): + vlist.append(next(data)[0]) + if integer: + value = np.array(vlist, dtype=np.int).reshape(shape) + else: + value = np.array(vlist, dtype=np.float).reshape(shape) + + var = varlib.Variable(name, value, symbol, units, description) + except Exception: + raise Exception("loadVar: loading the variable was not possible") + return var + + +def load_obs(obsfile): + """Load an observation from file. + + This reads a observation from a csv file. + + Parameters + ---------- + obsfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(obsfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + if next(data)[0] != "Observation": + raise Exception + name = next(data)[1] + steady = next(data)[1] == "steady" + description = next(data)[1] + if not steady: + timef = next(data)[1] + obsf = next(data)[1] + + if not steady: + time = load_var(TxtIO(zfile.open(timef))) + else: + time = None + + obs = load_var(TxtIO(zfile.open(obsf))) + + observation = varlib.Observation(name, obs, time, description) + except Exception: + raise Exception("loadObs: loading the observation was not possible") + return observation + + +def load_well(welfile): + """Load a well from file. + + This reads a well from a csv file. + + Parameters + ---------- + welfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(welfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + if next(data)[0] != "Well": + raise Exception + name = next(data)[1] + radf = next(data)[1] + coordf = next(data)[1] + welldf = next(data)[1] + aquidf = next(data)[1] + + rad = load_var(TxtIO(zfile.open(radf))) + coord = load_var(TxtIO(zfile.open(coordf))) + welld = load_var(TxtIO(zfile.open(welldf))) + aquid = load_var(TxtIO(zfile.open(aquidf))) + + well = varlib.Well(name, rad, coord, welld, aquid) + except Exception: + raise Exception("loadWell: loading the well was not possible") + return well + + +def load_campaign(cmpfile): + """Load a campaign from file. + + This reads a campaign from a csv file. + + Parameters + ---------- + cmpfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(cmpfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + if next(data)[0] != "Campaign": + raise Exception + name = next(data)[1] + description = next(data)[1] + timeframe = next(data)[1] + row = _nextr(data) + if row[1] == "None": + fieldsite = None + else: + fieldsite = load_fieldsite(BytIO(zfile.read(row[1]))) + wcnt = np.int(next(data)[1]) + wells = {} + for __ in range(wcnt): + row = _nextr(data) + wells[row[0]] = load_well(BytIO(zfile.read(row[1]))) + + tcnt = np.int(next(data)[1]) + tests = {} + for __ in range(tcnt): + row = _nextr(data) + tests[row[0]] = load_test(BytIO(zfile.read(row[1]))) + + campaign = campaignlib.Campaign( + name, fieldsite, wells, tests, timeframe, description + ) + except Exception: + raise Exception( + "loadPumpingTest: loading the pumpingtest " + "was not possible" + ) + return campaign + + +def load_fieldsite(fdsfile): + """Load a field site from file. + + This reads a field site from a csv file. + + Parameters + ---------- + fdsfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(fdsfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + if next(data)[0] != "Fieldsite": + raise Exception + name = next(data)[1] + description = next(data)[1] + coordinfo = next(data)[1] + if coordinfo == "None": + coordinates = None + else: + coordinates = load_var(TxtIO(zfile.open(coordinfo))) + fieldsite = campaignlib.FieldSite(name, description, coordinates) + except Exception: + raise Exception( + "loadFieldSite: loading the fieldsite " + "was not possible" + ) + return fieldsite + + +def load_test(tstfile): + """Load a test from file. + + This reads a test from a csv file. + + Parameters + ---------- + tstfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(tstfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + row = _nextr(data) + if row[0] != "Testtype": + raise Exception + if row[1] == "PumpingTest": + routine = _load_pumping_test + else: + raise Exception + except Exception: + raise Exception("loadTest: loading the test " + "was not possible") + + return routine(tstfile) + + +def _load_pumping_test(tstfile): + """Load a pumping test from file. + + This reads a pumping test from a csv file. + + Parameters + ---------- + tstfile : :class:`str` + Path to the file + """ + try: + with zipfile.ZipFile(tstfile, "r") as zfile: + info = TxtIO(zfile.open("info.csv")) + data = csv.reader(info) + if next(data)[1] != "PumpingTest": + raise Exception + name = next(data)[1] + description = next(data)[1] + timeframe = next(data)[1] + pumpingwell = next(data)[1] + rate_raw = TxtIO(zfile.open(next(data)[1])) + try: + pumpingrate = load_var(rate_raw) + except Exception: + pumpingrate = load_obs(rate_raw) + aquiferdepth = load_var(TxtIO(zfile.open(next(data)[1]))) + aquiferradius = load_var(TxtIO(zfile.open(next(data)[1]))) + obscnt = np.int(next(data)[1]) + observations = {} + for __ in range(obscnt): + row = _nextr(data) + observations[row[0]] = load_obs(BytIO(zfile.read(row[1]))) + + pumpingtest = testslib.PumpingTest( + name, + pumpingwell, + pumpingrate, + observations, + aquiferdepth, + aquiferradius, + description, + timeframe, + ) + except Exception: + raise Exception( + "loadPumpingTest: loading the pumpingtest " + "was not possible" + ) + return pumpingtest diff --git a/welltestpy/data/testslib.py b/welltestpy/data/testslib.py index f861c87..239f661 100644 --- a/welltestpy/data/testslib.py +++ b/welltestpy/data/testslib.py @@ -9,40 +9,19 @@ .. autosummary:: Test PumpingTest - load_test """ -from __future__ import absolute_import, division, print_function - from copy import deepcopy as dcopy -import os -import csv -import shutil -import zipfile -import tempfile -from io import TextIOWrapper as TxtIO import numpy as np -from welltestpy.tools import BytIO -from welltestpy.tools.plotter import plot_pump_test -from welltestpy.data.varlib import ( - Variable, - Observation, - StdyHeadObs, - DrawdownObs, - load_var, - load_obs, - _nextr, - _formstr, - _formname, -) -import welltestpy as wtp - +from ..tools import plotter +from . import varlib, data_io +from ..process import processlib -__all__ = ["Test", "PumpingTest", "load_test"] +__all__ = ["Test", "PumpingTest"] -class Test(object): +class Test: """General class for a well based test. This is a class for a well based test on a field site. @@ -61,7 +40,7 @@ class Test(object): """ def __init__(self, name, description="no description", timeframe=None): - self.name = _formstr(name) + self.name = data_io._formstr(name) self.description = str(description) self.timeframe = str(timeframe) self._testtype = "Test" @@ -77,6 +56,28 @@ def testtype(self): """:class:`str`: String containing the test type.""" return self._testtype + def plot(self, wells, exclude=None, fig=None, ax=None, **kwargs): + """Generate a plot of the pumping test. + + This will plot the test on the given figure axes. + + Parameters + ---------- + ax : :class:`Axes` + Axes where the plot should be done. + wells : :class:`dict` + Dictonary containing the well classes sorted by name. + exclude: :class:`list`, optional + List of wells that should be excluded from the plot. + Default: ``None`` + + Notes + ----- + This will be used by the Campaign class. + """ + # update ax (or create it if None) and return it + return ax + class PumpingTest(Test): """Class for a pumping test. @@ -125,69 +126,19 @@ def __init__( description="Pumpingtest", timeframe=None, ): - super(PumpingTest, self).__init__(name, description, timeframe) + super().__init__(name, description, timeframe) + self._pumpingrate = None + self._aquiferdepth = None + self._aquiferradius = None + self.__observations = {} self._testtype = "PumpingTest" self.pumpingwell = str(pumpingwell) - - if isinstance(pumpingrate, Variable): - self._pumpingrate = dcopy(pumpingrate) - elif isinstance(pumpingrate, Observation): - self._pumpingrate = dcopy(pumpingrate) - else: - self._pumpingrate = Variable( - "pumpingrate", - pumpingrate, - "Q", - "m^3/s", - "Pumpingrate at test '" + self.name + "'", - ) - if isinstance(self._pumpingrate, Variable) and not self.constant_rate: - raise ValueError("PumpingTest: 'pumpingrate' not scalar") - if ( - isinstance(self._pumpingrate, Observation) - and self._pumpingrate.state == "steady" - and not self.constant_rate - ): - raise ValueError("PumpingTest: 'pumpingrate' not scalar") - - if isinstance(aquiferdepth, Variable): - self._aquiferdepth = dcopy(aquiferdepth) - else: - self._aquiferdepth = Variable( - "aquiferdepth", - aquiferdepth, - "L_a", - "m", - "mean aquiferdepth for test '" + str(name) + "'", - ) - if not self._aquiferdepth.scalar: - raise ValueError("PumpingTest: 'aquiferdepth' needs to be scalar") - if self.aquiferdepth <= 0.0: - raise ValueError("PumpingTest: 'aquiferdepth' needs to be positiv") - - if isinstance(aquiferradius, Variable): - self._aquiferradius = dcopy(aquiferradius) - else: - self._aquiferradius = Variable( - "aquiferradius", - aquiferradius, - "R", - "m", - "mean aquiferradius for test '" + str(name) + "'", - ) - if not self._aquiferradius.scalar: - raise ValueError("PumpingTest: 'aquiferradius' needs to be scalar") - if self.aquiferradius <= 0.0: - raise ValueError( - "PumpingTest: 'aquiferradius' " + "needs to be positiv" - ) - - if observations is None: - self.__observations = {} - else: - self.observations = observations + self.pumpingrate = pumpingrate + self.aquiferdepth = aquiferdepth + self.aquiferradius = aquiferradius + self.observations = observations def make_steady(self, time="latest"): """ @@ -216,13 +167,13 @@ def make_steady(self, time="latest"): tout = float(time) for obs in self.observations: if self.observations[obs].state == "transient": - wtp.process.filterdrawdown(self.observations[obs], tout=tout) + processlib.filterdrawdown(self.observations[obs], tout=tout) del self.observations[obs].time if ( - isinstance(self._pumpingrate, Observation) + isinstance(self._pumpingrate, varlib.Observation) and self._pumpingrate.state == "transient" ): - wtp.process.filterdrawdown(self._pumpingrate, tout=tout) + processlib.filterdrawdown(self._pumpingrate, tout=tout) del self._pumpingrate.time def state(self, wells=None): @@ -268,60 +219,100 @@ def observationwells(self): @property def constant_rate(self): """:class:`bool`: state if this is a constant rate test.""" - return np.isscalar(self.pumpingrate) + return np.isscalar(self.rate) @property - def pumpingrate(self): + def rate(self): """:class:`float`: pumping rate at the pumping well.""" return self._pumpingrate.value + @property + def pumpingrate(self): + """:class:`float`: pumping rate variable at the pumping well.""" + return self._pumpingrate + @pumpingrate.setter def pumpingrate(self, pumpingrate): - tmp = dcopy(self._pumpingrate) - if isinstance(pumpingrate, Variable): + if isinstance(pumpingrate, (varlib.Variable, varlib.Observation)): self._pumpingrate = dcopy(pumpingrate) + elif self._pumpingrate is None: + self._pumpingrate = varlib.Variable( + "pumpingrate", + pumpingrate, + "Q", + "m^3/s", + "Pumpingrate at test '" + self.name + "'", + ) else: self._pumpingrate(pumpingrate) - if not self._pumpingrate.scalar: - self._pumpingrate = dcopy(tmp) - raise ValueError("PumpingTest: 'pumpingrate' needs to be scalar") + if ( + isinstance(self._pumpingrate, varlib.Variable) + and not self.constant_rate + ): + raise ValueError("PumpingTest: 'pumpingrate' not scalar") + if ( + isinstance(self._pumpingrate, varlib.Observation) + and self._pumpingrate.state == "steady" + and not self.constant_rate + ): + raise ValueError("PumpingTest: 'pumpingrate' not scalar") @property - def aquiferdepth(self): + def depth(self): """:class:`float`: aquifer depth at the field site.""" return self._aquiferdepth.value + @property + def aquiferdepth(self): + """:class:`float`: aquifer depth at the field site.""" + return self._aquiferdepth + @aquiferdepth.setter def aquiferdepth(self, aquiferdepth): - tmp = dcopy(self._aquiferdepth) - if isinstance(aquiferdepth, Variable): + if isinstance(aquiferdepth, varlib.Variable): self._aquiferdepth = dcopy(aquiferdepth) + elif self._aquiferdepth is None: + self._aquiferdepth = varlib.Variable( + "aquiferdepth", + aquiferdepth, + "L_a", + "m", + "mean aquiferdepth for test '" + str(self.name) + "'", + ) else: self._aquiferdepth(aquiferdepth) if not self._aquiferdepth.scalar: - self._aquiferdepth = dcopy(tmp) raise ValueError("PumpingTest: 'aquiferdepth' needs to be scalar") - if self.aquiferdepth <= 0.0: - self._aquiferdepth = dcopy(tmp) + if self.depth <= 0.0: raise ValueError("PumpingTest: 'aquiferdepth' needs to be positiv") @property - def aquiferradius(self): + def radius(self): """:class:`float`: aquifer radius at the field site.""" return self._aquiferradius.value + @property + def aquiferradius(self): + """:class:`float`: aquifer radius at the field site.""" + return self._aquiferradius + @aquiferradius.setter def aquiferradius(self, aquiferradius): - tmp = dcopy(self._aquiferradius) - if isinstance(aquiferradius, Variable): + if isinstance(aquiferradius, varlib.Variable): self._aquiferradius = dcopy(aquiferradius) + elif self._aquiferradius is None: + self._aquiferradius = varlib.Variable( + "aquiferradius", + aquiferradius, + "R", + "m", + "mean aquiferradius for test '" + str(self.name) + "'", + ) else: self._aquiferradius(aquiferradius) if not self._aquiferradius.scalar: - self._aquiferradius = dcopy(tmp) raise ValueError("PumpingTest: 'aquiferradius' needs to be scalar") - if self.aquiferradius <= 0.0: - self._aquiferradius = dcopy(tmp) + if self.radius <= 0.0: raise ValueError( "PumpingTest: 'aquiferradius' " + "needs to be positiv" ) @@ -333,22 +324,9 @@ def observations(self): @observations.setter def observations(self, obs): + self.__observations = {} if obs is not None: - if isinstance(obs, dict): - for k in obs.keys(): - if not isinstance(obs[k], Observation): - raise ValueError( - "PumpingTest: some 'observations' " - + "are not of type Observation" - ) - self.__observations = dcopy(obs) - else: - raise ValueError( - "PumpingTest: 'observations' should" - + " be given as dictonary" - ) - else: - self.__observations = {} + self.add_observations(obs) def add_steady_obs( self, @@ -368,8 +346,8 @@ def add_steady_obs( description : :class:`str`, optional Description of the Variable. Default: ``"Steady observation"`` """ - obs = StdyHeadObs(well, observation, description) - self.addobservations(obs) + obs = varlib.StdyHeadObs(well, observation, description) + self.add_observations(obs) def add_transient_obs( self, @@ -392,48 +370,57 @@ def add_transient_obs( description : :class:`str`, optional Description of the Variable. Default: ``"Drawdown observation"`` """ - obs = DrawdownObs(well, time, observation, description) - self.addobservations(obs) + obs = varlib.DrawdownObs(well, observation, time, description) + self.add_observations(obs) - def addobservations(self, obs): + def add_observations(self, obs): """Add some specified observations. - This will add observations to the pumping test. - Parameters ---------- - obs : :class:`dict` + obs : :class:`dict`, :class:`list`, :class:`Observation` Observations to be added. """ if isinstance(obs, dict): for k in obs: - if not isinstance(obs[k], Observation): + if not isinstance(obs[k], varlib.Observation): raise ValueError( - "PumpingTest_addobservations: some " + "PumpingTest_add_observations: some " + "'observations' are not " + "of type Observation" ) if k in self.observations: raise ValueError( - "PumpingTest_addobservations: some " + "PumpingTest_add_observations: some " + "'observations' are already present" ) for k in obs: self.__observations[k] = dcopy(obs[k]) - elif isinstance(obs, Observation): + elif isinstance(obs, varlib.Observation): if obs in self.observations: raise ValueError( - "PumpingTest_addobservations: " + "PumpingTest_add_observations: " + "'observation' are already present" ) self.__observations[obs.name] = dcopy(obs) else: - raise ValueError( - "PumpingTest_addobservations: 'observations' " - + "should be given as dictonary with well as key" - ) + try: + iter(obs) + except TypeError: + raise ValueError( + "PumpingTest_add_observations: 'obs' can't be read." + ) + else: + for ob in obs: + if not isinstance(ob, varlib.Observation): + raise ValueError( + "PumpingTest_add_observations: some " + + "'observations' are not " + + "of type Observation" + ) + self.__observations[ob.name] = dcopy(ob) - def delobservations(self, obs): + def del_observations(self, obs): """Delete some specified observations. This will delete observations from the pumping test. You can give a @@ -471,7 +458,7 @@ def plot(self, wells, exclude=None, fig=None, ax=None, **kwargs): ----- This will be used by the Campaign class. """ - plot_pump_test( + return plotter.plot_pump_test( pump_test=self, wells=wells, exclude=exclude, @@ -497,137 +484,4 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".tst"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Test_" + self.name - # ensure the name ends with '.csv' - if name[-4:] != ".tst": - name += ".tst" - name = _formname(name) - # create temporal directory for the included files - patht = tempfile.mkdtemp(dir=path) - # write the csv-file - # with open(patht+name[:-4]+".csv", 'w') as csvf: - with open(os.path.join(patht, "info.csv"), "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Testtype", "PumpingTest"]) - writer.writerow(["name", self.name]) - writer.writerow(["description", self.description]) - writer.writerow(["timeframe", self.timeframe]) - writer.writerow(["pumpingwell", self.pumpingwell]) - # define names for the variable-files (file extension added autom.) - pumprname = name[:-4] + "_PprVar" - aquidname = name[:-4] + "_AqdVar" - aquirname = name[:-4] + "_AqrVar" - # save variable-files - pumpr_path = self._pumpingrate.save(patht, pumprname) - pumpr_base = os.path.basename(pumpr_path) - writer.writerow(["pumpingrate", pumpr_base]) - aquid_path = self._aquiferdepth.save(patht, aquidname) - aquid_base = os.path.basename(aquid_path) - writer.writerow(["aquiferdepth", aquid_base]) - aquir_path = self._aquiferradius.save(patht, aquirname) - aquir_base = os.path.basename(aquir_path) - writer.writerow(["aquiferradius", aquir_base]) - okeys = tuple(self.observations.keys()) - writer.writerow(["Observations", len(okeys)]) - obsname = {} - for k in okeys: - obsname[k] = name[:-4] + "_" + k + "_Obs.obs" - writer.writerow([k, obsname[k]]) - self.observations[k].save(patht, obsname[k]) - # compress everything to one zip-file - file_path = os.path.join(path, name) - with zipfile.ZipFile(file_path, "w") as zfile: - zfile.write(os.path.join(patht, "info.csv"), "info.csv") - zfile.write(pumpr_path, pumpr_base) - zfile.write(aquir_path, aquir_base) - zfile.write(aquid_path, aquid_base) - for k in okeys: - zfile.write(os.path.join(patht, obsname[k]), obsname[k]) - # delete the temporary directory - shutil.rmtree(patht, ignore_errors=True) - return file_path - - -def load_test(tstfile): - """Load a test from file. - - This reads a test from a csv file. - - Parameters - ---------- - tstfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(tstfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - row = _nextr(data) - if row[0] != "Testtype": - raise Exception - if row[1] == "PumpingTest": - routine = _load_pumping_test - else: - raise Exception - except Exception: - raise Exception("loadTest: loading the test " + "was not possible") - - return routine(tstfile) - - -def _load_pumping_test(tstfile): - """Load a pumping test from file. - - This reads a pumping test from a csv file. - - Parameters - ---------- - tstfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(tstfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - if next(data)[1] != "PumpingTest": - raise Exception - name = next(data)[1] - description = next(data)[1] - timeframe = next(data)[1] - pumpingwell = next(data)[1] - rate_raw = TxtIO(zfile.open(next(data)[1])) - try: - pumpingrate = load_var(rate_raw) - except Exception: - pumpingrate = load_obs(rate_raw) - aquiferdepth = load_var(TxtIO(zfile.open(next(data)[1]))) - aquiferradius = load_var(TxtIO(zfile.open(next(data)[1]))) - obscnt = np.int(next(data)[1]) - observations = {} - for __ in range(obscnt): - row = _nextr(data) - observations[row[0]] = load_obs(BytIO(zfile.read(row[1]))) - - pumpingtest = PumpingTest( - name, - pumpingwell, - pumpingrate, - observations, - aquiferdepth, - aquiferradius, - description, - timeframe, - ) - except Exception: - raise Exception( - "loadPumpingTest: loading the pumpingtest " + "was not possible" - ) - return pumpingtest + return data_io.save_pumping_test(self, path, name) diff --git a/welltestpy/data/varlib.py b/welltestpy/data/varlib.py index 6010724..0a7473e 100644 --- a/welltestpy/data/varlib.py +++ b/welltestpy/data/varlib.py @@ -18,22 +18,13 @@ StdyHeadObs TimeSeries Well - load_var - load_obs - load_well """ -from __future__ import absolute_import, division, print_function - from copy import deepcopy as dcopy -import os -import csv -import shutil -import zipfile -import tempfile -from io import TextIOWrapper as TxtIO import numpy as np +from . import data_io + __all__ = [ "Variable", @@ -47,13 +38,10 @@ "StdyHeadObs", "TimeSeries", "Well", - "load_var", - "load_obs", - "load_well", ] -class Variable(object): +class Variable: """Class for a variable. This is a class for a physical variable which is either a scalar or an @@ -78,7 +66,7 @@ class Variable(object): def __init__( self, name, value, symbol="x", units="-", description="no description" ): - self.name = _formstr(name) + self.name = data_io._formstr(name) self.__value = None self.value = value self.symbol = str(symbol) @@ -114,11 +102,6 @@ def info(self): info += " -Symbol: " + str(self.symbol) + "\n" info += " -Units: " + str(self.units) + "\n" info += " -Description: " + str(self.description) + "\n" - # print(" Variable-name: "+str(self.name)) - # print(" -Value: "+str(self.value)) - # print(" -Symbol: "+str(self.symbol)) - # print(" -Units: "+str(self.units)) - # print(" -Description: "+str(self.description)) return info @property @@ -184,42 +167,7 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".var"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Var_" + self.name - # ensure the name ends with '.var' - if name[-4:] != ".var": - name += ".var" - name = _formname(name) - file_path = os.path.join(path, name) - # write the csv-file - with open(file_path, "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Variable"]) - writer.writerow(["name", self.name]) - writer.writerow(["symbol", self.symbol]) - writer.writerow(["units", self.units]) - writer.writerow(["description", self.description]) - if np.asanyarray(self.__value).dtype == np.int: - writer.writerow(["integer"]) - else: - writer.writerow(["float"]) - if self.scalar: - writer.writerow(["scalar"]) - writer.writerow(["value", self.value]) - else: - writer.writerow(["shape"] + list(np.shape(self.value))) - tmpvalue = np.reshape(self.value, -1) - writer.writerow(["values", len(tmpvalue)]) - for val in tmpvalue: - writer.writerow([val]) - return file_path + return data_io.save_var(self, path, name) class TimeVar(Variable): @@ -245,9 +193,7 @@ class TimeVar(Variable): def __init__( self, value, symbol="t", units="s", description="time given in seconds" ): - super(TimeVar, self).__init__( - "time", value, symbol, units, description - ) + super().__init__("time", value, symbol, units, description) if np.ndim(self.value) > 1: raise ValueError( "TimeVar: 'time' should have " + "at most one dimension" @@ -277,9 +223,7 @@ class HeadVar(Variable): def __init__( self, value, symbol="h", units="m", description="head given in meters" ): - super(HeadVar, self).__init__( - "head", value, symbol, units, description - ) + super().__init__("head", value, symbol, units, description) class TemporalVar(Variable): @@ -294,9 +238,7 @@ class TemporalVar(Variable): """ def __init__(self, value=0.0): - super(TemporalVar, self).__init__( - "temporal", value, description="temporal variable" - ) + super().__init__("temporal", value, description="temporal variable") class CoordinatesVar(Variable): @@ -346,12 +288,10 @@ def __init__( value = np.array([ilat, ilon]).T - super(CoordinatesVar, self).__init__( - "coordinates", value, symbol, units, description - ) + super().__init__("coordinates", value, symbol, units, description) -class Observation(object): +class Observation: """ Class for a observation. @@ -362,62 +302,41 @@ class Observation(object): ---------- name : :class:`str` Name of the Variable. - time : :class:`Variable` - Value of the Variable. observation : :class:`Variable` Name of the Variable. Default: ``"x"`` + time : :class:`Variable` + Value of the Variable. description : :class:`str`, optional Description of the Variable. Default: ``"Observation"`` """ - def __init__(self, name, time, observation, description="Observation"): + def __init__( + self, name, observation, time=None, description="Observation" + ): self.__it = None self.__itfinished = None - self.name = _formstr(name) + self._time = None + self._observation = None + self.name = data_io._formstr(name) self.description = str(description) - if isinstance(observation, Variable): - self._observation = dcopy(observation) - else: - raise ValueError( - "Observation: " - + "'observation' must be instance of 'variable'" - ) - - if time is not None: - if isinstance(time, Variable): - self._time = dcopy(time) - else: - self._time = TimeVar(time) - - self.__state = "transient" - else: - self.__state = "steady" + self._setobservation(observation) + self._settime(time) self._checkshape() - def __call__(self, in1=None, in2=None, time=None, observation=None): + def __call__(self, observation=None, time=None): """Call a variable. Here you can set a new value or you can get the value of the variable. Parameters ---------- - in1 : :class:`int` or :class:`float` or :class:`numpy.ndarray` or - :class:`Variable`, optional - New Value for time (if transient) or observation (if steady). - Default: ``"None"`` - in2 : :class:`int` or :class:`float` or :class:`numpy.ndarray` or - :class:`Variable`, optional - New Value for observation (if transient). + observation : scalar, :class:`numpy.ndarray`, :class:`Variable`, optional + New Value for observation. Default: ``"None"`` - time : :class:`int` or :class:`float` or :class:`numpy.ndarray` or - :class:`Variable`, optional + time : scalar, :class:`numpy.ndarray`, :class:`Variable`, optional New Value for time. Default: ``"None"`` - observation : :class:`int` or :class:`float` or :class:`numpy.ndarray` - or :class:`Variable`, optional - New Value for observation. - Default: ``"None"`` Returns ------- @@ -425,30 +344,13 @@ def __call__(self, in1=None, in2=None, time=None, observation=None): or :class:`numpy.ndarray` ``(time, observation)`` or ``observation``. """ - # in1 and in2 are for non-keyword call - if self.state == "transient": - if time is None: - time = in1 - if observation is None: - observation = in2 - tmp1 = dcopy(self._time) - tmp2 = dcopy(self._observation) - self._settime(time) + if observation is not None: self._setobservation(observation) - if not self._checkshape(): - self._settime(tmp1) - self._setobservation(tmp2) - raise ValueError( - "Observation: " - + "'observation' and 'time' have a " - + "shape-missmatch" - ) - return self.time, self.observation - else: - if observation is None: - observation = in1 - self._setobservation(observation) - return self.observation + if time is not None: + self._settime(time) + if observation is not None or time is not None: + self._checkshape() + return self.value def __repr__(self): """Represenetation.""" @@ -486,15 +388,6 @@ def info(self): info += self._time.info + "\n" info += " --- " + "\n" info += self._observation.info + "\n" - # print("Observation-name: "+str(self.name)) - # print(" -Description: "+str(self.description)) - # print(" -Kind: "+str(self.kind)) - # print(" -State: "+str(self.state)) - # if self.state == "transient": - # print(" --- ") - # self._time.info - # print(" --- ") - # self._observation.info return info @property @@ -506,7 +399,7 @@ def value(self): or :class:`numpy.ndarray` """ if self.state == "transient": - return self.time, self.observation + return self.observation, self.time return self.observation @property @@ -516,7 +409,7 @@ def state(self): Either ``"steady"`` or ``"transient"``. """ - return self.__state + return "steady" if self._time is None else "transient" @property def kind(self): @@ -530,9 +423,16 @@ def time(self): :class:`int` or :class:`float` or :class:`numpy.ndarray` """ - if self.state == "transient": - return self._time.value - return None + return self._time.value if self.state == "transient" else None + + @time.setter + def time(self, time): + self._settime(time) + self._checkshape() + + @time.deleter + def time(self): + self._time = None @property def observation(self): @@ -543,6 +443,11 @@ def observation(self): """ return self._observation.value + @observation.setter + def observation(self, observation): + self._setobservation(observation) + self._checkshape() + @property def units(self): """[:class:`tuple` of] :class:`str`: units of the observation.""" @@ -550,46 +455,6 @@ def units(self): return self._observation.units return self._time.units + "," + self._observation.units - @time.setter - def time(self, time): - if self.state == "steady": - self.__state = "transient" - self._settime(time) - if not self._checkshape(): - del self.time - raise ValueError( - "Observation: " - + "'time' has a " - + "shape-missmatch with 'observation'" - ) - else: - tmp = dcopy(self._time) - self._settime(time) - if not self._checkshape(): - self._settime(tmp) - raise ValueError( - "Observation: " - + "'time' has a " - + "shape-missmatch with 'observation'" - ) - - @time.deleter - def time(self): - self.__state = "steady" - del self._time - - @observation.setter - def observation(self, observation): - tmp = dcopy(self._observation) - self._setobservation(observation) - if not self._checkshape(): - self._setobservation(tmp) - raise ValueError( - "Observation: " - + "'observation' has a " - + "shape-missmatch with 'time'" - ) - def reshape(self): """Reshape obeservations to flat array.""" if self.state == "transient": @@ -601,23 +466,31 @@ def reshape(self): def _settime(self, time): if isinstance(time, Variable): self._time = dcopy(time) + elif self._time is None: + self._time = TimeVar(time) + elif time is None: + self._time = None else: self._time(time) def _setobservation(self, observation): if isinstance(observation, Variable): self._observation = dcopy(observation) + elif observation is None: + self._observation = None else: self._observation(observation) def _checkshape(self): - if self.state == "transient": - if ( - np.shape(self.time) - != np.shape(self.observation)[: len(np.shape(self._time()))] - ): - return False - return True + if self.state == "transient" and ( + np.shape(self.time) + != np.shape(self.observation)[: len(np.shape(self.time))] + ): + raise ValueError( + "Observation: " + + "'observation' has a " + + "shape-missmatch with 'time'" + ) def __iter__(self): """Iterate over Observations.""" @@ -627,13 +500,13 @@ def __iter__(self): self.__itfinished = False return self - def next(self): + def __next__(self): """Iterate through observations.""" if self.state == "transient": if self.__it.finished: raise StopIteration ret = ( - np.asscalar(self.__it[0]), + self.__it[0].item(), self.observation[self.__it.multi_index], ) self.__it.iternext() @@ -644,14 +517,6 @@ def next(self): self.__itfinished = True return ret - # for python 2&3 compatibility overwrite "__next__" with "next" - __next__ = next - - # def edit(self): - # """Edit the observed time-series with a graphical interface.""" - # if self.state == "transient" and len(np.shape(self.time)) == 1: - # Editor(self) - def save(self, path="", name=None): """Save an observation to file. @@ -669,50 +534,7 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".obs"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Obs_" + self.name - # ensure the name ends with '.obs' - if name[-4:] != ".obs": - name += ".obs" - name = _formname(name) - # create temporal directory for the included files - patht = tempfile.mkdtemp(dir=path) - # write the csv-file - # with open(patht+name[:-4]+".csv", 'w') as csvf: - with open(os.path.join(patht, "info.csv"), "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Observation"]) - writer.writerow(["name", self.name]) - writer.writerow(["state", self.state]) - writer.writerow(["description", self.description]) - if self.state == "steady": - obsname = name[:-4] + "_ObsVar.var" - writer.writerow(["observation", obsname]) - self._observation.save(patht, obsname) - else: - timname = name[:-4] + "_TimVar.var" - obsname = name[:-4] + "_ObsVar.var" - writer.writerow(["time", timname]) - writer.writerow(["observation", obsname]) - self._time.save(patht, timname) - self._observation.save(patht, obsname) - # compress everything to one zip-file - file_path = os.path.join(path, name) - with zipfile.ZipFile(file_path, "w") as zfile: - # zfile.write(patht+name[:-4]+".csv", name[:-4]+".csv") - zfile.write(os.path.join(patht, "info.csv"), "info.csv") - if self.state == "transient": - zfile.write(os.path.join(patht, timname), timname) - zfile.write(os.path.join(patht, obsname), obsname) - shutil.rmtree(patht, ignore_errors=True) - return file_path + return data_io.save_obs(self, path, name) class StdyObs(Observation): @@ -730,7 +552,7 @@ class StdyObs(Observation): """ def __init__(self, name, observation, description="Steady observation"): - super(StdyObs, self).__init__(name, None, observation, description) + super().__init__(name, observation, None, description) def _settime(self, time): """For steady observations, this raises a ``ValueError``.""" @@ -747,20 +569,20 @@ class TimeSeries(Observation): ---------- name : :class:`str` Name of the Variable. - time : :class:`Variable` - Time points of observation. values : :class:`Variable` Values of the time-series. + time : :class:`Variable` + Time points of the time-series. description : :class:`str`, optional - Description of the Variable. Default: ``"Timeseries"`` + Description of the Variable. Default: ``"Timeseries."`` """ - def __init__(self, name, time, values, description="Timeseries."): + def __init__(self, name, values, time, description="Timeseries."): if not isinstance(time, Variable): time = TimeVar(time) if not isinstance(values, Variable): values = Variable(name, values, description=description) - super(TimeSeries, self).__init__(name, time, values, description) + super().__init__(name, values, time, description) class DrawdownObs(Observation): @@ -771,22 +593,22 @@ class DrawdownObs(Observation): ---------- name : :class:`str` Name of the Variable. - time : :class:`Variable` - Time points of observation. observation : :class:`Variable` Observation. + time : :class:`Variable` + Time points of observation. description : :class:`str`, optional Description of the Variable. Default: ``"Drawdown observation"`` """ def __init__( - self, name, time, observation, description="Drawdown observation" + self, name, observation, time, description="Drawdown observation" ): if not isinstance(time, Variable): time = TimeVar(time) if not isinstance(observation, Variable): observation = HeadVar(observation) - super(DrawdownObs, self).__init__(name, time, observation, description) + super().__init__(name, observation, time, description) class StdyHeadObs(Observation): @@ -811,7 +633,7 @@ def __init__( ): if not isinstance(observation, Variable): observation = HeadVar(observation) - super(StdyHeadObs, self).__init__(name, None, observation, description) + super().__init__(name, observation, None, description) def _settime(self, time): """For steady observations, this raises a ``ValueError``.""" @@ -820,7 +642,7 @@ def _settime(self, time): ) -class Well(object): +class Well: """Class for a pumping-/observation-well. This is a class for a well within a aquifer-testing campaign. @@ -849,79 +671,16 @@ class Well(object): def __init__( self, name, radius, coordinates, welldepth=1.0, aquiferdepth=None ): - self.name = _formstr(name) + self._radius = None + self._coordinates = None + self._welldepth = None + self._aquiferdepth = None - if isinstance(radius, Variable): - self._radius = dcopy(radius) - else: - self._radius = Variable( - "radius", - radius, - "r", - "m", - "Inner radius of well '" + str(name) + "'", - ) - if not self._radius.scalar: - raise ValueError("Well: 'radius' needs to be scalar") - if self.radius < 0.0: - raise ValueError("Well: 'radius' needs to be positiv") - - if isinstance(coordinates, Variable): - self._coordinates = dcopy(coordinates) - else: - self._coordinates = Variable( - "coordinates", - coordinates, - "XY", - "m", - "coordinates of well '" + str(name) + "'", - ) - if np.shape(self.coordinates) != (2,) and not np.isscalar( - self.coordinates - ): - raise ValueError( - "Well: 'coordinates' should be given as " - + "[x,y] values or one single distance value" - ) - - if isinstance(welldepth, Variable): - self._welldepth = dcopy(welldepth) - else: - self._welldepth = Variable( - "welldepth", - welldepth, - "L_w", - "m", - "depth of well '" + str(name) + "'", - ) - if not self._welldepth.scalar: - raise ValueError("Well: 'welldepth' needs to be scalar") - if self.welldepth <= 0.0: - raise ValueError("Well: 'welldepth' needs to be positiv") - - if isinstance(aquiferdepth, Variable): - self._aquiferdepth = dcopy(aquiferdepth) - else: - if aquiferdepth is None: - self._aquiferdepth = Variable( - "aquiferdepth", - welldepth, - "L_a", - "m", - "aquiferdepth at well '" + str(name) + "'", - ) - else: - self._aquiferdepth = Variable( - "aquiferdepth", - aquiferdepth, - "L_a", - "m", - "aquiferdepth at well '" + str(name) + "'", - ) - if not self._aquiferdepth.scalar: - raise ValueError("Well: 'aquiferdepth' needs to be scalar") - if self.aquiferdepth <= 0.0: - raise ValueError("Well: 'aquiferdepth' needs to be positiv") + self.name = data_io._formstr(name) + self.wellradius = radius + self.coordinates = coordinates + self.welldepth = welldepth + self.aquiferdepth = aquiferdepth @property def info(self): @@ -934,18 +693,10 @@ def info(self): info += "Well-name: " + str(self.name) + "\n" info += "--" + "\n" info += self._radius.info + "\n" - info += self._coordinates.info + "\n" + info += self.coordinates.info + "\n" info += self._welldepth.info + "\n" info += self._aquiferdepth.info + "\n" info += "----" + "\n" - # print("----") - # print("Well-name: "+str(self.name)) - # print("--") - # self._radius.info - # self._coordinates.info - # self._welldepth.info - # self._aquiferdepth.info - # print("----") return info @property @@ -953,77 +704,120 @@ def radius(self): """:class:`float`: Radius of the well.""" return self._radius.value - @radius.setter - def radius(self, radius): - tmp = dcopy(self._radius) + @property + def wellradius(self): + """:class:`float`: Radius variable of the well.""" + return self._radius + + @wellradius.setter + def wellradius(self, radius): if isinstance(radius, Variable): self._radius = dcopy(radius) + elif self._radius is None: + self._radius = Variable( + "radius", + radius, + "r", + "m", + "Inner radius of well '" + str(self.name) + "'", + ) else: self._radius(radius) if not self._radius.scalar: - self._radius = dcopy(tmp) raise ValueError("Well: 'radius' needs to be scalar") if self.radius <= 0.0: - self._radius = dcopy(tmp) raise ValueError("Well: 'radius' needs to be positiv") @property - def coordinates(self): - """:class:`numpy.ndarray`: Coordinates of the well.""" + def pos(self): + """:class:`numpy.ndarray`: Position of the well.""" return self._coordinates.value + @property + def coordinates(self): + """:class:`numpy.ndarray`: Coordinates variable of the well.""" + return self._coordinates + @coordinates.setter def coordinates(self, coordinates): - tmp = dcopy(self._coordinates) if isinstance(coordinates, Variable): self._coordinates = dcopy(coordinates) + elif self._coordinates is None: + self._coordinates = Variable( + "coordinates", + coordinates, + "XY", + "m", + "coordinates of well '" + str(self.name) + "'", + ) else: self._coordinates(coordinates) - if np.shape(self.coordinates) != (2,) and not np.isscalar( - self.coordinates - ): - self._coordinates = dcopy(tmp) + if np.shape(self.pos) != (2,) and not np.isscalar(self.pos): raise ValueError( "Well: 'coordinates' should be given as " + "[x,y] values or one single distance value" ) @property - def welldepth(self): + def depth(self): """:class:`float`: Depth of the well.""" return self._welldepth.value + @property + def welldepth(self): + """:class:`float`: Depth variable of the well.""" + return self._welldepth + @welldepth.setter def welldepth(self, welldepth): - tmp = dcopy(self._welldepth) if isinstance(welldepth, Variable): self._welldepth = dcopy(welldepth) + elif self._welldepth is None: + self._welldepth = Variable( + "welldepth", + welldepth, + "L_w", + "m", + "depth of well '" + str(self.name) + "'", + ) else: self._welldepth(welldepth) if not self._welldepth.scalar: - self._welldepth = dcopy(tmp) raise ValueError("Well: 'welldepth' needs to be scalar") - if self.welldepth <= 0.0: - self._welldepth = dcopy(tmp) + if self.depth <= 0.0: raise ValueError("Well: 'welldepth' needs to be positiv") @property def aquiferdepth(self): """:class:`float`: Aquifer depth at the well.""" - return self._aquiferdepth.value + return self._aquiferdepth @aquiferdepth.setter def aquiferdepth(self, aquiferdepth): - tmp = dcopy(self._aquiferdepth) if isinstance(aquiferdepth, Variable): self._aquiferdepth = dcopy(aquiferdepth) + elif self._aquiferdepth is None: + if aquiferdepth is None: + self._aquiferdepth = Variable( + "aquiferdepth", + self.depth, + "L_a", + "m", + "aquiferdepth at well '" + str(self.name) + "'", + ) + else: + self._aquiferdepth = Variable( + "aquiferdepth", + aquiferdepth, + "L_a", + "m", + "aquiferdepth at well '" + str(self.name) + "'", + ) else: self._aquiferdepth(aquiferdepth) if not self._aquiferdepth.scalar: - self._aquiferdepth = dcopy(tmp) raise ValueError("Well: 'aquiferdepth' needs to be scalar") - if self.aquiferdepth <= 0.0: - self._aquiferdepth = dcopy(tmp) + if self.aquiferdepth.value <= 0.0: raise ValueError("Well: 'aquiferdepth' needs to be positiv") def distance(self, well): @@ -1035,9 +829,9 @@ def distance(self, well): Coordinates to calculate the distance to or another well. """ if isinstance(well, Well): - return np.linalg.norm(self.coordinates - well.coordinates) + return np.linalg.norm(self.pos - well.pos) try: - return np.linalg.norm(self.coordinates - well) + return np.linalg.norm(self.pos - well) except ValueError: raise ValueError( "Well: the distant-well needs to be an " @@ -1083,7 +877,7 @@ def __rand__(self, well): def __abs__(self): """Distance to origin.""" - return np.linalg.norm(self.coordinates) + return np.linalg.norm(self.pos) def save(self, path="", name=None): """Save a well to file. @@ -1102,212 +896,4 @@ def save(self, path="", name=None): ----- The file will get the suffix ``".wel"``. """ - path = os.path.normpath(path) - # create the path if not existing - if not os.path.exists(path): - os.makedirs(path) - # create a standard name if None is given - if name is None: - name = "Well_" + self.name - # ensure the name ends with '.csv' - if name[-4:] != ".wel": - name += ".wel" - name = _formname(name) - # create temporal directory for the included files - patht = tempfile.mkdtemp(dir=path) - # write the csv-file - # with open(patht+name[:-4]+".csv", 'w') as csvf: - with open(os.path.join(patht, "info.csv"), "w") as csvf: - writer = csv.writer( - csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" - ) - writer.writerow(["Well"]) - writer.writerow(["name", self.name]) - # define names for the variable-files - radiuname = name[:-4] + "_RadVar.var" - coordname = name[:-4] + "_CooVar.var" - welldname = name[:-4] + "_WedVar.var" - aquifname = name[:-4] + "_AqdVar.var" - # save variable-files - writer.writerow(["radius", radiuname]) - self._radius.save(patht, radiuname) - writer.writerow(["coordinates", coordname]) - self._coordinates.save(patht, coordname) - writer.writerow(["welldepth", welldname]) - self._welldepth.save(patht, welldname) - writer.writerow(["aquiferdepth", aquifname]) - self._aquiferdepth.save(patht, aquifname) - # compress everything to one zip-file - file_path = os.path.join(path, name) - with zipfile.ZipFile(file_path, "w") as zfile: - # zfile.write(patht+name[:-4]+".csv", name[:-4]+".csv") - zfile.write(os.path.join(patht, "info.csv"), "info.csv") - zfile.write(os.path.join(patht, radiuname), radiuname) - zfile.write(os.path.join(patht, coordname), coordname) - zfile.write(os.path.join(patht, welldname), welldname) - zfile.write(os.path.join(patht, aquifname), aquifname) - # delete the temporary directory - shutil.rmtree(patht, ignore_errors=True) - return file_path - - -# Loading routines ### - - -def load_var(varfile): - """Load a variable from file. - - This reads a variable from a csv file. - - Parameters - ---------- - varfile : :class:`str` - Path to the file - """ - try: - with open(varfile, "r") as vfile: - data = csv.reader(vfile) - if next(data)[0] != "Variable": - raise Exception - name = next(data)[1] - symbol = next(data)[1] - units = next(data)[1] - description = next(data)[1] - integer = next(data)[0] == "integer" - shapenfo = _nextr(data) - if shapenfo[0] == "scalar": - if integer: - value = np.int(next(data)[1]) - else: - value = np.float(next(data)[1]) - else: - shape = tuple(np.array(shapenfo[1:], dtype=np.int)) - vcnt = np.int(next(data)[1]) - vlist = [] - for __ in range(vcnt): - vlist.append(next(data)[0]) - if integer: - value = np.array(vlist, dtype=np.int).reshape(shape) - else: - value = np.array(vlist, dtype=np.float).reshape(shape) - - var = Variable(name, value, symbol, units, description) - except Exception: - try: - data = csv.reader(varfile) - if next(data)[0] != "Variable": - raise Exception - name = next(data)[1] - symbol = next(data)[1] - units = next(data)[1] - description = next(data)[1] - integer = next(data)[0] == "integer" - shapenfo = _nextr(data) - if shapenfo[0] == "scalar": - if integer: - value = np.int(next(data)[1]) - else: - value = np.float(next(data)[1]) - else: - shape = tuple(np.array(shapenfo[1:], dtype=np.int)) - vcnt = np.int(next(data)[1]) - vlist = [] - for __ in range(vcnt): - vlist.append(next(data)[0]) - if integer: - value = np.array(vlist, dtype=np.int).reshape(shape) - else: - value = np.array(vlist, dtype=np.float).reshape(shape) - - var = Variable(name, value, symbol, units, description) - except Exception: - raise Exception("loadVar: loading the variable was not possible") - return var - - -def load_obs(obsfile): - """Load an observation from file. - - This reads a observation from a csv file. - - Parameters - ---------- - obsfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(obsfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - if next(data)[0] != "Observation": - raise Exception - name = next(data)[1] - steady = next(data)[1] == "steady" - description = next(data)[1] - if not steady: - timef = next(data)[1] - obsf = next(data)[1] - - if not steady: - time = load_var(TxtIO(zfile.open(timef))) - else: - time = None - - obs = load_var(TxtIO(zfile.open(obsf))) - - observation = Observation(name, time, obs, description) - except Exception: - raise Exception("loadObs: loading the observation was not possible") - return observation - - -def load_well(welfile): - """Load a well from file. - - This reads a well from a csv file. - - Parameters - ---------- - welfile : :class:`str` - Path to the file - """ - try: - with zipfile.ZipFile(welfile, "r") as zfile: - info = TxtIO(zfile.open("info.csv")) - data = csv.reader(info) - if next(data)[0] != "Well": - raise Exception - name = next(data)[1] - radf = next(data)[1] - coordf = next(data)[1] - welldf = next(data)[1] - aquidf = next(data)[1] - - rad = load_var(TxtIO(zfile.open(radf))) - coord = load_var(TxtIO(zfile.open(coordf))) - welld = load_var(TxtIO(zfile.open(welldf))) - aquid = load_var(TxtIO(zfile.open(aquidf))) - - well = Well(name, rad, coord, welld, aquid) - except Exception: - raise Exception("loadWell: loading the well was not possible") - return well - - -# TOOLS ### - - -def _formstr(string): - # remove spaces, tabs, linebreaks and other separators - return "".join(str(string).split()) - - -def _formname(string): - # remove slashes - string = "".join(str(string).split(os.path.sep)) - # remove spaces, tabs, linebreaks and other separators - return _formstr(string) - - -def _nextr(data): - return tuple(filter(None, next(data))) + return data_io.save_well(self, path, name) diff --git a/welltestpy/estimate/__init__.py b/welltestpy/estimate/__init__.py index f935d17..f9955a5 100644 --- a/welltestpy/estimate/__init__.py +++ b/welltestpy/estimate/__init__.py @@ -4,22 +4,12 @@ .. currentmodule:: welltestpy.estimate -Subpackages -^^^^^^^^^^^ +Estimators +^^^^^^^^^^ -The following subpackages are provided +The following estimators are provided .. autosummary:: - estimatelib - spotpy_classes - -Estimation classes -^^^^^^^^^^^^^^^^^^ - -The following estimation classes are provided - -.. autosummary:: - TransientPumping ExtTheis3D ExtTheis2D Neuman2004 @@ -28,14 +18,30 @@ ExtThiem2D Neuman2004Steady Thiem - TypeCurve -""" -from __future__ import absolute_import -from welltestpy.estimate import estimatelib, spotpy_classes -from welltestpy.estimate.estimatelib import ( - TransientPumping, +Base Classes +^^^^^^^^^^^^ + +Transient +~~~~~~~~~ + +All transient estimators are derived from the following class + +.. autosummary:: + TransientPumping + +Steady Pumping +~~~~~~~~~~~~~~ + +All steady estimators are derived from the following class + +.. autosummary:: + SteadyPumping +""" +from . import estimators, spotpylib, steady_lib, transient_lib + +from .estimators import ( ExtTheis3D, ExtTheis2D, Neuman2004, @@ -45,10 +51,11 @@ Neuman2004Steady, Thiem, ) -from welltestpy.estimate.spotpy_classes import TypeCurve +from .transient_lib import TransientPumping +from .steady_lib import SteadyPumping -__all__ = [ - "TransientPumping", +__all__ = ["estimators", "spotpylib", "steady_lib", "transient_lib"] +__all__ += [ "ExtTheis3D", "ExtTheis2D", "Neuman2004", @@ -57,7 +64,5 @@ "ExtThiem2D", "Neuman2004Steady", "Thiem", - "TypeCurve", - "estimatelib", - "spotpy_classes", ] +__all__ += ["TransientPumping", "SteadyPumping"] diff --git a/welltestpy/estimate/estimatelib.py b/welltestpy/estimate/estimatelib.py deleted file mode 100755 index d84bdbc..0000000 --- a/welltestpy/estimate/estimatelib.py +++ /dev/null @@ -1,1846 +0,0 @@ -# -*- coding: utf-8 -*- -""" -welltestpy subpackage providing classes for parameter estimation. - -.. currentmodule:: welltestpy.estimate.estimatelib - -The following classes are provided - -.. autosummary:: - TransientPumping - SteadyPumping - ExtTheis3D - ExtTheis2D - Neuman2004 - Theis - ExtThiem3D - ExtThiem2D - Neuman2004Steady - Thiem -""" -from __future__ import absolute_import, division, print_function - -from copy import deepcopy as dcopy -import os -import time as timemodule - -import numpy as np -import spotpy -import anaflow as ana - -from welltestpy.data import PumpingTest -from welltestpy.process.processlib import normpumptest, filterdrawdown -from welltestpy.estimate.spotpy_classes import TypeCurve -from welltestpy.tools.plotter import ( - plotfit_transient, - plotfit_steady, - plotparainteract, - plotparatrace, - plotsensitivity, -) - -__all__ = [ - "TransientPumping", - "SteadyPumping", - "ExtTheis3D", - "ExtTheis2D", - "Neuman2004", - "Theis", - "ExtThiem3D", - "ExtThiem2D", - "Neuman2004Steady", - "Thiem", -] - - -def fast_rep(para_no, infer_fac=4, freq_step=2): - """Get number of iterations needed for the FAST algorithm. - - Parameters - ---------- - para_no : :class:`int` - Number of parameters in the model. - infer_fac : :class:`int`, optional - The inference fractor. Default: 4 - freq_step : :class:`int`, optional - The frequency step. Default: 2 - """ - return 2 * int( - para_no * (1 + 4 * infer_fac ** 2 * (1 + (para_no - 2) * freq_step)) - ) - - -class TransientPumping(object): - """Class to estimate transient Type-Curve parameters. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - type_curve : :any:`callable` - The given type-curve. Output will be reshaped to flat array. - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - fit_type : :class:`dict` or :any:`None` - Dictionary containing fitting type for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - fit_type can be "lin", "log" (np.exp(val) will be used) - or a callable function. - By default, values will be fit linearly. - Default: None - val_kw_names : :class:`dict` or :any:`None` - Dictionary containing keyword names in the type-curve for each value. - - {value-name: kwargs-name in type_curve} - - This is usefull if fitting is not done by linear values. - By default, parameter names will be value names. - Default: None - val_plot_names : :class:`dict` or :any:`None` - Dictionary containing keyword names in the type-curve for each value. - - {value-name: string for plot legend} - - This is usefull to get better plots. - By default, parameter names will be value names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - type_curve, - val_ranges, - val_fix=None, - fit_type=None, - val_kw_names=None, - val_plot_names=None, - testinclude=None, - generate=False, - ): - val_fix = {} if val_fix is None else val_fix - fit_type = {} if fit_type is None else fit_type - val_kw_names = {} if val_kw_names is None else val_kw_names - val_plot_names = {} if val_plot_names is None else val_plot_names - self.setup_kw = { - "type_curve": type_curve, - "val_ranges": val_ranges, - "val_fix": val_fix, - "fit_type": fit_type, - "val_kw_names": val_kw_names, - "val_plot_names": val_plot_names, - } - """:class:`dict`: TypeCurve Spotpy Setup definition""" - self.name = name - """:class:`str`: Name of the Estimation""" - self.campaign_raw = dcopy(campaign) - """:class:`welltestpy.data.Campaign`:\ - Copy of the original input campaign""" - self.campaign = dcopy(campaign) - """:class:`welltestpy.data.Campaign`:\ - Copy of the input campaign to be modified""" - - self.prate = None - """:class:`float`: Pumpingrate at the pumping well""" - - self.time = None - """:class:`numpy.ndarray`: time points of the observation""" - self.rad = None - """:class:`numpy.ndarray`: array of the radii from the wells""" - self.data = None - """:class:`numpy.ndarray`: observation data""" - self.radnames = None - """:class:`numpy.ndarray`: names of the radii well combination""" - - self.para = None - """:class:`list` of :class:`float`: estimated parameters""" - self.result = None - """:class:`list`: result of the spotpy estimation""" - self.sens = None - """:class:`list`: result of the spotpy sensitivity analysis""" - self.testinclude = {} - """:class:`dict`: dictonary of which tests should be included""" - - if testinclude is None: - tests = list(self.campaign.tests.keys()) - self.testinclude = {} - for test in tests: - self.testinclude[test] = self.campaign.tests[ - test - ].observationwells - elif not isinstance(testinclude, dict): - self.testinclude = {} - for test in testinclude: - self.testinclude[test] = self.campaign.tests[ - test - ].observationwells - else: - self.testinclude = testinclude - - for test in self.testinclude: - if not isinstance(self.campaign.tests[test], PumpingTest): - raise ValueError(test + " is not a pumping test.") - if not self.campaign.tests[test].constant_rate: - raise ValueError(test + " is not a constant rate test.") - if ( - not self.campaign.tests[test].state( - wells=self.testinclude[test] - ) - == "transient" - ): - raise ValueError(test + ": selection is not transient.") - - rwell_list = [] - rinf_list = [] - for test in self.testinclude: - pwell = self.campaign.tests[test].pumpingwell - rwell_list.append(self.campaign.wells[pwell].radius) - rinf_list.append(self.campaign.tests[test].aquiferradius) - self.rwell = min(rwell_list) - """:class:`float`: radius of the pumping wells""" - self.rinf = max(rinf_list) - """:class:`float`: radius of the furthest wells""" - - if generate: - self.setpumprate() - self.settime() - self.gen_data() - self.gen_setup() - - def setpumprate(self, prate=-1.0): - """Set a uniform pumping rate at all pumpingwells wells. - - We assume linear scaling by the pumpingrate. - - Parameters - ---------- - prate : :class:`float`, optional - Pumping rate. Default: ``-1.0`` - """ - for test in self.testinclude: - normpumptest(self.campaign.tests[test], pumpingrate=prate) - self.prate = prate - - def settime(self, time=None, tmin=10.0, tmax=np.inf, typ="quad", steps=10): - """Set uniform time points for the observations. - - Parameters - ---------- - time : :class:`numpy.ndarray`, optional - Array of specified time points. If ``None`` is given, they will - be determind by the observation data. - Default: ``None`` - tmin : :class:`float`, optional - Minimal time value. It will set a minimal value of 10s. - Default: ``10`` - tmax : :class:`float`, optional - Maximal time value. - Default: ``inf`` - typ : :class:`str` or :class:`float`, optional - Typ of the time selection. You can select from: - - * ``"exp"``: for exponential behavior - * ``"log"``: for logarithmic behavior - * ``"geo"``: for geometric behavior - * ``"lin"``: for linear behavior - * ``"quad"``: for quadratic behavior - * ``"cub"``: for cubic behavior - * :class:`float`: here you can specifi any exponent - ("quad" would be equivalent to 2) - - Default: "quad" - - steps : :class:`int`, optional - Number of generated time steps. Default: 10 - """ - if time is None: - for test in self.testinclude: - for obs in self.testinclude[test]: - temptime, _ = self.campaign.tests[test].observations[obs]() - tmin = max(tmin, temptime.min()) - tmax = min(tmax, temptime.max()) - tmin = tmax if tmin > tmax else tmin - time = ana.specialrange(tmin, tmax, steps, typ) - - for test in self.testinclude: - for obs in self.testinclude[test]: - filterdrawdown( - self.campaign.tests[test].observations[obs], tout=time - ) - - self.time = time - - def gen_data(self): - """Generate the observed drawdown at given time points. - - It will also generate an array containing all radii of all well - combinations. - """ - rad = np.array([]) - data = None - - radnames = [] - - for test in self.testinclude: - pwell = self.campaign.wells[self.campaign.tests[test].pumpingwell] - for obs in self.testinclude[test]: - _, temphead = self.campaign.tests[test].observations[obs]() - temphead = np.array(temphead).reshape(-1)[np.newaxis].T - - if data is None: - data = dcopy(temphead) - else: - data = np.hstack((data, temphead)) - - owell = self.campaign.wells[obs] - - if pwell == owell: - temprad = pwell.radius - else: - temprad = pwell - owell - rad = np.hstack((rad, temprad)) - - tempname = (self.campaign.tests[test].pumpingwell, obs) - radnames.append(tempname) - - # sort everything by the radii - idx = rad.argsort() - radnames = np.array(radnames) - self.rad = rad[idx] - self.data = data[:, idx] - self.radnames = radnames[idx] - - def gen_setup( - self, prate_kw="rate", rad_kw="rad", time_kw="time", dummy=False - ): - """Generate the Spotpy Setup. - - Parameters - ---------- - prate_kw : :class:`str`, optional - Keyword name for the pumping rate in the used type curve. - Default: "rate" - rad_kw : :class:`str`, optional - Keyword name for the radius in the used type curve. - Default: "rad" - time_kw : :class:`str`, optional - Keyword name for the time in the used type curve. - Default: "time" - dummy : :class:`bool`, optional - Add a dummy parameter to the model. This could be used to equalize - sensitivity analysis. - Default: False - """ - self.extra_kw_names = {"Qw": prate_kw, "rad": rad_kw, "time": time_kw} - self.setup_kw["val_fix"].setdefault(prate_kw, self.prate) - self.setup_kw["val_fix"].setdefault(rad_kw, self.rad) - self.setup_kw["val_fix"].setdefault(time_kw, self.time) - self.setup_kw.setdefault("data", self.data) - self.setup_kw["dummy"] = dummy - self.setup = TypeCurve(**self.setup_kw) - - def run( - self, - rep=5000, - parallel="seq", - run=True, - folder=None, - dbname=None, - traceplotname=None, - fittingplotname=None, - interactplotname=None, - estname=None, - ): - """Run the estimation. - - Parameters - ---------- - rep : :class:`int`, optional - The number of repetitions within the SCEua algorithm in spotpy. - Default: ``5000`` - parallel : :class:`str`, optional - State if the estimation should be run in parallel or not. Options: - - * ``"seq"``: sequential on one CPU - * ``"mpi"``: use the mpi4py package - - Default: ``"seq"`` - run : :class:`bool`, optional - State if the estimation should be executed. Otherwise all plots - will be done with the previous results. - Default: ``True`` - folder : :class:`str`, optional - Path to the output folder. If ``None`` the CWD is used. - Default: ``None`` - dbname : :class:`str`, optional - File-name of the database of the spotpy estimation. - If ``None``, it will be the current time + - ``"_db"``. - Default: ``None`` - traceplotname : :class:`str`, optional - File-name of the parameter trace plot of the spotpy estimation. - If ``None``, it will be the current time + - ``"_paratrace.pdf"``. - Default: ``None`` - fittingplotname : :class:`str`, optional - File-name of the fitting plot of the estimation. - If ``None``, it will be the current time + - ``"_fit.pdf"``. - Default: ``None`` - interactplotname : :class:`str`, optional - File-name of the parameter interaction plot - of the spotpy estimation. - If ``None``, it will be the current time + - ``"_parainteract.pdf"``. - Default: ``None`` - estname : :class:`str`, optional - File-name of the results of the spotpy estimation. - If ``None``, it will be the current time + - ``"_estimate"``. - Default: ``None`` - """ - if self.setup.dummy: - raise ValueError( - "Estimate: for parameter estimation" - + " you can't use a dummy paramter." - ) - act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") - - # generate the filenames - if folder is None: - folder = os.path.join(os.getcwd(), self.name) - folder = os.path.abspath(folder) - if not os.path.exists(folder): - os.makedirs(folder) - - if dbname is None: - dbname = os.path.join(folder, act_time + "_db") - elif not os.path.isabs(dbname): - dbname = os.path.join(folder, dbname) - if traceplotname is None: - traceplotname = os.path.join(folder, act_time + "_paratrace.pdf") - elif not os.path.isabs(traceplotname): - traceplotname = os.path.join(folder, traceplotname) - if fittingplotname is None: - fittingplotname = os.path.join(folder, act_time + "_fit.pdf") - elif not os.path.isabs(fittingplotname): - fittingplotname = os.path.join(folder, fittingplotname) - if interactplotname is None: - interactplotname = os.path.join(folder, act_time + "_interact.pdf") - elif not os.path.isabs(interactplotname): - interactplotname = os.path.join(folder, interactplotname) - if estname is None: - paraname = os.path.join(folder, act_time + "_estimate.txt") - elif not os.path.isabs(estname): - paraname = os.path.join(folder, estname) - - # generate the parameter-names for plotting - paranames = dcopy(self.setup.para_names) - paralabels = [self.setup.val_plot_names[name] for name in paranames] - - if parallel == "mpi": - # send the dbname of rank0 - from mpi4py import MPI - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - if rank == 0: - print(rank, "send dbname:", dbname) - for i in range(1, size): - comm.send(dbname, dest=i, tag=0) - else: - dbname = comm.recv(source=0, tag=0) - print(rank, "got dbname:", dbname) - else: - rank = 0 - - if run: - # initialize the sampler - sampler = spotpy.algorithms.sceua( - self.setup, - dbname=dbname, - dbformat="csv", - parallel=parallel, - save_sim=True, - db_precision=np.float64, - ) - # start the estimation with the sce-ua algorithm - sampler.sample(rep, ngs=10, kstop=100, pcento=1e-4, peps=1e-3) - - if rank == 0: - # save best parameter-set - self.result = sampler.getdata() - para_opt = spotpy.analyser.get_best_parameterset( - self.result, maximize=False - ) - void_names = para_opt.dtype.names - self.para = [] - for name in void_names: - self.para.append(para_opt[0][name]) - np.savetxt(paraname, self.para) - - if rank == 0: - # plot the estimation-results - plotparatrace( - self.result, - parameternames=paranames, - parameterlabels=paralabels, - stdvalues=self.para, - plotname=traceplotname, - ) - plotfit_transient( - self.setup, - self.data, - self.para, - self.rad, - self.time, - self.radnames, - self.extra_kw_names, - fittingplotname, - ) - plotparainteract(self.result, paralabels, interactplotname) - - def sensitivity( - self, - rep=None, - parallel="seq", - folder=None, - dbname=None, - plotname=None, - traceplotname=None, - sensname=None, - ): - """Run the sensitivity analysis. - - Parameters - ---------- - rep : :class:`int`, optional - The number of repetitions within the FAST algorithm in spotpy. - Default: estimated - parallel : :class:`str`, optional - State if the estimation should be run in parallel or not. Options: - - * ``"seq"``: sequential on one CPU - * ``"mpi"``: use the mpi4py package - - Default: ``"seq"`` - folder : :class:`str`, optional - Path to the output folder. If ``None`` the CWD is used. - Default: ``None`` - dbname : :class:`str`, optional - File-name of the database of the spotpy estimation. - If ``None``, it will be the current time + - ``"_sensitivity_db"``. - Default: ``None`` - plotname : :class:`str`, optional - File-name of the result plot of the sensitivity analysis. - If ``None``, it will be the current time + - ``"_sensitivity.pdf"``. - Default: ``None`` - traceplotname : :class:`str`, optional - File-name of the parameter trace plot of the spotpy sensitivity - analysis. - If ``None``, it will be the current time + - ``"_senstrace.pdf"``. - Default: ``None`` - sensname : :class:`str`, optional - File-name of the results of the FAST estimation. - If ``None``, it will be the current time + - ``"_estimate"``. - Default: ``None`` - """ - if len(self.setup.para_names) == 1 and not self.setup.dummy: - raise ValueError( - "Sensitivity: for estimation with only one parameter" - + " you have to use a dummy paramter." - ) - if rep is None: - rep = fast_rep(len(self.setup.para_names) + int(self.setup.dummy)) - - act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") - # generate the filenames - if folder is None: - folder = os.path.join(os.getcwd(), self.name) - folder = os.path.abspath(folder) - if not os.path.exists(folder): - os.makedirs(folder) - - if dbname is None: - dbname = os.path.join(folder, act_time + "_sensitivity_db") - elif not os.path.isabs(dbname): - dbname = os.path.join(folder, dbname) - if plotname is None: - plotname = os.path.join(folder, act_time + "_sensitivity.pdf") - elif not os.path.isabs(plotname): - plotname = os.path.join(folder, plotname) - if traceplotname is None: - traceplotname = os.path.join(folder, act_time + "_senstrace.pdf") - elif not os.path.isabs(traceplotname): - traceplotname = os.path.join(folder, traceplotname) - if sensname is None: - sensname = os.path.join(folder, act_time + "_FAST_estimate.txt") - elif not os.path.isabs(sensname): - sensname = os.path.join(folder, sensname) - - sens_base, sens_ext = os.path.splitext(sensname) - sensname1 = sens_base + "_S1" + sens_ext - - # generate the parameter-names for plotting - paranames = dcopy(self.setup.para_names) - paralabels = [self.setup.val_plot_names[name] for name in paranames] - - if self.setup.dummy: - paranames.append("dummy") - paralabels.append("dummy") - - if parallel == "mpi": - # send the dbname of rank0 - from mpi4py import MPI - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - if rank == 0: - print(rank, "send dbname:", dbname) - for i in range(1, size): - comm.send(dbname, dest=i, tag=0) - else: - dbname = comm.recv(source=0, tag=0) - print(rank, "got dbname:", dbname) - else: - rank = 0 - - # initialize the sampler - sampler = spotpy.algorithms.fast( - self.setup, - dbname=dbname, - dbformat="csv", - parallel=parallel, - save_sim=True, - db_precision=np.float64, - ) - sampler.sample(rep) - - if rank == 0: - data = sampler.getdata() - parmin = sampler.parameter()["minbound"] - parmax = sampler.parameter()["maxbound"] - bounds = list(zip(parmin, parmax)) - self.sens = sampler.analyze( - bounds, np.nan_to_num(data["like1"]), len(paranames), paranames - ) - np.savetxt(sensname, self.sens["ST"]) - np.savetxt(sensname1, self.sens["S1"]) - plotsensitivity(paralabels, self.sens, plotname) - plotparatrace( - data, - parameternames=paranames, - parameterlabels=paralabels, - stdvalues=None, - plotname=traceplotname, - ) - - -# Steady Pumping - - -class SteadyPumping(object): - """Class to estimate steady Type-Curve parameters. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - type_curve : :any:`callable` - The given type-curve. Output will be reshaped to flat array. - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - make_steady : :class:`bool`, optional - State if the tests should be converted to steady observations. - See: :any:`PumpingTest.make_steady`. - Default: True - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - fit_type : :class:`dict` or :any:`None` - Dictionary containing fitting type for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - fit_type can be "lin", "log" (np.exp(val) will be used) - or a callable function. - By default, values will be fit linearly. - Default: None - val_kw_names : :class:`dict` or :any:`None` - Dictionary containing keyword names in the type-curve for each value. - - {value-name: kwargs-name in type_curve} - - This is usefull if fitting is not done by linear values. - By default, parameter names will be value names. - Default: None - val_plot_names : :class:`dict` or :any:`None` - Dictionary containing keyword names in the type-curve for each value. - - {value-name: string for plot legend} - - This is usefull to get better plots. - By default, parameter names will be value names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - type_curve, - val_ranges, - make_steady=True, - val_fix=None, - fit_type=None, - val_kw_names=None, - val_plot_names=None, - testinclude=None, - generate=False, - ): - val_fix = {} if val_fix is None else val_fix - fit_type = {} if fit_type is None else fit_type - val_kw_names = {} if val_kw_names is None else val_kw_names - val_plot_names = {} if val_plot_names is None else val_plot_names - self.setup_kw = { - "type_curve": type_curve, - "val_ranges": val_ranges, - "val_fix": val_fix, - "fit_type": fit_type, - "val_kw_names": val_kw_names, - "val_plot_names": val_plot_names, - } - """:class:`dict`: TypeCurve Spotpy Setup definition""" - self.name = name - """:class:`str`: Name of the Estimation""" - self.campaign_raw = dcopy(campaign) - """:class:`welltestpy.data.Campaign`:\ - Copy of the original input campaign""" - self.campaign = dcopy(campaign) - """:class:`welltestpy.data.Campaign`:\ - Copy of the input campaign to be modified""" - - self.prate = None - """:class:`float`: Pumpingrate at the pumping well""" - - self.rad = None - """:class:`numpy.ndarray`: array of the radii from the wells""" - self.data = None - """:class:`numpy.ndarray`: observation data""" - self.radnames = None - """:class:`numpy.ndarray`: names of the radii well combination""" - self.r_ref = None - """:class:`float`: reference radius of the biggest distance""" - self.h_ref = None - """:class:`float`: reference head at the biggest distance""" - - self.para = None - """:class:`list` of :class:`float`: estimated parameters""" - self.result = None - """:class:`list`: result of the spotpy estimation""" - self.sens = None - """:class:`list`: result of the spotpy sensitivity analysis""" - self.testinclude = {} - """:class:`dict`: dictonary of which tests should be included""" - - if testinclude is None: - tests = list(self.campaign.tests.keys()) - self.testinclude = {} - for test in tests: - self.testinclude[test] = self.campaign.tests[ - test - ].observationwells - elif not isinstance(testinclude, dict): - self.testinclude = {} - for test in testinclude: - self.testinclude[test] = self.campaign.tests[ - test - ].observationwells - else: - self.testinclude = testinclude - - for test in self.testinclude: - if not isinstance(self.campaign.tests[test], PumpingTest): - raise ValueError(test + " is not a pumping test.") - if make_steady is not False: - if make_steady is True: - make_steady = "latest" - self.campaign.tests[test].make_steady(make_steady) - if not self.campaign.tests[test].constant_rate: - raise ValueError(test + " is not a constant rate test.") - if ( - not self.campaign.tests[test].state( - wells=self.testinclude[test] - ) - == "steady" - ): - raise ValueError(test + ": selection is not steady.") - - rwell_list = [] - rinf_list = [] - for test in self.testinclude: - pwell = self.campaign.tests[test].pumpingwell - rwell_list.append(self.campaign.wells[pwell].radius) - rinf_list.append(self.campaign.tests[test].aquiferradius) - self.rwell = min(rwell_list) - """:class:`float`: radius of the pumping wells""" - self.rinf = max(rinf_list) - """:class:`float`: radius of the furthest wells""" - - if generate: - self.setpumprate() - self.gen_data() - self.gen_setup() - - def setpumprate(self, prate=-1.0): - """Set a uniform pumping rate at all pumpingwells wells. - - We assume linear scaling by the pumpingrate. - - Parameters - ---------- - prate : :class:`float`, optional - Pumping rate. Default: ``-1.0`` - """ - for test in self.testinclude: - normpumptest(self.campaign.tests[test], pumpingrate=prate) - self.prate = prate - - def gen_data(self): - """Generate the observed drawdown. - - It will also generate an array containing all radii of all well - combinations. - """ - rad = np.array([]) - data = np.array([]) - - radnames = [] - - for test in self.testinclude: - pwell = self.campaign.wells[self.campaign.tests[test].pumpingwell] - for obs in self.testinclude[test]: - temphead = self.campaign.tests[test].observations[obs]() - data = np.hstack((data, temphead)) - - owell = self.campaign.wells[obs] - if pwell == owell: - temprad = pwell.radius - else: - temprad = pwell - owell - rad = np.hstack((rad, temprad)) - - tempname = (self.campaign.tests[test].pumpingwell, obs) - radnames.append(tempname) - - # sort everything by the radii - idx = rad.argsort() - radnames = np.array(radnames) - self.rad = rad[idx] - self.data = data[idx] - self.radnames = radnames[idx] - self.r_ref = self.rad[-1] - self.h_ref = self.data[-1] - - def gen_setup( - self, - prate_kw="rate", - rad_kw="rad", - r_ref_kw="r_ref", - h_ref_kw="h_ref", - dummy=False, - ): - """Generate the Spotpy Setup. - - Parameters - ---------- - prate_kw : :class:`str`, optional - Keyword name for the pumping rate in the used type curve. - Default: "rate" - rad_kw : :class:`str`, optional - Keyword name for the radius in the used type curve. - Default: "rad" - r_ref_kw : :class:`str`, optional - Keyword name for the reference radius in the used type curve. - Default: "r_ref" - h_ref_kw : :class:`str`, optional - Keyword name for the reference head in the used type curve. - Default: "h_ref" - dummy : :class:`bool`, optional - Add a dummy parameter to the model. This could be used to equalize - sensitivity analysis. - Default: False - """ - self.extra_kw_names = { - "Qw": prate_kw, - "rad": rad_kw, - "r_ref": r_ref_kw, - "h_ref": h_ref_kw, - } - self.setup_kw["val_fix"].setdefault(prate_kw, self.prate) - self.setup_kw["val_fix"].setdefault(rad_kw, self.rad) - self.setup_kw["val_fix"].setdefault(r_ref_kw, self.r_ref) - self.setup_kw["val_fix"].setdefault(h_ref_kw, self.h_ref) - self.setup_kw.setdefault("data", self.data) - self.setup_kw["dummy"] = dummy - self.setup = TypeCurve(**self.setup_kw) - - def run( - self, - rep=5000, - parallel="seq", - run=True, - folder=None, - dbname=None, - traceplotname=None, - fittingplotname=None, - interactplotname=None, - estname=None, - ): - """Run the estimation. - - Parameters - ---------- - rep : :class:`int`, optional - The number of repetitions within the SCEua algorithm in spotpy. - Default: ``5000`` - parallel : :class:`str`, optional - State if the estimation should be run in parallel or not. Options: - - * ``"seq"``: sequential on one CPU - * ``"mpi"``: use the mpi4py package - - Default: ``"seq"`` - run : :class:`bool`, optional - State if the estimation should be executed. Otherwise all plots - will be done with the previous results. - Default: ``True`` - folder : :class:`str`, optional - Path to the output folder. If ``None`` the CWD is used. - Default: ``None`` - dbname : :class:`str`, optional - File-name of the database of the spotpy estimation. - If ``None``, it will be the current time + - ``"_db"``. - Default: ``None`` - traceplotname : :class:`str`, optional - File-name of the parameter trace plot of the spotpy estimation. - If ``None``, it will be the current time + - ``"_paratrace.pdf"``. - Default: ``None`` - fittingplotname : :class:`str`, optional - File-name of the fitting plot of the estimation. - If ``None``, it will be the current time + - ``"_fit.pdf"``. - Default: ``None`` - interactplotname : :class:`str`, optional - File-name of the parameter interaction plot - of the spotpy estimation. - If ``None``, it will be the current time + - ``"_parainteract.pdf"``. - Default: ``None`` - estname : :class:`str`, optional - File-name of the results of the spotpy estimation. - If ``None``, it will be the current time + - ``"_estimate"``. - Default: ``None`` - """ - if self.setup.dummy: - raise ValueError( - "Estimate: for parameter estimation" - + " you can't use a dummy paramter." - ) - act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") - - # generate the filenames - if folder is None: - folder = os.path.join(os.getcwd(), self.name) - folder = os.path.abspath(folder) - if not os.path.exists(folder): - os.makedirs(folder) - - if dbname is None: - dbname = os.path.join(folder, act_time + "_db") - elif not os.path.isabs(dbname): - dbname = os.path.join(folder, dbname) - if traceplotname is None: - traceplotname = os.path.join(folder, act_time + "_paratrace.pdf") - elif not os.path.isabs(traceplotname): - traceplotname = os.path.join(folder, traceplotname) - if fittingplotname is None: - fittingplotname = os.path.join(folder, act_time + "_fit.pdf") - elif not os.path.isabs(fittingplotname): - fittingplotname = os.path.join(folder, fittingplotname) - if interactplotname is None: - interactplotname = os.path.join(folder, act_time + "_interact.pdf") - elif not os.path.isabs(interactplotname): - interactplotname = os.path.join(folder, interactplotname) - if estname is None: - paraname = os.path.join(folder, act_time + "_estimate.txt") - elif not os.path.isabs(estname): - paraname = os.path.join(folder, estname) - - # generate the parameter-names for plotting - paranames = dcopy(self.setup.para_names) - paralabels = [self.setup.val_plot_names[name] for name in paranames] - - if parallel == "mpi": - # send the dbname of rank0 - from mpi4py import MPI - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - if rank == 0: - print(rank, "send dbname:", dbname) - for i in range(1, size): - comm.send(dbname, dest=i, tag=0) - else: - dbname = comm.recv(source=0, tag=0) - print(rank, "got dbname:", dbname) - else: - rank = 0 - - if run: - # initialize the sampler - sampler = spotpy.algorithms.sceua( - self.setup, - dbname=dbname, - dbformat="csv", - parallel=parallel, - save_sim=True, - db_precision=np.float64, - ) - # start the estimation with the sce-ua algorithm - sampler.sample(rep, ngs=10, kstop=100, pcento=1e-4, peps=1e-3) - - if rank == 0: - # save best parameter-set - self.result = sampler.getdata() - para_opt = spotpy.analyser.get_best_parameterset( - self.result, maximize=False - ) - void_names = para_opt.dtype.names - self.para = [] - for name in void_names: - self.para.append(para_opt[0][name]) - np.savetxt(paraname, self.para) - - if rank == 0: - # plot the estimation-results - plotparatrace( - self.result, - parameternames=paranames, - parameterlabels=paralabels, - stdvalues=self.para, - plotname=traceplotname, - ) - plotfit_steady( - self.setup, - self.data, - self.para, - self.rad, - self.radnames, - self.extra_kw_names, - fittingplotname, - ) - plotparainteract(self.result, paralabels, interactplotname) - - def sensitivity( - self, - rep=None, - parallel="seq", - folder=None, - dbname=None, - plotname=None, - traceplotname=None, - sensname=None, - ): - """Run the sensitivity analysis. - - Parameters - ---------- - rep : :class:`int`, optional - The number of repetitions within the FAST algorithm in spotpy. - Default: estimated - parallel : :class:`str`, optional - State if the estimation should be run in parallel or not. Options: - - * ``"seq"``: sequential on one CPU - * ``"mpi"``: use the mpi4py package - - Default: ``"seq"`` - folder : :class:`str`, optional - Path to the output folder. If ``None`` the CWD is used. - Default: ``None`` - dbname : :class:`str`, optional - File-name of the database of the spotpy estimation. - If ``None``, it will be the current time + - ``"_sensitivity_db"``. - Default: ``None`` - plotname : :class:`str`, optional - File-name of the result plot of the sensitivity analysis. - If ``None``, it will be the current time + - ``"_sensitivity.pdf"``. - Default: ``None`` - traceplotname : :class:`str`, optional - File-name of the parameter trace plot of the spotpy sensitivity - analysis. - If ``None``, it will be the current time + - ``"_senstrace.pdf"``. - Default: ``None`` - sensname : :class:`str`, optional - File-name of the results of the FAST estimation. - If ``None``, it will be the current time + - ``"_estimate"``. - Default: ``None`` - """ - if len(self.setup.para_names) == 1 and not self.setup.dummy: - raise ValueError( - "Sensitivity: for estimation with only one parameter" - + " you have to use a dummy paramter." - ) - if rep is None: - rep = fast_rep(len(self.setup.para_names) + int(self.setup.dummy)) - - act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") - # generate the filenames - if folder is None: - folder = os.path.join(os.getcwd(), self.name) - folder = os.path.abspath(folder) - if not os.path.exists(folder): - os.makedirs(folder) - - if dbname is None: - dbname = os.path.join(folder, act_time + "_sensitivity_db") - elif not os.path.isabs(dbname): - dbname = os.path.join(folder, dbname) - if plotname is None: - plotname = os.path.join(folder, act_time + "_sensitivity.pdf") - elif not os.path.isabs(plotname): - plotname = os.path.join(folder, plotname) - if traceplotname is None: - traceplotname = os.path.join(folder, act_time + "_senstrace.pdf") - elif not os.path.isabs(traceplotname): - traceplotname = os.path.join(folder, traceplotname) - if sensname is None: - sensname = os.path.join(folder, act_time + "_FAST_estimate.txt") - elif not os.path.isabs(sensname): - sensname = os.path.join(folder, sensname) - - sens_base, sens_ext = os.path.splitext(sensname) - sensname1 = sens_base + "_S1" + sens_ext - - # generate the parameter-names for plotting - paranames = dcopy(self.setup.para_names) - paralabels = [self.setup.val_plot_names[name] for name in paranames] - - if self.setup.dummy: - paranames.append("dummy") - paralabels.append("dummy") - - if parallel == "mpi": - # send the dbname of rank0 - from mpi4py import MPI - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - if rank == 0: - print(rank, "send dbname:", dbname) - for i in range(1, size): - comm.send(dbname, dest=i, tag=0) - else: - dbname = comm.recv(source=0, tag=0) - print(rank, "got dbname:", dbname) - else: - rank = 0 - - # initialize the sampler - sampler = spotpy.algorithms.fast( - self.setup, - dbname=dbname, - dbformat="csv", - parallel=parallel, - save_sim=True, - db_precision=np.float64, - ) - sampler.sample(rep) - - if rank == 0: - data = sampler.getdata() - parmin = sampler.parameter()["minbound"] - parmax = sampler.parameter()["maxbound"] - bounds = list(zip(parmin, parmax)) - self.sens = sampler.analyze( - bounds, np.nan_to_num(data["like1"]), len(paranames), paranames - ) - np.savetxt(sensname, self.sens["ST"]) - np.savetxt(sensname1, self.sens["S1"]) - plotsensitivity(paralabels, self.sens, plotname) - plotparatrace( - data, - parameternames=paranames, - parameterlabels=paralabels, - stdvalues=None, - plotname=traceplotname, - ) - - -# ext_theis_3D - - -class ExtTheis3D(TransientPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters. It utilizes the extended theis solution in 3D which assumes - a log-normal distributed transmissivity field with a gaussian correlation - function and an anisotropy ratio 0 < e <= 1. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = { - "mu": (-16, -2), - "var": (0, 10), - "len_scale": (1, 50), - "lnS": (-13, -1), - "anis": (0, 1), - } - val_ranges = {} if val_ranges is None else val_ranges - val_fix = {"lat_ext": 1.0} if val_fix is None else val_fix - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log", "lnS": "log"} - val_kw_names = {"mu": "cond_gmean", "lnS": "storage"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - "lnS": r"$\ln(S)$", - "anis": "$e$", - } - super(ExtTheis3D, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.ext_theis_3d, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# ext_theis_2D - - -class ExtTheis2D(TransientPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters. It utilizes the extended theis solution in 2D which assumes - a log-normal distributed transmissivity field with a gaussian correlation - function. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = { - "mu": (-16, -2), - "var": (0, 10), - "len_scale": (1, 50), - "lnS": (-13, -1), - } - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log", "lnS": "log"} - val_kw_names = {"mu": "trans_gmean", "lnS": "storage"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - "lnS": r"$\ln(S)$", - } - super(ExtTheis2D, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.ext_theis_2d, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# neuman 2004 - - -class Neuman2004(TransientPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters. It utilizes the apparent Transmissivity from Neuman 2004 - which assumes a log-normal distributed transmissivity field - with an exponential correlation function. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = { - "mu": (-16, -2), - "var": (0, 10), - "len_scale": (1, 50), - "lnS": (-13, -1), - } - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log", "lnS": "log"} - val_kw_names = {"mu": "trans_gmean", "lnS": "storage"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - "lnS": r"$\ln(S)$", - } - super(Neuman2004, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.neuman2004, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# theis - - -class Theis(TransientPumping): - """Class for an estimation of homogeneous subsurface parameters. - - With this class you can run an estimation of homogeneous subsurface - parameters. It utilizes the theis solution. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = {"mu": (-16, -2), "lnS": (-13, -1)} - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log", "lnS": "log"} - val_kw_names = {"mu": "transmissivity", "lnS": "storage"} - val_plot_names = {"mu": r"$\ln(T)$", "lnS": r"$\ln(S)$"} - super(Theis, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.theis, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# ext_thiem_3d - - -class ExtThiem3D(SteadyPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters. It utilizes the extended thiem solution in 3D which assumes - a log-normal distributed transmissivity field with a gaussian correlation - function and an anisotropy ratio 0 < e <= 1. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - make_steady : :class:`bool`, optional - State if the tests should be converted to steady observations. - See: :any:`PumpingTest.make_steady`. - Default: True - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - make_steady=True, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = { - "mu": (-16, -2), - "var": (0, 10), - "len_scale": (1, 50), - "anis": (0, 1), - } - val_ranges = {} if val_ranges is None else val_ranges - val_fix = {"lat_ext": 1.0} if val_fix is None else val_fix - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log"} - val_kw_names = {"mu": "cond_gmean"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - "anis": "$e$", - } - super(ExtThiem3D, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.ext_thiem_3d, - val_ranges=val_ranges, - make_steady=make_steady, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# ext_thiem_2D - - -class ExtThiem2D(SteadyPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters. It utilizes the extended thiem solution in 2D which assumes - a log-normal distributed transmissivity field with a gaussian correlation - function. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - make_steady : :class:`bool`, optional - State if the tests should be converted to steady observations. - See: :any:`PumpingTest.make_steady`. - Default: True - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - make_steady=True, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = {"mu": (-16, -2), "var": (0, 10), "len_scale": (1, 50)} - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log"} - val_kw_names = {"mu": "trans_gmean"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - } - super(ExtThiem2D, self).__init__( - name=name, - campaign=campaign, - make_steady=make_steady, - type_curve=ana.ext_thiem_2d, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# neuman 2004 steady - - -class Neuman2004Steady(SteadyPumping): - """Class for an estimation of stochastic subsurface parameters. - - With this class you can run an estimation of statistical subsurface - parameters from steady drawdown. - It utilizes the apparent Transmissivity from Neuman 2004 - which assumes a log-normal distributed transmissivity field - with an exponential correlation function. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - make_steady : :class:`bool`, optional - State if the tests should be converted to steady observations. - See: :any:`PumpingTest.make_steady`. - Default: True - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - make_steady=True, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = {"mu": (-16, -2), "var": (0, 10), "len_scale": (1, 50)} - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log"} - val_kw_names = {"mu": "trans_gmean"} - val_plot_names = { - "mu": r"$\mu$", - "var": r"$\sigma^2$", - "len_scale": r"$\ell$", - } - super(Neuman2004Steady, self).__init__( - name=name, - campaign=campaign, - make_steady=make_steady, - type_curve=ana.neuman2004_steady, - val_ranges=val_ranges, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) - - -# thiem - - -class Thiem(SteadyPumping): - """Class for an estimation of homogeneous subsurface parameters. - - With this class you can run an estimation of homogeneous subsurface - parameters. It utilizes the thiem solution. - - Parameters - ---------- - name : :class:`str` - Name of the Estimation. - campaign : :class:`welltestpy.data.Campaign` - The pumping test campaign which should be used to estimate the - paramters - make_steady : :class:`bool`, optional - State if the tests should be converted to steady observations. - See: :any:`PumpingTest.make_steady`. - Default: True - val_ranges : :class:`dict` - Dictionary containing the fit-ranges for each value in the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Ranges should be a tuple containing min and max value. - val_fix : :class:`dict` or :any:`None` - Dictionary containing fixed values for the type-curve. - Names should be as in the type-curve signiture - or replaced in val_kw_names. - Default: None - testinclude : :class:`dict`, optional - dictonary of which tests should be included. If ``None`` is given, - all available tests are included. - Default: ``None`` - generate : :class:`bool`, optional - State if time stepping, processed observation data and estimation - setup should be generated with default values. - Default: ``False`` - """ - - def __init__( - self, - name, - campaign, - make_steady=True, - val_ranges=None, - val_fix=None, - testinclude=None, - generate=False, - ): - def_ranges = {"mu": (-16, -2)} - val_ranges = {} if val_ranges is None else val_ranges - for def_name, def_val in def_ranges.items(): - val_ranges.setdefault(def_name, def_val) - fit_type = {"mu": "log"} - val_kw_names = {"mu": "transmissivity"} - val_plot_names = {"mu": r"$\ln(T)$"} - super(Thiem, self).__init__( - name=name, - campaign=campaign, - type_curve=ana.thiem, - val_ranges=val_ranges, - make_steady=make_steady, - val_fix=val_fix, - fit_type=fit_type, - val_kw_names=val_kw_names, - val_plot_names=val_plot_names, - testinclude=testinclude, - generate=generate, - ) diff --git a/welltestpy/estimate/estimators.py b/welltestpy/estimate/estimators.py new file mode 100755 index 0000000..d9ecb9d --- /dev/null +++ b/welltestpy/estimate/estimators.py @@ -0,0 +1,650 @@ +# -*- coding: utf-8 -*- +""" +welltestpy subpackage providing classes for parameter estimation. + +.. currentmodule:: welltestpy.estimate.estimators + +The following classes are provided + +.. autosummary:: + ExtTheis3D + ExtTheis2D + Neuman2004 + Theis + ExtThiem3D + ExtThiem2D + Neuman2004Steady + Thiem +""" +import anaflow as ana + +from . import steady_lib, transient_lib + + +__all__ = [ + "ExtTheis3D", + "ExtTheis2D", + "Neuman2004", + "Theis", + "ExtThiem3D", + "ExtThiem2D", + "Neuman2004Steady", + "Thiem", +] + + +# ext_theis_3D + + +class ExtTheis3D(transient_lib.TransientPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters. It utilizes the extended theis solution in 3D which assumes + a log-normal distributed transmissivity field with a gaussian correlation + function and an anisotropy ratio 0 < e <= 1. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = { + "mu": (-16, -2), + "var": (0, 10), + "len_scale": (1, 50), + "lnS": (-13, -1), + "anis": (0, 1), + } + val_ranges = {} if val_ranges is None else val_ranges + val_fix = {"lat_ext": 1.0} if val_fix is None else val_fix + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log", "lnS": "log"} + val_kw_names = {"mu": "cond_gmean", "lnS": "storage"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + "lnS": r"$\ln(S)$", + "anis": "$e$", + } + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.ext_theis_3d, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# ext_theis_2D + + +class ExtTheis2D(transient_lib.TransientPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters. It utilizes the extended theis solution in 2D which assumes + a log-normal distributed transmissivity field with a gaussian correlation + function. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = { + "mu": (-16, -2), + "var": (0, 10), + "len_scale": (1, 50), + "lnS": (-13, -1), + } + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log", "lnS": "log"} + val_kw_names = {"mu": "trans_gmean", "lnS": "storage"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + "lnS": r"$\ln(S)$", + } + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.ext_theis_2d, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# neuman 2004 + + +class Neuman2004(transient_lib.TransientPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters. It utilizes the apparent Transmissivity from Neuman 2004 + which assumes a log-normal distributed transmissivity field + with an exponential correlation function. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = { + "mu": (-16, -2), + "var": (0, 10), + "len_scale": (1, 50), + "lnS": (-13, -1), + } + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log", "lnS": "log"} + val_kw_names = {"mu": "trans_gmean", "lnS": "storage"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + "lnS": r"$\ln(S)$", + } + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.neuman2004, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# theis + + +class Theis(transient_lib.TransientPumping): + """Class for an estimation of homogeneous subsurface parameters. + + With this class you can run an estimation of homogeneous subsurface + parameters. It utilizes the theis solution. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = {"mu": (-16, -2), "lnS": (-13, -1)} + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log", "lnS": "log"} + val_kw_names = {"mu": "transmissivity", "lnS": "storage"} + val_plot_names = {"mu": r"$\ln(T)$", "lnS": r"$\ln(S)$"} + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.theis, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# ext_thiem_3d + + +class ExtThiem3D(steady_lib.SteadyPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters. It utilizes the extended thiem solution in 3D which assumes + a log-normal distributed transmissivity field with a gaussian correlation + function and an anisotropy ratio 0 < e <= 1. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + make_steady : :class:`bool`, optional + State if the tests should be converted to steady observations. + See: :any:`PumpingTest.make_steady`. + Default: True + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + make_steady=True, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = { + "mu": (-16, -2), + "var": (0, 10), + "len_scale": (1, 50), + "anis": (0, 1), + } + val_ranges = {} if val_ranges is None else val_ranges + val_fix = {"lat_ext": 1.0} if val_fix is None else val_fix + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log"} + val_kw_names = {"mu": "cond_gmean"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + "anis": "$e$", + } + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.ext_thiem_3d, + val_ranges=val_ranges, + make_steady=make_steady, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# ext_thiem_2D + + +class ExtThiem2D(steady_lib.SteadyPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters. It utilizes the extended thiem solution in 2D which assumes + a log-normal distributed transmissivity field with a gaussian correlation + function. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + make_steady : :class:`bool`, optional + State if the tests should be converted to steady observations. + See: :any:`PumpingTest.make_steady`. + Default: True + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + make_steady=True, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = {"mu": (-16, -2), "var": (0, 10), "len_scale": (1, 50)} + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log"} + val_kw_names = {"mu": "trans_gmean"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + } + super().__init__( + name=name, + campaign=campaign, + make_steady=make_steady, + type_curve=ana.ext_thiem_2d, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# neuman 2004 steady + + +class Neuman2004Steady(steady_lib.SteadyPumping): + """Class for an estimation of stochastic subsurface parameters. + + With this class you can run an estimation of statistical subsurface + parameters from steady drawdown. + It utilizes the apparent Transmissivity from Neuman 2004 + which assumes a log-normal distributed transmissivity field + with an exponential correlation function. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + make_steady : :class:`bool`, optional + State if the tests should be converted to steady observations. + See: :any:`PumpingTest.make_steady`. + Default: True + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + make_steady=True, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = {"mu": (-16, -2), "var": (0, 10), "len_scale": (1, 50)} + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log"} + val_kw_names = {"mu": "trans_gmean"} + val_plot_names = { + "mu": r"$\mu$", + "var": r"$\sigma^2$", + "len_scale": r"$\ell$", + } + super().__init__( + name=name, + campaign=campaign, + make_steady=make_steady, + type_curve=ana.neuman2004_steady, + val_ranges=val_ranges, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) + + +# thiem + + +class Thiem(steady_lib.SteadyPumping): + """Class for an estimation of homogeneous subsurface parameters. + + With this class you can run an estimation of homogeneous subsurface + parameters. It utilizes the thiem solution. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + make_steady : :class:`bool`, optional + State if the tests should be converted to steady observations. + See: :any:`PumpingTest.make_steady`. + Default: True + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + make_steady=True, + val_ranges=None, + val_fix=None, + testinclude=None, + generate=False, + ): + def_ranges = {"mu": (-16, -2)} + val_ranges = {} if val_ranges is None else val_ranges + for def_name, def_val in def_ranges.items(): + val_ranges.setdefault(def_name, def_val) + fit_type = {"mu": "log"} + val_kw_names = {"mu": "transmissivity"} + val_plot_names = {"mu": r"$\ln(T)$"} + super().__init__( + name=name, + campaign=campaign, + type_curve=ana.thiem, + val_ranges=val_ranges, + make_steady=make_steady, + val_fix=val_fix, + fit_type=fit_type, + val_kw_names=val_kw_names, + val_plot_names=val_plot_names, + testinclude=testinclude, + generate=generate, + ) diff --git a/welltestpy/estimate/spotpy_classes.py b/welltestpy/estimate/spotpylib.py similarity index 89% rename from welltestpy/estimate/spotpy_classes.py rename to welltestpy/estimate/spotpylib.py index c2bdfb3..2be42db 100644 --- a/welltestpy/estimate/spotpy_classes.py +++ b/welltestpy/estimate/spotpylib.py @@ -2,22 +2,21 @@ """ welltestpy subpackage providing Spotpy classes for the estimating. -.. currentmodule:: welltestpy.estimate.spotpy_classes +.. currentmodule:: welltestpy.estimate.spotpylib The following functions and classes are provided .. autosummary:: TypeCurve + fast_rep """ -from __future__ import absolute_import, division, print_function - import functools as ft import numpy as np import spotpy -__all__ = ["TypeCurve"] +__all__ = ["TypeCurve", "fast_rep"] # functions for fitting @@ -30,14 +29,31 @@ "exp": np.log, "squareroot": lambda x: np.power(x, 2), "sqrt": lambda x: np.power(x, 2), - "quadratic": lambda x: np.sqrt(x), - "quad": lambda x: np.sqrt(x), + "quadratic": np.sqrt, + "quad": np.sqrt, "inverse": lambda x: 1.0 / x, "inv": lambda x: 1.0 / x, } -class TypeCurve(object): +def fast_rep(para_no, infer_fac=4, freq_step=2): + """Get number of iterations needed for the FAST algorithm. + + Parameters + ---------- + para_no : :class:`int` + Number of parameters in the model. + infer_fac : :class:`int`, optional + The inference fractor. Default: 4 + freq_step : :class:`int`, optional + The frequency step. Default: 2 + """ + return 2 * int( + para_no * (1 + 4 * infer_fac ** 2 * (1 + (para_no - 2) * freq_step)) + ) + + +class TypeCurve: r"""Spotpy class for an estimation of subsurface parameters. This class fits a given Type Curve to given data. diff --git a/welltestpy/estimate/steady_lib.py b/welltestpy/estimate/steady_lib.py new file mode 100755 index 0000000..53536b6 --- /dev/null +++ b/welltestpy/estimate/steady_lib.py @@ -0,0 +1,602 @@ +# -*- coding: utf-8 -*- +""" +welltestpy subpackage providing base classe for steady state estimations. + +.. currentmodule:: welltestpy.estimate.steady_lib + +The following classes are provided + +.. autosummary:: + SteadyPumping +""" +from copy import deepcopy as dcopy +import os +import time as timemodule + +import numpy as np +import spotpy + +from ..data import testslib +from ..process import processlib +from . import spotpylib +from ..tools import plotter + +__all__ = [ + "SteadyPumping", +] + + +class SteadyPumping: + """Class to estimate steady Type-Curve parameters. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + type_curve : :any:`callable` + The given type-curve. Output will be reshaped to flat array. + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + make_steady : :class:`bool`, optional + State if the tests should be converted to steady observations. + See: :any:`PumpingTest.make_steady`. + Default: True + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + fit_type : :class:`dict` or :any:`None` + Dictionary containing fitting type for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + fit_type can be "lin", "log" (np.exp(val) will be used) + or a callable function. + By default, values will be fit linearly. + Default: None + val_kw_names : :class:`dict` or :any:`None` + Dictionary containing keyword names in the type-curve for each value. + + {value-name: kwargs-name in type_curve} + + This is usefull if fitting is not done by linear values. + By default, parameter names will be value names. + Default: None + val_plot_names : :class:`dict` or :any:`None` + Dictionary containing keyword names in the type-curve for each value. + + {value-name: string for plot legend} + + This is usefull to get better plots. + By default, parameter names will be value names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + type_curve, + val_ranges, + make_steady=True, + val_fix=None, + fit_type=None, + val_kw_names=None, + val_plot_names=None, + testinclude=None, + generate=False, + ): + val_fix = {} if val_fix is None else val_fix + fit_type = {} if fit_type is None else fit_type + val_kw_names = {} if val_kw_names is None else val_kw_names + val_plot_names = {} if val_plot_names is None else val_plot_names + self.setup_kw = { + "type_curve": type_curve, + "val_ranges": val_ranges, + "val_fix": val_fix, + "fit_type": fit_type, + "val_kw_names": val_kw_names, + "val_plot_names": val_plot_names, + } + """:class:`dict`: TypeCurve Spotpy Setup definition""" + self.name = name + """:class:`str`: Name of the Estimation""" + self.campaign_raw = dcopy(campaign) + """:class:`welltestpy.data.Campaign`:\ + Copy of the original input campaign""" + self.campaign = dcopy(campaign) + """:class:`welltestpy.data.Campaign`:\ + Copy of the input campaign to be modified""" + + self.prate = None + """:class:`float`: Pumpingrate at the pumping well""" + + self.rad = None + """:class:`numpy.ndarray`: array of the radii from the wells""" + self.data = None + """:class:`numpy.ndarray`: observation data""" + self.radnames = None + """:class:`numpy.ndarray`: names of the radii well combination""" + self.r_ref = None + """:class:`float`: reference radius of the biggest distance""" + self.h_ref = None + """:class:`float`: reference head at the biggest distance""" + + self.estimated_para = {} + """:class:`dict`: estimated parameters by name""" + self.result = None + """:class:`list`: result of the spotpy estimation""" + self.sens = None + """:class:`dict`: result of the spotpy sensitivity analysis""" + self.testinclude = {} + """:class:`dict`: dictonary of which tests should be included""" + + if testinclude is None: + tests = list(self.campaign.tests.keys()) + self.testinclude = {} + for test in tests: + self.testinclude[test] = self.campaign.tests[ + test + ].observationwells + elif not isinstance(testinclude, dict): + self.testinclude = {} + for test in testinclude: + self.testinclude[test] = self.campaign.tests[ + test + ].observationwells + else: + self.testinclude = testinclude + + for test in self.testinclude: + if not isinstance(self.campaign.tests[test], testslib.PumpingTest): + raise ValueError(test + " is not a pumping test.") + if make_steady is not False: + if make_steady is True: + make_steady = "latest" + self.campaign.tests[test].make_steady(make_steady) + if not self.campaign.tests[test].constant_rate: + raise ValueError(test + " is not a constant rate test.") + if ( + not self.campaign.tests[test].state( + wells=self.testinclude[test] + ) + == "steady" + ): + raise ValueError(test + ": selection is not steady.") + + rwell_list = [] + rinf_list = [] + for test in self.testinclude: + pwell = self.campaign.tests[test].pumpingwell + rwell_list.append(self.campaign.wells[pwell].radius) + rinf_list.append(self.campaign.tests[test].radius) + self.rwell = min(rwell_list) + """:class:`float`: radius of the pumping wells""" + self.rinf = max(rinf_list) + """:class:`float`: radius of the furthest wells""" + + if generate: + self.setpumprate() + self.gen_data() + self.gen_setup() + + def setpumprate(self, prate=-1.0): + """Set a uniform pumping rate at all pumpingwells wells. + + We assume linear scaling by the pumpingrate. + + Parameters + ---------- + prate : :class:`float`, optional + Pumping rate. Default: ``-1.0`` + """ + for test in self.testinclude: + processlib.normpumptest( + self.campaign.tests[test], pumpingrate=prate + ) + self.prate = prate + + def gen_data(self): + """Generate the observed drawdown. + + It will also generate an array containing all radii of all well + combinations. + """ + rad = np.array([]) + data = np.array([]) + + radnames = [] + + for test in self.testinclude: + pwell = self.campaign.wells[self.campaign.tests[test].pumpingwell] + for obs in self.testinclude[test]: + temphead = self.campaign.tests[test].observations[obs]() + data = np.hstack((data, temphead)) + + owell = self.campaign.wells[obs] + if pwell == owell: + temprad = pwell.radius + else: + temprad = pwell - owell + rad = np.hstack((rad, temprad)) + + tempname = (self.campaign.tests[test].pumpingwell, obs) + radnames.append(tempname) + + # sort everything by the radii + idx = rad.argsort() + radnames = np.array(radnames) + self.rad = rad[idx] + self.data = data[idx] + self.radnames = radnames[idx] + self.r_ref = self.rad[-1] + self.h_ref = self.data[-1] + + def gen_setup( + self, + prate_kw="rate", + rad_kw="rad", + r_ref_kw="r_ref", + h_ref_kw="h_ref", + dummy=False, + ): + """Generate the Spotpy Setup. + + Parameters + ---------- + prate_kw : :class:`str`, optional + Keyword name for the pumping rate in the used type curve. + Default: "rate" + rad_kw : :class:`str`, optional + Keyword name for the radius in the used type curve. + Default: "rad" + r_ref_kw : :class:`str`, optional + Keyword name for the reference radius in the used type curve. + Default: "r_ref" + h_ref_kw : :class:`str`, optional + Keyword name for the reference head in the used type curve. + Default: "h_ref" + dummy : :class:`bool`, optional + Add a dummy parameter to the model. This could be used to equalize + sensitivity analysis. + Default: False + """ + self.extra_kw_names = { + "Qw": prate_kw, + "rad": rad_kw, + "r_ref": r_ref_kw, + "h_ref": h_ref_kw, + } + self.setup_kw["val_fix"].setdefault(prate_kw, self.prate) + self.setup_kw["val_fix"].setdefault(rad_kw, self.rad) + self.setup_kw["val_fix"].setdefault(r_ref_kw, self.r_ref) + self.setup_kw["val_fix"].setdefault(h_ref_kw, self.h_ref) + self.setup_kw.setdefault("data", self.data) + self.setup_kw["dummy"] = dummy + self.setup = spotpylib.TypeCurve(**self.setup_kw) + + def run( + self, + rep=5000, + parallel="seq", + run=True, + folder=None, + dbname=None, + traceplotname=None, + fittingplotname=None, + interactplotname=None, + estname=None, + ): + """Run the estimation. + + Parameters + ---------- + rep : :class:`int`, optional + The number of repetitions within the SCEua algorithm in spotpy. + Default: ``5000`` + parallel : :class:`str`, optional + State if the estimation should be run in parallel or not. Options: + + * ``"seq"``: sequential on one CPU + * ``"mpi"``: use the mpi4py package + + Default: ``"seq"`` + run : :class:`bool`, optional + State if the estimation should be executed. Otherwise all plots + will be done with the previous results. + Default: ``True`` + folder : :class:`str`, optional + Path to the output folder. If ``None`` the CWD is used. + Default: ``None`` + dbname : :class:`str`, optional + File-name of the database of the spotpy estimation. + If ``None``, it will be the current time + + ``"_db"``. + Default: ``None`` + traceplotname : :class:`str`, optional + File-name of the parameter trace plot of the spotpy estimation. + If ``None``, it will be the current time + + ``"_paratrace.pdf"``. + Default: ``None`` + fittingplotname : :class:`str`, optional + File-name of the fitting plot of the estimation. + If ``None``, it will be the current time + + ``"_fit.pdf"``. + Default: ``None`` + interactplotname : :class:`str`, optional + File-name of the parameter interaction plot + of the spotpy estimation. + If ``None``, it will be the current time + + ``"_parainteract.pdf"``. + Default: ``None`` + estname : :class:`str`, optional + File-name of the results of the spotpy estimation. + If ``None``, it will be the current time + + ``"_estimate"``. + Default: ``None`` + """ + if self.setup.dummy: + raise ValueError( + "Estimate: for parameter estimation" + + " you can't use a dummy paramter." + ) + act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") + + # generate the filenames + if folder is None: + folder = os.path.join(os.getcwd(), self.name) + folder = os.path.abspath(folder) + if not os.path.exists(folder): + os.makedirs(folder) + + if dbname is None: + dbname = os.path.join(folder, act_time + "_db") + elif not os.path.isabs(dbname): + dbname = os.path.join(folder, dbname) + if traceplotname is None: + traceplotname = os.path.join(folder, act_time + "_paratrace.pdf") + elif not os.path.isabs(traceplotname): + traceplotname = os.path.join(folder, traceplotname) + if fittingplotname is None: + fittingplotname = os.path.join(folder, act_time + "_fit.pdf") + elif not os.path.isabs(fittingplotname): + fittingplotname = os.path.join(folder, fittingplotname) + if interactplotname is None: + interactplotname = os.path.join(folder, act_time + "_interact.pdf") + elif not os.path.isabs(interactplotname): + interactplotname = os.path.join(folder, interactplotname) + if estname is None: + paraname = os.path.join(folder, act_time + "_estimate.txt") + elif not os.path.isabs(estname): + paraname = os.path.join(folder, estname) + + # generate the parameter-names for plotting + paranames = dcopy(self.setup.para_names) + paralabels = [self.setup.val_plot_names[name] for name in paranames] + + if parallel == "mpi": + # send the dbname of rank0 + from mpi4py import MPI + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + if rank == 0: + print(rank, "send dbname:", dbname) + for i in range(1, size): + comm.send(dbname, dest=i, tag=0) + else: + dbname = comm.recv(source=0, tag=0) + print(rank, "got dbname:", dbname) + else: + rank = 0 + + if run: + # initialize the sampler + sampler = spotpy.algorithms.sceua( + self.setup, + dbname=dbname, + dbformat="csv", + parallel=parallel, + save_sim=True, + db_precision=np.float64, + ) + # start the estimation with the sce-ua algorithm + sampler.sample(rep, ngs=10, kstop=100, pcento=1e-4, peps=1e-3) + + if rank == 0: + # save best parameter-set + self.result = sampler.getdata() + para_opt = spotpy.analyser.get_best_parameterset( + self.result, maximize=False + ) + void_names = para_opt.dtype.names + para = [] + header = [] + for name in void_names: + para.append(para_opt[0][name]) + header.append(name[3:]) + self.estimated_para[header[-1]] = para[-1] + np.savetxt(paraname, para, header=" ".join(header)) + + if rank == 0: + # plot the estimation-results + plotter.plotparatrace( + result=self.result, + parameternames=paranames, + parameterlabels=paralabels, + stdvalues=self.estimated_para, + plotname=traceplotname, + ) + plotter.plotfit_steady( + setup=self.setup, + data=self.data, + para=self.estimated_para, + rad=self.rad, + radnames=self.radnames, + extra=self.extra_kw_names, + plotname=fittingplotname, + ) + plotter.plotparainteract(self.result, paralabels, interactplotname) + + def sensitivity( + self, + rep=None, + parallel="seq", + folder=None, + dbname=None, + plotname=None, + traceplotname=None, + sensname=None, + ): + """Run the sensitivity analysis. + + Parameters + ---------- + rep : :class:`int`, optional + The number of repetitions within the FAST algorithm in spotpy. + Default: estimated + parallel : :class:`str`, optional + State if the estimation should be run in parallel or not. Options: + + * ``"seq"``: sequential on one CPU + * ``"mpi"``: use the mpi4py package + + Default: ``"seq"`` + folder : :class:`str`, optional + Path to the output folder. If ``None`` the CWD is used. + Default: ``None`` + dbname : :class:`str`, optional + File-name of the database of the spotpy estimation. + If ``None``, it will be the current time + + ``"_sensitivity_db"``. + Default: ``None`` + plotname : :class:`str`, optional + File-name of the result plot of the sensitivity analysis. + If ``None``, it will be the current time + + ``"_sensitivity.pdf"``. + Default: ``None`` + traceplotname : :class:`str`, optional + File-name of the parameter trace plot of the spotpy sensitivity + analysis. + If ``None``, it will be the current time + + ``"_senstrace.pdf"``. + Default: ``None`` + sensname : :class:`str`, optional + File-name of the results of the FAST estimation. + If ``None``, it will be the current time + + ``"_estimate"``. + Default: ``None`` + """ + if len(self.setup.para_names) == 1 and not self.setup.dummy: + raise ValueError( + "Sensitivity: for estimation with only one parameter" + + " you have to use a dummy paramter." + ) + if rep is None: + rep = spotpylib.fast_rep( + len(self.setup.para_names) + int(self.setup.dummy) + ) + + act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") + # generate the filenames + if folder is None: + folder = os.path.join(os.getcwd(), self.name) + folder = os.path.abspath(folder) + if not os.path.exists(folder): + os.makedirs(folder) + + if dbname is None: + dbname = os.path.join(folder, act_time + "_sensitivity_db") + elif not os.path.isabs(dbname): + dbname = os.path.join(folder, dbname) + if plotname is None: + plotname = os.path.join(folder, act_time + "_sensitivity.pdf") + elif not os.path.isabs(plotname): + plotname = os.path.join(folder, plotname) + if traceplotname is None: + traceplotname = os.path.join(folder, act_time + "_senstrace.pdf") + elif not os.path.isabs(traceplotname): + traceplotname = os.path.join(folder, traceplotname) + if sensname is None: + sensname = os.path.join(folder, act_time + "_FAST_estimate.txt") + elif not os.path.isabs(sensname): + sensname = os.path.join(folder, sensname) + + sens_base, sens_ext = os.path.splitext(sensname) + sensname1 = sens_base + "_S1" + sens_ext + + # generate the parameter-names for plotting + paranames = dcopy(self.setup.para_names) + paralabels = [self.setup.val_plot_names[name] for name in paranames] + + if self.setup.dummy: + paranames.append("dummy") + paralabels.append("dummy") + + if parallel == "mpi": + # send the dbname of rank0 + from mpi4py import MPI + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + if rank == 0: + print(rank, "send dbname:", dbname) + for i in range(1, size): + comm.send(dbname, dest=i, tag=0) + else: + dbname = comm.recv(source=0, tag=0) + print(rank, "got dbname:", dbname) + else: + rank = 0 + + # initialize the sampler + sampler = spotpy.algorithms.fast( + self.setup, + dbname=dbname, + dbformat="csv", + parallel=parallel, + save_sim=True, + db_precision=np.float64, + ) + sampler.sample(rep) + + if rank == 0: + data = sampler.getdata() + parmin = sampler.parameter()["minbound"] + parmax = sampler.parameter()["maxbound"] + bounds = list(zip(parmin, parmax)) + sens_est = sampler.analyze( + bounds, np.nan_to_num(data["like1"]), len(paranames), paranames + ) + self.sens = {} + for sen_typ in sens_est: + self.sens[sen_typ] = { + par: sen for par, sen in zip(paranames, sens_est[sen_typ]) + } + header = " ".join(paranames) + np.savetxt(sensname, sens_est["ST"], header=header) + np.savetxt(sensname1, sens_est["S1"], header=header) + plotter.plotsensitivity(paralabels, sens_est, plotname) + plotter.plotparatrace( + data, + parameternames=paranames, + parameterlabels=paralabels, + stdvalues=None, + plotname=traceplotname, + ) diff --git a/welltestpy/estimate/transient_lib.py b/welltestpy/estimate/transient_lib.py new file mode 100755 index 0000000..dcc1017 --- /dev/null +++ b/welltestpy/estimate/transient_lib.py @@ -0,0 +1,633 @@ +# -*- coding: utf-8 -*- +""" +welltestpy subpackage providing base classe for transient estimations. + +.. currentmodule:: welltestpy.estimate.transient_lib + +The following classes are provided + +.. autosummary:: + TransientPumping +""" +from copy import deepcopy as dcopy +import os +import time as timemodule + +import numpy as np +import spotpy +import anaflow as ana + +from ..data import testslib +from ..process import processlib +from . import spotpylib +from ..tools import plotter + +__all__ = [ + "TransientPumping", +] + + +class TransientPumping: + """Class to estimate transient Type-Curve parameters. + + Parameters + ---------- + name : :class:`str` + Name of the Estimation. + campaign : :class:`welltestpy.data.Campaign` + The pumping test campaign which should be used to estimate the + paramters + type_curve : :any:`callable` + The given type-curve. Output will be reshaped to flat array. + val_ranges : :class:`dict` + Dictionary containing the fit-ranges for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Ranges should be a tuple containing min and max value. + val_fix : :class:`dict` or :any:`None` + Dictionary containing fixed values for the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + Default: None + fit_type : :class:`dict` or :any:`None` + Dictionary containing fitting type for each value in the type-curve. + Names should be as in the type-curve signiture + or replaced in val_kw_names. + fit_type can be "lin", "log" (np.exp(val) will be used) + or a callable function. + By default, values will be fit linearly. + Default: None + val_kw_names : :class:`dict` or :any:`None` + Dictionary containing keyword names in the type-curve for each value. + + {value-name: kwargs-name in type_curve} + + This is usefull if fitting is not done by linear values. + By default, parameter names will be value names. + Default: None + val_plot_names : :class:`dict` or :any:`None` + Dictionary containing keyword names in the type-curve for each value. + + {value-name: string for plot legend} + + This is usefull to get better plots. + By default, parameter names will be value names. + Default: None + testinclude : :class:`dict`, optional + dictonary of which tests should be included. If ``None`` is given, + all available tests are included. + Default: ``None`` + generate : :class:`bool`, optional + State if time stepping, processed observation data and estimation + setup should be generated with default values. + Default: ``False`` + """ + + def __init__( + self, + name, + campaign, + type_curve, + val_ranges, + val_fix=None, + fit_type=None, + val_kw_names=None, + val_plot_names=None, + testinclude=None, + generate=False, + ): + val_fix = {} if val_fix is None else val_fix + fit_type = {} if fit_type is None else fit_type + val_kw_names = {} if val_kw_names is None else val_kw_names + val_plot_names = {} if val_plot_names is None else val_plot_names + self.setup_kw = { + "type_curve": type_curve, + "val_ranges": val_ranges, + "val_fix": val_fix, + "fit_type": fit_type, + "val_kw_names": val_kw_names, + "val_plot_names": val_plot_names, + } + """:class:`dict`: TypeCurve Spotpy Setup definition""" + self.name = name + """:class:`str`: Name of the Estimation""" + self.campaign_raw = dcopy(campaign) + """:class:`welltestpy.data.Campaign`:\ + Copy of the original input campaign""" + self.campaign = dcopy(campaign) + """:class:`welltestpy.data.Campaign`:\ + Copy of the input campaign to be modified""" + + self.prate = None + """:class:`float`: Pumpingrate at the pumping well""" + + self.time = None + """:class:`numpy.ndarray`: time points of the observation""" + self.rad = None + """:class:`numpy.ndarray`: array of the radii from the wells""" + self.data = None + """:class:`numpy.ndarray`: observation data""" + self.radnames = None + """:class:`numpy.ndarray`: names of the radii well combination""" + + self.estimated_para = {} + """:class:`dict`: estimated parameters by name""" + self.result = None + """:class:`list`: result of the spotpy estimation""" + self.sens = None + """:class:`dict`: result of the spotpy sensitivity analysis""" + self.testinclude = {} + """:class:`dict`: dictonary of which tests should be included""" + + if testinclude is None: + tests = list(self.campaign.tests.keys()) + self.testinclude = {} + for test in tests: + self.testinclude[test] = self.campaign.tests[ + test + ].observationwells + elif not isinstance(testinclude, dict): + self.testinclude = {} + for test in testinclude: + self.testinclude[test] = self.campaign.tests[ + test + ].observationwells + else: + self.testinclude = testinclude + + for test in self.testinclude: + if not isinstance(self.campaign.tests[test], testslib.PumpingTest): + raise ValueError(test + " is not a pumping test.") + if not self.campaign.tests[test].constant_rate: + raise ValueError(test + " is not a constant rate test.") + if ( + not self.campaign.tests[test].state( + wells=self.testinclude[test] + ) + == "transient" + ): + raise ValueError(test + ": selection is not transient.") + + rwell_list = [] + rinf_list = [] + for test in self.testinclude: + pwell = self.campaign.tests[test].pumpingwell + rwell_list.append(self.campaign.wells[pwell].radius) + rinf_list.append(self.campaign.tests[test].radius) + self.rwell = min(rwell_list) + """:class:`float`: radius of the pumping wells""" + self.rinf = max(rinf_list) + """:class:`float`: radius of the furthest wells""" + + if generate: + self.setpumprate() + self.settime() + self.gen_data() + self.gen_setup() + + def setpumprate(self, prate=-1.0): + """Set a uniform pumping rate at all pumpingwells wells. + + We assume linear scaling by the pumpingrate. + + Parameters + ---------- + prate : :class:`float`, optional + Pumping rate. Default: ``-1.0`` + """ + for test in self.testinclude: + processlib.normpumptest( + self.campaign.tests[test], pumpingrate=prate + ) + self.prate = prate + + def settime(self, time=None, tmin=10.0, tmax=np.inf, typ="quad", steps=10): + """Set uniform time points for the observations. + + Parameters + ---------- + time : :class:`numpy.ndarray`, optional + Array of specified time points. If ``None`` is given, they will + be determind by the observation data. + Default: ``None`` + tmin : :class:`float`, optional + Minimal time value. It will set a minimal value of 10s. + Default: ``10`` + tmax : :class:`float`, optional + Maximal time value. + Default: ``inf`` + typ : :class:`str` or :class:`float`, optional + Typ of the time selection. You can select from: + + * ``"exp"``: for exponential behavior + * ``"log"``: for logarithmic behavior + * ``"geo"``: for geometric behavior + * ``"lin"``: for linear behavior + * ``"quad"``: for quadratic behavior + * ``"cub"``: for cubic behavior + * :class:`float`: here you can specifi any exponent + ("quad" would be equivalent to 2) + + Default: "quad" + + steps : :class:`int`, optional + Number of generated time steps. Default: 10 + """ + if time is None: + for test in self.testinclude: + for obs in self.testinclude[test]: + _, temptime = self.campaign.tests[test].observations[obs]() + tmin = max(tmin, temptime.min()) + tmax = min(tmax, temptime.max()) + tmin = tmax if tmin > tmax else tmin + time = ana.specialrange(tmin, tmax, steps, typ) + + for test in self.testinclude: + for obs in self.testinclude[test]: + processlib.filterdrawdown( + self.campaign.tests[test].observations[obs], tout=time + ) + + self.time = time + + def gen_data(self): + """Generate the observed drawdown at given time points. + + It will also generate an array containing all radii of all well + combinations. + """ + rad = np.array([]) + data = None + + radnames = [] + + for test in self.testinclude: + pwell = self.campaign.wells[self.campaign.tests[test].pumpingwell] + for obs in self.testinclude[test]: + temphead, _ = self.campaign.tests[test].observations[obs]() + temphead = np.array(temphead).reshape(-1)[np.newaxis].T + + if data is None: + data = dcopy(temphead) + else: + data = np.hstack((data, temphead)) + + owell = self.campaign.wells[obs] + + if pwell == owell: + temprad = pwell.radius + else: + temprad = pwell - owell + rad = np.hstack((rad, temprad)) + + tempname = (self.campaign.tests[test].pumpingwell, obs) + radnames.append(tempname) + + # sort everything by the radii + idx = rad.argsort() + radnames = np.array(radnames) + self.rad = rad[idx] + self.data = data[:, idx] + self.radnames = radnames[idx] + + def gen_setup( + self, prate_kw="rate", rad_kw="rad", time_kw="time", dummy=False + ): + """Generate the Spotpy Setup. + + Parameters + ---------- + prate_kw : :class:`str`, optional + Keyword name for the pumping rate in the used type curve. + Default: "rate" + rad_kw : :class:`str`, optional + Keyword name for the radius in the used type curve. + Default: "rad" + time_kw : :class:`str`, optional + Keyword name for the time in the used type curve. + Default: "time" + dummy : :class:`bool`, optional + Add a dummy parameter to the model. This could be used to equalize + sensitivity analysis. + Default: False + """ + self.extra_kw_names = {"Qw": prate_kw, "rad": rad_kw, "time": time_kw} + self.setup_kw["val_fix"].setdefault(prate_kw, self.prate) + self.setup_kw["val_fix"].setdefault(rad_kw, self.rad) + self.setup_kw["val_fix"].setdefault(time_kw, self.time) + self.setup_kw.setdefault("data", self.data) + self.setup_kw["dummy"] = dummy + self.setup = spotpylib.TypeCurve(**self.setup_kw) + + def run( + self, + rep=5000, + parallel="seq", + run=True, + folder=None, + dbname=None, + traceplotname=None, + fittingplotname=None, + interactplotname=None, + estname=None, + ): + """Run the estimation. + + Parameters + ---------- + rep : :class:`int`, optional + The number of repetitions within the SCEua algorithm in spotpy. + Default: ``5000`` + parallel : :class:`str`, optional + State if the estimation should be run in parallel or not. Options: + + * ``"seq"``: sequential on one CPU + * ``"mpi"``: use the mpi4py package + + Default: ``"seq"`` + run : :class:`bool`, optional + State if the estimation should be executed. Otherwise all plots + will be done with the previous results. + Default: ``True`` + folder : :class:`str`, optional + Path to the output folder. If ``None`` the CWD is used. + Default: ``None`` + dbname : :class:`str`, optional + File-name of the database of the spotpy estimation. + If ``None``, it will be the current time + + ``"_db"``. + Default: ``None`` + traceplotname : :class:`str`, optional + File-name of the parameter trace plot of the spotpy estimation. + If ``None``, it will be the current time + + ``"_paratrace.pdf"``. + Default: ``None`` + fittingplotname : :class:`str`, optional + File-name of the fitting plot of the estimation. + If ``None``, it will be the current time + + ``"_fit.pdf"``. + Default: ``None`` + interactplotname : :class:`str`, optional + File-name of the parameter interaction plot + of the spotpy estimation. + If ``None``, it will be the current time + + ``"_parainteract.pdf"``. + Default: ``None`` + estname : :class:`str`, optional + File-name of the results of the spotpy estimation. + If ``None``, it will be the current time + + ``"_estimate"``. + Default: ``None`` + """ + if self.setup.dummy: + raise ValueError( + "Estimate: for parameter estimation" + + " you can't use a dummy paramter." + ) + act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") + + # generate the filenames + if folder is None: + folder = os.path.join(os.getcwd(), self.name) + folder = os.path.abspath(folder) + if not os.path.exists(folder): + os.makedirs(folder) + + if dbname is None: + dbname = os.path.join(folder, act_time + "_db") + elif not os.path.isabs(dbname): + dbname = os.path.join(folder, dbname) + if traceplotname is None: + traceplotname = os.path.join(folder, act_time + "_paratrace.pdf") + elif not os.path.isabs(traceplotname): + traceplotname = os.path.join(folder, traceplotname) + if fittingplotname is None: + fittingplotname = os.path.join(folder, act_time + "_fit.pdf") + elif not os.path.isabs(fittingplotname): + fittingplotname = os.path.join(folder, fittingplotname) + if interactplotname is None: + interactplotname = os.path.join(folder, act_time + "_interact.pdf") + elif not os.path.isabs(interactplotname): + interactplotname = os.path.join(folder, interactplotname) + if estname is None: + paraname = os.path.join(folder, act_time + "_estimate.txt") + elif not os.path.isabs(estname): + paraname = os.path.join(folder, estname) + + # generate the parameter-names for plotting + paranames = dcopy(self.setup.para_names) + paralabels = [self.setup.val_plot_names[name] for name in paranames] + + if parallel == "mpi": + # send the dbname of rank0 + from mpi4py import MPI + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + if rank == 0: + print(rank, "send dbname:", dbname) + for i in range(1, size): + comm.send(dbname, dest=i, tag=0) + else: + dbname = comm.recv(source=0, tag=0) + print(rank, "got dbname:", dbname) + else: + rank = 0 + + if run: + # initialize the sampler + sampler = spotpy.algorithms.sceua( + self.setup, + dbname=dbname, + dbformat="csv", + parallel=parallel, + save_sim=True, + db_precision=np.float64, + ) + # start the estimation with the sce-ua algorithm + sampler.sample(rep, ngs=10, kstop=100, pcento=1e-4, peps=1e-3) + + if rank == 0: + # save best parameter-set + self.result = sampler.getdata() + para_opt = spotpy.analyser.get_best_parameterset( + self.result, maximize=False + ) + void_names = para_opt.dtype.names + para = [] + header = [] + for name in void_names: + para.append(para_opt[0][name]) + header.append(name[3:]) + self.estimated_para[header[-1]] = para[-1] + np.savetxt(paraname, para, header=" ".join(header)) + + if rank == 0: + # plot the estimation-results + plotter.plotparatrace( + self.result, + parameternames=paranames, + parameterlabels=paralabels, + stdvalues=self.estimated_para, + plotname=traceplotname, + ) + plotter.plotfit_transient( + setup=self.setup, + data=self.data, + para=self.estimated_para, + rad=self.rad, + time=self.time, + radnames=self.radnames, + extra=self.extra_kw_names, + plotname=fittingplotname, + ) + plotter.plotparainteract(self.result, paralabels, interactplotname) + + def sensitivity( + self, + rep=None, + parallel="seq", + folder=None, + dbname=None, + plotname=None, + traceplotname=None, + sensname=None, + ): + """Run the sensitivity analysis. + + Parameters + ---------- + rep : :class:`int`, optional + The number of repetitions within the FAST algorithm in spotpy. + Default: estimated + parallel : :class:`str`, optional + State if the estimation should be run in parallel or not. Options: + + * ``"seq"``: sequential on one CPU + * ``"mpi"``: use the mpi4py package + + Default: ``"seq"`` + folder : :class:`str`, optional + Path to the output folder. If ``None`` the CWD is used. + Default: ``None`` + dbname : :class:`str`, optional + File-name of the database of the spotpy estimation. + If ``None``, it will be the current time + + ``"_sensitivity_db"``. + Default: ``None`` + plotname : :class:`str`, optional + File-name of the result plot of the sensitivity analysis. + If ``None``, it will be the current time + + ``"_sensitivity.pdf"``. + Default: ``None`` + traceplotname : :class:`str`, optional + File-name of the parameter trace plot of the spotpy sensitivity + analysis. + If ``None``, it will be the current time + + ``"_senstrace.pdf"``. + Default: ``None`` + sensname : :class:`str`, optional + File-name of the results of the FAST estimation. + If ``None``, it will be the current time + + ``"_estimate"``. + Default: ``None`` + """ + if len(self.setup.para_names) == 1 and not self.setup.dummy: + raise ValueError( + "Sensitivity: for estimation with only one parameter" + + " you have to use a dummy paramter." + ) + if rep is None: + rep = spotpylib.fast_rep( + len(self.setup.para_names) + int(self.setup.dummy) + ) + + act_time = timemodule.strftime("%Y-%m-%d_%H-%M-%S") + # generate the filenames + if folder is None: + folder = os.path.join(os.getcwd(), self.name) + folder = os.path.abspath(folder) + if not os.path.exists(folder): + os.makedirs(folder) + + if dbname is None: + dbname = os.path.join(folder, act_time + "_sensitivity_db") + elif not os.path.isabs(dbname): + dbname = os.path.join(folder, dbname) + if plotname is None: + plotname = os.path.join(folder, act_time + "_sensitivity.pdf") + elif not os.path.isabs(plotname): + plotname = os.path.join(folder, plotname) + if traceplotname is None: + traceplotname = os.path.join(folder, act_time + "_senstrace.pdf") + elif not os.path.isabs(traceplotname): + traceplotname = os.path.join(folder, traceplotname) + if sensname is None: + sensname = os.path.join(folder, act_time + "_FAST_estimate.txt") + elif not os.path.isabs(sensname): + sensname = os.path.join(folder, sensname) + + sens_base, sens_ext = os.path.splitext(sensname) + sensname1 = sens_base + "_S1" + sens_ext + + # generate the parameter-names for plotting + paranames = dcopy(self.setup.para_names) + paralabels = [self.setup.val_plot_names[name] for name in paranames] + + if self.setup.dummy: + paranames.append("dummy") + paralabels.append("dummy") + + if parallel == "mpi": + # send the dbname of rank0 + from mpi4py import MPI + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + if rank == 0: + print(rank, "send dbname:", dbname) + for i in range(1, size): + comm.send(dbname, dest=i, tag=0) + else: + dbname = comm.recv(source=0, tag=0) + print(rank, "got dbname:", dbname) + else: + rank = 0 + + # initialize the sampler + sampler = spotpy.algorithms.fast( + self.setup, + dbname=dbname, + dbformat="csv", + parallel=parallel, + save_sim=True, + db_precision=np.float64, + ) + sampler.sample(rep) + + if rank == 0: + data = sampler.getdata() + parmin = sampler.parameter()["minbound"] + parmax = sampler.parameter()["maxbound"] + bounds = list(zip(parmin, parmax)) + sens_est = sampler.analyze( + bounds, np.nan_to_num(data["like1"]), len(paranames), paranames + ) + self.sens = {} + for sen_typ in sens_est: + self.sens[sen_typ] = { + par: sen for par, sen in zip(paranames, sens_est[sen_typ]) + } + header = " ".join(paranames) + np.savetxt(sensname, sens_est["ST"], header=header) + np.savetxt(sensname1, sens_est["S1"], header=header) + plotter.plotsensitivity(paralabels, sens_est, plotname) + plotter.plotparatrace( + data, + parameternames=paranames, + parameterlabels=paralabels, + stdvalues=None, + plotname=traceplotname, + ) diff --git a/welltestpy/process/__init__.py b/welltestpy/process/__init__.py index 37c9af6..846e0a7 100644 --- a/welltestpy/process/__init__.py +++ b/welltestpy/process/__init__.py @@ -14,9 +14,7 @@ combinepumptest filterdrawdown """ -from __future__ import absolute_import - -from welltestpy.process.processlib import ( +from .processlib import ( normpumptest, combinepumptest, filterdrawdown, diff --git a/welltestpy/process/processlib.py b/welltestpy/process/processlib.py index c841143..bf31c72 100644 --- a/welltestpy/process/processlib.py +++ b/welltestpy/process/processlib.py @@ -11,13 +11,11 @@ combinepumptest filterdrawdown """ -from __future__ import absolute_import, division, print_function - from copy import deepcopy as dcopy import numpy as np from scipy import signal -from welltestpy.data.testslib import PumpingTest +from ..data import testslib __all__ = ["normpumptest", "combinepumptest", "filterdrawdown"] @@ -32,15 +30,18 @@ def normpumptest(pumptest, pumpingrate=-1.0, factor=1.0): factor : :class:`float`, optional Scaling factor that can be used for unit conversion. Default: ``1.0`` """ - if not isinstance(pumptest, PumpingTest): + if not isinstance(pumptest, testslib.PumpingTest): raise ValueError(str(pumptest) + " is no pumping test") - oldprate = dcopy(pumptest.pumpingrate) + if not pumptest.constant_rate: + raise ValueError(str(pumptest) + " is no constant rate pumping test") + + oldprate = dcopy(pumptest.rate) pumptest.pumpingrate = pumpingrate for obs in pumptest.observations: pumptest.observations[obs].observation *= ( - factor * pumpingrate / oldprate + factor * pumptest.rate / oldprate ) @@ -143,27 +144,27 @@ def combinepumptest( if pumpingrate is None: if infooftest1: - pumpingrate = temptest1.pumpingrate + pumpingrate = temptest1.rate else: - pumpingrate = temptest2.pumpingrate + pumpingrate = temptest2.rate normpumptest(temptest1, pumpingrate, factor1) normpumptest(temptest2, pumpingrate, factor2) - prate = temptest1.pumpingrate + prate = temptest1.rate if infooftest1: if pwell in temptest1.observations and pwell in temptest2.observations: - temptest2.delobservations(pwell) - aquiferdepth = temptest1.aquiferdepth - aquiferradius = temptest1.aquiferradius + temptest2.del_observations(pwell) + aquiferdepth = temptest1.depth + aquiferradius = temptest1.radius description = temptest1.description timeframe = temptest1.timeframe else: if pwell in temptest1.observations and pwell in temptest2.observations: - temptest1.delobservations(pwell) - aquiferdepth = temptest2.aquiferdepth - aquiferradius = temptest2.aquiferradius + temptest1.del_observations(pwell) + aquiferdepth = temptest2.depth + aquiferradius = temptest2.radius description = temptest2.description timeframe = temptest2.timeframe @@ -171,17 +172,17 @@ def combinepumptest( observations.update(temptest2.observations) if infooftest1: - aquiferdepth = temptest1.aquiferdepth - aquiferradius = temptest1.aquiferradius + aquiferdepth = temptest1.depth + aquiferradius = temptest1.radius description = temptest1.description timeframe = temptest1.timeframe else: - aquiferdepth = temptest2.aquiferdepth - aquiferradius = temptest2.aquiferradius + aquiferdepth = temptest2.depth + aquiferradius = temptest2.radius description = temptest2.description timeframe = temptest2.timeframe - finalpt = PumpingTest( + finalpt = testslib.PumpingTest( finalname, pwell, prate, @@ -213,9 +214,9 @@ def filterdrawdown(observation, tout=None, dxscale=2): Scale of time-steps used for smoothing. Default: ``2`` """ - time, head = observation() - time = np.array(time, dtype=float).reshape(-1) + head, time = observation() head = np.array(head, dtype=float).reshape(-1) + time = np.array(time, dtype=float).reshape(-1) if tout is None: tout = dcopy(time) diff --git a/welltestpy/tools/__init__.py b/welltestpy/tools/__init__.py index 1252069..885e8d6 100644 --- a/welltestpy/tools/__init__.py +++ b/welltestpy/tools/__init__.py @@ -4,23 +4,18 @@ .. currentmodule:: welltestpy.tools -Subpackages -^^^^^^^^^^^ - -The following subpackages are provided - -.. autosummary:: - plotter - trilib - Included functions ^^^^^^^^^^^^^^^^^^ -The following classes and functions are provided +The following functions are provided for point triangulation .. autosummary:: triangulate sym + +The following plotting routines are provided + +.. autosummary:: campaign_plot fadeline plot_well_pos @@ -31,22 +26,11 @@ plotparatrace plotsensitivity """ -from __future__ import absolute_import - -try: - import StringIO - - BytIO = StringIO.StringIO -except ImportError: - import io - - BytIO = io.BytesIO - -from welltestpy.tools import plotter, trilib +from . import plotter, trilib -from welltestpy.tools.trilib import triangulate, sym +from .trilib import triangulate, sym -from welltestpy.tools.plotter import ( +from .plotter import ( campaign_plot, fadeline, plot_well_pos, @@ -70,7 +54,5 @@ "plotparainteract", "plotparatrace", "plotsensitivity", - "plotter", - "trilib", - "BytIO", ] +__all__ += ["plotter", "trilib"] diff --git a/welltestpy/tools/plotter.py b/welltestpy/tools/plotter.py index cfd2981..5d2665e 100644 --- a/welltestpy/tools/plotter.py +++ b/welltestpy/tools/plotter.py @@ -18,8 +18,8 @@ plotparatrace plotsensitivity """ -from __future__ import absolute_import, division, print_function - +# pylint: disable=C0103 +import copy import warnings import functools as ft @@ -38,7 +38,7 @@ def _get_fig_ax( sub_kwargs=None, **fig_kwargs ): # pragma: no cover - # 0->None or given, 1->False, 2->True + # ax_case: 0->None (create one) or given, 1->False, 2->True ax_case = 1 + int(ax) if isinstance(ax, bool) else 0 sub_args = (111,) if sub_args is None else sub_args sub_kwargs = {} if sub_kwargs is None else sub_kwargs @@ -52,7 +52,7 @@ def _get_fig_ax( assert ax.get_figure() is fig return fig, ax # if ax=False we only want a figure - elif ax_case == 1: + if ax_case == 1: return plt.figure(**fig_kwargs) if fig is None else fig # if ax=True we want the current axis of the given figure assert fig is not None @@ -68,7 +68,24 @@ def _sort_lgd(ax, **kwargs): def dashes(i=1, max_n=12, width=1): - """Dashes for matplotlib.""" + """ + Dashes for matplotlib. + + Parameters + ---------- + i : int, optional + Number of dots. The default is 1. + max_n : int, optional + Maximal Number of dots. The default is 12. + width : float, optional + Linewidth. The default is 1. + + Returns + ------- + list + dashes list for matplotlib. + + """ return i * [width, width] + [max_n * 2 * width - 2 * i * width, width] @@ -106,21 +123,33 @@ def fadeline(ax, x, y, label=None, color=None, steps=20, **kwargs): kwargs["solid_capstyle"] = "butt" for i in range(steps): - if i == 0: - label0 = label - else: - label0 = None - ax.plot( - [xarr[i], xarr[i + 1]], - [yarr[i], yarr[i + 1]], - label=label0, - alpha=(steps - i) * (1.0 / steps) * 0.9 + 0.1, - **kwargs - ) + kwargs["label"] = label if i == 0 else None + kwargs["alpha"] = (steps - i) * (1.0 / steps) * 0.9 + 0.1 + ax.plot([xarr[i], xarr[i + 1]], [yarr[i], yarr[i + 1]], **kwargs) -def campaign_plot(campaign, select_test=None, fig=None, **kwargs): - """Plot an overview of the tests within the campaign.""" +def campaign_plot(campaign, select_test=None, fig=None, style="WTP", **kwargs): + """ + Plot an overview of the tests within the campaign. + + Parameters + ---------- + campaign : :class:`Campaign` + The campaign to be plotted. + select_test : dict, optional + The selected tests to be added to the plot. The default is None. + fig : Figure, optional + Matplotlib figure to plot on. The default is None. + style : str, optional + Plot stlye. The default is "WTP". + **kwargs : TYPE + Keyword arguments forwarded to the tests plotting routines. + + Returns + ------- + fig : Figure + The created matplotlib figure. + """ if select_test is None: tests = list(campaign.tests.keys()) else: @@ -128,7 +157,15 @@ def campaign_plot(campaign, select_test=None, fig=None, **kwargs): tests.sort() nroftests = len(tests) - with plt.style.context("ggplot"): + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) fig = _get_fig_ax(fig, ax=False, dpi=75, figsize=[8, 3 * nroftests]) for n, t in enumerate(tests): @@ -142,30 +179,59 @@ def campaign_plot(campaign, select_test=None, fig=None, **kwargs): def campaign_well_plot( - campaign, plot_tests=True, plot_well_names=True, fig=None + campaign, plot_tests=True, plot_well_names=True, fig=None, style="WTP" ): - """Plot of the well constellation within the campaign.""" + """ + Plot of the well constellation within the campaign. + + Parameters + ---------- + campaign : :class:`Campaign` + The campaign to be plotted. + plot_tests : bool, optional + DESCRIPTION. The default is True. + plot_well_names : TYPE, optional + DESCRIPTION. The default is True. + fig : Figure, optional + Matplotlib figure to plot on. The default is None. + style : str, optional + Plot stlye. The default is "WTP". + + Returns + ------- + ax : Axes + The created matplotlib axes. + + """ well_const0 = [] names = [] for w in campaign.wells: well_const0.append( - [ - campaign.wells[w].coordinates[0], - campaign.wells[w].coordinates[1], - ] + [campaign.wells[w].pos[0], campaign.wells[w].pos[1]] ) names.append(w) well_const = [well_const0] - with plt.style.context("ggplot"): - fig = plot_well_pos( - well_const, - names, - campaign.name, - plot_well_names=plot_well_names, - fig=fig, - ) + fig = plot_well_pos( + well_const, + names, + plot_well_names=plot_well_names, + fig=fig, + style=style, + ) + + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) + clrs = plt.rcParams["axes.prop_cycle"].by_key()["color"] + clr_n = len(clrs) fig, ax = _get_fig_ax(fig, ax=True) @@ -175,17 +241,17 @@ def campaign_well_plot( for i, t in enumerate(testlist): p_well = campaign.tests[t].pumpingwell for j, obs in enumerate(campaign.tests[t].observations): - x0 = campaign.wells[p_well].coordinates[0] - y0 = campaign.wells[p_well].coordinates[1] - x1 = campaign.wells[obs].coordinates[0] - y1 = campaign.wells[obs].coordinates[1] + x0 = campaign.wells[p_well].pos[0] + y0 = campaign.wells[p_well].pos[1] + x1 = campaign.wells[obs].pos[0] + y1 = campaign.wells[obs].pos[1] label = "'{}'".format(t) if j == 0 else None fadeline( ax=ax, x=[x0, x1], y=[y0, y1], label=label, - color="C" + str((i + 2) % 10), + color=clrs[(i + 2) % clr_n], linewidth=3, zorder=10, ) @@ -198,7 +264,7 @@ def campaign_well_plot( def plot_pump_test( - pump_test, wells, exclude=None, fig=None, ax=None, **kwargs + pump_test, wells, exclude=None, fig=None, ax=None, style="WTP", **kwargs ): """Plot a pumping test. @@ -211,14 +277,33 @@ def plot_pump_test( exclude: :class:`list`, optional List of wells that should be excluded from the plot. Default: ``None`` + fig : Figure, optional + Matplotlib figure to plot on. The default is None. ax : :class:`Axes` - Axes where the plot should be done. + Matplotlib axes to plot on. The default is None. + style : str, optional + Plot stlye. The default is "WTP". + + Returns + ------- + ax : Axes + The created matplotlib axes. Notes ----- This will be used by the Campaign class. """ - with plt.style.context("ggplot"): + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) + clrs = plt.rcParams["axes.prop_cycle"].by_key()["color"] + clr_n = len(clrs) fig, ax = _get_fig_ax(fig, ax) exclude = set() if exclude is None else set(exclude) well_set = set(wells) @@ -241,7 +326,7 @@ def plot_pump_test( ax1 = None ax2 = ax else: - return + raise ValueError("plot_pump_test: unknow state of pumping test.") for i, k in enumerate(plot_wells): if k != pump_test.pumpingwell: dist = wells[k] - wells[pump_test.pumpingwell] @@ -249,14 +334,14 @@ def plot_pump_test( dist = wells[pump_test.pumpingwell].radius if pump_test.observations[k].state == "transient": if abslab: - displace = np.abs(pump_test.observations[k].value[1]) + displace = np.abs(pump_test.observations[k].value[0]) else: - displace = pump_test.observations[k].value[1] + displace = pump_test.observations[k].value[0] ax1.plot( - pump_test.observations[k].value[0], + pump_test.observations[k].value[1], displace, linewidth=2, - color="C{}".format(i % 10), + color=clrs[i % clr_n], label=( pump_test.observations[k].name + " r={:1.2f}".format(dist) @@ -280,7 +365,7 @@ def plot_pump_test( ax2.scatter( dist, displace, color=color, label=label, ) - ax2.set_xlabel("r in {}".format(wells[k]._coordinates.units)) + ax2.set_xlabel("r in {}".format(wells[k].coordinates.units)) ax2.set_ylabel( abslab + "{}".format(pump_test.observations[k].labels) ) @@ -304,8 +389,6 @@ def plot_pump_test( title="Pumping test '{}'".format(pump_test.name), loc="upper left", bbox_to_anchor=(1, 1), - fancybox=True, - framealpha=0.75, ) if state == "mixed": # add a second legend ax2.legend(loc="upper right", fancybox=True, framealpha=0.75) @@ -321,9 +404,37 @@ def plot_well_pos( title="", filename=None, plot_well_names=True, + ticks_set="auto", fig=None, + style="WTP", ): - """Plot all well constellations and label the points with the names.""" + """ + Plot all well constellations and label the points with the names. + + Parameters + ---------- + well_const : list + List of well constellations. + names : list of str, optional + Names for the wells. The default is None. + title : str, optional + Plot title. The default is "". + filename : str, optional + Filename if the result should be saved. The default is None. + plot_well_names : bool, optional + Whether to plot the well-names. The default is True. + ticks_set : int or str, optional + Tick spacing in the plot. The default is "auto". + fig : Figure, optional + Matplotlib figure to plot on. The default is None. + style : str, optional + Plot stlye. The default is "WTP". + + Returns + ------- + fig : Figure + The created matplotlib figure. + """ # calculate Column- and Row-count for quadratic shape of the plot # total number of plots total_n = len(well_const) @@ -357,7 +468,28 @@ def plot_well_pos( space = 0.1 * max(abs(xmax - xmin), abs(ymax - ymin)) xspace = yspace = space - with plt.style.context("ggplot"): + if ticks_set == "auto": + # bit hacky auto-ticking to be more pleasant for the eyes + tick_list = [1, 2, 5, 10] + tk_space = space * 10 / 7 # assume about 7 ticks + scaling = np.log10(tk_space) + if np.log10(0.4) < scaling < 1: + # if space is less 10, choose nearest value in tick_list (by log) + ticks_set = min(tick_list, key=lambda x: abs(np.log(x / tk_space))) + else: + # k * 10 ** n as ticks (0.1, 0.2, ..., 10, 20, ..., 100, 200, ...) + space_pot = 10 ** int(np.floor(scaling)) + ticks_set = space_pot * int(np.around(tk_space / space_pot)) + + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) fig = _get_fig_ax( fig, ax=False, dpi=100, figsize=[9 * col_n, 5 * row_n] ) @@ -374,15 +506,16 @@ def plot_well_pos( ax.annotate( " " + name, (wells[j][0], wells[j][1]), zorder=100 ) - ax.xaxis.set_major_locator(ticker.MultipleLocator(5)) - ax.yaxis.set_major_locator(ticker.MultipleLocator(5)) - ax.set_xlabel("x distance in $[m]$") # , fontsize=16) - ax.set_ylabel("y distance in $[m]$") # , fontsize=16) + ax.xaxis.set_major_locator(ticker.MultipleLocator(ticks_set)) + ax.yaxis.set_major_locator(ticker.MultipleLocator(ticks_set)) + ax.set_xlabel("x distance in $[m]$") + ax.set_ylabel("y distance in $[m]$") if total_n > 1: ax.set_title("Result {}".format(i)) + if title: + fig.suptitle(title) fig.tight_layout(rect=[0, 0, 1, 0.95]) - if filename is not None: fig.savefig(filename, format="pdf") @@ -406,17 +539,33 @@ def plotfit_transient( plotname=None, fig=None, ax=None, + style="WTP", ): """Plot of transient estimation fitting.""" - with plt.style.context("ggplot"): + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style1 = "ggplot" + style2 = "default" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + else: + style1 = style2 = style + with plt.style.context(style1): clrs = plt.rcParams["axes.prop_cycle"].by_key()["color"] - with plt.style.context("default"): + clr_n = len(clrs) + with plt.style.context(style2): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) fig, ax = _get_fig_ax(fig, ax, ax_name=Axes3D.name, figsize=(12, 8)) val_fix = setup.val_fix for kwarg in ["time", "rad"]: val_fix.pop(extra[kwarg], None) - para_kw = setup.get_sim_kwargs(para) + para_ordered = np.empty(len(setup.para_names)) + for i, name in enumerate(setup.para_names): + para_ordered[i] = para[name] + para_kw = setup.get_sim_kwargs(para_ordered) val_fix.update(para_kw) plot_f = ft.partial(setup.func, **val_fix) @@ -430,7 +579,7 @@ def plotfit_transient( xydir = np.zeros_like(time) test_name = list(np.unique(radnames[:, 0])) test_name.sort() - rad_unique, rad_un_idx = np.unique(rad, return_index=True) + __, rad_un_idx = np.unique(rad, return_index=True) for ri, re in enumerate(rad): r1 = re * r_gen r11 = re * r_gen1 @@ -439,22 +588,24 @@ def plotfit_transient( h2 = plot_f(**{extra["time"]: timarr, extra["rad"]: re}).reshape( -1 ) - color = clrs[(test_name.index(radnames[ri, 0]) + 2) % 10] + color = clrs[(test_name.index(radnames[ri, 0]) + 2) % clr_n] alpha = 0.3 * (1 - (re - min(rad)) / (max(rad) - min(rad))) + 0.3 - zord = 1000 * (len(rad) - ri) + zord = 100 * (len(rad) - ri) if radnames[ri, 0] == radnames[ri, 1]: - label = "test {}".format(radnames[ri, 0]) + label = "test at '{}'".format(radnames[ri, 0]) label_eff = "fitted type curve" + eff_zord = zord + 100 # first line should be on top else: label = None label_eff = None + eff_zord = 1 if ri in rad_un_idx: ax.plot( r11, timarr, h2, - zorder=zord - 1000 * max(rad), + zorder=eff_zord, color="k", alpha=alpha, label=label_eff, @@ -469,14 +620,14 @@ def plotfit_transient( alpha=0.6, arrow_length_ratio=0.0, color=color, - zorder=zord + 300, + zorder=zord + 30, ) ax.scatter( r1, time, h1, depthshade=False, - zorder=zord + 600, + zorder=zord + 60, color=color, label=label, ) @@ -486,12 +637,11 @@ def plotfit_transient( h = plot_f(**{extra["time"]: te, extra["rad"]: radarr}).reshape(-1) ax.plot(radarr, t11, h, color="k", alpha=0.1, linestyle="--") - # ax.view_init(elev=45, azim=155) ax.view_init(elev=40, azim=125) ax.set_xlabel(r"$r$ in $\left[\mathrm{m}\right]$") ax.set_ylabel(r"$t$ in $\left[\mathrm{s}\right]$") ax.set_zlabel(r"$\tilde{h}$ in $\left[\mathrm{m}\right]$") - _sort_lgd(ax, loc="lower left", markerscale=2) + _sort_lgd(ax, loc="upper right", markerscale=2) fig.tight_layout() if plotname is not None: fig.savefig(plotname, format="pdf") @@ -510,12 +660,16 @@ def plotfit_steady( ax_ins=True, fig=None, ax=None, + style="WTP", ): """Plot of steady estimation fitting.""" val_fix = setup.val_fix val_fix.pop(extra["rad"], None) - para_kw = setup.get_sim_kwargs(para) + para_ordered = np.empty(len(setup.para_names)) + for i, name in enumerate(setup.para_names): + para_ordered[i] = para[name] + para_kw = setup.get_sim_kwargs(para_ordered) val_fix.update(para_kw) plot_f = ft.partial(setup.func, **val_fix) @@ -523,10 +677,18 @@ def plotfit_steady( test_name = list(np.unique(radnames[:, 0])) test_name.sort() - rad_unique, rad_un_idx = np.unique(rad, return_index=True) - with plt.style.context("ggplot"): + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) clrs = plt.rcParams["axes.prop_cycle"].by_key()["color"] + clr_n = len(clrs) fig, ax = _get_fig_ax(fig, ax, figsize=(9, 6)) if ax_ins: axins = ax.inset_axes([0.4, 0.07, 0.57, 0.5]) @@ -539,12 +701,21 @@ def plotfit_steady( ) axins.set_xscale("log") axins.set_facecolor("w") + axins.text( + 0.975, + 0.025, + "log-radius plot", + ha="right", + va="bottom", + bbox=dict(boxstyle="round", ec="k", fc="w"), + transform=axins.transAxes, + ) for ri, re in enumerate(rad): h = plot_f(**{extra["rad"]: re}).reshape(-1) h1 = data[ri] - color = clrs[(test_name.index(radnames[ri, 0]) + 2) % 10] + color = clrs[(test_name.index(radnames[ri, 0]) + 2) % clr_n] if radnames[ri, 0] == radnames[ri, 1]: - label = "test {}".format(radnames[ri, 0]) + label = "test at '{}'".format(radnames[ri, 0]) else: label = None ax.plot([re, re], [h, h1], alpha=0.6, color=color, zorder=100) @@ -561,6 +732,7 @@ def plotfit_steady( alpha=0.6, color="k", zorder=200, + label="fitted type curve", ) ax.set_xlabel(r"$r$ in $\left[\mathrm{m}\right]$") ax.set_ylabel(r"$\tilde{h}$ in $\left[\mathrm{m}\right]$") @@ -572,11 +744,19 @@ def plotfit_steady( return ax -def plotparainteract(result, paranames, plotname=None, fig=None): +def plotparainteract(result, paranames, plotname=None, fig=None, style="WTP"): """Plot of parameter interaction.""" import pandas as pd - with plt.style.context("default"): + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "default" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) fig, ax = _get_fig_ax(fig, ax=None, figsize=(12, 12)) fields = [par for par in result.dtype.names if par.startswith("par")] parameterdistribtion = result[fields] @@ -607,25 +787,36 @@ def plotparatrace( stdvalues=None, plotname=None, fig=None, + style="WTP", ): """Plot of parameter trace.""" rep = len(result) rows = len(parameternames) - with plt.style.context("ggplot"): - + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) + clrs = plt.rcParams["axes.prop_cycle"].by_key()["color"] fig = _get_fig_ax(fig, ax=False, figsize=(15, 3 * rows)) for j in range(rows): ax = fig.add_subplot(rows, 1, 1 + j) data = result["par" + parameternames[j]] - ax.plot(data, "-", color="C0") + ax.plot(data, "-", color=clrs[0]) if stdvalues is not None: ax.plot( - [stdvalues[j]] * rep, + [stdvalues[parameternames[j]]] * rep, "--", - label="best value: {:04.2f}".format(stdvalues[j]), + label="best value: {:04.2f}".format( + stdvalues[parameternames[j]] + ), color="k", alpha=0.7, ) @@ -651,11 +842,18 @@ def plotparatrace( def plotsensitivity( - paralabels, sensitivities, plotname=None, fig=None, ax=None + paralabels, sensitivities, plotname=None, fig=None, ax=None, style="WTP" ): """Plot of sensitivity results.""" - with plt.style.context("ggplot"): - + style = copy.deepcopy(plt.rcParams) if style is None else style + keep_fs = False + if style == "WTP": + style = "ggplot" + font_size = plt.rcParams.get("font.size", 10.0) + keep_fs = True + with plt.style.context(style): + if keep_fs: + plt.rcParams.update({"font.size": font_size}) fig, ax = _get_fig_ax(fig, ax) w_props = {"linewidth": 1, "edgecolor": "w", "width": 0.5} wedges, __ = ax.pie( diff --git a/welltestpy/tools/trilib.py b/welltestpy/tools/trilib.py index a4cc491..e8fa080 100644 --- a/welltestpy/tools/trilib.py +++ b/welltestpy/tools/trilib.py @@ -11,15 +11,9 @@ sym """ # pylint: disable=C0103 -from __future__ import absolute_import, division, print_function - from copy import deepcopy as dcopy import numpy as np -import matplotlib.pyplot as plt - -# use the ggplot style like R -plt.style.use("ggplot") __all__ = ["triangulate", "sym"]