33  Stream-Aquifer Exchange

Surface-Groundwater Interaction Pathways

TipFor Newcomers

You will learn:

  • How water moves between streams and aquifers (both directions!)
  • What “gaining” and “losing” streams mean
  • How to detect flow direction using water level and stream data
  • Why this exchange matters for both water supply and ecosystem health

Streams and aquifers constantly trade water. Sometimes the stream feeds the aquifer (losing stream); sometimes the aquifer feeds the stream (gaining stream). Understanding this exchange helps predict droughts, manage water rights, and protect stream ecosystems.

Data Sources Fused: USGS Stream Gauges + Groundwater Wells + HTEM Structure

33.1 What You Will Learn in This Chapter

By the end of this chapter, you will be able to:

  • Explain the difference between gaining and losing streams and why stream–aquifer exchange matters for both water supply and ecosystems.
  • Interpret stream discharge time series, seasonal distributions, and baseflow separation plots to infer groundwater contributions.
  • Describe how simple spatial and conceptual analyses (well proximity, hydraulic gradients, HTEM-derived K) help locate and characterize exchange zones.
  • Connect exchange concepts in this chapter to recharge estimation, streamflow variability, and fusion-based decision support in other parts of the book.

33.2 Overview

Stream-aquifer exchange is bidirectional - streams can recharge aquifers (losing streams) or aquifers can discharge to streams (gaining streams). This relationship depends on hydraulic gradients, aquifer properties, and streambed conductance. We combine stream discharge measurements, nearby well water levels, and HTEM-derived aquifer structure to quantify these exchanges.

Note💻 For Computer Scientists

Cross-correlation between stream discharge and well water levels reveals time lags and direction of influence. But the sign of the correlation matters:

  • Positive lag: Stream influences aquifer (recharge)
  • Negative lag: Aquifer influences stream (discharge)
  • No correlation: Disconnected systems

HTEM provides the structural context - high-resistivity channels show preferential flow paths.

Tip🌍 For Hydrologists

The exchange flux depends on hydraulic gradient and streambed conductance:

\[Q = K_{bed} \cdot A \cdot \frac{(h_{stream} - h_{aquifer})}{b}\]

Where: - \(K_{bed}\) = Streambed hydraulic conductivity (from HTEM resistivity) - \(A\) = Stream reach area - \(h\) = Hydraulic heads - \(b\) = Streambed thickness

HTEM resistivity near streams constrains \(K_{bed}\) values.

33.3 Analysis Approach

33.4 Stream Discharge Time Series

Note📊 Reading Stream Discharge Patterns

This time series reveals stream-aquifer interaction modes:

Discharge Pattern Interpretation Aquifer Connection
High, stable baseflow Consistent groundwater contribution Gaining stream (aquifer → stream)
Low baseflow, flashy peaks Surface runoff dominates Weak aquifer connection
Seasonal pattern Varies with water table elevation Seasonally reversing (gaining ↔︎ losing)
Declining trend Water table dropping Reduced baseflow contribution

What to Look For:

  • During dry periods (low precip): Does discharge drop to near-zero (losing/disconnected) or stay elevated (gaining)?
  • Storm response: Sharp spikes = surface runoff; Gradual rises = aquifer buffering
  • Baseflow recession: Slow decay after storms = aquifer sustaining flow

Why this matters: Gaining reaches provide ecosystem services (cool water, stable flow for fish). Losing reaches recharge the aquifer but are vulnerable to pollution.

Show code
if stream_df is not None:
    fig = go.Figure()

    fig.add_trace(go.Scatter(
        x=stream_df['date'],
        y=stream_df['discharge_cfs'],
        mode='lines',
        line=dict(color='steelblue', width=1),
        name='Stream Discharge',
        fill='tozeroy',
        fillcolor='rgba(70, 130, 180, 0.2)'
    ))

    fig.update_layout(
        title=f'Stream Discharge Time Series<br><sub>USGS Site {site_no}</sub>',
        xaxis_title='Date',
        yaxis_title='Discharge (cubic feet per second)',
        height=400,
        hovermode='x unified',
        template='plotly_white'
    )

    fig.show()
else:
    print("Stream data not available")
(a) Daily stream discharge showing high variability and seasonal patterns
(b)
Figure 33.1

33.5 Seasonal Discharge Patterns

Note📊 Interpreting Seasonal Flow Patterns

Box plots show discharge distribution by month:

Box Plot Feature Hydrological Meaning
High median in spring (Mar-May) Snowmelt + spring rains → High recharge to aquifer
Low median in late summer (Aug-Sep) Low precip + high ET → Baseflow only (aquifer discharge)
Wide boxes (high IQR) Variable flow regime (storm-driven)
Narrow boxes Stable flow (groundwater-dominated)
Outliers above box Flood events
Outliers below box Drought conditions

Reading for Stream-Aquifer Exchange:

  • Months with narrow boxes: Likely gaining stream periods (stable baseflow)
  • Months with wide boxes: Mixed gaining/losing or storm-runoff dominated
  • Summer low flows: Critical period—aquifer supports stream

Why this matters: Summer low-flow months reveal aquifer contribution. If discharge drops to near-zero, stream is losing or disconnected.

Show code
if stream_df is not None:
    # Add month column
    stream_df['month'] = stream_df['date'].dt.month
    stream_df['month_name'] = stream_df['date'].dt.strftime('%b')

    # Create box plot by month
    fig = go.Figure()

    months_order = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

    for month_num, month_name in enumerate(months_order, 1):
        month_data = stream_df[stream_df['month'] == month_num]['discharge_cfs']

        fig.add_trace(go.Box(
            y=month_data,
            name=month_name,
            boxmean='sd',
            marker_color='steelblue'
        ))

    fig.update_layout(
        title='Seasonal Stream Discharge Distribution',
        xaxis_title='Month',
        yaxis_title='Discharge (cfs)',
        height=400,
        showlegend=False,
        template='plotly_white'
    )

    fig.show()
else:
    print("Stream data not available")
Figure 33.2: Seasonal patterns in stream discharge reveal high flows in spring and early summer

33.6 Baseflow Separation Concept

Note📊 Understanding Baseflow Separation

This visualization partitions total streamflow into components:

Component What It Represents Color/Style
Total discharge (blue shaded area) All water in stream (surface + groundwater) Light blue fill
Baseflow (green line) Groundwater contribution only Green line with markers
Quickflow (gap between lines) Surface runoff (stormflow) Implied (area between)

Interpreting the Separation:

  • Baseflow line near top of shaded area: Stream is mostly groundwater-fed (gaining stream)
  • Large gap between baseflow and total: Runoff-dominated system (flashy response)
  • Flat baseflow line: Steady aquifer contribution
  • Declining baseflow: Aquifer water table dropping (drought impact)

Physical Meaning:

\[\text{Total Discharge} = \text{Baseflow (groundwater)} + \text{Quickflow (runoff)}\]

Baseflow Index (BFI) = Baseflow / Total Discharge: - BFI > 0.7: Groundwater-dominated (strong aquifer connection) - BFI = 0.4-0.7: Mixed system - BFI < 0.4: Runoff-dominated (weak aquifer connection)

Why this matters: Baseflow represents the minimum aquifer contribution. Monitoring baseflow trends reveals aquifer health independent of storm variability.

Show code
if stream_df is not None:
    # Simple baseflow separation using minimum monthly values (simplified)
    stream_df['year_month'] = stream_df['date'].dt.to_period('M')
    monthly_min = stream_df.groupby('year_month')['discharge_cfs'].min().reset_index()
    monthly_min['date'] = monthly_min['year_month'].dt.to_timestamp()

    # Sample subset for visualization
    sample_df = stream_df[stream_df['date'].dt.year >= stream_df['date'].dt.year.max() - 2]

    fig = go.Figure()

    # Total discharge
    fig.add_trace(go.Scatter(
        x=sample_df['date'],
        y=sample_df['discharge_cfs'],
        mode='lines',
        line=dict(color='steelblue', width=1),
        name='Total Discharge',
        fill='tozeroy',
        fillcolor='rgba(70, 130, 180, 0.2)'
    ))

    # Baseflow estimate (minimum monthly values)
    monthly_min_recent = monthly_min[monthly_min['date'] >= sample_df['date'].min()]
    fig.add_trace(go.Scatter(
        x=monthly_min_recent['date'],
        y=monthly_min_recent['discharge_cfs'],
        mode='lines+markers',
        line=dict(color='darkgreen', width=2),
        marker=dict(size=6),
        name='Estimated Baseflow (GW contribution)'
    ))

    fig.update_layout(
        title='Stream Discharge Components<br><sub>Baseflow represents groundwater contribution to stream</sub>',
        xaxis_title='Date',
        yaxis_title='Discharge (cfs)',
        height=400,
        hovermode='x unified',
        template='plotly_white',
        legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
    )

    fig.show()
else:
    print("Stream data not available")
Figure 33.3: Baseflow separation distinguishes groundwater contribution from surface runoff

33.7 Stream-Groundwater Exchange Zones

Note📊 Reading the Well Proximity Classification

This map color-codes wells by expected stream interaction strength:

Color Classification Expected Behavior Management Priority
Red Near stream (<1 km) Strongly coupled to stream stage High—monitor for surface water quality impacts
Orange Moderate distance (1-3 km) Moderate coupling Medium—transitional behavior
Gray Far from stream (>3 km) Minimal direct influence Low—primarily climate-driven

What the Spatial Pattern Reveals:

  • Clustering of red points: Stream reach with strong exchange zone
  • Isolated red points: Local hydraulic connection (paleo-channel, fracture)
  • Absence of red points near stream: Data gap or disconnected stream

Why this matters: Wells near streams are critical for detecting: - Stream-induced recharge (losing reaches) - Aquifer discharge to streams (gaining reaches) - Contamination pathways (streams can transport pollutants to wells)

Show code
if wells_df is not None and len(wells_df) > 0:
    # Classify wells by proximity to typical stream locations
    # Champaign County streams typically run NE-SW
    wells_df['stream_proximity_score'] = (
        np.abs(wells_df['Latitude'] - 40.1) +
        np.abs(wells_df['Longitude'] + 88.2)
    )

    # Classify into zones
    wells_df['zone'] = pd.cut(
        wells_df['stream_proximity_score'],
        bins=[0, 0.1, 0.3, np.inf],
        labels=['Near Stream', 'Moderate Distance', 'Far from Stream']
    )

    # Create scatter plot (NOT mapbox)
    fig = go.Figure()

    colors = {'Near Stream': '#e74c3c', 'Moderate Distance': '#f39c12', 'Far from Stream': '#95a5a6'}

    for zone in ['Near Stream', 'Moderate Distance', 'Far from Stream']:
        zone_data = wells_df[wells_df['zone'] == zone]

        fig.add_trace(go.Scatter(
            x=zone_data['Longitude'],
            y=zone_data['Latitude'],
            mode='markers',
            marker=dict(
                size=8,
                color=colors[zone],
                opacity=0.6,
                line=dict(width=0.5, color='white')
            ),
            text=[f"Well {p}<br>{m} measurements" for p, m in
                  zip(zone_data['P_NUMBER'], zone_data['measurement_count'])],
            name=zone,
            hovertemplate='<b>%{text}</b><br>Lat: %{y:.4f}<br>Lon: %{x:.4f}<extra></extra>'
        ))

    fig.update_layout(
        title='Groundwater Monitoring Wells by Stream Proximity<br><sub>Red=Near streams (high exchange potential), Gray=Far (low exchange)</sub>',
        xaxis_title='Longitude',
        yaxis_title='Latitude',
        height=500,
        template='plotly_white',
        legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
    )

    fig.update_xaxes(scaleanchor="y", scaleratio=1)

    fig.show()
else:
    print("Well data not available")
Figure 33.4: Spatial distribution of monitoring wells showing potential stream-aquifer interaction zones

33.8 Stream-Aquifer Gradient Schematic

Note📊 How to Read This Conceptual Diagram

This schematic illustrates the two exchange modes:

Side of Diagram Stream Type Hydraulic Gradient Water Flow Direction
Left (Blue/Green) Gaining stream Water table > Stream stage Aquifer → Stream (discharge)
Right (Red/Orange) Losing stream Stream stage > Water table Stream → Aquifer (recharge)

Key Features to Understand:

  • Blue dashed line: Water table elevation
  • Solid blue/red line: Stream water surface
  • Green arrow (left): Groundwater discharging into stream
  • Orange arrow (right): Stream water infiltrating to aquifer
  • Vertical gradient: Steeper gradient = faster exchange

Physical Principle:

\[\text{Exchange Direction} = \text{sign}(h_{stream} - h_{aquifer})\]

  • If \(h_{stream} > h_{aquifer}\): Losing stream (recharge)
  • If \(h_{aquifer} > h_{stream}\): Gaining stream (baseflow)

Why this matters: Exchange can reverse seasonally: - Spring high water: Stream stage high → Losing (recharges aquifer) - Summer low flow: Water table high → Gaining (sustains stream)

Show code
# Create conceptual diagram
fig = go.Figure()

# Gaining stream scenario (GW -> Stream)
x_gaining = np.linspace(0, 100, 50)
water_table_gaining = 210 - 0.05 * x_gaining + 2 * np.sin(x_gaining / 15)
stream_level = 205

# Losing stream scenario (Stream -> GW)
x_losing = np.linspace(110, 210, 50)
water_table_losing = 200 + 0.03 * (x_losing - 110) + 1.5 * np.sin((x_losing - 110) / 15)
stream_level_losing = 208

# Plot gaining stream
fig.add_trace(go.Scatter(
    x=x_gaining,
    y=water_table_gaining,
    mode='lines',
    line=dict(color='blue', width=3, dash='dash'),
    name='Water Table (Gaining)',
    fill='tozeroy',
    fillcolor='rgba(100, 149, 237, 0.2)'
))

fig.add_trace(go.Scatter(
    x=[45, 55],
    y=[stream_level, stream_level],
    mode='lines',
    line=dict(color='darkblue', width=6),
    name='Stream (Gaining)',
    showlegend=True
))

# Add arrow showing GW -> Stream
fig.add_annotation(
    x=50, y=stream_level + 2,
    ax=52, ay=water_table_gaining[25] - 2,
    xref='x', yref='y',
    axref='x', ayref='y',
    showarrow=True,
    arrowhead=2,
    arrowsize=1.5,
    arrowwidth=2,
    arrowcolor='green'
)

# Plot losing stream
fig.add_trace(go.Scatter(
    x=x_losing,
    y=water_table_losing,
    mode='lines',
    line=dict(color='red', width=3, dash='dash'),
    name='Water Table (Losing)',
    fill='tozeroy',
    fillcolor='rgba(255, 99, 71, 0.2)'
))

fig.add_trace(go.Scatter(
    x=[155, 165],
    y=[stream_level_losing, stream_level_losing],
    mode='lines',
    line=dict(color='darkred', width=6),
    name='Stream (Losing)',
    showlegend=True
))

# Add arrow showing Stream -> GW
fig.add_annotation(
    x=160, y=water_table_losing[25] + 2,
    ax=162, ay=stream_level_losing - 2,
    xref='x', yref='y',
    axref='x', ayref='y',
    showarrow=True,
    arrowhead=2,
    arrowsize=1.5,
    arrowwidth=2,
    arrowcolor='orange'
)

# Add labels
fig.add_annotation(x=50, y=220, text="<b>GAINING STREAM</b><br>GW discharges to stream",
                  showarrow=False, font=dict(size=12, color='blue'))
fig.add_annotation(x=160, y=220, text="<b>LOSING STREAM</b><br>Stream recharges aquifer",
                  showarrow=False, font=dict(size=12, color='red'))

fig.update_layout(
    title='Hydraulic Gradient Controls Stream-Aquifer Exchange Direction',
    xaxis_title='Distance (arbitrary units)',
    yaxis_title='Elevation (m)',
    height=400,
    template='plotly_white',
    showlegend=True,
    legend=dict(yanchor="bottom", y=0.01, xanchor="right", x=0.99)
)

fig.update_xaxes(showticklabels=False)

fig.show()
Figure 33.5: Conceptual model of hydraulic gradients controlling stream-aquifer exchange

33.9 Cross-Correlation Analysis (Conceptual)

Detect time lags between stream discharge and well responses:

Show code
def compute_cross_correlation(stream_ts, well_ts, max_lag_days=90):
    """
    Compute cross-correlation between stream and well time series.

    Returns:
        lags: Time lags (days)
        corr: Correlation coefficients
        optimal_lag: Lag with maximum absolute correlation
        direction: 'stream->aquifer' or 'aquifer->stream'
    """
    # This function would:
    # 1. Merge stream and well time series on date
    # 2. Normalize both series
    # 3. Compute cross-correlation at different time lags
    # 4. Identify optimal lag (maximum correlation)
    # 5. Determine exchange direction based on lag sign
    pass  # Conceptual only

# Example: Cross-correlation would reveal:
# - Positive lag: Stream influences aquifer (losing stream)
# - Negative lag: Aquifer influences stream (gaining stream)
# - No correlation: Systems are disconnected

33.10 HTEM-Constrained Streambed Conductivity

Show code
# Resistivity-to-K relationship (Archie's Law, simplified)
resistivity_range = np.logspace(0, 3, 100)  # 1 to 1000 ohm-m
k_estimated = 0.1 * (resistivity_range ** 0.8)  # Empirical relationship

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=resistivity_range,
    y=k_estimated,
    mode='lines',
    line=dict(color='purple', width=3),
    name='K = 0.1 × ρ^0.8',
    fill='tozeroy',
    fillcolor='rgba(128, 0, 128, 0.1)'
))

# Add reference zones
fig.add_vrect(x0=1, x1=35, fillcolor='brown', opacity=0.1,
              annotation_text="Clay/Silt", annotation_position="top left")
fig.add_vrect(x0=35, x1=120, fillcolor='orange', opacity=0.1,
              annotation_text="Mixed Sediments", annotation_position="top left")
fig.add_vrect(x0=120, x1=1000, fillcolor='gold', opacity=0.1,
              annotation_text="Sand/Gravel", annotation_position="top left")

fig.update_layout(
    title='Resistivity-Hydraulic Conductivity Relationship<br><sub>HTEM resistivity constrains streambed K values</sub>',
    xaxis_title='Electrical Resistivity (Ω·m)',
    yaxis_title='Hydraulic Conductivity (m/day)',
    xaxis_type='log',
    yaxis_type='log',
    height=400,
    template='plotly_white',
    showlegend=False
)

fig.show()
Figure 33.6: Empirical relationship between electrical resistivity and hydraulic conductivity
Note📊 Interpreting the Resistivity-K Relationship

This log-log plot shows how HTEM constrains hydraulic properties:

Resistivity Zone K Range (m/day) Material Exchange Potential
1-35 Ω·m (Brown) 0.01-1 Clay/Silt Low—weak exchange
35-120 Ω·m (Orange) 1-10 Mixed sediments Moderate exchange
120-1000 Ω·m (Yellow) 10-100+ Sand/Gravel High—strong exchange

Reading the Curve:

  • Power-law relationship: K ∝ ρ^0.8 (approximate empirical relationship)
  • Order-of-magnitude variation: 100× change in resistivity → ~60× change in K
  • Overlap zones: Resistivity 50-150 Ω·m could be sandy silt OR silty sand

Why This Matters for Exchange:

Exchange flux: \(Q = K_{bed} \cdot A \cdot \frac{\Delta h}{b}\)

  • High-K streambed (sand/gravel): Strong exchange, rapid recharge/discharge
  • Low-K streambed (clay): Weak exchange, stream hydraulically disconnected from aquifer
  • HTEM provides K estimate without drilling/testing

Management Application: Target high-resistivity stream reaches for: - Managed aquifer recharge (MAR) projects - Bank filtration water supply - Avoiding to protect from contamination (pollution highways)

Tip🌍 For Hydrologists: HTEM Constraints

The HTEM resistivity near streams provides critical constraints on streambed conductivity:

  • High resistivity (>100 Ω·m): Sand/gravel streambed → High K → Strong exchange
  • Low resistivity (<35 Ω·m): Clay-rich streambed → Low K → Weak exchange

This physical property directly controls the exchange flux:

\[Q = K_{bed} \cdot A \cdot \frac{\Delta h}{b}\]

Where HTEM provides \(K_{bed}\) through the resistivity-conductivity relationship.

33.11 Key Insights

Important🔍 Stream-Aquifer Exchange Findings

Spatial Patterns:

  • Stream-aquifer interaction zones vary spatially based on hydraulic gradients
  • Wells near streams show higher potential for exchange
  • Exchange direction (gaining vs losing) depends on relative water levels

Temporal Dynamics:

  • Stream discharge shows strong seasonal patterns (high in spring/summer)
  • Baseflow represents groundwater contribution to streams
  • Exchange can reverse seasonally based on precipitation and pumping

Data Integration Benefits:

  • USGS stream gauges provide surface water context
  • Groundwater wells reveal aquifer response to streams
  • HTEM resistivity constrains streambed conductivity

33.12 Implications for Management

  1. Water Supply: Gaining reaches provide baseflow to streams (ecological benefit)
  2. Recharge Zones: Losing reaches important for aquifer recharge during high flow
  3. Contamination Risk: Losing reaches are vulnerable to surface water contamination
  4. Monitoring Strategy: Focus on transitional zones where direction varies

33.13 References

  • Barlow, P. M., & Harbaugh, A. W. (2006). USGS Directions in MODFLOW Development. Ground Water, 44(6), 771-774.
  • Winter, T. C., et al. (1998). Ground Water and Surface Water: A Single Resource. USGS Circular 1139.
  • Kalbus, E., et al. (2006). Measuring methods for groundwater-surface water interactions. Hydrology and Earth System Sciences, 10(6), 873-887.

33.14 Next Steps

Chapter 5: HTEM-Groundwater Fusion - How subsurface structure controls water levels

Cross-Chapter Connections: - Uses stream data introduced in Part 1 - Applies correlation methods from Part 2 - Informs recharge estimation (Chapter 3) - Foundation for temporal fusion (Chapter 8)


33.15 Summary

Stream-aquifer exchange analysis quantifies surface-groundwater connectivity:

Gaining vs. losing reaches identified - Streams both recharge and discharge aquifer

Baseflow separation - Isolates groundwater contribution to streamflow

Seasonal patterns - Exchange direction varies with water table elevation

HTEM validation - Stream connectivity consistent with resistivity patterns

⚠️ 3-25 km well-stream separation - Direct correlation limited by monitoring gaps

Key Insight: Streams and aquifers are a single resource. Managing one without considering the other leads to unintended consequences.


33.16 Reflection Questions

  • Think about a specific stream reach in your region. Based on what you know (or can observe) about water levels and flows, would you expect it to be gaining, losing, or seasonally reversing, and what additional data from wells or gauges would confirm that?
  • Looking at the kinds of plots in this chapter (time series, seasonal distributions, baseflow separation, conceptual gradients), which two or three would you prioritize for communicating stream–aquifer connectivity to a non-technical stakeholder, and why?
  • How could HTEM-derived streambed conductivity information change your interpretation of exchange (for example, turning a suspected connection into a confirmed “leaky barrier” or “fast pathway”)?
  • If monitoring budgets are limited, how would you place new wells or gauges to reduce the 3–25 km separation mentioned in the summary and improve your ability to quantify exchange?