Source code for welltestpy.data.varlib

"""welltestpy subpackage providing flow datastructures for variables."""
import numbers
from copy import deepcopy as dcopy

import numpy as np

from . import data_io

__all__ = [
    "Variable",
    "TimeVar",
    "HeadVar",
    "TemporalVar",
    "CoordinatesVar",
    "Observation",
    "StdyObs",
    "DrawdownObs",
    "StdyHeadObs",
    "TimeSeries",
    "Well",
]


[docs]class Variable: """Class for a variable. This is a class for a physical variable which is either a scalar or an array. It has a name, a value, a symbol, a unit and a descrition string. Parameters ---------- name : :class:`str` Name of the Variable. value : :class:`int` or :class:`float` or :class:`numpy.ndarray` Value of the Variable. symbole : :class:`str`, optional Name of the Variable. Default: ``"x"`` units : :class:`str`, optional Units of the Variable. Default: ``"-"`` description : :class:`str`, optional Description of the Variable. Default: ``"no description"`` """ def __init__( self, name, value, symbol="x", units="-", description="no description" ): self.name = data_io._formstr(name) self.__value = None self.value = value self.symbol = str(symbol) self.units = str(units) self.description = str(description)
[docs] def __call__(self, value=None): """Call a variable. Here you can set a new value or you can get the value of the variable. Parameters ---------- value : :class:`int` or :class:`float` or :class:`numpy.ndarray`, optional Value of the Variable. Default: ``None`` Returns ------- value : :class:`int` or :class:`float` or :class:`numpy.ndarray` Value of the Variable. """ if value is not None: self.value = value return self.value
@property def info(self): """:class:`str`: Info about the Variable.""" info = "" info += " Variable-name: " + str(self.name) + "\n" info += " -Value: " + str(self.value) + "\n" info += " -Symbol: " + str(self.symbol) + "\n" info += " -Units: " + str(self.units) + "\n" info += " -Description: " + str(self.description) + "\n" return info @property def scalar(self): """:class:`bool`: State if the variable is of scalar type.""" return np.isscalar(self.__value) @property def label(self): """:class:`str`: String containing: ``symbol in units``.""" return f"{self.symbol} in {self.units}" @property def value(self): """:class:`int` or :class:`float` or :class:`numpy.ndarray`: Value.""" return self.__value @value.setter def value(self, value): if issubclass(np.asanyarray(value).dtype.type, numbers.Real): if np.ndim(np.squeeze(value)) == 0: self.__value = float(np.squeeze(value)) else: self.__value = np.squeeze(np.array(value, dtype=float)) elif issubclass(np.asanyarray(value).dtype.type, numbers.Integral): if np.ndim(np.squeeze(value)) == 0: self.__value = int(np.squeeze(value)) else: self.__value = np.squeeze(np.array(value, dtype=int)) else: raise ValueError("Variable: 'value' is neither integer nor float") def __repr__(self): """Representation.""" return f"{self.name} {self.symbol}: {self.value} {self.units}" def __str__(self): """Representation.""" return f"{self.name} {self.label}"
[docs] def save(self, 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"``. """ return data_io.save_var(self, path, name)
[docs]class TimeVar(Variable): """Variable class special for time series. Parameters ---------- value : :class:`int` or :class:`float` or :class:`numpy.ndarray` Value of the Variable. symbole : :class:`str`, optional Name of the Variable. Default: ``"t"`` units : :class:`str`, optional Units of the Variable. Default: ``"s"`` description : :class:`str`, optional Description of the Variable. Default: ``"time given in seconds"`` Notes ----- Here the variable should be at most 1 dimensional and the name is fix set to ``"time"``. """ def __init__( self, value, symbol="t", units="s", description="time given in seconds" ): super().__init__("time", value, symbol, units, description) if np.ndim(self.value) > 1: raise ValueError( "TimeVar: 'time' should have at most one dimension" )
[docs]class HeadVar(Variable): """ Variable class special for groundwater head. Parameters ---------- value : :class:`int` or :class:`float` or :class:`numpy.ndarray` Value of the Variable. symbole : :class:`str`, optional Name of the Variable. Default: ``"h"`` units : :class:`str`, optional Units of the Variable. Default: ``"m"`` description : :class:`str`, optional Description of the Variable. Default: ``"head given in meters"`` Notes ----- Here the variable name is fix set to ``"head"``. """ def __init__( self, value, symbol="h", units="m", description="head given in meters" ): super().__init__("head", value, symbol, units, description)
[docs]class TemporalVar(Variable): """ Variable class for a temporal variable. Parameters ---------- value : :class:`int` or :class:`float` or :class:`numpy.ndarray`, optional Value of the Variable. Default: ``0.0`` """ def __init__(self, value=0.0): super().__init__("temporal", value, description="temporal variable")
[docs]class CoordinatesVar(Variable): """Variable class special for coordinates. Parameters ---------- lat : :class:`int` or :class:`float` or :class:`numpy.ndarray` Lateral values of the coordinates. lon : :class:`int` or :class:`float` or :class:`numpy.ndarray` Longitutional values of the coordinates. symbole : :class:`str`, optional Name of the Variable. Default: ``"[Lat,Lon]"`` units : :class:`str`, optional Units of the Variable. Default: ``"[deg,deg]"`` description : :class:`str`, optional Description of the Variable. Default: ``"Coordinates given in degree-North and degree-East"`` Notes ----- Here the variable name is fix set to ``"coordinates"``. ``lat`` and ``lon`` should have the same shape. """ def __init__( self, lat, lon, symbol="[Lat,Lon]", units="[deg,deg]", 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) if ( len(ilat.shape) != 1 or len(ilon.shape) != 1 or ilat.shape != ilon.shape ): raise ValueError( "CoordinatesVar: 'lat' and 'lon' should have " "same quantity and should be given as lists" ) value = np.array([ilat, ilon]).T super().__init__("coordinates", value, symbol, units, description)
[docs]class Observation: """ Class for a observation. This is a class for time-dependent observations. It has a name and a description. Parameters ---------- name : :class:`str` Name 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, observation, time=None, description="Observation" ): self.__it = None self.__itfinished = None self._time = None self._observation = None self.name = data_io._formstr(name) self.description = str(description) self._setobservation(observation) self._settime(time) self._checkshape()
[docs] 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 ---------- observation : scalar, :class:`numpy.ndarray`, :class:`Variable`, optional New Value for observation. Default: ``"None"`` time : scalar, :class:`numpy.ndarray`, :class:`Variable`, optional New Value for time. Default: ``"None"`` Returns ------- [:class:`tuple` of] :class:`int` or :class:`float` or :class:`numpy.ndarray` ``(time, observation)`` or ``observation``. """ if observation is not None: self._setobservation(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): """Representation.""" return f"Observation '{self.name}' {self.label}" def __str__(self): """Representation.""" return self.__repr__() @property def labels(self): """[:class:`tuple` of] :class:`str`: ``symbol in units``.""" if self.state == "transient": return self._time.label, self._observation.label return self._observation.label @property def label(self): """[:class:`tuple` of] :class:`str`: ``symbol in units``.""" return self.labels @property def info(self): """Get information about the observation. Here you can display information about the observation. """ info = "" info += "Observation-name: " + str(self.name) + "\n" info += " -Description: " + str(self.description) + "\n" info += " -Kind: " + str(self.kind) + "\n" info += " -State: " + str(self.state) + "\n" if self.state == "transient": info += " --- \n" info += self._time.info + "\n" info += " --- \n" info += self._observation.info + "\n" return info @property def value(self): """ Value of the Observation. [:class:`tuple` of] :class:`int` or :class:`float` or :class:`numpy.ndarray` """ if self.state == "transient": return self.observation, self.time return self.observation @property def state(self): """ :class:`str`: String containing state of the observation. Either ``"steady"`` or ``"transient"``. """ return "steady" if self._time is None else "transient" @property def kind(self): """:class:`str`: name of the observation variable.""" return self._observation.name @property def time(self): """ Time values of the observation. :class:`int` or :class:`float` or :class:`numpy.ndarray` """ 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): """ Observed values of the observation. :class:`int` or :class:`float` or :class:`numpy.ndarray` """ 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.""" if self.state == "steady": return self._observation.units return f"{self._time.units}, {self._observation.units}"
[docs] def reshape(self): """Reshape observations to flat array.""" if self.state == "transient": tmp = len(np.shape(self.time)) self._settime(np.reshape(self.time, -1)) shp = np.shape(self.time) + np.shape(self.observation)[tmp:] self._setobservation(np.reshape(self.observation, shp))
def _settime(self, time): if isinstance(time, Variable): self._time = dcopy(time) elif time is None: self._time = None elif self._time is None: self._time = TimeVar(time) 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" and ( np.shape(self.time) != np.shape(self.observation)[: len(np.shape(self.time))] ): raise ValueError( "Observation: 'observation' has a shape-mismatch with 'time'" ) def __iter__(self): """Iterate over Observations.""" if self.state == "transient": self.__it = np.nditer(self.time, flags=["multi_index"]) else: self.__itfinished = False return self def __next__(self): """Iterate through observations.""" if self.state == "transient": if self.__it.finished: raise StopIteration ret = ( self.__it[0].item(), self.observation[self.__it.multi_index], ) self.__it.iternext() else: if self.__itfinished: raise StopIteration ret = self.observation self.__itfinished = True return ret
[docs] def save(self, 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"``. """ return data_io.save_obs(self, path, name)
[docs]class StdyObs(Observation): """ Observation class special for steady observations. Parameters ---------- name : :class:`str` Name of the Variable. observation : :class:`Variable` Name of the Variable. Default: ``"x"`` description : :class:`str`, optional Description of the Variable. Default: ``"Steady observation"`` """ def __init__(self, name, observation, description="Steady observation"): super().__init__(name, observation, None, description) 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")
class TimeSeries(Observation): """ Time series observation. Parameters ---------- name : :class:`str` Name of the Variable. 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."`` """ 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().__init__(name, values, time, description)
[docs]class DrawdownObs(Observation): """ Observation class special for drawdown observations. Parameters ---------- name : :class:`str` Name of the Variable. 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, observation, time, description="Drawdown observation" ): if not isinstance(time, Variable): time = TimeVar(time) if not isinstance(observation, Variable): observation = HeadVar(observation) super().__init__(name, observation, time, description)
[docs]class StdyHeadObs(Observation): """ Observation class special for steady drawdown observations. Parameters ---------- name : :class:`str` Name of the Variable. observation : :class:`Variable` Observation. description : :class:`str`, optional Description of the Variable. Default: ``"Steady observation"`` """ def __init__( self, name, observation, description="Steady State Drawdown observation", ): if not isinstance(observation, Variable): observation = HeadVar(observation) super().__init__(name, observation, None, description) 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")
[docs]class Well: """Class for a pumping-/observation-well. This is a class for a well within a aquifer-testing campaign. It has a name, a radius, coordinates and a depth. Parameters ---------- name : :class:`str` Name of the Variable. radius : :class:`Variable` or :class:`float` Value of the Variable. coordinates : :class:`Variable` or :class:`numpy.ndarray` Value of the Variable. welldepth : :class:`Variable` or :class:`float`, optional Depth of the well (in saturated zone). Default: 1.0 aquiferdepth : :class:`Variable` or :class:`float`, optional 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 ----- You can calculate the distance between two wells ``w1`` and ``w2`` by simply calculating the difference ``w1 - w2``. """ def __init__( 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): """Get information about the variable. Here you can display information about the variable. """ info = "" info += "----\n" info += "Well-name: " + str(self.name) + "\n" info += "--\n" info += self._radius.info + "\n" info += self.coordinates.info + "\n" info += self._welldepth.info + "\n" info += self._aquiferdepth.info + "\n" info += self._screensize.info + "\n" info += "----\n" return info @property def radius(self): """:class:`float`: Radius of the well.""" return self._radius.value @property def wellradius(self): """:class:`Variable`: 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", float(radius), "r", "m", 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 not self.radius > 0.0: raise ValueError("Well: 'radius' needs to be positive") @property def pos(self): """:class:`numpy.ndarray`: Position of the well.""" return self._coordinates.value @property def coordinates(self): """:class:`Variable`: Coordinates variable of the well.""" return self._coordinates @coordinates.setter def coordinates(self, coordinates): if isinstance(coordinates, Variable): self._coordinates = dcopy(coordinates) elif self._coordinates is None: self._coordinates = Variable( "coordinates", coordinates, "XY", "m", 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" ) @property def depth(self): """:class:`float`: Depth of the well.""" return self._welldepth.value @property def welldepth(self): """:class:`Variable`: Depth variable of the well.""" return self._welldepth @welldepth.setter def welldepth(self, welldepth): if isinstance(welldepth, Variable): self._welldepth = dcopy(welldepth) elif self._welldepth is None: self._welldepth = Variable( "welldepth", float(welldepth), "L_w", "m", f"depth of well '{self.name}'", ) else: self._welldepth(welldepth) if not self._welldepth.scalar: raise ValueError("Well: 'welldepth' needs to be scalar") if not self.depth > 0.0: raise ValueError("Well: 'welldepth' needs to be positive") @property 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 def aquiferdepth(self, aquiferdepth): if isinstance(aquiferdepth, Variable): self._aquiferdepth = dcopy(aquiferdepth) elif self._aquiferdepth is None: 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 not self.aquifer > 0.0: raise ValueError("Well: 'aquiferdepth' needs to be positive") @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")
[docs] def distance(self, well): """Calculate distance to the well. Parameters ---------- well : :class:`Well` or :class:`tuple` of :class:`float` Coordinates to calculate the distance to or another well. """ if isinstance(well, Well): return np.linalg.norm(self.pos - well.pos) try: return np.linalg.norm(self.pos - well) except ValueError: raise ValueError( "Well: the distant-well needs to be an " "instance of Well-class " "or a tuple of x-y coordinates " "or a single distance value " "and of same coordinates-type." )
def __repr__(self): """Representation.""" return f"{self.name} r={self.radius} at {self._coordinates}" def __sub__(self, well): """Distance between wells.""" return self.distance(well) def __add__(self, well): """Distance between wells.""" return self.distance(well) def __and__(self, well): """Distance between wells.""" return self.distance(well) def __abs__(self): """Distance to origin.""" return np.linalg.norm(self.pos)
[docs] def save(self, 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"``. """ return data_io.save_well(self, path, name)