Skip to content

Commit 0915985

Browse files
Kiguliclaude
andcommitted
Nonlinear sparse abstraction (interval arithmetic) + Van der Pol; fix ISSUE-0008
Generalize the abstraction: buildSparseReachGeneral takes a pluggable MeanBoundFn (sound per-dim mean enclosure over a source cell). Nonlinear systems supply it via interval arithmetic (abstraction::Ival + isquare); the affine nD builder now delegates to General with an affine mean functor (suite still green, so behaviour preserved). Van der Pol (nonlinear ARCH verification case) runs end-to-end through the sparse pipeline. Verified: VP interval mean bound is sound vs brute-force; VP interval-MC build is sound (pess <= opt, target=1); and benchmarks/validate_vp.cpp shows 6/6 start states have empirical reach (real nonlinear stochastic simulation) within the synthesized [lower, upper]. ISSUE-0008 (found by VP Monte-Carlo): grid-unaligned targets were double-counted (target-region aggregate + partial-overlap cells) -> lower bound too high (0.354 vs true 0.236). Fix: drop the aggregate; route window target cells to the absorbing TARGET (disjoint cells, no double count) in buildSparseReachGeneral and buildSparseReach1D. Second soundness bug caught by closed-loop validation that the internal differential tests missed. Suite: 44/44 cases, 603273 assertions, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d191219 commit 0915985

8 files changed

Lines changed: 312 additions & 44 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ __pycache__/
88
*.pyc
99
.pytest_cache/
1010
.docker-build.log
11+
12+
# partial/OOM baseline outputs
13+
tests/golden/

benchmarks/validate_vp.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// End-to-end verification on a NONLINEAR ARCH benchmark (Van der Pol, verification /
2+
// no input) through the sparse pipeline. Build the sparse interval-MC via interval-
3+
// arithmetic mean bounds, compute the reach interval [pessimistic, optimistic], then
4+
// simulate the real nonlinear stochastic system and check the empirical reach lies
5+
// within the synthesized bounds (abstraction soundness vs ground truth).
6+
//
7+
// Build: c++ -std=c++17 -O2 benchmarks/validate_vp.cpp \
8+
// src/abstraction.cpp src/solve.cpp src/omaximization.cpp src/graph_utils.cpp -o /tmp/vp
9+
#include "../src/abstraction.h"
10+
#include "../src/solve.h"
11+
#include <cstdio>
12+
#include <vector>
13+
#include <random>
14+
#include <cmath>
15+
16+
using namespace impact;
17+
using abstraction::Ival;
18+
using abstraction::isquare;
19+
20+
static void vpMean(const std::vector<double>& cl, const std::vector<double>& ch,
21+
const std::vector<double>&, std::vector<double>& muLo, std::vector<double>& muHi) {
22+
Ival X0(cl[0], ch[0]), X1(cl[1], ch[1]);
23+
Ival f0 = X0 + 0.1 * X1;
24+
Ival f1 = X1 + 0.1 * ((-1.0 * X0) + isquare(Ival(1.0) - X0) * X1);
25+
muLo = {f0.lo, f1.lo}; muHi = {f0.hi, f1.hi};
26+
}
27+
28+
int main() {
29+
abstraction::GridSpec g;
30+
g.dim_x = 2; g.dim_u = 0;
31+
g.xlb = {-5, -5}; g.xub = {5, 5}; g.eta = {0.2, 0.2};
32+
const double sd = 0.2;
33+
g.sigma = {sd, sd};
34+
g.tlo = {-1.2, -2.9}; g.thi = {-0.9, -2.0};
35+
36+
auto ab = abstraction::buildSparseReachGeneral(g, vpMean, 1e-7);
37+
auto lo = solve::maxReachPessimistic(ab.model, ab.targets, 1e-6);
38+
auto hi = solve::maxReachOptimistic(ab.model, ab.targets, 1e-6);
39+
40+
std::vector<int> Nd(2); std::vector<long long> stride(2); long long N = 1;
41+
for (int i = 0; i < 2; ++i) { Nd[i] = (int)std::llround((g.xub[i]-g.xlb[i])/g.eta[i]); stride[i]=N; N*=Nd[i]; }
42+
auto locate = [&](double x0, double x1, int& lin)->bool {
43+
double xs[2] = {x0, x1}; lin = 0;
44+
for (int i = 0; i < 2; ++i) { int j = (int)std::floor((xs[i]-g.xlb[i])/g.eta[i]);
45+
if (j < 0 || j >= Nd[i]) return false; lin += (int)(j*stride[i]); }
46+
return true;
47+
};
48+
auto inTargetCell = [&](double x0, double x1) {
49+
double xs[2] = {x0, x1};
50+
for (int i = 0; i < 2; ++i) { int j = (int)std::floor((xs[i]-g.xlb[i])/g.eta[i]);
51+
if (j < 0 || j >= Nd[i]) return false;
52+
double lo2 = g.xlb[i] + j*g.eta[i], hi2 = lo2 + g.eta[i];
53+
if (!(lo2 >= g.tlo[i]-1e-12 && hi2 <= g.thi[i]+1e-12)) return false; }
54+
return true;
55+
};
56+
57+
std::mt19937 rng(7);
58+
std::normal_distribution<double> gg(0.0, sd);
59+
const int TRIALS = 3000, HORIZON = 400;
60+
61+
printf("Van der Pol (nonlinear, verification): empirical reach vs [lower,upper]\n");
62+
printf("%-14s %8s %10s %8s %6s\n", "start(x0,x1)", "lower", "empirical", "upper", "in?");
63+
double tests[][2] = { {0.0,0.0}, {-1.0,-2.0}, {-2.0,-2.0}, {0.0,-3.0}, {-1.0,-1.0}, {2.0,0.0} };
64+
int ok = 0, n = 0;
65+
for (auto& st : tests) {
66+
int lin; if (!locate(st[0], st[1], lin)) continue;
67+
int succ = 0;
68+
for (int t = 0; t < TRIALS; ++t) {
69+
double x0 = st[0], x1 = st[1]; bool done = false;
70+
for (int k = 0; k < HORIZON && !done; ++k) {
71+
if (inTargetCell(x0, x1)) { ++succ; done = true; break; }
72+
int cl; if (!locate(x0, x1, cl)) { done = true; break; }
73+
double nx0 = x0 + 0.1*x1 + gg(rng);
74+
double nx1 = x1 + 0.1*(-x0 + (1-x0)*(1-x0)*x1) + gg(rng);
75+
x0 = nx0; x1 = nx1;
76+
}
77+
}
78+
double emp = (double)succ / TRIALS;
79+
double ci = 1.96 * std::sqrt(std::max(emp*(1-emp), 1e-9) / TRIALS);
80+
double L = lo.lower[lin], U = hi.upper[lin];
81+
bool good = (emp >= L - ci - 1e-2) && (emp <= U + ci + 1e-2);
82+
++n; ok += good;
83+
printf("(%4.1f,%4.1f) %8.3f %8.3f+-%.3f %8.3f %6s\n",
84+
st[0], st[1], L, emp, ci, U, good ? "yes" : "NO");
85+
}
86+
printf("\n%d/%d start states: empirical reach within [lower,upper].\n", ok, n);
87+
printf("states=%lld nnz=%lld\n", N, ab.nnz);
88+
return ok == n ? 0 : 1;
89+
}

issues/0008-target-double-count.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
id: ISSUE-0008
3+
title: Grid-unaligned target double-counted -> unsound (too-high) reach lower bound
4+
status: resolved
5+
severity: high
6+
labels: correctness, soundness, bug, abstraction
7+
created: 2026-06-25
8+
updated: 2026-06-25
9+
related:
10+
- src/abstraction.cpp
11+
- benchmarks/validate_vp.cpp
12+
---
13+
14+
## Summary
15+
The abstraction added BOTH a transition to the target REGION [tlo,thi] (an
16+
aggregate) AND transitions to each grid cell in the kernel window (skipping only
17+
cells FULLY inside the target). When the target region is not grid-aligned, cells
18+
that PARTIALLY overlap it are not skipped, so the overlap mass is counted twice:
19+
once in the target aggregate and once in the partial cell. This inflates reach and
20+
makes the (pessimistic) lower bound exceed the true value -> unsound.
21+
22+
## How it was found
23+
Van der Pol nonlinear end-to-end verification (`benchmarks/validate_vp.cpp`): from
24+
start (-1.0,-2.0) the synthesized lower bound was 0.354 but the empirical reach of
25+
the real nonlinear stochastic system was 0.236 < 0.354 — a lower-bound violation.
26+
(VP's target [-1.2,-0.9]x[-2.9,-2.0] is not aligned to the eta=0.2 grid.)
27+
28+
## Fix
29+
Remove the separate target-region aggregate. Transitions go to the DISJOINT grid
30+
cells in the window; a cell that is a target cell routes its mass to the absorbing
31+
TARGET. Disjoint cells cannot double-count. Applied in buildSparseReachGeneral
32+
(used by nD + nonlinear) and buildSparseReach1D. The target is thus the union of
33+
fully-inside target cells (a sound under-approximation of the region; the simulator
34+
uses the same target-cell criterion, keeping the comparison consistent).
35+
36+
## Verification of the fix
37+
- VP end-to-end: 6/6 start states have empirical reach within [lower,upper] (the
38+
fixed cell now has lower 0.106, empirical 0.236 in [0.106, 0.689]).
39+
- Unit suite still 44/44 (aligned-target tests unchanged: aggregate == window-routing
40+
when the target is grid-aligned).
41+
42+
## Classification
43+
`our-bug`, resolved. Second soundness bug caught by end-to-end Monte-Carlo
44+
validation (after ISSUE-0007); both were invisible to the internal differential
45+
tests. Reinforces keeping closed-loop validation in the verification toolbox.

issues/INDEX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ See [`README.md`](README.md) for conventions and the literature-counterexample p
99
| [0003](0003-pessimistic-interval-nature-trap.md) | Pessimistic interval iteration doesn't converge on nature-confinable ECs | resolved | medium | soundness, robust-ec |
1010
| [0006](0006-dense-transition-matrix-oom.md) | Dense transition matrix causes memory overflow even on "small" cases | in-progress | high | scalability, memory |
1111
| [0007](0007-abstraction-sink-too-loose.md) | Abstraction sink bound too loose -> reach under-estimation (found by Monte-Carlo) | resolved | high | correctness, abstraction |
12+
| [0008](0008-target-double-count.md) | Grid-unaligned target double-counted -> too-high reach lower bound (found by VP MC) | resolved | high | correctness, abstraction |
1213
| [0004](0004-ltlf-dfa-and-product.md) | Phase 2 — LTLf→DFA (done) + IMDP×DFA product + co-safe reachability | open | medium | enhancement, phase-2 |
1314
| [0005](0005-ltlf-dfa-state-explosion.md) | LTLf→DFA state explosion from syntactic normalization | resolved | high | correctness, bug |
1415

paper/IMPaCT-v2.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ Format per entry: `date — feature | decision + rationale | algorithm | refs |
201201
Also found a Monte-Carlo gotcha: validating an infinite-horizon reach needs a long
202202
sim horizon (truncation gave 0.73 vs true 1.0). Suite still 42/42, 54971 assertions.
203203

204+
- **Nonlinear abstraction + a real ARCH nonlinear benchmark (Van der Pol).**
205+
Generalized the abstraction (`buildSparseReachGeneral` + a `MeanBoundFn`): the
206+
per-dimension mean enclosure is pluggable, computed for nonlinear systems by
207+
**interval arithmetic** (`abstraction::Ival`, `isquare`). The affine n-D builder now
208+
delegates to it. VP (`x0'=x0+0.1x1`, `x1'=x1+0.1(-x0+(1-x0)²x1)`, no input) runs
209+
end-to-end through the sparse pipeline. Verified: the VP interval mean bound is
210+
SOUND (vs brute-force, 600k+ assertions); the VP interval-MC build is sound
211+
(pess ≤ opt, target = 1); and **6/6 start states have empirical reach (real
212+
nonlinear stochastic sim) within [lower, upper]** (`benchmarks/validate_vp.cpp`).
213+
- **ISSUE-0008 (found by VP MC, fixed).** Grid-unaligned targets were double-counted
214+
(separate target-region aggregate + partial-overlap cells) → lower bound too high
215+
(0.354 vs true 0.236). Fix: drop the aggregate; route window target cells to the
216+
absorbing TARGET (disjoint cells, no double count) in both builders. After the fix
217+
VP is 6/6; suite still 44/44, 603273 assertions. (Second soundness bug the
218+
closed-loop validation caught that internal differential tests missed.)
219+
204220
<!-- add new dated entries above this line -->
205221

206222
---

src/abstraction.cpp

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,17 @@ SparseReach buildSparseReach1D(const System1D& sys, double prune) {
7474
double muLo = std::min(m1, m2), muHi = std::max(m1, m2);
7575

7676
solve::ActionDist row;
77-
// mass into the target region [tlo,thi]
78-
Bound bT = transitionInterval1D(muLo, muHi, sys.sigma, sys.tlo, sys.thi);
79-
if (bT.hi > prune) row.push_back({TARGET, bT.lo, bT.hi});
80-
81-
// mass into non-target grid cells within the support window
77+
// Transitions to the (disjoint) grid cells in the support window; a target
78+
// cell routes its mass to the absorbing TARGET. No separate target-region
79+
// aggregate (which would double-count cells partially overlapping a
80+
// grid-unaligned target). See ISSUE-0008.
8281
int jmin = (int)std::floor((muLo - W - sys.xlb) / sys.eta);
8382
int jmax = (int)std::floor((muHi + W - sys.xlb) / sys.eta);
8483
jmin = std::max(0, jmin); jmax = std::min(N - 1, jmax);
8584
for (int j = jmin; j <= jmax; ++j) {
86-
if (isTargetCell(j)) continue; // counted in the TARGET aggregate
8785
double cl = cellLo(j), cr = cl + sys.eta;
8886
Bound b = transitionInterval1D(muLo, muHi, sys.sigma, cl, cr);
89-
if (b.hi > prune) row.push_back({j, b.lo, b.hi});
87+
if (b.hi > prune) row.push_back({isTargetCell(j) ? TARGET : j, b.lo, b.hi});
9088
}
9189

9290
// mass leaving the grid -> SINK (value 0), bounded TIGHTLY as the
@@ -104,22 +102,37 @@ SparseReach buildSparseReach1D(const System1D& sys, double prune) {
104102
return out;
105103
}
106104

107-
SparseReach buildSparseReachND(const SystemND& sys, double prune) {
108-
const int dx = sys.dim_x, du = sys.dim_u;
105+
// --- interval arithmetic (sound natural inclusion) for nonlinear mean bounds ----
106+
Ival operator+(const Ival& a, const Ival& b) { return Ival(a.lo + b.lo, a.hi + b.hi); }
107+
Ival operator-(const Ival& a, const Ival& b) { return Ival(a.lo - b.hi, a.hi - b.lo); }
108+
Ival operator*(const Ival& a, const Ival& b) {
109+
double p1 = a.lo*b.lo, p2 = a.lo*b.hi, p3 = a.hi*b.lo, p4 = a.hi*b.hi;
110+
return Ival(std::min({p1,p2,p3,p4}), std::max({p1,p2,p3,p4}));
111+
}
112+
Ival operator+(const Ival& a, double s) { return Ival(a.lo + s, a.hi + s); }
113+
Ival operator*(double s, const Ival& a) { return s >= 0 ? Ival(s*a.lo, s*a.hi) : Ival(s*a.hi, s*a.lo); }
114+
Ival isquare(const Ival& a) {
115+
if (a.lo >= 0) return Ival(a.lo*a.lo, a.hi*a.hi);
116+
if (a.hi <= 0) return Ival(a.hi*a.hi, a.lo*a.lo);
117+
double m = std::max(-a.lo, a.hi);
118+
return Ival(0.0, m*m);
119+
}
120+
121+
SparseReach buildSparseReachGeneral(const GridSpec& g, const MeanBoundFn& mean, double prune) {
122+
const int dx = g.dim_x, du = g.dim_u;
109123
std::vector<int> Nd(dx);
110124
std::vector<long long> stride(dx);
111125
long long N = 1;
112126
for (int i = 0; i < dx; ++i) {
113-
Nd[i] = std::max(1, (int)std::llround((sys.xub[i] - sys.xlb[i]) / sys.eta[i]));
127+
Nd[i] = std::max(1, (int)std::llround((g.xub[i] - g.xlb[i]) / g.eta[i]));
114128
stride[i] = N; N *= Nd[i];
115129
}
116130
const int TARGET = (int)N, SINK = (int)N + 1;
117131

118-
// input actions = Cartesian product of per-dimension input grids
119132
std::vector<std::vector<double>> upts(du);
120133
for (int k = 0; k < du; ++k) {
121-
int Mk = std::max(0, (int)std::llround((sys.uub[k] - sys.ulb[k]) / sys.ueta[k]));
122-
for (int t = 0; t <= Mk; ++t) upts[k].push_back(sys.ulb[k] + t * sys.ueta[k]);
134+
int Mk = std::max(0, (int)std::llround((g.uub[k] - g.ulb[k]) / g.ueta[k]));
135+
for (int t = 0; t <= Mk; ++t) upts[k].push_back(g.ulb[k] + t * g.ueta[k]);
123136
}
124137
std::vector<std::vector<double>> actions;
125138
if (du == 0) actions.push_back({});
@@ -138,61 +151,54 @@ SparseReach buildSparseReachND(const SystemND& sys, double prune) {
138151
out.model.assign((size_t)N + 2, {}); out.targets.insert(TARGET);
139152
out.actions = actions;
140153

141-
auto cellLoDim = [&](int i, int j) { return sys.xlb[i] + j * sys.eta[i]; };
154+
auto cellLoDim = [&](int i, int j) { return g.xlb[i] + j * g.eta[i]; };
142155
auto isTargetMi = [&](const std::vector<int>& mi) {
143156
for (int i = 0; i < dx; ++i) {
144-
double lo = cellLoDim(i, mi[i]), hi = lo + sys.eta[i];
145-
if (!(lo >= sys.tlo[i] - 1e-12 && hi <= sys.thi[i] + 1e-12)) return false;
157+
double lo = cellLoDim(i, mi[i]), hi = lo + g.eta[i];
158+
if (!(lo >= g.tlo[i] - 1e-12 && hi <= g.thi[i] + 1e-12)) return false;
146159
}
147160
return true;
148161
};
149162

150163
std::vector<int> mi(dx), wlo(dx), whi(dx), jt(dx);
151-
std::vector<double> muLo(dx), muHi(dx);
164+
std::vector<double> muLo(dx), muHi(dx), cellLo(dx), cellHi(dx);
152165
for (long long lin = 0; lin < N; ++lin) {
153166
for (int i = 0; i < dx; ++i) mi[i] = (int)((lin / stride[i]) % Nd[i]);
154167
if (isTargetMi(mi)) { out.model[lin].push_back({ {TARGET, 1.0, 1.0} }); out.nnz += 1; continue; }
168+
for (int i = 0; i < dx; ++i) { cellLo[i] = cellLoDim(i, mi[i]); cellHi[i] = cellLo[i] + g.eta[i]; }
155169

156170
for (const auto& u : actions) {
157-
for (int i = 0; i < dx; ++i) { // affine interval arithmetic for mean range
158-
double lo = sys.c.empty() ? 0.0 : sys.c[i], hi = lo;
159-
for (int j = 0; j < dx; ++j) {
160-
double a = sys.A[i][j], xl = cellLoDim(j, mi[j]), xr = xl + sys.eta[j];
161-
if (a >= 0) { lo += a * xl; hi += a * xr; } else { lo += a * xr; hi += a * xl; }
162-
}
163-
for (int k = 0; k < du; ++k) { lo += sys.B[i][k] * u[k]; hi += sys.B[i][k] * u[k]; }
164-
muLo[i] = lo; muHi[i] = hi;
165-
}
171+
mean(cellLo, cellHi, u, muLo, muHi); // SOUND per-dim mean enclosure
166172

173+
// Transitions to each grid cell in the per-dimension kernel window. Cells
174+
// are DISJOINT boxes, so there is no double counting. A window cell that is
175+
// a target cell routes its mass to the absorbing TARGET (no separate target
176+
// aggregate, which would double-count cells that partially overlap a
177+
// grid-unaligned target region).
167178
solve::ActionDist row;
168-
{ double tl = 1.0, th = 1.0; // target box aggregate
169-
for (int i = 0; i < dx; ++i) { Bound b = transitionInterval1D(muLo[i], muHi[i], sys.sigma[i], sys.tlo[i], sys.thi[i]); tl *= b.lo; th *= b.hi; }
170-
if (th > prune) row.push_back({TARGET, tl, th}); }
171-
172-
bool any = true; // per-dim kernel window
179+
bool any = true;
173180
for (int i = 0; i < dx; ++i) {
174-
double W = 6.0 * sys.sigma[i];
175-
wlo[i] = std::max(0, (int)std::floor((muLo[i] - W - sys.xlb[i]) / sys.eta[i]));
176-
whi[i] = std::min(Nd[i] - 1, (int)std::floor((muHi[i] + W - sys.xlb[i]) / sys.eta[i]));
181+
double W = 6.0 * g.sigma[i];
182+
wlo[i] = std::max(0, (int)std::floor((muLo[i] - W - g.xlb[i]) / g.eta[i]));
183+
whi[i] = std::min(Nd[i] - 1, (int)std::floor((muHi[i] + W - g.xlb[i]) / g.eta[i]));
177184
if (wlo[i] > whi[i]) any = false;
178185
jt[i] = wlo[i];
179186
}
180-
if (any) while (true) { // Cartesian product over windows
181-
if (!isTargetMi(jt)) {
182-
double pl = 1.0, ph = 1.0;
183-
for (int i = 0; i < dx; ++i) { double cl = cellLoDim(i, jt[i]), cr = cl + sys.eta[i];
184-
Bound b = transitionInterval1D(muLo[i], muHi[i], sys.sigma[i], cl, cr); pl *= b.lo; ph *= b.hi; }
185-
if (ph > prune) { long long lj = 0; for (int i = 0; i < dx; ++i) lj += (long long)jt[i] * stride[i];
187+
if (any) while (true) {
188+
double pl = 1.0, ph = 1.0;
189+
for (int i = 0; i < dx; ++i) { double cl = cellLoDim(i, jt[i]), cr = cl + g.eta[i];
190+
Bound b = transitionInterval1D(muLo[i], muHi[i], g.sigma[i], cl, cr); pl *= b.lo; ph *= b.hi; }
191+
if (ph > prune) {
192+
if (isTargetMi(jt)) { row.push_back({TARGET, pl, ph}); }
193+
else { long long lj = 0; for (int i = 0; i < dx; ++i) lj += (long long)jt[i] * stride[i];
186194
row.push_back({(int)lj, pl, ph}); }
187195
}
188196
int i = 0; for (; i < dx; ++i) { if (++jt[i] <= whi[i]) break; jt[i] = wlo[i]; }
189197
if (i == dx) break;
190198
}
191199

192-
// mass leaving the grid -> SINK, bounded tightly via the whole-grid-box
193-
// complement (product of per-dim in-grid probabilities).
194-
double gl = 1.0, gh = 1.0;
195-
for (int i = 0; i < dx; ++i) { Bound g = transitionInterval1D(muLo[i], muHi[i], sys.sigma[i], sys.xlb[i], sys.xub[i]); gl *= g.lo; gh *= g.hi; }
200+
double gl = 1.0, gh = 1.0; // outside-grid via grid-box complement
201+
for (int i = 0; i < dx; ++i) { Bound gg = transitionInterval1D(muLo[i], muHi[i], g.sigma[i], g.xlb[i], g.xub[i]); gl *= gg.lo; gh *= gg.hi; }
196202
row.push_back({SINK, std::max(0.0, 1.0 - gh), std::min(1.0, 1.0 - gl)});
197203
out.nnz += (long long)row.size();
198204
out.model[lin].push_back(std::move(row));
@@ -203,5 +209,28 @@ SparseReach buildSparseReachND(const SystemND& sys, double prune) {
203209
return out;
204210
}
205211

212+
SparseReach buildSparseReachND(const SystemND& sys, double prune) {
213+
GridSpec g;
214+
g.dim_x = sys.dim_x; g.dim_u = sys.dim_u;
215+
g.xlb = sys.xlb; g.xub = sys.xub; g.eta = sys.eta;
216+
g.ulb = sys.ulb; g.uub = sys.uub; g.ueta = sys.ueta;
217+
g.sigma = sys.sigma; g.tlo = sys.tlo; g.thi = sys.thi;
218+
const auto A = sys.A; const auto B = sys.B; const auto c = sys.c;
219+
const int dx = sys.dim_x, du = sys.dim_u;
220+
MeanBoundFn affine = [A, B, c, dx, du](const std::vector<double>& cl, const std::vector<double>& ch,
221+
const std::vector<double>& u,
222+
std::vector<double>& muLo, std::vector<double>& muHi) {
223+
muLo.assign(dx, 0.0); muHi.assign(dx, 0.0);
224+
for (int i = 0; i < dx; ++i) {
225+
double lo = c.empty() ? 0.0 : c[i], hi = lo;
226+
for (int j = 0; j < dx; ++j) { double a = A[i][j];
227+
if (a >= 0) { lo += a * cl[j]; hi += a * ch[j]; } else { lo += a * ch[j]; hi += a * cl[j]; } }
228+
for (int k = 0; k < du; ++k) { lo += B[i][k] * u[k]; hi += B[i][k] * u[k]; }
229+
muLo[i] = lo; muHi[i] = hi;
230+
}
231+
};
232+
return buildSparseReachGeneral(g, affine, prune);
233+
}
234+
206235
} // namespace abstraction
207236
} // namespace impact

0 commit comments

Comments
 (0)