Coverage for bim2sim/plugins/PluginComfort/bim2sim_comfort/task/ep_comfort_settings.py: 0%
181 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1"""Export EnergyPlus Comfort Settings.
3This module includes all functions for exporting Comfort Settings as EnergyPlus
4Input files (idf). Geometric preprocessing (includes EnergyPlus
5specific space boundary enrichment) and EnergyPlus Input File export must be
6executed before this module.
7"""
8import json
9import logging
10import os
11from pathlib import Path
13import bim2sim
14from bim2sim.elements.bps_elements import ThermalZone
15from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.task import \
16 IdfPostprocessing
17from bim2sim.tasks.base import ITask
18from bim2sim.utilities.common_functions import filter_elements
19from geomeppy import IDF
21logger = logging.getLogger(__name__)
24class ComfortSettings(ITask):
25 """
26 Create Comfort Settings for an EnergyPlus Input file.
28 Task to create Comfort Settings for an EnergyPlus Input file.
29 """
31 reads = ('elements', 'idf', 'sim_results_path')
32 touches = ('idf',)
34 def __init__(self, playground):
35 super().__init__(playground)
36 self.idf = None
38 def run(self, elements: dict, idf: IDF, sim_results_path: Path):
39 """Execute all methods to export comfort parameters to idf.
41 Execute all methods to export comfort parameters to idf.
42 Args:
43 elements: bim2sim elements
44 idf: eppy idf
45 sim_results_path (Path): path to simulation results.
46 """
47 logger.info("IDF extension in PluginComfort started ...")
48 self.add_comfort_to_people_enrichment(
49 idf, elements, self.playground.sim_settings.use_dynamic_clothing)
50 self.add_comfort_variables(idf)
51 # self.remove_empty_zones(idf)
52 self.remove_duplicate_names(idf)
53 self.remove_empty_zones(idf)
54 zone_dict_path = sim_results_path / self.prj_name / 'zone_dict.json'
55 if not zone_dict_path.exists():
56 IdfPostprocessing.write_zone_names(idf, elements, sim_results_path /
57 self.prj_name)
58 idf.save(idf.idfname)
60 return idf,
62 @staticmethod
63 def define_comfort_usage_dict():
64 """Define a new set of comfort parameters per use condition.
66 This method defines hardcoded schedules for clothing, air velocity
67 and work efficiency. Check values for applicability of these
68 parameters. The resulting json file is written to
69 bim2sim_comfort/assets/comfort_usage.json.
71 """
72 usage_path = Path(os.path.dirname(bim2sim.assets.__file__) +
73 '/enrichment/usage/UseConditions.json')
74 with open(usage_path, 'r+', encoding='utf-8') as file:
75 usage_dict = json.load(file)
77 comfort_usage_dict = {
78 'Default': {
79 'Clothing Insulation Schedule':
80 # ASHRAE 55, Trousers, long-sleeve shirt, suit jacket,
81 # vest, T-Shirt
82 [1.14] * 24,
83 'Air Velocity Schedule':
84 [0.0] * 24,
85 'Work Efficiency Schedule':
86 [0.00] * 24,
87 },
88 'Single office': {
89 'Clothing Insulation Schedule':
90 # ASHRAE 55, Trousers, long-sleeve Shirt + suit jacket
91 [0.96] * 24,
92 'Air Velocity Schedule':
93 [0.1] * 24,
94 'Work Efficiency Schedule':
95 [0.1] * 24,
96 },
97 'Living': {
98 'Clothing Insulation Schedule':
99 # ASHRAE 55, Sweat pants, long-sleeve sweatshirt
100 [0.74] * 24,
101 'Air Velocity Schedule':
102 [0.1] * 24,
103 'Work Efficiency Schedule':
104 [0.05] * 24,
105 },
106 'Bed room': {
107 'Clothing Insulation Schedule':
108 # ASHRAE 55, Sleepwear
109 [0.96] * 24,
110 'Air Velocity Schedule':
111 [0.1] * 24,
112 'Work Efficiency Schedule':
113 [0.0] * 24,
114 },
115 'WC and sanitary rooms in non-residential buildings': {
116 'Clothing Insulation Schedule':
117 # ASHRAE 55, Trousers, short-sleeve shirt
118 [0.57] * 24,
119 'Air Velocity Schedule':
120 [0.1] * 24,
121 'Work Efficiency Schedule':
122 [0.05] * 24,
123 },
124 'Kitchen in non-residential buildings': {
125 'Clothing Insulation Schedule':
126 # ASHRAE 55, Knee-length skirt, long-sleeve shirt, full slip
127 [0.67] * 24,
128 'Air Velocity Schedule':
129 [0.2] * 24,
130 'Work Efficiency Schedule':
131 [0.1] * 24,
132 },
133 'Traffic area': {
134 'Clothing Insulation Schedule':
135 # ASHRAE 55, Trousers, long-sleeve shirt + sweater + T-Shirt
136 [1.01] * 24,
137 'Air Velocity Schedule':
138 [0.2] * 24,
139 'Work Efficiency Schedule':
140 [0.1] * 24,
141 }
142 }
143 if not os.path.exists(Path(__file__).parent.parent / 'assets/'):
144 os.mkdir(Path(__file__).parent.parent / 'assets/')
145 with open(Path(__file__).parent.parent / 'assets/comfort_usage.json', 'w'
146 ) as cu:
147 json.dump(comfort_usage_dict, cu, indent=4)
148 cu.close()
150 def add_comfort_to_people_enrichment(self, idf: IDF, elements,
151 use_dynamic_clothing=False):
152 """Add comfort parameters to people objects in CreateIdf.
154 This method adds comfort parameters to people objects to the
155 input eppy idf.
156 Args:
157 idf: eppy idf
158 elements: bim2sim elements
159 use_dynamic_clothing: True if dynamic clothing (ASHRAE 55)
160 should be activated
162 """
163 spaces = filter_elements(elements, ThermalZone)
164 people_objs = idf.idfobjects['PEOPLE']
166 # load comfort schedules for individual usage definitions
167 with open(Path(__file__).parent.parent / 'assets/comfort_usage.json'
168 ) as cu:
169 plugin_comfort_dict = json.load(cu)
170 # define default schedules
171 self.set_day_week_year_limit_schedule(
172 idf, plugin_comfort_dict['Default']['Clothing Insulation Schedule'],
173 'Default_Clothing_Insulation_Schedule')
174 self.set_day_week_year_limit_schedule(
175 idf, plugin_comfort_dict['Default']['Air Velocity Schedule'],
176 'Default_Air_Velocity_Schedule')
177 self.set_day_week_year_limit_schedule(
178 idf, plugin_comfort_dict['Default']['Work Efficiency Schedule'],
179 'Default_Work_Efficiency_Schedule')
181 for space in spaces:
182 # get people_obj that has been defined in CreateIdf (internal loads)
183 people_obj = [p for p in people_objs if p.Name == space.guid][0]
184 if space.clothing_persons:
185 space_clothing = space.clothing_persons
186 if space.surround_clo_persons:
187 space_clothing += space.surround_clo_persons
188 clo_sched_name = ('Clothing_Insulation_Schedule_' +
189 space.usage.replace(',', ''))
190 if idf.getobject("SCHEDULE:YEAR", name=clo_sched_name) is None:
191 clothing = [space_clothing]*24
192 self.set_day_week_year_limit_schedule(
193 idf, clothing,
194 clo_sched_name)
195 else:
196 clo_sched_name = 'Default_Clothing_Insulation_Schedule'
198 if space.usage in plugin_comfort_dict.keys():
199 air_sched_name = ('Air_Velocity_Schedule_' +
200 space.usage.replace(',', ''))
201 work_eff_sched_name = ('Work_Efficiency_Schedule_' +
202 space.usage.replace(',', ''))
203 if idf.getobject("SCHEDULE:YEAR", name=air_sched_name) is None:
204 this_usage_dict = plugin_comfort_dict[space.usage]
205 self.set_day_week_year_limit_schedule(
206 idf, this_usage_dict['Air Velocity Schedule'],
207 air_sched_name)
208 self.set_day_week_year_limit_schedule(
209 idf, this_usage_dict['Work Efficiency Schedule'],
210 work_eff_sched_name)
211 else:
212 air_sched_name = 'Default_Air_Velocity_Schedule'
213 work_eff_sched_name = 'Default_Work_Efficiency_Schedule'
214 if use_dynamic_clothing:
215 people_obj.Clothing_Insulation_Calculation_Method = \
216 'DynamicClothingModelASHRAE55'
217 people_obj.\
218 Clothing_Insulation_Calculation_Method_Schedule_Name \
219 = 'DynamicClothingModelASHRAE55'
220 else:
221 people_obj.Clothing_Insulation_Schedule_Name = clo_sched_name
222 people_obj.Air_Velocity_Schedule_Name = air_sched_name
223 people_obj.Work_Efficiency_Schedule_Name = work_eff_sched_name
224 people_obj.Thermal_Comfort_Model_1_Type = 'Fanger'
225 people_obj.Thermal_Comfort_Model_2_Type = 'Pierce'
226 people_obj.Thermal_Comfort_Model_3_Type = 'AdaptiveASH55'
227 people_obj.Thermal_Comfort_Model_4_Type = 'AdaptiveCEN15251'
229 def add_comfort_to_people_manual(self, idf: IDF, elements,
230 use_dynamic_clothing=False):
231 """Manually add comfort parameters to people objects in CreateIdf.
233 This method manually adds comfort parameters to people objects to the
234 input eppy idf.
235 Args:
236 idf: eppy idf
237 elements: bim2sim elements
238 use_dynamic_clothing: True if dynamic clothing (ASHRAE 55)
239 should be activated
240 """
241 spaces = filter_elements(elements, ThermalZone)
242 people_objs = idf.idfobjects['PEOPLE']
244 # load comfort schedules for individual usage definitions
245 with open(Path(__file__).parent.parent / 'assets/comfort_usage.json'
246 ) as cu:
247 comfort_dict = json.load(cu)
248 # define default schedules
249 self.set_day_week_year_limit_schedule(
250 idf, comfort_dict['Default']['Clothing Insulation Schedule'],
251 'Default_Clothing_Insulation_Schedule')
252 self.set_day_week_year_limit_schedule(
253 idf, comfort_dict['Default']['Air Velocity Schedule'],
254 'Default_Air_Velocity_Schedule')
255 self.set_day_week_year_limit_schedule(
256 idf, comfort_dict['Default']['Work Efficiency Schedule'],
257 'Default_Work_Efficiency_Schedule')
259 for space in spaces:
260 # get people_obj that has been defined in CreateIdf(internal loads)
261 people_obj = [p for p in people_objs if p.Name == space.guid][0]
262 if space.usage in comfort_dict.keys():
263 clo_sched_name = ('Clothing_Insulation_Schedule_' +
264 space.usage.replace(',', ''))
265 air_sched_name = ('Air_Velocity_Schedule_' +
266 space.usage.replace(',', ''))
267 work_eff_sched_name = ('Work_Efficiency_Schedule_' +
268 space.usage.replace(',', ''))
269 if (idf.getobject("SCHEDULE:YEAR", name=clo_sched_name) is
270 None):
271 this_usage_dict = comfort_dict[space.usage]
272 self.set_day_week_year_limit_schedule(
273 idf, this_usage_dict['Clothing Insulation Schedule'],
274 clo_sched_name)
275 self.set_day_week_year_limit_schedule(
276 idf, this_usage_dict['Air Velocity Schedule'],
277 air_sched_name)
278 self.set_day_week_year_limit_schedule(
279 idf, this_usage_dict['Work Efficiency Schedule'],
280 work_eff_sched_name)
281 else:
282 clo_sched_name = 'Default_Clothing_Insulation_Schedule'
283 air_sched_name = 'Default_Air_Velocity_Schedule'
284 work_eff_sched_name = 'Default_Work_Efficiency_Schedule'
285 if use_dynamic_clothing:
286 people_obj.Clothing_Insulation_Calculation_Method = \
287 'DynamicClothingModelASHRAE55'
288 people_obj. \
289 Clothing_Insulation_Calculation_Method_Schedule_Name \
290 = 'DynamicClothingModelASHRAE55'
291 else:
292 people_obj.Clothing_Insulation_Schedule_Name = clo_sched_name
293 people_obj.Air_Velocity_Schedule_Name = air_sched_name
294 people_obj.Work_Efficiency_Schedule_Name = work_eff_sched_name
295 people_obj.Thermal_Comfort_Model_1_Type = 'Fanger'
296 people_obj.Thermal_Comfort_Model_2_Type = 'Pierce'
297 people_obj.Thermal_Comfort_Model_3_Type = 'AdaptiveASH55'
299 @staticmethod
300 def add_comfort_variables(idf: IDF):
301 """Add output variables for comfort measures to the input IDF file.
303 Args:
304 idf: eppy idf
306 """
307 idf.newidfobject(
308 "OUTPUT:VARIABLE",
309 Variable_Name="Zone Thermal Comfort Fanger Model PMV",
310 Reporting_Frequency="Hourly",
311 )
312 idf.newidfobject(
313 "OUTPUT:VARIABLE",
314 Variable_Name="Zone Thermal Comfort Fanger Model PPD",
315 Reporting_Frequency="Hourly",
316 )
317 idf.newidfobject(
318 "OUTPUT:VARIABLE",
319 Variable_Name="Zone Thermal Comfort Pierce Model Effective "
320 "Temperature PMV",
321 Reporting_Frequency="Hourly",
322 )
323 idf.newidfobject(
324 "OUTPUT:VARIABLE",
325 Variable_Name="Zone Thermal Comfort Pierce Model Discomfort Index",
326 Reporting_Frequency="Hourly",
327 )
328 idf.newidfobject(
329 "OUTPUT:VARIABLE",
330 Variable_Name="Zone Thermal Comfort ASHRAE 55 Adaptive Model 80% "
331 "Acceptability Status",
332 Reporting_Frequency="Hourly",
333 )
334 idf.newidfobject(
335 "OUTPUT:VARIABLE",
336 Variable_Name="Zone Thermal Comfort ASHRAE 55 Adaptive Model 90% "
337 "Acceptability Status",
338 Reporting_Frequency="Hourly",
339 )
340 idf.newidfobject(
341 "OUTPUT:VARIABLE",
342 Variable_Name="Zone Thermal Comfort ASHRAE 55 Adaptive Model "
343 "Running Average Outdoor Air Temperature",
344 Reporting_Frequency="Hourly",
345 )
346 idf.newidfobject(
347 "OUTPUT:VARIABLE",
348 Variable_Name="Zone Thermal Comfort CEN 15251 Adaptive Model "
349 "Category I Status",
350 Reporting_Frequency="Hourly",
351 )
352 idf.newidfobject(
353 "OUTPUT:VARIABLE",
354 Variable_Name="Zone Thermal Comfort CEN 15251 Adaptive Model "
355 "Category II Status",
356 Reporting_Frequency="Hourly",
357 )
358 idf.newidfobject(
359 "OUTPUT:VARIABLE",
360 Variable_Name="Zone Thermal Comfort CEN 15251 Adaptive Model "
361 "Category III Status",
362 Reporting_Frequency="Hourly",
363 )
364 idf.newidfobject(
365 "OUTPUT:VARIABLE",
366 Variable_Name="Zone Thermal Comfort CEN 15251 Adaptive Model "
367 "Running Average Outdoor Air Temperature",
368 Reporting_Frequency="Hourly",
369 )
370 idf.newidfobject(
371 "OUTPUT:VARIABLE",
372 Variable_Name="Zone Thermal Comfort CEN 15251 Adaptive Model "
373 "Temperature",
374 Reporting_Frequency="Hourly",
375 )
376 idf.newidfobject(
377 "OUTPUT:VARIABLE",
378 Variable_Name="Zone Thermal Comfort Clothing Value",
379 Reporting_Frequency="Hourly",
380 )
381 idf.newidfobject(
382 "OUTPUT:VARIABLE",
383 Variable_Name="Zone People Occupant Count",
384 Reporting_Frequency="Hourly",
385 )
387 if not "Zone Mean Air Temperature" in \
388 [v.Variable_Name for v in idf.idfobjects['OUTPUT:VARIABLE']]:
389 idf.newidfobject(
390 "OUTPUT:VARIABLE",
391 Variable_Name="Zone Mean Air Temperature",
392 Reporting_Frequency="Hourly",
393 )
395 @staticmethod
396 def set_day_week_year_limit_schedule(idf: IDF, schedule: list[float],
397 schedule_name: str,
398 limits_name: str = 'Any Number'):
399 """Set day, week and year schedule (hourly).
401 This function sets an hourly day, week and year schedule.
403 Args:
404 idf: idf file object
405 schedule: list of float values for the schedule (e.g.,
406 temperatures, loads)
407 schedule_name: str
408 limits_name: str, defaults set to 'Any Number'
409 """
410 if idf.getobject("SCHEDULETYPELIMITS", limits_name) is None:
411 idf.newidfobject("SCHEDULETYPELIMITS", Name=limits_name)
412 if idf.getobject("SCHEDULE:DAY:HOURLY", name=schedule_name) is None:
413 hours = {}
414 for i, l in enumerate(schedule[:24]):
415 hours.update({'Hour_' + str(i + 1): schedule[i]})
416 idf.newidfobject("SCHEDULE:DAY:HOURLY", Name=schedule_name,
417 Schedule_Type_Limits_Name=limits_name, **hours)
418 if idf.getobject("SCHEDULE:WEEK:COMPACT",
419 name=schedule_name) is None:
420 idf.newidfobject("SCHEDULE:WEEK:COMPACT", Name=schedule_name,
421 DayType_List_1="AllDays",
422 ScheduleDay_Name_1=schedule_name)
423 if idf.getobject("SCHEDULE:YEAR", name=schedule_name) is None:
424 idf.newidfobject("SCHEDULE:YEAR", Name=schedule_name,
425 Schedule_Type_Limits_Name=limits_name,
426 ScheduleWeek_Name_1=schedule_name,
427 Start_Month_1=1,
428 Start_Day_1=1,
429 End_Month_1=12,
430 End_Day_1=31)
432 @staticmethod
433 def remove_empty_zones(idf: IDF):
434 """Remove empty zones and zonegroups from idf.
436 Depending on the quality of the input ifc and their space
437 boundaries, empty thermal zones may be created (or be left after
438 corrections) in the resulting idf. This raises errors in thermal
439 comfort simulation. This method evaluates if a thermal zone has
440 surfaces in idf, and removes the zone from zonelists and zonegroups.
442 Args:
443 idf: eppy idf
445 """
446 zones = idf.idfobjects['ZONE']
447 surfaces = idf.getsurfaces()
448 zonelists = idf.idfobjects['ZONELIST']
449 zonegroups = idf.idfobjects['ZONEGROUP']
450 removed_zones = 0
451 for z in zones:
452 zone_has_surface = False
453 for s in surfaces:
454 if z.Name.upper() == s.Zone_Name.upper():
455 zone_has_surface = True
456 break
457 if not zone_has_surface:
458 idf.removeidfobject(z)
459 removed_zones +=1
460 if removed_zones > 0:
461 while zonelists:
462 for l in zonelists:
463 idf.removeidfobject(l)
464 while zonegroups:
465 for g in zonegroups:
466 idf.removeidfobject(g)
467 logger.warning('Removed %d empty zones from IDF', removed_zones)
469 @staticmethod
470 def remove_duplicate_names(idf: IDF):
471 """Test for duplicate idfobject names and remove objects.
473 EnergyPlus requires unique naming for IdfObjects, otherwise,
474 the simulation crashes. To increase the robustness of the
475 implementation, the idf is tested for duplicate names and duplicate
476 objects are removed. This decreases the accuracy of the model,
477 so the logging should be evaluated carefully for duplicate objects.
478 With a suitable IFC input model quality, no duplicate names should
479 be found.
481 Args:
482 idf: eppy idf
484 """
485 object_keys = [o for o in idf.idfobjects]
486 for key in object_keys:
487 names = []
488 objects = idf.idfobjects[key]
489 for o in objects:
490 if hasattr(o, 'Name'):
491 if o.Name.upper() in names:
492 logger.warning('DUPLICATE OBJECT: %s %s', key, o.Name)
493 idf.removeidfobject(o)
494 else:
495 names.append(o.Name.upper())
496 elif hasattr(o, 'Zone_Name'):
497 if o.Zone_Name.upper() in names:
498 logger.warning('DUPLICATE OBJECT: %s %s', key,
499 o.Zone_Name)
500 idf.removeidfobject(o)
501 else:
502 names.append(o.Zone_Name.upper())
503 else:
504 continue