Skip to content

Commit 4ffb348

Browse files
authored
Fix symmetries, round 2: uniform scaling (#1169)
1 parent 38b7f27 commit 4ffb348

File tree

2 files changed

+44
-30
lines changed

2 files changed

+44
-30
lines changed

src/workarounds/forwarddiff_rules.jl

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -94,47 +94,52 @@ Return `true` if a symmetry that holds for the primal part is broken by
9494
a perturbation in the lattice or in the positions, `false` otherwise.
9595
"""
9696
function is_symmetry_broken_by_dual(lattice, atoms, positions, symmetry::SymOp; tol_symmetry)
97+
# The symmetry operation is given by (W,w) where W is a 3x3 matrix and w a 3-vector,
98+
# both in reduced (fractional) coordinates.
9799
# For any lattice atom at position x, W*x + w should be in the lattice.
98-
# In cartesian coordinates, with a perturbed lattice A = A₀ + εA₁,
99-
# this means that for any atom position xcart in the unit cell and any 3 integers u,
100-
# there should be an atom at position ycart and 3 integers v such that:
101-
# Wcart * (xcart + A*u) + wcart = ycart + A*v
102-
# where
103-
# Wcart = A₀ * W * A₀⁻¹; note that W is an integer matrix
104-
# wcart = A₀ * w.
105-
#
106-
# In relative coordinates this gives:
107-
# A⁻¹ * Wcart * (A*x + A*u) + A⁻¹ * wcart = y + v (*)
100+
# This means that for any atom position x (in reduced coordinates) in the unit cell
101+
# and any 3 integers u, there should be an atom at position y and 3 integers v such that:
102+
# W * (x + u) + w = y + v
108103
#
104+
# Additionally, Wcart = A * W * A⁻¹ (in cartesian coordinates) must be orthogonal.
105+
#
109106
# The strategy is then to check that:
110-
# 1. A⁻¹ * Wcart * A is still an integer matrix (i.e. no dual part),
111-
# such that any change in u is easily compensated for in v.
112-
# 2. The primal component of (*), i.e. with ε=0, is already known to hold.
113-
# Since v does not have a dual component, it is enough to check that
114-
# the dual part of the following is 0:
115-
# A⁻¹ * Wcart * A*x + A⁻¹ * wcart - y
116-
117-
lattice_primal = ForwardDiff.value.(lattice)
118-
W = (compute_inverse_lattice(lattice) * lattice_primal
119-
* symmetry.W * compute_inverse_lattice(lattice_primal) * lattice)
120-
w = compute_inverse_lattice(lattice) * lattice_primal * symmetry.w
121-
107+
# 1. Metric invariance: Verify that the perturbed lattice metric
108+
# G = A' * A satisfies W' * G * W = G.
109+
# (This is equivalent to checking that Wcart = A * W * A⁻¹ is orthogonal.)
110+
# 2. Atomic mapping:
111+
# W is an integer matrix, such that any change in u is easily compensated for in v.
112+
# Thus we only need to check that for each atomic position `x`
113+
# `W*x + w = y (mod 1)` is still satisfied for some equivalent atom `y`
114+
# of the same species, up to numerical tolerance.
115+
# Only the dual (perturbation) components are checked,
116+
# the primal part is assumed to already obey the symmetry.
117+
118+
# TODO: work out the case of nested Duals for higher-order derivatives
119+
# (eg. symmetry breaking at higher order)
122120
is_dual_nonzero(x::AbstractArray) = any(x) do xi
123121
maximum(abs, ForwardDiff.partials(xi)) >= tol_symmetry
124122
end
125-
# Check 1.
126-
if is_dual_nonzero(W)
127-
return true
123+
124+
W = symmetry.W
125+
w = symmetry.w
126+
127+
# 1) Metric tensor check in fractional frame: Wᵀ G W == G (to first order)
128+
G = lattice'lattice # 3x3 metric tensor on the unit cube
129+
ΔG = W' * G * W - G
130+
if is_dual_nonzero(ΔG)
131+
return true # anisotropic lattice perturbation breaks this symmetry
128132
end
129133

134+
# 2) Atomic mapping check stays purely in reduced coordinates.
130135
atom_groups = [findall(Ref(pot) .== atoms) for pot in Set(atoms)]
131136
for group in atom_groups
132137
positions_group = positions[group]
133-
for position in positions_group
134-
i_other_at = find_symmetry_preimage(positions_group, position, symmetry; tol_symmetry)
135-
136-
# Check 2. with x = positions_group[i_other_at] and y = position
137-
if is_dual_nonzero(positions_group[i_other_at] + inv(W) * (w - position))
138+
for y in positions_group
139+
i_other = find_symmetry_preimage(positions_group, y, symmetry; tol_symmetry)
140+
x = positions_group[i_other]
141+
# Check that W*x + w - y has zero dual part (primal already ok by construction)
142+
if is_dual_nonzero(W * x + w - y)
138143
return true
139144
end
140145
end

test/forwarddiff.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,15 @@ end
331331
@test length(symmetries_filtered) == 4
332332
check_symmetries(symmetries_filtered, [mirrory, mirroryz])
333333
end
334+
335+
@testset "Isotropic lattice scaling" begin
336+
# Isotropic scaling should not break any symmetry
337+
lattice_modified = (1 + ε) * lattice
338+
symmetries_filtered = DFTK.remove_dual_broken_symmetries(lattice_modified, atoms, positions, symmetries_full)
339+
340+
@test length(symmetries_filtered) == length(symmetries_full)
341+
check_symmetries(symmetries_filtered, symmetries_full)
342+
end
334343
end
335344

336345
@testitem "Symmetry-breaking perturbation using ForwardDiff" #=

0 commit comments

Comments
 (0)