Skip to content

Commit dd03745

Browse files
jameskermodeclaude
andcommitted
ET path: skip neighbour search entirely for ETOneBody
ETOneBody is structurally a one-body model — its energy depends only on atom species (node_data), `site_grads` returns empty edge gradients unconditionally, and forces/virial are identically zero. Building an interaction graph for it (the `convert2et_full` onebody used rcut=3.0) ran a neighbour search whose edges were then discarded. Specialise the ETOneBodyPotential energy/forces/virial (and the StackedCalculator `_cached_*` dispatch) to build the node states directly and skip the graph entirely. node_data is rcut-independent, so the result is identical to the graph path. Also drop the now-meaningless rcut=3.0 in convert2et_full (→ 0.0, unused). Verified: test/et_models/test_et_calculators.jl passes (onebody energy + stacked energy/forces/virial unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2448a6e commit dd03745

1 file changed

Lines changed: 39 additions & 3 deletions

File tree

src/et_models/et_calculators.jl

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# See also: stackedcalc.jl for StackedCalculator (combines multiple calculators)
1515

1616
import AtomsCalculators
17-
import AtomsBase: AbstractSystem, ChemicalSpecies
17+
import AtomsBase: AbstractSystem, ChemicalSpecies, position, species
1818
import EquivariantTensors as ET
1919
using DecoratedParticles: PState
2020
using StaticArrays
@@ -699,6 +699,41 @@ function ETOneBodyPotential(model::ETOneBody, ps, st, rcut::Real)
699699
return WrappedSiteCalculator(model, ps, st, Float64(rcut))
700700
end
701701

702+
# ----------------------------------------------------------------------------
703+
# ETOneBody is a pure one-body model: energy depends only on atom species
704+
# (node_data), never on neighbours, and forces/virial are identically zero.
705+
# We therefore SKIP the interaction-graph / neighbour search entirely and build
706+
# the node states directly. `node_data` is rcut-independent (it loops over all
707+
# atoms regardless of edges), so this matches the graph path exactly — and these
708+
# methods also bypass the StackedCalculator graph cache (no graph is needed).
709+
# ----------------------------------------------------------------------------
710+
_onebody_nodes(sys::AbstractSystem) =
711+
[ PState(𝐫 = ustrip.(position(sys, i)), z = species(sys, i)) for i in 1:length(sys) ]
712+
713+
function _wrapped_energy(calc::ETOneBodyPotential, sys::AbstractSystem)
714+
Ei, _ = calc.model(_onebody_nodes(sys), calc.ps, calc.st)
715+
return sum(Ei)
716+
end
717+
_wrapped_forces(calc::ETOneBodyPotential, sys::AbstractSystem) =
718+
zeros(SVector{3, Float64}, length(sys))
719+
_wrapped_virial(calc::ETOneBodyPotential, sys::AbstractSystem) =
720+
zero(SMatrix{3, 3, Float64, 9})
721+
_wrapped_energy_forces_virial(calc::ETOneBodyPotential, sys::AbstractSystem) =
722+
(energy = _wrapped_energy(calc, sys),
723+
forces = zeros(SVector{3, Float64}, length(sys)),
724+
virial = zero(SMatrix{3, 3, Float64, 9}))
725+
726+
# StackedCalculator dispatch: onebody needs no graph, so ignore the cache.
727+
_cached_energy(c::ETOneBodyPotential, sys, gcache) = _wrapped_energy(c, sys) * u"eV"
728+
_cached_forces(c::ETOneBodyPotential, sys, gcache) = _wrapped_forces(c, sys) .* u"eV/Å"
729+
_cached_virial(c::ETOneBodyPotential, sys, gcache) = _wrapped_virial(c, sys) * u"eV"
730+
function _cached_efv(c::ETOneBodyPotential, sys, gcache)
731+
efv = _wrapped_energy_forces_virial(c, sys)
732+
return (energy = efv.energy * u"eV",
733+
forces = efv.forces .* u"eV/Å",
734+
virial = efv.virial * u"eV")
735+
end
736+
702737
# ============================================================================
703738
# ETOneBodyPotential Training Assembly (empty - no learnable parameters)
704739
# ============================================================================
@@ -802,8 +837,9 @@ function convert2et_full(model, ps, st; rng::AbstractRNG=default_rng())
802837
E0_dict = Dict(z => E0s[z.atomic_number] for z in zlist)
803838
et_onebody = one_body(E0_dict, x -> x.z)
804839
_, onebody_st = setup(rng, et_onebody)
805-
# Use minimum cutoff for graph construction (ETOneBody needs no neighbors)
806-
onebody_calc = WrappedSiteCalculator(et_onebody, nothing, onebody_st, 3.0)
840+
# ETOneBody needs no neighbour graph (energy depends only on species), so its
841+
# cutoff is irrelevant — pass 0.0 to make that explicit.
842+
onebody_calc = WrappedSiteCalculator(et_onebody, nothing, onebody_st, 0.0)
807843

808844
# 2. Convert pair potential to ETPairModel
809845
et_pair = convertpair(model)

0 commit comments

Comments
 (0)