1516X Push Back 1.0
1516X's robot code for the 2025-2026 VEX Robotics Competition
Loading...
Searching...
No Matches
MCL.cpp
Go to the documentation of this file.
1// Copyright 2026 California High Robotics, Team 1516X
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#include "MCL.h"
5#include "globals.h" // Assuming this is where your chassis and sensors are actually instantiated
6#include "pros/rtos.hpp"
7#include <algorithm>
8#include <cmath>
9#include <vector>
10#include <cstdio>
11
12namespace MCL {
13
14 double PARAMS_TRANS_BASE = 0.3;
15 double PARAMS_TRANS_GAIN = 0.025;
16
18
19 float w_slow = 0.0f, w_fast = 0.0f;
20 constexpr float ALPHA_SLOW = 0.001f;
21 constexpr float ALPHA_FAST = 0.1f;
22
23 // ── Field boundary ───────────────────────────────────────────────────
24 constexpr float FIELD_SIZE = 140.42f; // Could be 141.2
25 constexpr float HALF_SIZE = FIELD_SIZE * 0.5f; // 70.21"
26 constexpr float FIELD_MIN = -HALF_SIZE;
27 constexpr float FIELD_MAX = HALF_SIZE;
28
29 // Reject any sensor reading beyond this (inches)
30 constexpr float MAX_SENSOR_READING = 55.0f;
31
32 // ─────────────────────────── Particle Storage ────────────────────────
33 float particle_x[NUM_PARTICLES];
34 float particle_y[NUM_PARTICLES];
35 float particle_weights[NUM_PARTICLES];
36
37 pros::Mutex particle_mutex;
38
39 // ─────────────────────────── Sensor Configuration ──────────────────
40 struct SensorConfig {
41 float x; // offset Right of robot center (inches)
42 float y; // offset Forward of robot center (inches)
43 float angle; // mounting angle relative to robot forward (deg)
44 };
45
46
47 const std::vector<SensorConfig> SENSOR_CONFIGS = {
48 { 3.66f, 4.067f + 2.5, 0.0f }, // 0: front
49 { -6.309f -1.1, -0.068f, 90.0f }, // 1: right
50 { 3.93f, -2.691f + 2.7, 180.0f }, // 2: back
51 { -6.039f + 1.4, -0.068f, -90.0f } // 3: left
52 };
53
54 struct XorShift32 {
55 uint32_t state;
56 explicit XorShift32(uint32_t seed = pros::micros())
57 : state(seed == 0 ? 0x12345678u : seed) {}
58 inline uint32_t next_u32() {
59 uint32_t x = state;
60 x ^= x << 13; x ^= x >> 17; x ^= x << 5;
61 return state = x;
62 }
63 inline float next_f32() { return (next_u32() >> 8) * (1.0f / (1u << 24)); }
64 inline float uniform(float lo, float hi) { return lo + next_f32() * (hi - lo); }
65 inline float gaussian(float std_dev) {
66 const float u1 = std::max(next_f32(), 1e-12f);
67 const float u2 = next_f32();
68 return std_dev * std::sqrt(-2.0f * std::log(u1))
69 * std::cos(2.0f * (float)M_PI * u2);
70 }
71 } rng;
72
73 // ─────────────────────────── Utilities ──────────────────────────────
74 inline float degToRad(float d) { return d * (float)M_PI / 180.0f; }
75 inline float wrapAngle(float a) {
76 a = std::fmod(a + 180.0f, 360.0f);
77 if (a < 0.0f) a += 360.0f;
78 return a - 180.0f;
79 }
80
81 // ─────────────────────────── Initialisation ─────────────────────────
82 void StartMCL(double x, double y) {
83 particle_mutex.take();
84 rng = XorShift32(pros::micros());
85 for (int i = 0; i < NUM_PARTICLES; ++i) {
86 particle_x[i] = std::clamp<float>(
87 (float)x + rng.gaussian(2.0f), FIELD_MIN + 0.1f, FIELD_MAX - 0.1f);
88 particle_y[i] = std::clamp<float>(
89 (float)y + rng.gaussian(2.0f), FIELD_MIN + 0.1f, FIELD_MAX - 0.1f);
90 particle_weights[i] = 1.0f / NUM_PARTICLES;
91 }
92 w_slow = w_fast = 1.0f / NUM_PARTICLES;
93 particle_mutex.give();
94 }
95
96 // ─────────────────────────── Motion Update ──────────────────────────
97 void MotionUpdate(double dX_global, double dY_global, double dTheta, double robot_theta_deg) {
98 const float dist = (float)std::hypot(dX_global, dY_global);
99 const float turn_factor = std::abs((float)dTheta) * 0.05f;
100
101 const float c = 1.0f - std::clamp((float)global_Confidence, 0.0f, 1.0f);
102 const float transStd = (float)(PARAMS_TRANS_BASE + c * 0.3)
103 + (float)(PARAMS_TRANS_GAIN + c * 0.04) * (dist + turn_factor);
104
105 float math_theta_deg = 90.0f - (float)robot_theta_deg;
106 const float theta_rad = degToRad(math_theta_deg);
107
108 const float cos_t = std::cos(theta_rad);
109 const float sin_t = std::sin(theta_rad);
110
111 particle_mutex.take();
112 for (int i = 0; i < NUM_PARTICLES; ++i) {
113 const float nF = rng.gaussian(transStd);
114 const float nS = rng.gaussian(transStd * 0.6f);
115
116 const float noise_X = nF * cos_t - nS * sin_t;
117 const float noise_Y = nF * sin_t + nS * cos_t;
118
119 particle_x[i] = std::clamp<float>(
120 particle_x[i] + (float)dX_global + noise_X,
121 FIELD_MIN + 0.1f, FIELD_MAX - 0.1f);
122 particle_y[i] = std::clamp<float>(
123 particle_y[i] + (float)dY_global + noise_Y,
124 FIELD_MIN + 0.1f, FIELD_MAX - 0.1f);
125 }
126 particle_mutex.give();
127 }
128
129 // ─────────────────────────── Dynamic Raycast Sensor Update ──────────
130 void SensorUpdate(const std::vector<float>& measurements, float robot_theta_deg, float current_confidence) {
131 particle_mutex.take();
132 float sum_w = 0.0f;
133
134 float math_theta_deg = 90.0f - robot_theta_deg;
135 const float theta_rad = degToRad(math_theta_deg);
136 const float cos_t = std::cos(theta_rad);
137 const float sin_t = std::sin(theta_rad);
138
139 const float dynamic_sensor_sig = 1.5f + (1.0f - std::clamp(current_confidence, 0.0f, 1.0f)) * 4.0f;
140 const float dynamic_margin = dynamic_sensor_sig * 3.0f;
141
142 for (int i = 0; i < NUM_PARTICLES; ++i) {
143 float w = 1.0f;
144
145 for (size_t s = 0; s < measurements.size(); ++s) {
146 if (measurements[s] < 0.0f) continue;
147
148 const SensorConfig& sc = SENSOR_CONFIGS[s];
149
150 float sensor_x = particle_x[i] + sc.y * cos_t + sc.x * sin_t;
151 float sensor_y = particle_y[i] + sc.y * sin_t - sc.x * cos_t;
152
153 float absolute_lemlib_deg = robot_theta_deg + sc.angle;
154 float math_beam_deg = 90.0f - absolute_lemlib_deg;
155 const float beam_rad = degToRad(math_beam_deg);
156
157 float v_x = std::cos(beam_rad);
158 float v_y = std::sin(beam_rad);
159
160 float d_x = 999.0f;
161 if (v_x > 1e-4f) d_x = (FIELD_MAX - sensor_x) / v_x;
162 else if (v_x < -1e-4f) d_x = (FIELD_MIN - sensor_x) / v_x;
163
164 float d_y = 999.0f;
165 if (v_y > 1e-4f) d_y = (FIELD_MAX - sensor_y) / v_y;
166 else if (v_y < -1e-4f) d_y = (FIELD_MIN - sensor_y) / v_y;
167
168 float expected = std::min(d_x, d_y);
169 float err = measurements[s] - expected;
170
171 if (err > dynamic_margin) {
172 w *= 0.001f;
173 } else if (err < -dynamic_margin) {
174 w *= 0.4f;
175 } else {
176 w *= std::exp(-0.5f * err * err / (dynamic_sensor_sig * dynamic_sensor_sig));
177 }
178 }
179
180 particle_weights[i] = w;
181 sum_w += w;
182 }
183
184 const float w_avg = sum_w / NUM_PARTICLES;
185 if (w_slow < 1e-10f) w_slow = w_avg;
186 if (w_fast < 1e-10f) w_fast = w_avg;
187 w_slow += ALPHA_SLOW * (w_avg - w_slow);
188 w_fast += ALPHA_FAST * (w_avg - w_fast);
189
190 if (sum_w > 1e-10f) {
191 for (int i = 0; i < NUM_PARTICLES; ++i)
192 particle_weights[i] /= sum_w;
193 } else {
194 const float u = 1.0f / NUM_PARTICLES;
195 for (int i = 0; i < NUM_PARTICLES; ++i)
196 particle_weights[i] = u;
197 }
198
199 particle_mutex.give();
200 }
201
202 // ─────────────────────────── ESS & Resampling ────────────────────────
203 float computeESS() {
204 particle_mutex.take();
205 float sq = 0.0f;
206 for (int i = 0; i < NUM_PARTICLES; ++i)
208 particle_mutex.give();
209 return (sq > 1e-20f) ? 1.0f / sq : 0.0f;
210 }
211
212 void Resample() {
213 particle_mutex.take();
214
215 const float ratio = (w_slow > 1e-10f) ? (w_fast / w_slow) : 1.0f;
216 const float inject_rate = std::clamp(1.0f - ratio, 0.0f, 0.20f);
217 const int num_inject = (int)(NUM_PARTICLES * inject_rate);
218 const int num_keep = NUM_PARTICLES - num_inject;
219
220 static float new_x[NUM_PARTICLES];
221 static float new_y[NUM_PARTICLES];
222
223 float step = 1.0f / (num_keep > 0 ? num_keep : 1);
224 float r = rng.next_f32() * step;
225 float cum = particle_weights[0];
226 int j = 0;
227
228 // Keep the high-performing particles
229 for (int m = 0; m < num_keep; ++m) {
230 float u = r + (float)m * step;
231 while (u > cum && j < NUM_PARTICLES - 1) {
232 cum += particle_weights[++j];
233 }
234 new_x[m] = particle_x[j];
235 new_y[m] = particle_y[j];
236 }
237
238 // Inject new particles locally around our best guess in a tight cluster (1.5" std dev)
239 for (int m = num_keep; m < NUM_PARTICLES; ++m) {
240 new_x[m] = std::clamp<float>(global_X + rng.gaussian(1.5f), FIELD_MIN + 1.0f, FIELD_MAX - 1.0f);
241 new_y[m] = std::clamp<float>(global_Y + rng.gaussian(1.5f), FIELD_MIN + 1.0f, FIELD_MAX - 1.0f);
242 }
243
244 const float uniform_w = 1.0f / NUM_PARTICLES;
245 for (int i = 0; i < NUM_PARTICLES; ++i) {
246 particle_x[i] = new_x[i];
247 particle_y[i] = new_y[i];
248 particle_weights[i] = uniform_w;
249 }
250
251 particle_mutex.give();
252 }
253
254 // ─────────────────────────── Main Loop ──────────────────────────────
255 void MonteCarlo() {
256 lemlib::Pose prevOdom = chassis.getPose();
257 uint32_t now = pros::millis();
258
259 int print_counter = 0;
260 bool first_run = true; // Added for the EMA filter
261
262 while (true) {
263 lemlib::Pose currOdom = chassis.getPose();
264
265 const double dX_global = currOdom.x - prevOdom.x;
266 const double dY_global = currOdom.y - prevOdom.y;
267 const double dTheta = wrapAngle((float)(currOdom.theta - prevOdom.theta));
268
269 if (std::abs(dX_global) > 0.001 || std::abs(dY_global) > 0.001 || std::abs(dTheta) > 0.1) {
270
271 MotionUpdate(dX_global, dY_global, dTheta, currOdom.theta);
272
273 std::vector<float> measurements(4, -1.0f);
274 bool has_valid_reading = false;
275
276 auto try_read_sensor = [&](auto& sensor, int index) {
277 float val = sensor.get() / 25.4f;
278 if (val > 2.0f && val < MAX_SENSOR_READING) {
279 measurements[index] = val;
280 has_valid_reading = true;
281 }
282 };
283
284 try_read_sensor(frontDistance, 0);
285 try_read_sensor(rightDistance, 1);
286 try_read_sensor(backDistance, 2);
287 try_read_sensor(leftDistance, 3);
288
289 if (has_valid_reading) {
290 SensorUpdate(measurements, currOdom.theta, global_Confidence);
291 }
292
293 particle_mutex.take();
294
295 float max_w = -1.0f;
296 int best_idx = 0;
297 for (int i = 0; i < NUM_PARTICLES; ++i) {
298 if (particle_weights[i] > max_w) {
299 max_w = particle_weights[i];
300 best_idx = i;
301 }
302 }
303
304 float best_x = particle_x[best_idx];
305 float best_y = particle_y[best_idx];
306
307 float sumX = 0.0f, sumY = 0.0f, sumW = 0.0f;
308 float sumX2 = 0.0f, sumY2 = 0.0f;
309 const float CLUSTER_RADIUS = 15.0f;
310
311 for (int i = 0; i < NUM_PARTICLES; ++i) {
312 float dx = particle_x[i] - best_x;
313 float dy = particle_y[i] - best_y;
314
315 if ((dx * dx + dy * dy) <= (CLUSTER_RADIUS * CLUSTER_RADIUS)) {
316 float w = particle_weights[i];
317 sumX += w * particle_x[i];
318 sumY += w * particle_y[i];
319 sumX2 += w * particle_x[i] * particle_x[i];
320 sumY2 += w * particle_y[i] * particle_y[i];
321 sumW += w;
322 }
323 }
324
325 float mcl_std_dev = 999.0f;
326 float cluster_weight_ratio = 0.0f;
327
328 // --- RAW CLUSTER CALCULATION ---
329 float raw_X = best_x;
330 float raw_Y = best_y;
331
332 if (sumW > 1e-6f) {
333 raw_X = sumX / sumW;
334 raw_Y = sumY / sumW;
335
336 float meanX = raw_X;
337 float meanY = raw_Y;
338 float varX = (sumX2 / sumW) - (meanX * meanX);
339 float varY = (sumY2 / sumW) - (meanY * meanY);
340 mcl_std_dev = std::sqrt(std::max(0.0f, varX + varY));
341
342 cluster_weight_ratio = sumW;
343 }
344
345 // --- EXPONENTIAL MOVING AVERAGE (EMA) FILTER ---
346 const float EMA_ALPHA = 0.20f;
347
348 if (first_run) {
349 global_X = raw_X;
350 global_Y = raw_Y;
351 first_run = false;
352 } else {
353 global_X = global_X + EMA_ALPHA * (raw_X - global_X);
354 global_Y = global_Y + EMA_ALPHA * (raw_Y - global_Y);
355 }
356 // -----------------------------------------------
357
358 global_Theta = currOdom.theta;
359 global_Confidence = (w_slow > 1e-10f) ? std::min(w_fast / w_slow, 1.0f) : 0.0f;
360
361 particle_mutex.give();
362
363 if (++print_counter >= 3) {
364 printf("MCL: %.2f %.2f | ODOM: %.2f %.2f | CONF: %.2f | STD: %.2f | RATIO: %.2f\n",
365 global_X, global_Y, currOdom.x, currOdom.y, global_Confidence, mcl_std_dev, cluster_weight_ratio);
366 print_counter = 0;
367 }
368
369 if (global_Confidence > 0.65 && cluster_weight_ratio > 0.50f &&
370 std::abs(dX_global) < 5 && std::abs(dY_global) < 5) {
371
372 double diff_x = global_X - currOdom.x;
373 double diff_y = global_Y - currOdom.y;
374 double correction_mag = std::hypot(diff_x, diff_y);
375
376 if (correction_mag > 1.0 && correction_mag < 12.0) {
377 lemlib::Pose fusedPose(global_X, global_Y, currOdom.theta);
378 chassis.setPose(fusedPose);
379 currOdom = chassis.getPose();
380 }
381 }
382
383 const float ess = computeESS();
384 const float ratio = (w_slow > 1e-10f) ? (w_fast / w_slow) : 1.0f;
385 if (ess < NUM_PARTICLES * 0.5f || ratio < 0.9f) {
386 Resample();
387 }
388 }
389
390 prevOdom = currOdom;
391 pros::Task::delay_until(&now, 10);
392 }
393 }
394
395} // namespace MCL
396
pros::Distance backDistance(7)
Chassis chassis(drivebase, lateral_controller, angular_controller, sensors, &throttle_curve, &steer_curve)
pros::Distance leftDistance(5)
pros::Distance rightDistance(6)
pros::Distance frontDistance(8)
Definition MCL.cpp:12
constexpr float HALF_SIZE
Definition MCL.cpp:25
double PARAMS_TRANS_BASE
Definition MCL.cpp:14
double global_Theta
Definition MCL.cpp:17
void StartMCL(double x, double y)
Definition MCL.cpp:82
void MotionUpdate(double dX_global, double dY_global, double dTheta, double robot_theta_deg)
Definition MCL.cpp:97
double global_Y
Definition MCL.cpp:17
double global_Confidence
Definition MCL.cpp:17
float particle_x[NUM_PARTICLES]
Definition MCL.cpp:33
void MonteCarlo()
Definition MCL.cpp:255
constexpr float ALPHA_FAST
Definition MCL.cpp:21
float particle_y[NUM_PARTICLES]
Definition MCL.cpp:34
float degToRad(float d)
Definition MCL.cpp:74
double PARAMS_TRANS_GAIN
Definition MCL.cpp:15
float wrapAngle(float a)
Definition MCL.cpp:75
float particle_weights[NUM_PARTICLES]
Definition MCL.cpp:35
constexpr float FIELD_MIN
Definition MCL.cpp:26
constexpr float MAX_SENSOR_READING
Definition MCL.cpp:30
pros::Mutex particle_mutex
Definition MCL.cpp:37
const std::vector< SensorConfig > SENSOR_CONFIGS
Definition MCL.cpp:47
float w_slow
Definition MCL.cpp:19
double global_X
Definition MCL.cpp:17
float w_fast
Definition MCL.cpp:19
struct MCL::XorShift32 rng
void SensorUpdate(const std::vector< float > &measurements, float robot_theta_deg, float current_confidence)
Definition MCL.cpp:130
float computeESS()
Definition MCL.cpp:203
constexpr float FIELD_SIZE
Definition MCL.cpp:24
void Resample()
Definition MCL.cpp:212
constexpr float ALPHA_SLOW
Definition MCL.cpp:20
constexpr float FIELD_MAX
Definition MCL.cpp:27
uint32_t state
Definition MCL.cpp:55
XorShift32(uint32_t seed=pros::micros())
Definition MCL.cpp:56
uint32_t next_u32()
Definition MCL.cpp:58
float gaussian(float std_dev)
Definition MCL.cpp:65
float next_f32()
Definition MCL.cpp:63
float uniform(float lo, float hi)
Definition MCL.cpp:64