PyOpenMagnetics 1.1.2__tar.gz → 1.1.4__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.
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/PKG-INFO +7 -6
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/PyOpenMagnetics.pyi +35 -4
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/README.md +6 -5
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/examples/README.md +7 -6
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/examples/buck_inductor.py +9 -6
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/examples/flyback_design.py +22 -7
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/llms.txt +12 -7
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/notebooks/02_buck_inductor.ipynb +25 -10
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/pyproject.toml +5 -3
- pyopenmagnetics-1.1.4/src/logging.cpp +191 -0
- pyopenmagnetics-1.1.4/src/logging.h +19 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/module.cpp +2 -0
- pyopenmagnetics-1.1.4/tests/test_logging.py +259 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_magnetic_adviser.py +29 -15
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/.github/workflows/ci.yml +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/.github/workflows/publish.yml +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/.gitignore +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/CMakeLists.txt +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/LICENSE +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/api/MAS.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/api/mas_db_reader.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/api/validation.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/docs/compatibility.md +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/docs/errors.md +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/docs/performance.md +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/notebooks/01_getting_started.ipynb +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/notebooks/03_core_losses.ipynb +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/notebooks/README.md +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/requirements.txt +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/advisers.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/advisers.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/bobbin.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/bobbin.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/common.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/core.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/core.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/database.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/database.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/losses.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/losses.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/plotting.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/plotting.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/settings.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/settings.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/simulation.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/simulation.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/utils.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/utils.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/winding.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/winding.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/wire.cpp +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/src/wire.h +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/test.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/__init__.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/conftest.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_core.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_core_adviser.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_examples_integration.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_inputs.py +0 -0
- {pyopenmagnetics-1.1.2 → pyopenmagnetics-1.1.4}/tests/test_winding.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: PyOpenMagnetics
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Python wrapper for OpenMagnetics
|
|
5
5
|
Author-Email: Alfonso Martinez <Alfonso_VII@hotmail.com>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -121,12 +121,13 @@ processed_inputs = PyOpenMagnetics.process_inputs(inputs)
|
|
|
121
121
|
|
|
122
122
|
# Get magnetic recommendations
|
|
123
123
|
# core_mode: "available cores" (stock cores) or "standard cores" (all standard shapes)
|
|
124
|
-
|
|
124
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "standard cores")
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
# Result format: {"data": [{"mas": {...}, "scoring": float, "scoringPerFilter": {...}}, ...]}
|
|
127
|
+
for i, item in enumerate(result["data"]):
|
|
128
|
+
mag = item["mas"]["magnetic"]
|
|
129
|
+
core = mag["core"]["functionalDescription"]
|
|
130
|
+
print(f"{i+1}. {core['shape']['name']} - {core['material']['name']} (score: {item['scoring']:.3f})")
|
|
130
131
|
```
|
|
131
132
|
|
|
132
133
|
### Calculate Core Losses
|
|
@@ -505,6 +505,13 @@ def calculate_advised_cores(
|
|
|
505
505
|
weights: {"EFFICIENCY": 1.0, "DIMENSIONS": 0.5, "COST": 0.3}.
|
|
506
506
|
max_results: Maximum number of recommendations.
|
|
507
507
|
core_mode: "STANDARD_CORES" or "AVAILABLE_CORES" (stock only).
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
JSON object with "data" array containing ranked results.
|
|
511
|
+
Each result has:
|
|
512
|
+
- "mas": Mas object with magnetic, inputs, and optionally outputs
|
|
513
|
+
- "scoring": Overall float score
|
|
514
|
+
- "scoringPerFilter": Object with individual scores per filter
|
|
508
515
|
"""
|
|
509
516
|
...
|
|
510
517
|
|
|
@@ -512,11 +519,27 @@ def calculate_advised_magnetics(
|
|
|
512
519
|
inputs: Inputs,
|
|
513
520
|
max_results: int = 5,
|
|
514
521
|
core_mode: str = "STANDARD_CORES"
|
|
515
|
-
) ->
|
|
522
|
+
) -> JsonDict:
|
|
516
523
|
"""Get complete magnetic designs (core + winding).
|
|
517
524
|
|
|
525
|
+
Args:
|
|
526
|
+
inputs: Processed inputs (from process_inputs).
|
|
527
|
+
max_results: Maximum number of recommendations.
|
|
528
|
+
core_mode: "STANDARD_CORES" or "AVAILABLE_CORES" (stock only).
|
|
529
|
+
|
|
518
530
|
Returns:
|
|
519
|
-
|
|
531
|
+
JSON object with "data" array containing ranked results.
|
|
532
|
+
Each result has:
|
|
533
|
+
- "mas": Mas object with magnetic, inputs, and optionally outputs
|
|
534
|
+
- "scoring": Overall float score
|
|
535
|
+
- "scoringPerFilter": Object with individual scores per filter
|
|
536
|
+
(e.g., {"COST": 0.8, "LOSSES": 0.9, "DIMENSIONS": 0.7})
|
|
537
|
+
|
|
538
|
+
Example:
|
|
539
|
+
>>> result = PyOpenMagnetics.calculate_advised_magnetics(inputs, 5, "STANDARD_CORES")
|
|
540
|
+
>>> for item in result["data"]:
|
|
541
|
+
... mag = item["mas"]["magnetic"]
|
|
542
|
+
... print(f"Score: {item['scoring']}, Core: {mag['core']['functionalDescription']['shape']['name']}")
|
|
520
543
|
"""
|
|
521
544
|
...
|
|
522
545
|
|
|
@@ -524,8 +547,16 @@ def calculate_advised_magnetics_from_catalog(
|
|
|
524
547
|
inputs: Inputs,
|
|
525
548
|
catalog: List[Magnetic],
|
|
526
549
|
max_results: int = 5
|
|
527
|
-
) ->
|
|
528
|
-
"""Get designs from custom catalog of magnetics.
|
|
550
|
+
) -> JsonDict:
|
|
551
|
+
"""Get designs from custom catalog of magnetics.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
JSON object with "data" array containing ranked results.
|
|
555
|
+
Each result has:
|
|
556
|
+
- "mas": Mas object with magnetic data
|
|
557
|
+
- "scoring": Overall float score
|
|
558
|
+
- "scoringPerFilter": Object with individual scores per filter
|
|
559
|
+
"""
|
|
529
560
|
...
|
|
530
561
|
|
|
531
562
|
# =============================================================================
|
|
@@ -104,12 +104,13 @@ processed_inputs = PyOpenMagnetics.process_inputs(inputs)
|
|
|
104
104
|
|
|
105
105
|
# Get magnetic recommendations
|
|
106
106
|
# core_mode: "available cores" (stock cores) or "standard cores" (all standard shapes)
|
|
107
|
-
|
|
107
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "standard cores")
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
# Result format: {"data": [{"mas": {...}, "scoring": float, "scoringPerFilter": {...}}, ...]}
|
|
110
|
+
for i, item in enumerate(result["data"]):
|
|
111
|
+
mag = item["mas"]["magnetic"]
|
|
112
|
+
core = mag["core"]["functionalDescription"]
|
|
113
|
+
print(f"{i+1}. {core['shape']['name']} - {core['material']['name']} (score: {item['scoring']:.3f})")
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
### Calculate Core Losses
|
|
@@ -60,17 +60,18 @@ inputs = {
|
|
|
60
60
|
processed = PyOpenMagnetics.process_inputs(inputs)
|
|
61
61
|
|
|
62
62
|
# 3. Get recommendations
|
|
63
|
-
|
|
63
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(processed, 5, "STANDARD_CORES")
|
|
64
64
|
|
|
65
|
-
# 4. Analyze results
|
|
66
|
-
for
|
|
65
|
+
# 4. Analyze results - result["data"] contains list of {mas, scoring, scoringPerFilter}
|
|
66
|
+
for item in result["data"]:
|
|
67
|
+
mag = item["mas"]["magnetic"]
|
|
67
68
|
losses = PyOpenMagnetics.calculate_core_losses(
|
|
68
|
-
mag["
|
|
69
|
-
mag["
|
|
69
|
+
mag["core"],
|
|
70
|
+
mag["coil"],
|
|
70
71
|
processed,
|
|
71
72
|
{"coreLosses": "IGSE", "reluctance": "ZHANG"}
|
|
72
73
|
)
|
|
73
|
-
print(f"Core losses: {losses['coreLosses']:.3f} W")
|
|
74
|
+
print(f"Core losses: {losses['coreLosses']:.3f} W, Score: {item['scoring']:.3f}")
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
## Additional Resources
|
|
@@ -104,12 +104,13 @@ def design_buck_inductor():
|
|
|
104
104
|
"COST": 0.3
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
processed,
|
|
109
|
-
max_results=5,
|
|
110
|
-
core_mode="STANDARD_CORES"
|
|
107
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(
|
|
108
|
+
processed, 5, "STANDARD_CORES"
|
|
111
109
|
)
|
|
112
110
|
|
|
111
|
+
# Extract magnetics list from result (v1.1.2+ format: {"data": [...]})
|
|
112
|
+
magnetics = result["data"] if isinstance(result, dict) and "data" in result else result
|
|
113
|
+
|
|
113
114
|
print(f" Found {len(magnetics)} suitable designs")
|
|
114
115
|
|
|
115
116
|
# Analyze designs
|
|
@@ -121,8 +122,10 @@ def design_buck_inductor():
|
|
|
121
122
|
"reluctance": "ZHANG"
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
for i,
|
|
125
|
-
|
|
125
|
+
for i, item in enumerate(magnetics[:3]):
|
|
126
|
+
# In v1.1.2+, each item has "mas" containing the magnetic data
|
|
127
|
+
mas = item.get("mas", item) if isinstance(item, dict) else item
|
|
128
|
+
if not isinstance(mas, dict) or "magnetic" not in mas:
|
|
126
129
|
continue
|
|
127
130
|
|
|
128
131
|
magnetic = mas["magnetic"]
|
|
@@ -107,12 +107,18 @@ def design_flyback_transformer():
|
|
|
107
107
|
"COST": 0.3 # Tertiary: low cost
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
processed_inputs,
|
|
112
|
-
max_results=3,
|
|
113
|
-
core_mode="STANDARD_CORES"
|
|
110
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(
|
|
111
|
+
processed_inputs, 3, "STANDARD_CORES"
|
|
114
112
|
)
|
|
115
113
|
|
|
114
|
+
# Extract magnetics list from result (v1.1.2+ format: {"data": [...]})
|
|
115
|
+
# Note: If there's an error, data will be a string containing the exception message
|
|
116
|
+
data = result.get("data", result) if isinstance(result, dict) else result
|
|
117
|
+
if isinstance(data, str):
|
|
118
|
+
print(f" ✗ Error: {data}")
|
|
119
|
+
return None
|
|
120
|
+
magnetics = data
|
|
121
|
+
|
|
116
122
|
print(f" ✓ Found {len(magnetics)} suitable designs")
|
|
117
123
|
|
|
118
124
|
# Step 4: Analyze top recommendations
|
|
@@ -125,8 +131,10 @@ def design_flyback_transformer():
|
|
|
125
131
|
"coreTemperature": "MANIKTALA"
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
for i,
|
|
129
|
-
|
|
134
|
+
for i, item in enumerate(magnetics):
|
|
135
|
+
# In v1.1.2+, each item has "mas" containing the magnetic data
|
|
136
|
+
mas = item.get("mas", item) if isinstance(item, dict) else item
|
|
137
|
+
if not isinstance(mas, dict) or "magnetic" not in mas:
|
|
130
138
|
continue
|
|
131
139
|
|
|
132
140
|
magnetic = mas["magnetic"]
|
|
@@ -154,6 +162,9 @@ def design_flyback_transformer():
|
|
|
154
162
|
total_loss = core_loss + winding_loss
|
|
155
163
|
|
|
156
164
|
print(f"\nDesign #{i+1}: {shape_name} / {material_name}")
|
|
165
|
+
# Show scoring if available (v1.1.2+)
|
|
166
|
+
if 'scoring' in item:
|
|
167
|
+
print(f" Score: {item['scoring']:.3f}")
|
|
157
168
|
print(f" Core losses: {core_loss:.3f} W")
|
|
158
169
|
print(f" Winding losses: {winding_loss:.3f} W")
|
|
159
170
|
print(f" Total losses: {total_loss:.3f} W")
|
|
@@ -167,7 +178,11 @@ def design_flyback_transformer():
|
|
|
167
178
|
print("\n" + "=" * 60)
|
|
168
179
|
print("Design complete! Best design is #1")
|
|
169
180
|
|
|
170
|
-
|
|
181
|
+
# Return the first item's mas for backward compatibility
|
|
182
|
+
if magnetics:
|
|
183
|
+
first_item = magnetics[0]
|
|
184
|
+
return first_item.get("mas", first_item) if isinstance(first_item, dict) else first_item
|
|
185
|
+
return None
|
|
171
186
|
|
|
172
187
|
|
|
173
188
|
def explore_core_database():
|
|
@@ -368,21 +368,25 @@ cores = PyOpenMagnetics.calculate_advised_cores(
|
|
|
368
368
|
)
|
|
369
369
|
|
|
370
370
|
# Get complete magnetic designs
|
|
371
|
-
|
|
371
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(
|
|
372
372
|
processed,
|
|
373
373
|
max_results=5,
|
|
374
374
|
core_mode="STANDARD_CORES"
|
|
375
375
|
)
|
|
376
376
|
|
|
377
|
-
#
|
|
378
|
-
for
|
|
379
|
-
|
|
380
|
-
|
|
377
|
+
# Result structure: {"data": [{"mas": {...}, "scoring": float, "scoringPerFilter": {...}}, ...]}
|
|
378
|
+
for item in result["data"]:
|
|
379
|
+
mag = item["mas"]["magnetic"]
|
|
380
|
+
core_name = mag["core"]["functionalDescription"]["shape"]["name"]
|
|
381
|
+
material = mag["core"]["functionalDescription"]["material"]["name"]
|
|
382
|
+
score = item["scoring"]
|
|
383
|
+
print(f"{core_name} - {material} (score: {score:.3f})")
|
|
381
384
|
|
|
382
385
|
# From custom catalog
|
|
383
|
-
|
|
386
|
+
catalog_result = PyOpenMagnetics.calculate_advised_magnetics_from_catalog(
|
|
384
387
|
processed, catalog_magnetics, max_results=5
|
|
385
388
|
)
|
|
389
|
+
# Same result structure as calculate_advised_magnetics
|
|
386
390
|
```
|
|
387
391
|
|
|
388
392
|
### Simulation
|
|
@@ -538,7 +542,8 @@ flyback = {
|
|
|
538
542
|
"desiredTurnsRatios": [8.0, 19.2] # N_pri/N_sec for each output
|
|
539
543
|
}
|
|
540
544
|
inputs = PyOpenMagnetics.process_flyback(flyback)
|
|
541
|
-
|
|
545
|
+
result = PyOpenMagnetics.calculate_advised_magnetics(inputs, 5, "STANDARD_CORES")
|
|
546
|
+
# Result format: {"data": [{"mas": {...}, "scoring": float, "scoringPerFilter": {...}}, ...]}
|
|
542
547
|
```
|
|
543
548
|
|
|
544
549
|
### Buck Converter (Non-Isolated Step-Down)
|
|
@@ -153,20 +153,26 @@
|
|
|
153
153
|
"# Get magnetic design recommendations\n",
|
|
154
154
|
"print(\"\\nSearching for optimal designs...\")\n",
|
|
155
155
|
"try:\n",
|
|
156
|
-
"
|
|
156
|
+
" result = PyOpenMagnetics.calculate_advised_magnetics(\n",
|
|
157
157
|
" processed,\n",
|
|
158
158
|
" max_results=5,\n",
|
|
159
159
|
" core_mode=\"STANDARD_CORES\"\n",
|
|
160
160
|
" )\n",
|
|
161
161
|
" \n",
|
|
162
|
+
" # Extract magnetics list from result (v1.1.2+ format: {\"data\": [...]})\n",
|
|
163
|
+
" magnetics = result[\"data\"] if isinstance(result, dict) and \"data\" in result else result\n",
|
|
164
|
+
" \n",
|
|
162
165
|
" print(f\"\\nFound {len(magnetics)} suitable designs:\")\n",
|
|
163
|
-
" for i,
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
+
" for i, item in enumerate(magnetics[:5]):\n",
|
|
167
|
+
" # In v1.1.2+, each item has \"mas\" containing the magnetic data\n",
|
|
168
|
+
" mas = item.get(\"mas\", item) if isinstance(item, dict) else item\n",
|
|
169
|
+
" if isinstance(mas, dict) and 'magnetic' in mas:\n",
|
|
170
|
+
" magnetic = mas['magnetic']\n",
|
|
166
171
|
" core = magnetic['core']['functionalDescription']\n",
|
|
167
172
|
" shape_name = core['shape']['name'] if isinstance(core['shape'], dict) else core['shape']\n",
|
|
168
173
|
" mat_name = core['material']['name'] if isinstance(core['material'], dict) else core['material']\n",
|
|
169
|
-
"
|
|
174
|
+
" score = item.get('scoring', 'N/A')\n",
|
|
175
|
+
" print(f\" {i+1}. {shape_name} / {mat_name} (score: {score})\")\n",
|
|
170
176
|
"except Exception as e:\n",
|
|
171
177
|
" print(f\"Adviser error (may need more compute time): {e}\")"
|
|
172
178
|
]
|
|
@@ -236,9 +242,14 @@
|
|
|
236
242
|
" if 'magnetics' in dir() and len(magnetics) > 0:\n",
|
|
237
243
|
" print(\"=== Best Design Analysis ===\\n\")\n",
|
|
238
244
|
" \n",
|
|
239
|
-
" best
|
|
240
|
-
"
|
|
241
|
-
"
|
|
245
|
+
" # Get best result (first in the list)\n",
|
|
246
|
+
" best_item = magnetics[0]\n",
|
|
247
|
+
" \n",
|
|
248
|
+
" # In v1.1.2+, each item has \"mas\" containing the magnetic data\n",
|
|
249
|
+
" best_mas = best_item.get(\"mas\", best_item) if isinstance(best_item, dict) else best_item\n",
|
|
250
|
+
" \n",
|
|
251
|
+
" if isinstance(best_mas, dict) and 'magnetic' in best_mas:\n",
|
|
252
|
+
" magnetic = best_mas['magnetic']\n",
|
|
242
253
|
" core_fd = magnetic['core']['functionalDescription']\n",
|
|
243
254
|
" \n",
|
|
244
255
|
" # Extract shape and material names\n",
|
|
@@ -248,6 +259,10 @@
|
|
|
248
259
|
" print(f\"Shape: {shape}\")\n",
|
|
249
260
|
" print(f\"Material: {material}\")\n",
|
|
250
261
|
" \n",
|
|
262
|
+
" # Show scoring if available (v1.1.2+)\n",
|
|
263
|
+
" if 'scoring' in best_item:\n",
|
|
264
|
+
" print(f\"Score: {best_item['scoring']:.3f}\")\n",
|
|
265
|
+
" \n",
|
|
251
266
|
" # Check gapping\n",
|
|
252
267
|
" gapping = core_fd.get('gapping', [])\n",
|
|
253
268
|
" total_gap = sum(g.get('length', 0) for g in gapping) * 1000\n",
|
|
@@ -263,8 +278,8 @@
|
|
|
263
278
|
" print(f\" Parallels: {winding.get('numberParallels', 1)}\")\n",
|
|
264
279
|
" \n",
|
|
265
280
|
" # Show outputs if available\n",
|
|
266
|
-
" if 'outputs' in
|
|
267
|
-
" outputs =
|
|
281
|
+
" if 'outputs' in best_mas:\n",
|
|
282
|
+
" outputs = best_mas['outputs']\n",
|
|
268
283
|
" print(f\"\\nSimulation Results:\")\n",
|
|
269
284
|
" if 'coreLosses' in outputs:\n",
|
|
270
285
|
" print(f\" Core losses: {outputs['coreLosses']:.3f} W\")\n",
|
|
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "PyOpenMagnetics"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.4"
|
|
8
8
|
requires-python = ">=3.8"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Alfonso Martinez", email="Alfonso_VII@hotmail.com" },
|
|
@@ -56,11 +56,13 @@ before-all = [
|
|
|
56
56
|
"npm --version",
|
|
57
57
|
"npm install -g quicktype@23.0.170"
|
|
58
58
|
]
|
|
59
|
+
environment = { GIT_LFS_SKIP_SMUDGE=1, CMAKE_BUILD_PARALLEL_LEVEL="4" }
|
|
59
60
|
|
|
60
61
|
[tool.cibuildwheel.macos]
|
|
61
62
|
before-all = "npm --version && npm install -g quicktype@23.0.170"
|
|
62
|
-
environment = { MACOSX_DEPLOYMENT_TARGET="14.0", GIT_LFS_SKIP_SMUDGE=1 }
|
|
63
|
+
environment = { MACOSX_DEPLOYMENT_TARGET="14.0", GIT_LFS_SKIP_SMUDGE=1, CMAKE_BUILD_PARALLEL_LEVEL="3" }
|
|
63
64
|
|
|
64
65
|
[tool.cibuildwheel.windows]
|
|
65
66
|
before-all = "rmdir /S /Q build && npm --version && npm install -g quicktype@23.0.170"
|
|
66
|
-
archs = ["AMD64"]
|
|
67
|
+
archs = ["AMD64"]
|
|
68
|
+
environment = { GIT_LFS_SKIP_SMUDGE=1, CMAKE_BUILD_PARALLEL_LEVEL="2" }
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#include "logging.h"
|
|
2
|
+
|
|
3
|
+
// On Windows, ERROR is defined as a macro in windows.h/wingdi.h
|
|
4
|
+
// We need to undef it to use LogLevel::ERROR without macro expansion issues
|
|
5
|
+
#ifdef ERROR
|
|
6
|
+
#undef ERROR
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
namespace PyMKF {
|
|
10
|
+
|
|
11
|
+
// Static shared pointer to the string sink for capturing logs
|
|
12
|
+
static std::shared_ptr<OpenMagnetics::StringSink> stringSink = nullptr;
|
|
13
|
+
|
|
14
|
+
OpenMagnetics::LogLevel parse_log_level(const std::string& level) {
|
|
15
|
+
if (level == "TRACE" || level == "trace") return OpenMagnetics::LogLevel::TRACE;
|
|
16
|
+
if (level == "DEBUG" || level == "debug") return OpenMagnetics::LogLevel::DEBUG;
|
|
17
|
+
if (level == "INFO" || level == "info") return OpenMagnetics::LogLevel::INFO;
|
|
18
|
+
if (level == "WARNING" || level == "warning") return OpenMagnetics::LogLevel::WARNING;
|
|
19
|
+
if (level == "ERROR" || level == "error") return OpenMagnetics::LogLevel::ERROR;
|
|
20
|
+
if (level == "CRITICAL" || level == "critical") return OpenMagnetics::LogLevel::CRITICAL;
|
|
21
|
+
if (level == "OFF" || level == "off") return OpenMagnetics::LogLevel::OFF;
|
|
22
|
+
throw std::invalid_argument("Invalid log level: " + level + ". Valid levels: TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL, OFF");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
void set_log_level(std::string level) {
|
|
26
|
+
auto& logger = OpenMagnetics::Logger::getInstance();
|
|
27
|
+
logger.setLevel(parse_log_level(level));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
std::string get_log_level() {
|
|
31
|
+
auto& logger = OpenMagnetics::Logger::getInstance();
|
|
32
|
+
return OpenMagnetics::to_string(logger.getLevel());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void enable_string_sink() {
|
|
36
|
+
auto& logger = OpenMagnetics::Logger::getInstance();
|
|
37
|
+
if (!stringSink) {
|
|
38
|
+
stringSink = std::make_shared<OpenMagnetics::StringSink>();
|
|
39
|
+
logger.addSink(stringSink);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void disable_string_sink() {
|
|
44
|
+
if (stringSink) {
|
|
45
|
+
// Clear and reset the string sink
|
|
46
|
+
// Note: We can't remove sinks individually, so we just clear it
|
|
47
|
+
stringSink->clear();
|
|
48
|
+
stringSink = nullptr;
|
|
49
|
+
// Reinitialize logger with default console sink
|
|
50
|
+
auto& logger = OpenMagnetics::Logger::getInstance();
|
|
51
|
+
logger.clearSinks();
|
|
52
|
+
logger.addSink(std::make_shared<OpenMagnetics::ConsoleSink>());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
std::string get_logs() {
|
|
57
|
+
if (stringSink) {
|
|
58
|
+
return stringSink->getContents();
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void clear_logs() {
|
|
64
|
+
if (stringSink) {
|
|
65
|
+
stringSink->clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void log_message(std::string level, std::string message, std::string module) {
|
|
70
|
+
auto& logger = OpenMagnetics::Logger::getInstance();
|
|
71
|
+
logger.log(parse_log_level(level), module, message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
void register_logging_bindings(py::module& m) {
|
|
75
|
+
m.def("set_log_level", &set_log_level,
|
|
76
|
+
py::arg("level"),
|
|
77
|
+
R"pbdoc(
|
|
78
|
+
Set the minimum log level for the MKF logger.
|
|
79
|
+
|
|
80
|
+
Messages below this level will be ignored. The logger uses a
|
|
81
|
+
severity-based filtering system.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
level: The minimum log level. One of:
|
|
85
|
+
- "TRACE": Most detailed, for debugging internals
|
|
86
|
+
- "DEBUG": Debug information
|
|
87
|
+
- "INFO": General information
|
|
88
|
+
- "WARNING": Warning messages
|
|
89
|
+
- "ERROR": Error messages
|
|
90
|
+
- "CRITICAL": Critical errors
|
|
91
|
+
- "OFF": Disable all logging
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> PyOpenMagnetics.set_log_level("DEBUG")
|
|
95
|
+
>>> PyOpenMagnetics.set_log_level("WARNING")
|
|
96
|
+
)pbdoc");
|
|
97
|
+
|
|
98
|
+
m.def("get_log_level", &get_log_level,
|
|
99
|
+
R"pbdoc(
|
|
100
|
+
Get the current minimum log level.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
String representation of the current log level
|
|
104
|
+
(TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL, or OFF).
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> level = PyOpenMagnetics.get_log_level()
|
|
108
|
+
>>> print(level) # e.g., "ERROR"
|
|
109
|
+
)pbdoc");
|
|
110
|
+
|
|
111
|
+
m.def("enable_string_sink", &enable_string_sink,
|
|
112
|
+
R"pbdoc(
|
|
113
|
+
Enable capturing logs to an in-memory string buffer.
|
|
114
|
+
|
|
115
|
+
This is useful for testing or programmatic access to log messages.
|
|
116
|
+
Logs can be retrieved using get_logs() and cleared using clear_logs().
|
|
117
|
+
|
|
118
|
+
Note: The string sink is added in addition to the default console sink.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> PyOpenMagnetics.enable_string_sink()
|
|
122
|
+
>>> PyOpenMagnetics.set_log_level("DEBUG")
|
|
123
|
+
>>> # ... perform operations that generate logs ...
|
|
124
|
+
>>> logs = PyOpenMagnetics.get_logs()
|
|
125
|
+
)pbdoc");
|
|
126
|
+
|
|
127
|
+
m.def("disable_string_sink", &disable_string_sink,
|
|
128
|
+
R"pbdoc(
|
|
129
|
+
Disable the in-memory string sink and reset to console-only logging.
|
|
130
|
+
|
|
131
|
+
This clears any captured logs and removes the string sink,
|
|
132
|
+
restoring the default console-only logging behavior.
|
|
133
|
+
)pbdoc");
|
|
134
|
+
|
|
135
|
+
m.def("get_logs", &get_logs,
|
|
136
|
+
R"pbdoc(
|
|
137
|
+
Get all captured log messages from the string sink.
|
|
138
|
+
|
|
139
|
+
Returns the contents of the in-memory log buffer. Requires
|
|
140
|
+
enable_string_sink() to have been called first.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
String containing all captured log messages, or empty string
|
|
144
|
+
if string sink is not enabled.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> PyOpenMagnetics.enable_string_sink()
|
|
148
|
+
>>> PyOpenMagnetics.set_log_level("INFO")
|
|
149
|
+
>>> # ... perform operations ...
|
|
150
|
+
>>> logs = PyOpenMagnetics.get_logs()
|
|
151
|
+
>>> print(logs)
|
|
152
|
+
)pbdoc");
|
|
153
|
+
|
|
154
|
+
m.def("clear_logs", &clear_logs,
|
|
155
|
+
R"pbdoc(
|
|
156
|
+
Clear all captured log messages from the string sink.
|
|
157
|
+
|
|
158
|
+
Empties the in-memory log buffer without disabling the string sink.
|
|
159
|
+
Useful for clearing logs between test cases.
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
>>> PyOpenMagnetics.enable_string_sink()
|
|
163
|
+
>>> # ... perform some operations ...
|
|
164
|
+
>>> PyOpenMagnetics.clear_logs() # Start fresh
|
|
165
|
+
>>> # ... perform more operations ...
|
|
166
|
+
>>> logs = PyOpenMagnetics.get_logs() # Only new logs
|
|
167
|
+
)pbdoc");
|
|
168
|
+
|
|
169
|
+
m.def("log_message", &log_message,
|
|
170
|
+
py::arg("level"),
|
|
171
|
+
py::arg("message"),
|
|
172
|
+
py::arg("module") = "",
|
|
173
|
+
R"pbdoc(
|
|
174
|
+
Log a message at the specified level.
|
|
175
|
+
|
|
176
|
+
This allows Python code to log messages through the MKF logging
|
|
177
|
+
system, which can be useful for unified logging in mixed
|
|
178
|
+
Python/C++ workflows.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
level: Log level (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
182
|
+
message: The message to log
|
|
183
|
+
module: Optional module name for categorization
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> PyOpenMagnetics.log_message("INFO", "Starting calculation")
|
|
187
|
+
>>> PyOpenMagnetics.log_message("DEBUG", "Value computed", "MyModule")
|
|
188
|
+
)pbdoc");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
} // namespace PyMKF
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "common.h"
|
|
4
|
+
#include "support/Logger.h"
|
|
5
|
+
|
|
6
|
+
namespace PyMKF {
|
|
7
|
+
|
|
8
|
+
// Logging functions
|
|
9
|
+
void set_log_level(std::string level);
|
|
10
|
+
std::string get_log_level();
|
|
11
|
+
void enable_string_sink();
|
|
12
|
+
void disable_string_sink();
|
|
13
|
+
std::string get_logs();
|
|
14
|
+
void clear_logs();
|
|
15
|
+
void log_message(std::string level, std::string message, std::string module = "");
|
|
16
|
+
|
|
17
|
+
void register_logging_bindings(py::module& m);
|
|
18
|
+
|
|
19
|
+
} // namespace PyMKF
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#include "plotting.h"
|
|
11
11
|
#include "settings.h"
|
|
12
12
|
#include "utils.h"
|
|
13
|
+
#include "logging.h"
|
|
13
14
|
|
|
14
15
|
PYBIND11_MODULE(PyOpenMagnetics, m) {
|
|
15
16
|
m.doc() = "OpenMagnetics Python bindings for magnetic component design";
|
|
@@ -26,4 +27,5 @@ PYBIND11_MODULE(PyOpenMagnetics, m) {
|
|
|
26
27
|
PyMKF::register_plotting_bindings(m);
|
|
27
28
|
PyMKF::register_settings_bindings(m);
|
|
28
29
|
PyMKF::register_utils_bindings(m);
|
|
30
|
+
PyMKF::register_logging_bindings(m);
|
|
29
31
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for PyOpenMagnetics logging functionality.
|
|
3
|
+
|
|
4
|
+
These tests verify that the MKF logger is properly exposed in PyMKF
|
|
5
|
+
and that verbosity levels and log capture work correctly.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
import PyOpenMagnetics
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestLogLevel:
|
|
12
|
+
"""Tests for log level get/set functionality."""
|
|
13
|
+
|
|
14
|
+
def test_get_default_log_level(self):
|
|
15
|
+
"""Default log level should be ERROR."""
|
|
16
|
+
# Reset to ensure default state
|
|
17
|
+
PyOpenMagnetics.disable_string_sink()
|
|
18
|
+
level = PyOpenMagnetics.get_log_level()
|
|
19
|
+
assert level == "ERROR"
|
|
20
|
+
|
|
21
|
+
def test_set_log_level_debug(self):
|
|
22
|
+
"""Should be able to set log level to DEBUG."""
|
|
23
|
+
PyOpenMagnetics.set_log_level("DEBUG")
|
|
24
|
+
assert PyOpenMagnetics.get_log_level() == "DEBUG"
|
|
25
|
+
|
|
26
|
+
def test_set_log_level_info(self):
|
|
27
|
+
"""Should be able to set log level to INFO."""
|
|
28
|
+
PyOpenMagnetics.set_log_level("INFO")
|
|
29
|
+
assert PyOpenMagnetics.get_log_level() == "INFO"
|
|
30
|
+
|
|
31
|
+
def test_set_log_level_warning(self):
|
|
32
|
+
"""Should be able to set log level to WARNING."""
|
|
33
|
+
PyOpenMagnetics.set_log_level("WARNING")
|
|
34
|
+
assert PyOpenMagnetics.get_log_level() == "WARNING"
|
|
35
|
+
|
|
36
|
+
def test_set_log_level_error(self):
|
|
37
|
+
"""Should be able to set log level to ERROR."""
|
|
38
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
39
|
+
assert PyOpenMagnetics.get_log_level() == "ERROR"
|
|
40
|
+
|
|
41
|
+
def test_set_log_level_critical(self):
|
|
42
|
+
"""Should be able to set log level to CRITICAL."""
|
|
43
|
+
PyOpenMagnetics.set_log_level("CRITICAL")
|
|
44
|
+
assert PyOpenMagnetics.get_log_level() == "CRITICAL"
|
|
45
|
+
|
|
46
|
+
def test_set_log_level_trace(self):
|
|
47
|
+
"""Should be able to set log level to TRACE."""
|
|
48
|
+
PyOpenMagnetics.set_log_level("TRACE")
|
|
49
|
+
assert PyOpenMagnetics.get_log_level() == "TRACE"
|
|
50
|
+
|
|
51
|
+
def test_set_log_level_off(self):
|
|
52
|
+
"""Should be able to disable logging with OFF."""
|
|
53
|
+
PyOpenMagnetics.set_log_level("OFF")
|
|
54
|
+
assert PyOpenMagnetics.get_log_level() == "OFF"
|
|
55
|
+
|
|
56
|
+
def test_set_log_level_lowercase(self):
|
|
57
|
+
"""Log level should accept lowercase input."""
|
|
58
|
+
PyOpenMagnetics.set_log_level("debug")
|
|
59
|
+
assert PyOpenMagnetics.get_log_level() == "DEBUG"
|
|
60
|
+
|
|
61
|
+
def test_set_invalid_log_level(self):
|
|
62
|
+
"""Invalid log level should raise an exception."""
|
|
63
|
+
with pytest.raises(Exception) as exc_info:
|
|
64
|
+
PyOpenMagnetics.set_log_level("INVALID")
|
|
65
|
+
assert "Invalid log level" in str(exc_info.value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestStringSink:
|
|
69
|
+
"""Tests for capturing logs to string buffer."""
|
|
70
|
+
|
|
71
|
+
def setup_method(self):
|
|
72
|
+
"""Reset logging state before each test."""
|
|
73
|
+
PyOpenMagnetics.disable_string_sink()
|
|
74
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
75
|
+
|
|
76
|
+
def teardown_method(self):
|
|
77
|
+
"""Clean up after each test."""
|
|
78
|
+
PyOpenMagnetics.disable_string_sink()
|
|
79
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
80
|
+
|
|
81
|
+
def test_enable_string_sink(self):
|
|
82
|
+
"""Should be able to enable string sink."""
|
|
83
|
+
PyOpenMagnetics.enable_string_sink()
|
|
84
|
+
# Should not raise
|
|
85
|
+
logs = PyOpenMagnetics.get_logs()
|
|
86
|
+
assert logs == "" # Initially empty
|
|
87
|
+
|
|
88
|
+
def test_get_logs_without_sink(self):
|
|
89
|
+
"""get_logs should return empty string when sink not enabled."""
|
|
90
|
+
PyOpenMagnetics.disable_string_sink()
|
|
91
|
+
logs = PyOpenMagnetics.get_logs()
|
|
92
|
+
assert logs == ""
|
|
93
|
+
|
|
94
|
+
def test_clear_logs(self):
|
|
95
|
+
"""Should be able to clear captured logs."""
|
|
96
|
+
PyOpenMagnetics.enable_string_sink()
|
|
97
|
+
PyOpenMagnetics.set_log_level("INFO")
|
|
98
|
+
PyOpenMagnetics.log_message("INFO", "Test message")
|
|
99
|
+
|
|
100
|
+
logs_before = PyOpenMagnetics.get_logs()
|
|
101
|
+
assert "Test message" in logs_before
|
|
102
|
+
|
|
103
|
+
PyOpenMagnetics.clear_logs()
|
|
104
|
+
logs_after = PyOpenMagnetics.get_logs()
|
|
105
|
+
assert logs_after == ""
|
|
106
|
+
|
|
107
|
+
def test_disable_string_sink(self):
|
|
108
|
+
"""Should be able to disable string sink."""
|
|
109
|
+
PyOpenMagnetics.enable_string_sink()
|
|
110
|
+
PyOpenMagnetics.set_log_level("INFO")
|
|
111
|
+
PyOpenMagnetics.log_message("INFO", "Test message")
|
|
112
|
+
|
|
113
|
+
PyOpenMagnetics.disable_string_sink()
|
|
114
|
+
# After disabling, get_logs should return empty
|
|
115
|
+
logs = PyOpenMagnetics.get_logs()
|
|
116
|
+
assert logs == ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestLogMessage:
|
|
120
|
+
"""Tests for logging messages from Python."""
|
|
121
|
+
|
|
122
|
+
def setup_method(self):
|
|
123
|
+
"""Set up string sink for capturing logs."""
|
|
124
|
+
PyOpenMagnetics.disable_string_sink()
|
|
125
|
+
PyOpenMagnetics.enable_string_sink()
|
|
126
|
+
PyOpenMagnetics.set_log_level("TRACE")
|
|
127
|
+
PyOpenMagnetics.clear_logs()
|
|
128
|
+
|
|
129
|
+
def teardown_method(self):
|
|
130
|
+
"""Clean up after each test."""
|
|
131
|
+
PyOpenMagnetics.disable_string_sink()
|
|
132
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
133
|
+
|
|
134
|
+
def test_log_info_message(self):
|
|
135
|
+
"""Should capture INFO level messages."""
|
|
136
|
+
PyOpenMagnetics.log_message("INFO", "Test info message")
|
|
137
|
+
logs = PyOpenMagnetics.get_logs()
|
|
138
|
+
assert "INFO" in logs
|
|
139
|
+
assert "Test info message" in logs
|
|
140
|
+
|
|
141
|
+
def test_log_debug_message(self):
|
|
142
|
+
"""Should capture DEBUG level messages."""
|
|
143
|
+
PyOpenMagnetics.log_message("DEBUG", "Debug details")
|
|
144
|
+
logs = PyOpenMagnetics.get_logs()
|
|
145
|
+
assert "DEBUG" in logs
|
|
146
|
+
assert "Debug details" in logs
|
|
147
|
+
|
|
148
|
+
def test_log_warning_message(self):
|
|
149
|
+
"""Should capture WARNING level messages."""
|
|
150
|
+
PyOpenMagnetics.log_message("WARNING", "Warning occurred")
|
|
151
|
+
logs = PyOpenMagnetics.get_logs()
|
|
152
|
+
assert "WARNING" in logs
|
|
153
|
+
assert "Warning occurred" in logs
|
|
154
|
+
|
|
155
|
+
def test_log_error_message(self):
|
|
156
|
+
"""Should capture ERROR level messages."""
|
|
157
|
+
PyOpenMagnetics.log_message("ERROR", "An error happened")
|
|
158
|
+
logs = PyOpenMagnetics.get_logs()
|
|
159
|
+
assert "ERROR" in logs
|
|
160
|
+
assert "An error happened" in logs
|
|
161
|
+
|
|
162
|
+
def test_log_with_module_name(self):
|
|
163
|
+
"""Should include module name in log output."""
|
|
164
|
+
PyOpenMagnetics.log_message("INFO", "Module test", "MyTestModule")
|
|
165
|
+
logs = PyOpenMagnetics.get_logs()
|
|
166
|
+
assert "MyTestModule" in logs
|
|
167
|
+
assert "Module test" in logs
|
|
168
|
+
|
|
169
|
+
def test_log_filtering_by_level(self):
|
|
170
|
+
"""Messages below current level should be filtered."""
|
|
171
|
+
PyOpenMagnetics.set_log_level("WARNING")
|
|
172
|
+
PyOpenMagnetics.clear_logs()
|
|
173
|
+
|
|
174
|
+
PyOpenMagnetics.log_message("DEBUG", "Debug should be filtered")
|
|
175
|
+
PyOpenMagnetics.log_message("INFO", "Info should be filtered")
|
|
176
|
+
PyOpenMagnetics.log_message("WARNING", "Warning should appear")
|
|
177
|
+
PyOpenMagnetics.log_message("ERROR", "Error should appear")
|
|
178
|
+
|
|
179
|
+
logs = PyOpenMagnetics.get_logs()
|
|
180
|
+
assert "Debug should be filtered" not in logs
|
|
181
|
+
assert "Info should be filtered" not in logs
|
|
182
|
+
assert "Warning should appear" in logs
|
|
183
|
+
assert "Error should appear" in logs
|
|
184
|
+
|
|
185
|
+
def test_log_off_filters_all(self):
|
|
186
|
+
"""OFF level should filter all messages."""
|
|
187
|
+
PyOpenMagnetics.set_log_level("OFF")
|
|
188
|
+
PyOpenMagnetics.clear_logs()
|
|
189
|
+
|
|
190
|
+
PyOpenMagnetics.log_message("CRITICAL", "Critical message")
|
|
191
|
+
PyOpenMagnetics.log_message("ERROR", "Error message")
|
|
192
|
+
|
|
193
|
+
logs = PyOpenMagnetics.get_logs()
|
|
194
|
+
assert logs == ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestLogTimestamp:
|
|
198
|
+
"""Tests for log timestamp formatting."""
|
|
199
|
+
|
|
200
|
+
def setup_method(self):
|
|
201
|
+
"""Set up string sink for capturing logs."""
|
|
202
|
+
PyOpenMagnetics.disable_string_sink()
|
|
203
|
+
PyOpenMagnetics.enable_string_sink()
|
|
204
|
+
PyOpenMagnetics.set_log_level("INFO")
|
|
205
|
+
PyOpenMagnetics.clear_logs()
|
|
206
|
+
|
|
207
|
+
def teardown_method(self):
|
|
208
|
+
"""Clean up after each test."""
|
|
209
|
+
PyOpenMagnetics.disable_string_sink()
|
|
210
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
211
|
+
|
|
212
|
+
def test_log_has_timestamp(self):
|
|
213
|
+
"""Log messages should include timestamp."""
|
|
214
|
+
PyOpenMagnetics.log_message("INFO", "Timestamp test")
|
|
215
|
+
logs = PyOpenMagnetics.get_logs()
|
|
216
|
+
# Timestamp format: [YYYY-MM-DD HH:MM:SS.mmm]
|
|
217
|
+
import re
|
|
218
|
+
timestamp_pattern = r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]'
|
|
219
|
+
assert re.search(timestamp_pattern, logs), f"Expected timestamp in logs: {logs}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestMultipleLogMessages:
|
|
223
|
+
"""Tests for multiple sequential log messages."""
|
|
224
|
+
|
|
225
|
+
def setup_method(self):
|
|
226
|
+
"""Set up string sink for capturing logs."""
|
|
227
|
+
PyOpenMagnetics.disable_string_sink()
|
|
228
|
+
PyOpenMagnetics.enable_string_sink()
|
|
229
|
+
PyOpenMagnetics.set_log_level("DEBUG")
|
|
230
|
+
PyOpenMagnetics.clear_logs()
|
|
231
|
+
|
|
232
|
+
def teardown_method(self):
|
|
233
|
+
"""Clean up after each test."""
|
|
234
|
+
PyOpenMagnetics.disable_string_sink()
|
|
235
|
+
PyOpenMagnetics.set_log_level("ERROR")
|
|
236
|
+
|
|
237
|
+
def test_multiple_messages_captured(self):
|
|
238
|
+
"""Multiple log messages should all be captured."""
|
|
239
|
+
PyOpenMagnetics.log_message("INFO", "First message")
|
|
240
|
+
PyOpenMagnetics.log_message("DEBUG", "Second message")
|
|
241
|
+
PyOpenMagnetics.log_message("WARNING", "Third message")
|
|
242
|
+
|
|
243
|
+
logs = PyOpenMagnetics.get_logs()
|
|
244
|
+
assert "First message" in logs
|
|
245
|
+
assert "Second message" in logs
|
|
246
|
+
assert "Third message" in logs
|
|
247
|
+
|
|
248
|
+
def test_messages_in_order(self):
|
|
249
|
+
"""Log messages should appear in chronological order."""
|
|
250
|
+
PyOpenMagnetics.log_message("INFO", "AAA_FIRST")
|
|
251
|
+
PyOpenMagnetics.log_message("INFO", "BBB_SECOND")
|
|
252
|
+
PyOpenMagnetics.log_message("INFO", "CCC_THIRD")
|
|
253
|
+
|
|
254
|
+
logs = PyOpenMagnetics.get_logs()
|
|
255
|
+
pos_first = logs.find("AAA_FIRST")
|
|
256
|
+
pos_second = logs.find("BBB_SECOND")
|
|
257
|
+
pos_third = logs.find("CCC_THIRD")
|
|
258
|
+
|
|
259
|
+
assert pos_first < pos_second < pos_third
|
|
@@ -17,6 +17,19 @@ def parse_json_result(result):
|
|
|
17
17
|
return result
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def extract_magnetics_list(result):
|
|
21
|
+
"""
|
|
22
|
+
Extract list of magnetics from result.
|
|
23
|
+
|
|
24
|
+
In version 1.1.2+, the function returns {"data": [{"mas": {...}, "scoring": ..., ...}, ...]}
|
|
25
|
+
In version 1.1.0, it returned a list directly.
|
|
26
|
+
"""
|
|
27
|
+
parsed = parse_json_result(result)
|
|
28
|
+
if isinstance(parsed, dict) and "data" in parsed:
|
|
29
|
+
return parsed["data"]
|
|
30
|
+
return parsed
|
|
31
|
+
|
|
32
|
+
|
|
20
33
|
class TestMagneticAdviserBasic:
|
|
21
34
|
"""
|
|
22
35
|
Basic magnetic adviser tests.
|
|
@@ -32,7 +45,7 @@ class TestMagneticAdviserBasic:
|
|
|
32
45
|
|
|
33
46
|
# API: calculate_advised_magnetics(inputs, max_results, core_mode)
|
|
34
47
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
35
|
-
results =
|
|
48
|
+
results = extract_magnetics_list(result_json)
|
|
36
49
|
|
|
37
50
|
assert isinstance(results, list)
|
|
38
51
|
# Should return some results
|
|
@@ -45,15 +58,16 @@ class TestMagneticAdviserBasic:
|
|
|
45
58
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
46
59
|
|
|
47
60
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 3, "available cores")
|
|
48
|
-
results =
|
|
61
|
+
results = extract_magnetics_list(result_json)
|
|
49
62
|
|
|
50
63
|
if len(results) > 0:
|
|
51
64
|
for result in results:
|
|
52
65
|
assert isinstance(result, dict)
|
|
53
|
-
# Each result should
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
# Each result should have "mas" containing the magnetic data (v1.1.2+ format)
|
|
67
|
+
# or "magnetic" directly (v1.1.0 format)
|
|
68
|
+
mas = result.get("mas", result)
|
|
69
|
+
if isinstance(mas, dict) and "magnetic" in mas:
|
|
70
|
+
assert "core" in mas["magnetic"] or "coil" in mas["magnetic"]
|
|
57
71
|
|
|
58
72
|
|
|
59
73
|
class TestMagneticAdviserCoreModes:
|
|
@@ -70,7 +84,7 @@ class TestMagneticAdviserCoreModes:
|
|
|
70
84
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
71
85
|
|
|
72
86
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
73
|
-
results =
|
|
87
|
+
results = extract_magnetics_list(result_json)
|
|
74
88
|
|
|
75
89
|
assert isinstance(results, list)
|
|
76
90
|
|
|
@@ -82,7 +96,7 @@ class TestMagneticAdviserCoreModes:
|
|
|
82
96
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
83
97
|
|
|
84
98
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "standard cores")
|
|
85
|
-
results =
|
|
99
|
+
results = extract_magnetics_list(result_json)
|
|
86
100
|
|
|
87
101
|
assert isinstance(results, list)
|
|
88
102
|
|
|
@@ -95,7 +109,7 @@ class TestMagneticAdviserInputTypes:
|
|
|
95
109
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
96
110
|
|
|
97
111
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
98
|
-
results =
|
|
112
|
+
results = extract_magnetics_list(result_json)
|
|
99
113
|
|
|
100
114
|
assert isinstance(results, list)
|
|
101
115
|
|
|
@@ -108,7 +122,7 @@ class TestMagneticAdviserInputTypes:
|
|
|
108
122
|
processed_inputs = PyOpenMagnetics.process_inputs(transformer_inputs)
|
|
109
123
|
|
|
110
124
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
111
|
-
results =
|
|
125
|
+
results = extract_magnetics_list(result_json)
|
|
112
126
|
|
|
113
127
|
assert isinstance(results, list)
|
|
114
128
|
|
|
@@ -121,7 +135,7 @@ class TestMagneticAdviserInputTypes:
|
|
|
121
135
|
processed_inputs = PyOpenMagnetics.process_inputs(high_frequency_inputs)
|
|
122
136
|
|
|
123
137
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
124
|
-
results =
|
|
138
|
+
results = extract_magnetics_list(result_json)
|
|
125
139
|
|
|
126
140
|
assert isinstance(results, list)
|
|
127
141
|
|
|
@@ -134,7 +148,7 @@ class TestMagneticAdviserInputTypes:
|
|
|
134
148
|
processed_inputs = PyOpenMagnetics.process_inputs(flyback_inputs)
|
|
135
149
|
|
|
136
150
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
137
|
-
results =
|
|
151
|
+
results = extract_magnetics_list(result_json)
|
|
138
152
|
|
|
139
153
|
assert isinstance(results, list)
|
|
140
154
|
|
|
@@ -149,10 +163,10 @@ class TestMagneticAdviserFiltering:
|
|
|
149
163
|
"""
|
|
150
164
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
151
165
|
|
|
152
|
-
results_5 =
|
|
166
|
+
results_5 = extract_magnetics_list(
|
|
153
167
|
PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "available cores")
|
|
154
168
|
)
|
|
155
|
-
results_2 =
|
|
169
|
+
results_2 = extract_magnetics_list(
|
|
156
170
|
PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 2, "available cores")
|
|
157
171
|
)
|
|
158
172
|
|
|
@@ -167,7 +181,7 @@ class TestMagneticAdviserFiltering:
|
|
|
167
181
|
processed_inputs = PyOpenMagnetics.process_inputs(inductor_inputs)
|
|
168
182
|
|
|
169
183
|
result_json = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 1, "available cores")
|
|
170
|
-
results =
|
|
184
|
+
results = extract_magnetics_list(result_json)
|
|
171
185
|
|
|
172
186
|
assert len(results) <= 1
|
|
173
187
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|