|
| 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 |
0 commit comments