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)
-
+
## 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()
-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 @@
Documentation
{{ super() }}
- WellTestPy Links
- WellTestPy GitHub
- WellTestPy Zenodo DOI
- WellTestPy PyPI
+ welltestpy Links
+ welltestpy GitHub
+ welltestpy Zenodo DOI
+ welltestpy PyPI
GeoStat Framework
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"]