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.
- musica/__init__.py +23 -3
- musica/_musica.cp312-win_amd64.pyd +0 -0
- musica/_version.py +1 -1
- musica/backend.py +58 -0
- musica/carma/__init__.py +20 -0
- musica/carma/carma.py +1727 -0
- musica/constants.py +3 -0
- musica/cuda.py +13 -0
- musica/examples/__init__.py +1 -0
- musica/examples/carma_aluminum.py +124 -0
- musica/examples/carma_sulfate.py +246 -0
- musica/examples/examples.py +165 -0
- musica/examples/sulfate_box_model.py +439 -0
- musica/examples/ts1_latin_hypercube.py +245 -0
- musica/main.py +128 -0
- musica/mechanism_configuration/__init__.py +18 -0
- musica/mechanism_configuration/ancillary.py +6 -0
- musica/mechanism_configuration/arrhenius.py +149 -0
- musica/mechanism_configuration/branched.py +140 -0
- musica/mechanism_configuration/emission.py +82 -0
- musica/mechanism_configuration/first_order_loss.py +90 -0
- musica/mechanism_configuration/mechanism.py +93 -0
- musica/mechanism_configuration/phase.py +58 -0
- musica/mechanism_configuration/phase_species.py +58 -0
- musica/mechanism_configuration/photolysis.py +98 -0
- musica/mechanism_configuration/reaction_component.py +54 -0
- musica/mechanism_configuration/reactions.py +32 -0
- musica/mechanism_configuration/species.py +65 -0
- musica/mechanism_configuration/surface.py +98 -0
- musica/mechanism_configuration/taylor_series.py +136 -0
- musica/mechanism_configuration/ternary_chemical_activation.py +160 -0
- musica/mechanism_configuration/troe.py +160 -0
- musica/mechanism_configuration/tunneling.py +126 -0
- musica/mechanism_configuration/user_defined.py +99 -0
- musica/mechanism_configuration/utils.py +10 -0
- musica/micm/__init__.py +10 -0
- musica/micm/conditions.py +49 -0
- musica/micm/micm.py +135 -0
- musica/micm/solver.py +8 -0
- musica/micm/solver_result.py +24 -0
- musica/micm/state.py +220 -0
- musica/micm/utils.py +18 -0
- musica/tuvx/__init__.py +11 -0
- musica/tuvx/grid.py +98 -0
- musica/tuvx/grid_map.py +167 -0
- musica/tuvx/profile.py +130 -0
- musica/tuvx/profile_map.py +167 -0
- musica/tuvx/radiator.py +95 -0
- musica/tuvx/radiator_map.py +173 -0
- musica/tuvx/tuvx.py +283 -0
- musica-0.14.2.dist-info/DELVEWHEEL +2 -0
- {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/METADATA +146 -63
- musica-0.14.2.dist-info/RECORD +104 -0
- {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/WHEEL +1 -1
- musica-0.14.2.dist-info/entry_points.txt +3 -0
- musica-0.14.2.dist-info/licenses/AUTHORS.md +59 -0
- musica.libs/libaws-c-auth-0a61a643442f1c0912920b37d9fb0be5.dll +0 -0
- musica.libs/libaws-c-cal-eaafa5905de6c9ba274eb8737e6087dd.dll +0 -0
- musica.libs/libaws-c-common-b4aa4468297ae8e1664f9380a5510317.dll +0 -0
- musica.libs/libaws-c-compression-9f997952aeae03067122ca493c9081b5.dll +0 -0
- musica.libs/libaws-c-event-stream-fe9cc8e1692f60c2b5694a8959dbd7c3.dll +0 -0
- musica.libs/libaws-c-http-4a9d50ba6ad8882f5267ef89e5e4103a.dll +0 -0
- musica.libs/libaws-c-io-e454f1c7a44e77f8c957a016888754be.dll +0 -0
- musica.libs/libaws-c-mqtt-67c5fc291740f5cbc5e53fb767e93226.dll +0 -0
- musica.libs/libaws-c-s3-206db4af6e1a95637b1921ea596603b9.dll +0 -0
- musica.libs/libaws-c-sdkutils-5c9c62dafb8b774cd4a3386f95ef428d.dll +0 -0
- musica.libs/libaws-checksums-7e50fe01b862214958f4d2ab4215fde5.dll +0 -0
- musica.libs/libaws-cpp-sdk-core-7a9ba9c045ee16f5262e955d96865718.dll +0 -0
- musica.libs/libaws-cpp-sdk-s3-4eebff3923c6d250fb508da3c990e0ae.dll +0 -0
- musica.libs/libaws-crt-cpp-3173f1e6f504a96d88e8dbf9e04b3b14.dll +0 -0
- musica.libs/libbrotlicommon-c62c08223e450dfc2fff33c752cc2285.dll +0 -0
- musica.libs/libbrotlidec-ccde7c3978eb1d2e052b193f2968d30a.dll +0 -0
- musica.libs/libbz2-1-669a4bf9266d5f020e843aa5fd75b93c.dll +0 -0
- musica.libs/libcrypto-3-x64-237eeb55505d067eab5e0b886e519387.dll +0 -0
- musica.libs/libcurl-4-bdf865458887dc1235b192ec83729214.dll +0 -0
- musica.libs/libgcc_s_seh-1-5a3153f12338f79fbbb7bf095fc5cef1.dll +0 -0
- musica.libs/libgfortran-5-90848e0eacdecce3a9005faf5aaec7e7.dll +0 -0
- musica.libs/libgomp-1-b8afcf09fecd2f6f01e454c9a5f2c690.dll +0 -0
- musica.libs/libhdf5-320-eec6c8ba2fdde30d365786ffbff40989.dll +0 -0
- musica.libs/libhdf5_hl-320-7e26e1caaad6be4082d728cf08ab2de4.dll +0 -0
- musica.libs/libiconv-2-b37d1b4acab5310c4e4f6e2a961d1464.dll +0 -0
- musica.libs/libidn2-0-d17600177f3b4cd2521d595b3472d240.dll +0 -0
- musica.libs/libintl-8-e4d4ca6b37338fbb0a8c1246afa7258f.dll +0 -0
- musica.libs/liblzma-5-bd95aa0fda6e7c8e41b3843d6fc2942c.dll +0 -0
- musica.libs/libnetcdf-0623e518145bddd30cc615b6d7f2f9c1.dll +0 -0
- musica.libs/libnetcdff-7-982cb7ee026b78f05a79d00e735f91d1.dll +0 -0
- musica.libs/libnghttp2-14-6d49ed806389b4892bcf29c6ed6e3984.dll +0 -0
- musica.libs/libnghttp3-9-d3c9b57d760f6dae7d6a067a68126b84.dll +0 -0
- musica.libs/libngtcp2-16-a43356e6376d41ce4238e2c55581636a.dll +0 -0
- musica.libs/libngtcp2_crypto_ossl-0-b37121badf25a552e5654f27bf6ff093.dll +0 -0
- musica.libs/libopenblas-a16595c3cae114c5c7304aa8bb3c1272.dll +0 -0
- musica.libs/libpsl-5-4368d4c2412410a4a14f3e7f3227e295.dll +0 -0
- musica.libs/libquadmath-0-4edeffe0a60c96360445d33a1876dbda.dll +0 -0
- musica.libs/libssh2-1-f407a2b50419bd904c7eb2c101ae81ea.dll +0 -0
- musica.libs/libssl-3-x64-d2e43d36e6f87f6f1645717cd0871f86.dll +0 -0
- musica.libs/libstdc++-6-83061aaccaf8df77a3b584efef12bc7c.dll +0 -0
- musica.libs/libsz-2-d12f3d26417507ec8dea9964f9fe36a1.dll +0 -0
- musica.libs/libunistring-5-0473d7a71d94f08292beed694c34f7d1.dll +0 -0
- musica.libs/libwinpthread-1-9157bac12a85fb717fa3d2bf6712631a.dll +0 -0
- musica.libs/libxml2-16-7fe545d280fdef922282226eef91571f.dll +0 -0
- musica.libs/libzip-62d3c877b7842bc509fc000316a4731b.dll +0 -0
- musica.libs/libzstd-a25427164f8775046eb8ce488d7d0884.dll +0 -0
- musica.libs/zlib1-1dc85208162ee57fe97e892bb5160fe9.dll +0 -0
- _musica.cp312-win_amd64.pyd +0 -0
- lib/musica.lib +0 -0
- lib/yaml-cpp.lib +0 -0
- musica/CMakeLists.txt +0 -47
- musica/binding.cpp +0 -19
- musica/mechanism_configuration.cpp +0 -519
- musica/mechanism_configuration.py +0 -1291
- musica/musica.cpp +0 -214
- musica/test/examples/v0/config.json +0 -7
- musica/test/examples/v0/config.yaml +0 -3
- musica/test/examples/v0/reactions.json +0 -193
- musica/test/examples/v0/reactions.yaml +0 -142
- musica/test/examples/v0/species.json +0 -40
- musica/test/examples/v0/species.yaml +0 -19
- musica/test/examples/v1/full_configuration.json +0 -434
- musica/test/examples/v1/full_configuration.yaml +0 -271
- musica/test/test_analytical.py +0 -323
- musica/test/test_chapman.py +0 -123
- musica/test/test_parser.py +0 -693
- musica/test/tuvx.py +0 -10
- musica/tools/prepare_build_environment_linux.sh +0 -41
- musica/tools/prepare_build_environment_windows.sh +0 -22
- musica/tools/repair_wheel_gpu.sh +0 -25
- musica/types.py +0 -362
- musica-0.11.1.1.dist-info/RECORD +0 -30
- {musica-0.11.1.1.dist-info → musica-0.14.2.dist-info}/licenses/LICENSE +0 -0
musica/tuvx/grid_map.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 National Center for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
TUV-x GridMap class.
|
|
5
|
+
|
|
6
|
+
This module provides a class for managing collections of TUV-x grids.
|
|
7
|
+
The GridMap class allows dictionary-style access to grids using (name, units) tuples as keys.
|
|
8
|
+
|
|
9
|
+
Note: TUV-x is only available on macOS and Linux platforms.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Iterator
|
|
13
|
+
from .. import backend
|
|
14
|
+
from .grid import Grid
|
|
15
|
+
|
|
16
|
+
_backend = backend.get_backend()
|
|
17
|
+
|
|
18
|
+
GridMap = _backend._tuvx._GridMap if backend.tuvx_available() else None
|
|
19
|
+
|
|
20
|
+
if backend.tuvx_available():
|
|
21
|
+
original_init = GridMap.__init__
|
|
22
|
+
|
|
23
|
+
def __init__(self, **kwargs):
|
|
24
|
+
"""Initialize a GridMap instance.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
**kwargs: Additional arguments passed to the C++ constructor
|
|
28
|
+
"""
|
|
29
|
+
original_init(self, **kwargs)
|
|
30
|
+
|
|
31
|
+
GridMap.__init__ = __init__
|
|
32
|
+
|
|
33
|
+
def __str__(self):
|
|
34
|
+
"""User-friendly string representation."""
|
|
35
|
+
return f"GridMap(num_grids={len(self)})"
|
|
36
|
+
|
|
37
|
+
GridMap.__str__ = __str__
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
"""Detailed string representation for debugging."""
|
|
41
|
+
grid_details = []
|
|
42
|
+
for i in range(len(self)):
|
|
43
|
+
grid = self.get_grid_by_index(i)
|
|
44
|
+
grid_details.append(f"({grid.name}, {grid.units})")
|
|
45
|
+
return f"GridMap(grids={grid_details})"
|
|
46
|
+
|
|
47
|
+
GridMap.__repr__ = __repr__
|
|
48
|
+
|
|
49
|
+
def __len__(self):
|
|
50
|
+
"""Return the number of grids in the map."""
|
|
51
|
+
return self.get_number_of_grids()
|
|
52
|
+
|
|
53
|
+
GridMap.__len__ = __len__
|
|
54
|
+
|
|
55
|
+
def __bool__(self):
|
|
56
|
+
"""Return True if the map has any grids."""
|
|
57
|
+
return len(self) > 0
|
|
58
|
+
|
|
59
|
+
GridMap.__bool__ = __bool__
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, key) -> Grid:
|
|
62
|
+
"""Get a grid using dictionary-style access with (name, units) tuple as key.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
key: A tuple of (grid_name, grid_units)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The requested Grid object
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
KeyError: If no grid matches the given name and units
|
|
72
|
+
TypeError: If key is not a tuple of (str, str)
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
75
|
+
raise TypeError("Grid access requires a tuple of (name, units)")
|
|
76
|
+
name, units = key
|
|
77
|
+
try:
|
|
78
|
+
return self.get_grid(name, units)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise KeyError(f"No grid found with name='{name}' and units='{units}'") from e
|
|
81
|
+
|
|
82
|
+
GridMap.__getitem__ = __getitem__
|
|
83
|
+
|
|
84
|
+
def __setitem__(self, key, grid):
|
|
85
|
+
"""Add a grid to the map using dictionary-style access.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
key: A tuple of (grid_name, grid_units)
|
|
89
|
+
grid: The Grid object to add
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
TypeError: If key is not a tuple, or if key components are not strings
|
|
93
|
+
TypeError: If grid is not a Grid object
|
|
94
|
+
ValueError: If grid name/units don't match the key
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
97
|
+
raise TypeError("Grid assignment requires a tuple of (name, units)")
|
|
98
|
+
name, units = key
|
|
99
|
+
if not isinstance(name, str):
|
|
100
|
+
raise TypeError("Grid name must be a string")
|
|
101
|
+
if not isinstance(units, str):
|
|
102
|
+
raise TypeError("Grid units must be a string")
|
|
103
|
+
if not isinstance(grid, Grid):
|
|
104
|
+
raise TypeError("Value must be a Grid object")
|
|
105
|
+
if grid.name != name or grid.units != units:
|
|
106
|
+
raise ValueError("Grid name/units must match the key tuple")
|
|
107
|
+
self.add_grid(grid)
|
|
108
|
+
|
|
109
|
+
GridMap.__setitem__ = __setitem__
|
|
110
|
+
|
|
111
|
+
def __iter__(self) -> Iterator:
|
|
112
|
+
"""Return an iterator over (name, units) tuples of all grids."""
|
|
113
|
+
for i in range(len(self)):
|
|
114
|
+
grid = self.get_grid_by_index(i)
|
|
115
|
+
yield (grid.name, grid.units)
|
|
116
|
+
|
|
117
|
+
GridMap.__iter__ = __iter__
|
|
118
|
+
|
|
119
|
+
def __contains__(self, key) -> bool:
|
|
120
|
+
"""Check if a grid with given name and units exists in the map.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
key: A tuple of (grid_name, grid_units)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if a matching grid exists, False otherwise
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
129
|
+
return False
|
|
130
|
+
name, units = key
|
|
131
|
+
try:
|
|
132
|
+
grid = self.get_grid(str(name), str(units))
|
|
133
|
+
return grid is not None
|
|
134
|
+
except (ValueError, KeyError):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
GridMap.__contains__ = __contains__
|
|
138
|
+
|
|
139
|
+
def clear(self):
|
|
140
|
+
"""Remove all grids from the map."""
|
|
141
|
+
while len(self) > 0:
|
|
142
|
+
self.remove_grid_by_index(0)
|
|
143
|
+
|
|
144
|
+
GridMap.clear = clear
|
|
145
|
+
|
|
146
|
+
def items(self):
|
|
147
|
+
"""Return an iterator over (key, grid) pairs, where key is (name, units)."""
|
|
148
|
+
for i in range(len(self)):
|
|
149
|
+
grid = self.get_grid_by_index(i)
|
|
150
|
+
yield ((grid.name, grid.units), grid)
|
|
151
|
+
|
|
152
|
+
GridMap.items = items
|
|
153
|
+
|
|
154
|
+
def keys(self):
|
|
155
|
+
"""Return an iterator over grid keys (name, units) tuples."""
|
|
156
|
+
for i in range(len(self)):
|
|
157
|
+
grid = self.get_grid_by_index(i)
|
|
158
|
+
yield (grid.name, grid.units)
|
|
159
|
+
|
|
160
|
+
GridMap.keys = keys
|
|
161
|
+
|
|
162
|
+
def values(self):
|
|
163
|
+
"""Return an iterator over Grid objects in the map."""
|
|
164
|
+
for i in range(len(self)):
|
|
165
|
+
yield self.get_grid_by_index(i)
|
|
166
|
+
|
|
167
|
+
GridMap.values = values
|
musica/tuvx/profile.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 National Center for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
TUV-x Profile class.
|
|
5
|
+
|
|
6
|
+
This module provides a class for defining profiles in TUV-x. Profiles represent
|
|
7
|
+
physical quantities that vary along a grid, such as temperature or species
|
|
8
|
+
concentrations.
|
|
9
|
+
|
|
10
|
+
Note: TUV-x is only available on macOS and Linux platforms.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
import numpy as np
|
|
15
|
+
from .. import backend
|
|
16
|
+
from .grid import Grid
|
|
17
|
+
|
|
18
|
+
_backend = backend.get_backend()
|
|
19
|
+
|
|
20
|
+
Profile = _backend._tuvx._Profile if backend.tuvx_available() else None
|
|
21
|
+
|
|
22
|
+
if backend.tuvx_available():
|
|
23
|
+
original_init = Profile.__init__
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, name: str, units: str, grid: Grid,
|
|
26
|
+
edge_values: Optional[np.ndarray] = None,
|
|
27
|
+
midpoint_values: Optional[np.ndarray] = None,
|
|
28
|
+
layer_densities: Optional[np.ndarray] = None,
|
|
29
|
+
calculate_layer_densities: bool = False,
|
|
30
|
+
**kwargs):
|
|
31
|
+
"""Initialize a Profile instance.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Name of the profile
|
|
35
|
+
units: Units of the profile values
|
|
36
|
+
grid: Grid on which the profile is defined
|
|
37
|
+
edge_values: Optional array of values at grid edges (length num_sections + 1)
|
|
38
|
+
midpoint_values: Optional array of values at grid midpoints (length num_sections)
|
|
39
|
+
layer_densities: Optional array of layer densities (length num_sections)
|
|
40
|
+
calculate_layer_densities: If True, calculate layer densities from midpoint values
|
|
41
|
+
**kwargs: Additional arguments passed to the C++ constructor
|
|
42
|
+
"""
|
|
43
|
+
# Call the original C++ constructor correctly
|
|
44
|
+
original_init(self, name=name, units=units, grid=grid, **kwargs)
|
|
45
|
+
|
|
46
|
+
# Set optional values if provided, otherwise calculate them
|
|
47
|
+
if edge_values is None and midpoint_values is None:
|
|
48
|
+
self.edge_values = np.zeros(grid.num_sections + 1, dtype=np.float64)
|
|
49
|
+
self.midpoint_values = np.zeros(grid.num_sections, dtype=np.float64)
|
|
50
|
+
self.layer_densities = np.zeros(grid.num_sections, dtype=np.float64)
|
|
51
|
+
if edge_values is not None:
|
|
52
|
+
self.edge_values = edge_values
|
|
53
|
+
elif midpoint_values is not None:
|
|
54
|
+
edge_values = np.zeros(midpoint_values.size + 1, dtype=midpoint_values.dtype)
|
|
55
|
+
edge_values[1:-1] = 0.5 * (midpoint_values[:-1] + midpoint_values[1:])
|
|
56
|
+
# Extrapolate first and last edges
|
|
57
|
+
edge_values[0] = midpoint_values[0] - (edge_values[1] - midpoint_values[0])
|
|
58
|
+
edge_values[-1] = midpoint_values[-1] + (midpoint_values[-1] - edge_values[-2])
|
|
59
|
+
self.edge_values = edge_values
|
|
60
|
+
if midpoint_values is not None:
|
|
61
|
+
self.midpoint_values = midpoint_values
|
|
62
|
+
elif edge_values is not None:
|
|
63
|
+
self.midpoint_values = 0.5 * (edge_values[:-1] + edge_values[1:])
|
|
64
|
+
if layer_densities is not None:
|
|
65
|
+
if calculate_layer_densities:
|
|
66
|
+
raise ValueError("Cannot provide layer_densities and set calculate_layer_densities=True")
|
|
67
|
+
self.layer_densities = layer_densities
|
|
68
|
+
elif calculate_layer_densities:
|
|
69
|
+
self.calculate_layer_densities(grid)
|
|
70
|
+
|
|
71
|
+
Profile.__init__ = __init__
|
|
72
|
+
|
|
73
|
+
def calculate_layer_densities(self, grid: Grid, conv: Optional[float] = None):
|
|
74
|
+
"""Calculate layer densities from midpoint values and grid spacing.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
conv: Conversion factor to apply (default is 1.0 or 1.0e5 for height in km and concentrations in molecules cm-3)
|
|
78
|
+
"""
|
|
79
|
+
# Workaround for current non-SI units in TUV-x, layer densities must be in molecules/cm2
|
|
80
|
+
# and heights in km. This will be fixed in a future TUV-x release.
|
|
81
|
+
if conv is None:
|
|
82
|
+
if grid.name == "height" and grid.units == "km" and self.units == "molecule cm-3":
|
|
83
|
+
conv = 1e5
|
|
84
|
+
else:
|
|
85
|
+
conv = 1.0
|
|
86
|
+
deltas = grid.edges[1:] - grid.edges[:-1]
|
|
87
|
+
self.layer_densities = self.midpoint_values * deltas * conv
|
|
88
|
+
|
|
89
|
+
Profile.calculate_layer_densities = calculate_layer_densities
|
|
90
|
+
|
|
91
|
+
def __str__(self):
|
|
92
|
+
"""User-friendly string representation."""
|
|
93
|
+
return f"Profile(name={self.name}, units={self.units}, number_of_sections={self.number_of_sections})"
|
|
94
|
+
|
|
95
|
+
Profile.__str__ = __str__
|
|
96
|
+
|
|
97
|
+
def __repr__(self):
|
|
98
|
+
"""Detailed string representation for debugging."""
|
|
99
|
+
return (f"Profile(name={self.name}, units={self.units}, number_of_sections={self.number_of_sections}, "
|
|
100
|
+
f"edge_values={self.edge_values}, midpoint_values={self.midpoint_values}, "
|
|
101
|
+
f"layer_densities={self.layer_densities}, "
|
|
102
|
+
f"exo_layer_density={self.exo_layer_density})")
|
|
103
|
+
|
|
104
|
+
Profile.__repr__ = __repr__
|
|
105
|
+
|
|
106
|
+
def __len__(self):
|
|
107
|
+
"""Return the number of sections in the grid."""
|
|
108
|
+
return self.number_of_sections
|
|
109
|
+
|
|
110
|
+
Profile.__len__ = __len__
|
|
111
|
+
|
|
112
|
+
def __eq__(self, other):
|
|
113
|
+
"""Check equality with another Profile instance."""
|
|
114
|
+
if not isinstance(other, Profile):
|
|
115
|
+
return NotImplemented
|
|
116
|
+
return (self.name == other.name and
|
|
117
|
+
self.units == other.units and
|
|
118
|
+
self.number_of_sections == other.number_of_sections and
|
|
119
|
+
np.array_equal(self.edge_values, other.edge_values) and
|
|
120
|
+
np.array_equal(self.midpoint_values, other.midpoint_values) and
|
|
121
|
+
np.array_equal(self.layer_densities, other.layer_densities) and
|
|
122
|
+
self.exo_layer_density == other.exo_layer_density)
|
|
123
|
+
|
|
124
|
+
Profile.__eq__ = __eq__
|
|
125
|
+
|
|
126
|
+
def __bool__(self):
|
|
127
|
+
"""Return True if the profile has a name, units, and one or more sections."""
|
|
128
|
+
return bool(self.name) and bool(self.units) and self.number_of_sections > 0
|
|
129
|
+
|
|
130
|
+
Profile.__bool__ = __bool__
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 National Center for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
TUV-x ProfileMap class.
|
|
5
|
+
|
|
6
|
+
This module provides a class for managing collections of TUV-x profiles.
|
|
7
|
+
The ProfileMap class allows dictionary-style access to profiles using (name, units) tuples as keys.
|
|
8
|
+
|
|
9
|
+
Note: TUV-x is only available on macOS and Linux platforms.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Iterator
|
|
13
|
+
from .. import backend
|
|
14
|
+
from .profile import Profile
|
|
15
|
+
|
|
16
|
+
_backend = backend.get_backend()
|
|
17
|
+
|
|
18
|
+
ProfileMap = _backend._tuvx._ProfileMap if backend.tuvx_available() else None
|
|
19
|
+
|
|
20
|
+
if backend.tuvx_available():
|
|
21
|
+
original_init = ProfileMap.__init__
|
|
22
|
+
|
|
23
|
+
def __init__(self, **kwargs):
|
|
24
|
+
"""Initialize a ProfileMap instance.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
**kwargs: Additional arguments passed to the C++ constructor
|
|
28
|
+
"""
|
|
29
|
+
original_init(self, **kwargs)
|
|
30
|
+
|
|
31
|
+
ProfileMap.__init__ = __init__
|
|
32
|
+
|
|
33
|
+
def __str__(self):
|
|
34
|
+
"""User-friendly string representation."""
|
|
35
|
+
return f"ProfileMap(num_profiles={len(self)})"
|
|
36
|
+
|
|
37
|
+
ProfileMap.__str__ = __str__
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
"""Detailed string representation for debugging."""
|
|
41
|
+
profile_details = []
|
|
42
|
+
for i in range(len(self)):
|
|
43
|
+
profile = self.get_profile_by_index(i)
|
|
44
|
+
profile_details.append(f"({profile.name}, {profile.units})")
|
|
45
|
+
return f"ProfileMap(profiles={profile_details})"
|
|
46
|
+
|
|
47
|
+
ProfileMap.__repr__ = __repr__
|
|
48
|
+
|
|
49
|
+
def __len__(self):
|
|
50
|
+
"""Return the number of profiles in the map."""
|
|
51
|
+
return self.get_number_of_profiles()
|
|
52
|
+
|
|
53
|
+
ProfileMap.__len__ = __len__
|
|
54
|
+
|
|
55
|
+
def __bool__(self):
|
|
56
|
+
"""Return True if the map has any profiles."""
|
|
57
|
+
return len(self) > 0
|
|
58
|
+
|
|
59
|
+
ProfileMap.__bool__ = __bool__
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, key) -> Profile:
|
|
62
|
+
"""Get a profile using dictionary-style access with (name, units) tuple as key.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
key: A tuple of (profile_name, profile_units)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The requested Profile object
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
KeyError: If no profile matches the given name and units
|
|
72
|
+
TypeError: If key is not a tuple of (str, str)
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
75
|
+
raise TypeError("Profile access requires a tuple of (name, units)")
|
|
76
|
+
name, units = key
|
|
77
|
+
try:
|
|
78
|
+
return self.get_profile(name, units)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise KeyError(f"No profile found with name='{name}' and units='{units}'") from e
|
|
81
|
+
|
|
82
|
+
ProfileMap.__getitem__ = __getitem__
|
|
83
|
+
|
|
84
|
+
def __setitem__(self, key, profile):
|
|
85
|
+
"""Add a profile to the map using dictionary-style access.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
key: A tuple of (profile_name, profile_units)
|
|
89
|
+
profile: The Profile object to add
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
TypeError: If key is not a tuple, or if key components are not strings
|
|
93
|
+
TypeError: If profile is not a Profile object
|
|
94
|
+
ValueError: If profile name/units don't match the key
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
97
|
+
raise TypeError("Profile assignment requires a tuple of (name, units)")
|
|
98
|
+
name, units = key
|
|
99
|
+
if not isinstance(name, str):
|
|
100
|
+
raise TypeError("Profile name must be a string")
|
|
101
|
+
if not isinstance(units, str):
|
|
102
|
+
raise TypeError("Profile units must be a string")
|
|
103
|
+
if not isinstance(profile, Profile):
|
|
104
|
+
raise TypeError("Value must be a Profile object")
|
|
105
|
+
if profile.name != name or profile.units != units:
|
|
106
|
+
raise ValueError("Profile name/units must match the key tuple")
|
|
107
|
+
self.add_profile(profile)
|
|
108
|
+
|
|
109
|
+
ProfileMap.__setitem__ = __setitem__
|
|
110
|
+
|
|
111
|
+
def __iter__(self) -> Iterator:
|
|
112
|
+
"""Return an iterator over (name, units) tuples of all profiles."""
|
|
113
|
+
for i in range(len(self)):
|
|
114
|
+
profile = self.get_profile_by_index(i)
|
|
115
|
+
yield (profile.name, profile.units)
|
|
116
|
+
|
|
117
|
+
ProfileMap.__iter__ = __iter__
|
|
118
|
+
|
|
119
|
+
def __contains__(self, key) -> bool:
|
|
120
|
+
"""Check if a profile with given name and units exists in the map.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
key: A tuple of (profile_name, profile_units)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if a matching profile exists, False otherwise
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
129
|
+
return False
|
|
130
|
+
name, units = key
|
|
131
|
+
try:
|
|
132
|
+
profile = self.get_profile(str(name), str(units))
|
|
133
|
+
return profile is not None
|
|
134
|
+
except (ValueError, KeyError):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
ProfileMap.__contains__ = __contains__
|
|
138
|
+
|
|
139
|
+
def clear(self):
|
|
140
|
+
"""Remove all profiles from the map."""
|
|
141
|
+
while len(self) > 0:
|
|
142
|
+
self.remove_profile_by_index(0)
|
|
143
|
+
|
|
144
|
+
ProfileMap.clear = clear
|
|
145
|
+
|
|
146
|
+
def items(self):
|
|
147
|
+
"""Return an iterator over (key, profile) pairs, where key is (name, units)."""
|
|
148
|
+
for i in range(len(self)):
|
|
149
|
+
profile = self.get_profile_by_index(i)
|
|
150
|
+
yield ((profile.name, profile.units), profile)
|
|
151
|
+
|
|
152
|
+
ProfileMap.items = items
|
|
153
|
+
|
|
154
|
+
def keys(self):
|
|
155
|
+
"""Return an iterator over profile keys (name, units) tuples."""
|
|
156
|
+
for i in range(len(self)):
|
|
157
|
+
profile = self.get_profile_by_index(i)
|
|
158
|
+
yield (profile.name, profile.units)
|
|
159
|
+
|
|
160
|
+
ProfileMap.keys = keys
|
|
161
|
+
|
|
162
|
+
def values(self):
|
|
163
|
+
"""Return an iterator over Profile objects in the map."""
|
|
164
|
+
for i in range(len(self)):
|
|
165
|
+
yield self.get_profile_by_index(i)
|
|
166
|
+
|
|
167
|
+
ProfileMap.values = values
|
musica/tuvx/radiator.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
TUV-x Radiator class.
|
|
5
|
+
|
|
6
|
+
This module provides a class for defining radiators in TUV-x. Radiators represent
|
|
7
|
+
optically active species that should be considered in radiative transfer calculations.
|
|
8
|
+
|
|
9
|
+
Note: TUV-x is only available on macOS and Linux platforms.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional
|
|
13
|
+
import numpy as np
|
|
14
|
+
from .. import backend
|
|
15
|
+
from .grid import Grid
|
|
16
|
+
|
|
17
|
+
_backend = backend.get_backend()
|
|
18
|
+
|
|
19
|
+
Radiator = _backend._tuvx._Radiator if backend.tuvx_available() else None
|
|
20
|
+
|
|
21
|
+
if backend.tuvx_available():
|
|
22
|
+
original_init = Radiator.__init__
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, name: str, height_grid: Grid, wavelength_grid: Grid,
|
|
25
|
+
optical_depths: Optional[np.ndarray] = None,
|
|
26
|
+
single_scattering_albedos: Optional[np.ndarray] = None,
|
|
27
|
+
asymmetry_factors: Optional[np.ndarray] = None,
|
|
28
|
+
**kwargs):
|
|
29
|
+
"""Initialize a Radiator instance.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Name of the radiator
|
|
33
|
+
height_grid: Grid on which the radiator is defined (height)
|
|
34
|
+
wavelength_grid: Grid on which the radiator is defined (wavelength)
|
|
35
|
+
optical_depths: Optional 2D array of optical depths
|
|
36
|
+
(shape: num_heights x num_wavelengths)
|
|
37
|
+
single_scattering_albedos: Optional 2D array of single scattering albedos
|
|
38
|
+
(shape: num_heights x num_wavelengths)
|
|
39
|
+
asymmetry_factors: Optional 2D array of asymmetry parameters
|
|
40
|
+
(shape: num_heights x num_wavelengths)
|
|
41
|
+
**kwargs: Additional arguments passed to the C++ constructor
|
|
42
|
+
"""
|
|
43
|
+
# Call the original C++ constructor correctly
|
|
44
|
+
original_init(self, name=name, height_grid=height_grid,
|
|
45
|
+
wavelength_grid=wavelength_grid, **kwargs)
|
|
46
|
+
|
|
47
|
+
# Set properties if provided
|
|
48
|
+
if optical_depths is not None:
|
|
49
|
+
self.optical_depths = optical_depths
|
|
50
|
+
if single_scattering_albedos is not None:
|
|
51
|
+
self.single_scattering_albedos = single_scattering_albedos
|
|
52
|
+
if asymmetry_factors is not None:
|
|
53
|
+
self.asymmetry_factors = asymmetry_factors
|
|
54
|
+
|
|
55
|
+
Radiator.__init__ = __init__
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
"""User-friendly string representation."""
|
|
59
|
+
return (f"Radiator(name={self.name}, "
|
|
60
|
+
f"num_height_sections={self.number_of_height_sections}, "
|
|
61
|
+
f"num_wavelength_sections={self.number_of_wavelength_sections})")
|
|
62
|
+
|
|
63
|
+
Radiator.__str__ = __str__
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
"""Detailed string representation for debugging."""
|
|
67
|
+
return (f"Radiator(name={self.name}, "
|
|
68
|
+
f"num_height_sections={self.number_of_height_sections}, "
|
|
69
|
+
f"num_wavelength_sections={self.number_of_wavelength_sections}, "
|
|
70
|
+
f"optical_depths={self.optical_depths}, "
|
|
71
|
+
f"single_scattering_albedos={self.single_scattering_albedos}, "
|
|
72
|
+
f"asymmetry_factors={self.asymmetry_factors})")
|
|
73
|
+
|
|
74
|
+
Radiator.__repr__ = __repr__
|
|
75
|
+
|
|
76
|
+
def __eq__(self, other):
|
|
77
|
+
"""Check equality between two Radiator instances."""
|
|
78
|
+
if not isinstance(other, Radiator):
|
|
79
|
+
return NotImplemented
|
|
80
|
+
return (self.name == other.name and
|
|
81
|
+
np.array_equal(self.optical_depths, other.optical_depths) and
|
|
82
|
+
np.array_equal(self.single_scattering_albedos, other.single_scattering_albedos) and
|
|
83
|
+
np.array_equal(self.asymmetry_factors, other.asymmetry_factors) and
|
|
84
|
+
self.number_of_height_sections == other.number_of_height_sections and
|
|
85
|
+
self.number_of_wavelength_sections == other.number_of_wavelength_sections)
|
|
86
|
+
|
|
87
|
+
Radiator.__eq__ = __eq__
|
|
88
|
+
|
|
89
|
+
def __bool__(self):
|
|
90
|
+
"""Return True if the radiator has name and height and wavelength sections."""
|
|
91
|
+
return (bool(self.name) and
|
|
92
|
+
self.number_of_height_sections > 0 and
|
|
93
|
+
self.number_of_wavelength_sections > 0)
|
|
94
|
+
|
|
95
|
+
Radiator.__bool__ = __bool__
|