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.
Files changed (96) hide show
  1. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/AGENTS.md +34 -54
  2. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/CMakeLists.txt +48 -5
  3. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/PKG-INFO +133 -98
  4. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/PyOpenMagnetics.pyi +8 -0
  5. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/README.md +132 -97
  6. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0/_packaging}/__init__.py +1 -0
  7. pyopenmagnetics-1.7.0/_packaging/_build_info.py.in +5 -0
  8. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/errors.md +5 -0
  9. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/buck_inductor.py +8 -6
  10. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/complete_simulation_example.py +1 -5
  11. pyopenmagnetics-1.7.0/examples/converter_design_example.py +228 -0
  12. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_bobbin.py +0 -2
  13. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_coil.py +0 -2
  14. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_core.py +0 -2
  15. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/debug_plotting.py +0 -2
  16. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_220v_12v_2a_complete.py +9 -40
  17. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_design.py +4 -9
  18. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/plot_flyback_design.py +4 -4
  19. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/test_field_calc.py +0 -2
  20. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/test_field_plot.py +0 -2
  21. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/llms.txt +32 -30
  22. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/pyproject.toml +5 -4
  23. pyopenmagnetics-1.7.0/src/advisers.cpp +509 -0
  24. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/bobbin.cpp +36 -67
  25. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/cmc.cpp +6 -4
  26. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/converter.cpp +157 -30
  27. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/core.cpp +65 -121
  28. pyopenmagnetics-1.7.0/src/crossref.cpp +150 -0
  29. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/database.cpp +102 -144
  30. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/losses.cpp +119 -197
  31. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/module.cpp +16 -0
  32. pyopenmagnetics-1.7.0/src/settings.cpp +645 -0
  33. pyopenmagnetics-1.7.0/src/simulation.cpp +760 -0
  34. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/utils.cpp +73 -157
  35. pyopenmagnetics-1.7.0/src/winding.cpp +881 -0
  36. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/wire.cpp +87 -182
  37. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_converter_endpoints.py +9 -7
  38. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_core.py +23 -12
  39. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_core_adviser.py +8 -3
  40. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_examples_integration.py +1 -0
  41. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_magnetic_adviser.py +6 -12
  42. pyopenmagnetics-1.6.6/examples/converter_design_example.py +0 -336
  43. pyopenmagnetics-1.6.6/src/advisers.cpp +0 -565
  44. pyopenmagnetics-1.6.6/src/crossref.cpp +0 -164
  45. pyopenmagnetics-1.6.6/src/settings.cpp +0 -651
  46. pyopenmagnetics-1.6.6/src/simulation.cpp +0 -956
  47. pyopenmagnetics-1.6.6/src/winding.cpp +0 -1002
  48. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.github/workflows/ci.yml +0 -0
  49. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.github/workflows/publish.yml +0 -0
  50. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/.gitignore +0 -0
  51. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/LICENSE +0 -0
  52. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/MAS.py +0 -0
  53. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/mas_db_reader.py +0 -0
  54. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/api/validation.py +0 -0
  55. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/clear_cibuildwheel_cache.sh +0 -0
  56. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/compatibility.md +0 -0
  57. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/docs/performance.md +0 -0
  58. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/README.md +0 -0
  59. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_220v_12v_1a.py +0 -0
  60. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_bh_curve.png +0 -0
  61. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_core.png +0 -0
  62. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_summary.png +0 -0
  63. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/flyback_waveforms.png +0 -0
  64. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/list_plot_funcs.py +0 -0
  65. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/examples/plot_flyback_pyom.py +0 -0
  66. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/force_fresh_build.sh +0 -0
  67. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/01_getting_started.ipynb +0 -0
  68. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/02_buck_inductor.ipynb +0 -0
  69. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/03_core_losses.ipynb +0 -0
  70. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/notebooks/README.md +0 -0
  71. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/requirements.txt +0 -0
  72. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/advisers.h +0 -0
  73. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/bobbin.h +0 -0
  74. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/cmc.h +0 -0
  75. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/common.h +0 -0
  76. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/converter.h +0 -0
  77. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/core.h +0 -0
  78. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/crossref.h +0 -0
  79. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/database.h +0 -0
  80. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/logging.cpp +0 -0
  81. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/logging.h +0 -0
  82. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/losses.h +0 -0
  83. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/plotting.cpp +0 -0
  84. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/plotting.h +0 -0
  85. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/settings.h +0 -0
  86. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/simulation.h +0 -0
  87. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/utils.h +0 -0
  88. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/winding.h +0 -0
  89. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/src/wire.h +0 -0
  90. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/test.py +0 -0
  91. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/__init__.py +0 -0
  92. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/conftest.py +0 -0
  93. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_inputs.py +0 -0
  94. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_logging.py +0 -0
  95. {pyopenmagnetics-1.6.6 → pyopenmagnetics-1.7.0}/tests/test_plotting.py +0 -0
  96. {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 importlib.util, os, glob
115
+ import PyOpenMagnetics as PyOM
116
116
 
117
- pkg_dir = os.path.join(
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 importlib.util, os, glob, json
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
- # Design (takes 60-120s with "available cores")
437
- result = PyOM.design_magnetics_from_converter(
438
- "flyback", flyback, 1, "available cores", True, None
439
- )
440
-
441
- if isinstance(result, dict) and "error" in result:
442
- print(f"Method A failed: {result['error']}")
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
- d = result[0]
446
- mas_obj, score = (d[0], d[1]) if isinstance(d, (list, tuple)) else (d, "N/A")
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
- assert "error" not in processed
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
- designs = PyOM.calculate_advised_magnetics(
484
- mas_inputs, 1, "available cores",
485
- {"efficiency": 2.0, "cost": 1.0, "dimensions": 0.5} # optional weights
486
- )
487
-
488
- # Step 3: Parse
489
- d = designs[0]
490
- mas_obj, score = (d[0], d[1]) if isinstance(d, (list, tuple)) else (d, "N/A")
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("E 25/13/7") # ✅ verified (.pyi confirmed)
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.calculate_inductance(magnetic)
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 at frequency
615
- delta = PyOM.calculate_effective_skin_depth("copper", 100000, 25)
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
- rdc = PyOM.calculate_dc_resistance_per_meter("copper", 0.5e-3, 25)
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-05-v1.6.4-mkf-dragback-adviser-thermal")
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 "2025-02-26-01")
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
- install(FILES __init__.py DESTINATION .)
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.6.6
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
- #### MKF is pinned to a SHA (build reproducibility — ABT #73)
60
+ #### Build provenance
61
61
 
62
- This build compiles MKF by **globbing its `.cpp` files directly** into the
63
- extension (it does not drive MKF's own CMake). MKF `main` has since moved to
64
- "delete `converter_models` + link the Kirchhoff converter-model library
65
- (`libKirchhoffApi.so`)"; because Kirchhoff and its AAS sibling are **not yet
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
- `CMakeLists.txt` therefore pins the MKF FetchContent to the last self-contained
70
- commit — `MKF_GIT_TAG=2ae859dcae6c3b2e5a128893247b7714360e863f`, the exact SHA
71
- the shipping `.so` was built from — and a configure-time guard fails loudly if
72
- the checkout drifts. A clean rebuild is reproducible as-is:
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 # or: cmake -S . -B build && ninja -C build
76
+ rm -rf build && pip install . --no-deps -v
76
77
  ```
77
78
 
78
- To **advance** the pin you must first wire the Kirchhoff library build into
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
- **Important:** The compiled extension module may require special import handling:
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 importlib.util
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
- See [AGENTS.md](AGENTS.md) for complete import instructions and troubleshooting.
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
- # Define core and operating point
194
- core_data = {...} # Your core definition
195
- operating_point = {
196
- "name": "Nominal",
197
- "conditions": {"ambientTemperature": 25},
198
- "excitationsPerWinding": [
199
- {
200
- "frequency": 100000,
201
- "magneticFluxDensity": {
202
- "processed": {
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
- losses = PyOpenMagnetics.calculate_core_losses(core_data, operating_point, "IGSE")
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
- # Define coil requirements
221
- coil_functional_description = [
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
- # Wind the coil on the core
237
- result = PyOpenMagnetics.wind(core_data, coil_functional_description, bobbin_data, [1, 1], [])
238
- print(f"Winding successful: {result.get('windingResult', 'unknown')}")
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
- ## Flyback Converter Wizard
272
+ ## Converter-Based Design
242
273
 
243
- PyOpenMagnetics includes a complete flyback converter design wizard. See `flyback.py` for a full example:
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
- from flyback import design_flyback, create_mas_inputs, get_advised_magnetics
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
- # Create inputs for PyOpenMagnetics
269
- inputs = create_mas_inputs(specs, design)
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
- # Get recommended magnetics
272
- magnetics = get_advised_magnetics(inputs, max_results=5)
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, operating_point, model)` | Calculate core losses |
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(core, coil, bobbin, pattern, layers)` | Wind coils on a core |
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