@swmmrs/swmmrs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -0
- package/dist/README.md +264 -0
- package/dist/serial/README.md +264 -0
- package/dist/serial/swmmrs.d.ts +395 -0
- package/dist/serial/swmmrs.js +3169 -0
- package/dist/serial/swmmrs_bg.wasm +0 -0
- package/dist/snippets/swmmrs-parallel-5d1c71108133e81d/worker-pool/browser.js +98 -0
- package/dist/swmmrs.d.ts +422 -0
- package/dist/swmmrs.js +3330 -0
- package/dist/swmmrs_bg.wasm +0 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/lib/swmmrs/client.d.ts +11 -0
- package/lib/swmmrs/client.d.ts.map +1 -0
- package/lib/swmmrs/client.js +82 -0
- package/lib/swmmrs/client.js.map +1 -0
- package/lib/swmmrs/enums.d.ts +41 -0
- package/lib/swmmrs/enums.d.ts.map +1 -0
- package/lib/swmmrs/enums.js +2 -0
- package/lib/swmmrs/enums.js.map +1 -0
- package/lib/swmmrs/exceptions.d.ts +98 -0
- package/lib/swmmrs/exceptions.d.ts.map +1 -0
- package/lib/swmmrs/exceptions.js +86 -0
- package/lib/swmmrs/exceptions.js.map +1 -0
- package/lib/swmmrs/index.d.ts +27 -0
- package/lib/swmmrs/index.d.ts.map +1 -0
- package/lib/swmmrs/index.js +13 -0
- package/lib/swmmrs/index.js.map +1 -0
- package/lib/swmmrs/objects/amm.d.ts +114 -0
- package/lib/swmmrs/objects/amm.d.ts.map +1 -0
- package/lib/swmmrs/objects/amm.js +19 -0
- package/lib/swmmrs/objects/amm.js.map +1 -0
- package/lib/swmmrs/objects/collections.d.ts +109 -0
- package/lib/swmmrs/objects/collections.d.ts.map +1 -0
- package/lib/swmmrs/objects/collections.js +158 -0
- package/lib/swmmrs/objects/collections.js.map +1 -0
- package/lib/swmmrs/objects/definitions.d.ts +215 -0
- package/lib/swmmrs/objects/definitions.d.ts.map +1 -0
- package/lib/swmmrs/objects/definitions.js +69 -0
- package/lib/swmmrs/objects/definitions.js.map +1 -0
- package/lib/swmmrs/objects/lids.d.ts +446 -0
- package/lib/swmmrs/objects/lids.d.ts.map +1 -0
- package/lib/swmmrs/objects/lids.js +90 -0
- package/lib/swmmrs/objects/lids.js.map +1 -0
- package/lib/swmmrs/objects/links.d.ts +615 -0
- package/lib/swmmrs/objects/links.d.ts.map +1 -0
- package/lib/swmmrs/objects/links.js +90 -0
- package/lib/swmmrs/objects/links.js.map +1 -0
- package/lib/swmmrs/objects/nodes.d.ts +494 -0
- package/lib/swmmrs/objects/nodes.d.ts.map +1 -0
- package/lib/swmmrs/objects/nodes.js +147 -0
- package/lib/swmmrs/objects/nodes.js.map +1 -0
- package/lib/swmmrs/objects/options.d.ts +112 -0
- package/lib/swmmrs/objects/options.d.ts.map +1 -0
- package/lib/swmmrs/objects/options.js +27 -0
- package/lib/swmmrs/objects/options.js.map +1 -0
- package/lib/swmmrs/objects/rtk.d.ts +82 -0
- package/lib/swmmrs/objects/rtk.d.ts.map +1 -0
- package/lib/swmmrs/objects/rtk.js +19 -0
- package/lib/swmmrs/objects/rtk.js.map +1 -0
- package/lib/swmmrs/objects/subcatchments.d.ts +575 -0
- package/lib/swmmrs/objects/subcatchments.d.ts.map +1 -0
- package/lib/swmmrs/objects/subcatchments.js +243 -0
- package/lib/swmmrs/objects/subcatchments.js.map +1 -0
- package/lib/swmmrs/output.d.ts +412 -0
- package/lib/swmmrs/output.d.ts.map +1 -0
- package/lib/swmmrs/output.js +507 -0
- package/lib/swmmrs/output.js.map +1 -0
- package/lib/swmmrs/protocol.d.ts +80 -0
- package/lib/swmmrs/protocol.d.ts.map +1 -0
- package/lib/swmmrs/protocol.js +2 -0
- package/lib/swmmrs/protocol.js.map +1 -0
- package/lib/swmmrs/runtime.d.ts +22 -0
- package/lib/swmmrs/runtime.d.ts.map +1 -0
- package/lib/swmmrs/runtime.js +146 -0
- package/lib/swmmrs/runtime.js.map +1 -0
- package/lib/swmmrs/scenarios.d.ts +41 -0
- package/lib/swmmrs/scenarios.d.ts.map +1 -0
- package/lib/swmmrs/scenarios.js +2 -0
- package/lib/swmmrs/scenarios.js.map +1 -0
- package/lib/swmmrs/simulation.d.ts +249 -0
- package/lib/swmmrs/simulation.d.ts.map +1 -0
- package/lib/swmmrs/simulation.js +415 -0
- package/lib/swmmrs/simulation.js.map +1 -0
- package/lib/swmmrs/snapshots.d.ts +192 -0
- package/lib/swmmrs/snapshots.d.ts.map +1 -0
- package/lib/swmmrs/snapshots.js +2 -0
- package/lib/swmmrs/snapshots.js.map +1 -0
- package/lib/swmmrs/types.d.ts +123 -0
- package/lib/swmmrs/types.d.ts.map +1 -0
- package/lib/swmmrs/types.js +2 -0
- package/lib/swmmrs/types.js.map +1 -0
- package/lib/swmmrs/worker.d.ts +2 -0
- package/lib/swmmrs/worker.d.ts.map +1 -0
- package/lib/swmmrs/worker.js +488 -0
- package/lib/swmmrs/worker.js.map +1 -0
- package/package.json +33 -0
- package/src/swmmrs/client.ts +76 -0
- package/src/swmmrs/enums.ts +73 -0
- package/src/swmmrs/exceptions.ts +120 -0
- package/src/swmmrs/index.ts +79 -0
- package/src/swmmrs/objects/amm.ts +113 -0
- package/src/swmmrs/objects/collections.ts +153 -0
- package/src/swmmrs/objects/definitions.ts +246 -0
- package/src/swmmrs/objects/lids.ts +487 -0
- package/src/swmmrs/objects/links.ts +626 -0
- package/src/swmmrs/objects/nodes.ts +504 -0
- package/src/swmmrs/objects/options.ts +114 -0
- package/src/swmmrs/objects/rtk.ts +79 -0
- package/src/swmmrs/objects/subcatchments.ts +650 -0
- package/src/swmmrs/output.ts +798 -0
- package/src/swmmrs/protocol.ts +62 -0
- package/src/swmmrs/runtime.ts +188 -0
- package/src/swmmrs/scenarios.ts +40 -0
- package/src/swmmrs/simulation.ts +406 -0
- package/src/swmmrs/snapshots.ts +199 -0
- package/src/swmmrs/types.ts +123 -0
- package/src/swmmrs/worker.ts +233 -0
- package/worker-node.js +142 -0
- package/worker.js +1 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import { WorkerClient } from "./client.js";
|
|
2
|
+
import { LifecycleError } from "./exceptions.js";
|
|
3
|
+
import { bytes, createWorker } from "./runtime.js";
|
|
4
|
+
import type { ModelTime, NodeKind, LinkKind } from "./enums.js";
|
|
5
|
+
import type { FileContents } from "./types.js";
|
|
6
|
+
const EMPTY_TIMES: readonly ModelTime[] = [];
|
|
7
|
+
|
|
8
|
+
/** Supported result families in a binary output file. */
|
|
9
|
+
export type ResultElementType = "subcatchment" | "node" | "link" | "system";
|
|
10
|
+
|
|
11
|
+
/** Concentration units recorded for one pollutant. */
|
|
12
|
+
export type ConcentrationUnits =
|
|
13
|
+
| "milligrams_per_liter"
|
|
14
|
+
| "micrograms_per_liter"
|
|
15
|
+
| "counts_per_liter";
|
|
16
|
+
|
|
17
|
+
/** Flow units recorded in an output header. */
|
|
18
|
+
export type OutputFlowUnits = "cfs" | "gpm" | "mgd" | "cms" | "lps" | "mld";
|
|
19
|
+
|
|
20
|
+
/** Unit system inferred from recognized output flow units. */
|
|
21
|
+
export type OutputUnitSystem = "us" | "si";
|
|
22
|
+
|
|
23
|
+
/** Preserve one unknown signed 32-bit categorical code. */
|
|
24
|
+
export interface UnknownCode {
|
|
25
|
+
/** Unrecognized signed 32-bit value, retained without interpretation. */
|
|
26
|
+
readonly code: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Select one unknown result attribute by its stored signed code. */
|
|
30
|
+
export interface ResultAttributeCode {
|
|
31
|
+
/** Stored signed 32-bit result code; must be present in the selected family schema. */
|
|
32
|
+
readonly code: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Select one pollutant column by index, exact text, bytes, or metadata name. */
|
|
36
|
+
export interface PollutantAttribute {
|
|
37
|
+
/** Exact configured output pollutant selector; not supported for system results. */
|
|
38
|
+
readonly selector: OutputElementSelector;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Known subcatchment columns. Rainfall/infiltration use in/h or mm/h; evaporation uses in/day or mm/day; snow depth uses in or mm; runoff and groundwater outflow use header flow units; groundwater elevation uses ft or m; soil moisture is a fraction. */
|
|
42
|
+
export type SubcatchmentResultAttribute =
|
|
43
|
+
| "rainfall"
|
|
44
|
+
| "snow_depth"
|
|
45
|
+
| "evap_loss"
|
|
46
|
+
| "infil_loss"
|
|
47
|
+
| "runoff_rate"
|
|
48
|
+
| "gw_outflow_rate"
|
|
49
|
+
| "gw_table_elev"
|
|
50
|
+
| "soil_moisture";
|
|
51
|
+
|
|
52
|
+
/** Known node columns. Depth/head use ft or m; ponded volume uses ft³ or m³; lateral/total inflow and flooding losses use header flow units. */
|
|
53
|
+
export type NodeResultAttribute =
|
|
54
|
+
| "invert_depth"
|
|
55
|
+
| "hydraulic_head"
|
|
56
|
+
| "ponded_volume"
|
|
57
|
+
| "lateral_inflow"
|
|
58
|
+
| "total_inflow"
|
|
59
|
+
| "flooding_losses";
|
|
60
|
+
|
|
61
|
+
/** Known link columns. Flow uses header flow units; depth uses ft or m; velocity uses ft/s or m/s; volume uses ft³ or m³; capacity is dimensionless. */
|
|
62
|
+
export type LinkResultAttribute =
|
|
63
|
+
| "flow_rate"
|
|
64
|
+
| "flow_depth"
|
|
65
|
+
| "flow_velocity"
|
|
66
|
+
| "flow_volume"
|
|
67
|
+
| "capacity";
|
|
68
|
+
|
|
69
|
+
/** Known system columns. Temperature uses °F or °C; rainfall and `evap_infil_loss` use in/h or mm/h; `evap_rate` and `ptnl_evap_rate` use in/day or mm/day; snow depth uses in or mm; stored volume uses ft³ or m³; runoff and inflow/outflow/loss columns use header flow units. */
|
|
70
|
+
export type SystemResultAttribute =
|
|
71
|
+
| "air_temp"
|
|
72
|
+
| "rainfall"
|
|
73
|
+
| "snow_depth"
|
|
74
|
+
| "evap_infil_loss"
|
|
75
|
+
| "runoff_flow"
|
|
76
|
+
| "dry_weather_inflow"
|
|
77
|
+
| "gw_inflow"
|
|
78
|
+
| "rdii_inflow"
|
|
79
|
+
| "direct_inflow"
|
|
80
|
+
| "total_lateral_inflow"
|
|
81
|
+
| "flood_losses"
|
|
82
|
+
| "outfall_flows"
|
|
83
|
+
| "volume_stored"
|
|
84
|
+
| "evap_rate"
|
|
85
|
+
| "ptnl_evap_rate";
|
|
86
|
+
|
|
87
|
+
/** One subcatchment schema column, including pollutant and unknown codes. */
|
|
88
|
+
export type SubcatchmentSchemaEntry = SubcatchmentResultAttribute | PollutantAttribute | ResultAttributeCode;
|
|
89
|
+
/** One node schema column, including pollutant and unknown codes. */
|
|
90
|
+
export type NodeSchemaEntry = NodeResultAttribute | PollutantAttribute | ResultAttributeCode;
|
|
91
|
+
/** One link schema column, including pollutant and unknown codes. */
|
|
92
|
+
export type LinkSchemaEntry = LinkResultAttribute | PollutantAttribute | ResultAttributeCode;
|
|
93
|
+
/** One system schema column; system results have no pollutant columns. */
|
|
94
|
+
export type SystemSchemaEntry = SystemResultAttribute | ResultAttributeCode;
|
|
95
|
+
/** Result-column selector; the attribute must belong to the selected element family. */
|
|
96
|
+
export type OutputAttribute =
|
|
97
|
+
| SubcatchmentSchemaEntry
|
|
98
|
+
| NodeSchemaEntry
|
|
99
|
+
| LinkSchemaEntry
|
|
100
|
+
| SystemSchemaEntry;
|
|
101
|
+
|
|
102
|
+
/** Zero-based integer index or exact stored name. Text is UTF-8 encoded; byte/name comparisons are case-sensitive, unlike Simulation collection lookups. Missing or ambiguous names reject. */
|
|
103
|
+
export type OutputElementSelector = number | string | Uint8Array | OutputName;
|
|
104
|
+
|
|
105
|
+
/** Ordered family, element, and result-attribute selection. */
|
|
106
|
+
export interface SeriesSelection {
|
|
107
|
+
/** Result family whose schema is queried. */
|
|
108
|
+
readonly elementType: ResultElementType;
|
|
109
|
+
/** Required element selector for object families; must be null for system results. */
|
|
110
|
+
readonly element: OutputElementSelector | null;
|
|
111
|
+
/** Family-compatible built-in attribute, pollutant selector, or stored result code. */
|
|
112
|
+
readonly attribute: OutputAttribute;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Lossless stored output name, retaining bytes even when UTF-8 decoding fails. */
|
|
116
|
+
export class OutputName {
|
|
117
|
+
#bytes: Uint8Array;
|
|
118
|
+
/** Decoded UTF-8 text, or null for invalid UTF-8. A leading byte-order mark is preserved. */
|
|
119
|
+
readonly text: string | null;
|
|
120
|
+
|
|
121
|
+
/** Copy an exact stored name byte sequence.
|
|
122
|
+
* @param raw - Byte values copied into owned storage.
|
|
123
|
+
*/
|
|
124
|
+
constructor(raw: ArrayLike<number>) {
|
|
125
|
+
this.#bytes = new Uint8Array(raw);
|
|
126
|
+
this.text = decodeName(this.#bytes);
|
|
127
|
+
Object.freeze(this);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Return a defensive copy of the exact stored name bytes. */
|
|
131
|
+
get raw(): Uint8Array { return this.#bytes.slice(); }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Finalized status code, or null when no output trailer was present. */
|
|
135
|
+
export interface RunStatus {
|
|
136
|
+
/** Trailer status code, or null for incomplete output without a trailer. */
|
|
137
|
+
readonly code: number | null;
|
|
138
|
+
/** Whether a valid finalized trailer was present. */
|
|
139
|
+
readonly isFinalized: boolean;
|
|
140
|
+
/** Whether the finalized status code is zero. */
|
|
141
|
+
readonly isSuccess: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Fixed report schedule and available complete-period count. */
|
|
145
|
+
export class ReportTiming {
|
|
146
|
+
/** Nominal schedule origin in SWMM serial days, not an epoch-millisecond timestamp. */
|
|
147
|
+
readonly reportScheduleOrigin: number;
|
|
148
|
+
/** Fixed interval between report periods in seconds. */
|
|
149
|
+
readonly reportStepSeconds: number;
|
|
150
|
+
/** Number of available complete periods. */
|
|
151
|
+
readonly periodCount: number;
|
|
152
|
+
|
|
153
|
+
/** Construct report timing, normally obtained from reader metadata.
|
|
154
|
+
* @param reportScheduleOrigin - SWMM serial-day schedule origin.
|
|
155
|
+
* @param reportStepSeconds - Report interval in seconds.
|
|
156
|
+
* @param periodCount - Available complete-period count.
|
|
157
|
+
*/
|
|
158
|
+
constructor(reportScheduleOrigin: number, reportStepSeconds: number, periodCount: number) {
|
|
159
|
+
this.reportScheduleOrigin = reportScheduleOrigin;
|
|
160
|
+
this.reportStepSeconds = reportStepSeconds;
|
|
161
|
+
this.periodCount = periodCount;
|
|
162
|
+
Object.freeze(this);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Return the rounded timezone-free nominal date, not the exact stored serial-day value.
|
|
166
|
+
* @param period - Zero-based integer period index; period zero is one report step after the origin.
|
|
167
|
+
* @returns Nominal date, or null if the integer is outside the available range.
|
|
168
|
+
* @throws `TypeError` when the index is not an integer.
|
|
169
|
+
*/
|
|
170
|
+
nominalDate(period: number): ModelTime | null {
|
|
171
|
+
if (!Number.isInteger(period)) throw new TypeError("period must be an integer");
|
|
172
|
+
if (period < 0 || period >= this.periodCount) return null;
|
|
173
|
+
return modelTimeFromSerial(this.reportScheduleOrigin + (period + 1) * this.reportStepSeconds / 86_400);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Stored subcatchment identity and static area. */
|
|
178
|
+
export interface SubcatchmentMetadata {
|
|
179
|
+
/** Zero-based stored subcatchment index. */
|
|
180
|
+
readonly index: number;
|
|
181
|
+
/** Exact stored name. */
|
|
182
|
+
readonly name: OutputName;
|
|
183
|
+
/** Stored area in acres or hectares, according to header units. */
|
|
184
|
+
readonly area: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Stored node identity and static properties. */
|
|
188
|
+
export interface NodeMetadata {
|
|
189
|
+
/** Zero-based stored node index. */
|
|
190
|
+
readonly index: number;
|
|
191
|
+
/** Exact stored name. */
|
|
192
|
+
readonly name: OutputName;
|
|
193
|
+
/** Recognized node subtype or unrecognized stored code. */
|
|
194
|
+
readonly kind: NodeKind | UnknownCode;
|
|
195
|
+
/** Invert elevation in ft or m, according to header units. */
|
|
196
|
+
readonly invertElevation: number;
|
|
197
|
+
/** Maximum depth in ft or m. */
|
|
198
|
+
readonly maximumDepth: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Stored link identity and static properties. */
|
|
202
|
+
export interface LinkMetadata {
|
|
203
|
+
/** Zero-based stored link index. */
|
|
204
|
+
readonly index: number;
|
|
205
|
+
/** Exact stored name. */
|
|
206
|
+
readonly name: OutputName;
|
|
207
|
+
/** Recognized link subtype or unrecognized stored code. */
|
|
208
|
+
readonly kind: LinkKind | UnknownCode;
|
|
209
|
+
/** Inlet offset in ft or m, according to header units. */
|
|
210
|
+
readonly inletOffset: number;
|
|
211
|
+
/** Outlet offset in ft or m. */
|
|
212
|
+
readonly outletOffset: number;
|
|
213
|
+
/** Maximum depth in ft or m. */
|
|
214
|
+
readonly maximumDepth: number;
|
|
215
|
+
/** Stored link length in ft or m. */
|
|
216
|
+
readonly length: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Stored pollutant identity, name, and concentration units. */
|
|
220
|
+
export interface PollutantMetadata {
|
|
221
|
+
/** Zero-based stored pollutant index. */
|
|
222
|
+
readonly index: number;
|
|
223
|
+
/** Exact stored name. */
|
|
224
|
+
readonly name: OutputName;
|
|
225
|
+
/** Recognized concentration units or an unrecognized stored code. */
|
|
226
|
+
readonly concentrationUnits: ConcentrationUnits | UnknownCode;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Ordered physical result schemas for every output family. */
|
|
230
|
+
export interface ResultSchema {
|
|
231
|
+
/** Subcatchment columns in stored order. */
|
|
232
|
+
readonly subcatchment: readonly SubcatchmentSchemaEntry[];
|
|
233
|
+
/** Node columns in stored order. */
|
|
234
|
+
readonly node: readonly NodeSchemaEntry[];
|
|
235
|
+
/** Link columns in stored order. */
|
|
236
|
+
readonly link: readonly LinkSchemaEntry[];
|
|
237
|
+
/** System columns in stored order. */
|
|
238
|
+
readonly system: readonly SystemSchemaEntry[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Immutable metadata parsed from one output byte buffer. */
|
|
242
|
+
export interface OutputMetadata {
|
|
243
|
+
/** Stored numeric solver release identifier. */
|
|
244
|
+
readonly solverRelease: number;
|
|
245
|
+
/** Trailer finalization and success status. */
|
|
246
|
+
readonly runStatus: RunStatus;
|
|
247
|
+
/** Recognized flow units or an unrecognized header code. */
|
|
248
|
+
readonly flowUnits: OutputFlowUnits | UnknownCode;
|
|
249
|
+
/** Unit system inferred from recognized flow units; null otherwise. */
|
|
250
|
+
readonly unitSystem: OutputUnitSystem | null;
|
|
251
|
+
/** Validated nominal report schedule and complete-period count. */
|
|
252
|
+
readonly reportTiming: ReportTiming;
|
|
253
|
+
/** Subcatchment metadata in stored order. */
|
|
254
|
+
readonly subcatchments: readonly SubcatchmentMetadata[];
|
|
255
|
+
/** Node metadata in stored order. */
|
|
256
|
+
readonly nodes: readonly NodeMetadata[];
|
|
257
|
+
/** Link metadata in stored order. */
|
|
258
|
+
readonly links: readonly LinkMetadata[];
|
|
259
|
+
/** Pollutant metadata in stored order. */
|
|
260
|
+
readonly pollutants: readonly PollutantMetadata[];
|
|
261
|
+
/** Physical column schemas, including unknown codes. */
|
|
262
|
+
readonly resultSchema: ResultSchema;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Immutable selection-labelled values aligned to a bulk result axis. */
|
|
266
|
+
export interface OutputValueSeries {
|
|
267
|
+
/** Resolved selection, with numeric element/pollutant indices. */
|
|
268
|
+
readonly selection: SeriesSelection;
|
|
269
|
+
/** Values aligned to the enclosing result's time axis, in stored output units. */
|
|
270
|
+
readonly values: readonly number[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Immutable values and nominal dates for one selected series. */
|
|
274
|
+
export interface OutputTimeSeries {
|
|
275
|
+
/** Resolved selection, with numeric element/pollutant indices. */
|
|
276
|
+
readonly selection: SeriesSelection;
|
|
277
|
+
/** Rounded nominal dates for the selected period range. */
|
|
278
|
+
readonly times: readonly ModelTime[];
|
|
279
|
+
/** Values aligned to `times`, in stored output units. */
|
|
280
|
+
readonly values: readonly number[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Immutable column-oriented result for an ordered bulk request. */
|
|
284
|
+
export class BulkSeriesResult {
|
|
285
|
+
/** Shared rounded nominal dates for the selected period range. */
|
|
286
|
+
readonly times: readonly ModelTime[];
|
|
287
|
+
/** Columns in requested selection order, including duplicates. */
|
|
288
|
+
readonly series: readonly OutputValueSeries[];
|
|
289
|
+
|
|
290
|
+
/** Copy the outer axis and column arrays; normally constructed by the reader with immutable, aligned columns.
|
|
291
|
+
* @param times - Shared nominal date axis.
|
|
292
|
+
* @param series - Selection-labelled columns aligned to that axis.
|
|
293
|
+
*/
|
|
294
|
+
constructor(times: readonly ModelTime[], series: readonly OutputValueSeries[]) {
|
|
295
|
+
this.times = freezeArray(times);
|
|
296
|
+
this.series = freezeArray(series);
|
|
297
|
+
Object.freeze(this);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Read one value using offsets local to this result, not absolute file periods.
|
|
301
|
+
* @param periodOffset - Zero-based offset into `times`.
|
|
302
|
+
* @param selectionOffset - Zero-based offset into `series`.
|
|
303
|
+
* @returns Selected value in stored output units.
|
|
304
|
+
* @throws `RangeError` if either offset is non-integral or out of range.
|
|
305
|
+
*/
|
|
306
|
+
value(periodOffset: number, selectionOffset: number): number {
|
|
307
|
+
if (!Number.isInteger(periodOffset) || periodOffset < 0 || periodOffset >= this.times.length) {
|
|
308
|
+
throw new RangeError("period offset is outside this result");
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isInteger(selectionOffset) || selectionOffset < 0 || selectionOffset >= this.series.length) {
|
|
311
|
+
throw new RangeError("selection offset is outside this result");
|
|
312
|
+
}
|
|
313
|
+
return this.series[selectionOffset]!.values[periodOffset]!;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Range and I/O strategy for output queries. Bounds are half-open. */
|
|
318
|
+
export interface OutputReadOptions {
|
|
319
|
+
/** Inclusive integer period offset or timezone-free date; null/omission starts at zero. Dates resolve to the first nominal period at or after the bound. */
|
|
320
|
+
readonly start?: number | ModelTime | null;
|
|
321
|
+
/** Exclusive integer period offset or timezone-free date; null/omission ends at periodCount. Integer bounds must lie within 0…periodCount; reversed ranges reject. */
|
|
322
|
+
readonly end?: number | ModelTime | null;
|
|
323
|
+
/** Defaults to false (whole-period reads). True uses selective adjacent-run reads to reduce working memory without changing values. */
|
|
324
|
+
readonly lowMemory?: boolean;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Alias for callers that only need period/date bounds. */
|
|
328
|
+
export type OutputRange = Omit<OutputReadOptions, "lowMemory">;
|
|
329
|
+
|
|
330
|
+
/** Worker override for one standalone reader. */
|
|
331
|
+
export interface OutputReaderOptions {
|
|
332
|
+
/** Override the bundled worker asset location; resolved by the runtime when opening the reader. */
|
|
333
|
+
readonly workerUrl?: string | URL;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Structured output-reader failure preserved across the worker boundary. */
|
|
337
|
+
export class OutputError extends Error {
|
|
338
|
+
/** Machine-readable failure category, such as `invalid_period_range`, `element_not_found`, or `ambiguous_element`. */
|
|
339
|
+
readonly category: string;
|
|
340
|
+
/** Failing reader operation when available. */
|
|
341
|
+
readonly operation: string | undefined;
|
|
342
|
+
|
|
343
|
+
/** Construct a structured reader error.
|
|
344
|
+
* @param category - Failure classification.
|
|
345
|
+
* @param message - Human-readable diagnostic.
|
|
346
|
+
* @param operation - Optional operation context; omitted when unavailable.
|
|
347
|
+
*/
|
|
348
|
+
constructor(category: string, message: string, operation?: string) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.name = "OutputError";
|
|
351
|
+
this.category = category;
|
|
352
|
+
this.operation = operation;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
type Operation<A extends unknown[], R> = { args: A; result: R };
|
|
357
|
+
type NativeSelection = [ResultElementType, number, number];
|
|
358
|
+
type NativeAttribute = string | { readonly pollutant: number } | { readonly code: number };
|
|
359
|
+
type NativeName = { readonly raw: readonly number[]; readonly text: string | null };
|
|
360
|
+
type NativeMetadataPayload = {
|
|
361
|
+
readonly solverRelease: number;
|
|
362
|
+
readonly runStatus: { readonly code: number | null; readonly isFinalized: boolean; readonly isSuccess: boolean };
|
|
363
|
+
readonly flowUnits: string | { readonly code: number };
|
|
364
|
+
readonly unitSystem: "us" | "si" | null;
|
|
365
|
+
readonly reportTiming: { readonly reportScheduleOrigin: number; readonly reportStepSeconds: number; readonly periodCount: number };
|
|
366
|
+
readonly subcatchments: readonly { readonly index: number; readonly name: NativeName; readonly area: number }[];
|
|
367
|
+
readonly nodes: readonly { readonly index: number; readonly name: NativeName; readonly kind: string | { readonly code: number }; readonly invertElevation: number; readonly maximumDepth: number }[];
|
|
368
|
+
readonly links: readonly { readonly index: number; readonly name: NativeName; readonly kind: string | { readonly code: number }; readonly inletOffset: number; readonly outletOffset: number; readonly maximumDepth: number; readonly length: number }[];
|
|
369
|
+
readonly pollutants: readonly { readonly index: number; readonly name: NativeName; readonly concentrationUnits: string | { readonly code: number } }[];
|
|
370
|
+
readonly resultSchema: {
|
|
371
|
+
readonly subcatchment: readonly NativeAttribute[];
|
|
372
|
+
readonly node: readonly NativeAttribute[];
|
|
373
|
+
readonly link: readonly NativeAttribute[];
|
|
374
|
+
readonly system: readonly NativeAttribute[];
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
type NativeBulkPayload = {
|
|
378
|
+
readonly times: readonly number[];
|
|
379
|
+
readonly series: readonly {
|
|
380
|
+
readonly selection: { readonly elementType: ResultElementType; readonly element: number | null; readonly attribute: NativeAttribute };
|
|
381
|
+
readonly values: readonly number[];
|
|
382
|
+
}[];
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/** Operations consumed by the standalone output worker. Main extends the shared worker contract with these entries. */
|
|
386
|
+
export interface OutputOperations {
|
|
387
|
+
openOutput: Operation<[Uint8Array], NativeMetadataPayload>;
|
|
388
|
+
outputMetadata: Operation<[], NativeMetadataPayload>;
|
|
389
|
+
outputReadBulkSeries: Operation<[readonly NativeSelection[], number, number], NativeBulkPayload>;
|
|
390
|
+
outputReadBulkSeriesByPeriod: Operation<[readonly NativeSelection[], number, number], NativeBulkPayload>;
|
|
391
|
+
outputReadStoredDates: Operation<[number, number], readonly number[]>;
|
|
392
|
+
outputSubcatchmentSeries: Operation<[number, number, number, number], NativeBulkPayload>;
|
|
393
|
+
outputNodeSeries: Operation<[number, number, number, number], NativeBulkPayload>;
|
|
394
|
+
outputLinkSeries: Operation<[number, number, number, number], NativeBulkPayload>;
|
|
395
|
+
outputSystemSeries: Operation<[number, number, number], NativeBulkPayload>;
|
|
396
|
+
closeOutput: Operation<[], void>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
type OutputMethod = keyof OutputOperations;
|
|
400
|
+
type OutputCall = <K extends OutputMethod>(method: K, ...args: OutputOperations[K]["args"]) => Promise<OutputOperations[K]["result"]>;
|
|
401
|
+
|
|
402
|
+
/** Byte-owned standalone output reader with its own worker, independent of Simulation. Always await `close()`; copied results and metadata remain usable afterward. */
|
|
403
|
+
export class OutputReader implements AsyncDisposable {
|
|
404
|
+
/** Immutable parsed metadata, available without another worker query. */
|
|
405
|
+
readonly metadata: OutputMetadata;
|
|
406
|
+
readonly #client: WorkerClient;
|
|
407
|
+
#times: readonly ModelTime[] | undefined;
|
|
408
|
+
#closing: Promise<void> | undefined;
|
|
409
|
+
#closed = false;
|
|
410
|
+
|
|
411
|
+
private constructor(client: WorkerClient, metadata: OutputMetadata) {
|
|
412
|
+
this.#client = client;
|
|
413
|
+
this.metadata = metadata;
|
|
414
|
+
Object.freeze(this);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Open finalized or incomplete SWMM output in an independent worker.
|
|
418
|
+
* @param input - File contents, never a host path. In Node, pass bytes from `fs.readFile`.
|
|
419
|
+
* @param options - Worker asset override; defaults to the bundled worker.
|
|
420
|
+
* @returns Reader with immutable validated metadata and available complete periods.
|
|
421
|
+
* @throws {@link OutputError} for invalid output; a worker created during a failed open is stopped.
|
|
422
|
+
*/
|
|
423
|
+
static async open(input: FileContents, options: OutputReaderOptions = {}): Promise<OutputReader> {
|
|
424
|
+
const worker = await createWorker(options);
|
|
425
|
+
const client = new WorkerClient(worker);
|
|
426
|
+
const call = client.call as unknown as OutputCall;
|
|
427
|
+
try {
|
|
428
|
+
const payload = await call("openOutput", await bytes(input));
|
|
429
|
+
return new OutputReader(client, metadataFromPayload(payload));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
client.stop(error instanceof Error ? error : new Error(String(error)));
|
|
432
|
+
throw outputFailure(error, "openOutput");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** True when the opened bytes contained a valid finalized trailer. */
|
|
437
|
+
get isFinalized(): boolean { return this.metadata.runStatus.isFinalized; }
|
|
438
|
+
|
|
439
|
+
/** Lazily derive the shared nominal report-date axis from validated metadata. */
|
|
440
|
+
get times(): readonly ModelTime[] {
|
|
441
|
+
if (this.#times === undefined) {
|
|
442
|
+
const values = Array.from({ length: this.metadata.reportTiming.periodCount }, (_, period) =>
|
|
443
|
+
this.metadata.reportTiming.nominalDate(period)!,
|
|
444
|
+
);
|
|
445
|
+
this.#times = freezeArray(values);
|
|
446
|
+
}
|
|
447
|
+
return this.#times;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Read ordered selections; duplicates remain repeated columns and empty dimensions are preserved.
|
|
451
|
+
* @param selections - Family-compatible element/attribute selections in desired column order.
|
|
452
|
+
* @param options - Half-open range and I/O strategy; defaults to all periods and whole-period reads.
|
|
453
|
+
* @returns Detached immutable columns sharing one nominal date axis.
|
|
454
|
+
* @throws {@link OutputError} for missing/ambiguous elements, absent attributes, or invalid ranges; TypeError/RangeError for malformed selectors. Reads after close reject with LifecycleError.
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* import type { OutputReader } from "@swmmrs/swmmrs";
|
|
458
|
+
* declare const reader: OutputReader;
|
|
459
|
+
* const result = await reader.readBulkSeries([
|
|
460
|
+
* { elementType: "node", element: "J1", attribute: "hydraulic_head" },
|
|
461
|
+
* ], { start: 0, end: 24, lowMemory: true });
|
|
462
|
+
* console.log(result.times, result.series[0]?.values);
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
readBulkSeries(selections: readonly SeriesSelection[], options: OutputReadOptions = {}): Promise<BulkSeriesResult> {
|
|
466
|
+
const [start, end] = this.#resolveBounds(options);
|
|
467
|
+
const nativeSelections = selections.map((selection) => this.#nativeSelection(selection));
|
|
468
|
+
const method = options.lowMemory === true ? "outputReadBulkSeries" : "outputReadBulkSeriesByPeriod";
|
|
469
|
+
return this.#call(method, nativeSelections, start, end).then((payload) => bulkFromPayload(payload, this.metadata));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Read one subcatchment column with the validation and lifecycle contract of `readBulkSeries`.
|
|
473
|
+
* @param element - Zero-based index or exact stored name selector.
|
|
474
|
+
* @param attribute - Subcatchment attribute, pollutant, or stored result code.
|
|
475
|
+
* @param options - Half-open range and I/O strategy; defaults to all periods and whole-period reads.
|
|
476
|
+
* @returns Immutable selection, nominal dates, and aligned values.
|
|
477
|
+
*/
|
|
478
|
+
subcatchmentSeries(element: OutputElementSelector, attribute: SubcatchmentResultAttribute | PollutantAttribute | ResultAttributeCode, options: OutputReadOptions = {}): Promise<OutputTimeSeries> {
|
|
479
|
+
return this.#singleSeries({ elementType: "subcatchment", element, attribute }, options);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Read one node column with the validation and lifecycle contract of `readBulkSeries`.
|
|
483
|
+
* @param element - Zero-based index or exact stored name selector.
|
|
484
|
+
* @param attribute - Node attribute, pollutant, or stored result code.
|
|
485
|
+
* @param options - Half-open range and I/O strategy; defaults to all periods and whole-period reads.
|
|
486
|
+
* @returns Immutable selection, nominal dates, and aligned values.
|
|
487
|
+
*/
|
|
488
|
+
nodeSeries(element: OutputElementSelector, attribute: NodeResultAttribute | PollutantAttribute | ResultAttributeCode, options: OutputReadOptions = {}): Promise<OutputTimeSeries> {
|
|
489
|
+
return this.#singleSeries({ elementType: "node", element, attribute }, options);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Read one link column with the validation and lifecycle contract of `readBulkSeries`.
|
|
493
|
+
* @param element - Zero-based index or exact stored name selector.
|
|
494
|
+
* @param attribute - Link attribute, pollutant, or stored result code.
|
|
495
|
+
* @param options - Half-open range and I/O strategy; defaults to all periods and whole-period reads.
|
|
496
|
+
* @returns Immutable selection, nominal dates, and aligned values.
|
|
497
|
+
*/
|
|
498
|
+
linkSeries(element: OutputElementSelector, attribute: LinkResultAttribute | PollutantAttribute | ResultAttributeCode, options: OutputReadOptions = {}): Promise<OutputTimeSeries> {
|
|
499
|
+
return this.#singleSeries({ elementType: "link", element, attribute }, options);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Read one system column with the validation and lifecycle contract of `readBulkSeries`.
|
|
503
|
+
* @param attribute - System attribute or stored result code; pollutants are not supported.
|
|
504
|
+
* @param options - Half-open range and I/O strategy; defaults to all periods and whole-period reads.
|
|
505
|
+
* @returns Immutable selection with null element, nominal dates, and aligned values.
|
|
506
|
+
*/
|
|
507
|
+
systemSeries(attribute: SystemResultAttribute | ResultAttributeCode, options: OutputReadOptions = {}): Promise<OutputTimeSeries> {
|
|
508
|
+
return this.#singleSeries({ elementType: "system", element: null, attribute }, options);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Read exact stored report dates rather than the rounded nominal date axis; requires an open reader.
|
|
512
|
+
* @param options - Half-open period/date bounds; defaults to all complete periods.
|
|
513
|
+
* @returns Immutable SWMM serial-day values read from the output bytes.
|
|
514
|
+
* @throws {@link OutputError} for invalid ranges or unreadable output; LifecycleError after close.
|
|
515
|
+
*/
|
|
516
|
+
readStoredDates(options: OutputRange = {}): Promise<readonly number[]> {
|
|
517
|
+
const [start, end] = this.#resolveBounds(options);
|
|
518
|
+
return this.#call("outputReadStoredDates", start, end).then((values) => freezeArray(values));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Release the reader worker. Repeated calls share one cleanup request; future reads reject.
|
|
522
|
+
* @returns Resolves after cleanup. Previously copied metadata/results remain usable.
|
|
523
|
+
*/
|
|
524
|
+
async close(): Promise<void> {
|
|
525
|
+
if (this.#closing !== undefined) return this.#closing;
|
|
526
|
+
this.#closing = this.#call("closeOutput").finally(() => {
|
|
527
|
+
this.#closed = true;
|
|
528
|
+
this.#client.stop(new LifecycleError({ message: "Output reader is closed", operation: "closeOutput" }));
|
|
529
|
+
});
|
|
530
|
+
return this.#closing;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Delegate asynchronous disposal to `close()`.
|
|
534
|
+
* @returns Reader cleanup completion.
|
|
535
|
+
*/
|
|
536
|
+
[Symbol.asyncDispose](): Promise<void> { return this.close(); }
|
|
537
|
+
|
|
538
|
+
#singleSeries(selection: SeriesSelection, options: OutputReadOptions): Promise<OutputTimeSeries> {
|
|
539
|
+
return this.readBulkSeries([selection], options).then((result) => {
|
|
540
|
+
const series = result.series[0];
|
|
541
|
+
if (series === undefined) throw new OutputError("output", "native output returned no series");
|
|
542
|
+
return Object.freeze({ selection: series.selection, times: result.times, values: series.values });
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#nativeSelection(selection: SeriesSelection): NativeSelection {
|
|
547
|
+
if (selection.elementType === "system") {
|
|
548
|
+
if (selection.element !== null) throw new TypeError("system selections do not carry an element");
|
|
549
|
+
return ["system", 0, attributeCode(this.metadata, selection.elementType, selection.attribute)];
|
|
550
|
+
}
|
|
551
|
+
if (selection.element === null) throw new TypeError("non-system selections require an element");
|
|
552
|
+
return [
|
|
553
|
+
selection.elementType,
|
|
554
|
+
resolveElement(this.metadata, selection.elementType, selection.element),
|
|
555
|
+
attributeCode(this.metadata, selection.elementType, selection.attribute),
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#resolveBounds(options: OutputReadOptions): [number, number] {
|
|
560
|
+
if (options === null || typeof options !== "object") throw new TypeError("options must be an object");
|
|
561
|
+
if (options.lowMemory !== undefined && typeof options.lowMemory !== "boolean") {
|
|
562
|
+
throw new TypeError("lowMemory must be a boolean");
|
|
563
|
+
}
|
|
564
|
+
const count = this.metadata.reportTiming.periodCount;
|
|
565
|
+
const needsAxis = isModelTime(options.start) || isModelTime(options.end);
|
|
566
|
+
const axis = needsAxis ? this.times : EMPTY_TIMES;
|
|
567
|
+
const start = resolveBound(options.start, 0, count, axis);
|
|
568
|
+
const end = resolveBound(options.end, count, count, axis);
|
|
569
|
+
if (isModelTime(options.start) && isModelTime(options.end) && options.start > options.end) {
|
|
570
|
+
throw new OutputError("invalid_period_range", "start bound must not be later than end bound");
|
|
571
|
+
}
|
|
572
|
+
if (start > end) throw new OutputError("invalid_period_range", "resolved period range is inverted");
|
|
573
|
+
return [start, end];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#call<K extends OutputMethod>(method: K, ...args: OutputOperations[K]["args"]): Promise<OutputOperations[K]["result"]> {
|
|
577
|
+
if (this.#closed) return Promise.reject(new LifecycleError({ message: "Output reader is closed", operation: String(method) }));
|
|
578
|
+
const call = this.#client.call as unknown as OutputCall;
|
|
579
|
+
return call(method, ...args).catch((error) => { throw outputFailure(error, String(method)); });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function metadataFromPayload(payload: NativeMetadataPayload): OutputMetadata {
|
|
584
|
+
const timing = new ReportTiming(payload.reportTiming.reportScheduleOrigin, payload.reportTiming.reportStepSeconds, payload.reportTiming.periodCount);
|
|
585
|
+
const metadata: OutputMetadata = {
|
|
586
|
+
solverRelease: payload.solverRelease,
|
|
587
|
+
runStatus: Object.freeze({
|
|
588
|
+
code: payload.runStatus.code,
|
|
589
|
+
isFinalized: payload.runStatus.code !== null,
|
|
590
|
+
isSuccess: payload.runStatus.code === 0,
|
|
591
|
+
}),
|
|
592
|
+
flowUnits: category(payload.flowUnits) as OutputFlowUnits | UnknownCode,
|
|
593
|
+
unitSystem: payload.unitSystem,
|
|
594
|
+
reportTiming: timing,
|
|
595
|
+
subcatchments: freezeArray(payload.subcatchments.map((item) => Object.freeze({ index: item.index, name: nameFromPayload(item.name), area: item.area }))),
|
|
596
|
+
nodes: freezeArray(payload.nodes.map((item) => Object.freeze({ index: item.index, name: nameFromPayload(item.name), kind: category(item.kind) as NodeKind | UnknownCode, invertElevation: item.invertElevation, maximumDepth: item.maximumDepth }))),
|
|
597
|
+
links: freezeArray(payload.links.map((item) => Object.freeze({ index: item.index, name: nameFromPayload(item.name), kind: category(item.kind) as LinkKind | UnknownCode, inletOffset: item.inletOffset, outletOffset: item.outletOffset, maximumDepth: item.maximumDepth, length: item.length }))),
|
|
598
|
+
pollutants: freezeArray(payload.pollutants.map((item) => Object.freeze({ index: item.index, name: nameFromPayload(item.name), concentrationUnits: category(item.concentrationUnits) as ConcentrationUnits | UnknownCode }))),
|
|
599
|
+
resultSchema: Object.freeze({
|
|
600
|
+
subcatchment: freezeArray(payload.resultSchema.subcatchment.map((item) => attributeFromPayload(item, "subcatchment"))),
|
|
601
|
+
node: freezeArray(payload.resultSchema.node.map((item) => attributeFromPayload(item, "node"))),
|
|
602
|
+
link: freezeArray(payload.resultSchema.link.map((item) => attributeFromPayload(item, "link"))),
|
|
603
|
+
system: freezeArray(payload.resultSchema.system.map((item) => attributeFromPayload(item, "system"))),
|
|
604
|
+
}),
|
|
605
|
+
};
|
|
606
|
+
return Object.freeze(metadata);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function bulkFromPayload(payload: NativeBulkPayload, metadata: OutputMetadata): BulkSeriesResult {
|
|
610
|
+
const times = freezeArray(payload.times.map(modelTimeFromSerial));
|
|
611
|
+
const series = payload.series.map((item) => Object.freeze({
|
|
612
|
+
selection: selectionFromPayload(item.selection, metadata),
|
|
613
|
+
values: freezeArray(item.values),
|
|
614
|
+
}));
|
|
615
|
+
return new BulkSeriesResult(times, series);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function selectionFromPayload(payload: NativeBulkPayload["series"][number]["selection"], metadata: OutputMetadata): SeriesSelection {
|
|
619
|
+
const elementType = payload.elementType;
|
|
620
|
+
const attribute = attributeFromPayload(payload.attribute, elementType);
|
|
621
|
+
return Object.freeze({ elementType, element: elementType === "system" ? null : payload.element, attribute });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function attributeFromPayload<K extends ResultElementType>(value: NativeAttribute, _family: K): ResultSchema[K][number] {
|
|
625
|
+
const attribute = typeof value === "string" ? value
|
|
626
|
+
: Object.freeze("pollutant" in value ? { selector: value.pollutant } : { code: value.code });
|
|
627
|
+
return attribute as ResultSchema[K][number];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function attributeCode(metadata: OutputMetadata, family: ResultElementType, attribute: OutputAttribute): number {
|
|
631
|
+
if (typeof attribute === "string") {
|
|
632
|
+
const known = knownAttributeCode(family, attribute);
|
|
633
|
+
if (known === undefined) throw new TypeError(`unknown ${family} attribute ${attribute}`);
|
|
634
|
+
return known;
|
|
635
|
+
}
|
|
636
|
+
if ("selector" in attribute) {
|
|
637
|
+
const pollutant = resolvePollutant(metadata, attribute.selector);
|
|
638
|
+
const schema = schemaFor(metadata, family);
|
|
639
|
+
const entry = schema.find((candidate) => typeof candidate === "object" && candidate !== null && "selector" in candidate && candidate.selector === pollutant);
|
|
640
|
+
if (entry === undefined) throw new OutputError("attribute_not_found", `pollutant ${pollutant} is absent from ${family} schema`);
|
|
641
|
+
return pollutantCode(family, pollutant);
|
|
642
|
+
}
|
|
643
|
+
if (!Number.isInteger(attribute.code) || attribute.code < -2_147_483_648 || attribute.code > 2_147_483_647) {
|
|
644
|
+
throw new RangeError("result code must fit a signed 32-bit integer");
|
|
645
|
+
}
|
|
646
|
+
return attribute.code;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function knownAttributeCode(family: ResultElementType, attribute: string): number | undefined {
|
|
650
|
+
const known: Record<ResultElementType, readonly string[]> = {
|
|
651
|
+
subcatchment: ["rainfall", "snow_depth", "evap_loss", "infil_loss", "runoff_rate", "gw_outflow_rate", "gw_table_elev", "soil_moisture"],
|
|
652
|
+
node: ["invert_depth", "hydraulic_head", "ponded_volume", "lateral_inflow", "total_inflow", "flooding_losses"],
|
|
653
|
+
link: ["flow_rate", "flow_depth", "flow_velocity", "flow_volume", "capacity"],
|
|
654
|
+
system: ["air_temp", "rainfall", "snow_depth", "evap_infil_loss", "runoff_flow", "dry_weather_inflow", "gw_inflow", "rdii_inflow", "direct_inflow", "total_lateral_inflow", "flood_losses", "outfall_flows", "volume_stored", "evap_rate", "ptnl_evap_rate"],
|
|
655
|
+
};
|
|
656
|
+
const index = known[family].indexOf(attribute);
|
|
657
|
+
return index < 0 ? undefined : index;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function pollutantCode(family: ResultElementType, index: number): number {
|
|
661
|
+
const offset = family === "subcatchment" ? 8 : family === "node" ? 6 : 5;
|
|
662
|
+
if (family === "system") throw new TypeError("system selections do not support pollutants");
|
|
663
|
+
const code = offset + index;
|
|
664
|
+
if (code > 2_147_483_647) throw new RangeError("pollutant result code does not fit a signed 32-bit integer");
|
|
665
|
+
return code;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function resolveElement(metadata: OutputMetadata, family: Exclude<ResultElementType, "system">, selector: OutputElementSelector): number {
|
|
669
|
+
if (typeof selector === "number") {
|
|
670
|
+
if (!Number.isInteger(selector) || selector < 0) throw new RangeError("element index must be a nonnegative integer");
|
|
671
|
+
const count = family === "subcatchment" ? metadata.subcatchments.length : family === "node" ? metadata.nodes.length : metadata.links.length;
|
|
672
|
+
if (selector >= count) throw new OutputError("invalid_element_id", `invalid ${family} element index ${selector}`);
|
|
673
|
+
return selector;
|
|
674
|
+
}
|
|
675
|
+
const raw = selector instanceof OutputName ? selector.raw : typeof selector === "string" ? new TextEncoder().encode(selector) : selector;
|
|
676
|
+
const records = family === "subcatchment" ? metadata.subcatchments : family === "node" ? metadata.nodes : metadata.links;
|
|
677
|
+
let found = -1;
|
|
678
|
+
let count = 0;
|
|
679
|
+
for (const [index, record] of records.entries()) {
|
|
680
|
+
if (bytesEqual(record.name.raw, raw)) { found = index; count += 1; }
|
|
681
|
+
}
|
|
682
|
+
if (count === 0) throw new OutputError("element_not_found", `${family} element name was not found`);
|
|
683
|
+
if (count > 1) throw new OutputError("ambiguous_element", `${family} element name matched ${count} elements`);
|
|
684
|
+
return found;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function resolvePollutant(metadata: OutputMetadata, selector: OutputElementSelector): number {
|
|
688
|
+
if (typeof selector === "number") {
|
|
689
|
+
if (!Number.isInteger(selector) || selector < 0 || selector >= metadata.pollutants.length) throw new OutputError("pollutant_not_found", `pollutant index ${selector} was not found`);
|
|
690
|
+
return selector;
|
|
691
|
+
}
|
|
692
|
+
const raw = selector instanceof OutputName ? selector.raw : typeof selector === "string" ? new TextEncoder().encode(selector) : selector;
|
|
693
|
+
let found = -1;
|
|
694
|
+
let count = 0;
|
|
695
|
+
for (const [index, pollutant] of metadata.pollutants.entries()) {
|
|
696
|
+
if (bytesEqual(pollutant.name.raw, raw)) { found = index; count += 1; }
|
|
697
|
+
}
|
|
698
|
+
if (count === 0) throw new OutputError("pollutant_not_found", "pollutant name was not found");
|
|
699
|
+
if (count > 1) throw new OutputError("ambiguous_pollutant", `pollutant name matched ${count} pollutants`);
|
|
700
|
+
return found;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function schemaFor(metadata: OutputMetadata, family: ResultElementType): readonly OutputAttribute[] {
|
|
704
|
+
return metadata.resultSchema[family] as readonly OutputAttribute[];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function resolveBound(bound: number | ModelTime | null | undefined, fallback: number, count: number, axis: readonly ModelTime[]): number {
|
|
708
|
+
if (bound === undefined || bound === null) return fallback;
|
|
709
|
+
if (typeof bound === "number") {
|
|
710
|
+
if (!Number.isInteger(bound)) throw new TypeError("period bounds must be integers or ModelTime strings");
|
|
711
|
+
if (bound < 0 || bound > count) throw new RangeError("period bound is outside the available range");
|
|
712
|
+
return bound;
|
|
713
|
+
}
|
|
714
|
+
if (!isModelTime(bound)) throw new TypeError("date bounds must be timezone-free ModelTime strings");
|
|
715
|
+
let low = 0;
|
|
716
|
+
let high = axis.length;
|
|
717
|
+
while (low < high) {
|
|
718
|
+
const middle = (low + high) >>> 1;
|
|
719
|
+
if (axis[middle]! < bound) low = middle + 1;
|
|
720
|
+
else high = middle;
|
|
721
|
+
}
|
|
722
|
+
return low;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function isModelTime(value: unknown): value is ModelTime {
|
|
726
|
+
if (typeof value !== "string") return false;
|
|
727
|
+
const match = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$/.exec(value);
|
|
728
|
+
if (match === null) return false;
|
|
729
|
+
const year = Number(match[1]);
|
|
730
|
+
const month = Number(match[2]);
|
|
731
|
+
const day = Number(match[3]);
|
|
732
|
+
const hour = Number(match[4]);
|
|
733
|
+
const minute = Number(match[5]);
|
|
734
|
+
const second = Number(match[6]);
|
|
735
|
+
if (year < 1 || year > 9999 || month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
|
|
736
|
+
const date = new Date(0);
|
|
737
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
738
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
739
|
+
return date.getUTCFullYear() === year
|
|
740
|
+
&& date.getUTCMonth() === month - 1
|
|
741
|
+
&& date.getUTCDate() === day
|
|
742
|
+
&& date.getUTCHours() === hour
|
|
743
|
+
&& date.getUTCMinutes() === minute
|
|
744
|
+
&& date.getUTCSeconds() === second;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function modelTimeFromSerial(serial: number): ModelTime {
|
|
748
|
+
const totalSeconds = serial * 86_400;
|
|
749
|
+
if (!Number.isFinite(totalSeconds)) throw new OutputError("datetime_out_of_range", "nominal report date is outside the supported time range");
|
|
750
|
+
const rounded = roundHalfEven(totalSeconds);
|
|
751
|
+
const unixSeconds = rounded - 2_209_161_600;
|
|
752
|
+
if (unixSeconds < -62_135_596_800 || unixSeconds > 253_402_300_799) {
|
|
753
|
+
throw new OutputError("datetime_out_of_range", "nominal report date is outside the supported time range");
|
|
754
|
+
}
|
|
755
|
+
const date = new Date(unixSeconds * 1_000);
|
|
756
|
+
if (!Number.isFinite(date.getTime())) throw new OutputError("datetime_out_of_range", "nominal report date is outside the supported time range");
|
|
757
|
+
return date.toISOString().slice(0, 19) as ModelTime;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function roundHalfEven(value: number): number {
|
|
761
|
+
const lower = Math.floor(value);
|
|
762
|
+
const fraction = value - lower;
|
|
763
|
+
if (fraction < 0.5) return lower;
|
|
764
|
+
if (fraction > 0.5) return lower + 1;
|
|
765
|
+
return lower % 2 === 0 ? lower : lower + 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function decodeName(raw: Uint8Array): string | null {
|
|
769
|
+
try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw); }
|
|
770
|
+
catch { return null; }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function nameFromPayload(value: NativeName): OutputName {
|
|
774
|
+
return new OutputName(value.raw);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function category(value: string | { readonly code: number }): string | UnknownCode {
|
|
778
|
+
return typeof value === "string" ? value : Object.freeze({ code: value.code });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function bytesEqual(left: ArrayLike<number>, right: ArrayLike<number>): boolean {
|
|
782
|
+
if (left.length !== right.length) return false;
|
|
783
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function freezeArray<T>(values: readonly T[]): readonly T[] {
|
|
788
|
+
return Object.freeze(Array.from(values));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function outputFailure(error: unknown, operation: string): Error {
|
|
792
|
+
if (error instanceof OutputError) return error;
|
|
793
|
+
const source = error as { category?: unknown; message?: unknown; operation?: unknown } | null;
|
|
794
|
+
if (source && typeof source.category === "string") {
|
|
795
|
+
return new OutputError(source.category, typeof source.message === "string" ? source.message : String(error), typeof source.operation === "string" ? source.operation : operation);
|
|
796
|
+
}
|
|
797
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
798
|
+
}
|