11import numpy as np
22import pytest
33
4- from deepmd .dpmodel .utils .neighbor_graph import pad_and_guard_angles
4+ from deepmd .dpmodel .utils .neighbor_graph import build_angle_index , pad_and_guard_angles
55
66
77def test_pad_angles_dynamic_appends_min_guard ():
@@ -22,3 +22,101 @@ def test_pad_angles_overflow_raises():
2222 ai = np .zeros ((2 , 6 ), dtype = np .int64 )
2323 with pytest .raises (ValueError ):
2424 pad_and_guard_angles (ai , angle_capacity = 4 )
25+
26+
27+ def _angle_oracle (dst , evnorm , mask , a_rcut , ordered , include_self ):
28+ """All (edge_a, edge_b) sharing a center, both edges within a_rcut."""
29+ out = set ()
30+ for a in range (len (dst )):
31+ if not mask [a ] or evnorm [a ] >= a_rcut :
32+ continue
33+ for b in range (len (dst )):
34+ if not mask [b ] or evnorm [b ] >= a_rcut or dst [a ] != dst [b ]:
35+ continue
36+ if not include_self and a == b :
37+ continue
38+ if not ordered and b < a :
39+ continue
40+ out .add ((a , b ))
41+ return out
42+
43+
44+ def test_build_angle_index_matches_oracle ():
45+ # 4 edges, all dst=0; norms [0.5, 0.9, 2.5, 0.7], a_rcut=1.0
46+ # DEFAULT = unordered, no-self => within a_rcut {0,1,3} give {(0,1),(0,3),(1,3)}
47+ edge_index = np .array ([[1 , 2 , 3 , 1 ], [0 , 0 , 0 , 0 ]], dtype = np .int64 )
48+ edge_vec = np .array ([[0.5 , 0 , 0 ], [0.9 , 0 , 0 ], [2.5 , 0 , 0 ], [0.7 , 0 , 0 ]])
49+ edge_mask = np .array ([True , True , True , True ])
50+ ai , am = build_angle_index (edge_index , edge_vec , edge_mask , 4 , a_rcut = 1.0 )
51+ got = {(int (ai [0 , p ]), int (ai [1 , p ])) for p in range (ai .shape [1 ]) if am [p ]}
52+ evnorm = np .linalg .norm (edge_vec , axis = - 1 )
53+ assert got == _angle_oracle (
54+ [0 , 0 , 0 , 0 ], evnorm , edge_mask , 1.0 , ordered = False , include_self = False
55+ )
56+ assert got == {
57+ (0 , 1 ),
58+ (0 , 3 ),
59+ (1 , 3 ),
60+ } # unordered, no-self, edge 2 dropped (norm>a_rcut)
61+ assert all (
62+ 2 not in (int (ai [0 , p ]), int (ai [1 , p ])) for p in range (ai .shape [1 ]) if am [p ]
63+ )
64+ assert all (
65+ int (ai [0 , p ]) != int (ai [1 , p ]) for p in range (ai .shape [1 ]) if am [p ]
66+ ) # no self
67+
68+
69+ def test_build_angle_index_ordered_include_self ():
70+ # ordered + include_self: (0,0),(0,1),(0,3),(1,0),(1,1),(1,3),(3,0),(3,1),(3,3)
71+ edge_index = np .array ([[1 , 2 , 3 , 1 ], [0 , 0 , 0 , 0 ]], dtype = np .int64 )
72+ edge_vec = np .array ([[0.5 , 0 , 0 ], [0.9 , 0 , 0 ], [2.5 , 0 , 0 ], [0.7 , 0 , 0 ]])
73+ edge_mask = np .array ([True , True , True , True ])
74+ ai , am = build_angle_index (
75+ edge_index , edge_vec , edge_mask , 4 , a_rcut = 1.0 , ordered = True , include_self = True
76+ )
77+ got = {(int (ai [0 , p ]), int (ai [1 , p ])) for p in range (ai .shape [1 ]) if am [p ]}
78+ evnorm = np .linalg .norm (edge_vec , axis = - 1 )
79+ assert got == _angle_oracle (
80+ [0 , 0 , 0 , 0 ], evnorm , edge_mask , 1.0 , ordered = True , include_self = True
81+ )
82+
83+
84+ def test_build_angle_index_masked_edge ():
85+ # edge 1 masked out — should not appear in any angle
86+ edge_index = np .array ([[1 , 2 , 3 , 1 ], [0 , 0 , 0 , 0 ]], dtype = np .int64 )
87+ edge_vec = np .array ([[0.5 , 0 , 0 ], [0.9 , 0 , 0 ], [0.3 , 0 , 0 ], [0.7 , 0 , 0 ]])
88+ edge_mask = np .array ([True , False , True , True ])
89+ ai , am = build_angle_index (edge_index , edge_vec , edge_mask , 4 , a_rcut = 1.0 )
90+ got = {(int (ai [0 , p ]), int (ai [1 , p ])) for p in range (ai .shape [1 ]) if am [p ]}
91+ evnorm = np .linalg .norm (edge_vec , axis = - 1 )
92+ assert got == _angle_oracle (
93+ [0 , 0 , 0 , 0 ], evnorm , edge_mask , 1.0 , ordered = False , include_self = False
94+ )
95+ assert all (
96+ 1 not in (int (ai [0 , p ]), int (ai [1 , p ])) for p in range (ai .shape [1 ]) if am [p ]
97+ )
98+
99+
100+ def test_build_angle_index_torch_namespace ():
101+ # Step 4b: torch-namespace smoke test (function-level import for TID253)
102+ import torch
103+
104+ edge_index = np .array ([[1 , 2 , 3 , 1 ], [0 , 0 , 0 , 0 ]], dtype = np .int64 )
105+ edge_vec = np .array ([[0.5 , 0 , 0 ], [0.9 , 0 , 0 ], [2.5 , 0 , 0 ], [0.7 , 0 , 0 ]])
106+ edge_mask = np .array ([True , True , True , True ])
107+
108+ ai_np , am_np = build_angle_index (edge_index , edge_vec , edge_mask , 4 , a_rcut = 1.0 )
109+ got_np = {
110+ (int (ai_np [0 , p ]), int (ai_np [1 , p ])) for p in range (ai_np .shape [1 ]) if am_np [p ]
111+ }
112+
113+ t_edge_index = torch .from_numpy (edge_index )
114+ t_edge_vec = torch .from_numpy (edge_vec )
115+ t_edge_mask = torch .from_numpy (edge_mask )
116+ ai_t , am_t = build_angle_index (t_edge_index , t_edge_vec , t_edge_mask , 4 , a_rcut = 1.0 )
117+ got_t = {
118+ (int (ai_t [0 , p ].item ()), int (ai_t [1 , p ].item ()))
119+ for p in range (ai_t .shape [1 ])
120+ if am_t [p ].item ()
121+ }
122+ assert got_t == got_np
0 commit comments