PyOpenMagnetics 1.7.31__tar.gz → 1.7.32__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.31 → pyopenmagnetics-1.7.32}/CMakeLists.txt +1 -1
  2. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/PKG-INFO +1 -1
  3. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/pyproject.toml +1 -1
  4. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/database.cpp +55 -3
  5. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_magnetics_catalogue_load.py +35 -0
  6. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/.github/workflows/ci.yml +0 -0
  7. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/.github/workflows/publish.yml +0 -0
  8. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/.gitignore +0 -0
  9. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/AGENTS.md +0 -0
  10. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/LICENSE +0 -0
  11. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/PyOpenMagnetics.pyi +0 -0
  12. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/README.md +0 -0
  13. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/_packaging/__init__.py +0 -0
  14. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/_packaging/_build_info.py.in +0 -0
  15. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/api/MAS.py +0 -0
  16. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/api/mas_db_reader.py +0 -0
  17. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/api/validation.py +0 -0
  18. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/clear_cibuildwheel_cache.sh +0 -0
  19. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/docs/compatibility.md +0 -0
  20. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/docs/errors.md +0 -0
  21. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/docs/performance.md +0 -0
  22. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/README.md +0 -0
  23. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/buck_inductor.py +0 -0
  24. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/complete_simulation_example.py +0 -0
  25. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/converter_design_example.py +0 -0
  26. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/debug_bobbin.py +0 -0
  27. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/debug_coil.py +0 -0
  28. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/debug_core.py +0 -0
  29. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/debug_plotting.py +0 -0
  30. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_220v_12v_1a.py +0 -0
  31. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_220v_12v_2a_complete.py +0 -0
  32. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_bh_curve.png +0 -0
  33. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_core.png +0 -0
  34. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_design.py +0 -0
  35. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_summary.png +0 -0
  36. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/flyback_waveforms.png +0 -0
  37. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/list_plot_funcs.py +0 -0
  38. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/plot_flyback_design.py +0 -0
  39. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/plot_flyback_pyom.py +0 -0
  40. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/test_field_calc.py +0 -0
  41. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/examples/test_field_plot.py +0 -0
  42. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/force_fresh_build.sh +0 -0
  43. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/llms.txt +0 -0
  44. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/notebooks/01_getting_started.ipynb +0 -0
  45. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/notebooks/02_buck_inductor.ipynb +0 -0
  46. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/notebooks/03_core_losses.ipynb +0 -0
  47. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/notebooks/README.md +0 -0
  48. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/requirements.txt +0 -0
  49. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/advisers.cpp +0 -0
  50. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/advisers.h +0 -0
  51. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/bobbin.cpp +0 -0
  52. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/bobbin.h +0 -0
  53. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/cmc.cpp +0 -0
  54. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/cmc.h +0 -0
  55. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/common.h +0 -0
  56. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/converter.cpp +0 -0
  57. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/converter.h +0 -0
  58. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/core.cpp +0 -0
  59. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/core.h +0 -0
  60. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/crossref.cpp +0 -0
  61. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/crossref.h +0 -0
  62. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/database.h +0 -0
  63. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/logging.cpp +0 -0
  64. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/logging.h +0 -0
  65. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/losses.cpp +0 -0
  66. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/losses.h +0 -0
  67. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/module.cpp +0 -0
  68. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/plotting.cpp +0 -0
  69. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/plotting.h +0 -0
  70. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/settings.cpp +0 -0
  71. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/settings.h +0 -0
  72. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/simulation.cpp +0 -0
  73. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/simulation.h +0 -0
  74. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/utils.cpp +0 -0
  75. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/utils.h +0 -0
  76. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/winding.cpp +0 -0
  77. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/winding.h +0 -0
  78. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/wire.cpp +0 -0
  79. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/src/wire.h +0 -0
  80. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/test.py +0 -0
  81. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/__init__.py +0 -0
  82. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/conftest.py +0 -0
  83. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_converter_endpoints.py +0 -0
  84. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_core.py +0 -0
  85. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_core_adviser.py +0 -0
  86. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_datasheet_only_catalogue.py +0 -0
  87. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_examples_integration.py +0 -0
  88. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_inputs.py +0 -0
  89. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_logging.py +0 -0
  90. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_losses_not_evaluable_catalogue.py +0 -0
  91. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_magnetic_adviser.py +0 -0
  92. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_mas_schema_walk.py +0 -0
  93. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_material_validator.py +0 -0
  94. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/tests/test_plotting.py +0 -0
  95. {pyopenmagnetics-1.7.31 → pyopenmagnetics-1.7.32}/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-02-mkf-7b80390f")
145
+ set(MKF_FORCE_REFRESH "2026-10-03-mkf-5055470b")
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.31
3
+ Version: 1.7.32
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.31"
7
+ version = "1.7.32"
8
8
  requires-python = ">=3.8"
9
9
  authors = [
10
10
  { name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
@@ -234,12 +234,20 @@ struct RejectedRecord {
234
234
  // Read the whole stream before touching the cache, so a strict load is all-or-nothing
235
235
  // and a tolerant one is exact about what it dropped. Every failure is reported as
236
236
  // "<source>:<line> (part 'X'): <what actually went wrong>".
237
+ //
238
+ // Two records under one manufacturerInfo.reference are an error too. The cache is keyed
239
+ // by the reference, so the second used to replace the first without a word: a catalogue
240
+ // of 2 came back as 1 (nine WE Midcom parts whose OEM P/N cell read "#N/A" collapsed to
241
+ // one). Nothing here can tell which of the two is that part's real record, so a strict
242
+ // load refuses the file and a tolerant one rejects every record sharing the reference.
237
243
  std::vector<std::pair<std::string, OpenMagnetics::Magnetic>> stage_magnetics(std::istream& in,
238
244
  const std::string& source,
239
245
  bool expand,
240
246
  bool skipInvalid,
241
247
  std::vector<RejectedRecord>& rejected) {
242
248
  std::vector<std::pair<std::string, OpenMagnetics::Magnetic>> staged;
249
+ std::vector<size_t> stagedLineNumbers;
250
+ std::map<std::string, size_t> firstLineOfReference;
243
251
  std::string line;
244
252
  size_t lineNumber = 0;
245
253
  while (getline(in, line)) {
@@ -247,8 +255,9 @@ std::vector<std::pair<std::string, OpenMagnetics::Magnetic>> stage_magnetics(std
247
255
  if (line.find_first_not_of(" \t\r\n") == std::string::npos) {
248
256
  continue;
249
257
  }
258
+ std::pair<std::string, OpenMagnetics::Magnetic> record;
250
259
  try {
251
- staged.push_back(read_magnetic_line(line, expand));
260
+ record = read_magnetic_line(line, expand);
252
261
  }
253
262
  catch (const std::exception& e) {
254
263
  std::string reference;
@@ -262,7 +271,46 @@ std::vector<std::pair<std::string, OpenMagnetics::Magnetic>> stage_magnetics(std
262
271
  throw std::runtime_error(locate(source, lineNumber, reference) + ": " + e.what());
263
272
  }
264
273
  rejected.push_back({lineNumber, reference, e.what()});
274
+ continue;
275
+ }
276
+ auto [first, isNew] = firstLineOfReference.emplace(record.first, lineNumber);
277
+ if (!isNew && !skipInvalid) {
278
+ throw std::runtime_error(locate(source, lineNumber, record.first) + ": manufacturerInfo.reference '" +
279
+ record.first + "' is also the reference of line " +
280
+ std::to_string(first->second) + ", and the magnetics cache is keyed by it: "
281
+ "one of the two records would silently replace the other");
282
+ }
283
+ staged.push_back(std::move(record));
284
+ stagedLineNumbers.push_back(lineNumber);
285
+ }
286
+
287
+ if (skipInvalid) {
288
+ std::map<std::string, std::vector<size_t>> linesOfReference;
289
+ for (size_t index = 0; index < staged.size(); ++index) {
290
+ linesOfReference[staged[index].first].push_back(stagedLineNumbers[index]);
291
+ }
292
+ std::vector<std::pair<std::string, OpenMagnetics::Magnetic>> unique;
293
+ for (size_t index = 0; index < staged.size(); ++index) {
294
+ const auto& lines = linesOfReference[staged[index].first];
295
+ if (lines.size() == 1) {
296
+ unique.push_back(std::move(staged[index]));
297
+ continue;
298
+ }
299
+ std::string others;
300
+ for (auto otherLine : lines) {
301
+ if (otherLine != stagedLineNumbers[index]) {
302
+ others += (others.empty() ? "" : ", ") + std::to_string(otherLine);
303
+ }
304
+ }
305
+ rejected.push_back({stagedLineNumbers[index], staged[index].first,
306
+ "manufacturerInfo.reference '" + staged[index].first +
307
+ "' is also the reference of line " + others +
308
+ ", and the magnetics cache is keyed by it: every record under it is rejected,"
309
+ " since nothing tells which one is the part's real record"});
265
310
  }
311
+ staged = std::move(unique);
312
+ std::stable_sort(rejected.begin(), rejected.end(),
313
+ [](const RejectedRecord& a, const RejectedRecord& b) { return a.lineNumber < b.lineNumber; });
266
314
  }
267
315
  return staged;
268
316
  }
@@ -419,7 +467,9 @@ void register_database_bindings(py::module& m) {
419
467
  (ABT #823). The first record that cannot be loaded raises EngineError naming
420
468
  the file, the line number, the part reference and the underlying reason.
421
469
  Blank lines are skipped. Records are keyed by manufacturerInfo.reference, so
422
- a record without one is an error rather than an anonymous optional access.
470
+ a record without one is an error rather than an anonymous optional access, and
471
+ two records sharing one raise EngineError naming the reference and both lines
472
+ instead of the second silently replacing the first.
423
473
 
424
474
  Use load_magnetics_from_file_report() instead when a partial catalogue is
425
475
  acceptable and you want the list of rejected records.
@@ -440,7 +490,9 @@ void register_database_bindings(py::module& m) {
440
490
  The tolerant counterpart of load_magnetics_from_file (ABT #823): a bad record
441
491
  no longer aborts the batch, and the caller is told exactly which records were
442
492
  dropped and why instead of silently running on a truncated catalogue. A file
443
- that cannot be opened is still an error.
493
+ that cannot be opened is still an error. Records sharing a
494
+ manufacturerInfo.reference are all rejected, each naming the other lines,
495
+ since nothing tells which of them is the part's real record.
444
496
 
445
497
  Args:
446
498
  path: Path to the NDJSON file.
@@ -140,6 +140,27 @@ class TestStrictLoad:
140
140
  assert "manufacturerInfo" in message, message
141
141
  assert "anonymous.ndjson:1" in message, message
142
142
 
143
+ def test_two_records_sharing_a_reference_raise_and_name_it(self, tmp_path):
144
+ # The cache is keyed by manufacturerInfo.reference: the second record used to
145
+ # replace the first without a word (2 offered, "1" returned). Nine WE Midcom
146
+ # parts whose OEM P/N cell read "#N/A" collapsed to one that way.
147
+ first = magnetic("#N/A")
148
+ second = magnetic("#N/A")
149
+ second["coil"]["functionalDescription"][0]["numberTurns"] = 40
150
+ path = write_ndjson(tmp_path / "duplicates.ndjson", [magnetic("K1"), first, second])
151
+
152
+ with pytest.raises(PyOpenMagnetics.EngineError) as error:
153
+ PyOpenMagnetics.load_magnetics_from_file(path, True)
154
+
155
+ message = str(error.value)
156
+ assert "duplicates.ndjson:3" in message, message
157
+ assert "'#N/A'" in message, message
158
+ assert "line 2" in message, message
159
+
160
+ # All or nothing: none of the file reached the cache.
161
+ good = write_ndjson(tmp_path / "good.ndjson", [magnetic("K9")])
162
+ assert PyOpenMagnetics.load_magnetics_from_file(good, True) == "1"
163
+
143
164
 
144
165
  class TestReportingLoad:
145
166
  """load_magnetics_from_file_report: keep the good records, name the dropped ones."""
@@ -167,6 +188,20 @@ class TestReportingLoad:
167
188
  assert report["loaded"] == 2
168
189
  assert report["rejected"] == []
169
190
 
191
+ def test_records_sharing_a_reference_are_all_rejected(self, tmp_path):
192
+ path = write_ndjson(
193
+ tmp_path / "duplicates.ndjson",
194
+ [magnetic("L1"), magnetic("#N/A"), magnetic("L3"), magnetic("#N/A")],
195
+ )
196
+
197
+ report = PyOpenMagnetics.load_magnetics_from_file_report(path, True)
198
+
199
+ assert report["loaded"] == 2
200
+ assert report["cacheSize"] == 2
201
+ assert [(r["line"], r["reference"]) for r in report["rejected"]] == [(2, "#N/A"), (4, "#N/A")]
202
+ assert "line 4" in report["rejected"][0]["reason"]
203
+ assert "line 2" in report["rejected"][1]["reason"]
204
+
170
205
  def test_missing_file_still_raises(self, tmp_path):
171
206
  with pytest.raises(PyOpenMagnetics.EngineError):
172
207
  PyOpenMagnetics.load_magnetics_from_file_report(str(tmp_path / "nope.ndjson"), True)