"""Camera-motion kinematics shared by generation and offline evaluation.

The retrieved camera templates are artist-authored curves, but resampling and
retargeting can turn an otherwise intentional direction change into a sharp
per-frame corner.  This module rebuilds only a short neighbourhood around that
corner: the camera decelerates into the turn and accelerates away without a
compensating speed spike.

A continuous one-direction orbit remains untouched.  Only a persistent orbit
direction reversal (CW to CCW or the reverse) is treated as a turn event.
"""

from __future__ import annotations

from dataclasses import dataclass
import math

import numpy as np


TRANSLATION_KINEMATIC_POLICY_VERSION = (
    "yamo-angle-aware-turn-v7-local-impulse-authored-dynamics-guard"
)
DEFAULT_REVERSAL_ANGLE_DEGREES = 40.0
DEFAULT_STOP_SPEED_RATIO = 0.12
DEFAULT_REVERSAL_WINDOW_SECONDS = 0.35
DEFAULT_PERSISTENCE_SECONDS = 0.08
DEFAULT_MINIMUM_FLANK_SPEED_MPS = 0.08
DEFAULT_MINIMUM_FLANK_TRAVEL_METERS = 0.006
# With the five-frame persistence window, an ideal constant-curvature path has
# a concentration of 0.20 (one adjacent-frame turn divided by the five-frame
# flank-direction change).  Keep a small margin above that value so a fast,
# intentional arc remains untouched while concentrated 40--55 degree corners
# such as the reviewed Shot_020 (0.22) are still eligible.
DEFAULT_MINIMUM_CORNER_CONCENTRATION = 0.21
DEFAULT_MINIMUM_FLANK_DIRECTION_COHERENCE = 0.85
DEFAULT_SPEED_RATIO_TOLERANCE = 0.025
DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES = 1e-3
DEFAULT_MINIMUM_ORBIT_ANGULAR_SPEED_RADIANS_PER_SECOND = math.radians(2.0)
DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS = 0.75
# A regularizer is a repair pass, not another motion authoring stage.  The old
# 25 m/s2 and 350 m/s3 absolute floors let a quiet authored curve become several
# times rougher while still reporting a successful repair (the reviewed
# Shot_066 class rose from roughly 255 to 548 m/s3).  Keep only small numerical
# headroom: a rewrite may trade at most five percent of either peak, and the
# absolute allowance is deliberately tiny for already-quiet paths.
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO = 1.05
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2 = 1.0
DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2 = 0.5
DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO = 1.05
DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3 = 15.0
DEFAULT_JERK_GUARD_FLOOR_MPS3 = 10.0
# A second, deliberately narrow gate catches a concentrated turn that evades
# the 40-degree rule yet still carries an edit-visible sideways impulse.  The
# thresholds were calibrated against 2,479 authored turn events: the combined
# gate adds only 10 events (0.4%) beyond the existing abrupt-turn policy while
# covering the reviewed Shot_066 profile (high stop ratio plus ~22 m/s^2 and
# ~200 m/s^3 local lateral dynamics).  The zero-net-direction extension was
# separately scanned across 3,054 authored shots: it admitted one cut-boundary
# discontinuity, which the authored-dynamics guard correctly rejected.
DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES = 15.0
DEFAULT_IMPULSIVE_STOP_SPEED_RATIO = 0.78
DEFAULT_IMPULSIVE_CORNER_CONCENTRATION = 0.40
DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2 = 22.0
DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3 = 200.0
DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO = 0.72
DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS = 0.55
# A generated curve can satisfy the broad monotonic-speed definition of
# ``natural_easing`` while its turn is still concentrated enough to read as a
# lateral kick.  Keep this as a separate, narrower contradiction gate rather
# than weakening the micro-turn thresholds above.  The reviewed Shot_066 is
# 59.60 degrees / 0.812 stop ratio / 0.355 concentration / 22.69 m/s^2 /
# 167.84 m/s^3.  A scan of 1,395 detected events across 2,006 authored shots
# found only three grouped events satisfying every condition below (0.22%);
# all three also carried substantially larger 645--958 m/s^3 lateral jerk.
DEFAULT_NATURAL_IMPULSE_TARGET_EXCESS_RATIO = 0.08
DEFAULT_NATURAL_IMPULSE_CORNER_CONCENTRATION = 0.35
DEFAULT_NATURAL_IMPULSE_LATERAL_ACCELERATION_MPS2 = 22.0
DEFAULT_NATURAL_IMPULSE_LATERAL_JERK_MPS3 = 160.0


def turn_speed_ratio_limit(angle_degrees: float) -> float:
    """Return an artist-oriented speed target for a sharp direction change.

    A 40--55 degree corner receives only a mild slowdown, a right-angle corner
    receives a clear slowdown, and turns at or above 120 degrees approach a
    stop.  The piecewise curve is deliberately monotonic and inspectable in
    metadata.
    """

    if not math.isfinite(angle_degrees):
        raise ValueError("angle_degrees must be finite.")
    return float(
        np.interp(
            float(np.clip(angle_degrees, 0.0, 180.0)),
            np.asarray(
                [0.0, 40.0, 45.0, 50.0, 55.0, 60.0, 90.0, 120.0, 150.0, 180.0]
            ),
            np.asarray([1.0, 0.90, 0.87, 0.84, 0.80, 0.70, 0.45, 0.12, 0.0, 0.0]),
        )
    )


def natural_turn_speed_ratio_limit(angle_degrees: float) -> float:
    """Return the preservation threshold for an already eased sharp turn."""

    if not math.isfinite(angle_degrees):
        raise ValueError("angle_degrees must be finite.")
    return float(
        np.interp(
            float(np.clip(angle_degrees, 0.0, 180.0)),
            np.asarray(
                [0.0, 40.0, 45.0, 50.0, 55.0, 60.0, 90.0, 120.0, 150.0, 180.0]
            ),
            np.asarray([1.0, 0.90, 0.89, 0.87, 0.86, 0.85, 0.65, 0.40, 0.30, 0.25]),
        )
    )


@dataclass(frozen=True)
class TranslationReversal:
    """One persistent camera-translation direction-change event.

    The legacy type name is retained because existing generated metadata and
    downstream analysis use the ``translation*Reversal*`` field family.
    """

    frame: int
    angle_degrees: float
    stop_speed_ratio: float
    flank_speed_mps: float
    target_speed_ratio: float
    corner_concentration: float
    natural_easing_detected: bool
    impulsive: bool = False
    lateral_acceleration_mps2: float = 0.0
    lateral_jerk_mps3: float = 0.0

    @property
    def abrupt(self) -> bool:
        if self.impulsive:
            return self.stop_speed_ratio > (
                self.target_speed_ratio + DEFAULT_SPEED_RATIO_TOLERANCE
            )
        if self.natural_easing_detected and self.stop_speed_ratio <= (
            natural_turn_speed_ratio_limit(self.angle_degrees)
            + DEFAULT_SPEED_RATIO_TOLERANCE
        ):
            return False
        return self.stop_speed_ratio > (
            self.target_speed_ratio + DEFAULT_SPEED_RATIO_TOLERANCE
        )


def motion_family(motion_type: str) -> str:
    """Return the local kinematic family without importing the shot planner."""

    normalized = str(motion_type).strip().lower().replace("-", "_")
    if normalized in {"static", "fixed", "locked", "stationary", "still"}:
        return "static"
    if normalized.startswith(("orbit", "arc")):
        return "orbit"
    if normalized.startswith(("dolly", "push", "pull", "zoom")):
        return "dolly"
    if normalized.startswith(("truck", "lateral")):
        return "truck"
    if normalized.startswith(("crane", "pedestal", "boom", "jib")):
        return "crane"
    if normalized in {"drift", "handheld", "float", "floating"}:
        return "drift"
    return "unsupported"


def _validated_positions(positions_meters: np.ndarray) -> np.ndarray:
    positions = np.asarray(positions_meters, dtype=np.float64)
    if positions.ndim != 2 or positions.shape[1] != 3 or len(positions) == 0:
        raise ValueError("positions_meters must be a non-empty [frame, 3] array.")
    if not np.isfinite(positions).all():
        raise ValueError("positions_meters must contain only finite values.")
    return positions


def _primary_motion_signal(positions: np.ndarray, family: str) -> np.ndarray:
    if family == "dolly":
        return np.linalg.norm(positions[:, [0, 2]], axis=1)
    if family == "truck":
        return positions[:, 0]
    if family == "crane":
        return positions[:, 1]

    # Drift has no declared axis.  The principal spatial axis is stable for a
    # push-pull or side-to-side drift, including paths whose net delta is near
    # zero because they return to their starting point.
    centered = positions - np.mean(positions, axis=0, keepdims=True)
    if float(np.max(np.linalg.norm(centered, axis=1))) <= 1e-9:
        return np.zeros(len(positions), dtype=np.float64)
    _, _, axes = np.linalg.svd(centered, full_matrices=False)
    return centered @ axes[0]


def _angle_degrees(first: np.ndarray, second: np.ndarray) -> float:
    denominator = float(np.linalg.norm(first) * np.linalg.norm(second))
    if denominator <= 1e-12:
        return 0.0
    cosine = float(np.clip(np.dot(first, second) / denominator, -1.0, 1.0))
    return math.degrees(math.acos(cosine))


def _group_reversal_candidates(
    candidates: list[TranslationReversal],
    persistence_frames: int,
) -> list[TranslationReversal]:
    if not candidates:
        return []
    groups: list[list[TranslationReversal]] = [[candidates[0]]]
    for candidate in candidates[1:]:
        if candidate.frame - groups[-1][-1].frame <= persistence_frames:
            groups[-1].append(candidate)
        else:
            groups.append([candidate])
    events: list[TranslationReversal] = []
    for group in groups:
        low_speed_event = min(
            group,
            key=lambda event: (event.stop_speed_ratio, event.frame),
        )
        impulsive_events = [event for event in group if event.impulsive]
        representative = (
            max(
                impulsive_events,
                key=lambda event: (
                    event.lateral_jerk_mps3,
                    event.lateral_acceleration_mps2,
                    -event.frame,
                ),
            )
            if impulsive_events
            else low_speed_event
        )
        maximum_angle = max(event.angle_degrees for event in group)
        maximum_concentration = max(
            event.corner_concentration for event in group
        )
        maximum_lateral_acceleration = max(
            event.lateral_acceleration_mps2 for event in group
        )
        maximum_lateral_jerk = max(
            event.lateral_jerk_mps3 for event in group
        )
        maximum_angle_target = turn_speed_ratio_limit(maximum_angle)
        # A detector group represents one physical corner, but its clearest
        # slowdown, largest angle and largest lateral impulse commonly occur
        # on neighbouring centers.  Evaluate this contradiction after
        # grouping; requiring every signal on one center missed Shot_066 even
        # though the aggregate event was an edit-visible kick.
        natural_easing_impulse = bool(
            low_speed_event.natural_easing_detected
            and maximum_angle
            >= DEFAULT_REVERSAL_ANGLE_DEGREES
            - DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
            and low_speed_event.stop_speed_ratio
            >= DEFAULT_IMPULSIVE_STOP_SPEED_RATIO
            and low_speed_event.stop_speed_ratio - maximum_angle_target
            >= DEFAULT_NATURAL_IMPULSE_TARGET_EXCESS_RATIO
            and maximum_concentration
            >= DEFAULT_NATURAL_IMPULSE_CORNER_CONCENTRATION
            and maximum_lateral_acceleration
            >= DEFAULT_NATURAL_IMPULSE_LATERAL_ACCELERATION_MPS2
            and maximum_lateral_jerk
            >= DEFAULT_NATURAL_IMPULSE_LATERAL_JERK_MPS3
        )
        impulsive = bool(impulsive_events) or natural_easing_impulse
        events.append(
            TranslationReversal(
                frame=representative.frame,
                angle_degrees=maximum_angle,
                stop_speed_ratio=representative.stop_speed_ratio,
                flank_speed_mps=representative.flank_speed_mps,
                target_speed_ratio=(
                    min(
                        maximum_angle_target,
                        DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO,
                    )
                    if impulsive
                    else maximum_angle_target
                ),
                corner_concentration=maximum_concentration,
                natural_easing_detected=(representative.natural_easing_detected),
                impulsive=impulsive,
                lateral_acceleration_mps2=maximum_lateral_acceleration,
                lateral_jerk_mps3=maximum_lateral_jerk,
            )
        )
    return events


def analyze_translational_reversals(
    positions_meters: np.ndarray,
    motion_type: str,
    *,
    sample_rate: float = 60.0,
    reversal_angle_degrees: float = DEFAULT_REVERSAL_ANGLE_DEGREES,
    persistence_seconds: float = DEFAULT_PERSISTENCE_SECONDS,
    minimum_flank_speed_mps: float = DEFAULT_MINIMUM_FLANK_SPEED_MPS,
    minimum_flank_travel_meters: float = (DEFAULT_MINIMUM_FLANK_TRAVEL_METERS),
    minimum_corner_concentration: float = (DEFAULT_MINIMUM_CORNER_CONCENTRATION),
    minimum_flank_direction_coherence: float = (
        DEFAULT_MINIMUM_FLANK_DIRECTION_COHERENCE
    ),
) -> list[TranslationReversal]:
    """Find persistent, concentrated translation turns in one shot.

    The detector requires both sides of a turn to sustain meaningful
    travel.  Small pose-follow jitter therefore does not become an authored
    camera-direction event.  Typed motions additionally have to reverse their
    declared scalar axis (radius, lateral, or height).
    """

    positions = _validated_positions(positions_meters)
    if not math.isfinite(sample_rate) or sample_rate <= 0.0:
        raise ValueError("sample_rate must be finite and positive.")
    if not 0.0 < reversal_angle_degrees <= 180.0:
        raise ValueError("reversal_angle_degrees must be in (0, 180].")
    if not math.isfinite(persistence_seconds) or persistence_seconds <= 0.0:
        raise ValueError("persistence_seconds must be finite and positive.")
    if minimum_flank_speed_mps < 0.0 or not math.isfinite(minimum_flank_speed_mps):
        raise ValueError("minimum_flank_speed_mps must be finite and non-negative.")
    if minimum_flank_travel_meters < 0.0 or not math.isfinite(
        minimum_flank_travel_meters
    ):
        raise ValueError("minimum_flank_travel_meters must be finite and non-negative.")
    if not 0.0 <= minimum_corner_concentration <= 1.0:
        raise ValueError("minimum_corner_concentration must be between zero and one.")
    if not 0.0 <= minimum_flank_direction_coherence <= 1.0:
        raise ValueError(
            "minimum_flank_direction_coherence must be between zero and one."
        )

    family = motion_family(motion_type)
    if family in {"static", "unsupported"} or len(positions) < 7:
        return []

    persistence_frames = max(2, int(round(persistence_seconds * sample_rate)))
    if len(positions) < persistence_frames * 2 + 1:
        persistence_frames = max(2, (len(positions) - 1) // 2)
    if persistence_frames < 2:
        return []

    velocity = np.diff(positions, axis=0) * sample_rate
    speed = np.linalg.norm(velocity, axis=1)
    centers = np.arange(
        persistence_frames,
        len(positions) - persistence_frames,
        dtype=np.int64,
    )
    velocity_cumulative = np.vstack(
        (
            np.zeros((1, 3), dtype=np.float64),
            np.cumsum(velocity, axis=0),
        )
    )
    before_vectors = (
        velocity_cumulative[centers] - velocity_cumulative[centers - persistence_frames]
    ) / persistence_frames
    after_vectors = (
        velocity_cumulative[centers + persistence_frames] - velocity_cumulative[centers]
    ) / persistence_frames
    before_speeds = np.linalg.norm(before_vectors, axis=1)
    after_speeds = np.linalg.norm(after_vectors, axis=1)
    flank_speeds = np.minimum(before_speeds, after_speeds)
    speed_cumulative = np.concatenate((np.zeros(1, dtype=np.float64), np.cumsum(speed)))
    before_mean_speeds = (
        speed_cumulative[centers] - speed_cumulative[centers - persistence_frames]
    ) / persistence_frames
    after_mean_speeds = (
        speed_cumulative[centers + persistence_frames] - speed_cumulative[centers]
    ) / persistence_frames
    before_coherence = before_speeds / np.maximum(before_mean_speeds, 1e-12)
    after_coherence = after_speeds / np.maximum(after_mean_speeds, 1e-12)
    denominator = before_speeds * after_speeds
    cosine = np.divide(
        np.sum(before_vectors * after_vectors, axis=1),
        denominator,
        out=np.ones_like(denominator),
        where=denominator > 1e-12,
    )
    angles = np.degrees(np.arccos(np.clip(cosine, -1.0, 1.0)))

    unit_velocity = np.divide(
        velocity,
        speed[:, None],
        out=np.zeros_like(velocity),
        where=speed[:, None] > 1e-12,
    )
    adjacent_turn_cosine = np.sum(
        unit_velocity[:-1] * unit_velocity[1:],
        axis=1,
    )
    adjacent_turn_angles = np.degrees(
        np.arccos(np.clip(adjacent_turn_cosine, -1.0, 1.0))
    )
    corner_window_size = persistence_frames * 2 - 1
    corner_windows = np.lib.stride_tricks.sliding_window_view(
        adjacent_turn_angles,
        corner_window_size,
    )
    peak_corner_angles = np.max(
        corner_windows[centers - persistence_frames],
        axis=1,
    )
    corner_concentration = peak_corner_angles / np.maximum(angles, 1e-12)

    step_distance = speed / sample_rate
    distance_cumulative = np.concatenate(
        (np.zeros(1, dtype=np.float64), np.cumsum(step_distance))
    )
    before_travel = (
        distance_cumulative[centers] - distance_cumulative[centers - persistence_frames]
    )
    after_travel = (
        distance_cumulative[centers + persistence_frames] - distance_cumulative[centers]
    )
    valid = (
        (flank_speeds >= minimum_flank_speed_mps)
        & (
            angles
            >= reversal_angle_degrees
            - DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
        )
        & (before_coherence >= minimum_flank_direction_coherence)
        & (after_coherence >= minimum_flank_direction_coherence)
        & (corner_concentration >= minimum_corner_concentration)
        & (np.minimum(before_travel, after_travel) >= minimum_flank_travel_meters)
    )

    if family == "orbit":
        radius = np.linalg.norm(positions[:, [0, 2]], axis=1)
        azimuth = np.unwrap(np.arctan2(positions[:, 0], positions[:, 2]))
        angular_velocity = np.diff(azimuth) * sample_rate
        angular_windows = np.lib.stride_tricks.sliding_window_view(
            angular_velocity,
            persistence_frames,
        )
        incoming_angular_velocity = np.median(
            angular_windows[centers - persistence_frames],
            axis=1,
        )
        outgoing_angular_velocity = np.median(
            angular_windows[centers],
            axis=1,
        )
        valid &= incoming_angular_velocity * outgoing_angular_velocity < 0.0
        valid &= (
            np.minimum(
                np.abs(incoming_angular_velocity),
                np.abs(outgoing_angular_velocity),
            )
            >= DEFAULT_MINIMUM_ORBIT_ANGULAR_SPEED_RADIANS_PER_SECOND
        )
        valid &= radius[centers] >= 0.1

    # Keep the original, angle-aware policy result separate.  The impulsive
    # gate below is a narrow fallback for turns that the established policy
    # would otherwise preserve; it must not strengthen the slowdown already
    # selected for an ordinary 40+ degree corner.
    standard_valid = valid.copy()

    local_speed_windows = np.lib.stride_tricks.sliding_window_view(speed, 4)
    local_minimum_speeds = np.min(local_speed_windows[centers - 2], axis=1)
    ratios = local_minimum_speeds / np.maximum(flank_speeds, 1e-12)
    lateral_acceleration_peaks = np.zeros(len(centers), dtype=np.float64)
    lateral_jerk_peaks = np.zeros(len(centers), dtype=np.float64)
    if family != "orbit":
        acceleration = np.diff(velocity, axis=0) * sample_rate
        jerk = np.diff(acceleration, axis=0) * sample_rate
        for result_index, center in enumerate(centers):
            incoming = before_vectors[result_index]
            incoming_norm = float(np.linalg.norm(incoming))
            if incoming_norm <= 1e-12:
                continue
            incoming_unit = incoming / incoming_norm
            window_start = max(0, int(center) - persistence_frames)
            acceleration_window = acceleration[
                window_start : min(len(acceleration), int(center) + persistence_frames)
            ]
            jerk_window = jerk[
                window_start : min(len(jerk), int(center) + persistence_frames)
            ]
            if len(acceleration_window):
                lateral = acceleration_window - (
                    acceleration_window @ incoming_unit
                )[:, None] * incoming_unit
                lateral_acceleration_peaks[result_index] = float(
                    np.max(np.linalg.norm(lateral, axis=1))
                )
            if len(jerk_window):
                lateral = jerk_window - (
                    jerk_window @ incoming_unit
                )[:, None] * incoming_unit
                lateral_jerk_peaks[result_index] = float(
                    np.max(np.linalg.norm(lateral, axis=1))
                )
        # A one- or two-frame sideways kick can return to the original travel
        # direction, leaving the persistent flank angle close to zero.  Use the
        # absolute adjacent-frame turn as a second angle witness.  The existing
        # coherence, travel, concentration, acceleration and jerk gates keep a
        # deliberate distributed dolly/truck/arc from being classified as an
        # impulse; continuous orbit motion remains explicitly excluded.
        impulsive_valid = (
            (flank_speeds >= minimum_flank_speed_mps)
            & (
                np.maximum(angles, peak_corner_angles)
                >= DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES
                - DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
            )
            & (before_coherence >= minimum_flank_direction_coherence)
            & (after_coherence >= minimum_flank_direction_coherence)
            & (
                corner_concentration
                >= max(
                    minimum_corner_concentration,
                    DEFAULT_IMPULSIVE_CORNER_CONCENTRATION,
                )
            )
            & (np.minimum(before_travel, after_travel) >= minimum_flank_travel_meters)
            & (ratios >= DEFAULT_IMPULSIVE_STOP_SPEED_RATIO)
            & (
                lateral_acceleration_peaks
                >= DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2
            )
            & (lateral_jerk_peaks >= DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3)
        )
    else:
        impulsive_valid = np.zeros(len(centers), dtype=bool)
    persistence_speed_windows = np.lib.stride_tricks.sliding_window_view(
        speed,
        persistence_frames,
    )
    before_speed_sequence = persistence_speed_windows[centers - persistence_frames]
    after_speed_sequence = persistence_speed_windows[centers]
    trend_tolerance = flank_speeds[:, None] * 0.03
    before_monotonic_ratio = np.mean(
        np.diff(before_speed_sequence, axis=1)
        <= trend_tolerance[:, : persistence_frames - 1],
        axis=1,
    )
    after_monotonic_ratio = np.mean(
        np.diff(after_speed_sequence, axis=1)
        >= -trend_tolerance[:, : persistence_frames - 1],
        axis=1,
    )
    natural_speed_limits = np.asarray(
        [natural_turn_speed_ratio_limit(float(angle)) for angle in angles],
        dtype=np.float64,
    )
    natural_easing = (
        (before_monotonic_ratio >= 0.75)
        & (after_monotonic_ratio >= 0.75)
        & (ratios <= natural_speed_limits + DEFAULT_SPEED_RATIO_TOLERANCE)
    )
    standard_speed_limits = np.asarray(
        [turn_speed_ratio_limit(float(angle)) for angle in angles],
        dtype=np.float64,
    )
    standard_abrupt = standard_valid & ~(
        natural_easing
        & (ratios <= natural_speed_limits + DEFAULT_SPEED_RATIO_TOLERANCE)
    ) & (ratios > standard_speed_limits + DEFAULT_SPEED_RATIO_TOLERANCE)
    # Only upgrade events that evade the established abrupt-turn rule.  This
    # keeps authored 40--180 degree slowdown curves on their calibrated policy
    # while admitting the reviewed concentrated, high-jerk micro-turn class.
    impulsive_valid &= ~standard_abrupt
    if np.any(standard_abrupt):
        # Adjacent detector centers represent one physical corner and are
        # grouped later.  Clear the fallback throughout that grouping radius,
        # otherwise a nearby 39.9-degree center could relabel an ordinary
        # 40-degree event as impulsive and incorrectly strengthen its target.
        standard_abrupt_centers = centers[standard_abrupt]
        impulsive_valid &= ~np.any(
            np.abs(centers[:, None] - standard_abrupt_centers[None, :])
            <= persistence_frames,
            axis=1,
        )
    valid |= impulsive_valid
    candidates = [
        TranslationReversal(
            frame=int(frame),
            angle_degrees=float(angle),
            stop_speed_ratio=float(ratio),
            flank_speed_mps=float(flank_speed),
            target_speed_ratio=turn_speed_ratio_limit(float(angle)),
            corner_concentration=float(concentration),
            natural_easing_detected=bool(is_naturally_eased),
            impulsive=bool(is_impulsive),
            lateral_acceleration_mps2=float(lateral_acceleration),
            lateral_jerk_mps3=float(lateral_jerk),
        )
        for (
            frame,
            angle,
            ratio,
            flank_speed,
            concentration,
            is_naturally_eased,
            is_impulsive,
            lateral_acceleration,
            lateral_jerk,
        ) in zip(
            centers[valid],
            angles[valid],
            ratios[valid],
            flank_speeds[valid],
            corner_concentration[valid],
            natural_easing[valid],
            impulsive_valid[valid],
            lateral_acceleration_peaks[valid],
            lateral_jerk_peaks[valid],
        )
    ]

    # Multiple adjacent frame centers describe the same physical turn.  Keep
    # the center with the clearest low-speed evidence so a naturally eased
    # reversal is not falsely reported as abrupt.
    return _group_reversal_candidates(candidates, persistence_frames)


def _smootherstep(values: np.ndarray) -> np.ndarray:
    values = np.asarray(values, dtype=np.float64)
    return values**3 * (10.0 + values * (-15.0 + 6.0 * values))


def _slerp_unit_vectors(
    first: np.ndarray,
    second: np.ndarray,
    progress: np.ndarray,
) -> np.ndarray:
    first = np.asarray(first, dtype=np.float64)
    second = np.asarray(second, dtype=np.float64)
    first /= max(float(np.linalg.norm(first)), 1e-12)
    second /= max(float(np.linalg.norm(second)), 1e-12)
    progress = np.asarray(progress, dtype=np.float64)
    dot = float(np.clip(np.dot(first, second), -1.0, 1.0))
    if dot > 0.9995:
        values = (1.0 - progress[:, None]) * first + progress[:, None] * second
        return values / np.maximum(
            np.linalg.norm(values, axis=1, keepdims=True),
            1e-12,
        )
    angle = math.acos(dot)
    sine = math.sin(angle)
    return (
        np.sin((1.0 - progress[:, None]) * angle) / sine * first
        + np.sin(progress[:, None] * angle) / sine * second
    )


def _ease_through_stop(
    positions: np.ndarray,
    center: int,
    radius: int,
    *,
    target_speed_ratio: float = 0.0,
    turn_angle_degrees: float = 180.0,
) -> np.ndarray:
    """Reintegrate one turn under a no-overspeed S-curve envelope.

    Preserving the window's start, turn and end positions while inserting a
    stop is mathematically forced to create compensating overspeed.  Instead,
    this function preserves the incoming and outgoing velocity at the outer
    window edges, reduces travel near the corner, and shifts the remaining tail
    by the small positional difference.  No reconstructed step can exceed its
    source step, and constant-speed input becomes monotonic deceleration into
    the turn followed by monotonic acceleration away from it.
    """

    if radius < 2:
        raise ValueError("radius must be at least two frames.")
    if not 0.0 <= target_speed_ratio <= 1.0:
        raise ValueError("target_speed_ratio must be between zero and one.")
    if not 0.0 <= turn_angle_degrees <= 180.0:
        raise ValueError("turn_angle_degrees must be between zero and 180.")
    left = center - radius
    right = center + radius
    if left < 0 or right >= len(positions):
        raise ValueError("turn window must remain inside positions.")

    source = np.asarray(positions, dtype=np.float64)
    source_steps = np.diff(source, axis=0)
    source_speeds = np.linalg.norm(source_steps, axis=1)
    source_directions = np.divide(
        source_steps,
        source_speeds[:, None],
        out=np.zeros_like(source_steps),
        where=source_speeds[:, None] > 1e-12,
    )

    progress = np.linspace(0.0, 1.0, radius)
    easing = _smootherstep(progress)
    before_factor = 1.0 - (1.0 - target_speed_ratio) * easing
    after_factor = target_speed_ratio + (1.0 - target_speed_ratio) * easing
    before_speed = source_speeds[left:center] * before_factor
    after_speed = source_speeds[center:right] * after_factor
    # Do not force monotonicity with cumulative minima. That operation creates
    # a new slope discontinuity whenever an authored speed fluctuation becomes
    # the running minimum. The C2 factor is already monotonic for a constant-
    # speed corner and preserves the source curve's local dynamics otherwise.

    directions = source_directions[left:right].copy()
    if turn_angle_degrees < 150.0:
        flank = min(radius, max(2, int(round(radius * 0.25))))
        incoming = np.mean(source_steps[center - flank : center], axis=0)
        outgoing = np.mean(source_steps[center : center + flank], axis=0)
        if (
            float(np.linalg.norm(incoming)) > 1e-12
            and float(np.linalg.norm(outgoing)) > 1e-12
        ):
            smoothed_directions = _slerp_unit_vectors(
                incoming,
                outgoing,
                np.linspace(0.0, 1.0, radius * 2),
            )
            # Preserve the authored direction at both window boundaries for
            # every eligible angle, then hand control to the interpolated turn
            # with a C2-continuous envelope. Replacing an entire 60--149 degree
            # window created a velocity seam at its outer edge. The previous
            # sin-squared mild-turn blend also retained a C2 seam there.
            edge_progress = np.linspace(0.0, 1.0, radius * 2)
            direction_blend = np.where(
                edge_progress <= 0.5,
                _smootherstep(edge_progress * 2.0),
                _smootherstep((1.0 - edge_progress) * 2.0),
            )
            directions = (
                source_directions[left:right]
                * (1.0 - direction_blend[:, None])
                + smoothed_directions * direction_blend[:, None]
            )
            directions /= np.maximum(
                np.linalg.norm(directions, axis=1, keepdims=True),
                1e-12,
            )

    reconstructed_steps = (
        directions * np.concatenate((before_speed, after_speed))[:, None]
    )
    output = source.copy()
    output[left + 1 : right + 1] = output[left] + np.cumsum(
        reconstructed_steps,
        axis=0,
    )
    tail_offset = output[right] - source[right]
    if right + 1 < len(output):
        output[right + 1 :] = source[right + 1 :] + tail_offset
    return output


def translation_dynamics_metrics(
    positions_meters: np.ndarray,
    sample_rate: float,
) -> dict[str, float]:
    """Return peak translation speed, acceleration and jerk for auditing."""

    positions = _validated_positions(positions_meters)
    velocity = np.diff(positions, axis=0) * sample_rate
    acceleration = np.diff(velocity, axis=0) * sample_rate
    jerk = np.diff(acceleration, axis=0) * sample_rate

    def peak(values: np.ndarray) -> float:
        return float(np.max(np.linalg.norm(values, axis=1))) if len(values) else 0.0

    return {
        "speedMetersPerSecondMax": peak(velocity),
        "accelerationMetersPerSecondSquaredMax": peak(acceleration),
        "jerkMetersPerSecondCubedMax": peak(jerk),
    }


def _dynamics_regression_exceeds_guard(
    before: dict[str, float],
    after: dict[str, float],
) -> bool:
    """Reject a rewrite that introduces an edit-visible dynamics spike."""

    acceleration_before = before["accelerationMetersPerSecondSquaredMax"]
    acceleration_after = after["accelerationMetersPerSecondSquaredMax"]
    acceleration_limit = max(
        DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2,
        min(
            acceleration_before
            * DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO,
            acceleration_before
            + DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2,
        ),
    )
    jerk_before = before["jerkMetersPerSecondCubedMax"]
    jerk_after = after["jerkMetersPerSecondCubedMax"]
    jerk_limit = max(
        DEFAULT_JERK_GUARD_FLOOR_MPS3,
        min(
            jerk_before * DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO,
            jerk_before + DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3,
        ),
    )
    return bool(
        acceleration_after > acceleration_limit + 1e-9
        or jerk_after > jerk_limit + 1e-9
    )


def _local_translation_dynamics_metrics(
    positions_meters: np.ndarray,
    left: int,
    right: int,
    sample_rate: float,
) -> dict[str, float]:
    """Measure a rewrite window including every finite-difference seam.

    A shot-wide maximum can hide a new local kick when some unrelated frame is
    already rougher.  Three samples on either side cover the velocity,
    acceleration and jerk stencils, so this local measurement makes the guard
    sensitive to the exact neighbourhood changed by ``_ease_through_stop``.
    """

    positions = _validated_positions(positions_meters)
    start = max(0, int(left) - 3)
    stop = min(len(positions), int(right) + 4)
    return translation_dynamics_metrics(positions[start:stop], sample_rate)


def _event_metadata(events: list[TranslationReversal]) -> dict[str, object]:
    abrupt = [event for event in events if event.abrupt]
    impulsive = [event for event in events if event.impulsive]
    return {
        "count": len(events),
        "abruptCount": len(abrupt),
        "frames": [event.frame for event in events],
        "abruptFrames": [event.frame for event in abrupt],
        "anglesDegrees": [event.angle_degrees for event in events],
        "stopSpeedRatios": [event.stop_speed_ratio for event in events],
        "targetSpeedRatios": [event.target_speed_ratio for event in events],
        "cornerConcentrations": [event.corner_concentration for event in events],
        "naturalEasingDetected": [
            event.natural_easing_detected for event in events
        ],
        "impulsiveCount": len(impulsive),
        "impulsiveFrames": [event.frame for event in impulsive],
        "lateralAccelerationMetersPerSecondSquared": [
            event.lateral_acceleration_mps2 for event in events
        ],
        "lateralJerkMetersPerSecondCubed": [
            event.lateral_jerk_mps3 for event in events
        ],
        "worstStopSpeedRatio": (
            float(max(event.stop_speed_ratio for event in events)) if events else 0.0
        ),
        "worstTargetSpeedRatioExcess": (
            float(
                max(
                    event.stop_speed_ratio - event.target_speed_ratio
                    for event in abrupt
                )
            )
            if abrupt
            else 0.0
        ),
    }


def regularize_translational_reversals(
    positions_meters: np.ndarray,
    motion_type: str,
    *,
    sample_rate: float = 60.0,
    reversal_window_seconds: float = DEFAULT_REVERSAL_WINDOW_SECONDS,
    apply_policy: bool = True,
) -> tuple[np.ndarray, dict[str, object]]:
    """Apply angle-aware braking to abrupt turns and orbit reversals."""

    positions = _validated_positions(positions_meters)
    if not math.isfinite(reversal_window_seconds) or reversal_window_seconds <= 0.0:
        raise ValueError("reversal_window_seconds must be finite and positive.")
    if not isinstance(apply_policy, bool):
        raise ValueError("apply_policy must be a boolean.")
    family = motion_family(motion_type)
    before = analyze_translational_reversals(
        positions,
        motion_type,
        sample_rate=sample_rate,
    )
    abrupt_before = [event for event in before if event.abrupt]
    output = positions.copy()
    applied_frames: list[int] = []
    applied_angles: list[float] = []
    applied_target_speed_ratios: list[float] = []
    applied_impulsive_frames: list[int] = []
    applied_window_radii_frames: list[int] = []
    attempted_regularized_frames: list[int] = []
    dynamics_guard_rejected_frames: list[int] = []
    adaptive_radius_frames: list[int] = []
    dynamics_before = translation_dynamics_metrics(positions, sample_rate)
    attempted_dynamics_after = dict(dynamics_before)

    pass_count = 0
    pending_events = abrupt_before if apply_policy else []
    if family not in {"static", "unsupported"} and pending_events:
        requested_radius = max(
            6,
            int(round(reversal_window_seconds * sample_rate)),
        )
        impulsive_requested_radius = max(
            requested_radius,
            int(
                round(
                    DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS * sample_rate
                )
            ),
        )
        # Only abrupt events detected on the untouched input are eligible.
        # Re-detecting and rewriting new window-boundary events caused a
        # three-pass cascade in real shots, multiplying acceleration and jerk
        # even though the final turn counter eventually reached zero.
        event_frames = [event.frame for event in pending_events]
        for event_index, event in enumerate(pending_events):
            # The high-impulse fallback is intentionally rare, so give its
            # braking envelope a slightly longer C2 runway instead of forcing
            # a stronger speed change into the normal 0.35-second window.
            preferred_radius = (
                impulsive_requested_radius
                if event.impulsive
                else requested_radius
            )
            neighbor_limit = min(event.frame, len(output) - 1 - event.frame)
            if event_index > 0:
                neighbor_limit = min(
                    neighbor_limit,
                    (event.frame - event_frames[event_index - 1]) // 2,
                )
            if event_index + 1 < len(event_frames):
                neighbor_limit = min(
                    neighbor_limit,
                    (event_frames[event_index + 1] - event.frame) // 2,
                )
            preferred_radius = min(preferred_radius, neighbor_limit)
            if preferred_radius < 4:
                continue

            attempted_regularized_frames.append(event.frame)
            # First retain the established window.  If its C2 rewrite is still
            # locally rougher than the authored input, spend at most another
            # quarter second on the runway before giving up.  This adaptive
            # path fixes boundary-limited cases such as Shot_066 without
            # weakening the dynamics contract or flattening every motion.
            extension_step = max(2, int(round(0.05 * sample_rate)))
            maximum_adaptive_radius = min(
                neighbor_limit,
                preferred_radius + max(4, int(round(0.25 * sample_rate))),
            )
            candidate_radii = list(
                range(
                    preferred_radius,
                    maximum_adaptive_radius + 1,
                    extension_step,
                )
            )
            if candidate_radii[-1] != maximum_adaptive_radius:
                candidate_radii.append(maximum_adaptive_radius)

            # Keep the authored comparison region fixed while trying wider
            # runways.  If this ROI grew with ``radius``, an unrelated rough
            # frame newly entering (for example at r=27 after r=21 failed)
            # could raise the baseline maximum and hide a fresh center kick.
            fixed_local_before = _local_translation_dynamics_metrics(
                output,
                event.frame - preferred_radius,
                event.frame + preferred_radius,
                sample_rate,
            )
            accepted_output: np.ndarray | None = None
            accepted_radius = preferred_radius
            for radius in candidate_radii:
                candidate = _ease_through_stop(
                    output,
                    event.frame,
                    radius,
                    target_speed_ratio=event.target_speed_ratio,
                    turn_angle_degrees=event.angle_degrees,
                )
                local_before = _local_translation_dynamics_metrics(
                    output,
                    event.frame - radius,
                    event.frame + radius,
                    sample_rate,
                )
                local_after = _local_translation_dynamics_metrics(
                    candidate,
                    event.frame - radius,
                    event.frame + radius,
                    sample_rate,
                )
                fixed_local_after = _local_translation_dynamics_metrics(
                    candidate,
                    event.frame - preferred_radius,
                    event.frame + preferred_radius,
                    sample_rate,
                )
                candidate_dynamics = translation_dynamics_metrics(
                    candidate,
                    sample_rate,
                )
                for key in attempted_dynamics_after:
                    attempted_dynamics_after[key] = max(
                        attempted_dynamics_after[key],
                        candidate_dynamics[key],
                    )
                if _dynamics_regression_exceeds_guard(local_before, local_after):
                    continue
                if _dynamics_regression_exceeds_guard(
                    fixed_local_before,
                    fixed_local_after,
                ):
                    continue
                if _dynamics_regression_exceeds_guard(
                    dynamics_before,
                    candidate_dynamics,
                ):
                    continue
                accepted_output = candidate
                accepted_radius = radius
                break

            if accepted_output is None:
                dynamics_guard_rejected_frames.append(event.frame)
                continue
            output = accepted_output
            if accepted_radius != preferred_radius:
                adaptive_radius_frames.append(event.frame)
            applied_frames.append(event.frame)
            applied_angles.append(event.angle_degrees)
            applied_target_speed_ratios.append(event.target_speed_ratio)
            applied_window_radii_frames.append(accepted_radius)
            if event.impulsive:
                applied_impulsive_frames.append(event.frame)
        pass_count = int(bool(applied_frames))

    endpoint_adjustment = output[-1] - positions[-1]
    endpoint_adjustment_length = float(np.linalg.norm(endpoint_adjustment))
    endpoint_adjustment_clamped = (
        endpoint_adjustment_length > DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS
    )
    if endpoint_adjustment_clamped:
        correction_scale = (
            DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS / endpoint_adjustment_length
        )
        # A convex blend between source and slowed steps cannot exceed the
        # source peak speed.  Pathological multi-turn candidates are therefore
        # bounded spatially and remain eligible for the unresolved-turn penalty.
        output = positions + (output - positions) * correction_scale

    final_attempted_dynamics = translation_dynamics_metrics(output, sample_rate)
    for key in attempted_dynamics_after:
        attempted_dynamics_after[key] = max(
            attempted_dynamics_after[key],
            final_attempted_dynamics[key],
        )
    final_dynamics_guard_triggered = bool(applied_frames) and (
        _dynamics_regression_exceeds_guard(
            dynamics_before,
            final_attempted_dynamics,
        )
    )
    dynamics_guard_triggered = bool(
        dynamics_guard_rejected_frames or final_dynamics_guard_triggered
    )
    if final_dynamics_guard_triggered:
        # A rejected rewrite remains an abrupt candidate and is therefore
        # demoted by the normal selection penalty. Keeping the untouched curve
        # is safer than publishing a numerically "resolved" turn with an
        # edit-visible acceleration or jerk seam.
        dynamics_guard_rejected_frames.extend(
            frame
            for frame in applied_frames
            if frame not in dynamics_guard_rejected_frames
        )
        output = positions.copy()
        applied_frames.clear()
        applied_angles.clear()
        applied_target_speed_ratios.clear()
        applied_impulsive_frames.clear()
        applied_window_radii_frames.clear()
        adaptive_radius_frames.clear()
        pass_count = 0
        endpoint_adjustment_clamped = False

    after = analyze_translational_reversals(
        output,
        motion_type,
        sample_rate=sample_rate,
    )
    before_metadata = _event_metadata(before)
    after_metadata = _event_metadata(after)
    dynamics_after = translation_dynamics_metrics(output, sample_rate)
    path_length_before = float(
        np.sum(np.linalg.norm(np.diff(positions, axis=0), axis=1))
    )
    path_length_after = float(np.sum(np.linalg.norm(np.diff(output, axis=0), axis=1)))
    metadata: dict[str, object] = {
        "translationKinematicPolicyVersion": (TRANSLATION_KINEMATIC_POLICY_VERSION),
        "translationKinematicMotionFamily": family,
        "translationKinematicPolicyEligible": family not in {"static", "unsupported"},
        "translationKinematicPolicyApplicationEnabled": apply_policy,
        "translationKinematicPolicyApplied": bool(applied_frames),
        "translationKinematicDynamicsGuardTriggered": dynamics_guard_triggered,
        "translationKinematicAttemptedRegularizationCount": len(
            attempted_regularized_frames
        ),
        "translationKinematicAttemptedReversalFrames": (
            attempted_regularized_frames
        ),
        "translationKinematicDynamicsGuardRejectedFrames": (
            dynamics_guard_rejected_frames
        ),
        "translationKinematicAdaptiveRadiusFrames": adaptive_radius_frames,
        "translationDirectionReversalCountBefore": before_metadata["count"],
        "translationAbruptReversalCountBefore": before_metadata["abruptCount"],
        "translationDirectionReversalFramesBefore": before_metadata["frames"],
        "translationAbruptReversalFramesBefore": before_metadata["abruptFrames"],
        "translationDirectionReversalAnglesDegreesBefore": before_metadata[
            "anglesDegrees"
        ],
        "translationDirectionReversalStopSpeedRatiosBefore": before_metadata[
            "stopSpeedRatios"
        ],
        "translationDirectionReversalTargetSpeedRatiosBefore": before_metadata[
            "targetSpeedRatios"
        ],
        "translationDirectionReversalCornerConcentrationsBefore": before_metadata[
            "cornerConcentrations"
        ],
        "translationDirectionReversalNaturalEasingDetectedBefore": before_metadata[
            "naturalEasingDetected"
        ],
        "translationDirectionReversalLateralAccelerationMetersPerSecondSquaredBefore": (
            before_metadata["lateralAccelerationMetersPerSecondSquared"]
        ),
        "translationDirectionReversalLateralJerkMetersPerSecondCubedBefore": (
            before_metadata["lateralJerkMetersPerSecondCubed"]
        ),
        "translationWorstStopSpeedRatioBefore": before_metadata["worstStopSpeedRatio"],
        "translationRegularizedReversalCount": len(applied_frames),
        "translationKinematicRegularizationPassCount": pass_count,
        "translationRegularizedReversalFrames": applied_frames,
        "translationRegularizedTurnAnglesDegrees": applied_angles,
        "translationRegularizedTargetSpeedRatios": (applied_target_speed_ratios),
        "translationRegularizedWindowRadiiFrames": (
            applied_window_radii_frames
        ),
        "translationImpulsiveTurnCountBefore": before_metadata[
            "impulsiveCount"
        ],
        "translationImpulsiveTurnFramesBefore": before_metadata[
            "impulsiveFrames"
        ],
        "translationImpulsiveTurnRegularizedFrames": applied_impulsive_frames,
        "translationImpulsiveTurnCountAfter": after_metadata[
            "impulsiveCount"
        ],
        "translationImpulsiveTurnFramesAfter": after_metadata[
            "impulsiveFrames"
        ],
        "translationDirectionReversalCountAfter": after_metadata["count"],
        "translationAbruptReversalCountAfter": after_metadata["abruptCount"],
        "translationDirectionReversalFramesAfter": after_metadata["frames"],
        "translationAbruptReversalFramesAfter": after_metadata["abruptFrames"],
        "translationDirectionReversalAnglesDegreesAfter": after_metadata[
            "anglesDegrees"
        ],
        "translationDirectionReversalStopSpeedRatiosAfter": after_metadata[
            "stopSpeedRatios"
        ],
        "translationDirectionReversalTargetSpeedRatiosAfter": after_metadata[
            "targetSpeedRatios"
        ],
        "translationDirectionReversalCornerConcentrationsAfter": after_metadata[
            "cornerConcentrations"
        ],
        "translationDirectionReversalNaturalEasingDetectedAfter": after_metadata[
            "naturalEasingDetected"
        ],
        "translationDirectionReversalLateralAccelerationMetersPerSecondSquaredAfter": (
            after_metadata["lateralAccelerationMetersPerSecondSquared"]
        ),
        "translationDirectionReversalLateralJerkMetersPerSecondCubedAfter": (
            after_metadata["lateralJerkMetersPerSecondCubed"]
        ),
        "translationWorstStopSpeedRatioAfter": after_metadata["worstStopSpeedRatio"],
        "translationWorstTargetSpeedRatioExcessAfter": after_metadata[
            "worstTargetSpeedRatioExcess"
        ],
        "translationStopSpeedRatioLimit": DEFAULT_STOP_SPEED_RATIO,
        "translationReversalAngleMinimumDegrees": (DEFAULT_REVERSAL_ANGLE_DEGREES),
        "translationMinimumCornerConcentration": (DEFAULT_MINIMUM_CORNER_CONCENTRATION),
        "translationImpulsiveTurnPolicy": {
            "minimumAngleDegrees": DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES,
            "minimumStopSpeedRatio": DEFAULT_IMPULSIVE_STOP_SPEED_RATIO,
            "minimumCornerConcentration": (
                DEFAULT_IMPULSIVE_CORNER_CONCENTRATION
            ),
            "minimumLateralAccelerationMetersPerSecondSquared": (
                DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2
            ),
            "minimumLateralJerkMetersPerSecondCubed": (
                DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3
            ),
            "maximumTargetSpeedRatio": DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO,
            "decelerationAndAccelerationWindowSeconds": (
                DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS
            ),
            "naturalEasingContradiction": {
                "minimumOriginalTurnAngleDegrees": (
                    DEFAULT_REVERSAL_ANGLE_DEGREES
                ),
                "minimumTargetSpeedRatioExcess": (
                    DEFAULT_NATURAL_IMPULSE_TARGET_EXCESS_RATIO
                ),
                "minimumCornerConcentration": (
                    DEFAULT_NATURAL_IMPULSE_CORNER_CONCENTRATION
                ),
                "minimumLateralAccelerationMetersPerSecondSquared": (
                    DEFAULT_NATURAL_IMPULSE_LATERAL_ACCELERATION_MPS2
                ),
                "minimumLateralJerkMetersPerSecondCubed": (
                    DEFAULT_NATURAL_IMPULSE_LATERAL_JERK_MPS3
                ),
            },
            "continuousOrbitExcluded": True,
        },
        "translationSpeedMetersPerSecondMaxBefore": dynamics_before[
            "speedMetersPerSecondMax"
        ],
        "translationSpeedMetersPerSecondMaxAfter": dynamics_after[
            "speedMetersPerSecondMax"
        ],
        "translationAccelerationMetersPerSecondSquaredMaxBefore": (
            dynamics_before["accelerationMetersPerSecondSquaredMax"]
        ),
        "translationAccelerationMetersPerSecondSquaredMaxAfter": (
            dynamics_after["accelerationMetersPerSecondSquaredMax"]
        ),
        "translationJerkMetersPerSecondCubedMaxBefore": dynamics_before[
            "jerkMetersPerSecondCubedMax"
        ],
        "translationJerkMetersPerSecondCubedMaxAfter": dynamics_after[
            "jerkMetersPerSecondCubedMax"
        ],
        "translationAttemptedAccelerationMetersPerSecondSquaredMax": (
            attempted_dynamics_after[
                "accelerationMetersPerSecondSquaredMax"
            ]
        ),
        "translationAttemptedJerkMetersPerSecondCubedMax": (
            attempted_dynamics_after["jerkMetersPerSecondCubedMax"]
        ),
        "translationPathLengthRetentionRatio": (
            path_length_after / path_length_before
            if path_length_before > 1e-12
            else 1.0
        ),
        "translationEndpointAdjustmentMeters": float(
            np.linalg.norm(output[-1] - positions[-1])
        ),
        "translationEndpointAdjustmentLimitMeters": (
            DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS
        ),
        "translationEndpointAdjustmentClamped": endpoint_adjustment_clamped,
    }
    return output.astype(np.float32), metadata


def translational_reversal_selection_penalty(
    metrics: dict[str, object],
) -> float:
    """Softly demote candidates whose short shot cannot fit a safe slowdown."""

    abrupt_count = int(metrics.get("translationAbruptReversalCountAfter", 0))
    ratio_excess = float(
        metrics.get("translationWorstTargetSpeedRatioExcessAfter", 0.0)
    )
    dynamics_guard = bool(
        metrics.get("translationKinematicDynamicsGuardTriggered", False)
    )
    return (
        abrupt_count * 2.5
        + ratio_excess * 2.0
        + float(dynamics_guard) * 2.5
    )
