Skip to content

Commit df28cdc

Browse files
author
Han Wang
committed
test(dpmodel): real se_t-convention oracle for graph_angle_cos; fix antiparallel test name
1 parent 6ecafb9 commit df28cdc

1 file changed

Lines changed: 80 additions & 43 deletions

File tree

source/tests/common/dpmodel/test_graph_angle_cos_parity.py

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

245242
def 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

Comments
 (0)