Boundary-Radial Persistence Loss / run_experiment.py
Mechanism confirmed, baseline not beaten
1import json
2import numpy as np
3from boundary_radial import bars_from_radii, radial_loss, match_loss
4
5np.set_printoptions(precision=6, suppress=True)
6
7def fit_slope(x, y):
8 return float(np.polyfit(np.asarray(x), np.asarray(y), 1)[0])
9
10def main():
11 target = [3.0, 5.0]
12 # Prediction 1: identical boundary barcodes have exactly zero matching loss.
13 zero = radial_loss(target, target, unmatched=1.0)
14
15 # Prediction 2: a uniform radial displacement eps changes both endpoints,
16 # hence each matched circle costs 2|eps|, and two circles cost 4|eps|.
17 eps = np.linspace(0.0, 1.0, 11)
18 perturbed = [radial_loss([3.0 + e, 5.0 + e], target, unmatched=10.0)
19 for e in eps]
20 slope = fit_slope(eps[1:], perturbed[1:])
21 max_abs_fit_error = float(np.max(np.abs(np.asarray(perturbed) - 4*eps)))
22
23 # Prediction 3: with an empty target and unmatched penalty lambda, each
24 # predicted interval contributes lambda, so the loss is q*lambda.
25 lambdas = [0.25, 0.5, 1.0, 2.0]
26 qs = [0, 1, 2, 3, 4]
27 unmatched_table = []
28 for lam in lambdas:
29 vals = [radial_loss([3.0 + 2*j for j in range(q)], [], unmatched=lam)
30 for q in qs]
31 unmatched_table.append(vals)
32 # Fit all nonzero q/lambda values to q*lambda.
33 observed = np.array(unmatched_table)
34 expected = np.array([[q*lam for q in qs] for lam in lambdas])
35 unmatched_max_error = float(np.max(np.abs(observed-expected)))
36
37 # Matching is order-invariant: a permutation of target intervals has zero cost.
38 permutation_loss = radial_loss([3.0, 5.0, 7.0], [7.0, 3.0, 5.0], unmatched=10.0)
39
40 # Small baseline-vs-idea proxy: noisy candidate boundaries. The standard
41 # endpoint MSE and persistence L1 both recover the true radial vector here;
42 # this intentionally reports a sanity comparison, not a CNN claim.
43 rng = np.random.default_rng(7)
44 true = np.array([3.0, 5.0, 7.0])
45 noisy = true + rng.normal(0, 0.30, size=true.shape)
46 baseline_endpoint_l1 = float(np.sum(np.abs(noisy-true)))
47 idea_bar_loss = radial_loss(noisy.tolist(), true.tolist(), unmatched=10.0)
48
49 result = {
50 "zero_loss": zero,
51 "perturbation_eps": eps.tolist(),
52 "perturbation_loss": perturbed,
53 "predicted_perturbation_slope": 4.0,
54 "observed_perturbation_slope": slope,
55 "perturbation_max_abs_fit_error": max_abs_fit_error,
56 "unmatched_lambdas": lambdas,
57 "unmatched_qs": qs,
58 "unmatched_observed": observed.tolist(),
59 "unmatched_expected": expected.tolist(),
60 "unmatched_max_abs_error": unmatched_max_error,
61 "permutation_loss": permutation_loss,
62 "noisy_radii": noisy.tolist(),
63 "baseline_endpoint_l1": baseline_endpoint_l1,
64 "idea_persistence_loss": idea_bar_loss,
65 }
66 print(json.dumps(result, indent=2))
67 with open("results.json", "w") as f:
68 json.dump(result, f, indent=2)
69
70 assert zero < 1e-12
71 assert abs(slope - 4.0) < 1e-8 and max_abs_fit_error < 1e-8
72 assert unmatched_max_error < 1e-12
73 assert permutation_loss < 1e-12
74
75if __name__ == "__main__":
76 main()