musica 0.12.2__cp310-cp310-win32.whl → 0.13.0__cp310-cp310-win32.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.

Potentially problematic release.


This version of musica might be problematic. Click here for more details.

Files changed (63) hide show
  1. musica/CMakeLists.txt +4 -0
  2. musica/_musica.cp310-win32.pyd +0 -0
  3. musica/_version.py +1 -1
  4. musica/binding_common.cpp +6 -9
  5. musica/binding_common.hpp +17 -1
  6. musica/grid.cpp +206 -0
  7. musica/grid.py +98 -0
  8. musica/grid_map.cpp +117 -0
  9. musica/grid_map.py +167 -0
  10. musica/mechanism_configuration/__init__.py +18 -1
  11. musica/mechanism_configuration/ancillary.py +6 -0
  12. musica/mechanism_configuration/arrhenius.py +111 -269
  13. musica/mechanism_configuration/branched.py +116 -275
  14. musica/mechanism_configuration/emission.py +63 -52
  15. musica/mechanism_configuration/first_order_loss.py +73 -157
  16. musica/mechanism_configuration/mechanism.py +93 -0
  17. musica/mechanism_configuration/phase.py +44 -33
  18. musica/mechanism_configuration/phase_species.py +58 -0
  19. musica/mechanism_configuration/photolysis.py +77 -67
  20. musica/mechanism_configuration/reaction_component.py +54 -0
  21. musica/mechanism_configuration/reactions.py +17 -58
  22. musica/mechanism_configuration/species.py +45 -71
  23. musica/mechanism_configuration/surface.py +78 -74
  24. musica/mechanism_configuration/taylor_series.py +136 -0
  25. musica/mechanism_configuration/ternary_chemical_activation.py +138 -330
  26. musica/mechanism_configuration/troe.py +138 -330
  27. musica/mechanism_configuration/tunneling.py +105 -229
  28. musica/mechanism_configuration/user_defined.py +79 -68
  29. musica/mechanism_configuration.cpp +54 -162
  30. musica/musica.cpp +2 -5
  31. musica/profile.cpp +294 -0
  32. musica/profile.py +93 -0
  33. musica/profile_map.cpp +117 -0
  34. musica/profile_map.py +167 -0
  35. musica/test/examples/v1/full_configuration/full_configuration.json +91 -233
  36. musica/test/examples/v1/full_configuration/full_configuration.yaml +191 -290
  37. musica/test/integration/test_chapman.py +2 -2
  38. musica/test/integration/test_tuvx.py +72 -15
  39. musica/test/unit/test_grid.py +137 -0
  40. musica/test/unit/test_grid_map.py +126 -0
  41. musica/test/unit/test_parser.py +10 -10
  42. musica/test/unit/test_profile.py +169 -0
  43. musica/test/unit/test_profile_map.py +137 -0
  44. musica/test/unit/test_serializer.py +17 -16
  45. musica/test/unit/test_state.py +17 -4
  46. musica/test/unit/test_util_full_mechanism.py +78 -298
  47. musica/tuvx.cpp +94 -15
  48. musica/tuvx.py +92 -22
  49. musica/types.py +13 -5
  50. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/METADATA +14 -14
  51. musica-0.13.0.dist-info/RECORD +80 -0
  52. musica/mechanism_configuration/aqueous_equilibrium.py +0 -274
  53. musica/mechanism_configuration/condensed_phase_arrhenius.py +0 -309
  54. musica/mechanism_configuration/condensed_phase_photolysis.py +0 -88
  55. musica/mechanism_configuration/henrys_law.py +0 -44
  56. musica/mechanism_configuration/mechanism_configuration.py +0 -234
  57. musica/mechanism_configuration/simpol_phase_transfer.py +0 -217
  58. musica/mechanism_configuration/wet_deposition.py +0 -52
  59. musica-0.12.2.dist-info/RECORD +0 -70
  60. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/WHEEL +0 -0
  61. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/entry_points.txt +0 -0
  62. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/licenses/AUTHORS.md +0 -0
  63. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/licenses/LICENSE +0 -0
musica/profile.cpp ADDED
@@ -0,0 +1,294 @@
1
+ // Copyright (C) 2023-2025 National Center for Atmospheric Research
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // This file defines the Python bindings for the TUV-x Profile class in the musica library.
5
+ #include "binding_common.hpp"
6
+
7
+ #include <musica/tuvx/grid.hpp>
8
+ #include <musica/tuvx/profile.hpp>
9
+
10
+ #include <pybind11/numpy.h>
11
+ #include <pybind11/pybind11.h>
12
+ #include <pybind11/stl.h>
13
+
14
+ namespace py = pybind11;
15
+
16
+ void bind_tuvx_profile(py::module_ &profile)
17
+ {
18
+ py::class_<musica::Profile>(profile, "_Profile")
19
+ .def(py::init(
20
+ [](const py::kwargs &kwargs)
21
+ {
22
+ if (!kwargs.contains("name"))
23
+ throw py::value_error("Missing required argument: name");
24
+ if (!kwargs.contains("units"))
25
+ throw py::value_error("Missing required argument: units");
26
+ if (!kwargs.contains("grid"))
27
+ throw py::value_error("Missing required argument: grid");
28
+ if (!py::isinstance<py::str>(kwargs["name"]))
29
+ throw py::value_error("Argument 'name' must be a string");
30
+ if (!py::isinstance<py::str>(kwargs["units"]))
31
+ throw py::value_error("Argument 'units' must be a string");
32
+ if (!py::isinstance<musica::Grid>(kwargs["grid"].cast<py::object>()))
33
+ throw py::value_error("Argument 'grid' must be a Grid object");
34
+
35
+ std::string name = kwargs["name"].cast<std::string>();
36
+ std::string units = kwargs["units"].cast<std::string>();
37
+ musica::Grid *grid = kwargs["grid"].cast<musica::Grid *>();
38
+
39
+ musica::Error error;
40
+ auto profile_instance = new musica::Profile(name.c_str(), units.c_str(), grid, &error);
41
+ if (!musica::IsSuccess(error))
42
+ {
43
+ std::string message = "Error creating profile: " + std::string(error.message_.value_);
44
+ musica::DeleteError(&error);
45
+ throw py::value_error(message);
46
+ }
47
+ return profile_instance;
48
+ }))
49
+ .def("__del__", [](musica::Profile &profile) {})
50
+ .def_property_readonly(
51
+ "name",
52
+ [](musica::Profile &self)
53
+ {
54
+ musica::Error error;
55
+ std::string name = self.GetName(&error);
56
+ if (!musica::IsSuccess(error))
57
+ {
58
+ std::string message = "Error getting profile name: " + std::string(error.message_.value_);
59
+ musica::DeleteError(&error);
60
+ throw py::value_error(message);
61
+ }
62
+ musica::DeleteError(&error);
63
+ return name;
64
+ },
65
+ "The name of the profile")
66
+ .def_property_readonly(
67
+ "units",
68
+ [](musica::Profile &self)
69
+ {
70
+ musica::Error error;
71
+ std::string units = self.GetUnits(&error);
72
+ if (!musica::IsSuccess(error))
73
+ {
74
+ std::string message = "Error getting profile units: " + std::string(error.message_.value_);
75
+ musica::DeleteError(&error);
76
+ throw py::value_error(message);
77
+ }
78
+ musica::DeleteError(&error);
79
+ return units;
80
+ },
81
+ "The units of the profile")
82
+ .def_property_readonly(
83
+ "number_of_sections",
84
+ [](musica::Profile &self)
85
+ {
86
+ musica::Error error;
87
+ size_t num_sections = self.GetNumberOfSections(&error);
88
+ if (!musica::IsSuccess(error))
89
+ {
90
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
91
+ musica::DeleteError(&error);
92
+ throw py::value_error(message);
93
+ }
94
+ musica::DeleteError(&error);
95
+ return num_sections;
96
+ },
97
+ "The number of sections in the profile grid")
98
+ .def_property(
99
+ "edge_values",
100
+ // Getter - converts C++ array to numpy array
101
+ [](musica::Profile &self)
102
+ {
103
+ musica::Error error;
104
+ size_t size = self.GetNumberOfSections(&error) + 1;
105
+ if (!musica::IsSuccess(error))
106
+ {
107
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
108
+ musica::DeleteError(&error);
109
+ throw py::value_error(message);
110
+ }
111
+ auto result = py::array_t<double>(size);
112
+ py::buffer_info buf = result.request();
113
+ double *ptr = static_cast<double *>(buf.ptr);
114
+ self.GetEdgeValues(ptr, size, &error);
115
+ if (!musica::IsSuccess(error))
116
+ {
117
+ std::string message = "Error getting edge values: " + std::string(error.message_.value_);
118
+ musica::DeleteError(&error);
119
+ throw py::value_error(message);
120
+ }
121
+ return result;
122
+ },
123
+ // Setter - converts numpy array to C++ array
124
+ [](musica::Profile &self, py::array_t<double, py::array::c_style | py::array::forcecast> array)
125
+ {
126
+ py::buffer_info buf = array.request();
127
+ musica::Error error;
128
+ size_t size = self.GetNumberOfSections(&error) + 1;
129
+ if (!musica::IsSuccess(error))
130
+ {
131
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
132
+ musica::DeleteError(&error);
133
+ throw py::value_error(message);
134
+ }
135
+ if (buf.ndim != 1)
136
+ throw py::value_error("Array must be one-dimensional");
137
+ if (static_cast<size_t>(buf.size) != size)
138
+ throw py::value_error("Array size must be num_sections + 1");
139
+ double *ptr = static_cast<double *>(buf.ptr);
140
+ self.SetEdgeValues(ptr, size, &error);
141
+ if (!musica::IsSuccess(error))
142
+ {
143
+ std::string message = "Error setting edge values: " + std::string(error.message_.value_);
144
+ musica::DeleteError(&error);
145
+ throw py::value_error(message);
146
+ }
147
+ },
148
+ "Profile values at grid edges array of length num_sections + 1")
149
+ .def_property(
150
+ "midpoint_values",
151
+ // Getter - converts C++ array to numpy array
152
+ [](musica::Profile &self)
153
+ {
154
+ musica::Error error;
155
+ size_t size = self.GetNumberOfSections(&error);
156
+ if (!musica::IsSuccess(error))
157
+ {
158
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
159
+ musica::DeleteError(&error);
160
+ throw py::value_error(message);
161
+ }
162
+ auto result = py::array_t<double>(size);
163
+ py::buffer_info buf = result.request();
164
+ double *ptr = static_cast<double *>(buf.ptr);
165
+ self.GetMidpointValues(ptr, size, &error);
166
+ if (!musica::IsSuccess(error))
167
+ {
168
+ std::string message = "Error getting midpoint values: " + std::string(error.message_.value_);
169
+ musica::DeleteError(&error);
170
+ throw py::value_error(message);
171
+ }
172
+ return result;
173
+ },
174
+ // Setter - converts numpy array to C++ array
175
+ [](musica::Profile &self, py::array_t<double, py::array::c_style | py::array::forcecast> array)
176
+ {
177
+ py::buffer_info buf = array.request();
178
+ musica::Error error;
179
+ size_t size = self.GetNumberOfSections(&error);
180
+ if (!musica::IsSuccess(error))
181
+ {
182
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
183
+ musica::DeleteError(&error);
184
+ throw py::value_error(message);
185
+ }
186
+ if (buf.ndim != 1)
187
+ throw py::value_error("Array must be one-dimensional");
188
+ if (static_cast<size_t>(buf.size) != size)
189
+ throw py::value_error("Array size must be num_sections");
190
+ double *ptr = static_cast<double *>(buf.ptr);
191
+ self.SetMidpointValues(ptr, size, &error);
192
+ if (!musica::IsSuccess(error))
193
+ {
194
+ std::string message = "Error setting midpoint values: " + std::string(error.message_.value_);
195
+ musica::DeleteError(&error);
196
+ throw py::value_error(message);
197
+ }
198
+ },
199
+ "Profile values at grid midpoints array of length num_sections")
200
+ .def_property(
201
+ "layer_densities",
202
+ // Getter - converts C++ array to numpy array
203
+ [](musica::Profile &self)
204
+ {
205
+ musica::Error error;
206
+ size_t size = self.GetNumberOfSections(&error);
207
+ if (!musica::IsSuccess(error))
208
+ {
209
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
210
+ musica::DeleteError(&error);
211
+ throw py::value_error(message);
212
+ }
213
+ auto result = py::array_t<double>(size);
214
+ py::buffer_info buf = result.request();
215
+ double *ptr = static_cast<double *>(buf.ptr);
216
+ self.GetLayerDensities(ptr, size, &error);
217
+ if (!musica::IsSuccess(error))
218
+ {
219
+ std::string message = "Error getting layer densities: " + std::string(error.message_.value_);
220
+ musica::DeleteError(&error);
221
+ throw py::value_error(message);
222
+ }
223
+ return result;
224
+ },
225
+ // Setter - converts numpy array to C++ array
226
+ [](musica::Profile &self, py::array_t<double, py::array::c_style | py::array::forcecast> array)
227
+ {
228
+ py::buffer_info buf = array.request();
229
+ musica::Error error;
230
+ size_t size = self.GetNumberOfSections(&error);
231
+ if (!musica::IsSuccess(error))
232
+ {
233
+ std::string message = "Error getting number of grid sections: " + std::string(error.message_.value_);
234
+ musica::DeleteError(&error);
235
+ throw py::value_error(message);
236
+ }
237
+ if (buf.ndim != 1)
238
+ throw py::value_error("Array must be one-dimensional");
239
+ if (static_cast<size_t>(buf.size) != size)
240
+ throw py::value_error("Array size must be num_sections");
241
+ double *ptr = static_cast<double *>(buf.ptr);
242
+ self.SetLayerDensities(ptr, size, &error);
243
+ if (!musica::IsSuccess(error))
244
+ {
245
+ std::string message = "Error setting layer densities: " + std::string(error.message_.value_);
246
+ musica::DeleteError(&error);
247
+ throw py::value_error(message);
248
+ }
249
+ },
250
+ "Layer densities array of length num_sections")
251
+ .def_property(
252
+ "exo_layer_density",
253
+ // Getter
254
+ [](musica::Profile &self)
255
+ {
256
+ musica::Error error;
257
+ double density = self.GetExoLayerDensity(&error);
258
+ if (!musica::IsSuccess(error))
259
+ {
260
+ std::string message = "Error getting exospheric layer density: " + std::string(error.message_.value_);
261
+ musica::DeleteError(&error);
262
+ throw py::value_error(message);
263
+ }
264
+ return density;
265
+ },
266
+ // Setter
267
+ [](musica::Profile &self, double density)
268
+ {
269
+ musica::Error error;
270
+ self.SetExoLayerDensity(density, &error);
271
+ if (!musica::IsSuccess(error))
272
+ {
273
+ std::string message = "Error setting exospheric layer density: " + std::string(error.message_.value_);
274
+ musica::DeleteError(&error);
275
+ throw py::value_error(message);
276
+ }
277
+ },
278
+ "Exospheric layer density")
279
+ .def(
280
+ "calculate_exo_layer_density",
281
+ [](musica::Profile &self, double scale_height)
282
+ {
283
+ musica::Error error;
284
+ self.CalculateExoLayerDensity(scale_height, &error);
285
+ if (!musica::IsSuccess(error))
286
+ {
287
+ std::string message = "Error calculating exospheric layer density: " + std::string(error.message_.value_);
288
+ musica::DeleteError(&error);
289
+ throw py::value_error(message);
290
+ }
291
+ },
292
+ "Calculate the exospheric layer density using the given scale height",
293
+ py::arg("scale_height"));
294
+ }
musica/profile.py ADDED
@@ -0,0 +1,93 @@
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 Dict, 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
+ **kwargs):
30
+ """Initialize a Profile instance.
31
+
32
+ Args:
33
+ name: Name of the profile
34
+ units: Units of the profile values
35
+ grid: Grid on which the profile is defined
36
+ edge_values: Optional array of values at grid edges (length num_sections + 1)
37
+ midpoint_values: Optional array of values at grid midpoints (length num_sections)
38
+ layer_densities: Optional array of layer densities (length num_sections)
39
+ **kwargs: Additional arguments passed to the C++ constructor
40
+ """
41
+ # Call the original C++ constructor correctly
42
+ original_init(self, name=name, units=units, grid=grid, **kwargs)
43
+
44
+ # Set values if provided
45
+ if edge_values is not None:
46
+ self.edge_values = edge_values
47
+ if midpoint_values is not None:
48
+ self.midpoint_values = midpoint_values
49
+ if layer_densities is not None:
50
+ self.layer_densities = layer_densities
51
+
52
+ Profile.__init__ = __init__
53
+
54
+ def __str__(self):
55
+ """User-friendly string representation."""
56
+ return f"Profile(name={self.name}, units={self.units}, number_of_sections={self.number_of_sections})"
57
+
58
+ Profile.__str__ = __str__
59
+
60
+ def __repr__(self):
61
+ """Detailed string representation for debugging."""
62
+ return (f"Profile(name={self.name}, units={self.units}, number_of_sections={self.number_of_sections}, "
63
+ f"edge_values={self.edge_values}, midpoint_values={self.midpoint_values}, "
64
+ f"layer_densities={self.layer_densities}, "
65
+ f"exo_layer_density={self.exo_layer_density})")
66
+
67
+ Profile.__repr__ = __repr__
68
+
69
+ def __len__(self):
70
+ """Return the number of sections in the grid."""
71
+ return self.number_of_sections
72
+
73
+ Profile.__len__ = __len__
74
+
75
+ def __eq__(self, other):
76
+ """Check equality with another Profile instance."""
77
+ if not isinstance(other, Profile):
78
+ return NotImplemented
79
+ return (self.name == other.name and
80
+ self.units == other.units and
81
+ self.number_of_sections == other.number_of_sections and
82
+ np.array_equal(self.edge_values, other.edge_values) and
83
+ np.array_equal(self.midpoint_values, other.midpoint_values) and
84
+ np.array_equal(self.layer_densities, other.layer_densities) and
85
+ self.exo_layer_density == other.exo_layer_density)
86
+
87
+ Profile.__eq__ = __eq__
88
+
89
+ def __bool__(self):
90
+ """Return True if the profile has a name, units, and one or more sections."""
91
+ return bool(self.name) and bool(self.units) and self.number_of_sections > 0
92
+
93
+ Profile.__bool__ = __bool__
musica/profile_map.cpp ADDED
@@ -0,0 +1,117 @@
1
+ // Copyright (C) 2023-2025 National Center for Atmospheric Research
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ #include "binding_common.hpp"
4
+
5
+ #include <musica/tuvx/profile.hpp>
6
+ #include <musica/tuvx/profile_map.hpp>
7
+
8
+ #include <pybind11/pybind11.h>
9
+
10
+ namespace py = pybind11;
11
+
12
+ void bind_tuvx_profile_map(py::module& m)
13
+ {
14
+ py::class_<musica::ProfileMap>(m, "_ProfileMap")
15
+ .def(py::init<>(
16
+ []()
17
+ {
18
+ musica::Error error;
19
+ auto profile_map_instance = new musica::ProfileMap(&error);
20
+ if (!musica::IsSuccess(error))
21
+ {
22
+ std::string message = "Error creating ProfileMap: " + std::string(error.message_.value_);
23
+ musica::DeleteError(&error);
24
+ throw py::value_error(message);
25
+ }
26
+ return profile_map_instance;
27
+ }))
28
+ .def(
29
+ "add_profile",
30
+ [](musica::ProfileMap& self, musica::Profile* profile)
31
+ {
32
+ musica::Error error;
33
+ self.AddProfile(profile, &error);
34
+ if (error.code_ != 0)
35
+ {
36
+ std::string message = "Error adding profile: " + std::string(error.message_.value_);
37
+ musica::DeleteError(&error);
38
+ throw std::runtime_error(message);
39
+ }
40
+ DeleteError(&error);
41
+ })
42
+ .def(
43
+ "get_profile",
44
+ [](musica::ProfileMap& self, const std::string& name, const std::string& units)
45
+ {
46
+ musica::Error error;
47
+ musica::Profile* profile = self.GetProfile(name.c_str(), units.c_str(), &error);
48
+ if (!musica::IsSuccess(error))
49
+ {
50
+ std::string message = std::string(error.message_.value_);
51
+ musica::DeleteError(&error);
52
+ throw py::value_error("Error getting profile: " + message);
53
+ }
54
+ musica::DeleteError(&error);
55
+ return profile;
56
+ },
57
+ py::return_value_policy::reference)
58
+ .def(
59
+ "get_profile_by_index",
60
+ [](musica::ProfileMap& self, std::size_t index)
61
+ {
62
+ musica::Error error;
63
+ musica::Profile* profile = self.GetProfileByIndex(index, &error);
64
+ if (!musica::IsSuccess(error))
65
+ {
66
+ std::string message = std::string(error.message_.value_);
67
+ musica::DeleteError(&error);
68
+ throw py::value_error("Error getting profile by index: " + message);
69
+ }
70
+ musica::DeleteError(&error);
71
+ return profile;
72
+ },
73
+ py::return_value_policy::reference)
74
+ .def(
75
+ "remove_profile",
76
+ [](musica::ProfileMap& self, const std::string& name, const std::string& units)
77
+ {
78
+ musica::Error error;
79
+ self.RemoveProfile(name.c_str(), units.c_str(), &error);
80
+ if (!musica::IsSuccess(error))
81
+ {
82
+ std::string message = std::string(error.message_.value_);
83
+ musica::DeleteError(&error);
84
+ throw py::value_error("Error removing profile: " + message);
85
+ }
86
+ musica::DeleteError(&error);
87
+ })
88
+ .def(
89
+ "remove_profile_by_index",
90
+ [](musica::ProfileMap& self, std::size_t index)
91
+ {
92
+ musica::Error error;
93
+ self.RemoveProfileByIndex(index, &error);
94
+ if (!musica::IsSuccess(error))
95
+ {
96
+ std::string message = std::string(error.message_.value_);
97
+ musica::DeleteError(&error);
98
+ throw py::value_error("Error removing profile by index: " + message);
99
+ }
100
+ musica::DeleteError(&error);
101
+ })
102
+ .def(
103
+ "get_number_of_profiles",
104
+ [](musica::ProfileMap& self)
105
+ {
106
+ musica::Error error;
107
+ std::size_t num_profiles = self.GetNumberOfProfiles(&error);
108
+ if (!musica::IsSuccess(error))
109
+ {
110
+ std::string message = std::string(error.message_.value_);
111
+ musica::DeleteError(&error);
112
+ throw py::value_error("Error getting number of profiles: " + message);
113
+ }
114
+ musica::DeleteError(&error);
115
+ return num_profiles;
116
+ });
117
+ }
musica/profile_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 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, Sequence
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