Skip to content

Commit 6c80bc7

Browse files
authored
Add copy_spins!. Expose jitter argument in minimize_energy! (#465)
Also includes various updates to the documentation. In particular, `minimize_energy!` suggests a code pattern to search for the ground state.
1 parent 354641d commit 6c80bc7

11 files changed

Lines changed: 159 additions & 119 deletions

File tree

docs/src/library.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Units
3131
add_sample!
3232
clone_correlations
3333
clone_system
34+
copy_spins!
3435
dispersion
3536
dmvec
3637
domain_average

docs/src/versions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## v0.8.1
44
(In progress)
55

6-
* Fix plotting error with SCGA-calculated intensities ([#464](@ref)).
6+
* Fix plotting error with [`SCGA`](@ref)-calculated intensities ([#464](@ref)).
7+
* Add [`copy_spins!`](@ref). Expose `jitter` parameter in
8+
[`minimize_energy!`](@ref) ([#465](@ref)).
79

810
## v0.8.0
911
(Nov 30, 2025)

examples/03_LSWT_SU3_FeI2.jl

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,15 @@ set_onsite_coupling!(sys, S -> -D*S[3]^2, 1)
142142

143143
# ### Finding the ground state
144144

145-
# This model has been fitted so that energy minimization yields the physically
146-
# correct ground state. Knowing this, we could set the magnetic configuration
147-
# manually by calling [`set_dipole!`](@ref) on each site in the system. Another
148-
# approach, as we will demonstrate, is to search for the ground-state via
149-
# [`minimize_energy!`](@ref).
145+
# The model parameters have already been fitted so that energy minimization
146+
# yields the physically correct ground state. Knowing this, one could manually
147+
# set the magnetic configuration by calling [`set_dipole!`](@ref) at each site.
148+
# Alternatively, Sunny provides tools to interactively search for the ground
149+
# state.
150150
#
151-
# To reduce bias in the search, use [`resize_supercell`](@ref) to create a
152-
# relatively large system of 4×4×4 chemical cells. Randomize all spins
153-
# (represented as SU(3) coherent states) and minimize the energy.
151+
# Use [`resize_supercell`](@ref) to create a relatively large system of 4×4×4
152+
# chemical cells. Call [`randomize_spins!`](@ref) and [`minimize_energy!`](@ref)
153+
# in sequence.
154154

155155
sys = resize_supercell(sys, (4, 4, 4))
156156
randomize_spins!(sys)
@@ -161,36 +161,36 @@ minimize_energy!(sys)
161161

162162
plot_spins(sys; color=[S[3] for S in sys.dipoles])
163163

164-
# One could precisely quantify the Fourier-space static structure factor
165-
# ``\mathcal{S}(𝐪)`` of this spin configuration using
166-
# [`SampledCorrelationsStatic`](@ref). For the present purposes, however, it is
167-
# most convenient to use [`print_wrapped_intensities`](@ref), which effectively
168-
# averages ``\mathcal{S}(𝐪)`` over all Brillouin zones.
164+
# Finding the true ground state can be a challenging task. If the system is not
165+
# too large, a good strategy is repeated randomization and then energy
166+
# minimization. This particular system might require about 30 minimization runs
167+
# to find the defect-free ground state.
168+
#
169+
# Another tool is [`print_wrapped_intensities`](@ref). It reports weights
170+
# analogous to the static structure factor ``\mathcal{S}(𝐪)``, but without
171+
# accounting for phase interference between magnetic sublattices. To calculate
172+
# the true ``\mathcal{S}(𝐪)``, use instead [`SampledCorrelationsStatic`](@ref).
169173

170174
print_wrapped_intensities(sys)
171175

172-
# The known zero-field energy-minimizing magnetic structure of FeI₂ is a two-up,
173-
# two-down order. It can be described as a generalized spiral with a single
174-
# propagation wavevector ``𝐤``. Rotational symmetry allows for three equivalent
175-
# orientations: ``±𝐤 = [0, -1/4, 1/4]``, ``[1/4, 0, 1/4]``, or
176-
# ``[-1/4,1/4,1/4]``. Energy minimization of large systems can easily get
177-
# trapped in a metastable state with competing domains and defects. Nonetheless,
178-
# `print_wrapped_intensities` shows that a non-trivial fraction of the total
179-
# intensity is consistent with known ordering wavevectors.
176+
# The correct ground state for FeI₂ is known to be a generalized spiral with one
177+
# of three propagation wavevectors, ``[0, -1/4, 1/4]`` or ``[1/4, 0, 1/4]`` or
178+
# ``[-1/4, 1/4, 1/4]``, which are equivalent under 120° rotations. The result of
179+
# `print_wrapped_intensities` hints at this spiral phase.
180180
#
181-
# Let's break the three-fold symmetry by hand. The function
182-
# [`suggest_magnetic_supercell`](@ref) takes any number of ``𝐤`` modes and
183-
# suggests a magnetic cell shape that is commensurate with all of them.
181+
# Let's break the 3-fold symmetry by hand. The function
182+
# [`suggest_magnetic_supercell`](@ref) takes any number of propagation
183+
# wavevectors and suggests a commensurate magnetic cell.
184184

185185
suggest_magnetic_supercell([[0, -1/4, 1/4]])
186186

187-
# Using the minimal system returned by [`reshape_supercell`](@ref), it is now
188-
# easy to find the ground state. Plot the system again, now including "ghost"
189-
# spins out to 12Å, to verify that the magnetic order is consistent with FeI₂.
187+
# After calling [`reshape_supercell`](@ref), it becomes easy to find the ground
188+
# state. Plot the system again, now including "ghost" spins out to 12Å. The
189+
# correct two-up, two-down magnetic order is visually apparent.
190190

191-
sys_min = reshape_supercell(sys, [1 0 0; 0 2 1; 0 -2 1])
191+
sys_min = reshape_supercell(sys, [1 0 0; 0 1 -2; 0 1 2])
192192
randomize_spins!(sys_min)
193-
minimize_energy!(sys_min);
193+
minimize_energy!(sys_min)
194194
plot_spins(sys_min; color=[S[3] for S in sys_min.dipoles], ghost_radius=12)
195195

196196
# ### Spin wave theory

src/Integrators.jl

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,27 +142,24 @@ end
142142
"""
143143
suggest_timestep(sys, integrator; tol)
144144
145-
Suggests a timestep for the numerical integration of spin dynamics according to
146-
a given error tolerance `tol`. The `integrator` should be [`Langevin`](@ref) or
147-
[`ImplicitMidpoint`](@ref). The suggested ``dt`` will be inversely proportional
148-
to the magnitude of the effective field ``|dE/d𝐒|`` arising from the current
149-
spin configuration in `sys`. The recommended timestep ``dt`` scales like `√tol`,
150-
which assumes second-order accuracy of the integrator.
151-
152-
The system `sys` should be initialized to an equilibrium spin configuration for
153-
the target temperature. Alternatively, a reasonably timestep estimate can be
154-
obtained from any low-energy spin configuration. For this, one can use
155-
[`randomize_spins!`](@ref) and then [`minimize_energy!`](@ref).
156-
157-
Large `damping` magnitude or target temperature `kT` will tighten the timestep
158-
bound. If `damping` exceeds 1, it will rescale the suggested timestep by an
159-
approximate the factor ``1/damping``. If `kT` is the largest energy scale, then
160-
the suggested timestep will scale like `1/(damping*kT)`. Quantification of
161-
numerical error for stochastic dynamics is subtle. The stochastic Heun
162-
integration scheme is weakly convergent of order-1, such that errors in the
163-
estimates of averaged observables may scale like `dt`. This implies that the
164-
`tol` argument may actually scale like the _square_ of the true numerical error,
165-
and should be selected with this in mind.
145+
Suggests a timestep `dt` for spin dynamics simulation at a given error tolerance
146+
`tol`. The `integrator` should be [`Langevin`](@ref) or
147+
[`ImplicitMidpoint`](@ref). Ideally, the spin configuration in `sys` would be
148+
equilibrated to the target thermodynamic conditions. In practice, a
149+
configuration obtained from [`minimize_energy!`](@ref) should give a reasonable,
150+
if conservative, `dt` suggestion.
151+
152+
The suggested `dt` scales like `√tol`, consistent with a second order
153+
integration scheme. In most cases, `dt` will also be inversely proportional to
154+
the characteristic magnitude of the energy gradient, ``∂E/∂𝐒_i``. If the
155+
Langevin noise magnitude ``λ k_B T`` dominates, however, then its inverse will
156+
limit the `dt` scale.
157+
158+
Analysis of error in Langevin dynamics can be subtle. Sunny uses the stochastic
159+
Heun scheme, which has a weak convergence rate of order 1. This means that
160+
errors in certain statistical observables may scale like `dt` rather than
161+
`dt^2`. In such cases, the `tol` parameter controls the _square_ of the
162+
numerical error, and can be tightened appropriately.
166163
"""
167164
function suggest_timestep(sys::System, integrator::Union{Langevin, ImplicitMidpoint}; tol)
168165
(; dt) = integrator

src/MagneticOrdering.jl

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
"""
22
print_wrapped_intensities(sys::System; nmax=10)
33
4-
For Bravais lattices: Prints up to `nmax` wavevectors according to their
5-
instantaneous (static) structure factor intensities, listed in descending order.
6-
For non-Bravais lattices: Performs the same analysis for each spin sublattice
7-
independently; the output weights are naïvely averaged over sublattices, without
8-
incorporating phase shift information. This procedure therefore wraps all
9-
wavevectors into the first Brillouin zone. Each wavevector coordinate is given
10-
between ``-1/2`` and ``1/2`` in reciprocal lattice units (RLU). The output from
11-
this function will typically be used as input to
4+
Prints up to `nmax` wavevectors ``𝐪`` and their "wrapped" static structure
5+
factor weights. Each ``𝐪`` is exactly commensurate with the system volume and
6+
has components between ``-1/2`` and ``1/2`` in reciprocal lattice units (RLU).
7+
The output from this function will typically be used as input to
128
[`suggest_magnetic_supercell`](@ref).
139
14-
Because this function does not incorporate phase information in its averaging
15-
over sublattices, the printed weights are not directly comparable with
16-
experiment. For that purpose, use [`SampledCorrelationsStatic`](@ref) instead.
10+
For simplicity, phase interference between sublattices is neglected. The
11+
reported weights are the sum of static structure factors
12+
``\\mathcal{S}_{jj}(𝐪)`` calculated independently for each sublattice ``j`` of
13+
the chemical cell. This is mathematically equivalent to averaging
14+
``\\mathcal{S}(𝐪)`` over all cells of the infinite reciprocal lattice. It is in
15+
this sense that the intensities are "wrapped" into the first reciprocal cell.
16+
17+
To calculate the true ``\\mathcal{S}(𝐪)`` as an experimental observable, use
18+
[`SampledCorrelationsStatic`](@ref) instead.
1719
"""
1820
function print_wrapped_intensities(sys::System{N}; nmax=10) where N
1921
sys.crystal == orig_crystal(sys) || error("Cannot perform this analysis on reshaped system.")

src/Optimization.jl

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
struct MinimizationResult
22
converged :: Bool
3-
iterations :: Int
3+
niters :: Int
4+
energy :: Float64
45
data :: Optim.OptimizationResults
6+
7+
function MinimizationResult(niters, res)
8+
return new(Optim.converged(res), niters, Optim.minimum(res), res)
9+
end
510
end
611

712
function Base.show(io::IO, ::MIME"text/plain", res::MinimizationResult)
8-
(; converged, iterations, data) = res
13+
(; converged, niters, data) = res
914
Δx = number_to_simple_string(Optim.x_abschange(data); digits=3)
1015
g_res = number_to_simple_string(Optim.g_residual(data); digits=3)
1116
if converged
12-
print(io, "Converged in $iterations iterations")
17+
print(io, "Converged in $niters iterations")
1318
else
14-
print(io, "Non-converged after $iterations iterations: |Δx|=$Δx, |∂E/∂x|=$g_res")
19+
print(io, "Non-converged after $niters iterations: |Δx|=$Δx, |∂E/∂x|=$g_res")
1520
end
1621
end
1722

@@ -52,34 +57,52 @@ function Optim.project_tangent!(sm::SpinManifold, g, x)
5257
end
5358

5459
function optimize_with_restarts(; calc_f, calc_g!, x, method, maxiters, options_args)
55-
iters = 0
60+
niters = 0
5661
while true
57-
options = Optim.Options(; iterations=maxiters-iters, options_args...)
62+
options = Optim.Options(; iterations=maxiters-niters, options_args...)
5863
res = Optim.optimize(calc_f, calc_g!, x, method, options)
5964
x = Optim.minimizer(res)
60-
iters += Optim.iterations(res)
61-
if Optim.converged(res) || iters >= maxiters
62-
return (res, iters)
65+
niters += Optim.iterations(res)
66+
if Optim.converged(res) || niters >= maxiters
67+
return (; res, niters)
6368
end
6469
end
6570
end
6671

6772

6873
"""
69-
minimize_energy!(sys::System; maxiters=1000, kwargs...)
70-
71-
Optimizes the spin configuration in `sys` to minimize energy. A total of
72-
`maxiters` iterations will be attempted. Any remaining `kwargs` will be included
73-
in the `Options` constructor of the [Optim.jl
74-
package](https://github.com/JuliaNLSolvers/Optim.jl)
75-
76-
Convergence status is stored in the field `ret.converged` of the return value
77-
`ret`. Additional optimization statistics are stored in the field `ret.data`.
74+
minimize_energy!(sys::System; maxiters=1000, jitter=1e-8, kwargs...)
75+
76+
Optimizes the spin configuration in `sys` to find a local minimum of the energy.
77+
Large magnetic cells will be slower to converge; increase `maxiters` as needed.
78+
Prior to optimization, each spin will be randomly perturbed with the
79+
dimensionless `jitter` magnitude, which can be useful to break accidental
80+
symmetries. Any remaining `kwargs` will be included in the `Options` constructor
81+
of the [Optim.jl package](https://github.com/JuliaNLSolvers/Optim.jl).
82+
83+
Returns an object that can be inspected for optimization statistics.
84+
85+
!!! tip "Escaping local minima"
86+
To search for the global energy minimum, a simple strategy is to repeatedly
87+
call [`randomize_spins!`](@ref) and then `minimize_energy!`. The example
88+
below keeps the minimum energy spin state found within 100 runs.
89+
90+
```julia
91+
tmp_sys = clone_system(sys)
92+
for i in 1:100
93+
randomize_spins!(sys)
94+
minimize_energy!(sys)
95+
if energy(sys) < energy(tmp_sys)
96+
copy_spins!(tmp_sys, sys)
97+
end
98+
end
99+
copy_spins!(sys, tmp_sys)
100+
```
78101
"""
79-
function minimize_energy!(sys::System{N}; maxiters=1000, δ=1e-8, kwargs...) where N
102+
function minimize_energy!(sys::System{N}; maxiters=1000, jitter=1e-8, kwargs...) where N
80103
# Small perturbation to destabilize an accidental stationary point (local
81104
# maximum or saddle).
82-
perturb_spins!(sys, δ)
105+
perturb_spins!(sys, jitter)
83106

84107
# Optimization variables are normalized spins or coherent states. In case of
85108
# a vacancy, use an arbitrary representative on the sphere: [1, 1, …] / √N.
@@ -132,10 +155,10 @@ function minimize_energy!(sys::System{N}; maxiters=1000, δ=1e-8, kwargs...) whe
132155
manifold = SpinManifold(iszero(N) ? 3 : N, length(eachsite(sys)))
133156
method = Optim.ConjugateGradient(; alphaguess=LineSearches.InitialHagerZhang(; αmax=10.0), manifold)
134157
options_args = (; g_abstol, x_abstol, x_reltol=NaN, f_reltol=NaN, f_abstol=NaN, kwargs...)
135-
(res, iters) = optimize_with_restarts(; calc_f, calc_g!, x, method, maxiters, options_args)
158+
(; res, niters) = optimize_with_restarts(; calc_f, calc_g!, x, method, maxiters, options_args)
136159

137160
load_spins!(Optim.minimizer(res))
138-
mr = MinimizationResult(Optim.converged(res), iters, res)
161+
mr = MinimizationResult(niters, res)
139162
if !mr.converged
140163
@warn repr("text/plain", mr)
141164
end

src/Sunny.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ include("System/OnsiteCoupling.jl")
6363
include("System/Ewald.jl")
6464
include("System/Interactions.jl")
6565
export Moment, System, Site, clone_system, eachsite, position_to_site, global_position,
66-
magnetic_moment, set_coherent!, set_dipole!, polarize_spins!, randomize_spins!,
66+
magnetic_moment, set_coherent!, set_dipole!, polarize_spins!, copy_spins!, randomize_spins!,
6767
set_spin_rescaling!, set_spin_s_at!, set_spin_rescaling_for_static_sum_rule!,
6868
energy, energy_per_site, spin_label, set_onsite_coupling!, set_pair_coupling!,
6969
set_exchange!, dmvec, enable_dipole_dipole!, set_field!, to_inhomogeneous, set_field_at!,

src/Symmetry/Printing.jl

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ end
146146
"""
147147
print_bond(cryst::Crystal, b::Bond; b_ref=b)
148148
149-
Prints symmetry information for bond `b`. An optional symmetry-equivalent
150-
reference bond `b_ref` can be provided to keep a consistent meaning of the free
151-
parameters `A`, `B`, etc.
149+
Prints symmetry-allowed interactions for the [`Bond`](@ref) `b`. An optional
150+
symmetry-equivalent reference bond `b_ref` can be provided to keep a consistent
151+
meaning of the free parameters `A`, `B`, etc.
152152
"""
153153
function print_bond(cryst::Crystal, b::Bond; b_ref=b, io=stdout)
154154
# How many digits to use in printing coefficients
@@ -193,10 +193,12 @@ end
193193
"""
194194
print_symmetry_table(cryst::Crystal, max_dist)
195195
196-
Print symmetry information for all equivalence classes of sites and bonds, up to
197-
a maximum bond distance of `max_dist`. Equivalent to calling `print_bond(cryst,
198-
b)` for every bond `b` in `reference_bonds(cryst, max_dist)`, where
199-
`Bond(i, i, [0,0,0])` refers to a single site `i`.
196+
Prints the allowed interactions, as constrained by the spacegroup symmetries of
197+
the provided [`Crystal`](@ref). The bond distance cutoff `max_dist` is in global
198+
length units, e.g. angstrom.
199+
200+
The same information can be obtained from [`print_site`](@ref),
201+
[`print_bond`](@ref), and [`reference_bonds`](@ref).
200202
"""
201203
function print_symmetry_table(cryst::Crystal, max_dist; io=stdout)
202204
for b in reference_bonds(cryst, max_dist)
@@ -206,11 +208,11 @@ end
206208

207209

208210
"""
209-
print_suggested_frame(cryst, i)
211+
print_suggested_frame(cryst::Crystal, i)
210212
211-
Print a suggested reference frame, as a rotation matrix `R`, that can be used as
212-
input to `print_site()`. The purpose is to simplify the description of allowed
213-
anisotropies.
213+
Prints a suggested reference frame for atom `i`. This is given as a rotation `R`
214+
of the Cartesian basis. When passed to [`print_site`](@ref), it may simplify the
215+
description of symmetry-allowed anisotropies.
214216
"""
215217
function print_suggested_frame(cryst::Crystal, i::Int)
216218
R = suggest_frame_for_atom(cryst, i)
@@ -220,13 +222,13 @@ end
220222

221223

222224
"""
223-
print_site(cryst, i; i_ref=i, R=I)
225+
print_site(cryst::Crystal, i; i_ref=i, R=I)
224226
225-
Print symmetry information for the site `i`, including allowed g-tensor and
226-
allowed anisotropy operator. An optional symmetry-equivalent reference atom
227-
`i_ref` can be provided to keep a consistent meaning of the free parameters. An
228-
optional rotation matrix `R` can map to a new Cartesian reference frame for
229-
expression of the allowed anisotropy.
227+
Print symmetry information for atom `i`, including allowed g-tensor and allowed
228+
anisotropy operator. An optional symmetry-equivalent reference atom `i_ref` can
229+
be provided to keep a consistent meaning of the free parameters. An optional
230+
rotation matrix `R` will transform the Cartesian basis for expression of the
231+
allowed anisotropy.
230232
"""
231233
function print_site(cryst::Crystal, i; i_ref=i, R=Mat3(I), ks=[2,4,6], io=stdout)
232234
R_global = convert(Mat3, R)

src/Symmetry/SymmetryAnalysis.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,10 @@ end
195195

196196
""" reference_bonds(cryst::Crystal, max_dist)
197197
198-
Returns a full list of bonds, one for each symmetry equivalence class, up to
199-
distance `max_dist`. The reference bond `b` for each equivalence class is
200-
selected according to a scoring system that prioritizes simplification of the
201-
elements in `basis_for_symmetry_allowed_couplings(cryst, b)`."""
198+
Returns a list of [`Bond`](@ref)s, one for each symmetry equivalence class, up
199+
to the `max_dist` cutoff in length units. These reference bonds are
200+
heuristically selected to simplify the expression of symmetry-allowed
201+
interactions."""
202202
function reference_bonds(cryst::Crystal, max_dist::Float64; min_dist=0.0)
203203
# Bonds, one for each equivalence class
204204
ref_bonds = Bond[]

0 commit comments

Comments
 (0)