Skip to content

Latest commit

 

History

History
864 lines (695 loc) · 31.6 KB

File metadata and controls

864 lines (695 loc) · 31.6 KB

AGENTS.md — PyOpenMagnetics AI Assistant Guide

This file is the SINGLE SOURCE OF TRUTH for any AI agent using PyOpenMagnetics. Every instruction was derived from reading the C++ source code (converter.cpp, magnetics.cpp, cores.cpp, wires.cpp, settings.cpp, database.cpp, CoreAdviser.h/.cpp, MagneticAdviser.h/.cpp, MAS.hpp, Flyback.h/.cpp, all converter model headers, and all adviser files) and validated by running the library.

When this document contradicts the .pyi stubs or bundled examples, trust this document.


🚨 STOP! READ THIS FIRST — COMMON AGENT MISTAKES

Before writing ANY code, check these three things:

  1. NEVER use json.dumps() — Pass Python dicts directly to all functions. json.dumps() converts your dict to a string, causing schema errors.

  2. NEVER invent your own JSON structure — Copy the EXACT schemas from Section 5. Fields like "inputs", "outputs", "voltage" are WRONG. Use "inputVoltage", "operatingPoints", etc.

  3. NEVER abandon Method A after an error — If you get "Input JSON does not conform to schema!", the fix is to correct your JSON, NOT to fall back to manual calculations.

See Section 13.1 for wrong vs correct code examples.

⛔ THE #1 RULE — DO NOT USE MANUAL CALCULATIONS

When a user asks you to design a magnetic component for a power converter:

  1. ALWAYS use design_magnetics_from_converter() first (Method A — single call)
  2. If Method A fails (e.g., ngspice cannot initialize), fall back to process_converter() → calculate_advised_magnetics() (Method B)
  3. NEVER calculate turns ratios, inductance, core sizes, or wire gauges manually
  4. NEVER fall back to textbook formulas — fix the API call instead

The MKF engine handles complex magnetic field distributions, temperature-dependent material properties, geometrical fringing effects, and real commercial core database constraints that manual formulas cannot replicate.

â±ï¸ TIMEOUT IS NOT A FAILURE â€" DO NOT ABANDON METHOD A!

If Method A times out, this is NOT a reason to switch to Method B.

The design computation is CPU-intensive:

  • "available cores" searches 1301+ shapes and can take 60â€"180+ seconds
  • "standard cores" is faster (10â€"30 seconds) but has fewer options

If your environment has a short timeout (e.g., 2â€"3 minutes):

  1. First try: Use "standard cores" instead of "available cores" â€" this is much faster
  2. If still timing out: Increase your timeout to 5+ minutes, NOT switch to Method B
  3. Only use Method B if Method A returns an actual error (schema error, ngspice failure, etc.)
# ✅ CORRECT â€" use "standard cores" for faster execution
result = PyOM.design_magnetics_from_converter(
    "flyback", flyback, 3, "standard cores", True, None  # faster!
)

# ⌠WRONG â€" abandoning Method A just because "available cores" is slow
# Don't do this: "it timed out, so I'll use Method B instead"

Why this matters: Method A computes the optimal inductance, turns ratio, and operating waveforms automatically from the converter specification. Method B requires you to pre-calculate these values, which can introduce errors.


1. Installation

pip install PyOpenMagnetics

If the .whl is provided as base64-encoded parts:

import base64
with open("pyopenmagnetics-1.3.0.whl.base64.partaa", "rb") as f:
    part_a = f.read()
with open("pyopenmagnetics-1.3.0.whl.base64.partab", "rb") as f:
    part_b = f.read()
whl_data = base64.b64decode(part_a + part_b)
with open("pyopenmagnetics.whl", "wb") as f:
    f.write(whl_data)
# Then: pip install pyopenmagnetics.whl

2. Import — CRITICAL (Most Agents Fail Here)

The package has no __init__.py. A bare import PyOpenMagnetics gives an empty namespace with 0 functions. You MUST use importlib:

import importlib.util, os, glob

pkg_dir = os.path.join(
    os.path.dirname(__import__('PyOpenMagnetics').__path__[0]),
    'PyOpenMagnetics'
)
so_files = glob.glob(os.path.join(pkg_dir, 'PyOpenMagnetics.cpython-*'))
assert so_files, f"No .so/.pyd found in {pkg_dir}"

spec = importlib.util.spec_from_file_location('PyOpenMagnetics', so_files[0])
PyOM = importlib.util.module_from_spec(spec)
spec.loader.exec_module(PyOM)

# MANDATORY — must call before any other function
PyOM.load_databases({})

Verify:

assert len(dir(PyOM)) > 100, "Module not loaded properly"
assert hasattr(PyOM, 'design_magnetics_from_converter')
print(f"Materials: {len(PyOM.get_core_material_names())}")   # ~409
print(f"Shapes: {len(PyOM.get_core_shape_names(True))}")     # ~1301

3. Method A: design_magnetics_from_converter() (Preferred — Single Call)

This goes directly from converter specifications to complete, ranked magnetic component designs in a single call.

ngspice Is Bundled

ngspice is shipped inside the wheel at .ngspice/lib/libngspice.so. No separate install needed. The C++ code ignores the use_ngspice parameter: (void)useNgspice; — it always uses ngspice internally.

In sandboxed environments where dlopen is restricted, ngspice may fail to initialize — use Method B with use_ngspice=False in that case.

Signature (use POSITIONAL args — the .pyi has WRONG keyword names!)

result = PyOM.design_magnetics_from_converter(
    topology_name,    # str — REQUIRED
    converter_json,   # dict — REQUIRED (BASE class schema, see Section 5)
    max_results,      # int — 1-10
    core_mode_json,   # str — "available cores" or "standard cores"
    use_ngspice,      # bool — IGNORED (always True internally)
    weights_json      # dict or None
)

⚠� The .pyi Stub Has WRONG Keyword Names

.pyi says: topology=, converter=, core_mode=, weights=. Actual pybind11: topology_name=, converter_json=, core_mode_json=, weights_json=. Safest: use positional arguments.


🚨 Pass Python Dicts — NEVER json.dumps()!

The C++ pybind11 binding accepts Python dicts directly. json.dumps() turns your dict into a string → the C++ parser gets a string instead of an object → "Input JSON does not conform to schema!".

# � WRONG — json.dumps() causes schema error
result = PyOM.design_magnetics_from_converter("flyback", json.dumps(converter), ...)
result = PyOM.design_magnetics_from_converter("flyback", converter, 3, json.dumps("STANDARD_CORES"), ...)

# ✅ CORRECT — pass Python dict and plain string
result = PyOM.design_magnetics_from_converter("flyback", converter, 3, "available cores", True, None)

🚨 NEVER Abandon Method A After a Schema Error!

If design_magnetics_from_converter throws "Input JSON does not conform to schema!", the fix is to correct the JSON — NOT to fall back to calculate_advised_cores or manual math. Common fixes:

  1. Remove json.dumps() from all arguments
  2. Use "available cores" (lowercase + space), not "STANDARD_CORES"
  3. Use positional arguments, not keyword arguments
  4. For offline flyback: use the Advanced schema with desiredInductance

4. The THREE Things That MUST Be Correct (All Verified)

4.1 core_mode_json Must Be LOWERCASE WITH SPACE

From CoreAdviser.h from_json:

if (j == "available cores") x = CoreAdviserModes::AVAILABLE_CORES;
else if (j == "standard cores") x = CoreAdviserModes::STANDARD_CORES;
else if (j == "custom cores") x = CoreAdviserModes::CUSTOM_CORES;
else throw std::runtime_error("Input JSON does not conform to schema!");
� WRONG ✅ CORRECT
"AVAILABLE_CORES" "available cores"
"STANDARD_CORES" "standard cores"
"Available Cores" "available cores"
0 (integer) "available cores"
  • "available cores" — WE commercial cores (~1301 shapes, 60–120+ sec)
  • "standard cores" — generic shapes (faster, fewer options)

4.2 Flyback operatingPoints[].mode - Optional When currentRippleRatio Is Set

The mode field is optional if currentRippleRatio is provided. The C++ code will auto-infer the mode: CCM if ripple < 1.0, DCM if ripple >= 1.0.

If you omit both mode AND currentRippleRatio, you'll get an error: "Either current ripple ratio or mode is needed for the Flyback OperatingPoint Mode"

Valid mode values:

  • "Continuous Conduction Mode" (CCM — most common for >20W)
  • "Discontinuous Conduction Mode" (DCM)
  • "Boundary Mode Operation" (BCM)
  • "Quasi Resonant Mode" (QR)

Other topologies (Buck, Boost, Forward, LLC) do NOT need a mode field.

4.3 Use BASE Class Schema (NOT AdvancedFlyback)

design_magnetics_from_converter() uses OpenMagnetics::Flyback (base). process_converter() uses OpenMagnetics::AdvancedFlyback.

Field Method A (Base) Method B (Advanced)
desiredInductance � Causes schema error ✅ Required
desiredTurnsRatios � Causes schema error ✅ Required
desiredDutyCycle � Causes schema error ✅ Optional
currentRippleRatio ✅ REQUIRED (Flyback/Boost/Forward) ✅ Optional
mode in operatingPoints Optional (auto-inferred if currentRippleRatio set) Not needed

5. Converter JSON Schemas — All Topologies (Verified from MAS.hpp)

5.1 Flyback — Method A (BASE class)

flyback_base = {
    "currentRippleRatio": 0.4,          # REQUIRED (j.at)
    "diodeVoltageDrop": 0.5,            # REQUIRED (j.at)
    "efficiency": 0.88,                 # REQUIRED (j.at)
    "inputVoltage": {                   # REQUIRED (j.at)
        "minimum": 185.0,
        "nominal": 220.0,              # recommended
        "maximum": 265.0
    },
    "operatingPoints": [{               # REQUIRED (j.at)
        "ambientTemperature": 25.0,     # REQUIRED (j.at)
        "outputVoltages": [12.0],       # REQUIRED (j.at) — PLURAL
        "outputCurrents": [2.0],        # REQUIRED (j.at) — PLURAL
        "switchingFrequency": 100000.0, # optional (get_stack_optional)
        "mode": "Continuous Conduction Mode"  # Optional if currentRippleRatio is set (auto-inferred)
    }],
    "maximumDutyCycle": 0.45,           # optional
    "maximumDrainSourceVoltage": 800.0  # optional
}

5.2 Flyback — Method B (AdvancedFlyback)

flyback_advanced = {
    "inputVoltage": {"minimum": 185, "maximum": 265},
    "desiredInductance": 800e-6,         # AdvancedFlyback only
    "desiredTurnsRatios": [13.5],        # AdvancedFlyback only
    "desiredDutyCycle": [[0.45, 0.45]],  # AdvancedFlyback only
    "maximumDutyCycle": 0.45,
    "efficiency": 0.88,
    "diodeVoltageDrop": 0.5,
    "currentRippleRatio": 0.4,
    "operatingPoints": [{
        "outputVoltages": [12.0],
        "outputCurrents": [2.0],
        "switchingFrequency": 100000,
        "ambientTemperature": 25
    }]
}

5.3 Buck — Method A (BASE class)

buck_base = {
    "diodeVoltageDrop": 0.5,            # REQUIRED (j.at)
    "inputVoltage": {"minimum": 10.0, "maximum": 14.0},  # REQUIRED
    "operatingPoints": [{
        "ambientTemperature": 25.0,     # REQUIRED
        "outputVoltage": 3.3,           # REQUIRED — ⚠� SINGULAR!
        "outputCurrent": 5.0,           # REQUIRED — ⚠� SINGULAR!
        "switchingFrequency": 500000.0  # REQUIRED (j.at)
    }]
}

5.4 Boost — Method A (BASE class)

boost_base = {
    "currentRippleRatio": 0.3,          # REQUIRED (j.at)
    "diodeVoltageDrop": 0.7,            # REQUIRED (j.at)
    "efficiency": 0.92,                 # REQUIRED (j.at)
    "inputVoltage": {"minimum": 5.0, "maximum": 5.0},  # REQUIRED
    "operatingPoints": [{
        "ambientTemperature": 25.0,     # REQUIRED
        "outputVoltage": 12.0,          # REQUIRED — ⚠� SINGULAR!
        "outputCurrent": 1.0,           # REQUIRED — ⚠� SINGULAR!
        "switchingFrequency": 100000.0  # REQUIRED (j.at)
    }]
}

5.5 Forward (Single-Switch, Two-Switch, Active-Clamp) — Method A

forward_base = {
    "currentRippleRatio": 0.4,          # REQUIRED (j.at)
    "diodeVoltageDrop": 0.5,            # REQUIRED (j.at)
    "inputVoltage": {"minimum": 36.0, "maximum": 72.0},
    "operatingPoints": [{
        "ambientTemperature": 25.0,     # REQUIRED (j.at)
        "outputVoltages": [5.0],        # REQUIRED (j.at) — PLURAL
        "outputCurrents": [10.0],       # REQUIRED (j.at) — PLURAL
        "switchingFrequency": 200000.0  # REQUIRED (j.at) for Forward!
    }]
    # optional: "efficiency", "dutyCycle", "maximumSwitchCurrent"
}

Use topology strings: "single_switch_forward", "two_switch_forward", "active_clamp_forward".

5.6 LLC — Method A (BASE class)

llc_base = {
    "inputVoltage": {"minimum": 380.0, "maximum": 420.0},  # REQUIRED (j.at)
    "minSwitchingFrequency": 100000.0,  # REQUIRED (j.at)
    "maxSwitchingFrequency": 300000.0,  # REQUIRED (j.at)
    "operatingPoints": [{               # REQUIRED (j.at)
        "ambientTemperature": 25.0,
        "outputVoltages": [12.0],       # REQUIRED — PLURAL
        "outputCurrents": [5.0],        # REQUIRED — PLURAL
        "switchingFrequency": 200000.0  # Operating frequency
    }],
    # optional fields:
    "efficiency": 0.95,                 # optional
    "qualityFactor": 0.5,               # optional
    "resonantFrequency": 150000.0       # optional
}

5.7 PushPull — Same Schema as Forward

5.8 Singular vs Plural Field Names (Critical!)

Topology outputVoltage(s) outputCurrent(s)
Flyback outputVoltages (PLURAL, list) outputCurrents (PLURAL, list)
Forward outputVoltages (PLURAL, list) outputCurrents (PLURAL, list)
PushPull outputVoltages (PLURAL, list) outputCurrents (PLURAL, list)
LLC outputVoltages (PLURAL, list) outputCurrents (PLURAL, list)
Buck outputVoltage (SINGULAR, float) outputCurrent (SINGULAR, float)
Boost outputVoltage (SINGULAR, float) outputCurrent (SINGULAR, float)
DualActiveBridge outputVoltage (SINGULAR) outputCurrent (SINGULAR)

5.9 How to Estimate Flyback Parameters

If user says "220V to 12V, 2A" without detail:

Vin_dc_min ≈ 185 × √2 × 0.9 ≈ 235V
Dmax = 0.45, η = 0.88
n = (12+0.5)×0.55 / (235×0.45×0.88) ≈ 0.074
turnsRatio = 1/n ≈ 13.5   → for desiredTurnsRatios (Method B only)
Lm ≈ 800 µH                → for desiredInductance (Method B only)

6. Complete Working Examples

Method A: design_magnetics_from_converter() (single call)

import importlib.util, os, glob, json

# Load module
pkg_dir = os.path.join(
    os.path.dirname(__import__('PyOpenMagnetics').__path__[0]),
    'PyOpenMagnetics'
)
so_files = glob.glob(os.path.join(pkg_dir, 'PyOpenMagnetics.cpython-*'))
spec = importlib.util.spec_from_file_location('PyOpenMagnetics', so_files[0])
PyOM = importlib.util.module_from_spec(spec)
spec.loader.exec_module(PyOM)
PyOM.load_databases({})

# Define converter (BASE Flyback schema — NO desiredInductance!)
flyback = {
    "currentRippleRatio": 0.4,
    "diodeVoltageDrop": 0.5,
    "efficiency": 0.88,
    "inputVoltage": {"minimum": 185.0, "nominal": 220.0, "maximum": 265.0},
    "maximumDutyCycle": 0.45,
    "operatingPoints": [{
        "ambientTemperature": 25.0,
        "outputVoltages": [12.0],
        "outputCurrents": [2.0],
        "switchingFrequency": 100000.0,
        "mode": "Continuous Conduction Mode"   # Optional if currentRippleRatio is set (auto-inferred)
    }]
}

# Design (takes 60-120s with "available cores")
result = PyOM.design_magnetics_from_converter(
    "flyback", flyback, 1, "available cores", True, None
)

if isinstance(result, dict) and "error" in result:
    print(f"Method A failed: {result['error']}")
    # → Use Method B
else:
    d = result[0]
    mas_obj, score = (d[0], d[1]) if isinstance(d, (list, tuple)) else (d, "N/A")
    core = mas_obj["magnetic"]["core"]["functionalDescription"]
    coil = mas_obj["magnetic"]["coil"]["functionalDescription"]
    print(f"Core:  {core['shape'].get('name','?') if isinstance(core['shape'], dict) else core['shape']}")
    print(f"Material: {core['material'].get('name','?') if isinstance(core['material'], dict) else core['material']}")
    for w in coil:
        print(f"  {w.get('name','?')}: {w.get('numberTurns','?')} turns")

Method B: process_converter() → calculate_advised_magnetics() (no ngspice needed)

# Step 1: Process converter (AdvancedFlyback schema)
flyback_adv = {
    "inputVoltage": {"minimum": 185, "maximum": 265},
    "desiredInductance": 800e-6,
    "desiredTurnsRatios": [13.5],
    "desiredDutyCycle": [[0.45, 0.45]],
    "maximumDutyCycle": 0.45,
    "efficiency": 0.88,
    "diodeVoltageDrop": 0.5,
    "currentRippleRatio": 0.4,
    "operatingPoints": [{
        "outputVoltages": [12.0],
        "outputCurrents": [2.0],
        "switchingFrequency": 100000,
        "ambientTemperature": 25
    }]
}
processed = PyOM.process_converter("flyback", flyback_adv, use_ngspice=False)
assert "error" not in processed

# Step 2: Feed into adviser (with optional weights)
mas_inputs = {
    "designRequirements": processed["designRequirements"],
    "operatingPoints":    processed["operatingPoints"]
}
designs = PyOM.calculate_advised_magnetics(
    mas_inputs, 1, "available cores",
    {"efficiency": 2.0, "cost": 1.0, "dimensions": 0.5}  # optional weights
)

# Step 3: Parse
d = designs[0]
mas_obj, score = (d[0], d[1]) if isinstance(d, (list, tuple)) else (d, "N/A")
core = mas_obj["magnetic"]["core"]["functionalDescription"]
coil = mas_obj["magnetic"]["coil"]["functionalDescription"]
print(f"Core: {core['shape'].get('name','?')}, Material: {core['material'].get('name','?')}")
for w in coil:
    print(f"  {w['name']}: {w['numberTurns']} turns, {w.get('numberParallels',1)} parallels")

7. Weights (Priorities)

Both design_magnetics_from_converter() and calculate_advised_magnetics() accept an optional weights dict. The keys must be lowercase MagneticFilters enum values:

weights = {
    "efficiency": 2.0,    # prioritize low losses
    "cost": 1.0,          # consider cost
    "dimensions": 0.5     # size is less important
}

Valid weight keys (from MAS.hpp MagneticFilters from_json):

  • "cost" — minimize core/wire cost
  • "dimensions" — minimize physical size
  • "efficiency" — minimize total losses

Pass as the 4th argument to calculate_advised_magnetics() or 6th to design_magnetics_from_converter(). Pass None for default equal weights.


8. Settings

Control global behavior with the settings API:

# Get current settings
settings = PyOM.get_settings()

# Modify settings
PyOM.set_settings({
    "useOnlyCoresInStock": True,         # only use cores marked in stock
    "painterNumberPointsX": 100,         # plot X resolution
    "painterNumberPointsY": 100,         # plot Y resolution
    "coilAllowMarginTape": True,         # allow margin tape in coil designs
    "coilAllowInsulatedWire": True,      # allow insulated wire
    "magneticFieldMirroringDimension": 0 # simulation accuracy (0=fast, higher=more accurate)
})

# Reset to defaults
PyOM.reset_settings()

9. Database Access

⚠� Naming convention: single-item lookup functions use the find_*_by_name() prefix, NOT get_*_by_name(). get_core_shape_by_name() does not exist and will raise AttributeError.

Core Shapes

names    = PyOM.get_core_shape_names(True)        # ✅ verified — True=include toroidal
families = PyOM.get_core_shape_families()          # ✅ verified — NO argument (bool arg → TypeError)
shape    = PyOM.find_core_shape_by_name("E 25/13/7")  # ✅ verified

Core Materials

mat_names = PyOM.get_core_material_names()         # ✅ verified
material  = PyOM.find_core_material_by_name("3C95")  # ✅ verified (.pyi confirmed)

Wires

wire_names = PyOM.get_wire_names()                 # ✅ verified
wire       = PyOM.find_wire_by_name("Round 0.5 - Grade 1")  # ✅ verified (.pyi confirmed)

Bobbins

bobbin = PyOM.find_bobbin_by_name("E 25/13/7")    # ✅ verified (.pyi confirmed)

� Wrong names — do NOT use these

PyOM.get_core_shape_by_name(...)      # AttributeError — does not exist
PyOM.get_core_material_by_name(...)   # AttributeError — does not exist
PyOM.get_wire_by_name(...)            # AttributeError — does not exist
PyOM.get_bobbin_by_name(...)          # AttributeError — does not exist
PyOM.get_core_shape_families(True)    # TypeError — takes no arguments

10. Post-Design Analysis (All Verified)

Inductance Calculation

inductance = PyOM.calculate_inductance(magnetic)
# Returns dict with:
#   magnetizingInductance → {magnetizingInductance, coreReluctance, gappingReluctance, ...}

Core Losses

models = {"coreLosses": "IGSE"}  # or "STEINMETZ", "ROSHEN", etc.
losses = PyOM.calculate_core_losses(core, coil, operating_point, models)

Winding Losses

winding_losses = PyOM.calculate_winding_losses(magnetic, operating_point, temperature)

Full Simulation

sim_result = PyOM.simulate(mas)

Wire Utilities

# Skin depth at frequency
delta = PyOM.calculate_effective_skin_depth("copper", 100000, 25)

# DC resistance per meter for a given wire diameter
rdc = PyOM.calculate_dc_resistance_per_meter("copper", 0.5e-3, 25)

SPICE Export

subcircuit = PyOM.export_magnetic_as_subcircuit(magnetic)
# Returns SPICE subcircuit string

11. Supported Topologies

Topology string Method A class Singular/Plural Notes
"flyback" Flyback PLURAL + mode required Most common
"buck" Buck SINGULAR
"boost" Boost SINGULAR
"single_switch_forward" SingleSwitchForward PLURAL switchingFreq REQUIRED
"two_switch_forward" TwoSwitchForward PLURAL switchingFreq REQUIRED
"active_clamp_forward" ActiveClampForward PLURAL switchingFreq REQUIRED
"push_pull" PushPull PLURAL Same as Forward
"llc" Llc PLURAL
"isolated_buck" IsolatedBuck PLURAL
"isolated_buck_boost" IsolatedBuckBoost PLURAL
Others ("cllc", "dab", "psfb", "pshb") internal fallback varies

Per-topology thin wrappers for process_converter: process_flyback(), process_buck(), process_boost(), process_single_switch_forward(), process_two_switch_forward(), process_active_clamp_forward(), process_push_pull(), process_isolated_buck(), process_isolated_buck_boost(), process_current_transformer(json, turns_ratio, secondary_resistance=0.0)


12. Result Structure

Both methods return a list of [mas_dict, score] tuples:

d = result[0]                          # First (best) design
mas_obj = d[0]                         # MAS dict
score   = d[1]                         # float — higher is better

# Core info
core_fd = mas_obj["magnetic"]["core"]["functionalDescription"]
shape    = core_fd["shape"]            # dict with "name", dimensions, etc.
material = core_fd["material"]         # dict with "name", properties, etc.
gapping  = core_fd["gapping"]          # list of gap dicts

# Coil info
coil_fd = mas_obj["magnetic"]["coil"]["functionalDescription"]
for winding in coil_fd:
    name     = winding["name"]          # "primary", "secondary", etc.
    turns    = winding["numberTurns"]   # int
    parallels = winding.get("numberParallels", 1)
    wire     = winding.get("wire", {})  # wire specification

# Design requirements used
dr = mas_obj["inputs"]["designRequirements"]
mag_ind = dr["magnetizingInductance"]   # {nominal, minimum, maximum}
turns_ratios = dr["turnsRatios"]        # list of ratios

# Operating points used (with full waveforms)
ops = mas_obj["inputs"]["operatingPoints"]

13. Error Handling — DO NOT FALL BACK TO MANUAL MODE

Error Cause Fix
"Input JSON does not conform to schema!" (instant) Used json.dumps() on converter or core_mode Pass Python dict directly — NOT json.dumps(dict)
"Input JSON does not conform to schema!" (instant) Wrong core_mode_json Use "available cores" (lowercase + space!)
"Input JSON does not conform to schema!" (instant) Missing both mode and currentRippleRatio Add either mode OR currentRippleRatio (mode auto-inferred if ripple set)
"Input JSON does not conform to schema!" (instant) Used desiredInductance with Method A Remove it — base class doesn't accept it
"ngspice is not available" Sandbox restriction Use Method B with use_ngspice=False
"key 'currentRippleRatio' not found" Missing required field Add to JSON
"key 'outputVoltage' not found" Used plural for Buck/Boost Use singular: outputVoltage, outputCurrent
"key 'outputVoltages' not found" Used singular for Flyback/Forward Use plural: outputVoltages, outputCurrents
TypeError: incompatible function arguments Wrong keyword names Use positional args
Empty namespace (0 functions) Bare import PyOpenMagnetics Use importlib (Section 2)
get_core_shape_names() TypeError Missing bool arg Use get_core_shape_names(True)

13.1 Common Wrong Patterns vs Correct Patterns

❌ WRONG: Using json.dumps()

# This WILL FAIL with "Input JSON does not conform to schema!"
result = PyOM.design_magnetics_from_converter(
    "flyback",
    json.dumps(converter_spec),  # ❌ WRONG - json.dumps() converts dict to string!
    3
)

✅ CORRECT: Pass Python dict directly

# This works - pass the dict directly, no json.dumps()
result = PyOM.design_magnetics_from_converter(
    "flyback",
    converter_spec,  # ✅ CORRECT - Python dict, not string
    3,
    "standard cores",
    True,
    None
)

❌ WRONG: Inventing your own JSON structure

# This WILL FAIL - this is NOT the correct schema!
converter_spec = {
    "inputs": [{
        "name": "Primary",
        "voltage": {"nominal": 310.0}
    }],
    "outputs": [{
        "name": "Secondary",
        "voltage": 12.0,
        "current": 2.0
    }],
    "switchingFrequency": 100000.0
}

✅ CORRECT: Use the exact schema from Section 5.1

# This is the CORRECT Flyback schema for Method A
flyback = {
    "currentRippleRatio": 0.4,          # REQUIRED
    "diodeVoltageDrop": 0.5,            # REQUIRED
    "efficiency": 0.88,                 # REQUIRED
    "inputVoltage": {                   # REQUIRED - not "inputs"!
        "minimum": 185.0,
        "nominal": 220.0,
        "maximum": 265.0
    },
    "operatingPoints": [{               # REQUIRED - not "outputs"!
        "ambientTemperature": 25.0,
        "outputVoltages": [12.0],       # PLURAL - list!
        "outputCurrents": [2.0],        # PLURAL - list!
        "switchingFrequency": 100000.0
    }],
    "maximumDutyCycle": 0.45
}

❌ WRONG: Using keyword arguments from .pyi stubs

# This WILL FAIL - .pyi has wrong keyword names!
result = PyOM.design_magnetics_from_converter(
    topology="flyback",      # ❌ Wrong keyword
    converter=flyback,       # ❌ Wrong keyword
    max_results=3
)

✅ CORRECT: Use positional arguments

# Use positional arguments - they always work
result = PyOM.design_magnetics_from_converter(
    "flyback",           # topology_name (positional)
    flyback,             # converter_json (positional)
    3,                   # max_results (positional)
    "standard cores",    # core_mode_json (positional)
    True,                # use_ngspice (positional, ignored internally)
    None                 # weights_json (positional)
)

❌ WRONG: Abandoning Method A after schema error

# DON'T DO THIS!
try:
    result = PyOM.design_magnetics_from_converter(...)
except:
    print("Method A failed, doing manual calculations instead...")
    # ❌ WRONG - Never fall back to manual calculations!
    L = V * D / (f * delta_I)  # ❌ NO!

✅ CORRECT: Fix the JSON and retry

# If you get a schema error, FIX THE JSON, don't abandon Method A
# Check:
# 1. Did you use json.dumps()? Remove it.
# 2. Did you use the correct field names? Check Section 5.
# 3. Did you use "available cores" (lowercase with space)?
# 4. Did you include all REQUIRED fields?

14. Quick-Reference Checklists

Method A: design_magnetics_from_converter()

  • Used importlib to load .so (NOT bare import)
  • Called load_databases({}) after loading
  • Topology is lowercase ("flyback", not "Flyback")
  • core_mode_json is "available cores" (lowercase + space!)
  • JSON uses BASE class schema (NO desiredInductance, NO desiredTurnsRatios)
  • For Flyback: currentRippleRatio present (mode is optional, auto-inferred)
  • For Buck/Boost: outputVoltage/outputCurrent are SINGULAR
  • For Forward: switchingFrequency is REQUIRED in operatingPoints
  • Using positional arguments (keyword names in .pyi are wrong)
  • Passing Python dicts directly — NOT json.dumps() (causes schema error)
  • If error → fix JSON and retry, then Method B — NEVER manual calculations

Method B: process_converter() → calculate_advised_magnetics()

  • JSON uses Advanced schema (WITH desiredInductance, desiredTurnsRatios)
  • use_ngspice=False if ngspice can't init, True otherwise
  • Pass processed["designRequirements"] + processed["operatingPoints"] to adviser
  • Core mode also uses "available cores" (same lowercase convention)
  • Weights use lowercase: "efficiency", "cost", "dimensions"

15. Known Issues & Notes

  1. use_ngspice is ignored by design_magnetics_from_converter — (void)useNgspice; in source. ngspice is bundled in the wheel. In restricted sandboxes, use Method B.

  2. .pyi has wrong keyword names — use positional args or topology_name=, converter_json=, core_mode_json=, weights_json=.

  3. Two different Flyback classes — Flyback (base, Method A) vs AdvancedFlyback (Method B). Different JSON schemas. Never mix them.

  4. get_core_shape_names() requires boolean arg: True=include toroidal.

  5. Design computation is CPU-intensive"available cores" searches 1301+ shapes (60–180+ sec). Use "standard cores" for speed (10–30 sec). A timeout is NOT a failure — use "standard cores" or increase timeout, do NOT switch to Method B.

  6. All enum string values are lowercase — "available cores", "standard cores", "efficiency", "cost", "dimensions", etc. The only exception is FlybackModes which uses Title Case: "Continuous Conduction Mode".

  7. process_converter returns two keys — designRequirements (with magnetizingInductance, turnsRatios, name, topology) and operatingPoints (with full excitation waveforms per winding).

  8. Do NOT json.dumps() the arguments — the pybind11 binding accepts Python dicts directly. json.dumps(converter) turns the dict into a string, which the C++ JSON parser rejects with a schema error.

  9. NEVER abandon Method A after a schema error — the error means your JSON is wrong, not that the function is broken. Fix the JSON and retry. Do NOT fall back to calculate_advised_cores or manual textbook formulas.