RNCOA Aggregated Collision Loss / rncoa_experiment.py
Mechanism failed
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()