|
| 1 | +// End-to-end verification of the sparse pipeline against the REAL continuous system |
| 2 | +// (ISSUE-0006 / "actually verify the approaches"). We build a sparse IMDP for an |
| 3 | +// affine diagonal-Gaussian system, synthesize a robust controller, then simulate the |
| 4 | +// CONTINUOUS closed loop under that controller and check the empirical reach |
| 5 | +// probability lies within the synthesized [lower, upper] bounds (the abstraction's |
| 6 | +// soundness guarantee, validated against ground truth). |
| 7 | +// |
| 8 | +// Build: c++ -std=c++17 -O2 benchmarks/validate_montecarlo.cpp \ |
| 9 | +// src/abstraction.cpp src/solve.cpp src/omaximization.cpp src/graph_utils.cpp -o /tmp/mc |
| 10 | +#include "../src/abstraction.h" |
| 11 | +#include "../src/solve.h" |
| 12 | +#include "../src/omaximization.h" |
| 13 | +#include <cstdio> |
| 14 | +#include <vector> |
| 15 | +#include <random> |
| 16 | +#include <cmath> |
| 17 | + |
| 18 | +using namespace impact; |
| 19 | + |
| 20 | +int main() { |
| 21 | + // 2-D robot-style affine system: x0'=0.9 x0 + 1.4 u0 ; x1'=0.8 x1 + 1.4 u1 + noise |
| 22 | + abstraction::SystemND s; |
| 23 | + s.dim_x = 2; s.dim_u = 2; |
| 24 | + s.xlb = {-3, -3}; s.xub = {3, 3}; s.eta = {0.3, 0.3}; |
| 25 | + s.ulb = {-1, -1}; s.uub = {1, 1}; s.ueta = {0.5, 0.5}; // 5 pts/dim |
| 26 | + s.A = {{0.9, 0.0}, {0.0, 0.8}}; |
| 27 | + s.B = {{1.4, 0.0}, {0.0, 1.4}}; |
| 28 | + s.c = {0.0, 0.0}; |
| 29 | + const double sd = 0.8; // high noise -> reach values in (0,1): discriminating check |
| 30 | + s.sigma = {sd, sd}; |
| 31 | + s.tlo = {2.0, 2.0}; s.thi = {3.0, 3.0}; |
| 32 | + |
| 33 | + auto ab = abstraction::buildSparseReachND(s, 1e-9); |
| 34 | + auto val = solve::maxReachPessimistic(ab.model, ab.targets, 1e-6); // robust controller |
| 35 | + |
| 36 | + // grid geometry |
| 37 | + std::vector<int> Nd(2); std::vector<long long> stride(2); long long N = 1; |
| 38 | + for (int i = 0; i < 2; ++i) { Nd[i] = (int)std::llround((s.xub[i]-s.xlb[i])/s.eta[i]); stride[i]=N; N*=Nd[i]; } |
| 39 | + auto locate = [&](double x0, double x1, int& lin)->bool { |
| 40 | + double xs[2] = {x0, x1}; lin = 0; |
| 41 | + for (int i = 0; i < 2; ++i) { int j = (int)std::floor((xs[i]-s.xlb[i])/s.eta[i]); |
| 42 | + if (j < 0 || j >= Nd[i]) return false; lin += (int)(j*stride[i]); } |
| 43 | + return true; |
| 44 | + }; |
| 45 | + // success == entering a TARGET CELL (a cell whose box is fully inside the target |
| 46 | + // region), matching the abstraction's absorbing TARGET exactly. This alignment is |
| 47 | + // required for the robust lower-bound performance guarantee to apply. |
| 48 | + auto inTargetCell = [&](double x0, double x1) { |
| 49 | + double xs[2] = {x0, x1}; |
| 50 | + for (int i = 0; i < 2; ++i) { |
| 51 | + int j = (int)std::floor((xs[i]-s.xlb[i])/s.eta[i]); |
| 52 | + if (j < 0 || j >= Nd[i]) return false; |
| 53 | + double lo = s.xlb[i] + j*s.eta[i], hi = lo + s.eta[i]; |
| 54 | + if (!(lo >= s.tlo[i]-1e-12 && hi <= s.thi[i]+1e-12)) return false; |
| 55 | + } |
| 56 | + return true; |
| 57 | + }; |
| 58 | + |
| 59 | + // extract robust policy: argmax_a min_nature E[V_lower] (omax Sense::Min) |
| 60 | + std::vector<int> policy(N, 0); |
| 61 | + for (long long c = 0; c < N; ++c) { |
| 62 | + const auto& acts = ab.model[c]; |
| 63 | + double best = -1; int bi = 0; |
| 64 | + for (size_t a = 0; a < acts.size(); ++a) { |
| 65 | + std::vector<double> lo, hi, V; |
| 66 | + for (const auto& iv : acts[a]) { lo.push_back(iv.lo); hi.push_back(iv.hi); V.push_back(val.lower[iv.to]); } |
| 67 | + double q = omax::optimize(lo, hi, V, omax::Sense::Min).value; |
| 68 | + if (q > best) { best = q; bi = (int)a; } |
| 69 | + } |
| 70 | + policy[c] = bi; |
| 71 | + } |
| 72 | + |
| 73 | + std::mt19937 rng(2026); |
| 74 | + std::normal_distribution<double> g0(0.0, sd), g1(0.0, sd); |
| 75 | + const int TRIALS = 4000, HORIZON = 2000; |
| 76 | + |
| 77 | + printf("%-14s %8s %10s %8s %7s\n", "start(x0,x1)", "lower", "empirical", "upper", "in?"); |
| 78 | + int startsChecked = 0, startsOK = 0; |
| 79 | + double tests[][2] = { {-2.5,-2.5}, {-1.0,-1.0}, {0.0,0.0}, {1.0,1.0}, {1.5,1.5}, {-2.5,2.5} }; |
| 80 | + for (auto& st : tests) { |
| 81 | + int lin; if (!locate(st[0], st[1], lin)) continue; |
| 82 | + int succ = 0; |
| 83 | + for (int t = 0; t < TRIALS; ++t) { |
| 84 | + double x0 = st[0], x1 = st[1]; bool done = false; |
| 85 | + for (int k = 0; k < HORIZON && !done; ++k) { |
| 86 | + if (inTargetCell(x0, x1)) { ++succ; done = true; break; } |
| 87 | + int cl; if (!locate(x0, x1, cl)) { done = true; break; } // left grid -> fail |
| 88 | + const auto& u = ab.actions[policy[cl]]; |
| 89 | + double nx0 = 0.9*x0 + 1.4*u[0] + g0(rng); |
| 90 | + double nx1 = 0.8*x1 + 1.4*u[1] + g1(rng); |
| 91 | + x0 = nx0; x1 = nx1; |
| 92 | + } |
| 93 | + } |
| 94 | + double emp = (double)succ / TRIALS; |
| 95 | + double ci = 1.96 * std::sqrt(std::max(emp*(1-emp), 1e-9) / TRIALS); |
| 96 | + double lo = val.lower[lin], hi = val.upper[lin]; |
| 97 | + bool ok = (emp >= lo - ci - 5e-3); // robust guarantee: real (benign) noise >= worst-case lower |
| 98 | + ++startsChecked; startsOK += ok; |
| 99 | + printf("(%4.1f,%4.1f) %8.3f %8.3f+-%.3f %8.3f %7s\n", |
| 100 | + st[0], st[1], lo, emp, ci, hi, ok ? "yes" : "NO"); |
| 101 | + } |
| 102 | + printf("\n%d/%d start states: empirical reach >= robust lower bound (performance guarantee).\n", startsOK, startsChecked); |
| 103 | + printf("states=%lld actions=%zu nnz=%lld\n", N, ab.actions.size(), ab.nnz); |
| 104 | + return startsOK == startsChecked ? 0 : 1; |
| 105 | +} |
0 commit comments