Skip to content

Commit caaab45

Browse files
authored
Merge pull request #163 from pyswmm/dev-series-compare
Dev series compare
2 parents 5794c0b + 0f46e7c commit caaab45

5 files changed

Lines changed: 252 additions & 3 deletions

File tree

swmm-toolkit/src/swmm/toolkit/output.i

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,20 @@ and return a (possibly) different pointer */
6161

6262
%apply int *OUTPUT {
6363
int *version,
64-
int *time
64+
int *time,
65+
int *year,
66+
int *month,
67+
int *day,
68+
int *hour,
69+
int *minute,
70+
int *second,
71+
int *dayOfWeek
6572
}
6673

6774
%cstring_output_allocate_size(char **elementName, int *size, SMO_freeMemory(*$1));
6875

6976

70-
/* TYPEMAPS FOR MEMORY MANAGEMNET OF FLOAT ARRAYS */
77+
/* TYPEMAPS FOR MEMORY MANAGEMENT OF FLOAT ARRAYS */
7178
%typemap(in, numinputs=0)float **float_out (float *temp), int *int_dim (int temp){
7279
$1 = &temp;
7380
}
@@ -84,6 +91,23 @@ and return a (possibly) different pointer */
8491
}
8592

8693

94+
/* TYPEMAPS FOR MEMORY MANAGEMENT OF DOUBLE ARRAYS */
95+
%typemap(in, numinputs=0)double **double_out (double *temp), int *int_dim (int temp){
96+
$1 = &temp;
97+
}
98+
%typemap(argout) (double **double_out, int *int_dim) {
99+
if (*$1) {
100+
PyObject *o = PyList_New(*$2);
101+
double* temp = *$1;
102+
for(int i=0; i<*$2; i++) {
103+
PyList_SetItem(o, i, PyFloat_FromDouble((double)temp[i]));
104+
}
105+
$result = SWIG_AppendOutput($result, o);
106+
SMO_freeMemory(*$1);
107+
}
108+
}
109+
110+
87111
/* TYPEMAPS FOR MEMORY MANAGEMENT OF INT ARRAYS */
88112
%typemap(in, numinputs=0)int **int_out (int *temp), int *int_dim (int temp){
89113
$1 = &temp;
@@ -151,6 +175,8 @@ and return a (possibly) different pointer */
151175
%ignore SMO_clearError;
152176
%ignore SMO_checkError;
153177

178+
%noexception SMO_decodeDate;
179+
154180
%include "swmm_output.h"
155181

156182
%exception;

swmm-toolkit/src/swmm/toolkit/output_rename.i

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
%rename(get_times) SMO_getTimes;
2121
%rename(get_elem_name) SMO_getElementName;
2222

23+
%rename(get_date_time) SMO_getDateTime;
24+
%rename(get_date_series) SMO_getDateSeries;
25+
%rename(decode_date) SMO_decodeDate;
26+
2327
%rename(get_subcatch_series) SMO_getSubcatchSeries;
2428
%rename(get_node_series) SMO_getNodeSeries;
2529
%rename(get_link_series) SMO_getLinkSeries;

swmm-toolkit/swmm-solver

swmm-toolkit/tests/test_output.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,65 @@ def test_getelementname(handle):
8585
assert output.get_elem_name(handle, shared_enum.ElementType.NODE, 1) == "10"
8686

8787

88+
89+
def test_getdatetime(handle):
90+
date0 = output.get_date_time(handle, 0)
91+
date1 = output.get_date_time(handle, 1)
92+
assert isinstance(date0, (float, np.floating))
93+
94+
step_seconds = output.get_times(handle, shared_enum.Time.REPORT_STEP)
95+
step_days = step_seconds / 86400.0
96+
97+
# consecutive timestamps differ by exactly one report step (in days)
98+
assert np.isclose(date1 - date0, step_days)
99+
100+
# first timestamp should be strictly after the saved start date anchor
101+
assert date0 > output.get_start_date(handle)
102+
103+
104+
def test_getdateseries(handle):
105+
start, end = 0, 5
106+
dates = output.get_date_series(handle, start, end)
107+
108+
assert len(dates) == end - start + 1
109+
110+
step_days = output.get_times(handle, shared_enum.Time.REPORT_STEP) / 86400.0
111+
diffs = np.diff(dates)
112+
113+
# monotonic and evenly spaced by report step
114+
assert np.allclose(diffs, step_days)
115+
assert np.isclose(dates[-1], dates[0] + (end - start) * step_days)
116+
117+
118+
def test_decodedate(handle):
119+
# decoded components are plausible
120+
date0 = output.get_date_time(handle, 0)
121+
y, m, d, hh, mm, ss, dow = output.decode_date(date0)
122+
123+
assert 1 <= m <= 12
124+
assert 1 <= d <= 31
125+
assert 0 <= hh <= 23
126+
assert 0 <= mm <= 59
127+
assert 0 <= ss <= 59
128+
assert 1 <= dow <= 7
129+
130+
# consecutive decode respects the report step
131+
date1 = output.get_date_time(handle, 1)
132+
y1, m1, d1, hh1, mm1, ss1, dow1 = output.decode_date(date1)
133+
134+
step_seconds = output.get_times(handle, shared_enum.Time.REPORT_STEP)
135+
step_hours = (step_seconds // 3600) % 24
136+
137+
# minutes/seconds remain constant for steps divisible by 60s
138+
if step_seconds % 60 == 0:
139+
assert mm1 == mm
140+
assert ss1 == ss
141+
142+
# hour advances by step_hours modulo 24 (day rollover allowed)
143+
assert ((hh1 - hh) % 24) == step_hours
144+
145+
146+
88147
def test_getsubcatchseries(handle):
89148

90149
ref_array = np.array([0.0,

swmm-toolkit/tests/test_series.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
from datetime import datetime, timedelta
2+
import os
3+
import pytest
4+
from swmm.toolkit import solver, output, shared_enum
5+
6+
DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
7+
INPUT_FILE = os.path.join(DATA_PATH, "test_Example1.inp")
8+
REPORT_FILE = os.path.join(DATA_PATH, "temp_align.rpt")
9+
OUTPUT_FILE = os.path.join(DATA_PATH, "temp_align.out")
10+
11+
REPORT_STEP_SECONDS = 3600 # Example1
12+
13+
14+
def _correct_decimal_digits(t, r):
15+
"""
16+
Correct Decimal Digits (CDD) is computed as a bounded form of
17+
``-log10(abs(test_value - ref_value))``, which approximates how many
18+
decimal digits of ``test_value`` agree with ``ref_value``.
19+
"""
20+
import math
21+
22+
if t == r:
23+
return 10.0
24+
25+
tmp = abs(t - r)
26+
if tmp < 1.0e-7:
27+
tmp = 1.0e-7
28+
elif tmp > 2.0:
29+
tmp = 1.0
30+
31+
tmp = -math.log10(tmp)
32+
if tmp < 0.0:
33+
tmp = 0.0
34+
35+
return tmp
36+
37+
38+
def check_cdd_float(test: list[float], ref: list[float], cdd_tol: int) -> bool:
39+
"""
40+
Check the minimum number of correct decimal digits (CDD) between two
41+
float sequences. This function finds the minimum CDD over all element
42+
pairs in ``test`` and ``ref``, then checks whether ``floor(min_cdd)``
43+
is greater than or equal to ``cdd_tol``.
44+
45+
Parameters
46+
----------
47+
test : list[float]
48+
Sequence of test values to be compared.
49+
ref : list[float]
50+
Sequence of reference values used as the expected results.
51+
cdd_tol : int
52+
Required minimum number of correct decimal digits (integer threshold)
53+
that the minimum CDD over all pairs must meet or exceed.
54+
55+
Returns
56+
-------
57+
bool
58+
``True`` if ``test`` and ``ref`` have the same length and
59+
``floor(min_cdd) >= cdd_tol``; ``False`` otherwise (including if the
60+
sequences differ in length).
61+
"""
62+
import math
63+
64+
if len(test) != len(ref):
65+
return False
66+
67+
min_cdd = 10.0
68+
69+
for t, r in zip(test, ref):
70+
tmp = _correct_decimal_digits(t, r)
71+
72+
if tmp < min_cdd:
73+
min_cdd = tmp
74+
75+
return math.floor(min_cdd) >= cdd_tol
76+
77+
78+
def _get_current_datetime():
79+
y, m, d, hh, mm, ss = solver.simulation_get_current_datetime()
80+
return datetime(y, m, d, hh, mm, ss)
81+
82+
def build_link_flow_solver_tuples_aligned():
83+
tuples = []
84+
solver.swmm_open(INPUT_FILE, REPORT_FILE, OUTPUT_FILE)
85+
try:
86+
solver.swmm_start(0)
87+
# After start callback
88+
# period_end = _get_current_datetime
89+
# value = solver.link_get_result(0, shared_enum.LinkResult.FLOW)
90+
# tuples.append((period_end, value))
91+
92+
while True:
93+
# Before step callback
94+
#
95+
time_left = solver.swmm_stride(REPORT_STEP_SECONDS)
96+
# After step callback
97+
#
98+
if time_left == 0:
99+
break
100+
# Value for the interval that just ended; align to its period-end timestamp
101+
period_end = _get_current_datetime() - timedelta(seconds=REPORT_STEP_SECONDS)
102+
value = solver.link_get_result(0, shared_enum.LinkResult.FLOW)
103+
tuples.append((period_end, value))
104+
105+
# Before end callback
106+
period_end = _get_current_datetime() - timedelta(seconds=REPORT_STEP_SECONDS)
107+
value = solver.link_get_result(0, shared_enum.LinkResult.FLOW)
108+
tuples.append((period_end, value))
109+
110+
solver.swmm_end()
111+
# After end callback
112+
#
113+
114+
finally:
115+
solver.swmm_close()
116+
# After close callback
117+
#
118+
return tuples
119+
120+
def build_link_flow_output_tuples():
121+
EPOCH_SWMM = datetime(1899, 12, 30)
122+
h = output.init()
123+
output.open(h, os.path.join(DATA_PATH, "test_Example1.out"))
124+
try:
125+
start_days = output.get_start_date(h)
126+
rpt = output.get_times(h, shared_enum.Time.REPORT_STEP)
127+
n = output.get_times(h, shared_enum.Time.NUM_PERIODS)
128+
start_dt = EPOCH_SWMM + timedelta(days=start_days)
129+
vals = output.get_link_series(h, 0, shared_enum.LinkAttribute.FLOW_RATE, 0, n - 1)
130+
tuples = [(start_dt + timedelta(seconds=i * rpt), float(vals[i])) for i in range(n)]
131+
finally:
132+
output.close(h)
133+
return tuples
134+
135+
def test_compare_aligned_series():
136+
s = build_link_flow_solver_tuples_aligned()
137+
o = build_link_flow_output_tuples()
138+
139+
# times must match
140+
solver_times = [t.strftime("%Y-%m-%d %H:%M:%S") for t, _ in s]
141+
output_times = [t.strftime("%Y-%m-%d %H:%M:%S") for t, _ in o]
142+
assert solver_times == output_times, (
143+
"Time axes differ.\n"
144+
f"Solver times: {solver_times[:5]} ...\n"
145+
f"Output times: {output_times[:5]} ..."
146+
)
147+
148+
# values should match within tolerance
149+
solver_vals = [v for _, v in s]
150+
output_vals = [v for _, v in o]
151+
152+
assert check_cdd_float(solver_vals, output_vals, 1), (
153+
"Solver and output values differ. "
154+
"See zipped output for details:\n" +
155+
"\n".join(
156+
f"{t1.strftime('%Y-%m-%d %H:%M:%S')} | {v1:.6f} || {t2.strftime('%Y-%m-%d %H:%M:%S')} | {v2:.6f} | cdd={cdd(v1, v2):.2f}"
157+
for (t1, v1), (t2, v2) in list(zip(s, o))[:10]
158+
)
159+
)
160+

0 commit comments

Comments
 (0)