RNCOA Aggregated Collision Loss / rncoa_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn.functional as F
  6
  7SEED = 531
  8random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  9torch.set_num_threads(4)
 10
 11
 12def softmax_extreme(x, tau):
 13    return tau * torch.logsumexp(x / tau, dim=-1)
 14
 15
 16def softmin_extreme(x, tau):
 17    return -tau * torch.logsumexp(-x / tau, dim=-1)
 18
 19
 20def rncoa_loss(j, obstacle_min, obstacle_max, gamma, M=2.0, tau=0.05,
 21               lambda_gamma=0.01, lambda_c=10.0):
 22    """The authoritative displayed L_RNCOA formula, including its signs."""
 23    smax = softmax_extreme(j, tau)
 24    smin = softmin_extreme(j, tau)
 25    g1, g2 = gamma[..., 0], gamma[..., 1]
 26    v1 = F.relu(obstacle_min - M*g1 - smax)
 27    v2 = F.relu(smin - obstacle_max - M*g2)
 28    coupling = F.relu(g1 + g2 - 1.0)
 29    return (v1 + v2 + lambda_gamma*(g1+g2) + lambda_c*coupling,
 30            {"smax": smax, "smin": smin, "v1": v1, "v2": v2,
 31             "coupling": coupling})
 32
 33
 34def independent_overlap_loss(j, obstacle_min, obstacle_max):
 35    """Per-vertex overlap hinge; it does not encode whole-body separation."""
 36    return torch.minimum(j - obstacle_min, obstacle_max - j).clamp_min(0).sum(-1)
 37
 38
 39def exact_body_collision(j, lo, hi):
 40    return bool((j.max().item() >= lo) and (j.min().item() <= hi))
 41
 42
 43def analytic_check():
 44    cases = {
 45        "left_safe": torch.tensor([[-1.4, -1.1, -1.3, -1.2]]),
 46        "collision": torch.tensor([[-0.3, 0.1, 0.3, -0.1]]),
 47        "right_safe": torch.tensor([[1.1, 1.3, 1.2, 1.4]]),
 48        "straddling": torch.tensor([[-1.2, -0.2, 0.2, 1.2]]),
 49    }
 50    lo, hi = 0.0, 1.0
 51    rows = {}
 52    for name, j in cases.items():
 53        gamma = torch.zeros((1, 2))
 54        total, parts = rncoa_loss(j, lo, hi, gamma)
 55        indep = independent_overlap_loss(j, lo, hi)
 56        # Residuals of the two displayed inequalities at gamma=0.
 57        residuals = [float((lo - j.max()).item()), float((j.min() - hi).item())]
 58        rows[name] = {"rncoa_loss": float(total.item()),
 59                      "independent_loss": float(indep.item()),
 60                      "collision": exact_body_collision(j[0], lo, hi),
 61                      "constraint_residuals": residuals,
 62                      "rncoa_v1": float(parts["v1"].item()),
 63                      "rncoa_v2": float(parts["v2"].item())}
 64    x = torch.tensor([[[-1.0, 0.0, 2.0, 3.0]]])
 65    errors = []
 66    for tau in [0.2, 0.1, 0.05, 0.01]:
 67        errors.append({"tau": tau,
 68                       "smax_error": abs(float(softmax_extreme(x, tau))-3.0),
 69                       "smin_error": abs(float(softmin_extreme(x, tau))-(-1.0))})
 70    return rows, errors
 71
 72
 73def sampled_confusion(n=10000):
 74    """Random intervals test whether zero stated loss identifies collision-free bodies."""
 75    rng = np.random.default_rng(SEED)
 76    # Four vertices are a translated rectangular body in one obstacle coordinate.
 77    centers = rng.uniform(-1.5, 2.5, n)
 78    halfwidth = 0.55
 79    j = torch.tensor(np.stack([centers-halfwidth, centers-halfwidth,
 80                               centers+halfwidth, centers+halfwidth], axis=1), dtype=torch.float32)
 81    with torch.no_grad():
 82        loss, _ = rncoa_loss(j, 0., 1., torch.zeros(n, 2), M=2., tau=.01,
 83                             lambda_gamma=0., lambda_c=10.)
 84    collision = ((j.min(1).values <= 1.) & (j.max(1).values >= 0.)).numpy()
 85    zero = (loss.numpy() < 1e-6)
 86    return {"n": n, "collision_rate": float(collision.mean()),
 87            "zero_loss_rate_on_collisions": float(zero[collision].mean()),
 88            "zero_loss_rate_on_safe": float(zero[~collision].mean()),
 89            "false_negative_count": int((zero & collision).sum()),
 90            "false_positive_count": int((~zero & ~collision).sum())}
 91
 92
 93def tiny_optimization(steps=180):
 94    results = {}
 95    for method in ["independent", "rncoa"]:
 96        torch.manual_seed(SEED)
 97        p = torch.nn.Parameter(torch.tensor([[0.45, 0.55, 0.65, 0.35]]))
 98        opt = torch.optim.Adam([p], lr=0.025)
 99        initial = p.detach().clone()
100        for _ in range(steps):
101            opt.zero_grad()
102            if method == "independent":
103                loss = independent_overlap_loss(p, 0., 1.).mean()
104            else:
105                loss, _ = rncoa_loss(p, 0., 1., torch.zeros((1,2)), M=2., tau=.05,
106                                     lambda_gamma=0., lambda_c=10.)
107                loss = loss.mean()
108            loss.backward(); opt.step()
109        final = p.detach()[0]
110        eval_loss = (independent_overlap_loss(final[None],0.,1.) if method == "independent"
111                     else rncoa_loss(final[None],0.,1.,torch.zeros(1,2),lambda_gamma=0.)[0])
112        results[method] = {"initial_training_loss": float(loss.detach() * 0 +
113                         (independent_overlap_loss(initial,0.,1.) if method=="independent" else
114                          rncoa_loss(initial,0.,1.,torch.zeros(1,2),lambda_gamma=0.)[0]).item()),
115            "final_eval_loss": float(eval_loss.item()),
116            "final_vertices": [round(float(x), 5) for x in final],
117            "exact_collision": exact_body_collision(final, 0., 1.)}
118    return results
119
120
121def main():
122    analytic, smooth = analytic_check()
123    out = {"seed": SEED, "analytic_cases": analytic,
124           "smooth_extreme_errors": smooth,
125           "sampled_confusion": sampled_confusion(),
126           "optimization": tiny_optimization(),
127           "interpretation": "With obstacle interval [0,1], the displayed inequalities encode the wrong disjunctive direction for collision avoidance: gamma=0 is feasible for bodies inside/straddling the obstacle and infeasible for bodies wholly outside."
128           }
129    Path("results.json").write_text(json.dumps(out, indent=2))
130    print(json.dumps(out, indent=2))
131
132if __name__ == "__main__":
133    main()