PyOpenMagnetics 1.4.6__tar.gz → 1.5.1__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 (85) hide show
  1. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/CMakeLists.txt +9 -1
  2. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/PKG-INFO +1 -1
  3. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/complete_simulation_example.py +1 -2
  4. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_220v_12v_1a.py +1 -2
  5. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_220v_12v_2a_complete.py +1 -2
  6. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_design.py +1 -2
  7. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/pyproject.toml +1 -1
  8. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/converter.cpp +248 -88
  9. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/converter.h +7 -6
  10. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/core.cpp +98 -0
  11. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/core.h +1 -0
  12. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/losses.cpp +0 -23
  13. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/losses.h +0 -1
  14. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/simulation.cpp +1 -6
  15. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_core.py +84 -0
  16. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/.github/workflows/ci.yml +0 -0
  17. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/.github/workflows/publish.yml +0 -0
  18. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/.gitignore +0 -0
  19. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/AGENTS.md +0 -0
  20. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/LICENSE +0 -0
  21. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/PyOpenMagnetics.pyi +0 -0
  22. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/README.md +0 -0
  23. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/__init__.py +0 -0
  24. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/api/MAS.py +0 -0
  25. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/api/mas_db_reader.py +0 -0
  26. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/api/validation.py +0 -0
  27. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/clear_cibuildwheel_cache.sh +0 -0
  28. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/docs/compatibility.md +0 -0
  29. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/docs/errors.md +0 -0
  30. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/docs/performance.md +0 -0
  31. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/README.md +0 -0
  32. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/buck_inductor.py +0 -0
  33. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/converter_design_example.py +0 -0
  34. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/debug_bobbin.py +0 -0
  35. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/debug_coil.py +0 -0
  36. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/debug_core.py +0 -0
  37. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/debug_plotting.py +0 -0
  38. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_bh_curve.png +0 -0
  39. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_core.png +0 -0
  40. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_summary.png +0 -0
  41. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/flyback_waveforms.png +0 -0
  42. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/list_plot_funcs.py +0 -0
  43. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/plot_flyback_design.py +0 -0
  44. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/plot_flyback_pyom.py +0 -0
  45. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/test_field_calc.py +0 -0
  46. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/examples/test_field_plot.py +0 -0
  47. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/force_fresh_build.sh +0 -0
  48. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/llms.txt +0 -0
  49. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/notebooks/01_getting_started.ipynb +0 -0
  50. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/notebooks/02_buck_inductor.ipynb +0 -0
  51. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/notebooks/03_core_losses.ipynb +0 -0
  52. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/notebooks/README.md +0 -0
  53. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/requirements.txt +0 -0
  54. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/advisers.cpp +0 -0
  55. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/advisers.h +0 -0
  56. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/bobbin.cpp +0 -0
  57. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/bobbin.h +0 -0
  58. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/common.h +0 -0
  59. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/database.cpp +0 -0
  60. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/database.h +0 -0
  61. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/logging.cpp +0 -0
  62. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/logging.h +0 -0
  63. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/module.cpp +0 -0
  64. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/plotting.cpp +0 -0
  65. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/plotting.h +0 -0
  66. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/settings.cpp +0 -0
  67. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/settings.h +0 -0
  68. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/simulation.h +0 -0
  69. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/utils.cpp +0 -0
  70. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/utils.h +0 -0
  71. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/winding.cpp +0 -0
  72. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/winding.h +0 -0
  73. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/wire.cpp +0 -0
  74. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/src/wire.h +0 -0
  75. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/test.py +0 -0
  76. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/__init__.py +0 -0
  77. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/conftest.py +0 -0
  78. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_converter_endpoints.py +0 -0
  79. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_core_adviser.py +0 -0
  80. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_examples_integration.py +0 -0
  81. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_inputs.py +0 -0
  82. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_logging.py +0 -0
  83. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_magnetic_adviser.py +0 -0
  84. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_plotting.py +0 -0
  85. {pyopenmagnetics-1.4.6 → pyopenmagnetics-1.5.1}/tests/test_winding.py +0 -0
@@ -218,7 +218,15 @@ add_custom_command(
218
218
  OUTPUT "${MAS_DIRECTORY}/MAS.hpp"
219
219
  # Force regeneration by removing output first
220
220
  COMMAND ${CMAKE_COMMAND} -E remove -f ${MAS_DIRECTORY}/MAS.hpp
221
- COMMAND quicktype -l c++ -s schema ${MAS_DIR}/schemas/MAS.json
221
+ COMMAND quicktype -l c++ -s schema ${MAS_DIR}/schemas/MAS.json
222
+ # PEAS schemas: MAS main ($id https://psma.com/mas/...) now $refs shared
223
+ # PEAS primitives (https://psma.com/peas/...). quicktype resolves those
224
+ # external $refs by $id from sources passed with -S, so the PEAS schemas
225
+ # must be fed in here. PEAS is an MKF submodule at ${MKF_DIR}/PEAS.
226
+ -S ${MKF_DIR}/PEAS/schemas/utils.json
227
+ -S ${MKF_DIR}/PEAS/schemas/inputs/operatingPointExcitation.json
228
+ -S ${MKF_DIR}/PEAS/schemas/outputs/insulationCoordination.json
229
+ -S ${MKF_DIR}/PEAS/schemas/outputs/lossElementPerHarmonic.json
222
230
  -S ${MAS_DIR}/schemas/magnetic.json
223
231
  -S ${MAS_DIR}/schemas/magnetic/core.json
224
232
  -S ${MAS_DIR}/schemas/magnetic/coil.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyOpenMagnetics
3
- Version: 1.4.6
3
+ Version: 1.5.1
4
4
  Summary: Python wrapper for OpenMagnetics
5
5
  Author-Email: Alfonso Martinez <Alfonso_VII@hotmail.com>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -179,8 +179,7 @@ def simulate_losses(magnetic, specs):
179
179
  print("\n[1] Calculating core losses...")
180
180
  models = {
181
181
  "coreLosses": "IGSE",
182
- "reluctance": "ZHANG",
183
- "coreTemperature": "MANIKTALA"
182
+ "reluctance": "ZHANG"
184
183
  }
185
184
 
186
185
  try:
@@ -287,8 +287,7 @@ def design_flyback_transformer():
287
287
 
288
288
  models = {
289
289
  "coreLosses": "IGSE",
290
- "reluctance": "ZHANG",
291
- "coreTemperature": "MANIKTALA"
290
+ "reluctance": "ZHANG"
292
291
  }
293
292
 
294
293
  best_design = None
@@ -301,8 +301,7 @@ def design_flyback_transformer():
301
301
 
302
302
  models = {
303
303
  "coreLosses": "IGSE",
304
- "reluctance": "ZHANG",
305
- "coreTemperature": "MANIKTALA"
304
+ "reluctance": "ZHANG"
306
305
  }
307
306
 
308
307
  best_design = None
@@ -127,8 +127,7 @@ def design_flyback_transformer():
127
127
 
128
128
  models = {
129
129
  "coreLosses": "IGSE",
130
- "reluctance": "ZHANG",
131
- "coreTemperature": "MANIKTALA"
130
+ "reluctance": "ZHANG"
132
131
  }
133
132
 
134
133
  for i, item in enumerate(magnetics):
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "PyOpenMagnetics"
7
- version = "1.4.6"
7
+ version = "1.5.1"
8
8
  requires-python = ">=3.8"
9
9
  authors = [
10
10
  { name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
@@ -149,7 +149,75 @@ static std::string normalize_topology_name(const std::string& s) {
149
149
  return s;
150
150
  }
151
151
 
152
+ // ─────────────────────────────────────────────────────────────────────
153
+ // Base-spec fast path (ABT #11)
154
+ //
155
+ // The fast path normally constructs an Advanced* converter whose from_json
156
+ // hard-requires `desiredInductance` (e.g. AdvancedBuck, MKF Buck.h:219). A
157
+ // caller passing a BASE spec (operating point + currentRippleRatio, no
158
+ // desiredInductance) would otherwise get
159
+ // `[json.exception.out_of_range.403] key 'desiredInductance' not found`.
160
+ //
161
+ // When `desiredInductance` is absent we instead construct the BASE converter
162
+ // class, whose virtual process_design_requirements() DERIVES the inductance
163
+ // from the operating points + currentRippleRatio (or maximumSwitchCurrent) —
164
+ // the same MKF math the slow path uses. No L-derivation is duplicated here;
165
+ // it all stays in MKF (house rule). If neither desiredInductance nor a ripple
166
+ // driver is present, the base class throws a loud, specific exception rather
167
+ // than substituting a default.
168
+ //
169
+ // Two helpers cover the two simulate signatures: single-inductor topologies
170
+ // (Buck/Boost/Cuk/…) take a scalar inductance; transformer topologies
171
+ // (Flyback/Forward family/IsolatedBuck/…) take (turnsRatios, inductance).
172
+ // ─────────────────────────────────────────────────────────────────────
173
+ template <typename BaseConverter>
174
+ OpenMagnetics::Inputs process_base_spec_scalar_L(const json& converterJson, bool useNgspice, MAS::Topologies topology) {
175
+ BaseConverter converter(converterJson);
176
+ converter._assertErrors = true;
177
+ if (useNgspice) {
178
+ auto designReqs = converter.process_design_requirements();
179
+ double inductance = OpenMagnetics::resolve_dimensional_values(designReqs.get_magnetizing_inductance());
180
+ auto operatingPoints = converter.simulate_and_extract_operating_points(inductance);
181
+ OpenMagnetics::Inputs result;
182
+ result.set_design_requirements(designReqs);
183
+ result.set_operating_points(operatingPoints);
184
+ result.get_mutable_design_requirements().set_topology(topology);
185
+ return result;
186
+ } else {
187
+ OpenMagnetics::Inputs result = converter.process();
188
+ result.get_mutable_design_requirements().set_topology(topology);
189
+ return result;
190
+ }
191
+ }
192
+
193
+ template <typename BaseConverter>
194
+ OpenMagnetics::Inputs process_base_spec_vector_L(const json& converterJson, bool useNgspice, MAS::Topologies topology) {
195
+ BaseConverter converter(converterJson);
196
+ converter._assertErrors = true;
197
+ if (useNgspice) {
198
+ auto designReqs = converter.process_design_requirements();
199
+ std::vector<double> turnsRatios;
200
+ for (const auto& tr : designReqs.get_turns_ratios()) {
201
+ turnsRatios.push_back(OpenMagnetics::resolve_dimensional_values(tr));
202
+ }
203
+ double inductance = OpenMagnetics::resolve_dimensional_values(designReqs.get_magnetizing_inductance());
204
+ auto operatingPoints = converter.simulate_and_extract_operating_points(turnsRatios, inductance);
205
+ OpenMagnetics::Inputs result;
206
+ result.set_design_requirements(designReqs);
207
+ result.set_operating_points(operatingPoints);
208
+ result.get_mutable_design_requirements().set_topology(topology);
209
+ return result;
210
+ } else {
211
+ OpenMagnetics::Inputs result = converter.process();
212
+ result.get_mutable_design_requirements().set_topology(topology);
213
+ return result;
214
+ }
215
+ }
216
+
152
217
  OpenMagnetics::Inputs process_flyback_internal(const json& converterJson, bool useNgspice) {
218
+ if (!converterJson.contains("desiredInductance")) {
219
+ return process_base_spec_vector_L<OpenMagnetics::Flyback>(converterJson, useNgspice, MAS::Topologies::FLYBACK_CONVERTER);
220
+ }
153
221
  OpenMagnetics::AdvancedFlyback converter(converterJson);
154
222
  converter._assertErrors = true;
155
223
 
@@ -184,6 +252,9 @@ OpenMagnetics::Inputs process_flyback_internal(const json& converterJson, bool u
184
252
  }
185
253
 
186
254
  OpenMagnetics::Inputs process_buck_internal(const json& converterJson, bool useNgspice) {
255
+ if (!converterJson.contains("desiredInductance")) {
256
+ return process_base_spec_scalar_L<OpenMagnetics::Buck>(converterJson, useNgspice, MAS::Topologies::BUCK_CONVERTER);
257
+ }
187
258
  OpenMagnetics::AdvancedBuck converter(converterJson);
188
259
  converter._assertErrors = true;
189
260
 
@@ -204,6 +275,9 @@ OpenMagnetics::Inputs process_buck_internal(const json& converterJson, bool useN
204
275
  }
205
276
 
206
277
  OpenMagnetics::Inputs process_boost_internal(const json& converterJson, bool useNgspice) {
278
+ if (!converterJson.contains("desiredInductance")) {
279
+ return process_base_spec_scalar_L<OpenMagnetics::Boost>(converterJson, useNgspice, MAS::Topologies::BOOST_CONVERTER);
280
+ }
207
281
  OpenMagnetics::AdvancedBoost converter(converterJson);
208
282
  converter._assertErrors = true;
209
283
 
@@ -224,6 +298,9 @@ OpenMagnetics::Inputs process_boost_internal(const json& converterJson, bool use
224
298
  }
225
299
 
226
300
  OpenMagnetics::Inputs process_single_switch_forward_internal(const json& converterJson, bool useNgspice) {
301
+ if (!converterJson.contains("desiredInductance")) {
302
+ return process_base_spec_vector_L<OpenMagnetics::SingleSwitchForward>(converterJson, useNgspice, MAS::Topologies::SINGLE_SWITCH_FORWARD_CONVERTER);
303
+ }
227
304
  OpenMagnetics::AdvancedSingleSwitchForward converter(converterJson);
228
305
  converter._assertErrors = true;
229
306
 
@@ -248,6 +325,9 @@ OpenMagnetics::Inputs process_single_switch_forward_internal(const json& convert
248
325
  }
249
326
 
250
327
  OpenMagnetics::Inputs process_two_switch_forward_internal(const json& converterJson, bool useNgspice) {
328
+ if (!converterJson.contains("desiredInductance")) {
329
+ return process_base_spec_vector_L<OpenMagnetics::TwoSwitchForward>(converterJson, useNgspice, MAS::Topologies::TWO_SWITCH_FORWARD_CONVERTER);
330
+ }
251
331
  OpenMagnetics::AdvancedTwoSwitchForward converter(converterJson);
252
332
  converter._assertErrors = true;
253
333
 
@@ -272,6 +352,9 @@ OpenMagnetics::Inputs process_two_switch_forward_internal(const json& converterJ
272
352
  }
273
353
 
274
354
  OpenMagnetics::Inputs process_active_clamp_forward_internal(const json& converterJson, bool useNgspice) {
355
+ if (!converterJson.contains("desiredInductance")) {
356
+ return process_base_spec_vector_L<OpenMagnetics::ActiveClampForward>(converterJson, useNgspice, MAS::Topologies::ACTIVE_CLAMP_FORWARD_CONVERTER);
357
+ }
275
358
  OpenMagnetics::AdvancedActiveClampForward converter(converterJson);
276
359
  converter._assertErrors = true;
277
360
 
@@ -296,6 +379,9 @@ OpenMagnetics::Inputs process_active_clamp_forward_internal(const json& converte
296
379
  }
297
380
 
298
381
  OpenMagnetics::Inputs process_push_pull_internal(const json& converterJson, bool useNgspice) {
382
+ if (!converterJson.contains("desiredInductance")) {
383
+ return process_base_spec_vector_L<OpenMagnetics::PushPull>(converterJson, useNgspice, MAS::Topologies::PUSH_PULL_CONVERTER);
384
+ }
299
385
  OpenMagnetics::AdvancedPushPull converter(converterJson);
300
386
  converter._assertErrors = true;
301
387
 
@@ -428,6 +514,9 @@ OpenMagnetics::Inputs process_pshb_internal(const json& converterJson, bool useN
428
514
  }
429
515
 
430
516
  OpenMagnetics::Inputs process_isolated_buck_internal(const json& converterJson, bool useNgspice) {
517
+ if (!converterJson.contains("desiredInductance")) {
518
+ return process_base_spec_vector_L<OpenMagnetics::IsolatedBuck>(converterJson, useNgspice, MAS::Topologies::ISOLATED_BUCK_CONVERTER);
519
+ }
431
520
  OpenMagnetics::AdvancedIsolatedBuck converter(converterJson);
432
521
  converter._assertErrors = true;
433
522
 
@@ -452,6 +541,9 @@ OpenMagnetics::Inputs process_isolated_buck_internal(const json& converterJson,
452
541
  }
453
542
 
454
543
  OpenMagnetics::Inputs process_isolated_buck_boost_internal(const json& converterJson, bool useNgspice) {
544
+ if (!converterJson.contains("desiredInductance")) {
545
+ return process_base_spec_vector_L<OpenMagnetics::IsolatedBuckBoost>(converterJson, useNgspice, MAS::Topologies::ISOLATED_BUCK_BOOST_CONVERTER);
546
+ }
455
547
  OpenMagnetics::AdvancedIsolatedBuckBoost converter(converterJson);
456
548
  converter._assertErrors = true;
457
549
 
@@ -511,6 +603,9 @@ OpenMagnetics::Inputs process_pfc_internal(const json& converterJson) {
511
603
  // ─────────────────────────────────────────────────────────────────────
512
604
 
513
605
  OpenMagnetics::Inputs process_cuk_internal(const json& converterJson, bool useNgspice) {
606
+ if (!converterJson.contains("desiredInductance")) {
607
+ return process_base_spec_scalar_L<OpenMagnetics::Cuk>(converterJson, useNgspice, MAS::Topologies::CUK_CONVERTER);
608
+ }
514
609
  OpenMagnetics::AdvancedCuk converter(converterJson);
515
610
  converter._assertErrors = true;
516
611
  if (useNgspice) {
@@ -530,6 +625,9 @@ OpenMagnetics::Inputs process_cuk_internal(const json& converterJson, bool useNg
530
625
  }
531
626
 
532
627
  OpenMagnetics::Inputs process_sepic_internal(const json& converterJson, bool useNgspice) {
628
+ if (!converterJson.contains("desiredInductance")) {
629
+ return process_base_spec_scalar_L<OpenMagnetics::Sepic>(converterJson, useNgspice, MAS::Topologies::SEPIC_CONVERTER);
630
+ }
533
631
  OpenMagnetics::AdvancedSepic converter(converterJson);
534
632
  converter._assertErrors = true;
535
633
  if (useNgspice) {
@@ -549,6 +647,9 @@ OpenMagnetics::Inputs process_sepic_internal(const json& converterJson, bool use
549
647
  }
550
648
 
551
649
  OpenMagnetics::Inputs process_zeta_internal(const json& converterJson, bool useNgspice) {
650
+ if (!converterJson.contains("desiredInductance")) {
651
+ return process_base_spec_scalar_L<OpenMagnetics::Zeta>(converterJson, useNgspice, MAS::Topologies::ZETA_CONVERTER);
652
+ }
552
653
  OpenMagnetics::AdvancedZeta converter(converterJson);
553
654
  converter._assertErrors = true;
554
655
  if (useNgspice) {
@@ -568,6 +669,9 @@ OpenMagnetics::Inputs process_zeta_internal(const json& converterJson, bool useN
568
669
  }
569
670
 
570
671
  OpenMagnetics::Inputs process_four_switch_buck_boost_internal(const json& converterJson, bool useNgspice) {
672
+ if (!converterJson.contains("desiredInductance")) {
673
+ return process_base_spec_scalar_L<OpenMagnetics::FourSwitchBuckBoost>(converterJson, useNgspice, MAS::Topologies::FOUR_SWITCH_BUCK_BOOST_CONVERTER);
674
+ }
571
675
  OpenMagnetics::AdvancedFourSwitchBuckBoost converter(converterJson);
572
676
  converter._assertErrors = true;
573
677
  if (useNgspice) {
@@ -610,6 +714,35 @@ OpenMagnetics::Inputs process_asymmetric_half_bridge_internal(const json& conver
610
714
  }
611
715
 
612
716
  OpenMagnetics::Inputs process_weinberg_internal(const json& converterJson, bool useNgspice) {
717
+ // Base-spec fast path (ABT #11): derive L from the base Weinberg when no
718
+ // desiredInductance is supplied. Inlined (not via the shared helper)
719
+ // because Weinberg's simulate signature takes a SCALAR turns ratio
720
+ // (single-secondary topology), not the vector the other transformers use.
721
+ if (!converterJson.contains("desiredInductance")) {
722
+ OpenMagnetics::Weinberg converter(converterJson);
723
+ converter._assertErrors = true;
724
+ if (useNgspice) {
725
+ auto designReqs = converter.process_design_requirements();
726
+ std::vector<double> turnsRatios;
727
+ for (const auto& tr : designReqs.get_turns_ratios()) {
728
+ turnsRatios.push_back(OpenMagnetics::resolve_dimensional_values(tr));
729
+ }
730
+ if (turnsRatios.empty()) {
731
+ throw std::runtime_error("Weinberg converter requires at least one turns ratio");
732
+ }
733
+ double inductance = OpenMagnetics::resolve_dimensional_values(designReqs.get_magnetizing_inductance());
734
+ auto operatingPoints = converter.simulate_and_extract_operating_points(turnsRatios[0], inductance);
735
+ OpenMagnetics::Inputs result;
736
+ result.set_design_requirements(designReqs);
737
+ result.set_operating_points(operatingPoints);
738
+ result.get_mutable_design_requirements().set_topology(MAS::Topologies::WEINBERG_CONVERTER);
739
+ return result;
740
+ } else {
741
+ OpenMagnetics::Inputs result = converter.process();
742
+ result.get_mutable_design_requirements().set_topology(MAS::Topologies::WEINBERG_CONVERTER);
743
+ return result;
744
+ }
745
+ }
613
746
  OpenMagnetics::AdvancedWeinberg converter(converterJson);
614
747
  converter._assertErrors = true;
615
748
  if (useNgspice) {
@@ -788,16 +921,50 @@ json process_converter(const std::string& topologyName, json converterJson, bool
788
921
  return process_converter_internal(normalized, converterJson, useNgspice);
789
922
  }
790
923
 
924
+ // Build candidate magnetics from a converter object via the MKF template
925
+ // method, threading the `fast` flag (fast core-only adviser vs full
926
+ // winding+simulation adviser) and optional per-filter weights.
927
+ template <typename ConverterType>
928
+ static std::vector<std::pair<OpenMagnetics::Mas, double>> advise_from_converter_spec(
929
+ const json& converterJson, bool fast,
930
+ const std::map<OpenMagnetics::MagneticFilters, double>& weights,
931
+ int maxResults, OpenMagnetics::MagneticAdviser& adviser) {
932
+ ConverterType converter(converterJson);
933
+ converter._assertErrors = true;
934
+ return weights.empty()
935
+ ? adviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
936
+ : adviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
937
+ }
938
+
939
+ // Single-inductor, non-isolated family (Buck/Boost/Cuk/Sepic/Zeta/
940
+ // FourSwitchBuckBoost). Their Advanced* class requires ONLY desiredInductance
941
+ // on top of a plain converter spec, so we pick the model purely on the
942
+ // presence of that key: Advanced* when the user pins L (honoured as the
943
+ // nominal target), Base when absent (L derived from the ripple budget in
944
+ // MKF). Transformer/resonant families do NOT get this duality — their
945
+ // Advanced* from_json also requires desiredTurnsRatios (Flyback also
946
+ // desiredDutyCycle), i.e. design *outputs* a converter spec does not carry —
947
+ // so they always use the Base model, which derives turns + L itself.
948
+ template <typename Base, typename Advanced>
949
+ static std::vector<std::pair<OpenMagnetics::Mas, double>> advise_inductor_family(
950
+ const json& converterJson, bool fast,
951
+ const std::map<OpenMagnetics::MagneticFilters, double>& weights,
952
+ int maxResults, OpenMagnetics::MagneticAdviser& adviser) {
953
+ return converterJson.contains("desiredInductance")
954
+ ? advise_from_converter_spec<Advanced>(converterJson, fast, weights, maxResults, adviser)
955
+ : advise_from_converter_spec<Base>(converterJson, fast, weights, maxResults, adviser);
956
+ }
957
+
791
958
  // Simplified design_magnetics_from_converter using MKF template method
792
959
  json design_magnetics_from_converter(
793
960
  const std::string& topologyNameRaw,
794
- json converterJson,
795
- int maxResults,
796
- json coreModeJson,
797
- bool useNgspice,
798
- json weightsJson) {
799
-
800
- (void)useNgspice; // Template method always uses ngspice
961
+ json converterJson,
962
+ int maxResults,
963
+ json coreModeJson,
964
+ bool useNgspice,
965
+ json weightsJson,
966
+ bool fast) {
967
+
801
968
 
802
969
  // Accept MAS 1.0 camelCase, pre-1.0 Title Case, or internal short form.
803
970
  const std::string topologyName = normalize_topology_name(topologyNameRaw);
@@ -821,125 +988,109 @@ json design_magnetics_from_converter(
821
988
 
822
989
  std::vector<std::pair<OpenMagnetics::Mas, double>> masMagnetics;
823
990
 
824
- // Use MKF template method - handles all converters automatically.
825
- // For Buck / Boost the basic process_design_requirements() sets
826
- // only `magnetizingInductance.minimum` (a ripple-floor), and
827
- // the CoreAdviser picks the smallest core satisfying it
828
- // regardless of the L the user asked for. Dispatch to the
829
- // Advanced* subclass so spec.desiredInductance becomes the
830
- // nominal target (AdvancedBuck/Boost::process_design_requirements
831
- // override does this, MKF commit 8acd72c7).
991
+ // Build candidate magnetics from the converter spec via the MKF
992
+ // template method. Two independent axes (4 behaviours total):
832
993
  //
833
- // Flyback stays on the basic ctor: AdvancedFlyback's from_json
834
- // requires extra fields (desiredDutyCycle, desiredTurnsRatios)
835
- // that aren't in a normal converter spec. The basic
836
- // Flyback::process_design_requirements already sets nominal
837
- // from its V·s + duty-cycle derivation, so the magnetic gets
838
- // sized correctly without the Advanced* path.
994
+ // * Base vs Advanced* model chosen PER TOPOLOGY:
995
+ // - Single-inductor family (Buck/Boost/Cuk/Sepic/Zeta/
996
+ // FourSwitchBuckBoost): advise_inductor_family() picks
997
+ // Advanced* when desiredInductance is present (honoured as the
998
+ // nominal L target), Base otherwise (L derived from the ripple
999
+ // budget). This is ABT #11 — the slow path used to force
1000
+ // Advanced for Buck/Boost and throw on a base spec.
1001
+ // - Transformer/resonant families: always Base. Their Advanced*
1002
+ // from_json requires desiredTurnsRatios (Flyback also
1003
+ // desiredDutyCycle) — design outputs a converter spec lacks —
1004
+ // so only Base is constructible; it derives turns + L itself.
1005
+ // * fast flag — fast core-only adviser vs full winding+sim adviser,
1006
+ // threaded into get_advised_magnetic_from_converter for every
1007
+ // topology.
839
1008
  if (topologyName == "flyback" || topologyName == "advanced_flyback") {
840
1009
  OpenMagnetics::Flyback converter(converterJson);
841
1010
  converter._assertErrors = true;
842
1011
  masMagnetics = weights.empty()
843
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
844
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1012
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1013
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
845
1014
  }
846
1015
  else if (topologyName == "buck" || topologyName == "advanced_buck") {
847
- OpenMagnetics::AdvancedBuck converter(converterJson);
848
- converter._assertErrors = true;
849
- masMagnetics = weights.empty()
850
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
851
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1016
+ masMagnetics = advise_inductor_family<OpenMagnetics::Buck, OpenMagnetics::AdvancedBuck>(
1017
+ converterJson, fast, weights, maxResults, magneticAdviser);
852
1018
  }
853
1019
  else if (topologyName == "boost" || topologyName == "advanced_boost") {
854
- OpenMagnetics::AdvancedBoost converter(converterJson);
855
- converter._assertErrors = true;
856
- masMagnetics = weights.empty()
857
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
858
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1020
+ masMagnetics = advise_inductor_family<OpenMagnetics::Boost, OpenMagnetics::AdvancedBoost>(
1021
+ converterJson, fast, weights, maxResults, magneticAdviser);
859
1022
  }
860
1023
  else if (topologyName == "single_switch_forward") {
861
1024
  OpenMagnetics::SingleSwitchForward converter(converterJson);
862
1025
  converter._assertErrors = true;
863
1026
  masMagnetics = weights.empty()
864
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
865
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1027
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1028
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
866
1029
  }
867
1030
  else if (topologyName == "two_switch_forward") {
868
1031
  OpenMagnetics::TwoSwitchForward converter(converterJson);
869
1032
  converter._assertErrors = true;
870
1033
  masMagnetics = weights.empty()
871
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
872
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1034
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1035
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
873
1036
  }
874
1037
  else if (topologyName == "active_clamp_forward") {
875
1038
  OpenMagnetics::ActiveClampForward converter(converterJson);
876
1039
  converter._assertErrors = true;
877
1040
  masMagnetics = weights.empty()
878
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
879
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1041
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1042
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
880
1043
  }
881
1044
  else if (topologyName == "push_pull") {
882
1045
  OpenMagnetics::PushPull converter(converterJson);
883
1046
  converter._assertErrors = true;
884
1047
  masMagnetics = weights.empty()
885
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
886
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1048
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1049
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
887
1050
  }
888
1051
  else if (topologyName == "isolated_buck") {
889
1052
  OpenMagnetics::IsolatedBuck converter(converterJson);
890
1053
  converter._assertErrors = true;
891
1054
  masMagnetics = weights.empty()
892
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
893
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1055
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1056
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
894
1057
  }
895
1058
  else if (topologyName == "isolated_buck_boost") {
896
1059
  OpenMagnetics::IsolatedBuckBoost converter(converterJson);
897
1060
  converter._assertErrors = true;
898
1061
  masMagnetics = weights.empty()
899
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
900
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1062
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1063
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
901
1064
  }
902
1065
  else if (topologyName == "llc" || topologyName == "advanced_llc") {
903
1066
  OpenMagnetics::Llc converter(converterJson);
904
1067
  converter._assertErrors = true;
905
1068
  masMagnetics = weights.empty()
906
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
907
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1069
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1070
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
908
1071
  }
909
1072
  else if (topologyName == "cuk" || topologyName == "advanced_cuk") {
910
- OpenMagnetics::Cuk converter(converterJson);
911
- converter._assertErrors = true;
912
- masMagnetics = weights.empty()
913
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
914
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1073
+ masMagnetics = advise_inductor_family<OpenMagnetics::Cuk, OpenMagnetics::AdvancedCuk>(
1074
+ converterJson, fast, weights, maxResults, magneticAdviser);
915
1075
  }
916
1076
  else if (topologyName == "sepic" || topologyName == "advanced_sepic") {
917
- OpenMagnetics::Sepic converter(converterJson);
918
- converter._assertErrors = true;
919
- masMagnetics = weights.empty()
920
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
921
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1077
+ masMagnetics = advise_inductor_family<OpenMagnetics::Sepic, OpenMagnetics::AdvancedSepic>(
1078
+ converterJson, fast, weights, maxResults, magneticAdviser);
922
1079
  }
923
1080
  else if (topologyName == "zeta" || topologyName == "advanced_zeta") {
924
- OpenMagnetics::Zeta converter(converterJson);
925
- converter._assertErrors = true;
926
- masMagnetics = weights.empty()
927
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
928
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1081
+ masMagnetics = advise_inductor_family<OpenMagnetics::Zeta, OpenMagnetics::AdvancedZeta>(
1082
+ converterJson, fast, weights, maxResults, magneticAdviser);
929
1083
  }
930
1084
  else if (topologyName == "four_switch_buck_boost" || topologyName == "advanced_four_switch_buck_boost") {
931
- OpenMagnetics::FourSwitchBuckBoost converter(converterJson);
932
- converter._assertErrors = true;
933
- masMagnetics = weights.empty()
934
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
935
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1085
+ masMagnetics = advise_inductor_family<OpenMagnetics::FourSwitchBuckBoost, OpenMagnetics::AdvancedFourSwitchBuckBoost>(
1086
+ converterJson, fast, weights, maxResults, magneticAdviser);
936
1087
  }
937
1088
  else if (topologyName == "asymmetric_half_bridge" || topologyName == "advanced_asymmetric_half_bridge") {
938
1089
  OpenMagnetics::AsymmetricHalfBridge converter(converterJson);
939
1090
  converter._assertErrors = true;
940
1091
  masMagnetics = weights.empty()
941
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
942
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1092
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1093
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
943
1094
  }
944
1095
  else if (topologyName == "phase_shifted_full_bridge" || topologyName == "psfb" ||
945
1096
  topologyName == "advanced_phase_shifted_full_bridge" || topologyName == "advanced_psfb") {
@@ -952,36 +1103,36 @@ json design_magnetics_from_converter(
952
1103
  OpenMagnetics::Psfb converter(converterJson);
953
1104
  converter._assertErrors = true;
954
1105
  masMagnetics = weights.empty()
955
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
956
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1106
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1107
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
957
1108
  }
958
1109
  else if (topologyName == "weinberg" || topologyName == "advanced_weinberg") {
959
1110
  OpenMagnetics::Weinberg converter(converterJson);
960
1111
  converter._assertErrors = true;
961
1112
  masMagnetics = weights.empty()
962
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
963
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1113
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1114
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
964
1115
  }
965
1116
  else if (topologyName == "vienna" || topologyName == "advanced_vienna") {
966
1117
  OpenMagnetics::Vienna converter(converterJson);
967
1118
  converter._assertErrors = true;
968
1119
  masMagnetics = weights.empty()
969
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
970
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1120
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1121
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
971
1122
  }
972
1123
  else if (topologyName == "clllc" || topologyName == "advanced_clllc") {
973
1124
  OpenMagnetics::Clllc converter(converterJson);
974
1125
  converter._assertErrors = true;
975
1126
  masMagnetics = weights.empty()
976
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
977
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1127
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1128
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
978
1129
  }
979
1130
  else if (topologyName == "src" || topologyName == "advanced_src") {
980
1131
  OpenMagnetics::Src converter(converterJson);
981
1132
  converter._assertErrors = true;
982
1133
  masMagnetics = weights.empty()
983
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
984
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1134
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1135
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
985
1136
  }
986
1137
  else if (topologyName == "dab" || topologyName == "advanced_dab" ||
987
1138
  topologyName == "dual_active_bridge") {
@@ -992,18 +1143,26 @@ json design_magnetics_from_converter(
992
1143
  OpenMagnetics::Dab converter(converterJson);
993
1144
  converter._assertErrors = true;
994
1145
  masMagnetics = weights.empty()
995
- ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults)
996
- : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults);
1146
+ ? magneticAdviser.get_advised_magnetic_from_converter(converter, maxResults, fast)
1147
+ : magneticAdviser.get_advised_magnetic_from_converter(converter, weights, maxResults, fast);
997
1148
  }
998
1149
  else {
999
- // Fall back to old approach for other topologies
1150
+ // Fall back for topologies without a converter-model adviser path
1151
+ // (e.g. PFC, current transformer). dispatch_converter / the
1152
+ // process_*_internal it calls already pick Base vs Advanced on the
1153
+ // presence of desiredInductance (ABT #11), so the Base/Advanced
1154
+ // behaviour is preserved here too; only the adviser stage differs.
1000
1155
  json inputsJson = process_converter_internal(topologyName, converterJson, useNgspice);
1001
1156
  if (inputsJson.contains("error")) return inputsJson;
1002
-
1157
+
1003
1158
  OpenMagnetics::Inputs inputs(inputsJson);
1004
- masMagnetics = weights.empty()
1005
- ? magneticAdviser.get_advised_magnetic(inputs, maxResults)
1006
- : magneticAdviser.get_advised_magnetic(inputs, weights, maxResults);
1159
+ if (fast) {
1160
+ masMagnetics = magneticAdviser.get_advised_magnetic_fast(inputs, maxResults);
1161
+ } else if (weights.empty()) {
1162
+ masMagnetics = magneticAdviser.get_advised_magnetic(inputs, maxResults);
1163
+ } else {
1164
+ masMagnetics = magneticAdviser.get_advised_magnetic(inputs, weights, maxResults);
1165
+ }
1007
1166
  }
1008
1167
 
1009
1168
  // Build results (same as before)
@@ -3944,9 +4103,10 @@ void register_converter_bindings(py::module& m) {
3944
4103
 
3945
4104
  m.def("design_magnetics_from_converter", &design_magnetics_from_converter,
3946
4105
  "Design magnetic components from a converter specification.",
3947
- py::arg("topology_name"), py::arg("converter_json"),
4106
+ py::arg("topology_name"), py::arg("converter_json"),
3948
4107
  py::arg("max_results") = 1, py::arg("core_mode_json") = "available cores",
3949
- py::arg("use_ngspice") = true, py::arg("weights_json") = nullptr);
4108
+ py::arg("use_ngspice") = true, py::arg("weights_json") = nullptr,
4109
+ py::arg("fast") = false);
3950
4110
 
3951
4111
  m.def("process_flyback", &process_flyback, "Process Flyback converter.", py::arg("flyback"));
3952
4112
  m.def("process_buck", &process_buck, "Process Buck converter.", py::arg("buck"));
@@ -9,12 +9,13 @@ json process_converter(const std::string& topologyName, json converterJson, bool
9
9
 
10
10
  // Combined endpoint: converter -> magnetic designs
11
11
  json design_magnetics_from_converter(
12
- const std::string& topologyName,
13
- json converterJson,
14
- int maxResults,
15
- json coreModeJson,
16
- bool useNgspice = true,
17
- json weightsJson = nullptr);
12
+ const std::string& topologyName,
13
+ json converterJson,
14
+ int maxResults,
15
+ json coreModeJson,
16
+ bool useNgspice = true,
17
+ json weightsJson = nullptr,
18
+ bool fast = false);
18
19
 
19
20
  // Per-topology thin wrappers (from .pyi stubs)
20
21
  json process_flyback(json flybackJson);
@@ -430,6 +430,70 @@ double calculate_saturation_current_at_operating_point(
430
430
  return magnetic.calculate_saturation_current(op, temperature);
431
431
  }
432
432
 
433
+ double calculate_peak_winding_current(json magneticJson, json operatingPointJson, int windingIndex) {
434
+ OpenMagnetics::Magnetic magnetic(magneticJson);
435
+ MAS::OperatingPoint op;
436
+ from_json(operatingPointJson, op);
437
+
438
+ // Operating-point magnetizing inductance of the as-designed magnetic
439
+ // (gapped circuit + DC-bias rolloff). Same L the saturation-current
440
+ // accessor uses, so I_peak and I_sat stay on a consistent footing.
441
+ OpenMagnetics::MagnetizingInductance magnetizingInductanceCalc;
442
+ auto inductanceOutput =
443
+ magnetizingInductanceCalc.calculate_inductance_from_number_turns_and_gapping(
444
+ magnetic.get_mutable_core(), magnetic.get_mutable_coil(), &op);
445
+ auto inductanceNominal = inductanceOutput.get_magnetizing_inductance().get_nominal();
446
+ if (!inductanceNominal.has_value() || inductanceNominal.value() <= 0) {
447
+ throw std::runtime_error(
448
+ "calculate_peak_winding_current: operating-point magnetizing "
449
+ "inductance is missing or non-positive — cannot derive the "
450
+ "magnetizing current.");
451
+ }
452
+ double magnetizingInductance = inductanceNominal.value();
453
+
454
+ auto excitations = op.get_mutable_excitations_per_winding();
455
+ if (excitations.empty()) {
456
+ throw std::runtime_error(
457
+ "calculate_peak_winding_current: operating point has no winding "
458
+ "excitations.");
459
+ }
460
+
461
+ // The magnetizing (flux-driving) current is referred to the primary
462
+ // (winding 0): I_mag = (1/L) ∫ v_primary dt, derived from the primary
463
+ // excitation's voltage. addOffset=true folds the operating DC bias of
464
+ // the primary current back in, so the returned peak is the saturation-
465
+ // driving peak (DC + ripple), not just the AC ripple amplitude.
466
+ auto primaryExcitation = excitations[0];
467
+ auto magnetizingCurrent = OpenMagnetics::Inputs::calculate_magnetizing_current(
468
+ primaryExcitation, magnetizingInductance, true, true);
469
+ auto processed = magnetizingCurrent.get_processed();
470
+ if (!processed.has_value() || !processed->get_peak().has_value()) {
471
+ throw std::runtime_error(
472
+ "calculate_peak_winding_current: computed magnetizing current has "
473
+ "no processed peak.");
474
+ }
475
+ double primaryPeak = processed->get_peak().value();
476
+
477
+ if (windingIndex == 0) {
478
+ return primaryPeak;
479
+ }
480
+
481
+ // Saturation is a single-flux core property; the magnetizing current
482
+ // referred to winding i is the primary-referred value scaled by the
483
+ // turns ratio: I_mag_i = I_mag_0 · N_0 / N_i.
484
+ double numberTurnsPrimary =
485
+ static_cast<double>(magnetic.get_mutable_coil().get_number_turns(0));
486
+ double numberTurnsWinding =
487
+ static_cast<double>(magnetic.get_mutable_coil().get_number_turns(windingIndex));
488
+ if (numberTurnsWinding <= 0) {
489
+ throw std::runtime_error(
490
+ "calculate_peak_winding_current: winding " +
491
+ std::to_string(windingIndex) + " has no turns — cannot refer the "
492
+ "magnetizing current to it.");
493
+ }
494
+ return primaryPeak * numberTurnsPrimary / numberTurnsWinding;
495
+ }
496
+
433
497
  double calculate_temperature_from_core_thermal_resistance(json coreJson, double totalLosses) {
434
498
  OpenMagnetics::Core core(coreJson);
435
499
 
@@ -1073,6 +1137,40 @@ void register_core_bindings(py::module& m) {
1073
1137
  py::arg("magnetic_json"), py::arg("operating_point_json"),
1074
1138
  py::arg("temperature"));
1075
1139
 
1140
+ m.def("calculate_peak_winding_current",
1141
+ &calculate_peak_winding_current,
1142
+ R"pbdoc(
1143
+ Peak magnetizing (flux-driving) current at an operating point.
1144
+
1145
+ Returns the peak of the magnetizing current — the quantity that
1146
+ actually drives core flux and therefore saturation — for the
1147
+ as-designed magnetic at the supplied operating point. This is the
1148
+ consistent partner to ``calculate_saturation_current_at_operating_point``:
1149
+ both use the same operating-point magnetizing inductance, so the
1150
+ caller can compare I_peak against I_sat on the same footing.
1151
+
1152
+ The magnetizing current is computed by MKF from the primary voltage
1153
+ waveform (``I_mag = (1/L) ∫ v dt``) with the operating DC bias folded
1154
+ in, NOT read back from the input current. For a transformer this is
1155
+ the genuine flux-driving current, which differs from the primary
1156
+ winding current (the latter includes reflected load current that
1157
+ produces no net flux).
1158
+
1159
+ Args:
1160
+ magnetic_json: JSON Magnetic object (core + coil).
1161
+ operating_point_json: JSON OperatingPoint object with at least the
1162
+ primary winding voltage excitation.
1163
+ winding_index: Winding the result is referred to (default 0,
1164
+ primary). For other windings the primary-referred peak is
1165
+ scaled by the turns ratio N_0 / N_i — saturation is a single
1166
+ core-flux property, so this is largely a unit choice.
1167
+
1168
+ Returns:
1169
+ Peak magnetizing current in Amperes.
1170
+ )pbdoc",
1171
+ py::arg("magnetic_json"), py::arg("operating_point_json"),
1172
+ py::arg("winding_index") = 0);
1173
+
1076
1174
  m.def("calculate_saturation_current", &calculate_saturation_current,
1077
1175
  R"pbdoc(
1078
1176
  Calculate saturation current for a complete magnetic.
@@ -37,6 +37,7 @@ json load_core_data(json coresJson);
37
37
  json get_core_temperature_dependant_parameters(json coreData, double temperature);
38
38
  double calculate_core_maximum_magnetic_energy(json coreDataJson, json operatingPointJson);
39
39
  double calculate_saturation_current(json magneticJson, double temperature);
40
+ double calculate_peak_winding_current(json magneticJson, json operatingPointJson, int windingIndex);
40
41
  double calculate_temperature_from_core_thermal_resistance(json coreJson, double totalLosses);
41
42
 
42
43
  // Gap and reluctance
@@ -25,10 +25,6 @@ json calculate_core_losses(json coreData, json coilData, json inputsData, json m
25
25
  if (models.find("coreLosses") != models.end()) {
26
26
  OpenMagnetics::from_json(models["coreLosses"], coreLossesModelName);
27
27
  }
28
- auto coreTemperatureModelName = OpenMagnetics::defaults.coreTemperatureModelDefault;
29
- if (models.find("coreTemperature") != models.end()) {
30
- OpenMagnetics::from_json(models["coreTemperature"], coreTemperatureModelName);
31
- }
32
28
 
33
29
  OpenMagnetics::Magnetic magnetic;
34
30
  magnetic.set_core(core);
@@ -36,7 +32,6 @@ json calculate_core_losses(json coreData, json coilData, json inputsData, json m
36
32
 
37
33
  OpenMagnetics::MagneticSimulator magneticSimulator;
38
34
  magneticSimulator.set_core_losses_model_name(coreLossesModelName);
39
- magneticSimulator.set_core_temperature_model_name(coreTemperatureModelName);
40
35
  magneticSimulator.set_reluctance_model_name(reluctanceModelName);
41
36
  auto coreLossesOutput = magneticSimulator.calculate_core_losses(operatingPoint, magnetic);
42
37
  json result;
@@ -66,15 +61,6 @@ json get_core_losses_model_information(json material) {
66
61
  return info;
67
62
  }
68
63
 
69
- json get_core_temperature_model_information() {
70
- json info;
71
- info["information"] = OpenMagnetics::CoreTemperatureModel::get_models_information();
72
- info["errors"] = OpenMagnetics::CoreTemperatureModel::get_models_errors();
73
- info["internal_links"] = OpenMagnetics::CoreTemperatureModel::get_models_internal_links();
74
- info["external_links"] = OpenMagnetics::CoreTemperatureModel::get_models_external_links();
75
- return info;
76
- }
77
-
78
64
  json calculate_steinmetz_coefficients(json dataJson, json rangesJson) {
79
65
  try {
80
66
  std::vector<std::pair<double, double>> ranges;
@@ -383,7 +369,6 @@ void register_losses_bindings(py::module& m) {
383
369
  models_data: JSON dict specifying models to use:
384
370
  - "coreLosses": "STEINMETZ", "IGSE", "MSE", "BARG", "ROSHEN", "PROPRIETARY"
385
371
  - "reluctance": "ZHANG", "MUEHLETHALER", "PARTRIDGE", "STENGLEIN"
386
- - "coreTemperature": Model for temperature estimation
387
372
 
388
373
  Returns:
389
374
  JSON object containing:
@@ -423,14 +408,6 @@ void register_losses_bindings(py::module& m) {
423
408
  )pbdoc",
424
409
  py::arg("material"));
425
410
 
426
- m.def("get_core_temperature_model_information", &get_core_temperature_model_information,
427
- R"pbdoc(
428
- Get documentation for available core temperature models.
429
-
430
- Returns:
431
- JSON object with model information, errors, and reference links.
432
- )pbdoc");
433
-
434
411
  m.def("calculate_steinmetz_coefficients", &calculate_steinmetz_coefficients,
435
412
  R"pbdoc(
436
413
  Fit Steinmetz equation coefficients from measured loss data.
@@ -7,7 +7,6 @@ namespace PyMKF {
7
7
  // Core losses
8
8
  json calculate_core_losses(json coreData, json coilData, json inputsData, json modelsData);
9
9
  json get_core_losses_model_information(json material);
10
- json get_core_temperature_model_information();
11
10
  json calculate_steinmetz_coefficients(json dataJson, json rangesJson);
12
11
  json calculate_steinmetz_coefficients_with_error(json dataJson, json rangesJson);
13
12
 
@@ -15,14 +15,9 @@ json simulate(json inputsJson, json magneticJson, json modelsData) {
15
15
  if (!modelsData.is_null() && modelsData.find("coreLosses") != modelsData.end()) {
16
16
  OpenMagnetics::from_json(modelsData["coreLosses"], coreLossesModelName);
17
17
  }
18
- auto coreTemperatureModelName = OpenMagnetics::defaults.coreTemperatureModelDefault;
19
- if (!modelsData.is_null() && modelsData.find("coreTemperature") != modelsData.end()) {
20
- OpenMagnetics::from_json(modelsData["coreTemperature"], coreTemperatureModelName);
21
- }
22
-
18
+
23
19
  OpenMagnetics::MagneticSimulator magneticSimulator;
24
20
  magneticSimulator.set_core_losses_model_name(coreLossesModelName);
25
- magneticSimulator.set_core_temperature_model_name(coreTemperatureModelName);
26
21
  magneticSimulator.set_reluctance_model_name(reluctanceModelName);
27
22
  auto mas = magneticSimulator.simulate(inputs, magnetic);
28
23
 
@@ -181,6 +181,90 @@ class TestCoreCalculations:
181
181
  assert isinstance(gapping, list)
182
182
 
183
183
 
184
+ class TestPeakWindingCurrent:
185
+ """
186
+ Tests for calculate_peak_winding_current (ABT #16).
187
+
188
+ Verifies the binding that exposes the MKF-computed peak magnetizing
189
+ (flux-driving) current at an operating point, so Heaviside no longer
190
+ needs analytical I_peak fallbacks.
191
+ """
192
+
193
+ def _advise_magnetic(self, inputs):
194
+ proc = PyOpenMagnetics.process_inputs(inputs)
195
+ res = PyOpenMagnetics.calculate_advised_magnetics(proc, 1, "available cores")
196
+ data = json.loads(res) if isinstance(res, str) else res
197
+ data = data["data"] if isinstance(data, dict) and "data" in data else data
198
+ assert len(data) > 0, "adviser returned no magnetic"
199
+ mas = data[0].get("mas", data[0])
200
+ return mas["magnetic"], mas["inputs"]["operatingPoints"][0]
201
+
202
+ def test_inductor_peak_includes_dc_bias(self):
203
+ """
204
+ Peak magnetizing current must fold in the operating DC bias, not
205
+ return only the AC ripple amplitude. Input current here is a 5 A DC
206
+ bias with a 4 A peak-to-peak ripple.
207
+ """
208
+ op = {
209
+ "name": "Nominal",
210
+ "conditions": {"ambientTemperature": 25},
211
+ "excitationsPerWinding": [{
212
+ "frequency": 100000,
213
+ "current": {"waveform": {"data": [3, 7, 3],
214
+ "time": [0, 0.0000025, 0.00001]}},
215
+ "voltage": {"waveform": {"data": [-10, 30, 30, -10, -10],
216
+ "time": [0, 0, 0.0000025, 0.0000025, 0.00001]}},
217
+ }],
218
+ }
219
+ inputs = {
220
+ "designRequirements": {"magnetizingInductance": {"nominal": 100e-6},
221
+ "turnsRatios": []},
222
+ "operatingPoints": [op],
223
+ }
224
+ magnetic, op_used = self._advise_magnetic(inputs)
225
+
226
+ ipeak = PyOpenMagnetics.calculate_peak_winding_current(magnetic, op_used, 0)
227
+ # Default winding_index argument must match explicit 0.
228
+ assert ipeak == PyOpenMagnetics.calculate_peak_winding_current(magnetic, op_used)
229
+ assert ipeak > 0
230
+ # Must include the ~5 A DC bias, so well above the bare ripple amplitude.
231
+ assert ipeak > 4.0
232
+
233
+ # Consistent footing with I_sat at the same operating point.
234
+ isat = PyOpenMagnetics.calculate_saturation_current_at_operating_point(
235
+ magnetic, op_used, 25.0)
236
+ assert isat > ipeak # an advised design should not be saturating
237
+
238
+ def test_transformer_winding_index_scales_by_turns_ratio(self):
239
+ """
240
+ For a transformer the per-winding magnetizing peak is the primary
241
+ value scaled by the turns ratio N_0 / N_i.
242
+ """
243
+ op = {
244
+ "name": "Nominal",
245
+ "conditions": {"ambientTemperature": 25},
246
+ "excitationsPerWinding": [{
247
+ "frequency": 100000,
248
+ "current": {"waveform": {"data": [-5, 5, -5],
249
+ "time": [0, 0.0000025, 0.00001]}},
250
+ "voltage": {"waveform": {"data": [-10, 30, 30, -10, -10],
251
+ "time": [0, 0, 0.0000025, 0.0000025, 0.00001]}},
252
+ }],
253
+ }
254
+ inputs = {
255
+ "designRequirements": {"magnetizingInductance": {"nominal": 100e-6},
256
+ "turnsRatios": [{"nominal": 2.0}]},
257
+ "operatingPoints": [op],
258
+ }
259
+ magnetic, op_used = self._advise_magnetic(inputs)
260
+ turns = [w["numberTurns"]
261
+ for w in magnetic["coil"]["functionalDescription"]]
262
+
263
+ p0 = PyOpenMagnetics.calculate_peak_winding_current(magnetic, op_used, 0)
264
+ p1 = PyOpenMagnetics.calculate_peak_winding_current(magnetic, op_used, 1)
265
+ assert p1 == pytest.approx(p0 * turns[0] / turns[1], rel=1e-9)
266
+
267
+
184
268
  class TestCoreProcessedDescription:
185
269
  """Test suite for processed core descriptions."""
186
270
 
File without changes
File without changes