musica 0.12.2__cp39-cp39-macosx_15_0_arm64.whl → 0.13.0__cp39-cp39-macosx_15_0_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.

Files changed (66) hide show
  1. musica/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. musica/.dylibs/libgfortran.5.dylib +0 -0
  3. musica/.dylibs/libquadmath.0.dylib +0 -0
  4. musica/CMakeLists.txt +4 -0
  5. musica/_musica.cpython-39-darwin.so +0 -0
  6. musica/_version.py +1 -1
  7. musica/binding_common.cpp +6 -9
  8. musica/binding_common.hpp +17 -1
  9. musica/grid.cpp +206 -0
  10. musica/grid.py +98 -0
  11. musica/grid_map.cpp +117 -0
  12. musica/grid_map.py +167 -0
  13. musica/mechanism_configuration/__init__.py +18 -1
  14. musica/mechanism_configuration/ancillary.py +6 -0
  15. musica/mechanism_configuration/arrhenius.py +111 -269
  16. musica/mechanism_configuration/branched.py +116 -275
  17. musica/mechanism_configuration/emission.py +63 -52
  18. musica/mechanism_configuration/first_order_loss.py +73 -157
  19. musica/mechanism_configuration/mechanism.py +93 -0
  20. musica/mechanism_configuration/phase.py +44 -33
  21. musica/mechanism_configuration/phase_species.py +58 -0
  22. musica/mechanism_configuration/photolysis.py +77 -67
  23. musica/mechanism_configuration/reaction_component.py +54 -0
  24. musica/mechanism_configuration/reactions.py +17 -58
  25. musica/mechanism_configuration/species.py +45 -71
  26. musica/mechanism_configuration/surface.py +78 -74
  27. musica/mechanism_configuration/taylor_series.py +136 -0
  28. musica/mechanism_configuration/ternary_chemical_activation.py +138 -330
  29. musica/mechanism_configuration/troe.py +138 -330
  30. musica/mechanism_configuration/tunneling.py +105 -229
  31. musica/mechanism_configuration/user_defined.py +79 -68
  32. musica/mechanism_configuration.cpp +54 -162
  33. musica/musica.cpp +2 -5
  34. musica/profile.cpp +294 -0
  35. musica/profile.py +93 -0
  36. musica/profile_map.cpp +117 -0
  37. musica/profile_map.py +167 -0
  38. musica/test/examples/v1/full_configuration/full_configuration.json +91 -233
  39. musica/test/examples/v1/full_configuration/full_configuration.yaml +191 -290
  40. musica/test/integration/test_chapman.py +2 -2
  41. musica/test/integration/test_tuvx.py +72 -15
  42. musica/test/unit/test_grid.py +137 -0
  43. musica/test/unit/test_grid_map.py +126 -0
  44. musica/test/unit/test_parser.py +10 -10
  45. musica/test/unit/test_profile.py +169 -0
  46. musica/test/unit/test_profile_map.py +137 -0
  47. musica/test/unit/test_serializer.py +17 -16
  48. musica/test/unit/test_state.py +17 -4
  49. musica/test/unit/test_util_full_mechanism.py +78 -298
  50. musica/tuvx.cpp +94 -15
  51. musica/tuvx.py +92 -22
  52. musica/types.py +13 -5
  53. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/METADATA +14 -14
  54. musica-0.13.0.dist-info/RECORD +90 -0
  55. musica/mechanism_configuration/aqueous_equilibrium.py +0 -274
  56. musica/mechanism_configuration/condensed_phase_arrhenius.py +0 -309
  57. musica/mechanism_configuration/condensed_phase_photolysis.py +0 -88
  58. musica/mechanism_configuration/henrys_law.py +0 -44
  59. musica/mechanism_configuration/mechanism_configuration.py +0 -234
  60. musica/mechanism_configuration/simpol_phase_transfer.py +0 -217
  61. musica/mechanism_configuration/wet_deposition.py +0 -52
  62. musica-0.12.2.dist-info/RECORD +0 -80
  63. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/WHEEL +0 -0
  64. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/entry_points.txt +0 -0
  65. {musica-0.12.2.dist-info → musica-0.13.0.dist-info}/licenses/AUTHORS.md +0 -0
  66. {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"]
@@ -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"]