Coverage for fiqus/data/DataFiQuSPancake3D.py: 79%

1010 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2024-05-20 03:24 +0200

1from typing import Literal, Optional, Annotated 

2from contextvars import ContextVar 

3import logging 

4import math 

5import pathlib 

6import scipy.integrate 

7from functools import cached_property 

8from pydantic import ( 

9 BaseModel, 

10 PositiveFloat, 

11 NonNegativeFloat, 

12 PositiveInt, 

13 Field, 

14 field_validator, 

15 model_validator, 

16 computed_field, 

17 ValidationInfo, 

18) 

19import pandas as pd 

20import numpy as np 

21import matplotlib 

22from annotated_types import Len 

23 

24logger = logging.getLogger(__name__) 

25 

26# ====================================================================================== 

27# Available materials: ================================================================= 

28NormalMaterialName = Literal[ 

29 "Copper", "Hastelloy", "Silver", "Indium", "Stainless Steel" 

30] 

31SuperconductingMaterialName = Literal["HTSSuperPower"] 

32# ====================================================================================== 

33# ====================================================================================== 

34 

35# ====================================================================================== 

36# Material information: ================================================================ 

37resistivityMacroNames = { 

38 "Copper": "MATERIAL_Resistivity_Copper_T_B", 

39 "Hastelloy": "MATERIAL_Resistivity_Hastelloy_T", 

40 "Silver": "MATERIAL_Resistivity_Silver_T_B", 

41 "Indium": "MATERIAL_Resistivity_Indium_T", 

42 "Stainless Steel": "MATERIAL_Resistivity_SSteel_T", 

43} 

44thermalConductivityMacroNames = { 

45 "Copper": "MATERIAL_ThermalConductivity_Copper_T_B", 

46 "Hastelloy": "MATERIAL_ThermalConductivity_Hastelloy_T", 

47 "Silver": "MATERIAL_ThermalConductivity_Silver_T", 

48 "Indium": "MATERIAL_ThermalConductivity_Indium_T", 

49 "Stainless Steel": "MATERIAL_ThermalConductivity_SSteel_T", 

50} 

51heatCapacityMacroNames = { 

52 "Copper": "MATERIAL_SpecificHeatCapacity_Copper_T", 

53 "Hastelloy": "MATERIAL_SpecificHeatCapacity_Hastelloy_T", 

54 "Silver": "MATERIAL_SpecificHeatCapacity_Silver_T", 

55 "Indium": "MATERIAL_SpecificHeatCapacity_Indium_T", 

56 "Stainless Steel": "MATERIAL_SpecificHeatCapacity_SSteel_T", 

57} 

58getdpTSAOnlyResistivityFunctions = { 

59 "Indium": "TSA_CFUN_rhoIn_T_constantThickness_fct_only", 

60 "Stainless Steel": None, 

61} 

62getdpTSAMassResistivityFunctions = { 

63 "Indium": "TSA_CFUN_rhoIn_T_constantThickness_mass", 

64 "Stainless Steel": None, 

65} 

66getdpTSAStiffnessResistivityFunctions = { 

67 "Indium": "TSA_CFUN_rhoIn_T_constantThickness_stiffness", 

68 "Stainless Steel": None, 

69} 

70getdpTSAMassThermalConductivityFunctions = { 

71 "Indium": "TSA_CFUN_kIn_constantThickness_mass", 

72 "Stainless Steel": "TSA_CFUN_kSteel_T_constantThickness_mass", 

73} 

74getdpTSAStiffnessThermalConductivityFunctions = { 

75 "Indium": "TSA_CFUN_kIn_constantThickness_stiffness", 

76 "Stainless Steel": "TSA_CFUN_kSteel_T_constantThickness_stiffness", 

77} 

78getdpTSAMassHeatCapacityFunctions = { 

79 "Indium": "TSA_CFUN_CvIn_constantThickness_mass", 

80 "Stainless Steel": "TSA_CFUN_CvSteel_T_constantThickness_mass", 

81} 

82getdpTSARHSFunctions = { 

83 "Indium": "TSA_CFUN_rhoIn_T_constantThickness_rhs", 

84 "Stainless Steel": None, 

85} 

86getdpTSATripleFunctions = { 

87 "Indium": "TSA_CFUN_rhoIn_T_constantThickness_triple", 

88 "Stainless Steel": None, 

89} 

90getdpCriticalCurrentDensityFunctions = { 

91 "HTSSuperPower": "CFUN_HTS_JcFit_SUPERPOWER_T_B_theta", 

92} 

93# ====================================================================================== 

94# ====================================================================================== 

95 

96# ====================================================================================== 

97# Available quantities: ================================================================ 

98PositionRequiredQuantityName = Literal[ 

99 "magneticField", 

100 "axialComponentOfTheMagneticField", 

101 "magnitudeOfMagneticField", 

102 "currentDensity", 

103 "magnitudeOfCurrentDensity", 

104 "resistiveHeating", 

105 "temperature", 

106 "criticalCurrentDensity", 

107 "heatFlux", 

108 "resistivity", 

109 "thermalConductivity", 

110 "specificHeatCapacity", 

111 "jHTSOverjCritical", 

112 "criticalCurrent", 

113 "debug", 

114] 

115PositionNotRequiredQuantityName = Literal[ 

116 "currentThroughCoil", 

117 "voltageBetweenTerminals", 

118 "inductance", 

119 "timeConstant", 

120 "totalResistiveHeating", 

121 "magneticEnergy", 

122] 

123# ====================================================================================== 

124# ====================================================================================== 

125 

126# ====================================================================================== 

127# Quantity information: ================================================================ 

128EMQuantities = [ 

129 "magneticField", 

130 "magnitudeOfMagneticField", 

131 "currentDensity", 

132 "magnitudeOfCurrentDensity", 

133 "resistiveHeating", 

134 "criticalCurrentDensity", 

135 "resistivity", 

136 "jHTSOverjCritical", 

137 "criticalCurrent", 

138 "debug", 

139 "inductance", 

140 "timeConstant", 

141 "currentThroughCoil", 

142 "voltageBetweenTerminals", 

143 "totalResistiveHeating", 

144 "magneticEnergy", 

145] 

146ThermalQuantities = [ 

147 "temperature", 

148 "heatFlux", 

149 "thermalConductivity", 

150 "specificHeatCapacity", 

151 "debug", 

152] 

153quantityProperNames = { 

154 "axialComponentOfTheMagneticField": "Axila Component of the Magnetic Field", 

155 "magneticField": "Magnetic Field", 

156 "magnitudeOfMagenticField": "Magnitude of Magnetic Field", 

157 "currentDensity": "Current Density", 

158 "magnitudeOfCurrentDensity": "Magnitude of Current Density", 

159 "resistiveHeating": "Resistive Heating", 

160 "temperature": "Temperature", 

161 "currentThroughCoil": "Current Through Coil", 

162 "voltageBetweenTerminals": "Voltage Between Terminals", 

163 "criticalCurrentDensity": "Critical Current Density", 

164 "heatFlux": "Heat Flux", 

165 "resistivity": "Resistivity", 

166 "thermalConductivity": "Thermal Conductivity", 

167 "specificHeatCapacity": "Specific Heat Capacity", 

168 "jHTSOverjCritical": "jHTS/jCritical", 

169 "criticalCurrent": "Critical Current", 

170 "debug": "Debug", 

171 "inductance": "Inductance", 

172 "timeConstant": "Time Constant", 

173} 

174 

175quantityUnits = { 

176 "axialComponentOfTheMagneticField": "T", 

177 "magneticField": "T", 

178 "magnitudeOfMagneticField": "T", 

179 "currentDensity": "$A/m^2$", 

180 "magnitudeOfCurrentDensity": "$A/m^2$", 

181 "resistiveHeating": "W", 

182 "temperature": "K", 

183 "currentThroughCoil": "A", 

184 "voltageBetweenTerminals": "V", 

185 "criticalCurrentDensity": "$A/m^2$", 

186 "heatFlux": "$W/m^2$", 

187 "resistivity": "Ohm*m", 

188 "thermalConductivity": "W/m*K", 

189 "specificHeatCapacity": "J/kg*K", 

190 "jHTSOverjCritical": "A/A", 

191 "criticalCurrent": "A", 

192 "debug": "1", 

193 "inductance": "H", 

194 "timeConstant": "s", 

195} 

196 

197getdpQuantityNames = { 

198 "axialComponentOfTheMagneticField": "RESULT_axialComponentOfTheMagneticField", 

199 "magneticField": "RESULT_magneticField", 

200 "magnitudeOfMagneticField": "RESULT_magnitudeOfMagneticField", 

201 "currentDensity": "RESULT_currentDensity", 

202 "magnitudeOfCurrentDensity": "RESULT_magnitudeOfCurrentDensity", 

203 "resistiveHeating": "RESULT_resistiveHeating", 

204 "temperature": "RESULT_temperature", 

205 "currentThroughCoil": "RESULT_currentThroughCoil", 

206 "voltageBetweenTerminals": "RESULT_voltageBetweenTerminals", 

207 "criticalCurrentDensity": "RESULT_criticalCurrentDensity", 

208 "heatFlux": "RESULT_heatFlux", 

209 "resistivity": "RESULT_resistivity", 

210 "thermalConductivity": "RESULT_thermalConductivity", 

211 "specificHeatCapacity": "RESULT_specificHeatCapacity", 

212 "jHTSOverjCritical": "RESULT_jHTSOverjCritical", 

213 "criticalCurrent": "RESULT_criticalCurrent", 

214 "debug": "RESULT_debug", 

215 "inductance": "RESULT_inductance", 

216 "timeConstant": "RESULT_timeConstant", 

217} 

218 

219getdpPostOperationNames = { 

220 "axialComponentOfTheMagneticField": "POSTOP_axialComponentOfTheMagneticField", 

221 "magneticField": "POSTOP_magneticField", 

222 "magnitudeOfMagneticField": "POSTOP_magnitudeOfMagneticField", 

223 "currentDensity": "POSTOP_currentDensity", 

224 "magnitudeOfCurrentDensity": "POSTOP_magnitudeOfCurrentDensity", 

225 "resistiveHeating": "POSTOP_resistiveHeating", 

226 "temperature": "POSTOP_temperature", 

227 "currentThroughCoil": "POSTOP_currentThroughCoil", 

228 "voltageBetweenTerminals": "POSTOP_voltageBetweenTerminals", 

229 "criticalCurrentDensity": "POSTOP_criticalCurrentDensity", 

230 "heatFlux": "POSTOP_heatFlux", 

231 "resistivity": "POSTOP_resistivity", 

232 "thermalConductivity": "POSTOP_thermalConductivity", 

233 "specificHeatCapacity": "POSTOP_specificHeatCapacity", 

234 "jHTSOverjCritical": "POSTOP_jHTSOverjCritical", 

235 "criticalCurrent": "POSTOP_criticalCurrent", 

236 "debug": "POSTOP_debug", 

237 "inductance": "POSTOP_inductance", 

238 "timeConstant": "POSTOP_timeConstant", 

239} 

240 

241# ====================================================================================== 

242# ====================================================================================== 

243 

244# Global variables 

245geometry_input = ContextVar("geometry") 

246mesh_input = ContextVar("mesh") 

247solve_input = ContextVar("solve") 

248input_file_path = ContextVar("input_file_path") 

249all_break_points = [] 

250 

251 

252def getWindingOuterRadius(): 

253 """Return outer radius of the winding.""" 

254 geometry = geometry_input.get() 

255 return ( 

256 geometry["winding"]["innerRadius"] 

257 + geometry["winding"]["thickness"] 

258 + geometry["winding"]["numberOfTurns"] 

259 * (geometry["winding"]["thickness"] + geometry["contactLayer"]["thickness"]) 

260 ) 

261 

262 

263def getAirHeight(): 

264 """Return the height of the air.""" 

265 geometry = geometry_input.get() 

266 h = ( 

267 geometry["numberOfPancakes"] 

268 * (geometry["winding"]["height"] + geometry["gapBetweenPancakes"]) 

269 - geometry["gapBetweenPancakes"] 

270 + 2 * geometry["air"]["axialMargin"] 

271 ) 

272 return h 

273 

274 

275def getTransitionNotchAngle(): 

276 """Return transition notch angle of the winding.""" 

277 mesh = mesh_input.get() 

278 

279 azimuthalNumberOfElementsPerTurn = max( 

280 mesh["winding"]["azimuthalNumberOfElementsPerTurn"] 

281 ) 

282 

283 transitionNotchAngle = 2 * math.pi / azimuthalNumberOfElementsPerTurn 

284 

285 return transitionNotchAngle 

286 

287 

288def checkIfAirOrTerminalMeshIsStructured(): 

289 geometry = geometry_input.get() 

290 mesh = mesh_input.get() 

291 

292 structuredAirMesh = False 

293 structuredTerminalMesh = False 

294 if "air" in mesh: 

295 structuredAirMesh = mesh["air"]["structured"] 

296 if "terminals" in mesh: 

297 structuredTerminalMesh = mesh["terminals"]["structured"] 

298 structuredMesh = structuredAirMesh or structuredTerminalMesh 

299 

300 return structuredMesh 

301 

302 

303# ====================================================================================== 

304# FUNDAMENTAL CLASSES STARTS =========================================================== 

305# ====================================================================================== 

306class Pancake3DPositionInCoordinates(BaseModel): 

307 x: float = Field( 

308 title="x coordinate", 

309 description="x coordinate of the position.", 

310 ) 

311 y: float = Field( 

312 title="y coordinate", 

313 description="y coordinate of the position.", 

314 ) 

315 z: float = Field( 

316 title="z coordinate", 

317 description="z coordinate of the position.", 

318 ) 

319 

320 

321class Pancake3DPositionInTurnNumbers(BaseModel): 

322 turnNumber: float = Field( 

323 title="Turn Number", 

324 description=( 

325 "Winding turn number as a position input. It starts from 0 and it can be a" 

326 " float." 

327 ), 

328 ) 

329 whichPancakeCoil: Optional[PositiveInt] = Field( 

330 default=None, 

331 title="Pancake Coil Number", 

332 description="The first pancake coil is 1, the second is 2, etc.", 

333 ) 

334 

335 @field_validator("turnNumber") 

336 @classmethod 

337 def check_turnNumber(cls, turnNumber): 

338 geometry = geometry_input.get() 

339 

340 if turnNumber < 0: 

341 raise ValueError("Turn number cannot be less than 0.") 

342 elif turnNumber > geometry["winding"]["numberOfTurns"]: 

343 raise ValueError( 

344 "Turn number cannot be greater than the number of turns of the winding" 

345 f" ({geometry['numberOfPancakes']})." 

346 ) 

347 

348 return turnNumber 

349 

350 @field_validator("whichPancakeCoil") 

351 @classmethod 

352 def check_whichPancakeCoil(cls, whichPancakeCoil): 

353 geometry = geometry_input.get() 

354 

355 if whichPancakeCoil is not None: 

356 if whichPancakeCoil < 1: 

357 raise ValueError( 

358 "Pancake coil numbers start from 1. Therefore, it cannot be less" 

359 " than 1." 

360 ) 

361 elif whichPancakeCoil > geometry["numberOfPancakes"]: 

362 raise ValueError( 

363 "Pancake coil number cannot be greater than the number of pancakes" 

364 f" ({geometry['numberOfPancakes']})." 

365 ) 

366 else: 

367 return 1 

368 

369 return whichPancakeCoil 

370 

371 def compute_coordinates(self): 

372 geometry = geometry_input.get() 

373 mesh = mesh_input.get() 

374 

375 if geometry["contactLayer"]["thinShellApproximation"]: 

376 windingThickness = ( 

377 geometry["winding"]["thickness"] 

378 + geometry["contactLayer"]["thickness"] 

379 * (geometry["winding"]["numberOfTurns"] - 1) 

380 / geometry["winding"]["numberOfTurns"] 

381 ) 

382 gapThickness = 0 

383 else: 

384 windingThickness = geometry["winding"]["thickness"] 

385 gapThickness = geometry["contactLayer"]["thickness"] 

386 

387 innerRadius = geometry["winding"]["innerRadius"] 

388 initialTheta = 0.0 

389 if isinstance(mesh["winding"]["azimuthalNumberOfElementsPerTurn"], list): 

390 ane = mesh["winding"]["azimuthalNumberOfElementsPerTurn"][0] 

391 elif isinstance(mesh["winding"]["azimuthalNumberOfElementsPerTurn"], int): 

392 ane = mesh["winding"]["azimuthalNumberOfElementsPerTurn"] 

393 else: 

394 raise ValueError( 

395 "The azimuthal number of elements per turn must be either an integer" 

396 " or a list of integers." 

397 ) 

398 

399 numberOfPancakes = geometry["numberOfPancakes"] 

400 gapBetweenPancakes = geometry["gapBetweenPancakes"] 

401 windingHeight = geometry["winding"]["height"] 

402 

403 turnNumber = self.turnNumber 

404 whichPancake = self.whichPancakeCoil 

405 

406 elementStartTurnNumber = math.floor(turnNumber / (1 / ane)) * (1 / ane) 

407 elementEndTurnNumber = elementStartTurnNumber + 1 / ane 

408 

409 class point: 

410 def __init__(self, x, y, z): 

411 self.x = x 

412 self.y = y 

413 self.z = z 

414 

415 def __add__(self, other): 

416 return point(self.x + other.x, self.y + other.y, self.z + other.z) 

417 

418 def __sub__(self, other): 

419 return point(self.x - other.x, self.y - other.y, self.z - other.z) 

420 

421 def __mul__(self, scalar): 

422 return point(self.x * scalar, self.y * scalar, self.z * scalar) 

423 

424 def __truediv__(self, scalar): 

425 return point(self.x / scalar, self.y / scalar, self.z / scalar) 

426 

427 def rotate(self, degrees): 

428 return point( 

429 self.x * math.cos(degrees) - self.y * math.sin(degrees), 

430 self.x * math.sin(degrees) + self.y * math.cos(degrees), 

431 self.z, 

432 ) 

433 

434 def normalize(self): 

435 return self / math.sqrt(self.x**2 + self.y**2 + self.z**2) 

436 

437 if whichPancake % 2 == 1: 

438 # If the spiral is counter-clockwise, the initial theta angle decreases, 

439 # and r increases as the theta angle decreases. 

440 multiplier = 1 

441 elif whichPancake % 2 == 0: 

442 # If the spiral is clockwise, the initial theta angle increases, and r 

443 # increases as the theta angle increases. 

444 multiplier = -1 

445 

446 # Mesh element's starting point: 

447 elementStartTheta = 2 * math.pi * elementStartTurnNumber * multiplier 

448 elementStartRadius = ( 

449 innerRadius 

450 + elementStartTheta 

451 / (2 * math.pi) 

452 * (gapThickness + windingThickness) 

453 * multiplier 

454 ) 

455 elementStartPointX = elementStartRadius * math.cos( 

456 initialTheta + elementStartTheta 

457 ) 

458 elementStartPointY = elementStartRadius * math.sin( 

459 initialTheta + elementStartTheta 

460 ) 

461 elementStartPointZ = ( 

462 -( 

463 numberOfPancakes * windingHeight 

464 + (numberOfPancakes - 1) * gapBetweenPancakes 

465 ) 

466 / 2 

467 + windingHeight / 2 

468 + (whichPancake - 1) * (windingHeight + gapBetweenPancakes) 

469 ) 

470 elementStartPoint = point( 

471 elementStartPointX, elementStartPointY, elementStartPointZ 

472 ) 

473 

474 # Mesh element's ending point: 

475 elementEndTheta = 2 * math.pi * elementEndTurnNumber * multiplier 

476 elementEndRadius = ( 

477 innerRadius 

478 + elementEndTheta 

479 / (2 * math.pi) 

480 * (gapThickness + windingThickness) 

481 * multiplier 

482 ) 

483 elementEndPointX = elementEndRadius * math.cos(initialTheta + elementEndTheta) 

484 elementEndPointY = elementEndRadius * math.sin(initialTheta + elementEndTheta) 

485 elementEndPointZ = elementStartPointZ 

486 elementEndPoint = point(elementEndPointX, elementEndPointY, elementEndPointZ) 

487 

488 turnNumberFraction = (turnNumber - elementStartTurnNumber) / ( 

489 elementEndTurnNumber - elementStartTurnNumber 

490 ) 

491 location = ( 

492 elementStartPoint 

493 + (elementEndPoint - elementStartPoint) * turnNumberFraction 

494 ) + (elementEndPoint - elementStartPoint).rotate( 

495 -math.pi / 2 

496 ).normalize() * windingThickness / 2 * multiplier 

497 

498 return location.x, location.y, location.z 

499 

500 @computed_field 

501 @cached_property 

502 def x(self) -> float: 

503 return self.compute_coordinates()[0] 

504 

505 @computed_field 

506 @cached_property 

507 def y(self) -> float: 

508 return self.compute_coordinates()[1] 

509 

510 @computed_field 

511 @cached_property 

512 def z(self) -> float: 

513 return self.compute_coordinates()[2] 

514 

515 

516Pancake3DPosition = Pancake3DPositionInCoordinates | Pancake3DPositionInTurnNumbers 

517 

518# ====================================================================================== 

519# FUNDAMENTAL CLASSES ENDS ============================================================= 

520# ====================================================================================== 

521 

522 

523# ====================================================================================== 

524# GEOMETRY CLASSES STARTS ============================================================== 

525# ====================================================================================== 

526class Pancake3DGeometryWinding(BaseModel): 

527 # Mandatory: 

528 r_i: PositiveFloat = Field( 

529 alias="innerRadius", 

530 title="Inner Radius", 

531 description="Inner radius of the winding.", 

532 ) 

533 t: PositiveFloat = Field( 

534 alias="thickness", 

535 title="Winding Thickness", 

536 description="Thickness of the winding.", 

537 ) 

538 N: float = Field( 

539 alias="numberOfTurns", 

540 ge=3, 

541 title="Number of Turns", 

542 description="Number of turns of the winding.", 

543 ) 

544 h: PositiveFloat = Field( 

545 alias="height", 

546 title="Winding Height", 

547 description="Height/width of the winding.", 

548 ) 

549 

550 # Optionals: 

551 name: str = Field( 

552 default="winding", 

553 title="Winding Name", 

554 description="The The name to be used in the mesh..", 

555 examples=["winding", "myWinding"], 

556 ) 

557 NofVolPerTurn: int = Field( 

558 default=2, 

559 validate_default=True, 

560 alias="numberOfVolumesPerTurn", 

561 ge=2, 

562 title="Number of Volumes Per Turn (Advanced Input)", 

563 description="The number of volumes per turn (CAD related, not physical).", 

564 ) 

565 

566 @field_validator("NofVolPerTurn") 

567 @classmethod 

568 def check_NofVolPerTurn(cls, NofVolPerTurn): 

569 geometry = geometry_input.get() 

570 mesh = mesh_input.get() 

571 

572 # Check if the NofVolPerTurn is compatible swith the azimuthal number of 

573 # elements per turn: 

574 for i, ane in enumerate(mesh["winding"]["azimuthalNumberOfElementsPerTurn"]): 

575 if ane % NofVolPerTurn != 0: 

576 raise ValueError( 

577 "The azimuthal number of elements per turn for the pancake coil" 

578 f" number {i+1} is ({ane}), but it must be divisible by the number" 

579 f" of volumes per turn ({geometry['winding']['NofVolPerTurn']})!" 

580 " So it needs to be rounded to" 

581 f" {math.ceil(ane/NofVolPerTurn)*NofVolPerTurn:.5f} or" 

582 f" {math.floor(ane/NofVolPerTurn)*NofVolPerTurn:.5f}." 

583 ) 

584 

585 structured = checkIfAirOrTerminalMeshIsStructured() 

586 

587 if structured: 

588 # If the mesh is structured, the number of volumes per turn must be 4: 

589 NofVolPerTurn = 4 

590 

591 return NofVolPerTurn 

592 

593 @computed_field 

594 @cached_property 

595 def theta_i(self) -> float: 

596 """Return start angle of the winding.""" 

597 return 0.0 

598 

599 @computed_field 

600 @cached_property 

601 def r_o(self) -> float: 

602 """Return outer radius of the winding.""" 

603 return getWindingOuterRadius() 

604 

605 @computed_field 

606 @cached_property 

607 def turnTol(self) -> float: 

608 """Return turn tolerance of the winding.""" 

609 geometry: Pancake3DGeometry = geometry_input.get() 

610 mesh: Pancake3DMesh = mesh_input.get() 

611 

612 # Calculate the turn tolerance required due to the geometrymetry input: 

613 # Turn tolerance is the smallest turn angle (in turns) that is allowed. 

614 if "dimTol" in geometry: 

615 dimTol = geometry["dimTol"] 

616 else: 

617 dimTol = 1e-8 

618 turnTol = geometry["winding"]["numberOfTurns"] % 1 

619 if math.isclose(turnTol, 0, abs_tol=dimTol): 

620 turnTol = 0.5 

621 

622 turnTolDueToTransition = getTransitionNotchAngle() / (2 * math.pi) 

623 

624 # Calculate the minimum turn tolerance possible due to the mesh input: 

625 minimumTurnTol = 1 / min(mesh["winding"]["azimuthalNumberOfElementsPerTurn"]) 

626 

627 if turnTol < minimumTurnTol: 

628 numberOfTurns = geometry["winding"]["numberOfTurns"] 

629 

630 raise ValueError( 

631 "The azimuthal number of elements per turn for one of the pancakes is" 

632 f" {min(mesh['winding']['azimuthalNumberOfElementsPerTurn'])}, and the" 

633 " number of turns is" 

634 f" {numberOfTurns:.5f}." 

635 " The number of turns must always be divisible by the (1/(the" 

636 " azimuthal number of elements per turn)) to ensure conformality." 

637 " Please change the number of turns or the azimuthal number of" 

638 " elemenets per turn. The closest possible number of turns value is" 

639 f" {round(numberOfTurns * min(mesh['winding']['azimuthalNumberOfElementsPerTurn']))/min(mesh['winding']['azimuthalNumberOfElementsPerTurn']):.5f}" 

640 ) 

641 else: 

642 # Minimum possible sections per turn is 16 (otherwise splines might collide 

643 # into each other). But it should be greater than the number of volumes per 

644 # turn and it should be divisible by both 1/turnTol and the number of 

645 # volumes per turn. 

646 sectionsPerTurn = 16 

647 if "numberOfVolumesPerTurn" in geometry["winding"]: 

648 numberOfVolumesPerTurn = geometry["winding"]["numberOfVolumesPerTurn"] 

649 else: 

650 numberOfVolumesPerTurn = 2 

651 

652 while ( 

653 ( 

654 math.fmod(sectionsPerTurn, (1 / turnTol)) > 1e-8 

655 and math.fmod(sectionsPerTurn, (1 / turnTol)) - (1 / turnTol) 

656 < -1e-8 

657 ) 

658 or ( 

659 math.fmod(sectionsPerTurn, (1 / turnTolDueToTransition)) > 1e-8 

660 and math.fmod(sectionsPerTurn, (1 / turnTolDueToTransition)) 

661 - (1 / turnTolDueToTransition) 

662 < -1e-8 

663 ) 

664 or sectionsPerTurn % numberOfVolumesPerTurn != 0 

665 or sectionsPerTurn < numberOfVolumesPerTurn 

666 ): 

667 sectionsPerTurn += 1 

668 

669 # Sections per turn will set the turn tolerance value as well. 

670 return 1.0 / sectionsPerTurn 

671 

672 @computed_field 

673 @cached_property 

674 def spt(self) -> float: 

675 """Return sections per turn of the winding.""" 

676 return int(1.0 / self.turnTol) 

677 

678 @computed_field 

679 @cached_property 

680 def totalTapeLength(self) -> float: 

681 """Return total tape length of the winding.""" 

682 geometry: Pancake3DGeometry = geometry_input.get() 

683 

684 # Calculate the total tape length of the coil: 

685 

686 # The same angle can be subtracted from both theta_1 and theta_2 to simplify the 

687 # calculations: 

688 theta2 = geometry["winding"]["numberOfTurns"] * 2 * math.pi 

689 theta1 = 0 

690 

691 # Since r = a * theta + b, r_1 = b since theta_1 = 0: 

692 b = geometry["winding"]["innerRadius"] 

693 

694 # Since r = a * theta + b, r_2 = a * theta2 + b: 

695 a = (getWindingOuterRadius() - b) / theta2 

696 

697 def integrand(t): 

698 return math.sqrt(a**2 + (a * t + b) ** 2) 

699 

700 totalTapeLength = abs(scipy.integrate.quad(integrand, theta1, theta2)[0]) 

701 

702 return totalTapeLength 

703 

704 

705class Pancake3DGeometryContactLayer(BaseModel): 

706 # Mandatory: 

707 tsa: bool = Field( 

708 alias="thinShellApproximation", 

709 title="Use Thin Shell Approximation", 

710 description=( 

711 "If True, the contact layer will be modeled with 2D shell elements (thin" 

712 " shell approximation), and if False, the contact layer will be modeled" 

713 " with 3D elements." 

714 ), 

715 ) 

716 t: PositiveFloat = Field( 

717 alias="thickness", 

718 title="Contact Layer Thickness", 

719 description="Thickness of the contact layer.", 

720 ) 

721 

722 # Optionals: 

723 name: str = Field( 

724 default="contactLayer", 

725 title="Contact Layer Name", 

726 description="The name to be used in the mesh.", 

727 examples=["myContactLayer"], 

728 ) 

729 

730 

731class Pancake3DGeometryTerminalBase(BaseModel): 

732 # Mandatory: 

733 t: PositiveFloat = Field( 

734 alias="thickness", 

735 title="Terminal Thickness", 

736 description="Thickness of the terminal's tube.", 

737 ) # thickness 

738 

739 @field_validator("t") 

740 @classmethod 

741 def check_t(cls, t): 

742 geometry = geometry_input.get() 

743 

744 if t < geometry["winding"]["thickness"] / 2: 

745 raise ValueError( 

746 "Terminal's thickness is smaller than half of the winding's thickness!" 

747 " Please increase the terminal's thickness." 

748 ) 

749 

750 return t 

751 

752 

753class Pancake3DGeometryInnerTerminal(Pancake3DGeometryTerminalBase): 

754 name: str = Field( 

755 default="innerTerminal", 

756 title="Terminal Name", 

757 description="The name to be used in the mesh.", 

758 examples=["innerTerminal", "outerTeminal"], 

759 ) 

760 

761 @computed_field 

762 @cached_property 

763 def r(self) -> float: 

764 """Return inner radius of the inner terminal.""" 

765 geometry = geometry_input.get() 

766 

767 innerRadius = geometry["winding"]["innerRadius"] - 2 * self.t 

768 if innerRadius < 0: 

769 raise ValueError( 

770 "Inner terminal's radius is smaller than 0! Please decrease the inner" 

771 " terminal's thickness or increase the winding's inner radius." 

772 ) 

773 

774 return innerRadius 

775 

776 

777class Pancake3DGeometryOuterTerminal(Pancake3DGeometryTerminalBase): 

778 name: str = Field( 

779 default="outerTerminal", 

780 title="Terminal Name", 

781 description="The name to be used in the mesh.", 

782 examples=["innerTerminal", "outerTeminal"], 

783 ) 

784 

785 @computed_field 

786 @cached_property 

787 def r(self) -> float: 

788 """Return outer radius of the outer terminal.""" 

789 outerRadius = getWindingOuterRadius() + 2 * self.t 

790 

791 return outerRadius 

792 

793 

794class Pancake3DGeometryTerminals(BaseModel): 

795 # 1) User inputs: 

796 i: Pancake3DGeometryInnerTerminal = Field(alias="inner") 

797 o: Pancake3DGeometryOuterTerminal = Field(alias="outer") 

798 

799 # Optionals: 

800 firstName: str = Field( 

801 default="firstTerminal", description="name of the first terminal" 

802 ) 

803 lastName: str = Field( 

804 default="lastTerminal", description="name of the last terminal" 

805 ) 

806 

807 @computed_field 

808 @cached_property 

809 def transitionNotchAngle(self) -> float: 

810 """Return transition notch angle of the terminals.""" 

811 return getTransitionNotchAngle() 

812 

813 

814class Pancake3DGeometryAirBase(BaseModel): 

815 # Mandatory: 

816 margin: PositiveFloat = Field( 

817 alias="axialMargin", 

818 title="Axial Margin of the Air", 

819 description=( 

820 "Axial margin between the ends of the air and first/last pancake coils." 

821 ), 

822 ) # axial margin 

823 

824 # Optionals: 

825 name: str = Field( 

826 default="air", 

827 title="Air Name", 

828 description="The name to be used in the mesh.", 

829 examples=["air", "myAir"], 

830 ) 

831 shellTransformation: bool = Field( 

832 default=False, 

833 alias="shellTransformation", 

834 title="Use Shell Transformation", 

835 description=( 

836 "Generate outer shell air to apply shell transformation if True (GetDP" 

837 " related, not physical)" 

838 ), 

839 ) 

840 shellTransformationMultiplier: float = Field( 

841 default=1.2, 

842 gt=1.1, 

843 alias="shellTransformationMultiplier", 

844 title="Shell Transformation Multiplier (Advanced Input)", 

845 description=( 

846 "multiply the air's outer dimension by this value to get the shell's outer" 

847 " dimension" 

848 ), 

849 ) 

850 cutName: str = Field( 

851 default="Air-Cut", 

852 title="Air Cut Name", 

853 description="name of the cut (cochain) to be used in the mesh", 

854 examples=["Air-Cut", "myAirCut"], 

855 ) 

856 shellVolumeName: str = Field( 

857 default="air-Shell", 

858 title="Air Shell Volume Name", 

859 description="name of the shell volume to be used in the mesh", 

860 examples=["air-Shell", "myAirShell"], 

861 ) 

862 fragment: bool = Field( 

863 default=False, 

864 alias="generateGapAirWithFragment", 

865 title="Generate Gap Air with Fragment (Advanced Input)", 

866 description=( 

867 "generate the gap air with gmsh/model/occ/fragment if true (CAD related," 

868 " not physical)" 

869 ), 

870 ) 

871 

872 @field_validator("margin") 

873 @classmethod 

874 def check_margin(cls, margin): 

875 geometry = geometry_input.get() 

876 windingHeight = geometry["winding"]["height"] 

877 

878 if margin < windingHeight / 2: 

879 raise ValueError( 

880 "Axial margin is smaller than half of the winding's height! Please" 

881 " increase the axial margin." 

882 ) 

883 

884 return margin 

885 

886 @computed_field 

887 @cached_property 

888 def h(self) -> float: 

889 """Return total height of the air.""" 

890 h = getAirHeight() 

891 

892 return h 

893 

894 

895class Pancake3DGeometryAirCylinder(Pancake3DGeometryAirBase): 

896 type: Literal["cylinder"] = Field(default="cylinder", title="Air Type") 

897 r: PositiveFloat = Field( 

898 default=None, 

899 alias="radius", 

900 title="Air Radius", 

901 description="Radius of the air (for cylinder type air).", 

902 ) 

903 

904 @field_validator("r") 

905 @classmethod 

906 def check_r(cls, r): 

907 geometry = geometry_input.get() 

908 outerTerminalOuterRadius = ( 

909 getWindingOuterRadius() + 2 * geometry["terminals"]["outer"]["thickness"] 

910 ) 

911 

912 if r < outerTerminalOuterRadius * 1.5: 

913 raise ValueError( 

914 "Radius of the air must be at least 1.5 times the outer radius of the" 

915 " winding! Please increase the radius of the air." 

916 ) 

917 

918 return r 

919 

920 @computed_field 

921 @cached_property 

922 def shellOuterRadius(self) -> float: 

923 """Return outer radius of the air.""" 

924 shellOuterRadius = self.shellTransformationMultiplier * self.r 

925 

926 return shellOuterRadius 

927 

928 

929class Pancake3DGeometryAirCuboid(Pancake3DGeometryAirBase): 

930 type: Literal["cuboid"] = Field(default="cuboid", title="Air Type") 

931 a: PositiveFloat = Field( 

932 default=None, 

933 alias="sideLength", 

934 title="Air Side Length", 

935 description="Side length of the air (for cuboid type air).", 

936 ) 

937 

938 @field_validator("a") 

939 @classmethod 

940 def check_a(cls, a): 

941 geometry = geometry_input.get() 

942 outerTerminalOuterRadius = ( 

943 getWindingOuterRadius() + 2 * geometry["terminals"]["outer"]["thickness"] 

944 ) 

945 

946 if a / 2 < outerTerminalOuterRadius * 1.5: 

947 raise ValueError( 

948 "Half of the side length of the air must be at least 1.5 times the" 

949 " outer radius of the winding! Please increase the side length of the" 

950 " air." 

951 ) 

952 

953 return a 

954 

955 @computed_field 

956 @cached_property 

957 def shellSideLength(self) -> float: 

958 """Return outer radius of the air.""" 

959 shellSideLength = self.shellTransformationMultiplier * self.a 

960 

961 return shellSideLength 

962 

963 

964Pancake3DGeometryAir = Annotated[ 

965 Pancake3DGeometryAirCylinder | Pancake3DGeometryAirCuboid, 

966 Field(discriminator="type"), 

967] 

968# ====================================================================================== 

969# GEOMETRY CLASSES ENDS ================================================================ 

970# ====================================================================================== 

971 

972 

973# ====================================================================================== 

974# MESH CLASSES STARTS ================================================================== 

975# ====================================================================================== 

976class Pancake3DMeshWinding(BaseModel): 

977 # Mandatory: 

978 axne: list[PositiveInt] | PositiveInt = Field( 

979 alias="axialNumberOfElements", 

980 title="Axial Number of Elements", 

981 description=( 

982 "The number of axial elements for the whole height of the coil. It can be" 

983 " either a list of integers to specify the value for each pancake coil" 

984 " separately or an integer to use the same setting for each pancake coil." 

985 ), 

986 ) 

987 

988 ane: list[PositiveInt] | PositiveInt = Field( 

989 alias="azimuthalNumberOfElementsPerTurn", 

990 title="Azimuthal Number of Elements Per Turn", 

991 description=( 

992 "The number of azimuthal elements per turn of the coil. It can be either a" 

993 " list of integers to specify the value for each pancake coil separately or" 

994 " an integer to use the same setting for each pancake coil." 

995 ), 

996 ) 

997 

998 rne: list[PositiveInt] | PositiveInt = Field( 

999 alias="radialNumberOfElementsPerTurn", 

1000 title="Winding Radial Number of Elements Per Turn", 

1001 description=( 

1002 "The number of radial elements per tape of the winding. It can be either a" 

1003 " list of integers to specify the value for each pancake coil separately or" 

1004 " an integer to use the same setting for each pancake coil." 

1005 ), 

1006 ) 

1007 

1008 # Optionals: 

1009 axbc: list[PositiveFloat] | PositiveFloat = Field( 

1010 default=[1], 

1011 alias="axialDistributionCoefficient", 

1012 title="Axial Bump Coefficients", 

1013 description=( 

1014 "If 1, it won't affect anything. If smaller than 1, elements will get finer" 

1015 " in the axial direction at the ends of the coil. If greater than 1," 

1016 " elements will get coarser in the axial direction at the ends of the coil." 

1017 " It can be either a list of floats to specify the value for each pancake" 

1018 " coil separately or a float to use the same setting for each pancake coil." 

1019 ), 

1020 ) 

1021 

1022 elementType: ( 

1023 list[Literal["tetrahedron", "hexahedron", "prism"]] 

1024 | Literal["tetrahedron", "hexahedron", "prism"] 

1025 ) = Field( 

1026 default=["tetrahedron"], 

1027 title="Element Type", 

1028 description=( 

1029 "The element type of windings and contact layers. It can be either a" 

1030 " tetrahedron, hexahedron, or a prism. It can be either a list of strings" 

1031 " to specify the value for each pancake coil separately or a string to use" 

1032 " the same setting for each pancake coil." 

1033 ), 

1034 ) 

1035 

1036 @field_validator("axne", "ane", "rne", "axbc", "elementType") 

1037 @classmethod 

1038 def check_inputs(cls, value, info: ValidationInfo): 

1039 geometry = geometry_input.get() 

1040 

1041 numberOfPancakes = geometry["numberOfPancakes"] 

1042 

1043 structuredMesh = checkIfAirOrTerminalMeshIsStructured() 

1044 

1045 if not isinstance(value, list): 

1046 value = [value] * numberOfPancakes 

1047 elif len(value) == 1: 

1048 value = value * numberOfPancakes 

1049 else: 

1050 if len(value) != numberOfPancakes: 

1051 raise ValueError( 

1052 "The length of the input list must be equal to the number of" 

1053 " pancake coils!" 

1054 ) 

1055 if info.field_name == "ane": 

1056 if value[0] < 7: 

1057 raise ValueError( 

1058 "Azimuthal number of elements per turn must be greater than or" 

1059 " equal to 7!" 

1060 ) 

1061 

1062 if structuredMesh: 

1063 if len(set(value)) != 1: 

1064 raise ValueError( 

1065 "If structured mesh is used, the same mesh setting must be used for" 

1066 " all pancake coils!" 

1067 ) 

1068 

1069 if info.field_name == "elementType": 

1070 if value[0] != "tetrahedron": 

1071 raise ValueError( 

1072 "If structured air or terminal mesh is used, the element type" 

1073 " must be tetrahedron!" 

1074 ) 

1075 

1076 if info.field_name == "ane": 

1077 if value[0] % 4 != 0: 

1078 raise ValueError( 

1079 "If structured mesh is used, the number of azimuthal elements" 

1080 " per turn must be divisible by 4!" 

1081 ) 

1082 

1083 return value 

1084 

1085 

1086class Pancake3DMeshContactLayer(BaseModel): 

1087 # Mandatory: 

1088 rne: list[PositiveInt] = Field( 

1089 alias="radialNumberOfElementsPerTurn", 

1090 title="Contact Layer Radial Number of Elements Per Turn", 

1091 description=( 

1092 "The number of radial elements per tape of the contact layer. It can be" 

1093 " either a list of integers to specify the value for each pancake coil" 

1094 " separately or an integer to use the same setting for each pancake coil." 

1095 ), 

1096 ) 

1097 

1098 @field_validator("rne") 

1099 @classmethod 

1100 def check_inputs(cls, value): 

1101 geometry = geometry_input.get() 

1102 

1103 structuredMesh = checkIfAirOrTerminalMeshIsStructured() 

1104 

1105 numberOfPancakeCoils = geometry["numberOfPancakes"] 

1106 

1107 if not isinstance(value, list): 

1108 value = [value] * numberOfPancakeCoils 

1109 elif len(value) == 1: 

1110 value = value * numberOfPancakeCoils 

1111 else: 

1112 if len(value) != numberOfPancakeCoils: 

1113 raise ValueError( 

1114 "The length of the input list must be equal to the number of" 

1115 " pancake coils!" 

1116 ) 

1117 

1118 if structuredMesh: 

1119 if len(set(value)) != 1: 

1120 raise ValueError( 

1121 "If structured mesh is used, the same mesh setting must be used for" 

1122 " all pancake coils!" 

1123 ) 

1124 

1125 return value 

1126 

1127 

1128class Pancake3DMeshAirAndTerminals(BaseModel): 

1129 # Optionals: 

1130 structured: bool = Field( 

1131 default=False, 

1132 title="Structure Mesh", 

1133 description=( 

1134 "If True, the mesh will be structured. If False, the mesh will be" 

1135 " unstructured." 

1136 ), 

1137 ) 

1138 radialElementSize: PositiveFloat = Field( 

1139 default=1, 

1140 title="Radial Element Size", 

1141 description=( 

1142 "If structured mesh is used, the radial element size can be set. It is the" 

1143 " radial element size in terms of the winding's radial element size." 

1144 ), 

1145 ) 

1146 

1147 

1148# ====================================================================================== 

1149# MESH CLASSES ENDS ==================================================================== 

1150# ====================================================================================== 

1151 

1152 

1153# ====================================================================================== 

1154# SOLVE CLASSES STARTS ================================================================= 

1155# ====================================================================================== 

1156class Pancake3DSolveAir(BaseModel): 

1157 # 1) User inputs: 

1158 

1159 # Mandatory: 

1160 permeability: PositiveFloat = Field( 

1161 title="Permeability of Air", 

1162 description="Permeability of air.", 

1163 ) 

1164 

1165 

1166class Pancake3DSolveIcVsLength(BaseModel): 

1167 lengthValues: list[float] = Field( 

1168 title="Tape Length Values", 

1169 description="Tape length values that corresponds to criticalCurrentValues.", 

1170 ) 

1171 criticalCurrentValues: list[float] = Field( 

1172 title="Critical Current Values", 

1173 description="Critical current values that corresponds to lengthValues.", 

1174 ) 

1175 

1176 

1177class Pancake3DSolveMaterialBase(BaseModel): 

1178 name: str 

1179 

1180 # Optionals: 

1181 rrr: PositiveFloat = Field( 

1182 default=100, 

1183 alias="residualResistanceRatio", 

1184 title="Residual Resistance Ratio", 

1185 description=( 

1186 "Residual-resistivity ratio (also known as Residual-resistance ratio or" 

1187 " just RRR) is the ratio of the resistivity of a material at reference" 

1188 " temperature and at 0 K." 

1189 ), 

1190 ) 

1191 rrrRefT: PositiveFloat = Field( 

1192 default=295, 

1193 alias="residualResistanceRatioReferenceTemperature", 

1194 title="Residual Resistance Ratio Reference Temperature", 

1195 description="Reference temperature for residual resistance ratio", 

1196 ) 

1197 

1198 @computed_field 

1199 @cached_property 

1200 def resistivityMacroName(self) -> str: 

1201 """Return the resistivity macro name of the material.""" 

1202 if self.name not in resistivityMacroNames: 

1203 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1204 

1205 return resistivityMacroNames[self.name] 

1206 

1207 @computed_field 

1208 @cached_property 

1209 def thermalConductivityMacroName(self) -> str: 

1210 """Return the thermal conductivity macro name of the material.""" 

1211 if self.name not in thermalConductivityMacroNames: 

1212 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1213 

1214 return thermalConductivityMacroNames[self.name] 

1215 

1216 @computed_field 

1217 @cached_property 

1218 def heatCapacityMacroName(self) -> str: 

1219 """Return the heat capacity macro name of the material.""" 

1220 if self.name not in heatCapacityMacroNames: 

1221 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1222 

1223 return heatCapacityMacroNames[self.name] 

1224 

1225 @computed_field 

1226 @cached_property 

1227 def getdpTSAOnlyResistivityFunction(self) -> str: 

1228 """Return the GetDP function name of the material's resistivity.""" 

1229 if self.name not in getdpTSAOnlyResistivityFunctions: 

1230 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1231 

1232 return getdpTSAOnlyResistivityFunctions[self.name] 

1233 

1234 @computed_field 

1235 @cached_property 

1236 def getdpTSAMassResistivityFunction(self) -> str: 

1237 """Return the GetDP function name of the material's mass resistivity.""" 

1238 if self.name not in getdpTSAMassResistivityFunctions: 

1239 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1240 

1241 return getdpTSAMassResistivityFunctions[self.name] 

1242 

1243 @computed_field 

1244 @cached_property 

1245 def getdpTSAStiffnessResistivityFunction(self) -> str: 

1246 """Return the GetDP function name of the material's stiffness resistivity.""" 

1247 if self.name not in getdpTSAStiffnessResistivityFunctions: 

1248 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1249 

1250 return getdpTSAStiffnessResistivityFunctions[self.name] 

1251 

1252 @computed_field 

1253 @cached_property 

1254 def getdpTSAMassThermalConductivityFunction(self) -> str: 

1255 """Return the GetDP function name of the material's mass thermal conductivity.""" 

1256 if self.name not in getdpTSAMassThermalConductivityFunctions: 

1257 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1258 

1259 return getdpTSAMassThermalConductivityFunctions[self.name] 

1260 

1261 @computed_field 

1262 @cached_property 

1263 def getdpTSAStiffnessThermalConductivityFunction(self) -> str: 

1264 """Return the GetDP function name of the material's stiffness thermal conductivity.""" 

1265 if self.name not in getdpTSAStiffnessThermalConductivityFunctions: 

1266 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1267 

1268 return getdpTSAStiffnessThermalConductivityFunctions[self.name] 

1269 

1270 @computed_field 

1271 @cached_property 

1272 def getdpTSAMassHeatCapacityFunction(self) -> str: 

1273 """Return the GetDP function name of the material's mass heat capacity.""" 

1274 if self.name not in getdpTSAMassHeatCapacityFunctions: 

1275 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1276 

1277 return getdpTSAMassHeatCapacityFunctions[self.name] 

1278 

1279 @computed_field 

1280 @cached_property 

1281 def getdpTSARHSFunction(self) -> str: 

1282 """Return the GetDP function name of the material's RHS.""" 

1283 if self.name not in getdpTSARHSFunctions: 

1284 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1285 

1286 return getdpTSARHSFunctions[self.name] 

1287 

1288 @computed_field 

1289 @cached_property 

1290 def getdpTSATripleFunction(self) -> str: 

1291 """Return the GetDP function name of the material's triple.""" 

1292 if self.name not in getdpTSATripleFunctions: 

1293 return "NOT_DEFINED_IN_DATA_FIQUS_PANCAKE3D" 

1294 

1295 return getdpTSATripleFunctions[self.name] 

1296 

1297 

1298class Pancake3DSolveNormalMaterial(Pancake3DSolveMaterialBase): 

1299 # Mandatory: 

1300 name: NormalMaterialName = Field( 

1301 title="Material Name", 

1302 ) 

1303 

1304 

1305class Pancake3DSolveSuperconductingMaterial(Pancake3DSolveMaterialBase): 

1306 # Mandatory: 

1307 name: SuperconductingMaterialName = Field( 

1308 title="Superconduncting Material Name", 

1309 ) 

1310 nValue: PositiveFloat = Field( 

1311 default=30, 

1312 alias="N-Value for E-J Power Law", 

1313 description="N-value for E-J power law.", 

1314 ) 

1315 IcAtTinit: PositiveFloat | str | Pancake3DSolveIcVsLength = Field( 

1316 alias="criticalCurrentAtInitialTemperature", 

1317 title="Critical Current at Initial Temperature", 

1318 description=( 

1319 "Critical current at initial temperature. The critical current value will" 

1320 " change with temperature depending on the superconductor material.\nEither" 

1321 " the same critical current for the whole tape or the critical current with" 

1322 " respect to the tape length can be specified. To specify the same critical" 

1323 " current for the entire tape, just use a scalar. To specify critical" 

1324 " current with respect to the tape length: a CSV file can be used, or" 

1325 " lengthValues and criticalCurrentValues can be given as lists. The data" 

1326 " will be linearly interpolated.\nIf a CSV file is to be used, the input" 

1327 " should be the name of a CSV file (which is in the same folder as the" 

1328 " input file) instead of a scalar. The first column of the CSV file will be" 

1329 " the tape length, and the second column will be the critical current." 

1330 ), 

1331 examples=[230, "IcVSlength.csv"], 

1332 ) 

1333 

1334 # Optionals: 

1335 electricFieldCriterion: PositiveFloat = Field( 

1336 default=1e-4, 

1337 title="Electric Field Criterion", 

1338 description=( 

1339 "The electric field that defines the critical current density, i.e., the" 

1340 " electric field at which the current density reaches the critical current" 

1341 " density." 

1342 ), 

1343 ) 

1344 jCriticalScalingNormalToWinding: PositiveFloat = Field( 

1345 default=1, 

1346 title="Critical Current Scaling Normal to Winding", 

1347 description=( 

1348 "Critical current scaling normal to winding, i.e., along the c_axis. " 

1349 " We have Jc_cAxis = scalingFactor * Jc_abPlane." 

1350 " A factor of 1 means no scaling such that the HTS layer is isotropic." 

1351 ), 

1352 ) 

1353 minimumPossibleResistivity: NonNegativeFloat = Field( 

1354 default=0, 

1355 title="Minimum Possible Resistivity", 

1356 description=( 

1357 "The resistivity of the winding won't be lower than this value, no matter" 

1358 " what." 

1359 ), 

1360 ) 

1361 maximumPossibleResistivity: PositiveFloat = Field( 

1362 default=1, 

1363 title="Maximum Possible Resistivity", 

1364 description=( 

1365 "The resistivity of the winding won't be higher than this value, no matter" 

1366 " what." 

1367 ), 

1368 ) 

1369 

1370 @computed_field 

1371 @cached_property 

1372 def IcValues(self) -> list[float]: 

1373 """Return the critical current values of the material.""" 

1374 if hasattr(self.IcAtTinit, "criticalCurrentValues"): 

1375 return self.IcAtTinit.criticalCurrentValues 

1376 elif isinstance(self.IcAtTinit, str): 

1377 csv_file_path = pathlib.Path(input_file_path.get()).parent / self.IcAtTinit 

1378 # return the second column: 

1379 IcValues = list(pd.read_csv(csv_file_path, header=None).iloc[:, 1]) 

1380 for Ic in IcValues: 

1381 if Ic < 0: 

1382 raise ValueError( 

1383 "Critical current values in the CSV file should be positive!" 

1384 ) 

1385 return IcValues 

1386 else: 

1387 return [self.IcAtTinit] 

1388 

1389 @computed_field 

1390 @cached_property 

1391 def lengthValues(self) -> list[float]: 

1392 """Return the length values of the material.""" 

1393 if hasattr(self.IcAtTinit, "lengthValues"): 

1394 return self.IcAtTinit.lengthValues 

1395 elif isinstance(self.IcAtTinit, str): 

1396 csv_file_path = pathlib.Path(input_file_path.get()).parent / self.IcAtTinit 

1397 # return the first column: 

1398 lengthValues = list(pd.read_csv(csv_file_path, header=None).iloc[:, 0]) 

1399 for length in lengthValues: 

1400 if length < 0: 

1401 raise ValueError("Tape lengths in the CSV file should be positive!") 

1402 return lengthValues 

1403 else: 

1404 return [1] 

1405 

1406 @computed_field 

1407 @cached_property 

1408 def getdpCriticalCurrentDensityFunction(self) -> str: 

1409 """Return the GetDP function name of the material's critical current density.""" 

1410 if self.name not in getdpCriticalCurrentDensityFunctions: 

1411 raise ValueError( 

1412 f"Critical current density of the material '{self.name}' is not defined" 

1413 " in FiQuS!" 

1414 ) 

1415 

1416 return getdpCriticalCurrentDensityFunctions[self.name] 

1417 

1418 

1419class Pancake3DSolveHTSMaterialBase(BaseModel): 

1420 relativeThickness: float = Field( 

1421 le=1, 

1422 title="Relative Thickness (only for winding)", 

1423 description=( 

1424 "Winding tapes generally consist of more than one material. Therefore, when" 

1425 " materials are given as a list in winding, their relative thickness," 

1426 " (thickness of the material) / (thickness of the winding), should be" 

1427 " specified." 

1428 ), 

1429 ) 

1430 

1431 

1432class Pancake3DSolveHTSNormalMaterial( 

1433 Pancake3DSolveHTSMaterialBase, Pancake3DSolveNormalMaterial 

1434): 

1435 pass 

1436 

1437 

1438class Pancake3DSolveHTSSuperconductingMaterial( 

1439 Pancake3DSolveHTSMaterialBase, Pancake3DSolveSuperconductingMaterial 

1440): 

1441 pass 

1442 

1443 

1444class Pancake3DSolveHTSShuntLayerMaterial(Pancake3DSolveNormalMaterial): 

1445 name: NormalMaterialName = Field( 

1446 default="Copper", 

1447 title="Material Name", 

1448 ) 

1449 relativeHeight: float = Field( 

1450 default=0.0, 

1451 ge=0, 

1452 le=1, 

1453 title="Relative Height of the Shunt Layer", 

1454 description=( 

1455 "HTS 2G coated conductor are typically plated, usually " 

1456 " using copper. The relative height of the shunt layer is the " 

1457 " width of the shunt layer divided by the width of the tape. " 

1458 " 0 means no shunt layer." 

1459 ), 

1460 ) 

1461 

1462 

1463class Pancake3DSolveMaterial(BaseModel): 

1464 # 1) User inputs: 

1465 

1466 # Mandatory: 

1467 

1468 # Optionals: 

1469 resistivity: Optional[PositiveFloat] = Field( 

1470 default=None, 

1471 title="Resistivity", 

1472 description=( 

1473 "A scalar value. If this is given, material properties won't be used for" 

1474 " resistivity." 

1475 ), 

1476 ) 

1477 thermalConductivity: Optional[PositiveFloat] = Field( 

1478 default=None, 

1479 title="Thermal Conductivity", 

1480 description=( 

1481 "A scalar value. If this is given, material properties won't be used for" 

1482 " thermal conductivity." 

1483 ), 

1484 ) 

1485 specificHeatCapacity: Optional[PositiveFloat] = Field( 

1486 default=None, 

1487 title="Specific Heat Capacity", 

1488 description=( 

1489 "A scalar value. If this is given, material properties won't be used for" 

1490 " specific heat capacity." 

1491 ), 

1492 ) 

1493 material: Optional[Pancake3DSolveNormalMaterial] = Field( 

1494 default=None, 

1495 title="Material", 

1496 description="Material from STEAM material library.", 

1497 ) 

1498 

1499 @model_validator(mode="after") 

1500 @classmethod 

1501 def check_material(cls, model: "Pancake3DSolveMaterial"): 

1502 if model.material is None: 

1503 if model.resistivity is None: 

1504 raise ValueError( 

1505 "Resistivity of the material is not given, and no material is" 

1506 " provided!" 

1507 ) 

1508 if model.thermalConductivity is None: 

1509 raise ValueError( 

1510 "Thermal conductivity of the material is not given, and no material" 

1511 " is provided!" 

1512 ) 

1513 if model.specificHeatCapacity is None: 

1514 raise ValueError( 

1515 "Specific heat capacity of the material is not given, and no" 

1516 " material is provided!" 

1517 ) 

1518 

1519 return model 

1520 

1521 

1522class Pancake3DSolveShuntLayerMaterial(Pancake3DSolveMaterial): 

1523 material: Optional[Pancake3DSolveHTSShuntLayerMaterial] = Field( 

1524 default=Pancake3DSolveHTSShuntLayerMaterial(), 

1525 title="Material", 

1526 description="Material from STEAM material library.", 

1527 ) 

1528 

1529 

1530class Pancake3DSolveContactLayerMaterial(Pancake3DSolveMaterial): 

1531 resistivity: PositiveFloat | Literal["perfectlyInsulating"] = Field( 

1532 default=None, 

1533 title="Resistivity", 

1534 description=( 

1535 'A scalar value or "perfectlyInsulating". If "perfectlyInsulating" is' 

1536 " given, the contact layer will be perfectly insulating. If this value is" 

1537 " given, material properties won't be used for resistivity." 

1538 ), 

1539 ) 

1540 numberOfThinShellElements: PositiveInt = Field( 

1541 default=1, 

1542 title="Number of Thin Shell Elements (Advanced Input)", 

1543 description=( 

1544 "Number of thin shell elements in the FE formulation (GetDP related, not" 

1545 " physical and only used when TSA is set to True)" 

1546 ), 

1547 ) 

1548 

1549 @field_validator("resistivity") 

1550 @classmethod 

1551 def checkPerfectlyInsulatingCase(cls, resistivity): 

1552 if resistivity == "perfectlyInsulating": 

1553 geometry = geometry_input.get() 

1554 if geometry["numberOfPancakes"] > 1: 

1555 raise ValueError( 

1556 "Contact layer can't be perfectly insulating for multi-pancake" 

1557 " coils!" 

1558 ) 

1559 

1560 return resistivity 

1561 

1562 

1563Pancake3DHTSMaterial = Annotated[ 

1564 Pancake3DSolveHTSNormalMaterial | Pancake3DSolveHTSSuperconductingMaterial, 

1565 Field(discriminator="name"), 

1566] 

1567 

1568 

1569class Pancake3DSolveWindingMaterial(Pancake3DSolveMaterial): 

1570 material: Optional[list[Pancake3DHTSMaterial]] = Field( 

1571 default=None, 

1572 title="Materials of HTS CC", 

1573 description="List of materials of HTS CC.", 

1574 ) 

1575 

1576 shuntLayer: Pancake3DSolveShuntLayerMaterial = Field( 

1577 default=Pancake3DSolveShuntLayerMaterial(), 

1578 title="Shunt Layer Properties", 

1579 description="Material properties of the shunt layer.", 

1580 ) 

1581 

1582 @field_validator("material") 

1583 @classmethod 

1584 def checkIfRelativeThicknessesSumToOne(cls, material): 

1585 if material is not None: 

1586 totalRelativeThickness = sum( 

1587 material.relativeThickness for material in material 

1588 ) 

1589 if not math.isclose(totalRelativeThickness, 1, rel_tol=1e-3): 

1590 raise ValueError( 

1591 "Sum of relative thicknesses of HTS materials should be 1!" 

1592 ) 

1593 

1594 return material 

1595 

1596 @computed_field 

1597 @cached_property 

1598 def relativeThicknessOfNormalConductor(self) -> PositiveFloat: 

1599 """Return the relative thickness of the normal conductor.""" 

1600 if self.material is None: 

1601 return 0 

1602 else: 

1603 # look at normal materials inside self.material and sum their relativeThickness 

1604 return sum( 

1605 material.relativeThickness 

1606 for material in self.material 

1607 if isinstance(material, Pancake3DSolveHTSNormalMaterial) 

1608 ) 

1609 

1610 @computed_field 

1611 @cached_property 

1612 def relativeThicknessOfSuperConductor(self) -> PositiveFloat: 

1613 """Return the relative thickness of the super conductor.""" 

1614 if self.material is None: 

1615 return 0 

1616 else: 

1617 # look at superconducting materials inside self.material and sum their relativeThickness 

1618 return sum( 

1619 material.relativeThickness 

1620 for material in self.material 

1621 if isinstance(material, Pancake3DSolveHTSSuperconductingMaterial) 

1622 ) 

1623 

1624 @computed_field 

1625 @cached_property 

1626 def normalConductors(self) -> list[Pancake3DSolveHTSNormalMaterial]: 

1627 """Return the normal conductors of the winding.""" 

1628 if self.material is None: 

1629 return [] 

1630 else: 

1631 return [ 

1632 material 

1633 for material in self.material 

1634 if isinstance(material, Pancake3DSolveHTSNormalMaterial) 

1635 ] 

1636 

1637 @computed_field 

1638 @cached_property 

1639 def superConductor(self) -> Pancake3DSolveHTSSuperconductingMaterial: 

1640 """Return the super conductor of the winding.""" 

1641 if self.material is None: 

1642 return None 

1643 else: 

1644 superConductors = [ 

1645 material 

1646 for material in self.material 

1647 if isinstance(material, Pancake3DSolveHTSSuperconductingMaterial) 

1648 ] 

1649 if len(superConductors) == 0: 

1650 return None 

1651 elif len(superConductors) == 1: 

1652 return superConductors[0] 

1653 else: 

1654 raise ValueError( 

1655 "There should be only one superconductor in the winding!" 

1656 ) 

1657 

1658 

1659class Pancake3DSolveTerminalMaterialAndBoundaryCondition(Pancake3DSolveMaterial): 

1660 cooling: Literal["adiabatic", "fixedTemperature", "cryocooler"] = Field( 

1661 default="fixedTemperature", 

1662 title="Cooling condition", 

1663 description=( 

1664 "Cooling condition of the terminal. It can be either adiabatic, fixed" 

1665 " temperature, or cryocooler." 

1666 ), 

1667 ) 

1668 transitionNotch: Pancake3DSolveMaterial = Field( 

1669 title="Transition Notch Properties", 

1670 description="Material properties of the transition notch volume.", 

1671 ) 

1672 terminalContactLayer: Pancake3DSolveMaterial = Field( 

1673 title="Transition Layer Properties", 

1674 description=( 

1675 "Material properties of the transition layer between terminals and" 

1676 " windings." 

1677 ), 

1678 ) 

1679 

1680 

1681class Pancake3DSolveToleranceBase(BaseModel): 

1682 # Mandatory: 

1683 quantity: str 

1684 relative: NonNegativeFloat = Field( 

1685 title="Relative Tolerance", 

1686 description="Relative tolerance for the quantity.", 

1687 ) 

1688 absolute: NonNegativeFloat = Field( 

1689 title="Absolute Tolerance", description="Absolute tolerance for the quantity" 

1690 ) 

1691 

1692 # Optionals: 

1693 normType: Literal["L1Norm", "MeanL1Norm", "L2Norm", "MeanL2Norm", "LinfNorm"] = ( 

1694 Field( 

1695 default="L2Norm", 

1696 alias="normType", 

1697 title="Norm Type", 

1698 description=( 

1699 "Sometimes, tolerances return a vector instead of a scalar (ex," 

1700 " solutionVector). Then, the magnitude of the tolerance should be" 

1701 " calculated with a method. Norm type selects this method." 

1702 ), 

1703 ) 

1704 ) 

1705 

1706 

1707class Pancake3DSolvePositionRequiredTolerance(Pancake3DSolveToleranceBase): 

1708 # Mandatory: 

1709 quantity: PositionRequiredQuantityName = Field( 

1710 title="Quantity", description="Name of the quantity for tolerance." 

1711 ) 

1712 position: Pancake3DPosition = Field( 

1713 title="Probing Position of the Quantity", 

1714 description="Probing position of the quantity for tolerance.", 

1715 ) 

1716 

1717 

1718class Pancake3DSolvePositionNotRequiredTolerance(Pancake3DSolveToleranceBase): 

1719 # Mandatory: 

1720 quantity: ( 

1721 Literal[ 

1722 "solutionVector", 

1723 ] 

1724 | PositionNotRequiredQuantityName 

1725 ) = Field( 

1726 title="Quantity", 

1727 description="Name of the quantity for tolerance.", 

1728 ) 

1729 

1730 

1731Pancake3DSolveTolerance = Annotated[ 

1732 Pancake3DSolvePositionRequiredTolerance 

1733 | Pancake3DSolvePositionNotRequiredTolerance, 

1734 Field(discriminator="quantity"), 

1735] 

1736 

1737 

1738class Pancake3DSolveSettingsWithTolerances(BaseModel): 

1739 tolerances: list[Pancake3DSolveTolerance] = Field( 

1740 title="Tolerances for Adaptive Time Stepping", 

1741 description=( 

1742 "Time steps or nonlinear iterations will be refined until the tolerances" 

1743 " are satisfied." 

1744 ), 

1745 ) 

1746 

1747 @computed_field 

1748 @cached_property 

1749 def postOperationTolerances(self) -> list[Pancake3DSolveTolerance]: 

1750 """Return the post operation tolerances.""" 

1751 tolerances = [ 

1752 tolerance 

1753 for tolerance in self.tolerances 

1754 if "solutionVector" not in tolerance.quantity 

1755 ] 

1756 return tolerances 

1757 

1758 @computed_field 

1759 @cached_property 

1760 def systemTolerances(self) -> list[Pancake3DSolveTolerance]: 

1761 """Return the system tolerances.""" 

1762 tolerances = [ 

1763 tolerance 

1764 for tolerance in self.tolerances 

1765 if "solutionVector" in tolerance.quantity 

1766 ] 

1767 return tolerances 

1768 

1769 

1770class Pancake3DSolveAdaptiveTimeLoopSettings(Pancake3DSolveSettingsWithTolerances): 

1771 # Mandatory: 

1772 initialStep: PositiveFloat = Field( 

1773 alias="initialStep", 

1774 title="Initial Step for Adaptive Time Stepping", 

1775 description="Initial step for adaptive time stepping", 

1776 ) 

1777 minimumStep: PositiveFloat = Field( 

1778 alias="minimumStep", 

1779 title="Minimum Step for Adaptive Time Stepping", 

1780 description=( 

1781 "The simulation will be aborted if a finer time step is required than this" 

1782 " minimum step value." 

1783 ), 

1784 ) 

1785 maximumStep: PositiveFloat = Field( 

1786 alias="maximumStep", 

1787 description="Bigger steps than this won't be allowed", 

1788 ) 

1789 

1790 # Optionals: 

1791 integrationMethod: Literal[ 

1792 "Euler", "Gear_2", "Gear_3", "Gear_4", "Gear_5", "Gear_6" 

1793 ] = Field( 

1794 default="Euler", 

1795 alias="integrationMethod", 

1796 title="Integration Method", 

1797 description="Integration method for transient analysis", 

1798 ) 

1799 breakPoints_input: list[float] = Field( 

1800 default=[0], 

1801 alias="breakPoints", 

1802 title="Break Points for Adaptive Time Stepping", 

1803 description="Make sure to solve the system for these times.", 

1804 ) 

1805 

1806 @field_validator("breakPoints_input") 

1807 @classmethod 

1808 def updateGlobalBreakPointsList(cls, breakPoints_input): 

1809 all_break_points.extend(breakPoints_input) 

1810 return breakPoints_input 

1811 

1812 @model_validator(mode="after") 

1813 @classmethod 

1814 def check_time_steps(cls, model: "Pancake3DSolveAdaptiveTimeLoopSettings"): 

1815 """ 

1816 Checks if the time steps are consistent. 

1817 

1818 :param values: dictionary of time steps 

1819 :type values: dict 

1820 :return: dictionary of time steps 

1821 :rtype: dict 

1822 """ 

1823 

1824 if model.initialStep < model.minimumStep: 

1825 raise ValueError( 

1826 "Initial time step cannot be smaller than the minimum time step!" 

1827 ) 

1828 

1829 if model.initialStep > model.maximumStep: 

1830 raise ValueError( 

1831 "Initial time step cannot be greater than the maximum time step!" 

1832 ) 

1833 

1834 if model.minimumStep > model.maximumStep: 

1835 raise ValueError( 

1836 "Minimum time step cannot be greater than the maximum time step!" 

1837 ) 

1838 

1839 return model 

1840 

1841 @computed_field 

1842 @cached_property 

1843 def breakPoints(self) -> list[float]: 

1844 """Return the break points for adaptive time stepping.""" 

1845 breakPoints = list(set(all_break_points)) 

1846 breakPoints.sort() 

1847 return breakPoints 

1848 

1849 

1850class Pancake3DSolveFixedTimeLoopSettings(BaseModel): 

1851 # Mandatory: 

1852 step: PositiveFloat = Field( 

1853 title="Step for Fixed Time Stepping", 

1854 description="Time step for fixed time stepping.", 

1855 ) 

1856 

1857 

1858class Pancake3DSolveFixedLoopInterval(BaseModel): 

1859 # Mandatory: 

1860 startTime: NonNegativeFloat = Field( 

1861 title="Start Time of the Interval", 

1862 description="Start time of the interval.", 

1863 ) 

1864 endTime: NonNegativeFloat = Field( 

1865 title="End Time of the Interval", 

1866 description="End time of the interval.", 

1867 ) 

1868 step: PositiveFloat = Field( 

1869 title="Step for the Interval", 

1870 description="Time step for the interval", 

1871 ) 

1872 

1873 @model_validator(mode="after") 

1874 @classmethod 

1875 def check_time_steps(cls, model: "Pancake3DSolveFixedLoopInterval"): 

1876 """ 

1877 Checks if the time steps are consistent. 

1878 

1879 :param values: dictionary of time steps 

1880 :type values: dict 

1881 :return: dictionary of time steps 

1882 :rtype: dict 

1883 """ 

1884 

1885 if model.startTime > model.endTime: 

1886 raise ValueError("Start time cannot be greater than the end time!") 

1887 

1888 interval = model.endTime - model.startTime 

1889 if ( 

1890 math.fmod(interval, model.step) > 1e-8 

1891 and math.fmod(interval, model.step) - model.step < -1e-8 

1892 ): 

1893 raise ValueError("Time interval must be a multiple of the time step!") 

1894 

1895 return model 

1896 

1897 

1898class Pancake3DSolveTimeBase(BaseModel): 

1899 # Mandatory: 

1900 start: float = Field( 

1901 title="Start Time", description="Start time of the simulation." 

1902 ) 

1903 end: float = Field(title="End Time", description="End time of the simulation.") 

1904 

1905 # Optionals: 

1906 extrapolationOrder: Literal[0, 1, 2, 3] = Field( 

1907 default=1, 

1908 alias="extrapolationOrder", 

1909 title="Extrapolation Order", 

1910 description=( 

1911 "Before solving for the next time steps, the previous solutions can be" 

1912 " extrapolated for better convergence." 

1913 ), 

1914 ) 

1915 

1916 @model_validator(mode="after") 

1917 @classmethod 

1918 def check_time_steps(cls, model: "Pancake3DSolveTimeBase"): 

1919 """ 

1920 Checks if the time steps are consistent. 

1921 """ 

1922 

1923 if model.start > model.end: 

1924 raise ValueError("Start time cannot be greater than the end time!") 

1925 

1926 return model 

1927 

1928 

1929class Pancake3DSolveTimeAdaptive(Pancake3DSolveTimeBase): 

1930 timeSteppingType: Literal["adaptive"] = "adaptive" 

1931 adaptive: Pancake3DSolveAdaptiveTimeLoopSettings = Field( 

1932 alias="adaptiveSteppingSettings", 

1933 title="Adaptive Time Loop Settings", 

1934 description=( 

1935 "Adaptive time loop settings (only used if stepping type is adaptive)." 

1936 ), 

1937 ) 

1938 

1939 @model_validator(mode="after") 

1940 @classmethod 

1941 def check_time_steps(cls, model: "Pancake3DSolveTimeAdaptive"): 

1942 """ 

1943 Checks if the time steps are consistent. 

1944 """ 

1945 if model.adaptive.initialStep > model.end: 

1946 raise ValueError("Initial time step cannot be greater than the end time!") 

1947 

1948 return model 

1949 

1950 

1951class Pancake3DSolveTimeFixed(Pancake3DSolveTimeBase): 

1952 timeSteppingType: Literal["fixed"] = "fixed" 

1953 fixed: ( 

1954 list[Pancake3DSolveFixedLoopInterval] | Pancake3DSolveFixedTimeLoopSettings 

1955 ) = Field( 

1956 alias="fixedSteppingSettings", 

1957 title="Fixed Time Loop Settings", 

1958 description="Fixed time loop settings (only used if stepping type is fixed).", 

1959 ) 

1960 

1961 @model_validator(mode="after") 

1962 @classmethod 

1963 def check_time_steps(cls, model: "Pancake3DSolveTimeFixed"): 

1964 if isinstance(model.fixed, list): 

1965 for i in range(len(model.fixed) - 1): 

1966 if model.fixed[i].endTime != model.fixed[i + 1].startTime: 

1967 raise ValueError( 

1968 "End time of the previous interval must be equal to the start" 

1969 " time of the next interval!" 

1970 ) 

1971 

1972 if model.fixed[0].startTime != model.start: 

1973 raise ValueError( 

1974 "Start time of the first interval must be equal to the start time" 

1975 " of the simulation!" 

1976 ) 

1977 

1978 else: 

1979 if model.fixed.step > model.end: 

1980 raise ValueError("Time step cannot be greater than the end time!") 

1981 

1982 if not ( 

1983 math.isclose( 

1984 (model.end - model.start) % model.fixed.step, 0, abs_tol=1e-8 

1985 ) 

1986 ): 

1987 raise ValueError("Time interval must be a multiple of the time step!") 

1988 

1989 

1990 return model 

1991 

1992 

1993Pancake3DSolveTime = Annotated[ 

1994 Pancake3DSolveTimeAdaptive | Pancake3DSolveTimeFixed, 

1995 Field(discriminator="timeSteppingType"), 

1996] 

1997 

1998 

1999class Pancake3DSolveNonlinearSolverSettings(Pancake3DSolveSettingsWithTolerances): 

2000 # Optionals: 

2001 maxIter: PositiveInt = Field( 

2002 default=100, 

2003 alias="maximumNumberOfIterations", 

2004 title="Maximum Number of Iterations", 

2005 description="Maximum number of iterations allowed for the nonlinear solver.", 

2006 ) 

2007 relaxationFactor: float = Field( 

2008 default=0.7, 

2009 gt=0, 

2010 alias="relaxationFactor", 

2011 title="Relaxation Factor", 

2012 description=( 

2013 "Calculated step changes of the solution vector will be multiplied with" 

2014 " this value for better convergence." 

2015 ), 

2016 ) 

2017 

2018 

2019class Pancake3DSolveInitialConditions(BaseModel): 

2020 # 1) User inputs: 

2021 

2022 # Mandatory: 

2023 T: PositiveFloat = Field( 

2024 alias="temperature", 

2025 title="Initial Temperature", 

2026 description="Initial temperature of the pancake coils.", 

2027 ) 

2028 

2029 

2030class Pancake3DSolveLocalDefect(BaseModel): 

2031 # Mandatory: 

2032 value: NonNegativeFloat = Field( 

2033 alias="value", 

2034 title="Value", 

2035 description="Value of the local defect.", 

2036 ) 

2037 startTurn: NonNegativeFloat = Field( 

2038 alias="startTurn", 

2039 title="Start Turn", 

2040 description="Start turn of the local defect.", 

2041 ) 

2042 endTurn: PositiveFloat = Field( 

2043 alias="endTurn", 

2044 title="End Turn", 

2045 description="End turn of the local defect.", 

2046 ) 

2047 

2048 startTime: NonNegativeFloat = Field( 

2049 alias="startTime", 

2050 title="Start Time", 

2051 description="Start time of the local defect.", 

2052 ) 

2053 

2054 # Optionals: 

2055 transitionDuration: NonNegativeFloat = Field( 

2056 default=0, 

2057 title="Transition Duration", 

2058 description=( 

2059 "Transition duration of the local defect. If not given, the transition will" 

2060 " be instantly." 

2061 ), 

2062 ) 

2063 whichPancakeCoil: Optional[PositiveInt] = Field( 

2064 default=None, 

2065 title="Pancake Coil Number", 

2066 description="The first pancake coil is 1, the second is 2, etc.", 

2067 ) 

2068 

2069 @field_validator("startTime") 

2070 @classmethod 

2071 def updateGlobalBreakPointsList(cls, startTime): 

2072 all_break_points.append(startTime) 

2073 return startTime 

2074 

2075 @field_validator("startTime") 

2076 @classmethod 

2077 def check_start_time(cls, startTime): 

2078 solve = solve_input.get() 

2079 start_time = solve["time"]["start"] 

2080 end_time = solve["time"]["end"] 

2081 

2082 if startTime < start_time or startTime > end_time: 

2083 raise ValueError( 

2084 "Start time of the local defect should be between the start and end" 

2085 " times!" 

2086 ) 

2087 

2088 return startTime 

2089 

2090 @field_validator("endTurn") 

2091 @classmethod 

2092 def check_end_turn(cls, endTurn, info: ValidationInfo): 

2093 geometry = geometry_input.get() 

2094 

2095 if endTurn > geometry["winding"]["numberOfTurns"]: 

2096 raise ValueError( 

2097 "End turn of the local defect can't be greater than the number of" 

2098 " turns!" 

2099 ) 

2100 

2101 if endTurn < info.data["startTurn"]: 

2102 raise ValueError( 

2103 "End turn of the local defect can't be smaller than the start turn!" 

2104 ) 

2105 

2106 return endTurn 

2107 

2108 @field_validator("whichPancakeCoil") 

2109 @classmethod 

2110 def check_which_pancake_coil(cls, whichPancakeCoil): 

2111 geometry = geometry_input.get() 

2112 

2113 if whichPancakeCoil is None: 

2114 if geometry["numberOfPancakes"] == 1: 

2115 whichPancakeCoil = 1 

2116 else: 

2117 raise ValueError( 

2118 "whickPancakeCoil (pancake coil number) should be given if there" 

2119 " are more than one pancake coil!" 

2120 ) 

2121 

2122 return whichPancakeCoil 

2123 

2124 @computed_field 

2125 @cached_property 

2126 def zTop(self) -> float: 

2127 """Return the z-coordinate of the top of the pancake coil.""" 

2128 geometry = geometry_input.get() 

2129 

2130 zTop = self.zBottom + geometry["winding"]["height"] 

2131 

2132 return zTop 

2133 

2134 @computed_field 

2135 @cached_property 

2136 def zBottom(self) -> float: 

2137 """Return the z-coordinate of the bottom of the pancake coil.""" 

2138 geometry = geometry_input.get() 

2139 

2140 if not self.whichPancakeCoil: 

2141 self.whichPancakeCoil = 1 

2142 

2143 zBottom = -( 

2144 geometry["numberOfPancakes"] * geometry["winding"]["height"] 

2145 + (geometry["numberOfPancakes"] - 1) 

2146 * geometry["gapBetweenPancakes"] 

2147 ) / 2 + (self.whichPancakeCoil - 1) * ( 

2148 geometry["winding"]["height"] + geometry["gapBetweenPancakes"] 

2149 ) 

2150 

2151 return zBottom 

2152 

2153 

2154class Pancake3DSolveLocalDefects(BaseModel): 

2155 # 1) User inputs: 

2156 

2157 jCritical: Optional[Pancake3DSolveLocalDefect] = Field( 

2158 default=None, 

2159 alias="criticalCurrentDensity", 

2160 title="Local Defect for Critical Current Density", 

2161 description="Set critical current density locally.", 

2162 ) 

2163 

2164 @field_validator("jCritical") 

2165 @classmethod 

2166 def check_jCritical_local_defect(cls, jCritical): 

2167 if jCritical is not None: 

2168 solve = solve_input.get() 

2169 

2170 if "material" in solve["winding"]: 

2171 windingMaterials = [ 

2172 material["name"] for material in solve["winding"]["material"] 

2173 ] 

2174 else: 

2175 windingMaterials = [] 

2176 

2177 superconducting_material_is_used = "HTSSuperPower" in windingMaterials 

2178 

2179 if not superconducting_material_is_used: 

2180 raise ValueError( 

2181 "Local defects can only be set if superconducting material is used!" 

2182 ) 

2183 

2184 return jCritical 

2185 

2186 

2187class Pancake3DSolveQuantityBase(BaseModel): 

2188 # Mandatory: 

2189 quantity: PositionNotRequiredQuantityName | PositionRequiredQuantityName = Field( 

2190 title="Quantity", 

2191 description="Name of the quantity to be saved.", 

2192 ) 

2193 

2194 @computed_field 

2195 @cached_property 

2196 def getdpQuantityName(self) -> str: 

2197 """Return the GetDP name of the quantity.""" 

2198 if self.quantity not in getdpQuantityNames: 

2199 raise ValueError(f"Quantity '{self.quantity}' is not defined in FiQuS!") 

2200 

2201 return getdpQuantityNames[self.quantity] 

2202 

2203 @computed_field 

2204 @cached_property 

2205 def getdpPostOperationName(self) -> str: 

2206 """Return the GetDP name of the post operation.""" 

2207 if self.quantity not in getdpPostOperationNames: 

2208 raise ValueError(f"Quantity '{self.quantity}' is not defined in FiQuS!") 

2209 

2210 return getdpPostOperationNames[self.quantity] 

2211 

2212 

2213class Pancake3DSolveSaveQuantity(Pancake3DSolveQuantityBase): 

2214 # Optionals: 

2215 timesToBeSaved: list[float] = Field( 

2216 default=[], 

2217 title="Times to be Saved", 

2218 description=( 

2219 "List of times that wanted to be saved. If not given, all the time steps" 

2220 " will be saved." 

2221 ), 

2222 ) 

2223 

2224 @field_validator("timesToBeSaved") 

2225 @classmethod 

2226 def updateGlobalBreakPointsList(cls, timesToBeSaved): 

2227 all_break_points.extend(timesToBeSaved) 

2228 return timesToBeSaved 

2229 

2230 @field_validator("timesToBeSaved") 

2231 @classmethod 

2232 def check_times_to_be_saved(cls, timesToBeSaved): 

2233 solve = solve_input.get() 

2234 start_time = solve["time"]["start"] 

2235 end_time = solve["time"]["end"] 

2236 

2237 for time in timesToBeSaved: 

2238 if time < start_time or time > end_time: 

2239 raise ValueError( 

2240 "Times to be saved should be between the start and end times!" 

2241 ) 

2242 

2243 return timesToBeSaved 

2244 

2245 

2246# ====================================================================================== 

2247# SOLVE CLASSES ENDS =================================================================== 

2248# ====================================================================================== 

2249 

2250# ====================================================================================== 

2251# POSTPROCESS CLASSES STARTS =========================================================== 

2252# ====================================================================================== 

2253 

2254 

2255class Pancake3DPostprocessTimeSeriesPlotBase(Pancake3DSolveQuantityBase): 

2256 # Mandatory: 

2257 quantity: str 

2258 

2259 @computed_field 

2260 @cached_property 

2261 def fileName(self) -> str: 

2262 """Return the name of the file to be saved.""" 

2263 return f"{self.quantity}(TimeSeriesPlot)" 

2264 

2265 @computed_field 

2266 @cached_property 

2267 def quantityProperName(self) -> str: 

2268 """Return the proper name of the quantity.""" 

2269 if self.quantity not in quantityProperNames: 

2270 raise ValueError( 

2271 f"Quantity '{self.quantity}'s proper name is not defined! Please" 

2272 ' update the dictionary "quantityProperNames" in the file' 

2273 ' "fiqus/data/DataFiQuSPancake3D.py".' 

2274 ) 

2275 

2276 return quantityProperNames[self.quantity] 

2277 

2278 @computed_field 

2279 @cached_property 

2280 def units(self) -> str: 

2281 """Return the units of the quantity.""" 

2282 if self.quantity not in quantityUnits: 

2283 raise ValueError( 

2284 f"Quantity '{self.quantity}'s units are not defined! Please update" 

2285 ' the dictionary "quantityUnits" in the file' 

2286 ' "fiqus/data/DataFiQuSPancake3D.py".' 

2287 ) 

2288 

2289 return quantityUnits[self.quantity] 

2290 

2291 

2292class Pancake3DPostprocessTimeSeriesPlotPositionRequired( 

2293 Pancake3DPostprocessTimeSeriesPlotBase 

2294): 

2295 # Mandatory: 

2296 quantity: PositionRequiredQuantityName = Field( 

2297 title="Quantity", 

2298 description="Name of the quantity to be plotted.", 

2299 ) 

2300 

2301 position: Pancake3DPosition = Field( 

2302 title="Probing Position", 

2303 description="Probing position of the quantity for time series plot.", 

2304 ) 

2305 

2306 

2307class Pancake3DPostprocessTimeSeriesPlotPositionNotRequired( 

2308 Pancake3DPostprocessTimeSeriesPlotBase 

2309): 

2310 # Mandatory: 

2311 quantity: PositionNotRequiredQuantityName = Field( 

2312 title="Quantity", 

2313 description="Name of the quantity to be plotted.", 

2314 ) 

2315 

2316 

2317Pancake3DPostprocessTimeSeriesPlot = Annotated[ 

2318 Pancake3DPostprocessTimeSeriesPlotPositionRequired 

2319 | Pancake3DPostprocessTimeSeriesPlotPositionNotRequired, 

2320 Field(discriminator="quantity"), 

2321] 

2322 

2323 

2324class Pancake3DPostprocessMagneticFieldOnPlane(BaseModel): 

2325 # Optional: 

2326 colormap: str = Field( 

2327 default="viridis", 

2328 title="Colormap", 

2329 description="Colormap for the plot.", 

2330 ) 

2331 streamLines: bool = Field( 

2332 default=True, 

2333 title="Stream Lines", 

2334 description=( 

2335 "If True, streamlines will be plotted. Note that magnetic field vectors may" 

2336 " have components perpendicular to the plane, and streamlines will be drawn" 

2337 " depending on the vectors' projection onto the plane." 

2338 ), 

2339 ) 

2340 interpolationMethod: Literal["nearest", "linear", "cubic"] = Field( 

2341 default="linear", 

2342 title="Interpolation Method", 

2343 description=( 

2344 "Interpolation type for the plot.\nBecause of the FEM basis function" 

2345 " selections of FiQuS, each mesh element has a constant magnetic field" 

2346 " vector. Therefore, for smooth 2D plots, interpolation can be" 

2347 " used.\nTypes:\nnearest: it will plot the nearest magnetic field value to" 

2348 " the plotting point.\nlinear: it will do linear interpolation to the" 

2349 " magnetic field values.\ncubic: it will do cubic interpolation to the" 

2350 " magnetic field values." 

2351 ), 

2352 ) 

2353 timesToBePlotted: Optional[list[float]] = Field( 

2354 default=None, 

2355 title="Times to be Plotted", 

2356 description=( 

2357 "List of times that wanted to be plotted. If not given, all the time steps" 

2358 " will be plotted." 

2359 ), 

2360 ) 

2361 planeNormal: Annotated[list[float], Len(min_length=3, max_length=3)] = Field( 

2362 default=[1, 0, 0], 

2363 title="Plane Normal", 

2364 description="Normal vector of the plane. The default is YZ-plane (1, 0, 0).", 

2365 ) 

2366 planeXAxisUnitVector: Annotated[list[float], Len(min_length=3, max_length=3)] = ( 

2367 Field( 

2368 default=[0, 1, 0], 

2369 title="Plane X Axis", 

2370 description=( 

2371 "If an arbitrary plane is wanted to be plotted, the arbitrary plane's X" 

2372 " axis unit vector must be specified. The dot product of the plane's X" 

2373 " axis and the plane's normal vector must be zero." 

2374 ), 

2375 ) 

2376 ) 

2377 

2378 @field_validator("timesToBePlotted") 

2379 @classmethod 

2380 def updateGlobalBreakPointsList(cls, timesToBePlotted): 

2381 all_break_points.extend(timesToBePlotted) 

2382 return timesToBePlotted 

2383 

2384 @field_validator("colormap") 

2385 @classmethod 

2386 def check_color_map(cls, colorMap): 

2387 """ 

2388 Check if the colormap exists. 

2389 """ 

2390 if colorMap not in matplotlib.pyplot.colormaps(): 

2391 raise ValueError( 

2392 f"{colorMap} is not a valid colormap! Please see" 

2393 " https://matplotlib.org/stable/gallery/color/colormap_reference.html" 

2394 " for available colormaps." 

2395 ) 

2396 

2397 return colorMap 

2398 

2399 @computed_field 

2400 @cached_property 

2401 def onSection(self) -> list[list[float]]: 

2402 """Return the three corner points of the plane.""" 

2403 

2404 class unitVector: 

2405 def __init__(self, u, v, w) -> None: 

2406 length = math.sqrt(u**2 + v**2 + w**2) 

2407 self.u = u / length 

2408 self.v = v / length 

2409 self.w = w / length 

2410 

2411 def rotate(self, theta, withRespectTo): 

2412 # Rotate with respect to the withRespectTo vector by theta degrees: 

2413 # https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle 

2414 a = withRespectTo.u 

2415 b = withRespectTo.v 

2416 c = withRespectTo.w 

2417 

2418 rotationMatrix = np.array( 

2419 [ 

2420 [ 

2421 math.cos(theta) + a**2 * (1 - math.cos(theta)), 

2422 a * b * (1 - math.cos(theta)) - c * math.sin(theta), 

2423 a * c * (1 - math.cos(theta)) + b * math.sin(theta), 

2424 ], 

2425 [ 

2426 b * a * (1 - math.cos(theta)) + c * math.sin(theta), 

2427 math.cos(theta) + b**2 * (1 - math.cos(theta)), 

2428 b * c * (1 - math.cos(theta)) - a * math.sin(theta), 

2429 ], 

2430 [ 

2431 c * a * (1 - math.cos(theta)) - b * math.sin(theta), 

2432 c * b * (1 - math.cos(theta)) + a * math.sin(theta), 

2433 math.cos(theta) + c**2 * (1 - math.cos(theta)), 

2434 ], 

2435 ] 

2436 ) 

2437 vector = np.array([[self.u], [self.v], [self.w]]) 

2438 rotatedVector = rotationMatrix @ vector 

2439 return unitVector( 

2440 rotatedVector[0][0], 

2441 rotatedVector[1][0], 

2442 rotatedVector[2][0], 

2443 ) 

2444 

2445 def __pow__(self, otherUnitVector): 

2446 # Cross product: 

2447 u = self.v * otherUnitVector.w - self.w * otherUnitVector.v 

2448 v = self.w * otherUnitVector.u - self.u * otherUnitVector.w 

2449 w = self.u * otherUnitVector.v - self.v * otherUnitVector.u 

2450 return unitVector(u, v, w) 

2451 

2452 def __mul__(self, otherUnitVector) -> float: 

2453 # Dot product: 

2454 return ( 

2455 self.u * otherUnitVector.u 

2456 + self.v * otherUnitVector.v 

2457 + self.w * otherUnitVector.w 

2458 ) 

2459 

2460 planeNormal = unitVector( 

2461 self.planeNormal[0], self.planeNormal[1], self.planeNormal[2] 

2462 ) 

2463 planeXAxis = unitVector( 

2464 self.planeXAxis[0], self.planeXAxis[1], self.planeXAxis[2] 

2465 ) 

2466 

2467 if not math.isclose(planeNormal * planeXAxis, 0, abs_tol=1e-8): 

2468 raise ValueError( 

2469 "planeNormal and planeXAxis must be perpendicular to each" 

2470 " other! If planeNormal is chosen arbitrarily, planeXAxis must" 

2471 " be specified." 

2472 ) 

2473 

2474 # A plane that passes through the origin with the normal vector planeNormal 

2475 # can be defined as: 

2476 # a*x + b*y + c*z = 0 

2477 a = planeNormal.u 

2478 b = planeNormal.v 

2479 c = planeNormal.w 

2480 

2481 # Pick three points on the plane to define a rectangle. The points will 

2482 # be the corners of the rectangle. 

2483 

2484 # To do that, change coordinate system: 

2485 

2486 # Find a vector that is perpendicular to planeNormal: 

2487 randomVector = unitVector(c, a, b) 

2488 perpendicularVector1 = planeNormal**randomVector 

2489 

2490 # Rotate perperndicular vector with respect to the plane's normal vector 

2491 # by 90 degrees to find the second perpendicular vector: 

2492 perpendicularVector2 = perpendicularVector1.rotate(math.pi / 2, planeNormal) 

2493 

2494 # Build the transformation matrix to change from the plane's coordinate 

2495 # system to the global coordinate system: 

2496 transformationMatrix = np.array( 

2497 [ 

2498 [ 

2499 perpendicularVector1.u, 

2500 perpendicularVector1.v, 

2501 perpendicularVector1.w, 

2502 ], 

2503 [ 

2504 perpendicularVector2.u, 

2505 perpendicularVector2.v, 

2506 perpendicularVector2.w, 

2507 ], 

2508 [planeNormal.u, planeNormal.v, planeNormal.w], 

2509 ] 

2510 ) 

2511 transformationMatrix = np.linalg.inv(transformationMatrix) 

2512 

2513 # Select three points to define the rectangle. Pick large points because 

2514 # only the intersection of the rectangle and the mesh will be used. 

2515 geometry = geometry_input.get() 

2516 if geometry["air"]["type"] == "cuboid": 

2517 sideLength = geometry["air"]["sideLength"] 

2518 airMaxWidth = 2 * math.sqrt((sideLength / 2) ** 2 + (sideLength / 2) ** 2) 

2519 if geometry["air"]["type"] == "cylinder": 

2520 airMaxWidth = geometry["air"]["radius"] 

2521 

2522 airHeight = getAirHeight() 

2523 

2524 point1InPlaneCoordinates = np.array( 

2525 [5 * max(airMaxWidth, airHeight), 5 * max(airMaxWidth, airHeight), 0] 

2526 ) 

2527 point1 = transformationMatrix @ point1InPlaneCoordinates 

2528 

2529 point2InPlaneCoordinates = np.array( 

2530 [-5 * max(airMaxWidth, airHeight), 5 * max(airMaxWidth, airHeight), 0] 

2531 ) 

2532 point2 = transformationMatrix @ point2InPlaneCoordinates 

2533 

2534 point3InPlaneCoordinates = np.array( 

2535 [ 

2536 -5 * max(airMaxWidth, airHeight), 

2537 -5 * max(airMaxWidth, airHeight), 

2538 0, 

2539 ] 

2540 ) 

2541 point3 = transformationMatrix @ point3InPlaneCoordinates 

2542 

2543 # Round the point coordinates to the nearest multiple of the dimTol: 

2544 if "dimTol" in geometry: 

2545 dimTol = geometry["dimTol"] 

2546 else: 

2547 dimTol = 1e-8 

2548 

2549 point1[0] = round(point1[0] / dimTol) * dimTol 

2550 point1[1] = round(point1[1] / dimTol) * dimTol 

2551 point1[2] = round(point1[2] / dimTol) * dimTol 

2552 point2[0] = round(point2[0] / dimTol) * dimTol 

2553 point2[1] = round(point2[1] / dimTol) * dimTol 

2554 point2[2] = round(point2[2] / dimTol) * dimTol 

2555 point3[0] = round(point3[0] / dimTol) * dimTol 

2556 point3[1] = round(point3[1] / dimTol) * dimTol 

2557 point3[2] = round(point3[2] / dimTol) * dimTol 

2558 

2559 onSection = [ 

2560 [float(point1[0]), float(point1[1]), float(point1[2])], 

2561 [float(point2[0]), float(point2[1]), float(point2[2])], 

2562 [float(point3[0]), float(point3[1]), float(point3[2])], 

2563 ] 

2564 

2565 return onSection 

2566 

2567 

2568# ====================================================================================== 

2569# POSTPROCESS CLASSES ENDS ============================================================= 

2570# ====================================================================================== 

2571 

2572 

2573class Pancake3DGeometry(BaseModel): 

2574 # Mandatory: 

2575 N: PositiveInt = Field( 

2576 ge=1, 

2577 alias="numberOfPancakes", 

2578 title="Number of Pancakes", 

2579 description="Number of pancake coils stacked on top of each other.", 

2580 ) 

2581 

2582 gap: PositiveFloat = Field( 

2583 alias="gapBetweenPancakes", 

2584 title="Gap Between Pancakes", 

2585 description="Gap distance between the pancake coils.", 

2586 ) 

2587 

2588 wi: Pancake3DGeometryWinding = Field( 

2589 alias="winding", 

2590 title="Winding Geometry", 

2591 description="This dictionary contains the winding geometry information.", 

2592 ) 

2593 

2594 ii: Pancake3DGeometryContactLayer = Field( 

2595 alias="contactLayer", 

2596 title="Contact Layer Geometry", 

2597 description="This dictionary contains the contact layer geometry information.", 

2598 ) 

2599 

2600 ti: Pancake3DGeometryTerminals = Field( 

2601 alias="terminals", 

2602 title="Terminals Geometry", 

2603 description="This dictionary contains the terminals geometry information.", 

2604 ) 

2605 

2606 ai: Pancake3DGeometryAir = Field( 

2607 alias="air", 

2608 title="Air Geometry", 

2609 description="This dictionary contains the air geometry information.", 

2610 ) 

2611 

2612 # Optionals: 

2613 dimTol: PositiveFloat = Field( 

2614 default=1e-8, 

2615 alias="dimensionTolerance", 

2616 description="dimension tolerance (CAD related, not physical)", 

2617 ) 

2618 pancakeBoundaryName: str = Field( 

2619 default="PancakeBoundary", 

2620 description=( 

2621 "name of the pancake's curves that touches the air to be used in the mesh" 

2622 ), 

2623 ) 

2624 contactLayerBoundaryName: str = Field( 

2625 default="contactLayerBoundary", 

2626 description=( 

2627 "name of the contact layers's curves that touches the air to be used in the" 

2628 " mesh (only for TSA)" 

2629 ), 

2630 ) 

2631 

2632 

2633class Pancake3DMesh(BaseModel): 

2634 # Mandatory: 

2635 wi: Pancake3DMeshWinding = Field( 

2636 alias="winding", 

2637 title="Winding Mesh", 

2638 description="This dictionary contains the winding mesh information.", 

2639 ) 

2640 ii: Pancake3DMeshContactLayer = Field( 

2641 alias="contactLayer", 

2642 title="Contact Layer Mesh", 

2643 description="This dictionary contains the contact layer mesh information.", 

2644 ) 

2645 

2646 # Optionals: 

2647 ti: Pancake3DMeshAirAndTerminals = Field( 

2648 default=Pancake3DMeshAirAndTerminals(), 

2649 alias="terminals", 

2650 title="Terminal Mesh", 

2651 description="This dictionary contains the terminal mesh information.", 

2652 ) 

2653 ai: Pancake3DMeshAirAndTerminals = Field( 

2654 default=Pancake3DMeshAirAndTerminals(), 

2655 alias="air", 

2656 title="Air Mesh", 

2657 description="This dictionary contains the air mesh information.", 

2658 ) 

2659 

2660 # Mandatory: 

2661 relSizeMin: PositiveFloat = Field( 

2662 alias="minimumElementSize", 

2663 title="Minimum Element Size", 

2664 description=( 

2665 "The minimum mesh element size in terms of the largest mesh size in the" 

2666 " winding. This mesh size will be used in the regions close the the" 

2667 " winding, and then the mesh size will increate to maximum mesh element" 

2668 " size as it gets away from the winding." 

2669 ), 

2670 ) 

2671 relSizeMax: PositiveFloat = Field( 

2672 alias="maximumElementSize", 

2673 title="Maximum Element Size", 

2674 description=( 

2675 "The maximum mesh element size in terms of the largest mesh size in the" 

2676 " winding. This mesh size will be used in the regions close the the" 

2677 " winding, and then the mesh size will increate to maximum mesh element" 

2678 " size as it gets away from the winding." 

2679 ), 

2680 ) 

2681 

2682 @field_validator("relSizeMax") 

2683 @classmethod 

2684 def check_rel_size_max(cls, relSizeMax, info: ValidationInfo): 

2685 if relSizeMax < info.data["relSizeMin"]: 

2686 raise ValueError( 

2687 "Maximum mesh element size (maximumElementSize) cannot be smaller than" 

2688 " the minimum mesh element size (minimumElementSize)!" 

2689 ) 

2690 

2691 return relSizeMax 

2692 

2693 @computed_field 

2694 @cached_property 

2695 def sizeMin(self) -> float: 

2696 """Return the minimum mesh element size in real dimensions.""" 

2697 geometry = geometry_input.get() 

2698 

2699 meshLengthsPerElement = [] 

2700 

2701 # azimuthal elements: 

2702 for numberOfElements in self.wi.ane: 

2703 terminalOuterRadius = ( 

2704 getWindingOuterRadius() 

2705 + 2 * geometry["terminals"]["outer"]["thickness"] 

2706 ) 

2707 meshLengthsPerElement.append( 

2708 2 * math.pi * terminalOuterRadius / numberOfElements 

2709 ) 

2710 

2711 # radial elements: 

2712 for numberOfElements in self.wi.rne: 

2713 meshLengthsPerElement.append( 

2714 geometry["winding"]["thickness"] / numberOfElements 

2715 ) 

2716 

2717 if not geometry["contactLayer"]["thinShellApproximation"]: 

2718 # radial elements: 

2719 for numberOfElements in self.ii.rne: 

2720 meshLengthsPerElement.append( 

2721 geometry["contactLayer"]["thickness"] / numberOfElements 

2722 ) 

2723 

2724 # axial elements: 

2725 for numberOfElements in self.wi.axne: 

2726 meshLengthsPerElement.append( 

2727 geometry["winding"]["height"] / numberOfElements 

2728 ) 

2729 

2730 return max(meshLengthsPerElement) * self.relSizeMin 

2731 

2732 @computed_field 

2733 @cached_property 

2734 def sizeMax(self) -> float: 

2735 """Return the minimum mesh element size in real dimensions.""" 

2736 geometry = geometry_input.get() 

2737 

2738 meshLengthsPerElement = [] 

2739 

2740 # azimuthal elements: 

2741 for numberOfElements in self.wi.ane: 

2742 terminalOuterRadius = ( 

2743 getWindingOuterRadius() 

2744 + 2 * geometry["terminals"]["outer"]["thickness"] 

2745 ) 

2746 meshLengthsPerElement.append( 

2747 2 * math.pi * terminalOuterRadius / numberOfElements 

2748 ) 

2749 

2750 # radial elements: 

2751 for numberOfElements in self.wi.rne: 

2752 meshLengthsPerElement.append( 

2753 geometry["winding"]["thickness"] / numberOfElements 

2754 ) 

2755 

2756 if not geometry["contactLayer"]["thinShellApproximation"]: 

2757 # radial elements: 

2758 for numberOfElements in self.ii.rne: 

2759 meshLengthsPerElement.append( 

2760 geometry["contactLayer"]["thickness"] / numberOfElements 

2761 ) 

2762 

2763 # axial elements: 

2764 for numberOfElements in self.wi.axne: 

2765 meshLengthsPerElement.append( 

2766 geometry["winding"]["height"] / numberOfElements 

2767 ) 

2768 

2769 return max(meshLengthsPerElement) * self.relSizeMax 

2770 

2771 @computed_field 

2772 @cached_property 

2773 def stopGrowingDistance(self) -> float: 

2774 """Return the distance from the pancake coils to start growing the mesh elements.""" 

2775 geometry = geometry_input.get() 

2776 terminalOuterRadius = ( 

2777 getWindingOuterRadius() + 2 * geometry["terminals"]["outer"]["thickness"] 

2778 ) 

2779 

2780 if geometry["air"]["type"] == "cuboid": 

2781 sideLength = geometry["air"]["sideLength"] 

2782 stopGrowingDistance = sideLength / 2 - terminalOuterRadius 

2783 if geometry["air"]["type"] == "cylinder": 

2784 stopGrowingDistance = geometry["air"]["radius"] - terminalOuterRadius 

2785 

2786 return stopGrowingDistance 

2787 

2788 @computed_field 

2789 @cached_property 

2790 def startGrowingDistance(self) -> float: 

2791 geometry = geometry_input.get() 

2792 terminalOuterRadius = ( 

2793 getWindingOuterRadius() + 2 * geometry["terminals"]["outer"]["thickness"] 

2794 ) 

2795 terminalInnerRadius = ( 

2796 geometry["winding"]["innerRadius"] 

2797 - 2 * geometry["terminals"]["inner"]["thickness"] 

2798 ) 

2799 

2800 startGrowingDistance = (terminalOuterRadius - terminalInnerRadius) / 2 

2801 

2802 return startGrowingDistance 

2803 

2804 

2805class Pancake3DSolve(BaseModel): 

2806 # 1) User inputs: 

2807 t: Pancake3DSolveTime = Field( 

2808 alias="time", 

2809 title="Time Settings", 

2810 description="All the time related settings for transient analysis.", 

2811 ) 

2812 

2813 nls: Optional[Pancake3DSolveNonlinearSolverSettings] = Field( 

2814 alias="nonlinearSolver", 

2815 title="Nonlinear Solver Settings", 

2816 description="All the nonlinear solver related settings.", 

2817 ) 

2818 

2819 wi: Pancake3DSolveWindingMaterial = Field( 

2820 alias="winding", 

2821 title="Winding Properties", 

2822 description="This dictionary contains the winding material properties.", 

2823 ) 

2824 ii: Pancake3DSolveContactLayerMaterial = Field( 

2825 alias="contactLayer", 

2826 title="Contact Layer Properties", 

2827 description="This dictionary contains the contact layer material properties.", 

2828 ) 

2829 ti: Pancake3DSolveTerminalMaterialAndBoundaryCondition = Field( 

2830 alias="terminals", 

2831 title="Terminals Properties", 

2832 description=( 

2833 "This dictionary contains the terminals material properties and cooling" 

2834 " condition." 

2835 ), 

2836 ) 

2837 ai: Pancake3DSolveAir = Field( 

2838 alias="air", 

2839 title="Air Properties", 

2840 description="This dictionary contains the air material properties.", 

2841 ) 

2842 

2843 ic: Pancake3DSolveInitialConditions = Field( 

2844 alias="initialConditions", 

2845 title="Initial Conditions", 

2846 description="Initial conditions of the problem.", 

2847 ) 

2848 

2849 save: list[Pancake3DSolveSaveQuantity] = Field( 

2850 alias="quantitiesToBeSaved", 

2851 default=None, 

2852 title="Quantities to be Saved", 

2853 description="List of quantities to be saved.", 

2854 ) 

2855 

2856 # Mandatory: 

2857 type: Literal["electromagnetic", "thermal", "weaklyCoupled", "stronglyCoupled"] = ( 

2858 Field( 

2859 title="Simulation Type", 

2860 description=( 

2861 "FiQuS/Pancake3D can solve only electromagnetics and thermal or" 

2862 " electromagnetic and thermal coupled. In the weaklyCoupled setting," 

2863 " thermal and electromagnetics systems will be put into different" 

2864 " matrices, whereas in the stronglyCoupled setting, they all will be" 

2865 " combined into the same matrix. The solution should remain the same." 

2866 ), 

2867 ) 

2868 ) 

2869 

2870 # Optionals: 

2871 proTemplate: str = Field( 

2872 default="Pancake3D_template.pro", 

2873 description="file name of the .pro template file", 

2874 ) 

2875 

2876 localDefects: Pancake3DSolveLocalDefects = Field( 

2877 default=Pancake3DSolveLocalDefects(), 

2878 alias="localDefects", 

2879 title="Local Defects", 

2880 description=( 

2881 "Local defects (like making a small part of the winding normal conductor at" 

2882 " some time) can be introduced." 

2883 ), 

2884 ) 

2885 

2886 initFromPrevious: str = Field( 

2887 default="", 

2888 title="Full path to res file to continue from", 

2889 description=( 

2890 "The simulation is continued from an existing .res file. The .res file is" 

2891 " from a previous computation on the same geometry and mesh. The .res file" 

2892 " is taken from the folder Solution_<<initFromPrevious>>" 

2893 ), 

2894 ) 

2895 

2896 isothermalInAxialDirection: bool = Field( 

2897 default=False, 

2898 title="Equate DoF along axial direction", 

2899 description=( 

2900 "If True, the DoF along the axial direction will be equated. This means" 

2901 " that the temperature will be the same along the axial direction reducing" 

2902 " the number of DoF. This is only valid for the thermal analysis." 

2903 ), 

2904 ) 

2905 

2906 @computed_field 

2907 @cached_property 

2908 def systemsOfEquationsType(self) -> str: 

2909 

2910 windingMaterialIsGiven = self.wi.material is not None 

2911 contactLayerMaterialIsGiven = self.ii.material is not None 

2912 terminalMaterialIsGiven = self.ti.material is not None 

2913 notchMaterialIsGiven = self.ti.transitionNotch.material is not None 

2914 terminalContactLayerMaterialIsGiven = self.ti.terminalContactLayer.material is not None 

2915 

2916 if ( 

2917 windingMaterialIsGiven 

2918 or contactLayerMaterialIsGiven 

2919 or terminalMaterialIsGiven 

2920 or notchMaterialIsGiven 

2921 or terminalContactLayerMaterialIsGiven 

2922 ): 

2923 systemsOfEquationsType = "nonlinear" 

2924 else: 

2925 systemsOfEquationsType = "linear" 

2926 

2927 return systemsOfEquationsType 

2928 

2929 

2930class Pancake3DPostprocess(BaseModel): 

2931 """ 

2932 TO BE UPDATED 

2933 """ 

2934 

2935 # 1) User inputs: 

2936 timeSeriesPlots: list[Pancake3DPostprocessTimeSeriesPlot] = Field( 

2937 default=None, 

2938 title="Time Series Plots", 

2939 description="Values can be plotted with respect to time.", 

2940 ) 

2941 

2942 magneticFieldOnCutPlane: Optional[Pancake3DPostprocessMagneticFieldOnPlane] = Field( 

2943 default=None, 

2944 title="Magnetic Field on a Cut Plane", 

2945 description=( 

2946 "Color map of the magnetic field on the YZ plane can be plotted with" 

2947 " streamlines." 

2948 ), 

2949 ) 

2950 

2951 

2952class Pancake3D(BaseModel): 

2953 """ 

2954 Level 1: Class for FiQuS Pancake3D 

2955 """ 

2956 

2957 type: Literal["Pancake3D"] 

2958 geometry: Pancake3DGeometry = Field( 

2959 default=None, 

2960 title="Geometry", 

2961 description="This dictionary contains the geometry information.", 

2962 ) 

2963 mesh: Pancake3DMesh = Field( 

2964 default=None, 

2965 title="Mesh", 

2966 description="This dictionary contains the mesh information.", 

2967 ) 

2968 solve: Pancake3DSolve = Field( 

2969 default=None, 

2970 title="Solve", 

2971 description="This dictionary contains the solve information.", 

2972 ) 

2973 postproc: Pancake3DPostprocess = Field( 

2974 default=None, 

2975 title="Postprocess", 

2976 description="This dictionary contains the postprocess information.", 

2977 ) 

2978 input_file_path: str = Field( 

2979 default=None, 

2980 description="path of the input file (calculated by FiQuS)", 

2981 exclude=True, 

2982 ) 

2983 

2984 @model_validator(mode="before") 

2985 @classmethod 

2986 def set_context_variables(cls, model: "Pancake3D"): 

2987 """Set the context variables (geometry data model, mesh data model, solve data 

2988 model) of the Pancake3D model. They will be used in the submodel validators. 

2989 """ 

2990 

2991 if isinstance( 

2992 model["mesh"]["winding"]["azimuthalNumberOfElementsPerTurn"], int 

2993 ): 

2994 model["mesh"]["winding"]["azimuthalNumberOfElementsPerTurn"] = [ 

2995 model["mesh"]["winding"]["azimuthalNumberOfElementsPerTurn"] 

2996 ] * model["geometry"]["numberOfPancakes"] 

2997 

2998 if isinstance(model["mesh"]["winding"]["radialNumberOfElementsPerTurn"], int): 

2999 model["mesh"]["winding"]["radialNumberOfElementsPerTurn"] = [ 

3000 model["mesh"]["winding"]["radialNumberOfElementsPerTurn"] 

3001 ] * model["geometry"]["numberOfPancakes"] 

3002 

3003 if isinstance(model["mesh"]["winding"]["axialNumberOfElements"], int): 

3004 model["mesh"]["winding"]["axialNumberOfElements"] = [ 

3005 model["mesh"]["winding"]["axialNumberOfElements"] 

3006 ] * model["geometry"]["numberOfPancakes"] 

3007 

3008 if isinstance(model["mesh"]["winding"]["elementType"], str): 

3009 model["mesh"]["winding"]["elementType"] = [ 

3010 model["mesh"]["winding"]["elementType"] 

3011 ] * model["geometry"]["numberOfPancakes"] 

3012 

3013 if isinstance( 

3014 model["mesh"]["contactLayer"]["radialNumberOfElementsPerTurn"], int 

3015 ): 

3016 model["mesh"]["contactLayer"]["radialNumberOfElementsPerTurn"] = [ 

3017 model["mesh"]["contactLayer"]["radialNumberOfElementsPerTurn"] 

3018 ] * model["geometry"]["numberOfPancakes"] 

3019 

3020 geometry_input.set(model["geometry"]) 

3021 mesh_input.set(model["mesh"]) 

3022 solve_input.set(model["solve"]) 

3023 input_file_path.set(model["input_file_path"]) 

3024 

3025 return model