musica 0.12.2__cp39-cp39-win_arm64.whl → 0.13.0__cp39-cp39-win_arm64.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.cp39-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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tests for the Grid class."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import pytest # type: ignore
|
|
4
|
+
import numpy as np # type: ignore
|
|
5
|
+
from musica.grid import Grid, backend
|
|
6
|
+
|
|
7
|
+
# Skip all tests if TUV-x is not available
|
|
8
|
+
pytestmark = pytest.mark.skipif(not backend.tuvx_available(),
|
|
9
|
+
reason="TUV-x backend is not available")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_grid_initialization():
|
|
13
|
+
"""Test Grid initialization with various input combinations."""
|
|
14
|
+
# Test with num_sections
|
|
15
|
+
grid = Grid(name="test", units="m", num_sections=5)
|
|
16
|
+
assert grid.name == "test"
|
|
17
|
+
assert grid.units == "m"
|
|
18
|
+
assert grid.num_sections == 5
|
|
19
|
+
|
|
20
|
+
# Test with edges
|
|
21
|
+
edges = np.array([0, 1, 2, 3, 4, 5])
|
|
22
|
+
grid = Grid(name="test", units="m", edges=edges)
|
|
23
|
+
assert grid.name == "test"
|
|
24
|
+
assert grid.units == "m"
|
|
25
|
+
assert grid.num_sections == 5
|
|
26
|
+
np.testing.assert_array_equal(grid.edges, edges)
|
|
27
|
+
|
|
28
|
+
# Test with midpoints
|
|
29
|
+
midpoints = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
|
|
30
|
+
grid = Grid(name="test", units="m", midpoints=midpoints)
|
|
31
|
+
assert grid.name == "test"
|
|
32
|
+
assert grid.units == "m"
|
|
33
|
+
assert grid.num_sections == 5
|
|
34
|
+
np.testing.assert_array_equal(grid.midpoints, midpoints)
|
|
35
|
+
|
|
36
|
+
# Test with both edges and midpoints
|
|
37
|
+
grid = Grid(name="test", units="m", edges=edges, midpoints=midpoints)
|
|
38
|
+
assert grid.name == "test"
|
|
39
|
+
assert grid.units == "m"
|
|
40
|
+
assert grid.num_sections == 5
|
|
41
|
+
np.testing.assert_array_equal(grid.edges, edges)
|
|
42
|
+
np.testing.assert_array_equal(grid.midpoints, midpoints)
|
|
43
|
+
|
|
44
|
+
# Test invalid initialization
|
|
45
|
+
with pytest.raises(ValueError, match="At least one of num_sections, edges, or midpoints must be provided"):
|
|
46
|
+
Grid(name="test", units="m")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_grid_properties():
|
|
50
|
+
"""Test Grid property getters and setters."""
|
|
51
|
+
grid = Grid(name="test", units="m", num_sections=5)
|
|
52
|
+
|
|
53
|
+
# Test edges
|
|
54
|
+
edges = np.array([0, 1, 2, 3, 4, 5])
|
|
55
|
+
grid.edges = edges
|
|
56
|
+
np.testing.assert_array_equal(grid.edges, edges)
|
|
57
|
+
|
|
58
|
+
# Test midpoints
|
|
59
|
+
midpoints = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
|
|
60
|
+
grid.midpoints = midpoints
|
|
61
|
+
np.testing.assert_array_equal(grid.midpoints, midpoints)
|
|
62
|
+
|
|
63
|
+
# Test invalid edges size
|
|
64
|
+
with pytest.raises(ValueError, match="Array size must be num_sections \\+ 1"):
|
|
65
|
+
grid.edges = np.array([0, 1, 2])
|
|
66
|
+
|
|
67
|
+
# Test invalid midpoints size
|
|
68
|
+
with pytest.raises(ValueError, match="Array size must be num_sections"):
|
|
69
|
+
grid.midpoints = np.array([0.5, 1.5, 2.5])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_grid_string_methods():
|
|
73
|
+
"""Test string representations of Grid."""
|
|
74
|
+
grid = Grid(name="test", units="m", num_sections=5)
|
|
75
|
+
edges = np.array([0, 1, 2, 3, 4, 5])
|
|
76
|
+
grid.edges = edges
|
|
77
|
+
|
|
78
|
+
# Test str()
|
|
79
|
+
expected_str = "Grid(name=test, units=m, num_sections=5)"
|
|
80
|
+
assert str(grid) == expected_str
|
|
81
|
+
|
|
82
|
+
# Test repr()
|
|
83
|
+
assert repr(grid).startswith("Grid(name=test, units=m, num_sections=5")
|
|
84
|
+
assert "edges=" in repr(grid)
|
|
85
|
+
assert "midpoints=" in repr(grid)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_grid_comparison():
|
|
89
|
+
"""Test Grid comparison methods."""
|
|
90
|
+
grid1 = Grid(name="test", units="m", num_sections=5)
|
|
91
|
+
grid1.edges = np.array([0, 1, 2, 3, 4, 5])
|
|
92
|
+
|
|
93
|
+
# Test equal grids
|
|
94
|
+
grid2 = Grid(name="test", units="m", num_sections=5)
|
|
95
|
+
grid2.edges = np.array([0, 1, 2, 3, 4, 5])
|
|
96
|
+
assert grid1 == grid2
|
|
97
|
+
|
|
98
|
+
# Test unequal grids
|
|
99
|
+
grid3 = Grid(name="test", units="m", num_sections=6)
|
|
100
|
+
grid3.edges = np.array([0, 1, 2, 3, 4, 5, 6])
|
|
101
|
+
assert grid1 != grid3
|
|
102
|
+
|
|
103
|
+
grid4 = Grid(name="test", units="m", num_sections=5)
|
|
104
|
+
grid4.edges = np.array([0, 2, 4, 6, 8, 10])
|
|
105
|
+
assert grid1 != grid4
|
|
106
|
+
|
|
107
|
+
grid5 = Grid(name="different", units="m", num_sections=5)
|
|
108
|
+
grid5.edges = np.array([0, 1, 2, 3, 4, 5])
|
|
109
|
+
assert grid1 != grid5
|
|
110
|
+
|
|
111
|
+
grid6 = Grid(name="test", units="cm", num_sections=5)
|
|
112
|
+
grid6.edges = np.array([0, 1, 2, 3, 4, 5])
|
|
113
|
+
assert grid1 != grid6
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_grid_container_methods():
|
|
117
|
+
"""Test Grid container methods (len, contains)."""
|
|
118
|
+
grid = Grid(name="test", units="m", num_sections=5)
|
|
119
|
+
edges = np.array([0, 1, 2, 3, 4, 5])
|
|
120
|
+
grid.edges = edges
|
|
121
|
+
|
|
122
|
+
# Test len()
|
|
123
|
+
assert len(grid) == 5
|
|
124
|
+
|
|
125
|
+
# Test contains
|
|
126
|
+
assert 2.5 in grid
|
|
127
|
+
assert -1 not in grid
|
|
128
|
+
assert 6 not in grid
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_grid_bool():
|
|
132
|
+
"""Test Grid boolean evaluation."""
|
|
133
|
+
grid = Grid(name="test", units="m", num_sections=5)
|
|
134
|
+
assert bool(grid) is True
|
|
135
|
+
|
|
136
|
+
# Note: Cannot test num_sections=0 case as it's prevented by constructor
|
|
137
|
+
# but the __bool__ method would return False in that case
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 National Center for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Tests for the TUV-x GridMap class.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import numpy as np
|
|
9
|
+
from musica.grid import Grid, backend
|
|
10
|
+
from musica.grid_map import GridMap
|
|
11
|
+
|
|
12
|
+
# Skip all tests if TUV-x is not available
|
|
13
|
+
pytestmark = pytest.mark.skipif(not backend.tuvx_available(),
|
|
14
|
+
reason="TUV-x backend is not available")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_grid_map_creation():
|
|
18
|
+
"""Test creating a GridMap instance"""
|
|
19
|
+
grid_map = GridMap()
|
|
20
|
+
assert len(grid_map) == 0
|
|
21
|
+
assert bool(grid_map) is False
|
|
22
|
+
assert str(grid_map) == "GridMap(num_grids=0)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_grid_map_add_get_grid():
|
|
26
|
+
"""Test adding and retrieving grids from the map"""
|
|
27
|
+
grid_map = GridMap()
|
|
28
|
+
|
|
29
|
+
# Create a grid to add
|
|
30
|
+
grid = Grid(name="height", units="km", num_sections=5)
|
|
31
|
+
edges = np.array([0, 2, 4, 6, 8, 10], dtype=np.float64)
|
|
32
|
+
grid.edges = edges
|
|
33
|
+
|
|
34
|
+
# Test dictionary-style assignment
|
|
35
|
+
grid_map["height", "km"] = grid
|
|
36
|
+
assert len(grid_map) == 1
|
|
37
|
+
assert bool(grid_map) is True
|
|
38
|
+
|
|
39
|
+
# Test dictionary-style access
|
|
40
|
+
retrieved_grid = grid_map["height", "km"]
|
|
41
|
+
assert retrieved_grid.name == "height"
|
|
42
|
+
assert retrieved_grid.units == "km"
|
|
43
|
+
assert retrieved_grid.num_sections == 5
|
|
44
|
+
assert np.array_equal(retrieved_grid.edges, edges)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_grid_map_contains():
|
|
48
|
+
"""Test checking for grid existence"""
|
|
49
|
+
grid_map = GridMap()
|
|
50
|
+
grid = Grid(name="wavelength", units="nm", num_sections=3)
|
|
51
|
+
|
|
52
|
+
grid_map["wavelength", "nm"] = grid
|
|
53
|
+
|
|
54
|
+
assert ("wavelength", "nm") in grid_map
|
|
55
|
+
assert ("height", "km") not in grid_map
|
|
56
|
+
assert "invalid_key" not in grid_map
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_grid_map_iteration():
|
|
60
|
+
"""Test iterating over grids in the map"""
|
|
61
|
+
grid_map = GridMap()
|
|
62
|
+
|
|
63
|
+
grids = [
|
|
64
|
+
("height", "km", 5),
|
|
65
|
+
("wavelength", "nm", 3),
|
|
66
|
+
("time", "s", 2)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Add grids to map
|
|
70
|
+
for name, units, sections in grids:
|
|
71
|
+
grid = Grid(name=name, units=units, num_sections=sections)
|
|
72
|
+
grid_map[name, units] = grid
|
|
73
|
+
|
|
74
|
+
# Test keys()
|
|
75
|
+
keys = set(grid_map.keys())
|
|
76
|
+
expected_keys = {(name, units) for name, units, _ in grids}
|
|
77
|
+
assert keys == expected_keys
|
|
78
|
+
|
|
79
|
+
# Test values()
|
|
80
|
+
for grid in grid_map.values():
|
|
81
|
+
assert isinstance(grid, Grid)
|
|
82
|
+
assert (grid.name, grid.units) in expected_keys
|
|
83
|
+
|
|
84
|
+
# Test items()
|
|
85
|
+
for key, grid in grid_map.items():
|
|
86
|
+
assert isinstance(grid, Grid)
|
|
87
|
+
assert key in expected_keys
|
|
88
|
+
assert key == (grid.name, grid.units)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_grid_map_clear():
|
|
92
|
+
"""Test clearing all grids from the map"""
|
|
93
|
+
grid_map = GridMap()
|
|
94
|
+
|
|
95
|
+
grid = Grid(name="height", units="km", num_sections=5)
|
|
96
|
+
grid_map["height", "km"] = grid
|
|
97
|
+
|
|
98
|
+
assert len(grid_map) == 1
|
|
99
|
+
grid_map.clear()
|
|
100
|
+
assert len(grid_map) == 0
|
|
101
|
+
assert ("height", "km") not in grid_map
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_grid_map_errors():
|
|
105
|
+
"""Test error handling"""
|
|
106
|
+
grid_map = GridMap()
|
|
107
|
+
grid = Grid(name="height", units="km", num_sections=5)
|
|
108
|
+
|
|
109
|
+
# Test invalid key type
|
|
110
|
+
with pytest.raises(TypeError):
|
|
111
|
+
grid_map["invalid"] = grid
|
|
112
|
+
|
|
113
|
+
with pytest.raises(TypeError):
|
|
114
|
+
grid_map[123, "km"] = grid
|
|
115
|
+
|
|
116
|
+
# Test invalid value type
|
|
117
|
+
with pytest.raises(TypeError):
|
|
118
|
+
grid_map["height", "km"] = "not_a_grid"
|
|
119
|
+
|
|
120
|
+
# Test mismatched grid properties
|
|
121
|
+
with pytest.raises(ValueError):
|
|
122
|
+
grid_map["wrong", "units"] = grid
|
|
123
|
+
|
|
124
|
+
# Test accessing non-existent grid
|
|
125
|
+
with pytest.raises(KeyError):
|
|
126
|
+
_ = grid_map["nonexistent", "grid"]
|
musica/test/unit/test_parser.py
CHANGED
|
@@ -30,20 +30,12 @@ def test_hard_coded_full_v1_configuration():
|
|
|
30
30
|
def test_hard_coded_default_constructed_types():
|
|
31
31
|
arrhenius = mc.Arrhenius()
|
|
32
32
|
assert arrhenius.type == mc.ReactionType.Arrhenius
|
|
33
|
-
condensed_phase_arrhenius = mc.CondensedPhaseArrhenius()
|
|
34
|
-
assert condensed_phase_arrhenius.type == mc.ReactionType.CondensedPhaseArrhenius
|
|
35
|
-
condensed_phase_photolysis = mc.CondensedPhasePhotolysis()
|
|
36
|
-
assert condensed_phase_photolysis.type == mc.ReactionType.CondensedPhasePhotolysis
|
|
37
33
|
emission = mc.Emission()
|
|
38
34
|
assert emission.type == mc.ReactionType.Emission
|
|
39
35
|
first_order_loss = mc.FirstOrderLoss()
|
|
40
36
|
assert first_order_loss.type == mc.ReactionType.FirstOrderLoss
|
|
41
|
-
henrys_law = mc.HenrysLaw()
|
|
42
|
-
assert henrys_law.type == mc.ReactionType.HenrysLaw
|
|
43
37
|
photolysis = mc.Photolysis()
|
|
44
38
|
assert photolysis.type == mc.ReactionType.Photolysis
|
|
45
|
-
simpol_phase_transfer = mc.SimpolPhaseTransfer()
|
|
46
|
-
assert simpol_phase_transfer.type == mc.ReactionType.SimpolPhaseTransfer
|
|
47
39
|
surface = mc.Surface()
|
|
48
40
|
assert surface.type == mc.ReactionType.Surface
|
|
49
41
|
troe = mc.Troe()
|
|
@@ -52,13 +44,21 @@ def test_hard_coded_default_constructed_types():
|
|
|
52
44
|
assert ternary_chemical_activation.type == mc.ReactionType.TernaryChemicalActivation
|
|
53
45
|
tunneling = mc.Tunneling()
|
|
54
46
|
assert tunneling.type == mc.ReactionType.Tunneling
|
|
55
|
-
wet_deposition = mc.WetDeposition()
|
|
56
|
-
assert wet_deposition.type == mc.ReactionType.WetDeposition
|
|
57
47
|
branched = mc.Branched()
|
|
58
48
|
assert branched.type == mc.ReactionType.Branched
|
|
59
49
|
user_defined = mc.UserDefined()
|
|
60
50
|
assert user_defined.type == mc.ReactionType.UserDefined
|
|
61
51
|
|
|
62
52
|
|
|
53
|
+
def test_convert_v0_to_v1():
|
|
54
|
+
parser = Parser()
|
|
55
|
+
base = 'configs/v0'
|
|
56
|
+
configs = ['analytical', 'carbon_bond_5', 'chapman', 'robertson', 'TS1', 'surface']
|
|
57
|
+
for config in configs:
|
|
58
|
+
path = f"{base}/{config}/config.json"
|
|
59
|
+
mechanism = parser.parse_and_convert_v0(path)
|
|
60
|
+
assert mechanism is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
63
|
if __name__ == "__main__":
|
|
64
64
|
pytest.main([__file__])
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Tests for the Profile class."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import pytest # type: ignore
|
|
4
|
+
import numpy as np # type: ignore
|
|
5
|
+
from musica.grid import Grid
|
|
6
|
+
from musica.profile import Profile, backend
|
|
7
|
+
|
|
8
|
+
# Skip all tests if TUV-x is not available
|
|
9
|
+
pytestmark = pytest.mark.skipif(not backend.tuvx_available(),
|
|
10
|
+
reason="TUV-x backend is not available")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def sample_grid():
|
|
15
|
+
"""Create a sample grid for testing."""
|
|
16
|
+
grid = Grid(name="test_grid", units="m", num_sections=5)
|
|
17
|
+
grid.edges = np.array([0, 1, 2, 3, 4, 5])
|
|
18
|
+
grid.midpoints = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
|
|
19
|
+
return grid
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_profile_initialization(sample_grid):
|
|
23
|
+
"""Test Profile initialization with various input combinations."""
|
|
24
|
+
# Test basic initialization
|
|
25
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
26
|
+
assert profile.name == "test"
|
|
27
|
+
assert profile.units == "K"
|
|
28
|
+
|
|
29
|
+
# Test number_of_sections
|
|
30
|
+
assert profile.number_of_sections == 5
|
|
31
|
+
|
|
32
|
+
# Test with edge_values
|
|
33
|
+
edge_values = np.array([290, 280, 270, 260, 250, 240])
|
|
34
|
+
profile = Profile(name="test", units="K", grid=sample_grid, edge_values=edge_values)
|
|
35
|
+
np.testing.assert_array_equal(profile.edge_values, edge_values)
|
|
36
|
+
|
|
37
|
+
# Test with midpoint_values
|
|
38
|
+
midpoint_values = np.array([285, 275, 265, 255, 245])
|
|
39
|
+
profile = Profile(name="test", units="K", grid=sample_grid, midpoint_values=midpoint_values)
|
|
40
|
+
np.testing.assert_array_equal(profile.midpoint_values, midpoint_values)
|
|
41
|
+
|
|
42
|
+
# Test with layer_densities
|
|
43
|
+
layer_densities = np.array([1e25, 1e24, 1e23, 1e22, 1e21])
|
|
44
|
+
profile = Profile(name="test", units="K", grid=sample_grid, layer_densities=layer_densities)
|
|
45
|
+
np.testing.assert_array_equal(profile.layer_densities, layer_densities)
|
|
46
|
+
|
|
47
|
+
# Test with all values
|
|
48
|
+
profile = Profile(name="test", units="K", grid=sample_grid,
|
|
49
|
+
edge_values=edge_values,
|
|
50
|
+
midpoint_values=midpoint_values,
|
|
51
|
+
layer_densities=layer_densities)
|
|
52
|
+
np.testing.assert_array_equal(profile.edge_values, edge_values)
|
|
53
|
+
np.testing.assert_array_equal(profile.midpoint_values, midpoint_values)
|
|
54
|
+
np.testing.assert_array_equal(profile.layer_densities, layer_densities)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_profile_properties(sample_grid):
|
|
58
|
+
"""Test Profile property getters and setters."""
|
|
59
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
60
|
+
|
|
61
|
+
# Test number_of_sections
|
|
62
|
+
assert profile.number_of_sections == 5
|
|
63
|
+
|
|
64
|
+
# Test edge_values
|
|
65
|
+
edge_values = np.array([290, 280, 270, 260, 250, 240])
|
|
66
|
+
profile.edge_values = edge_values
|
|
67
|
+
np.testing.assert_array_equal(profile.edge_values, edge_values)
|
|
68
|
+
|
|
69
|
+
# Test midpoint_values
|
|
70
|
+
midpoint_values = np.array([285, 275, 265, 255, 245])
|
|
71
|
+
profile.midpoint_values = midpoint_values
|
|
72
|
+
np.testing.assert_array_equal(profile.midpoint_values, midpoint_values)
|
|
73
|
+
|
|
74
|
+
# Test layer_densities
|
|
75
|
+
layer_densities = np.array([1e25, 1e24, 1e23, 1e22, 1e21])
|
|
76
|
+
profile.layer_densities = layer_densities
|
|
77
|
+
np.testing.assert_array_equal(profile.layer_densities, layer_densities)
|
|
78
|
+
|
|
79
|
+
# Test exo_layer_density
|
|
80
|
+
profile.exo_layer_density = 1e20
|
|
81
|
+
assert profile.exo_layer_density == 1e20
|
|
82
|
+
|
|
83
|
+
# Test invalid edge_values size
|
|
84
|
+
with pytest.raises(ValueError, match="Array size must be num_sections \\+ 1"):
|
|
85
|
+
profile.edge_values = np.array([290, 280, 270])
|
|
86
|
+
|
|
87
|
+
# Test invalid midpoint_values size
|
|
88
|
+
with pytest.raises(ValueError, match="Array size must be num_sections"):
|
|
89
|
+
profile.midpoint_values = np.array([285, 275, 265])
|
|
90
|
+
|
|
91
|
+
# Test invalid layer_densities size
|
|
92
|
+
with pytest.raises(ValueError, match="Array size must be num_sections"):
|
|
93
|
+
profile.layer_densities = np.array([1e25, 1e24, 1e23])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_profile_string_methods(sample_grid):
|
|
97
|
+
"""Test string representations of Profile."""
|
|
98
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
99
|
+
edge_values = np.array([290, 280, 270, 260, 250, 240])
|
|
100
|
+
profile.edge_values = edge_values
|
|
101
|
+
|
|
102
|
+
# Test str()
|
|
103
|
+
assert str(profile).startswith("Profile(name=test, units=K, number_of_sections=5)")
|
|
104
|
+
|
|
105
|
+
# Test repr()
|
|
106
|
+
repr_str = repr(profile)
|
|
107
|
+
assert repr_str.startswith("Profile(name=test, units=K, number_of_sections=5")
|
|
108
|
+
assert "edge_values=" in repr_str
|
|
109
|
+
assert "midpoint_values=" in repr_str
|
|
110
|
+
assert "layer_densities=" in repr_str
|
|
111
|
+
assert "exo_layer_density=" in repr_str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_profile_comparison(sample_grid):
|
|
115
|
+
"""Test Profile comparison methods."""
|
|
116
|
+
edge_values1 = np.array([290, 280, 270, 260, 250, 240])
|
|
117
|
+
profile1 = Profile(name="test", units="K", grid=sample_grid)
|
|
118
|
+
profile1.edge_values = edge_values1
|
|
119
|
+
|
|
120
|
+
# Test equal profiles
|
|
121
|
+
profile2 = Profile(name="test", units="K", grid=sample_grid)
|
|
122
|
+
profile2.edge_values = edge_values1
|
|
123
|
+
assert profile1 == profile2
|
|
124
|
+
|
|
125
|
+
# Test unequal profiles
|
|
126
|
+
profile3 = Profile(name="different", units="K", grid=sample_grid)
|
|
127
|
+
profile3.edge_values = edge_values1
|
|
128
|
+
assert profile1 != profile3
|
|
129
|
+
|
|
130
|
+
profile4 = Profile(name="test", units="°C", grid=sample_grid)
|
|
131
|
+
profile4.edge_values = edge_values1
|
|
132
|
+
assert profile1 != profile4
|
|
133
|
+
|
|
134
|
+
edge_values2 = np.array([295, 285, 275, 265, 255, 245])
|
|
135
|
+
profile5 = Profile(name="test", units="K", grid=sample_grid)
|
|
136
|
+
profile5.edge_values = edge_values2
|
|
137
|
+
assert profile1 != profile5
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_profile_container_methods(sample_grid):
|
|
141
|
+
"""Test Profile container methods (len)."""
|
|
142
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
143
|
+
edge_values = np.array([290, 280, 270, 260, 250, 240])
|
|
144
|
+
profile.edge_values = edge_values
|
|
145
|
+
|
|
146
|
+
# Test len()
|
|
147
|
+
assert len(profile) == 5
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_profile_bool(sample_grid):
|
|
151
|
+
"""Test Profile boolean evaluation."""
|
|
152
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
153
|
+
assert bool(profile) is True # True if grid has sections
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_exo_layer_density_calculation(sample_grid):
|
|
157
|
+
"""Test exospheric layer density calculation."""
|
|
158
|
+
profile = Profile(name="test", units="K", grid=sample_grid)
|
|
159
|
+
edge_values = np.array([290, 280, 270, 260, 250, 240])
|
|
160
|
+
profile.edge_values = edge_values
|
|
161
|
+
|
|
162
|
+
layer_densities = np.array([1e25, 1e24, 1e23, 1e22, 1e21])
|
|
163
|
+
profile.layer_densities = layer_densities
|
|
164
|
+
|
|
165
|
+
# Test calculation with scale height
|
|
166
|
+
scale_height = 7000 # meters
|
|
167
|
+
profile.calculate_exo_layer_density(scale_height)
|
|
168
|
+
assert profile.exo_layer_density == pytest.approx(240.0 * scale_height, rel=1e-2)
|
|
169
|
+
assert profile.layer_densities[4] == pytest.approx(1.0e21 + 240.0 * scale_height, rel=1e-2)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright (C) 2023-2025 National Center for Atmospheric Research
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Tests for the TUV-x ProfileMap class.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import numpy as np
|
|
9
|
+
from musica.grid import Grid
|
|
10
|
+
from musica.profile import Profile
|
|
11
|
+
from musica.profile_map import ProfileMap, backend
|
|
12
|
+
|
|
13
|
+
# Skip all tests if TUV-x is not available
|
|
14
|
+
pytestmark = pytest.mark.skipif(not backend.tuvx_available(),
|
|
15
|
+
reason="TUV-x backend is not available")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def sample_grid():
|
|
20
|
+
"""Create a sample grid for testing."""
|
|
21
|
+
grid = Grid(name="height", units="km", num_sections=5)
|
|
22
|
+
grid.edges = np.array([0, 2, 4, 6, 8, 10], dtype=np.float64)
|
|
23
|
+
grid.midpoints = np.array([1, 3, 5, 7, 9], dtype=np.float64)
|
|
24
|
+
return grid
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def sample_profile(sample_grid):
|
|
29
|
+
"""Create a sample profile for testing."""
|
|
30
|
+
profile = Profile(name="temperature", units="K", grid=sample_grid)
|
|
31
|
+
profile.edge_values = np.array([300, 290, 280, 270, 260, 250], dtype=np.float64)
|
|
32
|
+
profile.midpoint_values = np.array([295, 285, 275, 265, 255], dtype=np.float64)
|
|
33
|
+
return profile
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_profile_map_creation():
|
|
37
|
+
"""Test creating a ProfileMap instance"""
|
|
38
|
+
profile_map = ProfileMap()
|
|
39
|
+
assert len(profile_map) == 0
|
|
40
|
+
assert bool(profile_map) is False
|
|
41
|
+
assert str(profile_map) == "ProfileMap(num_profiles=0)"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_profile_map_add_get_profile(sample_grid, sample_profile):
|
|
45
|
+
"""Test adding and retrieving profiles from the map"""
|
|
46
|
+
profile_map = ProfileMap()
|
|
47
|
+
|
|
48
|
+
# Test dictionary-style assignment
|
|
49
|
+
profile_map["temperature", "K"] = sample_profile
|
|
50
|
+
assert len(profile_map) == 1
|
|
51
|
+
assert bool(profile_map) is True
|
|
52
|
+
|
|
53
|
+
# Test dictionary-style access
|
|
54
|
+
retrieved_profile = profile_map["temperature", "K"]
|
|
55
|
+
assert retrieved_profile.name == "temperature"
|
|
56
|
+
assert retrieved_profile.units == "K"
|
|
57
|
+
assert np.array_equal(retrieved_profile.edge_values, sample_profile.edge_values)
|
|
58
|
+
assert np.array_equal(retrieved_profile.midpoint_values, sample_profile.midpoint_values)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_profile_map_contains(sample_grid, sample_profile):
|
|
62
|
+
"""Test checking for profile existence"""
|
|
63
|
+
profile_map = ProfileMap()
|
|
64
|
+
profile_map["temperature", "K"] = sample_profile
|
|
65
|
+
|
|
66
|
+
assert ("temperature", "K") in profile_map
|
|
67
|
+
assert ("pressure", "hPa") not in profile_map
|
|
68
|
+
assert "invalid_key" not in profile_map
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_profile_map_iteration(sample_grid):
|
|
72
|
+
"""Test iterating over profiles in the map"""
|
|
73
|
+
profile_map = ProfileMap()
|
|
74
|
+
|
|
75
|
+
# Create test profiles
|
|
76
|
+
profiles = [
|
|
77
|
+
("temperature", "K", [300, 290, 280, 270, 260, 250]),
|
|
78
|
+
("pressure", "hPa", [1000, 800, 600, 400, 200, 100]),
|
|
79
|
+
("density", "kg/m3", [1.2, 1.0, 0.8, 0.6, 0.4, 0.2])
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Add profiles to map
|
|
83
|
+
for name, units, edge_values in profiles:
|
|
84
|
+
profile = Profile(name=name, units=units, grid=sample_grid)
|
|
85
|
+
profile.edge_values = np.array(edge_values, dtype=np.float64)
|
|
86
|
+
profile_map[name, units] = profile
|
|
87
|
+
|
|
88
|
+
# Test keys()
|
|
89
|
+
keys = set(profile_map.keys())
|
|
90
|
+
expected_keys = {(name, units) for name, units, _ in profiles}
|
|
91
|
+
assert keys == expected_keys
|
|
92
|
+
|
|
93
|
+
# Test values()
|
|
94
|
+
for profile in profile_map.values():
|
|
95
|
+
assert isinstance(profile, Profile)
|
|
96
|
+
assert (profile.name, profile.units) in expected_keys
|
|
97
|
+
|
|
98
|
+
# Test items()
|
|
99
|
+
for key, profile in profile_map.items():
|
|
100
|
+
assert isinstance(profile, Profile)
|
|
101
|
+
assert key in expected_keys
|
|
102
|
+
assert key == (profile.name, profile.units)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_profile_map_clear(sample_grid, sample_profile):
|
|
106
|
+
"""Test clearing all profiles from the map"""
|
|
107
|
+
profile_map = ProfileMap()
|
|
108
|
+
profile_map["temperature", "K"] = sample_profile
|
|
109
|
+
|
|
110
|
+
assert len(profile_map) == 1
|
|
111
|
+
profile_map.clear()
|
|
112
|
+
assert len(profile_map) == 0
|
|
113
|
+
assert ("temperature", "K") not in profile_map
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_profile_map_errors(sample_grid, sample_profile):
|
|
117
|
+
"""Test error handling"""
|
|
118
|
+
profile_map = ProfileMap()
|
|
119
|
+
|
|
120
|
+
# Test invalid key type
|
|
121
|
+
with pytest.raises(TypeError):
|
|
122
|
+
profile_map["invalid"] = sample_profile
|
|
123
|
+
|
|
124
|
+
with pytest.raises(TypeError):
|
|
125
|
+
profile_map[123, "K"] = sample_profile
|
|
126
|
+
|
|
127
|
+
# Test invalid value type
|
|
128
|
+
with pytest.raises(TypeError):
|
|
129
|
+
profile_map["temperature", "K"] = "not_a_profile"
|
|
130
|
+
|
|
131
|
+
# Test mismatched profile properties
|
|
132
|
+
with pytest.raises(ValueError):
|
|
133
|
+
profile_map["wrong", "units"] = sample_profile
|
|
134
|
+
|
|
135
|
+
# Test accessing non-existent profile
|
|
136
|
+
with pytest.raises(KeyError):
|
|
137
|
+
_ = profile_map["nonexistent", "profile"]
|