PyOpenMagnetics 1.7.22__tar.gz → 1.7.24__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 (94) hide show
  1. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/CMakeLists.txt +1 -1
  2. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/PKG-INFO +1 -1
  3. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/PyOpenMagnetics.pyi +12 -0
  4. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/pyproject.toml +1 -1
  5. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/losses.cpp +66 -0
  6. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/losses.h +4 -0
  7. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/settings.cpp +6 -0
  8. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/simulation.cpp +46 -0
  9. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/simulation.h +1 -0
  10. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_datasheet_only_catalogue.py +15 -0
  11. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_examples_integration.py +16 -0
  12. pyopenmagnetics-1.7.24/tests/test_material_validator.py +40 -0
  13. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/.github/workflows/ci.yml +0 -0
  14. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/.github/workflows/publish.yml +0 -0
  15. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/.gitignore +0 -0
  16. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/AGENTS.md +0 -0
  17. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/LICENSE +0 -0
  18. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/README.md +0 -0
  19. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/_packaging/__init__.py +0 -0
  20. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/_packaging/_build_info.py.in +0 -0
  21. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/api/MAS.py +0 -0
  22. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/api/mas_db_reader.py +0 -0
  23. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/api/validation.py +0 -0
  24. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/clear_cibuildwheel_cache.sh +0 -0
  25. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/docs/compatibility.md +0 -0
  26. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/docs/errors.md +0 -0
  27. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/docs/performance.md +0 -0
  28. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/README.md +0 -0
  29. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/buck_inductor.py +0 -0
  30. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/complete_simulation_example.py +0 -0
  31. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/converter_design_example.py +0 -0
  32. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/debug_bobbin.py +0 -0
  33. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/debug_coil.py +0 -0
  34. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/debug_core.py +0 -0
  35. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/debug_plotting.py +0 -0
  36. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_220v_12v_1a.py +0 -0
  37. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_220v_12v_2a_complete.py +0 -0
  38. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_bh_curve.png +0 -0
  39. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_core.png +0 -0
  40. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_design.py +0 -0
  41. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_summary.png +0 -0
  42. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/flyback_waveforms.png +0 -0
  43. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/list_plot_funcs.py +0 -0
  44. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/plot_flyback_design.py +0 -0
  45. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/plot_flyback_pyom.py +0 -0
  46. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/test_field_calc.py +0 -0
  47. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/examples/test_field_plot.py +0 -0
  48. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/force_fresh_build.sh +0 -0
  49. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/llms.txt +0 -0
  50. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/notebooks/01_getting_started.ipynb +0 -0
  51. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/notebooks/02_buck_inductor.ipynb +0 -0
  52. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/notebooks/03_core_losses.ipynb +0 -0
  53. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/notebooks/README.md +0 -0
  54. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/requirements.txt +0 -0
  55. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/advisers.cpp +0 -0
  56. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/advisers.h +0 -0
  57. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/bobbin.cpp +0 -0
  58. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/bobbin.h +0 -0
  59. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/cmc.cpp +0 -0
  60. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/cmc.h +0 -0
  61. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/common.h +0 -0
  62. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/converter.cpp +0 -0
  63. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/converter.h +0 -0
  64. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/core.cpp +0 -0
  65. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/core.h +0 -0
  66. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/crossref.cpp +0 -0
  67. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/crossref.h +0 -0
  68. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/database.cpp +0 -0
  69. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/database.h +0 -0
  70. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/logging.cpp +0 -0
  71. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/logging.h +0 -0
  72. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/module.cpp +0 -0
  73. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/plotting.cpp +0 -0
  74. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/plotting.h +0 -0
  75. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/settings.h +0 -0
  76. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/utils.cpp +0 -0
  77. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/utils.h +0 -0
  78. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/winding.cpp +0 -0
  79. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/winding.h +0 -0
  80. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/wire.cpp +0 -0
  81. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/src/wire.h +0 -0
  82. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/test.py +0 -0
  83. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/__init__.py +0 -0
  84. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/conftest.py +0 -0
  85. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_converter_endpoints.py +0 -0
  86. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_core.py +0 -0
  87. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_core_adviser.py +0 -0
  88. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_inputs.py +0 -0
  89. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_logging.py +0 -0
  90. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_magnetic_adviser.py +0 -0
  91. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_magnetics_catalogue_load.py +0 -0
  92. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_mas_schema_walk.py +0 -0
  93. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/tests/test_plotting.py +0 -0
  94. {pyopenmagnetics-1.7.22 → pyopenmagnetics-1.7.24}/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-09-06-mkf-f5fc2563")
145
+ set(MKF_FORCE_REFRESH "2026-09-28-mkf-780d21de")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyOpenMagnetics
3
- Version: 1.7.22
3
+ Version: 1.7.24
4
4
  Summary: Python wrapper for OpenMagnetics
5
5
  Author-Email: Alfonso Martinez <Alfonso_VII@hotmail.com>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -512,6 +512,18 @@ def get_core_losses_model_information(material: CoreMaterial) -> JsonDict:
512
512
  """Get available loss models and data for material."""
513
513
  ...
514
514
 
515
+ def validate_material(material: CoreMaterial, extra_loss_points: List[JsonDict] = ...) -> JsonDict:
516
+ """Magnetic Blade Runner: physics check of one MAS core-material record.
517
+
518
+ Returns {reference, valid, materialClass, findings, skipped}; valid is False iff a finding
519
+ is IMPOSSIBLE.
520
+ """
521
+ ...
522
+
523
+ def validate_all_materials(core_materials_path: str = "", advanced_core_materials_path: str = "") -> JsonDict:
524
+ """Magnetic Blade Runner over a core-material catalogue (empty path = MKF's embedded one)."""
525
+ ...
526
+
515
527
  # =============================================================================
516
528
  # WINDING ENGINE
517
529
  # =============================================================================
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "PyOpenMagnetics"
7
- version = "1.7.22"
7
+ version = "1.7.24"
8
8
  requires-python = ">=3.8"
9
9
  authors = [
10
10
  { name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
@@ -1,4 +1,5 @@
1
1
  #include "losses.h"
2
+ #include "support/MaterialValidator.h"
2
3
 
3
4
  namespace PyMKF {
4
5
 
@@ -59,6 +60,35 @@ json get_core_losses_model_information(json material) {
59
60
  return info;
60
61
  }
61
62
 
63
+ json validate_material(json material, json extraLossPoints) {
64
+ std::vector<OpenMagnetics::MaterialLossPoint> points;
65
+ for (const auto& p : extraLossPoints) {
66
+ points.push_back(OpenMagnetics::MaterialLossPoint{p.at("frequency").get<double>(), p.at("magneticFluxDensityPeak").get<double>(),
67
+ p.at("temperature").get<double>(), p.at("volumetricLosses").get<double>(),
68
+ p.value("origin", std::string())});
69
+ }
70
+ json out = OpenMagnetics::MaterialValidator().validate(material, points);
71
+ return out;
72
+ }
73
+
74
+ json validate_all_materials(std::string coreMaterialsPath, std::string advancedCoreMaterialsPath) {
75
+ OpenMagnetics::MaterialValidator validator;
76
+ std::vector<OpenMagnetics::MaterialVerdict> verdicts;
77
+ if (coreMaterialsPath.empty()) {
78
+ if (!advancedCoreMaterialsPath.empty()) {
79
+ throw std::invalid_argument("advanced_core_materials_path needs core_materials_path");
80
+ }
81
+ verdicts = validator.validate_embedded_catalogue();
82
+ }
83
+ else {
84
+ verdicts = validator.validate_catalogue(coreMaterialsPath, advancedCoreMaterialsPath.empty() ? std::nullopt : std::optional<std::string>(advancedCoreMaterialsPath));
85
+ }
86
+ json out = OpenMagnetics::summarize_material_verdicts(verdicts);
87
+ out["verdicts"] = verdicts;
88
+ out["classTable"] = OpenMagnetics::MaterialValidator::class_table_json();
89
+ return out;
90
+ }
91
+
62
92
  json calculate_steinmetz_coefficients(json dataJson, json rangesJson) {
63
93
  std::vector<std::pair<double, double>> ranges;
64
94
  for (auto rangeJson : rangesJson) {
@@ -334,6 +364,42 @@ void register_losses_bindings(py::module& m) {
334
364
  py::arg("core_data"), py::arg("coil_data"), py::arg("inputs_data"), py::arg("models_data"),
335
365
  py::call_guard<py::gil_scoped_release>());
336
366
 
367
+ m.def("validate_material", &validate_material,
368
+ R"pbdoc(
369
+ Magnetic Blade Runner: check one MAS core-material record against physics.
370
+
371
+ Evaluates the record with MKF's own loss/permeability/saturation models and judges the
372
+ results against published class envelopes (each with its datasheet source).
373
+
374
+ Args:
375
+ material: one MAS core-material record (a line of core_materials.ndjson).
376
+ extra_loss_points: optional measured points kept outside the record, each
377
+ {frequency, magneticFluxDensityPeak, temperature, volumetricLosses, origin}.
378
+
379
+ Returns:
380
+ {reference, valid, materialClass, findings: [{code, severity (WARNING/SUSPICIOUS/
381
+ IMPOSSIBLE), component, reference, message, value, threshold}], skipped: ["RULE: reason"]}.
382
+ valid is False iff a finding is IMPOSSIBLE.
383
+ )pbdoc",
384
+ py::arg("material"), py::arg("extra_loss_points") = json::array(),
385
+ py::call_guard<py::gil_scoped_release>());
386
+
387
+ m.def("validate_all_materials", &validate_all_materials,
388
+ R"pbdoc(
389
+ Magnetic Blade Runner over a whole core-material catalogue.
390
+
391
+ Args:
392
+ core_materials_path: a core_materials.ndjson; empty = the catalogue built into MKF.
393
+ advanced_core_materials_path: optional advanced_core_materials.ndjson (real file,
394
+ not a git-LFS pointer) whose sinusoidal loss points are checked too.
395
+
396
+ Returns:
397
+ Summary {records, invalid, recordsWithSuspicious, unclassified, findingsByCode,
398
+ skippedByCode, provenance, impossible, suspicious} plus verdicts and classTable.
399
+ )pbdoc",
400
+ py::arg("core_materials_path") = "", py::arg("advanced_core_materials_path") = "",
401
+ py::call_guard<py::gil_scoped_release>());
402
+
337
403
  m.def("get_core_losses_model_information", &get_core_losses_model_information,
338
404
  R"pbdoc(
339
405
  Get documentation and metadata for available core loss models.
@@ -7,6 +7,10 @@ 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
+
11
+ // Magnetic Blade Runner: physics validation of MAS core-material records
12
+ json validate_material(json material, json extraLossPoints);
13
+ json validate_all_materials(std::string coreMaterialsPath, std::string advancedCoreMaterialsPath);
10
14
  json calculate_steinmetz_coefficients(json dataJson, json rangesJson);
11
15
  json calculate_steinmetz_coefficients_with_error(json dataJson, json rangesJson);
12
16
 
@@ -193,6 +193,9 @@ json get_settings() {
193
193
  // Memory kept for the turn-field sums of recently evaluated meshes (0 keeps nothing).
194
194
  settingsJson["magneticFieldTurnSumsCacheBytes"] = OpenMagnetics::settings.get_magnetic_field_turn_sums_cache_bytes();
195
195
 
196
+ // Thermal network: a turn overlapping its enclosure throws (true) or loses that conduction path with an ERROR logged (false).
197
+ settingsJson["thermalNetworkStrictGeometry"] = OpenMagnetics::settings.get_thermal_network_strict_geometry();
198
+
196
199
  // Models
197
200
  {
198
201
  json aux;
@@ -379,6 +382,9 @@ void set_settings(json settingsJson) {
379
382
  if (settingsJson.contains("harmonicAmplitudeThreshold")) OpenMagnetics::settings.set_harmonic_amplitude_threshold(settingsJson["harmonicAmplitudeThreshold"]);
380
383
  if (settingsJson.contains("magneticFieldTurnSumsCacheBytes")) OpenMagnetics::settings.set_magnetic_field_turn_sums_cache_bytes(settingsJson["magneticFieldTurnSumsCacheBytes"].get<size_t>());
381
384
 
385
+ // Thermal network
386
+ if (settingsJson.contains("thermalNetworkStrictGeometry")) OpenMagnetics::settings.set_thermal_network_strict_geometry(settingsJson["thermalNetworkStrictGeometry"]);
387
+
382
388
  // Models
383
389
  if (settingsJson.contains("magneticFieldStrengthModel")) {
384
390
  OpenMagnetics::MagneticFieldStrengthModels model;
@@ -23,6 +23,31 @@ json simulate(json inputsJson, json magneticJson, json modelsData) {
23
23
  return result;
24
24
  }
25
25
 
26
+ json build_datasheet(json inputsJson, json magneticJson, json modelsData) {
27
+ // Simulate the magnetic at the given operating points (the same models as simulate), then let
28
+ // MagneticSimulator::build_datasheet read the outputs into a manufacturer-style datasheet. Returns
29
+ // the magnetic's manufacturerInfo with datasheetInfo filled in; its other fields are kept.
30
+ OpenMagnetics::Inputs inputs(inputsJson);
31
+ OpenMagnetics::Magnetic magnetic(magneticJson);
32
+
33
+ auto reluctanceModelName = OpenMagnetics::defaults.reluctanceModelDefault;
34
+ find_reluctance_model(modelsData, reluctanceModelName);
35
+ auto coreLossesModelName = OpenMagnetics::defaults.coreLossesModelDefault;
36
+ if (!modelsData.is_null() && modelsData.find("coreLosses") != modelsData.end()) {
37
+ OpenMagnetics::from_json(modelsData["coreLosses"], coreLossesModelName);
38
+ }
39
+
40
+ OpenMagnetics::MagneticSimulator magneticSimulator;
41
+ magneticSimulator.set_core_losses_model_name(coreLossesModelName);
42
+ magneticSimulator.set_reluctance_model_name(reluctanceModelName);
43
+ auto mas = magneticSimulator.simulate(inputs, magnetic);
44
+ auto manufacturerInfo = magneticSimulator.build_datasheet(mas);
45
+
46
+ json result;
47
+ to_json(result, manufacturerInfo);
48
+ return result;
49
+ }
50
+
26
51
  std::string export_magnetic_as_subcircuit(json magneticJson) {
27
52
  // Returns the raw SPICE subcircuit netlist as a plain string. We must NOT
28
53
  // return nlohmann::ordered_json here: pybind11_json registers a caster for
@@ -418,6 +443,27 @@ void register_simulation_bindings(py::module& m) {
418
443
  )pbdoc",
419
444
  py::call_guard<py::gil_scoped_release>());
420
445
 
446
+ m.def("build_datasheet", &build_datasheet,
447
+ R"pbdoc(
448
+ Build a manufacturer-style datasheet for a magnetic from MKF's own simulation.
449
+
450
+ Simulates the magnetic at the given operating points, then reads the results into
451
+ manufacturerInfo.datasheetInfo (MagneticSimulator::build_datasheet): inductance at the
452
+ first operating point, DC resistance, the heat-limited rated current (40 K rise), the
453
+ peak saturation current at the hottest operating point, self-resonant frequency and an
454
+ impedance sweep, thermal and mechanical data.
455
+
456
+ Args:
457
+ inputs_json: MAS Inputs: the conditions the datasheet is stated at.
458
+ magnetic_json: the magnetic (core and coil).
459
+ models_json: models to simulate with, as for simulate().
460
+
461
+ Returns:
462
+ The magnetic's manufacturerInfo with datasheetInfo filled in.
463
+ )pbdoc",
464
+ py::arg("inputs_json"), py::arg("magnetic_json"), py::arg("models_json"),
465
+ py::call_guard<py::gil_scoped_release>());
466
+
421
467
  m.def("export_magnetic_as_subcircuit", &export_magnetic_as_subcircuit,
422
468
  R"pbdoc(
423
469
  Export a magnetic component as a SPICE-compatible subcircuit.
@@ -6,6 +6,7 @@ namespace PyMKF {
6
6
 
7
7
  // Simulation
8
8
  json simulate(json inputsJson, json magneticJson, json modelsData);
9
+ json build_datasheet(json inputsJson, json magneticJson, json modelsData);
9
10
 
10
11
  // Export
11
12
  std::string export_magnetic_as_subcircuit(json magneticJson);
@@ -142,3 +142,18 @@ def test_results_can_come_back_ranked_but_unsimulated(mixed_catalogue):
142
142
  built = references_of(simulated).index("BUILT")
143
143
  assert simulated["data"][built]["mas"].get("outputs"), "the default still simulates a modelled part"
144
144
  assert not ranked["data"][built]["mas"].get("outputs")
145
+
146
+
147
+ def test_build_datasheet_states_what_mkf_simulates_for_a_modelled_part():
148
+ """A modelled part with no datasheet gets one from MKF: the values a catalogue datasheet
149
+ states, computed at the given conditions, with the part's own manufacturerInfo kept."""
150
+ part = modelled_part("BUILT")
151
+ del part["manufacturerInfo"]["datasheetInfo"]
152
+ info = PyOpenMagnetics.build_datasheet(buck_inputs(), PyOpenMagnetics.magnetic_autocomplete(part, {}), {})
153
+
154
+ assert info["reference"] == "BUILT"
155
+ electrical = info["datasheetInfo"]["electrical"][0]
156
+ assert electrical["subtype"] == "inductor"
157
+ for key in ("inductance", "dcResistance", "ratedCurrents", "saturationCurrentPeak", "selfResonantFrequency"):
158
+ assert electrical.get(key) is not None, key
159
+ assert electrical["ratedCurrents"][0] > 0 and electrical["saturationCurrentPeak"] > 0
@@ -171,6 +171,22 @@ class TestSettingsManagement:
171
171
  assert after_reset is not None
172
172
  assert isinstance(after_reset, dict)
173
173
 
174
+ def test_thermal_network_strict_geometry_round_trip(self):
175
+ """thermalNetworkStrictGeometry defaults to strict and round-trips through set_settings."""
176
+ PyOpenMagnetics.reset_settings()
177
+ try:
178
+ assert PyOpenMagnetics.get_settings()["thermalNetworkStrictGeometry"] is True
179
+
180
+ settings = PyOpenMagnetics.get_settings()
181
+ settings["thermalNetworkStrictGeometry"] = False
182
+ PyOpenMagnetics.set_settings(settings)
183
+ assert PyOpenMagnetics.get_settings()["thermalNetworkStrictGeometry"] is False
184
+
185
+ PyOpenMagnetics.reset_settings()
186
+ assert PyOpenMagnetics.get_settings()["thermalNetworkStrictGeometry"] is True
187
+ finally:
188
+ PyOpenMagnetics.reset_settings()
189
+
174
190
 
175
191
  class TestBobbinCreation:
176
192
  """Test bobbin creation for cores."""
@@ -0,0 +1,40 @@
1
+ """Magnetic Blade Runner (MKF MaterialValidator) through the Python binding."""
2
+ import copy
3
+
4
+ import pytest
5
+ import PyOpenMagnetics
6
+
7
+
8
+ def _material(name):
9
+ return PyOpenMagnetics.get_material_data(name)
10
+
11
+
12
+ def test_real_power_ferrite_is_clean():
13
+ verdict = PyOpenMagnetics.validate_material(_material("N87"))
14
+ assert verdict["valid"] is True
15
+ assert verdict["findings"] == []
16
+ assert verdict["materialClass"] == "MnZn power ferrite"
17
+
18
+
19
+ def test_saturation_above_iron_is_impossible():
20
+ record = copy.deepcopy(_material("N87"))
21
+ record["name"] = "N87 Bs 3 T"
22
+ record["saturation"][0]["magneticFluxDensity"] = 3.0
23
+ verdict = PyOpenMagnetics.validate_material(record)
24
+ assert verdict["valid"] is False
25
+ assert any(f["code"] == "MAT_BSAT_CEILING" and f["severity"] == "IMPOSSIBLE" for f in verdict["findings"])
26
+
27
+
28
+ def test_extra_points_hysteresis_bound():
29
+ points = [{"frequency": 1e3, "magneticFluxDensityPeak": 0.05, "temperature": 25,
30
+ "volumetricLosses": 80e3, "origin": "manufacturer"}]
31
+ verdict = PyOpenMagnetics.validate_material(_material("P63"), points)
32
+ assert any(f["code"] == "MAT_LOSS_HYSTERESIS_BOUND" for f in verdict["findings"])
33
+
34
+
35
+ def test_embedded_catalogue_sweep():
36
+ summary = PyOpenMagnetics.validate_all_materials()
37
+ assert summary["records"] > 1000
38
+ assert len(summary["verdicts"]) == summary["records"]
39
+ assert "findingsByCode" in summary
40
+ assert len(summary["classTable"]) > 0