From 889804c0f01d780701de20e61b33b5e29e22b9c4 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 16:08:19 +0200 Subject: [PATCH 1/7] docs(plecs): add design spec for plecs_export_example CLI Captures the brainstormed design for a small CLI binary that turns a MAS Magnetic JSON into a .plecs snippet. Reuses existing CircuitSimulatorExporterPlecsModel and the Simba exporter test fixtures; establishes a new top-level examples/ convention for MKF. Refs: #57 --- .../2026-04-06-plecs-export-example-design.md | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-plecs-export-example-design.md diff --git a/docs/superpowers/specs/2026-04-06-plecs-export-example-design.md b/docs/superpowers/specs/2026-04-06-plecs-export-example-design.md new file mode 100644 index 00000000..6eee45ea --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-plecs-export-example-design.md @@ -0,0 +1,150 @@ +# PLECS Export Example — Design + +**Date:** 2026-04-06 +**Status:** Approved (brainstorming complete) +**Related:** +- Existing PLECS exporter: `src/processors/CircuitSimulatorPlecs.cpp` (#57) +- Reference pattern (Simba parallel exporter): `tests/TestCircuitSimulatorInterface.cpp:79-104` + +## Goal + +Provide a small, video-demo-friendly CLI binary that turns a MAS `Magnetic` JSON +file into a `.plecs` snippet, suitable for copy-paste into a larger PLECS file. + +The example also establishes a new top-level `examples/` convention for MKF +(currently MKF only builds the library and `MKF_tests`). + +## Non-goals + +- A general-purpose CLI for the rest of the MKF API. +- Support for full MAS files with operating points (only the `Magnetic` JSON shape + used by the existing Simba exporter test fixtures). +- A new test suite — the underlying PLECS exporter is already tested via + `tests/TestCircuitSimulatorPlecs.cpp`. +- New sample JSON files — existing fixtures in `tests/testData/` are reused. + +## CLI + +Positional only, no flag-parsing library: + +``` +plecs_export_example [frequency_Hz] [temperature_C] +``` + +Behavior: + +| Mode | Required positional args | Underlying call | +|--------------|---------------------------------------------------------------------|-----------------| +| `subcircuit` | `input`, `output`, `subcircuit`, `frequency_Hz`, `temperature_C` | `CircuitSimulatorExporterPlecsModel::export_magnetic_as_subcircuit(magnetic, freq, temp)` | +| `symbol` | `input`, `output`, `symbol` | `CircuitSimulatorExporterPlecsModel::export_magnetic_as_symbol(magnetic)` | + +Validation rules: + +- `argc < 4` → print usage to `stderr`, exit 1. +- `mode` not in `{subcircuit, symbol}` → error, exit 1. +- `mode == subcircuit` and `argc < 6` → error "subcircuit requires frequency and temperature", exit 1. +- `frequency_Hz` / `temperature_C` not parseable as `double` → error, exit 1. +- Input file open failure → error, exit 1. +- JSON parse failure → propagate `nlohmann::json::exception` message to `stderr`, exit 1. +- Output file open failure → error, exit 1. + +On success: prints `Wrote \n` to `stdout` and exits 0. + +## Layout (new files) + +``` +examples/ +├── CMakeLists.txt # plecs_export subdirectory hook +└── plecs_export/ + ├── CMakeLists.txt # add_executable(plecs_export_example main.cpp); target_link_libraries(... MKF) + └── main.cpp # ~50 lines +``` + +Root `CMakeLists.txt` change: + +```cmake +add_subdirectory(examples) +``` + +If MKF already defines an option pattern like `MKF_BUILD_TESTS`, mirror it as +`MKF_BUILD_EXAMPLES` (default `ON`) and gate the `add_subdirectory` on it. +Otherwise, unconditional. + +## main.cpp data flow + +1. Parse positional args, validate per the rules above. +2. Open input: `std::ifstream in(input_path);` +3. Parse JSON: `auto j = nlohmann::json::parse(in);` +4. Construct magnetic: `OpenMagnetics::Magnetic magnetic(j);` + (This one-arg constructor is the pattern used by the Simba exporter tests + at `tests/TestCircuitSimulatorInterface.cpp:82`.) +5. Construct exporter: `OpenMagnetics::CircuitSimulatorExporterPlecsModel exporter;` +6. Call the matching `export_magnetic_as_*`: + - `subcircuit` → `exporter.export_magnetic_as_subcircuit(magnetic, freq, temp);` + - `symbol` → `exporter.export_magnetic_as_symbol(magnetic);` + The returned `std::string` is the full content to write. +7. Write to output: `std::ofstream out(output_path); out << content;` +8. Print success line, return 0. + +### Why the example writes the file (not the exporter) + +Both `CircuitSimulatorExporterPlecsModel::export_magnetic_as_subcircuit` and +`export_magnetic_as_symbol` declare their `filePathOrFile` parameter as +`[[maybe_unused]]` (verified at `src/processors/CircuitSimulatorPlecs.cpp:622` +and `:668`). The functions return the generated string but do **not** write it +to disk. The example is therefore responsible for the `std::ofstream`. + +## Sample inputs (reused, not added) + +The existing Simba exporter fixtures in `tests/testData/` are top-level +`Magnetic` JSONs and parse cleanly through the same one-arg constructor. +Recommended for the video demo: + +- `tests/testData/test_circuitsimulatorexporter_simba_json_87.json` (basic E-core) +- `tests/testData/test_circuitsimulatorexporter_simba_json_ur_74.json` (U/R core) +- `tests/testData/test_circuitsimulatorexporter_simba_json_toroidal_core_100.json` (toroidal) +- `tests/testData/test_circuitsimulatorexporter_simba_json_ep_core_113.json` (EP core) + +## Demo commands for the video + +```bash +# Subcircuit (with curve fitting at 100 kHz, 25 °C) +./build/plecs_export_example \ + tests/testData/test_circuitsimulatorexporter_simba_json_87.json \ + /tmp/inductor.plecs subcircuit 100e3 25 + +# Symbol only +./build/plecs_export_example \ + tests/testData/test_circuitsimulatorexporter_simba_json_87.json \ + /tmp/inductor_symbol.plecs symbol +``` + +Open the resulting `.plecs` file in PLECS, or copy-paste its contents into a +larger `.plecs` schematic. + +## Error handling summary + +All errors go to `stderr` with a one-line message and a non-zero exit code. +No stack traces, no recovery. The example is short enough that failure modes +are obvious from context. + +## Risks and assumptions + +- **`OpenMagnetics::Magnetic(json)` constructor signature.** Verified to exist + and accept a `nlohmann::json` at `tests/TestCircuitSimulatorInterface.cpp:82`. + Implementation should `#include` whatever header that test includes. +- **CMake target linking.** The example links against the `MKF` library target. + Need to confirm the exact target name in the root `CMakeLists.txt` during + implementation (likely `MKF` based on `MKF_tests`). +- **`MKF_BUILD_EXAMPLES` option.** If MKF has no existing `MKF_BUILD_*` options, + the example builds unconditionally. Confirm during implementation. + +## Out of scope (YAGNI) + +- `--help` / `-h` text beyond the usage line +- Long-form flags (`--input`, `--output`, …) +- A flag-parsing library +- Reading frequency/temperature from a MAS operating point in the JSON +- Generating fresh sample JSON files +- A new test case for the example binary +- Cross-platform packaging From 5d2142a9673533e948126bc3cb5e9f4c97a1f6fb Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 16:33:25 +0200 Subject: [PATCH 2/7] docs(plecs): add implementation plan for plecs_export_example Five-task TDD-style plan: write main.cpp, add per-target and umbrella CMakeLists, wire INCLUDE_MKF_EXAMPLES into the root build, then smoke-test against an existing Simba fixture. Refs: #57 --- .../plans/2026-04-06-plecs-export-example.md | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-plecs-export-example.md diff --git a/docs/superpowers/plans/2026-04-06-plecs-export-example.md b/docs/superpowers/plans/2026-04-06-plecs-export-example.md new file mode 100644 index 00000000..395fd512 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-plecs-export-example.md @@ -0,0 +1,401 @@ +# PLECS Export Example Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a small CLI binary `plecs_export_example` that turns a MAS `Magnetic` JSON file into a `.plecs` snippet (subcircuit or symbol), and establish a top-level `examples/` directory in MKF. + +**Architecture:** Single `main.cpp` (~60 lines) under a new `examples/plecs_export/` directory. It loads a JSON file, constructs `OpenMagnetics::Magnetic` via its `nlohmann::json`-accepting constructor, calls the existing `CircuitSimulatorExporterPlecsModel::export_magnetic_as_subcircuit` or `export_magnetic_as_symbol`, and writes the returned string to disk. A new `examples/CMakeLists.txt` registers the target; the root `CMakeLists.txt` is gated on a new `INCLUDE_MKF_EXAMPLES` option mirroring the existing `INCLUDE_MKF_TESTS` pattern. + +**Tech Stack:** C++23, CMake, `nlohmann::json` (already in MKF), `MKF` shared/static library target, no flag-parsing library. + +**Spec:** `docs/superpowers/specs/2026-04-06-plecs-export-example-design.md` + +--- + +## File Structure + +| File | Action | Purpose | +|------|--------|---------| +| `examples/CMakeLists.txt` | Create | Adds the `plecs_export` subdirectory | +| `examples/plecs_export/CMakeLists.txt` | Create | Defines the `plecs_export_example` executable target | +| `examples/plecs_export/main.cpp` | Create | The CLI itself | +| `CMakeLists.txt` | Modify (root, around line 490) | Add `INCLUDE_MKF_EXAMPLES` option + `add_subdirectory(examples)` gate | + +No source files under `src/` are modified. No test files are added. + +--- + +## Task 1: Create the `examples/plecs_export/main.cpp` CLI + +**Files:** +- Create: `examples/plecs_export/main.cpp` + +- [ ] **Step 1: Create the file with the full CLI** + +Write `examples/plecs_export/main.cpp` with this exact content: + +```cpp +// PLECS export example for MKF. +// +// Usage: +// plecs_export_example [frequency_Hz] [temperature_C] +// +// Reads a MAS Magnetic JSON file and writes a .plecs snippet that can be +// copy-pasted into a larger PLECS schematic file. + +#include +#include +#include +#include +#include + +#include + +#include "processors/CircuitSimulatorInterface.h" + +namespace { + +void print_usage(std::ostream& os) { + os << "Usage: plecs_export_example " + << " [frequency_Hz] [temperature_C]\n" + << " subcircuit mode requires frequency_Hz and temperature_C.\n" + << " symbol mode ignores frequency_Hz and temperature_C.\n"; +} + +int fail(const std::string& message) { + std::cerr << "error: " << message << "\n"; + print_usage(std::cerr); + return 1; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 4) { + return fail("not enough arguments"); + } + + const std::string input_path = argv[1]; + const std::string output_path = argv[2]; + const std::string mode = argv[3]; + + if (mode != "subcircuit" && mode != "symbol") { + return fail("mode must be 'subcircuit' or 'symbol' (got '" + mode + "')"); + } + + double frequency = 0.0; + double temperature = 0.0; + if (mode == "subcircuit") { + if (argc < 6) { + return fail("subcircuit mode requires frequency_Hz and temperature_C"); + } + try { + frequency = std::stod(argv[4]); + temperature = std::stod(argv[5]); + } catch (const std::exception& e) { + return fail(std::string{"could not parse frequency/temperature: "} + e.what()); + } + } + + std::ifstream input_file(input_path); + if (!input_file.is_open()) { + return fail("could not open input file: " + input_path); + } + + nlohmann::json j; + try { + input_file >> j; + } catch (const std::exception& e) { + return fail(std::string{"failed to parse JSON: "} + e.what()); + } + + std::string content; + try { + OpenMagnetics::Magnetic magnetic(j); + OpenMagnetics::CircuitSimulatorExporterPlecsModel exporter; + if (mode == "subcircuit") { + content = exporter.export_magnetic_as_subcircuit(magnetic, frequency, temperature); + } else { + content = exporter.export_magnetic_as_symbol(magnetic); + } + } catch (const std::exception& e) { + return fail(std::string{"PLECS export failed: "} + e.what()); + } + + std::ofstream output_file(output_path); + if (!output_file.is_open()) { + return fail("could not open output file for writing: " + output_path); + } + output_file << content; + if (!output_file) { + return fail("failed while writing output file: " + output_path); + } + + std::cout << "Wrote " << output_path << "\n"; + return 0; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add examples/plecs_export/main.cpp +git commit -m "feat(examples): add plecs_export_example CLI source + +Small CLI that loads a MAS Magnetic JSON and emits a .plecs snippet +(subcircuit or symbol) using CircuitSimulatorExporterPlecsModel. +Build wiring follows in subsequent commits. + +Refs: #57" +``` + +--- + +## Task 2: Add per-target CMakeLists for the example + +**Files:** +- Create: `examples/plecs_export/CMakeLists.txt` + +- [ ] **Step 1: Create the per-target CMakeLists** + +Write `examples/plecs_export/CMakeLists.txt` with this exact content: + +```cmake +add_executable(plecs_export_example main.cpp) +target_link_libraries(plecs_export_example PRIVATE MKF) +# MKF's target_include_directories already exposes src/ and src/processors/ +# as PUBLIC, so plecs_export_example inherits the include paths it needs. +set_target_properties(plecs_export_example PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) +``` + +- [ ] **Step 2: Commit** + +```bash +git add examples/plecs_export/CMakeLists.txt +git commit -m "build(examples): add CMake target for plecs_export_example + +Defines the plecs_export_example executable, links against MKF, and +places the binary at \${CMAKE_BINARY_DIR} so the demo command line +matches ./build/plecs_export_example. + +Refs: #57" +``` + +--- + +## Task 3: Add the `examples/` umbrella CMakeLists + +**Files:** +- Create: `examples/CMakeLists.txt` + +- [ ] **Step 1: Create the umbrella CMakeLists** + +Write `examples/CMakeLists.txt` with this exact content: + +```cmake +# MKF examples — small standalone binaries that demonstrate library usage. +# Add new examples by creating a subdirectory with its own CMakeLists.txt +# and listing it here. + +add_subdirectory(plecs_export) +``` + +- [ ] **Step 2: Commit** + +```bash +git add examples/CMakeLists.txt +git commit -m "build(examples): add umbrella CMakeLists for examples/ + +Aggregates example subdirectories. Currently only plecs_export. + +Refs: #57" +``` + +--- + +## Task 4: Wire `examples/` into the root build + +**Files:** +- Modify: `CMakeLists.txt` (root, around line 490 — the slot just after the commented-out `magnetic_adviser` block and before `if(INCLUDE_MKF_TESTS)`) + +- [ ] **Step 1: Add the option and the gated `add_subdirectory`** + +Find this region in the root `CMakeLists.txt` (around lines 490-493): + +```cmake +# add_executable(magnetic_adviser "src/advisers/MagneticAdviser.cpp") +# target_link_libraries(magnetic_adviser MKF) + +if(INCLUDE_MKF_TESTS) +``` + +Replace it with: + +```cmake +# add_executable(magnetic_adviser "src/advisers/MagneticAdviser.cpp") +# target_link_libraries(magnetic_adviser MKF) + +option(INCLUDE_MKF_EXAMPLES "Build small standalone example binaries under examples/" ON) +if(INCLUDE_MKF_EXAMPLES) + add_subdirectory(examples) +endif() + +if(INCLUDE_MKF_TESTS) +``` + +(Mirrors the existing `INCLUDE_MKF_TESTS` pattern. Default `ON` so the example builds by default; downstream users who don't want it set `-DINCLUDE_MKF_EXAMPLES=OFF`.) + +- [ ] **Step 2: Configure CMake to verify the option and target are recognized** + +Run from repo root: + +```bash +cmake -S . -B build -G Ninja +``` + +Expected: configuration succeeds. The output should mention the new target `plecs_export_example` somewhere (or at least no error about `examples/CMakeLists.txt`). If the build dir already exists from prior work, this is a re-configure and should still succeed. + +If configuration fails on the new lines, fix the syntax in `CMakeLists.txt` and re-run. + +- [ ] **Step 3: Build only the new target** + +```bash +cmake --build build --target plecs_export_example +``` + +Expected: a clean build of `main.cpp` plus any MKF dependencies, and `build/plecs_export_example` exists afterwards. + +Verify the binary is present: + +```bash +ls -l build/plecs_export_example +``` + +Expected: file exists and is executable. + +Note: per `memory/feedback_no_full_compilation.md`, full MKF compilation is known to crash WSL. This task only builds the single new target, which compiles `main.cpp` and links against the already-built `MKF` library if it exists. If `MKF` itself has not yet been built in this checkout, the build will need to compile MKF as a dependency — in that case, stop and report this back rather than letting WSL die. + +- [ ] **Step 4: Commit** + +```bash +git add CMakeLists.txt +git commit -m "build: gate examples/ subdirectory on INCLUDE_MKF_EXAMPLES + +Mirrors the INCLUDE_MKF_TESTS option pattern. Defaults to ON so the +plecs_export_example binary builds by default; downstream consumers +can disable with -DINCLUDE_MKF_EXAMPLES=OFF. + +Refs: #57" +``` + +--- + +## Task 5: Smoke-test the binary against an existing fixture + +**Files:** +- None modified. This task only runs the binary. + +- [ ] **Step 1: Run subcircuit mode against the existing Simba fixture** + +```bash +./build/plecs_export_example \ + tests/testData/test_circuitsimulatorexporter_simba_json_87.json \ + /tmp/plecs_export_smoke_subcircuit.plecs \ + subcircuit 100e3 25 +``` + +Expected stdout: `Wrote /tmp/plecs_export_smoke_subcircuit.plecs` +Expected exit code: 0 +Expected file: `/tmp/plecs_export_smoke_subcircuit.plecs` exists and is non-empty. + +Verify: + +```bash +test -s /tmp/plecs_export_smoke_subcircuit.plecs && echo "non-empty OK" +head -3 /tmp/plecs_export_smoke_subcircuit.plecs +``` + +Expected: `non-empty OK` printed, and the first few lines look like a PLECS file (likely starting with `Plecs {` or a `%%` comment header from `assemble_plecs_file`). + +- [ ] **Step 2: Run symbol mode against the same fixture** + +```bash +./build/plecs_export_example \ + tests/testData/test_circuitsimulatorexporter_simba_json_87.json \ + /tmp/plecs_export_smoke_symbol.plecs \ + symbol +``` + +Expected stdout: `Wrote /tmp/plecs_export_smoke_symbol.plecs` +Expected exit code: 0 +Expected file: `/tmp/plecs_export_smoke_symbol.plecs` exists and is non-empty. + +Verify: + +```bash +test -s /tmp/plecs_export_smoke_symbol.plecs && echo "non-empty OK" +``` + +- [ ] **Step 3: Run argument validation paths** + +```bash +./build/plecs_export_example +echo "exit=$?" +``` +Expected: error message + usage to stderr, exit code 1. + +```bash +./build/plecs_export_example a.json b.plecs banana +echo "exit=$?" +``` +Expected: `error: mode must be 'subcircuit' or 'symbol' (got 'banana')`, exit code 1. + +```bash +./build/plecs_export_example a.json b.plecs subcircuit +echo "exit=$?" +``` +Expected: `error: subcircuit mode requires frequency_Hz and temperature_C`, exit code 1. + +```bash +./build/plecs_export_example /no/such/file.json /tmp/x.plecs symbol +echo "exit=$?" +``` +Expected: `error: could not open input file: /no/such/file.json`, exit code 1. + +- [ ] **Step 4: Clean up smoke-test artifacts** + +```bash +rm -f /tmp/plecs_export_smoke_subcircuit.plecs /tmp/plecs_export_smoke_symbol.plecs +``` + +(Nothing to commit in this task — it's a manual verification task. If any step failed, fix the issue in the appropriate prior task and re-run from Task 4 step 2.) + +--- + +## Self-Review Notes + +**Spec coverage check:** + +| Spec section | Covered by | +|--------------|------------| +| Layout (`examples/plecs_export/main.cpp`, two CMakeLists) | Tasks 1, 2, 3 | +| CLI positional shape | Task 1 step 1 (argv parsing) | +| `subcircuit` mode → `export_magnetic_as_subcircuit(magnetic, freq, temp)` | Task 1 step 1 | +| `symbol` mode → `export_magnetic_as_symbol(magnetic)` | Task 1 step 1 | +| Validation rules (argc, mode, freq/temp parse, file open, JSON parse, output write) | Task 1 step 1 + Task 5 step 3 | +| Example writes file itself (because `filePathOrFile` is `[[maybe_unused]]`) | Task 1 step 1 (`std::ofstream output_file`) | +| `OpenMagnetics::Magnetic(json)` constructor reuse | Task 1 step 1 | +| `INCLUDE_MKF_EXAMPLES` option mirroring `INCLUDE_MKF_TESTS` | Task 4 | +| `add_subdirectory(examples)` from root | Task 4 | +| Reuse existing `tests/testData/test_circuitsimulatorexporter_simba_json_87.json` | Task 5 | +| No new test, no new sample files | Confirmed by absence in plan | + +**Type / signature consistency:** +- `OpenMagnetics::CircuitSimulatorExporterPlecsModel::export_magnetic_as_subcircuit(Magnetic, double frequency, double temperature)` — matches signature at `src/processors/CircuitSimulatorInterface.h:573` (later positional args use defaults). +- `OpenMagnetics::CircuitSimulatorExporterPlecsModel::export_magnetic_as_symbol(Magnetic)` — matches signature at `src/processors/CircuitSimulatorInterface.h:572`. +- `OpenMagnetics::Magnetic(nlohmann::json)` — verified in use at `tests/TestCircuitSimulatorInterface.cpp:82`. +- Include path `processors/CircuitSimulatorInterface.h` — `src/processors/` is on `MKF`'s PUBLIC include path (root `CMakeLists.txt:485`), so this resolves once we link against `MKF`. + +**No placeholders:** all code shown in full, all commands shown in full, all expected outputs shown. From 1d9bca2364d1e6e36ff6818c667e29f78fe84817 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 16:50:26 +0200 Subject: [PATCH 3/7] feat(examples): add plecs_export_example CLI source Small CLI that loads a MAS Magnetic JSON and emits a .plecs snippet (subcircuit or symbol) using CircuitSimulatorExporterPlecsModel. Build wiring follows in subsequent commits. Refs: #57 --- examples/plecs_export/main.cpp | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 examples/plecs_export/main.cpp diff --git a/examples/plecs_export/main.cpp b/examples/plecs_export/main.cpp new file mode 100644 index 00000000..043bd2e0 --- /dev/null +++ b/examples/plecs_export/main.cpp @@ -0,0 +1,99 @@ +// PLECS export example for MKF. +// +// Usage: +// plecs_export_example [frequency_Hz] [temperature_C] +// +// Reads a MAS Magnetic JSON file and writes a .plecs snippet that can be +// copy-pasted into a larger PLECS schematic file. + +#include +#include +#include +#include +#include + +#include + +#include "processors/CircuitSimulatorInterface.h" + +namespace { + +void print_usage(std::ostream& os) { + os << "Usage: plecs_export_example " + << " [frequency_Hz] [temperature_C]\n" + << " subcircuit mode requires frequency_Hz and temperature_C.\n" + << " symbol mode ignores frequency_Hz and temperature_C.\n"; +} + +int fail(const std::string& message) { + std::cerr << "error: " << message << "\n"; + print_usage(std::cerr); + return 1; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 4) { + return fail("not enough arguments"); + } + + const std::string input_path = argv[1]; + const std::string output_path = argv[2]; + const std::string mode = argv[3]; + + if (mode != "subcircuit" && mode != "symbol") { + return fail("mode must be 'subcircuit' or 'symbol' (got '" + mode + "')"); + } + + double frequency = 0.0; + double temperature = 0.0; + if (mode == "subcircuit") { + if (argc < 6) { + return fail("subcircuit mode requires frequency_Hz and temperature_C"); + } + try { + frequency = std::stod(argv[4]); + temperature = std::stod(argv[5]); + } catch (const std::exception& e) { + return fail(std::string{"could not parse frequency/temperature: "} + e.what()); + } + } + + std::ifstream input_file(input_path); + if (!input_file.is_open()) { + return fail("could not open input file: " + input_path); + } + + nlohmann::json j; + try { + input_file >> j; + } catch (const std::exception& e) { + return fail(std::string{"failed to parse JSON: "} + e.what()); + } + + std::string content; + try { + OpenMagnetics::Magnetic magnetic(j); + OpenMagnetics::CircuitSimulatorExporterPlecsModel exporter; + if (mode == "subcircuit") { + content = exporter.export_magnetic_as_subcircuit(magnetic, frequency, temperature); + } else { + content = exporter.export_magnetic_as_symbol(magnetic); + } + } catch (const std::exception& e) { + return fail(std::string{"PLECS export failed: "} + e.what()); + } + + std::ofstream output_file(output_path); + if (!output_file.is_open()) { + return fail("could not open output file for writing: " + output_path); + } + output_file << content; + if (!output_file) { + return fail("failed while writing output file: " + output_path); + } + + std::cout << "Wrote " << output_path << "\n"; + return 0; +} From 4915c78478259d04a3dc74349eacce058be814f3 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 16:53:26 +0200 Subject: [PATCH 4/7] build(examples): add CMake target for plecs_export_example Defines the plecs_export_example executable, links against MKF, and places the binary at ${CMAKE_BINARY_DIR} so the demo command line matches ./build/plecs_export_example. Refs: #57 --- examples/plecs_export/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 examples/plecs_export/CMakeLists.txt diff --git a/examples/plecs_export/CMakeLists.txt b/examples/plecs_export/CMakeLists.txt new file mode 100644 index 00000000..2a5d36f9 --- /dev/null +++ b/examples/plecs_export/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(plecs_export_example main.cpp) +target_link_libraries(plecs_export_example PRIVATE MKF) +# MKF's target_include_directories already exposes src/ and src/processors/ +# as PUBLIC, so plecs_export_example inherits the include paths it needs. +set_target_properties(plecs_export_example PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) From 0dccf3b940b469b493e88e593d4341fd53dd1971 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 16:55:46 +0200 Subject: [PATCH 5/7] build(examples): add umbrella CMakeLists for examples/ Aggregates example subdirectories. Currently only plecs_export. Refs: #57 --- examples/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/CMakeLists.txt diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..7de38588 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,5 @@ +# MKF examples — small standalone binaries that demonstrate library usage. +# Add new examples by creating a subdirectory with its own CMakeLists.txt +# and listing it here. + +add_subdirectory(plecs_export) From 6171a799ad70b928d7efac7fb9470f2907ca5ad4 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Mon, 6 Apr 2026 17:11:13 +0200 Subject: [PATCH 6/7] build: gate examples/ subdirectory on INCLUDE_MKF_EXAMPLES Mirrors the INCLUDE_MKF_TESTS option pattern. Defaults to ON so the plecs_export_example binary builds by default; downstream consumers can disable with -DINCLUDE_MKF_EXAMPLES=OFF. Refs: #57 --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index bec17cb8..f8983ddf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -560,6 +560,11 @@ target_include_directories(MKF PUBLIC # add_executable(magnetic_adviser "src/advisers/MagneticAdviser.cpp") # target_link_libraries(magnetic_adviser MKF) +option(INCLUDE_MKF_EXAMPLES "Build small standalone example binaries under examples/" ON) +if(INCLUDE_MKF_EXAMPLES) + add_subdirectory(examples) +endif() + if(INCLUDE_MKF_TESTS) file(GLOB ALL_TEST_SOURCES CONFIGURE_DEPENDS "tests/*.cpp") From 81f6b409a21f70c82fda22c8a8d117e0340c1890 Mon Sep 17 00:00:00 2001 From: tinix84 Date: Tue, 10 Mar 2026 00:15:22 +0100 Subject: [PATCH 7/7] docs(sprint): add sprint plan for PLECS exporter (#55) Plan covers permeance-capacitance analogy mapping from MKF magnetic components to PLECS magnetic domain, following the existing SIMBA exporter pattern. Identifies open questions about file format and saturation modeling. Refs: #55 Co-Authored-By: Claude Opus 4.6 --- docs/sprints/sprint-55-plecs-exporter.md | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/sprints/sprint-55-plecs-exporter.md diff --git a/docs/sprints/sprint-55-plecs-exporter.md b/docs/sprints/sprint-55-plecs-exporter.md new file mode 100644 index 00000000..18cc490d --- /dev/null +++ b/docs/sprints/sprint-55-plecs-exporter.md @@ -0,0 +1,153 @@ +# Sprint: Add PLECS Equivalent Component Exporter (#55) + +**Issue:** https://github.com/OpenMagnetics/MKF/issues/55 +**Status:** WIP — Planning complete, implementation pending +**Date:** 2026-03-10 + +--- + +## Background + +MKF already exports magnetic equivalent circuits for **SIMBA** (JSON/.jsimba), **NgSpice** (SPICE netlist), +**LTSpice** (SPICE netlist + .asy symbol), and **NL5** (SPICE netlist). Issue #55 requests adding PLECS support. + +### PLECS Magnetic Domain — Key Differences + +PLECS uses the **permeance-capacitance analogy** (not reluctance-resistance): + +| Concept | Reluctance analogy (SIMBA) | Permeance-capacitance analogy (PLECS) | +|---------|---------------------------|---------------------------------------| +| Core/gap element | Reluctance (resistance-like) | Permeance (capacitance-like) | +| Winding coupling | Ideal transformer | Gyrator | +| MMF | Voltage source | Through variable (current-like) | +| Flux | Current | Across variable (voltage-like) | +| Energy storage | Inductors | Capacitors | +| Losses | Resistors in reluctance domain | Magnetic resistance (F²/Rm) | + +**Permeance** P = μ₀·μᵣ·A/l = 1/Reluctance + +### PLECS Magnetic Component Library + +| Component | Parameters | Notes | +|-----------|-----------|-------| +| Constant Permeance | P [Wb/A·t] | Core legs, air gaps | +| Variable Permeance | P(t), dP/dt, Φ (3-element vector) | Saturable/hysteretic cores | +| Saturable Core | BH curve, area, length | Uses Variable Permeance internally | +| Hysteretic Core | Jiles-Atherton params | Uses Variable Permeance internally | +| Winding | N (turns) | Gyrator interface to electrical domain | +| Magnetic Resistance | Rm [A·t·s/Wb] | Core losses (series with permeance) | +| Magnetic Ground | — | Reference node | + +### PLECS File Format + +PLECS uses `.plecs` files (XML-based). Structure needs reverse-engineering from example files, +but the key approach is generating a PLECS **subsystem** containing magnetic-domain components. + +**Alternative:** PLECS supports **Simulink/C-Script blocks** — we could generate a C-Script block +that wraps MKF calculations directly instead of building the magnetic circuit graphically. + +--- + +## Architecture Decision + +Follow the existing pattern: create `CircuitSimulatorExporterPlecsModel` inheriting from +`CircuitSimulatorExporterModel`, analogous to the SIMBA exporter but outputting +permeance-capacitance topology instead of reluctance network. + +### Mapping from MKF Magnetic to PLECS Components + +| MKF concept | SIMBA component | PLECS equivalent | +|-------------|----------------|-----------------| +| Core leg | `Linear Core` (μᵣ, A, l) | `Constant Permeance` (P = μ₀·μᵣ·A/l) | +| Air gap | `Air Gap` (μᵣ=1, A, l) | `Constant Permeance` (P = μ₀·A/l) | +| Winding | `Winding` (N) | `Winding` (N) — gyrator | +| Core losses | R-L ladder network | `Magnetic Resistance` in series | +| AC winding losses | R-L ladder / fracpole | Electrical-domain ladder (same as NgSpice/LTSpice) | +| Leakage inductance | Inductor (electrical) | Inductor (electrical) or leakage permeance | +| Ground | `Magnetic Ground` | `Magnetic Ground` | + +--- + +## Sprint Tasks + +### Phase 1: File Format Research & Scaffolding (1 session) + +- [ ] **T1.1** Obtain a PLECS example file with magnetic components and reverse-engineer the XML schema + - Ask user for a `.plecs` file with a simple transformer, OR + - Use PowerForge export as reference + - Document XML element names, attributes, coordinate system +- [ ] **T1.2** Add `PLECS` to `CircuitSimulatorExporterModels` enum +- [ ] **T1.3** Create `CircuitSimulatorExporterPlecsModel` class stub in `CircuitSimulatorInterface.h` + - Inherit from `CircuitSimulatorExporterModel` + - Declare `export_magnetic_as_subcircuit()` and `export_magnetic_as_symbol()` +- [ ] **T1.4** Add factory case for PLECS in `CircuitSimulatorExporterModel::factory()` + +### Phase 2: Core Implementation (1-2 sessions) + +- [ ] **T2.1** Implement XML generation helpers: + - `create_permeance(P, coordinates, name)` — constant permeance element + - `create_winding(N, coordinates, name)` — gyrator winding + - `create_magnetic_resistance(Rm, coordinates, name)` — loss element + - `create_magnetic_ground(coordinates, name)` + - `create_connection(from, to)` — magnetic domain wiring +- [ ] **T2.2** Implement `export_magnetic_as_subcircuit()`: + - Convert core geometry → permeance values (P = μ₀·μᵣ·A/l per leg) + - Convert air gaps → permeance values (P = μ₀·A_gap/l_gap) + - Place winding gyrators + - Add electrical-domain AC resistance ladder/fracpole (reuse existing logic) + - Add leakage inductance + - Wire magnetic ground + - Handle multi-column cores (E-core, UR-core, toroidal) +- [ ] **T2.3** Implement `export_magnetic_as_symbol()` (optional, if PLECS supports custom symbols) +- [ ] **T2.4** Add core loss modeling via `Magnetic Resistance`: + - Convert core loss coefficients to equivalent Rm value(s) + - Option: frequency-dependent Rm via multiple branches + +### Phase 3: Testing (1 session) + +- [ ] **T3.1** Create test fixtures: + - `test_circuitsimulatorexporter_plecs_simple_inductor.json` + - `test_circuitsimulatorexporter_plecs_transformer.json` + - `test_circuitsimulatorexporter_plecs_ep_core.json` + - `test_circuitsimulatorexporter_plecs_toroidal.json` +- [ ] **T3.2** Add test cases in `TestCircuitSimulatorInterface.cpp`: + - Basic inductor export (single winding, single gap) + - Transformer export (two windings, no gap) + - Gapped E-core with multiple columns + - Toroidal core + - Verify permeance values match expected P = μ₀·μᵣ·A/l +- [ ] **T3.3** Cross-validate: compare PLECS inductance/reluctance with SIMBA export for same magnetic + +### Phase 4: Documentation & Integration (1 session) + +- [ ] **T4.1** Add PLECS to docs (algorithms or models section) +- [ ] **T4.2** Add PLECS example to `getting-started/examples.md` +- [ ] **T4.3** Close issue #55 + +--- + +## Open Questions + +1. **File format:** We need a sample `.plecs` file with magnetic components to confirm XML schema. + The PLECS manual PDF is behind binary encoding. User may need to provide one. +2. **Saturation modeling:** Should we use `Constant Permeance` (linear) or `Saturable Core` + (nonlinear BH curve) for the initial implementation? + - Recommendation: Start with `Constant Permeance` (matches SIMBA's `Linear Core`), add + saturable core as a follow-up. +3. **Coordinate system:** PLECS uses a graphical schematic layout. Need to understand the + coordinate/positioning system from a real file. +4. **C-Script alternative:** If XML reverse-engineering proves difficult, we could generate a + PLECS C-Script block that calls MKF functions directly. This is more portable but less + visually integrated. + +--- + +## References + +- [PLECS Magnetic Domain Overview](https://www.plexim.com/products/plecs/magnetic) +- [Permeance-Capacitance Analogy Paper (IEEE)](https://ieeexplore.ieee.org/document/6251786/) +- [PLECS Magnetic Modeling PDF](https://www.plexim.com/sites/default/files/magnetic_modeling_PLECS.pdf) +- [PLECS User Manual](https://www.plexim.com/sites/default/files/plecsmanual.pdf) +- [Gyrator-Capacitor Model (Wikipedia)](https://en.wikipedia.org/wiki/Gyrator%E2%80%93capacitor_model) +- Existing SIMBA exporter: `src/processors/CircuitSimulatorInterface.cpp:769` +- Existing NgSpice exporter: `src/processors/CircuitSimulatorInterface.cpp:1222`