Linus/variant lut

This commit is contained in:
Linus Baumberger
2026-05-20 22:55:47 +02:00
committed by [Quy Anh] «Elliot» Nguyen
parent 18b2180322
commit 7ac4ce950b
2 changed files with 592 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# Optimization: LUT for exp()
- **Author**: Linus
- **Predecessor**: variant_baseline.cpp
- **File(s)**: variants/variant_lut_exp.cpp
- **Status**: positive at small N (10-15%), tied at large N
## What this changes
Replaced the exp() calls in pedestrian_repulsion_dt and
obstacle_repulsion_dt with a precomputed lookup table over the argument
range [-40, 0]. The table stores exp at evenly spaced sample points and we
interpolate with a cubic Hermite spline between them. Below -40 the value
is below FP noise relative to the force accumulator, so we just return 0.
The thing that makes this work cleanly: for exp(x), the derivative IS the
function value (d/dx exp(x) = exp(x)). So at every sample point we know
the exact slope without having to estimate it with a centered difference.
That gives O(h^4) accuracy with just 2 LUT loads per call.
## Code (relevant snippet)
```cpp
constexpr int EXP_LUT_SIZE = 69000;
constexpr double EXP_LUT_MIN = -40.0;
constexpr double EXP_LUT_MAX = 0.0;
constexpr double EXP_LUT_INV_STEP = (EXP_LUT_SIZE - 1) / (EXP_LUT_MAX - EXP_LUT_MIN);
static double exp_lut[EXP_LUT_SIZE];
// Populated before main() via a static initializer.
struct ExpLutInit {
ExpLutInit() {
const double step = (EXP_LUT_MAX - EXP_LUT_MIN) / (EXP_LUT_SIZE - 1);
for (int i = 0; i < EXP_LUT_SIZE; i++) {
exp_lut[i] = std::exp(EXP_LUT_MIN + i * step);
}
}
};
static ExpLutInit exp_lut_init_helper;
inline double fast_exp(double x) {
if (x <= EXP_LUT_MIN) return 0.0;
if (x >= EXP_LUT_MAX) return 1.0;
double pos = (x - EXP_LUT_MIN) * EXP_LUT_INV_STEP;
int idx = static_cast<int>(pos);
if (idx >= EXP_LUT_SIZE - 1) return 1.0;
double t = pos - idx;
double y0 = exp_lut[idx];
double y1 = exp_lut[idx + 1];
// For exp, slope = value. In normalized parameter space, slope = h * y.
constexpr double h_step = (EXP_LUT_MAX - EXP_LUT_MIN) / (EXP_LUT_SIZE - 1);
double t2 = t * t;
double t3 = t2 * t;
double h00 = 2.0*t3 - 3.0*t2 + 1.0;
double h10 = t3 - 2.0*t2 + t;
double h01 = -2.0*t3 + 3.0*t2;
double h11 = t3 - t2;
return y0 * (h00 + h_step * h10) + y1 * (h01 + h_step * h11);
}
```
Then in pedestrian_repulsion_dt and obstacle_repulsion_dt, the libm
`exp(...)` is replaced with `fast_exp(...)`.
## Benchmark
Conditions: min of 3 runs, N_ITER=1000, -O3 -march=native -flto.
On my laptop (Intel ULTRA 9 185H @ 2.2 GHz, WSL Ubuntu): about
15% faster at N=50, 12% at N=100, 8% at N=200, then roughly tied at N=500
and N=1000. The shrinking speedup matches a cache story (see Analysis).
### x86
**Machine**:
```
<paste machine info here>
```
**Frequency**: <e.g., 2 GHz>
| N | W(n) [Flops] | T(n) [Cycles] | Performance [Flops/Cycle] | Diff vs Baseline |
| ---- | ------------ | ------------- | ------------------------- | ---------------- |
| 50 | | | | |
| 100 | | | | |
| 200 | | | | |
| 500 | | | | |
| 1000 | | | | |
### ARM (Apple)
**Machine**:
```
<paste machine info here>
```
**Frequency**: <e.g., 3.2 GHz>
| N | W(n) [Flops] | T(n) [Cycles] | Performance [Flops/Cycle] | Diff vs Baseline |
| ---- | ------------ | ------------- | ------------------------- | ---------------- |
| 50 | | | | |
| 100 | | | | |
| 200 | | | | |
| 500 | | | | |
| 1000 | | | | |
## Analysis
The LUT replaces a libm exp (about 30 cycles) with a table lookup plus a
small cubic polynomial (about 18 cycles). At small N the table fits in L2
and the speedup is real, around 10-15%. As N grows the pedestrian array
starts competing with the LUT for L2 cache, and the LUT lookups end
up in L3 instead, which is about as expensive as the libm exp we were
trying to replace.
About verify: the LUT introduces a small per-call error, and chaos in the
simulation amplifies it over 4000 timesteps, but still passes our strict verification
If we are more tolerant with the "error" its possible to use a much smaller LUT. Maybe we could try that out
## Takeaway
LUT for exp works at small N where the table fits in cache. At large N the
cache pressure kills the gain. A smaller LUT (or range reduction into a
tiny table) would probably do better across the whole N range without the
cache penalty.
## References
- Tang, P. T. P. (1989). "Table-driven implementation of the exponential
function in IEEE floating-point arithmetic." ACM TOMS 15(2), 144-157.
https://dl.acm.org/doi/10.1145/63522.214389
- Cubic Hermite spline. Wikipedia.
https://en.wikipedia.org/wiki/Cubic_Hermite_spline
+454
View File
@@ -0,0 +1,454 @@
// This file is copied from the variant_baseline and then modified
// The goal is that we use a look up table to precompute expensive calculations
// especially to precompute the exp calls
// we call exp(two_b * NEG_TWO_SIGMA_INV)
// which is always negativ
// if a value is smaller then -40 its so small that we can ignore it
// for the other values we will create a lookup table
#include "../headers/board.hpp"
#include "../headers/constants.hpp"
#include "../headers/pedestrian.hpp"
#include "../headers/vector2d.hpp"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <iostream>
#include <math.h> // keep — forces code uses unqualified sqrt/exp
#include <random>
#include <tuple>
#include <vector>
#define SEED 42
#define BOARD_WIDTH 10.0
#define BOARD_LENGTH 50.0
#define WALL_EPS 1e-3
#define BOARD_WIDTH_MINUS_WALL_EPS 9.999
#ifndef RANDOM_SEED
#define RANDOM_SEED 0
#endif
#ifndef BENCHMARK
#define BENCHMARK 0
#endif
using namespace std;
// exp lookup table
constexpr int EXP_LUT_SIZE = 69000;
constexpr double EXP_LUT_MIN = -40.0;
constexpr double EXP_LUT_MAX = 0.0;
constexpr double EXP_LUT_INV_STEP = (EXP_LUT_SIZE - 1) / (EXP_LUT_MAX - EXP_LUT_MIN);
static double exp_lut[EXP_LUT_SIZE];
struct ExpLutInit {
ExpLutInit() {
const double step = (EXP_LUT_MAX - EXP_LUT_MIN) / (EXP_LUT_SIZE - 1);
for (int i = 0; i < EXP_LUT_SIZE; i++) {
exp_lut[i] = std::exp(EXP_LUT_MIN + i * step);
}
}
};
static ExpLutInit exp_lut_init_helper;
// i used the following things to get a better understanding of how i can solve that problem
// Tang, P. T. P. (1989). "Table-driven implementation of the exponential function in IEEE floating-point arithmetic." ACM TOMS 15(2), 144-157.
// https://dl.acm.org/doi/10.1145/63522.214389
// Cubic Hermite spline. Wikipedia.
// https://en.wikipedia.org/wiki/Cubic_Hermite_spline
inline double fast_exp(double x) {
if (x <= EXP_LUT_MIN)
return 0.0;
if (x >= EXP_LUT_MAX)
return 1.0;
double pos = (x - EXP_LUT_MIN) * EXP_LUT_INV_STEP;
int idx = static_cast<int>(pos);
if (idx >= EXP_LUT_SIZE - 1)
return 1.0;
double t = pos - idx;
double y0 = exp_lut[idx];
double y1 = exp_lut[idx + 1];
// For exp(x), derivative equals function value.
// In normalized parameter space (t in [0,1] across width h),
// dy/dt = h * y. No centered differences needed - we know the exact slope.
constexpr double h_step = (EXP_LUT_MAX - EXP_LUT_MIN) / (EXP_LUT_SIZE - 1);
double t2 = t * t;
double t3 = t2 * t;
double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
double h10 = t3 - 2.0 * t2 + t;
double h01 = -2.0 * t3 + 3.0 * t2;
double h11 = t3 - t2;
return y0 * (h00 + h_step * h10) + y1 * (h01 + h_step * h11);
}
// forces.cpp
// Flop count: 2 ADDs, 5 MULTs, 1 SQRT
double w(Vector2d e, Vector2d f) {
if (e.dot(f) >= f.norm() * cos_phi)
return dt_neg;
else
return c_neg_times_dt;
}
// Flop count: 2 ADDs, 4 MULTs
Vector2d acceleration_term_dt(const Pedestrian &alpha) {
// # 2 MULTs
Vector2d desired_velocity = alpha.e * alpha.desired_v0;
Vector2d actual_velocity = alpha.curr_vel;
// # 2 ADDs, 2 MULTs
Vector2d f_alpha_0_dt = (desired_velocity - actual_velocity) * dt_times_tau_alpha_inv;
return f_alpha_0_dt;
}
// Flop count: 12 ADDs, 23 MULTs, 1 DIV, 4 SQRTs, 1 EXP
Vector2d pedestrian_repulsion_dt(const Pedestrian &alpha, const Pedestrian &beta) {
// # 2 ADDs
// $\vec{r}_{\alpha\beta}$
Vector2d r_ab = alpha.curr_pos - beta.curr_pos;
// # 1 ADD, 2 MULTs, 1 SQRT
// $\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert$
double r_ab_norm = r_ab.norm();
r_ab_norm = r_ab_norm > EPS ? r_ab_norm : EPS; // Avoid division by zero
// $v_\beta \Delta t \vec{e}_\beta$
Vector2d s_b_e = beta.s_b_e;
// # 2 ADDs
// $\vec{r}_{\alpha\beta} - \vec{v}_{\beta}\varDelta_{t}\vec{e}_{\beta}$
Vector2d r_ab2 = r_ab - s_b_e;
// # 1 ADD, 2 MULTs, 1 SQRT
// $\Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert$
double r_ab2_norm = r_ab2.norm();
r_ab2_norm = r_ab2_norm > EPS ? r_ab2_norm : EPS; // Avoid division by zero
// # 1 ADD
// $\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert$
double r_ab_norm_sum = r_ab_norm + r_ab2_norm;
// # 1 ADD, 1 MULTs, 1 SQRT
//
// $2b = \sqrt{(\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert)^2 - (v_\beta \Delta t)^2}$
//
double two_b_sq = r_ab_norm_sum * r_ab_norm_sum - beta.s_b_sq;
two_b_sq = two_b_sq > EPS_SQ ? two_b_sq : EPS_SQ; // Avoid sqrt of negative and avoid division by zero
double two_b = sqrt(two_b_sq);
// # 5 MULTs, 1 DIV, 1 EXP
//
// $\frac{-V^0_{\alpha\beta}}{2\sigma}\,e^{-b/\sigma}\,\frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert\,\Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}$
//
double scalar_mult_common = (r_ab_norm_sum / (two_b * r_ab_norm * r_ab2_norm)) * (NEG_V0_AB_div_TWO_SIGMA * fast_exp(two_b * NEG_TWO_SIGMA_INV));
// # 1 MULT
//
// $\frac{-V^0_{\alpha\beta}}{2\sigma}\,e^{-b/\sigma}\,\frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert}$
//
double scalar_mult_r_ab = scalar_mult_common * r_ab2_norm;
// # 1 MULT
//
// $\frac{-V^0_{\alpha\beta}}{2\sigma}\,e^{-b/\sigma}\,\frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}$
//
double scalar_mult_r_ab2 = scalar_mult_common * r_ab_norm;
// # 2 ADDs, 4 MULTs
//
// $\vec{-f}_{\alpha \beta}(\vec{r}_{\alpha \beta}) = \frac{-V^0_{\alpha \beta}}{2\sigma}e^{-b / \sigma} 2\grad_{\vec{r}_{\alpha \beta}}b $
//
//
// $= \frac{-V^0_{\alpha\beta}}{2\sigma}\,e^{-b/\sigma}\,\frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert}\,\vec{r}_{\alpha \beta} + \frac{-V^0_{\alpha\beta}}{2\sigma}\,e^{-b/\sigma}\,\frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} -
// v_\beta \Delta t \vec{e}_\beta \Arrowvert}\,(\vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta)$
//
//
//
// $2\grad_{\vec{r}_{\alpha \beta}}b = \frac{(\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert)(\frac{\vec{r}_{\alpha \beta}}{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert} + \frac{\vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta}{\Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert})}{2b}$
//
//
// $= \frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert}\,\vec{r}_{\alpha \beta} + \frac{\Arrowvert \vec{r}_{\alpha \beta} \Arrowvert + \Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}{2b\,\Arrowvert \vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta \Arrowvert}\,(\vec{r}_{\alpha \beta} - v_\beta \Delta t \vec{e}_\beta)$
//
//
Vector2d f_alpha_beta_neg = r_ab * scalar_mult_r_ab + r_ab2 * scalar_mult_r_ab2;
// # 2 ADDs, 7 MULTs, 1 SQRT
//
// $\vec{F}_{\alpha\beta}(\vec{e}_{\alpha}, \vec{r}_{\alpha \beta}).dt = w(\vec{e}_{\alpha}, -\vec{f}_{\alpha\beta}(\vec{r}_{\alpha \beta})).\vec{f}_{\alpha\beta}(\vec{r}_{\alpha \beta}).dt$
//
Vector2d f_alpha_beta_dt = f_alpha_beta_neg * w(alpha.e, f_alpha_beta_neg);
return f_alpha_beta_dt;
}
// Flop count: 10 ADDs, 18 MULTs, 1 DIVs, 2 SQRTs, 1 EXP
Vector2d obstacle_repulsion_dt(const Pedestrian &alpha, const Obstacle &B) {
// # 7 ADDs, 5 MULTs
// $\vec{r}_{\alpha B}$
Vector2d r_aB = alpha.curr_pos - B.find_nearest_obstacle_point(alpha.curr_pos);
// # 1 ADD, 2 MULTs, 1 SQRT
// $\Arrowvert \vec{r}_{\alpha B} \Arrowvert$
double r_aB_norm = r_aB.norm();
r_aB_norm = r_aB_norm > EPS ? r_aB_norm : EPS; // Avoid division by zero
// # 2 MULTs, 1 DIV, 1 EXP
//
// $\frac{-U^0_{\alpha B}}{R} e^{- \Arrowvert \vec{r}_{\alpha B} \Arrowvert / R}$
//
double scalar_mult = (NEG_U0_AB_div_R * fast_exp(r_aB_norm * NEG_R_INV)) / r_aB_norm;
// # 2 MULTs
//
// $-\vec{F}_{\alpha B}(\vec{r}_{\alpha B}) = \frac{-U^0_{\alpha B}}{R} e^{- \Arrowvert \vec{r}_{\alpha B} \Arrowvert / R} \frac{\vec{r}_{\alpha B}}{\Arrowvert \vec{r}_{\alpha B} \Arrowvert}$
//
Vector2d f_alpha_B_neg = r_aB * scalar_mult;
// # 2 ADDs, 7 MULTs, 1 SQRT
//
// $\vec{F}_{\alpha B}(\vec{e}_{\alpha}, \vec{r}_{\alpha B}).dt = w(\vec{e}_{\alpha}, -\vec{F}_{\alpha B}(\vec{r}_{\alpha B})).\vec{F}_{\alpha B}(\vec{r}_{\alpha B}).dt$
//
Vector2d f_alpha_B_dt = f_alpha_B_neg * w(alpha.e, f_alpha_B_neg);
return f_alpha_B_dt;
}
// social_force_baseline.cpp
Pedestrian *init_pedestrians(int n_p) {
#if RANDOM_SEED
random_device rd; // Platform-provided random seed source.
mt19937 gen(rd()); // Non-reproducible initialization.
#else
mt19937 gen(SEED); // Reproducible initialization.
#endif
normal_distribution<double> velocity_gen(v0_mean, v0_std_dev);
uniform_real_distribution<double> y_gen(0.3, BOARD_WIDTH - 0.3);
uniform_real_distribution<double> x_gen_left(2.0, BOARD_LENGTH / 2.0);
uniform_real_distribution<double> x_gen_right(BOARD_LENGTH / 2.0, BOARD_LENGTH - 2.0);
Pedestrian *P = new Pedestrian[n_p];
int n_p_half = n_p / 2;
// Group of pedestrians walking from left to right.
for (int i = 0; i < n_p_half; i++) {
double x = x_gen_left(gen);
double y = y_gen(gen);
Vector2d start_pos = Vector2d(x, y);
Vector2d destination = Vector2d(BOARD_LENGTH + 30.0, y);
Vector2d init_e = destination - start_pos;
double init_e_norm = init_e.norm();
Vector2d init_e_unit = init_e / init_e_norm;
// Reject near-zero desired speeds to avoid pedestrians that barely move,
// and reject negative speeds because they would reverse the intended
// walking direction.
double desired_v0;
do {
desired_v0 = velocity_gen(gen);
} while (desired_v0 <= 0.1);
double v_max = 1.3 * desired_v0;
P[i] = Pedestrian(WHITE, Vector2d(), start_pos, init_e_unit, v_max, desired_v0, destination);
}
// Group of pedestrians walking from right to left.
for (int i = n_p_half; i < n_p; i++) {
double x = x_gen_right(gen);
double y = y_gen(gen);
Vector2d start_pos = Vector2d(x, y);
Vector2d destination = Vector2d(-30.0, y);
Vector2d init_e = destination - start_pos;
double init_e_norm = init_e.norm();
Vector2d init_e_unit = init_e / init_e_norm;
// Reject near-zero desired speeds to avoid pedestrians that barely move,
// and reject negative speeds because they would reverse the intended
// walking direction.
double desired_v0;
do {
desired_v0 = velocity_gen(gen);
} while (desired_v0 <= 0.1);
double v_max = 1.3 * desired_v0;
P[i] = Pedestrian(BLACK, Vector2d(), start_pos, init_e_unit, v_max, desired_v0, destination);
}
return P;
}
Board init_board_without_obstacles() { return Board(BOARD_LENGTH, BOARD_WIDTH, 0, nullptr); }
int run_simulation(Pedestrian *pedestrians, Board &board, int n_p, int n_iter, const char *dump_path) {
#if BENCHMARK
// Avoid compiler unused parameter warning
(void)dump_path;
#endif
#if !BENCHMARK
const int dump_interval = 1;
// Each sample stores: time step, pedestrian id, x, y, e.x, e.y, color.
vector<tuple<int, int, double, double, double, double, int>> samples;
if (dump_path) {
samples.reserve(static_cast<size_t>(n_iter / dump_interval + 1) * n_p);
// Store the initial state before the first simulation step.
for (int i = 0; i < n_p; i++) {
Pedestrian &p = pedestrians[i];
samples.emplace_back(0, i, p.curr_pos.x, p.curr_pos.y, p.e.x, p.e.y, p.color);
}
}
#endif
for (int t = 0; t < n_iter; t++) {
// Pass 1: Compute all forces from the current state.
for (int i = 0; i < n_p; i++) {
Pedestrian &p = pedestrians[i];
// # 2 ADDs, 4 MULTs
//
// $\vec{F}_\alpha(\vec{v}_\alpha, v^0_\alpha \vec{e}_\alpha) = \frac{1}{\tau_\alpha}(v^0_\alpha \vec{e}_\alpha - \vec{v}_\alpha)$
//
Vector2d f_alpha_dt = acceleration_term_dt(p);
// # (N-1) * (12 ADDs, 23 MULTs, 1 DIV, 4 SQRTs, 1 EXP)
// + (N-1) * 2 ADDs
//
// $\sum_{\beta} \vec{F}_{\alpha\beta}(\vec{e}_{\alpha},\vec{r}_{\alpha} - \vec{r}_{\beta})$
//
for (int j = 0; j < n_p; j++) {
if (i == j)
continue;
f_alpha_dt += pedestrian_repulsion_dt(p, pedestrians[j]);
}
// # board.n_obstacles * (10 ADDs, 18 MULTs, 1 DIV, 2 SQRTs, 1 EXP)
// + board.n_obstacles * 2 ADDs
//
// $\sum_{B} \vec{F}_{\alpha B}(\vec{e}_{\alpha}, \vec{r}_{\alpha} - \vec{r}_{B})$
//
for (int k = 0; k < board.n_obstacles; k++) {
f_alpha_dt += obstacle_repulsion_dt(p, board.obstacles[k]);
}
// # 2 ADDs
//
//
// $\frac{d\vec{w}_\alpha}{dt} = \vec{F}_{\alpha}(t)$
//
p.w += f_alpha_dt;
}
// Pass 2: Update pedestrian states after all forces have been computed.
// # (7 ADDs, 16 MULTs, 2 DIVs, 3 SQRTs) * n_p
for (int i = 0; i < n_p; i++) {
Pedestrian &p = pedestrians[i];
// # 1 ADD, 2 MULTs, 1 SQRT
double w_norm = p.w.norm();
double w_norm2 = w_norm > EPS ? w_norm : EPS; // Avoid division by zero.
// # 1 DIV
double correction_factor = std::min(1.0, p.v_max / w_norm2);
// # 2 MULTs
p.curr_vel = p.w * correction_factor;
// # 2 ADDs, 2 MULTs
//
// $\vec{r}_i^{t+1} = \vec{r}_i^t + \vec{v}_i^t dt$
//
p.curr_pos = p.curr_pos + p.curr_vel * dt;
// When a pedestrian reaches the end of the corridor, teleport them back
// to the opposite entrance while keeping the same y-coordinate. This
// maintains a constant pedestrian flow and makes lane formation easier to
// observe.
if (p.color == WHITE && p.curr_pos.x > (BOARD_LENGTH - 1.5)) {
p.curr_pos.x = 1.0;
p.destination.y = p.curr_pos.y;
p.w = Vector2d(p.desired_v0, 0.0);
p.curr_vel = p.w;
}
if (p.color == BLACK && p.curr_pos.x < 1.5) {
p.curr_pos.x = BOARD_LENGTH - 1.0;
p.destination.y = p.curr_pos.y;
p.w = Vector2d(-p.desired_v0, 0.0);
p.curr_vel = p.w;
}
// Keep pedestrians inside the board boundaries.
p.curr_pos.y = clamp(p.curr_pos.y, WALL_EPS, BOARD_WIDTH_MINUS_WALL_EPS);
// # 2 ADDs
Vector2d e = p.destination - p.curr_pos;
// # 1 ADD, 2 MULTs, 1 DIV, 1 SQRT
double e_norm = e.norm();
double e_norm2 = e_norm > EPS ? e_norm : EPS; // Avoid division by zero.
double e_norm_inv = 1.0 / e_norm2;
// # 2 MULTs
p.e = e * e_norm_inv;
// # 1 ADD, 2 MULTs
double velocity_norm_sq = p.curr_vel.dot(p.curr_vel);
// # 1 SQRT
double velocity_norm = std::sqrt(velocity_norm_sq);
// # 1 MULT
p.s_b_sq = velocity_norm_sq * delta_t_sq;
// # 3 MULTs
p.s_b_e = p.e * (velocity_norm * delta_t);
#if !BENCHMARK
// Store the current pedestrian state for visualization.
if (dump_path && (t % dump_interval == 0)) {
samples.emplace_back(t + 1, i, p.curr_pos.x, p.curr_pos.y, p.e.x, p.e.y, p.color);
}
#endif
}
}
#if !BENCHMARK
// Export recorded simulation states to CSV.
if (dump_path) {
ofstream out(dump_path);
out << "time,id,x,y,p.e.x,p.e.y,color\n";
for (auto &[t, id, x, y, e_x, e_y, c] : samples) {
out << t << ',' << id << ',' << x << ',' << y << ',' << e_x << ',' << e_y << ',' << c << '\n';
}
}
#endif
return 0;
}