@@ -36,14 +36,15 @@ def test_graph_angle_cos_matches_normalized_dot():
3636 np .testing .assert_allclose (vals , [0.0 ], atol = 1e-6 )
3737
3838
39- def test_graph_angle_cos_parallel ():
40- """Two neighbors in the SAME direction as seen from center: cos ~(1-eps).
41-
42- Use rcut=1.5 so only center→neighbor1 (dist=1) and center→neighbor2 (dist=2)
43- but atom1→atom2 (dist=1) is NOT seen from atom2 as a second neighbor.
44- Actually place center at 1.0, so neighbors at 0.0 and 2.0 are collinear.
45- rcut=1.1 => center (at 1.0) sees atoms at 0.0 (dist=1.0) and 2.0 (dist=1.0),
46- but those two atoms don't see each other (dist=2.0 > 1.1).
39+ def test_graph_angle_cos_antiparallel ():
40+ """Two neighbors in ANTIPARALLEL directions as seen from the center: cos ≈ -1.
41+
42+ Center is placed at (1,0,0) with one neighbor at (0,0,0) and another at
43+ (2,0,0). The edge vectors from center are (-1,0,0) and (+1,0,0), which
44+ point in opposite directions => cos ≈ -(1-eps).
45+
46+ rcut=1.1: center sees both neighbors (dist=1.0 each); the two neighbors
47+ do not see each other (dist=2.0 > 1.1), so exactly ONE angle is formed.
4748 """
4849 coord = np .array ([[[1.0 , 0 , 0 ], [0.0 , 0 , 0 ], [2.0 , 0 , 0 ]]])
4950 atype = np .array ([[0 , 0 , 0 ]])
@@ -52,12 +53,8 @@ def test_graph_angle_cos_parallel():
5253 real = np .asarray (ng .angle_mask )
5354 vals = np .asarray (cos )[real ]
5455 assert int (real .sum ()) == 1
55- # edge_vec = neighbor - center; both neighbors are in opposite x-directions
56- # from the center: edge to atom1=(0,0,0) is (-1,0,0); edge to atom2=(2,0,0) is (+1,0,0)
57- # For unit-norm vectors (norm=1.0):
58- # na = va / (1 + eps), nb = vb / (1 + eps)
59- # dot(na, nb) = -1 / (1+eps)^2
60- # cos = -1 / (1+eps)^2 * (1 - eps)
56+ # edge_vec = neighbor - center; edge to (0,0,0) is (-1,0,0); edge to (2,0,0) is (+1,0,0).
57+ # Antiparallel unit vectors: dot(na, nb) = -1 / (1+eps)^2, scaled by (1-eps).
6158 eps = 1e-6
6259 expected = - 1.0 / (1 + eps ) ** 2 * (1 - eps )
6360 np .testing .assert_allclose (vals , [expected ], rtol = 1e-6 )
@@ -243,49 +240,89 @@ def test_graph_angle_cos_parity_vs_dpa3_dense():
243240
244241
245242def test_matches_se_t_dot_form ():
246- """Cos * |va| * |vb| ≈ va·vb (the se_t unnormalized inner product).
243+ """Cross-check graph_angle_cos against an independent coordinate-based oracle.
244+
245+ se_t.py:428-437 computes ``env_ij = sum(rr_i * rr_j, -1)`` where
246+ ``rr_i = sw * diff / r^2`` (the 3-D columns of the env-mat). The raw
247+ unnormalized dot product ``va · vb`` (with ``va = r_a - r_center``) is the
248+ numerator that graph_angle_cos normalizes:
247249
248- se_t.py:428-437 uses `rr_i·rr_j` (unnormalized 3D vectors = edge_vec
249- without smoothing weights). graph_angle_cos(eps=1e-6) satisfies:
250+ graph_angle_cos = (1 - eps) * (va · vb) / ((|va| + eps) * (|vb| + eps))
250251
251- cos ≈ (va/‖va‖) · (vb/‖vb‖) * (1-eps)
252+ Inverting:
252253
253- So:
254- va·vb ≈ cos * (‖va‖ + eps) * (‖vb‖ + eps) / (1 - eps)
254+ graph_angle_cos * (|va| + eps) * (|vb| + eps) / (1 - eps) = va · vb
255255
256- We verify this identity up to the eps rounding (atol~1e-6 * ‖va‖*‖vb‖).
256+ **Why sw is factored out**: sw scales each env-mat vector by a scalar.
257+ When all neighbor distances are *below* ``rcut_smth``, the smooth switch
258+ function equals 1 exactly (``sw == 1``), so the sw factor contributes
259+ nothing and ``env_ij`` reduces to the plain geometry.
260+
261+ **Why this test is not tautological**: the reference ``va``, ``vb``, and
262+ ``env_ij = va · vb`` are computed DIRECTLY FROM COORDINATES in plain numpy,
263+ independent of the graph code path. The |va| and |vb| norms used to unwind
264+ ``cos`` are also recomputed from coordinates, NOT read from ``edge_vec``.
265+ This verifies that the graph stores ``edge_vec = neighbor - center``
266+ correctly and that ``graph_angle_cos`` faithfully encodes the geometry.
267+ With distances well below ``rcut_smth`` the identity holds to ``rtol=1e-12``
268+ because it is exact algebra over fp64; eps-induced rounding is negligible
269+ compared to fp64 relative precision.
257270 """
258- coord = np .array ([[[0.0 , 0 , 0 ], [1.0 , 0.5 , 0.3 ], [0.2 , 1.0 , 0.8 ]]])
271+ # All atoms within distance 0.5 of center; rcut_smth = 1.0 so sw == 1 for all.
272+ rng = np .random .default_rng (7 )
273+ center = np .array ([0.0 , 0.0 , 0.0 ])
274+ r_a = rng .uniform (0.1 , 0.4 , 3 ) # distance from center < rcut_smth=1.0
275+ r_b = rng .uniform (0.1 , 0.4 , 3 )
276+ coord = np .array ([[[* center ], [* r_a ], [* r_b ]]]) # (1, 3, 3), single frame
259277 atype = np .array ([[0 , 0 , 0 ]])
260- ng = attach_angles (build_neighbor_graph (coord , atype , None , 5.0 ), a_rcut = 5.0 )
278+
279+ rcut = 2.0
280+ a_rcut = 2.0
281+ ng = attach_angles (build_neighbor_graph (coord , atype , None , rcut ), a_rcut = a_rcut )
261282 ai = np .asarray (ng .angle_index )
262283 am = np .asarray (ng .angle_mask )
263- ev = np .asarray (ng .edge_vec )
284+ ei = np .asarray (ng .edge_index )
264285 cos = np .asarray (graph_angle_cos (ng .angle_index , ng .edge_vec ))
265286
266287 eps = 1e-6
267- for p in range (am .shape [0 ]):
268- if not am [p ]:
269- continue
288+
289+ # At least one valid angle must exist (atom 0 is the only center with ≥2 nei)
290+ valid_angles = [p for p in range (am .shape [0 ]) if am [p ]]
291+ assert len (valid_angles ) >= 1 , "No valid angles found — geometry problem"
292+
293+ for p in valid_angles :
270294 ea = int (ai [0 , p ])
271295 eb = int (ai [1 , p ])
272- va = ev [ea ]
273- vb = ev [eb ]
274- # unnormalized dot product (se_t convention)
275- dot_se_t = float (np .dot (va , vb ))
276- # reconstruct from graph cos
277- na_norm = np .linalg .norm (va )
278- nb_norm = np .linalg .norm (vb )
279- dot_reconstructed = (
280- float (cos [p ]) * (na_norm + eps ) * (nb_norm + eps ) / (1 - eps )
281- )
282- # tolerance: eps * ‖va‖*‖vb‖ dominates
283- tol = eps * max (na_norm * nb_norm , 1e-12 )
296+ center_node = int (ei [1 , ea ])
297+ na_node = int (ei [0 , ea ])
298+ nb_node = int (ei [0 , eb ])
299+
300+ # Reference: compute difference vectors FROM COORDINATES (independent of graph)
301+ r_center = coord [0 , center_node ]
302+ r_na = coord [0 , na_node ]
303+ r_nb = coord [0 , nb_node ]
304+ va_ref = r_na - r_center # (3,)
305+ vb_ref = r_nb - r_center # (3,)
306+
307+ # Reference: unnormalized dot product from coordinates (se_t convention)
308+ env_ij_ref = float (np .dot (va_ref , vb_ref ))
309+
310+ # Reference norms — from coordinates, NOT from edge_vec
311+ na_norm = float (np .linalg .norm (va_ref ))
312+ nb_norm = float (np .linalg .norm (vb_ref ))
313+
314+ # Graph: unwind graph_angle_cos back to the unnormalized dot product
315+ env_ij_graph = float (cos [p ]) * (na_norm + eps ) * (nb_norm + eps ) / (1 - eps )
316+
284317 np .testing .assert_allclose (
285- dot_reconstructed ,
286- dot_se_t ,
287- atol = tol ,
288- err_msg = f"se_t dot mismatch for angle { p } : va={ va } , vb={ vb } " ,
318+ env_ij_graph ,
319+ env_ij_ref ,
320+ rtol = 1e-12 ,
321+ err_msg = (
322+ f"se_t dot mismatch at angle { p } : "
323+ f"center={ center_node } , na={ na_node } , nb={ nb_node } , "
324+ f"va={ va_ref } , vb={ vb_ref } "
325+ ),
289326 )
290327
291328
0 commit comments