musica 0.11.1.1__cp312-cp312-win_amd64.whl → 0.14.2__cp312-cp312-win_amd64.whl

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