-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathmodel.py
More file actions
1706 lines (1443 loc) · 84.4 KB
/
model.py
File metadata and controls
1706 lines (1443 loc) · 84.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Contains all important components of a MagneticComponent.
Conductors, Core, AirGaps, Insulations, WindingWindow, StrayPath and the VirtualWindingWindow.
"""
# Python standard libraries
from dataclasses import dataclass
# 3rd party libraries
import numpy as np
import numpy.typing as npt
from typing import Optional, Union, Dict, Any
# Local libraries
import materialdatabase as mdb
from materialdatabase import Material, DataSource, DatasheetAttribute, ComplexDataType
import femmt.functions as ff
from femmt.functions_model import *
import femmt.functions_reluctance as fr
from femmt.enumerations import *
from femmt.constants import *
from femmt.functions_drawing import *
class Conductor:
"""
A winding defines a conductor which is wound around a magnetic component such as transformer or inductance.
The winding is defined by its conductor and the way it is placed in the magnetic component. To allow different
arrangements of the conductors in several winding windows (hexagonal or square packing, interleaved, ...) in
this class only the conductor parameters are specified.
"""
# TODO More documentation
conductor_type: ConductorType
conductor_arrangement: ConductorArrangement | None = None
wrap_para: WrapParaType | None = None
conductor_radius: float | None = None
winding_number: int
thickness: float | None = None
width: float | None = None
ff: float | None = None
strand_radius: float | None = None
n_strands: int = 0
n_layers: int
a_cell: float
cond_sigma: float
temperature: float
conductor_is_set: bool
# Not used in femmt_classes. Only needed for to_dict()
conductivity: ConductorMaterial | None = None
def __init__(self, winding_number: int, material: ConductorMaterial, parallel: bool = False,
temperature: float = 100):
"""Create a conductor object.
The winding_number sets the order of the conductors. Every conductor needs to have a unique winding number.
The conductor with the lowest winding number (starting from 0) will be treated as primary, second-lowest number as secondary and so on.
:param winding_number: Unique number for the winding
:type winding_number: int
:param material: Sets the conductivity for the conductor
:type material: float
:param temperature: temperature of winding material, default set to 100 °C
:type temperature: float
:param parallel: Set to True to introduce parallel conductors. Default set to False
:type parallel: bool
"""
if winding_number < 0:
raise Exception("Winding index cannot be negative.")
self.winding_number = winding_number
self.conductivity = material
self.conductor_is_set = False
self.parallel = parallel
self.temperature = temperature
dict_material_database = ff.wire_material_database()
if material.name in dict_material_database:
self.cond_sigma = ff.conductivity_temperature(material.name, temperature)
else:
raise Exception(f"Material {material.name} not found in database")
def set_rectangular_conductor(self, thickness: float = None, width: float = None):
"""
Set a rectangular, solid conductor.
:param thickness: thickness of the rectangular conductor in m
:type thickness: float
:param width: width of the rectangular conductor in m
:type width: float
"""
if self.conductor_is_set:
raise Exception("Only one conductor can be set for each winding!")
self.conductor_is_set = True
self.conductor_type = ConductorType.RectangularSolid
self.thickness = thickness
self.width = width
self.a_cell = None # can only be set after the width is determined
self.conductor_radius = 1 # Revisit
def set_solid_round_conductor(self, conductor_radius: float, conductor_arrangement: ConductorArrangement | None):
"""
Set a solid round conductor.
:param conductor_radius: conductor radius in m
:type conductor_radius: float
:param conductor_arrangement: conductor arrangement (Square / SquareFullWidth / Hexagonal)
:type conductor_arrangement: ConductorArrangement | None
"""
if self.conductor_is_set:
raise Exception("Only one conductor can be set for each winding!")
self.conductor_is_set = True
self.conductor_type = ConductorType.RoundSolid
self.conductor_arrangement = conductor_arrangement
self.conductor_radius = conductor_radius
self.a_cell = np.pi * conductor_radius ** 2
def set_litz_round_conductor(self, conductor_radius: float | None, number_strands: int | None,
strand_radius: float | None,
fill_factor: float | None, conductor_arrangement: ConductorArrangement):
"""
Set a round conductor made of litz wire.
Only 3 of the 4 parameters are needed. The other one needs to be none.
:param conductor_radius: conductor radius in m
:type conductor_radius: float | None
:param number_strands: number of strands inside the litz wire
:type number_strands: int | None
:param strand_radius: radius of a single strand in m
:type strand_radius: float | None
:param fill_factor: fill factor of the litz wire
:type fill_factor: float | None
:param conductor_arrangement: conductor arrangement (Square, SquareFullWidth, Hexagonal)
:type conductor_arrangement: ConductorArrangement
"""
if self.conductor_is_set:
raise Exception("Only one conductor can be set for each winding!")
self.conductor_is_set = True
self.conductor_type = ConductorType.RoundLitz
self.conductor_arrangement = conductor_arrangement
self.conductor_radius = conductor_radius
self.n_strands = number_strands
self.strand_radius = strand_radius
self.ff = fill_factor
if number_strands is None:
self.n_strands = int(conductor_radius ** 2 / strand_radius ** 2 * fill_factor)
elif conductor_radius is None:
self.conductor_radius = np.sqrt(number_strands * strand_radius ** 2 / fill_factor)
elif fill_factor is None:
ff_exact = number_strands * strand_radius ** 2 / conductor_radius ** 2
self.ff = np.around(ff_exact, decimals=2)
if self.ff > 0.90:
raise Exception(f"A fill factor of {self.ff} is unrealistic!")
elif strand_radius is None:
self.strand_radius = np.sqrt(conductor_radius ** 2 * fill_factor / number_strands)
else:
raise Exception("1 of the 4 parameters need to be None.")
self.n_layers = ff.litz_calculate_number_layers(self.n_strands)
self.a_cell = self.n_strands * self.strand_radius ** 2 * np.pi / self.ff
def __eq__(self, other):
"""Define how to compare two conductor objects."""
return self.__dict__ == other.__dict__
def __ne__(self, other):
"""Define how to use the not-equal method for conductor objects."""
return self.__dict__ != other.__dict__
def to_dict(self):
"""Transfer object parameters to a dictionary. Important method to create the final result-log."""
return {
"winding_number": self.winding_number,
"temperature": self.temperature,
"material": self.conductivity.name,
"conductor_type": self.conductor_type.name,
"thickness": self.thickness,
"conductor_radius": self.conductor_radius,
"conductor_arrangement": self.conductor_arrangement.name if self.conductor_arrangement is not None else None,
"number_strands": self.n_strands,
"strand_radius": self.strand_radius,
"fill_factor": self.ff
}
class CoreGeometry:
"""Describes the geometry of a magnetic core including window sizes and outer dimensions.
Supports both single and stacked core configurations, with optional detailed modeling of the outer leg shape.
"""
def __init__(self, core_type: CoreType, core_dimensions: object, detailed_core_model: bool):
"""Initialize a CoreGeometry object from dimensions and core configuration.
:param core_type: The type of the core (e.g., single or stacked).
:type core_type: CoreType
:param core_dimensions: An object containing core dimension attributes.
:type core_dimensions: object
:param detailed_core_model: Whether a detailed geometric model should be used.
:type detailed_core_model: bool
"""
self.core_type: CoreType = core_type
self.correct_outer_leg: bool = detailed_core_model
self.core_inner_diameter: float = core_dimensions.core_inner_diameter
self.window_w: float = core_dimensions.window_w
self.core_thickness: float = self.core_inner_diameter / 4
self.r_inner: float = self.window_w + self.core_inner_diameter / 2
if self.core_type == CoreType.Single:
self.window_h: float = core_dimensions.window_h
self.number_core_windows: int = 2
self.core_h: float = self.window_h + 2 * self.core_thickness
self.core_h_center_leg: float = self.core_h
elif self.core_type == CoreType.Stacked:
self.window_h_bot: float = core_dimensions.window_h_bot
self.window_h_top: float = core_dimensions.window_h_top
self.core_h: float = self.window_h_bot + 2 * self.core_thickness
self.number_core_windows: int = 4
if detailed_core_model:
self.core_h_center_leg: float = core_dimensions.core_h
self.r_outer: float = fr.calculate_r_outer(self.core_inner_diameter, self.window_w)
# Empirical correction based on core measurements
width_meas = 23e-3 # [m]
h_meas = 5.2e-3 # [m]
alpha = np.arcsin((width_meas / 2) / (self.core_inner_diameter / 2 + self.window_w))
h_outer = (h_meas * 4 * alpha * (self.core_inner_diameter / 2 + self.window_w)) / (2 * np.pi * (self.core_inner_diameter / 2 + self.window_w))
self.core_h: float = self.window_w + 2 * h_outer
else:
# set r_outer, so cross-section of outer leg has same cross-section as inner leg
# this is the default-case
self.r_outer: float = fr.calculate_r_outer(self.core_inner_diameter, self.window_w)
def to_dict(self) -> Dict[str, Union[float, CoreType, bool]]:
"""Return a dictionary representation of the core geometry.
Useful for serialization or configuration export.
:return: Dictionary with core geometry parameters.
:rtype: dict
"""
base = {
"core_type": self.core_type,
"core_inner_diameter": self.core_inner_diameter,
"correct_outer_leg": self.correct_outer_leg,
}
if self.core_type == CoreType.Single:
base.update({
"window_w": self.window_w,
"window_h": self.window_h,
"core_h": self.core_h
})
else:
base.update({
"window_w": self.window_w,
"window_h_bot": self.window_h_bot,
"window_h_top": self.window_h_top
})
return base
class LinearComplexCoreMaterial:
"""Encapsulate a magnetic core's material properties based on linear assumptions for simulation.
This includes magnetic permeability, electric permittivity, conductivity, and core loss modeling.
The data can be sourced from measurements, manufacturer datasheets, or set manually.
"""
def __init__(self,
mu_r_abs: float,
phi_mu_deg: float = 0,
dc_conductivity: float = 0,
eps_r_abs: float = 0,
phi_eps_deg: float = 0):
"""Create a CoreMaterial object describing electromagnetic and loss properties.
The class uses material database queries and supports both predefined and custom material configurations.
:param mu_r_abs: Relative permeability for custom materials.
:type mu_r_abs: float
:param phi_mu_deg: Loss angle in degrees for complex permeability.
:type phi_mu_deg: float or None
:param dc_conductivity: Electrical conductivity (only used for custom materials).
:type dc_conductivity: complex or None
"""
self.file_path_to_solver_folder: Optional[str] = None
self.material = 'custom'
self.model_type = CoreMaterialType.Linear
self.loss_approach = LossApproach.LossAngle
self.mu_r_abs = mu_r_abs
self.phi_mu_deg = phi_mu_deg
self.dc_conductivity = dc_conductivity
self.eps_r_abs = eps_r_abs
self.phi_eps_deg = phi_eps_deg
self.complex_permittivity = epsilon_0 * self.eps_r_abs * complex(
np.cos(np.deg2rad(self.phi_eps_deg)),
-np.sin(np.deg2rad(self.phi_eps_deg))
)
self.permeability_type = PermeabilityType.FixedLossAngle
self.permeability = {
"datasource": DataSource.Custom,
"datatype": ComplexDataType.complex_permeability
}
self.permittivity = {
"datasource": DataSource.Custom,
"datatype": ComplexDataType.complex_permittivity
}
def to_dict(self) -> Dict[str, Any]:
"""Return a dictionary representation of the core material.
Useful for serialization or logging.
:return: Dictionary of core material parameters.
:rtype: dict
"""
return {
"material_model_type": self.model_type,
"loss_approach": self.loss_approach.name,
"mu_r_abs": self.mu_r_abs,
"phi_mu_deg": self.phi_mu_deg,
"dc_conductivity": self.dc_conductivity,
"eps_r_abs": self.eps_r_abs,
"phi_eps_deg": self.phi_eps_deg
}
class ImportedComplexCoreMaterial:
"""Encapsulate a magnetic core's material properties based on imported data for simulation.
This includes magnetic permeability, electric permittivity, conductivity, and core loss modeling.
The data can be sourced from measurements, manufacturer datasheets, or set manually.
"""
def __init__(self,
material: Union[str, Material],
temperature: Optional[float],
permeability_datasource: Union[str, DataSource],
permittivity_datasource: Union[str, DataSource]):
"""Create a CoreMaterial object describing electromagnetic and loss properties.
The class uses material database queries and supports both predefined and custom material configurations.
:param material: The name of the core material or a Material object.
:type material: str or Material
:param temperature: Operating temperature in degrees Celsius.
:type temperature: float
:param permeability_datasource: Source of permeability data.
:type permeability_datasource: str or DataSource (from material database)
:param permittivity_datasource: Source of permittivity data.
:type permittivity_datasource: str or DataSource (from material database)
"""
# for class ImportedComplexCoreMaterial, the model_type is fixed to "Imported"
self.model_type = CoreMaterialType.Imported
# path where the material data is handed over to the FEM simulation as a .pro-file
self.file_path_to_solver_folder: Optional[str] = None
# core material database
self.database = mdb.Data()
# name of the material
self.material = Material(material)
# global core temperature
self.temperature = temperature
# general datasheet information
# constant permeability (used in simplified reluctance circuit)
self.mu_r_abs = self.database.get_datasheet_information(material=material,
attribute=DatasheetAttribute.InitialPermeability)
# resistivity
self.resistivity = self.database.get_datasheet_information(material=material,
attribute=DatasheetAttribute.Resistivity)
# density
self.density = self.database.get_datasheet_information(material=material,
attribute=DatasheetAttribute.Density)
# b_sat in T
self.b_sat = self.database.get_datasheet_information(material=material,
attribute=DatasheetAttribute.SaturationFluxDensity100)
# permeability meta information
self.magnetic_loss_approach = LossApproach.LossAngle
self.permeability_type = PermeabilityType.FromData
self.permeability_datasource = permeability_datasource
# ComplexPermeability class from material database
self.permeability = self.database.get_complex_permeability(
material=material,
data_source=permeability_datasource,
pv_fit_function=mdb.FitFunction.enhancedSteinmetz
)
self.permeability.fit_losses()
self.permeability.fit_permeability_magnitude()
# permittivity meta information
self.permittivity_datasource = permittivity_datasource
# initialize the permittivity with the datasheet dc_conductivity, as it will be updated with the
# actual material data at the specific frequency directly before the simulation
self.dc_conductivity = 1 / self.resistivity
self.complex_permittivity = complex(0, 0)
# ComplexPermittivity class from material database (except for datasheet)
if self.permittivity_datasource != DataSource.Datasheet:
self.permittivity = self.database.get_complex_permittivity(material=material,
data_source=permittivity_datasource)
self.permittivity.fit_sigma()
def update_permittivity(self, frequency: float) -> None:
"""Update permittivity and calculate equivalent conductivity at a given frequency.
Uses measurement data if available. Updates internal complex permittivity and conductivity.
:param frequency: Frequency in Hz.
:type frequency: float
"""
if self.permittivity_datasource == DataSource.Datasheet:
self.complex_permittivity = complex(0, 0)
self.dc_conductivity = 1 / self.resistivity
else:
eps_real, eps_imag = self.permittivity.fit_real_and_imaginary_part_at_f_and_T(f=frequency, T=self.temperature)
self.complex_permittivity = epsilon_0 * complex(eps_real, -eps_imag)
self.dc_conductivity = 0
def update_core_material_pro_file(self, frequency: int,
folder: str,
b_ref_vec: npt.NDArray[np.float64] = np.linspace(0, 0.3, 100),
plot_interpolation: bool = False) -> None:
"""Export permeability data to a .pro file for solver compatibility.
:param b_ref_vec:
:param frequency: Operating frequency in Hz.
:type frequency: int
:param folder: Directory path where the .pro file will be saved.
:type folder: str
:param plot_interpolation: If True, plots interpolation of data.
:type plot_interpolation: bool
"""
mu_r_real_vec, mu_r_imag_vec = self.permeability.fit_real_and_imaginary_part_at_f_and_T(
f_op=frequency,
T_op=self.temperature,
b_vals=b_ref_vec
)
write_permeability_pro_file(parent_directory=folder,
b_ref_vec=np.array(b_ref_vec).tolist(),
mu_r_real_vec=np.array(mu_r_real_vec).tolist(),
mu_r_imag_vec=np.array(mu_r_imag_vec).tolist())
def to_dict(self) -> Dict[str, Any]:
"""Return a dictionary representation of the core material.
Useful for serialization or logging.
:return: Dictionary of core material parameters.
:rtype: dict
"""
return {
"material_model_type": self.model_type,
"loss_approach": self.magnetic_loss_approach.name,
"material": self.material,
"temperature": self.temperature,
"permeability_datasource": self.permeability_datasource,
"permeability_datatype": ComplexDataType.complex_permeability,
"permittivity_datasource": self.permittivity_datasource,
"permittivity_datatype": ComplexDataType.complex_permittivity
}
class ElectrostaticCoreMaterial:
"""Defines material properties for electrostatic simulations."""
def __init__(self, eps_r: float):
"""
Define the dielectric constant of the core.
:param eps_r: Relative permittivity of the core material.
:type eps_r: float
"""
self.eps_r = eps_r
def to_dict(self):
"""Return a dictionary representation of the core material.
Useful for serialization or logging.
:return: Dictionary of core material parameters.
:rtype: dict
"""
return {
"eps_r": self.eps_r
}
class Core:
"""Combines geometry and material properties of a magnetic core.
This class acts as a wrapper around both the physical geometry (`CoreGeometry`) and
material properties (`CoreMaterial`) of the magnetic core. Useful for simulations and
solver interfacing.
"""
def __init__(self,
material: ImportedComplexCoreMaterial | LinearComplexCoreMaterial | ElectrostaticCoreMaterial,
core_type: CoreType = CoreType.Single,
core_dimensions: Optional[object] = None,
detailed_core_model: bool = False):
"""
Initialize a Core object with its geometry and material definitions.
:param core_type: Core configuration (Single or Stacked).
:type core_type: CoreType
:param core_dimensions: Object containing core dimensions like window size, diameters.
:type core_dimensions: object
:param detailed_core_model: Whether to model outer leg curvature and center leg in detail.
:type detailed_core_model: bool
:param material: Material name or 'custom' if manually defined.
:type material: str
"""
self.geometry: CoreGeometry = CoreGeometry(core_type, core_dimensions, detailed_core_model)
self.material = material
def update_permittivity(self, frequency: float) -> None:
"""Update permittivity based on a given frequency.
Used when frequency-dependent permittivity modeling is required.
:param frequency: Frequency in Hz.
:type frequency: float
"""
self.material.update_permittivity(frequency)
def update_core_material_pro_file(self, plot_interpolation: bool = False) -> None:
"""Generate or update permeability profile files used by the solver.
:param plot_interpolation: Whether to plot interpolation used for file generation.
:type plot_interpolation: bool
"""
self.material.update_core_material_pro_file(frequency=self.frequency,
folder=self.file_data.electro_magnetic_folder_path,
plot_interpolation=plot_interpolation)
def to_dict(self) -> Dict[str, Union[str, float, bool]]:
"""Return combined dictionary of core geometry and material properties.
:return: Combined dictionary for serialization.
:rtype: dict
"""
return {**self.geometry.to_dict(), **self.material.to_dict()}
class AirGaps:
"""
Contains methods and arguments to describe the air gaps in a magnetic component.
An air gap can be added with the add_air_gap function. It is possible to set different positions and heights.
"""
core: Core
midpoints: list[list[float]] #: list: [position_tag, air_gap_position, air_gap_h]
number: int
# Needed for to_dict
air_gap_settings: list
def __init__(self, method: AirGapMethod | None, core: Core | None):
"""Create an AirGaps object. An AirGapMethod needs to be set.
This determines the way the air gap will be added to the model. In order to calculate the air gap positions the core object needs to be given.
:param method: The method determines the way the air gap position is set.
:type method: AirGapMethod
:param core: The core object
:type core: Core
"""
self.method = method
self.core = core
self.midpoints = []
self.number = 0
self.air_gap_settings = []
def add_air_gap(self, leg_position: AirGapLegPosition, height: float, position_value: float | None = 0,
stacked_position: StackedPosition = None):
"""
Brings a single air gap to the core.
:param leg_position: CenterLeg, OuterLeg
:type leg_position: AirGapLegPosition
:param position_value: if AirGapMethod == Percent: 0...100, elif AirGapMethod == Manually: position height in [m]
:type position_value: float
:param height: Air gap height in [m]
:type height: float
:param stacked_position: Top, Bot
:type stacked_position: StackedPosition
"""
self.air_gap_settings.append({
"leg_position": leg_position.name,
"position_value": position_value,
"height": height,
"stacked_position": stacked_position})
for index, midpoint in enumerate(self.midpoints):
if midpoint[0] == leg_position and midpoint[1] + midpoint[2] < position_value - height \
and midpoint[1] - midpoint[2] > position_value + height:
raise Exception(f"Air gaps {index} and {len(self.midpoints)} are overlapping")
if leg_position == AirGapLegPosition.LeftLeg or leg_position == AirGapLegPosition.RightLeg:
raise Exception("Currently the leg positions LeftLeg and RightLeg are not supported")
if self.method == AirGapMethod.Center:
if self.number >= 1:
raise Exception("The 'center' position for air gaps can only have 1 air gap maximum")
else:
self.midpoints.append([0, 0, height])
self.number += 1
elif self.method == AirGapMethod.Manually:
self.midpoints.append([leg_position.value, position_value, height])
self.number += 1
elif self.method == AirGapMethod.Percent:
if position_value > 100 or position_value < 0:
raise Exception("AirGap position values for the percent method need to be between 0 and 100.")
# Calculate the maximum and minimum position in percent considering the winding window height and air gap length
max_position = 100 - (height / self.core.geometry.window_h) * 51
min_position = (height / self.core.geometry.window_h) * 51
# Adjust the position value if it exceeds the bounds of 0 to 100 percent
if position_value > max_position:
position_value = max_position
elif position_value < min_position:
position_value = min_position
position = position_value / 100 * self.core.geometry.window_h - self.core.geometry.window_h / 2
# # When the position is above the winding window it needs to be adjusted
if position + height / 2 > self.core.geometry.window_h / 2:
position -= (position + height / 2) - self.core.geometry.window_h / 2
elif position - height / 2 < -self.core.geometry.window_h / 2:
position += -self.core.geometry.window_h / 2 - (position - height / 2)
self.midpoints.append([leg_position.value, position, height])
self.number += 1
elif self.method == AirGapMethod.Stacked:
# Error check for air gap height exceeding core section height
if stacked_position == StackedPosition.Top and height > self.core.geometry.window_h_top:
raise ValueError(f"Air gap height ({height} m) exceeds the available top core section height "
f"({self.core.geometry.window_h_top} m).")
elif stacked_position == StackedPosition.Bot and height > self.core.geometry.window_h_bot:
raise ValueError(f"Air gap height ({height} m) exceeds the available bottom core section height "
f"({self.core.geometry.window_h_bot} m).")
# set midpoints
# TODO: handle top and bot
if stacked_position == StackedPosition.Bot:
self.midpoints.append([0, 0, height])
self.number += 1
if stacked_position == StackedPosition.Top:
self.midpoints.append([0, self.core.geometry.window_h_bot / 2 + self.core.geometry.core_thickness + height / 2, height])
self.number += 1
else:
raise Exception(f"Method {self.method} is not supported.")
def to_dict(self):
"""Transfer object parameters to a dictionary. Important method to create the final result-log."""
if self.number == 0:
return {}
content = {
"method": self.method.name,
"air_gap_number": len(self.air_gap_settings)
}
if self.number > 0:
content["air_gaps"] = self.air_gap_settings
return content
class Insulation:
"""
Defines insulation for the model.
An insulation between the winding window and the core can always be set.
When having an inductor only the primary2primary insulation is necessary.
When having a (integrated) transformer secondary2secondary and primary2secondary insulations can be set as well.
Only the isolation between winding window and core is drawn as a "physical" isolation (4 rectangles). All other isolations
are only describing a set distance between the object.
In general, it is not necessary to add an insulation object at all when no insulation is needed.
"""
conductor_type: ConductorType # it is needed here tempoarily
cond_cond: list[list[float]] # two-dimensional list with size NxN, where N is the number of windings (symmetrical isolation matrix)
core_cond: list[float] = None # list with size 4x1, with respectively isolation of cond_n -> [top_core, bot_core, left_core, right_core]
top_section_core_cond: list[float] = None # Top section insulations for integrated transformers, initially None
bot_section_core_cond: list[float] = None # Bottom section insulations for integrated transformers, initially None
turn_ins: list[float] # list of turn insulation of every winding -> [turn_ins_of_winding_1, turn_ins_of_winding_2, ...]
cond_air_cond: list[list[float]] # two-dimensional list with size NxN, where N is the number of windings (symmetrical isolation matrix)
er_turn_insulation: list[float]
er_layer_insulation: float = None
er_bobbin: float = None
bobbin_dimensions: None
thickness_of_layer_insulation: float
consistent_ins: bool = True
draw_insulation_between_layers: bool = True
flag_insulation: bool = True
add_turn_insulations: bool = True
max_aspect_ratio: float
def __init__(self, max_aspect_ratio: float = 10, flag_insulation: bool = True, bobbin_dimensions: None = None):
"""Create an insulation object.
Sets an insulation_delta value. In order to simplify the drawing of the isolations between core and winding window the isolation rectangles
are not exactly drawn at the specified position. They are slightly smaller and the offset can be changed with the insulation_delta variable.
In general, it is not recommended to change this value.
"""
# Default value for all insulations
# If the gaps between insulations and core (or windings) are to big/small just change this value
self.flag_insulation = flag_insulation
self.max_aspect_ratio = max_aspect_ratio
self.bobbin_dimensions = bobbin_dimensions
# As there is a gap between the core and the bobbin, the definition of bobbin parameters is needed in electrostatic simulation
if bobbin_dimensions is not None:
self.bobbin_inner_diameter = bobbin_dimensions.bobbin_inner_diameter
self.bobbin_window_w = bobbin_dimensions.bobbin_window_w
self.bobbin_window_h = bobbin_dimensions.bobbin_window_h
self.bobbin_h = bobbin_dimensions.bobbin_h
self.turn_ins = []
self.thickness_of_layer_insulation = 0.0
def set_flag_insulation(self, flag: bool): # to differentiate between the simulation with and without insulation
"""
Set the self.flag_insulation key.
:param flag: True to enable the insulation
:type flag: bool
"""
self.flag_insulation = flag
def add_winding_insulations(self, inner_winding_insulation: list[list[float]], per_layer_of_turns: bool = False):
"""Add a consistent insulation between turns of one winding and insulation between virtual winding windows.
Insulation between virtual winding windows is not always needed.
:param inner_winding_insulation: List of floats which represent the insulations between turns of the same winding. This does not correspond to
the order conductors are added to the winding! Instead, the winding number is important. The conductors are sorted by ascending winding number.
The lowest winding number therefore is combined with index 0. The second lowest with index 1 and so on.
:type inner_winding_insulation: list[list[float]]
:param per_layer_of_turns: If it is enabled, the insulation will be added between turns for every layer in every winding.
:type per_layer_of_turns: bool.
"""
if inner_winding_insulation == [[]]:
raise Exception("Inner winding insulations list cannot be empty.")
self.cond_cond = inner_winding_insulation
if per_layer_of_turns:
self.consistent_ins = False
else:
self.consistent_ins = True
def add_turn_insulation(self, insulation_thickness: list[float], dielectric_constant: list[float] = None):
"""Add insulation for turns in every winding.
:param insulation_thickness: List of floats which represent the insulation around every winding.
:type insulation_thickness: list[[float]]
:param dielectric_constant: relative permittivity of the insulation of the winding
:type dielectric_constant list[[float]]
"""
# Check for negative values
for t in insulation_thickness:
if t < 0:
raise ValueError(f"Turn insulation thickness must be positive, got {t}")
self.turn_ins = insulation_thickness
self.er_turn_insulation = dielectric_constant or [1.0] * len(insulation_thickness)
def add_insulation_between_layers(self, thickness: float = 0.0, dielectric_constant: float = None):
"""
Add an insulation (thickness_of_layer_insulation or tape insulation) between layers.
:param thickness: the thickness of the insulation between the layers of turns
:type thickness: float
:param dielectric_constant: relative permittivity of the insulation between the layers
:type dielectric_constant: float
"""
if thickness <= 0:
raise ValueError("insulation thickness must be greater than zero.")
else:
self.thickness_of_layer_insulation = thickness
self.er_layer_insulation = dielectric_constant if dielectric_constant is not None else 1.0
def add_core_insulations(self, top_core: float, bot_core: float, left_core: float, right_core: float, dielectric_constant: float = None):
"""Add insulations between the core and the winding window. Creating those will draw real rectangles in the model.
:param top_core: Insulation between winding window and top core
:type top_core: float
:param bot_core: Insulation between winding window and bottom core
:type bot_core: float
:param left_core: Insulation between winding window and left core
:type left_core: float
:param right_core: Insulation between winding window and right core
:type right_core: float
:param dielectric_constant: relative permittivity of the core insulation
:type dielectric_constant: float
"""
if top_core is None:
top_core = 0
if bot_core is None:
bot_core = 0
if left_core is None:
left_core = 0
if right_core is None:
right_core = 0
self.er_bobbin = dielectric_constant if dielectric_constant is not None else 1.0
self.core_cond = [top_core, bot_core, left_core, right_core]
self.core_cond = [top_core, bot_core, left_core, right_core]
def add_top_section_core_insulations(self, top_core: float, bot_core: float, left_core: float, right_core: float, dielectric_constant: float = None):
"""
Add insulations for the top section for integrated transformers.
:param top_core: Insulation between winding window and top section of the top section core
:type top_core: float
:param bot_core: Insulation between winding window and the bottom section of the top section core
:type bot_core: float
:param left_core: Insulation between winding window and left of the top section core
:type left_core: float
:param right_core: Insulation between winding window and right of the top section core
:type right_core: float
:param dielectric_constant: relative permittivity of the core insulation
:type dielectric_constant: float
"""
if top_core is None:
top_core = 0
if bot_core is None:
bot_core = 0
if left_core is None:
left_core = 0
if right_core is None:
right_core = 0
self.er_bobbin = dielectric_constant if dielectric_constant is not None else 1.0
self.top_section_core_cond = [top_core, bot_core, left_core, right_core]
def add_bottom_section_core_insulations(self, top_core: float, bot_core: float, left_core: float, right_core: float, dielectric_constant: float = None):
"""
Add insulations for the top section for integrated transformers.
:param top_core: Insulation between winding window and top section of the bot section core
:type top_core: float
:param bot_core: Insulation between winding window and the bottom section of the bot section core
:type bot_core: float
:param left_core: Insulation between winding window and left of the bot section core
:type left_core: float
:param right_core: Insulation between winding window and right of the bot section core
:type right_core: float
:param dielectric_constant: relative permittivity of the core insulation
:type dielectric_constant: float
"""
if top_core is None:
top_core = 0
if bot_core is None:
bot_core = 0
if left_core is None:
left_core = 0
if right_core is None:
right_core = 0
self.er_bobbin = dielectric_constant if dielectric_constant is not None else 1.0
self.bot_section_core_cond = [top_core, bot_core, left_core, right_core]
def to_dict(self):
"""Transfer object parameters to a dictionary."""
result = {
"inner_winding_insulations": self.cond_cond
}
if self.core_cond:
result["core_insulations"] = self.core_cond
if self.top_section_core_cond:
result["top_section_core_insulations"] = self.top_section_core_cond
if self.bot_section_core_cond:
result["bottom_section_core_insulations"] = self.bot_section_core_cond
return result
@dataclass
class StrayPath:
"""
Stray Path is mandatory when an integrated transformer shall be created.
A start_index and a length can be given. The start_index sets the position of the tablet.
start_index=0 will create the tablet between the lowest and second-lowest air gaps. start_index=1 will create the tablet
between the second lowest and third-lowest air gap. Therefore, it is necessary for the user to make sure that enough air gaps exist!
The length parameter sets the length of the tablet starting at the y-axis (not the right side of the center core). It therefore
determines the air gap between the tablet and the outer core leg.
"""
# TODO: Thickness of the stray path must be fitted for the real Tablet (effective area of the "stray air gap" is different in axi-symmetric approximation)
start_index: int # Air gaps are sorted from lowest to highest. This index refers to the air_gap index bottom up
length: float # Resembles the length of the whole tablet starting from the y-axis
class VirtualWindingWindow:
"""
Create a VirtualWindingWindow.
A virtual winding window is the area, where either some kind of interleaved conductors or a one winding
(primary, secondary,...) is placed in a certain way.
An instance of this class will be automatically created when the Winding is added to the MagneticComponent
"""
# Rectangular frame:
bot_bound: float
top_bound: float
left_bound: float
right_bound: float
winding_type: WindingType
winding_scheme: WindingScheme | InterleavedWindingScheme # Either WindingScheme or InterleavedWindingScheme (Depending on the winding)
wrap_para: WrapParaType
windings: list[Conductor]
turns: list[int]
winding_is_set: bool
winding_insulation: float
def __init__(self, bot_bound: float, top_bound: float, left_bound: float, right_bound: float):
"""Create a virtual winding window with given bounds.
By default, a virtual winding window is created by the WindingWindow class.
The parameter values are given in metres and depend on the axisymmetric coordinate system.
:param bot_bound: Bottom bound
:type bot_bound: float
:param top_bound: Top bound
:type top_bound: float
:param left_bound: Left bound
:type left_bound: float
:param right_bound: Right bound
:type right_bound: float
"""
self.zigzag = None
self.foil_horizontal_placing_strategy = None
self.foil_vertical_placing_strategy = None
self.placing_strategy = None
self.alignment = None
self.bot_bound = bot_bound
self.top_bound = top_bound
self.left_bound = left_bound
self.right_bound = right_bound
self.winding_is_set = False
self.group_size: int | None = None
def set_winding(self, conductor: Conductor, turns: int, winding_scheme: WindingScheme, alignment: Align | None = None,
placing_strategy: ConductorDistribution | None = None, zigzag: bool = False,
wrap_para_type: WrapParaType = None, foil_vertical_placing_strategy: FoilVerticalDistribution | None = None,
foil_horizontal_placing_strategy: FoilHorizontalDistribution | None = None):
"""Set a single winding to the current virtual winding window. A single winding always contains one conductor.
:param conductor: Conductor which will be set to the vww.
:type conductor: Conductor
:param turns: Number of turns of the conductor
:type turns: int
:param winding_scheme: Winding scheme defines the way the conductor is wrapped. Can be set to None.
:type winding_scheme: WindingScheme
:param placing_strategy: Placing strategy defines the way the conductors are placing in vww
:type placing_strategy: ConductorPlacingStrategy, optional
:param zigzag: Zigzag movement for conductors
:type placing_strategy: bool, define to False
:param wrap_para_type: Additional wrap parameter. Not always needed, defaults to None
:type wrap_para_type: WrapParaType, optional
:param foil_vertical_placing_strategy: foil_vertical_placing_strategy defines the way the rectangular foil vertical conductors are placing in vww