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

1"""Export EnergyPlus Comfort Settings. 

2 

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 

12 

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 

20 

21logger = logging.getLogger(__name__) 

22 

23 

24class ComfortSettings(ITask): 

25 """ 

26 Create Comfort Settings for an EnergyPlus Input file. 

27 

28 Task to create Comfort Settings for an EnergyPlus Input file. 

29 """ 

30 

31 reads = ('elements', 'idf', 'sim_results_path') 

32 touches = ('idf',) 

33 

34 def __init__(self, playground): 

35 super().__init__(playground) 

36 self.idf = None 

37 

38 def run(self, elements: dict, idf: IDF, sim_results_path: Path): 

39 """Execute all methods to export comfort parameters to idf. 

40 

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) 

59 

60 return idf, 

61 

62 @staticmethod 

63 def define_comfort_usage_dict(): 

64 """Define a new set of comfort parameters per use condition. 

65 

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. 

70 

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) 

76 

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() 

149 

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. 

153 

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 

161 

162 """ 

163 spaces = filter_elements(elements, ThermalZone) 

164 people_objs = idf.idfobjects['PEOPLE'] 

165 

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') 

180 

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' 

197 

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' 

228 

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. 

232 

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'] 

243 

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') 

258 

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' 

298 

299 @staticmethod 

300 def add_comfort_variables(idf: IDF): 

301 """Add output variables for comfort measures to the input IDF file. 

302 

303 Args: 

304 idf: eppy idf 

305 

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 ) 

386 

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 ) 

394 

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). 

400 

401 This function sets an hourly day, week and year schedule. 

402 

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) 

431 

432 @staticmethod 

433 def remove_empty_zones(idf: IDF): 

434 """Remove empty zones and zonegroups from idf. 

435 

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. 

441 

442 Args: 

443 idf: eppy idf 

444 

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) 

468 

469 @staticmethod 

470 def remove_duplicate_names(idf: IDF): 

471 """Test for duplicate idfobject names and remove objects. 

472 

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. 

480 

481 Args: 

482 idf: eppy idf 

483 

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