Coverage for bim2sim / plugins / PluginOpenFOAM / test / regression / test_openfoam.py: 0%

236 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-18 09:34 +0000

1import os 

2import sys 

3import filecmp 

4import difflib 

5import unittest 

6import logging 

7from pathlib import Path 

8 

9import difflib 

10import multiprocessing as mp 

11import queue # für queue.Empty 

12from typing import Optional 

13 

14import bim2sim 

15from bim2sim.tasks import common, bps 

16from bim2sim.plugins.PluginComfort.bim2sim_comfort import task as comfort_tasks 

17from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import task as ep_tasks 

18from bim2sim.plugins.PluginOpenFOAM.bim2sim_openfoam import task as of_tasks 

19from bim2sim.utilities.types import LOD, IFCDomain 

20from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler 

21from bim2sim.utilities.test import RegressionTestBase 

22 

23logger = logging.getLogger(__name__) 

24MAX_LINES = 200 

25 

26 

27class RegressionTestOpenFOAM(RegressionTestBase): 

28 """Class to set up and run CFD regression tests.""" 

29 

30 def setUp(self): 

31 self.old_stderr = sys.stderr 

32 self.working_dir = os.getcwd() 

33 self.ref_results_src_path = None 

34 self.results_src_dir = None 

35 self.results_dst_dir = None 

36 self.tester = None 

37 super().setUp() 

38 

39 def tearDown(self): 

40 os.chdir(self.working_dir) 

41 sys.stderr = self.old_stderr 

42 super().tearDown() 

43 

44 @staticmethod 

45 def _make_table_worker(q: mp.Queue, 

46 ref_lines, gen_lines, 

47 desc_from: str, desc_to: str, 

48 context_lines: int): 

49 try: 

50 differ = difflib.HtmlDiff(tabsize=4, wrapcolumn=80) 

51 table = differ.make_table( 

52 ref_lines, gen_lines, 

53 fromdesc=desc_from, todesc=desc_to, 

54 context=True, numlines=context_lines 

55 ) 

56 q.put(("ok", table)) 

57 except Exception as e: 

58 q.put(("err", f"{type(e).__name__}: {e}")) 

59 

60 def make_table_with_timeout(self, ref_lines, gen_lines, 

61 desc_from: str, desc_to: str, 

62 context_lines: int, 

63 timeout_s: float = 10.0) -> Optional[str]: 

64 q = mp.Queue(maxsize=1) 

65 p = mp.Process( 

66 target=self._make_table_worker, 

67 args=(q, ref_lines, gen_lines, desc_from, desc_to, context_lines), 

68 ) 

69 p.start() 

70 try: 

71 status, payload = q.get(timeout=timeout_s) 

72 except queue.Empty: 

73 p.kill() 

74 p.join() 

75 logger.warning('Table not finished due to timeout.') 

76 return None 

77 else: 

78 p.join() 

79 if status == "ok": 

80 return payload 

81 else: 

82 return None 

83 

84 def table_from_truncated_files(self, ref_lines, gen_lines, desc_from: str, 

85 desc_to: str, context_lines: int, 

86 truncated: bool): 

87 if len(ref_lines) > MAX_LINES or len(gen_lines) > MAX_LINES: 

88 truncated = True 

89 ref_trunc = ref_lines[:MAX_LINES] 

90 gen_trunc = gen_lines[:MAX_LINES] 

91 else: 

92 ref_trunc = ref_lines 

93 gen_trunc = gen_lines 

94 table_html = self.make_table_with_timeout(ref_trunc, gen_trunc, 

95 desc_from, desc_to, 

96 context_lines, 

97 timeout_s=10) 

98 return table_html, truncated 

99 

100 @staticmethod 

101 def truncate_html_tables(table_html: str, section_html: str): 

102 """Truncate individual html tables after 

103 MAX_LINES rows.""" 

104 lower = table_html.lower() 

105 tbody_start = lower.find("<tbody>") 

106 tbody_end = lower.find("</tbody>") 

107 if tbody_start != -1 and tbody_end != -1: 

108 prefix = table_html[:tbody_start + len("<tbody>")] 

109 tbody = table_html[tbody_start + len("<tbody>"):tbody_end] 

110 suffix = table_html[tbody_end:] 

111 rows = tbody.split("<tr") 

112 if len(rows) - 1 > MAX_LINES: 

113 truncated = True 

114 kept_rows = rows[0] # text before first <tr> 

115 for r in rows[1:MAX_LINES + 1]: 

116 kept_rows += "<tr" + r 

117 table_html = (prefix + kept_rows + "\n</tbody>" + 

118 suffix[suffix.lower().find("</table>"):]) 

119 else: 

120 truncated = False 

121 else: 

122 truncated = False 

123 section_html += f"{table_html}" 

124 if truncated: 

125 section_html += ( 

126 f"<p><i>Diff truncated to {MAX_LINES} rows in the HTML table; " 

127 f"additional diff rows were omitted.</i></p>") 

128 return section_html 

129 

130 def generate_html_diff_report(self, new_dir: Path, ref_dir: Path, 

131 output_html: str, context_lines: int = 5, 

132 truncate: str = None): 

133 """ 

134 Recursively compare reference vs generated directories and produce an HTML report. 

135 Returns a tuple (has_diffs: bool, html_path: str). 

136 """ 

137 html_sections = [] 

138 diffs_found = False 

139 final_diffs_found = 0 

140 for root, _, files in os.walk(ref_dir): 

141 rel_root = os.path.relpath(root, ref_dir) 

142 if 'temp' in rel_root.lower(): 

143 # exclude temp file directories from further diff checks as 

144 # they are obsolete 

145 break 

146 gen_root = new_dir / rel_root 

147 

148 if not gen_root.exists(): 

149 html_sections.append( 

150 f"<h3>Missing directory in generated: {rel_root}</h3>") 

151 diffs_found = True 

152 continue 

153 

154 for f in files: 

155 ref_file = Path(root) / f 

156 gen_file = gen_root / f 

157 if not gen_file.exists(): 

158 diffs_found = True 

159 html_sections.append(f"<h4>Missing file in generated: " 

160 f"{os.path.join(rel_root, f)}</h4>") 

161 final_diffs_found += 1 

162 continue 

163 if filecmp.cmp(ref_file, gen_file, shallow=False): 

164 continue # identical, skip 

165 diffs_found = True 

166 with open(ref_file, "r", encoding="utf-8", 

167 errors="replace") as rf: 

168 ref_lines = rf.read().splitlines() 

169 with open(gen_file, "r", encoding="utf-8", 

170 errors="replace") as gf: 

171 gen_lines = gf.read().splitlines() 

172 

173 desc_from = f"Reference: {os.path.join(rel_root, f)}" 

174 desc_to = f"Generated: {os.path.join(rel_root, f)}" 

175 truncated = False 

176 if truncate == 'file': 

177 table_html, truncated = self.table_from_truncated_files( 

178 ref_lines, gen_lines, desc_from, desc_to, 

179 context_lines, truncated) 

180 else: 

181 table_html = self.make_table_with_timeout( 

182 ref_lines, gen_lines, desc_from, desc_to, context_lines, 

183 timeout_s=10) 

184 if table_html is not None: 

185 if "No Differences Found".lower() in table_html.lower(): 

186 continue 

187 html_sections.append( 

188 f"<h2>{os.path.join(rel_root, f)}</h2>\n{table_html}") 

189 final_diffs_found += 1 

190 else: 

191 section_html = f"<h2>{os.path.join(rel_root, f)}</h2>\n" 

192 if truncate == 'table': 

193 section_html = self.truncate_scanned_table( 

194 table_html, section_html) 

195 else: 

196 section_html += f"{table_html}" 

197 if truncated: 

198 section_html += ( 

199 f"<p><i>Diff truncated to the first {MAX_LINES} lines of each file; " 

200 f"additional lines were omitted.</i></p>") 

201 html_sections.append(section_html) 

202 final_diffs_found += 1 

203 

204 # Check for unexpected extra files in generated_dir 

205 extra_files = [] 

206 for root, _, files in os.walk(new_dir): 

207 rel_root = os.path.relpath(root, new_dir) 

208 if 'temp' in rel_root.lower(): 

209 # exclude temp file directories from further diff checks as 

210 # they are obsolete 

211 break 

212 for f in files: 

213 gen_path = Path(root) / f 

214 ref_path = ref_dir / rel_root / f 

215 if not ref_path.exists(): 

216 extra_files.append(os.path.join(rel_root, f)) 

217 logger.warning( 

218 f"Extra file in generated: " 

219 f"{os.path.join(rel_root, f)}") 

220 final_diffs_found += 1 

221 

222 if extra_files: 

223 diffs_found = True 

224 extras_html = \ 

225 "<h3>Unexpected extra files in generated:</h3>\n<ul>\n" 

226 for ef in extra_files: 

227 extras_html += f"<li>{ef}</li>\n" 

228 extras_html += "</ul>\n" 

229 html_sections.insert(0, extras_html) # show extras near top 

230 

231 # Build full HTML page 

232 html_body = "\n<hr/>\n".join( 

233 html_sections) if html_sections else "<p>No differences found.</p>" 

234 html_page = f"""<!doctype html> 

235 <html lang="en"> 

236 <head> 

237 <meta charset="utf-8"> 

238 <title>Regression Test Diff Report</title> 

239 <style> 

240 body {{ font-family: Arial, sans-serif; padding: 1rem; }} 

241 h1 {{ margin-bottom: .5rem; }} 

242 table.diff {{ width: 100%; border-collapse: collapse; }} 

243 table.diff td, table.diff th {{ padding: 4px; vertical-align: top; font-family: monospace; }} 

244 /* difflib default classes: diff_header, diff_next, diff_add, diff_chg, diff_sub */ 

245 .diff_add {{ background-color: #99ffb6; }} /* added in generated */ 

246 .diff_chg {{ background-color: #ffe74d; }} /* changed */ 

247 .diff_sub {{ background-color: #ff808e; }} /* removed from reference */ 

248 .diff_header {{ background-color: #f0f0f0; font-weight: bold; }} 

249 .center {{ text-align:center; }} 

250 </style> 

251 </head> 

252 <body> 

253 <h1>Regression Test Diff Report</h1> 

254 <p><b>Reference dir:</b> {ref_dir}</p> 

255 <p><b>Generated dir:</b> {new_dir}</p> 

256 <hr/> 

257 {html_body} 

258 </body> 

259 </html> 

260 """ 

261 # Ensure parent exists 

262 out_path = Path(output_html) 

263 out_path.parent.mkdir(parents=True, exist_ok=True) 

264 out_path.write_text(html_page, encoding="utf-8") 

265 logger.warning(f"HTML diff report written to: {out_path}. Differences " 

266 f"were found in {final_diffs_found} files.") 

267 if final_diffs_found == 0: 

268 diffs_found = False 

269 return diffs_found, str(out_path) 

270 

271 def create_regression_setup(self): 

272 passed_regression_test = True 

273 ref_results_dir = Path(bim2sim.__file__).parent.parent \ 

274 / "test/resources/arch/regression_results" \ 

275 / self.project.name / 'OpenFOAM' 

276 sim_output_dir = self.project.paths.export / "OpenFOAM" 

277 regression_results_dir = (self.project.paths.root / 

278 "regression_results" / "cfd" / 

279 self.project.name / "OpenFOAM") 

280 regression_results_dir.mkdir(parents=True, exist_ok=True) 

281 html_report_path = regression_results_dir / "diff_report.html" 

282 logger.warning(f"Generating HTML diff report: {html_report_path}") 

283 has_diffs, report_path = self.generate_html_diff_report(sim_output_dir, 

284 ref_results_dir, 

285 html_report_path, 

286 truncate='file') 

287 if has_diffs: 

288 passed_regression_test = False 

289 logger.error( 

290 f"Regression test failed. Results are written to {report_path}.") 

291 return passed_regression_test 

292 

293 def run_regression_test(self): 

294 return self.create_regression_setup() 

295 

296 

297class TestRegressionOpenFOAMCase(RegressionTestOpenFOAM, unittest.TestCase): 

298 """Regression tests for PluginOpenFOAM.""" 

299 def test_regression_AC20_FZK_Haus(self): 

300 """Run PluginOpenFOAM regression test with AC20-FZK-Haus.ifc.""" 

301 ifc_path = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} 

302 project = self.create_project(ifc_path, "openfoam") 

303 

304 project.plugin_cls.default_tasks = [ 

305 common.LoadIFC, 

306 # common.CheckIfc, 

307 common.CreateElementsOnIfcTypes, 

308 bps.CreateSpaceBoundaries, 

309 bps.AddSpaceBoundaries2B, 

310 bps.CorrectSpaceBoundaries, 

311 common.CreateRelations, 

312 bps.DisaggregationCreationAndTypeCheck, 

313 bps.EnrichMaterial, 

314 bps.EnrichUseConditions, 

315 common.Weather, 

316 ep_tasks.CreateIdf, 

317 comfort_tasks.ComfortSettings, 

318 # ep_tasks.ExportIdfForCfd, 

319 # common.SerializeElements, 

320 ep_tasks.RunEnergyPlusSimulation, 

321 of_tasks.InitializeOpenFOAMSetup, 

322 of_tasks.CreateOpenFOAMGeometry, 

323 of_tasks.AddOpenFOAMComfort, 

324 of_tasks.CreateOpenFOAMMeshing, 

325 of_tasks.SetOpenFOAMBoundaryConditions, 

326 of_tasks.RunOpenFOAMMeshing, 

327 of_tasks.RunOpenFOAMSimulation 

328 ] 

329 

330 project.sim_settings.weather_file_path = \ 

331 (self.test_resources_path() / 

332 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') 

333 # project.sim_settings.ep_install_path = 'C://EnergyPlusV9-4-0/' 

334 project.sim_settings.cfd_export = True 

335 project.sim_settings.select_space_guid = '2RSCzLOBz4FAK$_wE8VckM' 

336 project.sim_settings.simulation_time = 12 

337 project.sim_settings.simulation_date = "01/14" 

338 project.sim_settings.add_heating = True 

339 project.sim_settings.heater_radiation = 0.6 

340 project.sim_settings.radiation_model = 'P1' 

341 project.sim_settings.add_airterminals = True 

342 project.sim_settings.inlet_type = 'SimpleStlDiffusor' 

343 project.sim_settings.outlet_type = 'SimpleStlDiffusor' 

344 project.sim_settings.mesh_size = 0.15 

345 project.sim_settings.cluster_max_runtime_simulation = "02:59:00" 

346 project.sim_settings.cluster_max_runtime_meshing = "00:20:00" 

347 project.sim_settings.cluster_jobname = "RegressionTest" 

348 project.sim_settings.cluster_compute_account = "test1234" 

349 project.sim_settings.cluster_cpu_per_node = 48 

350 project.sim_settings.n_procs = 72 

351 project.sim_settings.total_iterations = 5000 

352 

353 handler = DebugDecisionHandler(()) 

354 handler.handle(project.run()) 

355 

356 reg_test_res = self.run_regression_test() 

357 self.assertTrue(reg_test_res, 

358 "OpenFOAM Regression test did not finish successfully " 

359 "or created deviations.") 

360 

361 def test_regression_DigitalHub_SB89(self): 

362 """Run PluginOpenFOAM regression test with DigitalHub.""" 

363 ifc_paths = { 

364 IFCDomain.arch: 

365 Path(bim2sim.__file__).parent.parent / 

366 'test/resources/arch/ifc/FM_ARC_DigitalHub_with_SB89.ifc', 

367 IFCDomain.ventilation: 

368 Path(bim2sim.__file__).parent.parent / 

369 'test/resources/hydraulic/ifc/DigitalHub_Gebaeudetechnik' 

370 '-LUEFTUNG_v2.ifc', 

371 IFCDomain.hydraulic: 

372 Path(bim2sim.__file__).parent.parent / 

373 'test/resources/hydraulic/ifc/DigitalHub_Gebaeudetechnik-HEIZUNG_v2' 

374 '.ifc', 

375 } 

376 project = self.create_project(ifc_paths, "openfoam") 

377 project.plugin_cls.default_tasks = [ 

378 common.LoadIFC, 

379 # common.CheckIfc, 

380 common.CreateElementsOnIfcTypes, 

381 bps.CreateSpaceBoundaries, 

382 bps.AddSpaceBoundaries2B, 

383 bps.CorrectSpaceBoundaries, 

384 common.CreateRelations, 

385 bps.DisaggregationCreationAndTypeCheck, 

386 bps.EnrichMaterial, 

387 bps.EnrichUseConditions, 

388 common.Weather, 

389 ep_tasks.CreateIdf, 

390 comfort_tasks.ComfortSettings, 

391 # ep_tasks.ExportIdfForCfd, 

392 # common.SerializeElements, 

393 ep_tasks.RunEnergyPlusSimulation, 

394 of_tasks.InitializeOpenFOAMSetup, 

395 of_tasks.CreateOpenFOAMGeometry, 

396 of_tasks.AddOpenFOAMComfort, 

397 of_tasks.CreateOpenFOAMMeshing, 

398 of_tasks.SetOpenFOAMBoundaryConditions, 

399 of_tasks.RunOpenFOAMMeshing, 

400 of_tasks.RunOpenFOAMSimulation 

401 ] 

402 

403 project.sim_settings.weather_file_path = \ 

404 (self.test_resources_path() / 

405 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') 

406 project.sim_settings.prj_custom_usages = (Path( 

407 bim2sim.__file__).parent.parent / "test/resources/arch/custom_usages/" 

408 "customUsagesFM_ARC_DigitalHub_with_SB89.json") 

409 # project.sim_settings.ep_install_path = 'C://EnergyPlusV9-4-0/' 

410 project.sim_settings.cfd_export = True 

411 project.sim_settings.select_space_guid = '3hiy47ppf5B8MyZqbpTfpc' 

412 project.sim_settings.inlet_type = 'Original' 

413 project.sim_settings.outlet_type = 'Original' 

414 project.sim_settings.add_heating = True 

415 project.sim_settings.add_people = True 

416 project.sim_settings.add_floorheating = False 

417 project.sim_settings.add_airterminals = True 

418 project.sim_settings.add_comfort = True 

419 project.sim_settings.add_furniture = True 

420 project.sim_settings.add_people = True 

421 project.sim_settings.add_comfort = True 

422 project.sim_settings.furniture_setting = 'Office' 

423 project.sim_settings.furniture_amount = 8 

424 project.sim_settings.people_amount = 4 

425 project.sim_settings.people_setting = 'Seated' 

426 project.sim_settings.radiation_precondition_time = 4000 

427 project.sim_settings.radiation_model = 'preconditioned_fvDOM' 

428 project.sim_settings.output_keys = ['output_outdoor_conditions', 

429 'output_zone_temperature', 

430 'output_zone', 

431 'output_infiltration', 

432 'output_meters', 

433 'output_internal_gains'] 

434 

435 answers = ('Autodesk Revit', 'Autodesk Revit', *(None,) * 13, 

436 *('HVAC-AirTerminal',) * 3, *(None,) * 2, 2015) 

437 handler = DebugDecisionHandler(answers) 

438 handler.handle(project.run()) 

439 

440 reg_test_res = self.run_regression_test() 

441 self.assertTrue(reg_test_res, 

442 "OpenFOAM Regression test did not finish successfully " 

443 "or created deviations.")