Skip to content

feat(plecs): add PLECS circuit simulator processor and sample files#57

Merged
tinix84 merged 7 commits intomainfrom
feat/issue-56-plecs-sample-files
Apr 3, 2026
Merged

feat(plecs): add PLECS circuit simulator processor and sample files#57
tinix84 merged 7 commits intomainfrom
feat/issue-56-plecs-sample-files

Conversation

@tinix84
Copy link
Copy Markdown
Collaborator

@tinix84 tinix84 commented Apr 1, 2026

Summary

  • Add CircuitSimulatorPlecs processor for generating PLECS XML simulation files from MKF magnetic component models
  • Add PLECS sample files (simple inductor, saturable inductor, transformer, E-core transformer) for XML reverse-engineering
  • Add PLECS to the exporter enum and declare PlecsModel class in CircuitSimulatorInterface
  • Add Catch2 test suite for the PLECS processor

Test plan

  • Verify PLECS XML output matches expected structure for inductors and transformers
  • Run TestCircuitSimulatorPlecs test suite
  • Validate sample .plecs files can be opened in PLECS simulator

Closes #56

🤖 Generated with Claude Code

tinix84 and others added 4 commits March 31, 2026 19:50
…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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 07:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CircuitSimulatorExporterPlecsModel to generate PLECS schematic text for magnetic subcircuits and symbols.
  • Extends CircuitSimulatorExporterModels with PLECS and wires it into the exporter model factory.
  • Adds PLECS sample .plecs files 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.

Comment on lines +367 to +368
std::optional<std::string> filePathOrFile,
CircuitSimulatorExporterCurveFittingModes mode) {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
std::optional<std::string> filePathOrFile,
CircuitSimulatorExporterCurveFittingModes mode) {
[[maybe_unused]] std::optional<std::string> filePathOrFile,
[[maybe_unused]] CircuitSimulatorExporterCurveFittingModes mode) {

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +35
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));

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Plecs {
Name "simple_inductor"
Version "4.7"
CircuitModel "ContStateSpace"
StartTime "0.0"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +175
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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +147
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
}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +469 to +480

// 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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +589 to +596
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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
AlfVII and others added 3 commits April 2, 2026 19:51
…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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
@tinix84 tinix84 merged commit 652148e into main Apr 3, 2026
1 of 3 checks passed
@tinix84 tinix84 deleted the feat/issue-56-plecs-sample-files branch April 3, 2026 15:16
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
22.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide PLECS sample files for XML reverse-engineering

3 participants