-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathsoda_outgassing.jl
284 lines (243 loc) · 9.87 KB
/
soda_outgassing.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# # Illustration of using a carbon system solver
#
# This example illustrates how to use ClimaOceanBiogeochemistry's
# `UniversalRobustCarbonSystem` model in a 0-d context.
using ClimaOceanBiogeochemistry.CarbonSystemSolvers.UniversalRobustCarbonSolver: UniversalRobustCarbonSystem
using ClimaOceanBiogeochemistry.CarbonSystemSolvers: CarbonSystemParameters, CarbonSolverParameters, CarbonCoefficientParameters
using Oceananigans
using Oceananigans.Units
using Printf
using CairoMakie
#
# We are going to simulate two bottles of soda, one opened and left in the fridge
# the other opened and left on the counter to go flat.
#
# ## Model setup
# The 0-d grid represents a 10cm bottle of soda
grid = RectilinearGrid(size = 1,
z = (-1meter/10, 0),
topology = (Flat, Flat, Bounded))
# For CO₂ exchange, we use a FluxBoundaryCondition for the "top" of the dissolved inorganic carbon (DIC) tracer.
# We'll write a callback later to calculate the flux every time step.
co₂_flux = Field{Center, Center, Nothing}(grid)
dic_bcs = FieldBoundaryConditions(top = FluxBoundaryCondition(co₂_flux))
# Field arrays to store pCO₂ values filled in `compute_co₂_flux!`
soda_pCO₂ = Field{Center, Center, Nothing}(grid)
atmos_pCO₂ = Field{Center, Center, Nothing}(grid)
# Build function for CO₂ flux calculation. Dissolved CO₂ in the soda will exchange with the overlying atmosphere
# These are some coefficients and constants that we'll use in the calculation
Base.@kwdef struct CO₂_flux_parameters{FT}
surface_wind_speed :: FT = 10. # ms⁻¹
atmospheric_pCO₂ :: FT = 280e-6 # atm
exchange_coefficient :: FT = 0.337 # cm hr⁻¹
salinity :: FT = 0.0 # psu
alkalinity :: FT = 2.35e-3 # mol kg⁻¹
silicate :: FT = 0e-6 # mol kg⁻¹
phosphate :: FT = 0e-6 # mol kg⁻¹
initial_pH_guess :: FT = 8.0
reference_density :: FT = 1024.5 # kg m⁻³
end
"""
compute_co₂_flux!(simulation)
Returns the tendency due to CO₂ flux using the piston velocity
formulation of Wanninkhof (1992) and the solubility/activity of
CO₂ that depends on temperature, etc.
"""
@inline function compute_co₂_flux!(simulation; solver_params = ())
Nz = size(simulation.model.grid, 3)
## Get coefficients from CO₂_flux_parameters struct
## I really want the option to take these from the model
(; surface_wind_speed,
atmospheric_pCO₂,
exchange_coefficient,
salinity,
alkalinity,
silicate,
phosphate,
initial_pH_guess,
reference_density,
) = CO₂_flux_parameters()
U₁₀ = surface_wind_speed
pCO₂ᵃᵗᵐ = atmospheric_pCO₂
Kʷₐᵥₑ = exchange_coefficient
Sᴬ = salinity
Aᵀ = alkalinity
Siᵀ = silicate
Pᵀ = phosphate
pH = initial_pH_guess
ρʳᵉᶠ = reference_density
cmhr⁻¹_per_ms⁻¹ = 1 / 3.6e5 # conversion factor from cm/hr to m/s
co₂_flux = simulation.model.tracers.DIC.boundary_conditions.top.condition
Θᶜ = simulation.model.tracers.T[1,1,Nz]
Cᵀ = simulation.model.tracers.DIC[1,1,Nz]/ρʳᵉᶠ
## applied pressure in atm (i.e. Pˢᵘʳᶠ-Pᵃᵗᵐ)
## Positive when the can is sealed, then zero when the can is opens
## On average, the 12 ounce soda cans sold in the US tend to have a pressure of roughly 120 kPa when canned at 4 °C
if iteration(simulation) <= 1
Δpᵦₐᵣ = 0.2
else
## *pssshhhht* the bottle is opened
Δpᵦₐᵣ = 0.0
end
## compute soda pCO₂ using the UniversalRobustCarbonSystem solver
## Returns soda pCO₂ (in atm) and atmosphere/soda solubility coefficients (mol kg⁻¹ atm⁻¹)
(; pCO₂ᵒᶜᵉ, Pᵈⁱᶜₖₛₒₗₐ, Pᵈⁱᶜₖₛₒₗₒ) = UniversalRobustCarbonSystem(
pH = pH,
pCO₂ᵃᵗᵐ = pCO₂ᵃᵗᵐ,
Θᶜ = Θᶜ,
Sᴬ = Sᴬ,
Δpᵦₐᵣ = Δpᵦₐᵣ,
Cᵀ = Cᵀ,
Aᵀ = Aᵀ,
Pᵀ = Pᵀ,
Siᵀ = Siᵀ,
solver_params...,
)
## store the soda and atmospheric CO₂ concentrations into Fields
soda_pCO₂[1,1,Nz] = (pCO₂ᵒᶜᵉ * Pᵈⁱᶜₖₛₒₗₒ ) * ρʳᵉᶠ # Convert mol kg⁻¹ m s⁻¹ to mol m⁻² s⁻¹
atmos_pCO₂[1,1,Nz] = (pCO₂ᵃᵗᵐ * Pᵈⁱᶜₖₛₒₗₐ) * ρʳᵉᶠ # Convert mol kg⁻¹ to mol m⁻³
## compute schmidt number for DIC
kˢᶜ = CarbonCoefficientParameters(
a₀ = 2116.8,
a₁ = 136.25,
a₂ = 4.7353,
a₃ = 9.2307e-2,
a₄ = 7.555e-4,
a₅ = 660.0,
)
Cˢᶜᵈⁱᶜ = ( kˢᶜ.a₀ -
kˢᶜ.a₁ * Θᶜ +
kˢᶜ.a₂ * Θᶜ^2 -
kˢᶜ.a₃ * Θᶜ^3 +
kˢᶜ.a₄ * Θᶜ^4
)/kˢᶜ.a₅
## compute gas exchange coefficient/piston velocity and correct with Schmidt number
Kʷ = (
(Kʷₐᵥₑ * cmhr⁻¹_per_ms⁻¹) * U₁₀^2
) / sqrt(Cˢᶜᵈⁱᶜ)
## compute co₂ flux (-ve for uptake, +ve for outgassing since convention is +ve upwards in the soda)
co₂_flux[1,1,Nz] = - Kʷ * (
pCO₂ᵃᵗᵐ * Pᵈⁱᶜₖₛₒₗₐ -
pCO₂ᵒᶜᵉ * Pᵈⁱᶜₖₛₒₗₒ
) * ρʳᵉᶠ # Convert mol kg⁻¹ m s⁻¹ to mol m⁻² s⁻¹
return nothing
end
# # Simulation of a soda outgassing CO₂ in the fridge
model_open_in_the_fridge = NonhydrostaticModel(;
grid,
velocities = nothing,
buoyancy = nothing,
closure = nothing,
tracers = (:T, :DIC),
boundary_conditions = (; DIC=dic_bcs),
)
# Initial conditions for the refridgerated soda
Tᵢ = 4 # °C
DICᵢ = 2.4 # mol/m³
set!(model_open_in_the_fridge, T = Tᵢ, DIC = DICᵢ)
simulation = Simulation(model_open_in_the_fridge, Δt=10minutes, stop_time=24hours)
# Add an output writer...
fnm_fridge = "soda_in_the_fridge.jld2"
outputs = (; co₂_flux,
soda_pCO₂,
atmos_pCO₂,
model_open_in_the_fridge.tracers.T,
model_open_in_the_fridge.tracers.DIC,
)
simulation.output_writers[:jld2] = JLD2OutputWriter(
model_open_in_the_fridge, outputs;
filename = fnm_fridge,
schedule = TimeInterval(10minutes),
overwrite_existing = true,
)
# ... and don't forget to add a callback to compute the CO₂ flux
add_callback!(simulation, compute_co₂_flux!)
# Run the simulation
run!(simulation)
# # A simulation of a soda outgassing CO₂ on the counter
# We simulate soda Warming up on the counter using a forcing function
# linear increase from 4°C to 30°C over 12 hours then stops warming
temperature_increase(z, t, p) = ifelse(t <= 12hours, p.∂T∂t * p.Δt , 0.0)
warming = Forcing(
temperature_increase,
parameters=(∂T∂t=1e-6, Δt=10minutes),
)
# Build the second model for the soda on the counter
model_open_on_the_counter = NonhydrostaticModel(;
grid,
velocities = nothing,
buoyancy = nothing,
closure = nothing,
forcing = (; T=warming),
tracers = (:T, :DIC),
boundary_conditions = (; DIC=dic_bcs),
)
set!(model_open_on_the_counter, T = Tᵢ, DIC = DICᵢ)
simulation = Simulation(model_open_on_the_counter, Δt=10minutes, stop_time=24hours)
# Add an output writer...
fnm_counter = "soda_on_the_counter.jld2"
outputs = (; co₂_flux,
soda_pCO₂,
atmos_pCO₂,
model_open_on_the_counter.tracers.T,
model_open_on_the_counter.tracers.DIC,
)
simulation.output_writers[:jld2] = JLD2OutputWriter(
model_open_on_the_counter, outputs;
filename = fnm_counter,
schedule = TimeInterval(10minutes),
overwrite_existing = true)
# ..and don't forget to add a callback to compute the CO₂ flux
add_callback!(simulation, compute_co₂_flux!)
# Run the simulation
run!(simulation)
# ## Visualization
#
# All that's left is to visualize the results.
fridge_soda_pCO₂ = FieldTimeSeries(fnm_fridge, "soda_pCO₂")
fridge_atmo_co₂ = FieldTimeSeries(fnm_fridge, "atmos_pCO₂")
fridge_temp = FieldTimeSeries(fnm_fridge, "T")
counter_soda_pCO₂ = FieldTimeSeries(fnm_counter, "soda_pCO₂")
counter_atmo_co₂ = FieldTimeSeries(fnm_counter, "atmos_pCO₂")
counter_temp = FieldTimeSeries(fnm_counter, "T")
t = fridge_soda_pCO₂.times
nt = length(t)
fig = Figure(size=(1200, 900))
ax = Axis(fig[1,1], xlabel="Time", ylabel="CO₂ conc [mmol m⁻³]")
lines!(t/(3600),
interior(fridge_soda_pCO₂, 1, 1, 1, :)*1e3;
linestyle = :dash,
label = "fridge soda",
)
lines!(t/(3600),
interior(counter_soda_pCO₂, 1, 1, 1, :)*1e3;
linestyle = :solid,
label = "counter soda",
)
lines!(t/(3600),
interior(fridge_atmo_co₂, 1, 1, 1, :)*1e3;
linestyle = :dash,
label = "fridge saturated")
lines!(t/(3600),
interior(counter_atmo_co₂, 1, 1, 1, :)*1e3;
linestyle = :solid,
label = "counter saturated")
axislegend()
ax = Axis(fig[2,1], xlabel="Time", ylabel="Temp (°C)")
lines!(t/(3600),
interior(fridge_temp, 1, 1, 1, :),
linestyle = :dash,
label = "fridge soda temperature",
)
lines!(t/(3600),
interior(counter_temp, 1, 1, 1, :),
linestyle = :solid,
label = "counter soda temperature",
)
axislegend()
# The cool soda's CO₂ concentration approaches equilibrium with the atmosphere (the saturated CO₂ concentration) quickly.
# The warming soda continues to outgas, since the solubility of CO₂ decreases with temperature. It'll taste flatter because of the lower CO₂ concentration.
#save("soda_outgassing_0d.png", fig)
nothing #hide
fig
# 