Skip to content

Commit 0004b2c

Browse files
authored
Merge pull request #49 from AI-RICE/45-create-3f-5f-60s
issue45, create 3f_60s and 5f_60s
2 parents 648b0dd + 0931cc1 commit 0004b2c

12 files changed

Lines changed: 432 additions & 17 deletions

‎machine_design/designs/computation.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from ansys.aedt.core import Maxwell2d
44

5+
from machine_design.generic.compute_initpos import compute_initpos
6+
57
from .geometry import GeometryBase
68
from .reserved_variables import check_no_reserved_variable_names
79

@@ -41,6 +43,12 @@ def set_variables(self, m2d: Maxwell2d, *args): ...
4143
@abstractmethod
4244
def extract_results(self, solutions): ...
4345

46+
def set_initpos(self) -> None:
47+
Q = int(self.geometry.geom_params["SlotNumber"])
48+
p = self.geometry.PolePairs
49+
self.InitPos = compute_initpos(Q, p, self.phases, belt_offset=self.belt_offset)
50+
self.oper_params["InitPos"] = f"{self.InitPos}deg"
51+
4452
def push_variables(self, m2d: Maxwell2d) -> None:
4553
for k, v in self.oper_params.items():
4654
m2d[k] = v

‎machine_design/generic/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
from .compute_initpos import compute_initpos
12
from .transforms import electrical_angle, to_dq, to_phases
3+
from .winding import coil_name, phase_groups, sector_winding
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import math
2+
3+
from machine_design.generic.winding import sector_winding
4+
5+
6+
def compute_initpos(Q: int, p: int, m: int, belt_offset: int):
7+
"""This function aims to compute the value of InitPos for those machines with d-axis
8+
at 45deg.
9+
Override this method if a different rotor generator places the d-axis elsewhere."""
10+
11+
out = sector_winding(Q, p, m, belt_offset)
12+
xa = ya = 0.0
13+
for k, (phase_index, sign) in enumerate(out):
14+
if phase_index != 0: # only need phase A
15+
continue
16+
theta_e_deg = (0.5 + k) * (360.0 * p / Q)
17+
if sign > 0:
18+
eff_deg = theta_e_deg
19+
else:
20+
eff_deg = theta_e_deg + 180.0
21+
xa += math.cos(math.radians(eff_deg))
22+
ya += math.sin(math.radians(eff_deg))
23+
gamma_e_deg = math.degrees(math.atan2(ya, xa))
24+
return round((gamma_e_deg - 90.0) / p, 6)

‎machine_design/generic/winding.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Parametric stator winding-layout generator (pure algorithm, no AEDT).
2+
3+
Produces the coil -> (phase, polarity) map for the one-pole, anti-periodic FEA
4+
sector our machine models, for an arbitrary balanced integer-slot distributed
5+
winding given (Q slots, p pole-pairs, m phases). It replaces the hardcoded
6+
per-coil assignment in Design.assign_stator_coils / Design2.assign_stator_coils.
7+
8+
Convention (reverse-engineered from, and validated to exactly reproduce, the
9+
project's 40-slot/5-phase and 36-slot/3-phase windings):
10+
- the FEA sector spans ONE pole, n = Q/(2p) slots = coils, with anti-periodic BC;
11+
- q = Q/(2*p*m) slots per phase belt (must be a positive integer);
12+
- coils are created in slot order: coil 0 = "Coil", coil k = "Coil_{k}";
13+
- belt index of coil k: b = (k + 1) // q (raw, half-slot-offset origin)
14+
- phase index: ((m+1)//2 * b) mod m (standard (m+1)/2 progression)
15+
- polarity: +1 if b even else -1 (alternating belts; the wrap
16+
coil flips via anti-periodicity)
17+
"""
18+
19+
20+
def coil_name(k: int) -> str:
21+
"""AEDT object name for the k-th duplicated coil (0-based)."""
22+
return "Coil" if k == 0 else f"Coil_{k}"
23+
24+
25+
def sector_winding(Q: int, p: int, m: int, belt_offset: int = 1):
26+
"""Return a list (length n = Q/(2p)) of (phase_index 0..m-1, sign +/-1),
27+
one entry per sector coil in slot/creation order.
28+
29+
belt_offset shifts the belt origin (a pure spatial rotation of the winding,
30+
immaterial to performance, absorbed by the rotor-alignment / current angle):
31+
- belt_offset=1 -> PhaseA centred on the d-axis with an anti-periodic wrap;
32+
reproduces the project's 5-phase (40-slot) winding. Default / preferred.
33+
- belt_offset=0 -> belts start at slot 0; reproduces the legacy 3-phase
34+
(36-slot) base winding.
35+
New machines should use the default (1) and align the dq reference to it."""
36+
if Q % (2 * p * m) != 0:
37+
raise ValueError(f"non-integer slots/pole/phase: Q={Q}, 2*p*m={2 * p * m} (q=Q/(2pm) must be a positive integer for this winding)")
38+
n = Q // (2 * p) # coils in the one-pole sector
39+
q = Q // (2 * p * m) # slots per belt
40+
step = (m + 1) // 2
41+
out = []
42+
for k in range(n):
43+
b = (k + belt_offset) // q
44+
out.append(((step * b) % m, 1 if b % 2 == 0 else -1))
45+
return out
46+
47+
48+
def phase_groups(Q: int, p: int, m: int, belt_offset: int = 1):
49+
"""Return m lists; phase j -> [(coil_name, polarity_str), ...] for driving
50+
assign_coil / add_winding_coils generically."""
51+
groups = [[] for _ in range(m)]
52+
for k, (ph, sg) in enumerate(sector_winding(Q, p, m, belt_offset)):
53+
groups[ph].append((coil_name(k), "Positive" if sg > 0 else "Negative"))
54+
return groups
55+
56+
57+
if __name__ == "__main__":
58+
PHASE = "ABCDEFG"
59+
60+
# --- validation: must reproduce the existing hardcoded windings exactly ---
61+
def asmap(Q, p, m, off):
62+
g = {}
63+
for k, (ph, sg) in enumerate(sector_winding(Q, p, m, off)):
64+
g.setdefault(PHASE[ph], set()).add((k, sg))
65+
return g
66+
67+
EXPECT_5 = { # design2.assign_stator_coils (CS_i -> Coil_{i-1} -> slot i-1)
68+
"A": {(0, 1), (9, -1)},
69+
"B": {(3, 1), (4, 1)},
70+
"C": {(7, 1), (8, 1)},
71+
"D": {(1, -1), (2, -1)},
72+
"E": {(5, -1), (6, -1)},
73+
}
74+
EXPECT_3 = { # design.assign_stator_coils (PhaseA=CS1-3, B=CS7-9, C=CS4-6)
75+
"A": {(0, 1), (1, 1), (2, 1)},
76+
"B": {(6, 1), (7, 1), (8, 1)},
77+
"C": {(3, -1), (4, -1), (5, -1)},
78+
}
79+
assert asmap(40, 2, 5, 1) == EXPECT_5, "5-phase (offset 1) mismatch"
80+
assert asmap(36, 2, 3, 0) == EXPECT_3, "3-phase (offset 0) mismatch"
81+
print("OK reproduces BOTH hardcoded windings exactly: 5-phase 40-slot (offset 1), 3-phase 36-slot (offset 0)")
82+
83+
def show(Q, p, m):
84+
lay = sector_winding(Q, p, m)
85+
bins = {}
86+
for k, (ph, sg) in enumerate(lay):
87+
bins.setdefault(PHASE[ph], []).append(f"{coil_name(k)}{'+' if sg > 0 else '-'}")
88+
q = Q // (2 * p * m)
89+
print(f"\n(Q={Q}, p={p}, m={m}) sector coils={len(lay)} q={q}")
90+
for ph in sorted(bins):
91+
print(f" Phase {ph}: {bins[ph]}")
92+
93+
show(40, 2, 5) # current 5-phase
94+
show(36, 2, 3) # current 3-phase
95+
show(60, 2, 3) # common-stator target, 3-phase
96+
show(60, 2, 5) # common-stator target, 5-phase

‎motors/synrm_3f_36s.py‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,22 @@ def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None:
172172

173173
class Computation(ComputationBase):
174174
def set_oper_params(self):
175+
self.phases = 3
176+
self.belt_offset = 0
175177
f = 50 # [Hz]
176178
RotSpeed = 60 * f / self.geometry.PolePairs # [rpm]
177179
w = 2 * np.pi * f
178-
self.InitPos = -30 # deg
179180
self.oper_params = {
180181
"Id": "0.0A",
181182
"Iq": "0.0A",
182183
"epsI": "atan2(Iq,Id)", # current angle
183184
"Im": "sqrt(Id^2+Iq^2)",
184-
"InitPos": f"{self.InitPos}deg",
185185
"w": f"{w}Hz",
186186
"RotSpeed": f"{RotSpeed}rpm",
187187
"Nper": "1/6", # number of included periods
188188
"PointPer": "101", # number of time points per period
189189
}
190+
self.set_initpos()
190191

191192
def set_solution_expressions(self):
192193
self.solution_expressions = [
@@ -211,10 +212,6 @@ def set_post_params(self):
211212
"FluxLinkage(PhaseB)",
212213
"FluxLinkage(PhaseC)",
213214
): "FluxLinkage",
214-
("I_d", "I_q"): "Current_dq",
215-
("Flux_d", "Flux_q"): "FluxLinkage_dq",
216-
("Ui_d", "Ui_q"): "InducedVoltage_dq",
217-
("L_d", "L_q"): "Inductance_dq",
218215
}
219216

220217
def assign_stator_coils(self, m2d: Maxwell2d) -> None:

‎motors/synrm_3f_60s.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Anchor `synrm_3f_60s`: 3-phase synchronous reluctance machine, 60 stator slots."""
2+
3+
import numpy as np
4+
from ansys.aedt.core import Maxwell2d
5+
6+
from machine_design.generic.winding import phase_groups
7+
8+
from .synrm_3f_36s import Computation as BaseComputation
9+
from .synrm_3f_36s import Geometry as BaseGeometry
10+
11+
12+
class Geometry(BaseGeometry):
13+
def set_geom_params(self):
14+
super().set_geom_params()
15+
self.geom_params["SlotNumber"] = "60"
16+
17+
def set_slot_params(self):
18+
super().set_slot_params()
19+
self.slot_params["Bs0"] = "1.5mm"
20+
self.slot_params["Bs1"] = "2.0mm"
21+
self.slot_params["Bs2"] = "2.85mm"
22+
self.slot_params["Rs"] = "1.0mm"
23+
self.slot_params["SetAngle"] = "6deg"
24+
25+
def set_winds_params(self):
26+
super().set_winds_params()
27+
self.wind_params["CoilPitch"] = "15" # full pitch over one pole (60/4)
28+
29+
30+
class Computation(BaseComputation):
31+
def set_solution_expressions(self):
32+
self.solution_expressions = ["Moving1.Torque"]
33+
34+
def assign_stator_coils(self, m2d: Maxwell2d) -> None:
35+
# Excitations
36+
I_A = "Im * cos(w*Time+epsI)"
37+
I_B = "Im * cos(w*Time-120deg+epsI)"
38+
I_C = "Im * cos(w*Time-240deg+epsI)"
39+
40+
Q = int(self.geometry.geom_params["SlotNumber"])
41+
p = self.geometry.PolePairs
42+
# belt_offset=0 reproduces the legacy 3-phase(36-slot) base winding.
43+
groups = phase_groups(Q, p, 3, belt_offset=0)
44+
45+
for group in groups:
46+
for coil_name, polarity in group:
47+
m2d.assign_coil(assignment=[coil_name], conductors_number="Nc", polarity=polarity, name=f"CS_{coil_name}")
48+
49+
for phase_name, current, group in zip("ABC", [I_A, I_B, I_C], groups):
50+
m2d.assign_winding(
51+
assignment=None,
52+
winding_type="Current",
53+
is_solid=False,
54+
current=current,
55+
parallel_branches="ParallelPaths",
56+
name=f"Phase{phase_name}",
57+
)
58+
m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=[f"CS_{coil_name}" for coil_name, _ in group])
59+
60+
def extract_results(self, solutions):
61+
return {"Moving1.Torque": np.array(solutions.data_real("Moving1.Torque"))}

‎motors/synrm_5f_40s.py‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ def set_winds_params(self):
3131

3232
class Computation(ComputationBase):
3333
def set_oper_params(self):
34+
self.phases = 5
35+
self.belt_offset = 1
3436
f = 50 # [Hz]
3537
RotSpeed = 60 * f / self.geometry.PolePairs # [rpm]
3638
w = 2 * np.pi * f
37-
self.InitPos = -45 # deg
3839
self.RotSign = 1
3940
self.Rstat = 19.0
4041
self.Lew = 0.0
@@ -48,12 +49,12 @@ def set_oper_params(self):
4849
"epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic
4950
"Im1": "sqrt(Id1^2+Iq1^2)",
5051
"Im3": "sqrt(Id3^2+Iq3^2)",
51-
"InitPos": f"{self.InitPos}deg",
5252
"w": f"{w}Hz",
5353
"RotSpeed": f"{RotSpeed}rpm",
5454
"Nper": "1/10", # number of included periods
5555
"PointPer": "101", # number of time points per period
5656
}
57+
self.set_initpos()
5758

5859
def set_solution_expressions(self):
5960
self.solution_expressions = [
@@ -92,13 +93,6 @@ def set_post_params(self):
9293
"FluxLinkage(PhaseD)",
9394
"FluxLinkage(PhaseE)",
9495
): "FluxLinkage",
95-
("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq",
96-
("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq",
97-
("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq",
98-
("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq",
99-
("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq",
100-
("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main",
101-
("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling",
10296
}
10397

10498
def assign_stator_coils(self, m2d: Maxwell2d) -> None:

‎motors/synrm_5f_60s.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Anchor `synrm_5f_60s`: 5-phase synchronous reluctance machine, 60 stator slots.
2+
3+
Derives from `synrm_5f_40s`, overriding only the slotting and winding it needs for five
4+
phases. Excitation is dq1 + dq3.
5+
"""
6+
7+
import numpy as np
8+
from ansys.aedt.core import Maxwell2d
9+
10+
from machine_design.generic.winding import phase_groups
11+
12+
from .synrm_5f_40s import Computation as BaseComputation
13+
from .synrm_5f_40s import Geometry as BaseGeometry
14+
15+
16+
class Geometry(BaseGeometry):
17+
def set_geom_params(self):
18+
super().set_geom_params()
19+
self.geom_params["SlotNumber"] = "60"
20+
21+
def set_slot_params(self):
22+
super().set_slot_params()
23+
self.slot_params["Bs0"] = "1.5mm"
24+
self.slot_params["Bs1"] = "2.0mm"
25+
self.slot_params["Bs2"] = "2.85mm"
26+
self.slot_params["Rs"] = "1.0mm"
27+
self.slot_params["SetAngle"] = "6deg"
28+
29+
def set_winds_params(self):
30+
super().set_winds_params()
31+
self.wind_params["CoilPitch"] = "15"
32+
33+
34+
class Computation(BaseComputation):
35+
def set_solution_expressions(self):
36+
self.solution_expressions = ["Moving1.Torque"]
37+
38+
def assign_stator_coils(self, m2d: Maxwell2d) -> None:
39+
# Excitations
40+
I_A = "Im1*cos(w*Time+epsI1-pi) + Im3*cos(3*(w*Time)+epsI3-pi)"
41+
I_B = "Im1*cos(w*Time-72deg+epsI1-pi) + Im3*cos(3*(w*Time-72deg)+epsI3-pi)"
42+
I_C = "Im1*cos(w*Time-144deg+epsI1-pi) + Im3*cos(3*(w*Time-144deg)+epsI3-pi)"
43+
I_D = "Im1*cos(w*Time-216deg+epsI1-pi) + Im3*cos(3*(w*Time-216deg)+epsI3-pi)"
44+
I_E = "Im1*cos(w*Time-288deg+epsI1-pi) + Im3*cos(3*(w*Time-288deg)+epsI3-pi)"
45+
46+
Q = int(self.geometry.geom_params["SlotNumber"])
47+
p = self.geometry.PolePairs
48+
# belt_offset=1 reproduces the 5-phase(40-slot) base winding.
49+
groups = phase_groups(Q, p, 5, belt_offset=1)
50+
51+
for group in groups:
52+
for coil_name, polarity in group:
53+
m2d.assign_coil(assignment=[coil_name], conductors_number="Nc", polarity=polarity, name=f"CS_{coil_name}")
54+
55+
for phase_name, current, group in zip("ABCDE", [I_A, I_B, I_C, I_D, I_E], groups):
56+
m2d.assign_winding(
57+
assignment=None,
58+
winding_type="Current",
59+
is_solid=False,
60+
current=current,
61+
parallel_branches="ParallelPaths",
62+
name=f"Phase{phase_name}",
63+
)
64+
m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=[f"CS_{coil_name}" for coil_name, _ in group])
65+
66+
def extract_results(self, solutions):
67+
return {"Moving1.Torque": np.array(solutions.data_real("Moving1.Torque"))}

0 commit comments

Comments
 (0)