|
14 | 14 | # See also: stackedcalc.jl for StackedCalculator (combines multiple calculators) |
15 | 15 |
|
16 | 16 | import AtomsCalculators |
17 | | -import AtomsBase: AbstractSystem, ChemicalSpecies |
| 17 | +import AtomsBase: AbstractSystem, ChemicalSpecies, position, species |
18 | 18 | import EquivariantTensors as ET |
19 | 19 | using DecoratedParticles: PState |
20 | 20 | using StaticArrays |
@@ -699,6 +699,41 @@ function ETOneBodyPotential(model::ETOneBody, ps, st, rcut::Real) |
699 | 699 | return WrappedSiteCalculator(model, ps, st, Float64(rcut)) |
700 | 700 | end |
701 | 701 |
|
| 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 | + |
702 | 737 | # ============================================================================ |
703 | 738 | # ETOneBodyPotential Training Assembly (empty - no learnable parameters) |
704 | 739 | # ============================================================================ |
@@ -802,8 +837,9 @@ function convert2et_full(model, ps, st; rng::AbstractRNG=default_rng()) |
802 | 837 | E0_dict = Dict(z => E0s[z.atomic_number] for z in zlist) |
803 | 838 | et_onebody = one_body(E0_dict, x -> x.z) |
804 | 839 | _, 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) |
807 | 843 |
|
808 | 844 | # 2. Convert pair potential to ETPairModel |
809 | 845 | et_pair = convertpair(model) |
|
0 commit comments