Skip to content

Commit 20425e2

Browse files
committed
drop feed to 10% ok
1 parent 9636763 commit 20425e2

1 file changed

Lines changed: 208 additions & 14 deletions

File tree

src/emc/motion_planning/motion_planning_9d.cc

Lines changed: 208 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,39 @@ static void applyBidirectionalReachability(double &v_entry, double &v_exit,
10951095
v_entry = fmin(v_entry, v_bwd);
10961096
}
10971097

1098+
/**
1099+
* @brief Compute minimum exit velocity achievable by maximum jerk-limited
1100+
* braking from entry_vel over the given distance.
1101+
*
1102+
* Used for cross-feed deceleration corridors: when a segment enters at
1103+
* old-feed velocity but needs to converge toward new-feed max_vel,
1104+
* this gives the physically achievable exit after one segment of maximum
1105+
* braking. Returns 0 if full stop is possible within the distance.
1106+
*/
1107+
static double jerkLimitedMinExitVelocity(double v_entry, double distance,
1108+
double max_acc, double max_jrk)
1109+
{
1110+
if (v_entry <= 0 || distance <= 0) return 0;
1111+
1112+
// Can we brake to 0 within the distance?
1113+
double d_to_zero = jerkLimitedBrakingDistance(v_entry, 0, max_acc, max_jrk);
1114+
if (d_to_zero <= distance) return 0;
1115+
1116+
// Binary search: find minimum v_f such that braking distance fits.
1117+
// jerkLimitedBrakingDistance(v_entry, v_f) decreases as v_f increases
1118+
// (less deceleration → shorter distance).
1119+
double lo = 0, hi = v_entry;
1120+
for (int i = 0; i < 30; i++) {
1121+
double mid = 0.5 * (lo + hi);
1122+
double d_brake = jerkLimitedBrakingDistance(v_entry, mid, max_acc, max_jrk);
1123+
if (d_brake <= distance)
1124+
hi = mid; // Can brake to mid — try lower
1125+
else
1126+
lo = mid; // Can't brake to mid — need higher exit
1127+
if (hi - lo < 1e-6) break;
1128+
}
1129+
return hi;
1130+
}
10981131

10991132
/**
11001133
* @brief Compute chain exit cap: maximum safe exit velocity for the active
@@ -1663,6 +1696,14 @@ static bool computeAndStoreProfile(TC_STRUCT *tc, const RuckigProfileParams &p)
16631696
copyRuckigProfile(traj, &tc->shared_9d.profile);
16641697
return true;
16651698
} else {
1699+
if (FO_TRACE) {
1700+
rtapi_print_msg(RTAPI_MSG_ERR,
1701+
"FO_RUCKIG_FAIL result=%d seg=%d v0=%.3f vf=%.3f "
1702+
"max_v=%.3f max_a=%.3f max_j=%.1f dist=%.4f feed=%.3f\n",
1703+
(int)result, tc->id,
1704+
p.v_entry, p.v_exit, p.max_vel,
1705+
p.max_acc, p.max_jrk, p.target_dist, p.feed_scale);
1706+
}
16661707
__atomic_store_n(&tc->shared_9d.profile.valid, 0, __ATOMIC_RELEASE);
16671708
return false;
16681709
}
@@ -2471,6 +2512,18 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24712512
}
24722513
prev_exit_vel_known = true;
24732514
}
2515+
if (FO_TRACE) {
2516+
rtapi_print_msg(RTAPI_MSG_ERR,
2517+
"FO_ACTIVE_SKIP seg=%d term=%d prof_valid=%d "
2518+
"prof_feed=%.3f v0_override=%.1f prev_known=%d "
2519+
"prev_exit=%.3f bv=%d\n",
2520+
tc->id, tc->term_cond, tc->shared_9d.profile.valid,
2521+
tc->shared_9d.profile.computed_feed_scale,
2522+
v0_override, prev_exit_vel_known ? 1 : 0,
2523+
prev_exit_vel,
2524+
__atomic_load_n(&tc->shared_9d.branch.valid,
2525+
__ATOMIC_ACQUIRE));
2526+
}
24742527
continue;
24752528
}
24762529

@@ -2481,9 +2534,13 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24812534
double max_acc = tcGetTangentialMaxAccel_9D_user(tc);
24822535
double max_jrk = tc->maxjerk > 0 ? tc->maxjerk : default_jerk;
24832536

2484-
// Entry velocity (physical mm/s), capped by current segment's max_vel
2537+
// Entry velocity (physical mm/s). Do NOT cap by max_vel here —
2538+
// cross-feed junctions (BRANCH_SKIP_REPLAN) can have prev_exit at
2539+
// old feed exceeding new feed's max_vel. The Ruckig call below
2540+
// uses fmax(scaled_v_entry, max_vel) to allow deceleration from above
2541+
// the limit, matching the two-stage brake pattern in computeBranch().
24852542
double scaled_v_entry = prev_exit_vel_known
2486-
? fmin(prev_exit_vel, max_vel) : 0.0;
2543+
? prev_exit_vel : 0.0;
24872544
// Cap entry by previous segment's kink_vel
24882545
{
24892546
TC_STRUCT *prev_tc = (i > 0) ? tcqItem_user(queue, i - 1) : NULL;
@@ -2498,11 +2555,17 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24982555
scaled_v_exit = applyKinkVelCap(scaled_v_exit, v_exit, max_vel, tc->kink_vel);
24992556
double desired_fvel = scaled_v_exit;
25002557

2558+
// Cross-feed deceleration corridor detection
2559+
bool cross_feed_corridor = (scaled_v_entry > max_vel * 1.01);
2560+
25012561
// Forward-lookahead: cap exit by what the next segment can actually
25022562
// enter (jerk-limited braking to its exit vel). The backward pass
25032563
// uses trapezoidal for v_f≈0 which overestimates; this corrects the
25042564
// junction before the profile is committed.
2505-
if (scaled_v_exit > 0.0 && i + 1 < queue_len) {
2565+
// Skip when in cross-feed corridor — the lookahead uses backward-pass
2566+
// exits at the wrong feed scale, producing overly tight caps that
2567+
// cascade forward and crush entry velocities on short segments.
2568+
if (!cross_feed_corridor && scaled_v_exit > 0.0 && i + 1 < queue_len) {
25062569
TC_STRUCT *next_tc = tcqItem_user(queue, i + 1);
25072570
if (next_tc && next_tc->target > 1e-9) {
25082571
double nf = snap.forSegment(next_tc);
@@ -2511,11 +2574,6 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25112574
double na = tcGetTangentialMaxAccel_9D_user(next_tc);
25122575
double nj = next_tc->maxjerk > 0 ? next_tc->maxjerk : default_jerk;
25132576
double reach = jerkLimitedMaxEntryVelocity(nv_exit, next_tc->target, na, nj);
2514-
// Also cap by the successor's max velocity — the successor
2515-
// can't enter faster than its own max_vel regardless of
2516-
// deceleration capacity. Without this, the predecessor
2517-
// exits above the successor's max_vel, creating a junction
2518-
// gap that no branch can fix (too little remaining distance).
25192577
double nvel_limit = getEffectiveVelLimit(tp, next_tc);
25202578
double next_max = applyVLimit(tp, next_tc, nvel_limit * nf);
25212579
reach = fmin(reach, next_max);
@@ -2526,6 +2584,22 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25262584
}
25272585
}
25282586

2587+
// Cross-feed deceleration corridor: when entry exceeds the
2588+
// feed-scaled max_vel (e.g. after BRANCH_SKIP_REPLAN from 200%→10%),
2589+
// compute the physics-based minimum exit velocity from maximum
2590+
// jerk-limited braking over this segment's distance. This gives
2591+
// natural multi-segment deceleration: each segment brakes as hard
2592+
// as physics allow, chaining to the next until velocity converges
2593+
// to max_vel. The forward lookahead is skipped (above) because
2594+
// it uses backward-pass exits at the wrong feed scale.
2595+
if (cross_feed_corridor && tc->term_cond == TC_TERM_COND_TANGENT) {
2596+
double min_exit = jerkLimitedMinExitVelocity(
2597+
scaled_v_entry, tc->target, max_acc, max_jrk);
2598+
// Use the larger of: physics minimum, or max_vel (don't undershoot target)
2599+
scaled_v_exit = fmin(fmax(min_exit, max_vel), scaled_v_entry);
2600+
desired_fvel = scaled_v_exit;
2601+
}
2602+
25292603
// --- SKIP: profile already consistent with propagated v0 and current limits ---
25302604
if (tc->shared_9d.profile.valid) {
25312605
bool entry_ok = !prev_exit_vel_known ||
@@ -2596,14 +2670,41 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
25962670
}
25972671

25982672
// --- Compute and store Ruckig profile ---
2673+
double pre_bidir_v_entry = scaled_v_entry;
2674+
double pre_bidir_v_exit = scaled_v_exit;
25992675
atomicStoreDouble(&tc->shared_9d.entry_vel, scaled_v_entry);
26002676

26012677
applyBidirectionalReachability(scaled_v_entry, scaled_v_exit,
26022678
tc->target, max_acc, max_jrk);
26032679

2680+
// Log cross-feed or zero-entry profile writes (first 5 per pass)
2681+
if (FO_TRACE && fo_dirty_count < 5 &&
2682+
(fabs(pre_bidir_v_entry) < 0.01 ||
2683+
fabs(feed_scale - tc->shared_9d.profile.computed_feed_scale) > 0.1)) {
2684+
rtapi_print_msg(RTAPI_MSG_ERR,
2685+
"FO_FWDWRITE i=%d seg=%d type=%d "
2686+
"v_entry=%.3f(bidir:%.3f) v_exit=%.3f(bidir:%.3f) "
2687+
"feed=%.3f old_feed=%.3f prev_known=%d "
2688+
"max_vel=%.3f target=%.4f active=%d\n",
2689+
i, tc->id, tc->motion_type,
2690+
pre_bidir_v_entry, scaled_v_entry,
2691+
pre_bidir_v_exit, scaled_v_exit,
2692+
feed_scale, tc->shared_9d.profile.computed_feed_scale,
2693+
prev_exit_vel_known ? 1 : 0,
2694+
max_vel, tc->target, tc->active);
2695+
}
2696+
26042697
try {
2698+
// Raise Ruckig max_velocity when entry exceeds feed-scaled limit
2699+
// (cross-feed junction). Same pattern as computeBranch() two-stage
2700+
// brake: fmax(state.velocity, new_max_vel).
2701+
// The 0.1% margin avoids Ruckig ErrorExecutionTimeCalculation
2702+
// (-110) which fires when max_velocity == current_velocity
2703+
// exactly — a degenerate case that hits every cross-feed
2704+
// corridor segment since fmax(v0, max_vel) == v0 by definition.
2705+
double ruckig_max_vel = fmax(scaled_v_entry, max_vel) * 1.001;
26052706
RuckigProfileParams rp = {scaled_v_entry, scaled_v_exit,
2606-
max_vel, max_acc, max_jrk, tc->target,
2707+
ruckig_max_vel, max_acc, max_jrk, tc->target,
26072708
feed_scale, vel_limit, tp->vLimit, desired_fvel};
26082709
if (computeAndStoreProfile(tc, rp)) {
26092710
fo_dirty_count++;
@@ -2617,9 +2718,25 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
26172718
prev_exit_vel = 0.0;
26182719
}
26192720
} else {
2721+
if (FO_TRACE && cross_feed_corridor) {
2722+
rtapi_print_msg(RTAPI_MSG_ERR,
2723+
"FO_CORRIDOR_FAIL i=%d seg=%d v_entry=%.3f v_exit=%.3f "
2724+
"ruckig_max_vel=%.3f max_acc=%.3f max_jrk=%.1f "
2725+
"target=%.4f feed=%.3f corridor=%d\n",
2726+
i, tc->id, scaled_v_entry, scaled_v_exit,
2727+
ruckig_max_vel, max_acc, max_jrk,
2728+
tc->target, feed_scale, cross_feed_corridor ? 1 : 0);
2729+
}
26202730
prev_exit_vel = 0.0;
26212731
}
26222732
} catch (...) {
2733+
if (FO_TRACE && cross_feed_corridor) {
2734+
rtapi_print_msg(RTAPI_MSG_ERR,
2735+
"FO_CORRIDOR_THROW i=%d seg=%d v_entry=%.3f v_exit=%.3f "
2736+
"target=%.4f feed=%.3f\n",
2737+
i, tc->id, scaled_v_entry, scaled_v_exit,
2738+
tc->target, feed_scale);
2739+
}
26232740
__atomic_store_n(&tc->shared_9d.profile.valid, 0, __ATOMIC_RELEASE);
26242741
prev_exit_vel = 0.0;
26252742
}
@@ -2858,6 +2975,14 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
28582975
// set by RT, so we also check new_feed_scale to catch abort.
28592976
handoff_time = elapsed;
28602977
window_end_time = handoff_time + window_sec + 1.0;
2978+
} else if (tc->term_cond == TC_TERM_COND_TANGENT) {
2979+
// TANGENT segment: bypass margin — the branch doesn't need
2980+
// to decelerate to zero, it exits into a blend at whatever
2981+
// velocity is achievable. Let Ruckig + findAchievableExitVelocity
2982+
// decide what's feasible rather than pre-rejecting based on a
2983+
// conservative margin that can exceed short segment durations.
2984+
handoff_time = elapsed;
2985+
window_end_time = handoff_time + window_sec + 1.0;
28612986
} else {
28622987
handoff_time = profile_duration - min_handoff_margin;
28632988
if (handoff_time <= elapsed) {
@@ -2895,6 +3020,8 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
28953020
}
28963021

28973022
if (!state.valid) {
3023+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3024+
"FO_BRANCH_REJECT seg=%d reason=STATE_INVALID\n", tc->id);
28983025
return false;
28993026
}
29003027

@@ -2909,6 +3036,10 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
29093036
// Calculate remaining distance after handoff
29103037
double remaining = tc->target - state.position;
29113038
if (remaining < 1e-6) {
3039+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3040+
"FO_BRANCH_REJECT seg=%d reason=NO_DISTANCE remaining=%.6f "
3041+
"pos=%.3f target=%.3f\n",
3042+
tc->id, remaining, state.position, tc->target);
29123043
tc->shared_9d.requested_feed_scale = new_feed_scale;
29133044
return false;
29143045
}
@@ -2920,6 +3051,11 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
29203051
if (state.acceleration < -0.5 * tc->maxaccel && state.velocity > 0) {
29213052
double stop_dist = (state.velocity * state.velocity) / (2.0 * tc->maxaccel);
29223053
if (remaining <= stop_dist * 1.5) {
3054+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3055+
"FO_BRANCH_REJECT seg=%d reason=DECEL_LOCKED v=%.1f a=%.1f "
3056+
"remaining=%.3f stop_dist=%.3f\n",
3057+
tc->id, state.velocity, state.acceleration,
3058+
remaining, stop_dist);
29233059
tc->shared_9d.requested_feed_scale = new_feed_scale;
29243060
return false;
29253061
}
@@ -3095,6 +3231,10 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
30953231
}
30963232
} else {
30973233
// Even minimum feed can't satisfy — segment is genuinely locked
3234+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3235+
"FO_BRANCH_REJECT seg=%d reason=FEED_LIMIT_LOCKED "
3236+
"exit_hard=%.1f remaining=%.3f v=%.1f\n",
3237+
tc->id, exit_hard_limit, remaining, state.velocity);
30983238
return false;
30993239
}
31003240
}
@@ -3126,12 +3266,18 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
31263266

31273267
auto result = otg.calculate(input, traj);
31283268
if (result != ruckig::Result::Working && result != ruckig::Result::Finished) {
3269+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3270+
"FO_BRANCH_REJECT seg=%d reason=BRAKE_RUCKIG v=%.1f a=%.1f "
3271+
"target_v=%.1f\n",
3272+
tc->id, state.velocity, state.acceleration, stage1_target_vel);
31293273
return false;
31303274
}
31313275

31323276
// Brake profile is clean 7-phase, use phase-based sampling
31333277
copyRuckigProfile(traj, &brake_profile);
31343278
if (!brake_profile.valid) {
3279+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3280+
"FO_BRANCH_REJECT seg=%d reason=BRAKE_COPY\n", tc->id);
31353281
return false;
31363282
}
31373283

@@ -3189,17 +3335,29 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
31893335
// reject the branch and let the existing profile finish.
31903336
// Phase 4 TODO (Blending): review PARABOLIC handling here.
31913337
if (tc->term_cond != TC_TERM_COND_TANGENT && achievable_exit > 0.01) {
3338+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3339+
"FO_BRANCH_REJECT seg=%d reason=CANT_STOP "
3340+
"achievable=%.1f remaining_after_brake=%.3f\n",
3341+
tc->id, achievable_exit, remaining_after_brake);
31923342
return false;
31933343
}
31943344

31953345
// Kink-limited junctions: reject if can't decel to kink_vel
31963346
if (tc->kink_vel > 0 && achievable_exit > tc->kink_vel + 0.01) {
3347+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3348+
"FO_BRANCH_REJECT seg=%d reason=KINK_VEL "
3349+
"achievable=%.1f kink=%.1f\n",
3350+
tc->id, achievable_exit, tc->kink_vel);
31973351
return false;
31983352
}
31993353

32003354
// Downstream reachability: reject if can't decel to what
32013355
// next segment accepts. Feed change deferred to next cycle.
32023356
if (achievable_exit > downstream_exit_cap + 0.01) {
3357+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3358+
"FO_BRANCH_REJECT seg=%d reason=DS_CAP "
3359+
"achievable=%.1f ds_cap=%.1f\n",
3360+
tc->id, achievable_exit, downstream_exit_cap);
32033361
tc->shared_9d.requested_feed_scale = new_feed_scale;
32043362
return false;
32053363
}
@@ -3301,6 +3459,13 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale,
33013459
if (!use_single_stage &&
33023460
(profileHasNegativeVelocity(&brake_profile) ||
33033461
profileHasNegativeVelocity(&main_profile))) {
3462+
if (FO_TRACE) rtapi_print_msg(RTAPI_MSG_ERR,
3463+
"FO_BRANCH_REJECT seg=%d reason=NEG_VEL brake_neg=%d main_neg=%d "
3464+
"v=%.1f a=%.1f remaining=%.3f\n",
3465+
tc->id,
3466+
profileHasNegativeVelocity(&brake_profile) ? 1 : 0,
3467+
profileHasNegativeVelocity(&main_profile) ? 1 : 0,
3468+
state.velocity, state.acceleration, remaining);
33043469
return false;
33053470
}
33063471

@@ -3799,12 +3964,41 @@ static void executeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double feed)
37993964
}
38003965
}
38013966

3802-
g_feed_mgr.planning.branch_failed = !ok;
38033967
if (!ok) {
3804-
g_feed_mgr.planning.branch_failed_seg = tc->id;
3805-
// No rewrite happened — nothing to converge. Clear countdown
3806-
// so gate reopens as soon as GATE_CLEAR fires (RT moves to next seg).
3807-
g_feed_mgr.planning.convergence_remaining = 0;
3968+
// Branch can't fit within active segment (e.g. FEED_LIMIT_LOCKED —
3969+
// not enough remaining distance to brake to downstream exit cap).
3970+
// Instead of waiting for RT to advance past this segment, commit the
3971+
// new feed immediately and replan downstream. Active segment finishes
3972+
// at old profile (RT is safe). Gate stays closed; the convergence
3973+
// countdown from closeGate() ensures safe_depth segments are converged
3974+
// at the new feed before the gate reopens.
3975+
3976+
g_feed_mgr.committed_feed = emcmotStatus
3977+
? emcmotStatus->feed_scale : 1.0;
3978+
g_feed_mgr.committed_rapid = emcmotStatus
3979+
? emcmotStatus->rapid_scale : 1.0;
3980+
3981+
double budget = g_feed_mgr.planning.getBudget(
3982+
g_handoff_config.servo_cycle_time_sec);
3983+
double t0 = etime_user();
3984+
int segs = replanForward(tp, -1.0, budget);
3985+
double replan_s = etime_user() - t0;
3986+
3987+
// Update EMA costs + converged_depth (same tracker as success path).
3988+
g_feed_mgr.planning.onBranchCycleComplete(0.0, replan_s, segs);
3989+
3990+
if (FO_TRACE) {
3991+
int qlen = tcqLen_user(&tp->queue);
3992+
rtapi_print_msg(RTAPI_MSG_ERR,
3993+
"FO cy=%d BRANCH_SKIP_REPLAN committed=%.3f segs=%d/%d "
3994+
"budget=%.0fus actual=%.0fus needs_replan=%d\n",
3995+
g_fo_cycle, g_feed_mgr.committed_feed,
3996+
segs, qlen, budget * 1e6, replan_s * 1e6,
3997+
g_needs_replan ? 1 : 0);
3998+
}
3999+
// Gate stays closed — continuation path in checkFeedOverride handles
4000+
// remaining segments. convergence_remaining stays at 20 (from
4001+
// closeGate), so gate opens only after safe_depth converge + countdown.
38084002
}
38094003

38104004
if (ok) {

0 commit comments

Comments
 (0)