Coverage for fiqus/mains/MainPancake3D.py: 40%

225 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-11-13 01:34 +0000

1import os 

2import sys 

3import math 

4import time as Time 

5import shutil 

6from copy import deepcopy 

7from enum import Enum 

8 

9from fiqus.data.DataFiQuSPancake3D import ( 

10 Pancake3DGeometry, 

11 Pancake3DMesh, 

12 Pancake3DSolve, 

13 Pancake3DPostprocess, 

14 Pancake3DSolveSaveQuantity, 

15) 

16 

17from fiqus.geom_generators.GeometryPancake3DUtils import spiralCurve 

18 

19if len(sys.argv) == 3: 

20 sys.path.insert(0, os.path.join(os.getcwd(), "steam-fiqus-dev")) 

21 

22class direction(Enum): 

23 """ 

24 A class to specify direction easily. 

25 """ 

26 

27 ccw = 0 

28 cw = 1 

29class Base: 

30 """ 

31 Base class for geometry, mesh, and solution classes. It is created to avoid code 

32 duplication and to make the code more readable. Moreover, it guarantees that 

33 all the classes have the same fundamental methods and attributes. 

34 """ 

35 

36 def __init__( 

37 self, 

38 fdm, 

39 geom_folder=None, 

40 mesh_folder=None, 

41 solution_folder=None, 

42 ) -> None: 

43 """ 

44 Define the fundamental attributes of the class. 

45 

46 :param fdm: fiqus data model 

47 :param geom_folder: folder where the geometry related files are stored 

48 :type geom_folder: str 

49 :param mesh_folder: folder where the mesh related files are stored 

50 :type mesh_folder: str 

51 :param solution_folder: folder where the solution related files are stored 

52 :type solution_folder: str 

53 """ 

54 

55 self.magnet_name = fdm.general.magnet_name 

56 self.geom_folder = geom_folder 

57 self.mesh_folder = mesh_folder 

58 self.solution_folder = solution_folder 

59 

60 self.dm = fdm # Data model 

61 self.geo: Pancake3DGeometry = fdm.magnet.geometry # Geometry data 

62 self.mesh: Pancake3DMesh = fdm.magnet.mesh # Mesh data 

63 self.solve: Pancake3DSolve = fdm.magnet.solve # Solve data 

64 self.pp: Pancake3DPostprocess = fdm.magnet.postproc # Postprocess data 

65 

66 self.geo_gui = False 

67 self.mesh_gui = False 

68 self.solve_gui = False 

69 self.python_postprocess_gui = False 

70 

71 if fdm.run.launch_gui: 

72 if fdm.run.type == "start_from_yaml": 

73 self.python_postprocess_gui = True 

74 elif fdm.run.type == "geometry_only": 

75 self.geo_gui = True 

76 elif fdm.run.type == "mesh_only": 

77 self.mesh_gui = True 

78 elif fdm.run.type == "geometry_and_mesh": 

79 self.mesh_gui = True 

80 elif fdm.run.type == "solve_only": 

81 self.solve_gui = True 

82 elif fdm.run.type == "post_process_getdp_only": 

83 self.solve_gui = True 

84 elif fdm.run.type == "solve_with_post_process_python": 

85 self.python_postprocess_gui = True 

86 elif fdm.run.type == "post_process_python_only": 

87 self.python_postprocess_gui = True 

88 

89 # Geometry related files: 

90 if self.geom_folder is not None: 

91 self.brep_file = os.path.join(self.geom_folder, f"{self.magnet_name}.brep") 

92 self.vi_file = os.path.join(self.geom_folder, f"{self.magnet_name}.vi") 

93 self.geometry_data_file = os.path.join(self.geom_folder, "geometry.yaml") 

94 

95 # Mesh related files: 

96 if self.mesh_folder is not None: 

97 self.mesh_file = os.path.join(self.mesh_folder, f"{self.magnet_name}.msh") 

98 self.regions_file = os.path.join( 

99 self.mesh_folder, f"{self.magnet_name}.regions" 

100 ) 

101 self.mesh_data_file = os.path.join(self.mesh_folder, "mesh.yaml") 

102 # Solution related files: 

103 if self.solution_folder is not None: 

104 self.pro_file = os.path.join( 

105 self.solution_folder, f"{self.magnet_name}.pro" 

106 ) 

107 

108 

109from fiqus.geom_generators.GeometryPancake3D import Geometry 

110from fiqus.mesh_generators.MeshPancake3D import Mesh 

111from fiqus.getdp_runners.RunGetdpPancake3D import Solve 

112from fiqus.post_processors.PostProcessPancake3D import Postprocess 

113from fiqus.data.DataFiQuS import FDM 

114 

115 

116class MainPancake3D: 

117 """ 

118 The main class for working with simulations for high-temperature superconductor 

119 pancake coil magnets. 

120 

121 Geometry can be created and saved as a BREP file. Parameters like the number of 

122 turns, tape dimensions, contact layer thicknesses, and other dimensions can be 

123 specified. Contact layers can be modeled as two-dimensional shells or three-dimensional 

124 volumes. Moreover, multiple pancakes can be stacked on top of each other. 

125 

126 Using the BREP file created, a mesh can be generated and saved as an MSH file. 

127 Winding mesh can be structured, and parameters like, azimuthal number of elements 

128 per turn, axial number of elements, and radial number of elements per turn can be 

129 specified for each pancake coil. The appropriate regions will be assigned to the 

130 relevant volumes accordingly so that finite element simulations can be done. 

131 

132 Using the mesh files, GetDP can be used to analyze Pancake3D coils. 

133 

134 :param fdm: FiQuS data model 

135 """ 

136 

137 def __init__(self, fdm, verbose): 

138 self.fdm: FDM = fdm 

139 self.GetDP_path = None 

140 

141 self.geom_folder = None 

142 self.mesh_folder = None 

143 self.solution_folder = None 

144 

145 def generate_geometry(self, gui=False): 

146 """ 

147 Generates the geometry of the magnet and save it as a BREP file. Moreover, a 

148 text file with the extension VI (volume information file) is generated, which 

149 stores the names of the volume tags in JSON format. 

150 """ 

151 geometry = Geometry( 

152 self.fdm, 

153 self.geom_folder, 

154 self.mesh_folder, 

155 self.solution_folder, 

156 ) 

157 if self.fdm.magnet.geometry.conductorWrite: 

158 self.fdm.magnet.geometry.gapBetweenPancakes = 0.00024 

159 innerRadius = self.fdm.magnet.geometry.winding.innerRadius 

160 gap = self.fdm.magnet.geometry.winding.thickness 

161 # gap = 0.01 

162 turns = self.fdm.magnet.geometry.numberOfPancakes 

163 # turns = 4 

164 z = - self.fdm.magnet.geometry.winding.height/2 

165 initialTheta = 0.0 

166 # transitionNotchAngle = self.fdm.magnet.geometry.wi.t ##TODO pull from internal recalculated 

167 # transitionNotchAngle = 0.7853981633974483/2 

168 d = direction(0) 

169 sc = spiralCurve(innerRadius, 

170 gap, 

171 turns, 

172 z, 

173 initialTheta, 

174 transitionNotchAngle, 

175 self.fdm.magnet.geometry, 

176 direction=d, # TODO code this to understand cw direction 

177 cutPlaneNormal=None) 

178 else: 

179 geometry.generate_geometry() 

180 geometry.generate_vi_file() 

181 

182 self.model_file = geometry.brep_file 

183 

184 def load_geometry(self, gui=False): 

185 """ 

186 Loads the previously generated geometry from the BREP file. 

187 """ 

188 geometry = Geometry( 

189 self.fdm, 

190 self.geom_folder, 

191 self.mesh_folder, 

192 self.solution_folder, 

193 ) 

194 

195 geometry.load_geometry() 

196 self.model_file = geometry.brep_file 

197 

198 def pre_process(self, gui=False): 

199 pass 

200 

201 def mesh(self, gui=False): 

202 """ 

203 Generates the mesh of the magnet, creates the physical regions, and saves it as 

204 an MSH file. Moreover, a text file with the extension REGIONS is generated, 

205 which stores the names and tags of the physical regions in YAML format. 

206 """ 

207 mesh = Mesh( 

208 self.fdm, 

209 self.geom_folder, 

210 self.mesh_folder, 

211 self.solution_folder, 

212 ) 

213 

214 mesh.generate_mesh() 

215 mesh.generate_regions() 

216 mesh.generate_mesh_file() 

217 

218 self.model_file = mesh.mesh_file 

219 

220 return {"gamma": 0} # to be modified with mesh_parameters (see multipole) 

221 

222 def load_mesh(self, gui=False): 

223 """ 

224 Loads the previously generated mesh from the MSH file. 

225 """ 

226 mesh = Mesh( 

227 self.fdm, 

228 self.geom_folder, 

229 self.mesh_folder, 

230 self.solution_folder, 

231 ) 

232 mesh.load_mesh() 

233 

234 self.model_file = mesh.mesh_file 

235 

236 def solve_and_postprocess_getdp(self, gui=False): 

237 """ 

238 Simulates the Pancake3D magnet with GetDP using the created mesh file and post 

239 processes the results. 

240 """ 

241 # calculate inductance and time constant: 

242 if self.fdm.magnet.solve.quantitiesToBeSaved is not None: 

243 save_list = list( 

244 map( 

245 lambda quantity_object: quantity_object.quantity, 

246 self.fdm.magnet.solve.quantitiesToBeSaved, 

247 ) 

248 ) 

249 new_solve_object_for_inductance_and_time_constant = { 

250 "type": "electromagnetic", 

251 "time": { 

252 "timeSteppingType": "adaptive", 

253 "extrapolationOrder": 1, 

254 "adaptiveSteppingSettings": { 

255 "initialStep": 1, 

256 "minimumStep": 0.00001, 

257 "maximumStep": 20, 

258 "integrationMethod": "Euler", 

259 "tolerances": [ 

260 { 

261 "quantity": "magnitudeOfMagneticField", 

262 "position": { 

263 "x": 0, 

264 "y": 0, 

265 "z": 0, 

266 }, 

267 "relative": 0.05, # 5% 

268 "absolute": 0.00002, # 0.00002 T 

269 "normType": "LinfNorm", 

270 } 

271 ], 

272 }, 

273 }, 

274 "nonlinearSolver": self.fdm.magnet.solve.nonlinearSolver, 

275 "air": { 

276 "permeability": 1.2566e-06, 

277 }, 

278 "initialConditions": { 

279 "temperature": 4, 

280 }, 

281 } 

282 inductance = None 

283 inductance_solution_folder = os.path.join( 

284 self.solution_folder, 

285 "inductance", 

286 ) 

287 inductance_file = os.path.join( 

288 inductance_solution_folder, "Inductance-TimeTableFormat.csv" 

289 ) 

290 if "inductance" in save_list: 

291 # to calculate inductance and time constant, we should run the simulation 

292 # with specigic power supply and material settings. 

293 copy_fdm = deepcopy(self.fdm) 

294 

295 new_solve_object_for_inductance_and_time_constant["time"]["start"] = 0 

296 new_solve_object_for_inductance_and_time_constant["time"]["end"] = 100 

297 new_solve_object_for_inductance_and_time_constant["winding"] = { 

298 "resistivity": 1e-20, 

299 "thermalConductivity": 1, 

300 "specificHeatCapacity": 1, 

301 } 

302 

303 new_solve_object_for_inductance_and_time_constant["contactLayer"] = { 

304 "resistivity": 1e-2, 

305 "thermalConductivity": 1, 

306 "specificHeatCapacity": 1, 

307 "numberOfThinShellElements": 1, 

308 } 

309 new_solve_object_for_inductance_and_time_constant["terminals"] = { 

310 "resistivity": 1e-10, 

311 "thermalConductivity": 1, 

312 "specificHeatCapacity": 1, 

313 "terminalContactLayer": { 

314 "resistivity": 1e-10, 

315 "thermalConductivity": 1, 

316 "specificHeatCapacity": 1, 

317 }, 

318 "transitionNotch": { 

319 "resistivity": 1e-10, 

320 "thermalConductivity": 1, 

321 "specificHeatCapacity": 1, 

322 }, 

323 } 

324 solve_object = Pancake3DSolve( 

325 **new_solve_object_for_inductance_and_time_constant 

326 ) 

327 solve_object.quantitiesToBeSaved = [Pancake3DSolveSaveQuantity(quantity="inductance")] 

328 copy_fdm.magnet.solve = solve_object 

329 

330 new_power_supply_object_for_inductance = { 

331 "t_control_LUT": [0, 0.1, 100], 

332 "I_control_LUT": [0, 1, 1], 

333 } 

334 new_power_supply_object_for_inductance = PowerSupply( 

335 **new_power_supply_object_for_inductance 

336 ) 

337 copy_fdm.power_supply = new_power_supply_object_for_inductance 

338 

339 # generate the folder if it does not exist: 

340 if not os.path.exists(inductance_solution_folder): 

341 os.makedirs(inductance_solution_folder) 

342 else: 

343 shutil.rmtree(inductance_solution_folder) 

344 os.makedirs(inductance_solution_folder) 

345 

346 solve = Solve( 

347 copy_fdm, 

348 self.GetDP_path, 

349 self.geom_folder, 

350 self.mesh_folder, 

351 inductance_solution_folder, 

352 ) 

353 solve.assemble_pro() 

354 

355 start_time = Time.time() 

356 solve.run_getdp(solve=True, postOperation=True) 

357 

358 with open(inductance_file, "r") as f: 

359 # read the last line and second column: 

360 inductance = float(f.readlines()[-1].split()[1].replace(",", "")) 

361 

362 with open(inductance_file, "w") as f: 

363 f.write(str(inductance)) 

364 

365 if "timeConstant" in save_list: 

366 # to calculate inductance and time constant, we should run the simulation 

367 # with specigic power supply and material settings. 

368 copy_fdm = deepcopy(self.fdm) 

369 

370 if inductance is None: 

371 raise ValueError( 

372 "Time constant can not be calculated without inductance. Please" 

373 " add 'inductance' to the 'quantitiesToBeSaved' list in the" 

374 " 'solve' section of the YAML file." 

375 ) 

376 

377 # equivalent radial resistance of the winding: 

378 # https://doi.org/10.1109/TASC.2021.3063653 

379 R_eq = 0 

380 for i in range(1, int(copy_fdm.magnet.geometry.winding.numberOfTurns)): 

381 r_i = ( 

382 copy_fdm.magnet.geometry.winding.innerRadius 

383 + i * copy_fdm.magnet.geometry.winding.thickness 

384 + (i - 1) * copy_fdm.magnet.geometry.contactLayer.thickness 

385 + copy_fdm.magnet.geometry.contactLayer.thickness / 2 

386 ) 

387 w_d = copy_fdm.magnet.geometry.winding.height 

388 R_ct = ( 

389 copy_fdm.magnet.solve.contactLayer.resistivity 

390 * copy_fdm.magnet.geometry.contactLayer.thickness 

391 ) 

392 R_eq = R_eq + R_ct / (2 * math.pi * r_i * w_d) 

393 

394 estimated_time_constant = inductance / R_eq * 3 

395 

396 new_solve_object_for_inductance_and_time_constant["time"]["start"] = 0 

397 new_solve_object_for_inductance_and_time_constant["time"][ 

398 "adaptiveSteppingSettings" 

399 ]["initialStep"] = (estimated_time_constant / 18) 

400 new_solve_object_for_inductance_and_time_constant["time"][ 

401 "adaptiveSteppingSettings" 

402 ]["minimumStep"] = (estimated_time_constant / 512) 

403 new_solve_object_for_inductance_and_time_constant["time"][ 

404 "adaptiveSteppingSettings" 

405 ]["maximumStep"] = (estimated_time_constant / 18) 

406 new_solve_object_for_inductance_and_time_constant["time"]["start"] = 0 

407 new_solve_object_for_inductance_and_time_constant["time"]["end"] = ( 

408 0.1 + estimated_time_constant * 8 + 0.001 

409 ) 

410 new_solve_object_for_inductance_and_time_constant["winding"] = ( 

411 copy_fdm.magnet.solve.winding.model_dump() 

412 ) 

413 new_solve_object_for_inductance_and_time_constant["contactLayer"] = ( 

414 copy_fdm.magnet.solve.contactLayer.model_dump() 

415 ) 

416 new_solve_object_for_inductance_and_time_constant["terminals"] = ( 

417 copy_fdm.magnet.solve.terminals.model_dump() 

418 ) 

419 solve_object = Pancake3DSolve( 

420 **new_solve_object_for_inductance_and_time_constant 

421 ) 

422 solve_object.quantitiesToBeSaved = [ 

423 Pancake3DSolveSaveQuantity(quantity="timeConstant") 

424 ] 

425 

426 solve_object.contactLayer.resistivity = copy_fdm.magnet.solve.contactLayer.resistivity 

427 copy_fdm.magnet.solve = solve_object 

428 

429 new_power_supply_object_for_time_constant = { 

430 "t_control_LUT": [ 

431 0, 

432 0.1, 

433 0.1 + estimated_time_constant * 6, 

434 0.1 + estimated_time_constant * 6 + 0.001, 

435 0.1 + estimated_time_constant * 8 + 0.001, 

436 ], 

437 "I_control_LUT": [0, 1, 1, 0, 0], 

438 } 

439 new_power_supply_object_for_time_constant = PowerSupply( 

440 **new_power_supply_object_for_time_constant 

441 ) 

442 copy_fdm.power_supply = new_power_supply_object_for_time_constant 

443 

444 time_constant_solution_folder = os.path.join( 

445 self.solution_folder, 

446 "time_constant", 

447 ) 

448 # generate the folder if it does not exist: 

449 if not os.path.exists(time_constant_solution_folder): 

450 os.makedirs(time_constant_solution_folder) 

451 solve = Solve( 

452 copy_fdm, 

453 self.GetDP_path, 

454 self.geom_folder, 

455 self.mesh_folder, 

456 time_constant_solution_folder, 

457 ) 

458 solve.assemble_pro() 

459 

460 solve.run_getdp(solve=True, postOperation=True) 

461 

462 # then compute the time constant and write it to a file: 

463 # read axialComponentOfTheMagneticFieldForTimeConstant-TimeTableFormat.txt 

464 

465 time_constant_file = os.path.join( 

466 time_constant_solution_folder, 

467 "axialComponentOfTheMagneticFieldForTimeConstant-TimeTableFormat.csv", 

468 ) 

469 times = [] 

470 Bzs = [] 

471 isNegative = False 

472 with open(time_constant_file, "r") as f: 

473 for line in f.readlines(): 

474 time = float(line.split()[1]) 

475 Bz = float(line.split()[-1]) 

476 times.append(time) 

477 if Bz < 0: 

478 isNegative = True 

479 Bzs.append(-Bz) 

480 else: 

481 if isNegative: 

482 raise ValueError( 

483 "The magnetic field should not change sign." 

484 ) 

485 Bzs.append(Bz) 

486 

487 # find the maximum value of Bz: 

488 max_Bz = max(Bzs) 

489 

490 # during decay, find the time when Bz is 1/e of the maximum value (36.8%): 

491 # Do linear interpolation if required: 

492 percents_of_max_Bz = [] 

493 for Bz_i in Bzs: 

494 percents_of_max_Bz.append(Bz_i / max_Bz) 

495 

496 peak_index = percents_of_max_Bz.index(max(percents_of_max_Bz)) 

497 for percent in percents_of_max_Bz[peak_index:]: 

498 if percent < 0.368: 

499 index = percents_of_max_Bz.index(percent) 

500 break 

501 

502 # find the time when percents_of_max_Bz 

503 x1 = times[index - 1] 

504 x2 = times[index] 

505 y1 = percents_of_max_Bz[index - 1] 

506 y2 = percents_of_max_Bz[index] 

507 

508 time_constant = ( 

509 x1 + (0.368 - y1) * (x2 - x1) / (y2 - y1) - times[peak_index] 

510 ) 

511 

512 # write the time constant to a file: 

513 time_constant_file = os.path.join( 

514 time_constant_solution_folder, "time_constant.csv" 

515 ) 

516 with open(time_constant_file, "w") as f: 

517 f.write(str(time_constant)) 

518 

519 # main solver: 

520 if "inductance" in save_list: 

521 for save_object in self.fdm.magnet.solve.quantitiesToBeSaved: 

522 if save_object.quantity == "inductance": 

523 self.fdm.magnet.solve.quantitiesToBeSaved.remove(save_object) 

524 

525 if "timeConstant" in save_list: 

526 for save_object in self.fdm.magnet.solve.quantitiesToBeSaved: 

527 if save_object.quantity == "timeConstant": 

528 self.fdm.magnet.solve.quantitiesToBeSaved.remove(save_object) 

529 

530 solve = Solve( 

531 self.fdm, 

532 self.GetDP_path, 

533 self.geom_folder, 

534 self.mesh_folder, 

535 self.solution_folder, 

536 ) 

537 solve.assemble_pro() 

538 

539 start_time = Time.time() 

540 solve.run_getdp(solve=True, postOperation=True) 

541 

542 return Time.time() - start_time 

543 

544 def post_process_getdp(self, gui=False): 

545 solve = Solve( 

546 self.fdm, 

547 self.GetDP_path, 

548 self.geom_folder, 

549 self.mesh_folder, 

550 self.solution_folder, 

551 ) 

552 solve.assemble_pro() 

553 

554 start_time = Time.time() 

555 solve.run_getdp(solve=False, postOperation=True) 

556 

557 return Time.time() - start_time 

558 

559 def post_process_python(self, gui=False): 

560 """ 

561 To be written. 

562 """ 

563 postprocess = Postprocess( 

564 self.fdm, 

565 self.geom_folder, 

566 self.mesh_folder, 

567 self.solution_folder, 

568 ) 

569 

570 if self.fdm.magnet.postproc is not None: 

571 if self.fdm.magnet.postproc.timeSeriesPlots is not None: 

572 postprocess.plotTimeSeriesPlots() 

573 if self.fdm.magnet.postproc.magneticFieldOnCutPlane is not None: 

574 postprocess.plotMagneticFieldOnCutPlane() 

575 

576 return {"overall_error": 0} 

577 

578 def plot_python(self): 

579 pass