emachines 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emachines-0.1.0/.gitignore +41 -0
- emachines-0.1.0/CHANGELOG.md +28 -0
- emachines-0.1.0/LICENSE +21 -0
- emachines-0.1.0/PKG-INFO +96 -0
- emachines-0.1.0/README.md +61 -0
- emachines-0.1.0/emachines/__init__.py +4 -0
- emachines-0.1.0/emachines/magnetics/__init__.py +1 -0
- emachines-0.1.0/emachines/magnetics/bh_models.py +76 -0
- emachines-0.1.0/emachines/magnetics/iron_loss.py +97 -0
- emachines-0.1.0/emachines/motors/__init__.py +1 -0
- emachines-0.1.0/emachines/motors/pmsm.py +143 -0
- emachines-0.1.0/emachines/winding/__init__.py +1 -0
- emachines-0.1.0/emachines/winding/factors.py +105 -0
- emachines-0.1.0/pyproject.toml +57 -0
- emachines-0.1.0/settings.ini +22 -0
- emachines-0.1.0/tests/__init__.py +0 -0
- emachines-0.1.0/tests/test_iron_loss.py +32 -0
- emachines-0.1.0/tests/test_pmsm.py +59 -0
- emachines-0.1.0/tests/test_winding.py +75 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
*.egg
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Jupyter
|
|
14
|
+
.ipynb_checkpoints/
|
|
15
|
+
*.ipynb_checkpoints
|
|
16
|
+
|
|
17
|
+
# nbdev
|
|
18
|
+
/_docs/
|
|
19
|
+
/docs/_proc/
|
|
20
|
+
|
|
21
|
+
# Testing
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
25
|
+
|
|
26
|
+
# Distribution
|
|
27
|
+
dist/
|
|
28
|
+
*.whl
|
|
29
|
+
*.tar.gz
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
35
|
+
|
|
36
|
+
# macOS
|
|
37
|
+
.DS_Store
|
|
38
|
+
|
|
39
|
+
# Secrets
|
|
40
|
+
.env
|
|
41
|
+
*.env
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `emachines` will be documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
|
+
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
- FSCW winding factors via star-of-slots phasor method (migration from emdesigner)
|
|
9
|
+
- `emachines.winding.sos`: star-of-slots module
|
|
10
|
+
- `emachines.winding.mmf`: MMF harmonic spectrum
|
|
11
|
+
- PyPI registration
|
|
12
|
+
|
|
13
|
+
## [0.1.0] — 2026-05-07
|
|
14
|
+
### Added
|
|
15
|
+
- `emachines.winding.factors`: pitch factor (kp), distribution factor (kd), winding factor (kw)
|
|
16
|
+
- Integer-slot windings (q ≥ 1) fully supported
|
|
17
|
+
- FSCW (q < 1) raises ValueError with clear message — star-of-slots pending
|
|
18
|
+
- `emachines.magnetics.bh_models`: Fröhlich analytical BH model and curve fitting
|
|
19
|
+
- `emachines.magnetics.iron_loss`: Steinmetz and Bertotti three-term separation models
|
|
20
|
+
- `emachines.motors.pmsm`: PMSMParams dataclass, back-EMF, torque (excitation + reluctance), steady-state dq current solver
|
|
21
|
+
- 18 passing tests, 4 xfailed (FSCW — by design, pending star-of-slots migration)
|
|
22
|
+
- `pyproject.toml`: hatchling build, PyPI metadata, Python ≥ 3.9
|
|
23
|
+
- `settings.ini`: nbdev-ready configuration
|
|
24
|
+
- MIT License
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Corrected GitHub username in project URLs (`vnaveendeepak` → `NaveenDeepak`)
|
|
28
|
+
- Lowered minimum Python from 3.10 to 3.9 to match emdesigner Docker environment
|
emachines-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ND
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
emachines-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emachines
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Analytical electromechanical machine design library
|
|
5
|
+
Project-URL: Homepage, https://emdesignlabs.com
|
|
6
|
+
Project-URL: Repository, https://github.com/NaveenDeepak/emachines
|
|
7
|
+
Project-URL: Documentation, https://NaveenDeepak.github.io/emachines
|
|
8
|
+
Author-email: ND <NaveenDeepak@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: BLDC,MMF,PMSM,electric motor,electromagnetic,electromechanical,motor design,winding,winding factor
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: scipy>=1.10
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: jupyter; extra == 'dev'
|
|
28
|
+
Requires-Dist: nbdev; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: nbdev; extra == 'docs'
|
|
33
|
+
Requires-Dist: quarto; extra == 'docs'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# emachines
|
|
37
|
+
|
|
38
|
+
**Analytical electromechanical machine design library.**
|
|
39
|
+
|
|
40
|
+
`emachines` is the core physics and mathematics engine behind [emdesignlabs.com](https://emdesignlabs.com). It provides well-documented, tested, and citable implementations of the analytical models used in electric motor design.
|
|
41
|
+
|
|
42
|
+
## Scope
|
|
43
|
+
|
|
44
|
+
- **Winding analysis** — winding factors (kp, kd, kw), MMF harmonic spectra, slot/pole geometry
|
|
45
|
+
- **Magnetics** — BH curve models (Fröhlich, Jiles-Atherton), iron loss (Steinmetz, Bertotti)
|
|
46
|
+
- **Motor models** — PMSM dq-frame, DC motor, surface-PM analytical design
|
|
47
|
+
|
|
48
|
+
Every function documents the equation it implements and its bibliographic source.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install emachines
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For development (editable install alongside emdesignlabs):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install -e path/to/emachines
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from emachines.winding.factors import winding_factor
|
|
66
|
+
from emachines.magnetics.iron_loss import bertotti
|
|
67
|
+
from emachines.motors.pmsm import PMSMParams, torque
|
|
68
|
+
|
|
69
|
+
# Winding factor for 12s/10p, fundamental harmonic
|
|
70
|
+
kw1 = winding_factor(nu=1, Q=12, p=5, coil_span=1)
|
|
71
|
+
print(f"kw1 = {kw1:.4f}") # → 0.9330
|
|
72
|
+
|
|
73
|
+
# Bertotti iron loss at 400 Hz, 1.5 T
|
|
74
|
+
loss = bertotti(f=400, B_peak=1.5, k_h=0.02, k_e=1e-4, k_a=1e-3)
|
|
75
|
+
print(f"Total loss = {loss['total']:.2f} W/kg")
|
|
76
|
+
|
|
77
|
+
# PMSM torque from dq currents
|
|
78
|
+
motor = PMSMParams(p=3, Ld=5e-3, Lq=8e-3, psi_m=0.15)
|
|
79
|
+
Te = torque(motor, id=-3.0, iq=10.0)
|
|
80
|
+
print(f"Torque = {Te:.2f} N·m")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Design Philosophy
|
|
84
|
+
|
|
85
|
+
- **Equations first** — every function documents the formula it implements with LaTeX notation
|
|
86
|
+
- **Cited** — every formula traces back to a specific reference (textbook, paper, standard)
|
|
87
|
+
- **Tested** — validated against published datasets and reference tools (emetor, SWAT-EM)
|
|
88
|
+
- **Dependency-light** — only numpy and scipy required
|
|
89
|
+
|
|
90
|
+
## Status
|
|
91
|
+
|
|
92
|
+
`0.1.x` — Alpha. API may change between minor versions.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# emachines
|
|
2
|
+
|
|
3
|
+
**Analytical electromechanical machine design library.**
|
|
4
|
+
|
|
5
|
+
`emachines` is the core physics and mathematics engine behind [emdesignlabs.com](https://emdesignlabs.com). It provides well-documented, tested, and citable implementations of the analytical models used in electric motor design.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
- **Winding analysis** — winding factors (kp, kd, kw), MMF harmonic spectra, slot/pole geometry
|
|
10
|
+
- **Magnetics** — BH curve models (Fröhlich, Jiles-Atherton), iron loss (Steinmetz, Bertotti)
|
|
11
|
+
- **Motor models** — PMSM dq-frame, DC motor, surface-PM analytical design
|
|
12
|
+
|
|
13
|
+
Every function documents the equation it implements and its bibliographic source.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install emachines
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For development (editable install alongside emdesignlabs):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e path/to/emachines
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from emachines.winding.factors import winding_factor
|
|
31
|
+
from emachines.magnetics.iron_loss import bertotti
|
|
32
|
+
from emachines.motors.pmsm import PMSMParams, torque
|
|
33
|
+
|
|
34
|
+
# Winding factor for 12s/10p, fundamental harmonic
|
|
35
|
+
kw1 = winding_factor(nu=1, Q=12, p=5, coil_span=1)
|
|
36
|
+
print(f"kw1 = {kw1:.4f}") # → 0.9330
|
|
37
|
+
|
|
38
|
+
# Bertotti iron loss at 400 Hz, 1.5 T
|
|
39
|
+
loss = bertotti(f=400, B_peak=1.5, k_h=0.02, k_e=1e-4, k_a=1e-3)
|
|
40
|
+
print(f"Total loss = {loss['total']:.2f} W/kg")
|
|
41
|
+
|
|
42
|
+
# PMSM torque from dq currents
|
|
43
|
+
motor = PMSMParams(p=3, Ld=5e-3, Lq=8e-3, psi_m=0.15)
|
|
44
|
+
Te = torque(motor, id=-3.0, iq=10.0)
|
|
45
|
+
print(f"Torque = {Te:.2f} N·m")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Design Philosophy
|
|
49
|
+
|
|
50
|
+
- **Equations first** — every function documents the formula it implements with LaTeX notation
|
|
51
|
+
- **Cited** — every formula traces back to a specific reference (textbook, paper, standard)
|
|
52
|
+
- **Tested** — validated against published datasets and reference tools (emetor, SWAT-EM)
|
|
53
|
+
- **Dependency-light** — only numpy and scipy required
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
`0.1.x` — Alpha. API may change between minor versions.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Magnetics: BH curve models, iron loss, PM demagnetization."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BH curve models for soft magnetic materials.
|
|
3
|
+
|
|
4
|
+
Provides analytical fits to measured BH data and standard
|
|
5
|
+
approximation models used in motor design.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
Jiles, D.C., Atherton, D.L. (1986). Theory of ferromagnetic hysteresis.
|
|
9
|
+
Journal of Magnetism and Magnetic Materials, 61(1-2), 48-60.
|
|
10
|
+
Frölhich, O. (1881). Investigations of dynamoelectric machines.
|
|
11
|
+
Elektrotechnische Zeitschrift.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy.optimize import curve_fit
|
|
16
|
+
|
|
17
|
+
__all__ = ["frolich", "fit_frolich", "linear_region"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def frolich(H: np.ndarray, a: float, b: float) -> np.ndarray:
|
|
21
|
+
r"""
|
|
22
|
+
Fröhlich-Kennelly analytical BH approximation.
|
|
23
|
+
|
|
24
|
+
.. math::
|
|
25
|
+
B = \frac{H}{a + b \cdot |H|}
|
|
26
|
+
|
|
27
|
+
Simple two-parameter model. Accurate for moderate flux densities.
|
|
28
|
+
Breaks down near and above saturation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
H: Magnetic field intensity (A/m), array-like
|
|
32
|
+
a: Model parameter (dimensionless)
|
|
33
|
+
b: Model parameter (m/A)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Flux density B (T)
|
|
37
|
+
|
|
38
|
+
References:
|
|
39
|
+
Fröhlich (1881); see also Hanselman (2003), §3.2
|
|
40
|
+
"""
|
|
41
|
+
H = np.asarray(H, dtype=float)
|
|
42
|
+
return H / (a + b * np.abs(H))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fit_frolich(H: np.ndarray, B: np.ndarray) -> tuple[float, float]:
|
|
46
|
+
"""
|
|
47
|
+
Fit Fröhlich model parameters (a, b) to measured BH data.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
H: Measured field intensity values (A/m)
|
|
51
|
+
B: Measured flux density values (T)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Tuple (a, b) of fitted parameters
|
|
55
|
+
"""
|
|
56
|
+
H = np.asarray(H, dtype=float)
|
|
57
|
+
B = np.asarray(B, dtype=float)
|
|
58
|
+
(a, b), _ = curve_fit(frolich, H, B, p0=[1.0, 1.0], maxfev=5000)
|
|
59
|
+
return float(a), float(b)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def linear_region(H: np.ndarray, mu_r: float) -> np.ndarray:
|
|
63
|
+
r"""
|
|
64
|
+
Linear BH approximation: B = μ₀ · μr · H
|
|
65
|
+
|
|
66
|
+
Valid for low flux densities well below saturation.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
H: Magnetic field intensity (A/m)
|
|
70
|
+
mu_r: Relative permeability
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Flux density B (T)
|
|
74
|
+
"""
|
|
75
|
+
MU_0 = 4 * np.pi * 1e-7
|
|
76
|
+
return MU_0 * mu_r * np.asarray(H, dtype=float)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Iron (core) loss models.
|
|
3
|
+
|
|
4
|
+
Covers the classical Steinmetz equation, the improved generalized
|
|
5
|
+
Steinmetz equation (iGSE), and Bertotti's three-term separation model.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
Steinmetz, C.P. (1892). On the law of hysteresis.
|
|
9
|
+
AIEE Transactions, 9, 3-64.
|
|
10
|
+
Bertotti, G. (1988). General properties of power losses in soft
|
|
11
|
+
ferromagnetic materials. IEEE Trans. Magnetics, 24(1), 621-630.
|
|
12
|
+
Venkatachalam, K. et al. (2002). Accurate prediction of ferrite core
|
|
13
|
+
loss with nonsinusoidal waveforms. COMPEL, 21(4).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
__all__ = ["steinmetz", "bertotti"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def steinmetz(
|
|
22
|
+
f: float,
|
|
23
|
+
B_peak: float,
|
|
24
|
+
k_h: float,
|
|
25
|
+
alpha: float,
|
|
26
|
+
beta: float,
|
|
27
|
+
) -> float:
|
|
28
|
+
r"""
|
|
29
|
+
Classical Steinmetz core loss model (per unit mass).
|
|
30
|
+
|
|
31
|
+
.. math::
|
|
32
|
+
P_{core} = k_h \cdot f^{\alpha} \cdot \hat{B}^{\beta}
|
|
33
|
+
|
|
34
|
+
Valid for sinusoidal excitation. Parameters k_h, α, β are
|
|
35
|
+
material-specific and typically obtained by curve fitting to
|
|
36
|
+
manufacturer loss data.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
f: Electrical frequency (Hz)
|
|
40
|
+
B_peak: Peak flux density (T)
|
|
41
|
+
k_h: Hysteresis loss coefficient
|
|
42
|
+
alpha: Frequency exponent (typically 1.0–1.6)
|
|
43
|
+
beta: Flux density exponent (typically 1.6–2.2)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Core loss power density (W/kg)
|
|
47
|
+
|
|
48
|
+
References:
|
|
49
|
+
Steinmetz (1892); Bertotti (1988)
|
|
50
|
+
"""
|
|
51
|
+
return k_h * (f ** alpha) * (B_peak ** beta)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def bertotti(
|
|
55
|
+
f: float,
|
|
56
|
+
B_peak: float,
|
|
57
|
+
k_h: float,
|
|
58
|
+
k_e: float,
|
|
59
|
+
k_a: float,
|
|
60
|
+
alpha: float = 1.0,
|
|
61
|
+
beta: float = 2.0,
|
|
62
|
+
) -> dict[str, float]:
|
|
63
|
+
r"""
|
|
64
|
+
Bertotti three-term iron loss separation model (per unit mass).
|
|
65
|
+
|
|
66
|
+
Separates total loss into hysteresis, classical eddy current,
|
|
67
|
+
and excess (anomalous) loss components:
|
|
68
|
+
|
|
69
|
+
.. math::
|
|
70
|
+
P_{total} = \underbrace{k_h f \hat{B}^{\beta}}_{hysteresis}
|
|
71
|
+
+ \underbrace{k_e f^2 \hat{B}^2}_{eddy}
|
|
72
|
+
+ \underbrace{k_a f^{1.5} \hat{B}^{1.5}}_{excess}
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
f: Electrical frequency (Hz)
|
|
76
|
+
B_peak: Peak flux density (T)
|
|
77
|
+
k_h: Hysteresis loss coefficient (W·s/kg)
|
|
78
|
+
k_e: Eddy current loss coefficient (W·s²/kg)
|
|
79
|
+
k_a: Excess loss coefficient (W·s^1.5/kg)
|
|
80
|
+
alpha: Frequency exponent for hysteresis (default 1.0)
|
|
81
|
+
beta: Flux density exponent for hysteresis (default 2.0)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dict with keys: 'hysteresis', 'eddy', 'excess', 'total' (W/kg)
|
|
85
|
+
|
|
86
|
+
References:
|
|
87
|
+
Bertotti (1988), eq. 6
|
|
88
|
+
"""
|
|
89
|
+
p_hyst = k_h * (f ** alpha) * (B_peak ** beta)
|
|
90
|
+
p_eddy = k_e * (f ** 2) * (B_peak ** 2)
|
|
91
|
+
p_exc = k_a * (f ** 1.5) * (B_peak ** 1.5)
|
|
92
|
+
return {
|
|
93
|
+
"hysteresis": p_hyst,
|
|
94
|
+
"eddy": p_eddy,
|
|
95
|
+
"excess": p_exc,
|
|
96
|
+
"total": p_hyst + p_eddy + p_exc,
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Motor models: PMSM dq-model, DC motor, SPM."""
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permanent Magnet Synchronous Machine (PMSM) — dq-frame model.
|
|
3
|
+
|
|
4
|
+
Covers steady-state torque, power, back-EMF, and per-unit
|
|
5
|
+
dq-axis equations for surface-mount (SPM) and interior (IPM) variants.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
Hanselman, D.C. (2003). Brushless Permanent Magnet Motor Design. 2nd ed.
|
|
9
|
+
Mohan, N. (2011). Advanced Electric Drives. Wiley.
|
|
10
|
+
Pyrhönen, J. et al. (2008). Design of Rotating Electrical Machines.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
__all__ = ["back_emf", "torque", "dq_currents", "PMSMParams"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PMSMParams:
|
|
19
|
+
"""
|
|
20
|
+
Container for PMSM parameters.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
p: Number of pole pairs
|
|
24
|
+
Ld: d-axis inductance (H)
|
|
25
|
+
Lq: q-axis inductance (H)
|
|
26
|
+
psi_m: Permanent magnet flux linkage (Wb)
|
|
27
|
+
Rs: Stator resistance per phase (Ω)
|
|
28
|
+
J: Rotor inertia (kg·m²)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
p: int,
|
|
34
|
+
Ld: float,
|
|
35
|
+
Lq: float,
|
|
36
|
+
psi_m: float,
|
|
37
|
+
Rs: float = 0.0,
|
|
38
|
+
J: float = 0.0,
|
|
39
|
+
):
|
|
40
|
+
self.p = p
|
|
41
|
+
self.Ld = Ld
|
|
42
|
+
self.Lq = Lq
|
|
43
|
+
self.psi_m = psi_m
|
|
44
|
+
self.Rs = Rs
|
|
45
|
+
self.J = J
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_spm(self) -> bool:
|
|
49
|
+
"""True if surface-mount (Ld ≈ Lq, no reluctance torque)."""
|
|
50
|
+
return np.isclose(self.Ld, self.Lq, rtol=0.05)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def back_emf(omega_e: float, psi_m: float) -> float:
|
|
54
|
+
r"""
|
|
55
|
+
Peak phase back-EMF for a PMSM.
|
|
56
|
+
|
|
57
|
+
.. math::
|
|
58
|
+
E = \omega_e \cdot \psi_m
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
omega_e: Electrical angular velocity (rad/s)
|
|
62
|
+
psi_m: PM flux linkage (Wb)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Peak back-EMF (V)
|
|
66
|
+
|
|
67
|
+
References:
|
|
68
|
+
Hanselman (2003), eq. 7.3
|
|
69
|
+
"""
|
|
70
|
+
return omega_e * psi_m
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def torque(
|
|
74
|
+
params: PMSMParams,
|
|
75
|
+
id: float,
|
|
76
|
+
iq: float,
|
|
77
|
+
) -> float:
|
|
78
|
+
r"""
|
|
79
|
+
Electromagnetic torque from dq-frame currents.
|
|
80
|
+
|
|
81
|
+
.. math::
|
|
82
|
+
T_e = \frac{3}{2} p \left[\psi_m i_q + (L_d - L_q) i_d i_q \right]
|
|
83
|
+
|
|
84
|
+
First term: excitation (alignment) torque.
|
|
85
|
+
Second term: reluctance torque (zero for SPM where Ld = Lq).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
params: PMSMParams instance
|
|
89
|
+
id: d-axis current (A)
|
|
90
|
+
iq: q-axis current (A)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Electromagnetic torque Te (N·m)
|
|
94
|
+
|
|
95
|
+
References:
|
|
96
|
+
Hanselman (2003), eq. 7.10; Mohan (2011), §5.4
|
|
97
|
+
"""
|
|
98
|
+
T_excitation = params.psi_m * iq
|
|
99
|
+
T_reluctance = (params.Ld - params.Lq) * id * iq
|
|
100
|
+
return 1.5 * params.p * (T_excitation + T_reluctance)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def dq_currents(
|
|
104
|
+
params: PMSMParams,
|
|
105
|
+
omega_e: float,
|
|
106
|
+
Vd: float,
|
|
107
|
+
Vq: float,
|
|
108
|
+
) -> tuple[float, float]:
|
|
109
|
+
r"""
|
|
110
|
+
Steady-state dq currents from applied dq voltages.
|
|
111
|
+
|
|
112
|
+
Solves the steady-state voltage equations:
|
|
113
|
+
|
|
114
|
+
.. math::
|
|
115
|
+
V_d = R_s i_d - \omega_e L_q i_q
|
|
116
|
+
|
|
117
|
+
.. math::
|
|
118
|
+
V_q = R_s i_q + \omega_e (L_d i_d + \psi_m)
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
params: PMSMParams instance
|
|
122
|
+
omega_e: Electrical angular velocity (rad/s)
|
|
123
|
+
Vd: d-axis voltage (V)
|
|
124
|
+
Vq: q-axis voltage (V)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple (id, iq) in amperes
|
|
128
|
+
|
|
129
|
+
References:
|
|
130
|
+
Pyrhönen et al. (2008), §7.3
|
|
131
|
+
"""
|
|
132
|
+
Rs = params.Rs
|
|
133
|
+
Ld = params.Ld
|
|
134
|
+
Lq = params.Lq
|
|
135
|
+
psi_m = params.psi_m
|
|
136
|
+
we = omega_e
|
|
137
|
+
|
|
138
|
+
# 2x2 linear system: [Rs, -we*Lq; we*Ld, Rs] · [id; iq] = [Vd; Vq - we*psi_m]
|
|
139
|
+
A = np.array([[Rs, -we * Lq],
|
|
140
|
+
[we * Ld, Rs]])
|
|
141
|
+
b = np.array([Vd, Vq - we * psi_m])
|
|
142
|
+
id_, iq_ = np.linalg.solve(A, b)
|
|
143
|
+
return float(id_), float(iq_)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Winding analysis: factors, MMF harmonics, slot/pole geometry."""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Winding factor calculations.
|
|
3
|
+
|
|
4
|
+
Covers pitch factor (kp), distribution factor (kd), and combined
|
|
5
|
+
winding factor kw = kp · kd for the ν-th harmonic.
|
|
6
|
+
|
|
7
|
+
Current scope:
|
|
8
|
+
- Integer-slot windings (q ≥ 1): full support
|
|
9
|
+
- Fractional-slot concentrated windings (FSCW, q < 1):
|
|
10
|
+
requires the star-of-slots method — to be migrated from emdesigner.
|
|
11
|
+
Attempting to use distribution_factor() with q < 1 raises ValueError.
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Hanselman, D.C. (2003). Brushless Permanent Magnet Motor Design. 2nd ed.
|
|
15
|
+
Pyrhönen, J., Jokinen, T., Hrabovcová, V. (2008). Design of Rotating Electrical Machines.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
__all__ = ["pitch_factor", "distribution_factor", "winding_factor"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pitch_factor(nu: int, coil_span: int, pole_pitch: float) -> float:
|
|
24
|
+
r"""
|
|
25
|
+
Pitch (chording) factor kp for the ν-th harmonic.
|
|
26
|
+
|
|
27
|
+
.. math::
|
|
28
|
+
k_{p\nu} = \sin\!\left(\nu \cdot \frac{\pi}{2} \cdot \frac{y}{\tau_p}\right)
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
nu: Harmonic order ν (1 = fundamental)
|
|
32
|
+
coil_span: Coil span in slots (y)
|
|
33
|
+
pole_pitch: Pole pitch in slots (τp = Q / P)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Pitch factor kp ∈ [0, 1]
|
|
37
|
+
|
|
38
|
+
References:
|
|
39
|
+
Hanselman (2003), eq. 4.5
|
|
40
|
+
"""
|
|
41
|
+
return float(np.abs(np.sin(nu * np.pi / 2 * coil_span / pole_pitch)))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def distribution_factor(nu: int, Q: int, p: int, m: int = 3) -> float:
|
|
45
|
+
r"""
|
|
46
|
+
Distribution (belt) factor kd for the ν-th harmonic.
|
|
47
|
+
|
|
48
|
+
Valid for integer-slot windings only (q = Q/(2mp) ≥ 1).
|
|
49
|
+
For FSCW (q < 1), use the star-of-slots phasor method instead
|
|
50
|
+
(not yet implemented — tracked for migration from emdesigner).
|
|
51
|
+
|
|
52
|
+
.. math::
|
|
53
|
+
k_{d\nu} = \frac{\sin(\nu \cdot \pi / (2m))}{q \cdot \sin(\nu \cdot \pi / (2mq))}
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
nu: Harmonic order ν
|
|
57
|
+
Q: Total number of slots
|
|
58
|
+
p: Number of pole pairs
|
|
59
|
+
m: Number of phases (default 3)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Distribution factor kd ∈ (0, 1]
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If q < 1 (FSCW — use star-of-slots method instead)
|
|
66
|
+
|
|
67
|
+
References:
|
|
68
|
+
Pyrhönen et al. (2008), eq. 2.15
|
|
69
|
+
"""
|
|
70
|
+
q = Q / (2 * m * p)
|
|
71
|
+
if q < 1.0 - 1e-9:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"distribution_factor() requires q ≥ 1 (integer-slot winding). "
|
|
74
|
+
f"Got q = {q:.4f} (Q={Q}, p={p}, m={m}). "
|
|
75
|
+
f"For FSCW use the star-of-slots method (coming soon in emachines.winding.sos)."
|
|
76
|
+
)
|
|
77
|
+
num = np.sin(nu * np.pi / (2 * m))
|
|
78
|
+
den = q * np.sin(nu * np.pi / (2 * m * q))
|
|
79
|
+
if np.abs(den) < 1e-12:
|
|
80
|
+
return 1.0
|
|
81
|
+
return float(np.abs(num / den))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def winding_factor(nu: int, Q: int, p: int, coil_span: int, m: int = 3) -> float:
|
|
85
|
+
r"""
|
|
86
|
+
Combined winding factor kw = kp · kd for the ν-th harmonic.
|
|
87
|
+
|
|
88
|
+
Integer-slot windings only. For FSCW (Q/(2mp) < 1) this raises
|
|
89
|
+
ValueError — the star-of-slots method is needed and will be
|
|
90
|
+
provided in emachines.winding.sos once migrated from emdesigner.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
nu: Harmonic order ν (1 = fundamental)
|
|
94
|
+
Q: Total number of slots
|
|
95
|
+
p: Number of pole pairs
|
|
96
|
+
coil_span: Coil span in slots
|
|
97
|
+
m: Number of phases (default 3)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Winding factor kw ∈ [0, 1]
|
|
101
|
+
"""
|
|
102
|
+
pole_pitch = Q / (2 * p)
|
|
103
|
+
kp = pitch_factor(nu, coil_span, pole_pitch)
|
|
104
|
+
kd = distribution_factor(nu, Q, p, m)
|
|
105
|
+
return kp * kd
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "emachines"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Analytical electromechanical machine design library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ND", email = "NaveenDeepak@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"electric motor", "PMSM", "BLDC", "winding", "electromagnetic",
|
|
15
|
+
"electromechanical", "motor design", "winding factor", "MMF"
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"numpy>=1.24",
|
|
32
|
+
"scipy>=1.10",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest",
|
|
38
|
+
"pytest-cov",
|
|
39
|
+
"nbdev",
|
|
40
|
+
"jupyter",
|
|
41
|
+
]
|
|
42
|
+
docs = [
|
|
43
|
+
"nbdev",
|
|
44
|
+
"quarto",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://emdesignlabs.com"
|
|
49
|
+
Repository = "https://github.com/NaveenDeepak/emachines"
|
|
50
|
+
Documentation = "https://NaveenDeepak.github.io/emachines"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["emachines"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
addopts = "--tb=short"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[DEFAULT]
|
|
2
|
+
# nbdev settings
|
|
3
|
+
lib_name = emachines
|
|
4
|
+
user = NaveenDeepak
|
|
5
|
+
author = ND
|
|
6
|
+
author_email = NaveenDeepak@gmail.com
|
|
7
|
+
description = Analytical electromechanical machine design library
|
|
8
|
+
keywords = electric motor PMSM BLDC winding electromagnetic electromechanical
|
|
9
|
+
license = apache2
|
|
10
|
+
copyright = ND
|
|
11
|
+
branch = main
|
|
12
|
+
version = 0.1.0
|
|
13
|
+
min_python = 3.9
|
|
14
|
+
audience = Science/Research
|
|
15
|
+
language = English
|
|
16
|
+
custom_sidebar = false
|
|
17
|
+
nbs_path = nbs
|
|
18
|
+
lib_path = emachines
|
|
19
|
+
doc_path = docs
|
|
20
|
+
doc_host = https://NaveenDeepak.github.io
|
|
21
|
+
doc_baseurl = /emachines
|
|
22
|
+
git_url = https://github.com/NaveenDeepak/emachines
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tests for iron loss models."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from emachines.magnetics.iron_loss import steinmetz, bertotti
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_steinmetz_scales_with_frequency():
|
|
8
|
+
"""Loss increases with frequency."""
|
|
9
|
+
p1 = steinmetz(50, 1.0, k_h=0.01, alpha=1.5, beta=2.0)
|
|
10
|
+
p2 = steinmetz(100, 1.0, k_h=0.01, alpha=1.5, beta=2.0)
|
|
11
|
+
assert p2 > p1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_steinmetz_scales_with_flux():
|
|
15
|
+
"""Loss increases with flux density."""
|
|
16
|
+
p1 = steinmetz(50, 1.0, k_h=0.01, alpha=1.5, beta=2.0)
|
|
17
|
+
p2 = steinmetz(50, 1.5, k_h=0.01, alpha=1.5, beta=2.0)
|
|
18
|
+
assert p2 > p1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_bertotti_total_equals_sum_of_components():
|
|
22
|
+
"""Total loss = hysteresis + eddy + excess."""
|
|
23
|
+
result = bertotti(50, 1.5, k_h=0.02, k_e=1e-4, k_a=1e-3)
|
|
24
|
+
expected = result["hysteresis"] + result["eddy"] + result["excess"]
|
|
25
|
+
assert np.isclose(result["total"], expected)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_bertotti_all_components_positive():
|
|
29
|
+
result = bertotti(400, 1.0, k_h=0.01, k_e=5e-5, k_a=5e-4)
|
|
30
|
+
assert result["hysteresis"] > 0
|
|
31
|
+
assert result["eddy"] > 0
|
|
32
|
+
assert result["excess"] > 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Tests for PMSM dq-model."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
from emachines.motors.pmsm import PMSMParams, back_emf, torque, dq_currents
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def spm():
|
|
10
|
+
"""Typical SPM parameters (Ld = Lq)."""
|
|
11
|
+
return PMSMParams(p=3, Ld=5e-3, Lq=5e-3, psi_m=0.15, Rs=0.5)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def ipm():
|
|
16
|
+
"""Typical IPM parameters (Ld < Lq)."""
|
|
17
|
+
return PMSMParams(p=4, Ld=3e-3, Lq=8e-3, psi_m=0.12, Rs=0.3)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_back_emf(spm):
|
|
21
|
+
"""E = omega_e * psi_m"""
|
|
22
|
+
omega_e = 2 * np.pi * 50 * spm.p # electrical rad/s at 50 Hz mechanical
|
|
23
|
+
E = back_emf(omega_e, spm.psi_m)
|
|
24
|
+
assert E > 0
|
|
25
|
+
assert np.isclose(E, omega_e * spm.psi_m)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_spm_torque_no_reluctance(spm):
|
|
29
|
+
"""SPM: reluctance torque = 0, Te depends only on iq."""
|
|
30
|
+
Te_1 = torque(spm, id=0.0, iq=10.0)
|
|
31
|
+
Te_2 = torque(spm, id=-5.0, iq=10.0)
|
|
32
|
+
# For SPM (Ld=Lq), id has no effect on torque
|
|
33
|
+
assert np.isclose(Te_1, Te_2, atol=1e-10)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_ipm_reluctance_torque(ipm):
|
|
37
|
+
"""IPM: negative id adds reluctance torque (MTPA region)."""
|
|
38
|
+
Te_id0 = torque(ipm, id=0.0, iq=10.0)
|
|
39
|
+
Te_mtpa = torque(ipm, id=-5.0, iq=10.0)
|
|
40
|
+
assert Te_mtpa > Te_id0 # reluctance torque boosts total torque
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_torque_sign(spm):
|
|
44
|
+
"""Positive iq → positive torque (motoring)."""
|
|
45
|
+
assert torque(spm, id=0.0, iq=5.0) > 0
|
|
46
|
+
assert torque(spm, id=0.0, iq=-5.0) < 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_dq_currents_steady_state(spm):
|
|
50
|
+
"""Steady-state currents should satisfy voltage equations."""
|
|
51
|
+
omega_e = 2 * np.pi * 100.0
|
|
52
|
+
Vd, Vq = 0.0, 100.0
|
|
53
|
+
id_, iq_ = dq_currents(spm, omega_e, Vd, Vq)
|
|
54
|
+
|
|
55
|
+
# Verify voltage equations
|
|
56
|
+
Vd_check = spm.Rs * id_ - omega_e * spm.Lq * iq_
|
|
57
|
+
Vq_check = spm.Rs * iq_ + omega_e * (spm.Ld * id_ + spm.psi_m)
|
|
58
|
+
assert np.isclose(Vd_check, Vd, atol=1e-6)
|
|
59
|
+
assert np.isclose(Vq_check, Vq, atol=1e-6)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for winding factor calculations.
|
|
3
|
+
|
|
4
|
+
Reference values validated against emetor.com and SWAT-EM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import numpy as np
|
|
9
|
+
from emachines.winding.factors import pitch_factor, distribution_factor, winding_factor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPitchFactor:
|
|
13
|
+
def test_full_pitch_fundamental(self):
|
|
14
|
+
"""Full-pitch coil: kp1 = 1.0"""
|
|
15
|
+
assert np.isclose(pitch_factor(1, 3, 3.0), 1.0)
|
|
16
|
+
|
|
17
|
+
def test_chorded_coil(self):
|
|
18
|
+
"""5/6 chording: kp1 = sin(5π/12) ≈ 0.9659"""
|
|
19
|
+
assert np.isclose(pitch_factor(1, 5, 6.0), np.sin(5 * np.pi / 12), atol=1e-4)
|
|
20
|
+
|
|
21
|
+
def test_third_harmonic_elimination(self):
|
|
22
|
+
"""2/3 chording eliminates 3rd harmonic: kp3 = 0"""
|
|
23
|
+
assert np.isclose(pitch_factor(3, 4, 6.0), 0.0, atol=1e-10)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestDistributionFactor:
|
|
27
|
+
def test_q1_concentrated(self):
|
|
28
|
+
"""q=1: kd1 = 1.0"""
|
|
29
|
+
# Q=6, p=1, m=3 → q=1
|
|
30
|
+
assert np.isclose(distribution_factor(1, 6, 1, m=3), 1.0)
|
|
31
|
+
|
|
32
|
+
def test_q2_distributed(self):
|
|
33
|
+
"""q=2: kd1 ≈ 0.9659"""
|
|
34
|
+
# Q=12, p=1, m=3 → q=2
|
|
35
|
+
assert np.isclose(distribution_factor(1, 12, 1, m=3), 0.9659, atol=1e-3)
|
|
36
|
+
|
|
37
|
+
def test_fscw_raises(self):
|
|
38
|
+
"""FSCW (q < 1) raises ValueError — star-of-slots not yet implemented."""
|
|
39
|
+
# Q=12, p=5, m=3 → q=0.4
|
|
40
|
+
with pytest.raises(ValueError, match="star-of-slots"):
|
|
41
|
+
distribution_factor(1, 12, 5, m=3)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestWindingFactorIntegerSlot:
|
|
45
|
+
@pytest.mark.parametrize("Q,p,coil_span,expected_kw1", [
|
|
46
|
+
(24, 2, 5, 0.9330), # 24s/4p, 5/6 chording: kp=0.9659, kd=0.9659
|
|
47
|
+
(36, 3, 5, 0.9330), # 36s/6p, 5/6 chording: same q=2, same result
|
|
48
|
+
(12, 1, 6, 0.9659), # 12s/2p, full pitch (coil_span=pole_pitch=6): kp=1.0, kd=0.9659
|
|
49
|
+
])
|
|
50
|
+
def test_integer_slot_kw1(self, Q, p, coil_span, expected_kw1):
|
|
51
|
+
kw = winding_factor(1, Q, p, coil_span)
|
|
52
|
+
assert np.isclose(kw, expected_kw1, atol=0.001), \
|
|
53
|
+
f"Q={Q}, p={p}, coil_span={coil_span}: kw1={kw:.4f}, expected≈{expected_kw1}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestWindingFactorFSCW:
|
|
57
|
+
@pytest.mark.xfail(
|
|
58
|
+
reason="FSCW requires star-of-slots phasor method — "
|
|
59
|
+
"to be migrated from emdesigner.winding_engine",
|
|
60
|
+
raises=ValueError,
|
|
61
|
+
strict=True,
|
|
62
|
+
)
|
|
63
|
+
@pytest.mark.parametrize("Q,P,expected_kw1", [
|
|
64
|
+
(12, 10, 0.933),
|
|
65
|
+
(12, 8, 0.866),
|
|
66
|
+
( 9, 8, 0.945),
|
|
67
|
+
(12, 14, 0.933),
|
|
68
|
+
])
|
|
69
|
+
def test_fscw_kw1(self, Q, P, expected_kw1):
|
|
70
|
+
"""FSCW winding factors — xfail until star-of-slots is ported."""
|
|
71
|
+
p = P // 2
|
|
72
|
+
pole_pitch = Q / P
|
|
73
|
+
coil_span = max(1, round(pole_pitch))
|
|
74
|
+
kw = winding_factor(1, Q, p, coil_span)
|
|
75
|
+
assert np.isclose(kw, expected_kw1, atol=0.01)
|