PyOpenMagnetics 1.7.35__tar.gz → 1.7.37__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 (95) hide show
  1. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/CMakeLists.txt +15 -1
  2. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/PKG-INFO +1 -1
  3. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/pyproject.toml +1 -1
  4. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/core.cpp +12 -7
  5. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/losses.cpp +15 -5
  6. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/simulation.cpp +52 -3
  7. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/simulation.h +4 -1
  8. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_core.py +49 -3
  9. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/.github/workflows/ci.yml +0 -0
  10. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/.github/workflows/publish.yml +0 -0
  11. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/.gitignore +0 -0
  12. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/AGENTS.md +0 -0
  13. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/LICENSE +0 -0
  14. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/PyOpenMagnetics.pyi +0 -0
  15. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/README.md +0 -0
  16. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/_packaging/__init__.py +0 -0
  17. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/_packaging/_build_info.py.in +0 -0
  18. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/api/MAS.py +0 -0
  19. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/api/mas_db_reader.py +0 -0
  20. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/api/validation.py +0 -0
  21. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/clear_cibuildwheel_cache.sh +0 -0
  22. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/docs/compatibility.md +0 -0
  23. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/docs/errors.md +0 -0
  24. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/docs/performance.md +0 -0
  25. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/README.md +0 -0
  26. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/buck_inductor.py +0 -0
  27. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/complete_simulation_example.py +0 -0
  28. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/converter_design_example.py +0 -0
  29. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/debug_bobbin.py +0 -0
  30. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/debug_coil.py +0 -0
  31. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/debug_core.py +0 -0
  32. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/debug_plotting.py +0 -0
  33. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_220v_12v_1a.py +0 -0
  34. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_220v_12v_2a_complete.py +0 -0
  35. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_bh_curve.png +0 -0
  36. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_core.png +0 -0
  37. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_design.py +0 -0
  38. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_summary.png +0 -0
  39. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/flyback_waveforms.png +0 -0
  40. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/list_plot_funcs.py +0 -0
  41. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/plot_flyback_design.py +0 -0
  42. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/plot_flyback_pyom.py +0 -0
  43. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/test_field_calc.py +0 -0
  44. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/examples/test_field_plot.py +0 -0
  45. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/force_fresh_build.sh +0 -0
  46. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/llms.txt +0 -0
  47. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/notebooks/01_getting_started.ipynb +0 -0
  48. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/notebooks/02_buck_inductor.ipynb +0 -0
  49. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/notebooks/03_core_losses.ipynb +0 -0
  50. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/notebooks/README.md +0 -0
  51. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/requirements.txt +0 -0
  52. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/advisers.cpp +0 -0
  53. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/advisers.h +0 -0
  54. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/bobbin.cpp +0 -0
  55. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/bobbin.h +0 -0
  56. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/cmc.cpp +0 -0
  57. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/cmc.h +0 -0
  58. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/common.h +0 -0
  59. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/converter.cpp +0 -0
  60. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/converter.h +0 -0
  61. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/core.h +0 -0
  62. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/crossref.cpp +0 -0
  63. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/crossref.h +0 -0
  64. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/database.cpp +0 -0
  65. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/database.h +0 -0
  66. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/logging.cpp +0 -0
  67. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/logging.h +0 -0
  68. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/losses.h +0 -0
  69. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/module.cpp +0 -0
  70. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/plotting.cpp +0 -0
  71. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/plotting.h +0 -0
  72. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/settings.cpp +0 -0
  73. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/settings.h +0 -0
  74. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/utils.cpp +0 -0
  75. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/utils.h +0 -0
  76. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/winding.cpp +0 -0
  77. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/winding.h +0 -0
  78. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/wire.cpp +0 -0
  79. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/src/wire.h +0 -0
  80. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/test.py +0 -0
  81. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/__init__.py +0 -0
  82. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/conftest.py +0 -0
  83. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_converter_endpoints.py +0 -0
  84. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_core_adviser.py +0 -0
  85. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_datasheet_only_catalogue.py +0 -0
  86. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_examples_integration.py +0 -0
  87. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_inputs.py +0 -0
  88. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_logging.py +0 -0
  89. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_losses_not_evaluable_catalogue.py +0 -0
  90. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_magnetic_adviser.py +0 -0
  91. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_magnetics_catalogue_load.py +0 -0
  92. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_mas_schema_walk.py +0 -0
  93. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_material_validator.py +0 -0
  94. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_plotting.py +0 -0
  95. {pyopenmagnetics-1.7.35 → pyopenmagnetics-1.7.37}/tests/test_winding.py +0 -0
@@ -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-10-04-mkf-39b59172")
145
+ set(MKF_FORCE_REFRESH "2026-10-28-mkf-292dd4c2")
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
@@ -430,6 +430,20 @@ file(GLOB SOURCES src/*.cpp
430
430
  ${MKF_DIR}/src/support/*.cpp
431
431
  )
432
432
 
433
+ # MKF parses its MAS databases with simdjson (src/support/FastJsonParse.cpp), compiled from the single-header
434
+ # amalgamation as in MKF's own CMakeLists.txt; only those two files see its header.
435
+ if(EXISTS "${MKF_DIR}/src/support/FastJsonParse.cpp")
436
+ FetchContent_Declare(simdjson
437
+ GIT_REPOSITORY https://github.com/simdjson/simdjson.git
438
+ GIT_TAG tags/v5.0.5
439
+ GIT_SHALLOW TRUE
440
+ SOURCE_SUBDIR singleheader-not-a-cmake-project)
441
+ FetchContent_MakeAvailable(simdjson)
442
+ list(APPEND SOURCES "${simdjson_SOURCE_DIR}/singleheader/simdjson.cpp")
443
+ set_source_files_properties("${simdjson_SOURCE_DIR}/singleheader/simdjson.cpp" "${MKF_DIR}/src/support/FastJsonParse.cpp"
444
+ PROPERTIES INCLUDE_DIRECTORIES "${simdjson_SOURCE_DIR}/singleheader")
445
+ endif()
446
+
433
447
  # converter.cpp is now a thin shim over the Kirchhoff string API (libKirchhoffApi) — re-enabled.
434
448
 
435
449
  set(CCI_GENERATED_CPP "${CMAKE_BINARY_DIR}/generated/CciCoordinatesData.cpp")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyOpenMagnetics
3
- Version: 1.7.35
3
+ Version: 1.7.37
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.7.35"
7
+ version = "1.7.37"
8
8
  requires-python = ">=3.8"
9
9
  authors = [
10
10
  { name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
@@ -1,6 +1,7 @@
1
1
  #include "core.h"
2
2
  #include "constructive_models/CorePiece.h"
3
3
  #include "physical_models/ComplexPermeability.h"
4
+ #include "Defaults.h"
4
5
  #include <filesystem>
5
6
 
6
7
  namespace PyMKF {
@@ -284,7 +285,8 @@ json calculate_gap_reluctance(json coreGapData, std::string modelNameString) {
284
285
  auto reluctanceModel = OpenMagnetics::ReluctanceModel::factory(modelName);
285
286
  CoreGap coreGap(coreGapData);
286
287
 
287
- auto coreGapResult = reluctanceModel->get_gap_reluctance(coreGap);
288
+ // A standalone gap: no core material, so its storable energy is at the default reference flux density.
289
+ auto coreGapResult = reluctanceModel->get_gap_reluctance(coreGap, OpenMagnetics::Defaults().gapReluctanceEnergyFluxDensity);
288
290
  json result;
289
291
  to_json(result, coreGapResult);
290
292
  return result;
@@ -445,13 +447,16 @@ double calculate_peak_winding_current(json magneticJson, json operatingPointJson
445
447
  }
446
448
 
447
449
  // The magnetizing (flux-driving) current is referred to the primary
448
- // (winding 0): I_mag = (1/L) ∫ v_primary dt, derived from the primary
449
- // excitation's voltage. addOffset=true folds the operating DC bias of
450
- // the primary current back in, so the returned peak is the saturation-
451
- // driving peak (DC + ripple), not just the AC ripple amplitude.
452
- auto primaryExcitation = excitations[0];
450
+ // (winding 0): Faraday on the primary voltage, I_mag = (1/L) ∫ v dt,
451
+ // plus the DC the winding currents give (a single winding's own current,
452
+ // or the anchor of windings that do not always conduct together: a
453
+ // flyback, a forward's reset). This is MKF's one shared decision
454
+ // (Inputs::calculate_magnetizing_current on the operating point), so the
455
+ // returned peak is the saturation-driving peak (DC + ripple). The former
456
+ // call passed `true` into the dcCurrent slot, which C++ converted to a
457
+ // flat 1 A DC whatever the currents were.
453
458
  auto magnetizingCurrent = OpenMagnetics::Inputs::calculate_magnetizing_current(
454
- primaryExcitation, magnetizingInductance, true, true);
459
+ op, 0, magnetizingInductance, magnetic.get_mutable_coil().get_turns_ratios(), true);
455
460
  auto processed = magnetizingCurrent.get_processed();
456
461
  if (!processed.has_value() || !processed->get_peak().has_value()) {
457
462
  throw std::runtime_error(
@@ -32,7 +32,11 @@ json calculate_core_losses(json coreData, json coilData, json inputsData, json m
32
32
  OpenMagnetics::MagneticSimulator magneticSimulator;
33
33
  magneticSimulator.set_core_losses_model_name(coreLossesModelName);
34
34
  magneticSimulator.set_reluctance_model_name(reluctanceModelName);
35
- auto coreLossesOutput = magneticSimulator.calculate_core_losses(operatingPoint, magnetic);
35
+ // ABT #1876 (Alf, 2026-10-14): a point with no thermal operating point gets its core losses at ambient and
36
+ // NO temperature (temperatureOutput stays empty); its coreLosses.temperature is the ambient they were
37
+ // evaluated at, not an operating temperature.
38
+ std::optional<TemperatureOutput> temperatureOutput;
39
+ auto coreLossesOutput = magneticSimulator.calculate_core_losses(operatingPoint, magnetic, nullptr, &temperatureOutput);
36
40
  json result;
37
41
  to_json(result, coreLossesOutput);
38
42
 
@@ -44,8 +48,12 @@ json calculate_core_losses(json coreData, json coilData, json inputsData, json m
44
48
  result["voltageRms"] = operatingPoint.get_mutable_excitations_per_winding()[0].get_voltage().value().get_processed().value().get_rms().value();
45
49
  result["currentRms"] = operatingPoint.get_mutable_excitations_per_winding()[0].get_current().value().get_processed().value().get_rms().value();
46
50
  result["apparentPower"] = operatingPoint.get_mutable_excitations_per_winding()[0].get_voltage().value().get_processed().value().get_rms().value() * operatingPoint.get_mutable_excitations_per_winding()[0].get_current().value().get_processed().value().get_rms().value();
47
- result["maximumCoreTemperature"] = coreLossesOutput.get_temperature();
48
- result["maximumCoreTemperatureRise"] = coreLossesOutput.get_temperature() - operatingPoint.get_conditions().get_ambient_temperature();
51
+ // The core temperature the losses were solved at, only when the point has a thermal operating point:
52
+ // otherwise both keys are absent.
53
+ if (temperatureOutput) {
54
+ result["maximumCoreTemperature"] = coreLossesOutput.get_temperature();
55
+ result["maximumCoreTemperatureRise"] = coreLossesOutput.get_temperature() - operatingPoint.get_conditions().get_ambient_temperature();
56
+ }
49
57
 
50
58
  return result;
51
59
  }
@@ -353,8 +361,10 @@ void register_losses_bindings(py::module& m) {
353
361
  - voltageRms: RMS voltage in Volts
354
362
  - currentRms: RMS current in Amperes
355
363
  - apparentPower: Apparent power in VA
356
- - maximumCoreTemperature: Estimated max temperature in Celsius
357
- - maximumCoreTemperatureRise: Temperature rise in Kelvin
364
+ - maximumCoreTemperature: Core temperature the losses were solved at, in Celsius.
365
+ Absent when the point has no thermal operating point (the losses are then
366
+ those at ambient; analyze_results reports NO_THERMAL_OPERATING_POINT).
367
+ - maximumCoreTemperatureRise: Temperature rise in Kelvin; absent likewise.
358
368
 
359
369
  Example:
360
370
  >>> models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
@@ -1,4 +1,5 @@
1
1
  #include "simulation.h"
2
+ #include "processors/ResultsAnalyzer.h"
2
3
 
3
4
  namespace PyMKF {
4
5
 
@@ -23,6 +24,14 @@ json simulate(json inputsJson, json magneticJson, json modelsData) {
23
24
  return result;
24
25
  }
25
26
 
27
+ json analyze_results(json masJson) {
28
+ OpenMagnetics::Mas mas(masJson);
29
+ auto findings = OpenMagnetics::ResultsAnalyzer::analyze_results(mas);
30
+ json result;
31
+ OpenMagnetics::to_json(result, findings);
32
+ return result;
33
+ }
34
+
26
35
  json build_datasheet(json inputsJson, json magneticJson, json modelsData) {
27
36
  // Simulate the magnetic at the given operating points (the same models as simulate), then let
28
37
  // MagneticSimulator::build_datasheet read the outputs into a manufacturer-style datasheet. Returns
@@ -312,10 +321,30 @@ json sweep_magnetizing_inductance_over_dc_bias(json magneticJson, double start,
312
321
  return resultJson;
313
322
  }
314
323
 
315
- json sweep_core_losses_over_frequency(json magneticJson, json operatingPointJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title) {
324
+ // The generated from_json leaves a Topology untouched for a name it does not know, so an unknown
325
+ // name would reach the model as an uninitialised value: match it against each topology's own
326
+ // spelling and throw when none matches.
327
+ static std::optional<MAS::Topology> parse_optional_topology(const json& topologyJson) {
328
+ if (topologyJson.is_null()) {
329
+ return std::nullopt;
330
+ }
331
+ for (auto candidate : magic_enum::enum_values<MAS::Topology>()) {
332
+ json spelling;
333
+ to_json(spelling, candidate);
334
+ if (spelling == topologyJson) {
335
+ return candidate;
336
+ }
337
+ }
338
+ throw std::invalid_argument("Unknown topology " + topologyJson.dump());
339
+ }
340
+
341
+ // topologyJson: the design's designRequirements.topology, None when the design has none. The
342
+ // magnetizing current that drives the core depends on it (MKF ABT #1754).
343
+ json sweep_core_losses_over_frequency(json magneticJson, json operatingPointJson, json topologyJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title) {
316
344
  OpenMagnetics::Magnetic magnetic(magneticJson);
317
345
  OperatingPoint operatingPoint(operatingPointJson);
318
- auto result = OpenMagnetics::Sweeper::sweep_core_losses_over_frequency(magnetic, operatingPoint, start, stop, numberElements, temperature, mode, title);
346
+ auto topology = parse_optional_topology(topologyJson);
347
+ auto result = OpenMagnetics::Sweeper::sweep_core_losses_over_frequency(magnetic, operatingPoint, topology, start, stop, numberElements, temperature, mode, title);
319
348
  json resultJson;
320
349
  to_json(resultJson, result);
321
350
  return resultJson;
@@ -443,6 +472,25 @@ void register_simulation_bindings(py::module& m) {
443
472
  )pbdoc",
444
473
  py::call_guard<py::gil_scoped_release>());
445
474
 
475
+ m.def("analyze_results", &analyze_results,
476
+ R"pbdoc(
477
+ MKF's findings on a simulated MAS (ABT #1855).
478
+
479
+ simulate gives its numbers for every operating point and does not judge them; this says
480
+ whether they describe a part that works: a saturating core, a voltage that is not
481
+ volt-second balanced (simulated on its balanced part), a core or winding above a
482
+ temperature limit its part states (or a part that states none), a core driven past the
483
+ temperatures its material data covers.
484
+
485
+ Args:
486
+ mas_json: a simulated MAS, with one output per operating point.
487
+
488
+ Returns:
489
+ JSON array of {operatingPointIndex, operatingPointName (when the point has one),
490
+ code, message}. Raises when the MAS lacks an output for an operating point.
491
+ )pbdoc",
492
+ py::call_guard<py::gil_scoped_release>());
493
+
446
494
  m.def("build_datasheet", &build_datasheet,
447
495
  R"pbdoc(
448
496
  Build a manufacturer-style datasheet for a magnetic from MKF's own simulation.
@@ -776,13 +824,14 @@ void register_simulation_bindings(py::module& m) {
776
824
  m.def("sweep_core_losses_over_frequency", &sweep_core_losses_over_frequency,
777
825
  py::arg("magnetic_json"),
778
826
  py::arg("operating_point_json"),
827
+ py::arg("topology"),
779
828
  py::arg("start"),
780
829
  py::arg("stop"),
781
830
  py::arg("number_elements"),
782
831
  py::arg("temperature"),
783
832
  py::arg("mode"),
784
833
  py::arg("title"),
785
- "Sweep core losses over a frequency range.",
834
+ "Sweep core losses over a frequency range. topology: the design's designRequirements.topology, None when it has none.",
786
835
  py::call_guard<py::gil_scoped_release>());
787
836
 
788
837
  m.def("sweep_winding_losses_over_frequency", &sweep_winding_losses_over_frequency,
@@ -6,6 +6,9 @@ namespace PyMKF {
6
6
 
7
7
  // Simulation
8
8
  json simulate(json inputsJson, json magneticJson, json modelsData);
9
+ // ABT #1855: MKF's findings on a simulated MAS (one output per operating point), each naming its
10
+ // operating point: a JSON array of {operatingPointIndex, operatingPointName?, code, message}.
11
+ json analyze_results(json masJson);
9
12
  json build_datasheet(json inputsJson, json magneticJson, json modelsData);
10
13
 
11
14
  // Export
@@ -44,7 +47,7 @@ json sweep_resistance_over_frequency(json magneticJson, double start, double sto
44
47
  json sweep_magnetizing_inductance_over_frequency(json magneticJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title);
45
48
  json sweep_magnetizing_inductance_over_temperature(json magneticJson, double start, double stop, size_t numberElements, double frequency, std::string mode, std::string title);
46
49
  json sweep_magnetizing_inductance_over_dc_bias(json magneticJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title);
47
- json sweep_core_losses_over_frequency(json magneticJson, json operatingPointJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title);
50
+ json sweep_core_losses_over_frequency(json magneticJson, json operatingPointJson, json topologyJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title);
48
51
  json sweep_winding_losses_over_frequency(json magneticJson, json operatingPointJson, double start, double stop, size_t numberElements, double temperature, std::string mode, std::string title);
49
52
 
50
53
  // Matrix calculations
@@ -4,6 +4,7 @@ Tests for PyOpenMagnetics Core data retrieval and calculations.
4
4
  These tests mirror TestCore.cpp and TestCoreAdviser.cpp from MKF,
5
5
  verifying core shape, material, and gapping calculations.
6
6
  """
7
+ import math
7
8
  import pytest
8
9
  import PyOpenMagnetics
9
10
 
@@ -245,6 +246,42 @@ class TestPeakWindingCurrent:
245
246
  magnetic, op_used, 25.0)
246
247
  assert isat > ipeak # an advised design should not be saturating
247
248
 
249
+ def test_flyback_peak_is_the_primary_current_peak(self):
250
+ """
251
+ A CCM flyback, 1:1, 100 kHz, D = 0.5: the primary carries 2 -> 6 A
252
+ during the on-time, the secondary 6 -> 2 A during the off-time. Each
253
+ winding alone carries the magnetizing MMF while it conducts, so the
254
+ magnetizing current peaks at the primary current's peak, 6 A (MKF's
255
+ shared magnetizing-current decision, DC anchored where the primary
256
+ conducts alone). The binding used to pass `true` into MKF's dcCurrent
257
+ argument: a flat 1 A DC under a zero-mean Faraday ripple, ~3 A.
258
+ """
259
+ def winding(name, current, voltage):
260
+ return {
261
+ "name": name,
262
+ "frequency": 100000,
263
+ "current": {"waveform": {"data": current,
264
+ "time": [0, 0.000005, 0.000005, 0.00001]}},
265
+ "voltage": {"waveform": {"data": voltage,
266
+ "time": [0, 0.000005, 0.000005, 0.00001]}},
267
+ }
268
+ op = {
269
+ "name": "Nominal",
270
+ "conditions": {"ambientTemperature": 25},
271
+ "excitationsPerWinding": [
272
+ winding("Primary", [2, 6, 0, 0], [80, 80, -80, -80]),
273
+ winding("Secondary", [0, 0, 6, 2], [-80, -80, 80, 80]),
274
+ ],
275
+ }
276
+ inputs = {
277
+ "designRequirements": {"magnetizingInductance": {"nominal": 100e-6},
278
+ "turnsRatios": [{"nominal": 1.0}]},
279
+ "operatingPoints": [op],
280
+ }
281
+ magnetic, op_used = self._advise_magnetic(inputs)
282
+ ipeak = PyOpenMagnetics.calculate_peak_winding_current(magnetic, op_used, 0)
283
+ assert ipeak == pytest.approx(6.0, rel=0.05)
284
+
248
285
  def test_transformer_winding_index_scales_by_turns_ratio(self):
249
286
  """
250
287
  For a transformer the per-winding magnetizing peak is the primary
@@ -324,11 +361,20 @@ class TestComplexPermeabilityFrequencyRange:
324
361
  # A07's tabulated mu''(f) ends at ~17 MHz (mu' extended to it by Kramers-Kronig in MAS).
325
362
  assert 1e7 < span["maximum"] < 1e8
326
363
 
327
- def test_inside_the_span_answers_and_outside_it_throws(self):
364
+ def test_inside_the_span_answers_and_above_it_estimates(self):
365
+ # MKF ABT #1861: outside the tabulated span the complex permeability of a material the
366
+ # estimator covers is answered with the approved estimate, no longer thrown
367
+ # (MATERIAL_FREQUENCY_OUT_OF_SPAN). Inside the span the table answers, as before.
328
368
  material = PyOpenMagnetics.find_core_material_by_name("A07")
329
369
  span = PyOpenMagnetics.calculate_complex_permeability_frequency_range(material)
330
370
  assert span == PyOpenMagnetics.calculate_complex_permeability_frequency_range("A07")
331
371
  inside = PyOpenMagnetics.calculate_complex_permeability(material, span["maximum"] * 0.99)
332
372
  assert inside["imaginary"] > 0
333
- with pytest.raises(Exception):
334
- PyOpenMagnetics.calculate_complex_permeability(material, span["maximum"] * 1.01)
373
+ at_edge = PyOpenMagnetics.calculate_complex_permeability(material, span["maximum"])
374
+ above = PyOpenMagnetics.calculate_complex_permeability(material, span["maximum"] * 1.01)
375
+ # A finite estimate that continues the table: same sign of both parts as at the span's
376
+ # end, and mu'' still falling with frequency there.
377
+ for part in ("real", "imaginary"):
378
+ assert math.isfinite(above[part])
379
+ assert above[part] * at_edge[part] > 0
380
+ assert 0 < above["imaginary"] <= at_edge["imaginary"]