musica 0.14.4__cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- musica/__init__.py +11 -0
- musica/_musica.cpython-310-aarch64-linux-gnu.so +0 -0
- musica/_version.py +1 -0
- musica/backend.py +58 -0
- musica/carma/__init__.py +20 -0
- musica/carma/carma.py +1727 -0
- musica/constants.py +3 -0
- musica/cuda.py +13 -0
- musica/examples/__init__.py +1 -0
- musica/examples/carma_aluminum.py +124 -0
- musica/examples/carma_sulfate.py +246 -0
- musica/examples/examples.py +175 -0
- musica/examples/lorenz.py +295 -0
- musica/examples/sulfate_box_model.py +439 -0
- musica/examples/ts1_latin_hypercube.py +245 -0
- musica/main.py +128 -0
- musica/mechanism_configuration/__init__.py +18 -0
- musica/mechanism_configuration/ancillary.py +6 -0
- musica/mechanism_configuration/arrhenius.py +149 -0
- musica/mechanism_configuration/branched.py +140 -0
- musica/mechanism_configuration/emission.py +82 -0
- musica/mechanism_configuration/first_order_loss.py +90 -0
- musica/mechanism_configuration/mechanism.py +93 -0
- musica/mechanism_configuration/phase.py +58 -0
- musica/mechanism_configuration/phase_species.py +58 -0
- musica/mechanism_configuration/photolysis.py +98 -0
- musica/mechanism_configuration/reaction_component.py +54 -0
- musica/mechanism_configuration/reactions.py +32 -0
- musica/mechanism_configuration/species.py +65 -0
- musica/mechanism_configuration/surface.py +98 -0
- musica/mechanism_configuration/taylor_series.py +136 -0
- musica/mechanism_configuration/ternary_chemical_activation.py +160 -0
- musica/mechanism_configuration/troe.py +160 -0
- musica/mechanism_configuration/tunneling.py +126 -0
- musica/mechanism_configuration/user_defined.py +99 -0
- musica/mechanism_configuration/utils.py +10 -0
- musica/micm/__init__.py +10 -0
- musica/micm/conditions.py +49 -0
- musica/micm/micm.py +135 -0
- musica/micm/solver.py +8 -0
- musica/micm/solver_result.py +24 -0
- musica/micm/state.py +220 -0
- musica/micm/utils.py +18 -0
- musica/tuvx/__init__.py +11 -0
- musica/tuvx/grid.py +98 -0
- musica/tuvx/grid_map.py +167 -0
- musica/tuvx/profile.py +130 -0
- musica/tuvx/profile_map.py +167 -0
- musica/tuvx/radiator.py +95 -0
- musica/tuvx/radiator_map.py +173 -0
- musica/tuvx/tuvx.py +283 -0
- musica-0.14.4.dist-info/METADATA +427 -0
- musica-0.14.4.dist-info/RECORD +92 -0
- musica-0.14.4.dist-info/WHEEL +6 -0
- musica-0.14.4.dist-info/entry_points.txt +3 -0
- musica-0.14.4.dist-info/licenses/AUTHORS.md +59 -0
- musica-0.14.4.dist-info/licenses/LICENSE +201 -0
- musica.libs/libaec-34bb4966.so.0.0.8 +0 -0
- musica.libs/libblas-8ed0a6f9.so.3.8.0 +0 -0
- musica.libs/libbrotlicommon-b6e6c8bd.so.1.0.6 +0 -0
- musica.libs/libbrotlidec-5094ef0a.so.1.0.6 +0 -0
- musica.libs/libcom_err-6d8d18aa.so.2.1 +0 -0
- musica.libs/libcrypt-258f54d5.so.1.1.0 +0 -0
- musica.libs/libcrypto-3eda328c.so.1.1.1k +0 -0
- musica.libs/libcurl-7faeef02.so.4.5.0 +0 -0
- musica.libs/libdf-9661c601.so.0.0.0 +0 -0
- musica.libs/libgfortran-e1b7dfc8.so.5.0.0 +0 -0
- musica.libs/libgssapi_krb5-fe951f80.so.2.2 +0 -0
- musica.libs/libhdf5-463e48d5.so.103.1.0 +0 -0
- musica.libs/libhdf5_hl-74316838.so.100.1.2 +0 -0
- musica.libs/libidn2-1b2a13b7.so.0.3.6 +0 -0
- musica.libs/libjpeg-ee25248c.so.62.2.0 +0 -0
- musica.libs/libk5crypto-84470bb3.so.3.1 +0 -0
- musica.libs/libkeyutils-fe6e95a9.so.1.6 +0 -0
- musica.libs/libkrb5-26ef5d84.so.3.3 +0 -0
- musica.libs/libkrb5support-875e89dc.so.0.1 +0 -0
- musica.libs/liblapack-8d137073.so.3.8.0 +0 -0
- musica.libs/liblber-2-86b08e65.4.so.2.10.9 +0 -0
- musica.libs/libldap-2-5c1dd279.4.so.2.10.9 +0 -0
- musica.libs/libmfhdf-9c336c5f.so.0.0.0 +0 -0
- musica.libs/libnetcdf-71a067be.so.15.0.1 +0 -0
- musica.libs/libnetcdff-6a455dd4.so.7.0.0 +0 -0
- musica.libs/libnghttp2-3a94c239.so.14.17.0 +0 -0
- musica.libs/libpcre2-8-8701a61e.so.0.7.1 +0 -0
- musica.libs/libpsl-130094ea.so.5.3.1 +0 -0
- musica.libs/libsasl2-076b3c1f.so.3.0.0 +0 -0
- musica.libs/libselinux-5700a1fd.so.1 +0 -0
- musica.libs/libssh-e0d3bd94.so.4.8.7 +0 -0
- musica.libs/libssl-f60bf0e2.so.1.1.1k +0 -0
- musica.libs/libsz-81b556a2.so.2.0.1 +0 -0
- musica.libs/libtirpc-1fa9018c.so.3.0.0 +0 -0
- musica.libs/libunistring-be03fd41.so.2.1.0 +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple box model with MICM and CARMA
|
|
3
|
+
|
|
4
|
+
This script creates one column and solves gas-phase sulfuric acid formation with
|
|
5
|
+
MICM and nucleation, growth, and coagulation of aerosols with CARMA.
|
|
6
|
+
|
|
7
|
+
UNDER DEVELOPMENT
|
|
8
|
+
"""
|
|
9
|
+
import musica
|
|
10
|
+
from musica.constants import GAS_CONSTANT
|
|
11
|
+
import musica.mechanism_configuration as mc
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import numpy as np
|
|
14
|
+
import ussa1976
|
|
15
|
+
import xarray as xr
|
|
16
|
+
|
|
17
|
+
available = musica.backend.carma_available()
|
|
18
|
+
|
|
19
|
+
MOLEC_CM3_TO_MOLE_M3 = np.float64(1.0e6) / np.float64(6.022e23) # Convert from molecules/cm³ to moles/m³
|
|
20
|
+
NUMBER_OF_GRID_CELLS = 120 # Number of grid cells for the simulation
|
|
21
|
+
NUMBER_OF_AEROSOL_SECTIONS = 38 # Number of aerosol sections for the simulation
|
|
22
|
+
DENSITY_SULFATE = np.float64(1923.0) # Density of sulfate in kg/m³
|
|
23
|
+
MOLECULAR_MASS_H2O = np.float64(0.01801528) # kg/mol
|
|
24
|
+
MOLECULAR_MASS_H2SO4 = np.float64(0.098078479) # kg/mol
|
|
25
|
+
MOLECULAR_MASS_AIR = np.float64(0.029) # kg/mol (average molecular mass of air)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_mechanism():
|
|
29
|
+
"""
|
|
30
|
+
Creates a chemical mechanism for the gas-phase reactions involved in sulfate formation.
|
|
31
|
+
"""
|
|
32
|
+
HO2 = mc.Species(name="HO2")
|
|
33
|
+
H2O2 = mc.Species(name="H2O2")
|
|
34
|
+
OH = mc.Species(name="OH", constant_mixing_ratio_mol_mol=0.8e-11)
|
|
35
|
+
SO2 = mc.Species(name="SO2")
|
|
36
|
+
SO3 = mc.Species(name="SO3")
|
|
37
|
+
H2SO4 = mc.Species(name="H2SO4")
|
|
38
|
+
H2O = mc.Species(name="H2O")
|
|
39
|
+
M = mc.Species(name="M", is_third_body=True)
|
|
40
|
+
|
|
41
|
+
gas = mc.Phase(name="gas", species=[HO2, H2O2, OH, SO2, SO3, H2SO4, H2O, M])
|
|
42
|
+
|
|
43
|
+
rxn_2HO2_H2O2 = mc.Arrhenius(
|
|
44
|
+
name="2HO2 -> H2O2",
|
|
45
|
+
reactants=[HO2, HO2],
|
|
46
|
+
products=[H2O2],
|
|
47
|
+
gas_phase=gas,
|
|
48
|
+
A=3.0e-13 / MOLEC_CM3_TO_MOLE_M3,
|
|
49
|
+
Ea=-6.35099e-21)
|
|
50
|
+
|
|
51
|
+
rxn_2HO2_M_H2O2 = mc.Arrhenius(
|
|
52
|
+
name="2HO2 + M -> H2O2",
|
|
53
|
+
reactants=[HO2, HO2, M],
|
|
54
|
+
products=[H2O2],
|
|
55
|
+
gas_phase=gas,
|
|
56
|
+
A=2.1e-33 / (MOLEC_CM3_TO_MOLE_M3**2),
|
|
57
|
+
Ea=-1.2702e-20)
|
|
58
|
+
|
|
59
|
+
rxn_2HO2_H2O_H2O2 = mc.Arrhenius(
|
|
60
|
+
name="2H2O2 + H2O -> H2O2 + H2O",
|
|
61
|
+
reactants=[HO2, HO2, H2O],
|
|
62
|
+
products=[H2O2, H2O],
|
|
63
|
+
gas_phase=gas,
|
|
64
|
+
A=4.2e-34 / (MOLEC_CM3_TO_MOLE_M3**2),
|
|
65
|
+
Ea=-3.67253e-20)
|
|
66
|
+
|
|
67
|
+
rxn_2HO2_H2O_M_H2O2 = mc.Arrhenius(
|
|
68
|
+
name="2HO2 + H2O + M -> H2O2 + H2O",
|
|
69
|
+
reactants=[HO2, HO2, H2O, M],
|
|
70
|
+
products=[H2O2, H2O],
|
|
71
|
+
gas_phase=gas,
|
|
72
|
+
A=2.94e-54 / (MOLEC_CM3_TO_MOLE_M3**3),
|
|
73
|
+
Ea=-4.30762e-20)
|
|
74
|
+
|
|
75
|
+
rxn_H2O2_OH_HO2_H2O = mc.Arrhenius(
|
|
76
|
+
name="H2O2 + OH -> HO2 + H2O",
|
|
77
|
+
reactants=[H2O2, OH],
|
|
78
|
+
products=[HO2, H2O],
|
|
79
|
+
gas_phase=gas,
|
|
80
|
+
A=1.8e-12 / MOLEC_CM3_TO_MOLE_M3)
|
|
81
|
+
|
|
82
|
+
rxn_SO2_OH_SO3_HO2 = mc.Troe(
|
|
83
|
+
name="SO2 + OH -> SO3 + HO2",
|
|
84
|
+
reactants=[SO2, OH],
|
|
85
|
+
products=[SO3, HO2],
|
|
86
|
+
gas_phase=gas,
|
|
87
|
+
k0_A=2.9e-31 / MOLEC_CM3_TO_MOLE_M3,
|
|
88
|
+
k0_B=-4.1,
|
|
89
|
+
kinf_A=1.7e-12 / MOLEC_CM3_TO_MOLE_M3,
|
|
90
|
+
kinf_B=-0.2)
|
|
91
|
+
|
|
92
|
+
rxn_SO3_2H2O_H2SO4_H2O = mc.Arrhenius(
|
|
93
|
+
name="SO3 + 2H2O -> H2SO4 + H2O",
|
|
94
|
+
reactants=[SO3, H2O, H2O],
|
|
95
|
+
products=[H2SO4, H2O],
|
|
96
|
+
gas_phase=gas,
|
|
97
|
+
A=8.5e-41 / (MOLEC_CM3_TO_MOLE_M3**2),
|
|
98
|
+
C=6540.0)
|
|
99
|
+
|
|
100
|
+
rxn_SO2 = mc.Emission(
|
|
101
|
+
name="SO2",
|
|
102
|
+
products=[SO2],
|
|
103
|
+
gas_phase=gas
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return mc.Mechanism(
|
|
107
|
+
name="Sulfate Box Model",
|
|
108
|
+
phases=[gas],
|
|
109
|
+
reactions=[
|
|
110
|
+
rxn_2HO2_H2O2,
|
|
111
|
+
rxn_2HO2_M_H2O2,
|
|
112
|
+
rxn_2HO2_H2O_H2O2,
|
|
113
|
+
rxn_2HO2_H2O_M_H2O2,
|
|
114
|
+
rxn_H2O2_OH_HO2_H2O,
|
|
115
|
+
rxn_SO2_OH_SO3_HO2,
|
|
116
|
+
rxn_SO3_2H2O_H2SO4_H2O,
|
|
117
|
+
rxn_SO2
|
|
118
|
+
],
|
|
119
|
+
species=[HO2, H2O2, OH, SO2, SO3, H2SO4, H2O, M]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_carma_solver():
|
|
124
|
+
"""
|
|
125
|
+
Creates a CARMA solver with the sulfate box model configuration.
|
|
126
|
+
"""
|
|
127
|
+
params = musica.carma.CARMAParameters()
|
|
128
|
+
params.nz = NUMBER_OF_GRID_CELLS
|
|
129
|
+
params.nbin = NUMBER_OF_AEROSOL_SECTIONS
|
|
130
|
+
|
|
131
|
+
# Set up a group for sulfate particles
|
|
132
|
+
sulfate_group = musica.carma.CARMAGroupConfig(
|
|
133
|
+
name="sulfate",
|
|
134
|
+
shortname="SULF",
|
|
135
|
+
rmin=2.0e-10, # Minimum radius in meters
|
|
136
|
+
rmrat=2.0, # Radius ratio for the group
|
|
137
|
+
swelling_approach={
|
|
138
|
+
"algorithm": musica.carma.ParticleSwellingAlgorithm.WEIGHT_PERCENT_H2SO4,
|
|
139
|
+
"composition": musica.carma.ParticleSwellingComposition.NONE
|
|
140
|
+
},
|
|
141
|
+
is_sulfate=True,
|
|
142
|
+
do_drydep=True,
|
|
143
|
+
)
|
|
144
|
+
params.groups.append(sulfate_group)
|
|
145
|
+
|
|
146
|
+
# Set up an element for sulfate
|
|
147
|
+
sulfate_element = musica.carma.CARMAElementConfig(
|
|
148
|
+
name="Sulfate",
|
|
149
|
+
shortname="SULF",
|
|
150
|
+
rho=DENSITY_SULFATE, # Density in kg/m³
|
|
151
|
+
itype=musica.carma.ParticleType.VOLATILE,
|
|
152
|
+
icomposition=musica.carma.ParticleComposition.SULFURIC_ACID,
|
|
153
|
+
igroup=1, # Group index for sulfate
|
|
154
|
+
)
|
|
155
|
+
params.elements.append(sulfate_element)
|
|
156
|
+
|
|
157
|
+
# Set up gases for water and sulfuric acid
|
|
158
|
+
water = musica.carma.CARMAGasConfig(
|
|
159
|
+
name="Water Vapor",
|
|
160
|
+
shortname="H2O",
|
|
161
|
+
wtmol=MOLECULAR_MASS_H2O, # Molar mass of water in kg/mol
|
|
162
|
+
ivaprtn=musica.carma.VaporizationAlgorithm.H2O_MURPHY_2005,
|
|
163
|
+
icomposition=musica.carma.GasComposition.H2O,
|
|
164
|
+
dgc_threshold=0.1,
|
|
165
|
+
ds_threshold=0.1,
|
|
166
|
+
)
|
|
167
|
+
params.gases.append(water)
|
|
168
|
+
|
|
169
|
+
h2so4 = musica.carma.CARMAGasConfig(
|
|
170
|
+
name="Sulfuric Acid",
|
|
171
|
+
shortname="H2SO4",
|
|
172
|
+
wtmol=MOLECULAR_MASS_H2SO4, # Molar mass of sulfuric acid in kg/mol
|
|
173
|
+
ivaprtn=musica.carma.VaporizationAlgorithm.H2SO4_AYERS_1980,
|
|
174
|
+
icomposition=musica.carma.GasComposition.H2SO4,
|
|
175
|
+
dgc_threshold=0.1,
|
|
176
|
+
ds_threshold=0.1,
|
|
177
|
+
)
|
|
178
|
+
params.gases.append(h2so4)
|
|
179
|
+
|
|
180
|
+
# Add microphysical processes
|
|
181
|
+
h2so4_uptake = musica.carma.CARMAGrowthConfig(
|
|
182
|
+
ielem=1, # Element index for sulfate
|
|
183
|
+
igas=2, # Gas index for sulfuric acid
|
|
184
|
+
)
|
|
185
|
+
params.growths.append(h2so4_uptake)
|
|
186
|
+
|
|
187
|
+
nucleation = musica.carma.CARMANucleationConfig(
|
|
188
|
+
ielemfrom=1, # Element index for sulfate
|
|
189
|
+
ielemto=1, # Element index for sulfuric acid
|
|
190
|
+
igas=2, # Gas index for sulfuric acid
|
|
191
|
+
algorithm=musica.carma.ParticleNucleationAlgorithm.HOMOGENEOUS_NUCLEATION,
|
|
192
|
+
)
|
|
193
|
+
params.nucleations.append(nucleation)
|
|
194
|
+
|
|
195
|
+
coagulation = musica.carma.CARMACoagulationConfig(
|
|
196
|
+
igroup1=1, # Group index for sulfate (from)
|
|
197
|
+
igroup2=1, # Group index for sulfate (from)
|
|
198
|
+
igroup3=1, # Group index for sulfate (to)
|
|
199
|
+
algorithm=musica.carma.ParticleCollectionAlgorithm.FUCHS,
|
|
200
|
+
)
|
|
201
|
+
params.coagulations.append(coagulation)
|
|
202
|
+
|
|
203
|
+
# Set initialization options
|
|
204
|
+
params.initialization.do_substep = True
|
|
205
|
+
params.initialization.do_thermo = True
|
|
206
|
+
params.initialization.maxretries = 16
|
|
207
|
+
params.initialization.maxsubsteps = 32
|
|
208
|
+
params.initialization.dt_threshold = 1.0
|
|
209
|
+
params.initialization.sulfnucl_method = musica.carma.SulfateNucleationMethod.ZHAO_TURCO.value
|
|
210
|
+
|
|
211
|
+
return musica.carma.CARMA(params)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_initial_conditions():
|
|
215
|
+
"""
|
|
216
|
+
Initializes and returns the environmental conditions and chemical species concentrations
|
|
217
|
+
for a box model simulation.
|
|
218
|
+
Returns:
|
|
219
|
+
tuple:
|
|
220
|
+
- environmental_conditions (dict): Dictionary containing arrays for temperature (K)
|
|
221
|
+
and pressure (Pa) for each grid cell.
|
|
222
|
+
- initial_concentrations (dict): Dictionary containing arrays for the initial
|
|
223
|
+
concentrations (in mol/m^3) of chemical species ('HO2', 'H2O2', 'OH', 'SO2', 'SO3',
|
|
224
|
+
'H2SO4', 'H2O', 'M') for each grid cell.
|
|
225
|
+
Notes:
|
|
226
|
+
- The number of grid cells is determined by the constant NUMBER_OF_GRID_CELLS.
|
|
227
|
+
- Concentrations are calculated using the conversion factor MOLEC_CM3_TO_MOLE_M3.
|
|
228
|
+
- Some species are initialized to zero concentration.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
# Set up the vertical grid in CARMA
|
|
232
|
+
zmin = np.float64(0.0)
|
|
233
|
+
deltaz = np.float64(100.0)
|
|
234
|
+
grid = {
|
|
235
|
+
"vertical_center": zmin + (np.arange(NUMBER_OF_GRID_CELLS, dtype=np.float64) + 0.5) * deltaz,
|
|
236
|
+
"vertical_levels": zmin + np.arange(NUMBER_OF_GRID_CELLS + 1, dtype=np.float64) * deltaz
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
centered_variables = ussa1976.compute(z=grid["vertical_center"], variables=["t", "p"])
|
|
240
|
+
edge_variables = ussa1976.compute(z=grid["vertical_levels"], variables=["p"])
|
|
241
|
+
centered_variables["rho"] = centered_variables["p"] / (GAS_CONSTANT * centered_variables["t"])
|
|
242
|
+
environmental_conditions = {
|
|
243
|
+
"temperature": np.array(centered_variables["t"], dtype=np.float64),
|
|
244
|
+
"pressure": np.array(centered_variables["p"], dtype=np.float64),
|
|
245
|
+
"pressure levels": np.array(edge_variables["p"], dtype=np.float64),
|
|
246
|
+
"air density": np.array(centered_variables["rho"], dtype=np.float64)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
initial_concentrations = {
|
|
250
|
+
"HO2": environmental_conditions["air density"] * 80.0e-12,
|
|
251
|
+
"H2O2": environmental_conditions["air density"] * 1.0e-9,
|
|
252
|
+
"SO2": environmental_conditions["air density"] * 0.15e-9,
|
|
253
|
+
"SO3": np.zeros(NUMBER_OF_GRID_CELLS, dtype=np.float64),
|
|
254
|
+
"H2SO4": np.zeros(NUMBER_OF_GRID_CELLS, dtype=np.float64),
|
|
255
|
+
"H2O": environmental_conditions["air density"] * 0.004,
|
|
256
|
+
}
|
|
257
|
+
return grid, environmental_conditions, initial_concentrations
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def run_box_model():
|
|
261
|
+
# Create the mechanism and solvers
|
|
262
|
+
mechanism = create_mechanism()
|
|
263
|
+
solver = musica.MICM(mechanism=mechanism, solver_type=musica.SolverType.rosenbrock_standard_order)
|
|
264
|
+
state = solver.create_state(number_of_grid_cells=NUMBER_OF_GRID_CELLS)
|
|
265
|
+
|
|
266
|
+
carma = create_carma_solver()
|
|
267
|
+
|
|
268
|
+
# Set initial conditions
|
|
269
|
+
grid, environmental_conditions, initial_concentrations = get_initial_conditions()
|
|
270
|
+
state.set_conditions(environmental_conditions["temperature"], environmental_conditions["pressure"])
|
|
271
|
+
state.set_concentrations(initial_concentrations)
|
|
272
|
+
state.set_user_defined_rate_parameters({"EMIS.SO2": np.full(NUMBER_OF_GRID_CELLS, 1.0e-8, dtype=np.float64)})
|
|
273
|
+
|
|
274
|
+
# Run the simulation for 30 minutes with a timestep of 30 seconds
|
|
275
|
+
time_hours = 0.5
|
|
276
|
+
time_seconds = time_hours * np.float64(3600.0)
|
|
277
|
+
dt = np.float64(30.0)
|
|
278
|
+
num_steps = int(time_seconds / dt)
|
|
279
|
+
|
|
280
|
+
# Initialize combined state arrays
|
|
281
|
+
concentrations = [state.get_concentrations()]
|
|
282
|
+
current_temperature = environmental_conditions["temperature"].copy()
|
|
283
|
+
bin_state = None
|
|
284
|
+
|
|
285
|
+
# Set up additional CARMA state arrays
|
|
286
|
+
satliq_h2o = np.full(NUMBER_OF_GRID_CELLS, -1.0, dtype=np.float64)
|
|
287
|
+
satice_h2o = np.full(NUMBER_OF_GRID_CELLS, -1.0, dtype=np.float64)
|
|
288
|
+
satliq_h2so4 = np.full(NUMBER_OF_GRID_CELLS, -1.0, dtype=np.float64)
|
|
289
|
+
satice_h2so4 = np.full(NUMBER_OF_GRID_CELLS, -1.0, dtype=np.float64)
|
|
290
|
+
last_h2so4_mmr = np.zeros(NUMBER_OF_GRID_CELLS, dtype=np.float64)
|
|
291
|
+
|
|
292
|
+
for i_time in range(num_steps):
|
|
293
|
+
state.set_conditions(temperatures=current_temperature, pressures=environmental_conditions["pressure"])
|
|
294
|
+
solver.solve(state, dt)
|
|
295
|
+
micm_output = state.get_concentrations()
|
|
296
|
+
|
|
297
|
+
h2so4_mmr = np.array(micm_output["H2SO4"], dtype=np.float64) * MOLECULAR_MASS_H2SO4 / \
|
|
298
|
+
(environmental_conditions["air density"] * MOLECULAR_MASS_AIR)
|
|
299
|
+
h2o_mmr = np.array(micm_output["H2O"], dtype=np.float64) * MOLECULAR_MASS_H2O / \
|
|
300
|
+
(environmental_conditions["air density"] * MOLECULAR_MASS_AIR)
|
|
301
|
+
|
|
302
|
+
carma_state = carma.create_state(
|
|
303
|
+
vertical_center=grid["vertical_center"],
|
|
304
|
+
vertical_levels=grid["vertical_levels"],
|
|
305
|
+
temperature=current_temperature,
|
|
306
|
+
pressure=environmental_conditions["pressure"],
|
|
307
|
+
pressure_levels=environmental_conditions["pressure levels"],
|
|
308
|
+
time=i_time * dt,
|
|
309
|
+
time_step=dt,
|
|
310
|
+
latitude=-40.0,
|
|
311
|
+
longitude=-105.0,
|
|
312
|
+
specific_humidity=h2o_mmr,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if bin_state is None:
|
|
316
|
+
bin_state = carma_state.get_bins()
|
|
317
|
+
bin_state = bin_state.expand_dims({"time": [0.0]}).copy(deep=True)
|
|
318
|
+
bin_state["mass_mixing_ratio"].values[0, :, :, :] = 0.0
|
|
319
|
+
|
|
320
|
+
for i_bin in range(NUMBER_OF_AEROSOL_SECTIONS):
|
|
321
|
+
carma_state.set_bin(
|
|
322
|
+
bin_index=i_bin + 1,
|
|
323
|
+
element_index=1, # Sulfate element index
|
|
324
|
+
value=bin_state.isel(time=i_time, bin=i_bin, element=0)["mass_mixing_ratio"].values,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
carma_state.set_gas(
|
|
328
|
+
gas_index=1, # Water vapor gas index
|
|
329
|
+
value=h2o_mmr,
|
|
330
|
+
old_mmr=h2o_mmr,
|
|
331
|
+
gas_saturation_wrt_liquid=satliq_h2o,
|
|
332
|
+
gas_saturation_wrt_ice=satice_h2o
|
|
333
|
+
)
|
|
334
|
+
carma_state.set_gas(
|
|
335
|
+
gas_index=2, # Sulfuric acid gas index
|
|
336
|
+
value=h2so4_mmr,
|
|
337
|
+
old_mmr=last_h2so4_mmr,
|
|
338
|
+
gas_saturation_wrt_liquid=satliq_h2so4,
|
|
339
|
+
gas_saturation_wrt_ice=satice_h2so4
|
|
340
|
+
)
|
|
341
|
+
last_h2so4_mmr = h2so4_mmr
|
|
342
|
+
|
|
343
|
+
carma_state.step()
|
|
344
|
+
|
|
345
|
+
# Get updated state data from CARMA
|
|
346
|
+
bin_state = xr.concat([bin_state, carma_state.get_bins().expand_dims(
|
|
347
|
+
{"time": [(i_time + 1) * dt]})], dim="time")
|
|
348
|
+
|
|
349
|
+
gas_state, gas_index = carma_state.get_gases()
|
|
350
|
+
micm_output["H2O"] = np.array(
|
|
351
|
+
gas_state.isel(
|
|
352
|
+
gas=gas_index["H2O"])["gas_mass_mixing_ratio"],
|
|
353
|
+
dtype=np.float64) * environmental_conditions["air density"] * MOLECULAR_MASS_AIR / MOLECULAR_MASS_H2O
|
|
354
|
+
micm_output["H2SO4"] = np.array(
|
|
355
|
+
gas_state.isel(
|
|
356
|
+
gas=gas_index["H2SO4"])["gas_mass_mixing_ratio"],
|
|
357
|
+
dtype=np.float64) * environmental_conditions["air density"] * MOLECULAR_MASS_AIR / MOLECULAR_MASS_H2SO4
|
|
358
|
+
|
|
359
|
+
# save concentrations for output
|
|
360
|
+
concentrations.append(micm_output)
|
|
361
|
+
|
|
362
|
+
# prepare for next time step
|
|
363
|
+
current_temperature = carma_state.get_environmental_values()["temperature"]
|
|
364
|
+
state.set_concentrations({
|
|
365
|
+
"H2O": micm_output["H2O"],
|
|
366
|
+
"H2SO4": micm_output["H2SO4"]
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
# Collect output data
|
|
370
|
+
concentrations = pd.DataFrame(concentrations)
|
|
371
|
+
times = np.arange(num_steps + 1) * dt / 3600.0 # Time in hours
|
|
372
|
+
|
|
373
|
+
return concentrations, times, bin_state
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def plot_results(concentrations, times, sulfate_data=None):
|
|
377
|
+
"""Plots the results of the sulfate box model simulation.
|
|
378
|
+
Args:
|
|
379
|
+
concentrations (pd.DataFrame): DataFrame containing the concentrations of chemical species over time.
|
|
380
|
+
times (np.ndarray): Array of time values corresponding to the concentrations.
|
|
381
|
+
sulfate_data (xr.Dataset): xarray Dataset containing CARMA sulfate data over time.
|
|
382
|
+
"""
|
|
383
|
+
import matplotlib.pyplot as plt
|
|
384
|
+
|
|
385
|
+
# Create a 4-panel plot for H2O, H2SO4, SO2, and HO2 concentrations over time at the first grid cell
|
|
386
|
+
fig, axs = plt.subplots(2, 2, figsize=(14, 10), sharex=True)
|
|
387
|
+
species = ["H2O", "H2SO4", "SO2", "HO2"]
|
|
388
|
+
titles = [
|
|
389
|
+
"H2O Concentration (mol/m³)",
|
|
390
|
+
"H2SO4 Concentration (mol/m³)",
|
|
391
|
+
"SO2 Concentration (mol/m³)",
|
|
392
|
+
"HO2 Concentration (mol/m³)"
|
|
393
|
+
]
|
|
394
|
+
for ax, specie, title in zip(axs.flat, species, titles):
|
|
395
|
+
conc = np.array([row[0] for row in [c[specie] for _, c in concentrations.iterrows()]])
|
|
396
|
+
ax.plot(times, conc, label=specie)
|
|
397
|
+
ax.set_title(title)
|
|
398
|
+
ax.set_ylabel("Concentration (mol/m³)")
|
|
399
|
+
ax.legend()
|
|
400
|
+
ax.grid()
|
|
401
|
+
for ax in axs[1]:
|
|
402
|
+
ax.set_xlabel("Time (hours)")
|
|
403
|
+
plt.tight_layout()
|
|
404
|
+
plt.show()
|
|
405
|
+
|
|
406
|
+
# Plot total sulfate mass mixing ratio (sum over all bins) over time at the first vertical level
|
|
407
|
+
if sulfate_data is not None and hasattr(sulfate_data, "mass_mixing_ratio"):
|
|
408
|
+
total_mmr = sulfate_data["mass_mixing_ratio"].isel(vertical_center=0).sum(dim="bin")
|
|
409
|
+
total_mmr.plot(x="time")
|
|
410
|
+
plt.title("Total Sulfate Mass Mixing Ratio (First Vertical Level)")
|
|
411
|
+
plt.ylabel("Mass Mixing Ratio (kg/kg)")
|
|
412
|
+
plt.xlabel("Time (hours)")
|
|
413
|
+
plt.grid()
|
|
414
|
+
plt.show()
|
|
415
|
+
else:
|
|
416
|
+
print("SULFATE data structure:", sulfate_data)
|
|
417
|
+
if sulfate_data is not None:
|
|
418
|
+
print(
|
|
419
|
+
"Available variables:",
|
|
420
|
+
list(
|
|
421
|
+
sulfate_data.data_vars) if hasattr(
|
|
422
|
+
sulfate_data,
|
|
423
|
+
'data_vars') else "No data_vars available")
|
|
424
|
+
print(
|
|
425
|
+
"Available dimensions:",
|
|
426
|
+
list(
|
|
427
|
+
sulfate_data.dims) if hasattr(
|
|
428
|
+
sulfate_data,
|
|
429
|
+
'dims') else "No dims available")
|
|
430
|
+
else:
|
|
431
|
+
print("No SULFATE data available")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
if not available:
|
|
436
|
+
print("CARMA backend is not available.")
|
|
437
|
+
else:
|
|
438
|
+
concs, plot_times, sulfate_data = run_box_model()
|
|
439
|
+
plot_results(concs, plot_times, sulfate_data)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import musica
|
|
2
|
+
from musica.mechanism_configuration import Parser
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from scipy.stats import qmc
|
|
5
|
+
import xarray as xr
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
path = 'configs/v1/ts1/ts1.json'
|
|
9
|
+
|
|
10
|
+
parser = Parser()
|
|
11
|
+
mechanism = parser.parse(path)
|
|
12
|
+
|
|
13
|
+
solver = musica.MICM(mechanism=mechanism,
|
|
14
|
+
solver_type=musica.SolverType.rosenbrock_standard_order)
|
|
15
|
+
num_grid_cells = 100
|
|
16
|
+
state = solver.create_state(num_grid_cells)
|
|
17
|
+
|
|
18
|
+
# The initial conditions file contains various parameters with their values.
|
|
19
|
+
# It has three columns: parameter, value1, and value2.
|
|
20
|
+
# The meaning of value1 and value2 depends on the parameter type:
|
|
21
|
+
#
|
|
22
|
+
# | Parameter Prefix | value1 Meaning | value2 Meaning |
|
|
23
|
+
# |------------------|-------------------------------------|-------------------------------------|
|
|
24
|
+
# | SURF | Effective radius (meters) | Particle number concentration (#/m3)|
|
|
25
|
+
# | CONC | Initial concentration (mol m-3) | Unused |
|
|
26
|
+
# | ENV | Temperature (K) or Pressure (Pa) | Unused |
|
|
27
|
+
# | USER | User-defined parameter value | Unused |
|
|
28
|
+
conditions = pd.read_csv('configs/v1/ts1/initial_conditions.csv',
|
|
29
|
+
sep=',', names=['parameter', 'value1', 'value2'])
|
|
30
|
+
|
|
31
|
+
# grab the surface reactions, anything prefixed with SURF.
|
|
32
|
+
surface_reactions = conditions[conditions['parameter'].str.contains('SURF')]
|
|
33
|
+
|
|
34
|
+
# grab the initial concentrations, anything prefixed with CONC.
|
|
35
|
+
initial_concentrations = conditions[conditions['parameter'].str.contains(
|
|
36
|
+
'CONC')]
|
|
37
|
+
# remove CONC. from the parameter names
|
|
38
|
+
initial_concentrations.loc[:, 'parameter'] = initial_concentrations.loc[:,
|
|
39
|
+
'parameter'].str.replace('CONC.', '')
|
|
40
|
+
|
|
41
|
+
# grab the environmental conditions, anything prefixed with ENV.
|
|
42
|
+
environmental_conditions = conditions[conditions['parameter'].str.contains(
|
|
43
|
+
'ENV')]
|
|
44
|
+
|
|
45
|
+
# grab the user defined conditions, anything prefixed with USER.
|
|
46
|
+
user_defined_conditions = conditions[conditions['parameter'].str.contains(
|
|
47
|
+
'USER')]
|
|
48
|
+
|
|
49
|
+
# make sure the length of all the subsets matches the total length of conditions
|
|
50
|
+
assert len(surface_reactions) + len(initial_concentrations) + \
|
|
51
|
+
len(environmental_conditions) + \
|
|
52
|
+
len(user_defined_conditions) == len(conditions)
|
|
53
|
+
|
|
54
|
+
# To create a Latin Hypercube sample, we have to define the lower and upper bounds for each parameter
|
|
55
|
+
# We will use the initial conditions from the file and add a small perturbation to create a range
|
|
56
|
+
lower_bounds = []
|
|
57
|
+
upper_bounds = []
|
|
58
|
+
|
|
59
|
+
concentration_perturbation = 0.1
|
|
60
|
+
user_defined_perturbation = 0.05
|
|
61
|
+
surface_perturbation = 0.02
|
|
62
|
+
environmental_perturbation = 0.2
|
|
63
|
+
|
|
64
|
+
# Lets loop through each of our initial conditions arrays and create bounds
|
|
65
|
+
for concentration in initial_concentrations['value1']:
|
|
66
|
+
lower_bounds.append(float(concentration) *
|
|
67
|
+
(1 - concentration_perturbation))
|
|
68
|
+
upper_bounds.append(float(concentration) *
|
|
69
|
+
(1 + concentration_perturbation))
|
|
70
|
+
|
|
71
|
+
for user in user_defined_conditions['value1']:
|
|
72
|
+
lower_bounds.append(float(user) * (1 - user_defined_perturbation))
|
|
73
|
+
upper_bounds.append(float(user) * (1 + user_defined_perturbation))
|
|
74
|
+
|
|
75
|
+
for _, row in surface_reactions.iterrows():
|
|
76
|
+
# effective radius
|
|
77
|
+
lower_bounds.append(float(row['value1']) * (1 - surface_perturbation))
|
|
78
|
+
upper_bounds.append(float(row['value1']) * (1 + surface_perturbation))
|
|
79
|
+
# particle number concentration
|
|
80
|
+
lower_bounds.append(float(row['value2']) * (1 - surface_perturbation))
|
|
81
|
+
upper_bounds.append(float(row['value2']) * (1 + surface_perturbation))
|
|
82
|
+
|
|
83
|
+
for environmental in environmental_conditions['value1']:
|
|
84
|
+
lower_bounds.append(float(environmental) *
|
|
85
|
+
(1 - environmental_perturbation))
|
|
86
|
+
upper_bounds.append(float(environmental) *
|
|
87
|
+
(1 + environmental_perturbation))
|
|
88
|
+
|
|
89
|
+
# Now we can create the Latin Hypercube samples
|
|
90
|
+
sampler = qmc.LatinHypercube(d=len(lower_bounds))
|
|
91
|
+
# we will make one sample for each grid cell and solve all of the parameter combinations at once
|
|
92
|
+
sample = sampler.random(n=num_grid_cells)
|
|
93
|
+
scaled_sample = qmc.scale(sample, lower_bounds, upper_bounds)
|
|
94
|
+
|
|
95
|
+
# Now we need to extract the sampled values and set them on the state
|
|
96
|
+
# The order matches how we built the bounds arrays above
|
|
97
|
+
|
|
98
|
+
# Extract samples for each parameter type
|
|
99
|
+
current_index = 0
|
|
100
|
+
|
|
101
|
+
# 1. Extract concentration samples
|
|
102
|
+
concentration_samples = {}
|
|
103
|
+
species_ordering = state.get_species_ordering()
|
|
104
|
+
for _, concentration_name in enumerate(initial_concentrations['parameter']):
|
|
105
|
+
concentration_samples[concentration_name] = scaled_sample[:, current_index]
|
|
106
|
+
current_index += 1
|
|
107
|
+
|
|
108
|
+
# 2. Extract user-defined rate samples
|
|
109
|
+
user_defined_samples = {}
|
|
110
|
+
user_defined_ordering = state.get_user_defined_rate_parameters_ordering()
|
|
111
|
+
for _, user_param in enumerate(user_defined_conditions['parameter']):
|
|
112
|
+
# Remove USER. prefix to match the expected parameter name
|
|
113
|
+
user_defined_samples[user_param] = scaled_sample[:, current_index]
|
|
114
|
+
current_index += 1
|
|
115
|
+
|
|
116
|
+
# 3. Extract surface reaction samples (skip for now as they need special handling)
|
|
117
|
+
for _, row in surface_reactions.iterrows():
|
|
118
|
+
# Skip effective radius and particle concentration for now
|
|
119
|
+
user_defined_samples[f"{row['parameter']}.effective radius [m]"] = scaled_sample[:, current_index]
|
|
120
|
+
current_index += 1
|
|
121
|
+
user_defined_samples[f"{row['parameter']}.particle number concentration [# m-3]"] = scaled_sample[:, current_index]
|
|
122
|
+
current_index += 1
|
|
123
|
+
|
|
124
|
+
# 4. Extract environmental samples
|
|
125
|
+
temperature_samples = scaled_sample[:, current_index]
|
|
126
|
+
current_index += 1
|
|
127
|
+
pressure_samples = scaled_sample[:, current_index]
|
|
128
|
+
current_index += 1
|
|
129
|
+
|
|
130
|
+
# Now set all of the samples across all grid cells
|
|
131
|
+
state.set_conditions(temperature_samples, pressure_samples)
|
|
132
|
+
state.set_concentrations(concentration_samples)
|
|
133
|
+
state.set_user_defined_rate_parameters(user_defined_samples)
|
|
134
|
+
|
|
135
|
+
# setup and run the box model simulation
|
|
136
|
+
concentrations = [state.get_concentrations()]
|
|
137
|
+
times = []
|
|
138
|
+
time_step = 30 # seconds
|
|
139
|
+
simulation_length = 1 * 60 * 60 # 1 hour in seconds
|
|
140
|
+
current_time = 0
|
|
141
|
+
|
|
142
|
+
while current_time < simulation_length:
|
|
143
|
+
times.append(current_time)
|
|
144
|
+
solver.solve(state, time_step)
|
|
145
|
+
concentrations.append(state.get_concentrations())
|
|
146
|
+
current_time += time_step
|
|
147
|
+
|
|
148
|
+
conditions = state.get_conditions()
|
|
149
|
+
|
|
150
|
+
data_vars = {
|
|
151
|
+
"temperature": (["grid_cell"], conditions["temperature"]),
|
|
152
|
+
"pressure": (["grid_cell"], conditions["pressure"]),
|
|
153
|
+
"air density": (["grid_cell"], conditions["air_density"]),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for user_param, values in state.get_user_defined_rate_parameters().items():
|
|
157
|
+
data_vars[user_param] = (["grid_cell"], values)
|
|
158
|
+
|
|
159
|
+
for species, _ in species_ordering.items():
|
|
160
|
+
data = [concentrations[time_idx][species]
|
|
161
|
+
for time_idx in range(len(concentrations))]
|
|
162
|
+
data_vars[species] = (["time", "grid_cell"], data)
|
|
163
|
+
|
|
164
|
+
ds = xr.Dataset(
|
|
165
|
+
data_vars,
|
|
166
|
+
coords={
|
|
167
|
+
"time": times + [current_time],
|
|
168
|
+
"grid_cell": range(num_grid_cells),
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
print(ds)
|
|
173
|
+
ds.to_netcdf('ts1_latin_hypercube.nc')
|
|
174
|
+
|
|
175
|
+
# plot 5 grid cells for O3, OH, NO, NO2
|
|
176
|
+
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
|
|
177
|
+
|
|
178
|
+
# Convert time from seconds to hours for better readability
|
|
179
|
+
time_hours = ds['time'] / 3600
|
|
180
|
+
|
|
181
|
+
# Plot each species with 5 lines (one for each grid cell)
|
|
182
|
+
for i in range(5):
|
|
183
|
+
ax[0, 0].plot(time_hours, ds['O3'].isel(
|
|
184
|
+
grid_cell=i), label=f'Grid Cell {i}')
|
|
185
|
+
ax[0, 1].plot(time_hours, ds['OH'].isel(
|
|
186
|
+
grid_cell=i), label=f'Grid Cell {i}')
|
|
187
|
+
ax[1, 0].plot(time_hours, ds['NO'].isel(
|
|
188
|
+
grid_cell=i), label=f'Grid Cell {i}')
|
|
189
|
+
ax[1, 1].plot(time_hours, ds['NO2'].isel(
|
|
190
|
+
grid_cell=i), label=f'Grid Cell {i}')
|
|
191
|
+
|
|
192
|
+
for _ax in ax.flat:
|
|
193
|
+
_ax.grid(True, alpha=0.5)
|
|
194
|
+
_ax.spines[:].set_visible(False)
|
|
195
|
+
_ax.tick_params(width=0)
|
|
196
|
+
_ax.set_xlim(0, simulation_length / 3600) # Set x-axis limit to simulation length in hours
|
|
197
|
+
_ax.set_ylim(0, None)
|
|
198
|
+
_ax.set_ylabel('Concentration [mol m-3]')
|
|
199
|
+
_ax.set_xlabel('Time [hours]')
|
|
200
|
+
_ax.legend()
|
|
201
|
+
|
|
202
|
+
ax[0, 0].set_title('O3')
|
|
203
|
+
ax[0, 1].set_title('OH')
|
|
204
|
+
ax[1, 0].set_title('NO')
|
|
205
|
+
ax[1, 1].set_title('NO2')
|
|
206
|
+
|
|
207
|
+
fig.tight_layout()
|
|
208
|
+
fig.savefig('ts1_latin_hypercube_OH_O3_NO_NO2.png', dpi=300)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Plot minimum and maximum values for O3, OH, NO, NO2 across all grid cells
|
|
212
|
+
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
|
|
213
|
+
for i, species in enumerate(['O3', 'OH', 'NO', 'NO2']):
|
|
214
|
+
ax.flat[i].plot(time_hours, ds[species].min(dim='grid_cell'), label='Min')
|
|
215
|
+
ax.flat[i].plot(time_hours, ds[species].max(dim='grid_cell'), label='Max')
|
|
216
|
+
ax.flat[i].set_title(species)
|
|
217
|
+
ax.flat[i].set_ylabel('Concentration [mol m-3]')
|
|
218
|
+
ax.flat[i].set_xlabel('Time [hours]')
|
|
219
|
+
ax.flat[i].legend()
|
|
220
|
+
ax.flat[i].grid(True, alpha=0.5)
|
|
221
|
+
ax.flat[i].spines[:].set_visible(False)
|
|
222
|
+
ax.flat[i].tick_params(width=0)
|
|
223
|
+
# ax.flat[i].set_yscale('log')
|
|
224
|
+
|
|
225
|
+
fig.tight_layout()
|
|
226
|
+
fig.savefig('ts1_latin_hypercube_OH_O3_NO_NO2_min_max.png', dpi=300)
|
|
227
|
+
|
|
228
|
+
fig, ax = plt.subplots(3, 1, figsize=(10, 8))
|
|
229
|
+
|
|
230
|
+
# Plot temperature, pressure, and air density
|
|
231
|
+
ax[0].plot(ds['grid_cell'], ds['temperature'])
|
|
232
|
+
ax[0].set_title('Temperature')
|
|
233
|
+
ax[0].set_ylabel('Temperature [K]')
|
|
234
|
+
ax[0].set_xlabel('Grid Cell')
|
|
235
|
+
ax[1].plot(ds['grid_cell'], ds['pressure'])
|
|
236
|
+
ax[1].set_title('Pressure')
|
|
237
|
+
ax[1].set_ylabel('Pressure [Pa]')
|
|
238
|
+
ax[1].set_xlabel('Grid Cell')
|
|
239
|
+
ax[2].plot(ds['grid_cell'], ds['air density'])
|
|
240
|
+
ax[2].set_title('Air Density')
|
|
241
|
+
ax[2].set_ylabel('Air Density [kg m-3]')
|
|
242
|
+
ax[2].set_xlabel('Grid Cell')
|
|
243
|
+
|
|
244
|
+
fig.tight_layout()
|
|
245
|
+
fig.savefig('ts1_latin_hypercube_conditions.png', dpi=300)
|