Logging 📄#

This notebook introduces the concept of logging using Python’s built-in logging module. Logging is essential for tracking events that happen when some software runs. The module provides a way to configure different log handlers and severity levels.

This notebook can be downloaded from the source code here.

The logging levels available in gwrefpy are:

  • DEBUG: Detailed information, typically of interest only when diagnosing problems.

  • INFO: Confirmation that things are working as expected. This is the default logging level.

  • WARNING: An indication that something unexpected happened, or indicative of some problem in the near future

  • ERROR: Due to a more serious problem, the software has not been able to perform some function. Typically, these are issues will also raise exceptions.

Let’s start by importing the necessary libraries and setting the logging level to DEBUG using the set_logging_level function.

import gwrefpy as gr
import numpy as np
import pandas as pd

gr.set_log_level("DEBUG")  # Set logging level to DEBUG
Log level set to DEBUG

We can now create some well objects and a model to see the logging in action.

Hide code cell source

# Create some example data
n_days = 100
dates = pd.date_range("2020-01-01", periods=n_days, freq="D")

# Observed and reference values with some noise
values_obs1 = (
    25.75
    + 0.7 * np.sin(np.linspace(0, 4 * np.pi, n_days))
    + np.random.normal(0, 0.1, n_days)
)
values_obs1 = pd.Series(values_obs1, index=dates)
values_ref1 = (
    18.75
    + 0.3 * np.sin(np.linspace(0, 4 * np.pi, n_days))
    + np.random.normal(0, 0.05, n_days)
)
values_ref1 = pd.Series(values_ref1, index=dates)
# Creat the observed and reference wells
obs1 = gr.Well(name="Obs. well", is_reference=False)
obs1.add_timeseries(values_obs1) # <-- Here we will see logging messages
ref1 = gr.Well(name="Ref. well", is_reference=True)
ref1.add_timeseries(values_ref1) # <-- Here we will see logging messages

# Create the model and add the wells
model1 = gr.Model(name="Logging Example Model")
model1.add_well(obs1) # <-- Here we will see logging messages
model1.add_well(ref1) # <-- Here we will see logging messages
Added timeseries to well Obs. well
Added timeseries to well Ref. well
Well 'Obs. well' added to model 'Logging Example Model'.
Well 'Ref. well' added to model 'Logging Example Model'.

As you can see, we got logging messages in the console when adding the wells to the model. Now let’s fit the model to see more logging messages.

# Fit the model
model1.fit(
    obs1,
    ref1,
    offset="0D",
    tmin=dates[0],
    tmax=dates[-21],
)
Using linear regression method for fitting.
Fit completed for model 'Logging Example Model' with RMSE 0.14269974740731517.
Fitting model 'Logging Example Model' using reference well 'Ref. well' and observation well 'Obs. well'.
Fit Results: Obs. well ~ Ref. well
==================================
Statistic       Value        Description
--------------------------------------------------
RMSE            0.1427       Root Mean Square Error
R²              0.9043       Coefficient of Determination
R-value         0.9510       Correlation Coefficient
Slope           1.9834       Linear Regression Slope
Intercept       -11.3871     Linear Regression Intercept
P-value         0.0000       Statistical Significance
N               80           Number of Data Points
Std Error       0.1445       Standard Error
Confidence      95.0        % Confidence Level

Calibration Period: 2020-01-01 00:00:00 to 2020-03-20 00:00:00
Time Offset: 0D
Aggregation Method: mean
Fit Results: Obs. well ~ Ref. well
Statistic Value Description
RMSE 0.1427 Root Mean Square Error
R² 0.9043 Coefficient of Determination
R-value 0.9510 Correlation Coefficient
Slope 1.9834 Linear Regression Slope
Intercept -11.3871 Linear Regression Intercept
P-value 0.0000 Statistical Significance
N 80 Number of Data Points
Std Error 0.1445 Standard Error
Confidence 95.0% Confidence Level

Calibration Period: 2020-01-01 00:00:00 to 2020-03-20 00:00:00
Time Offset: 0D
Aggregation Method: mean

As you can see both the progress and the results of the fitting are logged to the console.

Usually, we do not want that much information printed to the console. For many practivale applications, the INFO logging level is sufficient. Let’s change the logging level and see how it affects the output. We will also enable logging to a file with the enable_file_logging function, here we can set the logging level to DEBUG to capture all messages in the file.

Tip

If you want to log your own messages in your code, you can use the get_logger function to get a logger object and then use its methods to log messages at different levels (e.g., logger.debug(), logger.info(), etc.).

In the example below, we will use the logging module directly to demonstrate this. The info and debug messages will be logged according to the logging level we set.

import logging
logger = logging.getLogger(__name__)

# Set logging level to INFO (default)
gr.set_log_level("INFO")
# Enable logging to a file with DEBUG level
gr.enable_file_logging("gwrefpy_debug.log", loglevel="DEBUG")
Log level set to INFO

Let’s create a new model and fit it again to see the detailed logging in the file.

Hide code cell source

# Observed and reference values with some noise
values_obs2 = (
    25.75
    + 0.7 * np.sin(np.linspace(0, 4 * np.pi, n_days))
    + np.random.normal(0, 0.1, n_days)
)
values_obs2 = pd.Series(values_obs2, index=dates)
values_ref2 = (
    18.75
    + 0.3 * np.sin(np.linspace(0, 4 * np.pi, n_days))
    + np.random.normal(0, 0.05, n_days)
)
values_ref2 = pd.Series(values_ref2, index=dates)
# Creat the observed and reference wells
logger.info("Creating wells with timeseries data")
obs2 = gr.Well(name="Obs. well 1", is_reference=False)
obs2.add_timeseries(values_obs2)
ref2 = gr.Well(name="Ref. well 1", is_reference=True)
ref2.add_timeseries(values_ref2)
logger.debug("Timeseries data added to wells") # <-- This will be logged to the file only 

# Create the model and add the wells
logger.info("Creating a new model")
model2 = gr.Model(name="Logging Example Model DEBUG")
model2.add_well(obs2)
model2.add_well(ref2)
logger.debug("The wells have been added to the model") # <-- This will be logged to the file only 
Creating wells with timeseries data
Creating a new model

Now, when we add the wells to the model, we will not see any logging messages in the console, but they will be logged to the file.

We can now fit the model again.

# Fit the model
logger.info("Running the fit method on the model")
model2.fit(
    obs2,
    ref2,
    offset="0D",
    tmin=dates[0],
    tmax=dates[-21],
)
Running the fit method on the model
Fitting model 'Logging Example Model DEBUG' using reference well 'Ref. well 1' and observation well 'Obs. well 1'.
Fit Results: Obs. well 1 ~ Ref. well 1
Statistic Value Description
RMSE 0.1670 Root Mean Square Error
R² 0.8712 Coefficient of Determination
R-value 0.9334 Correlation Coefficient
Slope 2.1444 Linear Regression Slope
Intercept -14.4441 Linear Regression Intercept
P-value 0.0000 Statistical Significance
N 80 Number of Data Points
Std Error 0.1692 Standard Error
Confidence 95.0% Confidence Level

Calibration Period: 2020-01-01 00:00:00 to 2020-03-20 00:00:00
Time Offset: 0D
Aggregation Method: mean

As you can see, not as much will be printed to the console. But if we instead open gwrefpy_debug.log file we can see all the detailed logging information.

# Print the log file content
with open("gwrefpy_debug.log", "r") as log_file:
    log_content = log_file.read()
print(log_content)
2025-09-26 12:03:05,620 [INFO] __main__: Creating wells with timeseries data
2025-09-26 12:03:05,620 [DEBUG] gwrefpy.well: Added timeseries to well Obs. well 1
2025-09-26 12:03:05,621 [DEBUG] gwrefpy.well: Added timeseries to well Ref. well 1
2025-09-26 12:03:05,621 [DEBUG] __main__: Timeseries data added to wells
2025-09-26 12:03:05,621 [INFO] __main__: Creating a new model
2025-09-26 12:03:05,621 [DEBUG] gwrefpy.model: Well 'Obs. well 1' added to model 'Logging Example Model DEBUG'.
2025-09-26 12:03:05,621 [DEBUG] gwrefpy.model: Well 'Ref. well 1' added to model 'Logging Example Model DEBUG'.
2025-09-26 12:03:05,621 [DEBUG] __main__: The wells have been added to the model
2025-09-26 12:03:05,625 [INFO] __main__: Running the fit method on the model
2025-09-26 12:03:05,626 [DEBUG] gwrefpy.fitbase: Using linear regression method for fitting.
2025-09-26 12:03:05,630 [DEBUG] gwrefpy.fitbase: Fit completed for model 'Logging Example Model DEBUG' with RMSE 0.1670235627371146.
2025-09-26 12:03:05,630 [INFO] gwrefpy.fitbase: Fitting model 'Logging Example Model DEBUG' using reference well 'Ref. well 1' and observation well 'Obs. well 1'.
2025-09-26 12:03:05,631 [DEBUG] gwrefpy.model: Fit Results: Obs. well 1 ~ Ref. well 1
======================================
Statistic       Value        Description
--------------------------------------------------
RMSE            0.1670       Root Mean Square Error
R²              0.8712       Coefficient of Determination
R-value         0.9334       Correlation Coefficient
Slope           2.1444       Linear Regression Slope
Intercept       -14.4441     Linear Regression Intercept
P-value         0.0000       Statistical Significance
N               80           Number of Data Points
Std Error       0.1692       Standard Error
Confidence      95.0        % Confidence Level

Calibration Period: 2020-01-01 00:00:00 to 2020-03-20 00:00:00
Time Offset: 0D
Aggregation Method: mean

As you can see, the log file contains detailed information about the program’s execution, module, time, and log level, including messages that are not necessarily printed to the console.

This concludes this notebook on logging in gwrefpy. For more information on logging, please refer to the logging documentation and the API for gwrefpy’s logging functions here.