File size: 6,146 Bytes
6342806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"""Causal counterfactual replay: does a perception error actually MATTER?

The SAME scenario is run twice through the closed loop (closedloop.py):

  actual     perception as-is (engine failure model + any injected faults)
  corrected  ground-truth-injected perception (perfect detection)

The two runs are diffed on planner output, trajectory, velocity, braking,
minimum separation and collision probability, and the causal chain is
answered stepwise:

  Q1  Would correct perception change the planner output?
  Q2  Would that change the vehicle's behavior (trajectory/velocity)?
  Q3  Would that change the safety outcome (separation / collision)?

Verdict (deterministic policy):
  BEHAVIORALLY_CONSEQUENTIAL  iff Q3 is yes (the perception difference
                              propagates all the way to a safety outcome)
  METRIC_ONLY                 otherwise — the error shows up in open-loop
                              metrics but does not change what the vehicle
                              does in any safety-relevant way (e.g. a
                              cosmetic class flip: the planner reacts to
                              geometry, not class).

Both assessments carry the scenario's data label; nothing here upgrades
simulation to reality.
"""

from __future__ import annotations

from typing import Dict, List, Optional

import numpy as np

from sensorflow.nextgen.closedloop import DEFAULT_ENGINE, run_closed_loop
from sensorflow.nextgen.models import (
    CAUSAL_BEHAVIORAL, CAUSAL_METRIC_ONLY, BehavioralAssessment,
    CausalReplayResult, DataLabel,
)
from sensorflow.nextgen.worldmodel import ActorTrack

# Deterministic thresholds of the causal policy (versioned via lineage).
PLANNER_DIFF_MPS2 = 0.5       # commanded-accel divergence that counts as "changed plan"
TRAJECTORY_DIFF_M = 0.5       # positional divergence that counts as "changed behavior"
VELOCITY_DIFF_MPS = 1.0
SEPARATION_DIFF_M = 0.5       # safety-outcome deltas
COLLISION_PROB_DIFF = 0.15
TTC_DIFF_S = 0.5


def _aligned(actual: List[Dict], corrected: List[Dict]):
    n = min(len(actual), len(corrected))
    return actual[:n], corrected[:n]


def causal_replay(actors: List[ActorTrack], environment: Dict[str, str],
                  scenario_id: str, data_label: DataLabel,
                  engine: str = DEFAULT_ENGINE, seed: int = 0,
                  faults: Optional[List[Dict]] = None) -> CausalReplayResult:
    actual = run_closed_loop(actors, environment, scenario_id, data_label,
                             engine=engine, seed=seed, corrected=False,
                             faults=faults)
    corrected = run_closed_loop(actors, environment, scenario_id, data_label,
                                engine=engine, seed=seed, corrected=True)

    ta, tc = _aligned(actual.trajectory, corrected.trajectory)
    if ta:
        pos_div = float(max(abs(a["x"] - c["x"]) + abs(a["y"] - c["y"])
                            for a, c in zip(ta, tc)))
        vel_div = float(max(abs(a["v"] - c["v"]) for a, c in zip(ta, tc)))
        acc_div = float(max(abs(a["a"] - c["a"]) for a, c in zip(ta, tc)))
    else:
        pos_div = vel_div = acc_div = 0.0
    # A truncated run means one branch collided and stopped early — that is
    # itself maximal behavioral divergence.
    if len(actual.trajectory) != len(corrected.trajectory):
        pos_div = max(pos_div, TRAJECTORY_DIFF_M * 2)

    ma, mc = actual.metrics, corrected.metrics
    sep_diff = _diff(ma.min_separation_m, mc.min_separation_m)
    col_prob_diff = ma.collision_probability - mc.collision_probability
    ttc_diff = _diff(ma.min_ttc_s, mc.min_ttc_s)
    brake_diff = ma.max_deceleration_mps2 - mc.max_deceleration_mps2

    q1 = acc_div > PLANNER_DIFF_MPS2
    q2 = q1 and (pos_div > TRAJECTORY_DIFF_M or vel_div > VELOCITY_DIFF_MPS)
    q3 = q2 and (ma.collision != mc.collision
                 or (sep_diff is not None and abs(sep_diff) > SEPARATION_DIFF_M)
                 or abs(col_prob_diff) > COLLISION_PROB_DIFF
                 or (ttc_diff is not None and abs(ttc_diff) > TTC_DIFF_S))

    chain = [
        {"question": "Would correct perception change the planner output?",
         "answer": q1,
         "evidence": f"max commanded-accel divergence {acc_div:.2f} m/s^2 "
                     f"(threshold {PLANNER_DIFF_MPS2})"},
        {"question": "Would that change the vehicle's behavior?",
         "answer": q2,
         "evidence": f"max trajectory divergence {pos_div:.2f} m, "
                     f"max velocity divergence {vel_div:.2f} m/s "
                     f"(thresholds {TRAJECTORY_DIFF_M} m / {VELOCITY_DIFF_MPS} m/s)"},
        {"question": "Would that change the safety outcome?",
         "answer": q3,
         "evidence": f"collision {ma.collision} vs {mc.collision}; "
                     f"min-separation diff "
                     f"{'n/a' if sep_diff is None else f'{sep_diff:+.2f} m'}; "
                     f"collision-probability diff {col_prob_diff:+.2f}; "
                     f"min-TTC diff "
                     f"{'n/a' if ttc_diff is None else f'{ttc_diff:+.2f} s'}"},
    ]

    return CausalReplayResult(
        scenario_id=scenario_id, data_label=data_label,
        actual=actual, corrected=corrected,
        diffs={
            "max_trajectory_divergence_m": round(pos_div, 3),
            "max_velocity_divergence_mps": round(vel_div, 3),
            "max_planner_divergence_mps2": round(acc_div, 3),
            "min_separation_diff_m": _round(sep_diff),
            "collision_probability_diff": round(col_prob_diff, 4),
            "min_ttc_diff_s": _round(ttc_diff),
            "max_braking_diff_mps2": round(brake_diff, 3),
            "collision_actual": ma.collision,
            "collision_corrected": mc.collision,
        },
        causal_chain=chain,
        verdict=CAUSAL_BEHAVIORAL if q3 else CAUSAL_METRIC_ONLY)


def _diff(a: Optional[float], b: Optional[float]) -> Optional[float]:
    if a is None or b is None:
        return None
    return a - b


def _round(v: Optional[float], nd: int = 3) -> Optional[float]:
    return None if v is None else round(float(v), nd)