musica 0.14.4__cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- musica/__init__.py +11 -0
- musica/_musica.cpython-310-aarch64-linux-gnu.so +0 -0
- musica/_version.py +1 -0
- 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 +175 -0
- musica/examples/lorenz.py +295 -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.4.dist-info/METADATA +427 -0
- musica-0.14.4.dist-info/RECORD +92 -0
- musica-0.14.4.dist-info/WHEEL +6 -0
- musica-0.14.4.dist-info/entry_points.txt +3 -0
- musica-0.14.4.dist-info/licenses/AUTHORS.md +59 -0
- musica-0.14.4.dist-info/licenses/LICENSE +201 -0
- musica.libs/libaec-34bb4966.so.0.0.8 +0 -0
- musica.libs/libblas-8ed0a6f9.so.3.8.0 +0 -0
- musica.libs/libbrotlicommon-b6e6c8bd.so.1.0.6 +0 -0
- musica.libs/libbrotlidec-5094ef0a.so.1.0.6 +0 -0
- musica.libs/libcom_err-6d8d18aa.so.2.1 +0 -0
- musica.libs/libcrypt-258f54d5.so.1.1.0 +0 -0
- musica.libs/libcrypto-3eda328c.so.1.1.1k +0 -0
- musica.libs/libcurl-7faeef02.so.4.5.0 +0 -0
- musica.libs/libdf-9661c601.so.0.0.0 +0 -0
- musica.libs/libgfortran-e1b7dfc8.so.5.0.0 +0 -0
- musica.libs/libgssapi_krb5-fe951f80.so.2.2 +0 -0
- musica.libs/libhdf5-463e48d5.so.103.1.0 +0 -0
- musica.libs/libhdf5_hl-74316838.so.100.1.2 +0 -0
- musica.libs/libidn2-1b2a13b7.so.0.3.6 +0 -0
- musica.libs/libjpeg-ee25248c.so.62.2.0 +0 -0
- musica.libs/libk5crypto-84470bb3.so.3.1 +0 -0
- musica.libs/libkeyutils-fe6e95a9.so.1.6 +0 -0
- musica.libs/libkrb5-26ef5d84.so.3.3 +0 -0
- musica.libs/libkrb5support-875e89dc.so.0.1 +0 -0
- musica.libs/liblapack-8d137073.so.3.8.0 +0 -0
- musica.libs/liblber-2-86b08e65.4.so.2.10.9 +0 -0
- musica.libs/libldap-2-5c1dd279.4.so.2.10.9 +0 -0
- musica.libs/libmfhdf-9c336c5f.so.0.0.0 +0 -0
- musica.libs/libnetcdf-71a067be.so.15.0.1 +0 -0
- musica.libs/libnetcdff-6a455dd4.so.7.0.0 +0 -0
- musica.libs/libnghttp2-3a94c239.so.14.17.0 +0 -0
- musica.libs/libpcre2-8-8701a61e.so.0.7.1 +0 -0
- musica.libs/libpsl-130094ea.so.5.3.1 +0 -0
- musica.libs/libsasl2-076b3c1f.so.3.0.0 +0 -0
- musica.libs/libselinux-5700a1fd.so.1 +0 -0
- musica.libs/libssh-e0d3bd94.so.4.8.7 +0 -0
- musica.libs/libssl-f60bf0e2.so.1.1.1k +0 -0
- musica.libs/libsz-81b556a2.so.2.0.1 +0 -0
- musica.libs/libtirpc-1fa9018c.so.3.0.0 +0 -0
- musica.libs/libunistring-be03fd41.so.2.1.0 +0 -0
musica/micm/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .utils import species_ordering, user_defined_rate_parameters_ordering
|
|
2
|
+
from .state import State
|
|
3
|
+
from .solver_result import SolverState, SolverStats, SolverResult
|
|
4
|
+
from .solver import SolverType
|
|
5
|
+
from .micm import MICM
|
|
6
|
+
from .conditions import Conditions
|
|
7
|
+
from .. import backend
|
|
8
|
+
_backend = backend.get_backend()
|
|
9
|
+
|
|
10
|
+
__version__ = _backend._micm._get_micm_version()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
from ..constants import GAS_CONSTANT
|
|
6
|
+
|
|
7
|
+
from .. import backend
|
|
8
|
+
|
|
9
|
+
_backend = backend.get_backend()
|
|
10
|
+
Conditions = _backend._micm._Conditions
|
|
11
|
+
original_init = Conditions.__init__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
temperature: Optional[Union[float, int]] = None,
|
|
17
|
+
pressure: Optional[Union[float, int]] = None,
|
|
18
|
+
air_density: Optional[Union[float, int]] = None,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Initializes the Conditions object with the given parameters.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
temperature (float): Temperature in Kelvin.
|
|
25
|
+
pressure (float): Pressure in Pascals.
|
|
26
|
+
air_density (float): Air density in mol m-3
|
|
27
|
+
"""
|
|
28
|
+
original_init(self)
|
|
29
|
+
if temperature is not None:
|
|
30
|
+
self.temperature = temperature
|
|
31
|
+
if pressure is not None:
|
|
32
|
+
self.pressure = pressure
|
|
33
|
+
if air_density is not None:
|
|
34
|
+
self.air_density = air_density
|
|
35
|
+
elif temperature is not None and pressure is not None:
|
|
36
|
+
self.air_density = 1.0 / (GAS_CONSTANT * temperature / pressure)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Conditions.__doc__ = """
|
|
40
|
+
Conditions class for the MICM solver. If air density is not provided,
|
|
41
|
+
it will be calculated from the Ideal Gas Law using the provided temperature and pressure.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
temperature (float): Temperature in Kelvin.
|
|
45
|
+
pressure (float): Pressure in Pascals.
|
|
46
|
+
air_density (float): Air density in mol m-3
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
Conditions.__init__ = __init__
|
musica/micm/micm.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Union, Any, TYPE_CHECKING, List
|
|
6
|
+
from os import PathLike
|
|
7
|
+
|
|
8
|
+
from .state import State
|
|
9
|
+
from .solver import SolverType
|
|
10
|
+
from .solver_result import SolverResult
|
|
11
|
+
from .. import backend
|
|
12
|
+
|
|
13
|
+
_backend = backend.get_backend()
|
|
14
|
+
|
|
15
|
+
create_solver = _backend._micm._create_solver
|
|
16
|
+
create_solver_from_mechanism = _backend._micm._create_solver_from_mechanism
|
|
17
|
+
micm_solve = _backend._micm._micm_solve
|
|
18
|
+
vector_size = _backend._micm._vector_size
|
|
19
|
+
|
|
20
|
+
# For type hints
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..mechanism_configuration import Mechanism
|
|
23
|
+
from .solver_result import SolverResult as SolverResultType
|
|
24
|
+
else:
|
|
25
|
+
Mechanism = _backend._mechanism_configuration._Mechanism
|
|
26
|
+
SolverResultType = SolverResult
|
|
27
|
+
|
|
28
|
+
FilePath = Union[str, "PathLike[str]"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MICM():
|
|
32
|
+
"""
|
|
33
|
+
The MICM class is a wrapper around the C++ MICM solver. It provides methods to create a solver,
|
|
34
|
+
create a state, and solve the system of equations.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
config_path : FilePath
|
|
39
|
+
Path to the configuration file.
|
|
40
|
+
mechanism : mechanism_configuration.Mechanism
|
|
41
|
+
Mechanism object which specifies the chemical mechanism to use.
|
|
42
|
+
solver_type : SolverType
|
|
43
|
+
Type of solver to use.
|
|
44
|
+
number_of_grid_cells : int
|
|
45
|
+
Number of grid cells to use. The default is 1.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config_path: FilePath = None,
|
|
51
|
+
mechanism: Mechanism = None,
|
|
52
|
+
solver_type: Any = None,
|
|
53
|
+
ignore_non_gas_phases: bool = True,
|
|
54
|
+
):
|
|
55
|
+
""" Initialize the MICM solver.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
config_path : FilePath, optional
|
|
60
|
+
Path to the configuration file. If provided, this will be used to create the solver.
|
|
61
|
+
mechanism : Mechanism, optional
|
|
62
|
+
Mechanism object which specifies the chemical mechanism to use. If provided, this will be used
|
|
63
|
+
to create the solver.
|
|
64
|
+
solver_type : SolverType, optional
|
|
65
|
+
Type of solver to use. If not provided, the default Rosenbrock (with standard-ordered matrices) solver type will be used.
|
|
66
|
+
ignore_non_gas_phases : bool, optional
|
|
67
|
+
If True, non-gas phases will be ignored when configuring micm with the mechanism. This is only needed
|
|
68
|
+
until micm properly supports non-gas phases. This option is only supported when passing in a mechanism.
|
|
69
|
+
"""
|
|
70
|
+
if solver_type is None:
|
|
71
|
+
solver_type = SolverType.rosenbrock_standard_order
|
|
72
|
+
self.__solver_type = solver_type
|
|
73
|
+
self.__vector_size = vector_size(solver_type)
|
|
74
|
+
if self.__vector_size <= 0:
|
|
75
|
+
raise ValueError(f"Invalid vector size: {self.__vector_size}")
|
|
76
|
+
if config_path is None and mechanism is None:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Either config_path or mechanism must be provided.")
|
|
79
|
+
if config_path is not None and mechanism is not None:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Only one of config_path or mechanism must be provided.")
|
|
82
|
+
if config_path is not None:
|
|
83
|
+
self.__solver = create_solver(config_path, solver_type)
|
|
84
|
+
elif mechanism is not None:
|
|
85
|
+
self.__solver = create_solver_from_mechanism(
|
|
86
|
+
mechanism, solver_type, ignore_non_gas_phases)
|
|
87
|
+
|
|
88
|
+
def solver_type(self) -> SolverType:
|
|
89
|
+
"""
|
|
90
|
+
Get the type of solver used.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
SolverType
|
|
95
|
+
The type of solver used.
|
|
96
|
+
"""
|
|
97
|
+
return self.__solver_type
|
|
98
|
+
|
|
99
|
+
def create_state(self, number_of_grid_cells: int = 1) -> State:
|
|
100
|
+
"""
|
|
101
|
+
Create a new state object.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
State
|
|
106
|
+
A new state object.
|
|
107
|
+
"""
|
|
108
|
+
return State(self.__solver, number_of_grid_cells, self.__vector_size)
|
|
109
|
+
|
|
110
|
+
def solve(
|
|
111
|
+
self,
|
|
112
|
+
state: State,
|
|
113
|
+
time_step: float,
|
|
114
|
+
) -> SolverResult:
|
|
115
|
+
"""
|
|
116
|
+
Solve the system of equations for the given state and time step.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
state : State
|
|
121
|
+
State object containing the initial conditions.
|
|
122
|
+
time_step : float
|
|
123
|
+
Time step in seconds.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
SolverResult
|
|
128
|
+
A SolverResult object containing the solver state and statistics.
|
|
129
|
+
"""
|
|
130
|
+
if not isinstance(state, State):
|
|
131
|
+
raise TypeError("state must be an instance of State.")
|
|
132
|
+
if not isinstance(time_step, (int, float)):
|
|
133
|
+
raise TypeError("time_step must be an int or float.")
|
|
134
|
+
|
|
135
|
+
return micm_solve(self.__solver, state.get_internal_state(), time_step)
|
musica/micm/solver.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module for exposing solver result types from MICM.
|
|
6
|
+
|
|
7
|
+
This module provides access to:
|
|
8
|
+
- SolverState: Enum representing the final state of the solver
|
|
9
|
+
- SolverStats: Statistics from a solver run
|
|
10
|
+
- SolverResult: Combined result containing both state and statistics
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .. import backend
|
|
14
|
+
|
|
15
|
+
_backend = backend.get_backend()
|
|
16
|
+
|
|
17
|
+
# Expose the SolverState enum
|
|
18
|
+
SolverState = _backend._micm._SolverState
|
|
19
|
+
|
|
20
|
+
# Expose the SolverStats class
|
|
21
|
+
SolverStats = _backend._micm._SolverResultsStats
|
|
22
|
+
|
|
23
|
+
# Expose the SolverResult class
|
|
24
|
+
SolverResult = _backend._micm._SolverResult
|
musica/micm/state.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Dict, List, Union, Any
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
from ..constants import GAS_CONSTANT
|
|
8
|
+
from .. import backend
|
|
9
|
+
from .utils import is_scalar_number, species_ordering, user_defined_rate_parameters_ordering
|
|
10
|
+
|
|
11
|
+
_backend = backend.get_backend()
|
|
12
|
+
|
|
13
|
+
create_state = _backend._micm._create_state
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class State():
|
|
17
|
+
"""
|
|
18
|
+
State class for the MICM solver. It contains the initial conditions and species concentrations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, solver: Any, number_of_grid_cells: int, vector_size: int = 0):
|
|
22
|
+
if number_of_grid_cells < 1:
|
|
23
|
+
raise ValueError("number_of_grid_cells must be greater than 0.")
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.__state = create_state(solver, number_of_grid_cells)
|
|
26
|
+
self.__species_ordering = species_ordering(self.__state)
|
|
27
|
+
self.__user_defined_rate_parameters_ordering = user_defined_rate_parameters_ordering(self.__state)
|
|
28
|
+
self.__number_of_grid_cells = number_of_grid_cells
|
|
29
|
+
self.__vector_size = vector_size
|
|
30
|
+
|
|
31
|
+
def get_internal_state(self) -> Any:
|
|
32
|
+
"""
|
|
33
|
+
Get the internal state object. This is used for passing the state to the solver.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
_State
|
|
38
|
+
Internal state object.
|
|
39
|
+
"""
|
|
40
|
+
return self.__state
|
|
41
|
+
|
|
42
|
+
def set_concentrations(self, concentrations: Dict[str, Union[Union[float, int], List[Union[float, int]]]]):
|
|
43
|
+
"""
|
|
44
|
+
Set the concentrations of the species in the state. Any species not in the
|
|
45
|
+
dictionary will be set to zero. The concentrations can be a single value when solving
|
|
46
|
+
for a single grid cell, or a list of values when solving for multiple grid cells.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
concentrations : Dict[str, Union[Union[float, int], List[Union[float, int]]]]
|
|
51
|
+
Dictionary of species names and their concentrations.
|
|
52
|
+
"""
|
|
53
|
+
n_species = len(self.__species_ordering)
|
|
54
|
+
for name, value in concentrations.items():
|
|
55
|
+
if name not in self.__species_ordering:
|
|
56
|
+
raise ValueError(f"Species {name} not found in the mechanism.")
|
|
57
|
+
i_species = self.__species_ordering[name]
|
|
58
|
+
if is_scalar_number(value):
|
|
59
|
+
value = [value]
|
|
60
|
+
if len(value) != self.__number_of_grid_cells:
|
|
61
|
+
raise ValueError(f"Concentration list for {name} must have length {self.__number_of_grid_cells}.")
|
|
62
|
+
for i_cell in range(self.__number_of_grid_cells):
|
|
63
|
+
group_index = i_cell // self.__vector_size
|
|
64
|
+
row_in_group = i_cell % self.__vector_size
|
|
65
|
+
idx = (group_index * n_species + i_species) * self.__vector_size + row_in_group
|
|
66
|
+
self.__state.concentrations[idx] = value[i_cell]
|
|
67
|
+
|
|
68
|
+
def set_user_defined_rate_parameters(
|
|
69
|
+
self, user_defined_rate_parameters: Dict[str, Union[Union[float, int], List[Union[float, int]]]]):
|
|
70
|
+
"""
|
|
71
|
+
Set the user-defined rate parameters in the state. Any parameter not in the
|
|
72
|
+
dictionary will be set to zero. The parameters can be a single value when solving
|
|
73
|
+
for a single grid cell, or a list of values when solving for multiple grid cells.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
user_defined_rate_parameters : Dict[str, Union[Union[float, int], List[Union[float, int]]]]
|
|
78
|
+
Dictionary of user-defined rate parameter names and their values.
|
|
79
|
+
"""
|
|
80
|
+
n_params = len(self.__user_defined_rate_parameters_ordering)
|
|
81
|
+
for name, value in user_defined_rate_parameters.items():
|
|
82
|
+
if name not in self.__user_defined_rate_parameters_ordering:
|
|
83
|
+
raise ValueError(f"User-defined rate parameter {name} not found in the mechanism.")
|
|
84
|
+
i_param = self.__user_defined_rate_parameters_ordering[name]
|
|
85
|
+
if is_scalar_number(value):
|
|
86
|
+
value = [value]
|
|
87
|
+
if len(value) != self.__number_of_grid_cells:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"User-defined rate parameter list for {name} must have length {self.__number_of_grid_cells}.")
|
|
90
|
+
for i_cell in range(self.__number_of_grid_cells):
|
|
91
|
+
group_index = i_cell // self.__vector_size
|
|
92
|
+
row_in_group = i_cell % self.__vector_size
|
|
93
|
+
idx = (group_index * n_params + i_param) * self.__vector_size + row_in_group
|
|
94
|
+
self.__state.user_defined_rate_parameters[idx] = value[i_cell]
|
|
95
|
+
|
|
96
|
+
def set_conditions(self,
|
|
97
|
+
temperatures: Optional[Union[Union[float, int], List[Union[float, int]]]] = None,
|
|
98
|
+
pressures: Optional[Union[Union[float, int], List[Union[float, int]]]] = None,
|
|
99
|
+
air_densities: Optional[Union[Union[float, int], List[Union[float, int]]]] = None):
|
|
100
|
+
"""
|
|
101
|
+
Set the conditions for the state. The individual conditions can be a single value
|
|
102
|
+
when solving for a single grid cell, or a list of values when solving for multiple grid cells.
|
|
103
|
+
If air density is not provided, it will be calculated from the Ideal Gas Law using the provided
|
|
104
|
+
temperature and pressure. If temperature or pressure are not provided, their values will remain
|
|
105
|
+
unchanged.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
temperatures : Optional[Union[float, List[float]]]
|
|
110
|
+
Temperature in Kelvin.
|
|
111
|
+
pressures : Optional[Union[float, List[float]]]
|
|
112
|
+
Pressure in Pascals.
|
|
113
|
+
air_densities : Optional[Union[float, List[float]]]
|
|
114
|
+
Air density in mol m-3. If not provided, it will be calculated from the Ideal Gas Law.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
empty = [None] * self.__number_of_grid_cells
|
|
118
|
+
|
|
119
|
+
def check_and_expand(param, name):
|
|
120
|
+
if param is not None:
|
|
121
|
+
if is_scalar_number(param):
|
|
122
|
+
if self.__number_of_grid_cells > 1:
|
|
123
|
+
raise ValueError(f"{name} must be a list of length {self.__number_of_grid_cells}.")
|
|
124
|
+
param = [param]
|
|
125
|
+
elif len(param) != self.__number_of_grid_cells:
|
|
126
|
+
raise ValueError(f"{name} must be a list of length {self.__number_of_grid_cells}.")
|
|
127
|
+
else:
|
|
128
|
+
param = empty
|
|
129
|
+
return param
|
|
130
|
+
temperatures = check_and_expand(temperatures, "temperatures")
|
|
131
|
+
pressures = check_and_expand(pressures, "pressures")
|
|
132
|
+
air_densities = check_and_expand(air_densities, "air_densities")
|
|
133
|
+
|
|
134
|
+
for condition, temp, pres, dens in zip(self.__state.conditions, temperatures, pressures, air_densities):
|
|
135
|
+
condition.temperature = temp if temp is not None else condition.temperature
|
|
136
|
+
condition.pressure = pres if pres is not None else condition.pressure
|
|
137
|
+
condition.air_density = dens if dens is not None else condition.pressure / \
|
|
138
|
+
(GAS_CONSTANT * condition.temperature)
|
|
139
|
+
|
|
140
|
+
def get_concentrations(self) -> Dict[str, List[float]]:
|
|
141
|
+
"""
|
|
142
|
+
Get the concentrations of the species in the state.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
Dict[str, List[float]]
|
|
147
|
+
Dictionary of species names and their concentrations.
|
|
148
|
+
"""
|
|
149
|
+
concentrations = {}
|
|
150
|
+
n_species = len(self.__species_ordering)
|
|
151
|
+
|
|
152
|
+
for species, i_species in self.__species_ordering.items():
|
|
153
|
+
concentrations[species] = []
|
|
154
|
+
for i_cell in range(self.__number_of_grid_cells):
|
|
155
|
+
group_index = i_cell // self.__vector_size
|
|
156
|
+
row_in_group = i_cell % self.__vector_size
|
|
157
|
+
idx = (group_index * n_species + i_species) * self.__vector_size + row_in_group
|
|
158
|
+
concentrations[species].append(self.__state.concentrations[idx])
|
|
159
|
+
return concentrations
|
|
160
|
+
|
|
161
|
+
def get_user_defined_rate_parameters(self) -> Dict[str, List[float]]:
|
|
162
|
+
"""
|
|
163
|
+
Get the user-defined rate parameters in the state.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
Dict[str, List[float]]
|
|
168
|
+
Dictionary of user-defined rate parameter names and their values.
|
|
169
|
+
"""
|
|
170
|
+
user_defined_rate_parameters = {}
|
|
171
|
+
n_params = len(self.__user_defined_rate_parameters_ordering)
|
|
172
|
+
for param, i_param in self.__user_defined_rate_parameters_ordering.items():
|
|
173
|
+
user_defined_rate_parameters[param] = []
|
|
174
|
+
for i_cell in range(self.__number_of_grid_cells):
|
|
175
|
+
group_index = i_cell // self.__vector_size
|
|
176
|
+
row_in_group = i_cell % self.__vector_size
|
|
177
|
+
idx = (group_index * n_params + i_param) * self.__vector_size + row_in_group
|
|
178
|
+
user_defined_rate_parameters[param].append(self.__state.user_defined_rate_parameters[idx])
|
|
179
|
+
return user_defined_rate_parameters
|
|
180
|
+
|
|
181
|
+
def get_conditions(self) -> Dict[str, List[float]]:
|
|
182
|
+
"""
|
|
183
|
+
Get the conditions for the state.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
Dict[str, List[float]]
|
|
188
|
+
Dictionary of conditions names and their values.
|
|
189
|
+
"""
|
|
190
|
+
conditions = {}
|
|
191
|
+
conditions["temperature"] = []
|
|
192
|
+
conditions["pressure"] = []
|
|
193
|
+
conditions["air_density"] = []
|
|
194
|
+
for i_cell in range(self.__number_of_grid_cells):
|
|
195
|
+
conditions["temperature"].append(self.__state.conditions[i_cell].temperature)
|
|
196
|
+
conditions["pressure"].append(self.__state.conditions[i_cell].pressure)
|
|
197
|
+
conditions["air_density"].append(self.__state.conditions[i_cell].air_density)
|
|
198
|
+
return conditions
|
|
199
|
+
|
|
200
|
+
def get_species_ordering(self) -> Dict[str, int]:
|
|
201
|
+
"""
|
|
202
|
+
Get the species ordering for the state.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
Dict[str, int]
|
|
207
|
+
Dictionary of species names and their indices.
|
|
208
|
+
"""
|
|
209
|
+
return self.__species_ordering
|
|
210
|
+
|
|
211
|
+
def get_user_defined_rate_parameters_ordering(self) -> Dict[str, int]:
|
|
212
|
+
"""
|
|
213
|
+
Get the user-defined rate parameters ordering for the state.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
Dict[str, int]
|
|
218
|
+
Dictionary of user-defined rate parameter names and their indices.
|
|
219
|
+
"""
|
|
220
|
+
return self.__user_defined_rate_parameters_ordering
|
musica/micm/utils.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .. import backend
|
|
7
|
+
|
|
8
|
+
_backend = backend.get_backend()
|
|
9
|
+
|
|
10
|
+
species_ordering = _backend._micm._species_ordering
|
|
11
|
+
user_defined_rate_parameters_ordering = _backend._micm._user_defined_rate_parameters_ordering
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_scalar_number(x):
|
|
15
|
+
return (
|
|
16
|
+
isinstance(x, (int, float, np.number))
|
|
17
|
+
and not isinstance(x, bool)
|
|
18
|
+
)
|
musica/tuvx/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .radiator import Radiator
|
|
2
|
+
from .radiator_map import RadiatorMap
|
|
3
|
+
from .profile import Profile
|
|
4
|
+
from .profile_map import ProfileMap
|
|
5
|
+
from .grid import Grid
|
|
6
|
+
from .grid_map import GridMap
|
|
7
|
+
from .tuvx import TUVX
|
|
8
|
+
from .. import backend
|
|
9
|
+
_backend = backend.get_backend()
|
|
10
|
+
|
|
11
|
+
__version__ = _backend._tuvx._get_tuvx_version() if backend.tuvx_available() else None
|
musica/tuvx/grid.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 University Corporation for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
TUV-x Grid class.
|
|
5
|
+
|
|
6
|
+
This module provides a class for defining grids on which TUV-x profiles exist.
|
|
7
|
+
Typically, this would be used to define vertical and wavelength grids.
|
|
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
|
+
|
|
16
|
+
_backend = backend.get_backend()
|
|
17
|
+
|
|
18
|
+
Grid = _backend._tuvx._Grid if backend.tuvx_available() else None
|
|
19
|
+
|
|
20
|
+
if backend.tuvx_available():
|
|
21
|
+
original_init = Grid.__init__
|
|
22
|
+
|
|
23
|
+
def __init__(self, *, name: str, units: str,
|
|
24
|
+
num_sections: Optional[int] = None,
|
|
25
|
+
edges: Optional[np.ndarray] = None,
|
|
26
|
+
midpoints: Optional[np.ndarray] = None,
|
|
27
|
+
**kwargs):
|
|
28
|
+
"""Initialize a Grid instance. Note that at least one of num_sections, edges, or midpoints
|
|
29
|
+
must be provided.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Name of the grid
|
|
33
|
+
units: Units of the grid values
|
|
34
|
+
num_sections: Optional number of grid sections
|
|
35
|
+
edges: Optional array of edge values (length num_sections + 1)
|
|
36
|
+
midpoints: Optional array of midpoint values (length num_sections)
|
|
37
|
+
**kwargs: Additional arguments passed to the C++ constructor
|
|
38
|
+
"""
|
|
39
|
+
if (num_sections is None and edges is None and midpoints is None):
|
|
40
|
+
raise ValueError("At least one of num_sections, edges, or midpoints must be provided.")
|
|
41
|
+
if (num_sections is None):
|
|
42
|
+
if (edges is not None):
|
|
43
|
+
num_sections = len(edges) - 1
|
|
44
|
+
elif (midpoints is not None):
|
|
45
|
+
num_sections = len(midpoints)
|
|
46
|
+
# Call the original C++ constructor correctly
|
|
47
|
+
original_init(self, name=name, units=units, num_sections=num_sections, **kwargs)
|
|
48
|
+
|
|
49
|
+
# Set edges or midpoints if provided
|
|
50
|
+
if edges is not None:
|
|
51
|
+
self.edges = edges
|
|
52
|
+
if midpoints is not None:
|
|
53
|
+
self.midpoints = midpoints
|
|
54
|
+
|
|
55
|
+
Grid.__init__ = __init__
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
"""User-friendly string representation."""
|
|
59
|
+
return f"Grid(name={self.name}, units={self.units}, num_sections={self.num_sections})"
|
|
60
|
+
|
|
61
|
+
Grid.__str__ = __str__
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
"""Detailed string representation for debugging."""
|
|
65
|
+
return (f"Grid(name={self.name}, units={self.units}, num_sections={self.num_sections}, "
|
|
66
|
+
f"edges={self.edges}, midpoints={self.midpoints})")
|
|
67
|
+
|
|
68
|
+
Grid.__repr__ = __repr__
|
|
69
|
+
|
|
70
|
+
def __len__(self):
|
|
71
|
+
"""Return the number of sections in the grid."""
|
|
72
|
+
return self.num_sections
|
|
73
|
+
|
|
74
|
+
Grid.__len__ = __len__
|
|
75
|
+
|
|
76
|
+
def __eq__(self, other):
|
|
77
|
+
"""Check equality with another Grid instance."""
|
|
78
|
+
if not isinstance(other, Grid):
|
|
79
|
+
return NotImplemented
|
|
80
|
+
return (self.name == other.name and
|
|
81
|
+
self.units == other.units and
|
|
82
|
+
self.num_sections == other.num_sections and
|
|
83
|
+
np.array_equal(self.edges, other.edges) and
|
|
84
|
+
np.array_equal(self.midpoints, other.midpoints))
|
|
85
|
+
|
|
86
|
+
Grid.__eq__ = __eq__
|
|
87
|
+
|
|
88
|
+
def __bool__(self):
|
|
89
|
+
"""Return True if the grid has sections."""
|
|
90
|
+
return self.num_sections > 0
|
|
91
|
+
|
|
92
|
+
Grid.__bool__ = __bool__
|
|
93
|
+
|
|
94
|
+
def __contains__(self, value):
|
|
95
|
+
"""Check if a value is within the grid bounds."""
|
|
96
|
+
return self.edges[0] <= value <= self.edges[-1]
|
|
97
|
+
|
|
98
|
+
Grid.__contains__ = __contains__
|