musica 0.12.2__cp312-cp312-win_amd64.whl → 0.13.0__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.
Potentially problematic release.
This version of musica might be problematic. Click here for more details.
- musica/CMakeLists.txt +4 -0
- musica/_musica.cp312-win_amd64.pyd +0 -0
- musica/_version.py +1 -1
- musica/binding_common.cpp +6 -9
- musica/binding_common.hpp +17 -1
- musica/grid.cpp +206 -0
- musica/grid.py +98 -0
- musica/grid_map.cpp +117 -0
- musica/grid_map.py +167 -0
- musica/mechanism_configuration/__init__.py +18 -1
- musica/mechanism_configuration/ancillary.py +6 -0
- musica/mechanism_configuration/arrhenius.py +111 -269
- musica/mechanism_configuration/branched.py +116 -275
- musica/mechanism_configuration/emission.py +63 -52
- musica/mechanism_configuration/first_order_loss.py +73 -157
- musica/mechanism_configuration/mechanism.py +93 -0
- musica/mechanism_configuration/phase.py +44 -33
- musica/mechanism_configuration/phase_species.py +58 -0
- musica/mechanism_configuration/photolysis.py +77 -67
- musica/mechanism_configuration/reaction_component.py +54 -0
- musica/mechanism_configuration/reactions.py +17 -58
- musica/mechanism_configuration/species.py +45 -71
- musica/mechanism_configuration/surface.py +78 -74
- musica/mechanism_configuration/taylor_series.py +136 -0
- musica/mechanism_configuration/ternary_chemical_activation.py +138 -330
- musica/mechanism_configuration/troe.py +138 -330
- musica/mechanism_configuration/tunneling.py +105 -229
- musica/mechanism_configuration/user_defined.py +79 -68
- musica/mechanism_configuration.cpp +54 -162
- musica/musica.cpp +2 -5
- musica/profile.cpp +294 -0
- musica/profile.py +93 -0
- musica/profile_map.cpp +117 -0
- musica/profile_map.py +167 -0
- musica/test/examples/v1/full_configuration/full_configuration.json +91 -233
- musica/test/examples/v1/full_configuration/full_configuration.yaml +191 -290
- musica/test/integration/test_chapman.py +2 -2
- musica/test/integration/test_tuvx.py +72 -15
- musica/test/unit/test_grid.py +137 -0
- musica/test/unit/test_grid_map.py +126 -0
- musica/test/unit/test_parser.py +10 -10
- musica/test/unit/test_profile.py +169 -0
- musica/test/unit/test_profile_map.py +137 -0
- musica/test/unit/test_serializer.py +17 -16
- musica/test/unit/test_state.py +17 -4
- musica/test/unit/test_util_full_mechanism.py +78 -298
- musica/tuvx.cpp +94 -15
- musica/tuvx.py +92 -22
- musica/types.py +13 -5
- {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/METADATA +14 -14
- musica-0.13.0.dist-info/RECORD +80 -0
- musica/mechanism_configuration/aqueous_equilibrium.py +0 -274
- musica/mechanism_configuration/condensed_phase_arrhenius.py +0 -309
- musica/mechanism_configuration/condensed_phase_photolysis.py +0 -88
- musica/mechanism_configuration/henrys_law.py +0 -44
- musica/mechanism_configuration/mechanism_configuration.py +0 -234
- musica/mechanism_configuration/simpol_phase_transfer.py +0 -217
- musica/mechanism_configuration/wet_deposition.py +0 -52
- musica-0.12.2.dist-info/RECORD +0 -70
- {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/WHEEL +0 -0
- {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/entry_points.txt +0 -0
- {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/licenses/AUTHORS.md +0 -0
- {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
|