PyOpenMagnetics 1.3.8__tar.gz → 1.3.10__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 (84) hide show
  1. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/CMakeLists.txt +65 -3
  2. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/PKG-INFO +1 -1
  3. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/pyproject.toml +1 -1
  4. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/converter.cpp +235 -0
  5. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/.github/workflows/ci.yml +0 -0
  6. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/.github/workflows/publish.yml +0 -0
  7. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/.gitignore +0 -0
  8. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/AGENTS.md +0 -0
  9. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/LICENSE +0 -0
  10. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/PyOpenMagnetics.pyi +0 -0
  11. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/README.md +0 -0
  12. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/api/MAS.py +0 -0
  13. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/api/mas_db_reader.py +0 -0
  14. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/api/validation.py +0 -0
  15. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/clear_cibuildwheel_cache.sh +0 -0
  16. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/docs/compatibility.md +0 -0
  17. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/docs/errors.md +0 -0
  18. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/docs/performance.md +0 -0
  19. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/README.md +0 -0
  20. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/buck_inductor.py +0 -0
  21. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/complete_simulation_example.py +0 -0
  22. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/converter_design_example.py +0 -0
  23. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/debug_bobbin.py +0 -0
  24. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/debug_coil.py +0 -0
  25. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/debug_core.py +0 -0
  26. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/debug_plotting.py +0 -0
  27. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_220v_12v_1a.py +0 -0
  28. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_220v_12v_2a_complete.py +0 -0
  29. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_bh_curve.png +0 -0
  30. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_core.png +0 -0
  31. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_design.py +0 -0
  32. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_summary.png +0 -0
  33. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/flyback_waveforms.png +0 -0
  34. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/list_plot_funcs.py +0 -0
  35. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/plot_flyback_design.py +0 -0
  36. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/plot_flyback_pyom.py +0 -0
  37. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/test_field_calc.py +0 -0
  38. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/examples/test_field_plot.py +0 -0
  39. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/force_fresh_build.sh +0 -0
  40. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/llms.txt +0 -0
  41. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/notebooks/01_getting_started.ipynb +0 -0
  42. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/notebooks/02_buck_inductor.ipynb +0 -0
  43. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/notebooks/03_core_losses.ipynb +0 -0
  44. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/notebooks/README.md +0 -0
  45. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/requirements.txt +0 -0
  46. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/advisers.cpp +0 -0
  47. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/advisers.h +0 -0
  48. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/bobbin.cpp +0 -0
  49. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/bobbin.h +0 -0
  50. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/common.h +0 -0
  51. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/converter.h +0 -0
  52. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/core.cpp +0 -0
  53. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/core.h +0 -0
  54. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/database.cpp +0 -0
  55. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/database.h +0 -0
  56. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/logging.cpp +0 -0
  57. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/logging.h +0 -0
  58. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/losses.cpp +0 -0
  59. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/losses.h +0 -0
  60. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/module.cpp +0 -0
  61. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/plotting.cpp +0 -0
  62. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/plotting.h +0 -0
  63. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/settings.cpp +0 -0
  64. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/settings.h +0 -0
  65. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/simulation.cpp +0 -0
  66. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/simulation.h +0 -0
  67. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/utils.cpp +0 -0
  68. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/utils.h +0 -0
  69. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/winding.cpp +0 -0
  70. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/winding.h +0 -0
  71. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/wire.cpp +0 -0
  72. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/src/wire.h +0 -0
  73. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/test.py +0 -0
  74. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/__init__.py +0 -0
  75. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/conftest.py +0 -0
  76. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_converter_endpoints.py +0 -0
  77. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_core.py +0 -0
  78. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_core_adviser.py +0 -0
  79. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_examples_integration.py +0 -0
  80. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_inputs.py +0 -0
  81. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_logging.py +0 -0
  82. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_magnetic_adviser.py +0 -0
  83. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_plotting.py +0 -0
  84. {pyopenmagnetics-1.3.8 → pyopenmagnetics-1.3.10}/tests/test_winding.py +0 -0
@@ -120,14 +120,25 @@ include_directories("${CMAKE_BINARY_DIR}/_deps/eigen-src")
120
120
  if(NOT LOCAL_MKF_MAS)
121
121
  message(STATUS "Fetching MKF")
122
122
  # Force fresh clone by using a unique timestamp - update this when MKF changes
123
- set(MKF_FORCE_REFRESH "2025-02-26-01")
123
+ set(MKF_FORCE_REFRESH "2026-04-29-extra-components")
124
124
  # Tell MKF to disable matplotplusplus and use SVG-based Painter instead
125
125
  set(INCLUDE_PYMKF ON CACHE BOOL "Build Python interface" FORCE)
126
+ # GIT_SUBMODULES_RECURSE pulls in CAS/EAS (added 2026-04 alongside the
127
+ # ExtraComponentsMode API). MAS is also a submodule of MKF but PyMKF
128
+ # fetches it independently below for fast-update reasons; CAS/EAS only
129
+ # exist as MKF submodules, so we need them populated under MKF/.
130
+ # NOTE: we cannot use GIT_SHALLOW with GIT_SUBMODULES — shallow clone
131
+ # skips submodule init. Drop the shallow flag for MKF so CAS/EAS/
132
+ # cci_coords are actually checked out. cci_coords MUST stay in this
133
+ # list — the CCI generator reads from MKF/cci_coords/coordinates/.
134
+ # MAS is fetched separately below (not a sub-fetch of MKF here) so
135
+ # leave it out, otherwise FetchContent doubles the clone.
126
136
  FetchContent_Declare(MKF
127
137
  GIT_REPOSITORY https://github.com/OpenMagnetics/MKF.git
128
138
  GIT_TAG main
129
139
  GIT_PROGRESS TRUE
130
- GIT_SHALLOW TRUE)
140
+ GIT_SUBMODULES "CAS" "EAS" "cci_coords"
141
+ GIT_SUBMODULES_RECURSE TRUE)
131
142
 
132
143
  message(STATUS "Fetching mas")
133
144
  # Skip Git LFS to avoid bandwidth quota issues - data files are optional for build
@@ -237,6 +248,42 @@ add_custom_target(PyMASGeneration
237
248
  /bin/echo "RUNNING PyMASGeneration"
238
249
  DEPENDS "${MAS_DIRECTORY}/MAS.hpp")
239
250
 
251
+ # ──────────────────────────────────────────────────────────────────
252
+ # CAS.hpp generation (mirrors MKF/CMakeLists.txt:419-451)
253
+ # CAS = Capacitor Adviser Schema. EAS = Element Adviser Schema.
254
+ # Both are submodules of MKF (added 2026-04). MKF/Topology.h #includes
255
+ # <CAS.hpp> for the get_extra_components_inputs API; we must generate
256
+ # the header here so PyOpenMagnetics's TU sees it.
257
+ # ──────────────────────────────────────────────────────────────────
258
+ set(CAS_DIRECTORY "${CMAKE_BINARY_DIR}/CAS/")
259
+ set(CAS_DIR "${MKF_DIR}/CAS")
260
+ set(EAS_DIR "${MKF_DIR}/EAS")
261
+ file(MAKE_DIRECTORY "${CAS_DIRECTORY}")
262
+
263
+ add_custom_command(
264
+ OUTPUT "${CAS_DIRECTORY}/CAS.hpp"
265
+ COMMAND ${CMAKE_COMMAND} -E remove -f "${CAS_DIRECTORY}/CAS.hpp"
266
+ COMMAND quicktype -l c++ -s schema ${CAS_DIR}/schemas/inputs.json
267
+ -S ${CAS_DIR}/schemas/inputs/designRequirements.json
268
+ -S ${EAS_DIR}/schemas/utils.json
269
+ -S ${EAS_DIR}/schemas/inputs/twoTerminalOperatingPoint.json
270
+ -o ${CAS_DIRECTORY}/CAS.hpp --namespace CAS --source-style single-source
271
+ --type-style pascal-case --member-style underscore-case
272
+ --enumerator-style upper-underscore-case --no-boost
273
+ --top-level Inputs
274
+ DEPENDS
275
+ "${CAS_DIR}/schemas/inputs.json"
276
+ "${CAS_DIR}/schemas/inputs/designRequirements.json"
277
+ "${EAS_DIR}/schemas/utils.json"
278
+ "${EAS_DIR}/schemas/inputs/twoTerminalOperatingPoint.json"
279
+ USES_TERMINAL)
280
+
281
+ add_custom_target(PyCASGeneration
282
+ /bin/echo "RUNNING PyCASGeneration"
283
+ DEPENDS "${CAS_DIRECTORY}/CAS.hpp")
284
+ add_dependencies(PyCASGeneration PyMASGeneration)
285
+ include_directories("${CAS_DIRECTORY}")
286
+
240
287
  message(STATUS "Compiling PyOpenMagnetics with modular structure")
241
288
  file(GLOB SOURCES src/*.cpp
242
289
  ${MKF_DIR}/src/*.cpp
@@ -247,11 +294,26 @@ file(GLOB SOURCES src/*.cpp
247
294
  ${MKF_DIR}/src/processors/*.cpp
248
295
  ${MKF_DIR}/src/support/*.cpp
249
296
  )
297
+
298
+ set(CCI_GENERATED_CPP "${CMAKE_BINARY_DIR}/generated/CciCoordinatesData.cpp")
299
+ find_package(Python3 REQUIRED COMPONENTS Interpreter)
300
+ add_custom_command(
301
+ OUTPUT "${CCI_GENERATED_CPP}"
302
+ COMMAND "${Python3_EXECUTABLE}"
303
+ "${MKF_DIR}/scripts/generate_cci_data.py"
304
+ "${MKF_DIR}/cci_coords"
305
+ "${CCI_GENERATED_CPP}"
306
+ DEPENDS "${MKF_DIR}/scripts/generate_cci_data.py"
307
+ COMMENT "Generating embedded CCI coordinates"
308
+ VERBATIM)
309
+ add_custom_target(cci_data_gen DEPENDS "${CCI_GENERATED_CPP}")
310
+ list(APPEND SOURCES "${CCI_GENERATED_CPP}")
311
+
250
312
  message(STATUS SOURCES)
251
313
  message(STATUS ${SOURCES})
252
314
  pybind11_add_module(PyOpenMagnetics ${SOURCES})
253
315
 
254
- add_dependencies(PyOpenMagnetics PyMASGeneration)
316
+ add_dependencies(PyOpenMagnetics PyMASGeneration PyCASGeneration cci_data_gen)
255
317
 
256
318
  target_link_libraries(PyOpenMagnetics PUBLIC nlohmann_json::nlohmann_json levmar rapidfuzz::rapidfuzz)
257
319
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyOpenMagnetics
3
- Version: 1.3.8
3
+ Version: 1.3.10
4
4
  Summary: Python wrapper for OpenMagnetics
5
5
  Author-Email: Alfonso Martinez <Alfonso_VII@hotmail.com>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "PyOpenMagnetics"
7
- version = "1.3.8"
7
+ version = "1.3.10"
8
8
  requires-python = ">=3.8"
9
9
  authors = [
10
10
  { name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
@@ -629,6 +629,220 @@ json process_current_transformer(json ctJson, double turnsRatio, double secondar
629
629
  return process_converter("current_transformer", ctJson, true);
630
630
  }
631
631
 
632
+ // ─────────────────────────────────────────────────────────────────────
633
+ // get_extra_components_inputs (MKF 2026-04-29 API)
634
+ //
635
+ // Returns a JSON list describing the design requirements / operating
636
+ // points of any extra components a topology brings along besides its
637
+ // main magnetic — resonant tank caps for LLC, snubber nets, auxiliary
638
+ // inductors, etc. The downstream caller can pipe each entry into a
639
+ // component-search (TAS) or a fresh PyOM design call.
640
+ //
641
+ // Each list entry is one of:
642
+ // { "kind": "magnetic", "inputs": { ...MAS::Inputs JSON... } }
643
+ // { "kind": "capacitor", "inputs": { ...CAS::Inputs JSON... } }
644
+ //
645
+ // Arguments:
646
+ // topology_name — same key set as dispatch_converter (e.g. "llc",
647
+ // "psfb", "active_clamp_forward").
648
+ // converter_json — the converter spec used to construct the simple
649
+ // topology class (NOT the AdvancedXxx version).
650
+ // mode — "IDEAL" or "REAL". IDEAL returns archetypal
651
+ // requirements without parasitics; REAL refines
652
+ // with the main magnetic's actual leakage,
653
+ // winding resistance, etc.
654
+ // magnetic_json — optional MAS::Magnetic JSON, required by some
655
+ // topologies in REAL mode. Pass null in IDEAL.
656
+ // ─────────────────────────────────────────────────────────────────────
657
+
658
+ namespace {
659
+
660
+ // Construct the simple topology subclass (NOT AdvancedXxx), run the
661
+ // design pipeline (process() / process_design_requirements()) so the
662
+ // internal resonant-tank / extra-component values are populated, and
663
+ // then invoke get_extra_components_inputs on it. The simple classes
664
+ // own the design physics, so they're what the new API was designed
665
+ // against. Some topologies require process() to have run first; we
666
+ // invoke it via the SFINAE-detected member that exists on each.
667
+ template <typename TopologyT>
668
+ std::vector<std::variant<OpenMagnetics::Inputs, CAS::Inputs>>
669
+ collect_extra_components(const json& converterJson,
670
+ OpenMagnetics::ExtraComponentsMode mode,
671
+ std::optional<OpenMagnetics::Magnetic> magnetic) {
672
+ TopologyT topology(converterJson);
673
+ topology._assertErrors = true;
674
+ // Run process() — the Topology base class entry point that wires
675
+ // process_design_requirements() + process_operating_points() and
676
+ // populates the internal Lr / Cr / inductance ratio that
677
+ // get_extra_components_inputs() reads. The Inputs return value is
678
+ // discarded; we only care about the side effects on `topology`.
679
+ topology.process();
680
+ return topology.get_extra_components_inputs(mode, magnetic);
681
+ }
682
+
683
+ std::vector<std::variant<OpenMagnetics::Inputs, CAS::Inputs>>
684
+ dispatch_extra_components(const std::string& topologyName,
685
+ const json& converterJson,
686
+ OpenMagnetics::ExtraComponentsMode mode,
687
+ std::optional<OpenMagnetics::Magnetic> magnetic) {
688
+ if (topologyName == "flyback")
689
+ return collect_extra_components<OpenMagnetics::Flyback>(converterJson, mode, magnetic);
690
+ if (topologyName == "buck")
691
+ return collect_extra_components<OpenMagnetics::Buck>(converterJson, mode, magnetic);
692
+ if (topologyName == "boost")
693
+ return collect_extra_components<OpenMagnetics::Boost>(converterJson, mode, magnetic);
694
+ if (topologyName == "single_switch_forward")
695
+ return collect_extra_components<OpenMagnetics::SingleSwitchForward>(converterJson, mode, magnetic);
696
+ if (topologyName == "two_switch_forward")
697
+ return collect_extra_components<OpenMagnetics::TwoSwitchForward>(converterJson, mode, magnetic);
698
+ if (topologyName == "active_clamp_forward")
699
+ return collect_extra_components<OpenMagnetics::ActiveClampForward>(converterJson, mode, magnetic);
700
+ if (topologyName == "push_pull")
701
+ return collect_extra_components<OpenMagnetics::PushPull>(converterJson, mode, magnetic);
702
+ if (topologyName == "llc")
703
+ return collect_extra_components<OpenMagnetics::Llc>(converterJson, mode, magnetic);
704
+ if (topologyName == "dab")
705
+ return collect_extra_components<OpenMagnetics::Dab>(converterJson, mode, magnetic);
706
+ if (topologyName == "phase_shifted_full_bridge" || topologyName == "psfb")
707
+ return collect_extra_components<OpenMagnetics::Psfb>(converterJson, mode, magnetic);
708
+ if (topologyName == "phase_shifted_half_bridge" || topologyName == "pshb")
709
+ return collect_extra_components<OpenMagnetics::Pshb>(converterJson, mode, magnetic);
710
+ if (topologyName == "cllc")
711
+ return collect_extra_components<OpenMagnetics::CllcConverter>(converterJson, mode, magnetic);
712
+ if (topologyName == "isolated_buck")
713
+ return collect_extra_components<OpenMagnetics::IsolatedBuck>(converterJson, mode, magnetic);
714
+ if (topologyName == "isolated_buck_boost")
715
+ return collect_extra_components<OpenMagnetics::IsolatedBuckBoost>(converterJson, mode, magnetic);
716
+ throw std::invalid_argument(
717
+ "get_extra_components_inputs: topology '" + topologyName +
718
+ "' has no extra-components dispatch (or hasn't been wired in PyMKF)."
719
+ );
720
+ }
721
+
722
+ } // namespace
723
+
724
+ // ─────────────────────────────────────────────────────────────────────
725
+ // generate_ngspice_circuit (MKF native SPICE generator)
726
+ //
727
+ // Returns the canonical ngspice deck for a converter at a given operating
728
+ // point, sized from the magnetic that was designed in Phase 3. This
729
+ // replaces hand-rolled netlist templates on the Proteus side: MKF owns
730
+ // the topology wiring (rectifier polarity, gate-drive timing, dead-time,
731
+ // snubbers, K-coupling) so the simulation always validates the actual
732
+ // designed magnetic.
733
+ //
734
+ // Args mirror the C++ method: turns_ratios (Np:Ns vector), magnetizing
735
+ // inductance, optional vin / operating-point indices. The simple
736
+ // topology class is constructed and process()'d first so internal state
737
+ // (Lr / Cr for LLC, etc.) is populated before deck generation.
738
+ // ─────────────────────────────────────────────────────────────────────
739
+ namespace {
740
+ // Isolated topologies (transformer): pass turns ratios + magnetizing inductance.
741
+ template <typename TopologyT>
742
+ std::string generate_spice_isolated(const json& converterJson,
743
+ const std::vector<double>& turnsRatios,
744
+ double magnetizingInductance,
745
+ size_t vinIdx, size_t opIdx) {
746
+ TopologyT topology(converterJson);
747
+ topology._assertErrors = true;
748
+ topology.process();
749
+ return topology.generate_ngspice_circuit(turnsRatios, magnetizingInductance, vinIdx, opIdx);
750
+ }
751
+
752
+ // Non-isolated topologies (single inductor): just pass the inductance.
753
+ template <typename TopologyT>
754
+ std::string generate_spice_inductor(const json& converterJson,
755
+ double inductance,
756
+ size_t vinIdx, size_t opIdx) {
757
+ TopologyT topology(converterJson);
758
+ topology._assertErrors = true;
759
+ topology.process();
760
+ return topology.generate_ngspice_circuit(inductance, vinIdx, opIdx);
761
+ }
762
+
763
+ } // namespace
764
+
765
+ json generate_ngspice_circuit(const std::string& topologyName,
766
+ json converterJson,
767
+ std::vector<double> turnsRatios,
768
+ double magnetizingInductance,
769
+ size_t vinIdx,
770
+ size_t opIdx) {
771
+ try {
772
+ std::string spice;
773
+ // Non-isolated single-inductor topologies — magnetizingInductance
774
+ // arg is interpreted as the main inductor value.
775
+ if (topologyName == "buck")
776
+ spice = generate_spice_inductor<OpenMagnetics::Buck>(converterJson, magnetizingInductance, vinIdx, opIdx);
777
+ else if (topologyName == "boost")
778
+ spice = generate_spice_inductor<OpenMagnetics::Boost>(converterJson, magnetizingInductance, vinIdx, opIdx);
779
+ // Isolated topologies — turnsRatios + magnetizing inductance.
780
+ else if (topologyName == "flyback") spice = generate_spice_isolated<OpenMagnetics::Flyback>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
781
+ else if (topologyName == "single_switch_forward") spice = generate_spice_isolated<OpenMagnetics::SingleSwitchForward>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
782
+ else if (topologyName == "two_switch_forward") spice = generate_spice_isolated<OpenMagnetics::TwoSwitchForward>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
783
+ else if (topologyName == "active_clamp_forward") spice = generate_spice_isolated<OpenMagnetics::ActiveClampForward>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
784
+ else if (topologyName == "push_pull") spice = generate_spice_isolated<OpenMagnetics::PushPull>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
785
+ else if (topologyName == "llc") spice = generate_spice_isolated<OpenMagnetics::Llc>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
786
+ // CLLC is intentionally not wired here yet — its
787
+ // generate_ngspice_circuit signature takes (double turnsRatio,
788
+ // CllcResonantParameters&, ...), which doesn't fit the uniform
789
+ // (vector<double> turnsRatios, double Lm, ...) shape used by every
790
+ // other topology. Add a separate dispatch path when we need it.
791
+ else if (topologyName == "dab") spice = generate_spice_isolated<OpenMagnetics::Dab>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
792
+ else if (topologyName == "phase_shifted_full_bridge" || topologyName == "psfb") spice = generate_spice_isolated<OpenMagnetics::Psfb>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
793
+ else if (topologyName == "phase_shifted_half_bridge" || topologyName == "pshb") spice = generate_spice_isolated<OpenMagnetics::Pshb>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
794
+ else if (topologyName == "isolated_buck") spice = generate_spice_isolated<OpenMagnetics::IsolatedBuck>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
795
+ else if (topologyName == "isolated_buck_boost") spice = generate_spice_isolated<OpenMagnetics::IsolatedBuckBoost>(converterJson, turnsRatios, magnetizingInductance, vinIdx, opIdx);
796
+ else return json{{"error", "generate_ngspice_circuit: unknown topology '" + topologyName + "'"}};
797
+ return json{{"netlist", spice}};
798
+ } catch (const std::exception& exc) {
799
+ return json{{"error", std::string("generate_ngspice_circuit: ") + exc.what()}};
800
+ }
801
+ }
802
+
803
+ json get_extra_components_inputs(const std::string& topologyName,
804
+ json converterJson,
805
+ const std::string& modeStr,
806
+ json magneticJson) {
807
+ try {
808
+ OpenMagnetics::ExtraComponentsMode mode;
809
+ if (modeStr == "IDEAL" || modeStr == "ideal")
810
+ mode = OpenMagnetics::ExtraComponentsMode::IDEAL;
811
+ else if (modeStr == "REAL" || modeStr == "real")
812
+ mode = OpenMagnetics::ExtraComponentsMode::REAL;
813
+ else
814
+ return json{{"error", "mode must be 'IDEAL' or 'REAL', got: " + modeStr}};
815
+
816
+ std::optional<OpenMagnetics::Magnetic> magnetic;
817
+ if (!magneticJson.is_null()) {
818
+ magnetic = OpenMagnetics::Magnetic(magneticJson);
819
+ }
820
+
821
+ auto components = dispatch_extra_components(topologyName, converterJson, mode, magnetic);
822
+
823
+ json out = json::array();
824
+ for (const auto& comp : components) {
825
+ json entry;
826
+ std::visit([&entry](const auto& inputs) {
827
+ using T = std::decay_t<decltype(inputs)>;
828
+ json inputsJson;
829
+ to_json(inputsJson, inputs);
830
+ if constexpr (std::is_same_v<T, OpenMagnetics::Inputs>) {
831
+ entry["kind"] = "magnetic";
832
+ } else {
833
+ // CAS::Inputs
834
+ entry["kind"] = "capacitor";
835
+ }
836
+ entry["inputs"] = inputsJson;
837
+ }, comp);
838
+ out.push_back(entry);
839
+ }
840
+ return out;
841
+ } catch (const std::exception& exc) {
842
+ return json{{"error", std::string("get_extra_components_inputs: ") + exc.what()}};
843
+ }
844
+ }
845
+
632
846
  void register_converter_bindings(py::module& m) {
633
847
  m.def("process_converter", &process_converter,
634
848
  "Process a converter topology specification to Inputs.",
@@ -651,6 +865,27 @@ void register_converter_bindings(py::module& m) {
651
865
  m.def("process_isolated_buck_boost", &process_isolated_buck_boost, "Process Isolated Buck-Boost.", py::arg("isolated_buck_boost"));
652
866
  m.def("process_current_transformer", &process_current_transformer, "Process Current Transformer.",
653
867
  py::arg("ct"), py::arg("turns_ratio"), py::arg("secondary_resistance") = 0.0);
868
+
869
+ m.def("generate_ngspice_circuit", &generate_ngspice_circuit,
870
+ "Return the canonical ngspice SPICE deck for the topology at a "
871
+ "given operating point, sized from the magnetic design (turns "
872
+ "ratios + magnetizing inductance). Wrapper around each Topology "
873
+ "subclass's generate_ngspice_circuit method; processes the "
874
+ "topology first so internal state (Lr/Cr for LLC, etc.) is "
875
+ "populated. Returns {'netlist': '<spice>'} or {'error': '...'}.",
876
+ py::arg("topology_name"), py::arg("converter_json"),
877
+ py::arg("turns_ratios"), py::arg("magnetizing_inductance"),
878
+ py::arg("vin_index") = 0, py::arg("op_index") = 0);
879
+
880
+ m.def("get_extra_components_inputs", &get_extra_components_inputs,
881
+ "Return the design requirements for extra components a topology brings "
882
+ "alongside its main magnetic — resonant tank Lr/Cr for LLC, snubber "
883
+ "components for PSFB, etc. Each entry is {kind: 'magnetic'|'capacitor', "
884
+ "inputs: <MAS or CAS Inputs JSON>}. Mode 'IDEAL' returns archetypal "
885
+ "requirements; 'REAL' refines using the supplied main magnetic's "
886
+ "leakage / DCR / etc.",
887
+ py::arg("topology_name"), py::arg("converter_json"),
888
+ py::arg("mode") = "IDEAL", py::arg("magnetic_json") = nullptr);
654
889
  }
655
890
 
656
891
  } // namespace PyMKF