musica 0.11.1.1__cp312-cp312-win_amd64.whl → 0.14.2__cp312-cp312-win_amd64.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.
Files changed (129) hide show
  1. musica/__init__.py +23 -3
  2. musica/_musica.cp312-win_amd64.pyd +0 -0
  3. musica/_version.py +1 -1
  4. musica/backend.py +58 -0
  5. musica/carma/__init__.py +20 -0
  6. musica/carma/carma.py +1727 -0
  7. musica/constants.py +3 -0
  8. musica/cuda.py +13 -0
  9. musica/examples/__init__.py +1 -0
  10. musica/examples/carma_aluminum.py +124 -0
  11. musica/examples/carma_sulfate.py +246 -0
  12. musica/examples/examples.py +165 -0
  13. musica/examples/sulfate_box_model.py +439 -0
  14. musica/examples/ts1_latin_hypercube.py +245 -0
  15. musica/main.py +128 -0
  16. musica/mechanism_configuration/__init__.py +18 -0
  17. musica/mechanism_configuration/ancillary.py +6 -0
  18. musica/mechanism_configuration/arrhenius.py +149 -0
  19. musica/mechanism_configuration/branched.py +140 -0
  20. musica/mechanism_configuration/emission.py +82 -0
  21. musica/mechanism_configuration/first_order_loss.py +90 -0
  22. musica/mechanism_configuration/mechanism.py +93 -0
  23. musica/mechanism_configuration/phase.py +58 -0
  24. musica/mechanism_configuration/phase_species.py +58 -0
  25. musica/mechanism_configuration/photolysis.py +98 -0
  26. musica/mechanism_configuration/reaction_component.py +54 -0
  27. musica/mechanism_configuration/reactions.py +32 -0
  28. musica/mechanism_configuration/species.py +65 -0
  29. musica/mechanism_configuration/surface.py +98 -0
  30. musica/mechanism_configuration/taylor_series.py +136 -0
  31. musica/mechanism_configuration/ternary_chemical_activation.py +160 -0
  32. musica/mechanism_configuration/troe.py +160 -0
  33. musica/mechanism_configuration/tunneling.py +126 -0
  34. musica/mechanism_configuration/user_defined.py +99 -0
  35. musica/mechanism_configuration/utils.py +10 -0
  36. musica/micm/__init__.py +10 -0
  37. musica/micm/conditions.py +49 -0
  38. musica/micm/micm.py +135 -0
  39. musica/micm/solver.py +8 -0
  40. musica/micm/solver_result.py +24 -0
  41. musica/micm/state.py +220 -0
  42. musica/micm/utils.py +18 -0
  43. musica/tuvx/__init__.py +11 -0
  44. musica/tuvx/grid.py +98 -0
  45. musica/tuvx/grid_map.py +167 -0
  46. musica/tuvx/profile.py +130 -0
  47. musica/tuvx/profile_map.py +167 -0
  48. musica/tuvx/radiator.py +95 -0
  49. musica/tuvx/radiator_map.py +173 -0
  50. musica/tuvx/tuvx.py +283 -0
  51. musica-0.14.2.dist-info/DELVEWHEEL +2 -0
  52. {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/METADATA +146 -63
  53. musica-0.14.2.dist-info/RECORD +104 -0
  54. {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/WHEEL +1 -1
  55. musica-0.14.2.dist-info/entry_points.txt +3 -0
  56. musica-0.14.2.dist-info/licenses/AUTHORS.md +59 -0
  57. musica.libs/libaws-c-auth-0a61a643442f1c0912920b37d9fb0be5.dll +0 -0
  58. musica.libs/libaws-c-cal-eaafa5905de6c9ba274eb8737e6087dd.dll +0 -0
  59. musica.libs/libaws-c-common-b4aa4468297ae8e1664f9380a5510317.dll +0 -0
  60. musica.libs/libaws-c-compression-9f997952aeae03067122ca493c9081b5.dll +0 -0
  61. musica.libs/libaws-c-event-stream-fe9cc8e1692f60c2b5694a8959dbd7c3.dll +0 -0
  62. musica.libs/libaws-c-http-4a9d50ba6ad8882f5267ef89e5e4103a.dll +0 -0
  63. musica.libs/libaws-c-io-e454f1c7a44e77f8c957a016888754be.dll +0 -0
  64. musica.libs/libaws-c-mqtt-67c5fc291740f5cbc5e53fb767e93226.dll +0 -0
  65. musica.libs/libaws-c-s3-206db4af6e1a95637b1921ea596603b9.dll +0 -0
  66. musica.libs/libaws-c-sdkutils-5c9c62dafb8b774cd4a3386f95ef428d.dll +0 -0
  67. musica.libs/libaws-checksums-7e50fe01b862214958f4d2ab4215fde5.dll +0 -0
  68. musica.libs/libaws-cpp-sdk-core-7a9ba9c045ee16f5262e955d96865718.dll +0 -0
  69. musica.libs/libaws-cpp-sdk-s3-4eebff3923c6d250fb508da3c990e0ae.dll +0 -0
  70. musica.libs/libaws-crt-cpp-3173f1e6f504a96d88e8dbf9e04b3b14.dll +0 -0
  71. musica.libs/libbrotlicommon-c62c08223e450dfc2fff33c752cc2285.dll +0 -0
  72. musica.libs/libbrotlidec-ccde7c3978eb1d2e052b193f2968d30a.dll +0 -0
  73. musica.libs/libbz2-1-669a4bf9266d5f020e843aa5fd75b93c.dll +0 -0
  74. musica.libs/libcrypto-3-x64-237eeb55505d067eab5e0b886e519387.dll +0 -0
  75. musica.libs/libcurl-4-bdf865458887dc1235b192ec83729214.dll +0 -0
  76. musica.libs/libgcc_s_seh-1-5a3153f12338f79fbbb7bf095fc5cef1.dll +0 -0
  77. musica.libs/libgfortran-5-90848e0eacdecce3a9005faf5aaec7e7.dll +0 -0
  78. musica.libs/libgomp-1-b8afcf09fecd2f6f01e454c9a5f2c690.dll +0 -0
  79. musica.libs/libhdf5-320-eec6c8ba2fdde30d365786ffbff40989.dll +0 -0
  80. musica.libs/libhdf5_hl-320-7e26e1caaad6be4082d728cf08ab2de4.dll +0 -0
  81. musica.libs/libiconv-2-b37d1b4acab5310c4e4f6e2a961d1464.dll +0 -0
  82. musica.libs/libidn2-0-d17600177f3b4cd2521d595b3472d240.dll +0 -0
  83. musica.libs/libintl-8-e4d4ca6b37338fbb0a8c1246afa7258f.dll +0 -0
  84. musica.libs/liblzma-5-bd95aa0fda6e7c8e41b3843d6fc2942c.dll +0 -0
  85. musica.libs/libnetcdf-0623e518145bddd30cc615b6d7f2f9c1.dll +0 -0
  86. musica.libs/libnetcdff-7-982cb7ee026b78f05a79d00e735f91d1.dll +0 -0
  87. musica.libs/libnghttp2-14-6d49ed806389b4892bcf29c6ed6e3984.dll +0 -0
  88. musica.libs/libnghttp3-9-d3c9b57d760f6dae7d6a067a68126b84.dll +0 -0
  89. musica.libs/libngtcp2-16-a43356e6376d41ce4238e2c55581636a.dll +0 -0
  90. musica.libs/libngtcp2_crypto_ossl-0-b37121badf25a552e5654f27bf6ff093.dll +0 -0
  91. musica.libs/libopenblas-a16595c3cae114c5c7304aa8bb3c1272.dll +0 -0
  92. musica.libs/libpsl-5-4368d4c2412410a4a14f3e7f3227e295.dll +0 -0
  93. musica.libs/libquadmath-0-4edeffe0a60c96360445d33a1876dbda.dll +0 -0
  94. musica.libs/libssh2-1-f407a2b50419bd904c7eb2c101ae81ea.dll +0 -0
  95. musica.libs/libssl-3-x64-d2e43d36e6f87f6f1645717cd0871f86.dll +0 -0
  96. musica.libs/libstdc++-6-83061aaccaf8df77a3b584efef12bc7c.dll +0 -0
  97. musica.libs/libsz-2-d12f3d26417507ec8dea9964f9fe36a1.dll +0 -0
  98. musica.libs/libunistring-5-0473d7a71d94f08292beed694c34f7d1.dll +0 -0
  99. musica.libs/libwinpthread-1-9157bac12a85fb717fa3d2bf6712631a.dll +0 -0
  100. musica.libs/libxml2-16-7fe545d280fdef922282226eef91571f.dll +0 -0
  101. musica.libs/libzip-62d3c877b7842bc509fc000316a4731b.dll +0 -0
  102. musica.libs/libzstd-a25427164f8775046eb8ce488d7d0884.dll +0 -0
  103. musica.libs/zlib1-1dc85208162ee57fe97e892bb5160fe9.dll +0 -0
  104. _musica.cp312-win_amd64.pyd +0 -0
  105. lib/musica.lib +0 -0
  106. lib/yaml-cpp.lib +0 -0
  107. musica/CMakeLists.txt +0 -47
  108. musica/binding.cpp +0 -19
  109. musica/mechanism_configuration.cpp +0 -519
  110. musica/mechanism_configuration.py +0 -1291
  111. musica/musica.cpp +0 -214
  112. musica/test/examples/v0/config.json +0 -7
  113. musica/test/examples/v0/config.yaml +0 -3
  114. musica/test/examples/v0/reactions.json +0 -193
  115. musica/test/examples/v0/reactions.yaml +0 -142
  116. musica/test/examples/v0/species.json +0 -40
  117. musica/test/examples/v0/species.yaml +0 -19
  118. musica/test/examples/v1/full_configuration.json +0 -434
  119. musica/test/examples/v1/full_configuration.yaml +0 -271
  120. musica/test/test_analytical.py +0 -323
  121. musica/test/test_chapman.py +0 -123
  122. musica/test/test_parser.py +0 -693
  123. musica/test/tuvx.py +0 -10
  124. musica/tools/prepare_build_environment_linux.sh +0 -41
  125. musica/tools/prepare_build_environment_windows.sh +0 -22
  126. musica/tools/repair_wheel_gpu.sh +0 -25
  127. musica/types.py +0 -362
  128. musica-0.11.1.1.dist-info/RECORD +0 -30
  129. {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/licenses/LICENSE +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)