Gaussian Plumes: Simulation


Lecture 11

October 5, 2026

Review of Last Class

The Gaussian Plume Model

\[\begin{aligned} C(x,y,z) &= \frac{Q}{2\pi u\sigma_y\sigma_z}\exp\left(\frac{-y^2}{2\sigma_y^2}\right) \times \\ &\left[\exp\left(\frac{-(z-H)^2}{2\sigma_z^2}\right) + \exp\left(\frac{-(z+H)^2}{2\sigma_z^2}\right)\right] \end{aligned}\]

Number of key assumptions: each was used in converting the advection-diffusion equation to a 2D heat equation and obtaining this solution. Without these holding, this formula does not work.

Questions?

Poll Everywhere QR Code

Text: VSRIKRISH to 22333

URL: https://pollev.com/vsrikrish

See Results

Gaussian Plume Model Behavior

Visualizing a 3D Model

3D visualization is challenging. We can only ever look at 2D slices of \(C(x, y, z)\), such as:

  • Ground-level view, \(z = 0\).
  • Vertical section, \(y = 0\).
  • Crosswind profile, fixed \(x\).

The Ground-Level Footprint

Code
downwind_positions = 0.05:0.02:8.0
crosswind_positions = -700:5:700
footprint = [plume_ugm3(x, y, 0.0, plume_params) for y in crosswind_positions, x in downwind_positions]

p_foot = contourf(downwind_positions, crosswind_positions, footprint,
    color=:viridis, linewidth=0, colorbar_title="\nC  [µg/m³]", colorbar_titlefontsize=18, clims=(0, 2000),
    xlabel="Distance downwind  [km]", ylabel="Crosswind  [m]")
plot!(p_foot, size=(1050, 340), left_margin=12mm, right_margin=22mm,
    bottom_margin=12mm, top_margin=5mm)
1 2 3 4 5 6 7 Distance downwind [km] −500 −250 0 250 500 Crosswind [m] 0 250 500 750 1000 1250 1500 1750 2000 C [µg/m³]
Figure 1: Ground-level concentration. The domain is 8 km long and 1.4 km wide, so the plume is far narrower than it looks here.

Stability class F: moderately stable, a clear night.

Vertical Section

Code
heights = 0:2:200
section_positions = 0.2:0.02:8.0
section = [plume_ugm3(x, 0.0, z, plume_params) for z in heights, x in section_positions]

# Immediately above the stack the plume is a few metres across and the
# concentration there is enormous. Cap the scale at the ground-level range so
# the descent to the ground stays visible instead of being washed out.
p_section = contourf(section_positions, heights, section, color=:viridis, linewidth=0, colorbar_title="\nC  [µg/m³]", colorbar_titlefontsize=18,
    clims=(0, 2000), xlabel="Distance downwind  [km]", ylabel="Height  [m]")

# Height of the highest concentration at each distance, on a finer vertical grid
fine_heights = 0:0.5:200
max_heights = [fine_heights[argmax([plume_ugm3(x, 0.0, z, plume_params) for z in fine_heights])]
    for x in section_positions]
plot!(p_section, section_positions, max_heights, color=cb_vermillion, linewidth=3,
    label="height of maximum")
hline!(p_section, [plume_params.H], color=:black, linewidth=2, linestyle=:dash,
    label="stack height", legend=:topright, xlims=(0.2, 8), ylims=(0, 200))
plot!(p_section, size=(1050, 430), left_margin=12mm, right_margin=22mm,
    bottom_margin=12mm, top_margin=5mm)
2 4 6 8 Distance downwind [km] 0 50 100 150 200 Height [m] 0 250 500 750 1000 1250 1500 1750 2000 C [µg/m³] height of maximum stack height
Figure 2: Vertical section through the centreline. The plume leaves the stack at 41 m and reaches the ground by roughly 1 km.

Crosswind Profiles

Code
profile_distances = [1, 3, 8]                  # [km]
profile_colours = [cb_vermillion, cb_blue, cb_green]
profile_styles = [:solid, :dash, :dot]
crosswind = -600:2:600                          # [m]

p_cross = plot(xlabel="Ground-Level Crosswind Distance  [m]", ylabel="C  [µg/m³]",
    legend=:topright)
for i in 1:length(profile_distances)
    xk = profile_distances[i]
    plot!(p_cross, crosswind, [plume_ugm3(xk, y, 0.0, plume_params) for y in crosswind],
        color=profile_colours[i], linestyle=profile_styles[i], label="$(xk) km")
end
plot!(p_cross, size=(1200, 450), left_margin=12mm, right_margin=8mm,
    bottom_margin=12mm, top_margin=5mm)
−500 −250 0 250 500 Ground-Level Crosswind Distance [m] 0 500 1000 1500 C [µg/m³] 1 km 3 km 8 km
Figure 3: Ground-level concentration across the plume at three distances downwind.

Where Is The Worst Pollution?

Code
centerline_positions = 0.05:0.01:8.0
centerline = [plume_ugm3(x, 0.0, 0.0, plume_params) for x in centerline_positions]
peak_value, peak_index = findmax(centerline)
peak_distance = centerline_positions[peak_index]

p_line = plot(centerline_positions, centerline, color=:black, linewidth=4,
    xlabel="Distance downwind  [km]", ylabel="C  [µg/m³]",
    legend=false)
scatter!(p_line, [peak_distance], [peak_value], color=cb_vermillion, markersize=9)
plot!(p_line, size=(1050, 330), left_margin=12mm, right_margin=8mm,
    bottom_margin=12mm, top_margin=5mm)
0 2 4 6 8 Distance downwind [km] 0 500 1000 1500 C [µg/m³]
Figure 4: Ground-level centreline concentration against downwind distance.

Why Does This Occur Downwind?

\[C(x, 0, 0) = \frac{Q}{\pi u\,\sigma_y\sigma_z}\exp\left(-\frac{H^2}{2\sigma_z^2}\right)\]

Near the stack, which factor controls \(C\)? Far downwind?

\(C\) peaks where arrival (the exponential) rises as fast as dilution falls, in percentage terms: \(\sigma_z \approx H/\sqrt{2} =\) 29 m, about 3.3 km, where the exponential is \(e^{-1}\). The actual peak is at 2.5 km: \(\sigma_y\) grows faster than \(\sigma_z\).

Stack Height Changes The Ground-Level \(C\)

Code
standard = 196                                  # 1-hour SO₂ standard, 75 ppb  [µg/m³]
stack_heights = [41, 60, 100]                   # effective stack height  [m]
height_colours = [cb_vermillion, cb_blue, cb_green]
height_styles = [:solid, :dash, :dot]
far_positions = 0.05:0.01:20                    # [km]

height_peaks = zeros(length(stack_heights))
height_peak_distances = zeros(length(stack_heights))
p_height = plot(xlabel="Distance downwind  [km]", ylabel="Centreline C  [µg/m³]",
    legend=:topright)
for i in 1:length(stack_heights)
    params_h = (; plume_params..., H=stack_heights[i])
    centreline_h = [plume_ugm3(x, 0.0, 0.0, params_h) for x in far_positions]
    height_peaks[i], j = findmax(centreline_h)
    height_peak_distances[i] = far_positions[j]
    plot!(p_height, far_positions, centreline_h, color=height_colours[i],
        linestyle=height_styles[i], label="H = $(stack_heights[i]) m")
end
hline!(p_height, [standard], color=:black, linestyle=:dashdot, linewidth=2, label="1-hour SO₂ standard, $(standard) µg/m³")
plot!(p_height, size=(1200, 450), left_margin=12mm, right_margin=8mm,
    bottom_margin=12mm, top_margin=5mm)
0 5 10 15 20 Distance downwind [km] 0 500 1000 1500 Centreline C [µg/m³] H = 41 m H = 60 m H = 100 m 1-hour SO₂ standard, 196 µg/m³
Figure 5: Ground-level centreline concentration for three effective stack heights, with the same emissions. Dash-dot: the 1-hour SO₂ standard.

Stability Changes The Profile Too

Code
# Martin (1976) coefficients: sigma_y = a x^0.894, sigma_z = c x^d + f, with x in km
stability_coefs = Dict(
    "B" => (; a=156, c_near=106.6, d_near=1.149, f_near=3.3, c_far=108.2, d_far=1.098, f_far=2),
    "D" => (; a=68, c_near=33.2, d_near=0.725, f_near=-1.7, c_far=44.5, d_far=0.516, f_far=-13),
    "F" => (; a=34, c_near=14.35, d_near=0.740, f_near=-0.35, c_far=62.6, d_far=0.180, f_far=-48.6))

# Ground-level centreline concentration [µg/m³] under a given stability class
function centreline_class(xk, coef, params)
    if xk <= 0.05
        return 0.0
    end
    sy = coef.a * xk^0.894
    if xk < 1
        sz = coef.c_near * xk^coef.d_near + coef.f_near
    else
        sz = coef.c_far * xk^coef.d_far + coef.f_far
    end
    return params.Q / (π * params.u_wind * sy * sz) * exp(-params.H^2 / (2sz^2)) * 1e6
end

class_names = ["B", "D", "F"]
class_labels = ["B (sunny afternoon)", "D (overcast)", "F (clear night)"]
class_colours = [cb_orange, cb_purple, cb_blue]
class_styles = [:solid, :dash, :dot]
near_positions = 0.05:0.005:10                  # [km]

class_peaks = zeros(length(class_names))
class_peak_distances = zeros(length(class_names))
p_class = plot(xlabel="Distance downwind  [km]", ylabel="C  [µg/m³]", legend=:topright)
for i in 1:length(class_names)
    c = [centreline_class(x, stability_coefs[class_names[i]], plume_params) for x in near_positions]
    class_peaks[i], j = findmax(c)
    class_peak_distances[i] = near_positions[j]
    plot!(p_class, near_positions, c, color=class_colours[i], linestyle=class_styles[i],
        label=class_labels[i])
end
hline!(p_class, [standard], color=:black, linestyle=:dashdot, linewidth=2, label="1-hour SO₂ standard, $(standard) µg/m³")
plot!(p_class, size=(1200, 450), left_margin=12mm, right_margin=8mm,
    bottom_margin=12mm, top_margin=5mm)
0.0 2.5 5.0 7.5 10.0 Distance downwind [km] 0 1000 2000 3000 C [µg/m³] B (sunny afternoon) D (overcast) F (clear night) 1-hour SO₂ standard, 196 µg/m³
Figure 6: Ground-level centreline concentration for the same stack under three stability classes. Dash-dot: the 1-hour SO₂ standard.

The Area Above the Standard

Code
area_dx, area_dy = 0.02, 2                      # grid spacing: [km] downwind, [m] crosswind
area_x = (area_dx/2):area_dx:10
area_y = (-1000 + area_dy/2):area_dy:1000
area_C = [plume_ugm3(x, y, 0.0, plume_params) for y in area_y, x in area_x]
# Each grid cell above the standard contributes its own area, dx × dy.
area_above = count(area_C .> standard) * (area_dx * 1000) * area_dy / 1e6   # [km²]

p_area = contourf(area_x, area_y, area_C, color=:viridis, linewidth=0, colorbar_title="\nC  [µg/m³]", colorbar_titlefontsize=18, clims=(0, 2000),
    xlabel="Distance downwind  [km]", ylabel="Crosswind  [m]")
contour!(p_area, area_x, area_y, area_C, levels=[standard], color=:black, linewidth=3,
    colorbar_entry=false)
plot!(p_area, size=(1050, 310), left_margin=12mm, right_margin=22mm,
    bottom_margin=12mm, top_margin=5mm)
2 4 6 8 Distance downwind [km] −750 −500 −250 0 250 500 750 Crosswind [m] 0 250 500 750 1000 1250 1500 1750 2000 C [µg/m³]
Figure 7: Ground-level footprint with the standard’s contour (black). The exceedance continues past the 10 km edge of the domain.

About 5.0 km² is above the 1-hour SO\(_2\) standard (196 µg/m³) within 10 km; the plume is still above it at the edge.

Impact of the Spatial Grid

Code
# Crosswind samples offset by half a cell, so none sits exactly on the centreline.
# The plume is symmetric, so sample one side and double the area.
function grid_summary(crosswind_spacing; downwind_spacing=0.05)
    xs = (downwind_spacing/2):downwind_spacing:10
    ys = (crosswind_spacing/2):crosswind_spacing:1000
    samples = [plume_ugm3(x, y, 0.0, plume_params) for y in ys, x in xs]
    area = 2 * count(samples .> standard) * (downwind_spacing * 1000) * crosswind_spacing / 1e6
    return maximum(samples), area
end

println("| Crosswind spacing (m) | Peak (µg/m³) | Area above standard (km²) |")
println("|---:|---:|---:|")
for spacing in [400, 100, 20, 2]
    grid_peak, grid_area = grid_summary(spacing)
    println("| $(spacing) | $(Int(round(grid_peak))) | $(round(grid_area; digits=2)) |")
end
Crosswind spacing (m) Peak (µg/m³) Area above standard (km²)
400 571 5.4
100 1454 5.05
20 1732 5.03
2 1747 5.03

Only the peak needs a sample near the centreline.

How To Reduce Pollution?

The 1-hour SO\(_2\) standard is 75 ppb, about 196 µg/m³. At \(H = 41\) m the peak is 1747 µg/m³: about 9 times the standard.

Two ways to comply:

  • Raise the stack: 100 m is enough here.
  • Cut emissions by a factor of about 9, since \(C\) is proportional to \(Q\).

Impact of Multiple Sources

Code
stacks = [(0, -150), (1, 0), (2, 150)]          # (downwind [km], crosswind [m]) of each stack
stack_params = (; plume_params..., Q=10)        # our stack, with emissions cut tenfold

# The plume equation is linear in Q, so concentrations from separate stacks add.
combined(x, y) = sum(plume_ugm3(x - s[1], y - s[2], 0.0, stack_params) for s in stacks)

super_x = -0.5:0.02:12
super_y = -700:5:700
super_C = [combined(x, y) for y in super_y, x in super_x]
combined_peak, k = findmax(super_C)
combined_peak_distance = super_x[k[2]]
single_peak = maximum(plume_ugm3(x, 0.0, 0.0, stack_params) for x in super_x)

p_super = contourf(super_x, super_y, super_C, color=:viridis, linewidth=0, colorbar_title="\nC  [µg/m³]", colorbar_titlefontsize=18,
    xlabel="Distance downwind  [km]", ylabel="Crosswind  [m]")
contour!(p_super, super_x, super_y, super_C, levels=[standard], color=:black, linewidth=3,
    colorbar_entry=false)
scatter!(p_super, [s[1] for s in stacks], [s[2] for s in stacks], color=:white,
    markerstrokecolor=:black, markersize=9, label=false)
plot!(p_super, size=(1050, 330), left_margin=12mm, right_margin=22mm,
    bottom_margin=12mm, top_margin=5mm)
0.0 2.5 5.0 7.5 10.0 Distance downwind [km] −500 −250 0 250 500 Crosswind [m] 0 50 100 150 200 250 C [µg/m³]
Figure 8: Ground-level footprint from three facilities like ours (white markers), each cut to 10 g/s. Black: the standard’s contour.

Each facility cuts to 10 g/s and complies alone (175 µg/m³). Together: 282 µg/m³ at 5.5 km, where no single plume peaks.

Key Takeaways and Upcoming Schedule

Key Takeaways

  • The ground-level peak is downwind of the stack: the plume must spread down to the ground first.
  • A taller stack lowers the peak and moves it out.
  • Whether the grid resolution matters depends on what you report.
  • Concentrations add and can change the area as well as peak level.

Next Classes

Wednesday: Model calibration and validation.

Assessments

Mini-Project 1: Due Thursday, October 22.

Homework 4: Due Thursday, October 8.

References