Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pysagas/optimisation/cart3d/cart3d.py: 0%

332 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-30 04:27 +0000

1import os 

2import time 

3import glob 

4import pickle 

5import shutil 

6import subprocess 

7import numpy as np 

8import pandas as pd 

9import multiprocess as mp 

10from pysagas.flow import FlowState 

11from pysagas.cfd.cart3d import Cart3D 

12from hypervehicle.generator import Generator 

13from pyoptsparse import Optimizer, Optimization 

14from pysagas.optimisation.optimiser import ShapeOpt 

15from hypervehicle.utilities import SensitivityStudy 

16from pysagas.sensitivity.cart3d import Cart3DSensitivityCalculator 

17from pysagas.optimisation.cart3d.utilities import C3DPrep, combine_sense_data 

18 

19 

20np.seterr(all="ignore") 

21 

22 

23class Cart3DShapeOpt(ShapeOpt): 

24 """A wrapper to perform shape optimisation with Cart3D.""" 

25 

26 C3D_errors = [ 

27 "==> ADAPT failed", 

28 "Check cart3d.out in AD_A_J for more clues", 

29 "==> adjointErrorEst_quad failed again, status = 1", 

30 "ERROR: CUBES failed", 

31 "ERROR: ADAPT failed with status = 1", 

32 "ERROR", 

33 ] 

34 

35 def __init__( 

36 self, 

37 freestream: FlowState, 

38 optimiser: Optimizer, 

39 vehicle_generator: Generator, 

40 objective_callback: callable, 

41 jacobian_callback: callable, 

42 A_ref: float = 1.0, 

43 l_ref: float = 1.0, 

44 optimiser_options: dict = None, 

45 sensitivity_filename: str = "all_components_sensitivity.csv", 

46 working_dir_name: str = "working_dir", 

47 sim_directory_name: str = "simulation", 

48 basefiles_dir_name: str = "basefiles", 

49 c3d_log_name: str = "C3D_log", 

50 c3d_info_file: str = None, 

51 matching_tolerance: float = 1e-5, 

52 save_evolution: bool = True, 

53 write_config_xml: bool = True, 

54 sensitivity_kwargs: dict = None, 

55 ) -> None: 

56 """Initialise Cart3D Shape Optimiser. 

57 

58 Parameters 

59 ---------- 

60 a_inf : float 

61 The freestream speed of sound (m/s). 

62 

63 rho_inf : float 

64 The freestream density (kg/m^3). 

65 

66 V_inf : float 

67 The freestream velocity (m/s). 

68 

69 A_ref : float 

70 The aerodynamic reference area (m^2). 

71 

72 optimiser : Optimizer 

73 The pyoptsparse Optimizer object of choice. 

74 

75 optimiser_options : dict, optional 

76 The options to pass to the optimiser. See the PyOptSparse 

77 documentation for solver-specific options. The default is None. 

78 

79 vehicle_generator : Generator 

80 The vehicle generator object. 

81 

82 objective_callback : callable 

83 The callback function to compute and return the objective function 

84 value and constraint violation values. 

85 

86 jacobian_callback : callable 

87 The callback function to compute and return the Jacobian of the 

88 objective function value and constraint violation values. 

89 

90 sensitivity_filename : str, optional 

91 The filename of the combined components sensitivities. The default 

92 is 'all_components_sensitivity.csv'. 

93 

94 working_dir_name : str, optional 

95 The name of the working directory. The default is 'working_dir'. 

96 

97 sim_dir_name : str, optional 

98 The name of the simulation directory. The default is 'simulation'. 

99 

100 basefiles_dir_name : str, optional 

101 The name of the base files directory. The default is 'basefiles'. 

102 

103 c3d_log_name : str, optional 

104 The name to use for the Cart3D logfile. The default is C3D_log. 

105 

106 save_evolution : bool, optional 

107 A boolean flag to save geometry files of the evolving geometry. 

108 If True, the files will be saved to the evolution directory. The 

109 default is True. 

110 

111 write_config_xml : bool, optional 

112 A boolean flag to write the Cart3D Config.xml file when running 

113 comp2tri. If the geometry may be perturbed to a state where one 

114 component is no longer part of the wetted surface, writing the 

115 comp2tri file can cause set-up issues, and so it can be beneficial 

116 to turn it off. The default is True. 

117 """ 

118 # Define global variable so that functions can access them 

119 global c3d_logname, _matching_tolerance, _max_matching_tol, _matching_target 

120 global sens_filename, basefiles_dir, _c3dprepper, sim_dir_name 

121 global _A_ref, _l_ref 

122 global _freestream 

123 global home_dir, working_dir, f_sense_filename 

124 global obj_cb, jac_cb 

125 global generator 

126 global save_comptri, evo_dir 

127 global moo 

128 moo = False 

129 

130 _freestream = freestream 

131 

132 save_comptri = save_evolution 

133 

134 generator = vehicle_generator 

135 

136 # Construct paths 

137 home_dir = os.getcwd() 

138 basefiles_dir = os.path.join(home_dir, basefiles_dir_name) 

139 working_dir = os.path.join(home_dir, working_dir_name) 

140 sim_dir_name = sim_directory_name 

141 sens_filename = sensitivity_filename 

142 f_sense_filename = "F_sensitivities.csv" 

143 c3d_logname = c3d_log_name 

144 

145 if save_comptri: 

146 # Create evolution history directory 

147 evo_dir = os.path.join(home_dir, "evolution") 

148 if not os.path.exists(evo_dir): 

149 os.mkdir(evo_dir) 

150 

151 # Save callback functions 

152 obj_cb = objective_callback 

153 jac_cb = jacobian_callback 

154 

155 # Save reference area 

156 _A_ref = A_ref 

157 _l_ref = l_ref 

158 

159 # Create instance of Cart3D prepper 

160 _c3dprepper = C3DPrep( 

161 logfile=c3d_logname, info_file=c3d_info_file, write_config=write_config_xml 

162 ) 

163 

164 # Save sensitivity kwargs 

165 global _sens_kwargs 

166 _sens_kwargs = sensitivity_kwargs if sensitivity_kwargs else {} 

167 

168 # Other settings 

169 _matching_tolerance = matching_tolerance 

170 _max_matching_tol = 0.1 

171 _matching_target = 0.9 

172 

173 # Construct optimisation problem 

174 self.opt_problem = Optimization( 

175 name="Cart3D-PySAGAS Shape Optimisation", 

176 objFun=evaluate_objective, 

177 sens=evaluate_gradient, 

178 ) 

179 

180 # Complete super initialisation 

181 super().__init__( 

182 optimiser=optimiser, 

183 working_dir=working_dir, 

184 optimiser_options=optimiser_options, 

185 ) 

186 

187 

188class Cart3DMooShapeOpt(Cart3DShapeOpt): 

189 def __init__( 

190 self, 

191 a_inf: float, 

192 rho_inf: float, 

193 V_inf: float, 

194 A_ref: float, 

195 optimiser: Optimizer, 

196 vehicle_generator: Generator, 

197 objective_callback: callable, 

198 jacobian_callback: callable, 

199 mach_aoa_points: list[tuple[float, float]], 

200 optimiser_options: dict = None, 

201 sensitivity_filename: str = "all_components_sensitivity.csv", 

202 working_dir_name: str = "working_dir", 

203 sim_directory_name: str = "simulation", 

204 basefiles_dir_name: str = "basefiles", 

205 c3d_log_name: str = "C3D_log", 

206 c3d_info_file: str = None, 

207 matching_tolerance: float = 0.00001, 

208 save_evolution: bool = True, 

209 write_config_xml: bool = True, 

210 ) -> None: 

211 super().__init__( 

212 a_inf, 

213 rho_inf, 

214 V_inf, 

215 A_ref, 

216 optimiser, 

217 vehicle_generator, 

218 objective_callback, 

219 jacobian_callback, 

220 optimiser_options, 

221 sensitivity_filename, 

222 working_dir_name, 

223 sim_directory_name, 

224 basefiles_dir_name, 

225 c3d_log_name, 

226 c3d_info_file, 

227 matching_tolerance, 

228 save_evolution, 

229 write_config_xml, 

230 ) 

231 global sim_points 

232 sim_points = mach_aoa_points 

233 

234 global moo 

235 moo = True 

236 

237 # Overload 

238 self.opt_problem = Optimization( 

239 name="Cart3D-PySAGAS Shape Optimisation", 

240 objFun=evaluate_moo_objective, 

241 sens=evaluate_moo_gradient, 

242 ) 

243 

244 self.opt_problem: Optimization 

245 self.optimiser = optimiser(options=optimiser_options) 

246 

247 

248def evaluate_moo_objective(x: dict) -> dict: 

249 print("Evaluating objective function.") 

250 

251 # Pre-process parameters 

252 _process_parameters(x) 

253 

254 print("Deploying simulation tasks to pool.") 

255 pool = mp.Pool() 

256 results = [] 

257 for result in pool.starmap(aero_wrapper, sim_points): 

258 results.append(result) 

259 print(" Done.") 

260 

261 # Load properties 

262 properties_dir = glob.glob("*_properties") 

263 if properties_dir: 

264 volmass = pd.read_csv( 

265 glob.glob(os.path.join(properties_dir[0], "*volmass.csv"))[0], 

266 index_col=0, 

267 ) 

268 

269 # Fetch user-defined properties 

270 properties_file = glob.glob(os.path.join(properties_dir[0], "*properties.csv")) 

271 if len(properties_file) > 0: 

272 # File exists, load it 

273 properties = pd.read_csv( 

274 properties_file[0], 

275 index_col=0, 

276 )["0"] 

277 else: 

278 properties = None 

279 

280 # Objective function callback 

281 funcs = obj_cb( 

282 parameters=_unwrap_x(x), results=results, volmass=volmass, properties=properties 

283 ) 

284 

285 return funcs, False 

286 

287 

288def aero_wrapper(mach, aoa): 

289 freestream = FlowState(mach=mach, pressure=101e3, temperature=288, aoa=aoa) 

290 c3d = Cart3D(stl_files=stl_files, freestream=freestream, verbosity=0) 

291 flow_result = c3d.solve() 

292 return flow_result 

293 

294 

295def sens_wrapper(mach, aoa): 

296 freestream = FlowState(mach=mach, pressure=101e3, temperature=288, aoa=aoa) 

297 c3d = Cart3D(stl_files=stl_files, freestream=freestream, verbosity=0) 

298 

299 # Append sensitivity data to tri file 

300 sim_dir = os.path.join(working_dir, f"M{mach}A{float(aoa)}") 

301 components_filepath = os.path.join(sim_dir, "Components.i.tri") 

302 combine_sense_data( 

303 components_filepath=components_filepath, 

304 sensitivity_files=sensitivity_files, 

305 match_target=_matching_target, 

306 tol_0=_matching_tolerance, 

307 max_tol=_max_matching_tol, 

308 outdir=sim_dir, 

309 verbosity=0, 

310 ) 

311 

312 # Run the sensitivity solver 

313 sens_file = os.path.join(sim_dir, "all_components_sensitivity.csv") 

314 sens_result = c3d.solve_sens(sensitivity_filepath=sens_file) 

315 flow_result = c3d.flow_result 

316 

317 return flow_result, sens_result 

318 

319 

320def evaluate_moo_gradient(x: dict, objective: dict) -> dict: 

321 """Evaluates the gradient function at the parameter set `x`.""" 

322 print("Evaluating gradient function.") 

323 

324 # Pre-process parameters 

325 _process_parameters(x) 

326 

327 print("Deploying sensitivity tasks to pool.") 

328 pool = mp.Pool() 

329 results = [] 

330 for result in pool.starmap(sens_wrapper, sim_points): 

331 results.append(result) 

332 print(" Done.") 

333 

334 # Calculate Jacobian 

335 properties_dir = glob.glob("*_properties") 

336 if properties_dir: 

337 scalar_sens_dir = os.path.join("scalar_sensitivities") 

338 vm = pd.read_csv( 

339 glob.glob(os.path.join(properties_dir[0], "*volmass.csv"))[0], 

340 index_col=0, 

341 ) 

342 vm_sens = pd.read_csv( 

343 os.path.join(scalar_sens_dir, "volmass_sensitivity.csv"), 

344 index_col=0, 

345 )[x.keys()] 

346 

347 # Fetch user-defined properties and sensitivities 

348 properties_file = glob.glob(os.path.join(properties_dir[0], "*properties.csv")) 

349 if len(properties_file) > 0: 

350 # File exists, load it 

351 properties = pd.read_csv( 

352 properties_file[0], 

353 index_col=0, 

354 )["0"] 

355 

356 # Also load sensitivity file 

357 property_sens = pd.read_csv( 

358 os.path.join(scalar_sens_dir, "property_sensitivity.csv"), 

359 index_col=0, 

360 )[x.keys()] 

361 else: 

362 properties = None 

363 property_sens = None 

364 

365 else: 

366 # No properties data found 

367 vm = None 

368 vm_sens = None 

369 properties = None 

370 property_sens = None 

371 

372 # Call function 

373 jac = jac_cb( 

374 parameters=_unwrap_x(x), 

375 results=results, 

376 volmass=vm, 

377 volmass_sens=vm_sens, 

378 properties=properties, 

379 property_sens=property_sens, 

380 ) 

381 

382 return jac 

383 

384 

385def evaluate_objective(x: dict) -> dict: 

386 """Evaluates the objective function at the parameter set `x`.""" 

387 # Pre-process parameters 

388 _process_parameters(x) 

389 

390 # Run Cart3D simulation 

391 sim_success, loads_dict, _ = _run_simulation() 

392 

393 # Load properties 

394 properties_dir = glob.glob("*_properties") 

395 if properties_dir: 

396 volmass = pd.read_csv( 

397 glob.glob(os.path.join(properties_dir[0], "*volmass.csv"))[0], 

398 index_col=0, 

399 ) 

400 

401 # Load COG data 

402 cog = np.loadtxt( 

403 glob.glob(os.path.join(properties_dir[0], "*cog.txt"))[0], delimiter="," 

404 ) 

405 

406 # Fetch user-defined properties 

407 properties_file = glob.glob(os.path.join(properties_dir[0], "*properties.csv")) 

408 if len(properties_file) > 0: 

409 # File exists, load it 

410 properties = pd.read_csv( 

411 properties_file[0], 

412 index_col=0, 

413 )["0"] 

414 else: 

415 properties = None 

416 

417 else: 

418 # No properties data found 

419 volmass = None 

420 properties = None 

421 cog = None 

422 

423 # Call objective function 

424 failed = False 

425 if sim_success: 

426 # Evaluate objective function 

427 funcs: dict[str, dict[str, list]] = obj_cb( 

428 loads_dict=loads_dict, 

429 volmass=volmass, 

430 properties=properties, 

431 cog=cog, 

432 ) 

433 

434 # Check for nans 

435 for v1 in funcs.values(): 

436 if any(np.isnan(v1)): 

437 failed = True 

438 

439 else: 

440 # Simulation failed 

441 # funcs = {} 

442 loads_dict = {"C_L-entire": 0, "C_D-entire": 1} 

443 # funcs = obj_cb( 

444 # loads_dict=loads_dict, 

445 # volmass=volmass, 

446 # properties=properties, 

447 # cog=cog, 

448 # ) 

449 failed = True 

450 

451 if failed: 

452 funcs = {} 

453 

454 return funcs, failed 

455 

456 

457def evaluate_gradient(x: dict, objective: dict) -> dict: 

458 """Evaluates the gradient function at the parameter set `x`.""" 

459 # Pre-process parameters 

460 _process_parameters(x) 

461 

462 # Run Cart3D simulation 

463 _, loads_dict, components_plt_filepath = _run_simulation() 

464 

465 # Initialise filepaths 

466 components_filepath = components_plt_filepath 

467 sensitivity_filepath = os.path.join(working_dir, sens_filename) 

468 

469 # Create PySAGAS wrapper and run 

470 try: 

471 wrapper = Cart3DSensitivityCalculator( 

472 freestream=_freestream, 

473 sensitivity_filepath=sensitivity_filepath, 

474 components_filepath=components_filepath, 

475 verbosity=0, 

476 ) 

477 

478 except ValueError: 

479 # The sensitivity data does not match the point data, regenerate it 

480 tri_components_filepath = os.path.join( 

481 working_dir, sim_dir_name, "Components.i.tri" 

482 ) 

483 sensitivity_files = glob.glob(os.path.join("*sensitivity*")) 

484 combine_sense_data( 

485 tri_components_filepath, 

486 sensitivity_files=sensitivity_files, 

487 match_target=_matching_target, 

488 tol_0=_matching_tolerance, 

489 max_tol=_max_matching_tol, 

490 outdir=working_dir, 

491 verbosity=0, 

492 ) 

493 

494 # Re-instantiate the wrapper 

495 wrapper = Cart3DSensitivityCalculator( 

496 freestream=_freestream, 

497 sensitivity_filepath=sensitivity_filepath, 

498 components_filepath=components_filepath, 

499 verbosity=0, 

500 ) 

501 

502 result = wrapper.calculate(**_sens_kwargs) 

503 F_sense = result.f_sens 

504 M_sens = result.m_sens 

505 

506 # Non-dimensionalise 

507 coef_sens = F_sense / (_freestream.q * _A_ref) 

508 moment_coef_sens = M_sens / (_freestream.q * _A_ref * _l_ref) 

509 

510 # Calculate Jacobian 

511 properties_dir = glob.glob("*_properties") 

512 if properties_dir: 

513 scalar_sens_dir = os.path.join("scalar_sensitivities") 

514 vm = pd.read_csv( 

515 glob.glob(os.path.join(properties_dir[0], "*volmass.csv"))[0], 

516 index_col=0, 

517 ) 

518 vm_sens = pd.read_csv( 

519 os.path.join(scalar_sens_dir, "volmass_sensitivity.csv"), 

520 index_col=0, 

521 )[x.keys()] 

522 

523 # Load COG data 

524 cog = np.loadtxt( 

525 glob.glob(os.path.join(properties_dir[0], "*cog.txt"))[0], delimiter="," 

526 ) 

527 cog_sens_files = glob.glob( 

528 os.path.join(scalar_sens_dir, "*cog_sensitivity.txt") 

529 ) 

530 cog_sens = {} 

531 for file in cog_sens_files: 

532 param = file.split("_cog_sensitivity.txt")[0].split("/")[-1] 

533 cog_sens[param] = np.loadtxt(file, delimiter=",") 

534 

535 # Fetch user-defined properties and sensitivities 

536 properties_file = glob.glob(os.path.join(properties_dir[0], "*properties.csv")) 

537 if len(properties_file) > 0: 

538 # File exists, load it 

539 properties = pd.read_csv( 

540 properties_file[0], 

541 index_col=0, 

542 )["0"] 

543 

544 # Also load sensitivity file 

545 property_sens = pd.read_csv( 

546 os.path.join(scalar_sens_dir, "property_sensitivity.csv"), 

547 index_col=0, 

548 )[x.keys()] 

549 else: 

550 properties = None 

551 property_sens = None 

552 

553 else: 

554 # No properties data found 

555 vm = None 

556 vm_sens = None 

557 properties = None 

558 property_sens = None 

559 cog = None 

560 cog_sens = None 

561 

562 # Call function 

563 failed = False 

564 try: 

565 jac: dict[str, dict[str, list]] = jac_cb( 

566 parameters=x, 

567 coef_sens=coef_sens, 

568 moment_coef_sens=moment_coef_sens, 

569 loads_dict=loads_dict, 

570 volmass=vm, 

571 volmass_sens=vm_sens, 

572 properties=properties, 

573 property_sens=property_sens, 

574 cog=cog, 

575 cog_sens=cog_sens, 

576 ) 

577 

578 # Check for nans 

579 for v1 in jac.values(): 

580 for v2 in v1.values(): 

581 if any(np.isnan(v2)): 

582 failed = True 

583 

584 except Exception as e: 

585 print(f"Exception calling jacobian callback: {e}") 

586 failed = True 

587 

588 if failed: 

589 jac = {} 

590 

591 return jac, failed 

592 

593 

594def _process_parameters(x): 

595 # Move into working directory 

596 os.chdir(working_dir) 

597 

598 # Load existing parameters to compare 

599 already_started = _compare_parameters(x) 

600 

601 if not already_started: 

602 # These parameters haven't run yet, prepare the working directory 

603 if save_comptri: 

604 # Move components file into evolution directory 

605 comp_filepath = os.path.join(working_dir, sim_dir_name, "Components.i.tri") 

606 

607 # Check if file exists 

608 if os.path.exists(comp_filepath): 

609 # Determine new name 

610 new_filename = f"{len(os.listdir(evo_dir)):04d}.tri" 

611 

612 # Copy file over 

613 shutil.copyfile( 

614 comp_filepath, 

615 os.path.join(evo_dir, new_filename), 

616 ) 

617 

618 # Delete any files from previous run 

619 _clean_dir(working_dir) 

620 

621 # Dump design parameters to file 

622 with open("parameters.pkl", "wb") as f: 

623 pickle.dump(x, f) 

624 

625 # Generate vehicle and geometry sensitivities 

626 if len(glob.glob("*sensitivity*")) == 0 or not already_started: 

627 # No sensitivity files generated yet, or this is new geometry 

628 print("Running sensitivity study.") 

629 parameters = _unwrap_x(x) 

630 ss = SensitivityStudy(vehicle_constructor=generator, verbosity=0) 

631 ss.dvdp(parameter_dict=parameters, perturbation=2, write_nominal_stl=True) 

632 ss.to_csv() 

633 print(" Done.") 

634 

635 if moo: 

636 # Multi-objective optimisation 

637 global stl_files, sensitivity_files 

638 stl_files = glob.glob("*.stl") 

639 sensitivity_files = glob.glob("*sensitivity*") 

640 

641 # Also copy aero.csh and input.cntl into working directory 

642 for filename in ["input.cntl", "aero.csh"]: 

643 shutil.copyfile( 

644 os.path.join(home_dir, filename), 

645 os.path.join(working_dir, filename), 

646 ) 

647 shutil.copymode( 

648 os.path.join(home_dir, filename), 

649 os.path.join(working_dir, filename), 

650 ) 

651 

652 

653def _run_simulation(no_attempts: int = 3): 

654 """Prepare and run the CFD simulation with Cart3D. The simulation will be 

655 run in the 'simulation' subdirectory of the iteration directory. 

656 """ 

657 # Make simulation directory 

658 sim_dir = os.path.join(working_dir, sim_dir_name) 

659 run_intersect = False 

660 components_filepath = os.path.join(sim_dir, "Components.i.tri") 

661 if not os.path.exists(sim_dir): 

662 os.mkdir(sim_dir) 

663 run_intersect = True 

664 else: 

665 # Check for intersected file 

666 run_intersect = not os.path.exists(components_filepath) 

667 intersected = True 

668 

669 # Attempt component intersection 

670 sim_success = False 

671 for attempt in range(no_attempts): 

672 # Run intersect 

673 if run_intersect: 

674 _c3dprepper._log(f"SHAPEOPT INTERSECT ATTEMPT {attempt+1}") 

675 intersected = _c3dprepper.intersect_stls() 

676 

677 # Check for intersection 

678 if intersected: 

679 # Prepare rest of simulation directory 

680 if not os.path.exists(os.path.join(sim_dir, "input.cntl")): 

681 # Move files to simulation directory (including Components.i.tri) 

682 _c3dprepper.run_autoinputs() 

683 os.system( 

684 f"mv *.tri Config.xml input.c3d preSpec.c3d.cntl {sim_dir} >> {c3d_logname} 2>&1" 

685 ) 

686 

687 # Copy sim files and permissions 

688 for filename in ["input.cntl", "aero.csh"]: 

689 shutil.copyfile( 

690 os.path.join(basefiles_dir, filename), 

691 os.path.join(sim_dir, filename), 

692 ) 

693 shutil.copymode( 

694 os.path.join(basefiles_dir, filename), 

695 os.path.join(sim_dir, filename), 

696 ) 

697 

698 # Create all_components_sensitivity.csv 

699 if not os.path.exists(sens_filename): 

700 combine_sense_data( 

701 components_filepath, 

702 sensitivity_files=glob.glob("*sensitivity*"), 

703 match_target=_matching_target, 

704 tol_0=_matching_tolerance, 

705 max_tol=_max_matching_tol, 

706 verbosity=0, 

707 ) 

708 

709 # Run Cart3D and await result 

710 os.chdir(sim_dir) 

711 target_adapt = _infer_adapt(sim_dir) 

712 c3d_donefile = os.path.join(sim_dir, target_adapt, "FLOW", "DONE") 

713 _c3dprepper._log(f"Waiting for DONE file: {c3d_donefile}") 

714 run_cmd = "./aero.csh restart" 

715 _restarts = 0 

716 if not os.path.exists(c3d_donefile): 

717 with open(c3d_logname, "a") as f: 

718 subprocess.run( 

719 run_cmd, shell=True, stdout=f, stderr=subprocess.STDOUT 

720 ) 

721 while not os.path.exists(c3d_donefile): 

722 # Wait... 

723 time.sleep(5) 

724 

725 # Check for C3D failure 

726 running, e = _c3d_running() 

727 

728 if not running: 

729 # C3D failed, try restart it 

730 if _restarts > 3: 

731 return False 

732 

733 f = open(c3d_logname, "a") 

734 subprocess.run( 

735 run_cmd, shell=True, stdout=f, stderr=subprocess.STDOUT 

736 ) 

737 f.close() 

738 _restarts += 1 

739 

740 sim_success = True 

741 break 

742 

743 if sim_success: 

744 # Sim finished successfully, read loads file 

745 loads_dict = _read_c3d_loads( 

746 os.path.join(working_dir, sim_dir_name, "BEST/FLOW/loadsCC.dat") 

747 ) 

748 components_plt_filepath = os.path.join( 

749 working_dir, sim_dir_name, "BEST/FLOW/Components.i.plt" 

750 ) 

751 

752 else: 

753 loads_dict = None 

754 components_plt_filepath = None 

755 

756 # Change back to working directory 

757 os.chdir(working_dir) 

758 

759 return sim_success, loads_dict, components_plt_filepath 

760 

761 

762def _read_c3d_loads( 

763 loadsCC_filepath: str, 

764 b_frame: bool = True, 

765 v_frame: bool = True, 

766 moments: bool = True, 

767) -> dict: 

768 load_dict = {} 

769 with open(loadsCC_filepath, "r") as file: 

770 for line in file: 

771 if line[0] == "#": 

772 # Commented line, skip 

773 continue 

774 

775 # Remove linebreaks and multiple spaces 

776 line = " ".join(line.split()) 

777 words = line.split(":") 

778 

779 if len(words) == 0 or len(words) == 1: # skip if empty line 

780 continue 

781 

782 text = words[0] 

783 number = float(words[1]) 

784 word = text.split(" ") 

785 tag = word[0] 

786 coeff = word[-1] 

787 coeff = coeff[1:4] 

788 if b_frame is True: 

789 if coeff in ["C_A", "C_Y", "C_N"]: # get force in b_frame 

790 load_dict["{0}-{1}".format(coeff, tag)] = number 

791 if v_frame is True: 

792 if coeff in ["C_D", "C_S", "C_L"]: # get force in v_frame 

793 load_dict["{0}-{1}".format(coeff, tag)] = number 

794 if moments is True: 

795 if coeff in ["C_l", "C_m", "C_n", "C_M"]: # get moment coeff 

796 load_dict["{0}-{1}".format(coeff, tag)] = number 

797 

798 return load_dict 

799 

800 

801def _c3d_running() -> bool: 

802 """Watches the Cart3D log file to check for errors and return False 

803 if Cart3D has stopped running.""" 

804 with open(c3d_logname) as f: 

805 # Get last line in log file 

806 for line in f: 

807 pass 

808 

809 # Check if if it is in the known errors 

810 for e in Cart3DShapeOpt.C3D_errors: 

811 if e in line: 

812 return False, e 

813 

814 # No errors 

815 return True, None 

816 

817 

818def _infer_adapt(sim_dir) -> str: 

819 with open(f"{sim_dir}/aero.csh", "r") as f: 

820 lines = f.readlines() 

821 

822 for line in lines: 

823 if line.find("set n_adapt_cycles") != -1: 

824 return f"adapt{int(line.split('=')[-1]):02d}" 

825 

826 

827def _compare_parameters(x): 

828 """Compares the current parameters x to the last run simulation 

829 parameters.""" 

830 try: 

831 with open("parameters.pkl", "rb") as f: 

832 xp = pickle.load(f) 

833 

834 # Compare to current parameters 

835 already_run = x == xp 

836 

837 except FileNotFoundError: 

838 # Simulation not run yet 

839 already_run = False 

840 

841 return already_run 

842 

843 

844def _clean_dir(directory: str, keep: list = None): 

845 """Deletes everything in a directory except for what is specified 

846 in keep.""" 

847 all_files = os.listdir(directory) 

848 if keep is None: 

849 # Convert to empty list 

850 keep = [] 

851 rm_files = set(all_files) - set(keep) 

852 for f in rm_files: 

853 if os.path.isdir(f): 

854 shutil.rmtree(f) 

855 else: 

856 os.remove(f) 

857 

858 

859def _unwrap_x(x: dict) -> dict: 

860 """Unwraps an ordered dictionary.""" 

861 unwrapped = {} 

862 for key, val in x.items(): 

863 if len(val) == 1: 

864 unwrapped[key] = val[0] 

865 else: 

866 unwrapped[key] = val 

867 return unwrapped