feat(plecs): add PLECS circuit simulator processor and sample files#57
feat(plecs): add PLECS circuit simulator processor and sample files#57
Conversation
…ring Add E42/21/15 two-winding transformer PLECS model with N87 core material, 24/12 turn ratio, saturable permeance, and core loss equivalent. This sample file supports reverse-engineering the PLECS XML schema for the upcoming PLECS exporter (#55). Refs: #56 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ering Add simple_inductor.plecs (E32/16/9, N87, 20 turns, 1mm gap), saturable_inductor.plecs (E32/16/9, N87 with BH curve, 20 turns, 0.5mm gap), and ecore_transformer.plecs (E42/21/15, 3-leg magnetic circuit, 24/12 turns, center-leg 1mm gap). All files use the same PLECS format as transformer.plecs with parameterized init commands and magnetic circuit schematics. Refs: #56 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add PLECS to CircuitSimulatorExporterModels enum, from_json/to_json serialization, factory method, and class declaration with all method signatures in CircuitSimulatorInterface.h/.cpp.
Add CircuitSimulatorPlecs processor for generating PLECS XML simulation files and corresponding Catch2 test. Update simple_inductor sample file with revised XML structure. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds initial support for exporting OpenMagnetics magnetic models into PLECS .plecs XML schematics, along with sample PLECS files and a Catch2 test suite to validate basic exporter output.
Changes:
- Introduces
CircuitSimulatorExporterPlecsModelto generate PLECS schematic text for magnetic subcircuits and symbols. - Extends
CircuitSimulatorExporterModelswithPLECSand wires it into the exporter model factory. - Adds PLECS sample
.plecsfiles and a new Catch2 test suite for the PLECS exporter.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/processors/CircuitSimulatorPlecs.cpp |
Implements PLECS export generation (header/init commands/schematic emitters). |
src/processors/CircuitSimulatorInterface.h |
Adds PLECS to the exporter enum and declares CircuitSimulatorExporterPlecsModel. |
src/processors/CircuitSimulatorInterface.cpp |
Hooks PLECS into the exporter model factory and updates the unsupported-model error message. |
tests/TestCircuitSimulatorPlecs.cpp |
Adds Catch2 tests that generate .plecs output and check for expected tokens. |
tests/testData/simple_inductor.plecs |
Adds a sample PLECS model file (currently placed under tests/testData/). |
tests/testData/saturable_inductor.plecs |
Adds a sample PLECS model file (currently placed under tests/testData/). |
tests/testData/transformer.plecs |
Adds a sample PLECS model file (currently placed under tests/testData/). |
tests/testData/ecore_transformer.plecs |
Adds a sample PLECS model file (currently placed under tests/testData/). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::optional<std::string> filePathOrFile, | ||
| CircuitSimulatorExporterCurveFittingModes mode) { |
There was a problem hiding this comment.
filePathOrFile and mode are currently unused in this implementation, which can trigger compiler warnings and makes it unclear whether these parameters are intentionally ignored for PLECS. If they’re not needed, consider removing the parameter names or marking them [[maybe_unused]]; if they are needed, wire them into the export logic (e.g., curve-fitting mode for loss models, including external schematic fragments).
| std::optional<std::string> filePathOrFile, | |
| CircuitSimulatorExporterCurveFittingModes mode) { | |
| [[maybe_unused]] std::optional<std::string> filePathOrFile, | |
| [[maybe_unused]] CircuitSimulatorExporterCurveFittingModes mode) { |
| auto plecsFile = outputFilePath; | ||
| plecsFile.append("Test_Plecs_Simple_Inductor.plecs"); | ||
| std::filesystem::remove(plecsFile); | ||
|
|
||
| CircuitSimulatorExporter exporter(CircuitSimulatorExporterModels::PLECS); | ||
| exporter.export_magnetic_as_subcircuit(magnetic, 100000, 25, plecsFile.string()); | ||
|
|
||
| REQUIRE(std::filesystem::exists(plecsFile)); | ||
|
|
There was a problem hiding this comment.
The test writes into .../output/... but never ensures the output directory exists. On a clean checkout this will cause the exporter’s ofstream to fail and the exists() assertion to fail. Consider calling std::filesystem::create_directories(outputFilePath); before exporting in each test case (or once in a shared setup).
| Plecs { | ||
| Name "simple_inductor" | ||
| Version "4.7" | ||
| CircuitModel "ContStateSpace" | ||
| StartTime "0.0" |
There was a problem hiding this comment.
The issue/PR description calls for PLECS reverse‑engineering sample files to be placed under tests/testData/plecs/, but this file is added directly under tests/testData/. If the intended location is the subdirectory, move the sample files accordingly (and update any tests/tools that load them).
| Component { | ||
| Type Reference | ||
| SrcComponent "Components/Magnetic/Components/P_sat" | ||
| Name "P_satTr" | ||
| Show off | ||
| Position [405, 240] | ||
| Direction up | ||
| Flipped off | ||
| LabelPosition east | ||
| Frame [-8, -15; 8, 15] | ||
| Parameter { | ||
| Variable "fitting" | ||
| Value "1" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "A" | ||
| Value "A_e" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "l" | ||
| Value "l_e" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "mu_r_unsat" | ||
| Value "mu_r" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "mu_r_sat" | ||
| Value "1" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "B_sat" | ||
| Value "B_sat" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "F_init" | ||
| Value "0" | ||
| Show off | ||
| } | ||
| Terminal { | ||
| Type MagneticPort | ||
| Position [0, -20] | ||
| Direction up | ||
| } | ||
| Terminal { | ||
| Type MagneticPort | ||
| Position [0, 20] | ||
| Direction down | ||
| } | ||
| } | ||
| Component { | ||
| Type Reference | ||
| SrcComponent "Components/Magnetic/Components/P_air" | ||
| Name "P_air2" | ||
| Show off | ||
| Position [440, 355] | ||
| Direction up | ||
| Flipped on |
There was a problem hiding this comment.
Per Issue #56, the transformer.plecs sample is specified as an ungapped 2‑winding transformer with a core-loss Magnetic Resistance (Rm) in series with the core permeance. This file currently includes a P_air element and does not include any magnetic resistance/core-loss element, so it doesn’t match the stated topology/parameters. Either update the sample to match the spec (core-loss element, no air-gap element) or update the issue/PR description so the intended reverse‑engineering target is unambiguous.
| Component { | ||
| Type MagneticInterface | ||
| Name "MagInt1" | ||
| Show off | ||
| Position [440, 290] | ||
| Direction up | ||
| Flipped off | ||
| LabelPosition east | ||
| Parameter { | ||
| Variable "n" | ||
| Value "n_turns" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "Polarity" | ||
| Value "1" | ||
| Show off | ||
| } | ||
| } | ||
| Component { | ||
| Type Reference | ||
| SrcComponent "Components/Magnetic/Components/P_sat" | ||
| Name "P_satCore" | ||
| Show off | ||
| Position [405, 240] | ||
| Direction up | ||
| Flipped off | ||
| LabelPosition east | ||
| Frame [-8, -15; 8, 15] | ||
| Parameter { | ||
| Variable "fitting" | ||
| Value "1" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "A" | ||
| Value "A_e" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "l" | ||
| Value "l_e" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "mu_r_unsat" | ||
| Value "mu_r" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "mu_r_sat" | ||
| Value "1" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "B_sat" | ||
| Value "B_sat" | ||
| Show off | ||
| } | ||
| Parameter { | ||
| Variable "F_init" | ||
| Value "0" | ||
| Show off | ||
| } | ||
| Terminal { | ||
| Type MagneticPort | ||
| Position [0, -20] | ||
| Direction up | ||
| } | ||
| Terminal { | ||
| Type MagneticPort | ||
| Position [0, 20] | ||
| Direction down | ||
| } | ||
| } |
There was a problem hiding this comment.
Issue #56 specifies that saturable_inductor.plecs should use a saturable core with an explicit B-H curve (5–10 points up to ~0.4 T). This sample appears to use the generic P_sat component without any embedded/defined B-H curve data points, so it may not support the intended XML reverse‑engineering. Consider updating the model to include the B-H table data (or adjusting the issue/PR description if P_sat is the chosen representation).
|
|
||
| // Emit windings | ||
| for (size_t i = 0; i < numWindings; ++i) { | ||
| std::string miName = "MagInt_w" + std::to_string(i); | ||
| std::string turnsVar = (numWindings == 1) ? "n_turns" : "n_w" + std::to_string(i); | ||
| bool isPrimary = windings[i].get_isolation_side() == IsolationSide::PRIMARY; | ||
| int yPos = yBase + static_cast<int>(i) * ySpacing; | ||
|
|
||
| if (isPrimary) { | ||
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | ||
| } else { | ||
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos - ySpacing}, "down", true); |
There was a problem hiding this comment.
In the single-column topology, secondary windings are emitted at yPos - ySpacing. For a typical 2‑winding part where winding[0] is PRIMARY and winding[1] is SECONDARY, this places both MagneticInterface blocks at the same Y coordinate (overlapping symbols) because yPos for i=1 becomes yBase + ySpacing and then subtracts ySpacing. Consider making Y placement purely index-based (e.g., yBase - i*ySpacing) and using direction/flipped only for orientation, or computing separate Y tracks for PRIMARY vs SECONDARY windings.
| // Emit windings | |
| for (size_t i = 0; i < numWindings; ++i) { | |
| std::string miName = "MagInt_w" + std::to_string(i); | |
| std::string turnsVar = (numWindings == 1) ? "n_turns" : "n_w" + std::to_string(i); | |
| bool isPrimary = windings[i].get_isolation_side() == IsolationSide::PRIMARY; | |
| int yPos = yBase + static_cast<int>(i) * ySpacing; | |
| if (isPrimary) { | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | |
| } else { | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos - ySpacing}, "down", true); | |
| int primaryIndex = 0; | |
| int secondaryIndex = 0; | |
| // Emit windings | |
| for (size_t i = 0; i < numWindings; ++i) { | |
| std::string miName = "MagInt_w" + std::to_string(i); | |
| std::string turnsVar = (numWindings == 1) ? "n_turns" : "n_w" + std::to_string(i); | |
| bool isPrimary = windings[i].get_isolation_side() == IsolationSide::PRIMARY; | |
| int yPos = yBase; | |
| if (isPrimary) { | |
| yPos = yBase - primaryIndex * ySpacing; | |
| primaryIndex++; | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | |
| } else { | |
| yPos = yBase + secondaryIndex * ySpacing; | |
| secondaryIndex++; | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "down", true); |
| for (size_t i = 0; i < numWindings; ++i) { | ||
| std::string miName = "MagInt_w" + std::to_string(i); | ||
| std::string turnsVar = "n_w" + std::to_string(i); | ||
| int yPos = yWindingBase + static_cast<int>(i) * ySpacing; | ||
| if (windings[i].get_isolation_side() == IsolationSide::PRIMARY) { | ||
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | ||
| } else { | ||
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos - ySpacing}, "down", true); |
There was a problem hiding this comment.
The same Y-placement logic in the multi-column (E-core) branch can overlap PRIMARY/SECONDARY windings for 2‑winding parts (yPos - ySpacing for SECONDARY collapses onto the PRIMARY Y). This makes the generated schematic hard to inspect in PLECS and can hide components behind each other.
| for (size_t i = 0; i < numWindings; ++i) { | |
| std::string miName = "MagInt_w" + std::to_string(i); | |
| std::string turnsVar = "n_w" + std::to_string(i); | |
| int yPos = yWindingBase + static_cast<int>(i) * ySpacing; | |
| if (windings[i].get_isolation_side() == IsolationSide::PRIMARY) { | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | |
| } else { | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos - ySpacing}, "down", true); | |
| int primaryIndex = 0; | |
| int secondaryIndex = 0; | |
| for (size_t i = 0; i < numWindings; ++i) { | |
| std::string miName = "MagInt_w" + std::to_string(i); | |
| std::string turnsVar = "n_w" + std::to_string(i); | |
| if (windings[i].get_isolation_side() == IsolationSide::PRIMARY) { | |
| int yPos = yWindingBase + primaryIndex * ySpacing; | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "up", false); | |
| ++primaryIndex; | |
| } else { | |
| int yPos = yWindingBase - secondaryIndex * ySpacing; | |
| schematic << emit_magnetic_interface(miName, turnsVar, 1, {magX, yPos}, "down", true); | |
| ++secondaryIndex; |
…nto shared helpers - Replace hardcoded B_sat=0.49 with core.get_magnetic_flux_density_saturation() - Fix multi-column mode only using first center gap (removed break, added gap chaining with proper vertical spacing) - Fix electrical connections hardcoded to 2 windings — now loops by isolation_side so each secondary gets its own R_load - Extract build_physical_init, build_magnetic_schematic, and build_electrical_schematic helpers to eliminate ~200 lines of duplication between export_magnetic_as_subcircuit and export_magnetic_as_symbol - Add 3 new tests: B_sat from material, 3-winding transformer, distributed gaps Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Merge emit_magnetic_connection, emit_wire_connection, emit_signal_connection into single emit_connection(type, ...) to eliminate code duplication - Extract assemble_plecs_file() helper from both export functions - Add EnsureOutputDir for test output directory creation - transformer.plecs: replace P_air with MagneticResistance (Rm) per spec - saturable_inductor.plecs: add B-H curve data (7-point N87), set fitting=2 Refs: #56 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|


Summary
CircuitSimulatorPlecsprocessor for generating PLECS XML simulation files from MKF magnetic component modelsPlecsModelclass inCircuitSimulatorInterfaceTest plan
TestCircuitSimulatorPlecstest suite.plecsfiles can be opened in PLECS simulatorCloses #56
🤖 Generated with Claude Code