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.
Files changed (92) hide show
  1. musica/__init__.py +11 -0
  2. musica/_musica.cpython-310-aarch64-linux-gnu.so +0 -0
  3. musica/_version.py +1 -0
  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 +175 -0
  13. musica/examples/lorenz.py +295 -0
  14. musica/examples/sulfate_box_model.py +439 -0
  15. musica/examples/ts1_latin_hypercube.py +245 -0
  16. musica/main.py +128 -0
  17. musica/mechanism_configuration/__init__.py +18 -0
  18. musica/mechanism_configuration/ancillary.py +6 -0
  19. musica/mechanism_configuration/arrhenius.py +149 -0
  20. musica/mechanism_configuration/branched.py +140 -0
  21. musica/mechanism_configuration/emission.py +82 -0
  22. musica/mechanism_configuration/first_order_loss.py +90 -0
  23. musica/mechanism_configuration/mechanism.py +93 -0
  24. musica/mechanism_configuration/phase.py +58 -0
  25. musica/mechanism_configuration/phase_species.py +58 -0
  26. musica/mechanism_configuration/photolysis.py +98 -0
  27. musica/mechanism_configuration/reaction_component.py +54 -0
  28. musica/mechanism_configuration/reactions.py +32 -0
  29. musica/mechanism_configuration/species.py +65 -0
  30. musica/mechanism_configuration/surface.py +98 -0
  31. musica/mechanism_configuration/taylor_series.py +136 -0
  32. musica/mechanism_configuration/ternary_chemical_activation.py +160 -0
  33. musica/mechanism_configuration/troe.py +160 -0
  34. musica/mechanism_configuration/tunneling.py +126 -0
  35. musica/mechanism_configuration/user_defined.py +99 -0
  36. musica/mechanism_configuration/utils.py +10 -0
  37. musica/micm/__init__.py +10 -0
  38. musica/micm/conditions.py +49 -0
  39. musica/micm/micm.py +135 -0
  40. musica/micm/solver.py +8 -0
  41. musica/micm/solver_result.py +24 -0
  42. musica/micm/state.py +220 -0
  43. musica/micm/utils.py +18 -0
  44. musica/tuvx/__init__.py +11 -0
  45. musica/tuvx/grid.py +98 -0
  46. musica/tuvx/grid_map.py +167 -0
  47. musica/tuvx/profile.py +130 -0
  48. musica/tuvx/profile_map.py +167 -0
  49. musica/tuvx/radiator.py +95 -0
  50. musica/tuvx/radiator_map.py +173 -0
  51. musica/tuvx/tuvx.py +283 -0
  52. musica-0.14.4.dist-info/METADATA +427 -0
  53. musica-0.14.4.dist-info/RECORD +92 -0
  54. musica-0.14.4.dist-info/WHEEL +6 -0
  55. musica-0.14.4.dist-info/entry_points.txt +3 -0
  56. musica-0.14.4.dist-info/licenses/AUTHORS.md +59 -0
  57. musica-0.14.4.dist-info/licenses/LICENSE +201 -0
  58. musica.libs/libaec-34bb4966.so.0.0.8 +0 -0
  59. musica.libs/libblas-8ed0a6f9.so.3.8.0 +0 -0
  60. musica.libs/libbrotlicommon-b6e6c8bd.so.1.0.6 +0 -0
  61. musica.libs/libbrotlidec-5094ef0a.so.1.0.6 +0 -0
  62. musica.libs/libcom_err-6d8d18aa.so.2.1 +0 -0
  63. musica.libs/libcrypt-258f54d5.so.1.1.0 +0 -0
  64. musica.libs/libcrypto-3eda328c.so.1.1.1k +0 -0
  65. musica.libs/libcurl-7faeef02.so.4.5.0 +0 -0
  66. musica.libs/libdf-9661c601.so.0.0.0 +0 -0
  67. musica.libs/libgfortran-e1b7dfc8.so.5.0.0 +0 -0
  68. musica.libs/libgssapi_krb5-fe951f80.so.2.2 +0 -0
  69. musica.libs/libhdf5-463e48d5.so.103.1.0 +0 -0
  70. musica.libs/libhdf5_hl-74316838.so.100.1.2 +0 -0
  71. musica.libs/libidn2-1b2a13b7.so.0.3.6 +0 -0
  72. musica.libs/libjpeg-ee25248c.so.62.2.0 +0 -0
  73. musica.libs/libk5crypto-84470bb3.so.3.1 +0 -0
  74. musica.libs/libkeyutils-fe6e95a9.so.1.6 +0 -0
  75. musica.libs/libkrb5-26ef5d84.so.3.3 +0 -0
  76. musica.libs/libkrb5support-875e89dc.so.0.1 +0 -0
  77. musica.libs/liblapack-8d137073.so.3.8.0 +0 -0
  78. musica.libs/liblber-2-86b08e65.4.so.2.10.9 +0 -0
  79. musica.libs/libldap-2-5c1dd279.4.so.2.10.9 +0 -0
  80. musica.libs/libmfhdf-9c336c5f.so.0.0.0 +0 -0
  81. musica.libs/libnetcdf-71a067be.so.15.0.1 +0 -0
  82. musica.libs/libnetcdff-6a455dd4.so.7.0.0 +0 -0
  83. musica.libs/libnghttp2-3a94c239.so.14.17.0 +0 -0
  84. musica.libs/libpcre2-8-8701a61e.so.0.7.1 +0 -0
  85. musica.libs/libpsl-130094ea.so.5.3.1 +0 -0
  86. musica.libs/libsasl2-076b3c1f.so.3.0.0 +0 -0
  87. musica.libs/libselinux-5700a1fd.so.1 +0 -0
  88. musica.libs/libssh-e0d3bd94.so.4.8.7 +0 -0
  89. musica.libs/libssl-f60bf0e2.so.1.1.1k +0 -0
  90. musica.libs/libsz-81b556a2.so.2.0.1 +0 -0
  91. musica.libs/libtirpc-1fa9018c.so.3.0.0 +0 -0
  92. musica.libs/libunistring-be03fd41.so.2.1.0 +0 -0
musica/carma/carma.py ADDED
@@ -0,0 +1,1727 @@
1
+ """
2
+ CARMA aerosol model Python interface.
3
+
4
+ This module provides a simplified Python interface to the CARMA aerosol model.
5
+ It allows users to create a CARMA instance and run simulations with specified parameters.
6
+
7
+ Note: CARMA is only available on macOS and Linux platforms.
8
+ """
9
+
10
+ from typing import Dict, Optional, List, Union, Any, Tuple
11
+ from ctypes import c_void_p
12
+ import numpy as np
13
+ import xarray as xr
14
+ from enum import Enum
15
+ from .. import backend
16
+
17
+ _backend = backend.get_backend()
18
+
19
+
20
+ class ParticleShape(Enum):
21
+ """Enumeration for particle shapes used in CARMA."""
22
+ SPHERE = 1
23
+ HEXAGON = 2
24
+ CYLINDER = 3
25
+
26
+
27
+ class ParticleType(Enum):
28
+ """Enumeration for particle types used in CARMA."""
29
+ INVOLATILE = 1
30
+ VOLATILE = 2
31
+ CORE_MASS = 3
32
+ VOLATILE_CORE = 4
33
+ CORE_MASS_TWO_MOMENTS = 5
34
+
35
+
36
+ class ParticleSwellingAlgorithm(Enum):
37
+ """Enumeration for particle swelling algorithms used in CARMA."""
38
+ NONE = 0
39
+ FITZGERALD = 1
40
+ GERBER = 2
41
+ WEIGHT_PERCENT_H2SO4 = 3
42
+ PETTERS = 4
43
+
44
+
45
+ class ParticleSwellingComposition(Enum):
46
+ """Enumeration for particle swelling compositions used in CARMA."""
47
+ NONE = 0
48
+ AMMONIUM_SULFATE = 1
49
+ SEA_SALT = 2
50
+ URBAN = 3
51
+ RURAL = 4
52
+
53
+
54
+ class ParticleFallVelocityAlgorithm(Enum):
55
+ """Enumeration for particle fall velocity algorithms used in CARMA."""
56
+ NONE = 0
57
+ STANDARD_SPHERICAL_ONLY = 1
58
+ STANDARD_SHAPE_SUPPORT = 2
59
+ HEYMSFIELD_2010 = 3
60
+
61
+
62
+ class MieCalculationAlgorithm(Enum):
63
+ """Enumeration for Mie calculation algorithms used in CARMA."""
64
+ TOON_1981 = 1
65
+ BOHREN_1983 = 2
66
+ BOTET_1997 = 3
67
+
68
+
69
+ class OpticsAlgorithm(Enum):
70
+ """Enumeration for optics algorithms used in CARMA."""
71
+ NONE = 0
72
+ FIXED = 1
73
+ MIXED_YU_2015 = 2
74
+ SULFATE_YU_2015 = 3
75
+ MIXED_H2O_YU_2015 = 4
76
+ MIXED_CORE_SHELL = 5
77
+ MIXED_VOLUME = 6
78
+ MIXED_MAXWELL = 7
79
+ SULFATE = 8
80
+
81
+
82
+ class VaporizationAlgorithm(Enum):
83
+ """Enumeration for vaporization algorithms used in CARMA."""
84
+ NONE = 0
85
+ H2O_BUCK_1981 = 1
86
+ H2O_MURPHY_2005 = 2
87
+ H2O_GOFF_1946 = 3
88
+ H2SO4_AYERS_1980 = 4
89
+
90
+
91
+ class GasComposition(Enum):
92
+ """Enumeration for gas compositions used in CARMA."""
93
+ NONE = 0
94
+ H2O = 1
95
+ H2SO4 = 2
96
+ SO2 = 3
97
+
98
+
99
+ class ParticleComposition(Enum):
100
+ """Enumeration for particle compositions used in CARMA."""
101
+ ALUMINUM = 1
102
+ SULFURIC_ACID = 2
103
+ DUST = 3
104
+ ICE = 4
105
+ WATER = 5
106
+ BLACK_CARBON = 6
107
+ ORGANIC_CARBON = 7
108
+ OTHER = 8
109
+
110
+
111
+ class ParticleCollectionAlgorithm(Enum):
112
+ """Enumeration for particle collection algorithms used in CARMA."""
113
+ NONE = 0
114
+ CONSTANT = 1
115
+ FUCHS = 2
116
+ DATA = 3
117
+
118
+
119
+ class ParticleNucleationAlgorithm(Enum):
120
+ """Enumeration for particle nucleation algorithms used in CARMA."""
121
+ NONE = 0
122
+ AEROSOL_FREEZING_TABAZDEH_2000 = 1
123
+ AEROSOL_FREEZING_KOOP_2000 = 2
124
+ AEROSOL_FREEZING_MURRAY_2010 = 3
125
+ DROPLET_ACTIVATION = 256
126
+ AEROSOL_FREEZING = 512
127
+ DROPLET_FREEZING = 1024
128
+ ICE_MELTING = 2048
129
+ HETEROGENEOUS_NUCLEATION = 4096
130
+ HOMOGENEOUS_NUCLEATION = 8192
131
+ HETEROGENEOUS_SULFURIC_ACID_NUCLEATION = 16384
132
+
133
+
134
+ class SulfateNucleationMethod(Enum):
135
+ """Enumeration for sulfate nucleation methods used in CARMA."""
136
+ NONE = 0
137
+ ZHAO_TURCO = 1
138
+ VEHKAMAKI = 2
139
+
140
+
141
+ class CarmaCoordinates(Enum):
142
+ """Enumeration for CARMA coordinates."""
143
+ CARTESIAN = 1
144
+ SIGMA = 2
145
+ LONGITUDE_LATITUDE = 3
146
+ LAMBERT_CONFORMAL = 4
147
+ POLAR_STEREOGRAPHIC = 5
148
+ MERCATOR = 6
149
+ HYBRID = 7
150
+
151
+
152
+ class CARMAWavelengthBin:
153
+ """Configuration for a CARMA wavelength bin.
154
+
155
+ A CARMA wavelength bin represents a specific wavelength range used in optical calculations.
156
+ """
157
+
158
+ def __init__(self, center: float, width: float, do_emission: bool = True):
159
+ """
160
+ Initialize a CARMA wavelength bin.
161
+
162
+ Args:
163
+ center: Center wavelength in micrometers.
164
+ width: Width of the wavelength bin in micrometers.
165
+ do_emission: Whether to include this wavelength in emission calculations (default: True).
166
+ """
167
+ self.center = center
168
+ self.width = width
169
+ self.do_emission = do_emission
170
+
171
+ def to_dict(self) -> Dict:
172
+ """Convert to dictionary."""
173
+ return {k: v for k, v in self.__dict__.items()}
174
+
175
+
176
+ class CARMAGroupConfig:
177
+ """Configuration for a CARMA particle group.
178
+
179
+ A CARMA particle group represents a collection of particles with similar properties.
180
+ """
181
+
182
+ def __init__(self,
183
+ name: str = "default_group",
184
+ shortname: str = "",
185
+ rmin: float = 1e-9,
186
+ rmrat: float = 2.0,
187
+ rmassmin: float = 0.0,
188
+ ishape: int = ParticleShape.SPHERE,
189
+ eshape: float = 1.0,
190
+ swelling_approach: dict = {
191
+ "algorithm": ParticleSwellingAlgorithm.NONE,
192
+ "composition": ParticleSwellingComposition.NONE
193
+ },
194
+ fall_velocity_routine: int = ParticleFallVelocityAlgorithm.STANDARD_SPHERICAL_ONLY,
195
+ mie_calculation_algorithm: int = MieCalculationAlgorithm.TOON_1981,
196
+ optics_algorithm: int = OpticsAlgorithm.FIXED,
197
+ is_ice: bool = False,
198
+ is_fractal: bool = False,
199
+ is_cloud: bool = False,
200
+ is_sulfate: bool = False,
201
+ do_wetdep: bool = False,
202
+ do_drydep: bool = False,
203
+ do_vtran: bool = True,
204
+ solfac: float = 0.3,
205
+ scavcoef: float = 0.1,
206
+ dpc_threshold: float = 0.0,
207
+ rmon: float = 0.0,
208
+ df: Optional[List[float]] = None,
209
+ falpha: float = 1.0,
210
+ neutral_volfrc: float = 0.0):
211
+ """
212
+ Initialize a CARMA group configuration.
213
+
214
+ Args:
215
+ name: Name of the group (default: "default_group")
216
+ shortname: Short name for the group (default: "")
217
+ rmin: Radius of particles in the first bin [m] (default: 1e-9)
218
+ rmrat: Ratio of masses of particles in consecutive bins (default: 2.0)
219
+ rmassmin: Minimum mass of particles [kg] (default: 0.0)
220
+ ishape: Shape of the particles (default: ParticleShape.SPHERE)
221
+ eshape: Ratio of particle length / diameter (default: 1.0)
222
+ swelling_approach: Dictionary specifying swelling algorithm and composition (default: NONE)
223
+ fall_velocity_routine: Algorithm for fall velocity (default: STANDARD_SPHERICAL_ONLY)
224
+ mie_calculation_algorithm: Algorithm for Mie calculations (default: TOON_1981)
225
+ optics_algorithm: Algorithm for optics (default: FIXED)
226
+ is_ice: Whether the particles are ice (default: False)
227
+ is_fractal: Whether the particles are fractal (default: False)
228
+ is_cloud: Whether the group is a cloud (default: False)
229
+ is_sulfate: Whether the group is sulfate (default: False)
230
+ do_wetdep: Whether to include wet deposition (default: False)
231
+ do_drydep: Whether to include dry deposition (default: False)
232
+ do_vtran: Whether to include vertical transport (default: True)
233
+ solfac: Solubility factor for wet deposition (default: 0.3)
234
+ scavcoef: Scavenging coefficient for wet deposition (default: 0.1)
235
+ dpc_threshold: Threshold for dry particle collection (default: 0.0)
236
+ rmon: Monomer radius of fractal particles [m] (default: 0.0)
237
+ df: List of fractal dimensions for each size bin (default: None)
238
+ falpha: Fractal packing coefficient (default: 1.0)
239
+ neutral_volfrc: Neutral volume fraction for fractal particles (default: 0.0)
240
+ """
241
+ self.name = name
242
+ self.shortname = shortname
243
+ self.rmin = rmin
244
+ self.rmrat = rmrat
245
+ self.rmassmin = rmassmin
246
+ self.ishape = ishape
247
+ self.eshape = eshape
248
+ self.swelling_approach = swelling_approach
249
+ self.fall_velocity_routine = fall_velocity_routine
250
+ self.mie_calculation_algorithm = mie_calculation_algorithm
251
+ self.optics_algorithm = optics_algorithm
252
+ self.is_ice = is_ice
253
+ self.is_fractal = is_fractal
254
+ self.is_cloud = is_cloud
255
+ self.is_sulfate = is_sulfate
256
+ self.do_wetdep = do_wetdep
257
+ self.do_drydep = do_drydep
258
+ self.do_vtran = do_vtran
259
+ self.solfac = solfac
260
+ self.scavcoef = scavcoef
261
+ self.dpc_threshold = dpc_threshold
262
+ self.rmon = rmon
263
+ self.df = df or []
264
+ self.falpha = falpha
265
+ self.neutral_volfrc = neutral_volfrc
266
+
267
+ def to_dict(self) -> Dict:
268
+ """Convert to dictionary, serializing enums in swelling_approach as well."""
269
+ result = {}
270
+ for k, v in self.__dict__.items():
271
+ if k == "swelling_approach" and isinstance(v, dict):
272
+ # Serialize enum values inside swelling_approach dict
273
+ result[k] = {sk: (sv.value if isinstance(sv, Enum) else sv)
274
+ for sk, sv in v.items()}
275
+ else:
276
+ result[k] = v.value if isinstance(v, Enum) else v
277
+ return result
278
+
279
+
280
+ class CARMAElementConfig:
281
+ """Configuration for a CARMA particle element.
282
+
283
+ A CARMA particle element represents one of the components of a cloud or aerosol particle.
284
+ """
285
+
286
+ def __init__(self,
287
+ igroup: int = 1,
288
+ isolute: int = 0,
289
+ name: str = "default_element",
290
+ shortname: str = "",
291
+ itype: int = ParticleType.INVOLATILE,
292
+ icomposition: int = ParticleComposition.OTHER,
293
+ is_shell: bool = True,
294
+ rho: float = 1000.0,
295
+ rhobin: Optional[List[float]] = None,
296
+ arat: Optional[List[float]] = None,
297
+ kappa: float = 0.0,
298
+ refidx: Optional[List[List[float]]] = None):
299
+ """
300
+ Initialize a CARMA element configuration.
301
+
302
+ Args:
303
+ igroup: Group ID this element belongs to (default: 1)
304
+ isolute: Index of the solute (default: 0)
305
+ name: Name of the element (default: "default_element")
306
+ shortname: Short name for the element (default: "")
307
+ itype: Type of the particle (default: ParticleType.INVOLATILE)
308
+ icomposition: Composition of the particle (default: ParticleComposition.OTHER)
309
+ is_shell: For core/shell optics, whether this element is part of the shell (True) or core (False) (default: True)
310
+ rho: Density of the element in kg/m3 (default: 1.0)
311
+ rhobin: List of densities for each size bin in kg/m3 (default: None)
312
+ arat: List of area ratios for each size bin (default: None)
313
+ kappa: Hygroscopicity parameter (default: 0.0)
314
+ refidx: List of lists of refractive indices for each wavelength bin (default: None)
315
+ """
316
+ self.igroup = igroup
317
+ self.isolute = isolute
318
+ self.name = name
319
+ self.shortname = shortname
320
+ self.itype = itype
321
+ self.icomposition = icomposition
322
+ self.is_shell = is_shell
323
+ self.rho = rho
324
+ self.rhobin = rhobin or []
325
+ self.arat = arat or []
326
+ self.kappa = kappa
327
+ self.refidx = refidx or []
328
+
329
+ def to_dict(self) -> Dict:
330
+ """Convert to dictionary."""
331
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
332
+
333
+
334
+ class CARMASoluteConfig:
335
+ """Configuration for a CARMA solute.
336
+
337
+ A CARMA solute represents a chemical species that can dissolve in water and affect particle properties.
338
+ """
339
+
340
+ def __init__(self,
341
+ name: str = "default_solute",
342
+ shortname: str = "",
343
+ ions: int = 0,
344
+ wtmol: float = 0.0,
345
+ rho: float = 0.0):
346
+ """
347
+ Initialize a CARMA solute configuration.
348
+
349
+ Args:
350
+ name: Name of the solute (default: "default_solute")
351
+ shortname: Short name for the solute (default: "")
352
+ ions: Number of ions (default: 0)
353
+ wtmol: Molecular weight in kg/mol (default: 0.0)
354
+ rho: Density in kg/m3 (default: 0.0)
355
+ """
356
+ self.name = name
357
+ self.shortname = shortname
358
+ self.ions = ions
359
+ self.wtmol = wtmol
360
+ self.rho = rho
361
+
362
+ def to_dict(self) -> Dict:
363
+ """Convert to dictionary."""
364
+ return {k: v for k, v in self.__dict__.items()}
365
+
366
+
367
+ class CARMAGasConfig:
368
+ """Configuration for a CARMA gas.
369
+
370
+ A CARMA gas represents a gaseous species in the atmosphere.
371
+ """
372
+
373
+ def __init__(self,
374
+ name: str = "default_gas",
375
+ shortname: str = "",
376
+ wtmol: float = 0.0,
377
+ ivaprtn: VaporizationAlgorithm = VaporizationAlgorithm.NONE,
378
+ icomposition: GasComposition = GasComposition.NONE,
379
+ dgc_threshold: float = 0.0,
380
+ ds_threshold: float = 0.0,
381
+ refidx: Optional[List[List[float]]] = None):
382
+ """
383
+ Initialize a CARMA gas configuration.
384
+
385
+ Args:
386
+ name: Name of the gas (default: "default_gas")
387
+ shortname: Short name for the gas (default: "")
388
+ wtmol: Molecular weight in kg/mol (default: 0.0)
389
+ ivaprtn: Vaporization algorithm used for this gas (default: VaporizationAlgorithm.NONE)
390
+ icomposition: Composition of the gas (default: GasComposition.NONE)
391
+ dgc_threshold: Threshold for gas density gradient (default: 0.0)
392
+ ds_threshold: Threshold for gas saturation (default: 0.0)
393
+ refidx: Reference indices for gas (default: None)
394
+ """
395
+ self.name = name
396
+ self.shortname = shortname
397
+ self.wtmol = wtmol
398
+ self.ivaprtn = ivaprtn
399
+ self.icomposition = icomposition
400
+ self.dgc_threshold = dgc_threshold
401
+ self.ds_threshold = ds_threshold
402
+ self.refidx = refidx or []
403
+
404
+ def to_dict(self) -> Dict:
405
+ """Convert to dictionary."""
406
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
407
+
408
+
409
+ class CARMACoagulationConfig:
410
+ """Configuration for CARMA coagulation process.
411
+
412
+ This class defines how particles coagulate in the CARMA model.
413
+ """
414
+
415
+ def __init__(self,
416
+ igroup1: int = 1,
417
+ igroup2: int = 1,
418
+ igroup3: int = 1,
419
+ algorithm: int = ParticleCollectionAlgorithm.CONSTANT,
420
+ ck0: float = -1.0,
421
+ grav_e_coll0: float = 0.0,
422
+ use_ccd: bool = False):
423
+ """
424
+ Initialize a CARMA coagulation configuration.
425
+
426
+ Args:
427
+ igroup1: First group index (default: 1)
428
+ igroup2: Second group index (default: 1)
429
+ igroup3: Third group index (default: 1)
430
+ algorithm: Coagulation algorithm (default: ParticleCollectionAlgorithm.CONSTANT)
431
+ ck0: Collection efficiency constant (default: -1.0). If -1.0, it will not be specified when setting up the coagulation process in carma
432
+ grav_e_coll0: Gravitational collection efficiency constant (default: 0.0)
433
+ use_ccd: Whether to use constant collection efficiency data (default: False)
434
+ """
435
+ self.igroup1 = igroup1
436
+ self.igroup2 = igroup2
437
+ self.igroup3 = igroup3
438
+ self.algorithm = algorithm
439
+ self.ck0 = ck0
440
+ self.grav_e_coll0 = grav_e_coll0
441
+ self.use_ccd = use_ccd
442
+
443
+ def to_dict(self) -> Dict:
444
+ """Convert to dictionary."""
445
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
446
+
447
+
448
+ class CARMAGrowthConfig:
449
+ """Configuration for CARMA particle growth process.
450
+
451
+ This class defines how particles grow in the CARMA model.
452
+ """
453
+
454
+ def __init__(self,
455
+ ielem: int = 0,
456
+ igas: int = 0):
457
+ """
458
+ Initialize a CARMA growth configuration.
459
+
460
+ Args:
461
+ ielem: Element index for the particles (default: 0)
462
+ igas: Index of the gas (default: 0)
463
+ """
464
+ self.ielem = ielem
465
+ self.igas = igas
466
+
467
+ def to_dict(self) -> Dict:
468
+ """Convert to dictionary."""
469
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
470
+
471
+
472
+ class CARMANucleationConfig:
473
+ """Configuration for CARMA particle nucleation process.
474
+
475
+ This class defines how new particles are formed in the CARMA model.
476
+ """
477
+
478
+ def __init__(self,
479
+ ielemfrom: int = 0,
480
+ ielemto: int = 0,
481
+ algorithm: ParticleNucleationAlgorithm = ParticleNucleationAlgorithm.NONE,
482
+ rlh_nuc: float = 0.0,
483
+ igas: int = 0,
484
+ ievp2elem: int = 0):
485
+ """
486
+ Initialize a CARMA nucleation configuration.
487
+
488
+ Args:
489
+ ielemfrom: Element index to nucleate from (default: 0)
490
+ ielemto: Element index to nucleate to (default: 0)
491
+ algorithm: Nucleation algorithm (default: ParticleNucleationAlgorithm.NONE)
492
+ rlh_nuc: Latent heat of nucleation [m2 s-2] (default: 0.0)
493
+ igas: Gas index to nucleate from (default: 0)
494
+ ievp2elem: Element index to evaporate to (if applicable) (default: 0)
495
+ """
496
+ self.ielemfrom = ielemfrom
497
+ self.ielemto = ielemto
498
+ self.algorithm = algorithm
499
+ self.rlh_nuc = rlh_nuc
500
+ self.igas = igas
501
+ self.ievp2elem = ievp2elem
502
+
503
+ def to_dict(self) -> Dict:
504
+ """Convert to dictionary."""
505
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
506
+
507
+
508
+ class CARMAInitializationConfig:
509
+ """Configuration for CARMA initialization.
510
+
511
+ This class defines how the CARMA model is initialized before running simulations.
512
+ """
513
+
514
+ def __init__(self,
515
+ do_cnst_rlh: bool = False,
516
+ do_detrain: bool = False,
517
+ do_fixedinit: bool = False,
518
+ do_incloud: bool = False,
519
+ do_explised: bool = False,
520
+ do_substep: bool = False,
521
+ do_thermo: bool = False,
522
+ do_vdiff: bool = False,
523
+ do_vtran: bool = False,
524
+ do_drydep: bool = False,
525
+ do_pheat: bool = False,
526
+ do_pheatatm: bool = False,
527
+ do_clearsky: bool = False,
528
+ do_partialinit: bool = False,
529
+ do_coremasscheck: bool = False,
530
+ sulfnucl_method: SulfateNucleationMethod = SulfateNucleationMethod.NONE,
531
+ vf_const: float = 0.0,
532
+ minsubsteps: int = 1,
533
+ maxsubsteps: int = 1,
534
+ maxretries: int = 5,
535
+ conmax: float = 1.0e-1,
536
+ dt_threshold: float = 0.0,
537
+ cstick: float = 1.0,
538
+ gsticki: float = 0.93,
539
+ gstickl: float = 1.0,
540
+ tstick: float = 1.0):
541
+ """
542
+ Initialize a CARMA initialization configuration.
543
+
544
+ Args:
545
+ do_cnst_rlh: Use constant values for latent heats (default: False)
546
+ do_detrain: Do detrainment (default: False)
547
+ do_fixedinit: Use fixed initialization from reference atmosphere (default: False)
548
+ do_incloud: Do in-cloud processes (growth, coagulation) (default: False)
549
+ do_explised: Do sedimentation with substepping (default: False)
550
+ do_substep: Do substepping (default: False)
551
+ do_thermo: Do thermodynamic processes (default: False)
552
+ do_vdiff: Do Brownian diffusion (default: False)
553
+ do_vtran: Do sedimentation (default: True)
554
+ do_drydep: Do dry deposition (default: False)
555
+ do_pheat: Do particle heating (default: False)
556
+ do_pheatatm: Do particle heating of atmosphere (default: False)
557
+ do_clearsky: Do clear sky growth and coagulation (default: False)
558
+ do_partialinit: Do initialization of coagulation from reference atmosphere (requires do_fixedinit) (default: False)
559
+ do_coremasscheck: Check core mass for particles (default: False)
560
+ sulfnucl_method: Method for sulfate nucleation (default: SulfateNucleationMethod.NONE)
561
+ vf_const: Constant fall velocity [m/s] (0: off) (default: 0.0)
562
+ minsubsteps: Minimum number of substeps (default: 1)
563
+ maxsubsteps: Maximum number of substeps (default: 1)
564
+ maxretries: Maximum number of retries (default: 5)
565
+ conmax: Minimum relative concentration to consider (default: 1.0e-1)
566
+ dt_threshold: Convergence criteria for temperature [fraction] (0: off) (default: 0.0)
567
+ cstick: Accommodation coefficient for coagulation (default: 1.0)
568
+ gsticki: Accommodation coefficient for growth of ice (default: 0.93)
569
+ gstickl: Accommodation coefficient for growth of liquid (default: 1.0)
570
+ tstick: Accommodation coefficient temperature (default: 1.0)
571
+ """
572
+ self.do_cnst_rlh = do_cnst_rlh
573
+ self.do_detrain = do_detrain
574
+ self.do_fixedinit = do_fixedinit
575
+ self.do_incloud = do_incloud
576
+ self.do_explised = do_explised
577
+ self.do_substep = do_substep
578
+ self.do_thermo = do_thermo
579
+ self.do_vdiff = do_vdiff
580
+ self.do_vtran = do_vtran
581
+ self.do_drydep = do_drydep
582
+ self.do_pheat = do_pheat
583
+ self.do_pheatatm = do_pheatatm
584
+ self.do_clearsky = do_clearsky
585
+ self.do_partialinit = do_partialinit
586
+ self.do_coremasscheck = do_coremasscheck
587
+ self.sulfnucl_method = sulfnucl_method
588
+ self.vf_const = vf_const
589
+ self.minsubsteps = minsubsteps
590
+ self.maxsubsteps = maxsubsteps
591
+ self.maxretries = maxretries
592
+ self.conmax = conmax
593
+ self.dt_threshold = dt_threshold
594
+ self.cstick = cstick
595
+ self.gsticki = gsticki
596
+ self.gstickl = gstickl
597
+ self.tstick = tstick
598
+
599
+ def to_dict(self) -> Dict:
600
+ """Convert to dictionary, converting Enums to ints."""
601
+ return {k: (v.value if isinstance(v, Enum) else v) for k, v in self.__dict__.items()}
602
+
603
+
604
+ class CARMAParameters:
605
+ """
606
+ Parameters for CARMA aerosol model simulation.
607
+
608
+ This class encapsulates all the parameters needed to configure and run
609
+ a CARMA simulation, including model dimensions, time stepping, and
610
+ spatial parameters.
611
+ """
612
+
613
+ def __init__(self,
614
+ nbin: int = 5,
615
+ nz: int = 1,
616
+ dtime: float = 1800.0,
617
+ wavelength_bins: Optional[List[CARMAWavelengthBin]] = None,
618
+ groups: Optional[List[CARMAGroupConfig]] = None,
619
+ elements: Optional[List[CARMAElementConfig]] = None,
620
+ solutes: Optional[List[CARMASoluteConfig]] = None,
621
+ gases: Optional[List[CARMAGasConfig]] = None,
622
+ coagulations: Optional[List[CARMACoagulationConfig]] = None,
623
+ growths: Optional[List[CARMAGrowthConfig]] = None,
624
+ nucleations: Optional[List[CARMANucleationConfig]] = None,
625
+ initialization: Optional[CARMAInitializationConfig] = None):
626
+ """
627
+ Initialize CARMA parameters.
628
+
629
+ Args:
630
+ nbin: Number of size bins (default: 5)
631
+ nz: Number of vertical levels (default: 1)
632
+ dtime: Time step in seconds (default: 1800.0)
633
+ wavelength_bins: List of CARMAWavelengthBin objects defining the wavelength grid (default: None)
634
+ groups: List of group configurations (default: None)
635
+ elements: List of element configurations (default: None)
636
+ solutes: List of solute configurations (default: None)
637
+ gases: List of gas configurations (default: None)
638
+ coagulations: List of coagulation configurations (default: None)
639
+ growths: List of growth configurations (default: None)
640
+ nucleations: List of nucleation configurations (default: None)
641
+ initialization: Initialization configuration (default: None)
642
+ """
643
+ self.nbin = nbin
644
+ self.dtime = dtime
645
+ self.nz = nz
646
+
647
+ # Initialize lists
648
+ self.wavelength_bins = wavelength_bins or []
649
+ self.groups = groups or []
650
+ self.elements = elements or []
651
+ self.solutes = solutes or []
652
+ self.gases = gases or []
653
+ self.coagulations = coagulations or []
654
+ self.growths = growths or []
655
+ self.nucleations = nucleations or []
656
+ self.initialization = initialization or CARMAInitializationConfig()
657
+
658
+ def add_wavelength_bin(self, wavelength_bin: CARMAWavelengthBin):
659
+ """Add a wavelength bin configuration."""
660
+ self.wavelength_bins.append(wavelength_bin)
661
+
662
+ def add_group(self, group: CARMAGroupConfig):
663
+ """Add a group configuration."""
664
+ self.groups.append(group)
665
+
666
+ def add_element(self, element: CARMAElementConfig):
667
+ """Add an element configuration."""
668
+ self.elements.append(element)
669
+
670
+ def add_solute(self, solute: CARMASoluteConfig):
671
+ """Add a solute configuration."""
672
+ self.solutes.append(solute)
673
+
674
+ def add_gas(self, gas: CARMAGasConfig):
675
+ """Add a gas configuration."""
676
+ self.gases.append(gas)
677
+
678
+ def add_coagulation(self, coagulation: CARMACoagulationConfig):
679
+ """Add a coagulation configuration."""
680
+ self.coagulations.append(coagulation)
681
+
682
+ def add_growth(self, growth: CARMAGrowthConfig):
683
+ """Add a growth configuration."""
684
+ self.growths.append(growth)
685
+
686
+ def add_nucleation(self, nucleation: CARMANucleationConfig):
687
+ """Add a nucleation configuration."""
688
+ self.nucleations.append(nucleation)
689
+
690
+ def set_initialization(self, initialization: CARMAInitializationConfig):
691
+ """Set the initialization configuration."""
692
+ self.initialization = initialization
693
+
694
+ def __repr__(self):
695
+ """String representation of CARMAParameters."""
696
+ return (f"CARMAParameters(nbin={self.nbin}, dtime={self.dtime}, nz={self.nz}, "
697
+ f"wavelength_bins={len(self.wavelength_bins)}, "
698
+ f"groups={len(self.groups)}, elements={len(self.elements)}, "
699
+ f"solutes={len(self.solutes)}, gases={len(self.gases)}, "
700
+ f"coagulations={len(self.coagulations)}, growths={len(self.growths)}, "
701
+ f"nucleations={len(self.nucleations)})")
702
+
703
+ def __str__(self):
704
+ """String representation of CARMAParameters."""
705
+ return (f"CARMAParameters(nbin={self.nbin}, dtime={self.dtime}, nz={self.nz}, "
706
+ f"wavelength_bins={len(self.wavelength_bins)}, "
707
+ f"groups={len(self.groups)}, elements={len(self.elements)}, "
708
+ f"solutes={len(self.solutes)}, gases={len(self.gases)}, "
709
+ f"coagulations={len(self.coagulations)}, growths={len(self.growths)}, "
710
+ f"nucleations={len(self.nucleations)})")
711
+
712
+ def to_dict(self) -> Dict:
713
+ """Convert parameters to dictionary for C++ interface."""
714
+ # Get all basic attributes
715
+ params_dict = {}
716
+ for k, v in self.__dict__.items():
717
+ if not k.startswith('__') and not callable(v):
718
+ if k == 'groups':
719
+ params_dict[k] = [group.to_dict() for group in v]
720
+ elif k == 'elements':
721
+ params_dict[k] = [element.to_dict() for element in v]
722
+ elif k == 'solutes':
723
+ params_dict[k] = [solute.to_dict() for solute in v]
724
+ elif k == 'gases':
725
+ params_dict[k] = [gas.to_dict() for gas in v]
726
+ elif k == 'coagulations':
727
+ params_dict[k] = [coagulation.to_dict()
728
+ for coagulation in v]
729
+ elif k == 'growths':
730
+ params_dict[k] = [growth.to_dict() for growth in v]
731
+ elif k == 'nucleations':
732
+ params_dict[k] = [nucleation.to_dict() for nucleation in v]
733
+ elif k == 'wavelength_bins':
734
+ params_dict[k] = [bin.to_dict() for bin in v]
735
+ elif k == 'initialization':
736
+ params_dict[k] = v.to_dict() if v else None
737
+ else:
738
+ if isinstance(v, Enum):
739
+ params_dict[k] = v.value
740
+ else:
741
+ params_dict[k] = v
742
+ return params_dict
743
+
744
+ @classmethod
745
+ def from_dict(cls, params_dict: Dict) -> 'CARMAParameters':
746
+ """Create parameters from dictionary."""
747
+ # Handle lists separately
748
+ wavelength_bins = []
749
+ if 'wavelength_bins' in params_dict:
750
+ wavelength_bins = [CARMAWavelengthBin(**bin_dict)
751
+ for bin_dict in params_dict['wavelength_bins']]
752
+ del params_dict['wavelength_bins']
753
+ groups = []
754
+ if 'groups' in params_dict:
755
+ groups = [CARMAGroupConfig(**group_dict)
756
+ for group_dict in params_dict['groups']]
757
+ del params_dict['groups']
758
+
759
+ elements = []
760
+ if 'elements' in params_dict:
761
+ elements = [CARMAElementConfig(**element_dict)
762
+ for element_dict in params_dict['elements']]
763
+ del params_dict['elements']
764
+
765
+ solutes = []
766
+ if 'solutes' in params_dict:
767
+ solutes = [CARMASoluteConfig(**solute_dict)
768
+ for solute_dict in params_dict['solutes']]
769
+ del params_dict['solutes']
770
+
771
+ gases = []
772
+ if 'gases' in params_dict:
773
+ gases = [CARMAGasConfig(**gas_dict)
774
+ for gas_dict in params_dict['gases']]
775
+ del params_dict['gases']
776
+
777
+ coagulations = []
778
+ if 'coagulations' in params_dict:
779
+ coagulations = [CARMACoagulationConfig(**coag_dict)
780
+ for coag_dict in params_dict['coagulations']]
781
+ del params_dict['coagulations']
782
+
783
+ growths = []
784
+ if 'growths' in params_dict:
785
+ growths = [CARMAGrowthConfig(**growth_dict)
786
+ for growth_dict in params_dict['growths']]
787
+ del params_dict['growths']
788
+
789
+ nucleations = []
790
+ if 'nucleations' in params_dict:
791
+ nucleations = [CARMANucleationConfig(**nucleation_dict)
792
+ for nucleation_dict in params_dict['nucleations']]
793
+ del params_dict['nucleations']
794
+
795
+ initialization = None
796
+ if 'initialization' in params_dict and params_dict['initialization']:
797
+ initialization = CARMAInitializationConfig(
798
+ **params_dict['initialization'])
799
+ del params_dict['initialization']
800
+
801
+ return cls(
802
+ wavelength_bins=wavelength_bins,
803
+ groups=groups,
804
+ elements=elements,
805
+ solutes=solutes,
806
+ gases=gases,
807
+ coagulations=coagulations,
808
+ growths=growths,
809
+ nucleations=nucleations,
810
+ initialization=initialization,
811
+ **params_dict)
812
+
813
+
814
+ class CARMASurfaceProperties:
815
+ """
816
+ Represents the surface properties used in CARMA simulations.
817
+
818
+ This class encapsulates the surface properties such as friction velocity,
819
+ aerodynamic resistance, and area fraction, which are used in CARMA simulations
820
+ to model the interaction between the atmosphere and the surface.
821
+ """
822
+
823
+ def __init__(self,
824
+ surface_friction_velocity: float = 0.0,
825
+ aerodynamic_resistance: float = 0.0,
826
+ area_fraction: float = 0.0):
827
+ """
828
+ Initialize CARMASurfaceProperties instance.
829
+
830
+ Args:
831
+ surface_friction_velocity: Friction velocity at the surface [m/s] (default: 0.0)
832
+ aerodynamic_resistance: Aerodynamic resistance at the surface [s/m] (default: 0.0)
833
+ area_fraction: Area fraction of the surface [fraction] (default: 0.0)
834
+ """
835
+ self.surface_friction_velocity = surface_friction_velocity
836
+ self.aerodynamic_resistance = aerodynamic_resistance
837
+ self.area_fraction = area_fraction
838
+
839
+ def __repr__(self):
840
+ """Represent the surface properties as a string."""
841
+ return (f"CARMASurfaceProperties("
842
+ f"surface_friction_velocity={self.surface_friction_velocity}, "
843
+ f"aerodynamic_resistance={self.aerodynamic_resistance}, "
844
+ f"area_fraction={self.area_fraction})")
845
+
846
+ def __str__(self):
847
+ """String representation of surface properties."""
848
+ return (f"Surface Friction Velocity: {self.surface_friction_velocity} m/s, "
849
+ f"Aerodynamic Resistance: {self.aerodynamic_resistance} s/m, "
850
+ f"Area Fraction: {self.area_fraction}")
851
+
852
+ def to_dict(self) -> Dict:
853
+ """Convert surface properties to dictionary."""
854
+ return {
855
+ 'surface_friction_velocity': self.surface_friction_velocity,
856
+ 'aerodynamic_resistance': self.aerodynamic_resistance,
857
+ 'area_fraction': self.area_fraction
858
+ }
859
+
860
+
861
+ class CARMAState:
862
+ """
863
+ Represents the environmental variables used in CARMA simulations."""
864
+
865
+ def __init__(self,
866
+ carma_pointer: c_void_p,
867
+ vertical_center: List[float],
868
+ vertical_levels: List[float],
869
+ pressure: List[float],
870
+ pressure_levels: List[float],
871
+ temperature: List[float],
872
+ original_temperature: Optional[List[float]] = None,
873
+ relative_humidity: Optional[List[float]] = None,
874
+ specific_humidity: Optional[List[float]] = None,
875
+ radiative_intensity: Optional[List[List[float]]] = None,
876
+ time: float = 0.0,
877
+ time_step: float = 1.0,
878
+ latitude: float = 0.0,
879
+ longitude: float = 0.0,
880
+ coordinates: CarmaCoordinates = CarmaCoordinates.CARTESIAN,
881
+ gases: Optional[List[CARMAGasConfig]] = None,
882
+ ):
883
+ """
884
+ Initialize a CARMAState instance.
885
+
886
+ Args:
887
+ carma_pointer: Pointer to the CARMA C++ instance
888
+ vertical_center: List of vertical center heights in meters
889
+ vertical_levels: List of vertical levels in meters
890
+ pressure: List of pressures at vertical centers in Pascals
891
+ pressure_levels: List of pressures at vertical levels in Pascals
892
+ temperature: List of temperatures at vertical centers in Kelvin
893
+ original_temperature: List of original temperatures at vertical centers in Kelvin (default: None) If None, will use temperature
894
+ relative_humidity: List of relative humidity at vertical centers in percent (default: None)
895
+ specific_humidity: List of specific humidity at vertical centers in kg/kg (default: None)
896
+ radiative_intensity: List of radiative intensity at vertical centers in W/m² (wavelength, vertical_center) (default: None)
897
+ time: Simulation time in seconds (default: 0.0)
898
+ time_step: Time step in seconds (default: 1.0)
899
+ latitude: Latitude in degrees (default: 0.0)
900
+ longitude: Longitude in degrees (default: 0.0)
901
+ coordinates: Coordinate system for the simulation (default: Cartesian)
902
+ gases: List of gas configurations
903
+ """
904
+ self.gases = gases or []
905
+ self.longitude = longitude
906
+ self.latitude = latitude
907
+ self.coordinates = coordinates
908
+ self.n_levels = len(vertical_center)
909
+ if original_temperature is None:
910
+ original_temperature = temperature
911
+ self.vertical_center = vertical_center
912
+ self.vertical_levels = vertical_levels
913
+
914
+ self.dimensions = _backend._carma._get_dimensions(carma_pointer)
915
+ self._carma_state_instance = _backend._carma._create_carma_state(
916
+ carma_pointer=carma_pointer,
917
+ time=time,
918
+ time_step=time_step,
919
+ latitude=latitude,
920
+ longitude=longitude,
921
+ coordinates=coordinates.value,
922
+ temperature=temperature,
923
+ original_temperature=original_temperature,
924
+ pressure=pressure,
925
+ pressure_levels=pressure_levels,
926
+ vertical_center=vertical_center,
927
+ vertical_levels=vertical_levels,
928
+ relative_humidity=relative_humidity,
929
+ specific_humidity=specific_humidity,
930
+ radiative_intensity=radiative_intensity
931
+ )
932
+
933
+ def __del__(self):
934
+ """Clean up the CARMAState instance."""
935
+ if hasattr(self, '_carma_state_instance') and self._carma_state_instance is not None:
936
+ _backend._carma._delete_carma_state(self._carma_state_instance)
937
+
938
+ def __repr__(self):
939
+ """String representation of CARMAState."""
940
+ return (f"CARMAState")
941
+
942
+ def __str__(self):
943
+ """String representation of CARMAState."""
944
+ return (f"CARMAState")
945
+
946
+ def to_dict(self) -> Dict:
947
+ """Convert CARMAState to dictionary."""
948
+ return {k: v for k, v in self.__dict__.items() if not k.startswith('__') and not callable(v)}
949
+
950
+ def set_bin(self,
951
+ bin_index: int,
952
+ element_index: int,
953
+ value: Union[float, List[float]],
954
+ surface_mass: Optional[float] = 0.0):
955
+ """
956
+ Set the value for a specific bin and element.
957
+
958
+ Args:
959
+ bin_index: Index of the size bin (1-indexed)
960
+ element_index: Index of the element (1-indexed)
961
+ value: Value to set, can be a single float or a list of floats
962
+ surface_mass: Optional surface mass for the bin [kg m-2] (default: 0.0)
963
+ """
964
+ if np.isscalar(value):
965
+ value = np.repeat(value, self.n_levels).tolist()
966
+ elif not isinstance(value, list):
967
+ # Ensure value is a list of floats with length n_levels
968
+ if isinstance(value, np.ndarray):
969
+ value = value.tolist()
970
+ if not isinstance(value, list):
971
+ # Convert to list if not already
972
+ value = list(value)
973
+ if len(value) != self.n_levels:
974
+ raise ValueError(
975
+ f"Value must be a scalar or a list of length {self.n_levels}, got length {len(value)}")
976
+ if not all(isinstance(v, float) for v in value):
977
+ raise ValueError("All elements in value must be floats")
978
+ _backend._carma._set_bin(
979
+ self._carma_state_instance, bin_index, element_index, value, surface_mass)
980
+
981
+ def set_detrain(self, bin_index: int, element_index: int, value: float):
982
+ """
983
+ Set the mass of the detrained condensate for the bin
984
+
985
+ Args:
986
+ bin_index: Index of the size bin (1-indexed)
987
+ element_index: Index of the element (1-indexed)
988
+ value: Value to set
989
+ """
990
+ if np.isscalar(value):
991
+ value = np.repeat(value, self.n_levels).tolist()
992
+ elif not isinstance(value, list):
993
+ value = list(value)
994
+ _backend._carma._set_detrain(
995
+ self._carma_state_instance, bin_index, element_index, value)
996
+
997
+ def set_gas(self,
998
+ gas_index: int,
999
+ value: Union[float,
1000
+ List[float]],
1001
+ old_mmr: Optional[List[float]] = None,
1002
+ gas_saturation_wrt_ice: Optional[List[float]] = None,
1003
+ gas_saturation_wrt_liquid: Optional[List[float]] = None):
1004
+ """
1005
+ Set the value for a specific gas.
1006
+
1007
+ Args:
1008
+ gas_index: Index of the gas (1-indexed)
1009
+ value: Value to set, can be a single float or a list of floats
1010
+ old_mmr: Optional list of old mass mixing ratios for the gas (default: None)
1011
+ gas_saturation_wrt_ice: Optional list of gas saturation with respect to ice (default: None)
1012
+ gas_saturation_wrt_liquid: Optional list of gas saturation with respect to liquid (default: None)
1013
+ """
1014
+ if np.isscalar(value):
1015
+ value = np.repeat(value, self.n_levels).tolist()
1016
+ elif not isinstance(value, list):
1017
+ value = list(value)
1018
+ if old_mmr is None:
1019
+ old_mmr = []
1020
+ if gas_saturation_wrt_ice is None:
1021
+ gas_saturation_wrt_ice = []
1022
+ if gas_saturation_wrt_liquid is None:
1023
+ gas_saturation_wrt_liquid = []
1024
+ if not isinstance(old_mmr, list):
1025
+ old_mmr = list(old_mmr)
1026
+ if not isinstance(gas_saturation_wrt_ice, list):
1027
+ gas_saturation_wrt_ice = list(gas_saturation_wrt_ice)
1028
+ if not isinstance(gas_saturation_wrt_liquid, list):
1029
+ gas_saturation_wrt_liquid = list(gas_saturation_wrt_liquid)
1030
+ _backend._carma._set_gas(
1031
+ self._carma_state_instance,
1032
+ gas_index,
1033
+ value,
1034
+ old_mmr,
1035
+ gas_saturation_wrt_ice,
1036
+ gas_saturation_wrt_liquid)
1037
+
1038
+ def get_step_statistics(self) -> Dict[str, Any]:
1039
+ """
1040
+ Get the step statistics for the current CARMAState.
1041
+
1042
+ Returns:
1043
+ Dict[str, Any]: Dictionary containing step statistics such as
1044
+ number of substeps, convergence status, etc.
1045
+ """
1046
+ return _backend._carma._get_step_statistics(self._carma_state_instance)
1047
+
1048
+ def get_bins(self) -> xr.Dataset:
1049
+ """
1050
+ Get the CARMA aerosol state data for all bins and elements.
1051
+
1052
+ Returns:
1053
+ Dataset: Aerosol bin properties for all bins and elements
1054
+ """
1055
+
1056
+ # Collect bin data for each property into arrays
1057
+ # Shape: [number_of_bins, number_of_elements, n_levels] for vertical properties
1058
+ # and [number_of_bins, number_of_elements] for surface/level properties
1059
+
1060
+ number_of_bins = self.dimensions["number_of_bins"]
1061
+ number_of_elements = self.dimensions["number_of_elements"]
1062
+ n_levels = len(self.vertical_center)
1063
+ n_edges = len(self.vertical_levels)
1064
+
1065
+ # Initialize property arrays
1066
+ properties = [
1067
+ "mass_mixing_ratio",
1068
+ "number_mixing_ratio",
1069
+ "number_density",
1070
+ "nucleation_rate",
1071
+ "wet_particle_radius",
1072
+ "wet_particle_density",
1073
+ "dry_particle_density",
1074
+ "particle_mass_on_surface",
1075
+ "sedimentation_flux",
1076
+ "fall_velocity",
1077
+ "deposition_velocity",
1078
+ "delta_particle_temperature",
1079
+ "kappa",
1080
+ "total_mass_mixing_ratio"
1081
+ ]
1082
+
1083
+ # Determine which properties are per vertical_center, per vertical_level, or per bin/element only
1084
+ per_vertical_center = [
1085
+ "mass_mixing_ratio",
1086
+ "number_mixing_ratio",
1087
+ "number_density",
1088
+ "nucleation_rate",
1089
+ "wet_particle_radius",
1090
+ "wet_particle_density",
1091
+ "dry_particle_density",
1092
+ "delta_particle_temperature",
1093
+ "kappa",
1094
+ "total_mass_mixing_ratio"
1095
+ ]
1096
+ per_vertical_level = [
1097
+ "fall_velocity"
1098
+ ]
1099
+ per_bin_element = [
1100
+ "particle_mass_on_surface",
1101
+ "sedimentation_flux",
1102
+ "deposition_velocity"
1103
+ ]
1104
+
1105
+ # Prepare arrays
1106
+ data = {prop: [] for prop in properties}
1107
+ for i_bin in range(number_of_bins):
1108
+ for i_elem in range(number_of_elements):
1109
+ bin_value = _backend._carma._get_bin(
1110
+ self._carma_state_instance,
1111
+ i_bin + 1,
1112
+ i_elem + 1
1113
+ )
1114
+ for prop in properties:
1115
+ data[prop].append(bin_value.get(prop))
1116
+
1117
+ # Reshape arrays
1118
+ def reshape(arr, shape):
1119
+ return np.array(arr).reshape(shape)
1120
+
1121
+ # Helper for units and long names
1122
+ def _get_attributes(prop):
1123
+ attributes = {
1124
+ "mass_mixing_ratio": {
1125
+ "units": "kg kg-1",
1126
+ "long_name": "Aerosol particle mass mixing ratio"
1127
+ },
1128
+ "number_mixing_ratio": {
1129
+ "units": "kg-1",
1130
+ "long_name": "Aerosol particle number mixing ratio"
1131
+ },
1132
+ "number_density": {
1133
+ "units": "m-3",
1134
+ "long_name": "Aerosol particle number density"
1135
+ },
1136
+ "nucleation_rate": {
1137
+ "units": "m-3 s-1",
1138
+ "long_name": "Aerosol particle nucleation rate"
1139
+ },
1140
+ "wet_particle_radius": {
1141
+ "units": "m",
1142
+ "long_name": "Wet aerosol particle radius"
1143
+ },
1144
+ "wet_particle_density": {
1145
+ "units": "kg m-3",
1146
+ "long_name": "Wet aerosol particle density"
1147
+ },
1148
+ "dry_particle_density": {
1149
+ "units": "kg m-3",
1150
+ "long_name": "Dry aerosol particle density"
1151
+ },
1152
+ "particle_mass_on_surface": {
1153
+ "units": "kg m-2",
1154
+ "long_name": "Aerosol particle mass on surface"
1155
+ },
1156
+ "sedimentation_flux": {
1157
+ "units": "kg m-2 s-1",
1158
+ "long_name": "Aerosol particle sedimentation flux"
1159
+ },
1160
+ "fall_velocity": {
1161
+ "units": "m s-1",
1162
+ "long_name": "Aerosol particle fall velocity"
1163
+ },
1164
+ "deposition_velocity": {
1165
+ "units": "m s-1",
1166
+ "long_name": "Aerosol particle deposition velocity"
1167
+ },
1168
+ "delta_particle_temperature": {
1169
+ "units": "K",
1170
+ "long_name": "Aerosol particle temperature change"
1171
+ },
1172
+ "kappa": {
1173
+ "units": "-",
1174
+ "long_name": "Aerosol particle hygroscopicity parameter"
1175
+ },
1176
+ "total_mass_mixing_ratio": {
1177
+ "units": "kg m-3",
1178
+ "long_name": "Total aerosol particle mass mixing ratio"
1179
+ }
1180
+ }
1181
+ return attributes.get(prop, {"units": "", "long_name": ""})
1182
+
1183
+ dataset_vars = {}
1184
+
1185
+ # Per-vertical_center properties: shape (number_of_bins, number_of_elements, n_levels)
1186
+ for prop in per_vertical_center:
1187
+ arr = reshape(data[prop], (number_of_bins, number_of_elements, n_levels))
1188
+ dataset_vars[prop] = (("bin", "element", "vertical_center"), arr, _get_attributes(prop))
1189
+
1190
+ # Per-vertical_level properties: shape (number_of_bins, number_of_elements, n_edges)
1191
+ for prop in per_vertical_level:
1192
+ arr = reshape(data[prop], (number_of_bins, number_of_elements, n_edges))
1193
+ dataset_vars[prop] = (("bin", "element", "vertical_level"), arr, _get_attributes(prop))
1194
+
1195
+ # Per-bin/element only properties: shape (number_of_bins, number_of_elements)
1196
+ for prop in per_bin_element:
1197
+ arr = reshape(data[prop], (number_of_bins, number_of_elements))
1198
+ dataset_vars[prop] = (("bin", "element"), arr, _get_attributes(prop))
1199
+
1200
+ return xr.Dataset(
1201
+ data_vars=dataset_vars,
1202
+ coords={
1203
+ "bin": np.arange(1, number_of_bins + 1),
1204
+ "element": np.arange(1, number_of_elements + 1),
1205
+ "vertical_center": self.vertical_center,
1206
+ "vertical_level": self.vertical_levels
1207
+ }
1208
+ )
1209
+
1210
+ def get_detrained_masses(self) -> xr.Dataset:
1211
+ """
1212
+ Get the mass of the detrained condensate for the bin for each particle in the grid
1213
+
1214
+ Returns:
1215
+ List[float]: Mass of the detrained condensate for the specified bin and element
1216
+ """
1217
+
1218
+ number_of_bins = self.dimensions["number_of_bins"]
1219
+ number_of_elements = self.dimensions["number_of_elements"]
1220
+ n_levels = len(self.vertical_center)
1221
+
1222
+ # Initialize property arrays
1223
+ properties = [
1224
+ "mass_mixing_ratio",
1225
+ "number_mixing_ratio",
1226
+ "number_density",
1227
+ "wet_particle_radius",
1228
+ "wet_particle_density"
1229
+ ]
1230
+
1231
+ # Prepare arrays
1232
+ data = {prop: [] for prop in properties}
1233
+ for i_bin in range(number_of_bins):
1234
+ for i_elem in range(number_of_elements):
1235
+ bin_value = _backend._carma._get_detrain(
1236
+ self._carma_state_instance,
1237
+ i_bin + 1,
1238
+ i_elem + 1
1239
+ )
1240
+ for prop in properties:
1241
+ data[prop].append(bin_value.get(prop))
1242
+
1243
+ # Reshape arrays
1244
+ def reshape(arr, shape):
1245
+ return np.array(arr).reshape(shape)
1246
+
1247
+ # Helper for units
1248
+ def _get_units(prop):
1249
+ units = {
1250
+ "mass_mixing_ratio": "kg kg-1",
1251
+ "number_mixing_ratio": "kg-1",
1252
+ "number_density": "m-3",
1253
+ "wet_particle_radius": "m",
1254
+ "wet_particle_density": "kg m-3"
1255
+ }
1256
+ return units.get(prop, "")
1257
+
1258
+ dataset_vars = {}
1259
+
1260
+ # Per-vertical_center properties: shape (number_of_bins, number_of_elements, n_levels)
1261
+ for prop in properties:
1262
+ arr = reshape(data[prop], (number_of_bins, number_of_elements, n_levels))
1263
+ dataset_vars[prop] = (("bin", "element", "vertical_center"), arr, {"units": _get_units(prop)})
1264
+
1265
+ return xr.Dataset(
1266
+ data_vars=dataset_vars,
1267
+ coords={
1268
+ "bin": np.arange(1, number_of_bins + 1),
1269
+ "element": np.arange(1, number_of_elements + 1),
1270
+ "vertical_center": self.vertical_center
1271
+ }
1272
+ )
1273
+
1274
+ def get_gases(self) -> Tuple[xr.Dataset, Dict[str, int]]:
1275
+ """
1276
+ Get the values for all gases.
1277
+
1278
+ Returns:
1279
+ Tuple[xr.Dataset, Dict[str, int]] A dataset containing values for all gases and a mapping of gas names to their indices.
1280
+ """
1281
+ number_of_gases = self.dimensions["number_of_gases"]
1282
+ n_levels = len(self.vertical_center)
1283
+
1284
+ # Initialize property arrays
1285
+ properties = [
1286
+ "mass_mixing_ratio",
1287
+ "gas_saturation_wrt_ice",
1288
+ "gas_saturation_wrt_liquid",
1289
+ "gas_vapor_pressure_wrt_ice",
1290
+ "gas_vapor_pressure_wrt_liquid",
1291
+ "weight_pct_aerosol_composition"
1292
+ ]
1293
+
1294
+ # Prepare arrays
1295
+ data = {prop: [] for prop in properties}
1296
+ for i_gas in range(number_of_gases):
1297
+ gas_value = _backend._carma._get_gas(
1298
+ self._carma_state_instance,
1299
+ i_gas + 1
1300
+ )
1301
+ for prop in properties:
1302
+ data[prop].append(gas_value.get(prop))
1303
+
1304
+ # Reshape arrays
1305
+ def reshape(arr, shape):
1306
+ return np.array(arr).reshape(shape)
1307
+
1308
+ # Helper for units
1309
+ def _get_units(prop):
1310
+ units = {
1311
+ "mass_mixing_ratio": "kg kg-1",
1312
+ "gas_saturation_wrt_ice": "none",
1313
+ "gas_saturation_wrt_liquid": "none",
1314
+ "gas_vapor_pressure_wrt_ice": "none",
1315
+ "gas_vapor_pressure_wrt_liquid": "none",
1316
+ "weight_pct_aerosol_composition": "none"
1317
+ }
1318
+ return units.get(prop, "")
1319
+
1320
+ dataset_vars = {}
1321
+
1322
+ # Per-vertical_center properties: shape (number_of_gases, n_levels)
1323
+ for prop in properties:
1324
+ arr = reshape(data[prop], (number_of_gases, n_levels))
1325
+ if prop == 'mass_mixing_ratio':
1326
+ dataset_vars[f'gas_{prop}'] = (("gas", "vertical_center"), arr, {"units": _get_units(prop)})
1327
+ else:
1328
+ dataset_vars[prop] = (("gas", "vertical_center"), arr, {"units": _get_units(prop)})
1329
+
1330
+ return xr.Dataset(
1331
+ data_vars=dataset_vars,
1332
+ coords={
1333
+ "gas": [gas.shortname for gas in self.gases],
1334
+ "vertical_center": self.vertical_center
1335
+ }
1336
+ ), {gas.shortname: idx for idx, gas in enumerate(self.gases)}
1337
+
1338
+ def get_environmental_values(self) -> xr.Dataset:
1339
+ """
1340
+ Get all environmental conditions for the current CARMAState.
1341
+
1342
+ Returns:
1343
+ xr.Dataset: Dataset containing all environmental conditions
1344
+ """
1345
+ n_levels = len(self.vertical_center)
1346
+
1347
+ data = _backend._carma._get_environmental_values(self._carma_state_instance)
1348
+
1349
+ # Reshape arrays
1350
+ def reshape(arr, shape):
1351
+ return np.array(arr).reshape(shape)
1352
+
1353
+ # Helper for units and long names
1354
+ def _get_attributes(prop):
1355
+ attributes = {
1356
+ "temperature": {
1357
+ "units": "K",
1358
+ "long_name": "Temperature"
1359
+ },
1360
+ "pressure": {
1361
+ "units": "Pa",
1362
+ "long_name": "Pressure"
1363
+ },
1364
+ "air_density": {
1365
+ "units": "kg m-3",
1366
+ "long_name": "Air density"
1367
+ },
1368
+ "latent_heat": {
1369
+ "units": "K s-1",
1370
+ "long_name": "Latent heat release rate"
1371
+ }
1372
+ }
1373
+ return attributes.get(prop, {"units": "", "long_name": ""})
1374
+
1375
+ dataset_vars = {}
1376
+
1377
+ for prop in data:
1378
+ if prop is not None:
1379
+ arr = reshape(data[prop], (n_levels,))
1380
+ dataset_vars[prop] = (("vertical_center"), arr, _get_attributes(prop))
1381
+
1382
+ return xr.Dataset(
1383
+ data_vars=dataset_vars,
1384
+ coords={
1385
+ "vertical_center": self.vertical_center
1386
+ }
1387
+ )
1388
+
1389
+ def set_temperature(self, temperature: Union[float, List[float]]):
1390
+ """
1391
+ Set the temperature for the vertical levels.
1392
+
1393
+ Args:
1394
+ temperature: Temperature value to set, can be a single float or a list of floats
1395
+ """
1396
+ if np.isscalar(temperature):
1397
+ temperature = np.repeat(temperature, self.n_levels).tolist()
1398
+ elif not isinstance(temperature, list):
1399
+ temperature = list(temperature)
1400
+ _backend._carma._set_temperature(
1401
+ self._carma_state_instance, temperature)
1402
+
1403
+ def set_air_density(self, air_density: Union[float, List[float]]):
1404
+ """
1405
+ Set the air density for the vertical levels.
1406
+
1407
+ Args:
1408
+ air_density: Air density value to set, can be a single float or a list of floats
1409
+ """
1410
+ if np.isscalar(air_density):
1411
+ air_density = np.repeat(air_density, self.n_levels).tolist()
1412
+ elif not isinstance(air_density, list):
1413
+ air_density = list(air_density)
1414
+ _backend._carma._set_air_density(
1415
+ self._carma_state_instance, air_density)
1416
+
1417
+ def step(
1418
+ self,
1419
+ cloud_fraction: Optional[List[float]] = None,
1420
+ critical_relative_humidity: Optional[List[float]] = None,
1421
+ land: Optional[CARMASurfaceProperties] = None,
1422
+ ocean: Optional[CARMASurfaceProperties] = None,
1423
+ ice: Optional[CARMASurfaceProperties] = None
1424
+ ):
1425
+ """
1426
+ Perform a single step in the CARMA simulation.
1427
+
1428
+ Args:
1429
+ cloud_fraction: Optional list of cloud fractions for each vertical level (default: None)
1430
+ critical_relative_humidity: Optional list of critical relative humidities for each vertical level (default: None)
1431
+ land: Optional CARMASurfaceProperties instance representing land surface properties (default: None)
1432
+ ocean: Optional CARMASurfaceProperties instance representing ocean surface properties (default: None)
1433
+ ice: Optional CARMASurfaceProperties instance representing ice surface properties (default: None)
1434
+ """
1435
+ _backend._carma._step(
1436
+ self._carma_state_instance,
1437
+ cloud_fraction=cloud_fraction,
1438
+ critical_relative_humidity=critical_relative_humidity,
1439
+ land=land,
1440
+ ocean=ocean,
1441
+ ice=ice
1442
+ )
1443
+
1444
+
1445
+ class CARMA:
1446
+ """
1447
+ A Python interface to the CARMA aerosol model.
1448
+
1449
+ This class provides a simplified interface for running CARMA simulations
1450
+ with configurable parameters.
1451
+ """
1452
+
1453
+ def __init__(self, parameters: CARMAParameters):
1454
+ """
1455
+ Initialize a CARMA instance.
1456
+
1457
+ Raises:
1458
+ ValueError: If CARMA backend is not available
1459
+ """
1460
+ if not backend.carma_available():
1461
+ raise ValueError(
1462
+ "CARMA backend is not available on this platform.")
1463
+
1464
+ self._carma_instance = _backend._carma._create_carma(
1465
+ parameters.to_dict())
1466
+ self.__parameters = parameters
1467
+
1468
+ def __del__(self):
1469
+ """Clean up the CARMA instance."""
1470
+ if hasattr(self, '_carma_instance') and self._carma_instance is not None:
1471
+ _backend._carma._delete_carma(self._carma_instance)
1472
+
1473
+ def __repr__(self):
1474
+ """String representation of CARMA instance."""
1475
+ return f"CARMA() - Version: {version if version else 'Not available'}"
1476
+
1477
+ def __str__(self):
1478
+ """String representation of CARMA instance."""
1479
+ return f"CARMA() - Version: {version if version else 'Not available'}"
1480
+
1481
+ def create_state(self, **kwargs) -> CARMAState:
1482
+ """
1483
+ Create a CARMAState instance based on the current parameters.
1484
+
1485
+ Args:
1486
+ **kwargs: Additional keyword arguments to pass to CARMAState
1487
+
1488
+ Returns:
1489
+ CARMAState: Instance containing environmental variables for the simulation
1490
+ """
1491
+
1492
+ return CARMAState(
1493
+ self._carma_instance,
1494
+ gases=self.__parameters.gases,
1495
+ **kwargs
1496
+ )
1497
+
1498
+ def get_group_properties(self) -> Tuple[xr.Dataset, Dict[str, int]]:
1499
+ """
1500
+ Get the group properties for all groups.
1501
+
1502
+ Returns:
1503
+ Tuple[xr.Dataset, Dict[str, int]]: The group properties for all groups and a dictionary with their indices
1504
+ """
1505
+ number_of_groups = len(self.__parameters.groups)
1506
+ number_of_elements = len(self.__parameters.elements)
1507
+ number_of_bins = self.__parameters.nbin
1508
+ number_of_wavelength_bins = len(self.__parameters.wavelength_bins)
1509
+
1510
+ if number_of_groups == 0:
1511
+ return xr.Dataset(), {}
1512
+
1513
+ # Initialize the property arrays
1514
+ properties = [
1515
+ "bin_radius",
1516
+ "bin_radius_lower_bound",
1517
+ "bin_radius_upper_bound",
1518
+ "bin_width",
1519
+ "bin_mass",
1520
+ "bin_width_mass",
1521
+ "bin_volume",
1522
+ "projected_area_ratio",
1523
+ "radius_ratio",
1524
+ "porosity_ratio",
1525
+ "extinction_coefficient",
1526
+ "single_scattering_albedo",
1527
+ "asymmetry_factor",
1528
+ "element_index_of_core_mass_elements",
1529
+ "number_of_monomers_per_bin",
1530
+ "particle_number_element_for_group",
1531
+ "number_of_core_mass_elements_for_group",
1532
+ "last_prognostic_bin"
1533
+ ]
1534
+
1535
+ # Group arrays by shape
1536
+ per_bin = [
1537
+ "bin_radius",
1538
+ "bin_radius_lower_bound",
1539
+ "bin_radius_upper_bound",
1540
+ "bin_width",
1541
+ "bin_mass",
1542
+ "bin_width_mass",
1543
+ "bin_volume",
1544
+ "projected_area_ratio",
1545
+ "radius_ratio",
1546
+ "porosity_ratio",
1547
+ "number_of_monomers_per_bin"
1548
+ ]
1549
+ per_bin_and_wavelength = [
1550
+ "extinction_coefficient",
1551
+ "single_scattering_albedo",
1552
+ "asymmetry_factor"
1553
+ ]
1554
+ per_element = [
1555
+ "element_index_of_core_mass_elements",
1556
+ ]
1557
+ per_group = [
1558
+ "particle_number_element_for_group",
1559
+ "number_of_core_mass_elements_for_group",
1560
+ "last_prognostic_bin"
1561
+ ]
1562
+
1563
+ # Prepare arrays
1564
+ data = {prop: [] for prop in properties}
1565
+ for i_group in range(number_of_groups):
1566
+ group_value = _backend._carma._get_group_properties(self._carma_instance, i_group + 1)
1567
+ for prop in properties:
1568
+ data[prop].append(group_value.get(prop))
1569
+
1570
+ # Reshape arrays
1571
+ def reshape(arr, shape):
1572
+ return np.array(arr).reshape(shape)
1573
+
1574
+ # Helper for units
1575
+ def _get_units(prop):
1576
+ units = {
1577
+ "bin_radius": "m",
1578
+ "bin_radius_lower_bound": "m",
1579
+ "bin_radius_upper_bound": "m",
1580
+ "bin_width": "m",
1581
+ "bin_mass": "kg",
1582
+ "bin_width_mass": "kg",
1583
+ "bin_volume": "m^3",
1584
+ "projected_area_ratio": "-",
1585
+ "radius_ratio": "-",
1586
+ "porosity_ratio": "-",
1587
+ "extinction_coefficient": "-",
1588
+ "single_scattering_albedo": "-",
1589
+ "asymmetry_factor": "-",
1590
+ "element_index_of_core_mass_elements": "-",
1591
+ "number_of_monomers_per_bin": "-",
1592
+ "particle_number_element_for_group": "-",
1593
+ "number_of_core_mass_elements_for_group": "-",
1594
+ "last_prognostic_bin": "-"
1595
+ }
1596
+ return units.get(prop, "")
1597
+
1598
+ dataset_vars = {}
1599
+
1600
+ # Per-bin properties: shape (number_of_groups, number_of_bins)
1601
+ for prop in per_bin:
1602
+ arr = reshape(data[prop], (number_of_groups, number_of_bins))
1603
+ dataset_vars[prop] = (("group", "bin"), arr, {"units": _get_units(prop)})
1604
+
1605
+ # Per-bin-and-wavelength properties: shape (number_of_groups, number_of_bins, number_of_wavelength_bins)
1606
+ for prop in per_bin_and_wavelength:
1607
+ arr = reshape(data[prop], (number_of_groups, number_of_bins, number_of_wavelength_bins))
1608
+ dataset_vars[prop] = (("group", "bin", "wavelength"), arr, {"units": _get_units(prop)})
1609
+
1610
+ # Per-element properties: shape (number_of_groups, number_of_elements)
1611
+ for prop in per_element:
1612
+ arr = reshape(data[prop], (number_of_groups, number_of_elements))
1613
+ dataset_vars[prop] = (("group", "element"), arr, {"units": _get_units(prop)})
1614
+
1615
+ # Per-group properties: shape (number_of_groups,)
1616
+ for prop in per_group:
1617
+ arr = reshape(data[prop], (number_of_groups,))
1618
+ dataset_vars[prop] = (("group",), arr, {"units": _get_units(prop)})
1619
+
1620
+ return (xr.Dataset(
1621
+ data_vars=dataset_vars,
1622
+ coords={
1623
+ "group": np.arange(1, number_of_groups + 1),
1624
+ "bin": np.arange(1, number_of_bins + 1),
1625
+ "wavelength": np.arange(1, number_of_wavelength_bins + 1),
1626
+ "element": np.arange(1, number_of_elements + 1)
1627
+ }
1628
+ ), self.__parameters.groups)
1629
+
1630
+ def get_element_properties(self) -> Tuple[xr.Dataset, Dict[str, int]]:
1631
+ """
1632
+ Get the element properties for all elements
1633
+
1634
+ Returns:
1635
+ Tuple[xr.Dataset, Dict[str, Any]]: The properties for each element and a dictionary of element indices
1636
+ """
1637
+ number_of_elements = len(self.__parameters.elements)
1638
+ number_of_bins = self.__parameters.nbin
1639
+ number_of_wavelength_bins = len(self.__parameters.wavelength_bins)
1640
+ number_of_refractive_indices = None
1641
+
1642
+ if number_of_elements == 0:
1643
+ return xr.Dataset(), {}
1644
+
1645
+ # Initialize the property arrays
1646
+ properties = [
1647
+ "mass_density",
1648
+ "refractive_indices",
1649
+ "hygroscopicity_parameter"
1650
+ ]
1651
+
1652
+ # Group arrays by shape
1653
+ per_bin = ["mass_density"]
1654
+ per_wavelength_and_refidx = ["refractive_indices"]
1655
+ per_element = ["hygroscopicity_parameter"]
1656
+
1657
+ # Prepare arrays
1658
+ data = {prop: [] for prop in properties}
1659
+ for i_elem in range(number_of_elements):
1660
+ element_value = _backend._carma._get_element_properties(self._carma_instance, i_elem + 1)
1661
+ if number_of_refractive_indices is None:
1662
+ number_of_refractive_indices = element_value["number_of_refractive_indices"]
1663
+ else:
1664
+ if number_of_refractive_indices != element_value["number_of_refractive_indices"]:
1665
+ raise ValueError("Inconsistent number of refractive indices found.")
1666
+ for prop in properties:
1667
+ data[prop].append(element_value.get(prop))
1668
+
1669
+ # Reshape arrays
1670
+ def reshape(arr, shape):
1671
+ return np.array(arr).reshape(shape)
1672
+
1673
+ # Helper for units
1674
+ def _get_units(prop):
1675
+ units = {
1676
+ "mass_density": "kg m-3",
1677
+ "refractive_indices": "-",
1678
+ "hygroscopicity_parameter": "-"
1679
+ }
1680
+ return units.get(prop, "")
1681
+
1682
+ dataset_vars = {}
1683
+
1684
+ # Per-bin properties: shape (number_of_elements, number_of_bins)
1685
+ for prop in per_bin:
1686
+ arr = reshape(data[prop], (number_of_elements, number_of_bins))
1687
+ dataset_vars[prop] = (("element", "bin"), arr, {"units": _get_units(prop)})
1688
+
1689
+ # Per-wavelength and refractive index properties: shape
1690
+ # (number_of_elements, number_of_refractive_indices,
1691
+ # number_of_wavelength_bins)
1692
+ for prop in per_wavelength_and_refidx:
1693
+ arr = reshape(data[prop], (number_of_elements, number_of_refractive_indices, number_of_wavelength_bins))
1694
+ dataset_vars[prop] = (("element", "refractive_index", "wavelength"), arr, {"units": _get_units(prop)})
1695
+
1696
+ # Per-element properties: shape (number_of_elements,)
1697
+ for prop in per_element:
1698
+ arr = reshape(data[prop], (number_of_elements,))
1699
+ dataset_vars[prop] = (("element",), arr, {"units": _get_units(prop)})
1700
+
1701
+ return (xr.Dataset(
1702
+ data_vars=dataset_vars,
1703
+ coords={
1704
+ "bin": np.arange(1, number_of_bins + 1),
1705
+ "wavelength": np.arange(1, number_of_wavelength_bins + 1),
1706
+ "refractive_index": np.arange(1, number_of_refractive_indices + 1),
1707
+ "element": np.arange(1, number_of_elements + 1)
1708
+ }
1709
+ ), self.__parameters.elements)
1710
+
1711
+ def get_gas_properties(self) -> CARMAGasConfig:
1712
+ """
1713
+ Get the gas properties for all gases.
1714
+
1715
+ Returns:
1716
+ CARMAGasConfig: The gas configurations
1717
+ """
1718
+ return self.__parameters.gases
1719
+
1720
+ def get_solute_properties(self) -> CARMASoluteConfig:
1721
+ """
1722
+ Get the solute properties for all solutes.
1723
+
1724
+ Returns:
1725
+ CARMASoluteConfig: The solute configurations
1726
+ """
1727
+ return self.__parameters.solutes