OpenSWMM Engine  6.0.0-alpha.3
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.3)
Loading...
Searching...
No Matches
TableData.hpp
Go to the documentation of this file.
1
31
32#ifndef OPENSWMM_ENGINE_TABLE_DATA_HPP
33#define OPENSWMM_ENGINE_TABLE_DATA_HPP
34
36#include <string>
37#include <vector>
38#include <unordered_map>
39#include <cstdio>
40#include <cstdint>
41#include <cassert>
42#include <algorithm>
43#include <cmath>
44
45namespace openswmm {
46
47// ============================================================================
48// Table types (matching legacy SWMM enums.h)
49// ============================================================================
50
69
70// ============================================================================
71// TableCursor
72// ============================================================================
73
86 int index = 0;
87 int direction = +1;
88
89 void reset() noexcept { index = 0; direction = +1; }
90};
91
92// ============================================================================
93// Table
94// ============================================================================
95
111 std::vector<double> x;
112 std::vector<double> y;
113 int num_cols = 1;
114 std::size_t file_row_start = 0;
115
116 std::size_t num_rows() const noexcept { return x.size(); }
117 bool empty() const noexcept { return x.empty(); }
118};
119
120struct Table {
121 std::string id;
123
129 std::string comment;
130 std::vector<double> x;
131 std::vector<double> y;
133
134 // ---- File-backed time series support ----
135 bool is_file_based = false;
136 std::FILE* file_handle = nullptr;
142 double dx_min = 0.0;
143 double x_min = 0.0;
144 double x_max = 0.0;
145 int num_cols = 1;
146 std::size_t total_rows = 0;
147 std::size_t num_cache_rows = 8192;
149 std::vector<long> row_offsets;
150 std::vector<std::string> column_ids;
151 std::unordered_map<std::string, int> column_map;
152
153 static constexpr std::size_t INDEX_STRIDE = 4096;
154
156 std::size_t size() const noexcept { return x.size(); }
157
159 bool empty() const noexcept { return x.empty(); }
160};
161
162// ============================================================================
163// Cursor-optimized lookup
164// ============================================================================
165
187inline double table_lookup_cursor(Table& tbl, double x_query) noexcept {
188 const int n = static_cast<int>(tbl.x.size());
189 if (n == 0) return 0.0;
190 if (n == 1) return tbl.y[0];
191
192 // Clamp below first entry
193 if (x_query <= tbl.x[0]) {
194 tbl.cursor.index = 0;
195 tbl.cursor.direction = +1;
196 return tbl.y[0];
197 }
198
199 // Clamp above last entry — strictly above only: legacy table_lookup
200 // (src/legacy/engine/table.c:400) interpolates the last segment when
201 // x == x[n-1] (its scan condition is `x <= x2`), so equality must fall
202 // through to the interpolation below for bit parity.
203 if (x_query > tbl.x[n - 1]) {
204 tbl.cursor.index = n - 1;
205 tbl.cursor.direction = -1;
206 return tbl.y[n - 1];
207 }
208
209 // Start from cursor position and seek in the most likely direction
210 int idx = std::clamp(tbl.cursor.index, 0, n - 2);
211
212 // Forward seek
213 while (idx < n - 1 && tbl.x[idx + 1] < x_query) {
214 ++idx;
215 tbl.cursor.direction = +1;
216 }
217
218 // Backward seek — `>=` so a query landing exactly on an interior knot
219 // selects the segment ENDING at that knot, matching legacy table_lookup's
220 // first-match linear scan (`if (x <= x2) return table_interpolate(...)`,
221 // src/legacy/engine/table.c:426).
222 while (idx > 0 && tbl.x[idx] >= x_query) {
223 --idx;
224 tbl.cursor.direction = -1;
225 }
226
227 tbl.cursor.index = idx;
228
229 // PARITY: op-for-op transliteration of legacy table_interpolate
230 // (src/legacy/engine/table.c:54-67): multiply THEN divide —
231 // y1 + (x - x1) * (y2 - y1) / dx
232 // (the previous t=(x-x1)/dx; y1+t*(y2-y1) form rounds differently by
233 // 1 ULP), and the legacy degenerate-segment guard |dx| < 1e-20 → mean.
234 const double x1 = tbl.x[idx], y1 = tbl.y[idx];
235 const double x2 = tbl.x[idx + 1], y2 = tbl.y[idx + 1];
236 const double dx = x2 - x1;
237 if (std::fabs(dx) < 1.0e-20) return (y1 + y2) / 2.;
238 return y1 + (x_query - x1) * (y2 - y1) / dx;
239}
240
258inline double table_tseries_lookup_cursor(Table& tbl, double x_query) noexcept {
259 const int n = static_cast<int>(tbl.x.size());
260 if (n == 0) return 0.0;
261
262 // Before first entry or after last entry → 0 (series not active)
263 if (x_query <= tbl.x[0]) {
264 tbl.cursor.index = 0;
265 tbl.cursor.direction = +1;
266 return (x_query < tbl.x[0]) ? 0.0 : tbl.y[0];
267 }
268 if (x_query >= tbl.x[n - 1]) {
269 tbl.cursor.index = n - 1;
270 tbl.cursor.direction = -1;
271 return (x_query > tbl.x[n - 1]) ? 0.0 : tbl.y[n - 1];
272 }
273
274 // Delegate to cursor-based linear interpolation for the in-range case
275 return table_lookup_cursor(tbl, x_query);
276}
277
292inline double table_step_cursor(Table& tbl, double x_query) noexcept {
293 const int n = static_cast<int>(tbl.x.size());
294 if (n == 0) return 0.0;
295 if (n == 1) return tbl.y[0];
296
297 // Before first entry → 0 (no rain before first recorded value)
298 if (x_query < tbl.x[0]) {
299 tbl.cursor.index = 0;
300 tbl.cursor.direction = +1;
301 return 0.0;
302 }
303
304 // At or past last entry → return last value.
305 // The caller (Gage.cpp) handles the rain interval cutoff and returns 0
306 // after entry_time + rainInterval. This allows the last entry's value to
307 // be used for its full recording interval before going to zero.
308 if (x_query >= tbl.x[n - 1]) {
309 tbl.cursor.index = n - 1;
310 tbl.cursor.direction = -1;
311 return tbl.y[n - 1];
312 }
313
314 // Seek to the interval containing x_query: x[idx] <= x_query < x[idx+1]
315 int idx = std::clamp(tbl.cursor.index, 0, n - 2);
316
317 while (idx < n - 1 && tbl.x[idx + 1] <= x_query) {
318 ++idx;
319 tbl.cursor.direction = +1;
320 }
321 while (idx > 0 && tbl.x[idx] > x_query) {
322 --idx;
323 tbl.cursor.direction = -1;
324 }
325
326 tbl.cursor.index = idx;
327 return tbl.y[idx];
328}
329
330// ============================================================================
331// Storage volume by trapezoidal integration of area curve
332// ============================================================================
333
342inline double table_getStorageVolume(Table& tbl, double depth) noexcept {
343 const int n = static_cast<int>(tbl.x.size());
344 if (n == 0 || depth <= 0.0) return 0.0;
345
346 double x1 = tbl.x[0];
347 double a1 = tbl.y[0];
348
349 // Target below first entry — triangular approximation
350 if (depth <= x1) {
351 if (x1 < 1.0e-6) return 0.0;
352 return (a1 / x1) * depth * depth / 2.0;
353 }
354
355 // Traverse entries using end-area (trapezoidal) method
356 double v = 0.0;
357 double dx = 0.0, dy = 0.0;
358 for (int i = 1; i < n; ++i) {
359 double x2 = tbl.x[i];
360 double a2 = tbl.y[i];
361 if (x2 >= depth) {
362 // Bracketed — interpolate area at target depth.
363 // PARITY: legacy table_getStorageVolume (table.c:624) calls
364 // table_interpolate (table.c:54): multiply THEN divide,
365 // |dx| < 1e-20 degenerate guard → mean of end areas.
366 const double dxi = x2 - x1;
367 double a;
368 if (std::fabs(dxi) < 1.0e-20) a = (a1 + a2) / 2.;
369 else a = a1 + (depth - x1) * (a2 - a1) / dxi;
370 return v + (a1 + a) / 2.0 * (depth - x1);
371 }
372 dx = x2 - x1;
373 dy = a2 - a1;
374 v += (a1 + a2) / 2.0 * dx;
375 x1 = x2;
376 a1 = a2;
377 }
378
379 // Extrapolate beyond last entry
380 if (dx > 1.0e-6) {
381 double s = dy / dx;
382 double a = a1 + s * (depth - x1);
383 if (a < 0.0) {
384 v -= a1 * a1 / s / 2.0;
385 } else {
386 v += (a1 + a) / 2.0 * (depth - x1);
387 }
388 }
389 return v;
390}
391
392// ============================================================================
393// Inverse volume-to-depth for storage curves
394// ============================================================================
395
408inline double table_getStorageDepth(Table& tbl, double volume) noexcept {
409 const int n = static_cast<int>(tbl.x.size());
410 if (n == 0 || volume <= 0.0) return 0.0;
411
412 double x1 = tbl.x[0];
413 double a1 = tbl.y[0];
414 double v_accum = 0.0;
415
416 for (int i = 1; i < n; ++i) {
417 double x2 = tbl.x[i];
418 double a2 = tbl.y[i];
419 double dv = (a1 + a2) / 2.0 * (x2 - x1);
420
421 if (v_accum + dv >= volume) {
422 // Solve the quadratic: target volume falls in this interval.
423 // Area varies linearly: a(d) = a1 + s*(d - x1), s = (a2-a1)/(x2-x1)
424 // Volume: dv = a1*(d-x1) + s*(d-x1)^2/2
425 // Rearrange to standard form and apply quadratic formula.
426 double dx = x2 - x1;
427 double dv_need = volume - v_accum;
428 if (dx < 1.0e-10) return x1;
429
430 double s = (a2 - a1) / dx;
431 double depth;
432 if (std::fabs(s) < 1.0e-10) {
433 // Rectangular slice: dv = a1 * dd
434 depth = (a1 > 0.0) ? x1 + dv_need / a1 : x1;
435 } else {
436 // Quadratic: s/2 * dd^2 + a1 * dd - dv_need = 0
437 double disc = a1 * a1 + 2.0 * s * dv_need;
438 if (disc < 0.0) disc = 0.0;
439 depth = x1 + (-a1 + std::sqrt(disc)) / s;
440 }
441 return std::min(depth, x2);
442 }
443
444 v_accum += dv;
445 x1 = x2;
446 a1 = a2;
447 }
448
449 // Volume exceeds curve range — extrapolate linearly using last slope
450 if (tbl.x.size() >= 2) {
451 int last = n - 1;
452 double dx = tbl.x[last] - tbl.x[last - 1];
453 double da = tbl.y[last] - tbl.y[last - 1];
454 double s = (dx > 1.0e-10) ? da / dx : 0.0;
455 double dv_need = volume - v_accum;
456 double a1_ext = tbl.y[last];
457 double depth;
458 if (std::fabs(s) < 1.0e-10) {
459 depth = (a1_ext > 0.0) ? tbl.x[last] + dv_need / a1_ext : tbl.x[last];
460 } else {
461 double disc = a1_ext * a1_ext + 2.0 * s * dv_need;
462 if (disc < 0.0) disc = 0.0;
463 depth = tbl.x[last] + (-a1_ext + std::sqrt(disc)) / s;
464 }
465 return depth;
466 }
467 return tbl.x[n - 1];
468}
469
470// ============================================================================
471// Generic inverse lookup (y → x)
472// ============================================================================
473
484inline double table_inverseLookup(const Table& tbl, double y_query) noexcept {
485 const int n = static_cast<int>(tbl.x.size());
486 if (n == 0) return 0.0;
487 if (n == 1) return tbl.x[0];
488
489 if (y_query <= tbl.y[0]) return tbl.x[0];
490 if (y_query >= tbl.y[n - 1]) return tbl.x[n - 1];
491
492 for (int i = 1; i < n; ++i) {
493 if (tbl.y[i] >= y_query) {
494 double dy = tbl.y[i] - tbl.y[i - 1];
495 if (dy <= 0.0) return tbl.x[i - 1];
496 double t = (y_query - tbl.y[i - 1]) / dy;
497 return tbl.x[i - 1] + t * (tbl.x[i] - tbl.x[i - 1]);
498 }
499 }
500 return tbl.x[n - 1];
501}
502
503// ============================================================================
504// Extrapolating lookup (extends beyond table bounds)
505// ============================================================================
506
519inline double table_lookupEx(const Table& tbl, double x_query) noexcept {
520 // PARITY: op-for-op transliteration of legacy table_lookupEx
521 // (src/legacy/engine/table.c:469-505):
522 // - below first entry: x/x1*y1 when x1 > 0 (line through the origin),
523 // else y1 — NOT first-interval slope extrapolation;
524 // - in range: table_interpolate (multiply THEN divide, |dx| < 1e-20
525 // degenerate guard → mean);
526 // - above last entry: extrapolate with the slope `s` of the last
527 // segment seen during the walk (skipping x2 == x1 segments),
528 // clamped at s >= 0.
529 const int n = static_cast<int>(tbl.x.size());
530 if (n == 0) return 0.0;
531
532 double x1 = tbl.x[0];
533 double y1 = tbl.y[0];
534 double s = 0.0;
535 if (x_query <= x1) {
536 if (x1 > 0.0) return x_query / x1 * y1;
537 else return y1;
538 }
539 for (int i = 1; i < n; ++i) {
540 const double x2 = tbl.x[i];
541 const double y2 = tbl.y[i];
542 if (x2 != x1) s = (y2 - y1) / (x2 - x1);
543 if (x_query <= x2) {
544 const double dx = x2 - x1;
545 if (std::fabs(dx) < 1.0e-20) return (y1 + y2) / 2.;
546 return y1 + (x_query - x1) * (y2 - y1) / dx;
547 }
548 x1 = x2;
549 y1 = y2;
550 }
551 if (s < 0.0) s = 0.0;
552 return y1 + s * (x_query - x1);
553}
554
555// ============================================================================
556// Step-function interval lookup (first entry > x)
557// ============================================================================
558
572inline double table_intervalLookup(const Table& tbl, double x_query) noexcept {
573 const int n = static_cast<int>(tbl.x.size());
574 if (n == 0) return 0.0;
575
576 for (int i = 0; i < n; ++i) {
577 if (tbl.x[i] > x_query) return tbl.y[i];
578 }
579 return tbl.y[n - 1];
580}
581
582// ============================================================================
583// Slope at a point
584// ============================================================================
585
597inline double table_getSlope(const Table& tbl, double x_query) noexcept {
598 const int n = static_cast<int>(tbl.x.size());
599 if (n < 2) return 0.0;
600
601 // Use first interval for x below range
602 if (x_query <= tbl.x[0]) {
603 double dx = tbl.x[1] - tbl.x[0];
604 return (dx > 0.0) ? (tbl.y[1] - tbl.y[0]) / dx : 0.0;
605 }
606
607 // Use last interval for x above range
608 if (x_query >= tbl.x[n - 1]) {
609 double dx = tbl.x[n - 1] - tbl.x[n - 2];
610 return (dx > 0.0) ? (tbl.y[n - 1] - tbl.y[n - 2]) / dx : 0.0;
611 }
612
613 for (int i = 1; i < n; ++i) {
614 if (tbl.x[i] >= x_query) {
615 double dx = tbl.x[i] - tbl.x[i - 1];
616 return (dx > 0.0) ? (tbl.y[i] - tbl.y[i - 1]) / dx : 0.0;
617 }
618 }
619 return 0.0;
620}
621
622// ============================================================================
623// Maximum y in non-decreasing portion
624// ============================================================================
625
635inline double table_getMaxY(const Table& tbl) noexcept {
636 const int n = static_cast<int>(tbl.x.size());
637 if (n == 0) return 0.0;
638 double ymax = tbl.y[0];
639 for (int i = 1; i < n; ++i) {
640 if (tbl.y[i] < ymax) break;
641 ymax = tbl.y[i];
642 }
643 return ymax;
644}
645
646// ============================================================================
647// TableData — SoA collection of all tables
648// ============================================================================
649
658struct TableData {
659 std::vector<Table> tables;
660
661 std::size_t count() const noexcept { return tables.size(); }
662
663 Table& operator[](int idx) { return tables[static_cast<std::size_t>(idx)]; }
664 const Table& operator[](int idx) const { return tables[static_cast<std::size_t>(idx)]; }
665
670 int add(const std::string& id, TableType type) {
671 tables.push_back({id, type, {}, {}, {}});
672 return static_cast<int>(tables.size()) - 1;
673 }
674
678 void reset_cursors() noexcept {
679 for (auto& t : tables) t.cursor.reset();
680 }
681};
682
683// ============================================================================
684// Table validation
685// ============================================================================
686
688 bool valid = true;
689 std::vector<std::string> errors;
690 std::vector<std::string> warnings;
691};
692
694
695// ============================================================================
696// File-backed table API
697// ============================================================================
698
699bool table_open_file(Table& tbl, std::size_t boundary_rows = 128);
700std::size_t table_load_cache(Table& tbl, std::size_t start_row);
701
702// ============================================================================
703// Multicolumn lookup API
704// ============================================================================
705
706double table_lookup_column(Table& tbl, int col_idx, double x_query);
707double table_step_column(Table& tbl, int col_idx, double x_query);
708
709} /* namespace openswmm */
710
711#endif /* OPENSWMM_ENGINE_TABLE_DATA_HPP */
Carrier for an external file reference in a SWMM model.
Definition NodeCoupling.cpp:15
double table_getStorageVolume(Table &tbl, double depth) noexcept
Compute storage volume by trapezoidal integration of an area-vs-depth curve, matching legacy table_ge...
Definition TableData.hpp:342
TableType
Type of data stored in a Table.
Definition TableData.hpp:55
@ CURVE_PUMP5
Pump curve type 5 (head vs flow, variable speed)
Definition TableData.hpp:67
@ CURVE_PUMP1
Pump curve type 1 (ON/OFF depth)
Definition TableData.hpp:63
@ CURVE_PUMP4
Pump curve type 4 (depth vs speed)
Definition TableData.hpp:66
@ CURVE_RATING
Outfall/weir rating curve.
Definition TableData.hpp:59
@ CURVE_TIDAL
Tidal stage curve.
Definition TableData.hpp:62
@ CURVE_SHAPE
Cross-section shape curve.
Definition TableData.hpp:60
@ CURVE_STORAGE
Storage node volume-depth curve.
Definition TableData.hpp:57
@ CURVE_DIVERSION
Diversion rating curve.
Definition TableData.hpp:58
@ CURVE_CONTROL
Control rule action curve.
Definition TableData.hpp:61
@ CURVE_PUMP2
Pump curve type 2 (head vs flow)
Definition TableData.hpp:64
@ CURVE_PUMP3
Pump curve type 3 (volume vs time)
Definition TableData.hpp:65
double table_getSlope(const Table &tbl, double x_query) noexcept
Compute the slope (dy/dx) at a given x value.
Definition TableData.hpp:597
double table_getStorageDepth(Table &tbl, double volume) noexcept
Invert a storage area-vs-depth curve to find depth from volume.
Definition TableData.hpp:408
double table_step_cursor(Table &tbl, double x_query) noexcept
Piecewise-constant (step function) table lookup with cursor.
Definition TableData.hpp:292
double table_lookup_column(Table &tbl, int col_idx, double x_query)
Definition TableData.cpp:641
TableValidation validate_table(Table &tbl)
Definition TableData.cpp:282
double table_getMaxY(const Table &tbl) noexcept
Return the maximum y value in the non-decreasing leading portion.
Definition TableData.hpp:635
bool table_open_file(Table &tbl, std::size_t boundary_rows)
Definition TableData.cpp:401
double table_intervalLookup(const Table &tbl, double x_query) noexcept
Step-function lookup: return y for the first x entry > x_query.
Definition TableData.hpp:572
@ TIMESERIES
Data from an in-file [TIMESERIES].
Definition GageData.hpp:42
double table_lookupEx(const Table &tbl, double x_query) noexcept
Linear-interpolating lookup with linear extrapolation outside bounds.
Definition TableData.hpp:519
double table_inverseLookup(const Table &tbl, double y_query) noexcept
Inverse lookup: given y, find corresponding x by linear interpolation.
Definition TableData.hpp:484
double table_lookup_cursor(Table &tbl, double x_query) noexcept
Look up a value in a Table using the bidirectional cursor.
Definition TableData.hpp:187
std::size_t table_load_cache(Table &tbl, std::size_t start_row)
Definition TableData.cpp:526
double table_tseries_lookup_cursor(Table &tbl, double x_query) noexcept
Time-series linear interpolation with out-of-range → 0 behaviour.
Definition TableData.hpp:258
double table_step_column(Table &tbl, int col_idx, double x_query)
Definition TableData.cpp:694
Two-string carrier for any external file path that appears in a SWMM .inp file.
Definition FilePathPair.hpp:47
A single time series or rating curve.
Definition TableData.hpp:110
bool empty() const noexcept
Definition TableData.hpp:117
std::size_t num_rows() const noexcept
Definition TableData.hpp:116
std::size_t file_row_start
Row offset in file (cache only)
Definition TableData.hpp:114
std::vector< double > x
Independent variable values.
Definition TableData.hpp:111
int num_cols
Number of value columns.
Definition TableData.hpp:113
std::vector< double > y
Dependent values (flat: row-major, num_cols per row)
Definition TableData.hpp:112
Bidirectional cursor tracking the last accessed index in a Table.
Definition TableData.hpp:85
int index
Index of the last successful lookup entry.
Definition TableData.hpp:86
int direction
Last seek direction: +1 = forward, -1 = backward.
Definition TableData.hpp:87
void reset() noexcept
Definition TableData.hpp:89
SoA collection of all time series and curves in the model.
Definition TableData.hpp:658
std::vector< Table > tables
All tables in index order.
Definition TableData.hpp:659
std::size_t count() const noexcept
Definition TableData.hpp:661
int add(const std::string &id, TableType type)
Add a new empty table with the given ID and type.
Definition TableData.hpp:670
void reset_cursors() noexcept
Reset all cursors (call before re-running a simulation).
Definition TableData.hpp:678
Table & operator[](int idx)
Definition TableData.hpp:663
const Table & operator[](int idx) const
Definition TableData.hpp:664
Definition TableData.hpp:120
long data_start_offset
File offset to first data row.
Definition TableData.hpp:148
std::size_t total_rows
Total data rows in file.
Definition TableData.hpp:146
TableBlock cache
Sliding cache window for file lookups.
Definition TableData.hpp:141
std::vector< long > row_offsets
Sparse byte-offset index into file.
Definition TableData.hpp:149
std::string comment
Object comment from the INP file (';'-prefixed lines immediately above the first row of this table),...
Definition TableData.hpp:129
std::size_t num_cache_rows
Cache window size (rows)
Definition TableData.hpp:147
std::unordered_map< std::string, int > column_map
Column name → index.
Definition TableData.hpp:151
TableCursor cursor
Bidirectional lookup cursor.
Definition TableData.hpp:132
TableType type
Table type (TIMESERIES, CURVE_*, etc.)
Definition TableData.hpp:122
bool empty() const noexcept
True if the table has at least one data point.
Definition TableData.hpp:159
double dx_min
Minimum inter-entry x spacing.
Definition TableData.hpp:142
std::string id
Table identifier (from input file)
Definition TableData.hpp:121
double x_max
Maximum x value in file.
Definition TableData.hpp:144
std::vector< double > y
Dependent variable (flow, volume, etc.)
Definition TableData.hpp:131
std::FILE * file_handle
Open file handle (owned)
Definition TableData.hpp:136
FilePathPair file_path
Definition TableData.hpp:137
bool is_file_based
True if data is read from external file.
Definition TableData.hpp:135
TableBlock last_boundary
Last rows from file (for validation)
Definition TableData.hpp:140
static constexpr std::size_t INDEX_STRIDE
Rows between offset index entries.
Definition TableData.hpp:153
std::vector< double > x
Independent variable (time, depth, etc.)
Definition TableData.hpp:130
double x_min
Minimum x value in file.
Definition TableData.hpp:143
TableBlock first_boundary
First rows from file (for validation)
Definition TableData.hpp:139
int num_cols
Number of value columns.
Definition TableData.hpp:145
std::size_t size() const noexcept
Number of data points.
Definition TableData.hpp:156
std::vector< std::string > column_ids
Column identifiers.
Definition TableData.hpp:150
Definition TableData.hpp:687
std::vector< std::string > errors
Definition TableData.hpp:689
std::vector< std::string > warnings
Definition TableData.hpp:690
bool valid
Definition TableData.hpp:688