Skip to content

Commit dd85b19

Browse files
authored
Merge pull request #23 from BuildingEnergySimulationTools/faster-reading-of-outputs
Faster reading of outputs
2 parents 8bf4985 + f113fc8 commit dd85b19

5 files changed

Lines changed: 137 additions & 45 deletions

File tree

energytool/base/idfobject_utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def add_output_variable(
246246
idf: IDF,
247247
key_values: str | list,
248248
variables,
249-
reporting_frequency: str = "Hourly",
249+
reporting_frequency: str = "Timestep",
250250
):
251251
"""
252252
This function allows you to add output:variable object to an EnergyPlus IDF file.
@@ -260,7 +260,8 @@ def add_output_variable(
260260
:param variables: The names of the variables to output. This can be a single
261261
variable name (string) or a list of variable names (list of strings).
262262
:param reporting_frequency: The reporting frequency for the output
263-
variables (e.g., "Hourly", "Daily", etc.). Default is "Hourly."
263+
variables (e.g., "Hourly", "Daily", etc.). Default is "Timestep" of the
264+
simulation.
264265
:return: None
265266
266267
The function iterates through the specified key values and variables, checking if
@@ -288,11 +289,13 @@ def add_output_variable(
288289
if key == "*":
289290
del_output_variable(idf, var)
290291

292+
freq = getattr(idf, "output_frequency", reporting_frequency)
293+
291294
idf.newidfobject(
292295
"OUTPUT:VARIABLE",
293296
Key_Value=key,
294297
Variable_Name=var,
295-
Reporting_Frequency=reporting_frequency,
298+
Reporting_Frequency=freq,
296299
)
297300

298301

energytool/building.py

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import os
55
import tempfile
66
import shutil
7+
import time
78

8-
from contextlib import contextmanager
9+
from contextlib import contextmanager, nullcontext
910
from copy import deepcopy
1011
from pathlib import Path
1112

@@ -15,6 +16,9 @@
1516
from eppy.runner.run_functions import run
1617
import eppy.json_functions as json_functions
1718

19+
import sqlite3
20+
import pandas as pd
21+
1822
import energytool.base.idf_utils
1923
from energytool.base.parse_results import read_eplus_res
2024
from energytool.outputs import get_results
@@ -39,6 +43,7 @@ class SimuOpt(enum.Enum):
3943
OUTPUTS = "outputs"
4044
EPW_FILE = "epw_file"
4145
VERBOSE = "verbose"
46+
OUTPUT_FREQUENCY = "OUTPUT_FREQUENCY"
4247

4348

4449
@contextmanager
@@ -53,7 +58,65 @@ def temporary_directory():
5358
yield temp_dir
5459

5560
finally:
56-
shutil.rmtree(temp_dir)
61+
for _ in range(20):
62+
try:
63+
shutil.rmtree(temp_dir)
64+
break
65+
except PermissionError:
66+
time.sleep(0.2)
67+
68+
69+
def ensure_sql_output(idf):
70+
objs = idf.idfobjects["OUTPUT:SQLITE"]
71+
if not objs:
72+
idf.newidfobject("OUTPUT:SQLITE", Option_Type="SimpleAndTabular")
73+
74+
75+
def read_sql_timeseries(sql_path, ref_year=None, unify_frequency=True):
76+
77+
query = """
78+
SELECT
79+
t.Month,
80+
t.Day,
81+
t.Hour,
82+
t.Minute,
83+
rdd.KeyValue || ':' || rdd.Name || ' [' || rdd.Units || '](' || rdd.ReportingFrequency || ')' AS variable,
84+
rd.Value
85+
FROM ReportData rd
86+
JOIN ReportDataDictionary rdd
87+
ON rd.ReportDataDictionaryIndex = rdd.ReportDataDictionaryIndex
88+
JOIN Time t
89+
ON rd.TimeIndex = t.TimeIndex
90+
"""
91+
92+
with sqlite3.connect(sql_path) as conn:
93+
df = pd.read_sql_query(query, conn)
94+
95+
if ref_year is None:
96+
ref_year = 2000
97+
98+
dt = pd.to_datetime(
99+
dict(
100+
year=ref_year,
101+
month=df.Month,
102+
day=df.Day,
103+
hour=df.Hour,
104+
minute=df.Minute,
105+
)
106+
)
107+
108+
df["datetime"] = dt
109+
110+
df = df.pivot(index="datetime", columns="variable", values="Value")
111+
df = df.sort_index()
112+
113+
if unify_frequency:
114+
step = df.index.to_series().diff().dropna().mode()[0]
115+
full_index = pd.date_range(df.index.min(), df.index.max(), freq=step)
116+
df = df.reindex(full_index)
117+
df = df.ffill()
118+
119+
return df
57120

58121

59122
class Building(Model):
@@ -200,6 +263,7 @@ def simulate(
200263
self,
201264
property_dict=None,
202265
simulation_options=None,
266+
working_directory=None,
203267
idf_save_path=None,
204268
**simulation_kwargs,
205269
) -> pd.DataFrame:
@@ -333,6 +397,12 @@ def simulate(
333397
),
334398
)
335399

400+
output_frequency = simulation_options.get(
401+
SimuOpt.OUTPUT_FREQUENCY.value,
402+
"Timestep",
403+
)
404+
working_idf.output_frequency = output_frequency
405+
336406
# PRE-PROCESS
337407
system_list = [sys for sublist in working_syst.values() for sys in sublist]
338408
for system in system_list:
@@ -342,29 +412,37 @@ def simulate(
342412
if SimuOpt.VERBOSE.value not in simulation_options.keys():
343413
simulation_options[SimuOpt.VERBOSE.value] = "v"
344414

415+
ensure_sql_output(working_idf)
416+
417+
import gc
418+
419+
gc.collect()
420+
345421
# SIMULATE
346-
with temporary_directory() as temp_dir:
422+
if working_directory is None:
423+
context = temporary_directory()
424+
else:
425+
working_directory = Path(working_directory)
426+
working_directory.mkdir(parents=True, exist_ok=True)
427+
context = nullcontext(working_directory)
428+
429+
with context as temp_dir:
430+
347431
working_idf.saveas((Path(temp_dir) / "in.idf").as_posix(), encoding="utf-8")
348432
idd_ref = working_idf.idd_version
349433
run(
350434
idf=working_idf,
351435
weather=epw_path,
352-
output_directory=temp_dir.replace("\\", "/"),
436+
output_directory=Path(temp_dir).as_posix(),
353437
annual=False,
354438
design_day=False,
355-
idd=None,
356-
epmacro=False,
357-
expandobjects=False,
358-
readvars=True,
359-
output_prefix=None,
360-
output_suffix=None,
361-
version=False,
439+
readvars=False,
362440
verbose=simulation_options[SimuOpt.VERBOSE.value],
363441
ep_version=f"{idd_ref[0]}-{idd_ref[1]}-{idd_ref[2]}",
364442
)
365443

366-
eplus_res = read_eplus_res(
367-
Path(temp_dir) / "eplusout.csv", ref_year=ref_year
444+
eplus_res = read_sql_timeseries(
445+
Path(temp_dir) / "eplusout.sql", ref_year=ref_year
368446
)
369447

370448
# Save IDF file after pre-process

tests/base/test_idfobject_utils.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,35 @@ def test_add_output_zone_variable(self, toy_idf):
3131
add_output_variable(toy_idf, key_values="Zone_1", variables="Conso")
3232

3333
to_test = [elmt["obj"] for elmt in toy_idf.idfobjects["Output:Variable"]]
34-
ref = [["OUTPUT:VARIABLE", "Zone_1", "Conso", "Hourly"]]
34+
ref = [["OUTPUT:VARIABLE", "Zone_1", "Conso", "Timestep"]]
3535
assert to_test == ref
3636

3737
add_output_variable(toy_idf, key_values=["Zone_1", "Zone_2"], variables="Conso")
3838

3939
to_test = [elmt["obj"] for elmt in toy_idf.idfobjects["Output:Variable"]]
4040
ref = [
41-
["OUTPUT:VARIABLE", "Zone_1", "Conso", "Hourly"],
42-
["OUTPUT:VARIABLE", "Zone_2", "Conso", "Hourly"],
41+
["OUTPUT:VARIABLE", "Zone_1", "Conso", "Timestep"],
42+
["OUTPUT:VARIABLE", "Zone_2", "Conso", "Timestep"],
4343
]
4444
assert to_test == ref
4545

4646
add_output_variable(toy_idf, key_values="Zone_3", variables=["Conso", "Elec"])
4747

4848
to_test = [elmt["obj"] for elmt in toy_idf.idfobjects["Output:Variable"]]
4949
ref = [
50-
["OUTPUT:VARIABLE", "Zone_1", "Conso", "Hourly"],
51-
["OUTPUT:VARIABLE", "Zone_2", "Conso", "Hourly"],
52-
["OUTPUT:VARIABLE", "Zone_3", "Conso", "Hourly"],
53-
["OUTPUT:VARIABLE", "Zone_3", "Elec", "Hourly"],
50+
["OUTPUT:VARIABLE", "Zone_1", "Conso", "Timestep"],
51+
["OUTPUT:VARIABLE", "Zone_2", "Conso", "Timestep"],
52+
["OUTPUT:VARIABLE", "Zone_3", "Conso", "Timestep"],
53+
["OUTPUT:VARIABLE", "Zone_3", "Elec", "Timestep"],
5454
]
5555
assert to_test == ref
5656

5757
add_output_variable(toy_idf, key_values="*", variables="Conso")
5858

5959
to_test = [elmt["obj"] for elmt in toy_idf.idfobjects["Output:Variable"]]
6060
ref = [
61-
["OUTPUT:VARIABLE", "Zone_3", "Elec", "Hourly"],
62-
["OUTPUT:VARIABLE", "*", "Conso", "Hourly"],
61+
["OUTPUT:VARIABLE", "Zone_3", "Elec", "Timestep"],
62+
["OUTPUT:VARIABLE", "*", "Conso", "Timestep"],
6363
]
6464
assert to_test == ref
6565

tests/test_building.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def test_building(self):
2121
"epw_file": (RESOURCES_PATH / "B4R_weather_Paris_2020.epw").as_posix(),
2222
}
2323
simulation_options = {
24-
SimuOpt.OUTPUTS.value: f"{OutputCategories.SYSTEM.value}|{OutputCategories.RAW.value}"
24+
SimuOpt.OUTPUTS.value: f"{OutputCategories.SYSTEM.value}|{OutputCategories.RAW.value}",
25+
SimuOpt.OUTPUT_FREQUENCY.value: "Hourly",
2526
}
2627

2728
res = test_build.simulate(
@@ -62,7 +63,7 @@ def test_building(self):
6263
"[J](Hourly)": 15276675722.295742,
6364
"BLOCK2:APPTX2E IDEAL LOADS AIR:"
6465
"Zone Ideal Loads Supply Air Total Heating Energy "
65-
"[J](Hourly) ": 15677421945.907581,
66+
"[J](Hourly)": 15677421945.907581,
6667
},
6768
rel=0.05,
6869
)
@@ -84,6 +85,7 @@ def test_boundaries(self):
8485
SimuOpt.OUTPUTS.value: f""
8586
f"{OutputCategories.SYSTEM.value}|"
8687
f"{OutputCategories.RAW.value}",
88+
SimuOpt.OUTPUT_FREQUENCY.value: "Hourly",
8789
}
8890

8991
res = test_build.simulate(
@@ -117,7 +119,7 @@ def test_boundaries(self):
117119
"[J](Hourly)": 3571575271.9865627,
118120
"BLOCK2:APPTX2E IDEAL LOADS AIR:"
119121
"Zone Ideal Loads Supply Air Total Heating Energy "
120-
"[J](Hourly) ": 3636379021.517704,
122+
"[J](Hourly)": 3636379021.517704,
121123
},
122124
rel=0.05,
123125
)

tests/test_system.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,19 @@ def test_heater_simple(self, idf):
146146
"OUTPUT:VARIABLE",
147147
"*",
148148
"Zone Other Equipment Total Heating Energy",
149-
"Hourly",
149+
"Hourly", # as defined in idf
150150
],
151151
[
152152
"OUTPUT:VARIABLE",
153153
"Block1:ApptX1W Ideal Loads Air",
154154
"Zone Ideal Loads Supply Air Total Heating Energy",
155-
"Hourly",
155+
"Timestep",
156156
],
157157
[
158158
"OUTPUT:VARIABLE",
159159
"Block1:ApptX1E Ideal Loads Air",
160160
"Zone Ideal Loads Supply Air Total Heating Energy",
161-
"Hourly",
161+
"Timestep",
162162
],
163163
]
164164

@@ -188,7 +188,7 @@ def test_heating_auxiliary(self):
188188
"OUTPUT:VARIABLE",
189189
"Block1:ApptX1W Ideal Loads Air",
190190
"Zone Ideal Loads Supply Air Total Heating Energy",
191-
"Hourly",
191+
"Timestep",
192192
],
193193
]
194194
energyplus_results = read_eplus_res(RESOURCES_PATH / "test_res.csv")
@@ -250,11 +250,14 @@ def test_ahu(self):
250250
"outputs": "SYSTEM",
251251
},
252252
)
253-
254-
assert results.sum().to_dict() == {
255-
"TOTAL_SYSTEM_Energy_[J]": 634902466.3027192,
256-
"VENTILATION_Energy_[J]": 634902466.3027192,
257-
}
253+
# results outputs : 30min
254+
assert results.sum().to_dict() == approx(
255+
{
256+
"TOTAL_SYSTEM_Energy_[J]": 634902466.3027192 * 2,
257+
"VENTILATION_Energy_[J]": 634902466.3027192 * 2,
258+
},
259+
rel=0.05,
260+
)
258261

259262
def test_dhw_ideal_external(self):
260263
building = Building(idf_path=RESOURCES_PATH / "test.idf")
@@ -270,10 +273,13 @@ def test_dhw_ideal_external(self):
270273
},
271274
)
272275

273-
assert results.sum().to_dict() == {
274-
"DHW_Energy_[J]": 8430408987.330694,
275-
"TOTAL_SYSTEM_Energy_[J]": 8430408987.330694,
276-
}
276+
assert results.sum().to_dict() == approx(
277+
{
278+
"DHW_Energy_[J]": 8430408987.330694,
279+
"TOTAL_SYSTEM_Energy_[J]": 8430408987.330694,
280+
},
281+
rel=0.05,
282+
)
277283

278284
def test_artificial_lighting(self):
279285
building = Building(idf_path=RESOURCES_PATH / "test.idf")
@@ -337,10 +343,13 @@ def test_ahu_control(self):
337343
},
338344
)
339345

340-
assert results.sum().to_dict() == {
341-
"TOTAL_SYSTEM_Energy_[J]": 5800244198.831992,
342-
"VENTILATION_Energy_[J]": 5800244198.831992,
343-
}
346+
assert results.sum().to_dict() == approx(
347+
{
348+
"TOTAL_SYSTEM_Energy_[J]": 5800244198.831992*2,
349+
"VENTILATION_Energy_[J]": 5800244198.831992*2,
350+
},
351+
rel=0.05,
352+
)
344353

345354
def test_other_equipments(self):
346355
tested_idf = IDF(RESOURCES_PATH / "test.idf")

0 commit comments

Comments
 (0)