|
1 | 1 | import numpy as np |
2 | 2 | import pytest |
3 | 3 |
|
4 | | -from deepmd.dpmodel.utils.neighbor_graph import build_angle_index, pad_and_guard_angles |
| 4 | +from deepmd.dpmodel.utils.neighbor_graph import ( |
| 5 | + GraphLayout, |
| 6 | + build_angle_index, |
| 7 | + pad_and_guard_angles, |
| 8 | +) |
5 | 9 |
|
6 | 10 |
|
7 | 11 | def test_pad_angles_dynamic_appends_min_guard(): |
@@ -120,3 +124,82 @@ def test_build_angle_index_torch_namespace(): |
120 | 124 | if am_t[p].item() |
121 | 125 | } |
122 | 126 | assert got_t == got_np |
| 127 | + |
| 128 | + |
| 129 | +def test_build_angle_index_multi_center(): |
| 130 | + # Edges with MIXED centers: dst=[0,1,0,1,2]; exercises the dst[a]!=dst[b] exclusion |
| 131 | + # src=[1,2,3,4,5], dst=[0,1,0,1,2], all norms within a_rcut |
| 132 | + # Expected angles per center: |
| 133 | + # dst=0: edges {0,2} => {(0,2),(2,0)} |
| 134 | + # dst=1: edges {1,3} => {(1,3),(3,1)} |
| 135 | + # dst=2: edge {4} => no pairs |
| 136 | + edge_index = np.array([[1, 2, 3, 4, 5], [0, 1, 0, 1, 2]], dtype=np.int64) |
| 137 | + edge_vec = np.array( |
| 138 | + [[0.3, 0, 0], [0.4, 0, 0], [0.5, 0, 0], [0.6, 0, 0], [0.7, 0, 0]] |
| 139 | + ) |
| 140 | + edge_mask = np.array([True, True, True, True, True]) |
| 141 | + ai, am = build_angle_index(edge_index, edge_vec, edge_mask, 6, a_rcut=1.0) |
| 142 | + got = {(int(ai[0, p]), int(ai[1, p])) for p in range(ai.shape[1]) if am[p]} |
| 143 | + evnorm = np.linalg.norm(edge_vec, axis=-1) |
| 144 | + expected = _angle_oracle( |
| 145 | + [0, 1, 0, 1, 2], evnorm, edge_mask, 1.0, ordered=False, include_self=False |
| 146 | + ) |
| 147 | + assert got == expected |
| 148 | + # Verify no cross-center angles |
| 149 | + assert all( |
| 150 | + edge_index[1, int(ai[0, p])] == edge_index[1, int(ai[1, p])] |
| 151 | + for p in range(ai.shape[1]) |
| 152 | + if am[p] |
| 153 | + ) |
| 154 | + |
| 155 | + |
| 156 | +def test_build_angle_index_static_layout(): |
| 157 | + # Test with static layout.angle_capacity; shape must be (2, capacity) |
| 158 | + edge_index = np.array([[1, 2, 3, 1], [0, 0, 0, 0]], dtype=np.int64) |
| 159 | + edge_vec = np.array([[0.5, 0, 0], [0.9, 0, 0], [2.5, 0, 0], [0.7, 0, 0]]) |
| 160 | + edge_mask = np.array([True, True, True, True]) |
| 161 | + layout = GraphLayout(edge_capacity=100, angle_capacity=10) |
| 162 | + ai, am = build_angle_index( |
| 163 | + edge_index, edge_vec, edge_mask, 4, a_rcut=1.0, layout=layout |
| 164 | + ) |
| 165 | + # Check static shape |
| 166 | + assert ai.shape == (2, 10) |
| 167 | + assert am.shape == (10,) |
| 168 | + # Check real angles match the dynamic result |
| 169 | + got_static = {(int(ai[0, p]), int(ai[1, p])) for p in range(ai.shape[1]) if am[p]} |
| 170 | + ai_dyn, am_dyn = build_angle_index(edge_index, edge_vec, edge_mask, 4, a_rcut=1.0) |
| 171 | + got_dynamic = { |
| 172 | + (int(ai_dyn[0, p]), int(ai_dyn[1, p])) |
| 173 | + for p in range(ai_dyn.shape[1]) |
| 174 | + if am_dyn[p] |
| 175 | + } |
| 176 | + assert got_static == got_dynamic |
| 177 | + # Check mask counts match |
| 178 | + assert int(am.sum()) == int(am_dyn.sum()) |
| 179 | + |
| 180 | + |
| 181 | +def test_build_angle_index_ordered_no_self(): |
| 182 | + # Test ordered=True, include_self=False; should be symmetric pairs excluding diagonals |
| 183 | + edge_index = np.array([[1, 2, 3, 1], [0, 0, 0, 0]], dtype=np.int64) |
| 184 | + edge_vec = np.array([[0.5, 0, 0], [0.9, 0, 0], [2.5, 0, 0], [0.7, 0, 0]]) |
| 185 | + edge_mask = np.array([True, True, True, True]) |
| 186 | + ai, am = build_angle_index( |
| 187 | + edge_index, edge_vec, edge_mask, 4, a_rcut=1.0, ordered=True, include_self=False |
| 188 | + ) |
| 189 | + got = {(int(ai[0, p]), int(ai[1, p])) for p in range(ai.shape[1]) if am[p]} |
| 190 | + evnorm = np.linalg.norm(edge_vec, axis=-1) |
| 191 | + expected = _angle_oracle( |
| 192 | + [0, 0, 0, 0], evnorm, edge_mask, 1.0, ordered=True, include_self=False |
| 193 | + ) |
| 194 | + assert got == expected |
| 195 | + # Verify no self-angles and ordered includes both directions |
| 196 | + assert all( |
| 197 | + int(ai[0, p]) != int(ai[1, p]) for p in range(ai.shape[1]) if am[p] |
| 198 | + ) # no self |
| 199 | + for pair in got: |
| 200 | + a, b = pair |
| 201 | + # For ordered=True, include_self=False, we expect both (a,b) and (b,a) |
| 202 | + # if they are different and both within a_rcut and same center |
| 203 | + rev_pair = (b, a) |
| 204 | + if a != b and a not in [2] and b not in [2]: # edge 2 is outside a_rcut |
| 205 | + assert rev_pair in got # symmetric pairs should both exist |
0 commit comments