gwtransport#
gwtransport: A Python package for solving one-dimensional groundwater transport problems.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
advection#
Advective Transport Modeling Along Aquifer Pore Volumes.
Water infiltrates and is transported in parallel along multiple aquifer pore volumes to extraction. For each aquifer pore volume, transport is 1D advection with linear or non-linear sorption; there is no microdispersion or molecular diffusion, while the spread across aquifer pore volumes provides macrodispersion. Forward and backward modeling are supported. No assumption is made about whether the flow is radial or orthogonal.
Available functions:
infiltration_to_extraction()- Arbitrary pore volume distribution, flow-weighted averaging. Supports explicit distribution of aquifer pore volumes with flow-weighted averaging. Flexible output time resolution via cout_tedges. Use case: Known pore volume distribution from streamline analysis.gamma_infiltration_to_extraction()- Gamma-distributed pore volumes, flow-weighted averaging. Models aquifer heterogeneity with 2-parameter gamma distribution. Parameterizable via (alpha, beta) or (mean, std). Discretizes gamma distribution into equal-probability bins. Use case: Heterogeneous aquifer with calibrated gamma parameters.extraction_to_infiltration()- Arbitrary pore volume distribution, deconvolution. Inverts forward transport for arbitrary pore volume distributions. Symmetric inverse of infiltration_to_extraction. Flow-weighted averaging in reverse direction. Use case: Estimating infiltration history from extraction data.gamma_extraction_to_infiltration()- Gamma-distributed pore volumes, deconvolution. Inverts forward transport for gamma-distributed pore volumes. Symmetric inverse of gamma_infiltration_to_extraction. Use case: Calibrating infiltration conditions from extraction measurements.infiltration_to_extraction_nonlinear_sorption()- Exact front tracking with nonlinear sorption. Event-driven algorithm that solves 1D advective transport with Freundlich or Langmuir isotherm using analytical integration of shock and rarefaction waves. Machine-precision physics (no numerical dispersion). Returns bin-averaged concentrations together with the full piecewise analytical structure (events, segments, wave list) for downstream analysis. Use case: Sharp concentration fronts with exact mass balance required, across a distribution of aquifer pore volumes (macrodispersion). Forward modeling only; nonlinear sorption has no inverse.
Note on dispersion: The spreading from the pore volume distribution (APVD) represents
macrodispersion—aquifer-scale velocity heterogeneity that depends on both aquifer
properties and hydrological boundary conditions. To add microdispersion and molecular
diffusion separately (when APVD comes from streamline analysis), use gwtransport.diffusion.
See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for details.
Note on cross-compound calibration: When APVD is calibrated from measurements of one
compound (e.g., temperature with D_m ~ 0.1 m²/day) and used to predict another (e.g., a
solute with D_m ~ 1e-4 m²/day), the molecular diffusion contribution is baked into the
calibrated std. The cleanest fix is to calibrate with gwtransport.diffusion_fast
instead, which keeps the three contributions separate.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.advection.gamma_infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, retardation_factor=1.0, spinup='constant')[source]#
Compute the concentration of the extracted water by shifting cin with its residence time.
The compound is retarded in the aquifer with a retardation factor. The residence time is computed based on the flow rate of the water in the aquifer and the pore volume of the aquifer. The aquifer pore volume is approximated by a (shifted) gamma distribution parameterized by either (mean, std, loc) or (alpha, beta, loc).
This function represents infiltration to extraction modeling by flow-weighted averaging.
Provide either (mean, std) or (alpha, beta);
locis optional and defaults to 0.- Parameters:
cin (
ArrayLike) – Concentration of the compound in infiltrating water or temperature of infiltrating water. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time edges for both cin and flow data. Used to compute the cumulative concentration. Has a length of one more than cin and flow.cout_tedges (
DatetimeIndex) – Time edges for the output data. Used to compute the cumulative concentration. Has a length of one more than the desired output length.mean (
float|None, default:None) – Mean of the gamma distribution of the aquifer pore volume. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution of the aquifer pore volume (invariant under thelocshift).loc (
float, default:0.0) – Location (minimum pore volume) of the gamma distribution. Must satisfy0 <= loc < mean. Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution of the aquifer pore volume (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution of the aquifer pore volume (must be > 0).n_bins (
int, default:100) – Number of bins to discretize the gamma distribution. Default 100.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption/interaction.spinup (
str|float|None, default:'constant') – Forwarded toinfiltration_to_extraction(). Default"constant"warm-starts the system beforetedges[0].
- Returns:
Concentration of the compound in the extracted water, or temperature. Same units as cin.
- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionTransport with explicit pore volume distribution
gamma_extraction_to_infiltrationReverse operation (deconvolution)
gwtransport.gamma.binsCreate gamma distribution bins
gwtransport.residence_time.fullCompute residence times
gwtransport.diffusion.infiltration_to_extractionAdd microdispersion and molecular diffusion
- Gamma Distribution Model
Two-parameter pore volume model
- 8. Gamma Distribution Adequacy
When gamma distribution is adequate
Notes
The APVD is only time-invariant under the steady-streamlines assumption (see 2. Steady Streamlines).
The spreading from the gamma-distributed pore volumes represents macrodispersion (aquifer-scale heterogeneity). When
stdcomes from calibration on measurements, it absorbs all mixing: macrodispersion, microdispersion, and an average molecular diffusion contribution. When calibrating with the diffusion module, these three components are taken into account separately. Whenstdcomes from streamline analysis, it represents macrodispersion only; microdispersion and molecular diffusion can be added viagwtransport.diffusion_fastorgwtransport.diffusion.For cross-compound prediction (calibrating on temperature and predicting a solute), calibrate with
gwtransport.diffusion_fastso the three contributions are tracked separately rather than lumped into a single calibratedstd. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for background.Examples
Basic usage with alpha and beta parameters:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.utils import compute_time_edges >>> from gwtransport.advection import gamma_infiltration_to_extraction >>> >>> # Create input data with aligned time edges >>> dates = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> tedges = compute_time_edges( ... tedges=None, tstart=None, tend=dates, number_of_bins=len(dates) ... ) >>> >>> # Create output time edges (can be different alignment) >>> cout_dates = pd.date_range(start="2020-01-05", end="2020-01-15", freq="D") >>> cout_tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cout_dates, number_of_bins=len(cout_dates) ... ) >>> >>> # Input concentration and flow (same length, aligned with tedges) >>> cin = pd.Series(np.ones(len(dates)), index=dates) >>> flow = pd.Series(np.ones(len(dates)) * 100, index=dates) # 100 m³/day >>> >>> # Run gamma_infiltration_to_extraction with alpha/beta parameters >>> cout = gamma_infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... alpha=10.0, ... beta=10.0, ... n_bins=5, ... ) >>> cout.shape (11,)
Using mean and std parameters instead:
>>> cout = gamma_infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... mean=100.0, ... std=20.0, ... n_bins=5, ... )
With retardation factor:
>>> cout = gamma_infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... alpha=10.0, ... beta=10.0, ... retardation_factor=2.0, # Doubles residence time ... )
- gwtransport.advection.gamma_extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, retardation_factor=1.0, regularization_strength=1e-10, spinup='constant')[source]#
Compute the concentration of the infiltrating water from extracted water (deconvolution).
The compound is retarded in the aquifer with a retardation factor. The residence time is computed based on the flow rate of the water in the aquifer and the pore volume of the aquifer. The aquifer pore volume is approximated by a (shifted) gamma distribution parameterized by either (mean, std, loc) or (alpha, beta, loc).
This function inverts the forward flow-weighted averaging (deconvolution). It is symmetric to gamma_infiltration_to_extraction.
Provide either (mean, std) or (alpha, beta);
locis optional and defaults to 0.- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water or temperature of extracted water. The model assumes this value is constant over each interval[cout_tedges[i], cout_tedges[i+1]).flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Has a length of one more than flow.cout_tedges (
DatetimeIndex) – Time edges for the cout data. Has a length of one more than cout.mean (
float|None, default:None) – Mean of the gamma distribution of the aquifer pore volume. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution of the aquifer pore volume (invariant under thelocshift).loc (
float, default:0.0) – Location (minimum pore volume) of the gamma distribution. Must satisfy0 <= loc < mean. Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution of the aquifer pore volume (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution of the aquifer pore volume (must be > 0).n_bins (
int, default:100) – Number of bins to discretize the gamma distribution. Default 100.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption/interaction.regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter λ. Seeextraction_to_infiltration()for details. Default is 1e-10.spinup (
str|float|None, default:'constant') – Forwarded toextraction_to_infiltration(). Default"constant"warm-starts the system beforetedges[0].
- Returns:
Concentration of the compound in the infiltrating water, or temperature. Same units as cout.
- Return type:
GenericAlias[floating]
See also
extraction_to_infiltrationDeconvolution with explicit pore volume distribution
gamma_infiltration_to_extractionForward operation (flow-weighted averaging)
gwtransport.gamma.binsCreate gamma distribution bins
gwtransport.diffusion.extraction_to_infiltrationDeconvolution with microdispersion and molecular diffusion
- Gamma Distribution Model
Two-parameter pore volume model
- 8. Gamma Distribution Adequacy
When gamma distribution is adequate
Notes
The APVD is only time-invariant under the steady-streamlines assumption (see 2. Steady Streamlines).
The spreading from the gamma-distributed pore volumes represents macrodispersion (aquifer-scale heterogeneity). When
stdcomes from calibration on measurements, it absorbs all mixing: macrodispersion, microdispersion, and an average molecular diffusion contribution. When calibrating with the diffusion module, these three components are taken into account separately. Whenstdcomes from streamline analysis, it represents macrodispersion only; microdispersion and molecular diffusion can be added viagwtransport.diffusion_fastorgwtransport.diffusion.For cross-compound prediction (calibrating on temperature and predicting a solute), calibrate with
gwtransport.diffusion_fastso the three contributions are tracked separately rather than lumped into a single calibratedstd. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for background.Examples
Basic usage with alpha and beta parameters:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.utils import compute_time_edges >>> from gwtransport.advection import gamma_extraction_to_infiltration >>> >>> # Create cin/flow time edges >>> cin_dates = pd.date_range(start="2019-12-25", end="2020-01-15", freq="D") >>> tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cin_dates, number_of_bins=len(cin_dates) ... ) >>> >>> # Create cout time edges >>> cout_dates = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cout_dates, number_of_bins=len(cout_dates) ... ) >>> >>> # Input concentration and flow >>> cout = np.ones(len(cout_dates)) >>> flow = np.ones(len(cin_dates)) * 100 # 100 m³/day >>> >>> # Run gamma_extraction_to_infiltration with alpha/beta parameters >>> cin = gamma_extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... alpha=10.0, ... beta=10.0, ... n_bins=5, ... ) >>> cin.shape (22,)
Using mean and std parameters instead:
>>> cin = gamma_extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... mean=100.0, ... std=20.0, ... n_bins=5, ... )
With retardation factor:
>>> cin = gamma_extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... alpha=10.0, ... beta=10.0, ... retardation_factor=2.0, # Doubles residence time ... )
- gwtransport.advection.infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, aquifer_pore_volumes, retardation_factor=1.0, spinup='constant')[source]#
Compute the concentration of the extracted water using flow-weighted advection.
This function implements an infiltration to extraction advection model where cin and flow values correspond to the same aligned time bins defined by tedges.
Pure advection is volume-stationary, so the weights are built on the cumulative-throughflow-volume axis rather than by inverting residence times:
Map the cin and cout time edges to cumulative throughflow volume.
Back-project each cout bin by every retarded pore volume to its infiltration-time source window. The window spans one cout bin’s worth of volume, so it overlaps only a narrow band of cin bins.
Compute the flow-weighted time overlap of each window with those cin bins, normalize per streamtube (each row sums to 1), and average over the streamtubes whose source window lies fully inside the cin range.
- Parameters:
cin (
ArrayLike) – Concentration values of infiltrating water or temperature [concentration units]. Length must match the number of time bins defined by tedges. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).flow (
ArrayLike) – Flow rate values in the aquifer [m³/day]. Length must match cin and the number of time bins defined by tedges. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time edges defining bins for both cin and flow data. Has length of len(cin) + 1 and len(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Has length of desired output + 1. Can have different time alignment and resolution than tedges.aquifer_pore_volumes (
ArrayLike) – Array of aquifer pore volumes [m³] representing the distribution of residence times in the aquifer system.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption/interaction.spinup (
str|float|None, default:'constant') –How to treat cout bins where one or more streamtube source windows fall outside the cin time range. Default is
"constant"."constant"— warm-start: shifttedges[0]backward byretardation_factor * max(aquifer_pore_volumes) / flow[0]and treat cin and flow as constant at their first value over the extended window. The forward strict-validity logic then has no NaN cout bins from spin-up; right-edge spin-up (cout extending past the cin range) is unchanged.None— strict mass-conservation: NaN whenever any streamtube has not fully broken through into the cin range, or extraction flow during the bin is zero. Bundle row sums to 1 across cin.float in [0, 1] — fraction threshold: emit cout when at least
spinup * n_pvstreamtubes have contributed; the bundle is then a count-mean over the contributing subset. Warning: this conserves mass per row but NOT cin → cout mass; with a delta cin pulse andspinup=0.0you reproduce the issue #161 over-attribution (Σ cout > Σ cin).
- Returns:
Flow-weighted concentration in the extracted water. Same units as cin. Length equals
len(cout_tedges) - 1. NaN values mark cout bins where the chosenspinuppolicy is not satisfied: the default"constant"leaves NaN for any cout bin extending past the end of the flow record (a cout edge beyondtedges[-1], whose back-projected source window leaves the cin range) and for zero-throughflow bins;spinup=Noneadditionally NaNs left-edge spin-up bins; a float threshold relaxes either case in exchange for non-mass-conserving count-mean output.- Return type:
GenericAlias[floating]- Raises:
ValueError – If tedges length doesn’t match cin/flow arrays plus one, or if infiltration time edges become non-monotonic (invalid input conditions).
See also
gamma_infiltration_to_extractionTransport with gamma-distributed pore volumes
extraction_to_infiltrationReverse operation (deconvolution)
gwtransport.residence_time.fullCompute residence times from flow and pore volume
gwtransport.residence_time.freundlich_retardationCompute concentration-dependent retardation
- The Central Concept: Pore Volume Distribution
Background on aquifer heterogeneity modeling
- Core Transport Equation
Flow-weighted averaging approach
Examples
Basic usage with pandas Series:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.utils import compute_time_edges >>> from gwtransport.advection import infiltration_to_extraction >>> >>> # Create input data >>> dates = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> tedges = compute_time_edges( ... tedges=None, tstart=None, tend=dates, number_of_bins=len(dates) ... ) >>> >>> # Create output time edges (different alignment) >>> cout_dates = pd.date_range(start="2020-01-05", end="2020-01-15", freq="D") >>> cout_tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cout_dates, number_of_bins=len(cout_dates) ... ) >>> >>> # Input concentration and flow >>> cin = pd.Series(np.ones(len(dates)), index=dates) >>> flow = pd.Series(np.ones(len(dates)) * 100, index=dates) # 100 m³/day >>> >>> # Define distribution of aquifer pore volumes >>> aquifer_pore_volumes = np.array([50, 100, 200]) # m³ >>> >>> # Run infiltration_to_extraction >>> cout = infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... ) >>> cout.shape (11,)
Using array inputs instead of pandas Series:
>>> # Convert to arrays >>> cin_values = cin.values >>> flow_values = flow.values >>> >>> cout = infiltration_to_extraction( ... cin=cin_values, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... )
With constant retardation factor (linear sorption):
>>> cout = infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... retardation_factor=2.0, # Compound moves twice as slowly ... )
Note: For concentration-dependent retardation (nonlinear sorption), use infiltration_to_extraction_nonlinear_sorption instead, as this function only supports constant (float) retardation factors.
Using single pore volume:
>>> single_volume = np.array([100]) # Single 100 m³ pore volume >>> cout = infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=single_volume, ... )
- gwtransport.advection.extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, aquifer_pore_volumes, retardation_factor=1.0, regularization_strength=1e-10, spinup='constant')[source]#
Compute the concentration of the infiltrating water from extracted water (deconvolution).
Inverts the forward transport model by solving the linear system
W_forward @ cin = coutwhereW_forwardis the weight matrix frominfiltration_to_extraction(). Uses Tikhonov regularization to smoothly blend data fitting with a physically motivated target (transpose-and-normalize of the forward matrix).Well-determined modes (large singular values relative to √λ) are dominated by the data; poorly-determined modes are pulled toward the target. This avoids edge oscillations and is less sensitive to the regularization parameter than truncated SVD (
rcond).- Parameters:
cout (
ArrayLike) – Concentration values of extracted water [concentration units]. Length must match the number of time bins defined by cout_tedges. The model assumes this value is constant over each interval[cout_tedges[i], cout_tedges[i+1]).flow (
ArrayLike) – Flow rate values in the aquifer [m³/day]. Length must match the number of time bins defined by tedges. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time edges defining bins for both cin (output) and flow data. Has length of len(flow) + 1. Output cin has length len(tedges) - 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Has length of len(cout) + 1. Can have different time alignment and resolution than tedges.aquifer_pore_volumes (
ArrayLike) – Array of aquifer pore volumes [m³] representing the distribution of residence times in the aquifer system.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption/interaction.regularization_strength (
float, default:1e-10) –Tikhonov regularization parameter λ. Controls the tradeoff between fitting the data (
||W cin - cout||²) and staying close to the regularization target (λ ||cin - cin_target||²). The target is the transpose-and-normalize of the forward matrix applied to cout.Larger values trust the target more (smoother, more biased); smaller values trust the data more (noisier, less biased). The solution varies continuously with λ. Default is 1e-10.
A good starting value for noisy data is
λ ≈ (noise_std / signal_amplitude)². For example, temperature data with 0.05 °C noise and ~10 °C seasonal amplitude suggestsregularization_strength ≈ (0.05 / 10)² ≈ 2.5e-5. Increase by a factor of 2-10 for additional smoothing. For noiseless synthetic data (e.g., roundtrip tests), the default 1e-10 preserves machine precision.spinup (
str|float|None, default:'constant') – Spin-up policy applied when building the forward weight matrix used to set up the inverse problem. Same semantics as ininfiltration_to_extraction(); default"constant"shiftstedges[0]backward byretardation_factor * max(aquifer_pore_volumes) / flow[0]so the inverse problem has no spin-up zero-rows for cout bins inside the original tedges range. The warm-start prefix is solved for internally but dropped before returning, so the output cin stays aligned with the user-providedtedges(lengthlen(tedges) - 1), not the padded grid. PassingNonekeeps the strict-validity behavior (zero-rows in W from incomplete breakthrough).
- Returns:
Concentration in the infiltrating water. Same units as cout. Length equals len(tedges) - 1 (unchanged whether or not
spinup="constant"shiftedtedges[0]). NaN values indicate cin bins with no temporal overlap with the extraction data. The forward weight matrix used to set up the inverse problem treats spin-up and zero-flow cout bins as zero-rows according to thespinuppolicy.- Return type:
GenericAlias[floating]- Raises:
ValueError – If tedges length doesn’t match flow plus one, if cout_tedges length doesn’t match cout plus one, or if inputs contain NaN.
See also
gamma_extraction_to_infiltrationDeconvolution with gamma-distributed pore volumes
infiltration_to_extractionForward operation (flow-weighted averaging)
gwtransport.residence_time.fullCompute residence times from flow and pore volume
gwtransport.utils.solve_tikhonovSolver used for inversion
- The Central Concept: Pore Volume Distribution
Background on aquifer heterogeneity modeling
- Core Transport Equation
Flow-weighted averaging approach
Notes
NaN values in
coutare rejected. The Tikhonov solver here does not mask NaN rows, so any NaN incoutwould poison the solution. This differs fromgwtransport.deposition.extraction_to_deposition(), whose regularized solver excludes NaNcoutrows by construction.Examples
Basic usage with pandas Series:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.utils import compute_time_edges >>> from gwtransport.advection import extraction_to_infiltration >>> >>> # Create cin/flow time edges >>> cin_dates = pd.date_range(start="2019-12-25", end="2020-01-15", freq="D") >>> tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cin_dates, number_of_bins=len(cin_dates) ... ) >>> >>> # Create cout time edges >>> cout_dates = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = compute_time_edges( ... tedges=None, tstart=None, tend=cout_dates, number_of_bins=len(cout_dates) ... ) >>> >>> # Input concentration and flow >>> cout = np.ones(len(cout_dates)) >>> flow = np.ones(len(cin_dates)) * 100 # 100 m³/day >>> >>> # Define distribution of aquifer pore volumes >>> aquifer_pore_volumes = np.array([50, 100, 200]) # m³ >>> >>> # Run extraction_to_infiltration >>> cin = extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... ) >>> cin.shape (22,)
Round-trip reconstruction (symmetric with infiltration_to_extraction). The default
spinup="constant"warm-starts the left edge; the cout window must therefore stay inside the cin window with margin matching the longest residence time on the right (forward NaN at the right edge would otherwise be rejected byextraction_to_infiltration):>>> from gwtransport.advection import infiltration_to_extraction >>> rt_cout_dates = pd.date_range(start="2020-01-01", end="2020-01-10", freq="D") >>> rt_cout_tedges = compute_time_edges( ... tedges=None, ... tstart=None, ... tend=rt_cout_dates, ... number_of_bins=len(rt_cout_dates), ... ) >>> cin_original = np.sin(np.linspace(0, 2 * np.pi, len(cin_dates))) + 2 >>> cout_rt = infiltration_to_extraction( ... cin=cin_original, ... flow=flow, ... tedges=tedges, ... cout_tedges=rt_cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... ) >>> cin_recovered = extraction_to_infiltration( ... cout=cout_rt, ... flow=flow, ... tedges=tedges, ... cout_tedges=rt_cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... )
- gwtransport.advection.infiltration_to_extraction_nonlinear_sorption(*, cin, flow, tedges, cout_tedges, aquifer_pore_volumes, freundlich_k=None, freundlich_n=None, bulk_density=None, porosity=None, retardation_factor=None, langmuir_s_max=None, langmuir_k_l=None, max_iterations=10000)[source]#
Compute extracted concentration with complete diagnostic information.
Returns both bin-averaged concentrations and detailed simulation structure for each pore volume.
Exactly one sorption model must be specified:
retardation_factorfor constant (linear) retardation.freundlich_k+freundlich_n+bulk_density+porosityfor Freundlich isotherm.langmuir_s_max+langmuir_k_l+bulk_density+porosityfor Langmuir isotherm.
- Parameters:
cin (
ArrayLike) – Infiltration concentration [mg/L or any units]. Length = len(tedges) - 1. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).flow (
ArrayLike) – Flow rate [m³/day]. Must be non-negative. Length = len(tedges) - 1. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time bin edges. Length = len(cin) + 1.cout_tedges (
DatetimeIndex) – Output time bin edges. Can be different from tedges. Length = number of output bins + 1 (n+1 edges for n output values).aquifer_pore_volumes (
ArrayLike) – Array of aquifer pore volumes [m³] representing the distribution of residence times in the aquifer system. Each pore volume must be positive.freundlich_k (
float|None, default:None) – Freundlich coefficient [(m³/kg)^(1/n)]. Must be positive. Same convention (isotherms = k_f * C^(1/n)) asgwtransport.residence_time.freundlich_retardation().freundlich_n (
float|None, default:None) – Freundlich exponent [-]. Must be positive and != 1.bulk_density (
float|None, default:None) – Bulk density [kg/m³]. Must be positive. Shared by Freundlich and Langmuir models.porosity (
float|None, default:None) – Porosity [-]. Must be in (0, 1). Shared by Freundlich and Langmuir models.retardation_factor (
float|None, default:None) – Constant retardation factor [-]. Must be >= 1.0.langmuir_s_max (
float|None, default:None) – Langmuir maximum sorption capacity [mg/kg]. Must be positive.langmuir_k_l (
float|None, default:None) – Langmuir half-saturation constant [mg/L]. Must be positive.max_iterations (
int, default:10000) – Maximum number of events. Default 10000.
- Return type:
- Returns:
cout (
numpy.ndarray) – Flow-weighted concentrations averaged across all pore volumes. Output bins whose source window leaves the inlet flow record (e.g. cout bins before first breakthrough, or extending past the flow record) are returned as0.0, not NaN; the front-tracking solver clamps such out-of-range windows to the last known state rather than masking them. An output bin with zero throughflow (every overlapping input bin has zero flow) has an undefined flow-weighted average and is returned as NaN, matchinginfiltration_to_extraction().structures (
listofdict) – List of detailed simulation structures, one for each pore volume, with keys:’waves’: List[Wave] - All wave objects created during simulation
’events’: List[dict] - All events; each record carries
"theta"(m³) and"type". Translate to user-facing time t viatracker_state.t_at_theta(event["theta"])if needed.’theta_first_arrival’: float - Cumulative flow at first nonzero arrival [m³]
’n_events’: int - Total number of events
’n_shocks’: int - Number of shocks created
’n_rarefactions’: int - Number of rarefactions created
’n_characteristics’: int - Number of characteristics created
’theta_current’: float - Final simulation cumulative flow [m³]
’sorption’: SorptionModel - Sorption object
’tracker_state’: FrontTrackerState - Complete simulation state
’aquifer_pore_volume’: float - Pore volume for this simulation
See also
infiltration_to_extractionConvolution-based approach for linear retardation
gamma_infiltration_to_extractionFor distributions of pore volumes
- Non-Linear Sorption: Exact Solutions
Freundlich isotherm and front-tracking theory
- 1. Advection-Dominated Transport
When diffusion/dispersion is negligible
Examples
cout, structures = infiltration_to_extraction_nonlinear_sorption( cin=cin, flow=flow, tedges=tedges, cout_tedges=cout_tedges, aquifer_pore_volumes=np.array([500.0]), freundlich_k=0.01, freundlich_n=2.0, bulk_density=1500.0, porosity=0.3, ) # Access spin-up period for first pore volume theta_first = structures[0]["theta_first_arrival"] t_first = structures[0]["tracker_state"].t_at_theta(theta_first) print(f"First arrival: θ={theta_first:.2f} m³ (t={t_first:.2f} days)") # Analyze events for first pore volume for event in structures[0]["events"]: print(f"θ={event['theta']:.2f}: {event['type']}")
deposition#
Deposition Analysis for 1D Aquifer Systems.
Areal deposition supplies mass to the groundwater, mixed instantaneously over the height of the aquifer. The aquifer has a constant thickness with a finite pore volume; water with zero concentration infiltrates at one end and is extracted at the other, whether the flow is radial or orthogonal. Transport is 1D advection with linear sorption; there is no microdispersion, molecular diffusion, or macrodispersion. Forward and backward modeling are supported.
The model is a source term (positive deposition adds mass to the water); it does NOT model removal processes such as pathogen attachment, particle filtration, or chemical precipitation, which would remove mass from the water and require the opposite sign convention.
Available functions:
deposition_to_extraction()- Compute concentrations from deposition rates (convolution). Given deposition rate time series [g/m²/day], computes resulting concentration changes in extracted water [g/m³]. The areal deposition flux is mixed instantaneously over the aquifer thickness, so a parcel’s concentration gain is proportional to its residence time. Accounts for aquifer geometry (porosity, thickness) and residence time distribution.extraction_to_deposition()- Compute deposition rates from concentration changes (deconvolution). Given concentration change time series in extracted water [g/m³], estimates deposition rate history [g/m²/day] that produced those changes. Uses Tikhonov regularization toward a physically motivated target (transpose-and-normalize of the forward matrix). Handles NaN values in concentration data by excluding corresponding time periods.extraction_to_deposition_full()- Full-featured inverse solver exposing all options of the nullspace-based solver (solve_underdetermined_system()). Allows choosing between different nullspace objectives ('squared_differences','summed_differences', or custom callables) and optimization methods.compute_deposition_weights()- Build the banded weight operator relating deposition rates to concentration changes in a compact banded layout. Useful for custom inverse solvers. Used by deposition_to_extraction (forward), extraction_to_deposition (reverse), and extraction_to_deposition_full. Each weight is a water parcel’s residence-time contribution to its concentration gain under areal deposition mixed over the aquifer thickness, independent of whether the flow geometry is radial or orthogonal.spinup_duration()- Compute spinup duration for deposition modeling. Returns the earliest extraction time at which the extracted water was infiltrated at the start of the flow series (equivalently, the time at which cumulative flow first reachesretardation_factor * aquifer_pore_volume). Before this duration the extracted concentration lacks complete deposition history. Useful for determining the valid analysis period and identifying when boundary effects are negligible.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.deposition.compute_deposition_weights(*, flow, tedges, cout_tedges, aquifer_pore_volume, porosity, thickness, retardation_factor=1.0)[source]#
Build the deposition weight operator in a compact banded layout.
Row
kof the dense(n_cout, n_cin)operator isband_vals[k]placed at columns[col_start[k], col_start[k] + full_band). The operator is genuinely banded – rowkis nonzero only on the cin bins whose cumulative through-flow volume lies in the residence-time window[min(start_vol_k, start_vol_{k+1}), max(start_vol_k, start_vol_{k+1}) + R * aquifer_pore_volume]– so each band has at mostfull_bandslots, bounded byR * aquifer_pore_volumein volume (independent of record lengthn_cin). The window is located bynumpy.searchsorted()on the cumulative flow volumeflow_cum; the per-cell math reusesgwtransport.deposition_utils._clipped_linear_integralrestricted to the band columns, so each row sums tor_k = residence_time_k / (retardation_factor * porosity * thickness). Reconstruct the dense(n_cout, n_cin)matrix withgwtransport.advection_utils._densify_weightswhen a dense operator is required (the nullspace inverse).- Parameters:
flow (
ArrayLike) – Flow rates in aquifer [m³/day]. Length must equallen(tedges) - 1.tedges (
DatetimeIndex) – Time bin edges for flow data.cout_tedges (
DatetimeIndex) – Time bin edges for output concentration data.aquifer_pore_volume (
float) – Aquifer pore volume [m³].porosity (
float) – Aquifer porosity [dimensionless].thickness (
float) – Aquifer thickness [m].retardation_factor (
float, default:1.0) – Compound retardation factor, by default 1.0.
- Return type:
tuple[GenericAlias[floating],GenericAlias[int_],GenericAlias[bool],GenericAlias[bool]]- Returns:
band_vals (
numpy.ndarray) – Banded weights of shape(n_cout, full_band). Slotband_vals[k, b]is the weight on cin bincol_start[k] + b. Rowksums tor_k = residence_time_k / (retardation_factor * porosity * thickness); invalid rows (NaN residence time, zero-flow cout bins) are zero.col_start (
numpy.ndarrayofint) – First cin bin index of each cout row’s band, shape(n_cout,).row_valid (
numpy.ndarrayofbool) – True for cout bins whose residence-time window is fully defined and carries flow (the finite, nonzero rows), shape(n_cout,).spinup_row (
numpy.ndarrayofbool) – True for cout bins whose residence time is undefined (spin-up period), shape(n_cout,). These rows carry an all-zero band; the forward path returns NaN for these bins (distinct from zero-flow cout bins, which return 0).
See also
gwtransport.advection_utils._densify_weights: Reconstruct the dense matrix.
- gwtransport.deposition.deposition_to_extraction(*, dep, flow, tedges, cout_tedges, aquifer_pore_volume, porosity, thickness, retardation_factor=1.0, spinup='constant')[source]#
Compute concentrations from deposition rates (convolution).
- Parameters:
dep (
ArrayLike) – Deposition rates [g/m²/day]. Length must equal len(tedges) - 1.flow (
ArrayLike) – Flow rates in aquifer [m³/day]. Length must equal len(tedges) - 1. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex|ndarray) – Time bin edges for deposition and flow data.cout_tedges (
DatetimeIndex|ndarray) – Time bin edges for output concentration data.aquifer_pore_volume (
float) – Aquifer pore volume [m³].porosity (
float) – Aquifer porosity [dimensionless].thickness (
float) – Aquifer thickness [m].retardation_factor (
float, default:1.0) – Compound retardation factor, by default 1.0.spinup (
str|None, default:'constant') – Spin-up policy applied before computing deposition weights. Default"constant"shiftstedges[0]backward byretardation_factor * aquifer_pore_volume / flow[0]and treatsdepandflowas constant at their first observed values over the prepended interval.Nonekeeps the existing strict-validity behavior (NaN cout rows during spin-up). A float raisesNotImplementedError– the fraction-threshold mode is not implemented for deposition (matching the diffusion family).
- Returns:
Concentration changes [g/m³] with length len(cout_tedges) - 1.
Zero-extraction-flow cout bins (no water leaves the aquifer over the bin) return
0.0, not NaN. This deliberately differs from advection, which returns NaN for its undefined zero-flow output: the deposition source term is defined even with no water (an areal flux still supplies mass), and a bin that extracts zero volume carries zero mass, so0.0is the physically correct value rather than an undefined result. NaN is reserved for spin-up bins whose residence time is not yet resolved.- Return type:
GenericAlias[floating]- Raises:
ValueError – If tedges does not have one more element than dep or flow, if input arrays contain NaN values, or if physical parameters are out of valid range (porosity not in (0, 1), non-positive thickness or aquifer pore volume).
NotImplementedError – If
spinupis a float (the fraction-threshold mode is not implemented for deposition).
See also
extraction_to_depositionInverse operation (deconvolution)
spinup_durationEarliest extraction time with a fully resolved deposition history
gwtransport.advection.infiltration_to_extractionFor concentration transport without deposition
- Core Transport Equation
Flow-weighted averaging approach
Notes
This is a source term – positive
depraisescout. Sink processes (pathogen attachment, first-order decay, particle filtration) require the opposite sign convention and are not modelled here.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.deposition import deposition_to_extraction >>> dates = pd.date_range("2020-01-01", "2020-01-10", freq="D") >>> tedges = pd.date_range("2019-12-31 12:00", "2020-01-10 12:00", freq="D") >>> cout_tedges = pd.date_range("2020-01-03 12:00", "2020-01-12 12:00", freq="D") >>> dep = np.ones(len(dates)) >>> flow = np.full(len(dates), 100.0) >>> cout = deposition_to_extraction( ... dep=dep, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volume=500.0, ... porosity=0.3, ... thickness=10.0, ... ) >>> print(f"First finite cout: {cout[np.isfinite(cout)][0]:.4f} g/m³") First finite cout: 1.6667 g/m³
- gwtransport.deposition.extraction_to_deposition(*, cout, flow, tedges, cout_tedges, aquifer_pore_volume, porosity, thickness, retardation_factor=1.0, regularization_strength=1e-10, spinup='constant')[source]#
Compute deposition rates from concentration changes (deconvolution).
Inverts the forward model by solving
W @ dep = coutwhereWis the weight matrix fromcompute_deposition_weights(). Uses Tikhonov regularization to smoothly blend data fitting with a physically motivated target (transpose-and-normalize of the forward matrix).Well-determined modes (large singular values relative to
sqrt(λ)) are dominated by the data; poorly-determined modes are pulled toward the target.- Parameters:
cout (
ArrayLike) – Concentration changes in extracted water [g/m³]. Length must equal len(cout_tedges) - 1. May contain NaN values, which will be excluded from the computation along with corresponding rows in the weight matrix. The model assumes this value is constant over each interval[cout_tedges[i], cout_tedges[i+1]).flow (
ArrayLike) – Flow rates in aquifer [m³/day]. Length must equal len(tedges) - 1. Must not contain NaN values. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex|ndarray) – Time bin edges for deposition and flow data. Length must equal len(flow) + 1.cout_tedges (
DatetimeIndex|ndarray) – Time bin edges for output concentration data. Length must equal len(cout) + 1.aquifer_pore_volume (
float) – Aquifer pore volume [m³].porosity (
float) – Aquifer porosity [dimensionless].thickness (
float) – Aquifer thickness [m].retardation_factor (
float, default:1.0) – Compound retardation factor, by default 1.0. Values > 1.0 indicate slower transport due to sorption/interaction.regularization_strength (
float, default:1e-10) –Tikhonov regularization parameter λ. Controls the tradeoff between fitting the data (
||W dep - cout||²) and staying close to the regularization target (λ ||dep - dep_target||²). The target is the transpose-and-normalize of the forward matrix applied to cout.Larger values trust the target more (smoother, more biased); smaller values trust the data more (noisier, less biased). Default is 1e-10.
spinup (
str|None, default:'constant') – Spin-up policy applied before building the forward weight matrix. Default"constant"shiftstedges[0]backward byretardation_factor * aquifer_pore_volume / flow[0]and treats flow as constant at its first value over the prepended interval; the recovered deposition vector is sliced back to the originaltedgeslength so the public output shape is unchanged.Nonekeeps strict-validity behavior. A float raisesNotImplementedError– the fraction-threshold mode is not implemented for deposition (matching the diffusion family).
- Returns:
Mean deposition rates [g/m²/day] between tedges. Length equals len(tedges) - 1.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If input dimensions are incompatible or if flow contains NaN values.
NotImplementedError – If
spinupis a float (the fraction-threshold mode is not implemented for deposition).
See also
deposition_to_extractionForward operation (convolution)
extraction_to_deposition_fullFull solver with nullspace options
spinup_durationEarliest extraction time with a fully resolved deposition history
gwtransport.advection.extraction_to_infiltrationFor concentration transport without deposition
gwtransport.utils.solve_inverse_transport_bandedBanded Tikhonov solver used for inversion
- Core Transport Equation
Flow-weighted averaging approach
Notes
This is a source term – positive
depraisescout. Sink processes (pathogen attachment, first-order decay, particle filtration) require the opposite sign convention and are not modelled here.The forward model is
W @ dep = cout, where the weight matrixWencodes the physical relationship between deposition rates and concentrations.Wis genuinely banded – rowiis nonzero only on the cin bins inside its residence-time window – and is built and solved in a compact banded layout (peak memoryO(n_cin * band), never the denseO(n_cout * n_cin)). Unlike advection (where rows sum to ~1), deposition rows sum tor_i = residence_time_i / (retardation_factor * porosity * thickness). Rows are rescaled byr_ibefore solving: for systems wherecoutlies in the column space ofWthis preserves the exactdep, while for overdetermined systems with noise it is equivalent to weighted least squares with weights1 / r_i^2(shorter residence times get more weight; under constant flow allr_iare equal and this reduces to OLS). The rescaling puts the regularization target (transpose-and-normalize ofWapplied tocout) on the same scale asdep, which controls the regularization scale. Rows where the residence time cannot be computed (spin-up period) and zero-flow cout bins are excluded automatically; NaN values incoutare also excluded. The banded Tikhonov solve stays well-defined viaregularization_strengtheven whenWis rank-deficient (constant flow with integerRT/dtmakes it a uniform moving average with exact transfer-function zeros), so no rank-deficiency warning is emitted.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.deposition import extraction_to_deposition >>> >>> dates = pd.date_range("2020-01-01", "2020-01-10", freq="D") >>> tedges = pd.date_range("2019-12-31 12:00", "2020-01-10 12:00", freq="D") >>> cout_tedges = pd.date_range("2020-01-03 12:00", "2020-01-12 12:00", freq="D") >>> >>> flow = np.full(len(dates), 100.0) # m³/day >>> cout = np.ones(len(cout_tedges) - 1) * 10.0 # g/m³ >>> >>> dep = extraction_to_deposition( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volume=500.0, ... porosity=0.3, ... thickness=10.0, ... ) >>> print(f"Deposition rates shape: {dep.shape}") Deposition rates shape: (10,) >>> print(f"Mean deposition rate: {np.nanmean(dep):.2f} g/m²/day") Mean deposition rate: 6.00 g/m²/day
- gwtransport.deposition.extraction_to_deposition_full(*, cout, flow, tedges, cout_tedges, aquifer_pore_volume, porosity, thickness, retardation_factor=1.0, nullspace_objective='squared_differences', optimization_method='BFGS', rcond=None, spinup='constant')[source]#
Compute deposition rates from concentration changes using nullspace solver.
Full-featured inverse solver exposing all options of
solve_underdetermined_system(). For most use cases, preferextraction_to_deposition()which uses Tikhonov regularization.- Parameters:
cout (
ArrayLike) – Concentration changes in extracted water [g/m³]. Length must equal len(cout_tedges) - 1. May contain NaN values, which will be excluded from the computation along with corresponding rows in the weight matrix.flow (
ArrayLike) – Flow rates in aquifer [m³/day]. Length must equal len(tedges) - 1. Must not contain NaN values.tedges (
DatetimeIndex|ndarray) – Time bin edges for deposition and flow data. Length must equal len(flow) + 1.cout_tedges (
DatetimeIndex|ndarray) – Time bin edges for output concentration data. Length must equal len(cout) + 1.aquifer_pore_volume (
float) – Aquifer pore volume [m³].porosity (
float) – Aquifer porosity [dimensionless].thickness (
float) – Aquifer thickness [m].retardation_factor (
float, default:1.0) – Compound retardation factor, by default 1.0.nullspace_objective (
str|Callable, default:'squared_differences') –Objective function to minimize in the nullspace. Options:
"squared_differences": Minimize sum of squared differences between adjacent deposition rates (default, smooth solutions)."summed_differences": Minimize sum of absolute differences (sparse/piecewise constant solutions).callable : Custom objective
f(coeffs, x_ls, nullspace_basis).
optimization_method (
str, default:'BFGS') – Scipy optimization method. Default is"BFGS".rcond (
float|None, default:None) – Cutoff for small singular values in the least-squares step. Default is None (uses numpy default).spinup (
str|None, default:'constant') – Spin-up policy applied before building the forward weight matrix. Default"constant"shiftstedges[0]backward byretardation_factor * aquifer_pore_volume / flow[0]; the recovered deposition is sliced back to the originaltedgeslength.Nonekeeps strict-validity behavior. A float raisesNotImplementedError– the fraction-threshold mode is not implemented for deposition (matching the diffusion family). Seeextraction_to_deposition()for full semantics.
- Returns:
Mean deposition rates [g/m²/day] between tedges. Length equals len(tedges) - 1.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If cout_tedges does not have one more element than cout, if tedges does not have one more element than flow, if flow contains NaN values, or if physical parameters are out of valid range (porosity not in (0, 1), non-positive thickness or aquifer pore volume).
NotImplementedError – If
spinupis a float (the fraction-threshold mode is not implemented for deposition).
See also
extraction_to_depositionRecommended solver using Tikhonov regularization.
spinup_durationEarliest extraction time with a fully resolved deposition history.
gwtransport.utils.solve_underdetermined_systemUnderlying solver.
Notes
This is a source term – positive
depraisescout. Sink processes (pathogen attachment, first-order decay, particle filtration) require the opposite sign convention and are not modelled here.
- gwtransport.deposition.spinup_duration(*, flow, tedges, aquifer_pore_volume, retardation_factor=1.0)[source]#
Compute the spinup duration for deposition modeling.
The spinup duration is the smallest extraction time
t*(relative totedges[0]) at which the extracted water was infiltrated exactly attedges[0]: equivalently, the time at which the cumulative flow first reachesretardation_factor * aquifer_pore_volume. For extraction times earlier thant*the extracted concentration lacks complete deposition history. Under constant flow this equalsaquifer_pore_volume * retardation_factor / flow.- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for the flow data.aquifer_pore_volume (
float) – Pore volume of the aquifer [m³].retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless], by default 1.0.
- Returns:
Spinup duration in days.
- Return type:
- Raises:
ValueError – If the cumulative flow over the entire
tedgeswindow does not reachretardation_factor * aquifer_pore_volume, indicating the flow timeseries is too short to characterise the spin-up duration.
See also
deposition_to_extractionForward solver that uses the spin-up duration to resolve NaN cout rows.
extraction_to_depositionInverse solver.
diffusion_fast#
Fast closed-form 1D advection-dispersion transport (Kreft-Zuber flux concentration).
This module shares the conceptual model of gwtransport.diffusion – advection with
microdispersion (alpha_L) and molecular diffusion (D_m) along orthogonal (Cartesian)
flow paths, one independent streamtube per aquifer pore volume, with the spread across the
pore volume distribution providing macrodispersion and linear sorption entering through the
retardation factor. It computes the
same physics as gwtransport.diffusion – the Kreft-Zuber (1978) flux concentration
C_F at the outlet of the streamtube bundle – but evaluates the bin-averaged breakthrough
in closed form instead of by Gauss-Legendre quadrature: a faster but still exact
implementation.
For each streamtube (one aquifer pore volume) the resident concentration in moving-frame
cumulative-volume (V) coordinates is the Gaussian CDF
C_R = 0.5 * erfc((L - xi) / (2 * sqrt(D_t))), with D_t = D_m * tau + alpha_L * xi
the moving-frame dispersion product. Its bin-average over a cout bin has the closed-form
antiderivative I(x) = 0.5*x + 0.5*[x*erf(x/s) + (s/sqrt(pi))*exp(-(x/s)^2)],
s = 2*sqrt(D_t). Evaluating I once per cout edge with D_t carried per edge
and differencing yields the flux concentration C_F directly – not merely C_R –
because dD_t/dx = D_m/v_s + alpha_L = D_s/v_s is exactly the Kreft-Zuber flux coefficient
at the solute-front velocity v_s = Q*L/(R*V_pore) (using d(tau)/dx = 1/v_s with
tau = R*V/(L*Q)). The dispersive boundary-flux correction therefore emerges from the
D_t variation across the bin; no explicit correction term is added.
The elapsed time tau and travel distance xi are read directly from the time and
cumulative-volume edges (tau_ij = t_cout_i - t_cin_j, xi geometric), so no per-cell
quadrature and no residence-time inversion is needed. The result reproduces
gwtransport.diffusion to machine precision when the cout grid aligns with the flow
grid (supply flow_out on the output grid). The coefficient matrix is built only on the
breakthrough band – the cumulative-volume band where the bin-averaged C_F is unsaturated,
the only region with non-zero coefficients – so the build cost scales with the band width
(a few percent of the matrix at realistic dispersion) rather than with the full grid.
Streamtube assumption (no cross-sectional area parameter)#
Each entry in aquifer_pore_volumes is an independent 1D streamtube; molecular diffusion
enters the V-space variance through D_m * tau and microdispersion through
alpha_L * xi. streamline_length / molecular_diffusivity /
longitudinal_dispersivity may be a scalar (shared by all streamtubes) or an array with
one value per pore volume, exactly as in gwtransport.diffusion.
When to choose this module vs gwtransport.diffusion#
Both modules implement the same physics (Bear resident concentration + Kreft-Zuber flux
concentration on 1D streamtubes, with retardation and the moving-frame variance
D_t = D_m*tau + alpha_L*xi), and both accept per-streamtube streamline_length /
molecular_diffusivity / longitudinal_dispersivity arrays. Whenever the cout grid is
at or finer than the flow grid, this module reproduces gwtransport.diffusion to
machine precision for every parameter regime – including retardation_factor != 1 with
molecular_diffusivity > 0, where the antiderivative’s slope dD_t/dx = D_s/v_s already
carries the solute-front Kreft-Zuber flux coefficient natively – while being ~80-90x faster
even before banding (closed form, no Gauss-Legendre quadrature, no residence-time inversion),
and the banded build computes only the non-zero breakthrough band – faster still at the
weak-to-moderate dispersion of realistic problems. So it is the right default. The only case that favours
gwtransport.diffusion is a cout grid coarser than the flow detail: this module
treats flow_out as constant within each cout bin, whereas gwtransport.diffusion
integrates the full tedges-resolution flow within each cout bin – a ~0.1%-of-peak
difference for a rapidly-varying cin over wide cout bins under variable flow.
Available functions:
infiltration_to_extraction()– forward transport.extraction_to_infiltration()– inverse via Tikhonov regularization.gamma_infiltration_to_extraction()– gamma-distributed APVD (forward).gamma_extraction_to_infiltration()– same, inverse.
References
Kreft, A., & Zuber, A. (1978). On the physical meaning of the dispersion equation and its solutions for different initial and boundary conditions. Chemical Engineering Science, 33(11), 1471-1480.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.diffusion_fast.infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Compute extracted concentration with advection, microdispersion, and molecular diffusion.
Fast closed-form counterpart of
gwtransport.diffusion.infiltration_to_extraction(). Reports the Kreft-Zuber (1978) flux concentrationC_Fand reproduces the slow module to machine precision when the cout grid aligns with the flow grid (supplyflow_out).- Parameters:
cin (
ArrayLike) – Concentration of the compound in the infiltrating water. Lengthlen(tedges) - 1.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Lengthlen(tedges) - 1.tedges (
DatetimeIndex) – Time edges for cin and flow data. Lengthlen(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Lengthlen(output) + 1.aquifer_pore_volumes (
ArrayLike) – Aquifer pore volumes [m³] – one independent streamtube per entry.streamline_length (
GenericAlias[floating] |float) – Travel distance L [m]: a scalar (shared by all streamtubes) or an array with one value per aquifer pore volume. Must be positive.molecular_diffusivity (
GenericAlias[floating] |float) – Effective molecular diffusivity D_m [m²/day]: scalar or one value per pore volume. Must be non-negative.longitudinal_dispersivity (
GenericAlias[floating] |float) – Longitudinal dispersivity alpha_L [m] (microdispersion): scalar or one value per pore volume. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0). Values > 1.0 indicate slower transport.flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid (aligned tocout_tedges, lengthlen(cout_tedges) - 1); constant within each cout bin, likeflowis within eachtedgesbin. It defines the cout-bin volumes and the outlet velocity. Required when ``cout_tedges`` differs from ``tedges``; may be omitted only whencout_tedgesequalstedges(then it equalsflow). Default None.spinup (
str|None, default:'constant') –"constant"(default) extendstedgesby 100 years on each side so a constant warm-start fills the left-edge spin-up region;Noneleaves spin-up cout as NaN.saturation_threshold (
float, default:7.0) – Breakthrough-band cutoffU(default 7.0). The coefficient matrix is built only on the cumulative-volume band where the breakthrough is unsaturated (|x| < U * 2*sqrt(D_t)), which is the only region with non-zero coefficients.Uaround 7 (any value above ~6) reproduces the full dense build to machine precision; a smaller value narrows the band – faster – at the cost of dropping breakthrough tails of orderexp(-U**2).
- Returns:
Bin-averaged Kreft-Zuber flux concentration
C_Fin the extracted water. Lengthlen(cout_tedges) - 1. NaN where no infiltration data has broken through.- Return type:
GenericAlias[floating]
See also
gwtransport.diffusion.infiltration_to_extractionQuadrature reference; prefer for cout grids coarser than the flow detail.
extraction_to_infiltrationInverse operation.
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion.
- gwtransport.diffusion_fast.extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Reconstruct infiltration concentration from extracted water (deconvolution).
Inverts the forward model by building the same closed-form flux-concentration matrix as
infiltration_to_extraction()and solvingW @ cin = coutvia Tikhonov regularization. Fast closed-form counterpart ofgwtransport.diffusion.extraction_to_infiltration().- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water. Lengthlen(cout_tedges) - 1.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Lengthlen(tedges) - 1.tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Lengthlen(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Lengthlen(cout) + 1.aquifer_pore_volumes (
ArrayLike) – Aquifer pore volumes [m³] – one independent streamtube per entry.streamline_length (
GenericAlias[floating] |float) – Travel distance L [m]: a scalar (shared by all streamtubes) or an array with one value per aquifer pore volume. Must be positive.molecular_diffusivity (
GenericAlias[floating] |float) – Effective molecular diffusivity D_m [m²/day]: scalar or one value per pore volume. Must be non-negative.longitudinal_dispersivity (
GenericAlias[floating] |float) – Longitudinal dispersivity alpha_L [m] (microdispersion): scalar or one value per pore volume. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter (default 1e-10).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid (aligned tocout_tedges). Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged concentration in the infiltrating water. Length
len(tedges) - 1. NaN where no extraction data constrains the bin.- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionForward operation.
gwtransport.diffusion.extraction_to_infiltrationQuadrature reference.
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion.
- gwtransport.diffusion_fast.gamma_infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Compute extracted concentration for a gamma-distributed pore volume distribution.
Convenience wrapper around
infiltration_to_extraction()that discretizes a (shifted) gamma aquifer pore-volume distribution inton_binsequal-probability streamtubes. Provide either (mean, std) or (alpha, beta);locdefaults to 0.- Parameters:
cin (
ArrayLike) – Concentration of the compound in infiltrating water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin and flow data. Lengthlen(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins.mean (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution [m³].std (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution [m³].loc (
float, default:0.0) – Location (minimum pore volume) [m³],0 <= loc < mean. Default 0.0.alpha (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).beta (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).n_bins (
int, default:100) – Number of equal-probability streamtubes. Default 100.streamline_length (
float) – Travel distance L [m], applied to all gamma streamtubes. Must be positive.molecular_diffusivity (
float) – Effective molecular diffusivity D_m [m²/day], applied to all streamtubes. Must be non-negative.longitudinal_dispersivity (
float) – Longitudinal dispersivity alpha_L [m] (microdispersion), applied to all streamtubes. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid. Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged Kreft-Zuber flux concentration
C_Fin the extracted water. Lengthlen(cout_tedges) - 1.- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionTransport with an explicit pore volume distribution.
gamma_extraction_to_infiltrationReverse operation.
gwtransport.gamma.binsCreate gamma distribution bins.
- Gamma Distribution Model
Two-parameter pore volume model.
- gwtransport.diffusion_fast.gamma_extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Reconstruct infiltration concentration for a gamma-distributed pore volume distribution.
Convenience wrapper around
extraction_to_infiltration()that discretizes a (shifted) gamma aquifer pore-volume distribution inton_binsequal-probability streamtubes. Provide either (mean, std) or (alpha, beta);locdefaults to 0.- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Lengthlen(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Lengthlen(cout) + 1.mean (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution [m³].std (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution [m³].loc (
float, default:0.0) – Location (minimum pore volume) [m³],0 <= loc < mean. Default 0.0.alpha (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).beta (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).n_bins (
int, default:100) – Number of equal-probability streamtubes. Default 100.streamline_length (
float) – Travel distance L [m], applied to all gamma streamtubes. Must be positive.molecular_diffusivity (
float) – Effective molecular diffusivity D_m [m²/day], applied to all streamtubes. Must be non-negative.longitudinal_dispersivity (
float) – Longitudinal dispersivity alpha_L [m] (microdispersion), applied to all streamtubes. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter (default 1e-10).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid. Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged concentration in the infiltrating water. Length
len(tedges) - 1.- Return type:
GenericAlias[floating]
See also
extraction_to_infiltrationDeconvolution with an explicit pore volume distribution.
gamma_infiltration_to_extractionForward operation.
gwtransport.gamma.binsCreate gamma distribution bins.
- Gamma Distribution Model
Two-parameter pore volume model.
diffusion_fast_fast#
Fast approximate 1D advection-dispersion transport (Kreft-Zuber flux concentration).
This module shares the conceptual model of gwtransport.diffusion and
gwtransport.diffusion_fast – advection with microdispersion (alpha_L) and molecular
diffusion (D_m) along orthogonal (Cartesian) flow paths, one independent streamtube per aquifer
pore volume, the spread across the pore volume distribution providing macrodispersion, and linear
sorption via the retardation factor. It
targets the bin-averaged Kreft-Zuber (1978) flux concentration C_F on the streamtube bundle, but
trades exactness for a single fast (~1.5 ms) native-grid evaluation that does not depend on the flow
being constant.
It is approximate: where gwtransport.diffusion_fast reproduces the quadrature reference to
machine precision, this module is accurate to ~3e-4 in the common regime and degrades in a
documented corner (below). When you need machine precision, use gwtransport.diffusion_fast.
How it works – an operator split in two coordinates#
The moving-frame dispersion product D_t = D_m*tau + alpha_L*xi mixes a time term (molecular
diffusion D_m*tau) and a volume term (microdispersion alpha_L*xi). The two are split into
the coordinate each is stationary in, so the dominant part is built once and is flow-independent:
Advection + macrodispersion + microdispersion are the exact skewed
D_m=0Kreft-Zuber breakthrough, applied banded on the native cumulative-volume grid. The whole aquifer pore volume distribution (APVD) is pre-summed into a single 1D antiderivativeIbar(dV)– exact for any APVD shape – finely sampled once and read back by interpolation. This part is volume-stationary, hence flow-independent (constant and strongly variable flow alike).Molecular diffusion is a symmetric time-domain Gaussian applied to the outlet signal (variance
2*D_m*tau_bt*(R*Vbar/L)^2/Q^2). This is the only modelling approximation: the true Kreft-Zuber molecular breakthrough is skewed, and at realistic (sub-bin) spreading the Gaussian is nearly a no-op, so the molecular term is dropped rather than skewed.
tedges need not be regularly spaced and cout_tedges need not equal tedges (supply
flow_out when they differ): step 1 runs on the native cumulative-volume grid for any spacing.
Only the molecular Gaussian assumes a roughly regular grid – it convolves in bin-index space using
the mean bin width – so a strongly irregular grid adds a small extra error to the (usually
sub-dominant) molecular term; use gwtransport.diffusion_fast for the molecular-dominated +
irregular-grid corner.
Accuracy (vs gwtransport.diffusion_fast, flow-independent unless noted)#
~3e-4 whenever microdispersion is present (
alpha_L > 0– the typical groundwater regime, Peclet number >> 1), constant and variable flow, for realistic solute diffusivities (D_m~ 1e-4) orR = 1. Here molecular diffusion is sub-dominant, so approximating it barely matters. This survives retardation for a typical APVD (measured <~1e-3 up toR = 3).In the molecular-diffusion-dominated corner (
alpha_L~ 0): ~1e-4 for smooth inputs, but degrading to ~1e-2 for sharp inputs (and ~5e-2 for sharp inputs with a very wide / bimodal APVD or a large single pore volume), because the symmetric time-Gaussian cannot reproduce the skewed molecular breakthrough. Retardation enlarges this corner: the Gaussian’s variance scales assigma_t^2 ~ D_m * R^3, soR > 1reaches the ~1e-2 looseness at a smallerD_m– a sharp input atD_m = 0.01degrades from ~8e-4 atR = 1to ~1.7e-2 atR = 2and ~3.5e-2 atR = 3. Usegwtransport.diffusion_fastfor exact results in this regime – in particular for heat transport (R > 1with a largeD_m).
The inverse (extraction_to_infiltration()) deconvolves the same approximate operator the
forward applies. It assembles W = G . M directly in banded form (one Ibar gather plus a
sparse G . M product – no per-pore-volume closed-form loop, no dense (n_cout, n_cin) matrix)
and solves it with banded Tikhonov regularisation (banded Cholesky, O(n * band**2)), so it is
much faster than gwtransport.diffusion_fast’s reverse, especially for many streamtubes.
Inverting exactly the forward operator makes a round trip self-consistent: it recovers the input up
to the deconvolution conditioning, with the only error being the forward operator’s approximation of
gwtransport.diffusion_fast (use that module when the approximation is unacceptable).
Available functions:
infiltration_to_extraction()– forward transport (approximate).extraction_to_infiltration()– inverse via banded Tikhonov regularisation (approximate).gamma_infiltration_to_extraction()– gamma-distributed APVD (forward).gamma_extraction_to_infiltration()– same, inverse.
References
Kreft, A., & Zuber, A. (1978). On the physical meaning of the dispersion equation and its solutions for different initial and boundary conditions. Chemical Engineering Science, 33(11), 1471-1480.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.diffusion_fast_fast.infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Compute extracted concentration with advection, microdispersion, and molecular diffusion (approximate).
Fast approximate counterpart of
gwtransport.diffusion_fast.infiltration_to_extraction(). The advection + macrodispersion + microdispersion (alpha_L) part is the exact skewedD_m=0Kreft-Zuber breakthrough applied on the native cumulative-volume grid; molecular diffusion (D_m) is a symmetric time-domain Gaussian. The result is flow-independent and accurate to ~3e-4 wheneveralpha_L > 0(the typical regime) for realistic soluteD_m(~1e-4) orR = 1. It loosens to ~1e-2 (sharp inputs) in the molecular-diffusion-dominated corner (alpha_L~ 0), and retardation enlarges that corner because the Gaussian variance grows asD_m * R^3(a sharp input atD_m = 0.01reaches ~1.7e-2 atR = 2and ~3.5e-2 atR = 3). For machine precision – or heat transport withR > 1and largeD_m– usegwtransport.diffusion_fast.- Parameters:
cin (
ArrayLike) – Concentration of the compound in the infiltrating water. Lengthlen(tedges) - 1.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Lengthlen(tedges) - 1.tedges (
DatetimeIndex) – Time edges for cin and flow data. Lengthlen(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Lengthlen(output) + 1.aquifer_pore_volumes (
ArrayLike) – Aquifer pore volumes [m³] – one independent streamtube per entry. Any distribution shape (the APVD is pre-summed exactly).streamline_length (
GenericAlias[floating] |float) – Travel distance L [m]: a scalar (shared by all streamtubes) or an array with one value per aquifer pore volume. Must be positive.molecular_diffusivity (
GenericAlias[floating] |float) – Effective molecular diffusivity D_m [m²/day]: scalar or one value per pore volume. Must be non-negative.longitudinal_dispersivity (
GenericAlias[floating] |float) – Longitudinal dispersivity alpha_L [m] (microdispersion): scalar or one value per pore volume. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0). Values > 1.0 indicate slower transport.flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid (aligned tocout_tedges, lengthlen(cout_tedges) - 1). Required whencout_tedgesdiffers fromtedges; may be omitted only whencout_tedgesequalstedges. Default None.spinup (
str|None, default:'constant') –"constant"(default) extendstedgesby 100 years on each side so a constant warm-start fills the left-edge spin-up region;Noneleaves spin-up cout as NaN.saturation_threshold (
float, default:7.0) – Breakthrough-band cutoffU(default 7.0). Sets how far into the breakthrough tail the banded build reaches; seegwtransport.diffusion_fast.infiltration_to_extraction().
- Returns:
Bin-averaged Kreft-Zuber flux concentration
C_Fin the extracted water. Lengthlen(cout_tedges) - 1. NaN where no infiltration data has broken through.- Return type:
GenericAlias[floating]
See also
gwtransport.diffusion_fast.infiltration_to_extractionExact (machine-precision) counterpart; use it when approximation is unacceptable, especially in the molecular-dominant regime.
gwtransport.diffusion.infiltration_to_extractionQuadrature reference.
extraction_to_infiltrationInverse operation (deconvolves this same operator).
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion.
- gwtransport.diffusion_fast_fast.extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Reconstruct infiltration concentration from extracted water (fast approximate deconvolution).
Inverts the same approximate operator the forward applies: it assembles
W = G . M(the advection+macro+micro bandMtimes the molecular time-GaussianG) directly in banded form and deconvolves it with banded Tikhonov regularization (_solve_reverse_banded– banded Cholesky on the normal equations,O(n * band**2)). It buildsWfrom oneIbargather plus a sparseG . Mproduct – no per-pore-volume closed-form loop and no dense(n_cout, n_cin)matrix – so it is much faster thangwtransport.diffusion_fast.extraction_to_infiltration()(which evaluates the exact breakthrough per streamtube), especially for many streamtubes. Because the deconvolved operator is exactly the forward operator, a forward-then-inverse round trip recoverscinup to the deconvolution conditioning and regularization; the approximation lives entirely in the forward operator vsgwtransport.diffusion_fast.- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water. Lengthlen(cout_tedges) - 1.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Lengthlen(tedges) - 1.tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Lengthlen(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Lengthlen(cout) + 1.aquifer_pore_volumes (
ArrayLike) – Aquifer pore volumes [m³] – one independent streamtube per entry.streamline_length (
GenericAlias[floating] |float) – Travel distance L [m]: scalar or one value per pore volume. Must be positive.molecular_diffusivity (
GenericAlias[floating] |float) – Effective molecular diffusivity D_m [m²/day]: scalar or one value per pore volume. Must be non-negative.longitudinal_dispersivity (
GenericAlias[floating] |float) – Longitudinal dispersivity alpha_L [m] (microdispersion): scalar or one value per pore volume. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter (default 1e-10). Must be strictly positive: the banded solver relies on it to make the normal equations positive-definite (it cannot return the denselambda = 0minimum-norm solution).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid (aligned tocout_tedges). Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged concentration in the infiltrating water. Length
len(tedges) - 1. NaN where no extraction data constrains the bin.- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionForward operation (the operator inverted here).
gwtransport.diffusion_fast.extraction_to_infiltrationExact (machine-precision) counterpart; use it when the approximation is unacceptable.
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion.
- gwtransport.diffusion_fast_fast.gamma_infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Compute extracted concentration for a gamma-distributed pore volume distribution (approximate).
Convenience wrapper around
infiltration_to_extraction()that discretizes a (shifted) gamma aquifer pore-volume distribution inton_binsequal-probability streamtubes. Provide either (mean, std) or (alpha, beta);locdefaults to 0. Approximate – seeinfiltration_to_extraction().- Parameters:
cin (
ArrayLike) – Concentration of the compound in infiltrating water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin and flow data. Lengthlen(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Lengthlen(result) + 1.mean (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution.std (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution.loc (
float, default:0.0) – Location (minimum pore volume),0 <= loc < mean. Default 0.0.alpha (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).beta (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).n_bins (
int, default:100) – Number of equal-probability streamtubes. Default 100.streamline_length (
float) – Travel distance L [m], applied to all gamma streamtubes. Must be positive.molecular_diffusivity (
float) – Effective molecular diffusivity D_m [m²/day], applied to all streamtubes. Must be non-negative.longitudinal_dispersivity (
float) – Longitudinal dispersivity alpha_L [m] (microdispersion), applied to all streamtubes. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid. Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged Kreft-Zuber flux concentration
C_Fin the extracted water. Lengthlen(cout_tedges) - 1. NaN where no infiltration data has broken through.- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionTransport with an explicit pore volume distribution.
gamma_extraction_to_infiltrationReverse operation.
gwtransport.gamma.binsCreate gamma distribution bins.
- Gamma Distribution Model
Two-parameter pore volume model.
- gwtransport.diffusion_fast_fast.gamma_extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, flow_out=None, spinup='constant', saturation_threshold=7.0)[source]#
Reconstruct infiltration concentration for a gamma-distributed pore volume distribution.
Convenience wrapper around
extraction_to_infiltration()that discretizes a (shifted) gamma aquifer pore-volume distribution inton_binsequal-probability streamtubes. Provide either (mean, std) or (alpha, beta);locdefaults to 0. Fast approximate banded deconvolution (seeextraction_to_infiltration()).- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Lengthlen(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Lengthlen(cout) + 1.mean (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution.std (
float|None, default:None) – Mean and standard deviation of the gamma pore-volume distribution.loc (
float, default:0.0) – Location (minimum pore volume),0 <= loc < mean. Default 0.0.alpha (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).beta (
float|None, default:None) – Shape and scale parameters of the gamma distribution (alternative to mean/std).n_bins (
int, default:100) – Number of equal-probability streamtubes. Default 100.streamline_length (
float) – Travel distance L [m], applied to all gamma streamtubes. Must be positive.molecular_diffusivity (
float) – Effective molecular diffusivity D_m [m²/day], applied to all streamtubes. Must be non-negative.longitudinal_dispersivity (
float) – Longitudinal dispersivity alpha_L [m] (microdispersion), applied to all streamtubes. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0).regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter (default 1e-10).flow_out (
ArrayLike|None, default:None) – Extraction flow rate [m³/day] on the output grid. Seeinfiltration_to_extraction(). Default None.spinup (
str|None, default:'constant') – Seeinfiltration_to_extraction(). Default"constant".saturation_threshold (
float, default:7.0) – Seeinfiltration_to_extraction(). Default 7.0.
- Returns:
Bin-averaged concentration in the infiltrating water. Length
len(tedges) - 1.- Return type:
GenericAlias[floating]
See also
extraction_to_infiltrationDeconvolution with an explicit pore volume distribution.
gamma_infiltration_to_extractionForward operation.
gwtransport.gamma.binsCreate gamma distribution bins.
- Gamma Distribution Model
Two-parameter pore volume model.
diffusion#
Analytical solutions for 1D advection-dispersion transport.
Water infiltrates and is transported in parallel along multiple aquifer pore volumes to extraction. For each aquifer pore volume, transport is 1D advection with microdispersion, molecular diffusion, and linear sorption; the spread across aquifer pore volumes provides macrodispersion. Forward and backward modeling are supported. The flow is assumed orthogonal.
The orthogonal-flow (Cartesian) geometry is what makes the Kreft-Zuber breakthrough the exact 1D solution used below.
Key functions:
infiltration_to_extraction()- Main transport function combining advection, microdispersion, and molecular diffusion with explicit pore volume distribution and streamline lengths.extraction_to_infiltration()- Inverse operation (deconvolution with dispersion).gamma_infiltration_to_extraction()- Gamma-distributed pore volumes with dispersion. Models aquifer heterogeneity with 2-parameter gamma distribution. Parameterizable via (alpha, beta) or (mean, std). Discretizes gamma distribution into equal-probability bins.gamma_extraction_to_infiltration()- Gamma-distributed pore volumes, deconvolution with dispersion. Symmetric inverse of gamma_infiltration_to_extraction.
When to choose this module vs gwtransport.diffusion_fast#
This is the reference implementation: it evaluates the bin-averaged Kreft-Zuber flux
concentration by resolution-aware composite Gauss-Legendre quadrature (splitting at
flow-bin boundaries, with extra front-centred panels wherever a sharp breakthrough front
is otherwise under-resolved).
Prefer it only when the output grid is coarser than the flow detail – it integrates the
full within-bin flow, which the closed-form gwtransport.diffusion_fast approximates as
constant per output bin. Otherwise that module computes the same physics to machine
precision for every parameter regime (including retardation_factor != 1 with non-zero
molecular diffusivity, whose flux correction it also evaluates in closed form) and is
~80-90x faster (no quadrature, no residence-time inversion). Both modules accept
per-streamtube streamline_length / molecular_diffusivity /
longitudinal_dispersivity arrays (heterogeneous flow paths – partially-penetrating
wells, wedge-shaped capture zones).
Reported outlet concentration: Kreft-Zuber (1978) flux concentration#
The outlet concentration reported by this module is the flux concentration
C_F(L, t) = C_R(L, t) - (D_s / v_s) * dC_R/dx |_{x=L}
with the solute-front (retarded-frame) velocity v_s = Q L / (R V_pore) and the dispersion D_s = D_m + alpha_L * v_s, so the flux coefficient is D_s / v_s = D_m / v_s + alpha_L = R D_m / v_fluid + alpha_L (with the fluid velocity v_fluid = Q L / V_pore). The resident profile C_R solves the retarded ADE with advection v_s and dispersion D_s, so its flux-vs-resident correction must use v_s — not v_fluid; pairing v_s with the moving-frame variance below is what conserves mass for R > 1 with D_m > 0.
— the solute mass flux at the outlet divided by the volumetric fluid flux. This
is what is measured when sampling the extracted fluid. The resident
concentration C_R is Bear (1972) eq. 10.6.4, the variable-flow moving-frame
Ogata-Banks solution
C_R(L, V; t_j) = 0.5 * erfc((L - xi_j(V)) / (2 * sqrt(D_t(V))))
with the dispersion variance accumulated in the moving (Lagrangian) frame:
D_t(V) = sigma^2(V) / 2 = D_m * tau(V) + alpha_L * xi(V)
where:
D_m is the effective molecular (or thermal) diffusivity [m²/day]
alpha_L is the longitudinal dispersivity [m]
tau(V) is the elapsed time since infiltration [day], with V the cumulative extracted volume
xi(V) = L (V - V_j) / (R V_pore) is the distance the parcel has actually travelled [m]
The K-Z flux-correction term is what makes the column-sum invariant
integral Q c_out dt = integral Q c_in dt hold under arbitrary variable Q.
Without it, the leading-order C_R loses O(1/Pe) per column under variable Q +
pure D_m (issue #180).
Implementation: the bin-averaged C_F is computed by resolution-aware composite
Gauss-Legendre quadrature in volume space, split at flow-bin boundaries so each
sub-interval sees a linear t(V). Within a sub-interval the erf-like front has
width sqrt(4*D_t) (in volume units); for near-zero dispersivity this can be
orders of magnitude below the flow-bin width, so a single fixed-order rule
cannot resolve it. Sub-intervals whose front is under-resolved are therefore
tiled with front-centred panels (fine near the front, flat tails outside),
which restores the column-mass invariant to ~1e-11 for every dispersion regime;
smooth/already-resolved sub-intervals keep the plain single 16-point rule. The
variance is evaluated at each quadrature node from the parcel’s own tau and xi
histories — never capped at the residence time. The K-Z identity requires
Bear’s formula to satisfy the variable-coefficient ADE exactly, which holds only
when D_t is allowed to keep growing past breakthrough.
Macrodispersion vs microdispersion#
This module adds microdispersion (alpha_L) and molecular diffusion (D_m) on top of macrodispersion captured by the pore volume distribution (APVD). Both represent velocity heterogeneity at different scales. Microdispersion is an aquifer property; macrodispersion depends additionally on hydrological boundary conditions. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for guidance on when to use each approach and how to avoid double-counting spreading effects.
Streamtube assumption (no cross-sectional area parameter)#
Each entry in aquifer_pore_volumes is treated as an independent 1D streamtube. There is
no cross-sectional area parameter: the variance budget uses 2 D_m tau (molecular
diffusion in time) and 2 alpha_L xi (microdispersion in travelled distance), with
the streamline length L and the pore volume V_pore together fixing the implicit
streamtube cross-section A = V_pore / L. Callers who need distributed-area effects must
provide multiple streamtubes (via aquifer_pore_volumes or the gamma-parameterised
wrappers).
References
Bear, J. (1972). Dynamics of Fluids in Porous Media. American Elsevier
Publishing Company. Equation 10.6.4 (variable-flow Ogata-Banks form). Provides
the resident concentration C_R.
Kreft, A., & Zuber, A. (1978). On the physical meaning of the dispersion equation and its solutions for different initial and boundary conditions. Chemical Engineering Science, 33(11), 1471-1480. Eq. 2 gives the resident-to- flux concentration transformation; Eq. 1 is the mass-balance identity that makes the column-sum invariant exact.
- gwtransport.diffusion.infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, spinup='constant')[source]#
Compute extracted concentration with advection, microdispersion, and molecular diffusion.
This function models 1D solute transport through an aquifer system along orthogonal (Cartesian) flow paths. Each aquifer pore volume is an independent streamline carrying advection with microdispersion (alpha_L) and molecular diffusion (D_m); the spread across the pore volume distribution provides macrodispersion. Linear sorption enters via the retardation factor.
The physical model assumes:
Water infiltrates with concentration cin at time t_in
Water travels distance L through aquifer with residence time tau = V_pore / Q
During transport, microdispersion and molecular diffusion spread each streamline, while the spread across pore volumes provides macrodispersion
At extraction, the concentration is a dispersed breakthrough curve
The reported extracted concentration is the Kreft-Zuber (1978) flux concentration at the outlet, defined as the solute mass flux divided by the volumetric fluid flux. This is what is measured when sampling the outflowing fluid. Compared to Bear’s leading-order resident concentration, it includes the dispersive boundary flux
-D_s * dC_R/dxatx = L(with the solute-front dispersionD_s = D_m + alpha_L * v_sand velocityv_s = Q L / (R V_pore)), which is what makes the column-sum invariantintegral Q c_out dt = integral Q c_in dthold exactly under variable flow.Microdispersion and molecular diffusion enter as the moving-frame variance
sigma^2(V) = 2 * D_m * tau(V) + 2 * alpha_L * xi(V),
where
tau(V)is the elapsed time since infiltration andxi(V)is the distance the parcel has actually travelled. Evaluating sigma^2 at each quadrature node — and avoiding any artificial capping past breakthrough — keeps Bear’s formula an exact solution of the variable-coefficient ADE, which the Kreft-Zuber identity relies on.- Parameters:
cin (
ArrayLike) – Concentration of the compound in infiltrating water [concentration units]. Length must match the number of time bins defined by tedges. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length must match cin and the number of time bins defined by tedges. The model assumes this value is constant over each interval[tedges[i], tedges[i+1]).tedges (
DatetimeIndex) – Time edges defining bins for both cin and flow data. Has length of len(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Has length of desired output + 1. The output concentration is averaged over each bin.aquifer_pore_volumes (
ArrayLike) – Array of aquifer pore volumes [m³] representing the distribution of flow paths. Each pore volume determines the residence time for that flow path: tau = V_pore / Q.streamline_length (
ArrayLike) – Array of travel distances [m] corresponding to each pore volume. Must have the same length as aquifer_pore_volumes.molecular_diffusivity (
ArrayLike) –Effective (retarded-frame) molecular diffusivity [m²/day]. Can be a scalar (same for all pore volumes) or an array with the same length as aquifer_pore_volumes. Must be non-negative. For solute transport, this is the molecular diffusion coefficient D_m [m²/day] — typically ~1e-5 m²/day, negligible compared to microdispersion. For heat transport, pass the thermal diffusivity D_th = lambda / (rho*c)_eff [m²/day], typically 0.01-0.1 m²/day.
Internally, this contributes
2 * molecular_diffusivity * tauto the variance, wheretauis the elapsed time in days (no extra factor of R). The retardation factor instead enters the flux coefficientD_s/v_s = R D_m / v_fluid + alpha_Lthrough the solute-front velocityv_s = Q L / (R V_pore). For heat transport, the thermal diffusivity already represents the effective diffusivity D_eff in the porous matrix; for solutes the contribution is typically negligible.longitudinal_dispersivity (
ArrayLike) – Longitudinal dispersivity [m]. Can be a scalar (same for all pore volumes) or an array with the same length as aquifer_pore_volumes. Must be non-negative. Represents microdispersion from pore-scale velocity variations. Set to 0 for pure molecular diffusion.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption.spinup (
str|None, default:'constant') – Spin-up policy (default'constant').'constant'extends tedges by 100 years on each side so that output bins near the boundary are always informed.Nonedisables the extension; output bins without sufficient upstream data become NaN. Float fraction-threshold mode is not implemented and raisesNotImplementedError.
- Returns:
Bin-averaged concentration in the extracted water. Same units as cin. Length equals len(cout_tedges) - 1. NaN values indicate time periods with no valid contributions from the infiltration data.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If input dimensions are inconsistent, if diffusivity is negative, or if aquifer_pore_volumes and streamline_length have different lengths.
See also
extraction_to_infiltrationInverse operation (deconvolution)
gwtransport.advection.infiltration_to_extractionPure advection (no dispersion)
gwtransport.diffusion_fast.infiltration_to_extractionFast closed-form equivalent
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion
Notes
The algorithm constructs a coefficient matrix W where cout = W @ cin:
For each pore volume, build a cell grid in cumulative volume space:
cells span
(V_cout[i], V_cout[i+1]) x V_cin[j]for each (cout-bin i, cin-edge j)delta_volume = V_cout - V_cin - r_vpv encodes the parcel’s offset from the outlet at each (cout-edge, cin-edge)
For each cell, compute the bin-averaged Kreft-Zuber flux concentration
frac[i, j] = (1/dV_i) * integral C_F(L, V; t_j) dVby resolution-aware composite Gauss-Legendre quadrature in volume space, split at flow-bin boundaries so thatt(V)is linear within each sub-interval. Where the erf-like front (widthsqrt(4*D_t)in volume units) is under-resolved by a single 16-point rule – as for near-zero dispersivity – the sub-interval is tiled with front-centred panels; smooth sub-intervals keep the single rule. The moving-frame varianceD_t = D_m*tau + alpha_L*xiis evaluated at each quadrature node (never capped at the residence time).Coefficient for bin:
coeff[i,j] = frac[i, j] - frac[i, j+1]. This is the contribution of cin[j] to cout[i] in the W matrix.Average coefficients across all pore volumes.
The K-Z flux-correction term in C_F = C_R - (D_s/v_s) * dC_R/dx (solute-front velocity v_s = Q L / (R V_pore), dispersion D_s = D_m + alpha_L * v_s) is what makes the column-sum invariant exact under variable Q; see the module docstring for the derivation.
Examples
Basic usage with constant flow:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.diffusion import infiltration_to_extraction >>> >>> # Create time edges >>> tedges = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = pd.date_range(start="2020-01-05", end="2020-01-25", freq="D") >>> >>> # Input concentration (step function) and constant flow >>> cin = np.zeros(len(tedges) - 1) >>> cin[5:10] = 1.0 # Pulse of concentration >>> flow = np.ones(len(tedges) - 1) * 100.0 # 100 m³/day >>> >>> # Single pore volume of 500 m³, travel distance 100 m >>> aquifer_pore_volumes = np.array([500.0]) >>> streamline_length = np.array([100.0]) >>> >>> # Compute with dispersion (molecular diffusion + dispersivity) >>> # Scalar values broadcast to all pore volumes >>> cout = infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... streamline_length=streamline_length, ... molecular_diffusivity=1e-4, # m²/day, same for all pore volumes ... longitudinal_dispersivity=1.0, # m, same for all pore volumes ... )
With multiple pore volumes (heterogeneous aquifer):
>>> # Distribution of pore volumes and corresponding travel distances >>> aquifer_pore_volumes = np.array([400.0, 500.0, 600.0]) >>> streamline_length = np.array([80.0, 100.0, 120.0]) >>> >>> # Scalar diffusion parameters broadcast to all pore volumes >>> cout = infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... streamline_length=streamline_length, ... molecular_diffusivity=1e-4, # m²/day ... longitudinal_dispersivity=1.0, # m ... )
- gwtransport.diffusion.extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, aquifer_pore_volumes, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, spinup='constant')[source]#
Compute infiltration concentration from extracted water (deconvolution with dispersion).
Inverts the forward transport model by building the forward coefficient matrix
W_forwardfrominfiltration_to_extraction()and solvingW_forward @ cin = coutvia Tikhonov regularization. Well-determined modes are dominated by the data; poorly-determined modes are pulled toward the physically motivated target (transpose-and-normalize of the forward matrix).- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water [concentration units]. Length must match the number of time bins defined by cout_tedges.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length must match the number of time bins defined by tedges.tedges (
DatetimeIndex) – Time edges defining bins for cin (output) and flow data. Has length of len(flow) + 1. Output cin has length len(tedges) - 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Has length of len(cout) + 1. Can have different time alignment and resolution than tedges.aquifer_pore_volumes (
ArrayLike) – Array of aquifer pore volumes [m³] representing the distribution of flow paths. Each pore volume determines the residence time for that flow path: tau = V_pore / Q.streamline_length (
ArrayLike) – Array of travel distances [m] corresponding to each pore volume. Must have the same length as aquifer_pore_volumes.molecular_diffusivity (
ArrayLike) – Effective molecular diffusivity [m²/day]. Can be a scalar (same for all pore volumes) or an array with the same length as aquifer_pore_volumes. Must be non-negative. Seeinfiltration_to_extraction()for details on the physical interpretation and the interaction with retardation_factor.longitudinal_dispersivity (
ArrayLike) – Longitudinal dispersivity [m]. Can be a scalar (same for all pore volumes) or an array with the same length as aquifer_pore_volumes. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer (default 1.0). Values > 1.0 indicate slower transport due to sorption.regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter λ. Seegwtransport.advection.extraction_to_infiltration()for details. Default is 1e-10.spinup (
str|None, default:'constant') – Spin-up policy (default'constant').'constant'extends tedges by 100 years on each side so that output bins near the boundary are always informed.Nonedisables the extension; output bins without sufficient upstream data become NaN. Float fraction-threshold mode is not implemented and raisesNotImplementedError.
- Returns:
Bin-averaged concentration in the infiltrating water. Same units as cout. Length equals len(tedges) - 1. NaN values indicate time periods with no valid contributions from the extraction data.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If input dimensions are inconsistent, if diffusivity is negative, or if aquifer_pore_volumes and streamline_length have different lengths.
See also
infiltration_to_extractionForward operation (convolution)
gwtransport.advection.extraction_to_infiltrationPure advection (no dispersion)
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion
Notes
The algorithm builds the forward coefficient matrix
W_forward(same as used byinfiltration_to_extraction()) and solvesW_forward @ cin = coutusinggwtransport.utils.solve_tikhonov(). This ensures mathematical consistency between forward and inverse operations.NaN values in
coutare rejected. The Tikhonov solver here does not mask NaN rows, so any NaN incoutwould poison the solution. This differs fromgwtransport.deposition.extraction_to_deposition(), whose regularized solver excludes NaNcoutrows by construction.Examples
Basic usage with constant flow:
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.diffusion import extraction_to_infiltration >>> >>> # Create time edges: tedges for cin/flow, cout_tedges for cout >>> tedges = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = pd.date_range(start="2020-01-05", end="2020-01-25", freq="D") >>> >>> # Extracted concentration and constant flow >>> cout = np.zeros(len(cout_tedges) - 1) >>> cout[5:10] = 1.0 # Observed pulse at extraction >>> flow = np.ones(len(tedges) - 1) * 100.0 # 100 m³/day >>> >>> # Single pore volume of 500 m³, travel distance 100 m >>> aquifer_pore_volumes = np.array([500.0]) >>> streamline_length = np.array([100.0]) >>> >>> # Reconstruct infiltration concentration >>> cin = extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... aquifer_pore_volumes=aquifer_pore_volumes, ... streamline_length=streamline_length, ... molecular_diffusivity=1e-4, ... longitudinal_dispersivity=1.0, ... )
- gwtransport.diffusion.gamma_infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, spinup='constant')[source]#
Compute extracted concentration with advection and dispersion for gamma-distributed pore volumes.
Combines advection with microdispersion and molecular diffusion along each streamline (gamma-distributed pore volumes, whose spread provides macrodispersion). This is a convenience wrapper around
infiltration_to_extraction()that parameterizes the aquifer pore volume distribution as a (shifted) gamma distribution.Provide either (mean, std) or (alpha, beta);
locis optional and defaults to 0.- Parameters:
cin (
ArrayLike) – Concentration of the compound in infiltrating water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin and flow data. Has length len(cin) + 1.cout_tedges (
DatetimeIndex) – Time edges for output data bins. Has length of desired output + 1.mean (
float|None, default:None) – Mean of the gamma distribution of the aquifer pore volume. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution of the aquifer pore volume (invariant under thelocshift).loc (
float, default:0.0) – Location (minimum pore volume) of the gamma distribution. Must satisfy0 <= loc < mean. Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution of the aquifer pore volume (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution of the aquifer pore volume (must be > 0).n_bins (
int, default:100) – Number of bins to discretize the gamma distribution. Default is 100.streamline_length (
float) – Travel distance through the aquifer [m]. Applied uniformly to all gamma-discretized pore volumes.molecular_diffusivity (
float) – Effective molecular diffusivity [m²/day]. Must be non-negative. Seeinfiltration_to_extraction()for details on the interaction with retardation_factor.longitudinal_dispersivity (
float) – Longitudinal dispersivity [m]. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0). Values > 1.0 indicate slower transport.spinup (
str|None, default:'constant') – Spin-up policy (default'constant').'constant'extends tedges by 100 years on each side so that output bins near the boundary are always informed.Nonedisables the extension; output bins without sufficient upstream data become NaN. Float fraction-threshold mode is not implemented and raisesNotImplementedError.
- Returns:
Bin-averaged concentration in the extracted water. Length equals len(cout_tedges) - 1. NaN values indicate time periods with no valid contributions from the infiltration data.
- Return type:
GenericAlias[floating]
See also
infiltration_to_extractionTransport with explicit pore volume distribution
gamma_extraction_to_infiltrationReverse operation (deconvolution)
gwtransport.gamma.binsCreate gamma distribution bins
gwtransport.advection.gamma_infiltration_to_extractionPure advection (no dispersion)
- Gamma Distribution Model
Two-parameter pore volume model
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion
Notes
The APVD is only time-invariant under the steady-streamlines assumption (see 2. Steady Streamlines).
The spreading from the gamma-distributed pore volumes represents macrodispersion (aquifer-scale heterogeneity). When
stdcomes from calibration on measurements, it absorbs all mixing: macrodispersion, microdispersion, and an average molecular diffusion contribution. Whenstdcomes from streamline analysis, it represents macrodispersion only; microdispersion and molecular diffusion can be added via the dispersion parameters. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for guidance on when to add microdispersion.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.diffusion import gamma_infiltration_to_extraction >>> >>> tedges = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = pd.date_range(start="2020-01-05", end="2020-01-25", freq="D") >>> cin = np.zeros(len(tedges) - 1) >>> cin[5:10] = 1.0 >>> flow = np.ones(len(tedges) - 1) * 100.0 >>> >>> cout = gamma_infiltration_to_extraction( ... cin=cin, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... mean=500.0, ... std=100.0, ... n_bins=5, ... streamline_length=100.0, ... molecular_diffusivity=1e-4, ... longitudinal_dispersivity=1.0, ... )
- gwtransport.diffusion.gamma_extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, streamline_length, molecular_diffusivity, longitudinal_dispersivity, retardation_factor=1.0, regularization_strength=1e-10, spinup='constant')[source]#
Compute infiltration concentration from extracted water for gamma-distributed pore volumes.
Inverts the forward transport model (advection + dispersion with gamma-distributed pore volumes) via Tikhonov regularization. This is a convenience wrapper around
extraction_to_infiltration()that parameterizes the aquifer pore volume distribution as a (shifted) gamma distribution.Provide either (mean, std) or (alpha, beta);
locis optional and defaults to 0.- Parameters:
cout (
ArrayLike) – Concentration of the compound in extracted water.flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day].tedges (
DatetimeIndex) – Time edges for cin (output) and flow data. Has length of len(flow) + 1.cout_tedges (
DatetimeIndex) – Time edges for cout data bins. Has length of len(cout) + 1.mean (
float|None, default:None) – Mean of the gamma distribution of the aquifer pore volume. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution of the aquifer pore volume (invariant under thelocshift).loc (
float, default:0.0) – Location (minimum pore volume) of the gamma distribution. Must satisfy0 <= loc < mean. Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution of the aquifer pore volume (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution of the aquifer pore volume (must be > 0).n_bins (
int, default:100) – Number of bins to discretize the gamma distribution. Default is 100.streamline_length (
float) – Travel distance through the aquifer [m]. Applied uniformly to all gamma-discretized pore volumes.molecular_diffusivity (
float) – Effective molecular diffusivity [m²/day]. Must be non-negative. Seeinfiltration_to_extraction()for details on the interaction with retardation_factor.longitudinal_dispersivity (
float) – Longitudinal dispersivity [m]. Must be non-negative.retardation_factor (
float, default:1.0) – Retardation factor (default 1.0). Values > 1.0 indicate slower transport.regularization_strength (
float, default:1e-10) – Tikhonov regularization parameter. Default is 1e-10.spinup (
str|None, default:'constant') – Spin-up policy (default'constant').'constant'extends tedges by 100 years on each side so that output bins near the boundary are always informed.Nonedisables the extension; output bins without sufficient upstream data become NaN. Float fraction-threshold mode is not implemented and raisesNotImplementedError.
- Returns:
Bin-averaged concentration in the infiltrating water. Length equals len(tedges) - 1. NaN values indicate time periods with no valid contributions from the extraction data.
- Return type:
GenericAlias[floating]
See also
extraction_to_infiltrationDeconvolution with explicit pore volume distribution
gamma_infiltration_to_extractionForward operation (convolution)
gwtransport.gamma.binsCreate gamma distribution bins
gwtransport.advection.gamma_extraction_to_infiltrationPure advection (no dispersion)
- Gamma Distribution Model
Two-parameter pore volume model
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
Macrodispersion vs microdispersion
Notes
The APVD is only time-invariant under the steady-streamlines assumption (see 2. Steady Streamlines).
The spreading from the gamma-distributed pore volumes represents macrodispersion (aquifer-scale heterogeneity). When
stdcomes from calibration on measurements, it absorbs all mixing: macrodispersion, microdispersion, and an average molecular diffusion contribution. Whenstdcomes from streamline analysis, it represents macrodispersion only; microdispersion and molecular diffusion can be added via the dispersion parameters. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for guidance on when to add microdispersion.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.diffusion import gamma_extraction_to_infiltration >>> >>> tedges = pd.date_range(start="2020-01-01", end="2020-01-20", freq="D") >>> cout_tedges = pd.date_range(start="2020-01-05", end="2020-01-25", freq="D") >>> cout = np.zeros(len(cout_tedges) - 1) >>> cout[5:10] = 1.0 >>> flow = np.ones(len(tedges) - 1) * 100.0 >>> >>> cin = gamma_extraction_to_infiltration( ... cout=cout, ... flow=flow, ... tedges=tedges, ... cout_tedges=cout_tedges, ... mean=500.0, ... std=100.0, ... n_bins=5, ... streamline_length=100.0, ... molecular_diffusivity=1e-4, ... longitudinal_dispersivity=1.0, ... )
examples#
Example Data Generation for Groundwater Transport Modeling.
This module provides utilities to generate synthetic datasets for demonstrating and testing groundwater transport models. It creates realistic flow patterns, concentration/temperature time series, and deposition events suitable for testing advection, diffusion, and deposition analysis functions.
Available functions:
generate_example_data()- Generate comprehensive synthetic dataset with flow and concentration time series. Creates seasonal flow patterns with optional spill events, input concentration data via synthetic sinusoidal patterns, constant values, or real KNMI soil temperature, and extracted concentration computed through gamma-distributed pore volume transport. When diffusion parameters are provided, uses the diffusion module instead of pure advection. Returns DataFrame with flow, cin, cout columns plus attrs containing generation parameters and aquifer properties, and time edges (tedges).generate_temperature_example_data()- Convenience wrapper aroundgenerate_example_data()with sensible defaults for temperature transport including thermal retardation, thermal diffusivity, and longitudinal dispersivity.generate_ec_example_data()- Convenience wrapper aroundgenerate_example_data()with sensible defaults for electrical conductivity (EC) transport. EC is a conservative tracer (retardation factor 1.0) with negligible molecular diffusivity compared to thermal transport.generate_example_deposition_timeseries()- Generate synthetic deposition time series for pathogen/contaminant deposition analysis. Combines baseline deposition, seasonal patterns, random noise, and episodic contamination events with exponential decay. Returns Series with deposition rates [ng/m²/day] and attrs containing generation parameters, and time edges (tedges). Useful for testing extraction_to_deposition deconvolution and deposition_to_extraction convolution functions.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.examples.generate_example_data(*, date_start='2020-01-01', date_end='2021-12-31', date_freq='D', flow_mean=100.0, flow_amplitude=30.0, flow_noise=10.0, cin_method='synthetic', cin_mean=12.0, cin_amplitude=8.0, measurement_noise=1.0, aquifer_pore_volumes=None, aquifer_pore_volume_gamma_mean=None, aquifer_pore_volume_gamma_std=None, aquifer_pore_volume_gamma_loc=None, aquifer_pore_volume_gamma_nbins=None, retardation_factor=1.0, molecular_diffusivity=None, longitudinal_dispersivity=None, streamline_length=None, rng=None)[source]#
Generate synthetic concentration/temperature and flow data for groundwater transport.
Creates a synthetic dataset with seasonal flow patterns, input concentration (cin), and output concentration (cout) computed via gamma-distributed pore volume transport. When
molecular_diffusivity,longitudinal_dispersivity, andstreamline_lengthare provided, the diffusion module is used instead of pure advection.- Parameters:
date_start (
str, default:'2020-01-01') – Start and end dates for the generated time series (YYYY-MM-DD).date_end (
str, default:'2021-12-31') – Start and end dates for the generated time series (YYYY-MM-DD).date_freq (
str, default:'D') – Frequency string for pandas.date_range.flow_mean (
float, default:100.0) – Mean flow rate [m³/day].flow_amplitude (
float, default:30.0) – Seasonal amplitude of flow rate [m³/day].flow_noise (
float, default:10.0) – Random noise level for flow rate [m³/day].cin_method (
str, default:'synthetic') –Method for generating infiltration concentration. Options:
"synthetic": Seasonal sinusoidal pattern defined bycin_meanandcin_amplitude. Measurement noise is applied."constant": Constant value equal tocin_mean. Measurement noise is still applied."soil_temperature": Real soil temperature data from KNMI station 260.
cin_mean (
float, default:12.0) – Mean value of infiltrating concentration.cin_amplitude (
float, default:8.0) – Seasonal amplitude of infiltration concentration (only used for"synthetic"method).measurement_noise (
float, default:1.0) – Standard deviation of the Gaussian measurement noise applied independently tocinandcout. Because the two noise draws are independent, applying the forward operator todf['cin']does not exactly reproducedf['cout']whenmeasurement_noise > 0; the underlying noiseless signals remain consistent.aquifer_pore_volumes (
ArrayLike|None, default:None) – Discrete aquifer pore volumes [m³] representing the distribution of residence times. When provided, the gamma distribution is bypassed and none of theaquifer_pore_volume_gamma_*parameters may be passed. WhenNone, the pore volume distribution is built from the gamma parameters below.aquifer_pore_volume_gamma_mean (
float|None, default:None) – Mean pore volume of the aquifer gamma distribution [m³] (default 1000.0 when unset). Must be strictly greater thanaquifer_pore_volume_gamma_loc. Mutually exclusive withaquifer_pore_volumes.aquifer_pore_volume_gamma_std (
float|None, default:None) – Standard deviation of aquifer pore volume gamma distribution [m³] (default 200.0 when unset; invariant under thelocshift). Mutually exclusive withaquifer_pore_volumes.aquifer_pore_volume_gamma_loc (
float|None, default:None) – Location (minimum pore volume) of the aquifer gamma distribution [m³] (default 0.0 when unset). Must satisfy0 <= loc < mean. Mutually exclusive withaquifer_pore_volumes.aquifer_pore_volume_gamma_nbins (
int|None, default:None) – Number of bins to discretize the aquifer pore volume gamma distribution (default 250 when unset). Mutually exclusive withaquifer_pore_volumes.retardation_factor (
float, default:1.0) – Retardation factor for transport.molecular_diffusivity (
float|None, default:None) – Effective molecular diffusivity [m²/day]. When provided together withlongitudinal_dispersivityandstreamline_length, the diffusion module is used instead of pure advection. For solutes, typically ~1e-5 m²/day (negligible). For heat, use thermal diffusivity ~0.01-0.1 m²/day.longitudinal_dispersivity (
float|None, default:None) – Longitudinal dispersivity [m]. Must be provided together withmolecular_diffusivityandstreamline_length.streamline_length (
float|None, default:None) – Travel distance along the streamline [m]. Must be provided together withmolecular_diffusivityandlongitudinal_dispersivity.rng (
Generator|int|None, default:None) – Source of randomness for the synthetic flow noise, spill events, and measurement noise. Accepted in any form supported bynumpy.random.default_rng(). Pass an integer (ornumpy.random.Generator) for reproducible output;Nonedraws fresh entropy each call.
- Returns:
A tuple containing:
pandas.DataFrame: DataFrame with columns
'flow','cin','cout'and metadata attributes for the aquifer parameters.pandas.DatetimeIndex: Time edges (tedges) used for the flow calculations.
- Return type:
- Raises:
ValueError – If
cin_methodis not one of the supported methods, if only some of the diffusion parameters are provided, or ifaquifer_pore_volumesis passed together with anyaquifer_pore_volume_gamma_*parameter.
See also
generate_temperature_example_dataWrapper with thermal transport defaults.
generate_ec_example_dataWrapper with EC transport defaults.
- gwtransport.examples.generate_temperature_example_data(*, date_start='2020-01-01', date_end='2021-12-31', date_freq='D', flow_mean=100.0, flow_amplitude=30.0, flow_noise=10.0, cin_method='synthetic', cin_mean=12.0, cin_amplitude=8.0, measurement_noise=1.0, aquifer_pore_volumes=None, aquifer_pore_volume_gamma_mean=None, aquifer_pore_volume_gamma_std=None, aquifer_pore_volume_gamma_loc=None, aquifer_pore_volume_gamma_nbins=None, retardation_factor=2.0, molecular_diffusivity=0.05, longitudinal_dispersivity=1.0, streamline_length=100.0, rng=None)[source]#
Generate synthetic temperature and flow data for groundwater transport examples.
Convenience wrapper around
generate_example_data()with sensible defaults for temperature transport: thermal retardation factor, thermal diffusivity, longitudinal dispersivity, and streamline length.Typical parameter values for temperature transport in various sand types:
Parameter
Fine sand
Medium sand
Coarse sand/gravel
retardation_factor R
2.0–3.0
1.5–2.5
1.2–2.0
molecular_diffusivity (m²/day)
0.03–0.06
0.05–0.08
0.08–0.12
longitudinal_dispersivity (m)
0.1–1.0
0.5–5.0
1.0–10.0
streamline_length (m)
site-specific
- Parameters:
retardation_factor (
float, default:2.0) – Thermal retardation factor.molecular_diffusivity (
float, default:0.05) – Thermal diffusivity [m²/day].longitudinal_dispersivity (
float, default:1.0) – Longitudinal dispersivity [m].streamline_length (
float, default:100.0) – Travel distance along the streamline [m].
- Returns:
- Return type:
See also
generate_example_dataGeneric version with full parameter control.
generate_ec_example_dataWrapper with EC transport defaults.
Notes
All other parameters are forwarded unchanged to
generate_example_data(); see that function for their descriptions.
- gwtransport.examples.generate_ec_example_data(*, date_start='2020-01-01', date_end='2021-12-31', date_freq='D', flow_mean=100.0, flow_amplitude=30.0, flow_noise=10.0, cin_method='synthetic', cin_mean=500.0, cin_amplitude=150.0, measurement_noise=10.0, aquifer_pore_volumes=None, aquifer_pore_volume_gamma_mean=None, aquifer_pore_volume_gamma_std=None, aquifer_pore_volume_gamma_loc=None, aquifer_pore_volume_gamma_nbins=None, retardation_factor=1.0, molecular_diffusivity=5e-05, longitudinal_dispersivity=1.0, streamline_length=100.0, rng=None)[source]#
Generate synthetic electrical conductivity and flow data for groundwater transport examples.
Convenience wrapper around
generate_example_data()with sensible defaults for electrical conductivity (EC) transport. EC is a conservative tracer: dissolved ions travel at water velocity without retardation.Typical parameter values for EC (dissolved ion) transport in various sand types. The molecular diffusivity represents effective ionic diffusion in porous media (free-water D_0 reduced by porosity/tortuosity). It is negligible compared to microdispersion at field scale.
Parameter
Fine sand
Medium sand
Coarse sand/gravel
retardation_factor R
1.0
1.0
1.0
molecular_diffusivity (m²/day)
3e-5 – 5e-5
4e-5 – 8e-5
5e-5 – 1e-4
longitudinal_dispersivity (m)
0.1–1.0
0.5–5.0
1.0–10.0
streamline_length (m)
site-specific
- Parameters:
cin_mean (
float, default:500.0) – Mean infiltration EC [uS/cm, typical surface water EC].cin_amplitude (
float, default:150.0) – Seasonal amplitude of infiltration EC [uS/cm].measurement_noise (
float, default:10.0) – Standard deviation of the Gaussian measurement noise [uS/cm].retardation_factor (
float, default:1.0) – Retardation factor (1.0 for a conservative tracer).molecular_diffusivity (
float, default:5e-05) – Effective ionic diffusion [m²/day].longitudinal_dispersivity (
float, default:1.0) – Longitudinal dispersivity [m].streamline_length (
float, default:100.0) – Travel distance along the streamline [m].
- Returns:
- Return type:
See also
generate_example_dataGeneric version with full parameter control.
generate_temperature_example_dataWrapper with thermal transport defaults.
Notes
All other parameters are forwarded unchanged to
generate_example_data(); see that function for their descriptions.
- gwtransport.examples.generate_example_deposition_timeseries(*, date_start='2018-01-01', date_end='2023-12-31', freq='D', base=0.8, seasonal_amplitude=0.3, noise_scale=0.1, event_dates=None, event_magnitude=3.0, event_duration=30, event_decay_scale=10.0, ensure_non_negative=True, rng=None)[source]#
Generate synthetic deposition timeseries for groundwater transport examples.
- Parameters:
date_start (
str, default:'2018-01-01') – Start and end dates for the generated time series (YYYY-MM-DD).date_end (
str, default:'2023-12-31') – Start and end dates for the generated time series (YYYY-MM-DD).freq (
str, default:'D') – Frequency string for pandas.date_range (default ‘D’).base (
float, default:0.8) – Baseline deposition rate (ng/m²/day).seasonal_amplitude (
float, default:0.3) – Amplitude of the annual seasonal sinusoidal pattern (ng/m²/day).noise_scale (
float, default:0.1) – Standard deviation of Gaussian noise added to the signal (ng/m²/day).event_dates (
ArrayLike|DatetimeIndex|None, default:None) – Dates (strings or pandas-compatible) at which to place episodic events. Time-zone-naive entries are interpreted as UTC to match the generateddatesindex. If None, a sensible default list is used.event_magnitude (
float, default:3.0) – Peak deposition added at event onset (ng/m²/day). Decays exponentially overevent_durationdays at rateevent_decay_scale.event_duration (
int, default:30) – Duration of each event in days.event_decay_scale (
float, default:10.0) – Decay scale used in the exponential decay for event time series.ensure_non_negative (
bool, default:True) – If True, negative values are clipped to zero.rng (
Generator|int|None, default:None) – Source of randomness for the additive Gaussian noise. Accepted in any form supported bynumpy.random.default_rng(). Pass an integer (ornumpy.random.Generator) for reproducible output;Nonedraws fresh entropy each call.
- Returns:
A tuple containing:
pandas.Series: Deposition time series (ng/m²/day) indexed by UTC timestamps.
pandas.DatetimeIndex: Time bin edges (n+1 edges for n values).
- Return type:
- Raises:
ValueError – If
event_decay_scaleorevent_durationis not positive, or if anyevent_datesentry falls outside the generateddatesrange.
See also
gwtransport.deposition.deposition_to_extractionForward operator consuming this data.
gwtransport.deposition.extraction_to_depositionInverse operator.
fronttracking#
Front tracking module for exact nonlinear transport modeling.
fronttracking.events#
Event detection for front tracking in (V, θ) coordinates.
All intersections are pure line/line geometry in the (V, θ) plane because every wave speed dV/dθ is independent of flow. Functions return θ-coordinates of intersections; the solver translates to user-facing t at the API boundary.
Events include:
Characteristic-characteristic collisions
Shock-shock collisions
Shock-characteristic collisions
Rarefaction boundary interactions
Outlet crossings
All calculations return exact floating-point results with machine precision.
- gwtransport.fronttracking.events.is_outlet_crossing_pinned(concentration, sorption)[source]#
Whether a boundary state is pinned by the
c_minretardation floor.A crossing scheduled for such a state is a non-physical artifact (its speed is a floor artifact, not physics); the caller drops it so it does not pollute the solver’s event record /
theta_current.- Parameters:
concentration (
float) – Boundary-state concentration [mass/volume].sorption (
SorptionModel) – Sorption model (suppliesc_minandretardation).
- Returns:
Trueonly whenconcentrationis at/belowc_minAND the floored retardationR(c_min)is inflated pastOUTLET_PIN_RETARDATION.- Return type:
- class gwtransport.fronttracking.events.EventType(*values)[source]#
Bases:
EnumAll possible event types in front tracking simulation.
- CHAR_CHAR_COLLISION = 'characteristic_collision'#
Two characteristics intersect (will form shock).
- SHOCK_SHOCK_COLLISION = 'shock_collision'#
Two shocks collide (will merge).
- SHOCK_CHAR_COLLISION = 'shock_characteristic_collision'#
Shock catches or is caught by characteristic.
- RAREF_CHAR_COLLISION = 'rarefaction_characteristic_collision'#
Rarefaction boundary intersects with characteristic.
- SHOCK_RAREF_COLLISION = 'shock_rarefaction_collision'#
Shock intersects with rarefaction boundary.
- RAREF_RAREF_COLLISION = 'rarefaction_rarefaction_collision'#
Rarefaction boundary intersects with another rarefaction boundary.
- DSW_FAN_EXHAUSTED = 'decaying_shock_fan_exhausted'#
A decaying shock’s fan is exhausted (c_decay reached c_fan_tail).
- OUTLET_CROSSING = 'outlet_crossing'#
Wave crosses outlet boundary.
- class gwtransport.fronttracking.events.Event(theta, event_type, waves_involved, location, boundary_type=None)[source]#
Bases:
objectA single event in the simulation, ordered by cumulative flow θ.
The solver’s priority queue orders
(theta, counter, ...)tuples, notEventobjects, so this dataclass intentionally defines no ordering.- Parameters:
theta (
float) – Cumulative flow at which the event occurs [m³].event_type (
EventType) – Type of event.waves_involved (
list) – List of wave objects involved in this event.location (
float) – Volumetric position at which the event occurs [m³].boundary_type (
str|None, default:None) – Which rarefaction boundary collided:'head'or'tail'. Set for rarefaction collision events.
- __init__(theta, event_type, waves_involved, location, boundary_type=None)#
- gwtransport.fronttracking.events.find_characteristic_intersection(char1, char2, theta_current)[source]#
Find exact analytical intersection of two characteristics in (V, θ).
Returns (θ_intersect, V_intersect) if the intersection lies in the future (θ > θ_current) and both characteristics are active there; otherwise None.
- gwtransport.fronttracking.events.find_shock_shock_intersection(shock1, shock2, theta_current)[source]#
Find exact analytical intersection of two shocks in (V, θ).
- gwtransport.fronttracking.events.find_shock_characteristic_intersection(shock, char, theta_current)[source]#
Find exact analytical intersection of a shock and a characteristic in (V, θ).
- gwtransport.fronttracking.events.find_rarefaction_boundary_intersections(raref, other_wave, theta_current)[source]#
Intersections of a rarefaction’s head/tail with another wave.
Both rarefaction boundaries propagate at characteristic speeds (head at
1/R(c_head), tail at1/R(c_tail)), so we synthesize temporaryCharacteristicWaveinstances and reuse the analytical helpers.
- gwtransport.fronttracking.events.find_outlet_crossing(wave, v_outlet, theta_current)[source]#
Find the cumulative flow θ at which the wave crosses
v_outlet.Handles
CharacteristicWave,ShockWave, andDecayingShockWave. Rarefaction outlet crossings are handled by the callers directly (the solver andoutput.pysplit them into head/tail boundary crossings), so aRarefactionWavenever reaches this function and returnsNone.Assumes positive flow (waves always move toward larger V). Returns None if the wave has already passed the outlet, is not active, or moves backward. The “already past” check uses a relative tolerance so that a wave whose crossing event has just been processed (and is at v_outlet ± a few ULPs) does not re-emit a duplicate crossing one ULP later.
fronttracking.handlers#
Event handlers for front tracking in (V, θ) coordinates.
Each handler receives the waves involved in an event and returns the new waves created by the interaction. In (V, θ) coordinates every wave speed is flow-free, so handlers depend only on concentrations and the sorption isotherm — flow does not appear.
All handlers enforce physical correctness:
Mass conservation (Rankine-Hugoniot condition)
Entropy conditions (Lax condition for shocks)
Causality (no backward-traveling information)
Handlers modify wave states in-place by deactivating parent waves and creating new child waves.
- gwtransport.fronttracking.handlers.handle_characteristic_collision(char1, char2, theta_event, v_event)[source]#
Two characteristics collide → emit a shock.
The faster characteristic catches the slower one from behind. By the entropy condition this compressive interaction is always a shock, independently of the sorption regime (Freundlich n>1, n<1, or constant retardation).
- Parameters:
char1 (
CharacteristicWave) – Colliding characteristics.char2 (
CharacteristicWave) – Colliding characteristics.theta_event (
float) – Cumulative flow at which the collision occurs [m³].v_event (
float) – Position at which the collision occurs [m³].
- Returns:
Single shock created at the collision point.
- Return type:
- Raises:
RuntimeError – If the resulting shock fails the Lax entropy condition.
- gwtransport.fronttracking.handlers.handle_shock_collision(shock1, shock2, theta_event, v_event)[source]#
Two shocks collide → merge into a single shock connecting outer states.
The merged shock has
c_leftfrom the faster (upstream) shock,c_rightfrom the slower (downstream) shock; its speed is recomputed via Rankine-Hugoniot.- Parameters:
- Returns:
Single merged shock.
- Return type:
- Raises:
RuntimeError – If the merged shock violates the entropy condition.
- gwtransport.fronttracking.handlers.handle_shock_characteristic_collision(shock, char, theta_event, v_event)[source]#
Shock catches or is caught by a characteristic.
The characteristic concentration modifies one side of the shock:
Shock catches char (shock faster): modifies
c_right.Char catches shock (char faster): modifies
c_left.
If the resulting shock satisfies entropy it is emitted (compression); otherwise a rarefaction is created (expansion) to preserve mass balance.
- Return type:
- gwtransport.fronttracking.handlers.handle_shock_rarefaction_collision(shock, raref, theta_event, v_event, boundary_type)[source]#
Shock interacts with a rarefaction fan (tail or head boundary).
Every shock↔rarefaction collision is resolved exactly by a single
DecayingShockWavewhose trajectory subsumes the fan + shock together, for anyNonlinearSorption:Head collision (rarefaction head catches the leading shock): the decaying side is the left,
c_decay_initial = raref.c_head,c_fixed = shock.c_right, andc_fan_tail = raref.c_tail(the fan’s other boundary, which bounds the decay so partial drying is handled).Tail collision (trailing shock catches the rarefaction tail): the decaying side is the right,
c_decay_initial = raref.c_tail,c_fixed = shock.c_left, andc_fan_tail = raref.c_head.
The fan is bounded by
c_fan_tail: the solver’sDSW_FAN_EXHAUSTEDevent spawns a regular shock once the decaying side reaches it, so partial drying (raref.c_tail != shock.c_right) is resolved exactly. If the rarefaction boundary is not faster than the shock (degenerate solver/test input), both waves are deactivated and nothing is emitted.- Returns:
[DecayingShockWave]for a physical collision, or[]for degenerate input.- Return type:
- gwtransport.fronttracking.handlers.handle_rarefaction_characteristic_collision(raref, char, theta_event, v_event, boundary_type)[source]#
Rarefaction boundary intersects a characteristic.
The safe option (b) from the front-tracking rebuild plan: when a characteristic’s concentration matches the boundary concentration to within tolerance the characteristic is absorbed; otherwise an informative
RuntimeErroris raised because deactivating it would silently destroy mass.- Raises:
RuntimeError – If the characteristic’s concentration does not match the colliding rarefaction boundary concentration within tolerance, or if
boundary_typeis not'head'or'tail'.- Return type:
- gwtransport.fronttracking.handlers.handle_outlet_crossing(wave, theta_event, v_outlet)[source]#
Record a wave crossing the outlet boundary.
The wave is NOT deactivated — it remains for concentration queries at points between its origin and the outlet. The returned event record holds the cumulative flow
thetaat which the crossing occurs; the solver translates this to the user-facing time when appending tostate.events.- Return type:
- gwtransport.fronttracking.handlers.create_inlet_waves_at_theta(c_prev, c_new, theta, sorption)[source]#
Emit the wave produced by a step change in inlet concentration.
All inlet waves originate at the inlet face
V = 0. Wave type is determined by characteristic speed comparison in (V, θ):s_new > s_prev: compression → shock.s_new < s_prev: expansion → rarefaction.equal: contact discontinuity → characteristic.
For shocks the entropy condition is verified; if violated, an empty list is returned (mass balance may be affected — a known limitation handled by
DecayingShockWave).- Return type:
fronttracking.math#
Mathematical Foundation for Front Tracking with Nonlinear Sorption.
This module provides exact analytical computations for:
Freundlich, Langmuir, and constant retardation models
Brooks-Corey and van Genuchten-Mualem unsaturated conductivity models (for Kinematic-Wave percolation, see
gwtransport.percolation)Shock velocities via Rankine-Hugoniot condition
Characteristic velocities and positions
First arrival time calculations
Entropy condition verification
All sorption-class computations are exact analytical formulas; the
van Genuchten-Mualem class uses scipy.optimize.brentq for the two
inversions that have no closed form.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- class gwtransport.fronttracking.math.NonlinearSorption[source]#
Bases:
ABCAbstract base for concentration-dependent sorption models.
Subclasses must implement retardation, total_concentration, and concentration_from_retardation. Shock velocity and entropy checking are provided generically via the Rankine-Hugoniot and Lax conditions.
See also
FreundlichSorptionFreundlich isotherm implementation.
LangmuirSorptionLangmuir isotherm implementation.
ConstantRetardationLinear (constant R) retardation model.
- abstractmethod total_concentration(c)[source]#
Compute total concentration (dissolved + sorbed per unit pore volume).
- abstractmethod concentration_from_retardation(r)[source]#
Invert retardation factor to obtain concentration.
- shock_speed(c_left, c_right)[source]#
Compute shock speed dV/dθ via Rankine-Hugoniot in (V, θ) coordinates.
With cumulative-flow coordinate θ = ∫flow(t’) dt’, the PDE
∂C_T/∂t + flow·∂C/∂V = 0becomes∂C_T/∂θ + ∂C/∂V = 0, and Rankine-Hugoniot reduces to:dV_s/dθ = (C_R - C_L) / (C_T(C_R) - C_T(C_L))
Flow drops out entirely; the result is a property of the sorption isotherm alone.
- c_and_total_from_retardation(r)[source]#
Return
(c, C_T(c))at a given retardationr.Default implementation calls
concentration_from_retardation(r)thentotal_concentration(c)— two independent root-finds for sorptions where both routes back-solve the same equation (e.g. vG-Mualem withL ≠ 0). Subclasses for which both can be computed from a single root-find should override this for ~2× speedup of the IBP fan integrators.
- fan_converges_at_infinity()[source]#
Whether a
c_apex=0fan’s∫ c dθconverges asθ → +∞.True when
c → 0asR → ∞(sobase·c → 0faster thanbase → ∞): Brooks-Corey, van Genuchten-Mualem, Langmuir, and Freundlichn > 1. The only divergent case is Freundlichn < 1(c → ∞asR → ∞), which overrides this toFalse. Used by the universal temporal fan integrator to reject a+∞upper bound when the integral diverges.- Return type:
- class gwtransport.fronttracking.math.FreundlichSorption(k_f, n, bulk_density, porosity, c_min=1e-12)[source]#
Bases:
NonlinearSorptionFreundlich sorption isotherm with exact analytical methods.
The Freundlich isotherm is: s(C) = k_f * C^(1/n)
where: - s is sorbed concentration [mass/mass of solid] - C is dissolved concentration [mass/volume of water] - k_f is Freundlich coefficient [(volume/mass)^(1/n)] - n is Freundlich exponent (dimensionless)
For n > 1: Higher C travels faster For n < 1: Higher C travels slower For n = 1: linear (not supported, use ConstantRetardation instead)
- Parameters:
k_f (
float) – Freundlich coefficient [(m³/kg)^(1/n)]. Must be positive.n (
float) – Freundlich exponent [-]. Must be positive and != 1.bulk_density (
float) – Bulk density of porous medium [kg/m³]. Must be positive.porosity (
float) – Porosity [-]. Must be in (0, 1).c_min (
float, default:1e-12) – Minimum concentration threshold (the dry-soil singularity floor). For n>1, prevents infinite retardation as C→0. Default1e-12for all n.
Notes
- The retardation factor is defined as:
- R(C) = 1 + (rho_b/n_por) * ds/dC
= 1 + (rho_b*k_f)/(n_por*n) * C^((1/n)-1)
For Freundlich sorption, R depends on C, which creates nonlinear wave behavior.
For n>1 (higher C travels faster), R(C)→∞ as C→0, which can cause extremely slow wave propagation. The c_min parameter prevents this by enforcing a minimum concentration, making R(C) finite for all C≥0.
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> r = sorption.retardation(5.0) >>> c_back = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c_back, 5.0)) True
- __post_init__()[source]#
Validate parameters after initialization.
- Raises:
ValueError – If any parameter is outside its valid range:
k_f<= 0,n<= 0,n== 1,bulk_density<= 0,porosityoutside (0, 1), orc_min< 0.
- retardation(c)[source]#
Compute retardation factor R(C).
The retardation factor relates concentration speed to pore water speed in (V, θ) coordinates:
dV/dθ = 1 / R(C)
For Freundlich sorption:
R(C) = 1 + (rho_b*k_f)/(n_por*n) * C^((1/n)-1)
- Parameters:
c (
float|GenericAlias[double]) – Dissolved concentration [mass/volume]. Non-negative.- Returns:
r – Retardation factor [-]. Always >= 1.0.
- Return type:
Notes
For n > 1: R decreases with increasing C (higher C travels faster)
For n < 1: R increases with increasing C (higher C travels slower)
n<1 with c_min=0: R(0)=1 (no sorption at zero, physically correct) because clamping to
c_min=0leavesC^((1/n)-1) = 0^positive = 0.Otherwise:
cis clamped toc_minbefore evaluation. This pairs withtotal_concentration(), which also clamps toc_min.
Clamping with
np.maximumbefore the power keeps a single general path for every(n, c_min)combination and avoids raising the base to a fractional power on negativec.
- total_concentration(c)[source]#
Compute total concentration (dissolved + sorbed per unit pore volume).
- Total concentration includes both dissolved and sorbed mass:
- C_total = C + (rho_b/n_por) * s(C)
= C + (rho_b/n_por) * k_f * C^(1/n)
- Parameters:
c (
float|GenericAlias[double]) – Dissolved concentration [mass/volume]. Non-negative.- Returns:
c_total – Total concentration [mass/volume]. Always >= c.
- Return type:
Notes
- This is the conserved quantity in the transport equation:
∂C_total/∂t + ∂(flow*C)/∂v = 0
The flux term only includes dissolved concentration because sorbed mass is immobile.
For
c = 0,c^(1/n) = 0exactly (no singularity for anyn > 0), soC_T(0) = 0is physically correct and noc_minclamp is needed here.c_minis only required to keepretardation()finite asc -> 0forn > 1; clampingtotal_concentrationtoc_minwould bias Rankine-Hugoniot shock speeds whenc_R = 0(e.g. the canonical 0->c->0 pulse). Negativecis clamped to0defensively.
- concentration_from_retardation(r)[source]#
Invert retardation factor to obtain concentration analytically.
Given R, solves R = retardation(C) for C. This is used in rarefaction waves where the self-similar solution gives R as a function of position and time.
- Parameters:
r (
float|GenericAlias[double]) – Retardation factor [-]. Must be >= 1.0.- Returns:
c – Dissolved concentration [mass/volume]. Non-negative.
- Return type:
Notes
- This inverts the relation:
R = 1 + (rho_b*k_f)/(n_por*n) * C^((1/n)-1)
- The analytical solution is:
C = [(R-1) * n_por*n / (rho_b*k_f)]^(n/(1-n))
For n = 1 (linear sorption), the exponent n/(1-n) is undefined, which is why linear sorption must use ConstantRetardation class instead.
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> r = sorption.retardation(5.0) >>> c = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c, 5.0, rtol=1e-14)) True
- fan_converges_at_infinity()[source]#
Freundlich
n > 1:c → 0asR → ∞(converges).n < 1:c → ∞(diverges).- Return type:
- __init__(k_f, n, bulk_density, porosity, c_min=1e-12)#
- class gwtransport.fronttracking.math.ConstantRetardation(retardation_factor)[source]#
Bases:
objectConstant (linear) retardation model.
For linear sorption: s(C) = K_d * C This gives constant retardation: R(C) = 1 + (rho_b/n_por) * K_d = constant
This is a special case where concentration-dependent behavior disappears. Used for conservative tracers or as approximation for weak sorption.
- Parameters:
retardation_factor (
float) – Constant retardation factor [-]. Must be >= 1.0. R = 1.0 means no retardation (conservative tracer).
Notes
With constant retardation: - All concentrations travel at same speed in (V, θ): dV/dθ = 1/R - No rarefaction waves form (all concentrations travel together) - Shocks occur only at concentration discontinuities at inlet - Solution reduces to simple θ-shifting (and then t-shifting via the θ↔t map)
This is equivalent to a single-pore-volume advective time-shift (the deterministic limit of
gwtransport.advection.infiltration_to_extraction()) in the gwtransport package.Examples
>>> sorption = ConstantRetardation(retardation_factor=2.0) >>> sorption.retardation(5.0) 2.0 >>> sorption.retardation(10.0) 2.0
- __post_init__()[source]#
Validate parameters after initialization.
- Raises:
ValueError – If
retardation_factoris less than 1.0.
- total_concentration(c)[source]#
Compute total concentration for linear sorption.
- For constant retardation:
C_total = C * R
- concentration_from_retardation(r)[source]#
Not applicable for constant retardation.
With constant R, all concentrations have the same retardation, so inversion is not meaningful. This method raises an error.
- Raises:
NotImplementedError – Always raised for constant retardation.
- Return type:
- shock_speed(c_left, c_right)[source]#
Compute shock speed dV/dθ for constant retardation.
With constant R,
dV/dθ = 1 / Rfor any concentration pair — identical to every characteristic speed.
- check_entropy_condition(c_left, c_right, shock_speed)[source]#
Entropy condition for constant retardation: trivially satisfied.
With constant R every characteristic speed equals the shock speed in θ-space, so the Lax condition holds as an equality regardless of
c_left/c_right.- Returns:
satisfies – Always True.
- Return type:
- __init__(retardation_factor)#
- class gwtransport.fronttracking.math.LangmuirSorption(s_max, k_l, bulk_density, porosity)[source]#
Bases:
NonlinearSorptionLangmuir sorption isotherm with exact analytical methods.
The Langmuir isotherm is: s(C) = s_max * C / (K_L + C)
where: - s is sorbed concentration [mass/mass of solid] - C is dissolved concentration [mass/volume of water] - s_max is maximum sorption capacity [mass/mass of solid] - K_L is half-saturation constant [mass/volume]
Retardation always decreases with C (favorable isotherm), and R(0) is finite — unlike Freundlich with n > 1, no minimum concentration threshold is needed.
- Parameters:
s_max (
float) – Maximum sorption capacity [mass/mass of solid]. Must be positive.k_l (
float) – Half-saturation constant [mass/volume]. Concentration at which s = s_max / 2. Must be positive.bulk_density (
float) – Bulk density of porous medium [kg/m³]. Must be positive.porosity (
float) – Porosity [-]. Must be in (0, 1).
See also
FreundlichSorptionFreundlich isotherm (unbounded sorption).
ConstantRetardationLinear (constant R) retardation model.
- Non-Linear Sorption: Exact Solutions
Background on nonlinear sorption.
Notes
- The retardation factor is defined as:
R(C) = 1 + (rho_b * s_max * K_L) / (n_por * (K_L + C)^2)
Key properties:
R(0) = 1 + rho_b * s_max / (n_por * K_L) – finite for all parameters
R -> 1 as C -> infinity (all sorption sites saturated)
R always decreases with increasing C (higher C travels faster)
Shocks form on concentration increases, rarefaction fans on decreases
Examples
>>> sorption = LangmuirSorption( ... s_max=0.1, k_l=5.0, bulk_density=1500.0, porosity=0.3 ... ) >>> r = sorption.retardation(5.0) >>> c_back = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c_back, 5.0)) True
- __post_init__()[source]#
Validate parameters after initialization.
- Raises:
ValueError – If any parameter is outside its valid range:
s_max<= 0,k_l<= 0,bulk_density<= 0, orporosityoutside (0, 1).
- retardation(c)[source]#
Compute retardation factor R(C).
- For Langmuir sorption:
R(C) = 1 + A / (K_L + C)²
where A = rho_b * s_max * K_L / n_por.
- Parameters:
c (
float|GenericAlias[double]) – Dissolved concentration [mass/volume]. Non-negative.- Returns:
r – Retardation factor [-]. Always >= 1.0.
- Return type:
Notes
R(0) = 1 + rho_b * s_max / (n_por * K_L) — always finite
R decreases with increasing C (higher C travels faster)
R → 1 as C → ∞ (all sorption sites saturated)
- total_concentration(c)[source]#
Compute total concentration (dissolved + sorbed per unit pore volume).
- For Langmuir sorption:
C_total = C + (rho_b / n_por) * s_max * C / (K_L + C)
- concentration_from_retardation(r)[source]#
Invert retardation factor to obtain concentration analytically.
- Given R, solves R = 1 + A / (K_L + C)² for C:
C = sqrt(A / (R - 1)) - K_L
- Parameters:
r (
float|GenericAlias[double]) – Retardation factor [-]. Must be >= 1.0.- Returns:
c – Dissolved concentration [mass/volume]. Non-negative.
- Return type:
Notes
For R <= 1, returns 0.0 (unphysical region). For R >= R(0) = 1 + A/K_L², returns 0.0 (at or below zero concentration).
Examples
>>> sorption = LangmuirSorption( ... s_max=0.1, k_l=5.0, bulk_density=1500.0, porosity=0.3 ... ) >>> r = sorption.retardation(5.0) >>> c = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c, 5.0, rtol=1e-14)) True
- __init__(s_max, k_l, bulk_density, porosity)#
- class gwtransport.fronttracking.math.BrooksCoreyConductivity(theta_r, theta_s, k_s, brooks_corey_lambda)[source]#
Bases:
NonlinearSorptionBrooks-Corey unsaturated conductivity recast as a NonlinearSorption.
Used by
gwtransport.percolationto model gravity-driven percolation through a thick unsaturated zone via the Kinematic-Wave method. The closed-form conductivity curve\[\begin{split}K(\\theta) = K_s \\cdot \\Theta^a, \\qquad \\Theta = (\\theta - \\theta_r)/(\\theta_s - \\theta_r), \\qquad a = 3 + 2/\\lambda \\;(\\text{Burdine})\end{split}\]is recast in the framework’s
(C, C_T)variables by identifyingC ≡ K(the flux variable) andC_T ≡ θ - θ_r(the conserved storage). All three abstract methods have closed forms;shock_speedandcheck_entropy_conditionare inherited unchanged fromNonlinearSorption.- Parameters:
theta_r (
float) – Residual volumetric moisture content [-]. Must satisfy0 <= theta_r < theta_s.theta_s (
float) – Saturated volumetric moisture content [-]. Equal to the porosity for typical soils. Must satisfytheta_r < theta_s < 1.k_s (
float) – Saturated hydraulic conductivity [length/time]. Positive.brooks_corey_lambda (
float) – Pore-size distribution indexλ[-]. Positive. The exponenta = 3 + 2/λis the Burdine pore-connectivity result. The Mualem variant (L = 0.5) givesa = 2.5 + 2/λand is not implemented; a user wanting it can re-deriveλso the Burdineamatches the desired Mualem exponent.
See also
VanGenuchtenMualemConductivityVan Genuchten variant with brentq inversions.
FreundlichSorptionPower-law sorption isotherm (closed form, analogous shape).
gwtransport.percolation.root_zone_to_water_table_kinematic_waveThe public wrapper.
Notes
The retardation factor and total-concentration relation are:
\[\begin{split}C_T(C) = \\Delta\\theta \\cdot (C/K_s)^{1/a}, \\qquad R(C) = (\\Delta\\theta / (a K_s)) \\cdot (C/K_s)^{1/a - 1},\end{split}\]with
Δθ = θ_s − θ_r. Since1/a − 1 < 0always (a > 3),R(C) → ∞asC → 0(dry-soil singularity). The class clampsCto a small floor inretardationandconcentration_from_retardation(the same pattern asFreundlichSorptionwithn > 1);total_concentrationand the inheritedshock_speeddo not clamp, so the canonical wetting-front shockc_R = 0produces the correct Rankine-Hugoniot velocity.Examples
>>> sorption = BrooksCoreyConductivity( ... theta_r=0.01, theta_s=0.337, k_s=0.174, brooks_corey_lambda=0.25 ... ) >>> r = sorption.retardation(0.05) >>> c = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c, 0.05, rtol=1e-13)) True
- __post_init__()[source]#
Validate parameters and derive
a,delta_theta.- Raises:
ValueError – If any parameter is outside its valid range.
- Return type:
- concentration_from_retardation(r)[source]#
C = K_s · (R · a · K_s / Δθ)^{−a/(a−1)}. Result clamped at_C_MIN.
- __init__(theta_r, theta_s, k_s, brooks_corey_lambda)#
- class gwtransport.fronttracking.math.VanGenuchtenMualemConductivity(theta_r, theta_s, k_s, van_genuchten_n, mualem_l=0.5)[source]#
Bases:
NonlinearSorptionMualem prediction for the van Genuchten retention curve, recast as NonlinearSorption.
Used by
gwtransport.percolationfor Kinematic-Wave percolation with the standard Mualem-van Genuchten conductivity curve\[\begin{split}K(\\theta) = K_s \\cdot S_e^L \\cdot \\left[1 - \\left(1 - S_e^{1/m}\\right)^m\\right]^2, \\qquad S_e = (\\theta - \\theta_r)/(\\theta_s - \\theta_r), \\qquad m = 1 - 1/n_\\text{vG}.\end{split}\]The retention parameter
α_vGis not needed forK(θ)— the Kinematic-Wave approximation drops capillary suction, so only theK(S_e)curve matters. The two inversionsS_e(C)andS_e(R)have no closed form; both usescipy.optimize.brentqwithxtol = BRENTQ_XTOL = 1e-14.- Parameters:
theta_r (
float) – Residual volumetric moisture content [-].theta_s (
float) – Saturated volumetric moisture content [-].k_s (
float) – Saturated hydraulic conductivity [length/time].van_genuchten_n (
float) – Shape parametern_vG > 1.m = 1 − 1/n_vGis derived.mualem_l (
float, default:0.5) – Pore-connectivity parameterL. Default 0.5 (standard Mualem). Must satisfyL >= 0. SettingL = 0(Burdine variant) gives a closed-formS_e(C)inverse;L != 0requiresbrentq.
See also
BrooksCoreyConductivityBrooks-Corey closed-form variant.
gwtransport.percolation.root_zone_to_water_table_kinematic_waveThe public wrapper.
Notes
The closed-form derivative is
\[\begin{split}\\frac{dK_M}{dS_e} = K_s \\cdot S_e^{L-1} \\cdot U \\cdot \\left[L \\cdot U + 2 \\cdot S_e^{1/m} \\cdot T^{m-1}\\right],\end{split}\]with
T = 1 - S_e^{1/m}andU = 1 - T^m. Used forretardation(C)(after solvingS_e(C)) and for the brentq objective inconcentration_from_retardation(R). The formula is inlined at both call sites, not exposed as a separate method.The class checks monotonicity of
dK_M/dS_eat a single pair of sample points in__post_init__(cheap directional check). Truly pathological parameter combinations that yield a non-monotone curve surface as abrentqValueError at the first inversion call.Examples
>>> sorption = VanGenuchtenMualemConductivity( ... theta_r=0.01, theta_s=0.337, k_s=0.174, van_genuchten_n=2.28 ... ) >>> r = sorption.retardation(0.05) >>> c = sorption.concentration_from_retardation(r) >>> bool(np.isclose(c, 0.05, rtol=1e-12)) True
- __init__(theta_r, theta_s, k_s, van_genuchten_n, mualem_l=0.5)#
- __post_init__()[source]#
Validate parameters and run a single-sample monotonicity check.
- Raises:
ValueError – If parameters are outside their valid range, or if the cheap monotonicity sample at
S_e = 0.5vs0.99indicatesdK_M/dS_eis non-monotone (pathological).- Return type:
- retardation(c)[source]#
R = Δθ / (dK_M/dS_e)|_{S_e(C)}. Uses inlined derivative; clamps C at_C_MIN.
- concentration_from_retardation(r)[source]#
Invert
R(C) = r. SolvedK_M/dS_e(S_e) = Δθ/rvia brentq, thenC = K_M(S_e).
- c_and_total_from_retardation(r)[source]#
Return
(c, C_T)at retardationrfrom a SINGLE brentq call.Overrides the default base-class implementation (which calls
concentration_from_retardationandtotal_concentrationseparately and ends up doing two independent brentq solves on the same underlying equation). Halves the iterative-solver cost in the IBP fan integrators.
- gwtransport.fronttracking.math.SorptionModel = gwtransport.fronttracking.math.NonlinearSorption | gwtransport.fronttracking.math.ConstantRetardation#
Type alias for all sorption models accepted by the front-tracking solver.
- gwtransport.fronttracking.math.characteristic_speed(c, sorption)[source]#
Compute characteristic speed dV/dθ = 1/R(C).
In (V, θ) coordinates, every characteristic propagates at a flow-free speed determined solely by the local concentration and the sorption isotherm.
- Parameters:
c (
float) – Dissolved concentration [mass/volume].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- Returns:
speed – Characteristic speed dV/dθ.
- Return type:
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> s = characteristic_speed(c=5.0, sorption=sorption) >>> s > 0 True
- gwtransport.fronttracking.math.characteristic_position(c, sorption, theta_start, v_start, theta)[source]#
Compute position of a characteristic at cumulative flow θ.
Characteristics propagate linearly in θ:
V(θ) = v_start + characteristic_speed(C) * (θ - θ_start)
- Parameters:
c (
float) – Concentration carried by characteristic [mass/volume].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.theta_start (
float) – Cumulative flow at which the characteristic starts [m³].v_start (
float) – Starting position [m³].theta (
float) – Cumulative flow at which to evaluate position [m³].
- Returns:
position – Position at θ [m³], or None if θ < θ_start.
- Return type:
Examples
>>> sorption = ConstantRetardation(retardation_factor=2.0) >>> v = characteristic_position( ... c=5.0, sorption=sorption, theta_start=0.0, v_start=0.0, theta=1000.0 ... ) >>> bool(np.isclose(v, 500.0)) # v = (1/2) * 1000 = 500 True
- gwtransport.fronttracking.math.compute_first_front_arrival_theta(cin, theta_edges, aquifer_pore_volume, sorption)[source]#
Cumulative-flow θ at which
c_firstarrives at the outlet (end of spin-up).“Arrival” means the θ at which the
c_firstlevel is fully present at the outlet,θ_emit + V·R(c_first)forn<1andθ_emit + V·C_T(c_first)/c_firstforn>1/constant retardation.Warning
For
n<1withc_min > 0(defaultc_min = 1e-12inFreundlichSorption), the actual wave emitted is aRarefactionWavewhose head (c = c_min ≈ 0) reaches the outlet at θ ≈V·R(c_min) ≈ V— much earlier than the value this function returns (which is the tail arrivalV·R(c_first)). The function returns “tail arrival” semantics: the returned θ is a conservative end-of-spin-up where c ≤ c_first everywhere before it. Consult the solver event log for the true rarefaction head crossing.- Parameters:
cin (
GenericAlias[floating]) – Inlet concentration [mass/volume].theta_edges (
GenericAlias[floating]) – Cumulative-flow edges; lengthlen(cin) + 1.aquifer_pore_volume (
float) – Total pore volume [m³]. Must be positive.sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- Returns:
theta_first_arrival – Cumulative-flow θ at which
c_firstis fully present at the outlet [m³]. Returnsnp.infonly ifcinis identically zero.- Return type:
Examples
>>> cin = np.array([0.0, 10.0] + [10.0] * 10) >>> theta_edges = np.arange(0.0, 1300.0, 100.0) # constant flow=100, dt=1 >>> sorption = ConstantRetardation(retardation_factor=2.0) >>> theta_first = compute_first_front_arrival_theta( ... cin, theta_edges, 500.0, sorption ... ) >>> bool(np.isclose(theta_first, 100.0 + 500.0 * 2.0)) # θ_emit + V·R True
fronttracking.output#
Concentration extraction from front-tracking solutions (V, θ coordinates).
Every public function in this module takes θ (cumulative flow, m³). Callers
translate user-facing time t → θ at the API boundary via
FrontTrackerState.theta_at_t.
Functions#
concentration_at_point(v, theta, waves, sorption)
compute_breakthrough_curve(theta_array, v_outlet, waves, sorption)
compute_bin_averaged_concentration_exact(theta_bin_edges, v_outlet, waves, sorption, *, cin=None, theta_edges_inlet=None)
compute_domain_mass(theta, v_outlet, waves, sorption)
compute_cumulative_inlet_mass(theta, cin, theta_edges)
compute_cumulative_outlet_mass(theta, v_outlet, waves, sorption, *, cin, theta_edges)
compute_total_outlet_mass(v_outlet, sorption, *, cin, theta_edges) -> float
Outlet-mass functions use the PDE conservation identity
m_out(θ) = m_in(θ) − m_dom(θ) (Bear & Cheng 2010, Ch. 3: mass
conservation for transport with sorption). m_dom honors historical
wave activity via wave.was_active_at(theta) so retrospective queries
at θ before a collision event correctly attribute c at v_outlet.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.fronttracking.output.concentration_at_point(v, theta, waves, sorption)[source]#
Compute concentration at point (v, θ) with exact analytical value.
The function works entirely in (V, θ) coordinates: public callers must translate user-facing time t → θ at the API boundary (e.g., via
FrontTrackerState.theta_at_t).- Parameters:
v (
float) – Position [m³].theta (
float) – Cumulative flow [m³].waves (
Sequence[Wave]) – All waves in the simulation (active and inactive).sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (unused — kept for API symmetry; wave methods carry their own sorption reference).
- Returns:
concentration – Concentration at point (v, θ) [mass/volume].
- Return type:
Notes
Wave priority: decaying shocks first (closed-form analytical), then rarefaction fans (spatial extent), then most recently crossing shock or rarefaction tail, then characteristics. If no active wave controls the point, returns 0.0 (initial condition).
- gwtransport.fronttracking.output.compute_breakthrough_curve(theta_array, v_outlet, waves, sorption)[source]#
Concentration at the outlet evaluated over a θ-array (breakthrough curve).
- Parameters:
theta_array (
GenericAlias[floating]) – Cumulative-flow points at which to query the outlet concentration [m³]. Must be sorted in ascending order. Callers translate from user-facing time viaFrontTrackerState.theta_at_tbefore passing.v_outlet (
float) – Outlet position [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- Returns:
c_out – Concentration at
v_outletfor each θ intheta_array[mass/volume].- Return type:
GenericAlias[floating]
See also
concentration_at_pointPoint-wise concentration
compute_bin_averaged_concentration_exactBin-averaged concentrations
Examples
theta_array = np.linspace(0.0, tracker.state.theta_edges[-1], 1000) c_out = compute_breakthrough_curve( theta_array, v_outlet=500.0, waves=tracker.state.waves, sorption=sorption )
- gwtransport.fronttracking.output.identify_outlet_segments(theta_start, theta_end, v_outlet, waves, sorption)[source]#
Identify which waves control outlet concentration in θ-interval [theta_start, theta_end].
Finds all wave crossing events at the outlet and constructs segments where concentration is constant or varying (rarefaction). All times are expressed as cumulative flow θ [m³].
- Parameters:
theta_start (
float) – Start of cumulative-flow interval [m³].theta_end (
float) – End of cumulative-flow interval [m³].v_outlet (
float) – Outlet position [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- Returns:
segments – List of segment dictionaries, each containing:
- ’theta_start’float
Segment start θ [m³]
- ’theta_end’float
Segment end θ [m³]
- ’type’str
'constant','rarefaction', or'decaying_fan'.'decaying_fan'is owned by aDecayingShockWaveafter its head crossesv_outlet; c atv_outletthen follows the wave’s self-similar fan profile.
- ’concentration’float
For constant segments
- ’wave’Wave
For rarefaction and decaying_fan segments
- ’c_start’float
Concentration at segment start
- ’c_end’float
Concentration at segment end
- Return type:
Notes
Segments are constructed by:
Finding all wave crossing events at the outlet for θ in [theta_start, theta_end].
Sorting events by θ.
Creating constant-concentration segments between events.
Handling rarefaction and decaying-fan profiles with θ-varying concentration.
The segments completely partition the interval [theta_start, theta_end].
- gwtransport.fronttracking.output.integrate_rarefaction_exact(raref, v_outlet, theta_start, theta_end, sorption)[source]#
Exact θ-integral
∫ c(θ) dθof a rarefaction at the outlet.Convenience wrapper over
integrate_fan_exact()that pulls the fan apex fromraref.theta_start, raref.v_start. Returns the mass-like quantity∫ c dθ(=∫ c·flow dtin time coordinates).- Parameters:
raref (
RarefactionWave) – Rarefaction wave controlling the outlet.v_outlet (
float) – Outlet position [m³].theta_start (
float) – Integration range in cumulative flow [m³]. Either can be±np.inf.theta_end (
float) – Integration range in cumulative flow [m³]. Either can be±np.inf.sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (any NonlinearSorption subclass).
- Returns:
integral –
∫ c(θ) dθ[mass — i.e. concentration × volume].- Return type:
- gwtransport.fronttracking.output.integrate_fan_exact(theta_origin, v_origin, v_outlet, theta_start, theta_end, sorption, c_apex=0.0)[source]#
Exact θ-integral
∫ c(θ) dθfor any self-similar fan at the outlet.Decoupled from the wave object so the same closed-form math applies to both
RarefactionWave(apex =theta_start, v_start) andDecayingShockWave(apex =theta_origin, v_origin).- Parameters:
theta_origin (
float) – Cumulative flow and position at the fan’s apex [m³].v_origin (
float) – Cumulative flow and position at the fan’s apex [m³].v_outlet (
float) – Outlet position [m³].theta_start (
float) – Integration range in cumulative flow [m³].theta_endmay be+np.inf;theta_startmust be finite.theta_end (
float) – Integration range in cumulative flow [m³].theta_endmay be+np.inf;theta_startmust be finite.sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (any NonlinearSorption subclass).c_apex (
float, default:0.0) – Concentration on the constant side at the fan apex. ForRarefactionWavethis israref.c_tail; forDecayingShockWave(decay_side=’left’) this iswave.c_fixed. Forc_apex > 0the fan formula extrapolates past the physical fan range; the integration is clamped atθ_tail(wherec(θ_tail) = c_apex) and the constant-c_apex region beyond contributesc_apex · (theta_end − θ_tail). Default 0.0 preserves the c=0 apex behavior for canonical c_R=0 fans.
- Returns:
Mass-like quantity
∫ c(θ) dθ[mass — concentration × volume].- Return type:
- Raises:
TypeError – If the sorption model does not support exact fan integration.
- gwtransport.fronttracking.output.compute_bin_averaged_concentration_exact(theta_bin_edges, v_outlet, waves, sorption, *, cin=None, theta_edges_inlet=None)[source]#
θ-bin-averaged outlet concentration.
For each θ-bin
[θ_i, θ_{i+1}]:C_avg = (1 / Δθ) · ∫_{θ_i}^{θ_{i+1}} C(v_outlet, θ) dθWith
cin+theta_edges_inletprovided (recommended for multi-DSW cases), uses the conservation-law identityC_avg = (Δm_in − Δm_dom) / Δθper bin — analytical and explicit, no outlet-side fan dispatch. Otherwise falls back to outlet-segment integration (correct for canonical single-DSW cases; may miscount multi-DSW or n<1 mirror geometries).- Parameters:
theta_bin_edges (
GenericAlias[floating]) – Cumulative-flow OUTPUT bin edges [m³] (where C_avg is reported). Length N+1 for N bins. Callers translate t-bin edges withstate.theta_at_t.v_outlet (
float) – Outlet position [m³].waves (
Sequence[Wave]) – All waves from front tracking simulation.sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.cin (
ArrayLike|None, default:None) – Inlet concentration per inlet θ-bin. When provided withtheta_edges_inlet, the conservation form is used.theta_edges_inlet (
GenericAlias[floating] |None, default:None) – θ bin edges of the INLET (state.theta_edges), lengthlen(cin) + 1.
- Returns:
c_avg – Bin-averaged outlet concentrations [mass/volume]. Length N.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If any output θ-bin has non-positive width.
See also
concentration_at_pointPoint-wise concentration
compute_breakthrough_curveBreakthrough curve
compute_cumulative_outlet_massCumulative outlet mass via conservation
- gwtransport.fronttracking.output.compute_domain_mass(theta, v_outlet, waves, sorption)[source]#
Compute total mass in domain [0, v_outlet] at cumulative flow θ.
Integrates concentration over space:
M(θ) = ∫₀^v_outlet C_total(v, θ) dv
Exact analytical formulas for every wave type: constant regions (
C_total · Δv), RarefactionWave fan interiors and DecayingShockWave fan interiors (closed-form viaintegrate_fan_spatial_exact()).- Parameters:
theta (
float) – Cumulative flow at which to compute domain mass [m³].v_outlet (
float) – Outlet position (domain extent) [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- Returns:
mass – Total mass in domain [mass]. Closed-form analytical to machine precision.
- Return type:
See also
compute_cumulative_inlet_massCumulative inlet mass
compute_cumulative_outlet_massCumulative outlet mass
concentration_at_pointPoint-wise concentration
integrate_fan_spatial_exactClosed-form fan spatial integral
Examples
mass = compute_domain_mass( theta=2500.0, v_outlet=500.0, waves=tracker.state.waves, sorption=sorption ) mass >= 0.0
- gwtransport.fronttracking.output.integrate_fan_spatial_exact(theta_origin, v_origin, v_start, v_end, theta, sorption, c_apex=0.0)[source]#
Exact spatial integral
∫ C_total(v, θ) dvfor any self-similar fan.Decoupled from the wave object so the same closed-form math applies to
RarefactionWave(apex =theta_start, v_start) andDecayingShockWave(apex =theta_origin, v_origin).In (V, θ) the self-similar fan satisfies
R(C) = (θ - θ_origin)/(v - v_origin); definekappa = θ - θ_originandu = v - v_origin. The dissolved and sorbed contributions reduce to power-law forms inuthat admit closed forms via incomplete beta functions (Freundlich) or elementary sqrt operations (Langmuir).- Parameters:
theta_origin (
float) – Cumulative flow and position at the fan’s apex [m³].v_origin (
float) – Cumulative flow and position at the fan’s apex [m³].v_start (
float) – Integration range in v [m³].v_end (
float) – Integration range in v [m³].theta (
float) – Cumulative flow at which to evaluate [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (any NonlinearSorption subclass).c_apex (
float, default:0.0) – Concentration on the constant side at the fan apex (typically the parent rarefaction’sc_tailor the DSW’sc_fixedfordecay_side='left'). Forc_apex > 0the fan formula is unphysical foru < u_tail = kappa / R(c_apex); the integration is split into a constant-C_total(c_apex) region foru ∈ [u_start, u_tail]plus the fan integral foru ∈ [u_tail, u_end]. Default 0.0 preserves the c=0 apex behavior for canonical c_R=0 rarefactions.
- Returns:
Mass in the segment
[v_start, v_end].- Return type:
- Raises:
TypeError – If the sorption model does not support exact spatial integration.
- gwtransport.fronttracking.output.compute_cumulative_inlet_mass(theta, cin, theta_edges)[source]#
Cumulative inlet mass entering the domain from θ=0 to
theta.In cumulative-flow coordinates
M_in(θ) = ∫₀^θ cin(τ) dτ; for piecewise-constantcinthis is exact under summation over θ-bin widths.- Parameters:
- Returns:
mass_in – Cumulative inlet mass [mass].
- Return type:
Examples
mass_in = compute_cumulative_inlet_mass( theta=5000.0, cin=cin, theta_edges=theta_edges ) mass_in >= 0.0
- gwtransport.fronttracking.output.compute_cumulative_outlet_mass(theta, v_outlet, waves, sorption, *, cin, theta_edges)[source]#
Cumulative mass exiting through the outlet from θ=0 to
theta.Computed analytically via the conservation-law identity:
m_out(θ) = m_in(θ) − m_dom(θ)
derived from integrating the PDE
∂_θ C_T + ∂_V c = 0over the spatial domain[0, v_outlet](Bear & Cheng 2010, Ch. 3: mass conservation for advection with sorption). This sidesteps the multi-fan dispatch problem that the outlet-segment integration faces when several DSWs cover v_outlet simultaneously — every term on the right is purely spatial or a closed-form inlet sum, no ownership priority needed.- Parameters:
theta (
float) – Cumulative flow up to which to integrate [m³].v_outlet (
float) – Outlet position [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.cin (
ArrayLike) – Inlet concentration per θ-bin [mass/volume].theta_edges (
GenericAlias[floating]) – θ bin edges [m³], lengthlen(cin) + 1.
- Returns:
mass_out – Cumulative outlet mass [mass].
- Return type:
Examples
mass_out = compute_cumulative_outlet_mass( theta=5000.0, v_outlet=500.0, waves=tracker.state.waves, sorption=sorption, cin=cin, theta_edges=tracker.state.theta_edges, ) mass_out >= 0.0
- gwtransport.fronttracking.output.compute_total_outlet_mass(v_outlet, sorption, *, cin, theta_edges)[source]#
Total outlet mass over θ → ∞ (finite only for a returning-to-zero pulse).
The final inlet value
c_∞ = cin[-1]is the sustained boundary state as θ → ∞:For
c_∞ = 0(canonical c_R=0 pulse): injection ceases, the domain empties, and every injected mass unit eventually exits —m_out_total = m_in_total(the finite record integralΣ cin·Δθ). The wave list is not needed.For
c_∞ > 0(sustained ambient): the inlet keeps injectingc_∞forever, so the cumulative outlet mass grows without bound — return+inf. The previous formulam_in_total − C_T(c_∞)·v_outletpaired the FINITE record integral with the infinite-time steady-state fill and went negative wheneverm_in_total < C_T(c_∞)·v_outlet, which is not a physical outlet mass.
- Parameters:
v_outlet (
float) – Outlet position [m³] (unused forc_∞ = 0; the+infbranch does not need it).sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (kept for API symmetry; noC_Tevaluation is required).cin (
ArrayLike) – Inlet concentration per θ-bin [mass/volume].theta_edges (
GenericAlias[floating]) – θ bin edges [m³], lengthlen(cin) + 1.
- Returns:
m_in_totalforcin[-1] = 0;+infforcin[-1] > 0.- Return type:
See also
compute_cumulative_outlet_massCumulative outlet mass up to a finite θ (use this for a sustained
c_∞ > 0boundary, where the θ → ∞ total is unbounded).compute_domain_massSpatial integral of C_total in the aquifer
fronttracking.plot#
Visualization functions for front tracking.
This module provides plotting utilities for visualizing front-tracking simulations: - V-t diagrams showing wave propagation in space-time - Breakthrough curves showing concentration at outlet over time
Internally the simulation uses cumulative-flow coordinates (V, θ). All plots
remain in user-facing time t (days). Translation is done via the state’s
t_at_theta / theta_at_t methods at the plotting boundary.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.fronttracking.plot.plot_vt_diagram(state, ax=None, *, t_max=None, figsize=(14, 10), show_inactive=False, show_events=False)[source]#
Create V-t diagram showing all waves in space-time.
Plots characteristics (blue lines), shocks (red lines), and rarefactions (green fans) in the (time, position) plane. This visualization shows how waves propagate and interact throughout the simulation.
Internally the waves live in (V, θ); each wave’s straight-line θ-trajectory is converted back to user-facing time t via
state.t_at_thetabefore plotting.- Parameters:
state (
FrontTrackerState) – Complete simulation state containing all waves.ax (
Axes|None, default:None) – Existing axes to plot into. If None, a new figure and axes are created usingfigsize.t_max (
float|None, default:None) – Maximum time to plot [days]. If None, uses the input data time range.figsize (
tuple[float,float], default:(14, 10)) – Figure size in inches (width, height). Default (14, 10).show_inactive (
bool, default:False) – Whether to show inactive waves (deactivated by interactions). Default False.show_events (
bool, default:False) – Whether to show wave interaction events as markers. Default False.
- Returns:
ax – Axes object containing the V-t diagram.
- Return type:
See also
plot_breakthrough_curveOutlet breakthrough curve for the same state.
plot_wave_interactionsEvent timeline of wave interactions.
plot_front_tracking_summaryMulti-panel summary combining these views.
gwtransport.advection.infiltration_to_extraction_nonlinear_sorptionProduces the tracker state.
Notes
Characteristics appear as blue lines (constant speed in θ).
Shocks appear as thick red lines (jump discontinuities).
Rarefactions appear as green fans (smooth transition regions).
Outlet position is shown as a horizontal dashed line.
Only waves within domain [0, v_outlet] are plotted.
Examples
from gwtransport.fronttracking.solver import FrontTracker tracker = FrontTracker(cin, flow, tedges, aquifer_pore_volume, sorption) tracker.run() ax = plot_vt_diagram(tracker.state) ax.figure.savefig("vt_diagram.png")
- gwtransport.fronttracking.plot.plot_breakthrough_curve(state, ax=None, *, t_max=None, n_rarefaction_points=50, figsize=(12, 6), t_first_arrival=None)[source]#
Plot exact analytical concentration breakthrough curve at outlet.
Uses wave segment information to plot the exact analytical solution without discretization. Constant concentration regions are plotted as horizontal lines, and rarefaction regions are plotted using their exact self-similar solutions.
- Parameters:
state (
FrontTrackerState) – Complete simulation state containing all waves.ax (
Axes|None, default:None) – Existing axes to plot into. If None, a new figure and axes are created usingfigsize.t_max (
float|None, default:None) – Maximum time to plot [days]. If None, uses the input data time range.n_rarefaction_points (
int, default:50) – Number of points to use for plotting rarefaction segments (analytical curves). Default 50 per rarefaction segment.figsize (
tuple[float,float], default:(12, 6)) – Figure size in inches (width, height). Default (12, 6).t_first_arrival (
float|None, default:None) – First arrival time for marking spin-up period [days]. If None, spin-up period is not plotted.
- Returns:
ax – Axes object containing the breakthrough curve.
- Return type:
See also
plot_vt_diagramSpace-time diagram of the same waves.
plot_front_tracking_summaryMulti-panel summary combining these views.
gwtransport.fronttracking.output.compute_breakthrough_curveUnderlying analytical curve.
gwtransport.advection.infiltration_to_extraction_nonlinear_sorptionProduces the tracker state.
Notes
Uses identify_outlet_segments to get exact analytical segment boundaries
Constant concentration segments plotted as horizontal lines (no discretization)
Rarefaction segments plotted using exact self-similar solution
Shocks appear as instantaneous jumps at exact crossing times
No bin averaging or discretization artifacts
Examples
from gwtransport.fronttracking.solver import FrontTracker tracker = FrontTracker(cin, flow, tedges, aquifer_pore_volume, sorption) tracker.run() ax = plot_breakthrough_curve(tracker.state) ax.figure.savefig("exact_breakthrough.png")
- gwtransport.fronttracking.plot.plot_wave_interactions(state, ax=None, *, figsize=(14, 8))[source]#
Plot event timeline showing wave interactions.
Creates a scatter plot showing when and where different types of wave interactions occur during the simulation. Event records carry the cumulative flow at which the event occurred (
"theta"key) and position ("location"); this function translates θ → user-facing days viastate.t_at_thetafor display.- Parameters:
state (
FrontTrackerState) – Complete simulation state containing all events.ax (
Axes|None, default:None) – Existing axes to plot into. If None, a new figure and axes are created usingfigsize.figsize (
tuple[float,float], default:(14, 8)) – Figure size in inches (width, height). Default (14, 8).
- Returns:
ax – Axes object containing the event timeline.
- Return type:
Notes
Each event type is shown with a different color and marker.
Outlet crossings are shown separately from internal collisions.
Event locations are plotted in the (time, position) plane.
Examples
from gwtransport.fronttracking.solver import FrontTracker tracker = FrontTracker(cin, flow, tedges, aquifer_pore_volume, sorption) tracker.run() ax = plot_wave_interactions(tracker.state) ax.figure.savefig("wave_interactions.png")
- gwtransport.fronttracking.plot.plot_inlet_concentration(tedges, cin, ax=None, *, t_first_arrival=None, event_markers=None, color='blue', t_max=None, xlabel='Time [days]', ylabel='Concentration', title='Inlet Concentration', figsize=(8, 5), **step_kwargs)[source]#
Plot inlet concentration as step function with optional markers.
- Parameters:
tedges (
DatetimeIndex) – Time bin edges for inlet concentration. Length = len(cin) + 1.cin (
ArrayLike) – Inlet concentration values. Length = len(tedges) - 1.ax (
Axes|None, default:None) – Existing axes to plot into. If None, creates new figure.t_first_arrival (
float|None, default:None) – First arrival time to mark with vertical line [days].event_markers (
list[dict] |None, default:None) – Event markers to add. Each dict should have keys: ‘time’, ‘label’, ‘color’.color (
str, default:'blue') – Color for inlet concentration line. Default ‘blue’.t_max (
float|None, default:None) – Maximum time for x-axis [days]. If None, uses full range.xlabel (
str, default:'Time [days]') – Label for x-axis. Default ‘Time [days]’.ylabel (
str, default:'Concentration') – Label for y-axis. Default ‘Concentration’.title (
str, default:'Inlet Concentration') – Plot title. Default ‘Inlet Concentration’.figsize (
tuple[float,float], default:(8, 5)) – Figure size if creating new figure. Default (8, 5).**step_kwargs – Additional arguments passed to ax.plot().
- Returns:
ax – Axes object.
- Return type:
See also
plot_front_tracking_summaryMulti-panel summary that places this inlet panel.
- gwtransport.fronttracking.plot.plot_front_tracking_summary(structure, tedges, cin, cout_tedges, cout, *, figsize=(16, 10), show_exact=True, show_bin_averaged=True, show_events=True, show_inactive=False, t_max=None, title=None, inlet_color='blue', outlet_exact_color='blue', outlet_binned_color='red', first_arrival_color='green')[source]#
Create comprehensive 3-panel summary figure for front tracking simulation.
Creates a multi-panel visualization with: - Top-left: V-t diagram showing wave propagation - Top-right: Inlet concentration time series - Bottom: Outlet concentration (exact and/or bin-averaged)
- Parameters:
structure (
dict) – Structure returned from infiltration_to_extraction_nonlinear_sorption. Must contain keys: ‘tracker_state’, ‘theta_first_arrival’.tedges (
DatetimeIndex) – Time bin edges for inlet concentration. Length = len(cin) + 1.cin (
ArrayLike) – Inlet concentration values. Length = len(tedges) - 1.cout_tedges (
DatetimeIndex) – Output time bin edges for bin-averaged concentration. Length = len(cout) + 1.cout (
ArrayLike) – Bin-averaged output concentration values. Length = len(cout_tedges) - 1.figsize (
tuple[float,float], default:(16, 10)) – Figure size (width, height). Default (16, 10).show_exact (
bool, default:True) – Whether to show exact analytical breakthrough curve. Default True.show_bin_averaged (
bool, default:True) – Whether to show bin-averaged concentration. Default True.show_events (
bool, default:True) – Whether to show wave interaction events on V-t diagram. Default True.show_inactive (
bool, default:False) – Whether to show inactive waves on V-t diagram. Default False.t_max (
float|None, default:None) – Maximum time for plots [days]. If None, uses input data range.title (
str|None, default:None) – Overall figure title. If None, uses generic title.inlet_color (
str, default:'blue') – Color for inlet concentration. Default ‘blue’.outlet_exact_color (
str, default:'blue') – Color for exact outlet curve. Default ‘blue’.outlet_binned_color (
str, default:'red') – Color for bin-averaged outlet. Default ‘red’.first_arrival_color (
str, default:'green') – Color for first arrival marker. Default ‘green’.
- Return type:
- Returns:
fig (
matplotlib.figure.Figure) – Figure object.axes (
dict) – Dictionary with keys ‘vt’, ‘inlet’, ‘outlet’ containing axes objects.
See also
plot_vt_diagramThe top-left sub-panel.
plot_breakthrough_curveOutlet breakthrough curve for the same state.
plot_inlet_concentrationThe top-right sub-panel.
gwtransport.advection.infiltration_to_extraction_nonlinear_sorptionProduces
structure.
- gwtransport.fronttracking.plot.plot_sorption_comparison(pulse_favorable_structure, pulse_unfavorable_structure, pulse_tedges, pulse_cin, dip_favorable_structure, dip_unfavorable_structure, dip_tedges, dip_cin, *, figsize=(16, 12), t_max_pulse=None, t_max_dip=None)[source]#
Compare how each inlet produces different outputs with n>1 vs n<1 sorption.
Creates a 2x3 grid: - Row 1: Pulse inlet and its outputs with n>1 and n<1 sorption - Row 2: Dip inlet and its outputs with n>1 and n<1 sorption
This demonstrates how the SAME inlet timeseries produces DIFFERENT breakthrough curves depending on the sorption isotherm.
- Parameters:
pulse_favorable_structure (
dict) – Structure from pulse inlet with n>1 (higher C travels faster).pulse_unfavorable_structure (
dict) – Structure from pulse inlet with n<1 (lower C travels faster).pulse_tedges (
DatetimeIndex) – Time bin edges for pulse inlet. Length = len(pulse_cin) + 1.pulse_cin (
ArrayLike) – Pulse inlet concentration (e.g., 0->10->0). Length = len(pulse_tedges) - 1.dip_favorable_structure (
dict) – Structure from dip inlet with n>1 (higher C travels faster).dip_unfavorable_structure (
dict) – Structure from dip inlet with n<1 (lower C travels faster).dip_tedges (
DatetimeIndex) – Time bin edges for dip inlet. Length = len(dip_cin) + 1.dip_cin (
ArrayLike) – Dip inlet concentration (e.g., 10->2->10). Length = len(dip_tedges) - 1.figsize (
tuple[float,float], default:(16, 12)) – Figure size (width, height). Default (16, 12).t_max_pulse (
float|None, default:None) – Max time for pulse plots [days]. If None, auto-computed.t_max_dip (
float|None, default:None) – Max time for dip plots [days]. If None, auto-computed.
- Return type:
- Returns:
fig (
matplotlib.figure.Figure) – Figure object.axes (
numpy.ndarray) – 2x3 array of axes objects.
fronttracking.solver#
Event-driven front-tracking solver in (V, θ) coordinates.
The simulation runs entirely in cumulative-flow space θ. Every public
output — wave attributes, state.events[i]['theta'],
theta_first_arrival — is in θ. Translation to user-facing time t is
the caller’s responsibility via state.t_at_theta.
Time-varying flow is absorbed into the precomputed theta_edges array
at __init__; there is no flow-change event.
Algorithm:
Initialize waves from inlet boundary conditions (one per cin step at θ_edges[i]).
Find next event (earliest collision or outlet crossing in θ).
Advance θ to event.
Handle event (create new waves, deactivate old ones).
Repeat until no more events.
All calculations are exact analytical with machine precision.
- class gwtransport.fronttracking.solver.FrontTrackerState(waves, events, theta_current, v_outlet, sorption, cin, flow, tedges, tedges_days, theta_edges)[source]#
Bases:
objectComplete state of the front-tracking simulation in (V, θ).
- Parameters:
waves (
list[Wave]) – All waves created during simulation (includes inactive waves).events (
list[dict]) – Event history. Records use the"theta"key carrying the cumulative flow at which the event occurred [m³]. Callers translate to user-facing time viaFrontTrackerState.t_at_theta.theta_current (
float) – Current simulation cumulative flow [m³].v_outlet (
float) – Outlet position [m³].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.cin (
ndarray) – Inlet concentration time series [mass/volume].flow (
ndarray) – Flow rate time series [m³/day], one value per bin.tedges (
DatetimeIndex) – Time bin edges.tedges_days (
GenericAlias[floating]) –tedgesas days fromtedges[0], lengthlen(flow) + 1.theta_edges (
GenericAlias[floating]) – Cumulative flow at every bin edge.theta_edges[i] = sum_{k<i} flow[k] * (tedges_days[k+1] - tedges_days[k]). Lengthlen(flow) + 1.
- sorption: NonlinearSorption | ConstantRetardation#
- tedges: DatetimeIndex#
- t_at_theta(theta)[source]#
Translate cumulative flow θ back to user-facing time t [days].
Piecewise linear inversion of the (tedges_days → theta_edges) map. Implementation note on the (rare) zero-flow case: when a bin has
flow[i] == 0, θ is constant across[tedges_days[i], tedges_days[i+1]);np.searchsorted(..., side='right') - 1lands on the rightmost such bin, so this function returnstedges_days[i]for the right-most i sharing that θ. Events scheduled at zero-flow bin boundaries therefore align with the END of the zero-flow interval — pick one convention and call it documented.- Return type:
- theta_at_t(t)[source]#
Translate user-facing time t [days] to cumulative flow θ [m³].
Piecewise linear forward map. Outside the input range the boundary flow is extrapolated.
- Return type:
- theta_at_t_array(t)[source]#
Vectorized
theta_at_t: map an array of times t [days] to θ [m³].Element-wise identical to
theta_at_t(); replaces per-scalar loops in the plotting/output breakthrough routines.
- __init__(waves, events, theta_current, v_outlet, sorption, cin, flow, tedges, tedges_days, theta_edges)#
- class gwtransport.fronttracking.solver.FrontTracker(cin, flow, tedges, aquifer_pore_volume, sorption)[source]#
Bases:
objectEvent-driven front-tracking solver for nonlinear sorption transport.
- Parameters:
cin (
ArrayLike) – Inlet concentration time series [mass/volume]; lengthn.flow (
ArrayLike) – Flow rate time series [m³/day]; lengthn(one value per bin).tedges (
DatetimeIndex) – Time bin edges (lengthn+1).aquifer_pore_volume (
float) – Total pore volume [m³] — used as the outlet position.sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.
- state#
Complete simulation state.
- Type:
- theta_first_arrival#
Cumulative flow θ at which the first nonzero-concentration wave reaches the outlet [m³]. Translate to user-facing time via
state.t_at_theta(theta_first_arrival).- Type:
Notes
The solver works exclusively in cumulative flow θ; events appended to
state.eventscarry"theta". Translation to user-facing time t is the caller’s responsibility (usestate.t_at_theta).- handle_event(event)[source]#
Dispatch an event to its handler and record it (with t translated from θ).
- run(max_iterations=10000, *, verbose=False)[source]#
Process events in θ-order until the queue is empty or
max_iterationsis reached.
- verify_physics()[source]#
Verify physical correctness: every active shock satisfies Lax entropy.
Mass conservation is intentionally NOT checked here. The closed-form identity
m_out(θ) = m_in(θ) − m_dom(θ)makes any runtimem_in_domain + m_out_cumulative == m_in_cumulativetest tautological (residual identically zero, regardless of anycompute_domain_massbug), so it cannot catch a conservation error. The non-tautological, integral-based conservation check (an independent breakthrough integral compared to the inlet mass) lives ingwtransport.fronttracking.validation.verify_physics()check 7 and is exercised byTestEndToEndConservation/TestIndependentDomainMass.- Raises:
RuntimeError – If an active shock violates the Lax entropy condition.
- gwtransport.fronttracking.solver.find_unresolved_interaction(state)[source]#
Locate an unresolved wave–wave interaction inside the transport domain.
The event-driven solver resolves shock↔shock, shock↔characteristic and shock↔rarefaction collisions (the last into a
DecayingShockWave), but it never collides anything with a decaying shock, nor composes two fans that come to occupy the same region. Such an unresolved interaction leaves the non-interacting wave objects overlapping, so the exact nonlinear multi-front field is not represented — the publiccoutdegrades to a spurious linear superposition of a nonlinear operator (mass-fabricating once the reader clamps the resulting negative bins to zero). This detects the first offending interaction so the public API can refuse the input rather than return a wrong, non-conservative answer. Two complementary detectors run over the input θ-window(0, theta_edges[-1]]:Geometric fan overlap. When two or more fan-bearing waves (rarefactions / decaying shocks) cover a common point inside
[0, v_outlet], their composite field is wrong even when total mass happens to be conserved (a positive-but-wrongcoutwith no negative bin — e.g. two decaying shocks with a zero fan tail, or a later pulse’s fan sweeping an earlier one). A symptom-only proxy cannot see this class, so the geometric scan is a necessary complement.Conservation symptom. The cumulative outlet mass
m_out(θ) = m_in(θ) − m_dom(θ)must be non-decreasing in θ (mass exits the column, it never re-enters). A decrease beyond the FP-cancellation band means the reader’s domain-mass field transiently over-counts stored mass — the fingerprint of a shock overtaking another shock / rarefaction / decaying-shock fan. The two fans need NOT share an in-domain point (they may only cross beyondv_outlet), so the geometric scan misses this dominant multi-pulse class; the monotonicity check catches it.
- Parameters:
state (
FrontTrackerState) – Completed simulation state (afterFrontTracker.run()).- Returns:
A short description (position/θ and mechanism) of the first offending interaction, or
Nonewhen the solution is a clean single-front / well-separated superposition that the reader represents exactly.- Return type:
Notes
Both scans stay strictly inside the inlet θ-window, so the benign out-of-window saturation clamp (a single-front run whose output bins extend past the last injected mass) does not trip the symptom check. Well-separated pulses that clear a short column before overtaking one another (their fans never share an in-domain point and their cumulative outflow stays monotone) are correctly accepted.
fronttracking.validation#
Physics validation utilities for front tracking in (V, θ) coordinates.
This module provides functions to verify physical correctness of front-tracking
simulations, including entropy conditions, concentration bounds, mass conservation,
and event ordering. The solver runs in cumulative-flow coordinate
θ = ∫flow(t') dt'; events on state.events carry "theta" (m³). Because
flow ≥ 0 is enforced, θ is monotone non-decreasing in t, so θ-ordering and
chronological ordering are equivalent.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.fronttracking.validation.verify_physics(structure, cout, cout_tedges, cin, *, verbose=True, rtol=1e-10)[source]#
Run comprehensive physics verification checks on front tracking results.
Performs the following checks:
Entropy condition for all shocks
No negative concentrations (within tolerance)
Output concentration <= input maximum
Finite first arrival θ
No NaN values after spin-up period
Events θ-ordered (equivalent to chronological under non-negative flow)
Mass conservation: independent outlet integral + domain mass == inlet mass at θ_max
- Parameters:
structure (
dict) – Structure returned frominfiltration_to_extraction_nonlinear_sorption. Must contain keys:'waves','theta_first_arrival','events', and optionally'tracker_state'.cout (
ArrayLike) – Bin-averaged output concentrations.cout_tedges (
DatetimeIndex) – Output time edges for bins (only used for the spin-up mask).cin (
ArrayLike) – Input concentrations.verbose (
bool, default:True) – If True, print detailed results. If False, only return summary. Default True.rtol (
float, default:1e-10) – Relative tolerance for numerical checks. Default 1e-10. For the mass-balance check (7) the effective tolerance ismax(rtol, _MASS_BALANCE_RTOL)because that check integrates a shock-bearing breakthrough curve and is only first-order accurate (see_MASS_BALANCE_RTOL).
- Returns:
results – Dictionary containing:
'all_passed': bool - True if all checks passed'n_checks': int - Total number of checks performed'n_passed': int - Number of checks that passed'failures': list of str - Description of failed checks (empty if all passed)'checks': list of dict - Per-check result records; each has'name','passed','message'keys.'summary': str - One-line summary
- Return type:
Examples
results = verify_physics(structure, cout, cout_tedges, cin, verbose=False) print(results["summary"]) assert results["all_passed"]
fronttracking.waves#
Wave Representation for Front Tracking in (V, θ) coordinates.
This module implements wave classes for representing characteristics, shocks,
and rarefaction waves in the front tracking algorithm. Each wave stores its
formation position in cumulative-flow coordinate θ = ∫flow(t') dt' and
knows how to compute its position at any later θ.
The change from (V, t) to (V, θ) makes every wave velocity a property of the sorption isotherm alone — flow no longer enters into wave dynamics. Time- varying flow is absorbed entirely into the θ(t) mapping at the API boundary; no wave needs recreation when the flow rate changes.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- class gwtransport.fronttracking.waves.Wave(theta_start, v_start, *, is_active=True, theta_deactivation=inf)[source]#
Bases:
ABCAbstract base class for all wave types in front tracking.
All waves share common attributes and must implement methods for computing position and concentration. Waves can be active or inactive (deactivated waves are preserved for history but don’t participate in future interactions).
- Parameters:
- theta_deactivation: float = inf#
Cumulative flow at which the wave was deactivated (default
+∞).Historical record set by collision handlers when a wave is replaced (e.g., a parent rarefaction superseded by a
DecayingShockWave).is_active = Falseis the “current state” flag the solver uses for its event loop;theta_deactivationis the moment in θ-history when the wave stopped contributing. Retrospective queries (any θ in the past) must usewas_active_at(theta)instead ofis_activeso thatcompute_domain_massetc. correctly attribute c at v_outlet during the wave’s lifetime even after later events have deactivated the wave.
- was_active_at(theta)[source]#
Whether the wave was active at cumulative flow
theta(geometric truth).Use for retrospective queries —
is_activereflects only the wave’s current (post-simulation) state, which is wrong forcompute_domain_massand similar at θ before a deactivation event.- Parameters:
theta (
float) – Cumulative flow at which to query historical activity [m³].- Returns:
Truefortheta_start <= theta < theta_deactivation. A wave constructed withis_active=Falseand no recordedtheta_deactivation(default+∞) is treated as never-active — e.g., synthetic test fixtures that want the wave excluded from dispatch entirely.- Return type:
- deactivate(theta)[source]#
Mark the wave inactive at cumulative flow
theta(collision handler API).Sets both
is_active = False(solver event-loop flag) andtheta_deactivation = theta(historical record for retrospectivewas_active_atqueries).
- abstractmethod position_at_theta(theta)[source]#
Compute wave position at cumulative flow θ.
- Parameters:
theta (
float) – Cumulative flow [m³].- Returns:
position – Position [m³], or None if θ < θ_start or θ >= theta_deactivation. (Past-θ queries respect the wave’s historical lifetime; current-state queries before deactivation behave identically to the
is_activecheck.)- Return type:
- abstractmethod concentration_left()[source]#
Concentration on the left (upstream) side of the wave.
- Return type:
- abstractmethod concentration_right()[source]#
Concentration on the right (downstream) side of the wave.
- Return type:
- abstractmethod concentration_at_point(v, theta)[source]#
Compute concentration at point (v, θ) if the wave controls it.
- __init__(theta_start, v_start, *, is_active=True, theta_deactivation=inf)#
- class gwtransport.fronttracking.waves.CharacteristicWave(theta_start, v_start, concentration, sorption, *, is_active=True, theta_deactivation=inf)[source]#
Bases:
WaveCharacteristic line along which concentration is constant.
In smooth regions, concentration travels at speed
1/R(C)in (V, θ) coordinates. Along each characteristic line, the concentration value is constant. This is the fundamental solution element for hyperbolic conservation laws.- Parameters:
theta_start (
float) – Formation cumulative flow [m³].v_start (
float) – Starting position [m³].concentration (
float) – Constant concentration carried [mass/volume].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model determining the speed.is_active (
bool, default:True) – Activity status. Default True.
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> char = CharacteristicWave( ... theta_start=0.0, v_start=0.0, concentration=5.0, sorption=sorption ... ) >>> speed = char.speed() >>> bool(np.isclose(char.position_at_theta(1000.0), speed * 1000.0)) True
- sorption: NonlinearSorption | ConstantRetardation#
Sorption model determining the speed.
- position_at_theta(theta)[source]#
Position at cumulative flow θ.
V(θ) = v_start + speed * (θ - θ_start).
- concentration_left()[source]#
Concentration on the left (upstream) side; equals the carried value.
- Return type:
- concentration_right()[source]#
Concentration on the right (downstream) side; equals the carried value.
- Return type:
- concentration_at_point(v, theta)[source]#
Return the carried concentration if the characteristic has reached
vby θ.
- __init__(theta_start, v_start, concentration, sorption, *, is_active=True, theta_deactivation=inf)#
- class gwtransport.fronttracking.waves.ShockWave(theta_start, v_start, c_left, c_right, sorption, *, is_active=True, theta_deactivation=inf)[source]#
Bases:
WaveShock wave (discontinuity) with jump in concentration.
Shocks form when faster water overtakes slower water, creating a sharp front. In (V, θ) the shock speed is given by the Rankine-Hugoniot condition and is independent of flow:
dV_s/dθ = (C_R - C_L) / (C_T(C_R) - C_T(C_L))
- Parameters:
theta_start (
float) – Formation cumulative flow [m³].v_start (
float) – Formation position [m³].c_left (
float) – Concentration upstream (behind) shock [mass/volume].c_right (
float) – Concentration downstream (ahead of) shock [mass/volume].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model.is_active (
bool, default:True) – Activity status. Default True.speed (
float, optional) – Shock speed dV/dθ. Computed from Rankine-Hugoniot in__post_init__.
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> shock = ShockWave( ... theta_start=0.0, ... v_start=0.0, ... c_left=10.0, ... c_right=2.0, ... sorption=sorption, ... ) >>> shock.speed > 0 True >>> shock.satisfies_entropy() True
- sorption: NonlinearSorption | ConstantRetardation#
Sorption model.
- concentration_at_point(v, theta)[source]#
Return c_left if upstream of the shock at θ, c_right if downstream.
At the exact shock position the average is returned (convention; the shock is infinitesimally thin in practice).
- satisfies_entropy()[source]#
Check Lax entropy condition in (V, θ):
λ_θ(C_L) ≥ s ≥ λ_θ(C_R).- Return type:
- __init__(theta_start, v_start, c_left, c_right, sorption, *, is_active=True, theta_deactivation=inf)#
- class gwtransport.fronttracking.waves.RarefactionWave(theta_start, v_start, c_head, c_tail, sorption, *, is_active=True, theta_deactivation=inf)[source]#
Bases:
WaveRarefaction (expansion fan) with smooth concentration gradient.
Rarefactions form when slower water follows faster water, creating an expanding region where concentration varies smoothly. In (V, θ) the solution is self-similar in
(V - v_start)vs(θ - θ_start):R(C) = (θ - θ_start) / (V - v_start)
Head and tail propagate at flow-free speeds
1/R(C_head)and1/R(C_tail).- Parameters:
theta_start (
float) – Formation cumulative flow [m³].v_start (
float) – Formation position [m³].c_head (
float) – Concentration at leading edge (faster) [mass/volume].c_tail (
float) – Concentration at trailing edge (slower) [mass/volume].sorption (
NonlinearSorption|ConstantRetardation) – Sorption model (must be concentration-dependent).is_active (
bool, default:True) – Activity status. Default True.
- Raises:
ValueError – If head speed <= tail speed (would be a compression, not a rarefaction).
Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> raref = RarefactionWave( ... theta_start=0.0, ... v_start=0.0, ... c_head=10.0, ... c_tail=2.0, ... sorption=sorption, ... ) >>> raref.head_speed() > raref.tail_speed() True >>> raref.contains_point(v=150.0, theta=2000.0) True
- sorption: NonlinearSorption | ConstantRetardation#
Sorption model (must be concentration-dependent).
- __post_init__()[source]#
Cache head/tail celerities and verify this is a rarefaction (head faster than tail).
- head_speed()[source]#
Speed of rarefaction head dV/dθ = 1/R(C_head) (
+∞at a saturated state, R = 0).- Return type:
- tail_speed()[source]#
Speed of rarefaction tail dV/dθ = 1/R(C_tail) (
+∞at a saturated state, R = 0).- Return type:
- position_at_theta(theta)[source]#
Head position (leading edge of rarefaction). Implements abstract Wave method.
- contains_point(v, theta)[source]#
Return
Trueif(v, θ)lies between the fan’s tail and head.- Return type:
- concentration_left()[source]#
Upstream concentration is the trailing-edge value c_tail.
- Return type:
- concentration_right()[source]#
Downstream concentration is the leading-edge value c_head.
- Return type:
- concentration_at_point(v, theta)[source]#
Self-similar concentration inside the fan:
R(C) = (θ - θ_start)/(v - v_start).Outside the fan returns None. For
ConstantRetardation, rarefactions don’t form (all concentrations travel at the same speed), so this also returns None.Examples
>>> sorption = FreundlichSorption( ... k_f=0.01, n=2.0, bulk_density=1500.0, porosity=0.3 ... ) >>> raref = RarefactionWave(0.0, 0.0, 10.0, 2.0, sorption) >>> c = raref.concentration_at_point(v=150.0, theta=2000.0) >>> c is not None True >>> 2.0 <= c <= 10.0 True
- __init__(theta_start, v_start, c_head, c_tail, sorption, *, is_active=True, theta_deactivation=inf)#
- class gwtransport.fronttracking.waves.DecayingShockWave(theta_start, v_start, c_decay_initial, c_fixed, c_fan_tail, decay_side, v_origin, theta_origin, sorption, *, is_active=True, theta_deactivation=inf)[source]#
Bases:
WaveMerging shock with closed-form (or quadrature) trajectory in θ-space.
Formed when a rarefaction fan and a shock collide. The shock then has one side fed by the fan’s self-similar profile (the “decay” side) and the other side at the original outer state (the “fixed” side). Valid for any
NonlinearSorption.Two collision regimes are supported via
decay_side:'left'(favorable head-collision): the rarefaction’s head (faster) catches a leading shock. After collision, the shock’sc_leftdecays from the rarefaction head value towardc_fan_tail(the unchanged downstream c_right isc_fixed).'right'(unfavorable tail-collision, n<1 mirrored): a trailing shock catches the rarefaction’s tail. After collision, the shock’sc_rightdecays from the rarefaction tail value towardc_fan_tail(the unchanged upstream c_left isc_fixed).
The wave is valid only while
c_decay ∈ (c_fan_tail, c_decay_initial]; oncec_decayreachesc_fan_tailthe fan is exhausted (see the solver’sDSW_FAN_EXHAUSTEDevent).Dispatch.
_c_decay_at_theta_localis the single dispatch site (position, fan-exhaustion and outlet-crossing all route through it): a closed form is used where one exists, otherwise the per-wave cached numerical profile (_build_decay_profile()). No combination raises — anyNonlinearSorptionis valid. Withθ_local := θ − theta_originmeasured from the rarefaction apex,α := ρ_b · k_f / n_porfor Freundlich, andu_d := c_decay^(1/n):Freundlich,
c_fixed = 0(generaln > 0,n ≠ 1) — closed form: invariantθ_local · u_d^n = K · (n · u_d^(n-1) + α), positionV_s(θ) = v_origin + n · K / u_d(θ).Freundlich,
c_fixed > 0,n = 2andc_decay_initial > c_fixed— closed form: invariant(u_d - u_R)² · θ_local = K · (2 u_d + α)withu_R := c_fixed^(1/2), positionV_s(θ) = v_origin + 2 K · u_d(θ) / (u_d - u_R)². (Thec_decay_initial < c_fixedmirror falls through to numerical.)Langmuir,
c_fixed = 0— closed form: invariantθ_local · c_d² = K · ((K_L + c_d)² + a)witha := ρ_b · s_max · K_L / n_por, positionV_s(θ) = v_origin + K · (K_L + c_d)² / c_d².Brooks-Corey,
c_fixed = 0— closed form: invariantθ_local ∝ R(c_decay)^{a/(a−1)}(R·S = 1/aconstant), soR(c_d) = R(c0)·(θ_local/θ_local_coll)^{(a−1)/a}.Every other
(isotherm, c_fixed)combination (Freundlichc_fixed>0, n≠2, Langmuir/Brooks-Coreyc_fixed>0, any van Genuchten) — cached numerical profile (_build_decay_profile()): the decay-agnostic invariantθ_local(c_d) = θ_local_coll · exp(∫ R'/[(1 − R·S)·R] dc)with the symmetric secant speedS = (c − c_fixed)/(C_T(c) − C_T(c_fixed)), built once by composite quadrature and inverted forc_d(θ)by monotone spline interpolation.
Every path shares the fan-continuity identity
V_s = v_origin + θ_local / R(c_decay), whichposition_at_thetaandoutlet_crossing_thetause uniformly across all isotherms.The invariant constant
K(closed-form Freundlich/Langmuir only) is set in__post_init__from the collision IC(theta_start, c_decay_initial).- Parameters:
theta_start (
float) – Cumulative flow at which the merged wave forms (collision θ) [m³].v_start (
float) – Position at which the merged wave forms [m³]. Should equalv_origin + (V_s) at θ=theta_startfor a fan-consistent construction.c_decay_initial (
float) – Concentration on the decaying side at θ=theta_start [mass/volume]. Must be non-negative; a fully-drained collision value of0is floored to the shared dry-soil singularity floor_C_MINso the retardation and secant-speed evaluations stay finite (issue #222).c_fixed (
float) – Concentration on the non-decaying side [mass/volume]. Constant in θ. Non-negative.c_fan_tail (
float) – Concentration at the fan’s far boundary [mass/volume]. The wave is valid only whilec_decay ∈ (c_fan_tail, c_decay_initial]; atc_fan_tailthe fan is exhausted. Non-negative.decay_side (
str) –'left'or'right'. See class docstring.v_origin (
float) – Position of the rarefaction apex [m³].theta_origin (
float) – Cumulative flow at the rarefaction apex [m³]. Must satisfytheta_origin < theta_start.sorption (
NonlinearSorption) – Sorption model (any concentration-dependent isotherm).is_active (
bool, default:True) – Activity flag. Default True.
See also
ShockWaveLinear-θ shock (no decaying side).
RarefactionWaveSelf-similar expansion fan.
- sorption: NonlinearSorption#
Sorption model (any concentration-dependent isotherm).
- K: float#
Invariant constant set in
__post_init__(closed-form Freundlichc_fixed=0/n≈2and Langmuirc_fixed=0cases;nanfor every numerical case).
- __init__(theta_start, v_start, c_decay_initial, c_fixed, c_fan_tail, decay_side, v_origin, theta_origin, sorption, *, is_active=True, theta_deactivation=inf)#
- __post_init__()[source]#
Validate inputs and compute the closed-form invariant K when applicable.
- Return type:
- c_decay_at_theta(theta)[source]#
Concentration on the decaying side at cumulative flow θ.
Returns
Noneforθ < theta_startor when the wave is inactive; otherwise delegates to the single per-isotherm dispatch in_c_decay_at_theta_local.
- position_at_theta(theta)[source]#
Shock position
V_s(θ)via the fan-continuity identity.V_s = v_origin + θ_local / R(c_decay)for every isotherm. ReturnsNoneforθ < theta_startor when inactive.
- theta_at_fan_exhaustion()[source]#
Cumulative flow θ at which
c_decayreachesc_fan_tail.c_decay(θ)is strictly monotone fromc_decay_initialtowardc_fan_tail, so the exhaustion θ is well-defined. The crossing test is orientation-agnostic: it holds for both the shrinking decay (c_decay_initial > c_fan_tail) and the growing decay (c_decay_initial < c_fan_tail). ReturnsNonewhenc_fan_tailis not strictly betweenc_fixedandc_decay_initial— e.g. full drying (c_fan_tail == c_fixed), where the decay asymptotically merges with the fixed state and no finite exhaustion event occurs.
- outlet_crossing_theta(v_outlet)[source]#
Cumulative flow at which
V_s = v_outlet.Returns
Noneif the outlet is upstream of the wave’s birth position or no crossing exists in(theta_start, +∞). The wave’s current activity flag is not consulted — callers asking retrospectively about a historical crossing need the answer regardless of subsequent deactivation.The closed-form Freundlich/Langmuir cases invert the fan-continuity identity
V_s − v_origin = θ_local / R(c_decay)analytically (valid only when_c_decay_at_theta_localitself uses the closed form, so the same conditions are mirrored here); every other case inverts the monotoneV_s(θ)viabrentq.
- concentration_left()[source]#
Concentration on the left (upstream) side at θ=theta_start.
For
decay_side='left'returns the decaying c at the collision moment; fordecay_side='right'returns the fixed side.- Return type:
- concentration_right()[source]#
Concentration on the right (downstream) side at θ=theta_start.
For
decay_side='right'returns the decaying c at the collision moment; fordecay_side='left'returns the fixed side.- Return type:
- concentration_at_point(v, theta)[source]#
Concentration at
(v, θ)if controlled by this decaying shock.Three regions:
v == V_s(θ)(within FP): average of decay-side and fixed-side c.v > V_s(θ)(downstream): fixed-side c ifdecay_side='left'; decay-side c at θ ifdecay_side='right'.v < V_s(θ)(upstream, inside the fan): the fan’s self-similar concentrationR(c) = (θ − theta_origin)/(v − v_origin). Outside the fan — i.e. the decay-side characteristic from the apex hasn’t reached v yet, OR the point lies beyond thec_fan_tailboundary (the fan’s far edge) — returnsNone.
Returns
Noneforθ < theta_startor inactive waves.
gamma#
Gamma Distribution Utilities for Aquifer Pore Volume Heterogeneity.
This module provides utilities for working with gamma distributions to model heterogeneous aquifer pore volumes in groundwater transport analysis. The gamma distribution offers a flexible three-parameter model (shape, scale, location) for representing the natural variability in flow path lengths and residence times within aquifer systems. In heterogeneous aquifers, water travels through multiple flow paths with different pore volumes; the location parameter additionally represents a guaranteed minimum pore volume (for example, immobile porosity or a geometric minimum travel distance).
Parameterizations#
Two equivalent parameterizations are supported, each optionally with a location shift:
(mean, std, loc) — physically intuitive.
meanis the total expected value,stdis the spread (invariant under shift), andlocis the lower bound of support. Constraint:0 <= loc < mean.(alpha, beta, loc) — scipy-style.
alphais shape,betais scale, andlocis the lower bound of support. Constraint:alpha > 0,beta > 0,loc >= 0.
Conversion formulas (with constraint mean > loc):
alpha = ((mean - loc) / std) ** 2 beta = std ** 2 / (mean - loc) mean = alpha * beta + loc std = sqrt(alpha) * beta
When loc == 0 the three-parameter model reduces to the standard two-parameter
gamma distribution.
Available functions:
parse_parameters()- Parse and validate gamma distribution parameters from either (mean, std, loc) or (alpha, beta, loc). Requires exactly one parameter pair and raisesValueErrorif both are supplied; validates positivity and ordering constraints.mean_std_loc_to_alpha_beta()- Convert physically intuitive (mean, std, loc) parameters to gamma shape/scale parameters.alpha_beta_loc_to_mean_std()- Convert gamma (alpha, beta, loc) parameters back to (mean, std) for physical interpretation.bins()- Primary function for transport modeling. Creates discrete probability bins from the (optionally shifted) gamma distribution with equal-probability bins (default) or custom quantile edges. Returns bin edges, expected values (mean pore volume within each bin), and probability masses (weight in transport calculations).
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.gamma.parse_parameters(*, mean=None, std=None, loc=0.0, alpha=None, beta=None)[source]#
Parse parameters for gamma distribution.
Either
(mean, std)or(alpha, beta)must be provided.locis optional and defaults to 0, which recovers the standard two-parameter gamma distribution.- Parameters:
mean (
float|None, default:None) – Mean of the gamma distribution. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution. Must be positive. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for what std represents depending on APVD source.stdis invariant under thelocshift.loc (
float, default:0.0) – Location (horizontal shift) of the gamma distribution; the lower bound of support. Must satisfyloc >= 0and, whenmeanis supplied,loc < mean. Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution (must be > 0).
- Return type:
- Returns:
- Raises:
ValueError – If neither
(mean, std)nor(alpha, beta)is provided, if both pairs are provided, if only one of a pair is provided, ifalphaorbetaare not positive, iflocis negative, or ifmean <= loc.
- gwtransport.gamma.mean_std_loc_to_alpha_beta(*, mean, std, loc=0.0)[source]#
Convert mean, standard deviation, and location of gamma distribution to shape/scale.
The two-parameter shape/scale representation (
alpha,beta) is derived from the excess-over-locmoments:mean_excess = mean - loc,std_excess = std.- Parameters:
mean (
float) – Mean of the gamma distribution. Must be strictly greater thanloc.std (
float) – Standard deviation of the gamma distribution. Must be positive. See Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity for what std represents depending on APVD source.stdis invariant under thelocshift.loc (
float, default:0.0) – Location (horizontal shift) of the gamma distribution. Must satisfy0 <= loc < mean. Default is0.0.
- Return type:
- Returns:
- Raises:
ValueError – If
stdis not positive, iflocis negative, or ifmean <= loc.
See also
alpha_beta_loc_to_mean_stdConvert shape/scale/loc parameters to mean and std.
parse_parametersParse and validate gamma distribution parameters.
Examples
>>> from gwtransport.gamma import mean_std_loc_to_alpha_beta >>> mean_pore_volume = 30000.0 # m³ >>> std_pore_volume = 8100.0 # m³ >>> alpha, beta = mean_std_loc_to_alpha_beta( ... mean=mean_pore_volume, std=std_pore_volume ... ) >>> print(f"Shape parameter (alpha): {alpha:.2f}") Shape parameter (alpha): 13.72 >>> print(f"Scale parameter (beta): {beta:.2f}") Scale parameter (beta): 2187.00
With a 5000 m³ minimum pore volume:
>>> alpha, beta = mean_std_loc_to_alpha_beta(mean=30000.0, std=8100.0, loc=5000.0) >>> print(f"Shape parameter (alpha): {alpha:.2f}") Shape parameter (alpha): 9.53 >>> print(f"Scale parameter (beta): {beta:.2f}") Scale parameter (beta): 2624.40
- gwtransport.gamma.alpha_beta_loc_to_mean_std(*, alpha, beta, loc=0.0)[source]#
Convert shape, scale, and location of gamma distribution to mean and standard deviation.
Parameters are validated via
parse_parameters(), which raisesValueErrorifalphaorbetaare non-positive orlocis negative.- Parameters:
- Return type:
- Returns:
See also
mean_std_loc_to_alpha_betaConvert mean/std/loc to shape and scale parameters.
parse_parametersParse and validate gamma distribution parameters.
Examples
>>> from gwtransport.gamma import alpha_beta_loc_to_mean_std >>> alpha = 13.72 # shape parameter >>> beta = 2187.0 # scale parameter >>> mean, std = alpha_beta_loc_to_mean_std(alpha=alpha, beta=beta) >>> print(f"Mean pore volume: {mean:.0f} m³") Mean pore volume: 30006 m³ >>> print(f"Std pore volume: {std:.0f} m³") Std pore volume: 8101 m³
- gwtransport.gamma.bins(*, mean=None, std=None, loc=0.0, alpha=None, beta=None, n_bins=100, quantile_edges=None)[source]#
Divide a (shifted) gamma distribution into bins and compute bin properties.
If
n_binsis provided, the gamma distribution is divided inton_binsequal-mass bins. Ifquantile_edgesis provided, the distribution is divided into bins defined by those quantile edges. The quantile edges must be a strictly increasing 1-D array of at least 3 entries (>= 2 bins) in[0, 1], with the first and last entries exactly 0 and 1;n_binsis then ignored.- Parameters:
mean (
float|None, default:None) – Mean of the gamma distribution. Must be strictly greater thanloc.std (
float|None, default:None) – Standard deviation of the gamma distribution. Must be positive.loc (
float, default:0.0) – Location (horizontal shift) of the gamma distribution; the lower bound of support. Must satisfy0 <= loc < mean(orloc >= 0when using alpha/beta). Default is0.0.alpha (
float|None, default:None) – Shape parameter of gamma distribution (must be > 0).beta (
float|None, default:None) – Scale parameter of gamma distribution (must be > 0).n_bins (
int, default:100) – Number of bins to divide the gamma distribution (must be >= 2). Default is 100.quantile_edges (
ArrayLike|None, default:None) – Quantile edges for binning. Must be a strictly increasing 1-D array of at least 3 entries (>= 2 bins), all in[0, 1], with the first and last entries exactly 0 and 1. If provided,n_binsis ignored.
- Returns:
Dictionary with keys of type str and values of type numpy.ndarray:
lower_bound: lower bounds of bins (first one equalsloc)upper_bound: upper bounds of bins (last one is inf)edges: bin edges (lower_bound[0], upper_bound[0], …, upper_bound[-1])expected_values: expected values in bins. Is what you would expect to observe if you repeatedly sampled from the probability distribution, but only considered samples that fall within that particular bin.probability_mass: probability mass in bins (invariant underlocshift).
- Return type:
- Raises:
ValueError – If
n_binsis not greater than 1, ifquantile_edgesis not a strictly increasing 1-D array in[0, 1]with endpoints exactly 0 and 1, or if parameter validation inparse_parameters()fails.
See also
mean_std_loc_to_alpha_betaConvert mean/std/loc to alpha/beta parameters.
gwtransport.advection.gamma_infiltration_to_extractionUse bins for transport modeling.
- Gamma Distribution Model
Two-parameter pore volume model.
- Shifted Gamma Distribution (Minimum Pore Volume)
Shifted gamma with minimum pore volume.
- Macrodispersion and Microdispersion as Scale-Dependent Heterogeneity
What
stdrepresents (macrodispersion vs total spreading).- 8. Gamma Distribution Adequacy
When gamma distribution is adequate.
Examples
Create equal-mass bins for a gamma distribution:
>>> from gwtransport.gamma import bins >>> result = bins(mean=30000.0, std=8100.0, n_bins=5)
With a location parameter representing a minimum pore volume:
>>> result = bins(mean=30000.0, std=8100.0, loc=5000.0, n_bins=5) >>> float(result["edges"][0]) 5000.0
Create bins with custom quantile edges:
>>> import numpy as np >>> quantiles = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) >>> result = bins(mean=30000.0, std=8100.0, quantile_edges=quantiles) >>> print(f"Number of bins: {len(result['probability_mass'])}") Number of bins: 4
logremoval#
Log Removal Calculations for First-Order Decay Processes.
This module provides utilities to calculate log removal values from first-order decay processes, including pathogen inactivation and radioactive decay. The module supports basic log removal calculations and parallel flow arrangements where multiple flow paths operate simultaneously.
First-Order Decay Model#
The log removal from any first-order decay process is:
Log Removal = log10_decay_rate * residence_time
where log10_decay_rate has units [log10/day] and residence_time has units [days].
This is equivalent to exponential decay C_out/C_in = 10^(-mu * t), where mu is the
log10 decay rate and t is residence time. The natural-log decay rate constant lambda [1/day]
is related to mu by lambda = mu * ln(10).
This model applies to any process that follows first-order kinetics:
Pathogen inactivation: viruses, bacteria, and protozoa lose infectivity over time
Radioactive decay: isotopes used for groundwater dating (tritium, CFC, SF6)
Chemical degradation: first-order breakdown of contaminants
Pathogen Removal in Bank Filtration#
For pathogen removal during soil passage, total removal consists of two distinct mechanisms (Schijven and Hassanizadeh, 2000):
Inactivation (time-dependent): Pathogens lose infectivity over time through biological decay. This follows first-order kinetics and is modeled by this module as
LR_decay = log10_decay_rate * residence_time. The inactivation rate depends strongly on temperature and pathogen type.Attachment (geometry-dependent): Pathogens are physically removed by adsorption to soil grains and straining. This depends on aquifer geometry, distance, soil properties, and pH, and is NOT modeled by this module. Users should add this component separately based on site-specific data.
Total log removal = LR_decay (this module) + LR_attachment (user-specified).
At the Castricum dune recharge site, Schijven et al. (1999) found that attachment contributed approximately 97% of total MS2 removal, with inactivation contributing only 3%. Inactivation rates for common model viruses at 10 degrees C are typically 0.02-0.11 log10/day (Schijven and Hassanizadeh, 2000, Table 7).
Gamma-distribution parameter notation#
Several functions are parameterized by a gamma distribution. The parameter prefix marks which physical quantity is gamma-distributed, because two distinct quantities appear here:
rt_alpha/rt_beta/rt_loc(or the equivalentrt_mean/rt_std/rt_loc) parameterize the gamma distribution of the residence time (used bygamma_pdf(),gamma_cdf(),gamma_mean()).apv_alpha/apv_beta/apv_loc(or the equivalentapv_mean/apv_std/apv_loc) parameterize the gamma distribution of the aquifer pore volume (used bygamma_find_flow_for_target_mean()).
These prefixes are intentional and load-bearing: residence time and pore volume are different
quantities, so a bare alpha / beta / loc would be ambiguous in this module. Both the
shape/scale and the mean/std pairs are validated through gwtransport.gamma.parse_parameters(),
so invalid parameters (e.g. a negative shape) raise ValueError rather than silently returning
an unphysical result.
Available functions:
residence_time_to_log_removal()- Calculate log removal from residence times and decay rate coefficient. Uses formula: Log Removal = log10_decay_rate * residence_time. Handles single values, 1D arrays, or multi-dimensional arrays of residence times. Returns log removal values with same shape as input.decay_rate_to_log10_decay_rate()- Convert a natural-log decay rate constant lambda [1/day] to a log10 decay rate mu [log10/day].log10_decay_rate_to_decay_rate()- Convert a log10 decay rate mu [log10/day] to a natural-log decay rate constant lambda [1/day].parallel_mean()- Calculate weighted average log removal for parallel flow systems. Computes overall efficiency when multiple treatment paths operate in parallel with different log removal values and flow fractions. Uses formula: Total Log Removal = -log10(sum(F_i * 10^(-LR_i))) where F_i is flow fraction and LR_i is log removal for path i. Supports multi-dimensional arrays via axis parameter for batch processing. Assumes equal flow distribution if flow_fractions not provided.gamma_pdf()- Compute probability density function (PDF) of log removal given gamma-distributed residence time. Since R = mu*T and T ~ Gamma(alpha, beta), R follows a Gamma(alpha, mu*beta) distribution.gamma_cdf()- Compute cumulative distribution function (CDF) of log removal given gamma-distributed residence time. Returns probability that log removal is less than or equal to specified values.gamma_mean()- Compute effective (parallel) mean log removal for gamma-distributed residence time. Uses the moment generating function of the gamma distribution to compute the log-weighted average: LR_eff = mu * loc + alpha * log10(1 + beta * mu * ln(10)).gamma_find_flow_for_target_mean()- Find flow rate that produces specified target effective mean log removal given gamma-distributed aquifer pore volume. Forloc == 0this is the closed-form inverse: flow = beta * mu * ln(10) / (10^(target_mean / alpha) - 1); forloc > 0the transcendental equation is solved numerically.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.logremoval.residence_time_to_log_removal(*, residence_times, log10_decay_rate)[source]#
Compute log removal given residence times and a log10 decay rate.
This function calculates the log removal based on residence times and a log10 decay rate coefficient using first-order decay:
Log Removal = log10_decay_rate * residence_time
This corresponds to exponential decay of pathogen concentration: C_out/C_in = 10^(-log10_decay_rate * residence_time).
- Parameters:
residence_times (
ArrayLike) – Residence times in days. The formula evaluateslog10_decay_rate * residence_timesfor any real input; negative values produce negative log removal (mathematical amplification) and the caller is responsible for sign interpretation.log10_decay_rate (
float) – Log10 decay rate coefficient (log10/day). Relates residence time to log removal efficiency via first-order decay. Negative values correspond to first-order production rather than decay.
- Returns:
log_removals – Array of log removal values corresponding to the input residence times. Same shape as input residence_times.
- Return type:
GenericAlias[floating]
See also
decay_rate_to_log10_decay_rateConvert natural-log decay rate to log10 decay rate
log10_decay_rate_to_decay_rateConvert log10 decay rate to natural-log decay rate
gamma_meanCompute mean log removal for gamma-distributed residence times
gamma_find_flow_for_target_meanFind flow rate to achieve target log removal
parallel_meanCalculate weighted average for parallel flow systems
gwtransport.residence_time.fullCompute residence times from flow and pore volume
- Residence Time
Time in aquifer determines pathogen contact time
Notes
Log removal is a logarithmic measure of pathogen reduction: - Log 1 = 90% reduction - Log 2 = 99% reduction - Log 3 = 99.9% reduction
The first-order decay model is mathematically identical to radioactive decay used in tracer dating. To convert a published natural-log decay rate lambda [1/day] to log10_decay_rate mu [log10/day], use
decay_rate_to_log10_decay_rate().Examples
>>> import numpy as np >>> from gwtransport.logremoval import residence_time_to_log_removal >>> residence_times = np.array([10.0, 20.0, 50.0]) >>> log10_decay_rate = 0.2 >>> residence_time_to_log_removal( ... residence_times=residence_times, log10_decay_rate=log10_decay_rate ... ) array([ 2., 4., 10.])
>>> # Single residence time >>> residence_time_to_log_removal(residence_times=5.0, log10_decay_rate=0.3) np.float64(1.5)
>>> # 2D array of residence times >>> residence_times_2d = np.array([[10.0, 20.0], [30.0, 40.0]]) >>> residence_time_to_log_removal( ... residence_times=residence_times_2d, log10_decay_rate=0.1 ... ) array([[1., 2.], [3., 4.]])
- gwtransport.logremoval.decay_rate_to_log10_decay_rate(decay_rate)[source]#
Convert a natural-log decay rate constant to a log10 decay rate.
Converts lambda [1/day] to mu [log10/day] using the relationship mu = lambda / ln(10).
- Parameters:
decay_rate (
float) – Natural-log first-order decay rate constant lambda (1/day). For example, from tracer dating: lambda = ln(2) / half_life.- Returns:
log10_decay_rate – Log10 decay rate mu (log10/day).
- Return type:
See also
log10_decay_rate_to_decay_rateInverse conversion
residence_time_to_log_removalApply the log10 decay rate
Examples
>>> from gwtransport.logremoval import decay_rate_to_log10_decay_rate >>> import numpy as np >>> # Convert a decay rate of ln(2)/30 (half-life of 30 days) >>> decay_rate = np.log(2) / 30 >>> decay_rate_to_log10_decay_rate(decay_rate) np.float64(0.01003...)
- gwtransport.logremoval.log10_decay_rate_to_decay_rate(log10_decay_rate)[source]#
Convert a log10 decay rate to a natural-log decay rate constant.
Converts mu [log10/day] to lambda [1/day] using the relationship lambda = mu * ln(10).
- Parameters:
log10_decay_rate (
float) – Log10 decay rate mu (log10/day).- Returns:
decay_rate – Natural-log first-order decay rate constant lambda (1/day).
- Return type:
See also
decay_rate_to_log10_decay_rateInverse conversion
Examples
>>> from gwtransport.logremoval import log10_decay_rate_to_decay_rate >>> log10_decay_rate_to_decay_rate(0.2) np.float64(0.4605...)
- gwtransport.logremoval.parallel_mean(*, log_removals, flow_fractions=None, axis=None)[source]#
Calculate the weighted average log removal for a system with parallel flows.
This function computes the overall log removal efficiency of a parallel filtration system. If flow_fractions is not provided, it assumes equal distribution of flow across all paths.
The calculation uses the formula:
Total Log Removal = -log10(sum(F_i * 10^(-LR_i)))
Where: - F_i = fraction of flow through system i (decimal, sum to 1.0) - LR_i = log removal of system i
- Parameters:
log_removals (
ArrayLike) – Array of log removal values for each parallel flow. Each value represents the log10 reduction of pathogens. For multi-dimensional arrays, the parallel mean is computed along the specified axis.flow_fractions (
ArrayLike|None, default:None) – Array of flow fractions for each parallel flow. Must sum to 1.0 along the specified axis and have compatible shape with log_removals. If None, equal flow distribution is assumed (default is None).axis (
int|None, default:None) – Axis along which to compute the parallel mean for multi-dimensional arrays. If None, the reduction matches the waynp.mean/np.sumtreataxis=None: the parallel mean is computed over the flattened input (default is None).
- Returns:
The combined log removal value for the parallel system. Returns a scalar when axis=None, otherwise an array with the specified axis removed.
- Return type:
- Raises:
ValueError – If
flow_fractionsdoes not sum to 1.0 along the specified axis.
See also
residence_time_to_log_removalCompute log removal from residence times
Notes
Log removal is a logarithmic measure of pathogen reduction:
Log 1 = 90% reduction
Log 2 = 99% reduction
Log 3 = 99.9% reduction
For parallel flows, the combined removal is typically less effective than the best individual removal but better than the worst. For systems in series, log removals would be summed directly.
Examples
>>> import numpy as np >>> from gwtransport.logremoval import parallel_mean >>> # Three parallel streams with equal flow and log removals of 3, 4, and 5 >>> log_removals = np.array([3, 4, 5]) >>> parallel_mean(log_removals=log_removals) np.float64(3.431798275933005)
>>> # Two parallel streams with weighted flow >>> log_removals = np.array([3, 5]) >>> flow_fractions = np.array([0.7, 0.3]) >>> parallel_mean(log_removals=log_removals, flow_fractions=flow_fractions) np.float64(3.153044674980176)
>>> # Multi-dimensional array: parallel mean along axis 1 >>> log_removals_2d = np.array([[3, 4, 5], [2, 3, 4]]) >>> parallel_mean(log_removals=log_removals_2d, axis=1) array([3.43179828, 2.43179828])
- gwtransport.logremoval.gamma_pdf(*, r, rt_alpha=None, rt_beta=None, rt_loc=0.0, rt_mean=None, rt_std=None, log10_decay_rate)[source]#
Compute the PDF of log removal given (shifted) gamma-distributed residence time.
With residence time
T = T0 + rt_locwhereT0 ~ Gamma(rt_alpha, rt_beta), the log removalR = mu * Tfollows a shifted gamma distribution with shapert_alpha, scalemu * rt_beta, and locationmu * rt_loc.The residence-time distribution is specified with either
(rt_alpha, rt_beta)or(rt_mean, rt_std)(optionally shifted byrt_loc); both are routed throughgwtransport.gamma.parse_parameters().- Parameters:
r (
ArrayLike) – Log removal values at which to compute the PDF.rt_alpha (
float|None, default:None) – Shape parameter of the gamma distribution for residence time. Must be positive.rt_beta (
float|None, default:None) – Scale parameter of the gamma distribution for residence time (days). Must be positive.rt_loc (
float, default:0.0) – Location (minimum residence time, days) of the residence time distribution. Must be non-negative. Default is0.0.rt_mean (
float|None, default:None) – Mean residence time (days). Alternative tort_alpha; supply withrt_std. Must be strictly greater thanrt_loc.rt_std (
float|None, default:None) – Standard deviation of the residence time (days). Alternative tort_beta; supply withrt_mean. Must be positive.log10_decay_rate (
float) – Log10 decay rate mu (log10/day). Relates residence time to log removal via R = mu * T.
- Returns:
pdf – PDF values corresponding to the input r values.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If parameter validation in
gwtransport.gamma.parse_parameters()fails (e.g.rt_locnegative, non-positive shape/scale, or neither/both parameter pairs supplied).
See also
gamma_cdfCumulative distribution function of log removal
gamma_meanMean of the log removal distribution
- gwtransport.logremoval.gamma_cdf(*, r, rt_alpha=None, rt_beta=None, rt_loc=0.0, rt_mean=None, rt_std=None, log10_decay_rate)[source]#
Compute the CDF of log removal given (shifted) gamma-distributed residence time.
With residence time
T = T0 + rt_locwhereT0 ~ Gamma(rt_alpha, rt_beta), the CDF isP(R <= r) = P(mu*(T0 + rt_loc) <= r) = P(T0 <= (r - mu*rt_loc)/mu)which is the CDF of a shifted gamma distribution with locationmu * rt_loc.The residence-time distribution is specified with either
(rt_alpha, rt_beta)or(rt_mean, rt_std)(optionally shifted byrt_loc); both are routed throughgwtransport.gamma.parse_parameters().- Parameters:
r (
ArrayLike) – Log removal values at which to compute the CDF.rt_alpha (
float|None, default:None) – Shape parameter of the gamma distribution for residence time. Must be positive.rt_beta (
float|None, default:None) – Scale parameter of the gamma distribution for residence time (days). Must be positive.rt_loc (
float, default:0.0) – Location (minimum residence time, days) of the residence time distribution. Must be non-negative. Default is0.0.rt_mean (
float|None, default:None) – Mean residence time (days). Alternative tort_alpha; supply withrt_std. Must be strictly greater thanrt_loc.rt_std (
float|None, default:None) – Standard deviation of the residence time (days). Alternative tort_beta; supply withrt_mean. Must be positive.log10_decay_rate (
float) – Log10 decay rate mu (log10/day). Relates residence time to log removal via R = mu * T.
- Returns:
cdf – CDF values corresponding to the input r values.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If parameter validation in
gwtransport.gamma.parse_parameters()fails (e.g.rt_locnegative, non-positive shape/scale, or neither/both parameter pairs supplied).
See also
gamma_pdfProbability density function of log removal
gamma_meanMean of the log removal distribution
- gwtransport.logremoval.gamma_mean(*, rt_alpha=None, rt_beta=None, rt_loc=0.0, rt_mean=None, rt_std=None, log10_decay_rate)[source]#
Compute the effective (parallel) mean log removal for (shifted) gamma-distributed residence time.
When water travels through multiple flow paths with gamma-distributed residence times, the effective log removal is determined by mixing the output concentrations (not by averaging individual log removals). For a shifted gamma distribution
T = T0 + rt_locwithT0 ~ Gamma(alpha, beta), factoring the moment generating function gives:- LR_eff = -log10(E[10^(-mu*T)])
= -log10(10^(-mu*rt_loc) * E[10^(-mu*T0)]) = mu * rt_loc + alpha * log10(1 + beta * mu * ln(10))
The
rt_locterm shifts the whole log-removal distribution by a constantmu * rt_loc; the alpha/beta term is unchanged. This is always less than the arithmetic meanmu * (alpha * beta + rt_loc)because short residence time paths contribute disproportionately to the output concentration.The residence-time distribution is specified with either
(rt_alpha, rt_beta)or(rt_mean, rt_std)(optionally shifted byrt_loc); both are routed throughgwtransport.gamma.parse_parameters().- Parameters:
rt_alpha (
float|None, default:None) – Shape parameter of the gamma distribution for residence time. Must be positive.rt_beta (
float|None, default:None) – Scale parameter of the gamma distribution for residence time (days). Must be positive.rt_loc (
float, default:0.0) – Location (minimum residence time, days) of the residence time distribution. Must be non-negative. Default is0.0.rt_mean (
float|None, default:None) – Mean residence time (days). Alternative tort_alpha; supply withrt_std. Must be strictly greater thanrt_loc.rt_std (
float|None, default:None) – Standard deviation of the residence time (days). Alternative tort_beta; supply withrt_mean. Must be positive.log10_decay_rate (
float) – Log10 decay rate mu (log10/day).
- Returns:
mean – Effective (parallel) mean log removal value.
- Return type:
- Raises:
ValueError – If parameter validation in
gwtransport.gamma.parse_parameters()fails (e.g.rt_locnegative, non-positive shape/scale, or neither/both parameter pairs supplied).
See also
gamma_find_flow_for_target_meanFind flow for target mean log removal
parallel_meanDiscrete version of this calculation
gamma_pdfPDF of the log removal distribution
gamma_cdfCDF of the log removal distribution
- The Central Concept: Pore Volume Distribution
Why residence times are distributed
- gwtransport.logremoval.gamma_find_flow_for_target_mean(*, target_mean, apv_alpha=None, apv_beta=None, apv_loc=0.0, apv_mean=None, apv_std=None, log10_decay_rate)[source]#
Find the flow rate that produces a target effective mean log removal.
Given a (shifted) gamma-distributed aquifer pore volume with parameters
(apv_alpha, apv_beta, apv_loc), the residence time distribution at flowQis a shifted gamma with shapeapv_alpha, scaleapv_beta/Q, and locationapv_loc/Q. Fromgamma_mean():LR_eff = mu * apv_loc / Q + apv_alpha * log10(1 + (apv_beta/Q) * mu * ln(10))
For
apv_loc == 0this is closed-form:Q = apv_beta * mu * ln(10) / (10^(target_mean / apv_alpha) - 1)
For
apv_loc > 0the equation is transcendental and solved numerically withscipy.optimize.brentq()by bracketing the root in1/Q.The pore-volume distribution is specified with either
(apv_alpha, apv_beta)or(apv_mean, apv_std)(optionally shifted byapv_loc); both are routed throughgwtransport.gamma.parse_parameters().- Parameters:
target_mean (
float) – Target effective mean log removal value. Must be positive.apv_alpha (
float|None, default:None) – Shape parameter of the gamma distribution for aquifer pore volume. Must be positive.apv_beta (
float|None, default:None) – Scale parameter of the gamma distribution for aquifer pore volume. Must be positive.apv_loc (
float, default:0.0) – Location (minimum aquifer pore volume) of the gamma distribution. Must be non-negative. Default is0.0.apv_mean (
float|None, default:None) – Mean aquifer pore volume. Alternative toapv_alpha; supply withapv_std. Must be strictly greater thanapv_loc.apv_std (
float|None, default:None) – Standard deviation of the aquifer pore volume. Alternative toapv_beta; supply withapv_mean. Must be positive.log10_decay_rate (
float) – Log10 decay rate mu (log10/day).
- Returns:
flow – Flow rate (same units as apv_beta per day) that produces the target mean log removal.
- Return type:
- Raises:
ValueError – If
target_meanis not positive, iflog10_decay_rateis not positive (no decay can never produce a positive target log removal), or if parameter validation ingwtransport.gamma.parse_parameters()fails (e.g.apv_locnegative, non-positive shape/scale, or neither/both parameter pairs supplied).
See also
gamma_meanCompute effective mean log removal for given parameters
percolation#
Percolation through thick unsaturated zones via the Kinematic Wave method.
This module provides one public function:
root_zone_to_water_table_kinematic_wave()— exact front-tracking solver for gravity-driven percolation between the bottom of the root zone and the water table, following the Kinematic-Wave method described in Olsthoorn (2026, Stromingen 32(1)). Supports Brooks-Corey and van Genuchten-Mualem constitutive curves and a time-varying multiplicative scaling of K(θ) (e.g. for temperature-corrected viscosity).
Forward-only. Inverse mapping water_table_to_root_zone is not
provided. The KW unsaturated-zone problem is fundamentally one-way under
gravity: multiple q_root_zone(t) series produce indistinguishable
q_water_table(t) after the column’s intrinsic low-pass response,
making the inverse ill-posed. Users wanting an inverse should formulate
it as a regularised inverse problem outside this package.
Cumulative pore-volume coordinate. The position axis is cumulative
pore volume per unit cross-sectional area (units of length), not
geometric depth. For a soil of constant porosity n_p ≡ θ_s and
water-table depth z_wt, the conversion is V_out = θ_s · z_wt.
The docstring of root_zone_to_water_table_kinematic_wave()
spells out the recovery rule and the layered-porosity generalisation.
The full Kinematic-Wave derivation and the constitutive-curve references
are documented on root_zone_to_water_table_kinematic_wave().
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.percolation.root_zone_to_water_table_kinematic_wave(*, q_root_zone, tedges, q_water_table_tedges, cumulative_pore_volumes_outlet, theta_r, theta_s, k_s, brooks_corey_lambda=None, van_genuchten_n=None, mualem_l=0.5, k_scaling=None, max_iterations=10000)[source]#
Percolation flux at the water table by exact Kinematic-Wave front tracking.
Solves the nonlinear scalar conservation law
\[\begin{split}\\frac{\\partial \\theta_m}{\\partial t} + \\frac{\\partial K(\\theta_m)}{\\partial z} = 0\end{split}\]exactly via
gwtransport.fronttracking.solver.FrontTracker, using either a Brooks-Corey or a van Genuchten-Mualem constitutive curve. Implements the Kinematic-Wave method (see [3] for the general theory) described in Olsthoorn (2026) [1]. The capillary term∂ψ/∂zis dropped (gravity drainage only); real fronts are slightly smoothed by capillarity, so if smoothing matters use the Munsflow-style approach ingwtransport.diffusioninstead.- Parameters:
q_root_zone (
ArrayLike) – Root-zone leakage entering the unsaturated zone at the top boundary [length/time, e.g. m/day]. Piecewise constant over each[tedges[i], tedges[i+1])bin. Non-negative. Length =len(tedges) - 1. At any bin,q_root_zone <= f·K_smust hold (withf = k_scalingor 1) for the inlet inversion to be well-defined; the validator raisesValueErrorotherwise.tedges (
DatetimeIndex) – Time bin edges of the input series. Lengthn + 1fornbins.q_water_table_tedges (
DatetimeIndex) – Output time bin edges. Free monotone index whenk_scalingis None; must equaltedgeswhenk_scalingis set (the back-transformq_wt = f · coutis exact only on the input grid). Must lie within the input window[tedges[0], tedges[-1]](the flow series defines the system only there); querying beyond it raises.cumulative_pore_volumes_outlet (
ArrayLike) – Cumulative pore volume per unit cross-sectional area at the water table [length]. For a soil of constant porosity (n_p ≡ θ_s) and water-table depthz_wt, this isθ_s · z_wt. For layered porosity,∫₀^{z_wt} n_p(z') dz'. The geometric depth is recovered asz_wt = V_out / θ_s(uniform case). Array-like to support a distribution of column lengths in parallel (analogous togwtransport.advection.gamma_infiltration_to_extraction()); each entry must be positive.theta_r (
float) – Residual volumetric moisture content [-]. Must satisfy0 <= theta_r < theta_s.theta_s (
float) – Saturated volumetric moisture content [-]. Equal to the porosity for typical soils. Must satisfytheta_r < theta_s < 1.k_s (
float) – Saturated hydraulic conductivity [length/time]. Positive.brooks_corey_lambda (
float|None, default:None) – Brooks-Corey pore-size distribution index [-]. Set to use the Brooks-Corey branch. Mutually exclusive withvan_genuchten_n. Tabulated soil values are available in the Staringreeks [2].van_genuchten_n (
float|None, default:None) – Van Genuchten shape parametern_vG > 1. Set to use the van Genuchten-Mualem branch (numerical inversion via brentq). Mutually exclusive withbrooks_corey_lambda.mualem_l (
float, default:0.5) – Mualem pore-connectivity parameterL. Default 0.5 (standard Mualem). Honored only whenvan_genuchten_nis set.k_scaling (
ArrayLike|None, default:None) –Dimensionless time-only multiplicative factor
f(t)applied to the entireK(θ)curve:K(θ, t) = f(t) · K_reference(θ). Lengthn. Default None meansf ≡ 1. All entries must be strictly positive.The cumulative-flow trick in the underlying front-tracking solver absorbs
f(t)exactly: wave dynamics in cumulative effective time remain flow-free. Typical usage is a temperature-corrected viscosityf(t) = μ_ref / μ(T(t));μvaries ~60% between 5 °C and 25 °C, so seasonal swings of 30-50% in effectiveK_sare realistic for shallow soils.max_iterations (
int, default:10000) – Maximum number of solver events. Default 10000.
- Return type:
- Returns:
q_water_table (
ndarray) – Bin-averaged percolation flux at the water table [same units asq_root_zone], lengthlen(q_water_table_tedges) - 1, averaged across the columns incumulative_pore_volumes_outlet.structures (
listofdict) – Per-column simulation structures (same schema asgwtransport.advection.infiltration_to_extraction_nonlinear_sorption(), withaquifer_pore_volumerenamed tocumulative_pore_volume_outlet):waves— all wave objects.events— event history; each record has"theta"(cumulative effective time) and"type"keys. Translatethetato wall-clock time viatracker_state.t_at_theta(event["theta"]).theta_first_arrival— cumulative effective time at which the first nonzero arrival reaches the outlet.n_events,n_shocks,n_rarefactions,n_characteristics— counts.theta_current— final cumulative effective time.sorption— the sorption object.tracker_state— completeFrontTrackerStatefor the column (usestate.t_at_thetato translateθ → t).cumulative_pore_volume_outlet— the V_out for this column.
- Raises:
ValueError – If inputs are inconsistent (wrong lengths, NaN, negative
q_root_zoneork_scaling, non-finite or non-positivecumulative_pore_volumes_outletork_s), if neither or both sorption-parameter groups are supplied, ifq_root_zone > f(t) * k_sat any bin (saturation/ponding limit), or ifq_water_table_tedgesdoes not equaltedgeswhilek_scalingis provided.- Warns:
UserWarning – If output θ-bins extend beyond the inlet θ-window (i.e. the drying tail of
q_root_zonereaches zero and the column has not yet equilibrated by the last output bin). Bin averages in that region are clamped to zero.
See also
gwtransport.advection.infiltration_to_extraction_nonlinear_sorptionSolute transport with nonlinear sorption (analogous front-tracking algorithm in the saturated-zone domain).
gwtransport.diffusionMunsflow-style linearised advection-diffusion (complementary; smoothed fronts).
gwtransport.fronttracking.math.BrooksCoreyConductivityBrooks-Corey constitutive class.
gwtransport.fronttracking.math.VanGenuchtenMualemConductivityvan Genuchten-Mualem constitutive class.
- Kinematic-Wave Percolation Through Thick Unsaturated Zones
Background on the Kinematic-Wave method for unsaturated-zone percolation.
Notes
Cumulative pore-volume coordinate. The internal V axis is
V(z) = int_0^z n_p(z') dz'(units of length). For a uniform soil withn_p = theta_s,V = theta_s * z; depth is recovered asz = V / theta_s. The solver-side identificationflow = theta_s * f(t)(withfthe optional K-scaling) follows from the chain ruled/dz = theta_s * d/dV.Inlet boundary inversion. The solver works in a reference frame where
K = K_ref(theta_m); the time-varying scaling is moved to the boundary ascin_solver(t) = q_root_zone(t) / f(t)and recovered at the outlet asq_water_table(t) = f(t) * cout(t). The requirementcin_solver <= k_s(i.e.q_root_zone <= f * k_s) is the saturation/ponding admissibility check enforced by the validator.The KW approximation. Capillary stresses are neglected; flow is gravity-only. Wetting fronts are sharp shocks satisfying Rankine-Hugoniot
V_f = (K_1 - K_2)/(theta_1 - theta_2). Drying tails are self-similar rarefaction fans. Real fronts are slightly capillary- smoothed; if that smoothing matters, use Munsflow-style advection-diffusion (the article’s Munsflow method, mapped togwtransport.diffusionin this package).Initial condition. The column starts at
theta_m = theta_r(i.e.K = 0) everywhere. To start from field capacity or a long-term equilibrium, prepend a constant-q spin-up to the input series.Exact mass conservation. Both Brooks-Corey and van Genuchten-Mualem fan integrals use a closed-form integration-by-parts antiderivative derived from the universal identity
R = dC_T/dC: for the spatial fan integralG(u) = C_T(c) * u - kappa * c, and for the temporal fan integralF(theta) = c * (theta - theta_origin) - Delta_v * C_T(c). For Brooks-Corey bothcandC_Tat the endpoints are closed form; for van Genuchten-Mualem they require a singlebrentqcall per endpoint (transcendentalK(theta)). The Burdine variant (mualem_l = 0) admits a closed-form inverse and is fully free of root-finding.References
Examples
Reproduce a 10-year step-response for the article’s soil O05 (coarse sand, Brooks-Corey):
import numpy as np import pandas as pd from gwtransport.percolation import ( root_zone_to_water_table_kinematic_wave, ) tedges = pd.date_range("1995-01-01", "2005-01-01", freq="D") q_root = np.full(len(tedges) - 1, 1e-3) # 1 mm/day q_wt, structures = root_zone_to_water_table_kinematic_wave( q_root_zone=q_root, tedges=tedges, q_water_table_tedges=tedges, cumulative_pore_volumes_outlet=np.array([0.337 * 20.0]), theta_r=0.01, theta_s=0.337, k_s=0.174, brooks_corey_lambda=0.25, )
With time-varying water viscosity:
days = ((tedges[:-1] - tedges[0]) / pd.Timedelta(days=1)).values T = 10.0 + 5.0 * np.sin(2 * np.pi * days / 365.25) # °C mu_ref, dmu_dT = 1.31, -0.027 # mPa·s, linear around 10 °C mu = mu_ref + dmu_dT * (T - 10.0) k_scaling = mu_ref / mu q_wt_visc, _ = root_zone_to_water_table_kinematic_wave( q_root_zone=q_root, tedges=tedges, q_water_table_tedges=tedges, cumulative_pore_volumes_outlet=np.array([0.337 * 20.0]), theta_r=0.01, theta_s=0.337, k_s=0.174, brooks_corey_lambda=0.25, k_scaling=k_scaling, )
radial_asr#
Exact radial advection-dispersion transport for a single well (push-pull / ASR).
Water is injected in an infinite aquifer at a single fully-penetrating well and later recovered at the same well under a signed flow schedule (push-pull / ASR). Transport is radial advection with microdispersion, molecular diffusion, and linear sorption; the spread of velocities across the well screen provides macrodispersion. Forward and backward modeling are supported.
Computes the extracted flux concentration cout at a single fully-penetrating well driven by an
arbitrary signed flow schedule (positive = injection, negative = extraction, zero = rest) and an
arbitrary injected concentration cin. The physics is the exact radial advection-dispersion of the
radial ASR knowledge base: volume coordinate V(r) = pi b n (r^2 - r_w^2), Scheidegger
velocity-dependent dispersion D = alpha_L |u| + D_m (microdispersion alpha_L |u| plus molecular diffusion D_m), Kreft-Zuber flux boundary conditions, and
the exact per-phase kernels (Airy for D_m = 0; the log-derivative Riccati ODE for D_m > 0).
Nothing is reduced to a Gaussian; the exact
non-Gaussian breakthrough (with the correct skewness) is carried.
The forward map is grid-free end to end – no PDE is discretized, so none of the finite-volume
artefacts appear. A single inject-then-extract cycle with no intervening rest uses the closed-form echo operator
(gwtransport._radial_asr_compose, KB Sec. 10a) – exact for arbitrary within-phase variable flow,
with the exact temporal moments. Any other signed-flow schedule (more reversals / multi-cycle ASR, or a
single cycle with a rest under nonzero D_m) uses the reused-propagator-matrix engine
(gwtransport._radial_asr_reuse, KB addendum Sec. A1-A7), which composes the exact per-phase kernels
(Airy / Riccati / Bessel) through the interior two-point Green’s functions. Each per-reversal field
hand-off f_out = P @ f is a bounded linear operator; its matrix P is built once per distinct
(direction, phase volume) from a single batched de Hoog inversion and reused at every recurrence, so
the special-function + inversion cost is O(distinct phase volumes) rather than O(reversals). It is
bit-equivalent, to the de Hoog floor, to the per-reversal grid-free composition. Molecular diffusion during
pumping (the D_m > 0 Whittaker kernel) is evaluated through the log-derivative Riccati ODE
(gwtransport._radial_asr_kernels.resolvent_riccati) – exact to the de Hoog inversion floor at any
A_0/D_m, with no special-function precision cap, and reducing continuously to the Airy branch as
D_m -> 0. During a rest (Q = 0) advection and microdispersion vanish and molecular
diffusion acts alone on the wall-clock clock; it is carried exactly by the order-0 modified Bessel
pure-diffusion kernel, the dominant mixing for seasonal storage / ATES. The only
numerical steps are Gauss-Legendre quadrature and de Hoog Laplace inversion of exact special-function
kernels. An independent finite-volume solve of the same PDE (tests/src/_radial_asr_fv_oracle.py,
KB Sec. 9) is used only as a test oracle. The propagator matrices are assembled on the Bromwich
contour (Re s > 0), where the field hand-off is well-conditioned at any Peclet. The engine is chosen
automatically; cycles are expressed through the flow sign pattern, not
an argument.
The reported cout is the flow-weighted average over each output bin – defined on extraction bins
(flow < 0) and NaN on injection / rest bins (nothing is recovered there).
Macrodispersion within the well screen#
The well screen has a known height; macrodispersion is the spread of arrival times caused by
velocity heterogeneity across the screen. It is modelled as parallel streamtubes (pore_heights):
each streamtube is an independent radial cell carrying the full flow, with an effective pore height
that sets its velocity, and the output is the weight-averaged breakthrough. A streamtube of effective
height b has velocity proportional to 1/b (its pore volume to radius r is
pi b n (r^2 - r_w^2)), so smaller b means faster breakthrough.
gamma_infiltration_to_extraction() builds this ensemble from a gamma distribution of the layer
velocity within the fixed screen height (see that function); the mean velocity is set by the screen
height and the spread by a velocity coefficient of variation. The spread is a within-screen velocity
distribution – velocity heterogeneity across the well screen – not an aquifer pore-volume distribution.
Regional background flow (drift)#
With a steady uniform regional Darcy flux regional_flux (U, drift seepage v_d = U/n) the well
field is superimposed on a regional gradient, so the stored bubble drifts and recovery degrades. The
radial symmetry is broken and the transport is solved by an azimuthal Fourier-mode expansion
c(r, theta) = sum_m c_m(r) e^{i m theta} (m = 0 is the radial engine; drift couples m to
m +- 1), composed through the same per-phase interior Green’s functions
(gwtransport._radial_asr_drift_kernels). regional_flux = 0 (default) dispatches to the radial path
bit-for-bit. The engine is for the slow-drift envelope – the plume (including its rest-phase drift
displacement) must stay well inside the stagnation radius r_s = |A_0|/|v_d| (else a ValueError).
Rest phases (flow == 0) are propagated by the exact free-space drift kernel (translate + anisotropic
spread). The drift-induced recovery loss is validated against an independent 2-D finite-volume oracle.
Available functions:
infiltration_to_extraction()– forward transport (cin -> cout).extraction_to_infiltration()– inverse via Tikhonov regularization (cout -> cin).gamma_infiltration_to_extraction()– gamma-distributed screen velocity (forward).gamma_extraction_to_infiltration()– same, inverse.
References
The references below give the published closed-form solutions for the single-phase radial injection
problem (steady divergent flow from one well) – the per-phase forward kernel this module composes. The
convergent-extraction dual (KB Sec. 7) and the multi-cycle push-pull / ASR composition across flow
reversals are built on top of those kernels here and are not in the single-injection references. All
share the assumptions used here: a single fully-penetrating well in a homogeneous medium with steady
divergent flow v = Q / (2 pi b n r), plus retardation.
The D_m = 0 kernel (velocity-proportional microdispersion D = alpha_L |u|, Airy functions)
is the classical radial-dispersion problem: Tang & Babu (1979) under a Dirichlet (resident-concentration)
well boundary, and Chen (1987) under the Cauchy / third-type (flux) boundary used here – explicitly the
Kreft-Zuber flux concentration, with transfer function Ai(Y) / [Ai(Y0)/2 - p^(1/3) Ai'(Y0)] equal to
the flux operator this module evaluates. The D_m > 0 kernel (D = alpha_L |u| + D_m, Kummer /
confluent-hypergeometric functions) under the same flux boundary, with retardation, is Aichi & Akitaya
(2018) – whose well operator U(a,b) + 2a U(a+1,b+1) is this module’s Whittaker flux boundary; they
record the D_m -> 0 reduction to Chen (1987) as an open problem, which this module performs
continuously – the log-derivative Riccati kernel reduces smoothly to the Airy branch as D_m -> 0.
The alpha_L = 0 limit (constant diffusion, drift-dominated radial transport, Whittaker equation) is
Akanji & Falade (2019). Each is an injection-only solution; none treats extraction or multi-cycle push-pull.
Kreft, A., & Zuber, A. (1978). On the physical meaning of the dispersion equation and its solutions for different initial and boundary conditions. Chemical Engineering Science, 33(11), 1471-1480.
Tang, D. H., & Babu, D. K. (1979). Analytical solution of a velocity dependent dispersion problem. Water Resources Research, 15(6), 1471-1478.
Chen, C.-S. (1987). Analytical solutions for radial dispersion with Cauchy boundary at injection well. Water Resources Research, 23(7), 1217-1224.
Aichi, M., & Akitaya, K. (2018). Analytical solution for a radial advection-dispersion equation including both mechanical dispersion and molecular diffusion for a steady-state flow field in a horizontal aquifer caused by a constant rate injection from a well. Hydrological Research Letters, 12(3), 23-27.
Akanji, L. T., & Falade, G. K. (2019). Closed-form solution of radial transport of tracers in porous media influenced by linear drift. Energies, 12(1), 29.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.radial_asr.infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, pore_heights, porosity, well_radius, longitudinal_dispersivity, molecular_diffusivity=0.0, retardation_factor=1.0, weights=None, background=0.0, regional_flux=0.0, n_modes=None, n_quad=240)[source]#
Compute the extracted flux concentration at a radial well for a signed flow schedule.
- Parameters:
cin (
ArrayLike) – Injected concentration per time bin (used only on injection bins,flow > 0).flow (
ArrayLike) – Signed flow per time bin [m^3/day]:> 0injection,< 0extraction,0rest.tedges (
DatetimeIndex) – Time bin edges (n + 1fornbins).cout_tedges (
DatetimeIndex) – Output time bin edges; must equaltedges. Output is NaN on injection / rest bins.pore_heights (
ArrayLike) – Effective streamtube pore height(s)b[m] – a scalar (one homogeneous screen) or an array of streamtube heights for the velocity-heterogeneity macrodispersion ensemble (each streamtube carries the full flow; smallerb= faster). See the module docstring andgamma_infiltration_to_extraction().porosity (
float) – Porosityn[-].well_radius (
float) – Well (screen) radiusr_w[m].longitudinal_dispersivity (
float) – Longitudinal dispersivityalpha_L[m].molecular_diffusivity (
float, default:0.0) – Molecular diffusivityD_m[m^2/day]. Default 0.D_m = 0uses the vectorized Airy branch;D_m > 0uses the log-derivative Riccati kernel – exact to the de Hoog floor at anyA_0/D_mwith no precision cap, reducing continuously to the Airy branch asD_m -> 0.retardation_factor (
float, default:1.0) – Linear retardationR >= 1. Default 1.weights (
ArrayLike|None, default:None) – Per-streamtube averaging weights (same length aspore_heights). Default equal weights.background (
float, default:0.0) – Ambient aquifer concentrationc_bg. The deviationcin - c_bgis transported andc_bgis added back; constantcin = c_bgreturnscout = c_bg. Default 0.regional_flux (
float, default:0.0) – Steady uniform regional background Darcy fluxU[m/day] in+x(drift seepagev_d = U / n).0(default) reproduces the radial-symmetric engine bit-for-bit. A nonzero value engages the azimuthal-mode block engine, which captures the drift-induced recovery loss (the down-gradient plume is partly swept past the well). The slow-drift envelope requires the plume – including its rest-phase drift displacement – to stay well inside the stagnation radiusr_s = |A_0| / |v_d|(aValueErroris raised otherwise). Rest phases (flow == 0) are propagated by the exact free-space drift kernel (translationv_d t / Rplus anisotropic Gaussian spread, with a Neumann-image closure at the shut well face). See Feasibility envelope under regional drift for a worked multi-year feasibility table.n_modes (
int|None, default:None) – Azimuthal truncationMfor the drift engine (keeps modes-M .. M). DefaultNoneauto-sizesMfrom the plume-front drift ratioeps = v_d R_b / A_0and the rest-phase displacement (clamped to[2, 8]). Ignored whenregional_flux == 0.n_quad (
int, default:240) – Gauss-Legendre node count for the resident-profile superposition. Default 240.
- Returns:
Extracted flux concentration; NaN on injection and rest bins.
- Return type:
GenericAlias[floating]
- gwtransport.radial_asr.extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, pore_heights, porosity, well_radius, longitudinal_dispersivity, molecular_diffusivity=0.0, retardation_factor=1.0, weights=None, background=0.0, regional_flux=0.0, n_modes=None, regularization_strength=1e-10, n_quad=240)[source]#
Recover the injected concentration from extracted-water measurements (Tikhonov inverse).
Inverts the forward operator built by
infiltration_to_extraction(). Returns the injected concentration on injection bins (NaN on extraction / rest bins).- Parameters:
cout (
ArrayLike) – Measured extracted concentration (used on extraction bins,flow < 0).flow (
ArrayLike) – As ininfiltration_to_extraction().tedges (
DatetimeIndex) – As ininfiltration_to_extraction().cout_tedges (
DatetimeIndex) – As ininfiltration_to_extraction().pore_heights (
ArrayLike) – As ininfiltration_to_extraction().porosity (
float) – As ininfiltration_to_extraction().well_radius (
float) – As ininfiltration_to_extraction().longitudinal_dispersivity (
float) – As ininfiltration_to_extraction().molecular_diffusivity (
float, default:0.0) – As ininfiltration_to_extraction().retardation_factor (
float, default:1.0) – As ininfiltration_to_extraction().weights (
ArrayLike|None, default:None) – As ininfiltration_to_extraction().background (
float, default:0.0) – As ininfiltration_to_extraction().regional_flux (
float, default:0.0) – As ininfiltration_to_extraction().n_modes (
int|None, default:None) – As ininfiltration_to_extraction().n_quad (
int, default:240) – As ininfiltration_to_extraction().regularization_strength (
float, default:1e-10) – Tikhonov parameter. Default1e-10.
- Returns:
Recovered injected concentration; NaN on extraction / rest bins.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If
coutcontains NaN on any extraction bin (flow < 0), which would poison the least-squares solve. Structural NaN on injection / rest bins is allowed.
- gwtransport.radial_asr.gamma_infiltration_to_extraction(*, cin, flow, tedges, cout_tedges, porosity, well_radius, longitudinal_dispersivity, screen_height, velocity_cv, n_bins=100, molecular_diffusivity=0.0, retardation_factor=1.0, background=0.0, regional_flux=0.0, n_modes=None, n_quad=240)[source]#
Radial transport with gamma-distributed screen velocity (within-screen macrodispersion).
The well screen has a known height
screen_height; macrodispersion is the spread of arrival times from velocity heterogeneity across that fixed height. The layer velocity is gamma-distributed with mean equal to the homogeneous value (a streamtube at the mean velocity has effective pore heightscreen_height) and coefficient of variationvelocity_cv. A streamtube with velocity ratiorho(gamma, mean 1) has effective pore heightscreen_height / rho– faster layers are thinner and break through sooner. The gamma is discretized inton_binsequal-probability bins (gwtransport.gamma.bins()) and averaged by probability mass viainfiltration_to_extraction().- Parameters:
screen_height (
float) – Known well-screen heightH[m] (the fixed total; the mean streamtube velocity is set by it).velocity_cv (
float) – Coefficient of variation of the layer velocity (the macrodispersion strength).0is a homogeneous screen (a single streamtube, sharp breakthrough); typically< 1– larger values give a heavy slow-velocity tail.n_bins (
int, default:100) – Number of equal-probability velocity bins. Default 100.cin (
ArrayLike) – As ininfiltration_to_extraction().flow (
ArrayLike) – As ininfiltration_to_extraction().tedges (
DatetimeIndex) – As ininfiltration_to_extraction().cout_tedges (
DatetimeIndex) – As ininfiltration_to_extraction().porosity (
float) – As ininfiltration_to_extraction().well_radius (
float) – As ininfiltration_to_extraction().longitudinal_dispersivity (
float) – As ininfiltration_to_extraction().molecular_diffusivity (
float, default:0.0) – As ininfiltration_to_extraction().retardation_factor (
float, default:1.0) – As ininfiltration_to_extraction().background (
float, default:0.0) – As ininfiltration_to_extraction().regional_flux (
float, default:0.0) – As ininfiltration_to_extraction().n_modes (
int|None, default:None) – As ininfiltration_to_extraction().n_quad (
int, default:240) – As ininfiltration_to_extraction().
- Returns:
Extracted flux concentration; NaN on injection / rest bins.
- Return type:
GenericAlias[floating]
- gwtransport.radial_asr.gamma_extraction_to_infiltration(*, cout, flow, tedges, cout_tedges, porosity, well_radius, longitudinal_dispersivity, screen_height, velocity_cv, n_bins=100, molecular_diffusivity=0.0, retardation_factor=1.0, background=0.0, regional_flux=0.0, n_modes=None, regularization_strength=1e-10, n_quad=240)[source]#
Inverse of
gamma_infiltration_to_extraction()(gamma-distributed screen velocity).- Returns:
Recovered injected concentration; NaN on extraction / rest bins.
- Return type:
GenericAlias[floating]
recharge#
Recharge-Driven Transport for Aquifers with Areal Recharge.
Concentration at extraction has two sources. 1) Water infiltrates and is transported through an aquifer with constant thickness to extraction. 2) During transport, rainfall is mixed instantaneously over the height of the aquifer. In an unbounded aquifer all extracted water originates as recharge. Transport is advective with linear sorption; there is no microdispersion, molecular diffusion, or macrodispersion. Only forward modeling is supported. No assumption is made about whether the flow is radial or orthogonal. Two conceptual models share one entry point:
Unbounded aquifer (
aquifer_pore_volume=None): all extracted water originates as recharge. The residence-time distribution is exponential with meanretardation_factor * aquifer_pore_depth / N— independent of the pumping rate, hydraulic conductivity, capture-zone size, and planform shape (Haitjema, 1995). In the cumulative-recharge clocku(t) = ∫ N dt / (retardation_factor * aquifer_pore_depth)(pore volumes flushed) the model is the stationary unit filterdC/du = cin_recharge - C, which this module integrates in closed form per bin. No flow rate is needed.Bounded aquifer (
aquifer_pore_volumeset): the aquifer extent is capped at pore volumeaquifer_pore_volume(strip areaaquifer_pore_volume / aquifer_pore_depth). Water with concentrationcinenters at the upstream side at rateq_b = flow - N * areawhenever extraction exceeds the rainfall on the strip. When rainfall exceeds extraction (q_b < 0) the surplus flows out across the upstream boundary and is lost; the outside has no memory, so when extraction later dominates again the inflow carries the currentcin. The exact solution is the unbounded exponential kernel acting oncin_recharge, truncated at the boundary-entry time of the extracted water, with the residual tail weight placed as an atom oncinat the entry time. With zero recharge this reduces exactly to single-pore-volume piston flow (gwtransport.advection.infiltration_to_extraction()); with the boundary never feeding the well it reduces exactly to the unbounded model.
Available functions:
recharge_to_extraction()- Compute extracted concentration from recharge concentration (and, in the bounded model, upstream-boundary concentration). Exact closed-form solution; output is a flow-weighted (bounded) or recharge-weighted (unbounded) bin average.
References
Haitjema, H.M. (1995). On the residence time distribution in idealized groundwatersheds. Journal of Hydrology, 172(1-4), 127-146. https://doi.org/10.1016/0022-1694(95)02732-5
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.recharge.recharge_to_extraction(*, cin=None, cin_recharge, flow=None, recharge, tedges, cout_tedges, aquifer_pore_volume=None, aquifer_pore_depth, retardation_factor=1.0)[source]#
Compute the concentration of extracted water under uniform areal recharge.
Unbounded model (
aquifer_pore_volume=None): exponential residence-time distribution with meanretardation_factor * aquifer_pore_depth / N(Haitjema, 1995), exact for bin-constant inputs. Bounded model (aquifer_pore_volumeset, together withcinandflow): the exponential kernel is truncated at the upstream-boundary entry time and the residual weight is an atom oncin; water pushed out across the boundary during rainfall surplus is lost.- Parameters:
cin (
ArrayLike|None, default:None) – Concentration of the water entering at the upstream side of the bounded aquifer [concentration units]. Required whenaquifer_pore_volumeis set; must be None otherwise.cin_recharge (
ArrayLike) – Concentration of the recharge water entering via the surface [concentration units]. Length must equallen(tedges) - 1; constant over each interval[tedges[i], tedges[i+1]).flow (
ArrayLike|None, default:None) – Extraction rate [m3/day]. Required whenaquifer_pore_volumeis set; must be None otherwise, because the unbounded model is independent of the pumping rate (see Notes). Must be non-negative and NaN-free.recharge (
ArrayLike) – Areal recharge rate N [m/day; same length unit asaquifer_pore_depth]. Length must equallen(tedges) - 1. Must be non-negative and NaN-free.tedges (
DatetimeIndex) – Time bin edges for the input series.cout_tedges (
DatetimeIndex) – Time bin edges for the output series. Bins not fully inside thetedgesrange return NaN.aquifer_pore_volume (
float|None, default:None) – Pore volume of the bounded aquifer [m3]. The strip area between the upstream boundary and the well isaquifer_pore_volume / aquifer_pore_depth. Default None (unbounded).aquifer_pore_depth (
float) – Pore volume per unit surface area: porosity times saturated thickness [m]. The only static aquifer parameter of the unbounded model.retardation_factor (
float, default:1.0) – Compound retardation factor (>= 1.0), by default 1.0. Dilates the solute clock; mixing fractions are unaffected.
- Returns:
Extracted concentration per
cout_tedgesbin, lengthlen(cout_tedges) - 1. Flow-weighted bin average (bounded model) or recharge-weighted bin average (unbounded model). NaN for bins outside the input time range, for zero-recharge bins (unbounded), and for zero-extraction bins (bounded).- Return type:
GenericAlias[floating]- Raises:
ValueError – If array lengths do not match the bin-edge pattern, inputs contain NaN or negative values, physical parameters are out of range, or only part of the bounded-model triple (
cin,flow,aquifer_pore_volume) is provided.
See also
gwtransport.advection.infiltration_to_extractionZero-recharge limit of the bounded model.
gwtransport.deposition.deposition_to_extractionDistributed source along the flow path.
- Residence Time
Background on residence times.
- Core Transport Equation
Flow-weighted averaging approach.
Notes
The unbounded model needs no flow rate because the capture zone self-adjusts: the well always draws exactly its pumping rate from recharge, over a capture area
flow / N. Pumping harder widens the capture area proportionally, leaving the age composition of the extracted water – set by the ratio of pore storage per unit area (aquifer_pore_depth) to recharge per unit area (N) – unchanged, so the flow rate cancels exactly (Haitjema, 1995). In the bounded model the area is fixed byaquifer_pore_volumeinstead of adjusting to the well, so the flow rate no longer cancels and must be given.Spin-up follows the
"constant"policy: all inputs are treated as constant at their first values beforetedges[0]. For the bounded model this is the steady concentration profileC(V) = cr0 + (cin0 - cr0) * (V_R - apv) / (V_R - V)when the boundary feeds the well (q_b(0) > 0,V_R = flow[0] * aquifer_pore_depth / recharge[0]), and the uniform profilecin_recharge[0]otherwise.Under constant inputs with
flow > N * areathe extracted water is the mass-balance mixturecin_recharge + (cin - cin_recharge) * q_b / flow: an exponential residence-time density carrying the recharge fraction plus a piston atom of massq_b / flowat the boundary-to-well travel time.The exponential kernel lives on the dimensionless clock
uand is parameter-free; the pumping rate enters the bounded model only through the boundary-entry times. All formulas are closed-form (exp/log of bin-local quantities), exact to machine precision for bin-constant inputs.References
Haitjema, H.M. (1995). On the residence time distribution in idealized groundwatersheds. Journal of Hydrology, 172(1-4), 127-146. https://doi.org/10.1016/0022-1694(95)02732-5
Examples
>>> import numpy as np >>> import pandas as pd >>> from gwtransport.recharge import recharge_to_extraction >>> tedges = pd.date_range("2020-01-01", periods=11, freq="D") >>> cout = recharge_to_extraction( ... cin_recharge=np.full(10, 2.5), ... recharge=np.full(10, 0.002), ... tedges=tedges, ... cout_tedges=tedges[3:], ... aquifer_pore_depth=3.0, ... ) >>> np.allclose(cout, 2.5) True
residence_time#
Residence Time Calculations for Retarded Compound Transport.
This module provides functions to compute residence times for compounds traveling through aquifer systems, accounting for flow variability, pore volume, and retardation due to physical or chemical interactions with the aquifer matrix. Residence time represents the duration a compound spends traveling from infiltration to extraction points, depending on flow rate (higher flow yields shorter residence time), pore volume (larger volume yields longer residence time), and retardation factor (interaction with matrix yields longer residence time).
Available functions:
full()- Compute the flow-weighted mean residence time over output bins, per pore volume (full(n_pore_volumes, n_bins)array). Follows the package’s bin-edge convention and is the form consumed elsewhere in the package. Supports both forward (infiltration to extraction) and reverse (extraction to infiltration) directions.mean()- Compute the mean residence time over output bins for a discrete aquifer pore-volume distribution (an array of equally-weighted pore volumes). Collapses the pore-volume axis to a single per-bin series. Thespinuppolicy (default"constant") warm-starts the spin-up by extrapolating the boundary flow.gamma()- Compute the closed-form mean residence time over output bins for a (shifted) gamma aquifer pore-volume distribution, with no pore-volume discretization. Thespinuppolicy (default"constant") warm-starts the spin-up;spinup=0.0instead renormalizes over the covered sub-mass exactly.fraction_explained_full(),fraction_explained_mean(),fraction_explained_gamma()- Compute the advective fraction of each output bin that is explained by the flow record: the flow-weighted share of the bin whose retarded advective parcel was infiltrated/extracted inside the record.fullreturns one row per pore volume,meanthe equal-weight discrete-APVD mean, andgammathe closed-form (shifted) gamma-APVD value, mirroringfull()/mean()/gamma(). These are purely advective – molecular diffusion and microdispersion spread each bin over a range of infiltration times that is not captured here, so no bin is fully informed once dispersion is present (for that dispersive informed fraction use the captured kernel mass of the diffusion coefficient matrix).freundlich_retardation()- Compute concentration-dependent retardation factors from a Freundlich isotherm, for use as theretardation_factorinput to the transport functions.
Spin-up period#
The spin-up region is determined entirely by the supplied flow record (tedges, which
fixes the cumulative throughflow volume V from 0 at the record start to V_end at the
record end) together with the retarded pore volume retardation_factor * V_p – it is not a
length you set. A residence time for an output time needs the corresponding parcel to stay inside
the flow record:
direction='extraction_to_infiltration'looks back to the infiltration event, so the spin-up sits at the start of the output record: the residence time of a pore volumeV_pneedsV(t) >= retardation_factor * V_p(the extracted water was infiltrated before the record began otherwise).direction='infiltration_to_extraction'looks forward to the extraction event, so the spin-up sits at the end of the output record: it needsV_end - V(t) >= retardation_factor * V_p(the infiltrated water is extracted after the record ends otherwise).
The spin-up therefore lengthens with both the pore volume and the retardation factor, and is longest for the largest pore volumes of a distribution.
What happens in that region is governed by a spinup policy, following the package convention
(see gwtransport.advection); full(), mean() and
gamma() all share the contract spinup={'constant'} | None | float in
[0, 1] and the default is "constant" everywhere:
"constant"(default) warm-starts by extrapolating the boundary flow (flow held constant at its first/last value), so no in-record output isNaN.Noneis strict (no extrapolation), marking a pore volumeNaNfor any output bin its parcel leaves the record within. Where the pore-volume axis is collapsed –mean()over a discrete set,gamma()over the continuum – the bin mean then renormalizes over the covered streamtubes / sub-mass, emitted wherever any coverage remains.a
floatcovered-fraction threshold is the strict mode with a minimum coverage gate: the renormalized mean is emitted only where the covered streamtube fraction / sub-mass fraction is at leastspinup(0.0matchesNone; larger values demand more coverage). For the per-pore-volumefull()there is no axis to collapse, so thefloatbehaves exactly likeNone.
Output bins lying wholly outside tedges are NaN under every policy.
The fraction_explained_full() / fraction_explained_mean() /
fraction_explained_gamma() diagnostics report, per output bin, the advective fraction of the
pore-volume distribution that is out of spin-up (1.0 = advectively fully informed, 0.0 =
entirely in spin-up) and are the way to locate the spin-up region when the means warm-start over it.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.residence_time.full(*, flow, tedges, cout_tedges, aquifer_pore_volumes, direction='extraction_to_infiltration', retardation_factor=1.0, spinup='constant')[source]#
Compute the mean residence time over output bins, per pore volume.
The flow-weighted mean residence time is computed over each output interval
[cout_tedges[i], cout_tedges[i + 1])and returned as the full(n_pore_volumes, n_output_bins)array – one row per entry inaquifer_pore_volumes, without collapsing the pore-volume axis. The average is uniform in cumulative throughflow volume, matching the package’s bin-edge convention (and what the diffusion modules consume to compute a per-bin retarded velocity).- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data, as datetime64 objects, defining the flow intervals.cout_tedges (
DatetimeIndex|ndarray) – Output time edges as datetime64 objects;n + 1edges definenoutput bins.aquifer_pore_volumes (
ArrayLike) – Pore volume(s) of the aquifer [m³]. A single value or an array of pore volumes representing different flow paths.direction (
str, default:'extraction_to_infiltration') –Direction of the flow calculation:
’extraction_to_infiltration’: Extraction to infiltration modeling - how many days ago was the extracted water infiltrated.
’infiltration_to_extraction’: Infiltration to extraction modeling - how many days until the infiltrated water is extracted.
Default is ‘extraction_to_infiltration’.
retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. A value greater than 1.0 indicates the compound moves slower than water. Default is 1.0.spinup (
str|float|None, default:'constant') –How to treat the spin-up zone, where a pore volume’s retarded look-back/forward parcel leaves the flow record. Matches the package convention (see
gwtransport.advection).'constant'(default): warm-start – extrapolate the cumulative-volume-to-time map past the record at the boundary flow rates (flow held constant at its first/last value), so the residence time stays finite. No left-edge (extraction) or right-edge (infiltration) spin-upNaN.Noneor afloatin[0, 1]: strict – a pore volume whose parcel leaves the record at any point within an output bin isNaNfor that bin (all-or-nothing per bin), with no extrapolation. This function returns the full per-pore-volume array, so there is no pore-volume axis to collapse; thefloatcovered-fraction threshold therefore behaves identically toNonehere and only takes effect once the axis is collapsed inmean()/gamma().
Output bins lying wholly outside
tedgesareNaNunder either policy.
- Returns:
Mean residence time [days], shape
(n_pore_volumes, n_output_bins). The first dimension corresponds to the pore volumes and the second to thecout_tedgesbins. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole array is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]- Raises:
ValueError – If
tedgesdoes not have exactly one more element thanflow. Ifdirectionis not'extraction_to_infiltration'or'infiltration_to_extraction'. Ifspinupis not'constant',None, or a float in[0, 1].
See also
fraction_explained_fullAdvective fraction of each output bin explained, per pore volume
- Residence Time
Time in aquifer between infiltration and extraction
- Core Transport Equation
Flow-weighted averaging convention
Notes
With the default
spinup='constant'the spin-up zone is warm-started by extrapolating the boundary flow, so no in-record bin isNaN; usefraction_explained_mean()(orspinup=None) to locate the spin-up region. See the module docstring (Spin-up period) for the full rule.The single-streamtube residence time \(\tau(V) = \mathrm{sign}\,[T(V + \mathrm{sign}\,R V_p) - T(V)]\) is piecewise-linear in cumulative throughflow volume \(V\) (\(T\) is the volume \(\to\) time map, \(\mathrm{sign} = -1\) for
extraction_to_infiltrationand \(+1\) forinfiltration_to_extraction). Its flow-weighted bin average is therefore a closed-form difference of the antiderivative \(\Phi(x) = \int_0^x T(w)\,dw\) (piecewise- quadratic), evaluated at four points per pore volume and output bin:\[\bar\tau = \frac{1}{\Delta V}\int_{V_\mathrm{lo}}^{V_\mathrm{hi}} \tau(V)\,dV = \frac{\mathrm{sign}}{\Delta V}\bigl[ \Phi(V_\mathrm{hi} + \mathrm{sign}\,R V_p) - \Phi(V_\mathrm{lo} + \mathrm{sign}\,R V_p) - \Phi(V_\mathrm{hi}) + \Phi(V_\mathrm{lo})\bigr],\]where \(V\) is cumulative throughflow volume (\(dV = Q\,dt\)). This avoids materialising a per-streamtube integration grid, so memory and time scale as the output size \(O(n_\mathrm{pore\ volumes}\cdot n_\mathrm{bins})\). A zero-throughflow output bin (\(\Delta V \to 0\)) has a fixed volume while output time advances, so it degenerates to the pointwise residence time at the bin’s time midpoint.
Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.residence_time import full >>> flow_dates = pd.date_range(start="2023-01-01", end="2023-01-10", freq="D") >>> flow_values = np.full(len(flow_dates) - 1, 100.0) # Constant flow of 100 m³/day >>> mean_times = full( ... flow=flow_values, ... tedges=flow_dates, ... cout_tedges=flow_dates, ... aquifer_pore_volumes=200.0, ... direction="extraction_to_infiltration", ... ) >>> # 200 m³ / 100 m³/day = 2 days residence time; the default constant warm-start >>> # extrapolates the boundary flow, so the left-edge spin-up bins are also 2 days >>> print(mean_times) [[2. 2. 2. 2. 2. 2. 2. 2. 2.]]
- gwtransport.residence_time.mean(*, flow, tedges, cout_tedges, aquifer_pore_volumes, direction='extraction_to_infiltration', retardation_factor=1.0, spinup='constant')[source]#
Compute the mean residence time over output bins for a discrete APVD.
The mean is taken over a discrete set of equally-weighted aquifer pore volumes – one streamtube per entry in
aquifer_pore_volumes. Each streamtube’s flow-weighted bin average is computed withfull()and the pore-volume axis is then collapsed to a single per-output-bin series by averaging over the streamtubes that are valid in each bin. For a continuous (shifted) gamma pore-volume distribution evaluated in closed form, usegamma().The mean is over the valid streamtubes,
\[\bar\tau_b = \frac{1}{|V_b|}\sum_{i \in V_b} \tau_{i,b}, \qquad V_b = \{\, i : \tau_{i,b}\ \mathrm{finite} \,\}.\]With the default
spinup='constant'every streamtube is finite within the flow record (the boundary flow is extrapolated), so this is simply the mean over all pore volumes; withspinup=Noneit renormalizes over the streamtubes that have broken through.- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data, as datetime64 objects, defining the flow intervals.cout_tedges (
DatetimeIndex|ndarray) – Output time edges as datetime64 objects;n + 1edges definenoutput bins.aquifer_pore_volumes (
ArrayLike) – Discrete pore volumes [m³], one per (equally-weighted) streamtube. A single value collapses to the per-streamtube mean offull().direction (
str, default:'extraction_to_infiltration') – Direction of the flow calculation: * ‘extraction_to_infiltration’: how many days ago was the extracted water infiltrated * ‘infiltration_to_extraction’: how many days until the infiltrated water is extracted Default is ‘extraction_to_infiltration’.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. Default is 1.0.spinup (
str|float|None, default:'constant') – Spin-up policy, sharing the contract ofgamma().'constant'(default) warm-starts by extrapolating the boundary flow so no in-record bin isNaN;Noneleaves spin-up streamtubesNaNand the mean renormalizes over those that have broken through (emitted wherever at least one streamtube is valid). Afloatin[0, 1]is the covered-fraction threshold: the renormalized mean is emitted only where the fraction of valid streamtubes is at leastspinup(0.0matchesNone;1.0demands every streamtube; larger values demand more streamtubes to have broken through). Usefraction_explained_mean()to locate the spin-up region.
- Returns:
Mean residence time [days], shape
(n_output_bins,). Output bins with no valid streamtube (outside the flow record, or – withspinup=None– fully in the spin-up zone) are NaN; with afloatspinupso are bins whose valid-streamtube fraction is below the threshold. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole series is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]
See also
gammaExact closed-form mean for a continuous (shifted) gamma APVD
fullPer-pore-volume mean residence time over output bins
fraction_explained_meanAdvective fraction of each output bin explained by the record
gwtransport.gamma.binsDiscretize a gamma APVD into pore-volume bins
- Residence Time
Time in aquifer between infiltration and extraction
Notes
With
spinup=Nonethe spin-up is all-or-nothing per streamtube: a streamtube whose look-back/forward parcel leaves the flow record part-way through an output bin has aNaNbin average (inherited fromfull()) and is dropped from that bin’s mean entirely, rather than contributing its partially-covered share; the bin isNaNonly once every streamtube is in spin-up. In that mode the discrete mean differs fromgamma(), which renormalizes over the covered sub-mass exactly. See the module docstring (Spin-up period) for the full rule.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.residence_time import mean >>> flow_dates = pd.date_range(start="2023-01-01", end="2023-02-10", freq="D") >>> flow_values = np.full(len(flow_dates) - 1, 100.0) # 100 m³/day >>> tau_bar = mean( ... flow=flow_values, ... tedges=flow_dates, ... cout_tedges=flow_dates, ... aquifer_pore_volumes=[400.0, 600.0], # two equally-weighted streamtubes ... ) >>> # Deep in the record: mean pore volume 500 / 100 m³/day = 5 days >>> float(np.round(tau_bar[-1], 6)) 5.0
- gwtransport.residence_time.gamma(*, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, direction='extraction_to_infiltration', retardation_factor=1.0, spinup='constant', _max_tile_elements=1000000)[source]#
Compute the mean residence time over output bins for a (shifted) gamma APVD.
The expectation over a (shifted) gamma aquifer pore-volume distribution (APVD), parameterized by either
(mean, std, loc)or(alpha, beta, loc), is taken in closed form – no pore-volume binning and non_binsaccuracy/cost knob. The bin mean is flow-weighted (uniform in cumulative volume), matching the bin-edge convention of the package, and a single per-output-bin series is returned.The single-streamtube residence time is piecewise-linear in the pore volume \(V_p\), so its per-bin time integral \(G_b(V_p) = \int_{\mathrm{bin}} \tau\,dV\) is piecewise- quadratic in \(V_p\) and the covered length \(L_b(V_p)\) piecewise-linear. The bin mean is the ratio of two closed-form integrals against the gamma density – its zeroth, first and second partial moments (regularized incomplete gamma) – formed once after integrating. The
spinuppolicy sets what happens where part of the APVD lacks flow history:'constant'(default) extrapolates the boundary flow over the full distribution (the package default warm-start), while afloatthreshold renormalizes the mean over the covered sub-mass (0.0reproduces the exact covered-sub-mass conditional mean).- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data, as datetime64 objects, defining the flow intervals.cout_tedges (
DatetimeIndex|ndarray) – Output time edges as datetime64 objects;n + 1edges definenoutput bins.mean (
float|None, default:None) – Mean of the gamma APVD [m³]. Must be strictly greater thanloc. Provide either(mean, std)or(alpha, beta).std (
float|None, default:None) – Standard deviation of the gamma APVD [m³]. Must be positive.loc (
float, default:0.0) – Location (lower bound of support) of the gamma APVD [m³]; a guaranteed minimum pore volume. Must satisfy0 <= loc < mean. Default is 0.0.alpha (
float|None, default:None) – Shape parameter of the gamma APVD (must be > 0).beta (
float|None, default:None) – Scale parameter of the gamma APVD (must be > 0).direction (
str, default:'extraction_to_infiltration') – Direction of the flow calculation: * ‘extraction_to_infiltration’: how many days ago was the extracted water infiltrated * ‘infiltration_to_extraction’: how many days until the infiltrated water is extracted Default is ‘extraction_to_infiltration’.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. Default is 1.0.spinup (
str|float|None, default:'constant') –How to treat the spin-up zone, where part of the gamma APVD lacks flow history. Matches the package convention (see
gwtransport.advection).'constant'(default): warm-start – extrapolate the cumulative-volume-to-time map past the record at the boundary flow rates (flow held constant at its first/last value) and integrate the full distribution, so no in-record bin isNaN.Noneor afloatin[0, 1]: renormalize the mean over the covered sub-mass, emitting a bin only where the covered fraction of the distribution is at least the threshold.Noneand0.0both give the exact covered-sub-mass conditional mean (emit whenever any sub-mass is covered); larger values demand a larger covered fraction, and1.0requires the full distribution to be covered.
Output bins lying wholly outside
tedgesareNaNunder either policy.
- Returns:
APVD-mean residence time [days], shape
(n_output_bins,). Output bins outside the flow record are NaN; with afloatspinupso are bins whose covered fraction is below the threshold. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole series is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]- Raises:
ValueError – If
tedgesdoes not have exactly one more element thanflow. Ifdirectionis not'extraction_to_infiltration'or'infiltration_to_extraction'. Ifspinupis not'constant',None, or a float in[0, 1]. Gamma parameter validation is delegated togwtransport.gamma.parse_parameters().
See also
meanEqually-weighted mean for a discrete set of pore volumes
fullPer-pore-volume mean residence time over output bins
fraction_explained_meanAdvective fraction of each output bin explained by the record
gwtransport.gamma.binsDiscretize a gamma APVD into pore-volume bins
- Residence Time
Time in aquifer between infiltration and extraction
- Gamma Distribution Model
Two-parameter pore volume model
Notes
With the default
spinup='constant'the spin-up is warm-started exactly as inmean()(constant-boundary-flow extrapolation), so the two agree everywhere. Withspinup=0.0the spin-up is instead handled by exact covered-sub-mass renormalization: each output bin integrates over only the pore-volume sub-range with sufficient flow history. See the module docstring (Spin-up period) for the full rule.Examples
>>> import pandas as pd >>> import numpy as np >>> from gwtransport.residence_time import gamma >>> flow_dates = pd.date_range(start="2023-01-01", end="2023-02-10", freq="D") >>> flow_values = np.full(len(flow_dates) - 1, 100.0) # 100 m³/day >>> tau_bar = gamma( ... flow=flow_values, ... tedges=flow_dates, ... cout_tedges=flow_dates, ... mean=500.0, ... std=100.0, ... direction="extraction_to_infiltration", ... ) >>> # Deep in the record the mean residence time approaches mean / flow = 5 days >>> float(np.round(tau_bar[-1], 6)) 5.0
- gwtransport.residence_time.fraction_explained_full(*, flow, tedges, cout_tedges, aquifer_pore_volumes, direction='extraction_to_infiltration', retardation_factor=1.0)[source]#
Advective coverage per pore volume: the fraction of each output bin explained by the record.
For each streamtube (entry in
aquifer_pore_volumes) and each output bin[cout_tedges[i], cout_tedges[i + 1])this returns the flow-weighted fraction of the bin whose retarded advective parcel lies inside the supplied flow record – the share of the bin’s throughflow volume for which the look-back infiltration (extraction_to_infiltration) or look-forward extraction (infiltration_to_extraction) event is covered bycin.1.0means the whole bin is explained for that pore volume,0.0that none of it is. The full(n_pore_volumes, n_output_bins)array is returned – one row per pore volume, mirroringfull().Warning
This is a purely advective diagnostic: it uses only the cumulative-volume look-back
V(t) - retardation_factor * V_pand ignores molecular diffusion and longitudinal dispersion. Those spread each output bin over a range of infiltration times whose kernel tails extend outside any finite record, so a bin that is advectively “fully explained” (1.0) is not fully informed once dispersion is present. For the dispersive informed fraction of an advection-dispersion model use the captured kernel mass (the column sum of the diffusion coefficient matrix), not this function.- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data;n + 1edges fornflow values.cout_tedges (
DatetimeIndex|ndarray) – Output time edges;n + 1edges definenoutput bins.aquifer_pore_volumes (
ArrayLike) – Pore volume(s) of the aquifer [m³], one per streamtube.direction (
str, default:'extraction_to_infiltration') – Direction of the flow calculation. Default is ‘extraction_to_infiltration’.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. Default is 1.0.
- Returns:
Advective coverage [dimensionless], shape
(n_pore_volumes, n_output_bins), values in[0, 1]. Output bins lying wholly outsidetedgesareNaN. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole array is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]- Raises:
ValueError – If
tedgesdoes not have exactly one more element thanflow, or ifdirectionis not'extraction_to_infiltration'or'infiltration_to_extraction'.
See also
fraction_explained_meanEqual-weight mean of this over a discrete APVD
fraction_explained_gammaClosed-form coverage for a (shifted) gamma APVD
fullPer-pore-volume mean residence time over output bins
- Residence Time
Time in aquifer between infiltration and extraction
- gwtransport.residence_time.fraction_explained_mean(*, flow, tedges, cout_tedges, aquifer_pore_volumes, direction='extraction_to_infiltration', retardation_factor=1.0)[source]#
Advective coverage for a discrete APVD: equal-weight mean of
fraction_explained_full().Collapses the pore-volume axis of
fraction_explained_full()to a single per-output-bin series by averaging over the equally-weighted streamtubes inaquifer_pore_volumes– the coverage analogue ofmean().1.0means every streamtube fully explains the bin,0.0that none do.Warning
Purely advective – see
fraction_explained_full(). Molecular diffusion and longitudinal dispersion spreading are not captured, so a value of1.0is advective coverage, not full dispersive information.- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data;n + 1edges fornflow values.cout_tedges (
DatetimeIndex|ndarray) – Output time edges;n + 1edges definenoutput bins.aquifer_pore_volumes (
ArrayLike) – Discrete pore volumes [m³], one per (equally-weighted) streamtube.direction (
str, default:'extraction_to_infiltration') – Direction of the flow calculation. Default is ‘extraction_to_infiltration’.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. Default is 1.0.
- Returns:
Advective coverage [dimensionless], shape
(n_output_bins,), values in[0, 1]. Output bins lying wholly outsidetedgesareNaN. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole series is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]
See also
fraction_explained_fullPer-pore-volume coverage (the array this averages)
fraction_explained_gammaClosed-form coverage for a (shifted) gamma APVD
meanEqually-weighted mean residence time for a discrete APVD
Examples
>>> import numpy as np >>> import pandas as pd >>> from gwtransport.residence_time import fraction_explained_mean >>> tedges = pd.date_range("2020-01-01", periods=11, freq="D") >>> flow = np.full(10, 100.0) >>> fraction_explained_mean( ... flow=flow, ... tedges=tedges, ... cout_tedges=tedges, ... aquifer_pore_volumes=[200.0, 1500.0], ... ).tolist() [0.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
- gwtransport.residence_time.fraction_explained_gamma(*, flow, tedges, cout_tedges, mean=None, std=None, loc=0.0, alpha=None, beta=None, direction='extraction_to_infiltration', retardation_factor=1.0)[source]#
Closed-form advective coverage for a (shifted) gamma APVD.
The expectation of the advective in-record indicator over a (shifted) gamma aquifer pore-volume distribution (APVD), parameterized by either
(mean, std, loc)or(alpha, beta, loc), is taken in closed form – the continuum analogue offraction_explained_mean(), with no pore-volume binning. For each output bin it returns the flow-weighted fraction of the bin whose advective parcel lies inside the flow record.The flow-weighted bin average \(\frac{1}{\Delta V}\int_{V_\mathrm{lo}}^{V_\mathrm{hi}} F_{V_p}(\mathrm{threshold}(V))\,dV\) (with \(\mathrm{threshold}(V) = V / R\) for
extraction_to_infiltrationand \((V_\mathrm{end} - V) / R\) forinfiltration_to_extraction) is evaluated from the antiderivative of the shifted-gamma CDF,\[\Phi(x) = \int_\mathrm{loc}^{x} F_{V_p}(s)\,ds = y\,P(\alpha, y/\beta) - \alpha\beta\,P(\alpha + 1, y/\beta), \qquad y = \max(x - \mathrm{loc},\, 0),\]with \(P\) the regularized lower incomplete gamma function – two CDF evaluations per output edge, no quadrature and no pore-volume binning.
Warning
Purely advective – see
fraction_explained_full(). Molecular diffusion and longitudinal dispersion are not captured; a value of1.0is advective coverage, not full dispersive information.- Parameters:
flow (
ArrayLike) – Flow rate of water in the aquifer [m³/day]. Length matchestedgesminus one.tedges (
DatetimeIndex|ndarray) – Time edges for the flow data;n + 1edges fornflow values.cout_tedges (
DatetimeIndex|ndarray) – Output time edges;n + 1edges definenoutput bins.mean (
float|None, default:None) – Mean of the gamma APVD [m³]. Must be strictly greater thanloc. Provide either(mean, std)or(alpha, beta).std (
float|None, default:None) – Standard deviation of the gamma APVD [m³]. Must be positive.loc (
float, default:0.0) – Location (lower bound of support) of the gamma APVD [m³]. Must satisfy0 <= loc < mean. Default is 0.0.alpha (
float|None, default:None) – Shape parameter of the gamma APVD (must be > 0).beta (
float|None, default:None) – Scale parameter of the gamma APVD (must be > 0).direction (
str, default:'extraction_to_infiltration') – Direction of the flow calculation. Default is ‘extraction_to_infiltration’.retardation_factor (
float, default:1.0) – Retardation factor of the compound in the aquifer [dimensionless]. Default is 1.0.
- Returns:
Advective coverage [dimensionless], shape
(n_output_bins,), values in[0, 1]. Output bins lying wholly outsidetedgesareNaN. Negative orNaNflowmakes the cumulative-volume map non-monotone or undefined; the whole series is returned asNaN(the function refuses rather than raising).- Return type:
GenericAlias[floating]- Raises:
ValueError – If
tedgesdoes not have exactly one more element thanflow, or ifdirectionis not'extraction_to_infiltration'or'infiltration_to_extraction'. Gamma parameter validation is delegated togwtransport.gamma.parse_parameters().
See also
fraction_explained_meanDiscrete equal-weight APVD coverage
fraction_explained_fullPer-pore-volume coverage
gammaClosed-form mean residence time for a (shifted) gamma APVD
- Gamma Distribution Model
Two-parameter pore volume model
- gwtransport.residence_time.freundlich_retardation(*, concentration, freundlich_k, freundlich_n, bulk_density, porosity)[source]#
Compute concentration-dependent retardation factors using Freundlich isotherm.
The Freundlich isotherm relates sorbed concentration s to aqueous concentration C using the heterogeneity-index convention (matching
gwtransport.fronttracking.math.FreundlichSorptionandgwtransport.advection.infiltration_to_extraction_nonlinear_sorption(), so a fittedfreundlich_nis portable across the package):s = k_f * C ^ (1 / n)
The retardation factor is computed as:
R = 1 + (rho_b/θ) * ds/dC = 1 + (rho_b/θ) * k_f * (1/n) * C^(1/n - 1)
- Parameters:
concentration (
ArrayLike) – Concentration of compound in water [mass/volume]. One value per time bin, consistent with theflowarray passed to the transport function.freundlich_k (
float) – Freundlich coefficient [(m³/kg)^(1/n)] (under s = k_f * C^(1/n) with s dimensionless and C in [kg/m³]).freundlich_n (
float) – Freundlich sorption exponent [dimensionless] (heterogeneity index;n = 1recovers a linear isotherm).bulk_density (
float) – Bulk density of aquifer material [mass/volume].porosity (
float) – Porosity of aquifer [dimensionless, 0-1].
- Returns:
Retardation factors for each flow interval. Length equals len(concentration) for use as retardation_factor in the transport functions.
- Return type:
GenericAlias[floating]- Raises:
ValueError – If
porosityis not in(0, 1), ifbulk_densityis not positive, iffreundlich_kis negative, or if anyconcentrationis non-positive whilefreundlich_n > 1(the retardation factor diverges asC -> 0).
See also
fullCompute residence times from flow and pore volume
gwtransport.advection.infiltration_to_extraction_nonlinear_sorptionTransport with nonlinear sorption
- Non-Linear Sorption: Exact Solutions
Freundlich isotherm and concentration-dependent retardation
Examples
>>> concentration = np.array([0.1, 0.2, 0.3]) # same length as flow >>> R = freundlich_retardation( ... concentration=concentration, ... freundlich_k=0.5, ... freundlich_n=2.0, ... bulk_density=1600, # kg/m³ ... porosity=0.35, ... ) >>> # Use R as retardation_factor in the transport functions
deposition_utils#
Utility Functions for the Deposition Module.
This module provides the clipped-trapezoid integral helpers (_clipped_linear_integral
and _positive_part_integral) used by the deposition module’s banded weight builder to
integrate clip(y(x), y_lower, y_upper) over each cin bin of a streamtube’s residence window.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
utils#
General Utilities for 1D Groundwater Transport Modeling.
This module provides general-purpose utility functions for time series manipulation, interpolation, numerical operations, and data processing used throughout the gwtransport package. Functions include linear interpolation/averaging, bin overlap calculations, underdetermined system solvers, and external data retrieval.
Available functions:
step_plot_coords()- Compute step-plot coordinates from bin edges and bin-averaged values. Returns paired x/y arrays for plotting piecewise-constant functions withax.plot(x, y)._make_strictly_monotone(private) - Bump consecutive duplicates in a non-decreasing array byk * ulp(max)so it becomes strictly monotone. Used before V → t inversions to preventnp.interpfrom silently picking one limit at plateau levels.cumulative_flow_volume()- Cumulative infiltrated/extracted volume from per-bin flow rates and bin widths, prepended with a leading zero. Optionally bumped to strict monotonicity for V → t inversions.linear_interpolate()- Linear interpolation using numpy’s optimized interp function. Automatically handles unsorted data with configurable extrapolation (None for clamping, float for constant values). Handles multi-dimensional query arrays.linear_average()- Compute average values of piecewise linear time series between specified x-edges. Supports 1D or 2D edge arrays for batch processing. Handles NaN values and offers multiple extrapolation methods (‘nan’, ‘outer’, ‘raise’).time_bin_overlap()- Calculate fraction of time bins overlapping with specified time ranges. Similar to partial_isin but for time-based bin overlaps with list of (start, end) tuples.simplify_bins()- Simplify a piecewise-constant time series by merging adjacent bins whose values are within a tolerance. Uses volume-weighted (flow x width) averaging when flow is provided, otherwise width-weighted. Direction-independent via largest-jump splitting.compute_time_edges()- Compute DatetimeIndex of time bin edges from explicit edges, start times, or end times. Validates consistency with expected number of bins and handles uniform spacing extrapolation.
The inverse solvers below are two intentionally coexisting families: a Tikhonov family (the dense
solve_inverse_transport() and its banded equivalent solve_inverse_transport_banded(),
both fed by compute_reverse_target() and built on solve_tikhonov()) for the
overdetermined deconvolution in advection/diffusion, and a separate nullspace solver
(solve_underdetermined_system()) for the underdetermined deposition inverse.
solve_tikhonov()- Solve linear system with Tikhonov regularization toward a target. Well-determined modes follow the data; poorly-determined modes are pulled toward the target.compute_reverse_target()- Build the regularization target for the inverse problem by transposing and row-normalizing the forward coefficient matrix. Consumed bysolve_tikhonov()andsolve_inverse_transport().solve_inverse_transport()- Solve the inverse transport problem (deconvolution) via Tikhonov regularization. Shared by advection, diffusion, and diffusion_fastextraction_to_infiltrationfunctions.solve_inverse_transport_banded()- Memory-light banded equivalent ofsolve_inverse_transport()for a forward operator stored in banded layout. Assembles the Tikhonov normal equations directly in banded form and solves them via banded Cholesky.solve_underdetermined_system()- Solve underdetermined linear system (Ax = b, m < n) with nullspace regularization. Handles NaN values by row exclusion. Supports built-in objectives (‘squared_differences’, ‘summed_differences’) or custom callable objectives. Used bygwtransport.deposition.get_soil_temperature()- Download soil temperature data from KNMI weather stations with automatic caching. Supports stations 260 (De Bilt), 273 (Marknesse), 286 (Nieuw Beerta), 323 (Wilhelminadorp). Returns DataFrame with columns TB1-TB5, TNB1-TNB2, TXB1-TXB2 at various depths. Daily cache prevents redundant downloads._generate_failed_coverage_badge(private) - Generate SVG badge indicating failed coverage using genbadge library. Used in CI/CD workflows.
This file is part of gwtransport which is released under AGPL-3.0 license. See the ./LICENSE file or go to gwtransport/gwtransport for full license details.
- gwtransport.utils.step_plot_coords(edges, values)[source]#
Compute step-plot coordinates from bin edges and bin-averaged values.
Converts bin edges (n+1) and bin values (n) into paired x/y arrays suitable for plotting piecewise-constant (step) functions with
ax.plot(x, y).- Parameters:
edges (
ArrayLike) – Bin edges (n+1 elements for n bins). Can be numeric, datetime, or any type accepted bynumpy.repeat().values (
ArrayLike) – Bin-averaged values (n elements), one per bin.
- Return type:
- Returns:
Examples
>>> import numpy as np >>> edges = np.array([0.0, 1.0, 3.0, 6.0]) >>> values = np.array([2.0, 5.0, 1.0]) >>> x, y = step_plot_coords(edges, values) >>> x array([0., 1., 1., 3., 3., 6.]) >>> y array([2., 2., 5., 5., 1., 1.])
- gwtransport.utils.cumulative_flow_volume(flow, dt_days, *, strictly_monotone=False)[source]#
Cumulative infiltrated/extracted volume from per-bin flow rates.
Multiplies each per-bin flow rate by its bin width and accumulates, with a leading zero prepended so the result has one entry per bin edge (n+1 values for n bins). The result is the cumulative volume
Vat each time edge.- Parameters:
flow (
ArrayLike) – Flow rate per bin (m³/day), length n.dt_days (
ArrayLike) – Bin widths in days, length n (e.g.numpy.diffof edge days).strictly_monotone (
bool, default:False) – WhenTrue, bump consecutive duplicates (plateaus fromQ = 0bins) via_make_strictly_monotoneso the cumulative volume is strictly increasing. Required before a V → t inversion; leaveFalsewhen the plateaus must be preserved. Default isFalse.
- Returns:
Cumulative volume at each edge (length
len(flow) + 1), starting at zero.- Return type:
GenericAlias[floating]
See also
_make_strictly_monotone: Bump duplicates before V → t inversion.
- gwtransport.utils.linear_interpolate(*, x_ref, y_ref, x_query, left=None, right=None)[source]#
Linear interpolation using numpy’s optimized interp function.
Automatically handles unsorted reference data by sorting it first.
- Parameters:
x_ref (
ArrayLike) – Reference x-values. If unsorted, will be automatically sorted.y_ref (
ArrayLike) – Reference y-values corresponding to x_ref.x_query (
ArrayLike) – Query x-values where interpolation is needed. Array may have any shape.left (
float|None, default:None) –Value to return for x_query < x_ref[0].
If
left=None: clamp to y_ref[0] (default)If
left=float: use specified value (e.g.,np.nan)
right (
float|None, default:None) –Value to return for x_query > x_ref[-1].
If
right=None: clamp to y_ref[-1] (default)If
right=float: use specified value (e.g.,np.nan)
- Returns:
Interpolated y-values with the same shape as x_query.
- Return type:
GenericAlias[floating]
Examples
Basic interpolation with clamping (default):
>>> import numpy as np >>> from gwtransport.utils import linear_interpolate >>> x_ref = np.array([1.0, 2.0, 3.0, 4.0]) >>> y_ref = np.array([10.0, 20.0, 30.0, 40.0]) >>> x_query = np.array([0.5, 1.5, 2.5, 3.5, 4.5]) >>> linear_interpolate(x_ref=x_ref, y_ref=y_ref, x_query=x_query) array([10., 15., 25., 35., 40.])
Using NaN for extrapolation:
>>> linear_interpolate( ... x_ref=x_ref, y_ref=y_ref, x_query=x_query, left=np.nan, right=np.nan ... ) array([nan, 15., 25., 35., nan])
Handles unsorted reference data automatically:
>>> x_unsorted = np.array([3.0, 1.0, 4.0, 2.0]) >>> y_unsorted = np.array([30.0, 10.0, 40.0, 20.0]) >>> linear_interpolate(x_ref=x_unsorted, y_ref=y_unsorted, x_query=x_query) array([10., 15., 25., 35., 40.])
- gwtransport.utils.linear_average(*, x_data, y_data, x_edges, extrapolate_method='nan')[source]#
Compute the average value of a piecewise linear time series between specified x-edges.
- Parameters:
x_data (
ArrayLike) – x-coordinates of the time series data points, must be in ascending order.y_data (
ArrayLike) –y-coordinates of the time series data points. Can be 1D or 2D.
If 1D: shape
(n_data,)– a single series.If 2D: shape
(n_series_y, n_data)– multiple series sharing the samex_data. The leading axis is averaged independently per row. Cannot be combined with 2Dx_edges(each row ofx_edgesand each row ofy_datawould otherwise have to broadcast against each other, which is not supported).
x_edges (
ArrayLike) –x-coordinates of the integration edges.
If 1D: shape
(n_edges,), must be in ascending order.If 2D: shape
(n_series_x, n_edges), each row must be in ascending order.
extrapolate_method (
str, default:'nan') –Method for handling bin edges that fall outside
x_data. Default is'nan'.'outer': average over the in-range portion of each bin (clip-then-average). The bin width used for normalisation is the clipped width, not the original width. For example,x_data = y_data = [1, 2, 3]andx_edges = [0, 5]returns2.0(integral over[1, 3]divided by clipped width 2), not2.2(which a constant-extension scheme would give).'nan': bins that extend outsidex_dataare returned asnan.'raise': raise an error if any bin edge falls outsidex_data.
- Returns:
2D array of average values between consecutive pairs of x_edges. Shape is
(n_series, n_bins)wheren_bins = n_edges - 1andn_series = max(n_series_x, n_series_y). Bothx_edgesandy_databeing 1D yieldsn_series = 1.- Return type:
GenericAlias[floating]- Raises:
ValueError – If
x_edgesis not 1D or 2D. Ify_datais not 1D or 2D. If bothx_edgesandy_dataare 2D. Ifx_dataandy_datahave incompatible shapes or are empty. Ifx_edgeshas fewer than 2 values per row. Ifx_datais not in ascending order. Ifx_edgesrows are not in ascending order. Ifextrapolate_methodis'raise'and any edge falls outside the data range.
Notes
NaN handling is asymmetric between 1D and 2D ``y_data``.
1D
y_datais treated as a single series; internal NaN gaps are silently bridged by linear interpolation across the gap (vianp.interpwithleft=nan, right=nan).2D
y_datais treated row-wise; any output bin whose[edge_left, edge_right]touches a NaN segment in that row is set to NaN, while other rows are unaffected.
Callers that need NaN-bridging behaviour across multiple series must pre-fill (e.g.,
pd.DataFrame.interpolate) before calling.Examples
>>> import numpy as np >>> from gwtransport.utils import linear_average >>> x_data = [0, 1, 2, 3] >>> y_data = [0, 1, 1, 0] >>> x_edges = [0, 1.5, 3] >>> linear_average( ... x_data=x_data, y_data=y_data, x_edges=x_edges ... ) array([[0.666..., 0.666...]])
>>> x_edges_2d = [[0, 1.5, 3], [0.5, 2, 3]] >>> linear_average(x_data=x_data, y_data=y_data, x_edges=x_edges_2d) array([[0.66666667, 0.66666667], [0.91666667, 0.5 ]])
Multiple y-series with shared x_data and x_edges:
>>> y_data_2d = [[0, 1, 1, 0], [0, 2, 2, 0]] >>> linear_average(x_data=x_data, y_data=y_data_2d, x_edges=x_edges) array([[0.66666667, 0.66666667], [1.33333333, 1.33333333]])
- gwtransport.utils.time_bin_overlap(*, tedges, bin_tedges)[source]#
Calculate the fraction of each time bin that overlaps with each time range.
This function computes an array where element (i, j) represents the fraction of time bin j that overlaps with time range i. The computation uses vectorized operations to avoid loops.
- Parameters:
- Returns:
overlap_array – Array of shape (len(bin_tedges), n_bins) where n_bins is the number of time bins. Each element (i, j) represents the fraction of time bin j that overlaps with time range i. Values range from 0 (no overlap) to 1 (complete overlap).
- Return type:
GenericAlias[floating]- Raises:
ValueError – If
tedgesis not a 1D array, has fewer than 2 elements, or ifbin_tedgesis empty.
Notes
tedges must be sorted in ascending order
Uses vectorized operations to handle large arrays efficiently
Time ranges in bin_tedges can be in any order and can overlap
Examples
>>> import numpy as np >>> from gwtransport.utils import time_bin_overlap >>> tedges = np.array([0, 10, 20, 30]) >>> bin_tedges = [(5, 15), (25, 35)] >>> time_bin_overlap( ... tedges=tedges, bin_tedges=bin_tedges ... ) array([[0.5, 0.5, 0. ], [0. , 0. , 0.5]])
- gwtransport.utils.simplify_bins(*, edges, values, flow=None, tol=0.0)[source]#
Simplify a piecewise-constant time series by merging adjacent bins.
Splits at the largest value jump until the peak-to-peak range within every group does not exceed tol. The result is independent of scan direction.
- Parameters:
edges (
ArrayLike) – Bin edges with shape(n+1,). May be numeric or pandas Timestamps.values (
ArrayLike) – Bin-averaged values with shape(n,)(e.g., concentrations).flow (
ArrayLike|None, default:None) – Flow rate per bin with shape(n,)(e.g., m³/day). When provided, merged-bin values are weighted by volume (flow x bin width) instead of bin width alone.tol (
float, default:0.0) – Maximum peak-to-peak range within a merged group. Default is 0.0, which merges only runs of identical values.
- Return type:
tuple[GenericAlias[floating] |DatetimeIndex,GenericAlias[floating],GenericAlias[floating] |None]- Returns:
new_edges (
ndarrayorDatetimeIndex) – Simplified bin edges with shape(m+1,), preserving the type of edges.new_values (
ndarrayoffloat) – Volume-weighted (or width-weighted) average values per simplified bin, with shape(m,).new_flow (
ndarrayoffloatorNone) – Time-weighted (width-weighted) average flow per simplified bin, with shape(m,). None when flow is not provided.
- gwtransport.utils.compute_time_edges(*, tedges, tstart, tend, number_of_bins)[source]#
Compute time edges for binning data based on provided time parameters.
This function creates a DatetimeIndex of time bin edges from one of three possible input formats: explicit edges, start times, or end times. The resulting edges define the boundaries of time intervals for data binning.
Define either explicit time edges, or start and end times for each bin and leave the others at None.
- Parameters:
tedges (
DatetimeIndex|None) – Explicit time edges for the bins. If provided, must have one more element than the number of bins (n_bins + 1). Takes precedence over tstart and tend.tstart (
DatetimeIndex|None) – Start times for each bin. Must have the same number of elements as the number of bins. Used when tedges is None.tend (
DatetimeIndex|None) – End times for each bin. Must have the same number of elements as the number of bins. Used when both tedges and tstart are None.number_of_bins (
int) – The expected number of time bins. Used for validation against the provided time parameters.
- Returns:
Time edges defining the boundaries of the time bins. Has one more element than number_of_bins.
- Return type:
- Raises:
ValueError – If tedges has incorrect length (not number_of_bins + 1). If tstart has incorrect length (not equal to number_of_bins). If tend has incorrect length (not equal to number_of_bins). If none of tedges, tstart, or tend are provided.
Notes
When using tstart, the function assumes uniform spacing and extrapolates the final edge based on the spacing between the last two start times.
When using tend, the function assumes uniform spacing and extrapolates the first edge based on the spacing between the first two end times.
When
tstartortendare provided with non-uniformly-spaced bins, the extrapolated edge uses only the very first or very last interval and may be physically incorrect: the missing edge is implicitly assigned a bin width equal to that single neighbouring interval, which is unrelated to any other interval in the series. In such cases, supplytedgesdirectly so that all bin widths are explicit.All input time data is converted to pandas.DatetimeIndex for consistency.
- gwtransport.utils.get_soil_temperature(*, station_number=260, interpolate_missing_values=True)[source]#
Download soil temperature data from the KNMI and return it as a pandas DataFrame.
The data is available for the following KNMI weather stations: - 260: De Bilt, the Netherlands (vanaf 1981) - 273: Marknesse, the Netherlands (vanaf 1989) - 286: Nieuw Beerta, the Netherlands (vanaf 1990) - 323: Wilhelminadorp, the Netherlands (vanaf 1989)
TB1 = grondtemperatuur op 5 cm diepte (graden Celsius) tijdens de waarneming TB2 = grondtemperatuur op 10 cm diepte (graden Celsius) tijdens de waarneming TB3 = grondtemperatuur op 20 cm diepte (graden Celsius) tijdens de waarneming TB4 = grondtemperatuur op 50 cm diepte (graden Celsius) tijdens de waarneming TB5 = grondtemperatuur op 100 cm diepte (graden Celsius) tijdens de waarneming TNB2 = minimum grondtemperatuur op 10 cm diepte in de afgelopen 6 uur (graden Celsius) TNB1 = minimum grondtemperatuur op 5 cm diepte in de afgelopen 6 uur (graden Celsius) TXB1 = maximum grondtemperatuur op 5 cm diepte in de afgelopen 6 uur (graden Celsius) TXB2 = maximum grondtemperatuur op 10 cm diepte in de afgelopen 6 uur (graden Celsius)
- Parameters:
station_number (
int, default:260) – The KNMI station number for which to download soil temperature data. Default is 260 (De Bilt).interpolate_missing_values (
bool, default:True) – If True, missing values are interpolated and recent NaN values are extrapolated with the previous value. If False, missing values remain as NaN. Default is True.
- Returns:
DataFrame containing soil temperature data in Celsius with a DatetimeIndex. Columns include TB1, TB2, TB3, TB4, TB5, TNB1, TNB2, TXB1, TXB2.
- Return type:
Notes
KNMI: Royal Netherlands Meteorological Institute
The timeseries may contain NaN values for missing data.
- gwtransport.utils.solve_underdetermined_system(*, coefficient_matrix, rhs_vector, nullspace_objective='squared_differences', optimization_method='BFGS', rcond=None)[source]#
Solve an underdetermined linear system with nullspace regularization.
For an underdetermined system Ax = b where A has more columns than rows, multiple solutions exist. This function computes a least-squares solution and then selects a specific solution from the nullspace based on a regularization objective.
- Parameters:
coefficient_matrix (
ArrayLike) – Coefficient matrix of shape (m, n) where m < n (underdetermined). May contain NaN values in some rows, which will be excluded from the system.rhs_vector (
ArrayLike) – Right-hand side vector of length m. May contain NaN values corresponding to NaN rows in coefficient_matrix, which will be excluded from the system.nullspace_objective (
str|Callable[[GenericAlias[floating],GenericAlias[floating],GenericAlias[floating]],float], default:'squared_differences') –Objective function to minimize in the nullspace. Options:
”squared_differences” : Minimize sum of squared differences between adjacent elements:
sum((x[i+1] - x[i])**2)”summed_differences” : Minimize sum of absolute differences between adjacent elements:
sum(|x[i+1] - x[i]|)callable : Custom objective function with signature
objective(coeffs, x_ls, nullspace_basis)where:coeffs : optimization variables (nullspace coefficients)
x_ls : least-squares solution
nullspace_basis : nullspace basis matrix
Default is “squared_differences”.
optimization_method (
str, default:'BFGS') – Optimization method passed to scipy.optimize.minimize. Default is “BFGS”.rcond (
float|None, default:None) – Cutoff ratio for small singular values in bothnumpy.linalg.lstsqandscipy.linalg.null_space. Singular values smaller thanrcond * largest_singular_valueare treated as zero. Default is None, which uses the default of each function. Increasing rcond truncates more modes, expanding the nullspace available for smoothness optimization. Useful for noisy data.
- Returns:
Solution vector that minimizes the specified nullspace objective. Has length n (number of columns in coefficient_matrix).
- Return type:
GenericAlias[floating]- Raises:
ValueError – If optimization fails, if coefficient_matrix and rhs_vector have incompatible shapes, or if an unknown nullspace objective is specified.
Notes
The algorithm follows these steps:
Remove rows with NaN values from both coefficient_matrix and rhs_vector
Compute least-squares solution: x_ls = pinv(valid_matrix) @ valid_rhs
Compute nullspace basis: N = null_space(valid_matrix)
Find nullspace coefficients: coeffs = argmin objective(x_ls + N @ coeffs)
Return final solution: x = x_ls + N @ coeffs
For the built-in objectives:
“squared_differences” provides smooth solutions, minimizing rapid changes
“summed_differences” provides sparse solutions, promoting piecewise constant behavior
Examples
Basic usage with default squared differences objective:
>>> import numpy as np >>> from gwtransport.utils import solve_underdetermined_system >>> >>> # Create underdetermined system (2 equations, 4 unknowns) >>> matrix = np.array([[1, 2, 1, 0], [0, 1, 2, 1]]) >>> rhs = np.array([3, 4]) >>> >>> # Solve with squared differences regularization >>> x = solve_underdetermined_system(coefficient_matrix=matrix, rhs_vector=rhs) >>> print(f"Solution: {x}") >>> print(f"Residual: {np.linalg.norm(matrix @ x - rhs):.2e}")
With summed differences objective:
>>> x_sparse = solve_underdetermined_system( ... coefficient_matrix=matrix, ... rhs_vector=rhs, ... nullspace_objective="summed_differences", ... )
With custom objective function:
>>> def custom_objective(coeffs, x_ls, nullspace_basis): ... x = x_ls + nullspace_basis @ coeffs ... return np.sum(x**2) # Minimize L2 norm >>> >>> x_custom = solve_underdetermined_system( ... coefficient_matrix=matrix, ... rhs_vector=rhs, ... nullspace_objective=custom_objective, ... )
Handling NaN values:
>>> # System with missing data >>> matrix_nan = np.array([ ... [1, 2, 1, 0], ... [np.nan, np.nan, np.nan, np.nan], ... [0, 1, 2, 1], ... ]) >>> rhs_nan = np.array([3, np.nan, 4]) >>> >>> x_nan = solve_underdetermined_system( ... coefficient_matrix=matrix_nan, rhs_vector=rhs_nan ... )
- gwtransport.utils.compute_reverse_target(*, coeff_matrix, rhs_vector)[source]#
Compute reverse matrix target from forward coefficient matrix.
Constructs a target solution for the inverse problem by transposing the forward coefficient matrix and normalizing rows. For
W_forward[i,j]representing the fraction ofcin[j]arriving incout[i], the transpose-and-normalize approach reconstructscin[j]as a weighted average ofcoutbins, weighted by how muchcin[j]contributed to eachcoutbin.- Parameters:
- Returns:
Target solution vector of length n_cin. Entries with near-zero column sums in the forward matrix are set to NaN.
- Return type:
GenericAlias[floating]
See also
solve_tikhonovConsumes this target as the regularization reference.
- gwtransport.utils.solve_tikhonov(*, coefficient_matrix, rhs_vector, x_target, regularization_strength=1e-10, return_resolution=False)[source]#
Solve a linear system with Tikhonov regularization toward a target.
Minimizes
||A x - b||² + λ ||x - x_target||²by solving the equivalent augmented least-squares problem:[A; √λ I_v] x = [b; √λ x_target_v]
where
I_vselects only entries wherex_targetis not NaN.Well-determined modes (large singular values relative to √λ) are dominated by the data; poorly-determined modes are pulled toward
x_target. The solution varies continuously with λ, unlike the hard singular-value cutoff ofrcondin truncated SVD.- Parameters:
coefficient_matrix (
ArrayLike) – Coefficient matrix of shape (m, n). May contain NaN rows, which are excluded from the system.rhs_vector (
ArrayLike) – Right-hand side vector of length m. May contain NaN values corresponding to NaN rows in coefficient_matrix.x_target (
GenericAlias[floating]) – Target solution of length n, typically fromcompute_reverse_target(). NaN entries are excluded from the regularization term.regularization_strength (
float, default:1e-10) –Tikhonov parameter λ. Controls the tradeoff between fitting the data and staying close to
x_target. Larger values trust the target more; smaller values trust the data more. Default is 1e-10.A good starting value for noisy data is
λ ≈ (noise_std / signal_amplitude)². For noiseless synthetic data, the default 1e-10 preserves machine precision.return_resolution (
bool, default:False) – If True, also return the per-element fraction of the solution that comes from data (vs from the regularization target). Default is False.
- Returns:
If
return_resolutionis False (default), returns the solution vector of length n.If
return_resolutionis True, returns(x, fraction_data)wherefraction_data[j]is the diagonal of the model resolution matrixR = (A^T A + λ D)^{-1} A^T A:fraction_data[j] ≈ 1: element j is data-drivenfraction_data[j] ≈ 0: element j is target-drivenNon-regularized entries (NaN in
x_target):fraction_data[j] = 1.0
- Return type:
GenericAlias[floating] |tuple[GenericAlias[floating],GenericAlias[floating]]- Raises:
ValueError – If
coefficient_matrixandrhs_vectorhave incompatible shapes, or if all rows contain NaN values.
See also
compute_reverse_targetCompute the regularization target from the forward matrix.
solve_underdetermined_systemAlternative solver using nullspace optimization.
- gwtransport.utils.solve_inverse_transport(*, w_forward, observed, n_output, regularization_strength, valid_rows=None, warn_rank_deficient=False)[source]#
Solve the inverse transport problem via Tikhonov regularization.
Given the forward model
w_forward @ x = observed, recoversxby building the regularization target from the transpose ofw_forwardand solving the regularized least-squares problem.- Parameters:
w_forward (
GenericAlias[floating]) – Forward coefficient matrix with shape(n_obs, n_output).observed (
GenericAlias[floating]) – Observed values with shape(n_obs,)(e.g., extraction concentrations).n_output (
int) – Length of the output vector (e.g., number of cin bins).regularization_strength (
float) – Tikhonov regularization parameter.valid_rows (
GenericAlias[bool] |None, default:None) – Which observation rows are valid, with shape(n_obs,). If None, rows withrow_sum > 1e-10are considered valid.warn_rank_deficient (
bool, default:False) – If True, emit a warning when the forward matrix has rank deficiency among its active columns. Default is False.
- Returns:
Recovered signal with shape
(n_output,). NaN for bins with no active columns.- Return type:
GenericAlias[floating]- Warns:
UserWarning – When
warn_rank_deficient=Trueand the matrix is rank-deficient.
See also
solve_inverse_transport_bandedMemory-light banded equivalent.
- gwtransport.utils.solve_inverse_transport_banded(*, band_vals, col_start, observed, n_output, regularization_strength)[source]#
Solve the inverse transport problem from a banded forward operator.
Memory-light equivalent of
solve_inverse_transport()for a forward weight matrix stored in banded layout: rowkof the dense operatorWisband_vals[k]placed at columns[col_start[k], col_start[k] + full_band). The Tikhonov normal equations(WᵀW + λ D) x = Wᵀ observed + λ D x_targetare stored in banded form –WᵀWis symmetric with half-bandwidthfull_band - 1– and Cholesky-factored withscipy.linalg.cholesky_banded(). The Gram matrixWᵀWis built with a single dense BLAS matmul (~24xa per-diagonal scatter) before its sub-diagonals are read into the banded layout. FormingWᵀWsquares the condition number, so the bare Cholesky solve loses accuracy in the under-determined (spin-up nullspace) directions; corrected semi-normal equations restore it by refining with the residual evaluated throughWitself rather thanWᵀW(matching the dense least-squares solution to ~1e-10). The banded Cholesky factor, solve, and refinement stay atO(n_output * full_band); only the one-shot Gram assembly transiently materializesWandWᵀWdensely.The regularization target
x_targetis the transpose-and-normalize ofWapplied toobserved(the banded form ofcompute_reverse_target()), matching the dense solver. Columns with no forward contribution are decoupled (unit diagonal) so the system stays symmetric positive definite, and are returned as NaN.- Parameters:
band_vals (
GenericAlias[floating]) – Banded forward weights of shape(n_obs, full_band). Rows the caller considers invalid must already be zeroed (as_resolve_spinup_maskdoes); zero rows contribute nothing to the normal equations.col_start (
GenericAlias[int_]) – First output-column index of each row’s band, shape(n_obs,).observed (
GenericAlias[floating]) – Observed values of shape(n_obs,)(e.g. extraction concentrations). Must not contain NaN.n_output (
int) – Length of the output vector (number of cin bins).regularization_strength (
float) – Tikhonov parameter λ. Seesolve_inverse_transport(). Must be strictly positive: deconvolution is generically rank-deficient, and λ is what makes the banded Cholesky factor positive definite (unlike the dense least-squares path, this solver cannot return a λ=0 min-norm solution).
- Returns:
Recovered signal of shape
(n_output,). NaN for output bins with no forward contribution (zero column).- Return type:
GenericAlias[floating]- Raises:
ValueError – If
regularization_strengthis not strictly positive.
See also
solve_inverse_transport : Dense-matrix equivalent.
gwtransport.advection_utils._infiltration_to_extraction_weights: Banded builder.