• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wexlergroup / FreeBird.jl / 25113611074

29 Apr 2026 02:04PM UTC coverage: 84.91% (+1.0%) from 83.942%
25113611074

Pull #129

github

web-flow
Merge ae5542e01 into 1a743d79c
Pull Request #129: Grand-canonical nested sampling for lattice systems

207 of 215 new or added lines in 3 files covered. (96.28%)

3 existing lines in 1 file now uncovered.

2065 of 2432 relevant lines covered (84.91%)

615272.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.24
/src/SamplingSchemes/nested_sampling.jl
1
"""
2
    mutable struct NestedSamplingParameters <: SamplingParameters
3

4
The `NestedSamplingParameters` struct represents the parameters used in the nested sampling scheme.
5

6
# Fields
7
- `mc_steps::Int64`: The number of total Monte Carlo moves to perform. For a parallel MC routine, this number will be distributed among workers.
8
If `mc_steps` is not divisible by the number of workers, the actual number of MC moves per worker will be `ceil(mc_steps / nworkers())`.
9
- `initial_step_size::Float64`: The initial step size, which is the fallback step size if MC routine fails to accept a move.
10
- `step_size::Float64`: The on-the-fly step size used in the sampling process.
11
- `step_size_lo::Float64`: The lower bound of the step size.
12
- `step_size_up::Float64`: The upper bound of the step size.
13
- `accept_range::Tuple{Float64, Float64}`: The range of acceptance rates for adjusting the step size.
14
e.g. (0.25, 0.75) means that the step size will decrease if the acceptance rate is below 0.25 and increase if it is above 0.75.
15
- `fail_count::Int64`: The number of failed MC moves in a row.
16
- `allowed_fail_count::Int64`: The maximum number of failed MC moves allowed before resetting the step size.
17
- `energy_perturbation::Float64`: The perturbation value used to adjust the energy of the walkers.
18
- `random_seed::Int64`: The seed for the random number generator.
19
- `cluster_p::Float64`: Current cluster growth probability for geometric cluster moves (mutable runtime state).
20
- `cluster_accepted::Float64`: Accepted cluster moves in the current adjustment window.
21
- `cluster_total::Float64`: Total cluster moves attempted in the current adjustment window.
22
- `cluster_p_history::Vector{Float64}`: Trajectory of cluster_p values after each adaptive adjustment.
23
- `cluster_accept_history::Vector{Float64}`: Acceptance rate at each adaptive adjustment.
24
- `cluster_adjust_iterations::Vector{Int}`: NS iteration index at each adaptive adjustment.
25
"""
26
mutable struct NestedSamplingParameters <: SamplingParameters
27
    mc_steps::Int64
60✔
28
    initial_step_size::Float64
29
    step_size::Float64
30
    step_size_lo::Float64
31
    step_size_up::Float64
32
    accept_range::Tuple{Float64, Float64}
33
    fail_count::Int64
34
    allowed_fail_count::Int64
35
    energy_perturbation::Float64
36
    random_seed::Int64
37
    cluster_p::Float64
38
    cluster_accepted::Float64
39
    cluster_total::Float64
40
    cluster_p_history::Vector{Float64}
41
    cluster_accept_history::Vector{Float64}
42
    cluster_adjust_iterations::Vector{Int}
43
end
44

45
function NestedSamplingParameters(;
75✔
46
            mc_steps::Int64=200,
47
            initial_step_size::Float64=0.01,
48
            step_size::Float64=0.1,
49
            step_size_lo::Float64=1e-6,
50
            step_size_up::Float64=1.0,
51
            accept_range::Tuple{Float64, Float64}=(0.25, 0.75),
52
            fail_count::Int64=0,
53
            allowed_fail_count::Int64=100,
54
            energy_perturbation::Float64=1e-12,
55
            random_seed::Int64=1234,
56
            cluster_p::Float64=0.3,
57
            cluster_accepted::Float64=0.0,
58
            cluster_total::Float64=0.0,
59
            cluster_p_history::Vector{Float64}=Float64[],
60
            cluster_accept_history::Vector{Float64}=Float64[],
61
            cluster_adjust_iterations::Vector{Int}=Int[],
62
            )
63
    NestedSamplingParameters(mc_steps, initial_step_size, step_size, step_size_lo, step_size_up, accept_range, fail_count, allowed_fail_count, energy_perturbation, random_seed, cluster_p, cluster_accepted, cluster_total, cluster_p_history, cluster_accept_history, cluster_adjust_iterations)
60✔
64
end
65

66
"""
67
    LatticeNestedSamplingParameters(;
68
            mc_steps::Int64=100,
69
            energy_perturbation::Float64=1e-12,
70
            fail_count::Int64=0,
71
            allowed_fail_count::Int64=10,
72
            random_seed::Int64=1234,
73
            cluster_p::Float64=0.3,
74
            )
75
A convenience constructor for `NestedSamplingParameters` with default values suitable for lattice systems.
76
"""
77
function LatticeNestedSamplingParameters(;
50✔
78
            mc_steps::Int64=100,
79
            energy_perturbation::Float64=1e-12,
80
            fail_count::Int64=0,
81
            allowed_fail_count::Int64=10,
82
            random_seed::Int64=1234,
83
            cluster_p::Float64=0.3,
84
            )
85
    NestedSamplingParameters(mc_steps=mc_steps, fail_count=fail_count, allowed_fail_count=allowed_fail_count, energy_perturbation=energy_perturbation, random_seed=random_seed, cluster_p=cluster_p)
40✔
86
end
87

88

89
"""
90
    abstract type MCRoutine
91

92
An abstract type representing a Monte Carlo routine.
93

94
Currently, the following concrete types are supported:
95
- `MCRandomWalkMaxE`: A type for generating a new walker by performing a random walk for decorrelation on the
96
highest-energy walker.
97
- `MCRandomWalkClone`: A type for generating a new walker by cloning an existing walker and performing a random walk
98
for decorrelation.
99
- `MCNewSample`: A type for generating a new walker from a random configuration. Currently, it is intended to use 
100
this routine for lattice gas systems.
101
- `MCMixedMoves`: A type for generating a new walker by performing random walks and swapping atoms. Currently, it is
102
intended to use this routine for multi-component systems. The actual number of random walks and swaps to perform is
103
determined by the weights of the fields `walks_freq` and `swaps_freq`. See [`MCMixedMoves`](@ref).
104
- `MCRejectionSampling`: A type for generating a new walker by performing rejection sampling. Currently, it is intended
105
to use this routine for lattice gas systems.
106
- `MCDistributed`: A type for generating new walkers by performing random walks for decorrelation in parallel using Distributed.jl.
107
This routine supports multiple culling walkers and multiple decorrelation walkers. See [`MCDistributed`](@ref).
108
"""
109
abstract type MCRoutine end
110

111
"""
112
    abstract type MCRoutineParallel <: MCRoutine
113
(Internal) An abstract type representing a parallel Monte Carlo routine.
114
"""
115
abstract type MCRoutineParallel <: MCRoutine end
116

117
"""
118
    struct MCRandomWalkMaxE <: MCRoutine
119
A type for generating a new walker by performing a random walk for decorrelation on the highest-energy walker.
120
"""
121
struct MCRandomWalkMaxE <: MCRoutine 
122
    dims::Vector{Int64}
123
    function MCRandomWalkMaxE(dims::Vector{Int64}=[1, 2, 3])
65✔
124
        new(dims)
104✔
125
    end
126
end
127

128
"""
129
    struct MCRandomWalkClone <: MCRoutine
130
A type for generating a new walker by cloning an existing walker and performing a random walk for decorrelation.
131
"""
132
struct MCRandomWalkClone <: MCRoutine 
133
    dims::Vector{Int64}
134
    function MCRandomWalkClone(;dims::Vector{Int64}=[1, 2, 3])
45✔
135
        new(dims)
36✔
136
    end
137
end
138

139
"""
140
    struct MCRandomWalkCloneParallel <: MCRoutineParallel
141
A type for generating a new walker by cloning an existing walker and performing a random walk for decorrelation in parallel.
142
"""
143
struct MCRandomWalkCloneParallel <: MCRoutineParallel
144
    dims::Vector{Int64}
145
    function MCRandomWalkCloneParallel(;dims::Vector{Int64}=[1, 2, 3])
15✔
146
        new(dims)
12✔
147
    end
148
end
149

150
"""
151
    struct MCDistributed <: MCRoutineParallel
152
A type for generating new walkers by performing random walks for decorrelation in parallel using Distributed.jl.
153
# Fields
154
- `n_cull::Int64`: The number of lowest-energy walkers to cull (replace) in each iteration. The default is 1.
155
- `n_decorr::Int64`: The number of walkers to use for decorrelation (random walks). The default is `nworkers() - 1`.
156
- `dims::Vector{Int64}`: The dimensions along which to perform the random walks.
157
"""
158
struct MCDistributed <: MCRoutineParallel
159
    n_cull::Int64
160
    n_decorr::Int64
161
    dims::Vector{Int64}
162
    function MCDistributed(;n_cull::Int64=1, n_decorr::Int64=nworkers()-1, dims::Vector{Int64}=[1, 2, 3])
×
163
        if n_cull + n_decorr != nworkers()
×
164
            error("n_cull + n_decorr must be equal to the number of workers: $(nworkers())")
×
165
        end
166
        @info "Distributed nested sampling initiated: n_cull: $n_cull, n_decorr: $n_decorr, total workers: $(n_cull + n_decorr)"
×
167
        new(n_cull, n_decorr, dims)
×
168
    end
169
end
170

171
"""
172
    MCRandomWalkMaxEParallel <: MCRoutineParallel
173
A type for generating a new walker by performing a random walk for decorrelation on the highest-energy walker(s) in parallel.
174
"""
175
struct MCRandomWalkMaxEParallel <: MCRoutineParallel
176
    dims::Vector{Int64}
177
    function MCRandomWalkMaxEParallel(;dims::Vector{Int64}=[1, 2, 3])
15✔
178
        new(dims)
12✔
179
    end
180
end
181

182
"""
183
    struct MCNewSample <: MCRoutine
184
A type for generating a new walker from a random configuration. Currently, it is intended to use this routine for lattice gas systems.
185
"""
186
struct MCNewSample <: MCRoutine end
24✔
187

188
"""
189
    struct MCMixedMoves <: MCRoutine
190
A type for generating a new walker by performing a mix of random walks, atom swaps, and/or geometric cluster moves.
191
For atomistic systems, the `walks_freq` and `swaps_freq` fields control the ratio of random walks to atom swaps.
192
For lattice systems, `walks_freq` and `clusters_freq` control the ratio of local swap moves to geometric cluster moves.
193

194
# Fields
195
- `walks_freq::Int`: The frequency of random walks (atomistic) or local swaps (lattice) to perform.
196
- `swaps_freq::Int`: The frequency of atom swaps to perform (atomistic only).
197
- `clusters_freq::Int`: The frequency of geometric cluster moves to perform (lattice only, default 0).
198
- `initial_cluster_p::Float64`: Starting growth probability for cluster moves (default 0.3).
199
- `target_cluster_accept::Float64`: Target acceptance rate for adaptive cluster p tuning (default 0.3).
200
- `cluster_adjust_interval::Int`: Number of NS iterations between cluster p adjustments (default 50).
201
- `cluster_p_floor::Float64`: Lower bound for adaptive cluster p (default 0.01).
202
- `cluster_p_ceiling::Float64`: Upper bound for adaptive cluster p (default 1.0).
203
"""
204
mutable struct MCMixedMoves <: MCRoutine
205
    walks_freq::Int
40✔
206
    swaps_freq::Int
207
    clusters_freq::Int
208
    initial_cluster_p::Float64
209
    target_cluster_accept::Float64
210
    cluster_adjust_interval::Int
211
    cluster_p_floor::Float64
212
    cluster_p_ceiling::Float64
213
end
214

215
# Backward-compatible constructor: MCMixedMoves(5, 1)
216
function MCMixedMoves(walks_freq::Int, swaps_freq::Int)
16✔
217
    MCMixedMoves(walks_freq, swaps_freq, 0, 0.3, 0.3, 50, 0.01, 1.0)
16✔
218
end
219

220
# Keyword constructor for lattice use
221
function MCMixedMoves(;
30✔
222
    walks_freq::Int=1,
223
    swaps_freq::Int=0,
224
    clusters_freq::Int=1,
225
    initial_cluster_p::Float64=0.3,
226
    target_cluster_accept::Float64=0.3,
227
    cluster_adjust_interval::Int=50,
228
    cluster_p_floor::Float64=0.01,
229
    cluster_p_ceiling::Float64=1.0,
230
)
231
    MCMixedMoves(walks_freq, swaps_freq, clusters_freq,
24✔
232
                 initial_cluster_p, target_cluster_accept, cluster_adjust_interval,
233
                 cluster_p_floor, cluster_p_ceiling)
234
end
235

236
"""
237
    struct MCMixedMovesParallel <: MCRoutineParallel
238
A type for generating a new walker by performing random walks and swapping atoms in parallel. Currently, it is intended to use this routine for
239
multi-component systems. The actual number of random walks and swaps to perform is determined by the weights of the fields `walks_freq` and `swaps_freq`.
240
For example, if `walks_freq=4` and `swaps_freq=1`, then the probability of performing a random walk is 4/5, and the probability of performing a swap is 1/5.
241

242
# Fields
243
- `walks_freq::Int`: The frequency of random walks to perform.
244
- `swaps_freq::Int`: The frequency of atom swaps to perform.
245
"""
246
mutable struct MCMixedMovesParallel <: MCRoutineParallel
247
    walks_freq::Int
248
    swaps_freq::Int
249
end
250

251
"""
252
    struct MCRejectionSampling <: MCRoutine
253
A type for generating a new walker by performing rejection sampling. Currently, it is intended to use this routine for lattice gas systems.
254
"""
255
struct MCRejectionSampling <: MCRoutine end
4✔
256

257

258
# ======================================================================
259
# Grand-canonical nested sampling types
260
# ======================================================================
261

262
"""
263
    struct MCGrandCanonicalMoves <: MCRoutine
264

265
A type for generating a new walker using grand-canonical MCMC moves that mix
266
fixed-N moves (local swaps and/or geometric cluster moves) with single-site
267
particle insertion and deletion.
268

269
# Fields
270
- `p_move::Float64`: Probability of a fixed-N move per MCMC step (default 0.5).
271
- `p_insert::Float64`: Probability of a particle insertion per step (default 0.25).
272
  The deletion probability is `1 - p_move - p_insert`.
273
- `clusters_freq::Int`: Relative weight of cluster moves within the fixed-N branch (default 0 = disabled).
274
- `swaps_freq::Int`: Relative weight of local swaps within the fixed-N branch (default 1).
275
- `initial_cluster_p::Float64`: Starting growth probability for geometric cluster moves (default 0.3).
276
- `target_cluster_accept::Float64`: Target acceptance rate for adaptive cluster p tuning (default 0.3).
277
- `cluster_adjust_interval::Int`: Number of NS iterations between cluster p adjustments (default 50).
278
- `cluster_p_floor::Float64`: Lower bound for adaptive cluster p (default 0.01).
279
- `cluster_p_ceiling::Float64`: Upper bound for adaptive cluster p (default 1.0).
280

281
When `clusters_freq == 0` (the default), the fixed-N branch uses only local swaps
282
(`lattice_random_walk!`), preserving backward compatibility with existing scripts.
283
When `clusters_freq > 0`, the fixed-N branch mixes geometric cluster moves with
284
local swaps according to the `clusters_freq:swaps_freq` ratio.
285
"""
286
struct MCGrandCanonicalMoves <: MCRoutine
287
    p_move::Float64
288
    p_insert::Float64
289
    clusters_freq::Int
290
    swaps_freq::Int
291
    initial_cluster_p::Float64
292
    target_cluster_accept::Float64
293
    cluster_adjust_interval::Int
294
    cluster_p_floor::Float64
295
    cluster_p_ceiling::Float64
296
    function MCGrandCanonicalMoves(;
55✔
297
            p_move::Float64=0.5,
298
            p_insert::Float64=0.25,
299
            clusters_freq::Int=0,
300
            swaps_freq::Int=1,
301
            initial_cluster_p::Float64=0.3,
302
            target_cluster_accept::Float64=0.3,
303
            cluster_adjust_interval::Int=50,
304
            cluster_p_floor::Float64=0.01,
305
            cluster_p_ceiling::Float64=1.0)
306
        if p_move < 0.0 || p_insert < 0.0 || p_move + p_insert > 1.0
68✔
307
            throw(ArgumentError("p_move and p_insert must satisfy 0 <= p_move + p_insert <= 1"))
8✔
308
        end
309
        new(p_move, p_insert, clusters_freq, swaps_freq,
36✔
310
            initial_cluster_p, target_cluster_accept, cluster_adjust_interval,
311
            cluster_p_floor, cluster_p_ceiling)
312
    end
313
end
314

315
"""
316
    mutable struct GrandCanonicalNestedSamplingParameters <: SamplingParameters
317

318
Parameters for grand-canonical nested sampling on lattice systems.
319

320
The grand potential Ω = E − μN is used as the sorting quantity. Walkers have
321
variable particle count N, and the NS loop records (Ω, E, N) per iteration
322
for thermodynamic reweighting.
323

324
# Fields
325
- `mc_steps::Int64`: MCMC steps per replacement walker.
326
- `chemical_potential::Float64`: Chemical potential μ (unitless, in energy units of the Hamiltonian).
327
- `energy_perturbation::Float64`: Perturbation to break energy degeneracies.
328
- `random_seed::Int64`: Seed for the random number generator.
329
- `fail_count::Int64`: Consecutive failed replacements.
330
- `allowed_fail_count::Int64`: Maximum consecutive failures before warning.
331
- `init_occupation_p::Float64`: Per-site occupation probability for initial walkers.
332
- `n_max::Int64`: Upper bound on particle count per walker.
333
- `cluster_p::Float64`: Current cluster growth probability (mutable runtime state).
334
- `cluster_accepted::Float64`: Accepted cluster moves in current adjustment window.
335
- `cluster_total::Float64`: Total cluster moves attempted in current adjustment window.
336
- `cluster_p_history::Vector{Float64}`: Trajectory of cluster_p after each adjustment.
337
- `cluster_accept_history::Vector{Float64}`: Acceptance rate at each adjustment.
338
- `cluster_adjust_iterations::Vector{Int}`: NS iteration index at each adjustment.
339
"""
340
mutable struct GrandCanonicalNestedSamplingParameters <: SamplingParameters
341
    mc_steps::Int64
36✔
342
    chemical_potential::Float64
343
    energy_perturbation::Float64
344
    random_seed::Int64
345
    fail_count::Int64
346
    allowed_fail_count::Int64
347
    init_occupation_p::Float64
348
    n_max::Int64
349
    cluster_p::Float64
350
    cluster_accepted::Float64
351
    cluster_total::Float64
352
    cluster_p_history::Vector{Float64}
353
    cluster_accept_history::Vector{Float64}
354
    cluster_adjust_iterations::Vector{Int}
355
end
356

357
"""
358
    GrandCanonicalNestedSamplingParameters(;
359
        mc_steps=100, chemical_potential=0.0, energy_perturbation=1e-12,
360
        random_seed=1234, fail_count=0, allowed_fail_count=10,
361
        init_occupation_p=0.5, n_max=typemax(Int64),
362
        cluster_p=0.3, cluster_accepted=0.0, cluster_total=0.0,
363
        cluster_p_history=Float64[], cluster_accept_history=Float64[],
364
        cluster_adjust_iterations=Int[])
365

366
Convenience constructor for `GrandCanonicalNestedSamplingParameters`.
367

368
The `n_max` parameter sets an upper bound on the number of particles per walker.
369
Insertions are rejected when N ≥ n_max. Default is `typemax(Int64)` (no cap).
370

371
The `cluster_*` fields are mutable runtime state for adaptive cluster move tuning.
372
They are initialized from the static configuration on `MCGrandCanonicalMoves` at
373
the start of `grand_canonical_nested_sampling` when `clusters_freq > 0`.
374
"""
375
function GrandCanonicalNestedSamplingParameters(;
45✔
376
    mc_steps::Int64=100,
377
    chemical_potential::Float64=0.0,
378
    energy_perturbation::Float64=1e-12,
379
    random_seed::Int64=1234,
380
    fail_count::Int64=0,
381
    allowed_fail_count::Int64=10,
382
    init_occupation_p::Float64=0.5,
383
    n_max::Int64=typemax(Int64),
384
    cluster_p::Float64=0.3,
385
    cluster_accepted::Float64=0.0,
386
    cluster_total::Float64=0.0,
387
    cluster_p_history::Vector{Float64}=Float64[],
388
    cluster_accept_history::Vector{Float64}=Float64[],
389
    cluster_adjust_iterations::Vector{Int}=Int[],
390
)
391
    GrandCanonicalNestedSamplingParameters(
36✔
392
        mc_steps, chemical_potential, energy_perturbation,
393
        random_seed, fail_count, allowed_fail_count,
394
        init_occupation_p, n_max,
395
        cluster_p, cluster_accepted, cluster_total,
396
        cluster_p_history, cluster_accept_history, cluster_adjust_iterations,
397
    )
398
end
399

400
"""
401
    sort_by_energy!(liveset::LJAtomWalkers)
402

403
Sorts the walkers in the liveset by their energy in descending order.
404

405
# Arguments
406
- `liveset::LJAtomWalkers`: The liveset of walkers to be sorted.
407

408
# Returns
409
- `liveset::LJAtomWalkers`: The sorted liveset.
410
"""
411
function sort_by_energy!(liveset::AbstractLiveSet)
420✔
412
    sort!(liveset.walkers, by = x -> x.energy, rev=true)
2,195✔
413
    # println("after sort ats[1].system_data.energy: ", ats[1].system_data.energy)
414
    return liveset
420✔
415
end
416

417
"""
418
    update_iter!(liveset::AtomWalkers)
419

420
Update the iteration count for each walker in the liveset.
421

422
# Arguments
423
- `liveset::AtomWalkers`: The set of walkers to update.
424

425
"""
426
function update_iter!(liveset::AbstractLiveSet)
15,030✔
427
    for at in liveset.walkers
15,031✔
428
        at.iter += 1
1,421,827✔
429
    end
1,421,827✔
430
end
431

432
"""
433
    estimate_temperature(n_walker::Int, n_cull::Int, ediff::Float64)
434
Estimate the temperature for the nested sampling algorithm from dlog(ω)/dE.
435
"""
436
function estimate_temperature(n_walkers::Int, n_cull::Int, ediff::Float64, iter::Int=1)
×
437
    ω = (n_cull / (n_walkers + n_cull)) * (n_walkers / (n_walkers + n_cull))^iter
×
438
    β = log(ω) / ediff
×
439
    kb = 8.617333262145e-5 # eV/K
×
440
    T = 1 / (kb * β) # in Kelvin
×
441
    return T
×
442
end
443

444

445
"""
446
    nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutine)
447

448
Perform a single step of the nested sampling algorithm using the Monte Carlo random walk routine.
449

450
# Arguments
451
- `liveset::AtomWalkers`: The set of atom walkers.
452
- `ns_params::NestedSamplingParameters`: The parameters for nested sampling.
453
- `mc_routine::MCRoutine`: The Monte Carlo routine for generating new samples. See [`MCRoutine`](@ref).
454

455
# Returns
456
- `iter`: The iteration number after the step.
457
- `emax`: The highest energy recorded during the step.
458
- `liveset`: The updated set of atom walkers.
459
- `ns_params`: The updated nested sampling parameters.
460
"""
461
function nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutine; ns_iteration::Int=0)
224✔
462
    sort_by_energy!(liveset)
112✔
463
    ats = liveset.walkers
112✔
464
    lj = liveset.potential
112✔
465
    iter::Union{Missing,Int} = missing
112✔
466
    emax::Union{Missing,typeof(0.0u"eV")} = liveset.walkers[1].energy
112✔
467
    if mc_routine isa MCRandomWalkMaxE
112✔
468
        to_walk = deepcopy(ats[1])
192✔
469
    elseif mc_routine isa MCRandomWalkClone
16✔
470
        to_walk = deepcopy(rand(ats[2:end]))
24✔
471
    else
472
        error("Unsupported MCRoutine type: $mc_routine")
4✔
473
    end
474
    if length(mc_routine.dims) == 3
108✔
475
        accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax)
100✔
476
    elseif length(mc_routine.dims) == 2
8✔
477
        accept, rate, at = MC_random_walk_2D!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax; dims=mc_routine.dims)
4✔
478
        # @info "Doing a 2D random walk"
479
    elseif length(mc_routine.dims) == 1
4✔
480
        error("Unsupported dimensions: $(mc_routine.dims)")
4✔
481
    end
482
    # accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax)
483
    # @info "iter: $(liveset.walkers[1].iter), acceptance rate: $(round(rate; sigdigits=4)), emax: $(round(typeof(1.0u"eV"), emax; sigdigits=10)), is_accepted: $accept, step_size: $(round(ns_params.step_size; sigdigits=4))"
484
    if accept
104✔
485
        push!(ats, at)
97✔
486
        popfirst!(ats)
97✔
487
        update_iter!(liveset)
97✔
488
        ns_params.fail_count = 0
97✔
489
        iter = liveset.walkers[1].iter
97✔
490
    else
491
        # @warn "Failed to accept MC move"
492
        emax = missing
7✔
493
        ns_params.fail_count += 1
7✔
494
    end
495
    adjust_step_size(ns_params, rate)
171✔
496
    return iter, emax, liveset, ns_params
104✔
497
end
498

499
"""
500
    nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutineParallel)
501
Perform a single step of the nested sampling algorithm using the parallel Monte Carlo random walk routine.
502
# Arguments
503
- `liveset::AtomWalkers`: The set of atom walkers.
504
- `ns_params::NestedSamplingParameters`: The parameters for nested sampling.
505
- `mc_routine::MCRoutineParallel`: The parallel Monte Carlo routine for generating new samples. See [`MCRoutineParallel`](@ref).
506
# Returns
507
- `iter`: The iteration number after the step.
508
- `emax`: The highest energy recorded during the step.
509
- `liveset`: The updated set of atom walkers.
510
- `ns_params`: The updated nested sampling parameters.
511
"""
512
function nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCDistributed; ns_iteration::Int=0)
×
513
    sort_by_energy!(liveset)
×
514
    ats = liveset.walkers
×
515
    lj = liveset.potential
×
516
    iter::Union{Missing,Int} = missing
×
517
    emax::Union{Vector{Missing},Vector{typeof(0.0u"eV")}} = [liveset.walkers[i].energy for i in 1:nworkers()]
×
518

519
    to_walk_inds = sample(2:length(ats), nworkers(); replace=false)
×
520
    # println("to_walk_inds: ", to_walk_inds) # DEBUG
521
    
522
    to_walks = deepcopy.(ats[to_walk_inds])
×
523

524
    if length(mc_routine.dims) == 3
×
525
        random_walk_function = MC_random_walk!
×
526
    elseif length(mc_routine.dims) == 2
×
527
        random_walk_function = MC_random_walk_2D!
×
528
    else
529
        error("Unsupported dimensions: $(mc_routine.dims)")
×
530
    end
531

532
    mc_steps_per_worker = ceil(Int, ns_params.mc_steps / nworkers()) # distribute the total MC steps among workers
×
533

534
    walking = [remotecall(random_walk_function, workers()[i], mc_steps_per_worker, to_walk, lj, ns_params.step_size, emax[mc_routine.n_cull]) for (i,to_walk) in enumerate(to_walks)]
×
535
    walked = fetch.(walking)
×
536
    finalize.(walking) # finalize the remote calls, clear the memory
×
537

538
    accepted_rates = [x[2] for x in walked]
×
539
    rate = mean(accepted_rates)
×
540

541
    # sort!(walked, by = x -> x[3].energy, rev=true)
542
    # filter!(x -> x[1], walked) # remove the failed ones
543
    accepted_inds = findall(x -> x[1]==1, walked)
×
544

545
    if length(accepted_inds) < mc_routine.n_cull # if not enough accepted walkers
×
546
        ns_params.fail_count += 1
×
547
        emax = [missing]
×
548
        return iter, emax[end], liveset, ns_params
×
549
    else
550
        # pick one from the accepted ones
551
        picked = sample(accepted_inds, mc_routine.n_cull; replace=false)
×
552
        for (i, ind) in enumerate(picked)
×
553
            ats[i] = walked[ind][3]
×
554
        end
×
555
        # println("picked: ", picked) # DEBUG
556
        # remove the picked one from accepted_inds
557
        filter!(x -> x ∉ picked, accepted_inds)
×
558
        # println("remaining accepted_inds: ", accepted_inds) # DEBUG
559

560
        if !isempty(accepted_inds)
×
561
            for i in accepted_inds
×
562
                ats[to_walk_inds[i]] = walked[i][3]
×
563
                # println("Updating ats at index $(to_walk_inds[i])") # DEBUG
564
            end
×
565
        end
566
    end
567

568
    update_iter!(liveset)
×
569
    ns_params.fail_count = 0
×
570
    iter = liveset.walkers[1].iter
×
571

572
    adjust_step_size(ns_params, rate)
×
573
    return iter, emax[mc_routine.n_cull], liveset, ns_params
×
574
end
575

576
function nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutineParallel; ns_iteration::Int=0)
56✔
577
    sort_by_energy!(liveset)
28✔
578
    ats = liveset.walkers
28✔
579
    lj = liveset.potential
28✔
580
    iter::Union{Missing,Int} = missing
28✔
581
    emax::Union{Vector{Missing},Vector{typeof(0.0u"eV")}} = [liveset.walkers[i].energy for i in 1:nworkers()]
56✔
582

583
    if mc_routine isa MCRandomWalkMaxEParallel
28✔
584
        to_walk_inds = 1:nworkers()
8✔
585
    elseif mc_routine isa MCRandomWalkCloneParallel
24✔
586
        to_walk_inds = sample(2:length(ats), nworkers(); replace=false)
48✔
587
    end
588
    
589
    to_walks = deepcopy.(ats[to_walk_inds])
28✔
590

591
    if length(mc_routine.dims) == 3
28✔
592
        random_walk_function = MC_random_walk!
28✔
593
    elseif length(mc_routine.dims) == 2
×
594
        random_walk_function = MC_random_walk_2D!
×
595
    else
596
        error("Unsupported dimensions: $(mc_routine.dims)")
×
597
    end
598

599

600
    walking = [remotecall(random_walk_function, workers()[i], ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax[end]) for (i,to_walk) in enumerate(to_walks)]
28✔
601
    walked = fetch.(walking)
28✔
602
    finalize.(walking) # finalize the remote calls, clear the memory
28✔
603

604
    accepted_rates = [x[2] for x in walked]
28✔
605
    rate = mean(accepted_rates)
28✔
606

607
    if prod([x[1] for x in walked]) == 0 # if any of the walkers failed
28✔
608
        ns_params.fail_count += 1
×
609
        emax = [missing]
×
610
        return iter, emax[end], liveset, ns_params
×
611
    end
612

613
    # sort!(walked, by = x -> x[3].energy, rev=true)
614
    # filter!(x -> x[1], walked) # remove the failed ones
615

616
    for (i, at) in enumerate(walked)
28✔
617
        ats[i] = at[3]
56✔
618
    end
56✔
619

620
    update_iter!(liveset)
28✔
621
    ns_params.fail_count = 0
28✔
622
    iter = liveset.walkers[1].iter
28✔
623

624
    adjust_step_size(ns_params, rate)
35✔
625
    return iter, emax[end], liveset, ns_params
28✔
626
end
627

628
function nested_sampling_step!(liveset::LJSurfaceWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutineParallel; ns_iteration::Int=0)
16✔
629
    sort_by_energy!(liveset)
8✔
630
    ats = liveset.walkers
8✔
631
    lj = liveset.potential
8✔
632
    iter::Union{Missing,Int} = missing
8✔
633
    emax::Union{Vector{Missing},Vector{typeof(0.0u"eV")}} = [liveset.walkers[i].energy for i in 1:nworkers()]
16✔
634

635
    if mc_routine isa MCRandomWalkMaxEParallel
8✔
636
        to_walk_inds = 1:nworkers()
8✔
637
    elseif mc_routine isa MCRandomWalkCloneParallel
4✔
638
        to_walk_inds = sample(2:length(ats), nworkers(); replace=false)
8✔
639
    end
640
    
641
    to_walks = deepcopy.(ats[to_walk_inds])
8✔
642

643
    if length(mc_routine.dims) == 3
8✔
644
        random_walk_function = MC_random_walk!
8✔
645
    elseif length(mc_routine.dims) == 2
×
646
        random_walk_function = MC_random_walk_2D!
×
647
    else
648
        error("Unsupported dimensions: $(mc_routine.dims)")
×
649
    end
650

651

652
    walking = [remotecall(random_walk_function, workers()[i], ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax[end], liveset.surface) for (i,to_walk) in enumerate(to_walks)]
8✔
653
    walked = fetch.(walking)
8✔
654
    finalize.(walking) # finalize the remote calls, clear the memory
8✔
655

656
    accepted_rates = [x[2] for x in walked]
8✔
657
    rate = mean(accepted_rates)
8✔
658

659
    if prod([x[1] for x in walked]) == 0 # if any of the walkers failed
8✔
UNCOV
660
        ns_params.fail_count += 1
×
UNCOV
661
        emax = [missing]
×
UNCOV
662
        return iter, emax[end], liveset, ns_params
×
663
    end
664

665
    # sort!(walked, by = x -> x[3].energy, rev=true)
666
    # filter!(x -> x[1], walked) # remove the failed ones
667

668
    for (i, at) in enumerate(walked)
8✔
669
        ats[i] = at[3]
16✔
670
    end
16✔
671

672
    update_iter!(liveset)
8✔
673
    ns_params.fail_count = 0
8✔
674
    iter = liveset.walkers[1].iter
8✔
675

676
    adjust_step_size(ns_params, rate)
8✔
677
    return iter, emax[end], liveset, ns_params
8✔
678
end
679

680
function nested_sampling_step!(liveset::LJSurfaceWalkers, ns_params::NestedSamplingParameters, mc_routine::MCRoutine; ns_iteration::Int=0)
32✔
681
    sort_by_energy!(liveset)
16✔
682
    ats = liveset.walkers
16✔
683
    lj = liveset.potential
16✔
684
    iter::Union{Missing,Int} = missing
16✔
685
    emax::Union{Missing,typeof(0.0u"eV")} = liveset.walkers[1].energy
16✔
686
    if mc_routine isa MCRandomWalkMaxE
16✔
687
        to_walk = deepcopy(ats[1])
8✔
688
    elseif mc_routine isa MCRandomWalkClone
12✔
689
        to_walk = deepcopy(rand(ats[2:end]))
16✔
690
    else
691
        error("Unsupported MCRoutine type: $mc_routine")
4✔
692
    end
693
    if length(mc_routine.dims) == 3
12✔
694
        accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax, liveset.surface)
8✔
695
    else
696
        error("Unsupported dimensions: $(mc_routine.dims)")
4✔
697
    end
698
    # accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax)
699
    # @info "iter: $(liveset.walkers[1].iter), acceptance rate: $(round(rate; sigdigits=4)), emax: $(round(typeof(1.0u"eV"), emax; sigdigits=10)), is_accepted: $accept, step_size: $(round(ns_params.step_size; sigdigits=4))"
700
    if accept
8✔
701
        push!(ats, at)
8✔
702
        popfirst!(ats)
8✔
703
        update_iter!(liveset)
8✔
704
        ns_params.fail_count = 0
8✔
705
        iter = liveset.walkers[1].iter
8✔
706
    else
707
        # @warn "Failed to accept MC move"
708
        emax = missing
×
709
        ns_params.fail_count += 1
×
710
    end
711
    adjust_step_size(ns_params, rate)
8✔
712
    return iter, emax, liveset, ns_params
8✔
713
end
714

715
"""
716
    nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCMixedMoves)
717

718
Perform a single step of the nested sampling algorithm using the Monte Carlo mixed moves routine.
719
By default, this routine performs parallel decorrelation of multiple walkers.
720

721
Arguments
722
- `liveset::AtomWalkers`: The set of atom walkers.
723
- `ns_params::NestedSamplingParameters`: The parameters for nested sampling.
724
- `mc_routine::MCMixedMoves`: The Monte Carlo mixed moves routine.
725

726
Returns
727
- `iter`: The iteration number after the step.
728
- `emax`: The highest energy recorded during the step.
729
- `liveset`: The updated set of atom walkers.
730
- `ns_params`: The updated nested sampling parameters.
731

732
Note
733
- To invoke the parallel version of this routine, use `MCMixedMovesParallel` as the `mc_routine` argument.
734
"""
735
function nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCMixedMoves; ns_iteration::Int=0)
8✔
736
    sort_by_energy!(liveset)
4✔
737
    ats = liveset.walkers
4✔
738
    lj = liveset.potential
4✔
739
    iter::Union{Missing,Int} = missing
4✔
740
    emax::Union{Missing,typeof(0.0u"eV")} = liveset.walkers[1].energy
4✔
741
    to_walk = deepcopy(rand(ats[2:end]))
8✔
742

743
    accept, rate, at = MC_mixed_moves!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax, [mc_routine.walks_freq, mc_routine.swaps_freq])
8✔
744

745
    # accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax)
746
    # @info "iter: $(liveset.walkers[1].iter), acceptance rate: $(round(rate; sigdigits=4)), emax: $(round(typeof(1.0u"eV"), emax; sigdigits=10)), is_accepted: $accept, step_size: $(round(ns_params.step_size; sigdigits=4))"
747
    if accept
4✔
748
        push!(ats, at)
4✔
749
        popfirst!(ats)
4✔
750
        update_iter!(liveset)
4✔
751
        ns_params.fail_count = 0
4✔
752
        iter = liveset.walkers[1].iter
4✔
753
    else
754
        # @warn "Failed to accept MC move"
755
        emax = missing
×
756
        ns_params.fail_count += 1
×
757
    end
758
    adjust_step_size(ns_params, rate)
6✔
759

760
    return iter, emax, liveset, ns_params
4✔
761
end
762

763
function nested_sampling_step!(liveset::AtomWalkers, ns_params::NestedSamplingParameters, mc_routine::MCMixedMovesParallel; ns_iteration::Int=0)
×
764
    sort_by_energy!(liveset)
×
765
    ats = liveset.walkers
×
766
    lj = liveset.potential
×
767
    iter::Union{Missing,Int} = missing
×
768
    emax::Union{Vector{Missing},Vector{typeof(0.0u"eV")}} = [liveset.walkers[i].energy for i in 1:nworkers()]
×
769

770
    to_walk_inds = sample(2:length(ats), nworkers(); replace=false)
×
771
    # println("to_walk_inds: ", to_walk_inds) # DEBUG
772
    
773
    to_walks = deepcopy.(ats[to_walk_inds])
×
774

775
    walking = [remotecall(MC_mixed_moves!, workers()[i], ns_params.mc_steps, to_walk, lj, ns_params.step_size, emax[1], [mc_routine.walks_freq, mc_routine.swaps_freq]) for (i,to_walk) in enumerate(to_walks)]
×
776
    walked = fetch.(walking)
×
777
    finalize.(walking) # finalize the remote calls, clear the memory
×
778

779
    accepted_rates = [x[2] for x in walked]
×
780
    rate = mean(accepted_rates)
×
781

782
    # sort!(walked, by = x -> x[3].energy, rev=true)
783
    # filter!(x -> x[1], walked) # remove the failed ones
784
    accepted_inds = findall(x -> x[1]==1, walked)
×
785

786
    if length(accepted_inds) == 0 # if all of the walkers failed
×
787
        ns_params.fail_count += 1
×
788
        emax = [missing]
×
789
        return iter, emax[end], liveset, ns_params
×
790
    else
791
        # pick one from the accepted ones
792
        picked = rand(accepted_inds)
×
793
        ats[1] = walked[picked][3]
×
794
        # println("picked: ", picked) # DEBUG
795
        # remove the picked one from accepted_inds
796
        filter!(x -> x != picked, accepted_inds)
×
797
        # println("remaining accepted_inds: ", accepted_inds) # DEBUG
798

799
        if !isempty(accepted_inds)
×
800
            for i in accepted_inds
×
801
                ats[to_walk_inds[i]] = walked[i][3]
×
802
                # println("Updating ats at index $(to_walk_inds[i])") # DEBUG
803
            end
×
804
        end
805
    end
806

807
    update_iter!(liveset)
×
808
    ns_params.fail_count = 0
×
809
    iter = liveset.walkers[1].iter
×
810

811
    adjust_step_size(ns_params, rate)
×
812
    return iter, emax[1], liveset, ns_params
×
813
end
814

815
"""
816
    nested_sampling_step!(liveset::LatticeGasWalkers, ns_params::NestedSamplingParameters, mc_routine::MCMixedMoves)
817

818
Perform a single step of the nested sampling algorithm using a mix of geometric cluster moves and local swap moves.
819

820
The total `mc_steps` from `ns_params` are split between cluster moves and local swaps according to `clusters_freq` and
821
`walks_freq` in the `mc_routine`. Cluster moves use `geometric_cluster_swap!` with growth probability `ns_params.cluster_p`,
822
which is adaptively tuned to maintain `mc_routine.target_cluster_accept`. Local swaps use the standard `lattice_random_walk!`.
823

824
## Arguments
825
- `liveset::LatticeGasWalkers`: The liveset of lattice gas walkers.
826
- `ns_params::NestedSamplingParameters`: The parameters for nested sampling.
827
- `mc_routine::MCMixedMoves`: The mixed moves routine with cluster and local swap frequencies.
828

829
## Returns
830
- `iter`: The iteration number of the liveset after the step.
831
- `emax`: The maximum energy of the liveset after the step.
832
- `liveset::LatticeGasWalkers`: The updated liveset.
833
- `ns_params::NestedSamplingParameters`: The updated parameters.
834
"""
835
function nested_sampling_step!(liveset::LatticeGasWalkers,
264✔
836
                               ns_params::NestedSamplingParameters,
837
                               mc_routine::MCMixedMoves;
838
                               ns_iteration::Int=0)
839
    sort_by_energy!(liveset)
132✔
840
    ats = liveset.walkers
132✔
841
    h = liveset.hamiltonian
132✔
842
    iter::Union{Missing,Int} = missing
132✔
843
    emax::Union{Missing,Float64} = liveset.walkers[1].energy.val
132✔
844

845
    # Clone a random non-worst walker
846
    to_walk = deepcopy(rand(ats[2:end]))
264✔
847

848
    # Compute move counts from frequencies
849
    total_freq = mc_routine.walks_freq + mc_routine.clusters_freq
132✔
850
    n_local = round(Int, ns_params.mc_steps * mc_routine.walks_freq / max(total_freq, 1))
132✔
851
    n_cluster = ns_params.mc_steps - n_local
132✔
852

853
    # Apply cluster moves
854
    cluster_accepted = false
132✔
855
    cluster_rate = 0.0
132✔
856
    if n_cluster > 0
132✔
857
        cluster_accepted, cluster_rate, to_walk = MC_cluster_walk!(
132✔
858
            n_cluster, to_walk, h, emax, ns_params.cluster_p;
859
            energy_perturb=ns_params.energy_perturbation)
860
    end
861

862
    # Apply local swap moves
863
    local_accepted = false
132✔
864
    local_rate = 0.0
132✔
865
    if n_local > 0
132✔
866
        local_accepted, local_rate, to_walk = MC_random_walk!(
52✔
867
            n_local, to_walk, h, emax;
868
            energy_perturb=ns_params.energy_perturbation)
869
    end
870

871
    accept = cluster_accepted || local_accepted
132✔
872
    if accept
132✔
873
        push!(ats, to_walk)
128✔
874
        popfirst!(ats)
128✔
875
        update_iter!(liveset)
128✔
876
        ns_params.fail_count = 0
128✔
877
        iter = liveset.walkers[1].iter
128✔
878
    else
879
        emax = missing
4✔
880
        ns_params.fail_count += 1
4✔
881
    end
882

883
    # Accumulate cluster acceptance stats for adaptive tuning
884
    if n_cluster > 0
132✔
885
        ns_params.cluster_accepted += cluster_rate * n_cluster
132✔
886
        ns_params.cluster_total += n_cluster
132✔
887
        if mc_routine.cluster_adjust_interval > 0 &&
132✔
888
           ns_params.cluster_total >= mc_routine.cluster_adjust_interval * n_cluster
889
            window_rate = ns_params.cluster_accepted / max(ns_params.cluster_total, 1.0)
52✔
890
            adjust_cluster_p(ns_params, window_rate, ns_iteration;
52✔
891
                             target=mc_routine.target_cluster_accept,
892
                             floor=mc_routine.cluster_p_floor,
893
                             ceiling=mc_routine.cluster_p_ceiling)
894
            ns_params.cluster_accepted = 0.0
52✔
895
            ns_params.cluster_total = 0.0
52✔
896
        end
897
    end
898

899
    return iter, emax * unit(liveset.walkers[1].energy), liveset, ns_params
132✔
900
end
901

902
"""
903
    nested_sampling_step!(liveset::LatticeGasWalkers, ns_params::LatticeNestedSamplingParameters, mc_routine::MCRoutine)
904

905
Perform a single step of the nested sampling algorithm.
906

907
This function takes a `liveset` of lattice gas walkers, `ns_params` containing the parameters for nested sampling, and `mc_routine` representing the Monte Carlo
908
routine for generating new samples. It performs a single step of the nested sampling algorithm by updating the liveset with a new walker.
909

910
## Arguments
911
- `liveset::LatticeGasWalkers`: The liveset of lattice gas walkers.
912
- `ns_params::LatticeNestedSamplingParameters`: The parameters for nested sampling.
913
- `mc_routine::MCRoutine`: The Monte Carlo routine for generating new samples.
914

915
## Returns
916
- `iter`: The iteration number of the liveset after the step.
917
- `emax`: The maximum energy of the liveset after the step.
918
"""
919
function nested_sampling_step!(liveset::LatticeGasWalkers,
192✔
920
                               ns_params::NestedSamplingParameters,
921
                               mc_routine::MCRoutine;
922
                               ns_iteration::Int=0)
923
    sort_by_energy!(liveset)
96✔
924
    ats = liveset.walkers
96✔
925
    h = liveset.hamiltonian
96✔
926
    iter::Union{Missing,Int} = missing
96✔
927
    emax::Union{Missing,Float64} = liveset.walkers[1].energy.val
96✔
928
    if mc_routine isa MCRandomWalkMaxE
96✔
929
        to_walk = deepcopy(ats[1])
184✔
930
    elseif mc_routine isa MCRandomWalkClone
4✔
931
        to_walk = deepcopy(rand(ats[2:end]))
8✔
932
    else
933
        error("Unsupported MCRoutine type: $mc_routine")
×
934
    end
935
    accept, rate, at = MC_random_walk!(ns_params.mc_steps, to_walk, h, emax; energy_perturb=ns_params.energy_perturbation)
96✔
936

937
    # @info "iter: $(liveset.walkers[1].iter), acceptance rate: $rate, emax: $emax, is_accepted: $accept"
938
    if accept
96✔
939
        push!(ats, at)
89✔
940
        popfirst!(ats)
89✔
941
        update_iter!(liveset)
89✔
942
        ns_params.fail_count = 0
89✔
943
        iter = liveset.walkers[1].iter
89✔
944
    else
945
        # @warn "Failed to accept MC move"
946
        emax = missing
7✔
947
        ns_params.fail_count += 1
7✔
948
    end
949
    # adjust_step_size(ns_params, rate)
950
    return iter, emax * unit(liveset.walkers[1].energy), liveset, ns_params
96✔
951
end
952

953
"""
954
    nested_sampling_step!(liveset::LatticeGasWalkers, ns_params::LatticeNestedSamplingParameters, mc_routine::MCNewSample)
955

956
Perform a single step of the nested sampling algorithm.
957

958
This function takes a `liveset` of lattice gas walkers, `ns_params` containing the parameters for nested sampling, and `mc_routine` representing the Monte Carlo routine for generating new samples. It performs a single step of the nested sampling algorithm by updating the liveset with a new walker.
959

960
## Arguments
961
- `liveset::LatticeGasWalkers`: The liveset of lattice gas walkers.
962
- `ns_params::LatticeNestedSamplingParameters`: The parameters for nested sampling.
963
- `mc_routine::MCNewSample`: The Monte Carlo routine for generating new samples.
964

965
## Returns
966
- `iter`: The iteration number of the liveset after the step.
967
- `emax`: The maximum energy of the liveset after the step.
968
- `liveset::LatticeGasWalkers`: The updated liveset after the step.
969
- `ns_params::LatticeNestedSamplingParameters`: The updated nested sampling parameters after the step.
970
"""
971
function nested_sampling_step!(liveset::LatticeGasWalkers,
8✔
972
                               ns_params::NestedSamplingParameters,
973
                               mc_routine::MCNewSample;
974
                               ns_iteration::Int=0)
975
    sort_by_energy!(liveset)
4✔
976
    ats = liveset.walkers
4✔
977
    h = liveset.hamiltonian
4✔
978
    iter::Union{Missing,Int} = missing
4✔
979
    emax::Union{Missing,Float64} = liveset.walkers[1].energy.val
4✔
980

981
    to_walk = deepcopy(ats[1])
8✔
982

983
    accept, at = MC_new_sample!(to_walk, h, emax; energy_perturb=ns_params.energy_perturbation)
4✔
984

985
    # @info "iter: $(liveset.walkers[1].iter), emax: $emax, is_accepted: $accept"
986
    if accept
4✔
987
        push!(ats, at)
1✔
988
        popfirst!(ats)
1✔
989
        update_iter!(liveset)
1✔
990
        ns_params.fail_count = 0
1✔
991
        iter = liveset.walkers[1].iter
1✔
992
    else
993
        # @warn "Failed to accept MC move"
994
        emax = missing
3✔
995
        ns_params.fail_count += 1
3✔
996
    end
997
    # adjust_step_size(ns_params, rate)
998
    return iter, emax * unit(liveset.walkers[1].energy), liveset, ns_params
4✔
999
end
1000

1001

1002
function nested_sampling_step!(liveset::LatticeGasWalkers,
8✔
1003
                               ns_params::NestedSamplingParameters,
1004
                               mc_routine::MCRejectionSampling;
1005
                               ns_iteration::Int=0)
1006
    sort_by_energy!(liveset)
4✔
1007
    ats = liveset.walkers
4✔
1008
    h = liveset.hamiltonian
4✔
1009
    iter::Union{Missing,Int} = missing
4✔
1010
    emax::Union{Missing,Float64} = liveset.walkers[1].energy.val
4✔
1011

1012
    to_walk = deepcopy(ats[1])
8✔
1013

1014
    accept, at = MC_rejection_sampling!(to_walk, h, emax; energy_perturb=ns_params.energy_perturbation)
4✔
1015

1016
    # @info "iter: $(liveset.walkers[1].iter), emax: $emax, is_accepted: $accept"
1017
    if accept
4✔
1018
        push!(ats, at)
4✔
1019
        popfirst!(ats)
4✔
1020
        update_iter!(liveset)
4✔
1021
        ns_params.fail_count = 0
4✔
1022
        iter = liveset.walkers[1].iter
4✔
1023
    else
1024
        # @warn "Failed to accept MC move"
1025
        emax = missing
×
1026
        ns_params.fail_count += 1
×
1027
    end
1028
    # adjust_step_size(ns_params, rate)
1029
    return iter, emax * unit(liveset.walkers[1].energy), liveset, ns_params
4✔
1030
end
1031

1032

1033

1034
"""
1035
    nested_sampling(liveset::AbstractLiveSet, ns_params::NestedSamplingParameters, n_steps::Int64, mc_routine::MCRoutine; args...)
1036

1037
Perform a nested sampling loop for a given number of steps.
1038

1039
# Arguments
1040
- `liveset::AbstractLiveSet`: The initial set of walkers.
1041
- `ns_params::NestedSamplingParameters`: The parameters for nested sampling.
1042
- `n_steps::Int64`: The number of steps to perform.
1043
- `mc_routine::MCRoutine`: The Monte Carlo routine to use.
1044

1045
# Returns
1046
- `df`: A DataFrame containing the iteration number and maximum energy for each step.
1047
- `liveset`: The updated set of walkers.
1048
- `ns_params`: The updated nested sampling parameters.
1049
"""
1050
function nested_sampling(liveset::AbstractLiveSet, 
44✔
1051
                                ns_params::NestedSamplingParameters, 
1052
                                n_steps::Int64, 
1053
                                mc_routine::MCRoutine,
1054
                                save_strategy::DataSavingStrategy)
1055
    # Initialize cluster_p and reset counters from MCMixedMoves if applicable
1056
    if mc_routine isa MCMixedMoves && mc_routine.clusters_freq > 0
44✔
1057
        ns_params.cluster_p = mc_routine.initial_cluster_p
8✔
1058
        ns_params.cluster_accepted = 0.0
8✔
1059
        ns_params.cluster_total = 0.0
8✔
1060
        empty!(ns_params.cluster_p_history)
8✔
1061
        empty!(ns_params.cluster_accept_history)
8✔
1062
        empty!(ns_params.cluster_adjust_iterations)
8✔
1063
    end
1064
    df = DataFrame(iter=Int[], emax=Float64[])
44✔
1065
    for i in 1:n_steps
44✔
1066
        print_info = i % save_strategy.n_info == 0
320✔
1067
        write_walker_every_n(liveset.walkers[1], i, save_strategy)
320✔
1068
        iter, emax, liveset, ns_params = nested_sampling_step!(liveset, ns_params, mc_routine; ns_iteration=i)
335✔
1069
        @debug "n_step $i, iter: $iter, emax: $emax"
320✔
1070
        if ns_params.fail_count >= ns_params.allowed_fail_count
320✔
1071
            @warn "Failed to accept MC move $(ns_params.allowed_fail_count) times in a row. Reset step size!"
×
1072
            ns_params.fail_count = 0
×
1073
            ns_params.step_size = ns_params.initial_step_size
×
1074
        end
1075
        if !(iter isa typeof(missing))
320✔
1076
            push!(df, (iter, emax.val))
373✔
1077
        end
1078
        print_message(i, iter, emax, ns_params.step_size, print_info, liveset)
622✔
1079
        write_df_every_n(df, i, save_strategy)
320✔
1080
        write_ls_every_n(liveset, i, save_strategy)
320✔
1081
    end
596✔
1082
    return df, liveset, ns_params
44✔
1083
end
1084

1085
function print_message(i, iter, emax, step_size, print_info, liveset::LatticeWalkers)
208✔
1086
    if print_info && !(iter isa typeof(missing))
208✔
1087
        @info "iter: $(liveset.walkers[1].iter), emax: $(emax)"
38✔
1088
    elseif print_info && iter isa typeof(missing)
170✔
1089
        @info "MC move failed, step: $(i), emax: $(liveset.walkers[1].energy)"
2✔
1090
    end
1091
end
1092

1093
function print_message(i, iter, emax, step_size, print_info, liveset::AtomWalkers)
112✔
1094
    if print_info && !(iter isa typeof(missing))
112✔
1095
        @info "iter: $(liveset.walkers[1].iter), emax: $(emax.val), step_size: $(round(step_size; sigdigits=4))"
48✔
1096
    elseif print_info && iter isa typeof(missing)
64✔
1097
        @info "MC move failed, step: $(i), emax: $(liveset.walkers[1].energy.val), step_size: $(round(step_size; sigdigits=4))"
4✔
1098
    end
1099
end
1100

1101

1102
# ======================================================================
1103
# Grand-canonical nested sampling
1104
# ======================================================================
1105

1106
"""
1107
    _grand_potential(walker::LatticeWalker{1}, mu::Float64) -> typeof(0.0u"eV")
1108

1109
Compute Ω = E − μN for a single-component lattice walker.
1110
"""
1111
function _grand_potential(walker::LatticeWalker{1}, mu::Float64)
19,234,812✔
1112
    n = sum(walker.configuration.components[1])
19,234,812✔
1113
    return walker.energy - mu * n * unit(walker.energy)
19,234,812✔
1114
end
1115

1116
"""
1117
    _init_gc_walkers!(liveset::LatticeGasWalkers, gc_params::GrandCanonicalNestedSamplingParameters)
1118

1119
Initialize walkers with random microstates for grand-canonical NS.
1120
Each site is occupied independently with probability `gc_params.init_occupation_p`.
1121
"""
1122
function _init_gc_walkers!(liveset::LatticeGasWalkers, gc_params::GrandCanonicalNestedSamplingParameters)
24✔
1123
    h = liveset.hamiltonian
24✔
1124
    n_max = gc_params.n_max
24✔
1125
    for walker in liveset.walkers
24✔
1126
        random_microstate!(walker.configuration; p=gc_params.init_occupation_p)
940✔
1127
        # Enforce n_max: if too many particles, randomly delete until N ≤ n_max
1128
        n_occ = sum(walker.configuration.components[1])
940✔
1129
        if n_occ > n_max
1,880✔
1130
            occupied = findall(walker.configuration.components[1])
34✔
1131
            shuffle!(occupied)
68✔
1132
            for i in 1:(n_occ - n_max)
34✔
1133
                walker.configuration.components[1][occupied[i]] = false
105✔
1134
            end
105✔
1135
        end
1136
        assign_energy!(walker, h; perturb_energy=gc_params.energy_perturbation)
940✔
1137
    end
946✔
1138
    return liveset
24✔
1139
end
1140

1141
"""
1142
    nested_sampling_step!(liveset::LatticeGasWalkers,
1143
                          gc_params::GrandCanonicalNestedSamplingParameters,
1144
                          mc_routine::MCGrandCanonicalMoves;
1145
                          ns_iteration::Int=0)
1146

1147
Perform one step of grand-canonical nested sampling.
1148

1149
Sorts walkers by Ω = E − μN, removes the worst (highest Ω), clones a parent
1150
with Ω < Ω_worst, and decorrelates the clone via grand-canonical MCMC.
1151

1152
# Returns
1153
- `iter`: Iteration number (or `missing` if the step failed).
1154
- `omega_max`: The Ω value of the removed walker (with units).
1155
- `energy`: The E value of the removed walker.
1156
- `num_particles`: The N value of the removed walker.
1157
- `liveset`: The updated liveset.
1158
- `gc_params`: The updated parameters.
1159
"""
1160
function nested_sampling_step!(liveset::LatticeGasWalkers,
48,968✔
1161
                               gc_params::GrandCanonicalNestedSamplingParameters,
1162
                               mc_routine::MCGrandCanonicalMoves;
1163
                               ns_iteration::Int=0)
1164
    ats = liveset.walkers
24,484✔
1165
    h = liveset.hamiltonian
24,484✔
1166
    mu = gc_params.chemical_potential
24,484✔
1167
    n_walkers = length(ats)
24,484✔
1168

1169
    # Sort by grand potential (descending — worst first)
1170
    sort!(ats, by = w -> _grand_potential(w, mu), rev=true)
16,854,476✔
1171

1172
    iter::Union{Missing,Int} = missing
24,484✔
1173
    worst = ats[1]
24,484✔
1174
    omega_worst = _grand_potential(worst, mu)
24,484✔
1175
    energy_worst = worst.energy
24,484✔
1176
    n_worst = sum(worst.configuration.components[1])
24,484✔
1177

1178
    # Select parent: prefer walkers strictly below omega_worst
1179
    omega_max_val = omega_worst.val  # unitless for the MC function
24,484✔
1180
    eligible = [k for k in 2:n_walkers if _grand_potential(ats[k], mu) < omega_worst]
24,484✔
1181
    if !isempty(eligible)
24,484✔
1182
        parent_idx = rand(eligible)
24,484✔
1183
    else
NEW
1184
        parent_idx = rand(2:n_walkers)
×
1185
    end
1186
    to_walk = deepcopy(ats[parent_idx])
24,484✔
1187

1188
    # Decorrelate via GC MCMC
1189
    accept, rate, to_walk, cl_accepted, cl_total = MC_grand_canonical_walk!(
24,484✔
1190
        gc_params.mc_steps, to_walk, h, omega_max_val, mu;
1191
        p_move=mc_routine.p_move, p_insert=mc_routine.p_insert,
1192
        energy_perturb=gc_params.energy_perturbation,
1193
        n_max=gc_params.n_max,
1194
        clusters_freq=mc_routine.clusters_freq,
1195
        swaps_freq=mc_routine.swaps_freq,
1196
        cluster_p=gc_params.cluster_p)
1197

1198
    if accept
24,484✔
1199
        push!(ats, to_walk)
14,639✔
1200
        popfirst!(ats)
14,639✔
1201
        update_iter!(liveset)
14,639✔
1202
        gc_params.fail_count = 0
14,639✔
1203
        iter = liveset.walkers[1].iter
14,639✔
1204
    else
1205
        omega_worst = missing
9,845✔
1206
        energy_worst = missing
9,845✔
1207
        n_worst = missing
9,845✔
1208
        gc_params.fail_count += 1
9,845✔
1209
    end
1210

1211
    # Accumulate cluster acceptance stats and adapt cluster_p
1212
    if cl_total > 0
24,484✔
1213
        gc_params.cluster_accepted += cl_accepted
12,200✔
1214
        gc_params.cluster_total += cl_total
12,200✔
1215
        if mc_routine.cluster_adjust_interval > 0 &&
12,200✔
1216
           gc_params.cluster_total >= mc_routine.cluster_adjust_interval
1217
            window_rate = gc_params.cluster_accepted / max(gc_params.cluster_total, 1.0)
6,113✔
1218
            adjust_cluster_p(gc_params, window_rate, ns_iteration;
6,113✔
1219
                             target=mc_routine.target_cluster_accept,
1220
                             floor=mc_routine.cluster_p_floor,
1221
                             ceiling=mc_routine.cluster_p_ceiling)
1222
            gc_params.cluster_accepted = 0.0
6,113✔
1223
            gc_params.cluster_total = 0.0
6,113✔
1224
        end
1225
    end
1226

1227
    return iter, omega_worst, energy_worst, n_worst, liveset, gc_params
24,484✔
1228
end
1229

1230
"""
1231
    grand_canonical_nested_sampling(liveset::LatticeGasWalkers,
1232
                                    gc_params::GrandCanonicalNestedSamplingParameters,
1233
                                    n_steps::Int64,
1234
                                    mc_routine::MCGrandCanonicalMoves,
1235
                                    save_strategy::DataSavingStrategy)
1236

1237
Run the grand-canonical nested sampling loop.
1238

1239
Initializes walkers with random microstates, then iterates: remove the
1240
highest-Ω walker, record (Ω, E, N), replace with a decorrelated clone.
1241

1242
# Arguments
1243
- `liveset::LatticeGasWalkers`: The initial liveset (walkers will be re-initialized).
1244
- `gc_params::GrandCanonicalNestedSamplingParameters`: GC-NS parameters including μ.
1245
- `n_steps::Int64`: Number of NS iterations.
1246
- `mc_routine::MCGrandCanonicalMoves`: The GC move routine.
1247
- `save_strategy::DataSavingStrategy`: Strategy for periodic output.
1248

1249
# Returns
1250
- `df::DataFrame`: Columns `[:iter, :omega, :energy, :num_particles]`.
1251
- `liveset::LatticeGasWalkers`: The final liveset (surviving walkers).
1252
- `gc_params::GrandCanonicalNestedSamplingParameters`: Updated parameters.
1253
"""
1254
function grand_canonical_nested_sampling(liveset::LatticeGasWalkers,
20✔
1255
                                         gc_params::GrandCanonicalNestedSamplingParameters,
1256
                                         n_steps::Int64,
1257
                                         mc_routine::MCGrandCanonicalMoves,
1258
                                         save_strategy::DataSavingStrategy)
1259
    # Initialize walkers with random microstates
1260
    _init_gc_walkers!(liveset, gc_params)
20✔
1261

1262
    # Initialize cluster_p and reset counters from MCGrandCanonicalMoves if applicable
1263
    if mc_routine.clusters_freq > 0
20✔
1264
        gc_params.cluster_p = mc_routine.initial_cluster_p
8✔
1265
        gc_params.cluster_accepted = 0.0
8✔
1266
        gc_params.cluster_total = 0.0
8✔
1267
        empty!(gc_params.cluster_p_history)
8✔
1268
        empty!(gc_params.cluster_accept_history)
8✔
1269
        empty!(gc_params.cluster_adjust_iterations)
8✔
1270
    end
1271

1272
    df = DataFrame(iter=Int[], omega=Float64[], energy=Float64[], num_particles=Int[])
20✔
1273

1274
    for i in 1:n_steps
20✔
1275
        print_info = i % save_strategy.n_info == 0
24,480✔
1276
        write_walker_every_n(liveset.walkers[1], i, save_strategy)
24,480✔
1277

1278
        iter, omega, energy, n_par, liveset, gc_params = nested_sampling_step!(
24,480✔
1279
            liveset, gc_params, mc_routine; ns_iteration=i)
1280

1281
        @debug "GC-NS step $i, iter: $iter, omega: $omega, energy: $energy, N: $n_par"
24,480✔
1282

1283
        if gc_params.fail_count >= gc_params.allowed_fail_count
24,480✔
1284
            @warn "GC-NS: Failed $(gc_params.allowed_fail_count) times in a row."
466✔
1285
            gc_params.fail_count = 0
466✔
1286
        end
1287

1288
        if !(iter isa typeof(missing))
24,480✔
1289
            push!(df, (iter, omega.val, energy.val, n_par))
18,285✔
1290
        end
1291

1292
        if print_info && !(iter isa typeof(missing))
24,480✔
NEW
1293
            @info "GC-NS iter: $(iter), Ω: $(omega), E: $(energy), N: $(n_par)"
×
1294
        elseif print_info && iter isa typeof(missing)
24,480✔
NEW
1295
            @info "GC-NS MC move failed, step: $(i)"
×
1296
        end
1297

1298
        write_df_every_n(df, i, save_strategy)
24,480✔
1299
        write_ls_every_n(liveset, i, save_strategy)
24,480✔
1300
    end
48,940✔
1301

1302
    return df, liveset, gc_params
20✔
1303
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc