Intro to gwrefpy#

This notebook introduces the objects models and wells and how they are used in gwrefpy.

This notebook can be downloaded from the source code here.

Models and wells are important and they build the foundation of gwrefpy. Models are used to represent a site or area of interest, and wells are used to represent locations where groundwater data is collected. There is no limit to the number of models or wells that can be created in gwrefpy, nor is there a restriction on the order in which they are created. Models and wells can be created independently of one another, and they can be linked together later.

We will start by importing the necessary packages and then begin with creating a model. For this notebook we besides the gwrefpy also need the pandas package.

import gwrefpy as gr
import pandas as pd

Creating a Model#

A model is typically a representation of a site or area of interest. Models can be made up of any number of wells. In this case we create a model with the name ‘My First gwrefpy Model’

model = gr.Model(name="My First gwrefpy Model")

Creating Wells#

Models are made up of wells. A well is typically a location where groundwater data is collected. Wells can be created independently of models, and they can be linked to models later. Wells can either be an observation well or a reference well.

Reference wells#

Wells that are used to represent natural conditions.

Observation wells#

Wells that the user is interested in to see if they are influenced by anthropogenic activities.

In this notebook we create one observation wells and one reference wells to later include in our existing model.

Creating data timeseries#

To create our wells, we first need to convert our groundwater data to pandas timeseries for each well. In this example this is made by using CSV-files.

Tip

Data types can be other than CSV. Check the pandas documentation for inspiration.

df_obs1 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)
df_ref1 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)
df_obs2 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)
df_ref2 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)

#Create pandas series from the dataframes.

obs1_series = df_obs1.iloc[:, 0] 
ref1_series = df_ref1.iloc[:, 0]
obs2_series = df_obs2.iloc[:, 0]
ref2_series = df_ref2.iloc[:, 0]
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 df_obs1 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)
      2 df_ref1 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)
      3 df_obs2 = pd.read_csv(r'https://github.com/andersretznerSGU/gwrefpy/raw/dev/docs/user_guide/data/obs1_mod_wel.csv',sep=';', index_col=0, parse_dates=True)

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1026, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options, dtype_backend)
   1013 kwds_defaults = _refine_defaults_read(
   1014     dialect,
   1015     delimiter,
   (...)   1022     dtype_backend=dtype_backend,
   1023 )
   1024 kwds.update(kwds_defaults)
-> 1026 return _read(filepath_or_buffer, kwds)

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/parsers/readers.py:620, in _read(filepath_or_buffer, kwds)
    617 _validate_names(kwds.get("names", None))
    619 # Create the parser.
--> 620 parser = TextFileReader(filepath_or_buffer, **kwds)
    622 if chunksize or iterator:
    623     return parser

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1620, in TextFileReader.__init__(self, f, engine, **kwds)
   1617     self.options["has_index_names"] = kwds["has_index_names"]
   1619 self.handles: IOHandles | None = None
-> 1620 self._engine = self._make_engine(f, self.engine)

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1880, in TextFileReader._make_engine(self, f, engine)
   1878     if "b" not in mode:
   1879         mode += "b"
-> 1880 self.handles = get_handle(
   1881     f,
   1882     mode,
   1883     encoding=self.options.get("encoding", None),
   1884     compression=self.options.get("compression", None),
   1885     memory_map=self.options.get("memory_map", False),
   1886     is_text=is_text,
   1887     errors=self.options.get("encoding_errors", "strict"),
   1888     storage_options=self.options.get("storage_options", None),
   1889 )
   1890 assert self.handles is not None
   1891 f = self.handles.handle

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/common.py:728, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    725     codecs.lookup_error(errors)
    727 # open URLs
--> 728 ioargs = _get_filepath_or_buffer(
    729     path_or_buf,
    730     encoding=encoding,
    731     compression=compression,
    732     mode=mode,
    733     storage_options=storage_options,
    734 )
    736 handle = ioargs.filepath_or_buffer
    737 handles: list[BaseBuffer]

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/common.py:384, in _get_filepath_or_buffer(filepath_or_buffer, encoding, compression, mode, storage_options)
    382 # assuming storage_options is to be interpreted as headers
    383 req_info = urllib.request.Request(filepath_or_buffer, headers=storage_options)
--> 384 with urlopen(req_info) as req:
    385     content_encoding = req.headers.get("Content-Encoding", None)
    386     if content_encoding == "gzip":
    387         # Override compression based on Content-Encoding header

File ~/work/gwrefpy/gwrefpy/.venv/lib/python3.11/site-packages/pandas/io/common.py:289, in urlopen(*args, **kwargs)
    283 """
    284 Lazy-import wrapper for stdlib urlopen, as that imports a big chunk of
    285 the stdlib.
    286 """
    287 import urllib.request
--> 289 return urllib.request.urlopen(*args, **kwargs)

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:216, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    214 else:
    215     opener = _opener
--> 216 return opener.open(url, data, timeout)

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:525, in OpenerDirector.open(self, fullurl, data, timeout)
    523 for processor in self.process_response.get(protocol, []):
    524     meth = getattr(processor, meth_name)
--> 525     response = meth(req, response)
    527 return response

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:634, in HTTPErrorProcessor.http_response(self, request, response)
    631 # According to RFC 2616, "2xx" code indicates that the client's
    632 # request was successfully received, understood, and accepted.
    633 if not (200 <= code < 300):
--> 634     response = self.parent.error(
    635         'http', request, response, code, msg, hdrs)
    637 return response

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:557, in OpenerDirector.error(self, proto, *args)
    555     http_err = 0
    556 args = (dict, proto, meth_name) + args
--> 557 result = self._call_chain(*args)
    558 if result:
    559     return result

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:496, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    494 for handler in handlers:
    495     func = getattr(handler, meth_name)
--> 496     result = func(*args)
    497     if result is not None:
    498         return result

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:749, in HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
    746 fp.read()
    747 fp.close()
--> 749 return self.parent.open(new, timeout=req.timeout)

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:525, in OpenerDirector.open(self, fullurl, data, timeout)
    523 for processor in self.process_response.get(protocol, []):
    524     meth = getattr(processor, meth_name)
--> 525     response = meth(req, response)
    527 return response

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:634, in HTTPErrorProcessor.http_response(self, request, response)
    631 # According to RFC 2616, "2xx" code indicates that the client's
    632 # request was successfully received, understood, and accepted.
    633 if not (200 <= code < 300):
--> 634     response = self.parent.error(
    635         'http', request, response, code, msg, hdrs)
    637 return response

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:563, in OpenerDirector.error(self, proto, *args)
    561 if http_err:
    562     args = (dict, 'default', 'http_error_default') + orig_args
--> 563     return self._call_chain(*args)

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:496, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    494 for handler in handlers:
    495     func = getattr(handler, meth_name)
--> 496     result = func(*args)
    497     if result is not None:
    498         return result

File ~/.local/share/uv/python/cpython-3.11.14-linux-x86_64-gnu/lib/python3.11/urllib/request.py:643, in HTTPDefaultErrorHandler.http_error_default(self, req, fp, code, msg, hdrs)
    642 def http_error_default(self, req, fp, code, msg, hdrs):
--> 643     raise HTTPError(req.full_url, code, msg, hdrs, fp)

HTTPError: HTTP Error 404: Not Found

Next we create well objects and add our timeseries to them.

The well object

The well object takes three input arguments. A name as a string. A boolean to decide if it´s a reference or observation well. And the last argument is the pandas timeseries we created earlier.

Important

Wells must have unique names. However the model will check that aswell and you´ll receive an error if that’s not the case.

obs1= gr.Well(name="obs1", is_reference=False, timeseries=obs1_series)
ref1= gr.Well(name="ref1", is_reference=True, timeseries=ref1_series)
obs2= gr.Well(name="obs2", is_reference=False, timeseries=obs2_series)
ref2= gr.Well(name="ref2", is_reference=True, timeseries=ref2_series)

Now we move on to adding our newly created wells to our model.

Add wells to the model#

Next we add the wells to the model by using the add_well function. A model can have any number of wells. This can either be done through by entering a list as an input argument or by entering a single well.

#Adding wells from a list
model.add_well([obs1,ref1,obs2])

#Adding single well
model.add_well(ref2)

After adding the wells to the model, we can now doublecheck to see that they´re actually included in the model we’ve created.

Check which wells are included in the model#

Now we check that our wells are stored in the model by using the function get_wells to see which wells are reference wells and which are observation wells.

model.obs_wells
[Well(name=obs1), Well(name=obs2)]

Check reference wells

model.ref_wells
[Well(name=ref1), Well(name=ref2)]

We can also check if a specific well is in the model

model.get_wells('obs1')
model.get_wells(['obs1','ref1'])
[Well(name=obs1), Well(name=ref1)]

We can also check the names of the wells that are included in our model.

model.well_names
['obs1', 'ref1', 'obs2', 'ref2']

Now we have checked that the model contains two observation wells and two reference wells, which later can be used for further analysis.

We can also get a summary of the wells in the model.

model.wells_summary()
name well_type data_points start_date end_date mean_level latest_value latest_date latitude longitude elevation best_fit_ref_well best_rmse num_fits avg_rmse
0 obs1 observation 2078 2020-01-01 2025-09-08 15.467492 14.877 2025-09-08 None None None None None NaN NaN
1 obs2 observation 2078 2020-01-01 2025-09-08 15.467492 14.877 2025-09-08 None None None None None NaN NaN
2 ref1 reference 2078 2020-01-01 2025-09-08 15.467492 14.877 2025-09-08 None None None NaN NaN 0.0 None
3 ref2 reference 2078 2020-01-01 2025-09-08 15.467492 14.877 2025-09-08 None None None NaN NaN 0.0 None

Delete wells from the model#

We can also remove wells from the model by using the function delete_well. This function takes a single input argument, the name of the well.

model.delete_well([ref1])

# and check that it is deleted
model.well_names
['obs1', 'obs2', 'ref2']

This is all for the intro to gwrefpy notebook.

In future notebooks we will explore using a model to a explore the other parts of gwrefpy such as pairing of observation- and reference wells and fitting and then plotting.