PyOpenMagnetics 1.6.6__tar.gz → 1.7.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.
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/AGENTS.md +34 -54
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/CMakeLists.txt +48 -5
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/PKG-INFO +133 -98
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/PyOpenMagnetics.pyi +8 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/README.md +132 -97
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0/_packaging}/__init__.py +1 -0
- pyopenmagnetics-1.7.0/_packaging/_build_info.py.in +5 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/errors.md +5 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/buck_inductor.py +8 -6
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/complete_simulation_example.py +1 -5
- pyopenmagnetics-1.7.0/examples/converter_design_example.py +228 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_bobbin.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_coil.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_core.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_plotting.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_220v_12v_2a_complete.py +9 -40
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_design.py +4 -9
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/plot_flyback_design.py +4 -4
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/test_field_calc.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/test_field_plot.py +0 -2
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/llms.txt +32 -30
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/pyproject.toml +5 -4
- pyopenmagnetics-1.7.0/src/advisers.cpp +509 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/bobbin.cpp +36 -67
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/cmc.cpp +6 -4
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/converter.cpp +157 -30
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/core.cpp +65 -121
- pyopenmagnetics-1.7.0/src/crossref.cpp +150 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/database.cpp +102 -144
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/losses.cpp +119 -197
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/module.cpp +16 -0
- pyopenmagnetics-1.7.0/src/settings.cpp +645 -0
- pyopenmagnetics-1.7.0/src/simulation.cpp +760 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/utils.cpp +73 -157
- pyopenmagnetics-1.7.0/src/winding.cpp +881 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/wire.cpp +87 -182
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_converter_endpoints.py +9 -7
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_core.py +23 -12
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_core_adviser.py +8 -3
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_examples_integration.py +1 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_magnetic_adviser.py +6 -12
- pyopenmagnetics-1.6.6/examples/converter_design_example.py +0 -336
- pyopenmagnetics-1.6.6/src/advisers.cpp +0 -565
- pyopenmagnetics-1.6.6/src/crossref.cpp +0 -164
- pyopenmagnetics-1.6.6/src/settings.cpp +0 -651
- pyopenmagnetics-1.6.6/src/simulation.cpp +0 -956
- pyopenmagnetics-1.6.6/src/winding.cpp +0 -1002
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.github/workflows/ci.yml +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.github/workflows/publish.yml +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.gitignore +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/LICENSE +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/MAS.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/mas_db_reader.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/validation.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/clear_cibuildwheel_cache.sh +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/compatibility.md +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/performance.md +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/README.md +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_220v_12v_1a.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_bh_curve.png +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_core.png +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_summary.png +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_waveforms.png +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/list_plot_funcs.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/plot_flyback_pyom.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/force_fresh_build.sh +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/01_getting_started.ipynb +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/02_buck_inductor.ipynb +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/03_core_losses.ipynb +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/README.md +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/requirements.txt +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/advisers.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/bobbin.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/cmc.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/common.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/converter.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/core.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/crossref.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/database.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/logging.cpp +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/logging.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/losses.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/plotting.cpp +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/plotting.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/settings.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/simulation.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/utils.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/winding.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/wire.h +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/test.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/__init__.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/conftest.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_inputs.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_logging.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_plotting.py +0 -0
- {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_winding.py +0 -0
|
@@ -112,20 +112,9 @@ The package has **no `__init__.py`**. A bare `import PyOpenMagnetics` gives an
|
|
|
112
112
|
**empty namespace with 0 functions**. You MUST use `importlib`:
|
|
113
113
|
|
|
114
114
|
```python
|
|
115
|
-
import
|
|
115
|
+
import PyOpenMagnetics as PyOM
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
os.path.dirname(__import__('PyOpenMagnetics').__path__[0]),
|
|
119
|
-
'PyOpenMagnetics'
|
|
120
|
-
)
|
|
121
|
-
so_files = glob.glob(os.path.join(pkg_dir, 'PyOpenMagnetics.cpython-*'))
|
|
122
|
-
assert so_files, f"No .so/.pyd found in {pkg_dir}"
|
|
123
|
-
|
|
124
|
-
spec = importlib.util.spec_from_file_location('PyOpenMagnetics', so_files[0])
|
|
125
|
-
PyOM = importlib.util.module_from_spec(spec)
|
|
126
|
-
spec.loader.exec_module(PyOM)
|
|
127
|
-
|
|
128
|
-
# MANDATORY — must call before any other function
|
|
117
|
+
# MANDATORY - must call before any other function
|
|
129
118
|
PyOM.load_databases({})
|
|
130
119
|
```
|
|
131
120
|
|
|
@@ -404,17 +393,7 @@ Lm ≈ 800 µH → for desiredInductance (Method B onl
|
|
|
404
393
|
### Method A: `design_magnetics_from_converter()` (single call)
|
|
405
394
|
|
|
406
395
|
```python
|
|
407
|
-
import
|
|
408
|
-
|
|
409
|
-
# Load module
|
|
410
|
-
pkg_dir = os.path.join(
|
|
411
|
-
os.path.dirname(__import__('PyOpenMagnetics').__path__[0]),
|
|
412
|
-
'PyOpenMagnetics'
|
|
413
|
-
)
|
|
414
|
-
so_files = glob.glob(os.path.join(pkg_dir, 'PyOpenMagnetics.cpython-*'))
|
|
415
|
-
spec = importlib.util.spec_from_file_location('PyOpenMagnetics', so_files[0])
|
|
416
|
-
PyOM = importlib.util.module_from_spec(spec)
|
|
417
|
-
spec.loader.exec_module(PyOM)
|
|
396
|
+
import PyOpenMagnetics as PyOM
|
|
418
397
|
PyOM.load_databases({})
|
|
419
398
|
|
|
420
399
|
# Define converter (BASE Flyback schema — NO desiredInductance!)
|
|
@@ -433,23 +412,16 @@ flyback = {
|
|
|
433
412
|
}]
|
|
434
413
|
}
|
|
435
414
|
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
print(f"Method A failed: {
|
|
443
|
-
# → Use Method B
|
|
415
|
+
# design_magnetics_from_converter() builds MAS Inputs from the spec (the
|
|
416
|
+
# magnetic adviser is a separate step -- see Method B, which continues from
|
|
417
|
+
# here). Failures raise PyOM.EngineError (v1.7.0+).
|
|
418
|
+
try:
|
|
419
|
+
inputs = PyOM.design_magnetics_from_converter("flyback", flyback)
|
|
420
|
+
except PyOM.EngineError as e:
|
|
421
|
+
print(f"Method A failed: {e}")
|
|
444
422
|
else:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
core = mas_obj["magnetic"]["core"]["functionalDescription"]
|
|
448
|
-
coil = mas_obj["magnetic"]["coil"]["functionalDescription"]
|
|
449
|
-
print(f"Core: {core['shape'].get('name','?') if isinstance(core['shape'], dict) else core['shape']}")
|
|
450
|
-
print(f"Material: {core['material'].get('name','?') if isinstance(core['material'], dict) else core['material']}")
|
|
451
|
-
for w in coil:
|
|
452
|
-
print(f" {w.get('name','?')}: {w.get('numberTurns','?')} turns")
|
|
423
|
+
print(f"Turns ratios: {inputs['designRequirements']['turnsRatios']}")
|
|
424
|
+
print(f"Operating points: {len(inputs['operatingPoints'])}")
|
|
453
425
|
```
|
|
454
426
|
|
|
455
427
|
### Method B: `process_converter()` → `calculate_advised_magnetics()` (no ngspice needed)
|
|
@@ -473,21 +445,21 @@ flyback_adv = {
|
|
|
473
445
|
}]
|
|
474
446
|
}
|
|
475
447
|
processed = PyOM.process_converter("flyback", flyback_adv, use_ngspice=False)
|
|
476
|
-
|
|
448
|
+
# failures raise PyOM.EngineError -- no error-shaped return values (v1.7.0+)
|
|
477
449
|
|
|
478
450
|
# Step 2: Feed into adviser (with optional weights)
|
|
479
451
|
mas_inputs = {
|
|
480
452
|
"designRequirements": processed["designRequirements"],
|
|
481
453
|
"operatingPoints": processed["operatingPoints"]
|
|
482
454
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
# Step 3: Parse
|
|
489
|
-
d = designs[0]
|
|
490
|
-
mas_obj, score =
|
|
455
|
+
# calculate_advised_magnetics takes (inputs, max_results, core_mode) -- no weights
|
|
456
|
+
# argument (that is calculate_advised_cores). Inputs must be processed first.
|
|
457
|
+
mas_inputs = PyOM.process_inputs(mas_inputs)
|
|
458
|
+
designs = PyOM.calculate_advised_magnetics(mas_inputs, 1, "available cores")
|
|
459
|
+
|
|
460
|
+
# Step 3: Parse -- result is {"data": [{"mas", "scoring", ...}]}
|
|
461
|
+
d = designs["data"][0]
|
|
462
|
+
mas_obj, score = d["mas"], d["scoring"]
|
|
491
463
|
core = mas_obj["magnetic"]["core"]["functionalDescription"]
|
|
492
464
|
coil = mas_obj["magnetic"]["coil"]["functionalDescription"]
|
|
493
465
|
print(f"Core: {core['shape'].get('name','?')}, Material: {core['material'].get('name','?')}")
|
|
@@ -570,7 +542,7 @@ wire = PyOM.find_wire_by_name("Round 0.5 - Grade 1") # ✅ verified (
|
|
|
570
542
|
|
|
571
543
|
### Bobbins
|
|
572
544
|
```python
|
|
573
|
-
bobbin = PyOM.find_bobbin_by_name("
|
|
545
|
+
bobbin = PyOM.find_bobbin_by_name("Bobbin E25/7") # ✅ verified (.pyi confirmed)
|
|
574
546
|
```
|
|
575
547
|
|
|
576
548
|
### ⌠Wrong names — do NOT use these
|
|
@@ -588,7 +560,8 @@ PyOM.get_core_shape_families(True) # TypeError — takes no arguments
|
|
|
588
560
|
|
|
589
561
|
### Inductance Calculation
|
|
590
562
|
```python
|
|
591
|
-
inductance = PyOM.
|
|
563
|
+
inductance = PyOM.calculate_inductance_from_number_turns_and_gapping(
|
|
564
|
+
core, coil, operating_point, models)
|
|
592
565
|
# Returns dict with:
|
|
593
566
|
# magnetizingInductance → {magnetizingInductance, coreReluctance, gappingReluctance, ...}
|
|
594
567
|
```
|
|
@@ -611,11 +584,18 @@ sim_result = PyOM.simulate(mas)
|
|
|
611
584
|
|
|
612
585
|
### Wire Utilities
|
|
613
586
|
```python
|
|
614
|
-
# Skin depth
|
|
615
|
-
|
|
587
|
+
# Skin depth: the 2nd arg is a CURRENT SignalDescriptor (needs processed.label +
|
|
588
|
+
# processed.effectiveFrequency), NOT a bare frequency. Passing a number returns garbage.
|
|
589
|
+
current = {
|
|
590
|
+
"waveform": {"data": [-5, 5, -5], "time": [0, 5e-6, 10e-6]},
|
|
591
|
+
"processed": {"label": "triangular", "effectiveFrequency": 100000,
|
|
592
|
+
"rms": 2.9, "peak": 5, "peakToPeak": 10, "offset": 0, "dutyCycle": 0.5}
|
|
593
|
+
}
|
|
594
|
+
delta = PyOM.calculate_effective_skin_depth("copper", current, 25) # ~0.21 mm for Cu @100 kHz
|
|
616
595
|
|
|
617
596
|
# DC resistance per meter for a given wire diameter
|
|
618
|
-
|
|
597
|
+
wire = PyOM.find_wire_by_name("Round 0.5 - Grade 1")
|
|
598
|
+
rdc = PyOM.calculate_dc_resistance_per_meter(wire, 25) # (wire object, temperature)
|
|
619
599
|
```
|
|
620
600
|
|
|
621
601
|
### SPICE Export
|
|
@@ -142,7 +142,7 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
142
142
|
set(MKF_GIT_TAG "main"
|
|
143
143
|
CACHE STRING "MKF git ref for the vendored PyOM build")
|
|
144
144
|
# Force fresh clone by using a unique timestamp - update this when the pin changes
|
|
145
|
-
set(MKF_FORCE_REFRESH "2026-08-
|
|
145
|
+
set(MKF_FORCE_REFRESH "2026-08-10-v1.7.0-mkf-d9293885")
|
|
146
146
|
# Tell MKF to disable matplotplusplus and use SVG-based Painter instead
|
|
147
147
|
set(INCLUDE_PYMKF ON CACHE BOOL "Build Python interface" FORCE)
|
|
148
148
|
# GIT_SUBMODULES_RECURSE pulls in CAS/PEAS (added 2026-04 alongside the
|
|
@@ -166,7 +166,7 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
166
166
|
# Skip Git LFS to avoid bandwidth quota issues - data files are optional for build
|
|
167
167
|
set(ENV{GIT_LFS_SKIP_SMUDGE} "1")
|
|
168
168
|
# Force fresh clone by using a unique timestamp - update this when MAS changes
|
|
169
|
-
set(MAS_FORCE_REFRESH "
|
|
169
|
+
set(MAS_FORCE_REFRESH "2026-08-10-v1.7.0-mas-f736d41")
|
|
170
170
|
FetchContent_Declare(
|
|
171
171
|
mas
|
|
172
172
|
GIT_REPOSITORY https://github.com/OpenMagnetics/MAS.git
|
|
@@ -180,10 +180,14 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
180
180
|
message(STATUS "MAS_POPULATED: ${MAS_POPULATED}")
|
|
181
181
|
message(STATUS "MKF_POPULATED: ${MKF_POPULATED}")
|
|
182
182
|
|
|
183
|
-
# Always delete and repopulate MAS to ensure we get the latest
|
|
183
|
+
# Always delete and repopulate MAS to ensure we get the latest. The subbuild
|
|
184
|
+
# dir must go too: its stamp files survive the source deletion and make the
|
|
185
|
+
# next populate run a git "update" step inside the freshly-emptied mas-src,
|
|
186
|
+
# which fails with "fatal: not a git repository".
|
|
184
187
|
if(EXISTS ${CMAKE_BINARY_DIR}/_deps/mas-src)
|
|
185
188
|
message(STATUS "Removing old MAS source to force fresh clone")
|
|
186
189
|
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/mas-src)
|
|
190
|
+
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/mas-subbuild)
|
|
187
191
|
endif()
|
|
188
192
|
message(STATUS "Populating MAS")
|
|
189
193
|
FetchContent_Populate(mas)
|
|
@@ -198,6 +202,12 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
198
202
|
if(MAS_GIT_ERROR)
|
|
199
203
|
message(STATUS "MAS git error: ${MAS_GIT_ERROR}")
|
|
200
204
|
endif()
|
|
205
|
+
# Resolve the MAS commit for the wheel's _build_info.py (ABT #601).
|
|
206
|
+
execute_process(
|
|
207
|
+
COMMAND git -C ${mas_SOURCE_DIR} rev-parse HEAD
|
|
208
|
+
OUTPUT_VARIABLE MAS_GIT_HEAD
|
|
209
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
210
|
+
)
|
|
201
211
|
|
|
202
212
|
# Always delete and repopulate MKF so the checkout matches ${MKF_GIT_TAG}
|
|
203
213
|
# exactly, mirroring the MAS refresh above. FetchContent will NOT re-checkout
|
|
@@ -208,6 +218,8 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
208
218
|
if(EXISTS ${CMAKE_BINARY_DIR}/_deps/mkf-src)
|
|
209
219
|
message(STATUS "Removing old MKF source to force fresh checkout at ${MKF_GIT_TAG}")
|
|
210
220
|
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/mkf-src)
|
|
221
|
+
# Same stamp-staleness hazard as the MAS subbuild above.
|
|
222
|
+
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/mkf-subbuild)
|
|
211
223
|
endif()
|
|
212
224
|
message(STATUS "Populating MKF")
|
|
213
225
|
FetchContent_Populate(MKF)
|
|
@@ -238,7 +250,31 @@ if(NOT LOCAL_MKF_MAS)
|
|
|
238
250
|
else()
|
|
239
251
|
message(STATUS "Using local MKF at ${MKF_DIR}")
|
|
240
252
|
message(STATUS "Using local MAS at ${MAS_DIR}")
|
|
253
|
+
# Local-checkout builds still record their engine commits (ABT #601).
|
|
254
|
+
execute_process(
|
|
255
|
+
COMMAND git -C ${MKF_DIR} rev-parse HEAD
|
|
256
|
+
OUTPUT_VARIABLE MKF_GIT_HEAD
|
|
257
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
258
|
+
)
|
|
259
|
+
execute_process(
|
|
260
|
+
COMMAND git -C ${MAS_DIR} rev-parse HEAD
|
|
261
|
+
OUTPUT_VARIABLE MAS_GIT_HEAD
|
|
262
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
263
|
+
)
|
|
264
|
+
endif()
|
|
265
|
+
|
|
266
|
+
# ABT #601: bake the resolved MKF/MAS commits into the package so a wheel is
|
|
267
|
+
# self-describing (PyOpenMagnetics.__mkf_commit__ / __mas_commit__). Before this,
|
|
268
|
+
# MKF_GIT_HEAD was only message(STATUS)-ed to the CMake log and provenance could
|
|
269
|
+
# not be recovered from an install. Fail loudly rather than shipping a wheel with
|
|
270
|
+
# unknown provenance.
|
|
271
|
+
if(NOT MKF_GIT_HEAD OR NOT MAS_GIT_HEAD)
|
|
272
|
+
message(FATAL_ERROR
|
|
273
|
+
"Could not resolve MKF/MAS commits for _build_info.py "
|
|
274
|
+
"(MKF_GIT_HEAD='${MKF_GIT_HEAD}', MAS_GIT_HEAD='${MAS_GIT_HEAD}'). "
|
|
275
|
+
"The MKF/MAS source dirs must be git checkouts (ABT #601).")
|
|
241
276
|
endif()
|
|
277
|
+
configure_file(_packaging/_build_info.py.in "${CMAKE_BINARY_DIR}/_build_info.py" @ONLY)
|
|
242
278
|
|
|
243
279
|
message(STATUS "Compiling MAS")
|
|
244
280
|
|
|
@@ -570,8 +606,15 @@ install(TARGETS PyOpenMagnetics LIBRARY DESTINATION .)
|
|
|
570
606
|
|
|
571
607
|
# Install the package __init__.py so `import PyOpenMagnetics` exposes the
|
|
572
608
|
# compiled extension's API. Without it the wheel installs as an empty PEP-420
|
|
573
|
-
# namespace package and `PyOpenMagnetics.<fn>` resolves to nothing.
|
|
574
|
-
|
|
609
|
+
# namespace package and `PyOpenMagnetics.<fn>` resolves to nothing. It lives in
|
|
610
|
+
# _packaging/ (not the repo root) so the repo checkout cannot shadow the installed
|
|
611
|
+
# package when pytest runs from the repo root (ABT #597). The leading underscore
|
|
612
|
+
# matters: a directory literally named "packaging" shadows the REAL `packaging`
|
|
613
|
+
# PyPI library that build backends and pytest import, breaking `python -m build`
|
|
614
|
+
# and pytest with "No module named packaging.PyOpenMagnetics".
|
|
615
|
+
install(FILES _packaging/__init__.py DESTINATION .)
|
|
616
|
+
# Engine-commit provenance, configured above from the resolved MKF/MAS heads.
|
|
617
|
+
install(FILES "${CMAKE_BINARY_DIR}/_build_info.py" DESTINATION .)
|
|
575
618
|
|
|
576
619
|
# Install documentation files for AI assistants
|
|
577
620
|
install(FILES AGENTS.md llms.txt PyOpenMagnetics.pyi DESTINATION .)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: PyOpenMagnetics
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Python wrapper for OpenMagnetics
|
|
5
5
|
Author-Email: Alfonso Martinez <Alfonso_VII@hotmail.com>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -57,51 +57,48 @@ cd PyOpenMagnetics
|
|
|
57
57
|
pip install .
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
####
|
|
60
|
+
#### Build provenance
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
extension
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
published**, a from-scratch build of newer `main` cannot obtain that library
|
|
67
|
-
and **fails to link**.
|
|
62
|
+
The build compiles MKF by **globbing its `.cpp` files directly** into the
|
|
63
|
+
extension, tracking MKF/MAS `main`, and builds the Kirchhoff converter-model
|
|
64
|
+
library (`libKirchhoffApi.so`) as an ExternalProject. The exact engine commits a
|
|
65
|
+
wheel was compiled from are baked into the package:
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
```python
|
|
68
|
+
import PyOpenMagnetics
|
|
69
|
+
print(PyOpenMagnetics.__mkf_commit__) # MKF SHA this wheel was built from
|
|
70
|
+
print(PyOpenMagnetics.__mas_commit__) # MAS SHA this wheel was built from
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A clean rebuild:
|
|
73
74
|
|
|
74
75
|
```bash
|
|
75
|
-
rm -rf build && pip install . --no-deps -v
|
|
76
|
+
rm -rf build && pip install . --no-deps -v
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
`CMakeLists.txt` (publish AAS/Kirchhoff, then `add_subdirectory` +
|
|
80
|
-
`target_link_libraries(... libKirchhoffApi.so)`). Overriding
|
|
81
|
-
`-DMKF_GIT_TAG=main` before that lands will break the link.
|
|
82
|
-
|
|
83
|
-
### ⚠️ Import Instructions
|
|
79
|
+
### Importing and error handling
|
|
84
80
|
|
|
85
|
-
|
|
81
|
+
`import PyOpenMagnetics` works like any other package. Since v1.7.0 every engine
|
|
82
|
+
failure raises **`PyOpenMagnetics.EngineError`** (a `RuntimeError` subclass) —
|
|
83
|
+
functions never return error strings or `{"data": "<error>"}` objects:
|
|
86
84
|
|
|
87
85
|
```python
|
|
88
|
-
import
|
|
89
|
-
|
|
90
|
-
# Option 1: Direct loading (recommended)
|
|
91
|
-
so_path = '/path/to/PyOpenMagnetics.cpython-311-x86_64-linux-gnu.so'
|
|
92
|
-
spec = importlib.util.spec_from_file_location('PyOpenMagnetics', so_path)
|
|
93
|
-
PyOpenMagnetics = importlib.util.module_from_spec(spec)
|
|
94
|
-
spec.loader.exec_module(PyOpenMagnetics)
|
|
95
|
-
|
|
96
|
-
# Option 2: Create __init__.py (see AGENTS.md for details)
|
|
86
|
+
import PyOpenMagnetics
|
|
97
87
|
|
|
98
|
-
# Verify installation
|
|
99
88
|
PyOpenMagnetics.load_databases({})
|
|
100
89
|
print(f"✓ Loaded {len(PyOpenMagnetics.get_core_materials())} materials")
|
|
101
90
|
print(f"✓ Loaded {len(PyOpenMagnetics.get_core_shapes())} shapes")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
PyOpenMagnetics.find_core_shape_by_name("No Such Shape")
|
|
94
|
+
except PyOpenMagnetics.EngineError as e:
|
|
95
|
+
print(f"Engine error: {e}")
|
|
102
96
|
```
|
|
103
97
|
|
|
104
|
-
|
|
98
|
+
The only exception is the plotting family, which returns a discriminated union
|
|
99
|
+
`{"success": bool, "error": str, ...}` that callers branch on.
|
|
100
|
+
|
|
101
|
+
See [AGENTS.md](AGENTS.md) for more usage guidance.
|
|
105
102
|
|
|
106
103
|
## Quick Start
|
|
107
104
|
|
|
@@ -116,9 +113,11 @@ shape = PyOpenMagnetics.find_core_shape_by_name("E 42/21/15")
|
|
|
116
113
|
# Find a core material by name
|
|
117
114
|
material = PyOpenMagnetics.find_core_material_by_name("3C95")
|
|
118
115
|
|
|
119
|
-
# Create a core with gapping
|
|
116
|
+
# Create a core with gapping. "type" is mandatory; shape/material accept
|
|
117
|
+
# either the objects fetched above or plain name strings.
|
|
120
118
|
core_data = {
|
|
121
119
|
"functionalDescription": {
|
|
120
|
+
"type": "two-piece set",
|
|
122
121
|
"shape": shape,
|
|
123
122
|
"material": material,
|
|
124
123
|
"gapping": [{"type": "subtractive", "length": 0.001}], # 1mm gap
|
|
@@ -190,25 +189,50 @@ for i, item in enumerate(result["data"]):
|
|
|
190
189
|
```python
|
|
191
190
|
import PyOpenMagnetics
|
|
192
191
|
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"peakToPeak": 0.2, # 200 mT peak-to-peak
|
|
204
|
-
"offset": 0
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
]
|
|
209
|
-
}
|
|
192
|
+
# A complete core (see "Creating a Core" above)
|
|
193
|
+
core = PyOpenMagnetics.calculate_core_data({
|
|
194
|
+
"functionalDescription": {
|
|
195
|
+
"type": "two-piece set",
|
|
196
|
+
"shape": "E 42/21/15",
|
|
197
|
+
"material": "3C95",
|
|
198
|
+
"gapping": [{"type": "subtractive", "length": 0.0005}],
|
|
199
|
+
"numberStacks": 1
|
|
200
|
+
}
|
|
201
|
+
}, True)
|
|
210
202
|
|
|
211
|
-
|
|
203
|
+
# A wound coil on that core
|
|
204
|
+
bobbin = PyOpenMagnetics.create_basic_bobbin(core, True)
|
|
205
|
+
coil = PyOpenMagnetics.wind({
|
|
206
|
+
"bobbin": bobbin,
|
|
207
|
+
"functionalDescription": [{
|
|
208
|
+
"name": "Primary",
|
|
209
|
+
"numberTurns": 20,
|
|
210
|
+
"numberParallels": 1,
|
|
211
|
+
"isolationSide": "primary",
|
|
212
|
+
"wire": "Round 0.5 - Grade 1"
|
|
213
|
+
}]
|
|
214
|
+
}, 1, [1.0], [0], [])
|
|
215
|
+
|
|
216
|
+
# Inputs with the excitation waveforms (see the Design Adviser example)
|
|
217
|
+
inputs = PyOpenMagnetics.process_inputs({
|
|
218
|
+
"designRequirements": {
|
|
219
|
+
"magnetizingInductance": {"nominal": 100e-6},
|
|
220
|
+
"turnsRatios": []
|
|
221
|
+
},
|
|
222
|
+
"operatingPoints": [{
|
|
223
|
+
"name": "Nominal",
|
|
224
|
+
"conditions": {"ambientTemperature": 25},
|
|
225
|
+
"excitationsPerWinding": [{
|
|
226
|
+
"name": "Primary",
|
|
227
|
+
"frequency": 100000,
|
|
228
|
+
"current": {"waveform": {"data": [-1, 1, -1], "time": [0, 5e-6, 10e-6]}},
|
|
229
|
+
"voltage": {"waveform": {"data": [50, 50, -50, -50], "time": [0, 5e-6, 5e-6, 10e-6]}}
|
|
230
|
+
}]
|
|
231
|
+
}]
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
|
|
235
|
+
losses = PyOpenMagnetics.calculate_core_losses(core, coil, inputs, models)
|
|
212
236
|
print(f"Core losses: {losses['coreLosses']} W")
|
|
213
237
|
```
|
|
214
238
|
|
|
@@ -217,61 +241,66 @@ print(f"Core losses: {losses['coreLosses']} W")
|
|
|
217
241
|
```python
|
|
218
242
|
import PyOpenMagnetics
|
|
219
243
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
"name": "Primary",
|
|
224
|
-
"numberTurns": 50,
|
|
225
|
-
"numberParallels": 1,
|
|
226
|
-
"wire": "Round 0.5 - Grade 1"
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
"name": "Secondary",
|
|
230
|
-
"numberTurns": 10,
|
|
231
|
-
"numberParallels": 3,
|
|
232
|
-
"wire": "Round 1.0 - Grade 1"
|
|
233
|
-
}
|
|
234
|
-
]
|
|
244
|
+
# core from calculate_core_data(...) as above
|
|
245
|
+
bobbin = PyOpenMagnetics.create_basic_bobbin(core, True)
|
|
235
246
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
247
|
+
coil_spec = {
|
|
248
|
+
"bobbin": bobbin,
|
|
249
|
+
"functionalDescription": [
|
|
250
|
+
{
|
|
251
|
+
"name": "Primary",
|
|
252
|
+
"numberTurns": 50,
|
|
253
|
+
"numberParallels": 1,
|
|
254
|
+
"isolationSide": "primary",
|
|
255
|
+
"wire": "Round 0.5 - Grade 1"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "Secondary",
|
|
259
|
+
"numberTurns": 10,
|
|
260
|
+
"numberParallels": 3,
|
|
261
|
+
"isolationSide": "secondary",
|
|
262
|
+
"wire": "Round 1.00 - Grade 1"
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# wind(coil, repetitions, proportion_per_winding, pattern, margin_pairs)
|
|
268
|
+
coil = PyOpenMagnetics.wind(coil_spec, 1, [0.5, 0.5], [0, 1], [])
|
|
269
|
+
print(f"Wound {len(coil['turnsDescription'])} turns")
|
|
239
270
|
```
|
|
240
271
|
|
|
241
|
-
##
|
|
272
|
+
## Converter-Based Design
|
|
242
273
|
|
|
243
|
-
|
|
274
|
+
The converter surface builds complete MAS Inputs straight from converter
|
|
275
|
+
specifications (the Kirchhoff topology designer sizes inductance, turns ratios
|
|
276
|
+
and waveforms). See `examples/converter_design_example.py` for the full flow:
|
|
244
277
|
|
|
245
278
|
```python
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# Define flyback specifications
|
|
249
|
-
specs = {
|
|
250
|
-
"input_voltage_min": 90,
|
|
251
|
-
"input_voltage_max": 375,
|
|
252
|
-
"outputs": [{"voltage": 12, "current": 2, "diode_drop": 0.5}],
|
|
253
|
-
"switching_frequency": 100000,
|
|
254
|
-
"max_duty_cycle": 0.45,
|
|
255
|
-
"efficiency": 0.85,
|
|
256
|
-
"current_ripple_ratio": 0.4,
|
|
257
|
-
"force_dcm": False,
|
|
258
|
-
"safety_margin": 0.85,
|
|
259
|
-
"ambient_temperature": 40,
|
|
260
|
-
"max_drain_source_voltage": None,
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
# Calculate magnetic requirements
|
|
264
|
-
design = design_flyback(specs)
|
|
265
|
-
print(f"Required inductance: {design['min_inductance']*1e6:.1f} µH")
|
|
266
|
-
print(f"Turns ratio: {design['turns_ratios'][0]:.2f}")
|
|
279
|
+
import PyOpenMagnetics
|
|
267
280
|
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
flyback_specs = {
|
|
282
|
+
"inputVoltage": {"minimum": 185, "maximum": 265},
|
|
283
|
+
"desiredInductance": 800e-6, # optional pin; omit to let Kirchhoff size it
|
|
284
|
+
"desiredTurnsRatios": [13.5], # optional pin
|
|
285
|
+
"efficiency": 0.88,
|
|
286
|
+
"operatingPoints": [{
|
|
287
|
+
"outputVoltages": [12.0],
|
|
288
|
+
"outputCurrents": [2.0],
|
|
289
|
+
"switchingFrequency": 100000,
|
|
290
|
+
"ambientTemperature": 40
|
|
291
|
+
}]
|
|
292
|
+
}
|
|
270
293
|
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
inputs = PyOpenMagnetics.process_converter("flyback", flyback_specs)
|
|
295
|
+
processed = PyOpenMagnetics.process_inputs(inputs)
|
|
296
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(processed, 5, "standard cores")
|
|
297
|
+
for item in result["data"]:
|
|
298
|
+
print(item["mas"]["magnetic"]["manufacturerInfo"]["reference"], item["scoring"])
|
|
273
299
|
```
|
|
274
300
|
|
|
301
|
+
A TAS-shaped spec (an object with `designRequirements` / `operatingPoints[].outputs`)
|
|
302
|
+
is also accepted and passed to Kirchhoff untouched.
|
|
303
|
+
|
|
275
304
|
## API Reference
|
|
276
305
|
|
|
277
306
|
### Database Access
|
|
@@ -293,13 +322,13 @@ magnetics = get_advised_magnetics(inputs, max_results=5)
|
|
|
293
322
|
| `calculate_core_data(core, process)` | Calculate complete core data |
|
|
294
323
|
| `calculate_core_gapping(core, gapping)` | Calculate gapping configuration |
|
|
295
324
|
| `calculate_inductance_from_number_turns_and_gapping(...)` | Calculate inductance |
|
|
296
|
-
| `calculate_core_losses(core,
|
|
325
|
+
| `calculate_core_losses(core, coil, inputs, models)` | Calculate core losses |
|
|
297
326
|
|
|
298
327
|
### Winding Functions
|
|
299
328
|
|
|
300
329
|
| Function | Description |
|
|
301
330
|
|----------|-------------|
|
|
302
|
-
| `wind(
|
|
331
|
+
| `wind(coil, repetitions, proportions, pattern, margins)` | Wind coils on a core |
|
|
303
332
|
| `calculate_winding_losses(...)` | Calculate total winding losses |
|
|
304
333
|
| `calculate_ohmic_losses(...)` | Calculate DC losses |
|
|
305
334
|
| `calculate_skin_effect_losses(...)` | Calculate skin effect losses |
|
|
@@ -341,7 +370,13 @@ magnetics = get_advised_magnetics(inputs, max_results=5)
|
|
|
341
370
|
|
|
342
371
|
All 24 power topologies are exposed with a uniform API. Use the generic
|
|
343
372
|
`process_converter("<topology>", converter, use_ngspice)` (also accepts
|
|
344
|
-
`"advanced_<topology>"`), or the per-topology functions below.
|
|
373
|
+
`"advanced_<topology>"`), or the per-topology functions below. The converter
|
|
374
|
+
spec is either the legacy flat shape shown in "Converter-Based Design" above
|
|
375
|
+
(`inputVoltage`, optional `desiredInductance`/`desiredTurnsRatios`/`efficiency`/
|
|
376
|
+
`currentRippleRatio`, and `operatingPoints[]` with `outputVoltages[]`/
|
|
377
|
+
`outputCurrents[]`/`switchingFrequency`/`ambientTemperature`) or a TAS-shaped
|
|
378
|
+
spec, which is passed through untouched. Failures raise
|
|
379
|
+
`PyOpenMagnetics.EngineError`.
|
|
345
380
|
|
|
346
381
|
| Function family | Description |
|
|
347
382
|
|----------|-------------|
|
|
@@ -84,6 +84,14 @@ by reading AGENTS.md before starting.
|
|
|
84
84
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
+
class EngineError(RuntimeError):
|
|
88
|
+
"""Raised by every engine failure (v1.7.0+): C++ exceptions surface as this."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
__mkf_commit__: str
|
|
92
|
+
__mas_commit__: str
|
|
93
|
+
|
|
94
|
+
|
|
87
95
|
from typing import Dict, List, Any, Optional, Union, Literal, overload
|
|
88
96
|
|
|
89
97
|
# Type aliases for JSON-like structures
|