Custom Hamiltonians
The lattice sampling stack accepts user-defined Hamiltonians through a small extension contract: interacting_energy(lattice, hamiltonian) is the only place the stack evaluates a Hamiltonian, so one method serves LatticeGasWalkers construction, all of the nested-sampling loops, and exact_enumeration, on every single-component lattice geometry. Externally defined energies on fixed site sets are standard practice, machine-learned potentials among them, and this page records what a custom type must provide.
The contract
- Subtype
ClassicalHamiltonian. Fields are yours, including caches: nothing in the stack copies or introspects the Hamiltonian. - Define one energy method returning an energy-dimensioned
Unitful.Quantity(eV-convertible; a molar energy such as kJ/mol carries a different dimension and is rejected). A method returning a plain number raises a descriptiveArgumentErrorat walker setup. The validation lives on the walker-setup surface; the raw-latticewang_landauandnvt_monte_carloentry points evaluate the Hamiltonian directly and sit outside it. - Optionally, opt into two integrations:
AbstractHamiltonians._coupling_type(::MyHam)to compose underSiteFieldLatticeHamiltonian(its absence produces a self-explanatoryArgumentError), andAbstractLiveSets._n_coupled_shells(::MyHam)to participate in the uncoupled-shell warning at setup.
A worked example, a coordination-dependent energy no shipped pair, cluster, or site-field type expresses directly:
using FreeBird
using Unitful
struct CoordinationHamiltonian <: ClassicalHamiltonian
j::Float64
end
import FreeBird.EnergyEval: interacting_energy
function interacting_energy(lattice::SLattice, h::CoordinationHamiltonian)
occ = lattice.components[1]
doubly = count(s -> occ[s] &&
count(n -> occ[n], lattice.neighbors[s][1]) == 2,
eachindex(occ))
return h.j * doubly^2 * u"eV"
endWith that in place, the type is a drop-in for the shipped Hamiltonians:
walkers = [LatticeWalker(deepcopy(lattice), energy=0.0u"eV", iter=0) for _ in 1:32]
ls = LatticeGasWalkers(walkers, CoordinationHamiltonian(0.1))
df, ls, params = grand_canonical_nested_sampling(ls, gc_params, 1000, mc_routine, save_strategy)and exact_enumeration(lattice, CoordinationHamiltonian(0.1)) enumerates its fixed-particle-number spectrum.
Caching expensive energies
The evaluation path is serial (walker setup is a plain loop, and the nested-sampling loops evaluate one proposal at a time), so a plain Dict memoization keyed by the occupation vector is safe:
struct CachedHamiltonian <: ClassicalHamiltonian
cache::Dict{Vector{Bool},typeof(1.0u"eV")}
end
function interacting_energy(lattice::SLattice, h::CachedHamiltonian)
get!(h.cache, copy(lattice.components[1])) do
expensive_energy(lattice)
end
endTwo practical notes. First, define Base.show for cache-carrying types: the default struct show prints the entire cache whenever a liveset is displayed. Second, the serial-evaluation guarantee is what makes the unsynchronized Dict safe; wrap the cache before using any future parallel evaluation path.