diff --git a/.zenodo.json b/.zenodo.json index d145906..f66f155 100755 --- a/.zenodo.json +++ b/.zenodo.json @@ -15,6 +15,10 @@ "orcid": "0000-0001-9060-4008", "affiliation": "Helmholtz Centre for Environmental Research - UFZ", "name": "Sebastian M\u00fcller" + }, + { + "affiliation": "Utrecht University - The Netherlands", + "name": "Jarno Herrmann" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d66d262..dbd4e03 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,18 @@ All notable changes to **welltestpy** will be documented in this file. ## [Unreleased] +### Enhancements +- added `cooper_jacob_correction` to `process` (thanks to Jarno Herrmann) +- added `diagnostic_plots` module (thanks to Jarno Herrmann) +- added `screensize`, `screen`, `aquifer` and `is_piezometer` attribute to `Well` class +- added version information to output files +- added `__repr__` to `Campaign` + ### Changes - modernized packaging workflow using `pyproject.toml` - removed `setup.py` (use `pip>21.1` for editable installs) - removed `dev` as extra install dependencies +- better exceptions in loading routines ### Bugfixes - loading steady pumping tests was not possible due to a bug diff --git a/LICENSE b/LICENSE index 34c1d90..3c7c4e8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Sebastian Mueller +Copyright (c) 2021 Sebastian Müller, Jarno Herrmann 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/docs/source/conf.py b/docs/source/conf.py index 4f8d62a..b81f80b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -104,8 +104,8 @@ def setup(app): # General information about the project. curr_year = datetime.datetime.now().year project = "welltestpy" -copyright = "2018 - {}, Sebastian Mueller".format(curr_year) -author = "Sebastian Mueller" +copyright = "2018 - {}, Sebastian Müller, Jarno Herrmann".format(curr_year) +author = "Sebastian Müller, Jarno Herrmann" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -210,7 +210,7 @@ def setup(app): master_doc, "welltestpy.tex", "welltestpy Documentation", - "Sebastian Mueller", + author, "manual", ) ] diff --git a/setup.cfg b/setup.cfg index d0ccf3c..147e6e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,10 +4,10 @@ description = welltestpy - package to handle well-based Field-campaigns. long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/GeoStat-Framework/welltestpy -author = Sebastian Mueller -author_email = sebastian.mueller@ufz.de -maintainer = Sebastian Mueller -maintainer_email = sebastian.mueller@ufz.de +author = Sebastian Müller, Jarno Herrmann +author_email = info@geostat-framework.org +maintainer = Sebastian Müller +maintainer_email = info@geostat-framework.org license = MIT license_file = LICENSE platforms = any @@ -50,6 +50,7 @@ install_requires = pandas>=0.23.2,<2 scipy>=1.1.0,<2 spotpy>=1.5.0,<2 + packaging>=20 python_requires = >=3.6 zip_safe = False diff --git a/welltestpy/data/campaignlib.py b/welltestpy/data/campaignlib.py index f831509..15a5087 100644 --- a/welltestpy/data/campaignlib.py +++ b/welltestpy/data/campaignlib.py @@ -471,6 +471,14 @@ def diagnostic_plot(self, pumping_test, observation_well, **kwargs): f"diagnostic_plot: test '{pumping_test}' could not be found!" ) + def __repr__(self): + """Representation.""" + return ( + f"Campaign '{self.name}' at '{self.fieldsite}' with " + f"{len(self.wells)} wells and " + f"{len(self.tests)} tests" + ) + def save(self, path="", name=None): """Save the campaign to file. diff --git a/welltestpy/data/data_io.py b/welltestpy/data/data_io.py index ccfc476..9e2f0ba 100755 --- a/welltestpy/data/data_io.py +++ b/welltestpy/data/data_io.py @@ -16,13 +16,26 @@ from io import TextIOWrapper as TxtIO, BytesIO as BytIO import numbers import numpy as np +from packaging.version import parse as version_parse from . import varlib, campaignlib, testslib +try: + from .._version import __version__ +except ImportError: # pragma: nocover + # package is not installed + __version__ = "0.0.0.dev0" + # TOOLS ### +class LoadError(Exception): + """Loading error for all reading routines.""" + + pass + + def _formstr(string): # remove spaces, tabs, linebreaks and other separators return "".join(str(string).split()) @@ -39,6 +52,12 @@ def _nextr(data): return tuple(filter(None, next(data))) +def _check_version(version): + """At least check major version.""" + if version.major > version_parse(__version__).major: + raise ValueError(f"Unknown version '{version.public}'") + + # SAVE ### @@ -76,13 +95,13 @@ def save_var(var, path="", name=None): writer = csv.writer( csvf, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n" ) + writer.writerow(["wtp-version", __version__]) writer.writerow(["Variable"]) writer.writerow(["name", var.name]) writer.writerow(["symbol", var.symbol]) writer.writerow(["units", var.units]) writer.writerow(["description", var.description]) if issubclass(np.asanyarray(var.value).dtype.type, numbers.Integral): - # if np.asanyarray(var.value).dtype == int: writer.writerow(["integer"]) else: writer.writerow(["float"]) @@ -129,11 +148,11 @@ def save_obs(obs, path="", name=None): # 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(["wtp-version", __version__]) writer.writerow(["Observation"]) writer.writerow(["name", obs.name]) writer.writerow(["state", obs.state]) @@ -152,7 +171,6 @@ def save_obs(obs, path="", name=None): # 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) @@ -185,18 +203,18 @@ def save_well(well, path="", name=None): # create a standard name if None is given if name is None: name = "Well_" + well.name - # ensure the name ends with '.csv' + # ensure the name ends with '.wel' 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(["wtp-version", __version__]) writer.writerow(["Well"]) writer.writerow(["name", well.name]) # define names for the variable-files @@ -204,6 +222,7 @@ def save_well(well, path="", name=None): coordname = name[:-4] + "_CooVar.var" welldname = name[:-4] + "_WedVar.var" aquifname = name[:-4] + "_AqdVar.var" + screename = name[:-4] + "_ScrVar.var" # save variable-files writer.writerow(["radius", radiuname]) well.wellradius.save(patht, radiuname) @@ -213,15 +232,17 @@ def save_well(well, path="", name=None): well.welldepth.save(patht, welldname) writer.writerow(["aquiferdepth", aquifname]) well.aquiferdepth.save(patht, aquifname) + writer.writerow(["screensize", screename]) + well.screensize.save(patht, screename) # 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) + zfile.write(os.path.join(patht, screename), screename) # delete the temporary directory shutil.rmtree(patht, ignore_errors=True) return file_path @@ -251,18 +272,18 @@ def save_campaign(campaign, path="", name=None): # create a standard name if None is given if name is None: name = "Cmp_" + campaign.name - # ensure the name ends with '.csv' + # ensure the name ends with '.cmp' 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(["wtp-version", __version__]) writer.writerow(["Campaign"]) writer.writerow(["name", campaign.name]) writer.writerow(["description", campaign.description]) @@ -331,18 +352,18 @@ def save_fieldsite(fieldsite, path="", name=None): # create a standard name if None is given if name is None: name = "Field_" + fieldsite.name - # ensure the name ends with '.csv' + # ensure the name ends with '.fds' 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(["wtp-version", __version__]) writer.writerow(["Fieldsite"]) writer.writerow(["name", fieldsite.name]) writer.writerow(["description", fieldsite.description]) @@ -389,18 +410,18 @@ def save_pumping_test(pump_test, path="", name=None): # create a standard name if None is given if name is None: name = "Test_" + pump_test.name - # ensure the name ends with '.csv' + # ensure the name ends with '.tst' 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(["wtp-version", __version__]) writer.writerow(["Testtype", "PumpingTest"]) writer.writerow(["name", pump_test.name]) writer.writerow(["description", pump_test.description]) @@ -444,6 +465,46 @@ def save_pumping_test(pump_test, path="", name=None): # LOAD ### +def _load_var_data(data): + # default version string + version_string = "1.0.0" + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Variable": + raise ValueError( + f"load_var: expected 'Variable' but got '{header[0]}'" + ) + 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 = int(next(data)[1]) + else: + value = float(next(data)[1]) + else: + shape = tuple(np.array(shapenfo[1:], dtype=int)) + vcnt = int(next(data)[1]) + vlist = [] + for __ in range(vcnt): + vlist.append(next(data)[0]) + if integer: + value = np.array(vlist, dtype=int).reshape(shape) + else: + value = np.array(vlist, dtype=float).reshape(shape) + + return varlib.Variable(name, value, symbol, units, description) + + def load_var(varfile): """Load a variable from file. @@ -454,64 +515,32 @@ def load_var(varfile): varfile : :class:`str` Path to the file """ + cleanup = False 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 = int(next(data)[1]) - else: - value = float(next(data)[1]) - else: - shape = tuple(np.array(shapenfo[1:], dtype=int)) - vcnt = int(next(data)[1]) - vlist = [] - for __ in range(vcnt): - vlist.append(next(data)[0]) - if integer: - value = np.array(vlist, dtype=int).reshape(shape) - else: - value = np.array(vlist, dtype=float).reshape(shape) - - var = varlib.Variable(name, value, symbol, units, description) - except Exception: + # read file + data_file = open(varfile, "r") + except TypeError: # if it is an instance of TextIOWrapper try: + # read stream 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 = int(next(data)[1]) - else: - value = float(next(data)[1]) - else: - shape = tuple(np.array(shapenfo[1:], dtype=int)) - vcnt = int(next(data)[1]) - vlist = [] - for __ in range(vcnt): - vlist.append(next(data)[0]) - if integer: - value = np.array(vlist, dtype=int).reshape(shape) - else: - value = np.array(vlist, dtype=float).reshape(shape) - - var = varlib.Variable(name, value, symbol, units, description) - except Exception: - raise Exception("loadVar: loading the variable was not possible") + except Exception as exc: + raise LoadError( + f"load_var: couldn't read file '{varfile}'" + ) from exc + else: + data = csv.reader(data_file) + cleanup = True + + try: + var = _load_var_data(data) + except Exception as exc: + raise LoadError( + f"load_var: couldn't load variable '{varfile}'" + ) from exc + + if cleanup: + data_file.close() + return var @@ -525,29 +554,42 @@ def load_obs(obsfile): obsfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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 + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Observation": + raise ValueError( + f"load_obs: expected 'Observation' but got '{header[0]}'" + ) name = next(data)[1] steady = next(data)[1] == "steady" description = next(data)[1] if not steady: timef = next(data)[1] obsf = next(data)[1] - + # read time if not steady + time = None if not steady: time = load_var(TxtIO(zfile.open(timef))) - else: - time = None - + # read observation obs = load_var(TxtIO(zfile.open(obsf))) - + # generate observation object observation = varlib.Observation(name, obs, time, description) - except Exception: - raise Exception("loadObs: loading the observation was not possible") + except Exception as exc: + raise LoadError( + f"load_obs: couldn't load observation '{obsfile}'" + ) from exc return observation @@ -561,26 +603,46 @@ def load_well(welfile): welfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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 + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Well": + raise ValueError( + f"load_well: expected 'Well' but got '{header[0]}'" + ) name = next(data)[1] + # radius radf = next(data)[1] - coordf = next(data)[1] - welldf = next(data)[1] - aquidf = next(data)[1] - rad = load_var(TxtIO(zfile.open(radf))) + # coordinates + coordf = next(data)[1] coord = load_var(TxtIO(zfile.open(coordf))) + # well depth + welldf = next(data)[1] welld = load_var(TxtIO(zfile.open(welldf))) + # aquifer depth + aquidf = next(data)[1] 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") + # read screensize implemented in v1.1 + screend = None + if version.release >= (1, 1): + screenf = next(data)[1] + screend = load_var(TxtIO(zfile.open(screenf))) + + well = varlib.Well(name, rad, coord, welld, aquid, screend) + except Exception as exc: + raise LoadError(f"load_well: couldn't load well '{welfile}'") from exc return well @@ -594,12 +656,24 @@ def load_campaign(cmpfile): cmpfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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 + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Campaign": + raise ValueError( + f"load_campaign: expected 'Campaign' but got '{header[0]}'" + ) name = next(data)[1] description = next(data)[1] timeframe = next(data)[1] @@ -623,10 +697,10 @@ def load_campaign(cmpfile): campaign = campaignlib.Campaign( name, fieldsite, wells, tests, timeframe, description ) - except Exception: - raise Exception( - "loadPumpingTest: loading the pumpingtest " + "was not possible" - ) + except Exception as exc: + raise LoadError( + f"load_campaign: couldn't load campaign '{cmpfile}'" + ) from exc return campaign @@ -640,12 +714,25 @@ def load_fieldsite(fdsfile): fdsfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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 + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Fieldsite": + raise ValueError( + "load_fieldsite: expected 'Fieldsite' " + f"but got '{header[0]}'" + ) name = next(data)[1] description = next(data)[1] coordinfo = next(data)[1] @@ -654,10 +741,10 @@ def load_fieldsite(fdsfile): 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" - ) + except Exception as exc: + raise LoadError( + f"load_fieldsite: couldn't load fieldsite '{fdsfile}'" + ) from exc return fieldsite @@ -671,20 +758,30 @@ def load_test(tstfile): tstfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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": + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[0] != "Testtype": + raise ValueError( + f"load_test: expected 'Testtype' but got '{header[0]}'" + ) + if header[1] == "PumpingTest": routine = _load_pumping_test else: - raise Exception - except Exception: - raise Exception("loadTest: loading the test " + "was not possible") - + raise ValueError(f"load_test: unknown test type '{header[1]}'") + except Exception as exc: + raise LoadError(f"load_test: couldn't load test '{tstfile}'") from exc return routine(tstfile) @@ -698,12 +795,24 @@ def _load_pumping_test(tstfile): tstfile : :class:`str` Path to the file """ + # default version string + version_string = "1.0.0" 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 + first_line = _nextr(data) + if first_line[0] == "wtp-version": + version_string = first_line[1] + header = _nextr(data) + else: + header = first_line + version = version_parse(version_string) + _check_version(version) + if header[1] != "PumpingTest": + raise ValueError( + f"load_test: expected 'PumpingTest' but got '{header[1]}'" + ) name = next(data)[1] description = next(data)[1] timeframe = next(data)[1] @@ -731,8 +840,8 @@ def _load_pumping_test(tstfile): description, timeframe, ) - except Exception: - raise Exception( - "loadPumpingTest: loading the pumpingtest " + "was not possible" - ) + except Exception as exc: + raise LoadError( + f"load_test: couldn't load pumpingtest '{tstfile}'" + ) from exc return pumpingtest diff --git a/welltestpy/data/varlib.py b/welltestpy/data/varlib.py index 25f5e74..112ee78 100644 --- a/welltestpy/data/varlib.py +++ b/welltestpy/data/varlib.py @@ -112,7 +112,7 @@ def scalar(self): @property def label(self): """:class:`str`: String containing: ``symbol in units``.""" - return self.symbol + " in " + self.units + return f"{self.symbol} in {self.units}" @property def value(self): @@ -188,7 +188,7 @@ def __init__( super().__init__("time", value, symbol, units, description) if np.ndim(self.value) > 1: raise ValueError( - "TimeVar: 'time' should have " + "at most one dimension" + "TimeVar: 'time' should have at most one dimension" ) @@ -263,7 +263,7 @@ def __init__( lon, symbol="[Lat,Lon]", units="[deg,deg]", - description="Coordinates given in " + "degree-North and degree-East", + description="Coordinates given in degree-North and degree-East", ): ilat = np.array(np.squeeze(lat), ndmin=1) ilon = np.array(np.squeeze(lon), ndmin=1) @@ -274,8 +274,8 @@ def __init__( or ilat.shape != ilon.shape ): raise ValueError( - "CoordinatesVar: 'lat' and 'lon' should have" - + "same quantity and should be given as lists" + "CoordinatesVar: 'lat' and 'lon' should have " + "same quantity and should be given as lists" ) value = np.array([ilat, ilon]).T @@ -376,9 +376,9 @@ def info(self): info += " -Kind: " + str(self.kind) + "\n" info += " -State: " + str(self.state) + "\n" if self.state == "transient": - info += " --- " + "\n" + info += " --- \n" info += self._time.info + "\n" - info += " --- " + "\n" + info += " --- \n" info += self._observation.info + "\n" return info @@ -445,7 +445,7 @@ def units(self): """[:class:`tuple` of] :class:`str`: units of the observation.""" if self.state == "steady": return self._observation.units - return self._time.units + "," + self._observation.units + return f"{self._time.units}, {self._observation.units}" def reshape(self): """Reshape obeservations to flat array.""" @@ -460,6 +460,8 @@ def _settime(self, time): self._time = dcopy(time) elif time is None: self._time = None + elif self._time is None: + self._time = TimeVar(time) else: self._time(time) @@ -477,9 +479,7 @@ def _checkshape(self): != np.shape(self.observation)[: len(np.shape(self.time))] ): raise ValueError( - "Observation: " - + "'observation' has a " - + "shape-missmatch with 'time'" + "Observation: 'observation' has a shape-missmatch with 'time'" ) def __iter__(self): @@ -547,9 +547,7 @@ def __init__(self, name, observation, description="Steady observation"): def _settime(self, time): """For steady observations, this raises a ``ValueError``.""" if time is not None: - raise ValueError( - "Observation: " + "'time' not allowed in steady-state" - ) + raise ValueError("Observation: 'time' not allowed in steady-state") class TimeSeries(Observation): @@ -629,9 +627,7 @@ def __init__( def _settime(self, time): """For steady observations, this raises a ``ValueError``.""" if time is not None: - raise ValueError( - "Observation: " + "'time' not allowed in steady-state" - ) + raise ValueError("Observation: 'time' not allowed in steady-state") class Well: @@ -650,9 +646,13 @@ class Well: coordinates : :class:`Variable` or :class:`numpy.ndarray` Value of the Variable. welldepth : :class:`Variable` or :class:`float`, optional - Depth of the well. Default: 1.0 + Depth of the well (in saturated zone). Default: 1.0 aquiferdepth : :class:`Variable` or :class:`float`, optional - Depth of the aquifer at the well. Default: ``"None"`` + Aquifer depth at the well (saturated zone). Defaults to welldepth. + Default: ``"None"`` + screensize : :class:`Variable` or :class:`float`, optional + Size of the screen at the well. Defaults to 0.0. + Default: ``"None"`` Notes ----- @@ -661,18 +661,26 @@ class Well: """ def __init__( - self, name, radius, coordinates, welldepth=1.0, aquiferdepth=None + self, + name, + radius, + coordinates, + welldepth=1.0, + aquiferdepth=None, + screensize=None, ): self._radius = None self._coordinates = None self._welldepth = None self._aquiferdepth = None + self._screensize = None self.name = data_io._formstr(name) self.wellradius = radius self.coordinates = coordinates self.welldepth = welldepth self.aquiferdepth = aquiferdepth + self.screensize = screensize @property def info(self): @@ -681,14 +689,15 @@ def info(self): Here you can display informations about the variable. """ info = "" - info += "----" + "\n" + info += "----\n" info += "Well-name: " + str(self.name) + "\n" - info += "--" + "\n" + info += "--\n" info += self._radius.info + "\n" info += self.coordinates.info + "\n" info += self._welldepth.info + "\n" info += self._aquiferdepth.info + "\n" - info += "----" + "\n" + info += self._screensize.info + "\n" + info += "----\n" return info @property @@ -698,7 +707,7 @@ def radius(self): @property def wellradius(self): - """:class:`float`: Radius variable of the well.""" + """:class:`Variable`: Radius variable of the well.""" return self._radius @wellradius.setter @@ -708,16 +717,16 @@ def wellradius(self, radius): elif self._radius is None: self._radius = Variable( "radius", - radius, + float(radius), "r", "m", - "Inner radius of well '" + str(self.name) + "'", + f"Inner radius of well '{self.name}'", ) else: self._radius(radius) if not self._radius.scalar: raise ValueError("Well: 'radius' needs to be scalar") - if self.radius <= 0.0: + if not self.radius > 0.0: raise ValueError("Well: 'radius' needs to be positiv") @property @@ -727,7 +736,7 @@ def pos(self): @property def coordinates(self): - """:class:`numpy.ndarray`: Coordinates variable of the well.""" + """:class:`Variable`: Coordinates variable of the well.""" return self._coordinates @coordinates.setter @@ -740,14 +749,14 @@ def coordinates(self, coordinates): coordinates, "XY", "m", - "coordinates of well '" + str(self.name) + "'", + f"coordinates of well '{self.name}'", ) else: self._coordinates(coordinates) 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" + "[x,y] values or one single distance value" ) @property @@ -757,7 +766,7 @@ def depth(self): @property def welldepth(self): - """:class:`float`: Depth variable of the well.""" + """:class:`Variable`: Depth variable of the well.""" return self._welldepth @welldepth.setter @@ -767,21 +776,26 @@ def welldepth(self, welldepth): elif self._welldepth is None: self._welldepth = Variable( "welldepth", - welldepth, + float(welldepth), "L_w", "m", - "depth of well '" + str(self.name) + "'", + f"depth of well '{self.name}'", ) else: self._welldepth(welldepth) if not self._welldepth.scalar: raise ValueError("Well: 'welldepth' needs to be scalar") - if self.depth <= 0.0: + if not self.depth > 0.0: raise ValueError("Well: 'welldepth' needs to be positiv") @property - def aquiferdepth(self): + def aquifer(self): """:class:`float`: Aquifer depth at the well.""" + return self._aquiferdepth.value + + @property + def aquiferdepth(self): + """:class:`Variable`: Aquifer depth at the well.""" return self._aquiferdepth @aquiferdepth.setter @@ -789,29 +803,54 @@ def aquiferdepth(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) + "'", - ) + self._aquiferdepth = Variable( + "aquiferdepth", + self.depth if aquiferdepth is None else float(aquiferdepth), + "L_a", + self.welldepth.units, + f"aquifer depth at well '{self.name}'", + ) else: self._aquiferdepth(aquiferdepth) if not self._aquiferdepth.scalar: raise ValueError("Well: 'aquiferdepth' needs to be scalar") - if self.aquiferdepth.value <= 0.0: + if not self.aquifer > 0.0: raise ValueError("Well: 'aquiferdepth' needs to be positiv") + @property + def is_piezometer(self): + """:class:`bool`: Whether the well is only a standpipe piezometer.""" + return np.isclose(self.screen, 0) + + @property + def screen(self): + """:class:`float`: Screen size at the well.""" + return self._screensize.value + + @property + def screensize(self): + """:class:`Variable`: Screen size at the well.""" + return self._screensize + + @screensize.setter + def screensize(self, screensize): + if isinstance(screensize, Variable): + self._screensize = dcopy(screensize) + elif self._screensize is None: + self._screensize = Variable( + "screensize", + 0.0 if screensize is None else float(screensize), + "L_s", + self.welldepth.units, + f"screen size at well '{self.name}'", + ) + else: + self._screensize(screensize) + if not self._screensize.scalar: + raise ValueError("Well: 'screensize' needs to be scalar") + if self.screen < 0.0: + raise ValueError("Well: 'screensize' needs to be non-negative") + def distance(self, well): """Calculate distance to the well.