96 : config_(sanitizeConfig(
config)) {}
107 recent_shot_samples_.clear();
108 last_recent_sample_s_ = -std::numeric_limits<double>::infinity();
117 table_.reserve(points.size());
118 for (
const auto& point : points) {
119 if (!std::isfinite(point.distance_m) ||
120 !std::isfinite(point.hood_pitch_rad) ||
121 !std::isfinite(point.muzzle_speed_mps) ||
122 !std::isfinite(point.time_of_flight_s)) {
126 std::max(0.0, point.distance_m),
127 point.hood_pitch_rad,
128 std::max(0.0, point.muzzle_speed_mps),
129 std::max(0.0, point.time_of_flight_s),
132 std::sort(table_.begin(), table_.end(),
134 return a.distance_m < b.distance_m;
140 const std::vector<TablePoint>&
lookupTable()
const {
return table_; }
152 const Vector3& robot_velocity_mps,
153 const Vector3& target_position_m,
double now_s) {
155 if (table_.empty() || !finiteVector(flywheel_origin_m) ||
156 !finiteVector(robot_velocity_mps) || !finiteVector(target_position_m) ||
157 !std::isfinite(now_s)) {
161 trimRecentShotHistory(now_s);
164 flywheel_origin_m + robot_velocity_mps * config_.phase_delay_s;
165 Vector3 lookahead_origin = delayed_origin;
166 double lookahead_distance_m =
167 planarDistance(lookahead_origin, target_position_m);
169 for (
int i = 0; i < 20; ++i) {
170 const auto sampled = sampleTable(clampedDistance(lookahead_distance_m));
172 sampled.time_of_flight_s * std::max(0.0, config_.tof_scale);
173 lookahead_origin = delayed_origin + robot_velocity_mps * tof_s;
174 lookahead_distance_m =
175 planarDistance(lookahead_origin, target_position_m);
178 const double distance_clamped = clampedDistance(lookahead_distance_m);
179 const auto base = sampleTable(distance_clamped);
180 const double effective_tof_s =
181 base.time_of_flight_s * std::max(0.0, config_.tof_scale);
183 const Vector3 target_delta = target_position_m - lookahead_origin;
184 const double yaw_rad = std::atan2(target_delta.
y, target_delta.
x);
186 double hood_pitch_rad = base.hood_pitch_rad;
187 double flywheel_speed_mps = base.muzzle_speed_mps;
189 const RecentShotSample* recent =
190 findRecentShotSample(lookahead_origin, target_position_m);
191 if (recent !=
nullptr) {
192 const double pose_band =
193 std::max(config_.valid_distance_epsilon, config_.recent_pose_band_m);
194 const double pose_delta =
195 (recent->robot_translation_m - lookahead_origin).norm();
196 const double history_weight = 1.0 - std::min(1.0, pose_delta / pose_band);
198 lerp(hood_pitch_rad, recent->hood_pitch_rad, history_weight);
200 lerp(flywheel_speed_mps, recent->flywheel_speed_mps, history_weight);
203 hood_pitch_rad += config_.hood_offset_rad;
204 hood_pitch_rad += config_.hood_distance_slope_radpm * distance_clamped;
206 flywheel_speed_mps += config_.speed_offset_mps;
207 flywheel_speed_mps += config_.speed_distance_slope_mpspm * distance_clamped;
209 hood_pitch_rad = std::clamp(hood_pitch_rad, config_.min_pitch_rad,
210 config_.max_pitch_rad);
211 flywheel_speed_mps = std::clamp(flywheel_speed_mps, config_.min_speed_mps,
212 config_.max_speed_mps);
214 if (config_.ballistic_refinement_enabled) {
215 refineBallisticShot(lookahead_origin, target_position_m, yaw_rad,
216 hood_pitch_rad, flywheel_speed_mps);
220 lookahead_distance_m >=
221 (config_.min_distance_m - config_.valid_distance_epsilon) &&
222 lookahead_distance_m <=
223 (config_.max_distance_m + config_.valid_distance_epsilon);
233 saveRecentShotSample(now_s, lookahead_origin, target_position_m,
234 distance_clamped, hood_pitch_rad,
242 struct RecentShotSample {
243 double timestamp_s{0.0};
244 Vector3 robot_translation_m{};
245 Vector3 target_position_m{};
246 double distance_m{0.0};
247 double hood_pitch_rad{0.0};
248 double flywheel_speed_mps{0.0};
251 struct SampledValues {
252 double hood_pitch_rad{0.0};
253 double muzzle_speed_mps{0.0};
254 double time_of_flight_s{0.0};
257 static double lerp(
double a,
double b,
double t) {
return a + (b - a) * t; }
259 static bool finiteVector(
const Vector3& v) {
260 return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z);
263 static double sanitizeNonNegative(
double value,
double fallback) {
264 if (!std::isfinite(value)) {
267 return std::max(0.0, value);
273 sanitized.valid_distance_epsilon =
274 std::clamp(sanitizeNonNegative(sanitized.valid_distance_epsilon, 1e-6),
276 sanitized.min_distance_m =
277 sanitizeNonNegative(sanitized.min_distance_m, 1.3);
278 sanitized.max_distance_m =
279 sanitizeNonNegative(sanitized.max_distance_m, 5.8);
280 if (sanitized.max_distance_m <
281 sanitized.min_distance_m + sanitized.valid_distance_epsilon) {
282 sanitized.max_distance_m = sanitized.min_distance_m + 1.0;
285 sanitized.phase_delay_s =
286 std::isfinite(sanitized.phase_delay_s) ? sanitized.phase_delay_s : 0.03;
287 sanitized.tof_scale = sanitizeNonNegative(sanitized.tof_scale, 1.0);
289 sanitized.recent_shot_history_window_s =
290 sanitizeNonNegative(sanitized.recent_shot_history_window_s, 10.0);
291 sanitized.recent_shot_sample_period_s =
292 sanitizeNonNegative(sanitized.recent_shot_sample_period_s, 0.1);
293 sanitized.recent_pose_band_m =
294 std::max(sanitized.valid_distance_epsilon,
295 sanitizeNonNegative(sanitized.recent_pose_band_m, 0.75));
296 sanitized.recent_target_band_m =
297 std::max(sanitized.valid_distance_epsilon,
298 sanitizeNonNegative(sanitized.recent_target_band_m, 0.75));
300 sanitized.ballistic_refinement_iterations =
301 std::clamp(sanitized.ballistic_refinement_iterations, 0, 10);
302 sanitized.ballistic_time_step_s = std::clamp(
303 sanitizeNonNegative(sanitized.ballistic_time_step_s, 0.01), 1e-4, 0.05);
304 sanitized.ballistic_max_time_s = std::clamp(
305 sanitizeNonNegative(sanitized.ballistic_max_time_s, 2.5), 0.1, 10.0);
306 sanitized.ballistic_pitch_gain_radpm =
307 std::clamp(std::isfinite(sanitized.ballistic_pitch_gain_radpm)
308 ? sanitized.ballistic_pitch_gain_radpm
311 sanitized.ballistic_speed_gain_mpspm =
312 std::clamp(std::isfinite(sanitized.ballistic_speed_gain_mpspm)
313 ? sanitized.ballistic_speed_gain_mpspm
317 if (!finiteVector(sanitized.gravity_mps2)) {
318 sanitized.gravity_mps2 = Vector3(0.0, 0.0, -9.81);
320 sanitized.effective_gravity_scale = std::clamp(
321 sanitizeNonNegative(sanitized.effective_gravity_scale, 1.0), 0.0, 5.0);
322 sanitized.air_density_kgpm3 = std::clamp(
323 sanitizeNonNegative(sanitized.air_density_kgpm3, 1.225), 0.0, 5.0);
324 sanitized.drag_scale =
325 std::clamp(sanitizeNonNegative(sanitized.drag_scale, 1.0), 0.0, 10.0);
326 sanitized.magnus_coefficient = std::isfinite(sanitized.magnus_coefficient)
327 ? sanitized.magnus_coefficient
329 sanitized.magnus_scale =
330 std::clamp(sanitizeNonNegative(sanitized.magnus_scale, 1.0), 0.0, 10.0);
332 sanitized.projectile_mass_kg =
333 std::max(1e-6, sanitizeNonNegative(sanitized.projectile_mass_kg, 0.27));
334 sanitized.projectile_drag_coefficient = std::clamp(
335 sanitizeNonNegative(sanitized.projectile_drag_coefficient, 0.47), 0.0,
337 sanitized.projectile_reference_area_m2 =
338 sanitizeNonNegative(sanitized.projectile_reference_area_m2, 0.025);
339 if (sanitized.projectile_reference_area_m2 <= 0.0) {
340 sanitized.projectile_reference_area_m2 = 0.025;
342 if (!finiteVector(sanitized.assumed_spin_radps)) {
346 sanitized.min_pitch_rad =
347 std::isfinite(sanitized.min_pitch_rad) ? sanitized.min_pitch_rad : -0.2;
348 sanitized.max_pitch_rad =
349 std::isfinite(sanitized.max_pitch_rad) ? sanitized.max_pitch_rad : 1.35;
350 if (sanitized.max_pitch_rad < sanitized.min_pitch_rad) {
351 std::swap(sanitized.min_pitch_rad, sanitized.max_pitch_rad);
353 sanitized.min_speed_mps = sanitizeNonNegative(sanitized.min_speed_mps, 0.0);
354 sanitized.max_speed_mps =
355 sanitizeNonNegative(sanitized.max_speed_mps, 45.0);
356 if (sanitized.max_speed_mps < sanitized.min_speed_mps) {
357 sanitized.max_speed_mps = sanitized.min_speed_mps + 1.0;
363 double clampedDistance(
double distance_m)
const {
364 return std::clamp(distance_m, config_.min_distance_m,
365 config_.max_distance_m);
368 static double planarDistance(
const Vector3& a,
const Vector3& b) {
369 const Vector3 delta = b - a;
370 return std::sqrt(delta.x * delta.x + delta.y * delta.y);
373 struct BallisticSample {
374 bool reached_target_range{
false};
375 Vector3 position_m{};
376 double travelled_range_m{0.0};
380 Vector3 ballisticAcceleration(
const Vector3& velocity_mps)
const {
381 const Vector3 gravity_mps2 =
382 config_.gravity_mps2 * config_.effective_gravity_scale;
383 const Vector3 drag_force_n =
385 config_.projectile_reference_area_m2,
386 config_.air_density_kgpm3) *
388 const Vector3 magnus_force_n =
390 config_.magnus_coefficient) *
391 config_.magnus_scale;
392 const Vector3 accel_mps2 =
393 gravity_mps2 + (drag_force_n + magnus_force_n) *
394 (1.0 / std::max(1e-9, config_.projectile_mass_kg));
395 return finiteVector(accel_mps2) ? accel_mps2 : gravity_mps2;
398 BallisticSample simulateAtRange(
const Vector3& origin_m,
double yaw_rad,
399 double pitch_rad,
double speed_mps,
400 double target_planar_range_m)
const {
401 BallisticSample sample{};
402 sample.position_m = origin_m;
404 const double cos_pitch = std::cos(pitch_rad);
405 Vector3 velocity_mps(speed_mps * cos_pitch * std::cos(yaw_rad),
406 speed_mps * cos_pitch * std::sin(yaw_rad),
407 speed_mps * std::sin(pitch_rad));
409 const Vector3 forward(std::cos(yaw_rad), std::sin(yaw_rad), 0.0);
410 const double dt_s = config_.ballistic_time_step_s;
412 Vector3 previous_position_m = sample.position_m;
413 double previous_range_m = 0.0;
415 while (sample.time_s < config_.ballistic_max_time_s) {
416 previous_position_m = sample.position_m;
417 previous_range_m = sample.travelled_range_m;
419 const Vector3 accel0_mps2 = ballisticAcceleration(velocity_mps);
420 const Vector3 mid_velocity_mps =
421 velocity_mps + accel0_mps2 * (0.5 * dt_s);
422 const Vector3 accel_mid_mps2 = ballisticAcceleration(mid_velocity_mps);
424 velocity_mps += accel_mid_mps2 * dt_s;
425 sample.position_m += mid_velocity_mps * dt_s;
426 sample.time_s += dt_s;
428 const Vector3 offset_m = sample.position_m - origin_m;
429 sample.travelled_range_m =
430 offset_m.x * forward.x + offset_m.y * forward.y;
432 if (!finiteVector(sample.position_m) ||
433 !std::isfinite(sample.travelled_range_m)) {
434 sample.position_m = previous_position_m;
435 sample.travelled_range_m = previous_range_m;
439 if (sample.travelled_range_m >= target_planar_range_m) {
441 std::max(config_.valid_distance_epsilon,
442 sample.travelled_range_m - previous_range_m);
443 const double alpha = std::clamp(
444 (target_planar_range_m - previous_range_m) / denom, 0.0, 1.0);
445 sample.position_m = previous_position_m +
446 (sample.position_m - previous_position_m) * alpha;
447 sample.travelled_range_m = target_planar_range_m;
448 sample.reached_target_range =
true;
456 void refineBallisticShot(
const Vector3& origin_m,
457 const Vector3& target_position_m,
double yaw_rad,
458 double& hood_pitch_rad,
459 double& flywheel_speed_mps)
const {
460 const double target_planar_range_m =
461 planarDistance(origin_m, target_position_m);
462 if (!std::isfinite(target_planar_range_m) ||
463 target_planar_range_m < config_.valid_distance_epsilon) {
467 for (
int i = 0; i < config_.ballistic_refinement_iterations; ++i) {
469 simulateAtRange(origin_m, yaw_rad, hood_pitch_rad, flywheel_speed_mps,
470 target_planar_range_m);
472 if (!sample.reached_target_range) {
473 const double shortfall_m =
474 std::max(0.0, target_planar_range_m - sample.travelled_range_m);
475 flywheel_speed_mps += std::clamp(
476 shortfall_m * config_.ballistic_speed_gain_mpspm, 0.0, 2.0);
477 flywheel_speed_mps = std::clamp(
478 flywheel_speed_mps, config_.min_speed_mps, config_.max_speed_mps);
482 const double vertical_error_m = target_position_m.z - sample.position_m.z;
483 hood_pitch_rad += std::clamp(
484 vertical_error_m * config_.ballistic_pitch_gain_radpm, -0.08, 0.08);
485 flywheel_speed_mps += std::clamp(
486 vertical_error_m * config_.ballistic_speed_gain_mpspm * 0.5, -0.6,
489 hood_pitch_rad = std::clamp(hood_pitch_rad, config_.min_pitch_rad,
490 config_.max_pitch_rad);
491 flywheel_speed_mps = std::clamp(flywheel_speed_mps, config_.min_speed_mps,
492 config_.max_speed_mps);
496 SampledValues sampleTable(
double distance_m)
const {
497 if (table_.empty()) {
501 if (distance_m <= table_.front().distance_m) {
502 return {table_.front().hood_pitch_rad, table_.front().muzzle_speed_mps,
503 table_.front().time_of_flight_s};
505 if (distance_m >= table_.back().distance_m) {
506 return {table_.back().hood_pitch_rad, table_.back().muzzle_speed_mps,
507 table_.back().time_of_flight_s};
510 for (std::size_t i = 1; i < table_.size(); ++i) {
513 if (distance_m > next.distance_m) {
517 const double span = std::max(config_.valid_distance_epsilon,
518 next.distance_m - prev.distance_m);
520 std::clamp((distance_m - prev.distance_m) / span, 0.0, 1.0);
522 lerp(prev.hood_pitch_rad, next.hood_pitch_rad, t),
523 lerp(prev.muzzle_speed_mps, next.muzzle_speed_mps, t),
524 lerp(prev.time_of_flight_s, next.time_of_flight_s, t),
528 return {table_.back().hood_pitch_rad, table_.back().muzzle_speed_mps,
529 table_.back().time_of_flight_s};
532 void trimRecentShotHistory(
double now_s) {
533 while (!recent_shot_samples_.empty() &&
534 now_s - recent_shot_samples_.front().timestamp_s >
535 config_.recent_shot_history_window_s) {
536 recent_shot_samples_.pop_front();
540 const RecentShotSample* findRecentShotSample(
541 const Vector3& robot_translation_m,
542 const Vector3& target_position_m)
const {
543 const double pose_band =
544 std::max(config_.valid_distance_epsilon, config_.recent_pose_band_m);
545 const double target_band =
546 std::max(config_.valid_distance_epsilon, config_.recent_target_band_m);
548 const RecentShotSample* best =
nullptr;
549 double best_pose_delta = pose_band;
551 for (
const auto& sample : recent_shot_samples_) {
552 const double target_delta =
553 (sample.target_position_m - target_position_m).norm();
554 if (target_delta > target_band) {
558 const double pose_delta =
559 (sample.robot_translation_m - robot_translation_m).norm();
560 if (pose_delta <= best_pose_delta) {
561 best_pose_delta = pose_delta;
569 void saveRecentShotSample(
double now_s,
const Vector3& robot_translation_m,
570 const Vector3& target_position_m,
double distance_m,
571 double hood_pitch_rad,
double flywheel_speed_mps) {
572 if (now_s - last_recent_sample_s_ < config_.recent_shot_sample_period_s) {
576 recent_shot_samples_.push_back({now_s, robot_translation_m,
577 target_position_m, distance_m,
578 hood_pitch_rad, flywheel_speed_mps});
579 last_recent_sample_s_ = now_s;
580 trimRecentShotHistory(now_s);
584 std::vector<TablePoint> table_{};
586 std::deque<RecentShotSample> recent_shot_samples_{};
587 double last_recent_sample_s_{-std::numeric_limits<double>::infinity()};