Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c936467
copy-paste winding.py from vs_5f and create synrm_3f_60s.py
haitao-ZCU Sep 21, 2026
04a9bd8
adjust import order
haitao-ZCU Sep 21, 2026
81d193c
create 5f_60s
haitao-ZCU Sep 21, 2026
6f831ce
update 5f_60s
haitao-ZCU Sep 21, 2026
fa24cae
update set_post_params()
haitao-ZCU Sep 21, 2026
1df0ab8
create test file with ansys
haitao-ZCU Sep 21, 2026
f57f69e
update currents
haitao-ZCU Sep 21, 2026
4dc9913
ruff format
haitao-ZCU Sep 21, 2026
77696cc
import computation from 26s and 40s, delete inductance_computation()
haitao-ZCU Sep 22, 2026
c39ff42
ruff format
haitao-ZCU Sep 22, 2026
79ccc2f
Merge remote-tracking branch 'origin/main' into 45-create-3f-5f-60s
haitao-ZCU Sep 22, 2026
4528158
Merge remote-tracking branch 'origin/main' into 45-create-3f-5f-60s
haitao-ZCU Sep 22, 2026
0bdfd09
move winding.py to generic/
haitao-ZCU Sep 22, 2026
c689da4
simpilfy computation part
haitao-ZCU Sep 22, 2026
75ff35e
update 3f-60s, 5f-60s, and test file
haitao-ZCU Sep 29, 2026
22691f5
Merge remote-tracking branch 'origin/main' into 45-create-3f-5f-60s
haitao-ZCU Sep 29, 2026
c3ac7a3
computr InitPos function
haitao-ZCU Sep 29, 2026
7a57cfc
add description
haitao-ZCU Sep 29, 2026
424ad40
update gamma_e_deg
haitao-ZCU Sep 29, 2026
eca62e0
use compute initpos() to calculate InitPos for 3f_60s machine
haitao-ZCU Sep 29, 2026
5cf21ea
use compute initpos() to calculate InitPos for 5f_60s machine
haitao-ZCU Sep 29, 2026
ddab2f1
add compute initpos
haitao-ZCU Sep 29, 2026
dc9e2b1
ruff format
haitao-ZCU Sep 29, 2026
5098274
add set_initpos() for 3f_36s
haitao-ZCU Sep 29, 2026
22eeb32
add set_initpos() for 5f_40s
haitao-ZCU Sep 29, 2026
d49d709
delete set_oper_params()
haitao-ZCU Sep 29, 2026
e8987fa
delete set_oper_params()
haitao-ZCU Sep 29, 2026
d166e59
units consistency
haitao-ZCU Sep 29, 2026
2024075
delete unnecessay import
haitao-ZCU Sep 29, 2026
a524975
udate corresponding test file
haitao-ZCU Sep 29, 2026
b243076
print each vars to check units
haitao-ZCU Sep 29, 2026
aa8c5dd
ruff format
haitao-ZCU Sep 29, 2026
d528d6a
simplify set_post_params()
haitao-ZCU Sep 29, 2026
e192436
simplify set_post_params()
haitao-ZCU Sep 29, 2026
0d8f8d1
delete set_post_params()
haitao-ZCU Sep 29, 2026
1380f20
convergence
haitao-ZCU Sep 29, 2026
a61e9d9
revert units fix
haitao-ZCU Sep 30, 2026
76e8397
delete the function to force Nc be equal and update currents ratio to…
haitao-ZCU Sep 30, 2026
8a691ad
update convergence range to 5%
haitao-ZCU Sep 30, 2026
573674d
move set_initpos to abstract parent class
haitao-ZCU Sep 30, 2026
abdd4e3
set phases and belt for 3f
haitao-ZCU Sep 30, 2026
9edca4d
set phases and belt_offset for 5f and delete compute_initpos()
haitao-ZCU Sep 30, 2026
287646d
ruff format
haitao-ZCU Sep 30, 2026
0931cc1
update import order
haitao-ZCU Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions machine_design/designs/computation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from ansys.aedt.core import Maxwell2d

from machine_design.generic.compute_initpos import compute_initpos

from .geometry import GeometryBase
from .reserved_variables import check_no_reserved_variable_names

Expand Down Expand Up @@ -41,6 +43,12 @@ def set_variables(self, m2d: Maxwell2d, *args): ...
@abstractmethod
def extract_results(self, solutions): ...

def set_initpos(self) -> None:
Q = int(self.geometry.geom_params["SlotNumber"])
p = self.geometry.PolePairs
self.InitPos = compute_initpos(Q, p, self.phases, belt_offset=self.belt_offset)
self.oper_params["InitPos"] = f"{self.InitPos}deg"

def push_variables(self, m2d: Maxwell2d) -> None:
for k, v in self.oper_params.items():
m2d[k] = v
Expand Down
2 changes: 2 additions & 0 deletions machine_design/generic/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .compute_initpos import compute_initpos
from .transforms import electrical_angle, to_dq, to_phases
from .winding import coil_name, phase_groups, sector_winding
24 changes: 24 additions & 0 deletions machine_design/generic/compute_initpos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import math

from machine_design.generic.winding import sector_winding


def compute_initpos(Q: int, p: int, m: int, belt_offset: int):
"""This function aims to compute the value of InitPos for those machines with d-axis
at 45deg.
Override this method if a different rotor generator places the d-axis elsewhere."""

out = sector_winding(Q, p, m, belt_offset)
xa = ya = 0.0
for k, (phase_index, sign) in enumerate(out):
if phase_index != 0: # only need phase A
continue
theta_e_deg = (0.5 + k) * (360.0 * p / Q)
if sign > 0:
eff_deg = theta_e_deg
else:
eff_deg = theta_e_deg + 180.0
xa += math.cos(math.radians(eff_deg))
ya += math.sin(math.radians(eff_deg))
gamma_e_deg = math.degrees(math.atan2(ya, xa))
return round((gamma_e_deg - 90.0) / p, 6)
96 changes: 96 additions & 0 deletions machine_design/generic/winding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Parametric stator winding-layout generator (pure algorithm, no AEDT).

Produces the coil -> (phase, polarity) map for the one-pole, anti-periodic FEA
sector our machine models, for an arbitrary balanced integer-slot distributed
winding given (Q slots, p pole-pairs, m phases). It replaces the hardcoded
per-coil assignment in Design.assign_stator_coils / Design2.assign_stator_coils.

Convention (reverse-engineered from, and validated to exactly reproduce, the
project's 40-slot/5-phase and 36-slot/3-phase windings):
- the FEA sector spans ONE pole, n = Q/(2p) slots = coils, with anti-periodic BC;
- q = Q/(2*p*m) slots per phase belt (must be a positive integer);
- coils are created in slot order: coil 0 = "Coil", coil k = "Coil_{k}";
- belt index of coil k: b = (k + 1) // q (raw, half-slot-offset origin)
- phase index: ((m+1)//2 * b) mod m (standard (m+1)/2 progression)
- polarity: +1 if b even else -1 (alternating belts; the wrap
coil flips via anti-periodicity)
"""


def coil_name(k: int) -> str:
"""AEDT object name for the k-th duplicated coil (0-based)."""
return "Coil" if k == 0 else f"Coil_{k}"


def sector_winding(Q: int, p: int, m: int, belt_offset: int = 1):
"""Return a list (length n = Q/(2p)) of (phase_index 0..m-1, sign +/-1),
one entry per sector coil in slot/creation order.

belt_offset shifts the belt origin (a pure spatial rotation of the winding,
immaterial to performance, absorbed by the rotor-alignment / current angle):
- belt_offset=1 -> PhaseA centred on the d-axis with an anti-periodic wrap;
reproduces the project's 5-phase (40-slot) winding. Default / preferred.
- belt_offset=0 -> belts start at slot 0; reproduces the legacy 3-phase
(36-slot) base winding.
New machines should use the default (1) and align the dq reference to it."""
if Q % (2 * p * m) != 0:
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)")
n = Q // (2 * p) # coils in the one-pole sector
q = Q // (2 * p * m) # slots per belt
step = (m + 1) // 2
out = []
for k in range(n):
b = (k + belt_offset) // q
out.append(((step * b) % m, 1 if b % 2 == 0 else -1))
return out


def phase_groups(Q: int, p: int, m: int, belt_offset: int = 1):
"""Return m lists; phase j -> [(coil_name, polarity_str), ...] for driving
assign_coil / add_winding_coils generically."""
groups = [[] for _ in range(m)]
for k, (ph, sg) in enumerate(sector_winding(Q, p, m, belt_offset)):
groups[ph].append((coil_name(k), "Positive" if sg > 0 else "Negative"))
return groups


if __name__ == "__main__":
PHASE = "ABCDEFG"

# --- validation: must reproduce the existing hardcoded windings exactly ---
def asmap(Q, p, m, off):
g = {}
for k, (ph, sg) in enumerate(sector_winding(Q, p, m, off)):
g.setdefault(PHASE[ph], set()).add((k, sg))
return g

EXPECT_5 = { # design2.assign_stator_coils (CS_i -> Coil_{i-1} -> slot i-1)
"A": {(0, 1), (9, -1)},
"B": {(3, 1), (4, 1)},
"C": {(7, 1), (8, 1)},
"D": {(1, -1), (2, -1)},
"E": {(5, -1), (6, -1)},
}
EXPECT_3 = { # design.assign_stator_coils (PhaseA=CS1-3, B=CS7-9, C=CS4-6)
"A": {(0, 1), (1, 1), (2, 1)},
"B": {(6, 1), (7, 1), (8, 1)},
"C": {(3, -1), (4, -1), (5, -1)},
}
assert asmap(40, 2, 5, 1) == EXPECT_5, "5-phase (offset 1) mismatch"
assert asmap(36, 2, 3, 0) == EXPECT_3, "3-phase (offset 0) mismatch"
print("OK reproduces BOTH hardcoded windings exactly: 5-phase 40-slot (offset 1), 3-phase 36-slot (offset 0)")

def show(Q, p, m):
lay = sector_winding(Q, p, m)
bins = {}
for k, (ph, sg) in enumerate(lay):
bins.setdefault(PHASE[ph], []).append(f"{coil_name(k)}{'+' if sg > 0 else '-'}")
q = Q // (2 * p * m)
print(f"\n(Q={Q}, p={p}, m={m}) sector coils={len(lay)} q={q}")
for ph in sorted(bins):
print(f" Phase {ph}: {bins[ph]}")

show(40, 2, 5) # current 5-phase
show(36, 2, 3) # current 3-phase
show(60, 2, 3) # common-stator target, 3-phase
show(60, 2, 5) # common-stator target, 5-phase
9 changes: 3 additions & 6 deletions motors/synrm_3f_36s.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,22 @@ def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None:

class Computation(ComputationBase):
def set_oper_params(self):
self.phases = 3
self.belt_offset = 0
f = 50 # [Hz]
RotSpeed = 60 * f / self.geometry.PolePairs # [rpm]
w = 2 * np.pi * f
self.InitPos = -30 # deg
self.oper_params = {
"Id": "0.0A",
"Iq": "0.0A",
"epsI": "atan2(Iq,Id)", # current angle
"Im": "sqrt(Id^2+Iq^2)",
"InitPos": f"{self.InitPos}deg",
"w": f"{w}Hz",
"RotSpeed": f"{RotSpeed}rpm",
"Nper": "1/6", # number of included periods
"PointPer": "101", # number of time points per period
}
self.set_initpos()

def set_solution_expressions(self):
self.solution_expressions = [
Expand All @@ -211,10 +212,6 @@ def set_post_params(self):
"FluxLinkage(PhaseB)",
"FluxLinkage(PhaseC)",
): "FluxLinkage",
("I_d", "I_q"): "Current_dq",
("Flux_d", "Flux_q"): "FluxLinkage_dq",
("Ui_d", "Ui_q"): "InducedVoltage_dq",
("L_d", "L_q"): "Inductance_dq",
}

def assign_stator_coils(self, m2d: Maxwell2d) -> None:
Expand Down
61 changes: 61 additions & 0 deletions motors/synrm_3f_60s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Anchor `synrm_3f_60s`: 3-phase synchronous reluctance machine, 60 stator slots."""

import numpy as np
from ansys.aedt.core import Maxwell2d

from machine_design.generic.winding import phase_groups

from .synrm_3f_36s import Computation as BaseComputation
from .synrm_3f_36s import Geometry as BaseGeometry


class Geometry(BaseGeometry):
def set_geom_params(self):
super().set_geom_params()
self.geom_params["SlotNumber"] = "60"

def set_slot_params(self):
super().set_slot_params()
self.slot_params["Bs0"] = "1.5mm"
self.slot_params["Bs1"] = "2.0mm"
self.slot_params["Bs2"] = "2.85mm"
self.slot_params["Rs"] = "1.0mm"
self.slot_params["SetAngle"] = "6deg"

def set_winds_params(self):
super().set_winds_params()
self.wind_params["CoilPitch"] = "15" # full pitch over one pole (60/4)


class Computation(BaseComputation):
def set_solution_expressions(self):
self.solution_expressions = ["Moving1.Torque"]

def assign_stator_coils(self, m2d: Maxwell2d) -> None:
# Excitations
I_A = "Im * cos(w*Time+epsI)"
I_B = "Im * cos(w*Time-120deg+epsI)"
I_C = "Im * cos(w*Time-240deg+epsI)"

Q = int(self.geometry.geom_params["SlotNumber"])
p = self.geometry.PolePairs
# belt_offset=0 reproduces the legacy 3-phase(36-slot) base winding.
groups = phase_groups(Q, p, 3, belt_offset=0)

for group in groups:
for coil_name, polarity in group:
m2d.assign_coil(assignment=[coil_name], conductors_number="Nc", polarity=polarity, name=f"CS_{coil_name}")

for phase_name, current, group in zip("ABC", [I_A, I_B, I_C], groups):
m2d.assign_winding(
assignment=None,
winding_type="Current",
is_solid=False,
current=current,
parallel_branches="ParallelPaths",
name=f"Phase{phase_name}",
)
m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=[f"CS_{coil_name}" for coil_name, _ in group])

def extract_results(self, solutions):
return {"Moving1.Torque": np.array(solutions.data_real("Moving1.Torque"))}
12 changes: 3 additions & 9 deletions motors/synrm_5f_40s.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ def set_winds_params(self):

class Computation(ComputationBase):
def set_oper_params(self):
self.phases = 5
self.belt_offset = 1
f = 50 # [Hz]
RotSpeed = 60 * f / self.geometry.PolePairs # [rpm]
w = 2 * np.pi * f
self.InitPos = -45 # deg
self.RotSign = 1
self.Rstat = 19.0
self.Lew = 0.0
Expand All @@ -48,12 +49,12 @@ def set_oper_params(self):
"epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic
"Im1": "sqrt(Id1^2+Iq1^2)",
"Im3": "sqrt(Id3^2+Iq3^2)",
"InitPos": f"{self.InitPos}deg",
"w": f"{w}Hz",
"RotSpeed": f"{RotSpeed}rpm",
"Nper": "1/10", # number of included periods
"PointPer": "101", # number of time points per period
}
self.set_initpos()

def set_solution_expressions(self):
self.solution_expressions = [
Expand Down Expand Up @@ -92,13 +93,6 @@ def set_post_params(self):
"FluxLinkage(PhaseD)",
"FluxLinkage(PhaseE)",
): "FluxLinkage",
("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq",
("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq",
("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq",
("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq",
("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq",
("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main",
("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling",
}

def assign_stator_coils(self, m2d: Maxwell2d) -> None:
Expand Down
67 changes: 67 additions & 0 deletions motors/synrm_5f_60s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Anchor `synrm_5f_60s`: 5-phase synchronous reluctance machine, 60 stator slots.

Derives from `synrm_5f_40s`, overriding only the slotting and winding it needs for five
phases. Excitation is dq1 + dq3.
"""

import numpy as np
from ansys.aedt.core import Maxwell2d

from machine_design.generic.winding import phase_groups

from .synrm_5f_40s import Computation as BaseComputation
from .synrm_5f_40s import Geometry as BaseGeometry


class Geometry(BaseGeometry):
def set_geom_params(self):
super().set_geom_params()
self.geom_params["SlotNumber"] = "60"

def set_slot_params(self):
super().set_slot_params()
self.slot_params["Bs0"] = "1.5mm"
self.slot_params["Bs1"] = "2.0mm"
self.slot_params["Bs2"] = "2.85mm"
self.slot_params["Rs"] = "1.0mm"
self.slot_params["SetAngle"] = "6deg"

def set_winds_params(self):
super().set_winds_params()
self.wind_params["CoilPitch"] = "15"


class Computation(BaseComputation):
def set_solution_expressions(self):
self.solution_expressions = ["Moving1.Torque"]

def assign_stator_coils(self, m2d: Maxwell2d) -> None:
# Excitations
I_A = "Im1*cos(w*Time+epsI1-pi) + Im3*cos(3*(w*Time)+epsI3-pi)"
I_B = "Im1*cos(w*Time-72deg+epsI1-pi) + Im3*cos(3*(w*Time-72deg)+epsI3-pi)"
I_C = "Im1*cos(w*Time-144deg+epsI1-pi) + Im3*cos(3*(w*Time-144deg)+epsI3-pi)"
I_D = "Im1*cos(w*Time-216deg+epsI1-pi) + Im3*cos(3*(w*Time-216deg)+epsI3-pi)"
I_E = "Im1*cos(w*Time-288deg+epsI1-pi) + Im3*cos(3*(w*Time-288deg)+epsI3-pi)"

Q = int(self.geometry.geom_params["SlotNumber"])
p = self.geometry.PolePairs
# belt_offset=1 reproduces the 5-phase(40-slot) base winding.
groups = phase_groups(Q, p, 5, belt_offset=1)

for group in groups:
for coil_name, polarity in group:
m2d.assign_coil(assignment=[coil_name], conductors_number="Nc", polarity=polarity, name=f"CS_{coil_name}")

for phase_name, current, group in zip("ABCDE", [I_A, I_B, I_C, I_D, I_E], groups):
m2d.assign_winding(
assignment=None,
winding_type="Current",
is_solid=False,
current=current,
parallel_branches="ParallelPaths",
name=f"Phase{phase_name}",
)
m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=[f"CS_{coil_name}" for coil_name, _ in group])

def extract_results(self, solutions):
return {"Moving1.Torque": np.array(solutions.data_real("Moving1.Torque"))}
Loading
Loading