Coverage for src/gwtransport/examples.py: 98%

115 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 21:13 +0000

1""" 

2Example Data Generation for Groundwater Transport Modeling. 

3 

4This module provides utilities to generate synthetic datasets for demonstrating 

5and testing groundwater transport models. It creates realistic flow patterns, 

6concentration/temperature time series, and deposition events suitable for testing 

7advection, diffusion, and deposition analysis functions. 

8 

9Available functions: 

10 

11- :func:`generate_example_data` - Build a daily ``(DataFrame, tedges)`` pair with columns 

12 ``flow`` (seasonal sinusoid plus noise and randomly placed low-flow spill periods, floored at 

13 5 m³/day), ``cin`` (a seasonal sinusoid, a constant, or inline KNMI De Bilt soil temperature, 

14 selected with ``cin_method``), and ``cout``, obtained by transporting the noiseless ``cin`` 

15 through either a gamma-distributed or an explicitly listed set of aquifer pore volumes, using 

16 :mod:`gwtransport.advection` or, when the three diffusion parameters are given, 

17 :mod:`gwtransport.diffusion_fast`. Independent Gaussian measurement noise is added to ``cin`` 

18 and ``cout``, so the pair is not exactly consistent when ``measurement_noise > 0``. The aquifer 

19 and generation settings are recorded in ``df.attrs``. Consumed by 

20 ``examples/04_Deposition_Analysis_Bank_Filtration.ipynb``. 

21 

22- :func:`generate_temperature_example_data` - Same dataset viewed as heat transport: a thin 

23 wrapper over :func:`generate_example_data` whose defaults are thermal (retardation factor 2.0, 

24 diffusivity 0.05 m²/day, longitudinal dispersivity 1.0 m, streamline length 100 m), so the 

25 diffusion path is taken. Consumed by 

26 ``examples/01_Aquifer_Characterization_Temperature.ipynb``, 

27 ``examples/02_Residence_Time_Analysis.ipynb``, 

28 ``examples/03_Pathogen_Removal_Bank_Filtration.ipynb``, and 

29 ``examples/08_bank_filtration_timflow.ipynb``. 

30 

31- :func:`generate_example_deposition_timeseries` - Build a ``(Series, tedges)`` pair of surface 

32 deposition rates (ng/m²/day) on a UTC index: a baseline plus an annual sinusoid, Gaussian noise, 

33 and episodic events that start at the given dates and decay exponentially over 

34 ``event_duration`` days, optionally clipped at zero. Feeds 

35 :func:`gwtransport.deposition.deposition_to_extraction` in 

36 ``examples/04_Deposition_Analysis_Bank_Filtration.ipynb``. 

37 

38This file is part of gwtransport which is released under AGPL-3.0 license. 

39See the ./LICENSE file or go to https://github.com/gwtransport/gwtransport/blob/main/LICENSE for full license details. 

40""" 

41 

42import numpy as np 

43import numpy.typing as npt 

44import pandas as pd 

45 

46from gwtransport.advection import gamma_infiltration_to_extraction, infiltration_to_extraction 

47from gwtransport.diffusion_fast import gamma_infiltration_to_extraction as diffusion_gamma_infiltration_to_extraction 

48from gwtransport.diffusion_fast import infiltration_to_extraction as diffusion_infiltration_to_extraction 

49from gwtransport.gamma import mean_std_loc_to_alpha_beta 

50from gwtransport.utils import compute_time_edges 

51 

52_DEFAULT_GAMMA_MEAN = 1000.0 # m³ 

53_DEFAULT_GAMMA_STD = 200.0 # m³ 

54_DEFAULT_GAMMA_LOC = 0.0 # m³, minimum pore volume 

55_DEFAULT_GAMMA_NBINS = 250 

56 

57# KNMI station 260 (De Bilt) daily-mean soil temperature at 20 cm depth (column TB3, degrees 

58# Celsius), UTC, from 2020-01-01 onward. Stored inline -- rather than downloaded from KNMI via 

59# gwtransport.utils.get_soil_temperature -- so the cin_method="soil_temperature" examples run 

60# without network access (e.g. in the browser under Pyodide/JupyterLite). Rounded to 0.01 degrees. 

61_SOIL_TEMPERATURE_TB3_START = "2020-01-01" 

62_SOIL_TEMPERATURE_TB3 = (5.65, 5.62, 6.2, 6.43, 6.68, 6.8, 6.55, 7.07, 7.95, 8.4, 7.38, 7.07, 7.33, 7.65, 8.25, 7.7, 7.47, 7.28, 6.43, 5.8, 5.68, 5.68, 6.32, 5.98, 5.9, 5.8, 6.45, 6.82, 6.38, 6.45, 7.28, 8.05, 7.93, 8.0, 7.73, 6.85, 6.53, 6.32, 6.05, 6.78, 7.57, 6.93, 6.38, 6.03, 6.22, 6.68, 7.75, 8.1, 7.6, 7.18, 7.22, 7.18, 7.25, 7.8, 7.35, 7.47, 6.72, 6.2, 5.82, 6.1, 6.4, 6.38, 6.32, 6.2, 6.47, 6.57, 6.5, 6.82, 7.22, 7.65, 8.5, 8.85, 8.18, 7.75, 7.98, 8.07, 7.93, 8.0, 8.4, 8.38, 7.75, 7.12, 6.72, 6.53, 6.27, 6.43, 6.57, 7.12, 7.18, 6.53, 6.7, 6.6, 6.9, 7.43, 7.3, 7.5, 8.33, 9.07, 9.5, 10.18, 10.2, 10.18, 10.2, 10.35, 9.68, 9.4, 9.93, 10.43, 10.38, 10.55, 10.43, 10.45, 10.7, 11.1, 11.57, 11.55, 11.3, 11.43, 11.62, 11.2, 11.32, 11.2, 11.15, 11.18, 11.8, 11.73, 11.75, 12.07, 12.6, 13.0, 13.35, 12.65, 11.93, 11.6, 11.23, 11.25, 11.68, 11.73, 12.45, 13.38, 14.02, 14.38, 15.0, 14.55, 14.18, 13.93, 14.65, 15.38, 15.32, 15.18, 15.6, 15.82, 15.95, 16.4, 16.88, 16.52, 14.77, 13.9, 13.85, 14.3, 14.88, 14.6, 14.73, 15.5, 16.8, 17.42, 16.92, 17.15, 17.05, 17.4, 17.42, 17.17, 17.05, 17.27, 17.52, 18.33, 18.77, 19.27, 19.62, 18.67, 17.7, 17.02, 17.12, 17.33, 17.45, 17.17, 16.98, 16.8, 16.5, 16.7, 16.65, 16.7, 16.52, 16.58, 16.95, 17.55, 16.83, 17.2, 17.33, 18.0, 18.3, 18.15, 17.55, 17.17, 17.3, 17.62, 17.58, 17.73, 17.58, 17.55, 17.35, 17.33, 18.05, 18.85, 18.25, 17.67, 17.35, 17.73, 18.3, 19.08, 19.62, 20.15, 20.33, 20.45, 21.0, 20.95, 20.95, 20.95, 20.9, 20.38, 19.58, 19.1, 19.62, 19.95, 19.55, 18.73, 18.4, 17.75, 17.52, 17.15, 17.58, 17.15, 16.88, 16.98, 16.8, 16.52, 16.62, 17.12, 17.17, 16.62, 16.4, 16.67, 17.05, 16.65, 16.4, 16.12, 16.12, 16.42, 17.0, 17.5, 16.9, 16.1, 15.85, 15.77, 15.62, 15.6, 15.67, 15.82, 15.4, 14.93, 15.15, 15.38, 15.35, 15.57, 15.68, 14.85, 14.85, 14.52, 14.18, 14.18, 14.18, 14.18, 14.25, 13.65, 12.95, 12.82, 12.9, 12.75, 12.57, 12.45, 12.32, 12.02, 12.12, 11.98, 12.55, 13.2, 12.82, 12.95, 13.2, 12.65, 12.27, 12.18, 11.9, 12.32, 12.95, 12.83, 13.55, 13.1, 11.95, 10.98, 11.05, 10.73, 10.32, 10.73, 11.32, 11.9, 11.8, 11.15, 11.27, 11.62, 11.5, 11.43, 11.68, 11.55, 10.68, 10.05, 10.48, 9.93, 9.47, 9.48, 9.6, 9.92, 9.5, 9.1, 7.82, 8.02, 8.62, 8.6, 8.38, 7.5, 6.75, 6.93, 7.0, 6.7, 6.47, 6.12, 6.53, 7.3, 7.7, 8.38, 8.3, 8.38, 8.32, 8.1, 8.25, 8.07, 8.73, 9.2, 9.23, 8.07, 7.22, 7.2, 7.0, 6.88, 6.72, 6.55, 6.05, 6.18, 6.4, 6.18, 5.93, 5.9, 5.8, 5.75, 4.97, 4.28, 4.7, 5.6, 5.3, 5.12, 4.65, 4.53, 4.45, 4.65, 5.22, 6.03, 6.33, 6.05, 5.47, 5.3, 4.9, 4.88, 5.03, 5.53, 5.95, 5.95, 4.68, 4.1, 4.45, 5.53, 6.15, 6.25, 6.4, 5.4, 4.53, 4.07, 3.75, 3.47, 3.15, 2.85, 2.58, 2.43, 2.55, 4.18, 5.35, 5.55, 6.0, 6.65, 6.62, 7.15, 7.82, 7.97, 8.03, 7.2, 6.65, 6.57, 6.45, 6.32, 6.57, 6.18, 5.3, 4.88, 5.25, 5.97, 5.97, 6.43, 6.68, 6.65, 6.45, 6.75, 6.88, 6.9, 6.68, 6.57, 6.4, 6.85, 6.7, 6.88, 6.82, 7.15, 7.4, 7.62, 7.4, 7.88, 8.15, 8.52, 9.25, 8.52, 8.07, 8.18, 8.02, 7.4, 6.88, 6.8, 7.1, 7.6, 7.2, 6.88, 6.72, 6.72, 6.93, 7.03, 7.38, 7.73, 8.35, 8.65, 8.88, 8.57, 8.33, 8.45, 8.45, 8.48, 8.7, 8.93, 9.07, 8.77, 8.93, 8.93, 8.73, 9.05, 8.85, 8.88, 8.8, 8.73, 9.68, 11.3, 11.5, 11.6, 11.35, 11.77, 11.88, 11.83, 11.9, 11.85, 11.88, 11.93, 12.12, 11.95, 11.8, 12.12, 12.35, 12.1, 12.07, 12.02, 12.77, 13.2, 13.85, 14.05, 14.43, 15.15, 15.5, 15.6, 15.23, 14.95, 15.18, 15.75, 15.93, 16.17, 16.45, 15.9, 16.12, 16.65, 16.62, 17.45, 18.05, 18.12, 17.42, 17.1, 16.33, 16.15, 16.2, 16.1, 16.28, 16.52, 16.4, 16.62, 16.08, 15.62, 15.6, 16.08, 16.48, 16.65, 16.5, 16.52, 16.73, 16.85, 17.08, 17.12, 17.22, 17.4, 17.27, 17.33, 16.98, 17.52, 18.3, 18.5, 18.2, 18.45, 18.77, 18.33, 17.9, 18.1, 18.42, 18.27, 18.02, 17.75, 17.52, 17.52, 17.52, 17.48, 17.22, 17.08, 17.73, 18.2, 17.95, 17.85, 17.67, 17.8, 17.92, 18.33, 18.45, 18.1, 18.1, 18.28, 17.23, 17.15, 17.33, 17.33, 17.75, 18.92, 19.02, 18.65, 18.33, 18.4, 17.92, 17.77, 17.6, 17.62, 17.65, 17.67, 17.75, 17.6, 17.62, 17.38, 17.25, 17.58, 17.77, 17.83, 18.1, 18.12, 18.08, 17.48, 17.25, 17.58, 17.38, 17.2, 17.0, 16.92, 16.7, 16.0, 15.85, 15.8, 16.1, 16.52, 16.52, 16.65, 16.12, 15.75, 15.02, 14.98, 15.1, 15.1, 14.9, 14.7, 14.07, 14.2, 14.07, 13.8, 13.4, 13.68, 13.7, 13.18, 13.48, 13.88, 13.32, 13.27, 13.17, 13.52, 14.1, 13.55, 12.7, 12.07, 11.75, 11.57, 12.15, 12.7, 12.62, 12.32, 12.48, 12.45, 11.88, 11.5, 11.12, 11.1, 10.55, 10.68, 11.05, 10.77, 10.5, 10.73, 10.95, 11.02, 10.62, 11.1, 11.05, 10.52, 10.27, 10.15, 10.68, 11.0, 10.93, 10.0, 9.27, 9.4, 9.43, 9.07, 7.62, 7.03, 6.93, 6.97, 8.05, 7.62, 6.85, 6.97, 7.15, 7.22, 6.28, 6.38, 6.38, 6.45, 6.35, 6.72, 7.7, 8.12, 8.48, 8.48, 8.18, 8.42, 8.45, 8.38, 7.18, 5.78, 5.17, 6.05, 6.55, 5.28, 5.07, 6.25, 7.18, 7.85, 8.73, 9.02, 9.23, 9.27, 8.98, 7.95, 7.25, 6.88, 6.45, 5.57, 5.5, 5.38, 5.6, 6.12, 6.35, 6.55, 6.32, 6.5, 6.43, 6.25, 6.1, 5.65, 5.9, 6.4, 6.57, 6.47, 6.22, 6.35, 6.45, 6.72, 6.93, 6.4, 6.28, 6.72, 7.12, 7.28, 6.88, 6.95, 6.43, 6.7, 7.35, 7.7, 7.35, 6.3, 5.82, 6.43, 6.85, 7.35, 8.3, 7.75, 7.35, 6.9, 7.1, 6.7, 6.78, 6.95, 6.65, 6.2, 5.75, 5.55, 5.5, 5.9, 5.68, 5.45, 5.28, 5.18, 4.93, 4.85, 4.88, 5.05, 5.6, 6.25, 6.98, 7.53, 7.38, 7.15, 7.62, 7.2, 7.32, 7.2, 6.72, 7.2, 7.5, 7.62, 7.57, 7.68, 8.25, 8.35, 8.55, 8.45, 8.07, 7.2, 6.53, 6.25, 6.47, 7.5, 7.93, 8.12, 7.78, 7.65, 7.43, 7.62, 8.18, 9.05, 9.5, 9.7, 9.73, 9.48, 9.52, 9.62, 9.82, 9.9, 10.18, 10.3, 10.38, 10.18, 10.0, 9.88, 9.77, 9.98, 9.8, 9.75, 10.1, 10.27, 10.43, 10.68, 11.15, 11.65, 11.85, 11.73, 12.25, 12.52, 12.77, 12.6, 12.65, 12.93, 13.35, 13.48, 13.95, 15.48, 15.95, 15.65, 14.93, 15.55, 15.85, 15.0, 14.85, 15.1, 14.85, 14.48, 14.05, 13.98, 14.15, 14.32, 14.7, 14.98, 15.67, 16.75, 16.17, 16.1, 16.0, 16.1, 16.3, 16.35, 16.27, 15.88, 15.98, 16.27, 16.55, 17.17, 17.03, 16.42, 16.3, 16.5, 16.95, 18.27, 18.1, 17.98, 17.7, 17.4, 17.75, 18.02, 18.05, 17.83, 17.95, 17.73, 17.55, 17.27, 17.38, 17.12, 17.45, 17.17, 17.3, 17.5, 18.15, 18.15, 17.8, 17.73, 17.62, 17.92, 18.52, 19.05, 19.15, 18.55, 18.45, 18.7, 18.98, 18.67, 18.08, 17.75, 18.0, 18.4, 18.52, 18.4, 18.33, 18.83, 19.3, 19.25, 18.48, 18.4, 18.33, 18.17, 18.6, 18.95, 19.25, 19.33, 19.38, 19.75, 19.48, 19.38, 19.58, 19.3, 19.27, 18.8, 18.83, 19.1, 19.4, 19.73, 19.8, 19.1, 18.55, 18.25, 18.3, 18.25, 18.15, 18.12, 18.25, 18.33, 18.35, 18.7, 18.73, 18.4, 17.95, 17.8, 17.67, 17.55, 17.85, 17.83, 17.48, 17.02, 16.5, 16.08, 15.45, 15.6, 15.43, 15.12, 14.98, 15.38, 14.93, 14.77, 14.23, 13.88, 13.62, 13.43, 13.32, 13.88, 13.65, 13.77, 14.32, 14.3, 13.68, 13.82, 13.05, 12.68, 12.35, 12.3, 12.65, 13.0, 13.18, 13.38, 13.77, 13.85, 12.62, 12.25, 12.9, 13.32, 13.1, 13.57, 13.52, 13.23, 13.48, 13.55, 13.68, 13.75, 13.38, 13.27, 13.05, 12.65, 12.52, 11.58, 11.7, 11.77, 12.07, 12.02, 11.85, 11.85, 10.9, 10.23, 9.45, 9.7, 10.7, 10.65, 10.23, 9.73, 7.68, 6.82, 7.82, 8.45, 8.73, 8.9, 8.25, 8.32, 8.55, 9.12, 9.1, 8.1, 7.43, 7.07, 6.5, 6.57, 7.1, 6.82, 6.65, 6.07, 5.45, 5.47, 5.32, 4.43, 3.8, 3.22, 2.9, 2.8, 2.75, 2.7, 4.15, 6.07, 6.65, 7.25, 7.68, 7.4, 7.68, 6.6, 6.82, 7.7, 7.25, 8.23, 9.18, 9.2, 8.05, 8.2, 8.85, 9.02, 8.98, 8.65, 8.0, 7.6, 8.0, 8.35, 8.57, 7.93, 7.82, 6.98, 6.18, 4.9, 4.55, 4.38, 4.0, 3.75, 3.9, 4.33, 4.25, 4.12, 4.83, 4.8, 4.85, 5.28, 5.3, 5.38, 6.03, 6.7, 6.88, 7.1, 6.28, 5.28, 4.18, 3.5, 3.3, 4.53, 5.75, 6.15, 5.57, 5.0, 5.38, 6.8, 7.53, 7.95, 7.48, 7.65, 7.62, 7.75, 6.93, 6.4, 5.43, 4.75, 4.65, 4.38, 4.15, 4.7, 5.6, 5.88, 5.8, 5.8, 5.03, 4.78, 4.8, 4.43, 4.68, 6.38, 7.28, 6.53, 6.53, 6.95, 8.2, 8.65, 8.5, 8.62, 8.77, 9.18, 9.7, 9.42, 9.07, 8.25, 7.38, 7.68, 8.88, 9.43, 9.65, 9.38, 8.55, 7.88, 7.92, 7.97, 8.27, 8.98, 9.75, 9.8, 9.7, 9.9, 9.7, 9.35, 9.75, 9.77, 9.9, 10.02, 10.02, 9.88, 9.85, 10.3, 10.52, 10.82, 10.3, 9.8, 9.57, 10.23, 10.65, 10.93, 11.35, 11.52, 11.18, 11.48, 12.43, 12.65, 13.68, 13.98, 14.15, 13.85, 13.8, 13.9, 14.35, 14.93, 14.85, 13.98, 13.88, 13.88, 14.1, 14.57, 15.03, 15.85, 16.1, 14.95, 14.6, 14.57, 14.88, 15.48, 15.43, 15.18, 15.27, 15.68, 15.2, 15.3, 15.7, 15.98, 16.12, 16.45, 16.67, 17.08, 17.75, 18.2, 18.6, 18.58, 18.42, 18.33, 18.4, 18.45, 18.77, 18.58, 19.17, 19.48, 19.2, 18.7, 19.4, 20.1, 20.33, 19.45, 18.92, 18.75, 18.35, 18.25, 17.77, 17.73, 17.23, 17.02, 16.7, 17.45, 18.75, 19.83, 19.62, 19.9, 19.98, 19.27, 18.8, 19.23, 18.9, 18.75, 18.42, 18.85, 18.8, 18.73, 18.15, 17.67, 18.08, 17.6, 17.62, 17.67, 18.12, 18.52, 18.2, 18.0, 17.85, 17.75, 18.08, 18.0, 17.92, 17.48, 17.2, 17.35, 17.58, 17.92, 18.55, 19.22, 18.98, 18.65, 19.1, 18.77, 18.98, 19.12, 20.0, 19.78, 20.08, 19.9, 19.52, 19.67, 19.65, 19.1, 18.3, 17.88, 17.92, 17.3, 17.05, 17.2, 17.62, 17.65, 17.85, 18.33, 18.95, 19.23, 19.3, 19.58, 19.85, 20.2, 20.05, 19.67, 18.55, 17.98, 17.85, 18.23, 18.0, 17.67, 17.42, 17.35, 16.92, 16.65, 16.17, 15.98, 16.6, 16.62, 17.05, 16.92, 16.17, 16.52, 16.83, 17.1, 16.15, 15.93, 15.88, 16.25, 16.62, 16.4, 16.62, 16.55, 16.5, 16.48, 16.2, 14.57, 13.93, 13.5, 12.95, 13.43, 14.02, 13.6, 13.8, 13.3, 13.48, 12.92, 12.68, 12.88, 12.7, 12.85, 12.62, 12.62, 12.7, 12.55, 12.0, 11.65, 11.6, 11.55, 11.32, 11.23, 11.27, 10.93, 10.42, 10.2, 9.98, 10.68, 11.05, 10.93, 10.2, 9.5, 10.12, 10.82, 10.52, 9.73, 9.62, 9.98, 9.1, 8.45, 8.48, 7.85, 7.22, 6.9, 6.68, 5.98, 5.85, 5.83, 5.83, 6.05, 5.6, 5.6, 6.47, 7.22, 7.8, 7.9, 8.2, 8.25, 8.05, 8.1, 8.5, 8.35, 8.42, 8.32, 8.65, 8.5, 8.6, 9.0, 9.65, 9.7, 8.57, 8.57, 8.93, 8.65, 8.52, 8.42, 8.32, 8.92, 8.8, 8.5, 8.1, 7.15, 5.8, 4.82, 4.03, 3.47, 3.17, 3.38, 4.25, 4.43, 4.12, 3.88, 3.55, 3.2, 3.1, 3.07, 4.32, 5.22, 6.12, 6.18, 6.65, 6.05, 5.18, 4.93, 5.57, 6.05, 6.2, 6.1, 6.82, 7.47, 7.83, 7.97, 8.05, 7.05, 6.85, 7.78, 8.23, 8.02, 7.4, 7.68, 8.73, 9.12, 9.15, 9.27, 9.05, 8.68, 8.77, 9.0, 8.8, 7.97, 7.7, 7.68, 7.2, 6.97, 7.47, 7.9, 7.95, 8.25, 8.82, 8.23, 8.05, 8.07, 7.53, 7.32, 7.88, 8.1, 8.18, 8.73, 9.1, 9.75, 9.88, 9.32, 9.78, 10.07, 10.25, 10.6, 10.43, 10.05, 9.38, 9.0, 9.07, 9.4, 9.52, 9.43, 9.82, 9.77, 10.15, 9.95, 10.1, 10.43, 10.75, 11.12, 11.95, 12.38, 12.55, 12.07, 12.05, 12.3, 12.82, 13.1, 12.43, 11.32, 10.95, 10.73, 10.93, 10.9, 10.75, 10.5, 9.98, 10.35, 10.15, 10.25, 10.85, 11.52, 11.3, 12.43, 13.82, 15.18, 15.18, 13.68, 13.95, 14.0, 14.57, 14.7, 14.4, 14.7, 15.3, 15.77, 16.55, 16.8, 16.83, 16.75, 16.83, 16.72, 17.0, 16.8, 16.65, 16.77, 16.55, 16.33, 16.3, 16.33, 16.33, 16.25, 16.4, 16.43, 16.67, 16.62, 16.3, 16.27, 16.55, 16.92, 16.42, 16.05, 16.02, 15.83, 15.73, 15.25, 15.52, 15.52, 15.82, 15.88, 15.82, 15.98, 16.35, 16.33, 16.35, 16.6, 16.7, 16.9, 17.33, 18.02, 18.7, 19.3, 19.3, 18.65, 18.83, 18.55, 18.23, 17.58, 17.47, 17.45, 17.33, 17.15, 17.25, 18.17, 18.9, 18.83, 18.3, 17.65, 17.62, 18.1, 18.8, 18.67, 19.02, 19.67, 20.17, 20.5, 19.88, 20.07, 19.9, 19.58, 19.73, 19.6, 19.67, 19.65, 20.03, 20.38, 20.3, 20.15, 20.3, 19.98, 19.83, 19.95, 20.17, 19.88, 20.0, 19.65, 20.12, 20.52, 21.2, 21.38, 20.85, 20.55, 20.23, 20.12, 19.5, 19.45, 19.12, 18.62, 18.7, 18.7, 18.73, 18.27, 18.23, 18.58, 19.17, 19.05, 18.88, 19.0, 19.67, 19.98, 19.73, 19.48, 19.73, 19.92, 20.15, 19.38, 18.55, 16.95, 16.42, 16.0, 15.6, 15.6, 16.0, 16.17, 16.42, 16.83, 16.98, 16.9, 16.9, 17.2, 16.85, 16.67, 16.62, 16.25, 15.35, 14.93, 14.57, 14.7, 14.7, 14.2, 13.52, 13.1, 12.88, 13.52, 14.38, 14.68, 14.62, 14.05, 13.15, 13.1, 12.95, 12.32, 12.73, 13.82, 14.55, 14.73, 14.68, 14.73, 14.32, 13.77, 13.07, 13.02, 13.32, 13.75, 13.23, 13.52, 13.88, 14.07, 13.42, 13.27, 12.23, 11.2, 10.75, 10.93, 11.35, 10.88, 10.57, 10.85, 11.18, 11.1, 10.5, 10.9, 10.9, 10.88, 10.73, 9.95, 9.32, 8.45, 7.75, 7.12, 6.95, 7.75, 9.23, 9.25, 8.75, 8.7, 8.12, 7.28, 7.47, 8.05, 8.52, 7.72, 7.53, 8.08, 8.18, 8.25, 8.07, 7.88, 7.6, 7.35, 7.25, 6.82, 7.0, 7.98, 8.23, 8.35, 9.02, 8.1, 7.88, 8.07, 7.4, 7.05, 7.55, 8.2, 8.07, 7.35, 6.97, 7.15, 7.4, 7.32, 7.2, 6.28, 5.82, 5.55, 6.5, 6.53, 5.88, 5.45, 5.15, 4.68, 4.2, 4.22, 3.9, 4.75, 5.72, 5.57, 5.15, 4.8, 4.62, 4.5, 4.32, 4.78, 5.35, 5.93, 5.68, 5.78, 6.1, 6.47, 6.5, 5.8, 4.82, 4.1, 3.5, 3.22, 3.75, 4.72, 4.72, 4.6, 4.7, 4.6, 4.4, 4.77, 4.95, 4.3, 4.03, 4.05, 3.55, 3.05, 2.78, 2.85, 4.68, 6.2, 6.77, 7.25, 7.43, 7.47, 7.22, 7.05, 6.62, 6.0, 5.5, 5.22, 5.18, 5.35, 5.8, 6.2, 6.43, 6.5, 6.8, 6.55, 6.22, 5.8, 5.6, 5.57, 6.1, 5.95, 5.65, 6.23, 6.95, 7.97, 8.52, 8.85, 8.85, 8.88, 8.4, 8.15, 8.18, 8.45, 8.73, 8.62, 8.65, 9.07, 9.35, 9.65, 9.38, 9.0, 9.12, 9.27, 9.55, 9.68, 10.0, 10.7, 10.9, 11.18, 11.25, 10.92, 10.9, 10.85, 10.82, 11.25, 11.38, 11.57, 11.62, 11.62, 11.68, 11.88, 12.05, 12.2, 12.73, 13.27, 13.8, 13.77, 13.35, 12.55, 12.02, 12.07, 12.25, 12.27, 12.55, 13.07, 13.65, 13.65, 13.62, 13.7, 13.55, 13.88, 14.3, 14.1, 14.57, 14.73, 14.45, 14.02, 13.3, 13.68, 14.2, 14.27, 14.43, 14.4, 14.75, 15.65, 16.67, 16.12, 15.88, 15.75, 15.43, 15.5, 15.43, 15.4, 15.07, 15.38, 15.25, 16.3, 17.38, 18.67, 18.17, 17.8, 17.8, 18.02, 18.23, 18.23, 18.45, 18.77, 18.5, 18.02, 17.85, 18.55, 18.2, 18.35, 18.8, 19.55, 20.17, 20.92, 20.45, 19.58, 19.2, 18.73, 18.42, 17.98, 18.17, 18.73, 18.98, 19.2, 19.58, 19.48, 19.35, 18.95, 18.67, 18.95, 19.42, 19.7, 19.45, 19.3, 19.15, 19.25, 19.38, 19.17, 19.23, 18.73, 18.75, 18.22, 17.8, 17.45, 17.35, 17.12, 17.33, 17.48, 17.08, 17.17, 17.52, 17.8, 17.85, 18.12, 18.8, 19.12, 19.23, 18.8, 18.4, 17.3, 16.92, 17.23, 17.33, 16.83, 16.48, 16.17, 15.82, 15.7, 15.92, 16.3, 16.53, 16.12, 15.82, 16.08, 16.38, 16.77, 16.77, 17.22, 17.33, 17.12, 17.3, 17.83, 17.65, 16.45, 16.83, 16.4, 16.17, 15.93, 16.42, 16.12, 15.85, 16.17, 16.75, 17.0, 17.0, 15.85, 14.95, 14.7, 14.3, 14.32, 14.52, 14.57, 14.4, 14.32, 14.1, 13.93, 13.95, 13.98, 13.75, 13.7, 14.27, 14.8, 14.73, 14.62, 14.85, 14.95, 15.15, 15.07, 14.95, 14.65, 14.45, 14.23, 13.52, 13.65, 14.12, 14.12, 13.88, 13.28, 12.73, 12.25, 11.82, 11.52, 11.88, 12.1, 11.7, 11.73, 11.77, 11.57, 11.8, 12.12, 11.98, 11.58, 11.25, 11.57, 11.27, 11.32, 11.3, 11.25, 11.68, 11.65, 11.23, 10.2, 9.38, 9.18, 8.75, 8.25, 7.47, 7.03, 7.05, 7.45, 7.53, 7.22, 7.9, 8.62, 8.8, 8.27, 8.05, 8.27, 8.27, 8.23, 8.07, 8.6, 9.25, 9.77, 10.07, 9.88, 9.43, 9.4, 8.6, 8.68, 8.5, 8.57, 8.65, 9.15, 8.7, 8.48, 8.3, 7.72, 7.07, 5.9, 4.95, 4.4, 4.32, 4.4, 4.65, 4.4, 4.82, 4.97, 4.65, 4.6, 4.55, 4.5, 4.43, 4.3, 3.97, 3.78, 3.55, 3.4, 4.28, 5.3, 5.45, 6.07, 6.2, 5.7, 5.15, 4.85, 4.7, 4.6, 4.53, 4.78, 4.38, 3.8, 3.88, 4.32, 4.43, 4.17, 4.45, 5.07, 5.3, 4.65, 4.47, 4.82, 4.65, 5.22, 5.43, 5.55, 5.22, 5.75, 6.53, 6.57, 5.62, 4.55, 4.2, 4.7, 4.85, 4.5, 4.6, 5.47, 6.62, 7.22, 7.53, 8.1, 8.27, 8.4, 8.75, 8.18, 8.15, 7.83, 7.83, 7.72, 7.72, 8.2, 8.6, 8.43, 8.45, 8.62, 8.3, 8.42, 8.2, 7.65, 7.8, 8.05, 8.55, 8.3, 8.4, 8.27, 8.27, 8.52, 8.77, 8.98, 8.35, 7.7, 8.02, 8.02, 8.12, 8.0, 8.4, 8.88, 8.57, 9.15, 9.88, 9.52, 9.3, 9.38, 9.93, 10.52, 10.07, 9.98, 9.95, 9.82, 10.07, 10.73, 11.05, 11.38, 11.02, 10.77, 10.85, 10.73, 10.88, 11.12, 11.35, 11.52, 11.52, 11.68, 11.68, 11.75, 11.95, 12.23, 13.05, 13.25, 13.32, 12.77, 12.43, 12.57, 12.6, 12.73, 12.65, 12.0, 12.0, 12.12, 11.8, 11.7, 11.67, 12.23, 12.68, 13.05, 13.77, 14.57, 15.5, 16.25, 16.15, 16.42, 16.8, 16.35, 16.67, 16.9, 17.05, 16.67, 16.77, 16.48, 16.35, 16.2, 16.38, 16.2, 16.23, 16.0, 15.65, 15.4, 15.55, 16.1, 15.77, 15.82, 16.27, 17.35, 18.0, 19.4, 20.8) # fmt: skip # noqa: FURB152 

63 

64 

65def generate_example_data( 

66 *, 

67 date_start: str = "2020-01-01", 

68 date_end: str = "2021-12-31", 

69 date_freq: str = "D", 

70 flow_mean: float = 100.0, # m3/day 

71 flow_amplitude: float = 30.0, # m3/day 

72 flow_noise: float = 10.0, # m3/day 

73 cin_method: str = "synthetic", 

74 cin_mean: float = 12.0, 

75 cin_amplitude: float = 8.0, 

76 measurement_noise: float = 1.0, 

77 aquifer_pore_volumes: npt.ArrayLike | None = None, 

78 aquifer_pore_volume_gamma_mean: float | None = None, 

79 aquifer_pore_volume_gamma_std: float | None = None, 

80 aquifer_pore_volume_gamma_loc: float | None = None, 

81 aquifer_pore_volume_gamma_nbins: int | None = None, 

82 retardation_factor: float = 1.0, 

83 molecular_diffusivity: float | None = None, 

84 longitudinal_dispersivity: float | None = None, 

85 streamline_length: float | None = None, 

86 rng: np.random.Generator | int | None = None, 

87) -> tuple[pd.DataFrame, pd.DatetimeIndex]: 

88 """ 

89 Generate synthetic concentration/temperature and flow data for groundwater transport. 

90 

91 Creates a synthetic dataset with seasonal flow patterns, input concentration (cin), 

92 and output concentration (cout) computed via gamma-distributed pore volume transport. 

93 When ``molecular_diffusivity``, ``longitudinal_dispersivity``, and ``streamline_length`` 

94 are provided, the diffusion module is used instead of pure advection. 

95 

96 Parameters 

97 ---------- 

98 date_start, date_end : str 

99 Start and end dates for the generated time series (YYYY-MM-DD). 

100 date_freq : str, default "D" 

101 Frequency string for pandas.date_range. 

102 flow_mean : float, default 100.0 

103 Mean flow rate [m³/day]. 

104 flow_amplitude : float, default 30.0 

105 Seasonal amplitude of flow rate [m³/day]. 

106 flow_noise : float, default 10.0 

107 Random noise level for flow rate [m³/day]. 

108 cin_method : str, default "synthetic" 

109 Method for generating infiltration concentration. Options: 

110 

111 - ``"synthetic"``: Seasonal sinusoidal pattern defined by ``cin_mean`` and 

112 ``cin_amplitude``. Measurement noise is applied. 

113 - ``"constant"``: Constant value equal to ``cin_mean``. Measurement noise 

114 is still applied. 

115 - ``"soil_temperature"``: Real soil temperature data from KNMI station 260. 

116 cin_mean : float, default 12.0 

117 Mean value of infiltrating concentration. 

118 cin_amplitude : float, default 8.0 

119 Seasonal amplitude of infiltration concentration (only used for 

120 ``"synthetic"`` method). 

121 measurement_noise : float, default 1.0 

122 Standard deviation of the Gaussian measurement noise applied 

123 independently to ``cin`` and ``cout``. Because the two noise draws are 

124 independent, applying the forward operator to ``df['cin']`` does not 

125 exactly reproduce ``df['cout']`` when ``measurement_noise > 0``; the 

126 underlying noiseless signals remain consistent. 

127 aquifer_pore_volumes : array-like or None, default None 

128 Discrete aquifer pore volumes [m³] representing the distribution of 

129 residence times. When provided, the gamma distribution is bypassed and 

130 none of the ``aquifer_pore_volume_gamma_*`` parameters may be passed. 

131 When ``None``, the pore volume distribution is built from the gamma 

132 parameters below. 

133 aquifer_pore_volume_gamma_mean : float or None, default None 

134 Mean pore volume of the aquifer gamma distribution [m³] (default 1000.0 

135 when unset). Must be strictly greater than 

136 ``aquifer_pore_volume_gamma_loc``. Mutually exclusive with 

137 ``aquifer_pore_volumes``. 

138 aquifer_pore_volume_gamma_std : float or None, default None 

139 Standard deviation of aquifer pore volume gamma distribution [m³] 

140 (default 200.0 when unset; invariant under the ``loc`` shift). 

141 Mutually exclusive with ``aquifer_pore_volumes``. 

142 aquifer_pore_volume_gamma_loc : float or None, default None 

143 Location (minimum pore volume) of the aquifer gamma distribution [m³] 

144 (default 0.0 when unset). Must satisfy ``0 <= loc < mean``. Mutually 

145 exclusive with ``aquifer_pore_volumes``. 

146 aquifer_pore_volume_gamma_nbins : int or None, default None 

147 Number of bins to discretize the aquifer pore volume gamma distribution 

148 (default 250 when unset). Mutually exclusive with 

149 ``aquifer_pore_volumes``. 

150 retardation_factor : float, default 1.0 

151 Retardation factor for transport. 

152 molecular_diffusivity : float or None, default None 

153 Effective molecular diffusivity [m²/day]. When provided together with 

154 ``longitudinal_dispersivity`` and ``streamline_length``, the diffusion 

155 module is used instead of pure advection. For solutes, typically ~1e-5 

156 m²/day (negligible). For heat, use thermal diffusivity ~0.01-0.1 m²/day. 

157 longitudinal_dispersivity : float or None, default None 

158 Longitudinal dispersivity [m]. Must be provided together with 

159 ``molecular_diffusivity`` and ``streamline_length``. 

160 streamline_length : float or None, default None 

161 Travel distance along the streamline [m]. Must be provided together 

162 with ``molecular_diffusivity`` and ``longitudinal_dispersivity``. 

163 rng : numpy.random.Generator, int, or None, default None 

164 Source of randomness for the synthetic flow noise, spill events, and 

165 measurement noise. Accepted in any form supported by 

166 :func:`numpy.random.default_rng`. Pass an integer (or 

167 :class:`numpy.random.Generator`) for reproducible output; ``None`` 

168 draws fresh entropy each call. 

169 

170 Returns 

171 ------- 

172 tuple 

173 A tuple containing: 

174 

175 - pandas.DataFrame: DataFrame with columns ``'flow'``, ``'cin'``, 

176 ``'cout'`` and metadata attributes for the aquifer parameters. 

177 - pandas.DatetimeIndex: Time edges (tedges) used for the flow 

178 calculations. 

179 

180 Raises 

181 ------ 

182 ValueError 

183 If ``cin_method`` is not one of the supported methods, if only some 

184 of the diffusion parameters are provided, or if ``aquifer_pore_volumes`` 

185 is passed together with any ``aquifer_pore_volume_gamma_*`` parameter. 

186 

187 See Also 

188 -------- 

189 generate_temperature_example_data : Wrapper with thermal transport defaults. 

190 """ 

191 rng = np.random.default_rng(rng) 

192 

193 dates = pd.date_range(start=date_start, end=date_end, freq=date_freq).tz_localize("UTC") 

194 # Fractional elapsed days so the seasonal sinusoid resolves sub-daily sampling (integer .days 

195 # would stair-step, holding the seasonal constant within each calendar day). 

196 days = ((dates - dates[0]) / pd.Timedelta(days=1)).to_numpy() 

197 

198 # Generate flow data with seasonal pattern (higher in winter) 

199 seasonal_flow = flow_mean + flow_amplitude * np.sin(2 * np.pi * days / 365 + np.pi) 

200 flow = seasonal_flow + rng.normal(0, flow_noise, len(dates)) 

201 

202 min_days_for_spills = 60 

203 if len(dates) > min_days_for_spills: # Only add spills for longer time series 

204 n_spills = int(rng.integers(6, 16)) 

205 for _ in range(n_spills): 

206 spill_start = int(rng.integers(0, len(dates) - 30)) 

207 spill_duration = int(rng.integers(15, 45)) 

208 spill_magnitude = float(rng.uniform(2.0, 5.0)) 

209 

210 flow[spill_start : spill_start + spill_duration] /= spill_magnitude 

211 

212 # Enforce a positive flow floor after spills so residence times remain finite. 

213 flow = np.maximum(flow, 5.0) 

214 

215 # Generate infiltration concentration. nonoise is needed to compute cout. 

216 if cin_method == "synthetic": 

217 # Seasonal pattern with noise 

218 cin_nonoise = cin_mean + cin_amplitude * np.sin(2 * np.pi * days / 365) 

219 cin_values = cin_nonoise + rng.normal(0, measurement_noise, len(dates)) 

220 elif cin_method == "constant": 

221 # Constant value 

222 cin_nonoise = np.full(len(dates), cin_mean) 

223 cin_values = cin_nonoise + rng.normal(0, measurement_noise, len(dates)) 

224 elif cin_method == "soil_temperature": 

225 # Use the inline KNMI soil temperature data (already includes measurement noise). 

226 soil_temperature = pd.Series( 

227 _SOIL_TEMPERATURE_TB3, 

228 index=pd.date_range( 

229 start=_SOIL_TEMPERATURE_TB3_START, periods=len(_SOIL_TEMPERATURE_TB3), freq="D", tz="UTC" 

230 ), 

231 name="TB3", 

232 ) 

233 cin_nonoise = cin_values = soil_temperature.resample(date_freq).mean()[dates].values 

234 else: 

235 msg = f"Unknown cin_method: {cin_method}" 

236 raise ValueError(msg) 

237 

238 tedges = compute_time_edges(tedges=None, tstart=None, tend=dates, number_of_bins=len(dates)) 

239 

240 # Validate pore volume parameterization: either discrete volumes or gamma parameters, not both. 

241 gamma_set_by_user = [ 

242 name 

243 for name, value in { 

244 "aquifer_pore_volume_gamma_mean": aquifer_pore_volume_gamma_mean, 

245 "aquifer_pore_volume_gamma_std": aquifer_pore_volume_gamma_std, 

246 "aquifer_pore_volume_gamma_loc": aquifer_pore_volume_gamma_loc, 

247 "aquifer_pore_volume_gamma_nbins": aquifer_pore_volume_gamma_nbins, 

248 }.items() 

249 if value is not None 

250 ] 

251 if aquifer_pore_volumes is not None and gamma_set_by_user: 

252 msg = ( 

253 "aquifer_pore_volumes is mutually exclusive with the aquifer_pore_volume_gamma_* " 

254 f"parameters; got both aquifer_pore_volumes and {gamma_set_by_user}." 

255 ) 

256 raise ValueError(msg) 

257 

258 # Validate diffusion parameterization: all three parameters provided or none. 

259 diffusion_provided = (molecular_diffusivity, longitudinal_dispersivity, streamline_length) 

260 n_diffusion = sum(1 for p in diffusion_provided if p is not None) 

261 if 0 < n_diffusion < len(diffusion_provided): 

262 msg = "molecular_diffusivity, longitudinal_dispersivity, and streamline_length must all be provided together." 

263 raise ValueError(msg) 

264 # Validation above forbids partial-set states, so this conjunction is equivalent to any single check; 

265 # writing it in full lets the type checker narrow all three params to non-None inside the branches below. 

266 use_diffusion = ( 

267 molecular_diffusivity is not None and longitudinal_dispersivity is not None and streamline_length is not None 

268 ) 

269 

270 # Fill in gamma defaults so downstream callers see concrete values (not used when 

271 # aquifer_pore_volumes is supplied, but kept in scope for the attrs block below). 

272 gamma_mean = aquifer_pore_volume_gamma_mean if aquifer_pore_volume_gamma_mean is not None else _DEFAULT_GAMMA_MEAN 

273 gamma_std = aquifer_pore_volume_gamma_std if aquifer_pore_volume_gamma_std is not None else _DEFAULT_GAMMA_STD 

274 gamma_loc = aquifer_pore_volume_gamma_loc if aquifer_pore_volume_gamma_loc is not None else _DEFAULT_GAMMA_LOC 

275 gamma_nbins = ( 

276 aquifer_pore_volume_gamma_nbins if aquifer_pore_volume_gamma_nbins is not None else _DEFAULT_GAMMA_NBINS 

277 ) 

278 

279 # Compute cout. Branch on pore volume parameterization, then on diffusion. 

280 if aquifer_pore_volumes is not None: 

281 aquifer_pore_volumes_array = np.asarray(aquifer_pore_volumes, dtype=float) 

282 if use_diffusion: 

283 cout_values = diffusion_infiltration_to_extraction( 

284 cin=cin_nonoise, 

285 flow=flow, 

286 tedges=tedges, 

287 cout_tedges=tedges, 

288 aquifer_pore_volumes=aquifer_pore_volumes_array, 

289 streamline_length=streamline_length, 

290 molecular_diffusivity=molecular_diffusivity, 

291 longitudinal_dispersivity=longitudinal_dispersivity, 

292 retardation_factor=retardation_factor, 

293 ) 

294 else: 

295 cout_values = infiltration_to_extraction( 

296 cin=cin_nonoise, 

297 flow=flow, 

298 tedges=tedges, 

299 cout_tedges=tedges, 

300 aquifer_pore_volumes=aquifer_pore_volumes_array, 

301 retardation_factor=retardation_factor, 

302 ) 

303 elif use_diffusion: 

304 cout_values = diffusion_gamma_infiltration_to_extraction( 

305 cin=cin_nonoise, 

306 flow=flow, 

307 tedges=tedges, 

308 cout_tedges=tedges, 

309 mean=gamma_mean, 

310 std=gamma_std, 

311 loc=gamma_loc, 

312 n_bins=gamma_nbins, 

313 streamline_length=streamline_length, 

314 molecular_diffusivity=molecular_diffusivity, 

315 longitudinal_dispersivity=longitudinal_dispersivity, 

316 retardation_factor=retardation_factor, 

317 ) 

318 else: 

319 cout_values = gamma_infiltration_to_extraction( 

320 cin=cin_nonoise, 

321 flow=flow, 

322 tedges=tedges, 

323 cout_tedges=tedges, 

324 mean=gamma_mean, 

325 std=gamma_std, 

326 loc=gamma_loc, 

327 n_bins=gamma_nbins, 

328 retardation_factor=retardation_factor, 

329 ) 

330 

331 # Add some noise to represent measurement errors 

332 cout_values += rng.normal(0, measurement_noise, len(dates)) 

333 

334 df = pd.DataFrame( 

335 data={"flow": flow, "cin": cin_values, "cout": cout_values}, 

336 index=dates, 

337 ) 

338 df.attrs.update({ 

339 "description": "Example data for groundwater transport modeling", 

340 "source": "Synthetic data generated by gwtransport.examples.generate_example_data", 

341 "retardation_factor": retardation_factor, 

342 "date_start": date_start, 

343 "date_end": date_end, 

344 "date_freq": date_freq, 

345 "flow_mean": flow_mean, 

346 "flow_amplitude": flow_amplitude, 

347 "flow_noise": flow_noise, 

348 "cin_method": cin_method, 

349 "cin_mean": cin_mean, 

350 "cin_amplitude": cin_amplitude, 

351 "measurement_noise": measurement_noise, 

352 }) 

353 if aquifer_pore_volumes is not None: 

354 df.attrs["aquifer_pore_volume_parameterization"] = "discrete" 

355 df.attrs["aquifer_pore_volumes"] = aquifer_pore_volumes_array 

356 else: 

357 alpha, beta = mean_std_loc_to_alpha_beta(mean=gamma_mean, std=gamma_std, loc=gamma_loc) 

358 df.attrs.update({ 

359 "aquifer_pore_volume_parameterization": "gamma", 

360 "aquifer_pore_volume_gamma_mean": gamma_mean, 

361 "aquifer_pore_volume_gamma_std": gamma_std, 

362 "aquifer_pore_volume_gamma_loc": gamma_loc, 

363 "aquifer_pore_volume_gamma_alpha": alpha, 

364 "aquifer_pore_volume_gamma_beta": beta, 

365 "aquifer_pore_volume_gamma_nbins": gamma_nbins, 

366 }) 

367 if molecular_diffusivity is not None: 

368 df.attrs["molecular_diffusivity"] = molecular_diffusivity 

369 df.attrs["longitudinal_dispersivity"] = longitudinal_dispersivity 

370 df.attrs["streamline_length"] = streamline_length 

371 

372 return df, tedges 

373 

374 

375def generate_temperature_example_data( 

376 *, 

377 date_start: str = "2020-01-01", 

378 date_end: str = "2021-12-31", 

379 date_freq: str = "D", 

380 flow_mean: float = 100.0, 

381 flow_amplitude: float = 30.0, 

382 flow_noise: float = 10.0, 

383 cin_method: str = "synthetic", 

384 cin_mean: float = 12.0, 

385 cin_amplitude: float = 8.0, 

386 measurement_noise: float = 1.0, 

387 aquifer_pore_volumes: npt.ArrayLike | None = None, 

388 aquifer_pore_volume_gamma_mean: float | None = None, 

389 aquifer_pore_volume_gamma_std: float | None = None, 

390 aquifer_pore_volume_gamma_loc: float | None = None, 

391 aquifer_pore_volume_gamma_nbins: int | None = None, 

392 retardation_factor: float = 2.0, 

393 molecular_diffusivity: float = 0.05, 

394 longitudinal_dispersivity: float = 1.0, 

395 streamline_length: float = 100.0, 

396 rng: np.random.Generator | int | None = None, 

397) -> tuple[pd.DataFrame, pd.DatetimeIndex]: 

398 """ 

399 Generate synthetic temperature and flow data for groundwater transport examples. 

400 

401 Convenience wrapper around :func:`generate_example_data` with sensible 

402 defaults for temperature transport: thermal retardation factor, thermal 

403 diffusivity, longitudinal dispersivity, and streamline length. 

404 

405 Typical parameter values for temperature transport in various sand types: 

406 

407 +---------------------------------+------------+-------------+--------------------+ 

408 | Parameter | Fine sand | Medium sand | Coarse sand/gravel | 

409 +=================================+============+=============+====================+ 

410 | retardation_factor R | 2.0--3.0 | 1.5--2.5 | 1.2--2.0 | 

411 +---------------------------------+------------+-------------+--------------------+ 

412 | molecular_diffusivity (m²/day) | 0.03--0.06 | 0.05--0.08 | 0.08--0.12 | 

413 +---------------------------------+------------+-------------+--------------------+ 

414 | longitudinal_dispersivity (m) | 0.1--1.0 | 0.5--5.0 | 1.0--10.0 | 

415 +---------------------------------+------------+-------------+--------------------+ 

416 | streamline_length (m) | site-specific | 

417 +---------------------------------+------------+-------------+--------------------+ 

418 

419 Parameters 

420 ---------- 

421 retardation_factor : float, default 2.0 

422 Thermal retardation factor. 

423 molecular_diffusivity : float, default 0.05 

424 Thermal diffusivity [m²/day]. 

425 longitudinal_dispersivity : float, default 1.0 

426 Longitudinal dispersivity [m]. 

427 streamline_length : float, default 100.0 

428 Travel distance along the streamline [m]. 

429 

430 Returns 

431 ------- 

432 tuple 

433 See :func:`generate_example_data`. 

434 

435 See Also 

436 -------- 

437 generate_example_data : Generic version with full parameter control. 

438 

439 Notes 

440 ----- 

441 All other parameters are forwarded unchanged to :func:`generate_example_data`; 

442 see that function for their descriptions. 

443 """ 

444 return generate_example_data( 

445 date_start=date_start, 

446 date_end=date_end, 

447 date_freq=date_freq, 

448 flow_mean=flow_mean, 

449 flow_amplitude=flow_amplitude, 

450 flow_noise=flow_noise, 

451 cin_method=cin_method, 

452 cin_mean=cin_mean, 

453 cin_amplitude=cin_amplitude, 

454 measurement_noise=measurement_noise, 

455 aquifer_pore_volumes=aquifer_pore_volumes, 

456 aquifer_pore_volume_gamma_mean=aquifer_pore_volume_gamma_mean, 

457 aquifer_pore_volume_gamma_std=aquifer_pore_volume_gamma_std, 

458 aquifer_pore_volume_gamma_loc=aquifer_pore_volume_gamma_loc, 

459 aquifer_pore_volume_gamma_nbins=aquifer_pore_volume_gamma_nbins, 

460 retardation_factor=retardation_factor, 

461 molecular_diffusivity=molecular_diffusivity, 

462 longitudinal_dispersivity=longitudinal_dispersivity, 

463 streamline_length=streamline_length, 

464 rng=rng, 

465 ) 

466 

467 

468def generate_example_deposition_timeseries( 

469 *, 

470 date_start: str = "2018-01-01", 

471 date_end: str = "2023-12-31", 

472 freq: str = "D", 

473 base: float = 0.8, 

474 seasonal_amplitude: float = 0.3, 

475 noise_scale: float = 0.1, 

476 event_dates: npt.ArrayLike | pd.DatetimeIndex | None = None, 

477 event_magnitude: float = 3.0, 

478 event_duration: int = 30, 

479 event_decay_scale: float = 10.0, 

480 ensure_non_negative: bool = True, 

481 rng: np.random.Generator | int | None = None, 

482) -> tuple[pd.Series, pd.DatetimeIndex]: 

483 """ 

484 Generate synthetic deposition timeseries for groundwater transport examples. 

485 

486 Parameters 

487 ---------- 

488 date_start, date_end : str 

489 Start and end dates for the generated time series (YYYY-MM-DD). 

490 freq : str 

491 Frequency string for pandas.date_range (default 'D'). 

492 base : float 

493 Baseline deposition rate (ng/m²/day). 

494 seasonal_amplitude : float 

495 Amplitude of the annual seasonal sinusoidal pattern (ng/m²/day). 

496 noise_scale : float 

497 Standard deviation of Gaussian noise added to the signal (ng/m²/day). 

498 event_dates : list-like or None 

499 Dates (strings or pandas-compatible) at which to place episodic events. 

500 Time-zone-naive entries are interpreted as UTC to match the generated 

501 ``dates`` index. If None, a sensible default list is used. 

502 event_magnitude : float 

503 Peak deposition added at event onset (ng/m²/day). Decays exponentially 

504 over ``event_duration`` days at rate ``event_decay_scale``. 

505 event_duration : int 

506 Duration of each event in days. 

507 event_decay_scale : float 

508 Decay scale used in the exponential decay for event time series. 

509 ensure_non_negative : bool 

510 If True, negative values are clipped to zero. 

511 rng : numpy.random.Generator, int, or None, default None 

512 Source of randomness for the additive Gaussian noise. Accepted in any 

513 form supported by :func:`numpy.random.default_rng`. Pass an integer 

514 (or :class:`numpy.random.Generator`) for reproducible output; ``None`` 

515 draws fresh entropy each call. 

516 

517 Returns 

518 ------- 

519 tuple 

520 A tuple containing: 

521 

522 - pandas.Series: Deposition time series (ng/m²/day) indexed by UTC 

523 timestamps. 

524 - pandas.DatetimeIndex: Time bin edges (n+1 edges for n values). 

525 

526 Raises 

527 ------ 

528 ValueError 

529 If ``event_decay_scale`` or ``event_duration`` is not positive, or if any 

530 ``event_dates`` entry falls outside the generated ``dates`` range. 

531 

532 See Also 

533 -------- 

534 gwtransport.deposition.deposition_to_extraction : Forward operator consuming this data. 

535 gwtransport.deposition.extraction_to_deposition : Inverse operator. 

536 """ 

537 if event_decay_scale <= 0: 

538 msg = f"event_decay_scale must be positive, got {event_decay_scale}" 

539 raise ValueError(msg) 

540 if event_duration <= 0: 

541 msg = f"event_duration must be positive, got {event_duration}" 

542 raise ValueError(msg) 

543 

544 rng = np.random.default_rng(rng) 

545 

546 dates = pd.date_range(date_start, date_end, freq=freq).tz_localize("UTC") 

547 n_dates = len(dates) 

548 tedges = compute_time_edges(tedges=None, tstart=None, tend=dates, number_of_bins=n_dates) 

549 

550 # Base deposition rate with seasonal and event patterns. Use elapsed days (not the sample 

551 # index) so the period stays one year for any ``freq``, not one year of samples. 

552 days = ((dates - dates[0]) / pd.Timedelta(days=1)).to_numpy() 

553 seasonal_pattern = seasonal_amplitude * np.sin(2 * np.pi * days / 365.25) 

554 noise = noise_scale * rng.normal(0, 1, n_dates) 

555 

556 # Default event dates if not provided 

557 if event_dates is None: 

558 event_dates = ["2020-06-15", "2021-03-20", "2021-09-10", "2022-07-05"] 

559 event_dates_index = pd.DatetimeIndex(pd.to_datetime(np.asarray(event_dates))) 

560 # Match the timezone of `dates` so naive user input (and the string defaults) 

561 # can be compared against the tz-aware index in `get_indexer`. 

562 if event_dates_index.tz is None: 

563 event_dates_index = event_dates_index.tz_localize(dates.tz) 

564 else: 

565 event_dates_index = event_dates_index.tz_convert(dates.tz) 

566 

567 out_of_range = (event_dates_index < dates[0]) | (event_dates_index > dates[-1]) 

568 if out_of_range.any(): 

569 msg = ( 

570 f"event_dates contains {out_of_range.sum()} date(s) outside the dates range " 

571 f"[{dates[0]}, {dates[-1]}]: {event_dates_index[out_of_range].tolist()}" 

572 ) 

573 raise ValueError(msg) 

574 

575 # Vectorized event accumulation. For each event start, scatter a ``(n_events, event_duration)`` 

576 # decay block into ``event`` via ``np.add.at`` so overlapping events sum correctly. The 

577 # boundary mask drops indices that fall past the end of the series (preserves the loop's 

578 # ``min(event_idx + event_duration, n_dates)`` clipping). 

579 starts = dates.get_indexer(event_dates_index, method="nearest") 

580 cols = np.arange(event_duration) 

581 flat_indices = starts[:, None] + cols[None, :] 

582 valid = flat_indices < n_dates 

583 decay_block = np.broadcast_to(event_magnitude * np.exp(-cols / event_decay_scale), flat_indices.shape) 

584 event = np.zeros(n_dates) 

585 np.add.at(event, flat_indices[valid], decay_block[valid]) 

586 

587 # Combine all components 

588 total = base + seasonal_pattern + noise + event 

589 if ensure_non_negative: 

590 total = np.maximum(total, 0.0) 

591 

592 series = pd.Series(data=total, index=dates, name="deposition") 

593 series.attrs.update({ 

594 "description": "Example deposition time series for groundwater transport modeling", 

595 "source": "Synthetic data generated by gwtransport.examples.generate_example_deposition_timeseries", 

596 "base": base, 

597 "seasonal_amplitude": seasonal_amplitude, 

598 "noise_scale": noise_scale, 

599 "event_dates": [str(d.date()) for d in event_dates_index], 

600 "event_magnitude": event_magnitude, 

601 "event_duration": event_duration, 

602 "event_decay_scale": event_decay_scale, 

603 "date_start": date_start, 

604 "date_end": date_end, 

605 "date_freq": freq, 

606 }) 

607 

608 return series, tedges