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
76inline double lookup_exact(double x, const double* t, int n) noexcept {
77 const double delta = 1.0 / static_cast<double>(n - 1); // same rounding as legacy
78 int i = static_cast<int>(x / delta); // DIVIDE (legacy), not x*inv_delta
79 if (i >= n - 1)
80 return t[n - 1];
81
82 double x0 = i * delta;
83 double y = t[i] + (x - x0) * (t[i + 1] - t[i]) / delta; // grouping matches legacy
84
85 if (i < 2) { // quadratic refinement for low x
86 double x1 = (static_cast<double>(i) + 1.0) * delta;
87 double y2 =
88 y + (x - x0) * (x - x1) / (delta * delta) * (t[i] / 2.0 - t[i + 1] + t[i + 2] / 2.0);
89 if (y2 > 0.0)
90 y = y2;
91 }
92 if (y < 0.0)
93 y = 0.0;
94 return y;
95}
96
97// ============================================================================
98// Fast mode (opt-in, NOT bit-exact) — plan §6.
99//
100// Enabled per-build with -DSWMM_XSECT_FAST_LOOKUP. Substitutes the baseline
101// reciprocal-multiply form: normalize by a precomputed 1/y_full and index /
102// interpolate with `* inv_delta` (inv_delta = N-1 is an EXACT integer) instead
103// of `/ delta`. This removes ~3 IEEE divisions per element (measured ~25% faster
104// in the fused circular kernel on arm64) at the cost of ULP-level deviation from
105// legacy (measured well inside the abs 1e-8 / rel 1e-7 tolerance gate — see
106// tests/unit/engine/test_xsect_parity.cpp XSectFastMode). Default OFF: the
107// bit-exact `lookup_exact` path above is the shipped default.
108//
109// `lookup_fast` is always compiled (so the tolerance test can assert its bound
110// regardless of the active default), but is only *wired into the kernels* when
111// SWMM_XSECT_FAST_LOOKUP is defined.
112// ============================================================================
113
126inline double lookup_fast(double x, const double* t, int n) noexcept {
127 const double inv_delta = static_cast<double>(n - 1); // exact integer
128 const double delta = 1.0 / inv_delta;
129 if (x < 0.0)
130 x = 0.0;
131 else if (x > 1.0)
132 x = 1.0; // fast mode pre-clamps
133 int i = static_cast<int>(x * inv_delta);
134 if (i >= n - 1)
135 return t[n - 1];
136 double x0 = i * delta;
137 double y = t[i] + (x - x0) * (t[i + 1] - t[i]) * inv_delta;
138 if (i < 2) {
139 double x1 = (static_cast<double>(i) + 1.0) * delta;
140 double y2 = y + (x - x0) * (x - x1) * (inv_delta * inv_delta) *
141 (t[i] / 2.0 - t[i + 1] + t[i + 2] / 2.0);
142 if (y2 > 0.0)
143 y = y2;
144 }
145 if (y < 0.0)
146 y = 0.0;
147 return y;
148}
149
150// Mode-aware normalization. In the default (bit-exact) build `param` is y_full
151// and x = y/yFull via IEEE divide; under SWMM_XSECT_FAST_LOOKUP `param` is the
152// precomputed inv_y_full and x = depth*inv (clamped). The kernels feed the
153// matching per-link array (see XSectBatch.cpp norm_param()).
166inline double norm_x(double depth, double param) noexcept {
167#ifdef SWMM_XSECT_FAST_LOOKUP
168 double x = depth * param; // param == inv_y_full
169 if (x < 0.0)
170 x = 0.0;
171 else if (x > 1.0)
172 x = 1.0;
173 return x;
174#else
175 return (param > 0.0 && depth > 0.0) ? (depth / param) : 0.0; // param == y_full
176#endif
177}
178
189inline double norm_lookup(double depth, double param, const double* t, int n) noexcept {
190#ifdef SWMM_XSECT_FAST_LOOKUP
191 return lookup_fast(norm_x(depth, param), t, n);
192#else
193 return lookup_exact(norm_x(depth, param), t, n);
194#endif
195}
196
197// ============================================================================
198// Bucket LUT for locate() — plan XSECT_LOOKUP_ACCEL §4 item A1.
199//
200// `locate()` is a bisection over a monotone table: ~log2(n) data-dependent
201// branches (~6 on the 51-row transect/built-in tables). The LUT replaces the
202// first few of them with one multiply + truncate: the table's VALUE range is
203// split into kBuckets equal spans, and for each span we precompute an index
204// bracket that provably contains locate()'s answer. A short bisection inside
205// that bracket finishes the job.
206//
207// **Index identity (why this stays bit-exact).** `locate()`'s answer is the
208// unique index `max{ j <= jLast : table[j] <= y }`. This routine returns the
209// same index for every input — it only narrows the search window — so the
210// interpolation arithmetic downstream is byte-for-byte unchanged. The proof
211// rests on one property: `bucket_of()` is monotone non-decreasing in y (an
212// affine map with a positive scale, then a truncating cast, then a clamp), and
213// the table is checked monotone non-decreasing at build time. Then
214//
215// lo[b] = max{ j : bucket_of(table[j]) < b } (0 if the set is empty)
216//
217// satisfies table[lo[b]] <= y for every y in bucket b, and — because
218// `bucket_of(table[j])` is non-decreasing in j, so {j : bucket_of <= b} is a
219// prefix — `lo[b+1] + 1` is exactly `min{ j : bucket_of(table[j]) > b }`, whose
220// table value is strictly greater than y. That is precisely the bisection
221// invariant, on a window of one or two table cells instead of the whole table.
222//
223// A table that fails the monotonicity check (or is degenerate) leaves `scale`
224// at 0, which callers read as "no LUT" and fall back to plain bisection — so a
225// pathological table can never change a result, only its speed.
226// ============================================================================
227
232struct LocateLut {
233 static constexpr int kBuckets = 64;
234
235 double t0 = 0.0;
236 double scale = 0.0;
237 int j_last = 0;
238 unsigned char lo[kBuckets + 1] = {};
239};
240
252inline int lut_bucket(const LocateLut& L, double y) noexcept {
253 int b = static_cast<int>((y - L.t0) * L.scale);
254 if (b < 0)
255 b = 0;
256 else if (b >= LocateLut::kBuckets)
257 b = LocateLut::kBuckets - 1;
258 return b;
259}
260
274inline int locate_bisect(double y, const double* table, int jLast) noexcept {
275 int j1 = 0;
276 int j2 = jLast;
277 if (y <= table[0])
278 return 0;
279 if (y >= table[jLast])
280 return jLast;
281 while (j2 - j1 > 1) {
282 int j = (j1 + j2) >> 1;
283 if (y >= table[j])
284 j1 = j;
285 else
286 j2 = j;
287 }
288 return j1;
289}
290
303inline void build_locate_lut(LocateLut& L, const double* table, int jLast) noexcept {
304 L = LocateLut{};
305 if (!table || jLast < 2 || jLast > 254)
306 return;
307 for (int j = 1; j <= jLast; ++j)
308 if (!(table[j] >= table[j - 1]))
309 return; // non-monotone (or NaN) — no LUT
310
311 const double span = table[jLast] - table[0];
312 if (!(span > 0.0))
313 return; // flat table — nothing to bracket
314
315 L.t0 = table[0];
316 L.scale = static_cast<double>(LocateLut::kBuckets) / span;
317 L.j_last = jLast;
318 for (int b = 0; b <= LocateLut::kBuckets; ++b) {
319 int best = 0;
320 for (int j = 0; j <= jLast; ++j) {
321 if (lut_bucket(L, table[j]) >= b)
322 break; // bucket(table[.]) is sorted
323 best = j;
324 }
325 L.lo[b] = static_cast<unsigned char>(best);
326 }
327}
328
343inline int locate_lut(double y, const double* table, int jLast, const LocateLut& L) noexcept {
344 // `!(y > table[0])` (not `y <= table[0]`) also catches NaN, which plain
345 // bisection resolves to 0 because every `y >= table[j]` test is false.
346 if (!(y > table[0]))
347 return 0;
348 if (y >= table[jLast])
349 return jLast;
350
351 const int b = lut_bucket(L, y);
352 int j1 = L.lo[b];
353 int j2 = L.lo[b + 1] + 1;
354 if (j2 > jLast)
355 j2 = jLast;
356 while (j2 - j1 > 1) {
357 int jm = (j1 + j2) >> 1;
358 if (y >= table[jm])
359 j1 = jm;
360 else
361 j2 = jm;
362 }
363 return j1;
364}
365
378inline int locate_maybe_lut(double y, const double* table, int jLast, const LocateLut* L) noexcept {
379 if (L && L->scale > 0.0 && L->j_last == jLast)
380 return locate_lut(y, table, jLast, *L);
381 return locate_bisect(y, table, jLast);
382}
383
395inline void build_invlookup_lut(LocateLut& L, const double* table, int n_items) noexcept {
396 if (!table || n_items < 4) {
397 L = LocateLut{};
398 return;
399 }
400 int n = n_items;
401 if (table[n - 3] > table[n - 1])
402 n = n - 2;
403 build_locate_lut(L, table, n - 1);
404}
405
406} // namespace openswmm::xsect
407
408#endif // OPENSWMM_XSECT_LOOKUP_HPP
Definition XSectBatch.hpp:158
double lookup_fast(double x, const double *t, int n) noexcept
Interpolate normalized geometry using reciprocal multiplication and endpoint clamps.
Definition XSectLookup.hpp:126
double norm_lookup(double depth, double param, const double *t, int n) noexcept
Normalize a depth and dispatch the build-selected table interpolation.
Definition XSectLookup.hpp:189
double lookup_exact(double x, const double *t, int n) noexcept
Interpolate a normalized geometry table with the legacy arithmetic grouping.
Definition XSectLookup.hpp:76
int locate_maybe_lut(double y, const double *table, int jLast, const LocateLut *L) noexcept
Use a matching enabled bucket map or fall back to plain bisection.
Definition XSectLookup.hpp:378
void build_locate_lut(LocateLut &L, const double *table, int jLast) noexcept
Build a byte-indexed acceleration map for a monotone geometry table.
Definition XSectLookup.hpp:303
void build_invlookup_lut(LocateLut &L, const double *table, int n_items) noexcept
Build a map for the actual truncated extent used by inverse geometry lookup.
Definition XSectLookup.hpp:395
int locate_bisect(double y, const double *table, int jLast) noexcept
Find a lower bracketing index in a nondecreasing table.
Definition XSectLookup.hpp:274
int lut_bucket(const LocateLut &L, double y) noexcept
Map a value to one of64 locate-acceleration buckets.
Definition XSectLookup.hpp:252
double norm_x(double depth, double param) noexcept
Normalize a depth with the selected exact or fast-lookup operand convention.
Definition XSectLookup.hpp:166
int locate_lut(double y, const double *table, int jLast, const LocateLut &L) noexcept
Find the same lower bracket using a prebuilt bucket map.
Definition XSectLookup.hpp:343
double * y
Definition odesolve.c:37
Definition XSectLookup.hpp:232
int j_last
the jLast this map was built for
Definition XSectLookup.hpp:237
unsigned char lo[kBuckets+1]
lo[b] = max{ j : bucket(table[j]) < b }
Definition XSectLookup.hpp:238
double scale
kBuckets / (table[jLast] - t0); 0 == disabled
Definition XSectLookup.hpp:236
double t0
table[0] — the low end of the value range
Definition XSectLookup.hpp:235
static constexpr int kBuckets
Definition XSectLookup.hpp:233