numerics
Loading...
Searching...
No Matches
verlet_list.hpp
Go to the documentation of this file.
1/// @file verlet_list.hpp
2/// @brief Verlet neighbour list with skin-radius temporal caching
3///
4/// @par Algorithm (CS theory: amortized analysis via skin-radius invariant)
5///
6/// Building a cell list costs O(n + C) every timestep. For large n this
7/// is non-trivial. The Verlet list amortizes this over multiple steps:
8///
9/// build() -- O(n*k_ext) using the underlying CellList2D
10/// Store, for each particle i, all neighbours j within the
11/// *extended* cutoff r_ext = cutoff + skin. Also snapshot positions.
12///
13/// needs_rebuild(pos) -- O(n) per timestep
14/// If max |Deltar_i| > skin/2 since last build -> return true.
15/// Invariant: while max displacement < skin/2, every true neighbour
16/// within cutoff is guaranteed to still be in the cached list.
17/// (Any particle that has entered the cutoff from outside must have
18/// crossed the skin shell first, which triggers a rebuild.)
19///
20/// neighbors(i) -- O(1) per query, O(k) inner-loop iteration
21/// Returns the cached IntRange for particle i.
22/// Caller still checks |r_ij| < cutoff to skip stale far entries.
23///
24/// @par When does Verlet pay off?
25///
26/// Let tau = steps between rebuilds (determined by skin and particle speed).
27/// Cost per step with Verlet: O(n*k_ext / tau + n*k_ext)
28/// up build up force eval (same work)
29/// Cost per step without Verlet: O(n + C + n*k)
30/// up cell list rebuild each step
31///
32/// Verlet wins when tau is large (slow physics, small dt, big skin) and n
33/// is large enough that O(n+C) build cost is significant relative to
34/// O(n*k) force eval. For SPH with c0=10 m/s, dt=1 ms, h=0.025 m the
35/// skin choice ~0.5h gives tau ~= 1-3 steps -- borderline. For softer
36/// (slower) physics or larger n, the benefit is clear.
37///
38/// @tparam Scalar float or double
39#pragma once
40
41#include "cell_list.hpp"
42#include <vector>
43#include <cmath>
44#include <utility>
45
46namespace num {
47
48template<typename Scalar>
50public:
51 /// @param cutoff Interaction cutoff (e.g. 2h for SPH).
52 /// @param skin Skin thickness added to cutoff for the cached list.
53 /// Larger skin -> fewer rebuilds but more cache entries.
54 /// Typical: 0.3*cutoff (molecular dynamics) or
55 /// 0.5*cutoff (SPH, faster particle motion).
57 : cutoff_(cutoff), skin_(skin)
58 , ext_sq_((cutoff + skin) * (cutoff + skin))
59 {}
60
61 /// @brief Build the neighbour list using a pre-built CellList2D.
62 ///
63 /// The cell list must have been built with cell_size >= cutoff + skin so
64 /// that a single 3x3 query covers the full extended cutoff.
65 ///
66 /// PosAccessor: callable int -> std::pair<Scalar, Scalar>
67 template<typename PosAccessor>
69 starts_.resize(n + 1);
70 flat_.clear();
71 ref_x_.resize(n);
72 ref_y_.resize(n);
73
74 starts_[0] = 0;
75 for (int i = 0; i < n; ++i) {
76 auto [xi, yi] = get_pos(i);
77 ref_x_[i] = xi;
78 ref_y_[i] = yi;
79
80 cl.query(xi, yi, [&](int j) {
81 if (j == i) return;
82 auto [xj, yj] = get_pos(j);
83 const Scalar dx = xi - xj, dy = yi - yj;
84 if (dx * dx + dy * dy < ext_sq_)
85 flat_.push_back(j);
86 });
87
88 starts_[i + 1] = static_cast<int>(flat_.size());
89 }
90 }
91
92 /// @brief Check whether any particle has moved far enough to invalidate
93 /// the cached list. O(n), very cheap (just arithmetic, no memory
94 /// allocation).
95 ///
96 /// Returns true if the cell list and Verlet list must be rebuilt before
97 /// the next force evaluation.
98 template<typename PosAccessor>
99 bool needs_rebuild(PosAccessor&& get_pos, int n) const {
100 if (ref_x_.empty()) return true;
101 const Scalar half_skin_sq = (skin_ * Scalar(0.5)) * (skin_ * Scalar(0.5));
102 for (int i = 0; i < n; ++i) {
103 auto [xi, yi] = get_pos(i);
104 const Scalar dx = xi - ref_x_[i];
105 const Scalar dy = yi - ref_y_[i];
106 if (dx * dx + dy * dy > half_skin_sq) return true;
107 }
108 return false;
109 }
110
111 /// @brief Cached neighbours of particle i (within cutoff + skin).
112 ///
113 /// Caller should still distance-filter to the true cutoff.
114 IntRange neighbors(int i) const noexcept {
115 return { flat_.data() + starts_[i],
116 flat_.data() + starts_[i + 1] };
117 }
118
119 Scalar cutoff() const noexcept { return cutoff_; }
120 Scalar skin() const noexcept { return skin_; }
121 Scalar ext_cutoff() const noexcept { return cutoff_ + skin_; }
123 return starts_.empty() ? 0 : static_cast<int>(starts_.size()) - 1;
124 }
125
126private:
127 Scalar cutoff_, skin_, ext_sq_;
128 std::vector<int> flat_; ///< Flat neighbour storage (all particles)
129 std::vector<int> starts_; ///< starts_[i] = begin of particle i in flat_
130 std::vector<Scalar> ref_x_; ///< Positions at last build (for displacement check)
131 std::vector<Scalar> ref_y_;
132};
133
134} // namespace num
Cache-coherent 2D cell list for O(1) amortized neighbour queries.
Scalar ext_cutoff() const noexcept
void build(PosAccessor &&get_pos, int n, const CellList2D< Scalar > &cl)
Build the neighbour list using a pre-built CellList2D.
Scalar skin() const noexcept
bool needs_rebuild(PosAccessor &&get_pos, int n) const
Check whether any particle has moved far enough to invalidate the cached list. O(n),...
int n_particles() const noexcept
VerletList2D(Scalar cutoff, Scalar skin)
Scalar cutoff() const noexcept
IntRange neighbors(int i) const noexcept
Cached neighbours of particle i (within cutoff + skin).
constexpr T ipow(T x) noexcept
Compute x^N at compile time via repeated squaring.
Lightweight read-only range over a contiguous int array (C++17-safe span).
Definition cell_list.hpp:59