OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
XSectLookup.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2//
3// Copyright 2026 Caleb Buahin
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
55
56#ifndef OPENSWMM_XSECT_LOOKUP_HPP
57#define OPENSWMM_XSECT_LOOKUP_HPP
58
59namespace openswmm::xsect {
60
70inline double lookup_exact(double x, const double* t, int n) noexcept {
71 const double delta = 1.0 / static_cast<double>(n - 1); // same rounding as legacy
72 int i = static_cast<int>(x / delta); // DIVIDE (legacy), not x*inv_delta
73 if (i >= n - 1) return t[n - 1];
74
75 double x0 = i * delta;
76 double y = t[i] + (x - x0) * (t[i + 1] - t[i]) / delta; // grouping matches legacy
77
78 if (i < 2) { // quadratic refinement for low x
79 double x1 = (static_cast<double>(i) + 1.0) * delta;
80 double y2 = y + (x - x0) * (x - x1) / (delta * delta) *
81 (t[i] / 2.0 - t[i + 1] + t[i + 2] / 2.0);
82 if (y2 > 0.0) y = y2;
83 }
84 if (y < 0.0) y = 0.0;
85 return y;
86}
87
88// ============================================================================
89// Fast mode (opt-in, NOT bit-exact) — plan §6.
90//
91// Enabled per-build with -DSWMM_XSECT_FAST_LOOKUP. Substitutes the baseline
92// reciprocal-multiply form: normalize by a precomputed 1/y_full and index /
93// interpolate with `* inv_delta` (inv_delta = N-1 is an EXACT integer) instead
94// of `/ delta`. This removes ~3 IEEE divisions per element (measured ~25% faster
95// in the fused circular kernel on arm64) at the cost of ULP-level deviation from
96// legacy (measured well inside the abs 1e-8 / rel 1e-7 tolerance gate — see
97// tests/unit/engine/test_xsect_parity.cpp XSectFastMode). Default OFF: the
98// bit-exact `lookup_exact` path above is the shipped default.
99//
100// `lookup_fast` is always compiled (so the tolerance test can assert its bound
101// regardless of the active default), but is only *wired into the kernels* when
102// SWMM_XSECT_FAST_LOOKUP is defined.
103// ============================================================================
104
106inline double lookup_fast(double x, const double* t, int n) noexcept {
107 const double inv_delta = static_cast<double>(n - 1); // exact integer
108 const double delta = 1.0 / inv_delta;
109 if (x < 0.0) x = 0.0; else if (x > 1.0) x = 1.0; // fast mode pre-clamps
110 int i = static_cast<int>(x * inv_delta);
111 if (i >= n - 1) return t[n - 1];
112 double x0 = i * delta;
113 double y = t[i] + (x - x0) * (t[i + 1] - t[i]) * inv_delta;
114 if (i < 2) {
115 double x1 = (static_cast<double>(i) + 1.0) * delta;
116 double y2 = y + (x - x0) * (x - x1) * (inv_delta * inv_delta) *
117 (t[i] / 2.0 - t[i + 1] + t[i + 2] / 2.0);
118 if (y2 > 0.0) y = y2;
119 }
120 if (y < 0.0) y = 0.0;
121 return y;
122}
123
124// Mode-aware normalization. In the default (bit-exact) build `param` is y_full
125// and x = y/yFull via IEEE divide; under SWMM_XSECT_FAST_LOOKUP `param` is the
126// precomputed inv_y_full and x = depth*inv (clamped). The kernels feed the
127// matching per-link array (see XSectBatch.cpp norm_param()).
128inline double norm_x(double depth, double param) noexcept {
129#ifdef SWMM_XSECT_FAST_LOOKUP
130 double x = depth * param; // param == inv_y_full
131 if (x < 0.0) x = 0.0; else if (x > 1.0) x = 1.0;
132 return x;
133#else
134 return (param > 0.0 && depth > 0.0) ? (depth / param) : 0.0; // param == y_full
135#endif
136}
137
139inline double norm_lookup(double depth, double param, const double* t, int n) noexcept {
140#ifdef SWMM_XSECT_FAST_LOOKUP
141 return lookup_fast(norm_x(depth, param), t, n);
142#else
143 return lookup_exact(norm_x(depth, param), t, n);
144#endif
145}
146
147// ============================================================================
148// Bucket LUT for locate() — plan XSECT_LOOKUP_ACCEL §4 item A1.
149//
150// `locate()` is a bisection over a monotone table: ~log2(n) data-dependent
151// branches (~6 on the 51-row transect/built-in tables). The LUT replaces the
152// first few of them with one multiply + truncate: the table's VALUE range is
153// split into kBuckets equal spans, and for each span we precompute an index
154// bracket that provably contains locate()'s answer. A short bisection inside
155// that bracket finishes the job.
156//
157// **Index identity (why this stays bit-exact).** `locate()`'s answer is the
158// unique index `max{ j <= jLast : table[j] <= y }`. This routine returns the
159// same index for every input — it only narrows the search window — so the
160// interpolation arithmetic downstream is byte-for-byte unchanged. The proof
161// rests on one property: `bucket_of()` is monotone non-decreasing in y (an
162// affine map with a positive scale, then a truncating cast, then a clamp), and
163// the table is checked monotone non-decreasing at build time. Then
164//
165// lo[b] = max{ j : bucket_of(table[j]) < b } (0 if the set is empty)
166//
167// satisfies table[lo[b]] <= y for every y in bucket b, and — because
168// `bucket_of(table[j])` is non-decreasing in j, so {j : bucket_of <= b} is a
169// prefix — `lo[b+1] + 1` is exactly `min{ j : bucket_of(table[j]) > b }`, whose
170// table value is strictly greater than y. That is precisely the bisection
171// invariant, on a window of one or two table cells instead of the whole table.
172//
173// A table that fails the monotonicity check (or is degenerate) leaves `scale`
174// at 0, which callers read as "no LUT" and fall back to plain bisection — so a
175// pathological table can never change a result, only its speed.
176// ============================================================================
177
182struct LocateLut {
183 static constexpr int kBuckets = 64;
184
185 double t0 = 0.0;
186 double scale = 0.0;
187 int j_last = 0;
188 unsigned char lo[kBuckets + 1] = {};
189};
190
192inline int lut_bucket(const LocateLut& L, double y) noexcept {
193 int b = static_cast<int>((y - L.t0) * L.scale);
194 if (b < 0) b = 0;
195 else if (b >= LocateLut::kBuckets) b = LocateLut::kBuckets - 1;
196 return b;
197}
198
201inline int locate_bisect(double y, const double* table, int jLast) noexcept {
202 int j1 = 0;
203 int j2 = jLast;
204 if (y <= table[0]) return 0;
205 if (y >= table[jLast]) return jLast;
206 while (j2 - j1 > 1) {
207 int j = (j1 + j2) >> 1;
208 if (y >= table[j]) j1 = j;
209 else j2 = j;
210 }
211 return j1;
212}
213
218inline void build_locate_lut(LocateLut& L, const double* table, int jLast) noexcept {
219 L = LocateLut{};
220 if (!table || jLast < 2 || jLast > 254) return;
221 for (int j = 1; j <= jLast; ++j)
222 if (!(table[j] >= table[j - 1])) return; // non-monotone (or NaN) — no LUT
223
224 const double span = table[jLast] - table[0];
225 if (!(span > 0.0)) return; // flat table — nothing to bracket
226
227 L.t0 = table[0];
228 L.scale = static_cast<double>(LocateLut::kBuckets) / span;
229 L.j_last = jLast;
230 for (int b = 0; b <= LocateLut::kBuckets; ++b) {
231 int best = 0;
232 for (int j = 0; j <= jLast; ++j) {
233 if (lut_bucket(L, table[j]) >= b) break; // bucket(table[.]) is sorted
234 best = j;
235 }
236 L.lo[b] = static_cast<unsigned char>(best);
237 }
238}
239
241inline int locate_lut(double y, const double* table, int jLast,
242 const LocateLut& L) noexcept {
243 // `!(y > table[0])` (not `y <= table[0]`) also catches NaN, which plain
244 // bisection resolves to 0 because every `y >= table[j]` test is false.
245 if (!(y > table[0])) return 0;
246 if (y >= table[jLast]) return jLast;
247
248 const int b = lut_bucket(L, y);
249 int j1 = L.lo[b];
250 int j2 = L.lo[b + 1] + 1;
251 if (j2 > jLast) j2 = jLast;
252 while (j2 - j1 > 1) {
253 int jm = (j1 + j2) >> 1;
254 if (y >= table[jm]) j1 = jm;
255 else j2 = jm;
256 }
257 return j1;
258}
259
261inline int locate_maybe_lut(double y, const double* table, int jLast,
262 const LocateLut* L) noexcept {
263 if (L && L->scale > 0.0 && L->j_last == jLast)
264 return locate_lut(y, table, jLast, *L);
265 return locate_bisect(y, table, jLast);
266}
267
273inline void build_invlookup_lut(LocateLut& L, const double* table, int n_items) noexcept {
274 if (!table || n_items < 4) { L = LocateLut{}; return; }
275 int n = n_items;
276 if (table[n - 3] > table[n - 1]) n = n - 2;
277 build_locate_lut(L, table, n - 1);
278}
279
280} // namespace openswmm::xsect
281
282#endif // OPENSWMM_XSECT_LOOKUP_HPP
Definition XSectBatch.hpp:149
double lookup_fast(double x, const double *t, int n) noexcept
Reciprocal-multiply lookup (fast, NOT bit-exact — see §6).
Definition XSectLookup.hpp:106
double norm_lookup(double depth, double param, const double *t, int n) noexcept
Mode-aware normalize + table lookup (bit-exact by default; §6 fast if opted in).
Definition XSectLookup.hpp:139
double lookup_exact(double x, const double *t, int n) noexcept
Bit-exact transliteration of legacy xsect.c:lookup().
Definition XSectLookup.hpp:70
int locate_maybe_lut(double y, const double *table, int jLast, const LocateLut *L) noexcept
Dispatch: use the map when one was built for this exact table extent.
Definition XSectLookup.hpp:261
void build_locate_lut(LocateLut &L, const double *table, int jLast) noexcept
Definition XSectLookup.hpp:218
void build_invlookup_lut(LocateLut &L, const double *table, int n_items) noexcept
Definition XSectLookup.hpp:273
int locate_bisect(double y, const double *table, int jLast) noexcept
Definition XSectLookup.hpp:201
int lut_bucket(const LocateLut &L, double y) noexcept
Bucket index for a value — the ONE expression used by both build and query.
Definition XSectLookup.hpp:192
double norm_x(double depth, double param) noexcept
Definition XSectLookup.hpp:128
int locate_lut(double y, const double *table, int jLast, const LocateLut &L) noexcept
LUT-accelerated locate() — returns the identical index for every input.
Definition XSectLookup.hpp:241
double * y
Definition odesolve.c:28
Definition XSectLookup.hpp:182
int j_last
the jLast this map was built for
Definition XSectLookup.hpp:187
unsigned char lo[kBuckets+1]
lo[b] = max{ j : bucket(table[j]) < b }
Definition XSectLookup.hpp:188
double scale
kBuckets / (table[jLast] - t0); 0 == disabled
Definition XSectLookup.hpp:186
double t0
table[0] — the low end of the value range
Definition XSectLookup.hpp:185
static constexpr int kBuckets
Definition XSectLookup.hpp:183