pure-cpp 1.0.0
A C++ physics simulation benchmark comparing performance with Python implementations
space.cpp
Go to the documentation of this file.
1/**
2 * \file space.cpp
3 * \brief Implementation of the n-body simulation space, handling physics and
4 * collisions.
5 * \author Le Bars, Yoann
6 *
7 * This file is part of the pure C++ benchmark.
8 *
9 * This program is free software: you can redistribute it and/or modify it
10 * under the terms of the GNU General Public License as published by the Free
11 * Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17 * more details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
23#include "space.hpp"
24
25#include <omp.h>
26
27#include <Eigen/StdVector>
28#include <QDebug>
29#include <algorithm>
30#include <chrono>
31#include <cmath>
32#include <random>
33
34#include "pcg_random.hpp"
35
36// Type alias for profiling clock (used throughout the file).
37using ProfilingClock = std::chrono::high_resolution_clock;
38
39Model::Space::Space(const Configuration::SimulationConfig& config)
40 : dt_(config.max_dt),
41 previous_dt_(config.max_dt),
42 G_(config.universal_g),
43 epsilon_(config.epsilon),
44 coeffRestitution_(config.coeff_restitution),
45 coeffFriction_(config.coeff_friction),
46 coeffStaticFriction_(config.coeff_static_friction),
47 positionalCorrectionFactor_(config.positional_correction_factor),
48 linearDamping_(config.linear_damping),
49 angularDamping_(config.angular_damping),
50 diagnosticsEnabled_(config.diagnostics_enabled),
51 logFreq_(config.log_freq),
52 kdTree_(bodies_) {
53 initializeBodies(config.n_bodies, config.dens, config.seed);
54 dt_ = previous_dt_ = config.max_dt; // Set initial dt after initialisation
55}
56
57namespace {
58 // Bring Eigen types into this namespace for the PlacementGrid.
59 using Eigen::Vector3d;
60 using Eigen::Vector3i;
61
62 /**
63 * \brief A simple hash function for Eigen::Vector3i to allow its use as a
64 * key in std::unordered_map.
65 */
66 struct Vector3iHash {
67 std::size_t operator()(const Vector3i& v) const {
68 // A common way to combine hashes for vector types.
69 std::hash<int> hasher;
70 return hasher(v.x()) ^ (hasher(v.y()) << 1) ^ (hasher(v.z()) << 2);
71 }
72 };
73
74 /**
75 * \brief A spatial grid for Poisson Disk Sampling (Bridson's algorithm).
76 *
77 * This grid is used to accelerate overlap checks during Poisson Disk
78 * Sampling. Each cell stores the index of a point (or -1 if empty).
79 */
80 class PoissonDiskGrid {
81 public:
82 /// \brief The grid data: grid_[i][j][k] = point index or -1 if empty.
83 std::vector<std::vector<std::vector<int>>> grid_;
84
85 /// \brief The size of each grid cell.
86 double cell_size_;
87
88 /// \brief The dimensions of the grid (number of cells in each
89 /// dimension).
90 Vector3i grid_shape_;
91
92 /// \brief The domain dimensions (side length of the cube).
93 Vector3d dims_;
94
95 PoissonDiskGrid(double in_cell_size, const Vector3d& in_dims)
96 : cell_size_(in_cell_size), dims_(in_dims) {
97 // Calculate grid shape (number of cells in each dimension)
98 grid_shape_ = Vector3i(
99 static_cast<int>(std::ceil(in_dims.x() / in_cell_size)),
100 static_cast<int>(std::ceil(in_dims.y() / in_cell_size)),
101 static_cast<int>(std::ceil(in_dims.z() / in_cell_size)));
102
103 // Initialize grid with -1 (empty)
104 grid_.resize(grid_shape_.x());
105 for (int i = 0; i < grid_shape_.x(); ++i) {
106 grid_[i].resize(grid_shape_.y());
107 for (int j = 0; j < grid_shape_.y(); ++j) {
108 grid_[i][j].resize(grid_shape_.z(), -1);
109 }
110 }
111 }
112
113 /**
114 * \brief Gets the grid cell coordinates for a position.
115 * \param pos The 3D position.
116 * \return The cell coordinates as a Vector3i.
117 */
118 Vector3i getCellCoords(const Vector3d& pos) const {
119 // Convert position to grid coordinates (shift to positive domain)
120 const Vector3d shifted = pos + dims_ / 2.0;
121 return Vector3i(static_cast<int>(shifted.x() / cell_size_),
122 static_cast<int>(shifted.y() / cell_size_),
123 static_cast<int>(shifted.z() / cell_size_));
124 }
125
126 /**
127 * \brief Checks if a position is valid (within domain and not
128 * overlapping).
129 * \param pos The candidate position.
130 * \param min_dist_sq The minimum squared distance required.
131 * \param existing_points The vector of existing points.
132 * \return True if the position is valid.
133 */
134 bool isValid(const Vector3d& pos, double min_dist_sq,
135 const std::vector<Vector3d>& existing_points) const {
136 // Check if within domain
137 if (std::abs(pos.x()) >= dims_.x() / 2.0 ||
138 std::abs(pos.y()) >= dims_.y() / 2.0 ||
139 std::abs(pos.z()) >= dims_.z() / 2.0) {
140 return false;
141 }
142
143 // Get cell coordinates
144 const Vector3i cell = getCellCoords(pos);
145
146 // Check bounds
147 if (cell.x() < 0 || cell.x() >= grid_shape_.x() || cell.y() < 0 ||
148 cell.y() >= grid_shape_.y() || cell.z() < 0 ||
149 cell.z() >= grid_shape_.z()) {
150 return false;
151 }
152
153 // Check if cell is already occupied
154 if (grid_[cell.x()][cell.y()][cell.z()] != -1) {
155 return false;
156 }
157
158 // Check 3x3x3 neighborhood for nearby points
159 for (int i = -1; i <= 1; ++i) {
160 for (int j = -1; j <= 1; ++j) {
161 for (int k = -1; k <= 1; ++k) {
162 const int x = cell.x() + i;
163 const int y = cell.y() + j;
164 const int z = cell.z() + k;
165
166 if (x >= 0 && x < grid_shape_.x() && y >= 0 &&
167 y < grid_shape_.y() && z >= 0 &&
168 z < grid_shape_.z()) {
169 const int point_idx = grid_[x][y][z];
170 if (point_idx != -1) {
171 const double dist_sq =
172 (pos - existing_points[point_idx])
173 .squaredNorm();
174 if (dist_sq < min_dist_sq) {
175 return false;
176 }
177 }
178 }
179 }
180 }
181 }
182
183 return true;
184 }
185
186 /**
187 * \brief Adds a point to the grid.
188 * \param point_idx The index of the point in the points array.
189 * \param pos The position of the point.
190 */
191 void add(std::size_t point_idx, const Vector3d& pos) {
192 const Vector3i cell = getCellCoords(pos);
193 if (cell.x() >= 0 && cell.x() < grid_shape_.x() && cell.y() >= 0 &&
194 cell.y() < grid_shape_.y() && cell.z() >= 0 &&
195 cell.z() < grid_shape_.z()) {
196 grid_[cell.x()][cell.y()][cell.z()] =
197 static_cast<int>(point_idx);
198 }
199 }
200 };
201} // namespace
202
204 // Clean up the OpenMP locks.
205 for (std::size_t i = 0; i < graphLocks_.size(); ++i) {
206 omp_destroy_lock(&graphLocks_[i]);
207 }
208}
209
210void Model::Space::initializeBodies(std::size_t n, double dens,
211 unsigned int seed) {
212 // Validate input parameters
213 if (n == 0) {
214 throw std::invalid_argument("Number of bodies must be greater than 0");
215 }
216 if (dens <= 0.0) {
217 throw std::invalid_argument("Density must be greater than 0");
218 }
219 if (!std::isfinite(dens)) {
220 throw std::invalid_argument("Density must be a finite number");
221 }
222
223 if (seed == 0) {
224 /* Random device to create a seed. */
225 std::random_device rd;
226 seed = rd();
227 }
228 // Use the PCG generator instead of mt19937
229 Rng::Pcg32 gen(seed); // NOLINT
230 // Random values for bodies' masses.
231 std::uniform_real_distribution<> massDist(MIN_BODY_MASS, MAX_BODY_MASS);
232
233 /* Pre-generate all random properties in vectors, mirroring the
234 vectorised approach in the Python version. */
235 std::vector<double> masses(n);
236 std::vector<double> radii(n);
237 std::vector<Vector3d> velocities(n);
238 std::vector<Vector3d> angularVelocities(n);
239 std::uniform_real_distribution<> vDis(MIN_INITIAL_VELOCITY,
241 std::uniform_real_distribution<> omegaDis(-MAX_INITIAL_ANGULAR_VELOCITY,
243 for (std::size_t i = 0; i < n; ++i) {
244 masses[i] = massDist(gen);
245 radii[i] = masses[i] * dens;
246 velocities[i] = Vector3d(vDis(gen), vDis(gen), vDis(gen));
247 angularVelocities[i] =
248 Vector3d(omegaDis(gen), omegaDis(gen), omegaDis(gen));
249 }
250
251 // --- Poisson Disk Sampling for Body Placement ---
252 /* Use Poisson Disk Sampling (Bridson's algorithm) to generate
253 non-overlapping positions, matching the Python implementation.
254 This produces a more uniform and stable initial distribution. */
255 const double avgMass = (MIN_BODY_MASS + MAX_BODY_MASS) / 2.0;
256 const double avgRadius = avgMass * dens;
257 const double placementScale =
258 std::cbrt(static_cast<double>(n)) * avgRadius * PLACEMENT_SCALE_FACTOR;
259 const Vector3d dims(2.0 * placementScale, 2.0 * placementScale,
260 2.0 * placementScale);
261
262 // Find maximum radius for minimum distance calculation
263 const double maxRadius = *std::max_element(radii.begin(), radii.end());
264 const double minDist = 2.0 * maxRadius; // Minimum distance between centers
265 const double minDistSq = minDist * minDist;
266
267 // Cell size for acceleration grid
268 const double cellSize = minDist / std::sqrt(3.0);
269 PoissonDiskGrid grid(cellSize, dims);
270
271 // Generate positions using Poisson Disk Sampling
272 std::vector<Vector3d> positions;
273 positions.reserve(n);
274 std::vector<std::size_t> activeList;
275 constexpr int kRejectionSamples = 30;
276
277 // Start with a single random point
278 std::uniform_real_distribution<> initialDist(-placementScale,
279 placementScale);
280 Vector3d initialPoint(initialDist(gen), initialDist(gen), initialDist(gen));
281 positions.push_back(initialPoint);
282 grid.add(0, initialPoint);
283 activeList.push_back(0);
284
285 // Generate subsequent points
286 constexpr double TWO_PI = 2.0 * 3.14159265358979323846;
287 std::uniform_real_distribution<> angleDist(0.0, TWO_PI);
288 std::uniform_real_distribution<> radiusDist(minDist, 2.0 * minDist);
289
290 while (!activeList.empty() && positions.size() < n) {
291 // Choose a random active point
292 std::uniform_int_distribution<std::size_t> activeDist(
293 0, activeList.size() - 1);
294 const std::size_t activeIdx = activeList[activeDist(gen)];
295 const Vector3d& activePoint = positions[activeIdx];
296 bool foundCandidate = false;
297
298 // Try to generate a valid candidate around the active point
299 for (int attempt = 0; attempt < kRejectionSamples; ++attempt) {
300 // Generate random point in annulus around active point
301 // Using spherical coordinates
302 const double theta = angleDist(gen); // Azimuthal angle
303 const double phi = angleDist(gen); // Polar angle
304 const double radius = radiusDist(gen);
305
306 Vector3d offset(radius * std::sin(phi) * std::cos(theta),
307 radius * std::sin(phi) * std::sin(theta),
308 radius * std::cos(phi));
309 const Vector3d candidate = activePoint + offset;
310
311 // Check if candidate is valid (within domain and non-overlapping)
312 if (grid.isValid(candidate, minDistSq, positions)) {
313 positions.push_back(candidate);
314 grid.add(positions.size() - 1, candidate);
315 activeList.push_back(positions.size() - 1);
316 foundCandidate = true;
317 break;
318 }
319 }
320
321 // If no valid candidate found, remove from active list
322 if (!foundCandidate) {
323 activeList.erase(
324 std::remove(activeList.begin(), activeList.end(), activeIdx),
325 activeList.end());
326 }
327 }
328
329 // Handle case where not all bodies could be placed
330 const std::size_t nPlaced = positions.size();
331 if (nPlaced < n) {
332 qWarning().noquote()
333 << "Failed to generate" << n
334 << "non-overlapping positions. The density may be too high. Using"
335 << nPlaced << "bodies instead.";
336 }
337
338 // Place bodies using the generated positions
339 bodies_.reserve(nPlaced);
340 for (std::size_t i = 0; i < nPlaced; ++i) {
341 bodies_.emplaceBack(masses[i], radii[i], positions[i], velocities[i],
342 angularVelocities[i]);
343 }
344
345 // Initialise the collision graph, its locks, and the spatial grid.
346 // Use nPlaced instead of n in case not all bodies could be placed.
347 const std::size_t numBodies = nPlaced;
348 collisionGraph_.resize(numBodies);
349 graphLocks_.resize(numBodies);
350 for (std::size_t i = 0; i < numBodies; ++i) {
351 omp_init_lock(&graphLocks_[i]);
352 }
353 conflicts_.resize(numBodies);
354 /* Initialise thread-local collision pairs storage.
355 This size is fixed for the lifetime of the Space object. */
356 thread_local_collision_pairs_.resize(omp_get_max_threads());
357 thread_local_search_results_.resize(omp_get_max_threads());
358 thread_local_used_colors_.resize(omp_get_max_threads());
359 thread_local_neighbour_colours_.resize(omp_get_max_threads());
360
361 /* Pre-allocate some memory for the thread-local collision vectors to reduce
362 reallocations during the simulation loop. The exact number is a
363 heuristic. */
364 const std::size_t reserve_size = (numBodies * 10) / omp_get_max_threads();
365 for (auto& vec : thread_local_collision_pairs_) {
366 vec.reserve(reserve_size);
367 }
368 for (auto& vec : thread_local_search_results_) {
369 vec.reserve(50);
370 }
371 for (auto& vec : thread_local_neighbour_colours_) {
372 vec.reserve(50);
373 }
374
375 /* To avoid duplicating force calculation logic, we run one step of the
376 dynamics computation. This correctly calculates initial accelerations
377 and handles any initial collisions.
378 To prevent numerical explosions from bodies starting too close, we run a
379 few “settling” steps with a very small time step. This allows the system
380 to reach a more stable state before the main simulation begins. */
381 const double originalDt = dt_; // Save the real time step.
382 kdTree_.buildIndex();
383 previous_dt_ = epsilon_; // Use a tiny time step for settling.
384
385 for (int i = 0; i < SETTLING_STEPS; ++i) {
386 computeDynamics(0); // Iteration count doesn't matter for settling
387 // For settling, we force the next dt to be the same small value.
388 dt_ = previous_dt_;
389 }
390
391 dt_ = previous_dt_ = originalDt; // Restore the real time step.
392}
393
394std::tuple<Model::Vector3dVec, Model::QuaterniondVec>
396 const std::size_t numBodies = n();
397 Vector3dVec positions;
398 QuaterniondVec quaternions;
399
400 positions.reserve(numBodies);
401 quaternions.reserve(numBodies);
402
403 positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
404 quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
405
406 return {positions, quaternions};
407}
408
409void Model::Space::getDataForDisplay(Vector3dVec& out_positions,
410 QuaterniondVec& out_quaternions,
411 Vector3dVec& out_torques,
412 Vector3dVec& out_alphas) const {
413 const std::size_t numBodies = n();
414
415 out_positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
416 out_quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
417 out_torques.assign(bodies_.torque_.begin(),
418 bodies_.torque_.begin() + numBodies);
419 out_alphas.assign(bodies_.alpha_.begin(),
420 bodies_.alpha_.begin() + numBodies);
421}
422
423void Model::Space::printProfilingReport() const {
424 const double totalTime = profIntegration_ + profCollisionGraph_ +
425 profCollisionResponse_ + profForceCalculation_;
426
427 if (totalTime < epsilon_) {
428 qDebug().noquote() << "\n--- Profiling Report (No data) ---";
429 return;
430 }
431
432 const QString msg =
433 QString(
434 "\n--- Profiling Report ---\nIntegration: %1s "
435 "(%2%%)\nCollision Graph: %3s (%4%%)\nCollision Response: "
436 "%5s (%6%%)\nForce Calculation: %7s "
437 "(%8%%)\n------------------------\nTotal Instrumented Time:%9s")
438 .arg(profIntegration_, 9, 'f', 4)
439 .arg(profIntegration_ / totalTime * 100.0, 5, 'f', 1)
440 .arg(profCollisionGraph_, 9, 'f', 4)
441 .arg(profCollisionGraph_ / totalTime * 100.0, 5, 'f', 1)
442 .arg(profCollisionResponse_, 9, 'f', 4)
443 .arg(profCollisionResponse_ / totalTime * 100.0, 5, 'f', 1)
444 .arg(profForceCalculation_, 9, 'f', 4)
445 .arg(profForceCalculation_ / totalTime * 100.0, 5, 'f', 1)
446 .arg(totalTime, 9, 'f', 4);
447 qDebug().noquote() << msg;
448}
449
450std::tuple<double, double, double> Model::Space::calculateSystemEnergy() const {
451 double transKe = 0.0;
452 double rotKe = 0.0;
453 double potential = 0.0;
454 const std::size_t numBodies = n();
455 const double epsSq = epsilon_ * epsilon_;
456
457#pragma omp parallel
458 {
459 // --- Kinetic Energy Calculation (Vectorized) ---
460 // Use nowait to allow threads to proceed to potential energy
461 // immediately.
462#pragma omp for simd reduction(+ : transKe, rotKe) nowait
463 for (std::size_t i = 0; i < numBodies; ++i) {
464 // Direct SoA access avoids proxy overhead and helps
465 // auto-vectorization
466 transKe += 0.5 * bodies_.m_[i] * bodies_.v_[i].squaredNorm();
467
468 const double iInv = bodies_.iInv_[i];
469 rotKe += (iInv > 0.0)
470 ? 0.5 * (1.0 / iInv) * bodies_.omega_[i].squaredNorm()
471 : 0.0;
472 }
473
474 // --- Gravitational Potential Energy Calculation ---
475 /* This O(N^2) calculation is kept separate to allow for better parallel
476 load balancing of its triangular workload. This is a reduction over
477 `potential`. */
478#pragma omp for reduction(+ : potential) schedule(dynamic)
479 for (std::size_t i = 0; i < numBodies; ++i) {
480 const Vector3d& xi = bodies_.x_[i];
481 const double G_mi = G_ * bodies_.m_[i]; // Hoist multiplication
482 double local_potential = 0.0;
483
484 /* To avoid double-counting, we only calculate for pairs (i, j)
485 where j > i. */
486#pragma omp simd reduction(+ : local_potential)
487 for (std::size_t j = i + 1; j < numBodies; ++j) {
488 const double dSq = (xi - bodies_.x_[j]).squaredNorm();
489 local_potential += (dSq > epsSq)
490 ? (G_mi * bodies_.m_[j] / std::sqrt(dSq))
491 : 0.0;
492 }
493 potential -= local_potential;
494 }
495 }
496
497 return {transKe, rotKe, potential};
498}
499
501 /* The total correction is distributed between the two bodies based on
502 their inverse mass. */
503 const double totalInvMass =
504 ctx.b1.getInverseMass() + ctx.b2.getInverseMass();
505 if (totalInvMass < epsilon_) {
506 return;
507 }
508
509 // A small percentage of the overlap is corrected to avoid jittering.
510 constexpr double PERCENT = 0.8; // 80% correction
511 const Vector3d correction =
512 (ctx.overlap_ * PERCENT / totalInvMass) * ctx.nVec_;
513
514 if (ctx.b1.getInverseMass() > 0) { // NOLINT
515 ctx.b1.addToX(-correction * ctx.b1.getInverseMass()); // NOLINT
516 }
517 if (ctx.b2.getInverseMass() > 0) { // NOLINT
518 ctx.b2.addToX(correction * ctx.b2.getInverseMass()); // NOLINT
519 }
520}
521
523 const Vector3d& direction_vec) {
524 // Rotational inertia contribution for body 1: (r1 x n)^2 / I1
525 const double term1 = (ctx.r1Vec_.cross(direction_vec)).squaredNorm() *
526 ctx.b1.getInverseInertia(); // NOLINT
527 // Rotational inertia contribution for body 2: (r2 x n)^2 / I2
528 const double term2 = (ctx.r2Vec_.cross(direction_vec)).squaredNorm() *
529 ctx.b2.getInverseInertia(); // NOLINT
530
531 const double effectiveMassInv = ctx.b1.getInverseMass() +
532 ctx.b2.getInverseMass() + term1 +
533 term2; // NOLINT
534 return effectiveMassInv > epsilon_ ? 1.0 / effectiveMassInv : 0.0;
535}
536
537std::tuple<Model::Vector3d, double, double>
539 const double vRelNormal = ctx.vRel_.dot(ctx.nVec_);
540 if (vRelNormal > 0) {
541 // Bodies are already moving apart, no restitution needed.
542 return {Vector3d::Zero(), 0.0, 0.0};
543 }
544 const double effectiveMassN = getEffectiveMass(ctx, ctx.nVec_);
545
546 // --- Baumgarte Stabilisation ---
547 // Calculate the positional error correction impulse (bias).
548 const double bias_jn = (positionalCorrectionFactor_ / previous_dt_) *
549 std::max(0.0, ctx.overlap_ - 0.01) * effectiveMassN;
550
551 /* Impulse for restitution (velocity change), based only on the normal
552 velocity. */
553 const double restitution_jn =
554 -(1.0 + coeffRestitution_) * vRelNormal * effectiveMassN;
555
556 /* Total impulse magnitude including positional correction.
557 The physical restitution impulse cannot be attractive (negative). */
558 const double jn = std::max(0.0, restitution_jn) + bias_jn;
559
560 return {jn * ctx.nVec_, jn, bias_jn}; // NOLINT
561}
562
564 double jn,
565 const Vector3d& impulseN) {
566 /* Calculate the tangential component of the INITIAL relative velocity.
567 Since impulses are applied simultaneously, the friction impulse must
568 oppose the initial tangential velocity to ensure energy dissipation.
569 The work done is: W = impulseT · vT_initial, and we need W < 0. */
570 const Vector3d vT_initial =
571 ctx.vRel_ - ctx.vRel_.dot(ctx.nVec_) * ctx.nVec_;
572 const double vT_initialNorm = vT_initial.norm();
573
574 // If there’s no tangential velocity, there’s no friction.
575 if (vT_initialNorm < epsilon_) {
576 return Vector3d::Zero();
577 }
578
579 // Direction of tangential motion (based on initial relative velocity)
580 const Vector3d tVec = vT_initial / vT_initialNorm;
581 const double effectiveMassT = getEffectiveMass(ctx, tVec); // NOLINT
582
583 /* Calculate the relative velocity after the restitution impulse is
584 applied. This is used to determine the magnitude of friction needed.
585 */
586 const Vector3d delta_v1 = -impulseN * ctx.b1.getInverseMass();
587 const Vector3d delta_v2 = impulseN * ctx.b2.getInverseMass();
588
589 // Calculate the torques generated by the restitution impulse
590 const Vector3d torque1 = ctx.r1Vec_.cross(-impulseN);
591 const Vector3d torque2 = ctx.r2Vec_.cross(impulseN);
592
593 // Calculate the change in angular velocities
594 const Vector3d delta_omega1 = ctx.b1.getInverseInertia() * torque1;
595 const Vector3d delta_omega2 = ctx.b2.getInverseInertia() * torque2;
596
597 // Calculate the relative velocity after restitution
598 const Vector3d vRelAfterRestitution =
599 ((ctx.b2.v() + delta_v2) +
600 (ctx.b2.omega() + delta_omega2).cross(ctx.r2Vec_)) -
601 ((ctx.b1.v() + delta_v1) +
602 (ctx.b1.omega() + delta_omega1).cross(ctx.r1Vec_));
603
604 /* Project the tangential velocity after restitution onto the initial
605 tangential direction to determine the magnitude needed. */
606 const Vector3d vTAfterRestitution =
607 vRelAfterRestitution - vRelAfterRestitution.dot(ctx.nVec_) * ctx.nVec_;
608 const double vTAfterRestitutionAlongT = vTAfterRestitution.dot(tVec);
609
610 /* Calculate the magnitude of the impulse required to stop the
611 tangential motion along the initial tangential direction. */
612 const double jtRequiredMagnitude =
613 std::abs(vTAfterRestitutionAlongT) * effectiveMassT;
614
615 // --- Coulomb’s Law of Friction ---
616 /* The friction limit should be based on the physical normal force, not
617 including the positional correction bias. Calculate the physical
618 restitution impulse magnitude (without bias) for the friction limit.
619 */
620 const double vRelNormal = ctx.vRel_.dot(ctx.nVec_);
621 const double effectiveMassN = getEffectiveMass(ctx, ctx.nVec_);
622 const double restitution_jn =
623 (vRelNormal <= 0) ? std::max(0.0, -(1.0 + coeffRestitution_) *
624 vRelNormal * effectiveMassN)
625 : 0.0;
626 /* Use only the physical restitution impulse for friction limit, not the
627 bias. However, if jn_physical is very small (e.g., when vRelNormal ≈
628 0), use the provided jn as a fallback to allow friction testing in
629 cases where the normal velocity component is negligible. */
630 const double jn_physical = restitution_jn;
631 const double jn_for_friction = (jn_physical < epsilon_) ? jn : jn_physical;
632 const double staticFrictionLimit = coeffStaticFriction_ * jn_for_friction;
633
634 double jt; // The magnitude of the tangential impulse (signed).
635 if (jtRequiredMagnitude < staticFrictionLimit) {
636 /* Static friction: the impulse magnitude is exactly what's required
637 to stop the motion. The impulse must oppose the direction of
638 motion. */
639 jt = jtRequiredMagnitude;
640 } else {
641 /* Kinetic friction: the impulse magnitude is the kinetic friction
642 limit. The impulse must oppose the direction of motion.
643 Use jn_for_friction which falls back to jn if jn_physical is too
644 small. */
645 jt = coeffFriction_ * jn_for_friction;
646 }
647 /* Return the impulse. tVec points in the direction of INITIAL
648 tangential motion. jt is positive and represents the magnitude
649 needed. The impulse must oppose the initial tangential motion to
650 ensure energy dissipation. The work done by friction is: W = impulseT
651 · vT_initial. For energy dissipation, we need W < 0, so impulseT must
652 oppose vT_initial. Since tVec = vT_initial / |vT_initial|, we need
653 impulseT = -jt * tVec to ensure it opposes the initial motion
654 direction. This guarantees W = (-jt * tVec) · (|vT_initial| * tVec) =
655 -jt * |vT_initial| < 0. */
656 return -jt * tVec;
657}
658
659std::optional<Model::CollisionContext> Model::Space::createCollisionContext(
660 BodyProxy& b1, BodyProxy& b2) {
661 const Vector3d dVec = b2.x() - b1.x();
662 const double distSq = dVec.squaredNorm();
663 const double rSum = b1.r() + b2.r();
664
665 if (distSq >= rSum * rSum || distSq < epsilon_ * epsilon_) {
666 return std::nullopt;
667 }
668
669 const double dist = std::sqrt(distSq);
670 const Vector3d nVec = dVec / dist;
671 const Vector3d r1Vec = b1.r() * nVec;
672 const Vector3d r2Vec = -b2.r() * nVec;
673 const Vector3d vRel =
674 (b2.v() + b2.omega().cross(r2Vec)) - (b1.v() + b1.omega().cross(r1Vec));
675 const double overlap = (b1.r() + b2.r()) - dist;
676
677 return CollisionContext{b1, b2, nVec, r1Vec,
678 r2Vec, vRel, overlap}; // NOLINT
679}
680
682 auto initial_ctx = createCollisionContext(b1, b2);
683 if (!initial_ctx) {
684 return;
685 }
686
687 // Resolve interpenetration by moving bodies apart. This modifies body
688 // positions, which invalidates the collision context (vRel_, overlap_,
689 // r1Vec_, r2Vec_, nVec_ all depend on positions).
690 resolveInterpenetration(*initial_ctx);
691
692 // Recreate the collision context with updated positions to ensure
693 // accurate impulse calculations (nVec_, r1Vec_, r2Vec_, vRel_, overlap_).
694 auto context_opt = createCollisionContext(b1, b2);
695 if (!context_opt) {
696 return;
697 }
698 CollisionContext& ctx = *context_opt;
699
700 const auto [impulseN, jn, bias_jn] = applyRestitutionImpulse(ctx);
701
702 // The friction impulse is only applied if there is a contact impulse.
703 if (jn > 0) {
704 const Vector3d impulseT = applyFrictionImpulse(ctx, jn, impulseN);
705 const Vector3d totalImpulse = impulseN + impulseT;
706 const Vector3d torque1 = ctx.b1.applyImpulse(-totalImpulse, ctx.r1Vec_);
707 const Vector3d torque2 = ctx.b2.applyImpulse(totalImpulse, ctx.r2Vec_);
708 ctx.b1.accumulateTorque(torque1);
709 ctx.b2.accumulateTorque(torque2);
710 }
711}
712
714 const std::vector<std::vector<std::size_t>>& in_graph) {
715 // Implementation of a parallel greedy graph colouring algorithm.
716 // See space.hpp for detailed documentation and examples.
717 const std::size_t numBodies = n();
718 colors_.assign(numBodies, 0); // Reset colors to 0
719 std::fill(conflicts_.begin(), conflicts_.end(), 1);
720 bool hasConflicts = true;
721
722 while (hasConflicts) {
723 hasConflicts = false; // Assume no conflicts until one is found.
724
725#pragma omp parallel
726 {
727 const int tid = omp_get_thread_num();
728 auto& usedColors = thread_local_used_colors_[tid];
729 auto& neighbourColours = thread_local_neighbour_colours_[tid];
730
731 // Pass 1: Colour nodes in parallel based on current neighbour
732 // colours.
733#pragma omp for
734 for (std::size_t i = 0; i < numBodies; ++i) {
735 if (!conflicts_[i]) continue;
736
737 neighbourColours.clear();
738 for (const auto& neighbourIdx : in_graph[i]) {
739 int neighbor_color;
740#pragma omp atomic read
741 neighbor_color = colors_[neighbourIdx];
742 neighbourColours.push_back(neighbor_color);
743 }
744
745 // Clear usedColors vector from thread-local storage
746 std::fill(usedColors.begin(), usedColors.end(), 0);
747
748 for (const int neighbourColour : neighbourColours) {
749 // Resize if necessary to handle colors larger than
750 // initial max_color
751 if (neighbourColour >=
752 static_cast<int>(usedColors.size())) {
753 usedColors.resize(neighbourColour + 2, 0);
754 }
755 usedColors[neighbourColour] = 1;
756 }
757
758 int newColor = 0; // Find the smallest available colour.
759 while (newColor < static_cast<int>(usedColors.size()) &&
760 usedColors[newColor]) {
761 newColor++;
762 }
763 // If we've exhausted all colors in usedColors, we need a
764 // new one (this should be rare, but can happen in race
765 // conditions)
766 if (newColor >= static_cast<int>(usedColors.size())) {
767 // This color is guaranteed to be available since it's
768 // beyond all currently used colors
769 }
770
771#pragma omp atomic write
772 colors_[i] = newColor;
773 }
774
775 // Pass 2: Detect conflicts in parallel
776#pragma omp for reduction(| : hasConflicts)
777 for (std::size_t i = 0; i < numBodies; ++i) {
778 conflicts_[i] = 0;
779 for (const auto& neighbourIdx : in_graph[i]) {
780 if (colors_[i] == colors_[neighbourIdx]) {
781 conflicts_[i] = 1;
782 hasConflicts = true;
783 break; // One conflict is enough to mark this body
784 }
785 }
786 }
787 }
788 }
789}
790
791void Model::Space::logSystemEnergy(std::size_t iteration) {
792 if (!diagnosticsEnabled_ || iteration == 0 || iteration % logFreq_ != 0) {
793 return;
794 }
795
796 const auto [transKe, rotKe, potential] = calculateSystemEnergy();
797 const double totalEnergy = transKe + rotKe + potential;
798
799 // Single string so the logger does not insert spaces between tokens.
800 const QString msg = QString(
801 "\n--- Iteration %1: System Energy ---\n "
802 "Translational KE: %2\n "
803 "Rotational KE: %3\n Potential Energy: "
804 "%4\n Total Energy: "
805 "%5\n Time Step (dt): %6")
806 .arg(iteration)
807 .arg(transKe, 12, 'e', 4)
808 .arg(rotKe, 12, 'e', 4)
809 .arg(potential, 12, 'e', 4)
810 .arg(totalEnergy, 12, 'e', 4)
811 .arg(getPreviousTimeStep(), 12, 'e', 4);
812 qDebug().noquote() << msg;
813
814 // Check for energy increase, which indicates instability.
815 if (totalEnergy > lastTotalEnergy_ + epsilon_) {
816 qWarning().noquote()
817 << "\n========================================"
818 << "\n Total energy increased by "
819 << QString::asprintf("%.4e", totalEnergy - lastTotalEnergy_)
820 << "\n========================================";
821 }
822 lastTotalEnergy_ = totalEnergy;
823}
824
826 // Implementation of two-phase collision detection (broad-phase +
827 // narrow-phase). See space.hpp for detailed documentation and examples.
828 const std::size_t numBodies = n();
829 // Defensive check: Ensure the collision graph is sized correctly.
830 if (collisionGraph_.size() != numBodies) {
831 collisionGraph_.resize(numBodies);
832 }
833
834 // Rebuild the k-d tree with the new positions for broad-phase
835 // detection.
836 kdTree_.buildIndex();
837
838 // Find the maximum radius for a safe search radius.
839 // Cache it to avoid recalculating every frame (radii don't change
840 // during simulation, only positions do). Recalculate only if cache is
841 // invalid.
842 if (cached_max_radius_ == 0.0) {
843 cached_max_radius_ = 0.0;
844#pragma omp parallel for reduction(max : cached_max_radius_)
845 for (std::size_t i = 0; i < numBodies; ++i) {
846 cached_max_radius_ = std::max(cached_max_radius_, bodies_.r_[i]);
847 }
848 }
849 const double max_radius = cached_max_radius_;
850
851 // Find all potentially colliding pairs using thread-local storage.
852#pragma omp parallel
853 {
854 const int tid = omp_get_thread_num();
855
856 // Clear thread-local collision pairs for this thread efficiently
857 // within the existing parallel block.
858 thread_local_collision_pairs_[tid].clear();
859
860 // Clear previous frame's global graph data using work-sharing.
861 // The nowait clause allows threads to immediately proceed to the
862 // broad-phase search without a barrier, improving CPU utilization.
863#pragma omp for schedule(static) nowait
864 for (std::size_t i = 0; i < numBodies; ++i) {
865 collisionGraph_[i].clear();
866 }
867
868 // Use the pre-allocated thread-local results vector.
869 auto& results = thread_local_search_results_[tid];
870
871#pragma omp for schedule(dynamic)
872 for (std::size_t i = 0; i < numBodies; ++i) {
873 // Access SoA data directly to avoid BodyProxy overhead.
874 const double r_i = bodies_.r_[i];
875 const Vector3d& x_i = bodies_.x_[i];
876 const double search_radius = r_i + max_radius;
877
878 // Clear results vector for reuse (capacity is preserved).
879 results.clear();
880 kdTree_.radiusSearch(x_i.data(), search_radius, results);
881
882 // Narrow-Phase: Check actual collisions.
883 // Access SoA data directly to avoid repeated BodyProxy
884 // creation.
885 for (const auto& result : results) {
886 const std::size_t j = result.first;
887 if (i >= j) continue;
888
889 const double rSum = r_i + bodies_.r_[j];
890 const double distSq = result.second;
891 if (distSq < rSum * rSum) {
892 thread_local_collision_pairs_[tid].push_back({i, j});
893 }
894 }
895 }
896 }
897
898 // Merge thread-local results into the global graph.
899 for (const auto& localPairs : thread_local_collision_pairs_) {
900 for (const auto& pair : localPairs) {
901 collisionGraph_[pair.first].emplace_back(pair.second);
902 collisionGraph_[pair.second].emplace_back(pair.first);
903 }
904 }
905}
906
907void Model::Space::computeDynamics(std::size_t iteration) {
908 // --- 1. Diagnostics ---
909 logSystemEnergy(iteration);
910
911 // --- 2. Integration (Part 1) ---
912 /* Update positions based on the previous frame's state.
913 Capture dt_ before it may be modified by updateAdaptiveTimeStep().
914 Both integratePart1 and integratePart2 must use the same dt for the
915 Velocity Verlet scheme to be correct. */
916 const double current_dt = dt_;
917 {
918 const auto startTime = ProfilingClock::now();
919 bodies_.integratePart1(dt_, previous_dt_);
920 profIntegration_ +=
921 std::chrono::duration<double>(ProfilingClock::now() - startTime)
922 .count();
923 }
924
925 // --- 3. Collision Detection ---
926 {
927 const auto startTime = ProfilingClock::now();
928 buildCollisionGraph();
929 profCollisionGraph_ +=
930 std::chrono::duration<double>(ProfilingClock::now() - startTime)
931 .count();
932 }
933
934 // --- 4. Collision Response ---
935 {
936 const auto startTime = ProfilingClock::now();
937 resolveCollisions();
938 profCollisionResponse_ +=
939 std::chrono::duration<double>(ProfilingClock::now() - startTime)
940 .count();
941 }
942
943 // --- 5. Adaptive Time Stepping ---
944 updateAdaptiveTimeStep();
945
946 // --- 6. Force Calculation ---
947 {
948 const auto startTime = ProfilingClock::now();
949 applyGravity(current_dt, linearDamping_, angularDamping_);
950 profForceCalculation_ +=
951 std::chrono::duration<double>(ProfilingClock::now() - startTime)
952 .count();
953 }
954}
955
957 const std::size_t numBodies = n();
958 if (numBodies == 0) return;
959
960 // Colour the graph to partition collisions into independent sets.
961 colorGraph(collisionGraph_);
962 const int numColours =
963 colors_.empty() ? 0
964 : *std::max_element(colors_.begin(), colors_.end()) + 1;
965
966 // Process collisions in parallel, one colour at a time.
967#pragma omp parallel
968 {
969 for (int c = 0; c < numColours; ++c) {
970#pragma omp for schedule(dynamic)
971 for (std::size_t i = 0; i < numBodies; ++i) {
972 if (colors_[i] != c) continue;
973
974 for (const auto& j : collisionGraph_[i]) {
975 // To avoid double-processing, only handle pair (i, j) if i
976 // < j.
977 if (i < j) {
978 auto b1 = body(i);
979 auto b2 = body(j);
980 handleCollision(b1, b2);
981 }
982 }
983 }
984 }
985 }
986}
987
989 // Store the dt we just used for this frame's integration.
990 previous_dt_ = dt_;
991
992 double maxASq = 0.0;
993 double maxOmegaSq = 0.0;
994 double maxAlphaSq = 0.0;
995
996 double maxVSq = 0.0;
997 std::size_t fastestBodyIdx = 0;
998 const std::size_t numBodies = n();
999
1000#pragma omp parallel
1001 {
1002 double localMaxVSq = 0.0;
1003 std::size_t localFastestBodyIdx = 0;
1004
1005#pragma omp for reduction(max : maxASq, maxOmegaSq, maxAlphaSq) schedule(static)
1006 for (std::size_t i = 0; i < numBodies; ++i) {
1007 // Direct SoA access avoids BodyProxy overhead
1008 const double aSq = bodies_.a_[i].squaredNorm();
1009 const double vSq = bodies_.v_[i].squaredNorm();
1010 const double omegaSq = bodies_.omega_[i].squaredNorm();
1011 const double alphaSq = bodies_.alpha_[i].squaredNorm();
1012
1013 maxASq = std::max(maxASq, aSq);
1014 maxOmegaSq = std::max(maxOmegaSq, omegaSq);
1015 maxAlphaSq = std::max(maxAlphaSq, alphaSq);
1016
1017 if (vSq > localMaxVSq) {
1018 localMaxVSq = vSq;
1019 localFastestBodyIdx = i;
1020 }
1021 }
1022
1023#pragma omp critical
1024 {
1025 if (localMaxVSq > maxVSq) {
1026 maxVSq = localMaxVSq;
1027 fastestBodyIdx = localFastestBodyIdx;
1028 }
1029 }
1030 }
1031
1032 // Adapt dt for the next frame based on the current maximum
1033 // acceleration.
1034 double dtA;
1035 if (maxASq > epsilon_ * epsilon_) {
1036 dtA = std::sqrt(2.0 * TARGET_DX / std::sqrt(maxASq));
1037 } else {
1038 dtA = MAX_DT;
1039 }
1040
1041 double dtV;
1042 if (maxVSq > epsilon_ * epsilon_) {
1043 dtV = (0.5 * bodies_.r_[fastestBodyIdx]) / std::sqrt(maxVSq);
1044 } else {
1045 dtV = MAX_DT;
1046 }
1047
1048 // Rotational constraints: prevent bodies from rotating too far in a
1049 // single step. Max allowed rotation in radians (approx. 5.7 degrees),
1050 // matching Python implementation.
1051 constexpr double MAX_ANGLE = 0.1;
1052
1053 double dtOmega;
1054 if (maxOmegaSq > epsilon_ * epsilon_) {
1055 // Angular velocity constraint: omega * dt < max_angle
1056 // => dt < max_angle / sqrt(omega²)
1057 // => dt² < max_angle² / omega²
1058 dtOmega = MAX_ANGLE / std::sqrt(maxOmegaSq);
1059 } else {
1060 dtOmega = MAX_DT;
1061 }
1062
1063 double dtAlpha;
1064 if (maxAlphaSq > epsilon_ * epsilon_) {
1065 // Angular acceleration constraint: 0.5 * alpha * dt² < max_angle
1066 // => dt² < 2 * max_angle / sqrt(alpha²)
1067 // => dt < sqrt(2 * max_angle / sqrt(alpha²))
1068 dtAlpha = std::sqrt(2.0 * MAX_ANGLE / std::sqrt(maxAlphaSq));
1069 } else {
1070 dtAlpha = MAX_DT;
1071 }
1072
1073 // Combine all constraints and apply damping to smooth the change.
1074 const double targetDt = std::min({MAX_DT, dtA, dtV, dtOmega, dtAlpha});
1075 dt_ = dt_ * (1.0 - DT_DAMPING_FACTOR) + targetDt * DT_DAMPING_FACTOR;
1076}
1077
1078void Model::Space::applyGravity(double current_dt, double linear_damping,
1079 double angular_damping) {
1080 const std::size_t numBodies = n();
1081 const double epsSq = epsilon_ * epsilon_;
1082
1083#pragma omp parallel for schedule(dynamic)
1084 for (std::size_t i = 0; i < numBodies; ++i) {
1085 double fx = 0.0;
1086 double fy = 0.0;
1087 double fz = 0.0;
1088 const Vector3d& xi = bodies_.x_[i];
1089
1090 // Use SIMD reduction. We split the Vector3d into primitives
1091 // because some compilers struggle to reduce over objects.
1092#pragma omp simd reduction(+ : fx, fy, fz)
1093 for (std::size_t j = 0; j < numBodies; ++j) {
1094 const Vector3d dv = bodies_.x_[j] - xi;
1095 const double dSq = dv.squaredNorm();
1096
1097 // Branchless SIMD execution: prevent NaN/div-by-zero in inactive
1098 // lanes
1099 const double safe_dSq = (dSq > epsSq) ? dSq : 1.0;
1100 const double force_mag =
1101 (dSq > epsSq)
1102 ? (G_ * bodies_.m_[j] / (safe_dSq * std::sqrt(safe_dSq)))
1103 : 0.0;
1104
1105 fx += dv.x() * force_mag;
1106 fy += dv.y() * force_mag;
1107 fz += dv.z() * force_mag;
1108 }
1109
1110 // Direct SoA writes completely bypass proxy setters
1111 bodies_.a_[i] = Vector3d(fx, fy, fz);
1112
1113 /* Convert accumulated torque to angular acceleration and reset it.
1114 This includes torques from collision responses accumulated in
1115 handleCollision(). */
1116 bodies_.alpha_[i] = bodies_.torque_[i] * bodies_.iInv_[i];
1117 bodies_.torque_[i].setZero();
1118
1119 // --- Fused Integration Part 2 & Damping ---
1120 bodies_.omega_[i] =
1121 (bodies_.omega_[i] + 0.5 * bodies_.alpha_[i] * current_dt) *
1122 (1.0 - angular_damping);
1123 bodies_.v_[i] = (bodies_.v_[i] + 0.5 * bodies_.a_[i] * current_dt) *
1124 (1.0 - linear_damping);
1125 }
1126}
const Vector3d & v() const noexcept
Access to body velocities.
Definition: body.hpp:101
double getInverseInertia() const
Access to the inverse of the body's moment of inertia.
Definition: body.hpp:83
double getInverseMass() const
Access to the inverse of the body masses.
Definition: body.hpp:67
double r() const noexcept
Access to body radii.
Definition: body.hpp:76
const Vector3d & x() const noexcept
Access to body positions.
Definition: body.hpp:92
A proxy object that provides an AoS-like interface to a body stored in the Bodies SoA container.
Definition: body.hpp:142
void addToX(const Vector3d &correction)
Add a displacement to a position.
Definition: body.hpp:470
Vector3d & omega()
Access to body angular velocities.
Definition: body.hpp:464
void accumulateTorque(const Vector3d &torque)
Accumulates a torque vector to the body's total torque.
Definition: body.hpp:504
Vector3d applyImpulse(const Vector3d &in_J, const Vector3d &in_r_vec)
Apply an impulse to a body.
Definition: body.hpp:474
Vector3d applyFrictionImpulse(const CollisionContext &ctx, double jn, const Vector3d &impulseN)
Calculates the tangential (frictional) impulse for a collision.
Definition: space.cpp:563
~Space()
Destructor to clean up OpenMP locks.
Definition: space.cpp:203
double previous_dt_
Previous time step (in s).
Definition: space.hpp:566
void initializeBodies(std::size_t n, double dens, unsigned int seed)
Initialises the bodies in the simulation with random properties.
Definition: space.cpp:210
void resolveInterpenetration(CollisionContext &ctx)
Resolves interpenetration by moving bodies apart along the collision normal.
Definition: space.cpp:500
double getEffectiveMass(const CollisionContext &ctx, const Vector3d &direction_vec)
Calculates the effective mass of two colliding bodies in a given direction.
Definition: space.cpp:522
void logSystemEnergy(std::size_t iteration)
Logs system energy and checks for instability if diagnostics are enabled.
Definition: space.cpp:791
void resolveCollisions()
Resolves all detected collisions using graph colouring.
Definition: space.cpp:956
void colorGraph(const std::vector< std::vector< std::size_t > > &graph)
A greedy graph colouring algorithm to assign a colour (integer) to each body such that no two adjacen...
Definition: space.cpp:713
void getDataForDisplay(Vector3dVec &out_positions, QuaterniondVec &out_quaternions, Vector3dVec &out_torques, Vector3dVec &out_alphas) const
Retrieves all data required for display for all bodies.
Definition: space.cpp:409
void computeDynamics(std::size_t iteration)
Computes one full step of the simulation.
Definition: space.cpp:907
void applyGravity(double current_dt, double linear_damping, double angular_damping)
Calculates and applies gravitational forces, and completes Velocity Verlet part 2.
Definition: space.cpp:1078
Space()
Default constructor.
Definition: space.hpp:97
std::optional< CollisionContext > createCollisionContext(BodyProxy &b1, BodyProxy &b2)
Factory method to create a CollisionContext if two bodies are colliding.
Definition: space.cpp:659
double dt_
Time step (in s).
Definition: space.hpp:563
void updateAdaptiveTimeStep()
Determines the optimal time step for the next frame based on current velocities and accelerations.
Definition: space.cpp:988
std::tuple< Vector3d, double, double > applyRestitutionImpulse(CollisionContext &ctx)
Calculates and applies the normal impulse (restitution) for a collision.
Definition: space.cpp:538
void buildCollisionGraph()
Builds the collision graph using broad-phase and narrow-phase detection.
Definition: space.cpp:825
std::tuple< Vector3dVec, QuaterniondVec > getAllBodyTransforms() const
Retrieves the current positions and orientations of all bodies.
Definition: space.cpp:395
void handleCollision(BodyProxy &b1, BodyProxy &b2)
Handles collision detection and response between two bodies.
Definition: space.cpp:681
std::tuple< double, double, double > calculateSystemEnergy() const
Calculates the kinetic and potential energy of the system.
Definition: space.cpp:450
A 32-bit Permuted Congruential Generator (pcg32).
Definition: pcg_random.hpp:37
constexpr double MIN_BODY_MASS
Minimum mass for randomly generated bodies.
Definition: constants.hpp:73
constexpr double MIN_INITIAL_VELOCITY
Minimum initial velocity component for randomly generated bodies.
Definition: constants.hpp:79
constexpr double MAX_BODY_MASS
Maximum mass for randomly generated bodies.
Definition: constants.hpp:76
constexpr double TARGET_DX
Heuristic for the maximum distance a body should travel in one step, used for adaptive time stepping.
Definition: constants.hpp:39
constexpr double PLACEMENT_SCALE_FACTOR
Heuristic scaling factor for the initial placement volume of bodies.
Definition: constants.hpp:90
constexpr double MAX_INITIAL_VELOCITY
Maximum initial velocity component for randomly generated bodies.
Definition: constants.hpp:82
constexpr double DT_DAMPING_FACTOR
Damping factor for smoothing adaptive time step changes.
Definition: constants.hpp:56
constexpr double MAX_INITIAL_ANGULAR_VELOCITY
Maximum initial angular velocity component for randomly generated bodies (in rad/s).
Definition: constants.hpp:86
constexpr int SETTLING_STEPS
The number of "settling" steps to run at the start of the simulation.
Definition: constants.hpp:94
constexpr double MAX_DT
Maximum time step allowed for the simulation to ensure stability.
Definition: constants.hpp:42
A minimal C++ implementation of the PCG32 random number generator.
N-body simulation space with gravitational interaction and collision response.
A data container to hold all relevant information for a single collision event.
Definition: space.hpp:68
Vector3d vRel_
The relative velocity between the two bodies at the contact point.
Definition: space.hpp:81
BodyProxy & b1
A proxy to the first body in the collision.
Definition: space.hpp:70
BodyProxy & b2
A proxy to the second body in the collision.
Definition: space.hpp:72
Vector3d r1Vec_
The vector from the centre of body 1 to the contact point.
Definition: space.hpp:76
Vector3d r2Vec_
The vector from the centre of body 2 to the contact point.
Definition: space.hpp:78
Vector3d nVec_
The normalised vector pointing from body 1 to body 2.
Definition: space.hpp:74
double overlap_
The penetration depth of the two bodies.
Definition: space.hpp:83