Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
a817027
docs: design spec for async, topology-agnostic ghost communication
RudolfWeeber Jul 26, 2026
82769fd
docs: Ph0+Ph1 implementation plan for async ghost communication
RudolfWeeber Jul 26, 2026
b723ea1
core/ghosts: add Caliper markers to CellStructure ghost entry points
RudolfWeeber Jul 27, 2026
d0b4657
testsuite: pin ghost-communication Caliper labels
RudolfWeeber Jul 27, 2026
92cfe5e
benchmarks: ghost A/B runner + machine-idle gate
RudolfWeeber Jul 27, 2026
112e755
core/ghosts: add topology-agnostic HaloPlan data model
RudolfWeeber Jul 27, 2026
da9c0d8
core/ghosts: extract reusable particle packing (no behavior change)
RudolfWeeber Jul 27, 2026
17552cc
core/ghosts: async halo_exchange engine (p2p + local, split-phase)
RudolfWeeber Jul 27, 2026
5d3c4ed
core/ghosts: enforce one-NeighborComm-per-peer precondition in halo_e…
RudolfWeeber Jul 27, 2026
0e08d3f
cell_system: RegularDecomposition emits a direct-neighbor HaloPlan
RudolfWeeber Jul 27, 2026
d1d8211
benchmarks: fix ghost_ab idle gate falsely refusing on idle machine (…
RudolfWeeber Jul 27, 2026
626180f
benchmarks: capture staggered ghost-comm baseline (LJ + P3M, 1/2/4/8 …
RudolfWeeber Jul 27, 2026
d388d60
cell_system: route regular ghost exchange through async engine (parity)
RudolfWeeber Jul 27, 2026
314e7c3
core/ghosts: pack all NeighborComm cells in one archive (fix bond-buf…
RudolfWeeber Jul 27, 2026
bfe119d
core/ghosts: document + forward-note the pack_regions shift-equality …
RudolfWeeber Jul 27, 2026
ed0bd33
cell_system: n-square and hybrid emit HaloPlan collective section
RudolfWeeber Jul 27, 2026
5e527d2
core/ghosts: document collective PARTNUM-first invariant + plan point…
RudolfWeeber Jul 27, 2026
f5474fc
core/ghosts: remove legacy staggered engine; async HaloPlan is the on…
RudolfWeeber Jul 27, 2026
9bfddfd
test: drop unused variable in HaloExchange collective test (gcc-14 -W…
RudolfWeeber Jul 27, 2026
68f5ee9
docs: Ph2 implementation plan (unified HaloPlan validator)
RudolfWeeber Jul 27, 2026
c174e65
core/ghosts: add local HaloPlan validator (coverage, neighborship-mat…
RudolfWeeber Jul 27, 2026
4f7858d
test: cover recv-outside-ghost-set and isolated shape-mismatch in Hal…
RudolfWeeber Jul 27, 2026
bf5c6b9
core/ghosts: add cross-rank HaloPlan symmetry check (all-to-all send/…
RudolfWeeber Jul 27, 2026
98d380d
cell_system: validate HaloPlan at build under ADDITIONAL_CHECKS; drop…
RudolfWeeber Jul 27, 2026
25055ba
core/ghosts: validator handles collective section; guard RATTLE; vali…
RudolfWeeber Jul 27, 2026
92c5e98
core/ghosts: fix op-sanity assert -Werror portability
RudolfWeeber Jul 27, 2026
957fbb9
core/ghosts: fix HaloPlan validator/make_halo_plan for 1-rank & small…
RudolfWeeber Jul 27, 2026
043d3d8
docs: Ph3 plan (interior/boundary tags + Euclidean-vs-MI)
RudolfWeeber Jul 27, 2026
e4377fc
cell_system: classify interior/boundary cells; validator checks consi…
RudolfWeeber Jul 27, 2026
490b838
docs: Ph4 plan (latency hiding / 8-rank mitigation, profile-then-fix)
RudolfWeeber Jul 28, 2026
2430ffe
core/ghosts: Caliper phase markers (pack/post/wait/unpack) in halo_ex…
RudolfWeeber Jul 28, 2026
322f060
core/ghosts: reuse per-neighbor exchange buffers across steps (no per…
RudolfWeeber Jul 28, 2026
3e13be3
core/ghosts: RAII ExchangeBuffers ownership (fix GHOSTTRANS_NONE leak)
RudolfWeeber Jul 28, 2026
46fa759
docs: Ph4 Task 4.4 (progressive wait_any receive-unpack)
RudolfWeeber Jul 28, 2026
584571a
core/ghosts: progressive wait_any receive-unpack in halo_exchange_finish
RudolfWeeber Jul 28, 2026
cbaaf94
core/ghosts: hoist wait_any slot map into ExchangeBuffers scratch
RudolfWeeber Jul 28, 2026
d78ca2f
core: guard inactive Caliper markers with getenv-once check
RudolfWeeber Jul 28, 2026
2b2daf9
core/caliper: guard loop annotations; document CALI_CONFIG-only activ…
RudolfWeeber Jul 28, 2026
08090fd
Merge remote-tracking branch 'origin/python' into worktree-comm
RudolfWeeber Jul 29, 2026
9820911
docs: Ph5 plan (local transfers in start; wrap-aware interior tags; r…
RudolfWeeber Jul 29, 2026
2f74f28
docs: Ph5 Task 5.4 (non-zeroing CommBuf grow, perf-mandated)
RudolfWeeber Jul 29, 2026
0636ce0
docs: Ph5 eligibility += comm.size()>1 (skip step-2 split at 1 rank)
RudolfWeeber Jul 29, 2026
3b48757
core/ghosts: run local transfers in halo_exchange_start (overlap with…
RudolfWeeber Jul 29, 2026
50c874c
cell_system: boundary tag accounts for periodic-wrap neighborships; v…
RudolfWeeber Jul 29, 2026
24fe5fd
cell_system: cell_grid==1 wrap axes force all-boundary (fix false int…
RudolfWeeber Jul 29, 2026
5149cc3
docs: append fix-round entry to task-5.2-report.md
RudolfWeeber Jul 29, 2026
438dd54
core: overlap ghost force reduction with interior-cell velocity update
RudolfWeeber Jul 29, 2026
51f629d
core: exception-safe pending ghost reduce; caliper region ordering
RudolfWeeber Jul 29, 2026
d1c4c67
core/ghosts: grow exchange buffers without zero-fill (perf: -1% memse…
RudolfWeeber Jul 29, 2026
9f9cc38
docs: task-5.4 report (DefaultInitAllocator CommBuf scheme, audit, te…
RudolfWeeber Jul 29, 2026
99efa70
docs: Ph5 Task 5.5 (runtime-conditional quat/torque ghost payload)
RudolfWeeber Jul 29, 2026
5736762
core/ghosts: send quat/torque only when orientation physics is active
RudolfWeeber Jul 29, 2026
50b93bf
core: explicit Stoner-Wohlfarth arm in orientation ghost whitelist
RudolfWeeber Jul 29, 2026
5d39db5
core: fix PR 5404 CI (AppleClang unused lambda capture; doxygen refs)
RudolfWeeber Jul 29, 2026
7ea9cf3
core/ghosts: sweep remaining unresolvable doxygen refs in HaloExchang…
RudolfWeeber Jul 29, 2026
3c62b13
core/ghosts: reduce self-copy ghost forces back to owner (fix ICC 3-r…
RudolfWeeber Jul 29, 2026
2d0fb8a
test: pin LocalComm reduce role-swap in halo_exchange (regression for…
RudolfWeeber Jul 29, 2026
e839e36
cell_system: mark plan-exported cells boundary; diagnostic halo-plan …
RudolfWeeber Jul 29, 2026
430591b
core/ghosts: validate plan symmetry at first use, not construction
RudolfWeeber Jul 29, 2026
eba737a
docs: Ph5 Task CI-r3 (plan symmetry at first use, checkpoint fix)
RudolfWeeber Jul 29, 2026
ed1c582
docs: fix last unresolvable doxygen ref (for_each_interior_particle)
RudolfWeeber Jul 29, 2026
aa8620f
core: appease clang-tidy configs (emplace_back, guarded whitelist hel…
RudolfWeeber Jul 29, 2026
aae9223
core/ghosts: null-safe buffer pointers in collective exchange (UBSan)
RudolfWeeber Jul 29, 2026
e0a05b4
rm specs/plans/ledger
RudolfWeeber Jul 30, 2026
f5d2335
Merge branch 'python' of ssh://github.com/espressomd/espresso into wo…
RudolfWeeber Jul 30, 2026
cedc374
Reduce cross-L3 traffic of per-step buffer sweeps and parallelize resort
RudolfWeeber Aug 2, 2026
4c7945a
Merge branch 'l3_fixes' into comm
RudolfWeeber Aug 2, 2026
1a56a9e
Merge branch 'comm_tmp' into worktree-comm
RudolfWeeber Aug 3, 2026
433a96a
core/ghosts: exception-safe ghost-reduce finish; plug buffer-pool leak
RudolfWeeber Aug 3, 2026
60961fc
core: share Gay-Berne activity predicate; assert torque scatter impli…
RudolfWeeber Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions maintainer/benchmarks/ghost_ab.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Usage: ghost_ab.sh <label> (run from the build dir)
# Runs lj.py and p3m.py on 1/2/4/8 ranks after checking the machine is idle.
set -euo pipefail
label="${1:?need a label, e.g. baseline or async}"
build="$(pwd)"

# --- machine-idle gate (per project constraint) ---
load1=$(awk '{print $1}' /proc/loadavg)
ncpu="$(nproc)"
others=$(who | awk -v me="$(id -un)" '$1 != me {print $1}' | sort -u | wc -l)
if (( $(echo "${load1} > 2.0" | bc -l) )) || (( others > 0 )); then
echo "REFUSING to benchmark: load1=${load1}, other users=${others}, ncpu=${ncpu}." >&2
echo "Re-run when the machine is idle." >&2
exit 3
fi

out="${build}/ghost_ab_${label}.csv"
: > "${out}"
for ranks in 1 2 4 8; do
mpiexec --bind-to core -n "${ranks}" ./pypresso ../maintainer/benchmarks/lj.py \
--particles_per_core=1000 --volume_fraction=0.50 --output="${out}"
mpiexec --bind-to core -n "${ranks}" ./pypresso ../maintainer/benchmarks/p3m.py \
--particles_per_core=1000 --volume_fraction=0.25 --output="${out}"
done
echo "wrote ${out}"
4 changes: 3 additions & 1 deletion src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ add_library(
errorhandling.cpp
forces.cpp
forces_init.cpp
ghosts.cpp
ghosts/HaloExchange.cpp
ghosts/HaloPlanValidator.cpp
ghosts/particle_packing.cpp
integrate.cpp
npt.cpp
particle_node.cpp
Expand Down
224 changes: 224 additions & 0 deletions src/core/caliper_utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright (C) 2026 The ESPResSo project
*
* This file is part of ESPResSo.
*
* ESPResSo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ESPResSo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once

/**
* @file caliper_utils.hpp
* @brief Zero-overhead Caliper guards for the inactive (no @c CALI_CONFIG)
* case.
*
* When Caliper is compiled in but @c CALI_CONFIG is not set (the common
* production case), the standard @c CALI_MARK_BEGIN/END and
* @c CALI_CXX_MARK_FUNCTION macros still pay the full Caliper entry cost
* (thread-blackboard update, siglock acquisition). On a hot path with
* many markers per step this amounts to measurable wall-clock overhead.
*
* This header provides guarded replacements:
* - @c espresso_cali_active() — checked once per process; in steady state this
* is a single byte load + branch.
* - @c ESPRESSO_CALI_MARK_FUNCTION — RAII guard equivalent to
* @c CALI_CXX_MARK_FUNCTION but zero-cost when inactive.
* - @c ESPRESSO_CALI_MARK_BEGIN(name) / @c ESPRESSO_CALI_MARK_END(name)
* - @c EspressoCaliLoop — RAII loop wrapper replacing the
* @c ESPRESSO_CALI_MARK_LOOP_BEGIN / @c ESPRESSO_CALI_MARK_LOOP_ITERATION /
* @c ESPRESSO_CALI_MARK_LOOP_END macro triplet.
*
* All macros and types are no-ops / empty when @c ESPRESSO_CALIPER is not
* defined.
*
* @par Activation path limitation
* @c espresso_cali_active() detects Caliper activation **solely** via the
* @c CALI_CONFIG environment variable. Activation through
* @c CALI_SERVICES_ENABLE, Caliper config files, or programmatic
* @c cali::ConfigManager::start() is **not** detected — hot-path regions will
* silently produce no output for those activation paths. @c CALI_CONFIG is
* the supported activation path for ESPResSo profiling (used by
* @c testsuite/python/caliper.py). This restriction is intentional: checking
* @c CALI_CONFIG once and caching the result eliminates the Caliper entry cost
* (siglock + thread-blackboard update) on every hot-path marker when profiling
* is inactive.
*/

#ifdef ESPRESSO_CALIPER

#include <caliper/cali.h>
#include <cstdlib>

/**
* @brief Return true if Caliper is configured for this process.
*
* Reads @c CALI_CONFIG from the environment exactly once (on the first call)
* and caches the result. Subsequent calls pay only one byte load + branch.
*
* @c inline ensures a single shared static across all translation units
* (C++ ODR for inline functions with static locals).
*
* @note Only @c CALI_CONFIG activation is detected; see the file-level
* documentation for the rationale and limitation.
*/
inline bool espresso_cali_active() noexcept {
static const bool active = (std::getenv("CALI_CONFIG") != nullptr);
return active;
}

/**
* @brief RAII region guard: begin on construction, end on destruction.
*
* Conditionally calls @c cali_begin_region / @c cali_end_region only when
* @c espresso_cali_active() is true. Correctly handles all exit paths.
*/
struct EspressoCaliRegion {
const char *name_;
bool active_;

EspressoCaliRegion(const char *name, bool active)
: name_(name), active_(active) {
if (active_)
cali_begin_region(name_);
}
~EspressoCaliRegion() {
if (active_)
cali_end_region(name_);
}
EspressoCaliRegion(const EspressoCaliRegion &) = delete;
EspressoCaliRegion &operator=(const EspressoCaliRegion &) = delete;
};

/**
* @brief RAII iteration annotation for one pass through a loop body.
*
* Returned by @c EspressoCaliLoop::iteration(). When active, calls
* @c cali_begin_int on the loop's iteration attribute on construction and
* @c cali_end_byid on destruction — identical to what @c cali::Loop::Iteration
* does internally. When inactive, both operations are no-ops.
*
* The object must be stored at the top of the loop body so that its lifetime
* spans the loop body:
* @code
* for (int step = 0; ...) {
* auto cali_iter = cali_loop.iteration(step);
* // ...
* } // cali_iter destroyed here, closing the iteration region
* @endcode
*/
struct EspressoCaliIteration {
cali_id_t iter_attr_;
bool active_;

EspressoCaliIteration(cali_id_t iter_attr, int iter, bool active)
: iter_attr_(iter_attr), active_(active) {
if (active_)
cali_begin_int(iter_attr_, iter);
}
~EspressoCaliIteration() {
if (active_)
cali_end(iter_attr_);
}
EspressoCaliIteration(const EspressoCaliIteration &) = delete;
EspressoCaliIteration &operator=(const EspressoCaliIteration &) = delete;
// Moveable so it can be returned from iteration() with NRVO/move.
EspressoCaliIteration(EspressoCaliIteration &&other) noexcept
: iter_attr_(other.iter_attr_), active_(other.active_) {
other.active_ = false;
}
EspressoCaliIteration &operator=(EspressoCaliIteration &&) = delete;
};

/**
* @brief RAII loop wrapper replacing the @c CALI_CXX_MARK_LOOP_BEGIN /
* @c CALI_CXX_MARK_LOOP_ITERATION / @c CALI_CXX_MARK_LOOP_END macro triplet.
*
* When @c espresso_cali_active() is true at construction time, creates the
* same @c cali.loop region and @c iteration#name attribute as the raw Caliper
* macros would. When inactive, construction, iteration(), and destruction are
* all no-ops with no Caliper calls.
*
* Usage:
* @code
* EspressoCaliLoop cali_loop("Integration loop");
* for (int step = 0; step < n_steps; ++step) {
* auto cali_iter = cali_loop.iteration(step); // RAII: closed at }
* // ... loop body ...
* }
* // cali_loop destructor ends the loop region
* @endcode
*/
struct EspressoCaliLoop {
cali::Loop *loop_;
cali_id_t iter_attr_;

explicit EspressoCaliLoop(const char *name)
: loop_(nullptr), iter_attr_(CALI_INV_ID) {
if (espresso_cali_active()) {
loop_ = new cali::Loop(name);
iter_attr_ = cali_make_loop_iteration_attribute(name);
}
}
~EspressoCaliLoop() {
if (loop_)
loop_->end();
delete loop_;
}
EspressoCaliLoop(const EspressoCaliLoop &) = delete;
EspressoCaliLoop &operator=(const EspressoCaliLoop &) = delete;

/**
* @brief Return an RAII iteration annotation for the current step.
*
* The returned @c EspressoCaliIteration must be kept alive for the duration
* of the loop body; store it in a named local variable.
*/
EspressoCaliIteration iteration(int iter) const {
return EspressoCaliIteration(iter_attr_, iter, loop_ != nullptr);
}
};

// NOLINTBEGIN(cppcoreguidelines-macro-usage)

/**
* @brief Guarded drop-in replacement for @c CALI_CXX_MARK_FUNCTION.
*
* Captures @c __func__ at the call site so that the region name is the
* enclosing function name, not the destructor name.
*/
#define ESPRESSO_CALI_MARK_FUNCTION \
EspressoCaliRegion CALI_CREATE_VAR_NAME(__espresso_cali_fn, __LINE__)( \
__func__, ::espresso_cali_active())

/**
* @brief Guarded @c CALI_MARK_BEGIN — no-op when inactive.
*/
#define ESPRESSO_CALI_MARK_BEGIN(name) \
do { \
if (::espresso_cali_active()) \
CALI_MARK_BEGIN(name); \
} while (false)

/**
* @brief Guarded @c CALI_MARK_END — no-op when inactive.
*/
#define ESPRESSO_CALI_MARK_END(name) \
do { \
if (::espresso_cali_active()) \
CALI_MARK_END(name); \
} while (false)

// NOLINTEND(cppcoreguidelines-macro-usage)

#endif // ESPRESSO_CALIPER
98 changes: 66 additions & 32 deletions src/core/cell_system/AtomDecomposition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@

#include "cell_system/Cell.hpp"

#include "ghosts/HaloPlan.hpp"
#include "ghosts/HaloPlanValidator.hpp"
#include "ghosts/mark_boundary_cells.hpp"

#include <utils/Vector.hpp>

#include <boost/mpi/collectives/all_to_all.hpp>

#include <cassert>
#include <cstddef>
#include <limits>
#include <utility>
Expand All @@ -52,45 +57,40 @@ void AtomDecomposition::configure_neighbors() {
local().m_neighbors = Neighbors<Cell *>(red_neighbors, black_neighbors);
}

GhostCommunicator AtomDecomposition::prepare_comm() {
/* no need for comm for only 1 node */
GhostComm::HaloPlan AtomDecomposition::make_halo_plan() {
using GhostComm::CollectivePattern;
using GhostComm::CollectiveSection;
using GhostComm::HaloPlan;

HaloPlan plan;
plan.comm = m_comm;

// Single rank: no communication needed; collective section is None.
if (m_comm.size() == 1) {
return GhostCommunicator{m_comm, 0};
plan.collective = CollectiveSection{CollectivePattern::None, {}};
return plan;
}

auto ghost_comm =
GhostCommunicator{m_comm, static_cast<std::size_t>(m_comm.size())};
/* every node has its dedicated comm step */
for (int n = 0; n < m_comm.size(); n++) {
ghost_comm.communications[n].part_lists.resize(1);
ghost_comm.communications[n].part_lists[0] = &(cells.at(n).particles());
ghost_comm.communications[n].node = n;
// One cell pointer per rank: cells[root] is the ParticleList for that root.
// The engine uses op.direction to pick Broadcast (Push) or ReduceSum
// (Reduce) at run time, so we store Broadcast as the canonical marker that
// this section is active. run_collective reads op.direction to decide which
// MPI collective to invoke.
std::vector<ParticleList *> cell_ptrs;
cell_ptrs.reserve(static_cast<std::size_t>(m_comm.size()));
for (int n = 0; n < m_comm.size(); ++n) {
cell_ptrs.push_back(&cells.at(static_cast<std::size_t>(n)).particles());
}

return ghost_comm;
plan.collective =
CollectiveSection{CollectivePattern::Broadcast, std::move(cell_ptrs)};
return plan;
}

void AtomDecomposition::configure_comms() {
m_exchange_ghosts_comm = prepare_comm();
m_collect_ghost_force_comm = prepare_comm();

if (m_comm.size() > 1) {
for (int n = 0; n < m_comm.size(); n++) {
/* use the prefetched send buffers. Node 0 transmits first and never
* prefetches. */
if (m_comm.rank() == 0 || m_comm.rank() != n) {
m_exchange_ghosts_comm.communications[n].type = GHOST_BCST;
} else {
m_exchange_ghosts_comm.communications[n].type =
GHOST_BCST | GHOST_PREFETCH;
}
m_collect_ghost_force_comm.communications[n].type = GHOST_RDCE;
}
/* first round: all nodes except the first one prefetch their send data */
if (m_comm.rank() != 0) {
m_exchange_ghosts_comm.communications[0].type |= GHOST_PREFETCH;
}
}
m_halo_plan = make_halo_plan();
// NOTE: validation is deferred to the constructor, AFTER mark_cells() has
// populated local_cells()/ghost_cells(). Validating here would check empty
// spans (vacuously) since mark_cells() runs later.
}

void AtomDecomposition::mark_cells() {
Expand Down Expand Up @@ -156,6 +156,40 @@ AtomDecomposition::AtomDecomposition(boost::mpi::communicator comm,
configure_neighbors();
/* fill local and ghost cell lists */
mark_cells();
/* classify local cells as interior or boundary.
*
* AtomDecomposition has no spatial locality: the single local cell
* interacts with every other rank's cell and there is no subset of
* particles whose force contributions are guaranteed to arrive before
* the velocity update. Interior is therefore always empty and all local
* cells are boundary. mark_boundary_cells() handles the ghost-neighbour
* case (multi-rank); the explicit loop below catches the single-rank case
* where there are no ghost cells and no neighbours at all.
*/
GhostComm::mark_boundary_cells(AtomDecomposition::local_cells(),
AtomDecomposition::ghost_cells());
for (Cell *c : AtomDecomposition::local_cells()) {
c->m_is_boundary = true;
}
#ifdef ESPRESSO_ADDITIONAL_CHECKS
// Validate now that local_cells()/ghost_cells() are populated by
// mark_cells().
assert(GhostComm::report_violations(
GhostComm::validate_halo_plan(m_halo_plan,
AtomDecomposition::local_cells(),
AtomDecomposition::ghost_cells()),
"AtomDecomposition"));
// NOTE: validate_halo_plan_symmetry is NOT called here.
// During checkpoint loading, decompositions are transiently rebuilt while
// maximal_cutoff is rank-divergent (ranks may have different cell grids for a
// brief window before the next consistent rebuild). The transient plan is
// never used — it is immediately replaced — so the asymmetry is harmless.
// A construction-time collective all_to_all inside a ctor is also dangerous:
// if one rank aborts the others block forever in the collective.
// Symmetry is instead validated at FIRST USE of the plan in
// halo_exchange_start (see GhostComm::halo_exchange_start in
// HaloExchange.cpp).
#endif
}

Utils::Vector3d AtomDecomposition::max_cutoff() const {
Expand Down
Loading
Loading