diff --git a/.archive/__init__.py b/.archive/__init__.py
new file mode 100644
index 0000000..4f27d3b
--- /dev/null
+++ b/.archive/__init__.py
@@ -0,0 +1,19 @@
+"""
+Magnetic Expert - Domain knowledge for hardware engineers.
+
+Provides application-focused design guidance, not code-focused APIs.
+"""
+
+from .knowledge import APPLICATIONS, TOPOLOGIES, MATERIALS_GUIDE, TRADEOFFS
+from .examples import ExampleGenerator, generate_application_examples
+from .conversation import MagneticExpert
+
+__all__ = [
+ "APPLICATIONS",
+ "TOPOLOGIES",
+ "MATERIALS_GUIDE",
+ "TRADEOFFS",
+ "ExampleGenerator",
+ "generate_application_examples",
+ "MagneticExpert",
+]
diff --git a/.archive/conversation.py b/.archive/conversation.py
new file mode 100644
index 0000000..59a5054
--- /dev/null
+++ b/.archive/conversation.py
@@ -0,0 +1,456 @@
+"""
+Magnetic Expert Conversation System.
+
+Natural language interface for hardware engineers.
+Asks the right questions, explains trade-offs, generates designs.
+"""
+
+from typing import Optional
+from dataclasses import dataclass, field
+from .knowledge import APPLICATIONS, TOPOLOGIES, TRADEOFFS, suggest_topology
+from .examples import ExampleGenerator, DesignExample
+
+
+@dataclass
+class ConversationState:
+ """Tracks the state of a design conversation."""
+ application: Optional[str] = None
+ power_level: Optional[float] = None
+ input_voltage: Optional[tuple] = None
+ input_is_ac: bool = True
+ outputs: list[dict] = field(default_factory=list)
+ constraints: dict = field(default_factory=dict)
+ questions_asked: list[str] = field(default_factory=list)
+ answers: dict = field(default_factory=dict)
+
+
+class MagneticExpert:
+ """
+ AI-powered magnetic design expert.
+
+ Guides hardware engineers through design process using
+ domain knowledge and natural conversation.
+ """
+
+ def __init__(self):
+ self.state = ConversationState()
+ self.example_generator = ExampleGenerator()
+
+ def start(self, initial_input: str = None) -> dict:
+ """
+ Start a new design conversation.
+
+ Args:
+ initial_input: Optional initial description from user
+
+ Returns:
+ dict with message and options/questions
+ """
+ self.state = ConversationState()
+
+ if initial_input:
+ return self._parse_initial_input(initial_input)
+
+ return {
+ "message": "What are you designing today?",
+ "options": [
+ {"label": "USB charger / Power adapter", "value": "usb_charger"},
+ {"label": "Laptop adapter", "value": "laptop_adapter"},
+ {"label": "LED driver", "value": "led_driver"},
+ {"label": "Automotive DC-DC", "value": "automotive_dcdc"},
+ {"label": "Industrial power supply", "value": "din_rail_psu"},
+ {"label": "Medical equipment PSU", "value": "medical_psu"},
+ {"label": "PoE device", "value": "poe"},
+ {"label": "Inductor / Choke", "value": "inductor"},
+ {"label": "Something else", "value": "custom"},
+ ],
+ "examples": [
+ "65W USB-C charger for laptops",
+ "12V 3A buck converter",
+ "PFC inductor for 1kW power supply",
+ "Gate driver transformer for SiC",
+ ],
+ }
+
+ def _parse_initial_input(self, text: str) -> dict:
+ """Parse natural language input to extract specs."""
+ text_lower = text.lower()
+
+ # Detect application
+ app_keywords = {
+ "usb": "usb_charger",
+ "charger": "usb_charger",
+ "phone": "usb_charger",
+ "laptop": "laptop_adapter",
+ "notebook": "laptop_adapter",
+ "led": "led_driver",
+ "light": "led_driver",
+ "automotive": "automotive_dcdc",
+ "car": "automotive_dcdc",
+ "48v": "automotive_dcdc",
+ "medical": "medical_psu",
+ "hospital": "medical_psu",
+ "din": "din_rail_psu",
+ "industrial": "din_rail_psu",
+ "poe": "poe",
+ "ethernet": "poe",
+ "server": "server_psu",
+ "inductor": "inductor",
+ "choke": "inductor",
+ "pfc": "inductor",
+ }
+
+ for keyword, app in app_keywords.items():
+ if keyword in text_lower:
+ self.state.application = app
+ break
+
+ # Detect power level
+ import re
+ power_match = re.search(r'(\d+)\s*[wW]', text)
+ if power_match:
+ self.state.power_level = float(power_match.group(1))
+
+ # Detect voltage
+ voltage_match = re.search(r'(\d+)\s*[vV]', text)
+ if voltage_match:
+ v = float(voltage_match.group(1))
+ if v < 50: # Likely output voltage
+ self.state.outputs = [{"voltage": v, "current": 0}]
+ else: # Likely input voltage
+ self.state.input_voltage = (v * 0.9, v * 1.1)
+
+ # Continue conversation based on what we know
+ return self._next_question()
+
+ def answer(self, question_id: str, answer) -> dict:
+ """
+ Process user's answer to a question.
+
+ Args:
+ question_id: ID of the question being answered
+ answer: User's answer (string or selection)
+
+ Returns:
+ dict with next question or design result
+ """
+ self.state.answers[question_id] = answer
+ self.state.questions_asked.append(question_id)
+
+ # Update state based on answer
+ if question_id == "application":
+ self.state.application = answer
+ elif question_id == "power":
+ self.state.power_level = float(answer)
+ elif question_id == "input_voltage":
+ if answer == "universal":
+ self.state.input_voltage = (85, 265)
+ self.state.input_is_ac = True
+ elif answer == "12v_dc":
+ self.state.input_voltage = (10, 14)
+ self.state.input_is_ac = False
+ elif answer == "48v_dc":
+ self.state.input_voltage = (36, 57)
+ self.state.input_is_ac = False
+ # ... handle other cases
+ elif question_id == "output_voltage":
+ v = float(answer)
+ if self.state.outputs:
+ self.state.outputs[0]["voltage"] = v
+ else:
+ self.state.outputs = [{"voltage": v, "current": 0}]
+ elif question_id == "output_current":
+ if self.state.outputs:
+ self.state.outputs[0]["current"] = float(answer)
+
+ return self._next_question()
+
+ def _next_question(self) -> dict:
+ """Determine and return the next question to ask."""
+
+ # Check what info we still need
+ if not self.state.application:
+ return self.start()
+
+ app_info = APPLICATIONS.get(self.state.application, {})
+
+ if not self.state.power_level and "power" not in self.state.questions_asked:
+ return self._ask_power(app_info)
+
+ if not self.state.input_voltage and "input_voltage" not in self.state.questions_asked:
+ return self._ask_input_voltage(app_info)
+
+ if not self.state.outputs and "output_voltage" not in self.state.questions_asked:
+ return self._ask_output(app_info)
+
+ # Check for application-specific questions
+ questions = app_info.get("questions_to_ask", [])
+ for q in questions:
+ q_id = q.replace(" ", "_").replace("?", "")[:20].lower()
+ if q_id not in self.state.questions_asked:
+ return {
+ "question_id": q_id,
+ "message": q,
+ "type": "text", # or provide options
+ }
+
+ # We have enough info - generate design
+ return self._generate_design()
+
+ def _ask_power(self, app_info: dict) -> dict:
+ """Ask about power level."""
+ variants = app_info.get("variants", {})
+
+ if variants:
+ options = []
+ for name, spec in variants.items():
+ if isinstance(spec, dict) and "power" in spec:
+ p = spec["power"]
+ if isinstance(p, tuple):
+ options.append({"label": f"{p[0]}-{p[1]}W", "value": str(p[1])})
+ else:
+ options.append({"label": f"{p}W", "value": str(p)})
+
+ return {
+ "question_id": "power",
+ "message": "What power level do you need?",
+ "options": options[:8], # Limit options
+ "allow_custom": True,
+ }
+
+ return {
+ "question_id": "power",
+ "message": "What is the required output power (in Watts)?",
+ "type": "number",
+ "hint": "Total output power, e.g., 65 for a 65W charger",
+ }
+
+ def _ask_input_voltage(self, app_info: dict) -> dict:
+ """Ask about input voltage."""
+ input_options = app_info.get("input_voltage", {})
+
+ if input_options:
+ options = [
+ {"label": "Universal AC (85-265V)", "value": "universal"},
+ ]
+ if "us_only" in input_options:
+ options.append({"label": "US only (100-130V)", "value": "us_only"})
+ if "eu_only" in input_options:
+ options.append({"label": "EU only (200-240V)", "value": "eu_only"})
+
+ return {
+ "question_id": "input_voltage",
+ "message": "What is the input voltage?",
+ "options": options,
+ }
+
+ return {
+ "question_id": "input_voltage",
+ "message": "What is the input voltage range?",
+ "type": "text",
+ "hint": "e.g., '85-265V AC' or '36-57V DC'",
+ }
+
+ def _ask_output(self, app_info: dict) -> dict:
+ """Ask about output requirements."""
+ return {
+ "question_id": "output_voltage",
+ "message": "What output voltage do you need?",
+ "type": "number",
+ "hint": "e.g., 12 for 12V output",
+ "follow_up": {
+ "question_id": "output_current",
+ "message": "And what output current?",
+ "type": "number",
+ "hint": "In Amps, e.g., 5 for 5A",
+ },
+ }
+
+ def _generate_design(self) -> dict:
+ """Generate design based on collected information."""
+ # Calculate derived parameters
+ if not self.state.outputs[0].get("current") and self.state.power_level:
+ v = self.state.outputs[0]["voltage"]
+ self.state.outputs[0]["current"] = self.state.power_level / v
+
+ # Suggest topology
+ power = self.state.power_level or sum(
+ o["voltage"] * o["current"] for o in self.state.outputs
+ )
+ topology = suggest_topology(power, isolated=True)
+
+ # Find similar examples
+ examples = self.example_generator.generate_all()
+ similar = [e for e in examples if e.application == self.state.application]
+ similar = [e for e in similar
+ if abs(sum(o["voltage"] * o["current"] for o in e.outputs) - power) < power * 0.3]
+
+ return {
+ "status": "ready_to_design",
+ "message": f"""
+Based on your requirements, I recommend a **{topology}** topology.
+
+**Your Specs:**
+- Input: {self.state.input_voltage[0]:.0f}-{self.state.input_voltage[1]:.0f}V {'AC' if self.state.input_is_ac else 'DC'}
+- Output: {self.state.outputs[0]['voltage']:.1f}V @ {self.state.outputs[0]['current']:.2f}A ({power:.0f}W)
+
+**Similar designs you can reference:**
+{self._format_similar_examples(similar[:3])}
+
+Ready to run the design solver?
+ """,
+ "design_params": {
+ "topology": topology,
+ "vin_min": self.state.input_voltage[0],
+ "vin_max": self.state.input_voltage[1],
+ "vin_is_ac": self.state.input_is_ac,
+ "outputs": self.state.outputs,
+ "frequency_hz": 100000,
+ },
+ "similar_examples": similar[:5],
+ "actions": [
+ {"label": "Run Design Solver", "action": "solve"},
+ {"label": "Adjust Parameters", "action": "edit"},
+ {"label": "See More Examples", "action": "examples"},
+ ],
+ }
+
+ def _format_similar_examples(self, examples: list) -> str:
+ """Format examples for display."""
+ if not examples:
+ return "- No similar examples found"
+
+ lines = []
+ for e in examples:
+ power = sum(o["voltage"] * o["current"] for o in e.outputs)
+ lines.append(f"- {e.name} ({power:.0f}W)")
+
+ return "\n".join(lines)
+
+ def explain_tradeoff(self, topic: str) -> str:
+ """
+ Explain a design trade-off in plain language.
+
+ Args:
+ topic: Trade-off topic (e.g., "core_size_vs_loss")
+
+ Returns:
+ Plain language explanation
+ """
+ tradeoff = TRADEOFFS.get(topic)
+ if tradeoff:
+ return f"**{tradeoff['description']}**\n\n{tradeoff['explanation']}"
+
+ return f"I don't have specific guidance on '{topic}'. Could you rephrase?"
+
+ def explain_result(self, design_result) -> str:
+ """
+ Explain a design result in hardware engineer terms.
+
+ Args:
+ design_result: DesignResult from PyOpenMagnetics
+
+ Returns:
+ Plain language explanation
+ """
+ # Assess the design
+ flux_ok = design_result.bpk_tesla < 0.25
+ temp_ok = design_result.temp_rise_c < 40
+ margin_ok = design_result.saturation_margin > 0.2
+
+ explanation = f"""
+## Design Result: {design_result.core} with {design_result.material}
+
+### Quick Assessment
+- **Flux Density**: {design_result.bpk_tesla * 1000:.0f} mT {'✓ Good' if flux_ok else '⚠️ High - may saturate'}
+- **Temperature Rise**: ~{design_result.temp_rise_c:.0f}°C {'✓ Acceptable' if temp_ok else '⚠️ Hot - needs cooling'}
+- **Saturation Margin**: {design_result.saturation_margin * 100:.0f}% {'✓ Safe' if margin_ok else '⚠️ Tight'}
+
+### What to Order
+| Component | Specification |
+|-----------|---------------|
+| Core | {design_result.core} ({design_result.material}) |
+| Primary Wire | {design_result.primary_wire}, {design_result.primary_turns} turns |
+| Air Gap | {design_result.air_gap_mm:.2f} mm {'(spacer)' if design_result.air_gap_mm > 0.5 else '(ground core)'} |
+
+### Expected Performance
+- Core Loss: {design_result.core_loss_w:.2f} W
+- Copper Loss: {design_result.copper_loss_w:.2f} W
+- **Total Loss: {design_result.total_loss_w:.2f} W**
+
+### Things to Check
+"""
+ warnings = []
+
+ if not flux_ok:
+ warnings.append("- Consider larger core to reduce flux density")
+
+ if not temp_ok:
+ warnings.append("- Add heatsinking or increase core size")
+
+ if not margin_ok:
+ warnings.append("- Increase air gap or reduce turns for saturation margin")
+
+ if design_result.primary_turns > 80:
+ warnings.append("- High turn count - consider larger core to reduce turns")
+
+ if not warnings:
+ warnings.append("- Design looks good! Verify with prototype.")
+
+ explanation += "\n".join(warnings)
+
+ return explanation
+
+ def get_application_guide(self, application: str) -> str:
+ """
+ Get a complete guide for an application.
+
+ Args:
+ application: Application key
+
+ Returns:
+ Comprehensive guide text
+ """
+ app = APPLICATIONS.get(application)
+ if not app:
+ return f"Unknown application: {application}"
+
+ guide = f"""
+# {app['name']} Design Guide
+
+{app.get('description', '')}
+
+## Typical Specifications
+- Power range: {app.get('power_range', 'Varies')} W
+- Typical topology: **{app.get('typical_topology', 'Various')}**
+
+## Variants
+"""
+ for name, spec in app.get('variants', {}).items():
+ guide += f"- **{name}**: {spec}\n"
+
+ guide += f"""
+## Key Design Constraints
+"""
+ for c in app.get('key_constraints', []):
+ guide += f"- {c}\n"
+
+ guide += f"""
+## Applicable Standards
+"""
+ for s in app.get('standards', []):
+ guide += f"- {s}\n"
+
+ guide += f"""
+## Design Tips
+"""
+ for tip in app.get('design_tips', []):
+ guide += f"- {tip}\n"
+
+ guide += f"""
+## Common Mistakes to Avoid
+"""
+ for mistake in app.get('common_mistakes', []):
+ guide += f"- {mistake}\n"
+
+ return guide
diff --git a/.archive/examples.py b/.archive/examples.py
new file mode 100644
index 0000000..a1eeed2
--- /dev/null
+++ b/.archive/examples.py
@@ -0,0 +1,551 @@
+"""
+Example Generator for PyOpenMagnetics.
+
+Generates hundreds of real-world design examples to help hardware engineers
+understand magnetic design through practical applications.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional
+from .knowledge import APPLICATIONS, TOPOLOGIES, suggest_topology
+
+MAX_RESULTS = 50
+
+@dataclass
+class DesignExample:
+ """A complete design example with context."""
+ name: str
+ category: str
+ application: str
+ description: str
+
+ # Electrical specs
+ topology: str
+ vin_min: float
+ vin_max: float
+ vin_is_ac: bool
+ outputs: list[dict] # [{voltage, current}, ...]
+ frequency_hz: float
+
+ # Constraints
+ max_height_mm: Optional[float] = None
+ max_width_mm: Optional[float] = None
+ efficiency_target: Optional[float] = None
+ ambient_temp_c: float = 25.0
+
+ # Context
+ real_world_products: list[str] = field(default_factory=list)
+ standards: list[str] = field(default_factory=list)
+ design_notes: list[str] = field(default_factory=list)
+ common_mistakes: list[str] = field(default_factory=list)
+
+ def to_design_params(self) -> dict:
+ """Convert to parameters for Design API."""
+ return {
+ "topology": self.topology,
+ "vin_min": self.vin_min,
+ "vin_max": self.vin_max,
+ "vin_is_ac": self.vin_is_ac,
+ "outputs": self.outputs,
+ "frequency_hz": self.frequency_hz,
+ "max_height_mm": self.max_height_mm,
+ "max_width_mm": self.max_width_mm,
+ "efficiency_target": self.efficiency_target,
+ }
+
+
+class ExampleGenerator:
+ """Generate design examples for various applications."""
+
+ def __init__(self):
+ self.examples: list[DesignExample] = []
+
+ def generate_all(self) -> list[DesignExample]:
+ """Generate all examples across all categories."""
+ self.examples = []
+
+ # Consumer
+ self.examples.extend(self._generate_usb_chargers())
+ self.examples.extend(self._generate_laptop_adapters())
+ self.examples.extend(self._generate_led_drivers())
+
+ # Automotive
+ self.examples.extend(self._generate_automotive_dcdc())
+ self.examples.extend(self._generate_gate_drivers())
+
+ # Industrial
+ self.examples.extend(self._generate_din_rail())
+ self.examples.extend(self._generate_medical())
+
+ # Telecom
+ self.examples.extend(self._generate_poe())
+ self.examples.extend(self._generate_server_psu())
+
+ # Inductors
+ self.examples.extend(self._generate_pfc_inductors())
+ self.examples.extend(self._generate_filter_inductors())
+
+ return self.examples
+
+ def _generate_usb_chargers(self) -> list[DesignExample]:
+ """Generate USB charger examples."""
+ examples = []
+
+ # USB PD power levels
+ pd_levels = [
+ (20, 12, 1.67, "phone", ["Apple 20W", "Samsung 25W"]),
+ (30, 20, 1.5, "tablet", ["iPad charger", "Pixel 30W"]),
+ (45, 20, 2.25, "ultrabook", ["MacBook Air", "Surface"]),
+ (65, 20, 3.25, "laptop", ["MacBook Pro 13", "Dell XPS"]),
+ (100, 20, 5.0, "workstation", ["MacBook Pro 14"]),
+ (140, 28, 5.0, "pro_laptop", ["MacBook Pro 16"]),
+ ]
+
+ for power, vout, iout, use, products in pd_levels:
+ # Universal input version
+ examples.append(DesignExample(
+ name=f"USB PD {power}W Universal Charger",
+ category="consumer",
+ application="usb_charger",
+ description=f"{power}W USB-C charger for {use}, universal AC input",
+ topology="flyback",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=100000 if power < 65 else 130000,
+ max_height_mm=18 if power < 65 else 22,
+ efficiency_target=0.87 if power < 45 else 0.90,
+ real_world_products=products,
+ standards=["IEC 62368-1", "DoE Level VI"],
+ design_notes=[
+ f"{'GaN' if power >= 65 else 'Si'} MOSFETs recommended",
+ "EFD or EE core for compact size",
+ "Primary-side regulation for cost savings",
+ ],
+ ))
+
+ # Compact GaN version for higher power
+ if power >= 45:
+ examples.append(DesignExample(
+ name=f"USB PD {power}W GaN Compact Charger",
+ category="consumer",
+ application="usb_charger",
+ description=f"Compact {power}W GaN charger, 30% smaller",
+ topology="active_clamp_flyback" if power < 100 else "flyback",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=200000 if power < 100 else 150000,
+ max_height_mm=14 if power < 100 else 18,
+ efficiency_target=0.92,
+ real_world_products=["Anker Nano", "Baseus GaN"],
+ design_notes=[
+ "GaN enables 2x frequency for smaller magnetics",
+ "Active clamp recovers leakage energy",
+ "Planar or ELP core for slim profile",
+ ],
+ ))
+
+ # Multi-output chargers
+ examples.append(DesignExample(
+ name="USB PD 65W Dual Output Charger",
+ category="consumer",
+ application="usb_charger",
+ description="65W total with 45W + 20W USB-C outputs",
+ topology="flyback",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": 20, "current": 2.25}, {"voltage": 12, "current": 1.67}],
+ frequency_hz=100000,
+ efficiency_target=0.88,
+ design_notes=[
+ "Cross-regulation critical for multi-output",
+ "Mag amp or synchronous rectifier for regulation",
+ ],
+ ))
+
+ return examples
+
+ def _generate_laptop_adapters(self) -> list[DesignExample]:
+ """Generate laptop adapter examples."""
+ examples = []
+
+ adapters = [
+ (65, 19.5, 3.33, "standard laptop", ["Dell", "HP", "Lenovo"]),
+ (90, 19.5, 4.62, "workstation laptop", ["ThinkPad", "Precision"]),
+ (135, 20, 6.75, "gaming laptop", ["Razer", "Alienware"]),
+ (180, 19.5, 9.23, "high-end gaming", ["MSI", "ASUS ROG"]),
+ (230, 19.5, 11.8, "desktop replacement", ["Alienware Area-51m"]),
+ ]
+
+ for power, vout, iout, use_case, brands in adapters:
+ topology = "flyback" if power < 150 else "LLC"
+
+ examples.append(DesignExample(
+ name=f"Laptop Adapter {power}W",
+ category="consumer",
+ application="laptop_adapter",
+ description=f"{power}W adapter for {use_case}",
+ topology=topology,
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=100000 if topology == "flyback" else 100000,
+ efficiency_target=0.88 if power < 150 else 0.92,
+ real_world_products=brands,
+ standards=["IEC 62368-1", "ENERGY STAR"],
+ design_notes=[
+ f"{'LLC resonant' if topology == 'LLC' else 'Flyback'} for this power level",
+ "Active PFC required for power factor",
+ "Slim form factor requires careful thermal design",
+ ],
+ ))
+
+ return examples
+
+ def _generate_led_drivers(self) -> list[DesignExample]:
+ """Generate LED driver examples."""
+ examples = []
+
+ # LED driver variants
+ led_types = [
+ (12, 1.0, "12V LED strip", 24, "constant_voltage"),
+ (24, 2.5, "24V LED strip", 48, "constant_voltage"),
+ (36, 1.0, "LED panel", 48, "constant_current"),
+ (48, 1.5, "LED tube retrofit", 60, "constant_current"),
+ (100, 2.1, "High bay light", 150, "constant_current"),
+ (200, 1.4, "Street light", 320, "constant_current"),
+ ]
+
+ for vout, iout, application, vled, output_type in led_types:
+ power = vout * iout
+
+ examples.append(DesignExample(
+ name=f"LED Driver {power:.0f}W {application}",
+ category="consumer",
+ application="led_driver",
+ description=f"{power:.0f}W {output_type} LED driver for {application}",
+ topology="flyback",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=65000,
+ efficiency_target=0.87,
+ design_notes=[
+ f"{'Constant current' if output_type == 'constant_current' else 'Constant voltage'} output",
+ "Consider electrolytic-free for long life",
+ "Valley fill for high power factor" if power < 25 else "Active PFC required",
+ ],
+ standards=["IEC 61347", "ENERGY STAR"],
+ ))
+
+ return examples
+
+ def _generate_automotive_dcdc(self) -> list[DesignExample]:
+ """Generate automotive DC-DC examples."""
+ examples = []
+
+ # 48V to 12V converters
+ for power in [200, 500, 1000, 2000, 3000]:
+ examples.append(DesignExample(
+ name=f"48V to 12V {power}W Automotive DC-DC",
+ category="automotive",
+ application="automotive_dcdc",
+ description=f"{power}W DC-DC for 48V mild hybrid to 12V auxiliary",
+ topology="phase_shifted_full_bridge" if power > 1000 else "buck",
+ vin_min=36, vin_max=52, vin_is_ac=False,
+ outputs=[{"voltage": 12, "current": power / 12}],
+ frequency_hz=100000,
+ ambient_temp_c=85,
+ efficiency_target=0.95,
+ standards=["AEC-Q200", "ISO 16750", "CISPR 25"],
+ design_notes=[
+ f"{'Interleaved phases' if power > 1000 else 'Single phase'} recommended",
+ "Powder core for high DC bias handling",
+ "-40°C to +105°C operation required",
+ ],
+ ))
+
+ # HV to 12V (EV main DC-DC)
+ for power in [2000, 2500, 3000, 3500]:
+ examples.append(DesignExample(
+ name=f"HV to 12V {power}W EV DC-DC (LDC)",
+ category="automotive",
+ application="automotive_dcdc",
+ description=f"{power}W isolated DC-DC from HV battery to 12V",
+ topology="phase_shifted_full_bridge",
+ vin_min=250, vin_max=450, vin_is_ac=False,
+ outputs=[{"voltage": 12, "current": power / 12}],
+ frequency_hz=100000,
+ efficiency_target=0.96,
+ standards=["AEC-Q200", "ISO 16750"],
+ design_notes=[
+ "Reinforced isolation required (HV class)",
+ "Planar transformer for power density",
+ "Liquid cooling typical at this power",
+ ],
+ ))
+
+ return examples
+
+ def _generate_gate_drivers(self) -> list[DesignExample]:
+ """Generate isolated gate driver transformer examples."""
+ examples = []
+
+ # Gate driver variants
+ variants = [
+ ("Si IGBT", 15, -8, 3000, 100000),
+ ("SiC MOSFET", 20, -5, 5000, 200000),
+ ("GaN HEMT", 6, 0, 1500, 500000),
+ ]
+
+ for device, vg_pos, vg_neg, isolation, freq in variants:
+ examples.append(DesignExample(
+ name=f"Isolated Gate Driver for {device}",
+ category="automotive",
+ application="gate_driver",
+ description=f"Gate drive transformer for {device}, +{vg_pos}V/{vg_neg}V",
+ topology="flyback",
+ vin_min=10, vin_max=14, vin_is_ac=False,
+ outputs=[{"voltage": vg_pos, "current": 0.5}],
+ frequency_hz=freq,
+ max_height_mm=8,
+ design_notes=[
+ f"{isolation}V isolation required",
+ "Low interwinding capacitance critical",
+ "EP or EFD core for compact size",
+ f"CMTI > 100 V/ns for {device}",
+ ],
+ ))
+
+ return examples
+
+ def _generate_din_rail(self) -> list[DesignExample]:
+ """Generate DIN rail power supply examples."""
+ examples = []
+
+ variants = [
+ (24, 2.5, 60, "1.5 SU"),
+ (24, 5, 120, "3 SU"),
+ (24, 10, 240, "6 SU"),
+ (24, 20, 480, "9 SU"),
+ (48, 5, 240, "6 SU"),
+ (12, 5, 60, "3 SU"),
+ ]
+
+ for vout, iout, power, width in variants:
+ topology = "flyback" if power <= 150 else "LLC"
+
+ examples.append(DesignExample(
+ name=f"DIN Rail {vout}V {power}W PSU",
+ category="industrial",
+ application="din_rail_psu",
+ description=f"{power}W DIN rail power supply, {width} width",
+ topology=topology,
+ vin_min=85, vin_max=264, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=100000,
+ efficiency_target=0.89,
+ standards=["IEC 62368-1", "UL 508"],
+ design_notes=[
+ f"{'Single-stage flyback' if topology == 'flyback' else 'PFC + LLC'} architecture",
+ "DIN rail width limits core height",
+ "Consider redundancy capability",
+ ],
+ ))
+
+ return examples
+
+ def _generate_medical(self) -> list[DesignExample]:
+ """Generate medical power supply examples."""
+ examples = []
+
+ variants = [
+ (12, 4.17, 50, "BF", "patient monitor"),
+ (24, 2.5, 60, "BF", "infusion pump"),
+ (48, 1.5, 72, "2xMOPP", "diagnostic equipment"),
+ (12, 8.33, 100, "CF", "cardiac equipment"),
+ ]
+
+ for vout, iout, power, classification, application in variants:
+ examples.append(DesignExample(
+ name=f"Medical {power}W {classification} PSU",
+ category="industrial",
+ application="medical_psu",
+ description=f"{power}W medical PSU for {application}, {classification} rated",
+ topology="flyback",
+ vin_min=85, vin_max=264, vin_is_ac=True,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=100000,
+ efficiency_target=0.87,
+ standards=["IEC 60601-1", "IEC 60601-1-2"],
+ design_notes=[
+ f"{classification} classification requirements",
+ "8mm creepage/clearance for 2xMOPP",
+ "Patient leakage <100µA (BF) or <10µA (CF)",
+ "Triple-insulated wire mandatory",
+ ],
+ ))
+
+ return examples
+
+ def _generate_poe(self) -> list[DesignExample]:
+ """Generate PoE examples."""
+ examples = []
+
+ standards = [
+ ("802.3af", 15.4, 12, 1.0, "IP camera"),
+ ("802.3at", 30, 12, 2.0, "Access point"),
+ ("802.3bt Type 3", 60, 12, 4.0, "PTZ camera"),
+ ("802.3bt Type 4", 90, 48, 1.5, "Digital signage"),
+ ]
+
+ for std, poe_power, vout, iout, application in standards:
+ examples.append(DesignExample(
+ name=f"PoE PD {std} for {application}",
+ category="telecom",
+ application="poe",
+ description=f"PoE Powered Device converter ({std}) for {application}",
+ topology="flyback",
+ vin_min=36, vin_max=57, vin_is_ac=False,
+ outputs=[{"voltage": vout, "current": iout}],
+ frequency_hz=200000,
+ max_height_mm=10,
+ efficiency_target=0.88,
+ standards=["IEEE " + std],
+ design_notes=[
+ f"Input from PoE {std} ({poe_power}W max)",
+ "Compact design critical for integration",
+ "Consider 4-pair for high power",
+ ],
+ ))
+
+ return examples
+
+ def _generate_server_psu(self) -> list[DesignExample]:
+ """Generate server PSU examples."""
+ examples = []
+
+ levels = [
+ (800, "Gold", 0.87, "ATX"),
+ (1200, "Platinum", 0.90, "CRPS"),
+ (2000, "Titanium", 0.94, "CRPS"),
+ (3000, "Titanium", 0.94, "OCP"),
+ ]
+
+ for power, tier, eff, form in levels:
+ examples.append(DesignExample(
+ name=f"Server PSU {power}W {tier}",
+ category="telecom",
+ application="server_psu",
+ description=f"{power}W server power supply, 80 PLUS {tier}, {form} form factor",
+ topology="LLC",
+ vin_min=85, vin_max=264, vin_is_ac=True,
+ outputs=[{"voltage": 12, "current": power / 12}],
+ frequency_hz=100000,
+ efficiency_target=eff,
+ standards=["80 PLUS", "CRPS" if "CRPS" in form else "ATX"],
+ design_notes=[
+ "Interleaved bridgeless totem-pole PFC",
+ "LLC with synchronous rectification",
+ f"{'GaN for Titanium' if tier == 'Titanium' else 'SiC or Si'} MOSFETs",
+ "Digital control for efficiency optimization",
+ ],
+ ))
+
+ return examples
+
+ def _generate_pfc_inductors(self) -> list[DesignExample]:
+ """Generate PFC inductor examples."""
+ examples = []
+
+ levels = [
+ (100, 250e-6, 1.0, "CCM"),
+ (300, 400e-6, 2.0, "CCM"),
+ (600, 500e-6, 3.5, "CCM"),
+ (1000, 600e-6, 5.0, "CCM"),
+ (2000, 400e-6, 8.0, "interleaved"),
+ (3000, 300e-6, 12.0, "interleaved"),
+ ]
+
+ for power, inductance, idc, mode in levels:
+ examples.append(DesignExample(
+ name=f"PFC Inductor {power}W {mode}",
+ category="inductor",
+ application="pfc_inductor",
+ description=f"Boost PFC inductor for {power}W, {mode} mode",
+ topology="inductor",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": 400, "current": power / 400}],
+ frequency_hz=65000 if mode == "CCM" else 100000,
+ design_notes=[
+ f"Inductance: {inductance*1e6:.0f}µH",
+ f"DC current: {idc}A, ripple ~30%",
+ "Powder core (Kool Mu) for DC bias" if power < 1000 else "Amorphous for high power",
+ f"{'Single winding' if mode == 'CCM' else 'Coupled inductors'} topology",
+ ],
+ ))
+
+ return examples
+
+ def _generate_filter_inductors(self) -> list[DesignExample]:
+ """Generate EMI filter inductor examples."""
+ examples = []
+
+ # Common mode chokes
+ for current in [1, 3, 5, 10, 20]:
+ examples.append(DesignExample(
+ name=f"Common Mode Choke {current}A",
+ category="inductor",
+ application="emi_filter",
+ description=f"Common mode EMI choke, {current}A rated",
+ topology="inductor",
+ vin_min=85, vin_max=265, vin_is_ac=True,
+ outputs=[{"voltage": 0, "current": current}],
+ frequency_hz=100000,
+ design_notes=[
+ f"Rated current: {current}A",
+ "Nanocrystalline or high-µ ferrite core",
+ "Bifilar winding for balance",
+ "High impedance 150kHz-30MHz",
+ ],
+ ))
+
+ return examples
+
+ def get_examples_by_category(self, category: str) -> list[DesignExample]:
+ """Get all examples in a category."""
+ if not self.examples:
+ self.generate_all()
+ return [e for e in self.examples if e.category == category]
+
+ def get_examples_by_application(self, application: str) -> list[DesignExample]:
+ """Get all examples for an application."""
+ if not self.examples:
+ self.generate_all()
+ return [e for e in self.examples if e.application == application]
+
+ def get_examples_by_power_range(self, min_power: float, max_power: float) -> list[DesignExample]:
+ """Get examples within a power range."""
+ if not self.examples:
+ self.generate_all()
+ results = []
+ for e in self.examples:
+ power = sum(o["voltage"] * o["current"] for o in e.outputs)
+ if min_power <= power <= max_power:
+ results.append(e)
+ return results
+
+
+def generate_application_examples(application: str) -> list[DesignExample]:
+ """Generate examples for a specific application."""
+ generator = ExampleGenerator()
+ generator.generate_all()
+ return generator.get_examples_by_application(application)
+
+
+def get_example_count() -> dict:
+ """Get count of examples by category."""
+ generator = ExampleGenerator()
+ examples = generator.generate_all()
+
+ counts = {}
+ for e in examples:
+ counts[e.category] = counts.get(e.category, 0) + 1
+
+ return {"total": len(examples), "by_category": counts}
diff --git a/.archive/knowledge.py b/.archive/knowledge.py
new file mode 100644
index 0000000..fc905b2
--- /dev/null
+++ b/.archive/knowledge.py
@@ -0,0 +1,1202 @@
+"""
+Magnetic Design Knowledge Base.
+
+Domain expertise for power electronics applications.
+Written for hardware engineers, not software developers.
+"""
+
+# =============================================================================
+# APPLICATION KNOWLEDGE
+# =============================================================================
+
+APPLICATIONS = {
+ # -------------------------------------------------------------------------
+ # CONSUMER ELECTRONICS
+ # -------------------------------------------------------------------------
+ "usb_charger": {
+ "name": "USB Charger / Power Adapter",
+ "description": "Wall charger for phones, tablets, and small devices",
+ "power_range": (5, 240), # Watts
+ "variants": {
+ "5w_basic": {"power": 5, "vout": 5, "profile": "USB-A legacy"},
+ "10w_ipad": {"power": 10, "vout": 5, "profile": "Apple 2.1A"},
+ "18w_qc": {"power": 18, "vout": [5, 9, 12], "profile": "QC3.0"},
+ "20w_pd": {"power": 20, "vout": [5, 9, 12], "profile": "PD 20W"},
+ "30w_pd": {"power": 30, "vout": [5, 9, 15, 20], "profile": "PD 30W"},
+ "45w_pd": {"power": 45, "vout": [5, 9, 15, 20], "profile": "PD 45W"},
+ "65w_pd": {"power": 65, "vout": [5, 9, 15, 20], "profile": "PD 65W"},
+ "100w_pd": {"power": 100, "vout": [5, 9, 15, 20], "profile": "PD 100W"},
+ "140w_pd": {"power": 140, "vout": [5, 9, 15, 20, 28], "profile": "PD 3.1 EPR"},
+ "240w_pd": {"power": 240, "vout": [5, 9, 15, 20, 28, 48], "profile": "PD 3.1 EPR"},
+ },
+ "typical_topology": "flyback",
+ "alternative_topologies": ["active_clamp_flyback", "LLC"],
+ "input_voltage": {"universal_ac": (85, 265), "us_only": (100, 130), "eu_only": (200, 240)},
+ "key_constraints": ["size", "cost", "efficiency", "standby_power", "EMI"],
+ "standards": ["IEC 62368-1", "DoE Level VI", "CoC Tier 2", "FCC Part 15"],
+ "design_tips": [
+ "EFD cores for flat adapters, ETD for cubes",
+ "Higher frequency (>100kHz) enables smaller size but needs GaN",
+ "Standby power <75mW required for efficiency standards",
+ "Primary-side regulation can eliminate optocoupler",
+ ],
+ "common_mistakes": [
+ "Undersized transformer for thermal (runs too hot)",
+ "Insufficient creepage for safety standards",
+ "EMI filter too small for Class B",
+ "Not accounting for cable voltage drop",
+ ],
+ "questions_to_ask": [
+ "What power level? (affects topology choice)",
+ "Single output or PPS (programmable)?",
+ "GaN or Silicon? (affects frequency/size)",
+ "Target market? (affects standards)",
+ "Form factor preference? (cube, flat, travel)",
+ ],
+ },
+
+ "laptop_adapter": {
+ "name": "Laptop Power Adapter",
+ "description": "AC-DC adapter for laptops and notebooks",
+ "power_range": (45, 330),
+ "variants": {
+ "45w_ultrabook": {"power": 45, "vout": 20, "connector": "USB-C"},
+ "65w_standard": {"power": 65, "vout": [19, 20], "connector": "barrel/USB-C"},
+ "90w_workstation": {"power": 90, "vout": 19.5, "connector": "barrel"},
+ "100w_usbc": {"power": 100, "vout": 20, "connector": "USB-C"},
+ "140w_macbook": {"power": 140, "vout": 28, "connector": "MagSafe/USB-C"},
+ "180w_gaming": {"power": 180, "vout": 19.5, "connector": "barrel"},
+ "230w_gaming": {"power": 230, "vout": 19.5, "connector": "barrel"},
+ "330w_workstation": {"power": 330, "vout": 19.5, "connector": "barrel"},
+ },
+ "typical_topology": "flyback",
+ "topology_by_power": {
+ (0, 100): "flyback",
+ (100, 200): "active_clamp_flyback",
+ (200, 400): "LLC",
+ },
+ "key_constraints": ["efficiency", "weight", "slim_profile", "thermal"],
+ "design_tips": [
+ "LLC preferred >150W for efficiency",
+ "Slim adapters need EFD/ELP cores",
+ "Consider interleaved PFC for >100W",
+ "Heat pipes common in slim high-power designs",
+ ],
+ },
+
+ "led_driver": {
+ "name": "LED Driver",
+ "description": "Power supply for LED lighting",
+ "power_range": (3, 500),
+ "variants": {
+ "bulb_retrofit": {"power": (3, 15), "output": "constant_current"},
+ "tube_retrofit": {"power": (10, 30), "output": "constant_current"},
+ "panel_driver": {"power": (20, 60), "output": "constant_current"},
+ "high_bay": {"power": (100, 300), "output": "constant_current"},
+ "street_light": {"power": (50, 200), "output": "constant_current"},
+ "rgb_strip": {"power": (30, 150), "output": "constant_voltage"},
+ },
+ "typical_topology": "flyback",
+ "key_constraints": ["flicker", "PF", "THD", "dimming", "lifetime"],
+ "standards": ["ENERGY STAR", "DLC", "Title 24", "Zhaga"],
+ "design_tips": [
+ "Electrolytic-free for long life (>50k hours)",
+ "Valley-fill for high PF without boost PFC",
+ "Single-stage flyback for cost, two-stage for performance",
+ "TRIAC dimming needs specific control IC",
+ ],
+ "questions_to_ask": [
+ "Indoor or outdoor (IP rating)?",
+ "Dimming required? What method?",
+ "Flicker requirements (IEEE 1789)?",
+ "LED forward voltage and current?",
+ "Operating temperature range?",
+ ],
+ },
+
+ # -------------------------------------------------------------------------
+ # AUTOMOTIVE
+ # -------------------------------------------------------------------------
+ "automotive_dcdc": {
+ "name": "Automotive DC-DC Converter",
+ "description": "Voltage conversion for automotive systems",
+ "variants": {
+ "12v_aux": {
+ "description": "12V auxiliary from 48V mild hybrid",
+ "vin": (36, 52),
+ "vout": 12,
+ "power": (100, 3000),
+ },
+ "48v_from_hv": {
+ "description": "48V from high-voltage battery",
+ "vin": (250, 450),
+ "vout": 48,
+ "power": (1000, 5000),
+ },
+ "lv_from_hv": {
+ "description": "12V from high-voltage battery (main LDC)",
+ "vin": (250, 450),
+ "vout": 12,
+ "power": (1500, 3500),
+ },
+ },
+ "typical_topology": "phase_shifted_full_bridge",
+ "key_constraints": ["efficiency", "EMC", "temperature", "isolation", "weight"],
+ "standards": ["AEC-Q200", "ISO 16750", "CISPR 25", "LV 124"],
+ "design_tips": [
+ "Use nanocrystalline for EMI common mode chokes",
+ "Planar transformers common for high power density",
+ "Consider interleaving for current ripple",
+ "-40°C to +105°C ambient typical",
+ ],
+ "questions_to_ask": [
+ "Voltage class (LV / 48V / HV)?",
+ "Continuous vs peak power?",
+ "Bidirectional required?",
+ "Cooling method (air/liquid)?",
+ "ASIL safety level?",
+ ],
+ },
+
+ "ev_onboard_charger": {
+ "name": "EV On-Board Charger (OBC)",
+ "description": "AC-DC charger integrated in electric vehicle",
+ "variants": {
+ "3kw_level1": {"power": 3300, "input": "1-phase", "vout": (250, 450)},
+ "7kw_level2": {"power": 7000, "input": "1-phase", "vout": (250, 450)},
+ "11kw_level2": {"power": 11000, "input": "3-phase", "vout": (250, 450)},
+ "22kw_level2": {"power": 22000, "input": "3-phase", "vout": (250, 800)},
+ },
+ "typical_topology": "CLLC", # For bidirectional
+ "key_constraints": ["efficiency", "power_density", "bidirectional", "isolation"],
+ "design_tips": [
+ "CLLC topology for V2G bidirectional",
+ "SiC MOSFETs essential for high efficiency",
+ "Integrated OBC+DC-DC saves space",
+ "Nanocrystalline or ferrite for transformer",
+ ],
+ },
+
+ "gate_driver_isolated": {
+ "name": "Isolated Gate Driver Transformer",
+ "description": "Isolation transformer for driving power switches",
+ "applications": ["half_bridge", "full_bridge", "SiC", "GaN", "IGBT"],
+ "typical_specs": {
+ "vout_positive": (12, 20), # +Vg
+ "vout_negative": (-5, -8), # -Vg (for SiC/IGBT)
+ "isolation": (1500, 5000), # Vrms
+ "frequency": (100e3, 1e6),
+ },
+ "typical_topology": "flyback",
+ "key_constraints": ["propagation_delay", "CMTI", "isolation", "size"],
+ "design_tips": [
+ "Small EE or EP cores for gate drivers",
+ "Low interwinding capacitance critical for CMTI",
+ "Symmetric construction for matched delay",
+ "Triple-insulated wire for reinforced isolation",
+ ],
+ },
+
+ # -------------------------------------------------------------------------
+ # INDUSTRIAL
+ # -------------------------------------------------------------------------
+ "din_rail_psu": {
+ "name": "DIN Rail Power Supply",
+ "description": "Industrial power supply for control cabinets",
+ "variants": {
+ "5v_15w": {"vout": 5, "power": 15},
+ "12v_30w": {"vout": 12, "power": 30},
+ "24v_60w": {"vout": 24, "power": 60},
+ "24v_120w": {"vout": 24, "power": 120},
+ "24v_240w": {"vout": 24, "power": 240},
+ "24v_480w": {"vout": 24, "power": 480},
+ "48v_240w": {"vout": 48, "power": 240},
+ },
+ "typical_topology": "flyback",
+ "topology_by_power": {
+ (0, 150): "flyback",
+ (150, 300): "forward",
+ (300, 600): "LLC",
+ },
+ "key_constraints": ["efficiency", "reliability", "parallel_operation", "wide_temp"],
+ "standards": ["IEC 62368-1", "EN 61000-3-2", "UL 508"],
+ "design_tips": [
+ "DIN rail width limits core height",
+ "Spring terminals preferred over screw",
+ "Consider N+1 redundancy capability",
+ "Long-life electrolytic or film caps",
+ ],
+ },
+
+ "medical_psu": {
+ "name": "Medical Power Supply",
+ "description": "Isolated power supply for medical equipment",
+ "classifications": {
+ "BF": "Body Floating - patient contact, not cardiac",
+ "CF": "Cardiac Floating - direct cardiac connection",
+ "2xMOPP": "2 Means of Patient Protection",
+ "2xMOOP": "2 Means of Operator Protection",
+ },
+ "key_constraints": ["leakage_current", "isolation", "EMC", "reliability"],
+ "standards": ["IEC 60601-1", "IEC 60601-1-2", "IEC 60601-1-11"],
+ "design_tips": [
+ "8mm creepage/clearance for 2xMOPP",
+ "Earth leakage <300µA (NC), <500µA (SFC)",
+ "Patient leakage <100µA (BF), <10µA (CF)",
+ "EMC limits 6dB stricter than industrial",
+ "Triple insulated wire or margin tape",
+ ],
+ "questions_to_ask": [
+ "Applied part (patient contact)?",
+ "Classification needed (BF/CF)?",
+ "Home healthcare or clinical?",
+ "Defibrillator-proof required?",
+ ],
+ },
+
+ "vfd_magnetics": {
+ "name": "VFD Magnetics (Chokes & Filters)",
+ "description": "Inductors and filters for variable frequency drives",
+ "components": {
+ "dc_link_choke": {
+ "purpose": "Smooth DC bus ripple",
+ "typical_L": (0.5e-3, 5e-3),
+ "typical_I": (5, 500),
+ },
+ "output_filter": {
+ "purpose": "dV/dt filter for motor cable",
+ "typical_L": (50e-6, 500e-6),
+ },
+ "common_mode_choke": {
+ "purpose": "EMI suppression",
+ "typical_L": (1e-3, 50e-3),
+ },
+ "line_reactor": {
+ "purpose": "Input harmonic reduction",
+ "typical_L": (1e-3, 10e-3),
+ },
+ },
+ "core_materials": {
+ "dc_link": "powder_core", # Sendust, MPP, High Flux
+ "output": "powder_core",
+ "common_mode": "nanocrystalline",
+ "line_reactor": "silicon_steel",
+ },
+ },
+
+ # -------------------------------------------------------------------------
+ # TELECOM / DATACENTER
+ # -------------------------------------------------------------------------
+ "server_psu": {
+ "name": "Server Power Supply",
+ "description": "High-efficiency PSU for servers and datacenters",
+ "form_factors": {
+ "atx": {"width": 150, "height": 86, "depth": 140},
+ "1u": {"width": 40, "height": 40, "depth": 200},
+ "crps": {"width": 73.5, "height": 39, "depth": 185},
+ },
+ "power_levels": [500, 800, 1200, 1600, 2000, 2400, 3000],
+ "efficiency_tiers": {
+ "80_plus": 0.80,
+ "bronze": 0.82,
+ "silver": 0.85,
+ "gold": 0.87,
+ "platinum": 0.90,
+ "titanium": 0.94,
+ },
+ "typical_topology": "LLC",
+ "key_constraints": ["efficiency", "power_density", "holdup_time", "hot_swap"],
+ "design_tips": [
+ "Interleaved bridgeless totem-pole PFC",
+ "LLC with synchronous rectification",
+ "GaN for Titanium efficiency",
+ "Digital control for adaptive tuning",
+ ],
+ },
+
+ "poe_psu": {
+ "name": "Power over Ethernet",
+ "description": "Power delivery over Ethernet cable",
+ "standards": {
+ "802.3af": {"power": 15.4, "voltage": (44, 57), "name": "PoE"},
+ "802.3at": {"power": 30, "voltage": (50, 57), "name": "PoE+"},
+ "802.3bt_type3": {"power": 60, "voltage": (50, 57), "name": "PoE++"},
+ "802.3bt_type4": {"power": 90, "voltage": (52, 57), "name": "PoE++"},
+ },
+ "sides": {
+ "PSE": "Power Sourcing Equipment (switch/injector)",
+ "PD": "Powered Device (camera, AP, phone)",
+ },
+ "typical_topology": "flyback",
+ "design_tips": [
+ "Input voltage is already DC (from PoE)",
+ "PD-side needs flyback for isolation",
+ "Low standby power for PoE budget",
+ "Consider 4-pair for high power",
+ ],
+ },
+
+ "telecom_rectifier": {
+ "name": "Telecom Rectifier Module",
+ "description": "AC-DC module for -48V telecom systems",
+ "power_levels": [1000, 2000, 3000, 4000, 6000],
+ "output": {"voltage": -48, "range": (-42, -58)},
+ "typical_topology": "LLC",
+ "key_constraints": ["efficiency", "power_factor", "hot_swap", "N+1"],
+ "standards": ["ETSI EN 300 132-2", "Bellcore GR-1089"],
+ },
+
+ # -------------------------------------------------------------------------
+ # MAGNETICS-SPECIFIC (Not power supplies)
+ # -------------------------------------------------------------------------
+ "emi_filter": {
+ "name": "EMI Filter Components",
+ "description": "Common mode and differential mode chokes",
+ "components": {
+ "common_mode_choke": {
+ "purpose": "Attenuate common mode noise",
+ "frequency_range": (10e3, 30e6),
+ "core_material": "nanocrystalline",
+ "typical_L": (1e-3, 100e-3),
+ },
+ "differential_mode_choke": {
+ "purpose": "Attenuate differential mode noise",
+ "core_material": "powder_core",
+ "typical_L": (10e-6, 1e-3),
+ },
+ },
+ "design_tips": [
+ "CM choke: high impedance at noise frequency, low at 50/60Hz",
+ "Nanocrystalline best for broadband CM suppression",
+ "DM choke must handle DC bias without saturation",
+ "Split-winding reduces interwinding capacitance",
+ ],
+ },
+
+ "current_transformer": {
+ "name": "Current Transformer / Current Sense",
+ "description": "Galvanically isolated current measurement",
+ "applications": {
+ "metering": {"accuracy": "0.1-0.5%", "phase": "<0.5°"},
+ "protection": {"accuracy": "1-5%", "saturation_factor": ">20"},
+ "control": {"bandwidth": ">100kHz", "accuracy": "1-3%"},
+ },
+ "core_materials": {
+ "metering": "nanocrystalline", # Low loss, high permeability
+ "protection": "silicon_steel", # High saturation
+ "control": "ferrite", # High frequency
+ },
+ },
+
+ "pfc_choke": {
+ "name": "PFC Boost Inductor",
+ "description": "Inductor for power factor correction",
+ "modes": {
+ "CCM": "Continuous conduction mode - large L, low ripple",
+ "CrCM": "Critical conduction mode - variable freq, zero crossing",
+ "DCM": "Discontinuous mode - small L, high peak current",
+ },
+ "typical_topology": "boost",
+ "core_materials": {
+ "low_power": "ferrite", # <200W
+ "medium_power": "powder_core", # 200W-2kW (Kool Mu, sendust)
+ "high_power": "amorphous", # >2kW
+ },
+ "design_tips": [
+ "Powder cores have distributed gap - lower fringing EMI",
+ "Ferrite needs discrete gap - watch for fringing",
+ "DC bias rating critical - check µ vs H curves",
+ "Thermal management often limits inductor, not saturation",
+ ],
+ },
+}
+
+
+# =============================================================================
+# TOPOLOGY KNOWLEDGE
+# =============================================================================
+
+TOPOLOGIES = {
+ "flyback": {
+ "name": "Flyback",
+ "type": "isolated",
+ "power_range": (1, 200), # Typical watts
+ "advantages": [
+ "Simple, low component count",
+ "Multiple outputs easy",
+ "Wide input range",
+ "No output inductor",
+ ],
+ "disadvantages": [
+ "High peak currents",
+ "Transformer stress",
+ "EMI from fast edges",
+ "Efficiency limited >150W",
+ ],
+ "best_for": ["USB chargers", "auxiliary supplies", "LED drivers"],
+ "when_to_use": "Power <150W, multiple outputs, cost sensitive",
+ },
+
+ "forward": {
+ "name": "Forward",
+ "type": "isolated",
+ "power_range": (50, 500),
+ "variants": ["single_switch", "two_switch", "active_clamp"],
+ "advantages": [
+ "Lower peak currents than flyback",
+ "Better transformer utilization",
+ "Easier EMI filtering",
+ ],
+ "disadvantages": [
+ "Needs reset winding or clamp",
+ "Output inductor required",
+ "Core reset limits duty cycle",
+ ],
+ "best_for": ["Industrial PSU", "telecom", "medium power"],
+ "when_to_use": "Power 100-500W, single output, efficiency important",
+ },
+
+ "LLC": {
+ "name": "LLC Resonant",
+ "type": "isolated",
+ "power_range": (100, 10000),
+ "advantages": [
+ "Soft switching (ZVS/ZCS)",
+ "High efficiency",
+ "Low EMI",
+ "High power density",
+ ],
+ "disadvantages": [
+ "Complex control",
+ "Narrow gain range",
+ "Sensitive to component variation",
+ "Needs PFC front-end",
+ ],
+ "best_for": ["Server PSU", "EV chargers", "high efficiency apps"],
+ "when_to_use": "Power >200W, efficiency critical, DC input",
+ },
+
+ "buck": {
+ "name": "Buck (Step-Down)",
+ "type": "non-isolated",
+ "power_range": (0.1, 1000),
+ "advantages": [
+ "Simple, efficient",
+ "Continuous output current",
+ "Easy to parallel",
+ ],
+ "disadvantages": [
+ "Vout must be < Vin",
+ "No isolation",
+ "Input current discontinuous",
+ ],
+ "best_for": ["POL converters", "battery charging", "LED current"],
+ },
+
+ "boost": {
+ "name": "Boost (Step-Up)",
+ "type": "non-isolated",
+ "power_range": (1, 5000),
+ "advantages": [
+ "Step-up capability",
+ "Continuous input current",
+ "Good for PFC",
+ ],
+ "disadvantages": [
+ "Vout must be > Vin",
+ "Output current discontinuous",
+ "No short circuit protection inherent",
+ ],
+ "best_for": ["PFC", "battery boost", "solar MPPT"],
+ },
+
+ "phase_shifted_full_bridge": {
+ "name": "Phase-Shifted Full Bridge",
+ "type": "isolated",
+ "power_range": (500, 10000),
+ "advantages": [
+ "ZVS switching",
+ "Handles high power",
+ "Bidirectional capable",
+ ],
+ "disadvantages": [
+ "Complex control",
+ "Duty cycle loss",
+ "Many components",
+ ],
+ "best_for": ["EV DC-DC", "server PSU", "welding"],
+ },
+}
+
+
+# =============================================================================
+# MATERIAL GUIDE
+# =============================================================================
+
+MATERIALS_GUIDE = {
+ "ferrite": {
+ "types": {
+ "power": {
+ "examples": ["3C90", "3C95", "3C97", "N87", "N97", "PC95"],
+ "frequency": (20e3, 500e3),
+ "applications": ["transformers", "inductors"],
+ },
+ "high_frequency": {
+ "examples": ["3F3", "3F4", "N49", "PC200"],
+ "frequency": (500e3, 3e6),
+ "applications": ["resonant converters", "GaN designs"],
+ },
+ },
+ "advantages": ["Low cost", "High resistivity", "Many shapes"],
+ "disadvantages": ["Low saturation (~400mT)", "Temperature sensitive"],
+ "selection_tips": [
+ "3C95/N95 - General purpose 100kHz",
+ "3C97/N97 - Low loss at 100-300kHz",
+ "3F4/N49 - 500kHz-1MHz",
+ ],
+ },
+
+ "powder_core": {
+ "types": {
+ "MPP": {
+ "permeability": (14, 550),
+ "saturation": "1.5T",
+ "best_for": "Lowest loss, highest cost",
+ },
+ "High_Flux": {
+ "permeability": (14, 160),
+ "saturation": "1.5T",
+ "best_for": "DC bias, moderate loss",
+ },
+ "Kool_Mu": {
+ "permeability": (26, 125),
+ "saturation": "1.0T",
+ "best_for": "Good all-around, PFC",
+ },
+ "XFlux": {
+ "permeability": (26, 60),
+ "saturation": "1.6T",
+ "best_for": "Highest DC bias, lowest cost",
+ },
+ },
+ "advantages": ["Distributed gap", "High saturation", "DC bias tolerant"],
+ "disadvantages": ["Higher loss than ferrite", "Limited shapes"],
+ "selection_tips": [
+ "Kool Mu for PFC - good balance",
+ "High Flux for DC chokes - handles bias",
+ "MPP for filters - lowest loss",
+ ],
+ },
+
+ "nanocrystalline": {
+ "properties": {
+ "saturation": "1.2T",
+ "permeability": (15000, 150000),
+ "frequency": (1e3, 1e6),
+ },
+ "advantages": ["Very high permeability", "Low loss", "Excellent EMI suppression"],
+ "disadvantages": ["Brittle", "Limited shapes", "Higher cost"],
+ "best_for": ["Common mode chokes", "Current transformers", "High-end EMI filters"],
+ },
+
+ "amorphous": {
+ "properties": {
+ "saturation": "1.56T",
+ "frequency": (50, 100e3),
+ },
+ "advantages": ["High saturation", "Low loss at low frequency"],
+ "disadvantages": ["Difficult to cut", "Lower frequency than ferrite"],
+ "best_for": ["High power PFC", "Solar inverters", "Medium frequency transformers"],
+ },
+}
+
+
+# =============================================================================
+# POWDER CORE MATERIAL DATABASE
+# =============================================================================
+# Steinmetz parameters for core loss calculation: Pv = k * f^alpha * B^beta
+# Where Pv is in W/m³, f in Hz, B in Tesla
+# Permeability curve: mu_r = mu_i / (a + b * |H/oersted|^c) / 100
+# H in A/m, oersted = 79.5775 A/m
+
+POWDER_CORE_MATERIALS = {
+ # -------------------------------------------------------------------------
+ # MPP (Molypermalloy Powder) - Lowest loss, highest cost
+ # -------------------------------------------------------------------------
+ "CSC_MPP_26u": {
+ "name": "CSC MPP 26µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 26,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 32.0, "alpha": 1.27, "beta": 2.18},
+ "permeability_curve": {"a": 0.01, "b": 1.42e-07, "c": 2.030},
+ "frequency_range": (10e3, 500e3),
+ "applications": ["High Q filters", "Resonant inductors", "Low loss PFC"],
+ "notes": "Lowest core loss among powder cores, excellent for high frequency",
+ },
+ "CSC_MPP_60u": {
+ "name": "CSC MPP 60µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 60,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 32.0, "alpha": 1.27, "beta": 2.18},
+ "permeability_curve": {"a": 0.01, "b": 1.27e-07, "c": 2.412},
+ "frequency_range": (10e3, 500e3),
+ "applications": ["Flyback inductors", "Switching regulators"],
+ },
+ "Magnetics_MPP_60u": {
+ "name": "Magnetics MPP 60µ",
+ "family": "MPP",
+ "manufacturer": "Magnetics Inc",
+ "permeability": 60,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 52.3, "alpha": 1.15, "beta": 2.47},
+ "permeability_curve": {"a": 0.01, "b": 2.033e-07, "c": 2.436},
+ "frequency_range": (10e3, 500e3),
+ "applications": ["Flyback inductors", "Switching regulators"],
+ },
+ "CSC_MPP_125u": {
+ "name": "CSC MPP 125µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 125,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 17.8, "alpha": 1.35, "beta": 2.03},
+ "permeability_curve": {"a": 0.01, "b": 4.07e-07, "c": 2.523},
+ "frequency_range": (10e3, 300e3),
+ "applications": ["Output filters", "EMI filters"],
+ },
+ "CSC_MPP_147u": {
+ "name": "CSC MPP 147µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 147,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 13.7, "alpha": 1.38, "beta": 2.03},
+ "permeability_curve": {"a": 0.01, "b": 7.58e-07, "c": 2.471},
+ "frequency_range": (10e3, 300e3),
+ "applications": ["Output filters", "Noise filters"],
+ },
+ "CSC_MPP_160u": {
+ "name": "CSC MPP 160µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 160,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 13.7, "alpha": 1.38, "beta": 2.03},
+ "permeability_curve": {"a": 0.01, "b": 8.20e-07, "c": 2.495},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["Line filters", "Smoothing chokes"],
+ },
+ "CSC_MPP_173u": {
+ "name": "CSC MPP 173µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 173,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 13.7, "alpha": 1.38, "beta": 2.03},
+ "permeability_curve": {"a": 0.01, "b": 1.03e-06, "c": 2.513},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["Line filters"],
+ },
+ "CSC_MPP_200u": {
+ "name": "CSC MPP 200µ",
+ "family": "MPP",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 200,
+ "saturation_T": 0.75,
+ "steinmetz": {"k": 14.7, "alpha": 1.37, "beta": 2.05},
+ "permeability_curve": {"a": 0.01, "b": 1.18e-06, "c": 2.524},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["Line filters", "60Hz transformers"],
+ },
+
+ # -------------------------------------------------------------------------
+ # HIGH FLUX - Best DC bias, moderate loss
+ # -------------------------------------------------------------------------
+ "Magnetics_High_Flux_125u": {
+ "name": "Magnetics High Flux 125µ",
+ "family": "High_Flux",
+ "manufacturer": "Magnetics Inc",
+ "permeability": 125,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 0.0475, "alpha": 1.585, "beta": 1.43},
+ "permeability_curve": {"a": 0.01, "b": 1.46e-06, "c": 2.108},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["DC chokes", "PFC inductors", "Buck/Boost inductors"],
+ "notes": "Highest saturation flux density among powder cores",
+ },
+ "CSC_High_Flux_26u": {
+ "name": "CSC High Flux 26µ",
+ "family": "High_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 26,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 52.3, "alpha": 1.09, "beta": 2.25},
+ "permeability_curve": {"a": 0.01, "b": 3.41e-08, "c": 2.087},
+ "frequency_range": (10e3, 300e3),
+ "applications": ["High current DC chokes", "Energy storage"],
+ },
+ "CSC_High_Flux_60u": {
+ "name": "CSC High Flux 60µ",
+ "family": "High_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 60,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 45.6, "alpha": 1.11, "beta": 2.28},
+ "permeability_curve": {"a": 0.01, "b": 5.42e-08, "c": 2.326},
+ "frequency_range": (10e3, 250e3),
+ "applications": ["PFC boost inductors", "Buck converters"],
+ },
+ "CSC_High_Flux_125u": {
+ "name": "CSC High Flux 125µ",
+ "family": "High_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 125,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 27.0, "alpha": 1.23, "beta": 2.17},
+ "permeability_curve": {"a": 0.01, "b": 2.09e-07, "c": 2.386},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["DC-DC converters", "Output chokes"],
+ },
+ "CSC_High_Flux_147u": {
+ "name": "CSC High Flux 147µ",
+ "family": "High_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 147,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 34.4, "alpha": 1.17, "beta": 2.10},
+ "permeability_curve": {"a": 0.01, "b": 1.16e-07, "c": 2.619},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["Smoothing inductors"],
+ },
+ "CSC_High_Flux_160u": {
+ "name": "CSC High Flux 160µ",
+ "family": "High_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 160,
+ "saturation_T": 1.5,
+ "steinmetz": {"k": 34.4, "alpha": 1.17, "beta": 2.10},
+ "permeability_curve": {"a": 0.01, "b": 2.50e-07, "c": 2.475},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["Line inductors"],
+ },
+
+ # -------------------------------------------------------------------------
+ # SENDUST (Kool Mu equivalent) - Good all-around, cost effective
+ # -------------------------------------------------------------------------
+ "CSC_Sendust_26u": {
+ "name": "CSC Sendust 26µ",
+ "family": "Sendust",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 26,
+ "saturation_T": 1.0,
+ "steinmetz": {"k": 53.4, "alpha": 1.10, "beta": 2.05},
+ "permeability_curve": {"a": 0.01, "b": 1.23e-06, "c": 1.697},
+ "frequency_range": (10e3, 500e3),
+ "applications": ["PFC inductors", "High frequency chokes"],
+ "notes": "Good balance of cost, loss, and DC bias capability",
+ },
+ "CSC_Sendust_60u": {
+ "name": "CSC Sendust 60µ",
+ "family": "Sendust",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 60,
+ "saturation_T": 1.0,
+ "steinmetz": {"k": 62.3, "alpha": 1.10, "beta": 2.21},
+ "permeability_curve": {"a": 0.01, "b": 2.75e-06, "c": 1.782},
+ "frequency_range": (10e3, 300e3),
+ "applications": ["PFC boost inductors", "Output filters"],
+ },
+ "CSC_Sendust_75u": {
+ "name": "CSC Sendust 75µ",
+ "family": "Sendust",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 75,
+ "saturation_T": 1.0,
+ "steinmetz": {"k": 62.3, "alpha": 1.10, "beta": 2.21},
+ "permeability_curve": {"a": 0.01, "b": 4.58e-06, "c": 1.755},
+ "frequency_range": (10e3, 250e3),
+ "applications": ["General purpose inductors"],
+ },
+ "CSC_Sendust_90u": {
+ "name": "CSC Sendust 90µ",
+ "family": "Sendust",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 90,
+ "saturation_T": 1.0,
+ "steinmetz": {"k": 62.3, "alpha": 1.10, "beta": 2.21},
+ "permeability_curve": {"a": 0.01, "b": 9.54e-06, "c": 1.676},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["Line filters", "Output chokes"],
+ },
+ "CSC_Sendust_125u": {
+ "name": "CSC Sendust 125µ",
+ "family": "Sendust",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 125,
+ "saturation_T": 1.0,
+ "steinmetz": {"k": 62.3, "alpha": 1.10, "beta": 2.21},
+ "permeability_curve": {"a": 0.01, "b": 2.41e-05, "c": 1.626},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["EMI filters", "Smoothing chokes"],
+ },
+
+ # -------------------------------------------------------------------------
+ # MEGA FLUX (XFlux equivalent) - Highest DC bias, lowest cost
+ # -------------------------------------------------------------------------
+ "CSC_Mega_Flux_26u": {
+ "name": "CSC Mega Flux 26µ",
+ "family": "Mega_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 26,
+ "saturation_T": 1.6,
+ "steinmetz": {"k": 117.0, "alpha": 1.10, "beta": 2.17},
+ "permeability_curve": {"a": 0.01, "b": 9.96e-08, "c": 1.883},
+ "frequency_range": (10e3, 200e3),
+ "applications": ["High DC bias chokes", "Extreme energy storage"],
+ "notes": "Highest saturation, best for extreme DC bias applications",
+ },
+ "CSC_Mega_Flux_50u": {
+ "name": "CSC Mega Flux 50µ",
+ "family": "Mega_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 50,
+ "saturation_T": 1.6,
+ "steinmetz": {"k": 108.0, "alpha": 1.10, "beta": 2.15},
+ "permeability_curve": {"a": 0.01, "b": 7.35e-08, "c": 2.177},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["DC-DC converters", "Solar inverters"],
+ },
+ "CSC_Mega_Flux_60u": {
+ "name": "CSC Mega Flux 60µ",
+ "family": "Mega_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 60,
+ "saturation_T": 1.6,
+ "steinmetz": {"k": 108.0, "alpha": 1.10, "beta": 2.15},
+ "permeability_curve": {"a": 0.01, "b": 3.30e-07, "c": 1.982},
+ "frequency_range": (10e3, 150e3),
+ "applications": ["EV charger inductors", "High power DC chokes"],
+ },
+ "CSC_Mega_Flux_75u": {
+ "name": "CSC Mega Flux 75µ",
+ "family": "Mega_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 75,
+ "saturation_T": 1.6,
+ "steinmetz": {"k": 108.0, "alpha": 1.10, "beta": 2.15},
+ "permeability_curve": {"a": 0.01, "b": 1.11e-06, "c": 1.841},
+ "frequency_range": (10e3, 120e3),
+ "applications": ["Automotive DC-DC", "Welding power supplies"],
+ },
+ "CSC_Mega_Flux_90u": {
+ "name": "CSC Mega Flux 90µ",
+ "family": "Mega_Flux",
+ "manufacturer": "CSC (Chang Sung)",
+ "permeability": 90,
+ "saturation_T": 1.6,
+ "steinmetz": {"k": 108.0, "alpha": 1.10, "beta": 2.15},
+ "permeability_curve": {"a": 0.01, "b": 2.01e-06, "c": 1.828},
+ "frequency_range": (10e3, 100e3),
+ "applications": ["Line reactors", "Heavy duty chokes"],
+ },
+
+ # -------------------------------------------------------------------------
+ # FERRITE MATERIALS (for comparison)
+ # -------------------------------------------------------------------------
+ "EPCOS_N97": {
+ "name": "EPCOS N97",
+ "family": "Ferrite",
+ "manufacturer": "TDK/EPCOS",
+ "permeability": 2300,
+ "saturation_T": 0.41,
+ "steinmetz": {"k": 9.76, "alpha": 1.72, "beta": 2.91},
+ "frequency_range": (25e3, 500e3),
+ "applications": ["Power transformers", "Flyback converters"],
+ "notes": "Low loss ferrite, good for 100-300kHz",
+ },
+ "TDK_PC95": {
+ "name": "TDK PC95",
+ "family": "Ferrite",
+ "manufacturer": "TDK",
+ "permeability": 3300,
+ "saturation_T": 0.41,
+ "steinmetz": {"k": 11.8, "alpha": 2.00, "beta": 2.64},
+ "frequency_range": (25e3, 500e3),
+ "applications": ["Power transformers", "High efficiency designs"],
+ "notes": "Lowest loss at 100kHz among standard ferrites",
+ },
+}
+
+
+def get_powder_core_material(name: str) -> dict:
+ """Get material parameters by name."""
+ return POWDER_CORE_MATERIALS.get(name, {})
+
+
+def get_materials_by_family(family: str) -> list[str]:
+ """Get all materials in a family (MPP, High_Flux, Sendust, Mega_Flux, Ferrite)."""
+ return [k for k, v in POWDER_CORE_MATERIALS.items() if v.get("family") == family]
+
+
+def suggest_powder_core_material(
+ dc_bias_amps: float,
+ frequency_hz: float,
+ priority: str = "balanced"
+) -> list[str]:
+ """
+ Suggest appropriate powder core materials based on application requirements.
+
+ Args:
+ dc_bias_amps: DC bias current (higher = need High Flux or Mega Flux)
+ frequency_hz: Operating frequency in Hz
+ priority: "low_loss", "high_bias", "cost", or "balanced"
+
+ Returns:
+ List of recommended material names, best first
+ """
+ suggestions = []
+
+ for name, mat in POWDER_CORE_MATERIALS.items():
+ if mat.get("family") == "Ferrite":
+ continue # Skip ferrites for powder core suggestion
+
+ freq_range = mat.get("frequency_range", (0, 1e6))
+ if not (freq_range[0] <= frequency_hz <= freq_range[1]):
+ continue
+
+ # Score based on priority
+ score = 0
+ family = mat.get("family", "")
+
+ if priority == "low_loss":
+ if family == "MPP":
+ score = 100
+ elif family == "High_Flux":
+ score = 60
+ elif family == "Sendust":
+ score = 70
+ elif family == "Mega_Flux":
+ score = 40
+ elif priority == "high_bias":
+ if family == "Mega_Flux":
+ score = 100
+ elif family == "High_Flux":
+ score = 90
+ elif family == "Sendust":
+ score = 60
+ elif family == "MPP":
+ score = 40
+ elif priority == "cost":
+ if family == "Mega_Flux":
+ score = 100
+ elif family == "Sendust":
+ score = 90
+ elif family == "High_Flux":
+ score = 60
+ elif family == "MPP":
+ score = 40
+ else: # balanced
+ if family == "High_Flux":
+ score = 85
+ elif family == "Sendust":
+ score = 80
+ elif family == "MPP":
+ score = 70
+ elif family == "Mega_Flux":
+ score = 75
+
+ # Adjust for DC bias - prefer lower permeability for high bias
+ perm = mat.get("permeability", 60)
+ if dc_bias_amps > 50:
+ if perm <= 60:
+ score += 20
+ elif dc_bias_amps > 20:
+ if perm <= 90:
+ score += 10
+
+ suggestions.append((name, score))
+
+ suggestions.sort(key=lambda x: -x[1])
+ return [name for name, score in suggestions[:5]]
+
+
+def calculate_core_loss(
+ material_name: str,
+ frequency_hz: float,
+ flux_density_t: float,
+ volume_m3: float
+) -> float:
+ """
+ Calculate core loss using Steinmetz equation.
+
+ Pv = k * f^alpha * B^beta (W/m³)
+
+ Args:
+ material_name: Material identifier from POWDER_CORE_MATERIALS
+ frequency_hz: Operating frequency in Hz
+ flux_density_t: Peak flux density in Tesla
+ volume_m3: Core volume in m³
+
+ Returns:
+ Core loss in Watts
+ """
+ mat = POWDER_CORE_MATERIALS.get(material_name)
+ if not mat:
+ raise ValueError(f"Unknown material: {material_name}")
+
+ steinmetz = mat.get("steinmetz", {})
+ k = steinmetz.get("k", 1.0)
+ alpha = steinmetz.get("alpha", 1.5)
+ beta = steinmetz.get("beta", 2.5)
+
+ # Steinmetz equation: Pv = k * f^alpha * B^beta
+ pv = k * (frequency_hz ** alpha) * (flux_density_t ** beta)
+ return pv * volume_m3
+
+
+# =============================================================================
+# DESIGN TRADEOFFS
+# =============================================================================
+
+TRADEOFFS = {
+ "core_size_vs_loss": {
+ "description": "Core size vs power loss tradeoff",
+ "explanation": """
+Smaller core → Higher flux density → More core loss → Hotter
+
+Larger core → Lower flux density → Less loss → Cooler but bigger
+
+The sweet spot depends on:
+- Cooling capability (natural convection vs forced air)
+- Efficiency requirements
+- Size constraints
+- Operating temperature
+
+Rule of thumb: Design for 150-200mT at worst case for ferrite.
+Higher (250-300mT) acceptable if good cooling available.
+ """,
+ },
+
+ "frequency_vs_size": {
+ "description": "Switching frequency vs magnetics size",
+ "explanation": """
+Higher frequency → Smaller magnetics BUT:
+
+- More switching loss in MOSFETs
+- More AC winding loss (skin effect, proximity effect)
+- More EMI to filter
+- Core material must support frequency
+
+Typical sweet spots:
+- 65-100kHz: Silicon MOSFETs, standard ferrite
+- 100-200kHz: High performance Si or SiC
+- 200-500kHz: GaN, low-loss ferrite (3F4, N49)
+- 500kHz-1MHz: GaN only, specialized cores
+ """,
+ },
+
+ "gap_vs_inductance": {
+ "description": "Air gap vs inductance and saturation",
+ "explanation": """
+More gap → Lower inductance factor (AL) BUT:
+- Higher saturation current (stores more energy)
+- More turns needed for same inductance
+- More fringing flux (EMI, eddy loss nearby)
+
+Less gap (or no gap):
+- Higher AL, fewer turns
+- Lower saturation current
+- Core material limits energy storage
+
+For energy storage (flyback, PFC): Gap is essential
+For transformers (LLC, forward): Minimal or no gap
+ """,
+ },
+
+ "wire_solid_vs_litz": {
+ "description": "Solid wire vs Litz wire selection",
+ "explanation": """
+Litz wire reduces AC resistance by mitigating skin/proximity effect.
+
+Use Litz when: skin depth < wire radius
+Skin depth = 66mm / sqrt(f_kHz) for copper
+
+At 100kHz: δ = 0.21mm → Litz helps for wire > 0.4mm
+At 500kHz: δ = 0.09mm → Litz helps for wire > 0.2mm
+
+Litz downsides:
+- Lower fill factor (30-40% vs 60%)
+- More expensive (3-5x)
+- Harder to terminate
+- Worse thermal conductivity
+
+Often better to use multiple paralleled smaller solid wires
+instead of expensive Litz.
+ """,
+ },
+
+ "turns_ratio_optimization": {
+ "description": "Flyback turns ratio selection",
+ "explanation": """
+Higher turns ratio (more primary turns per secondary):
+- Lower secondary current, smaller rectifier
+- Higher primary voltage stress
+- Better cross-regulation on multi-output
+
+Lower turns ratio:
+- Higher secondary current
+- Lower primary voltage stress
+- Lower leakage inductance
+
+Optimal ratio balances:
+- Duty cycle around 40-50% at low line
+- Primary voltage < MOSFET rating
+- Manageable secondary current
+ """,
+ },
+}
+
+
+def get_application_info(app_key: str) -> dict:
+ """Get detailed information about an application."""
+ return APPLICATIONS.get(app_key, {})
+
+
+def get_topology_info(topology: str) -> dict:
+ """Get information about a topology."""
+ return TOPOLOGIES.get(topology, {})
+
+
+def suggest_topology(power: float, isolated: bool = True, efficiency_priority: bool = False) -> str:
+ """Suggest appropriate topology based on requirements."""
+ if not isolated:
+ return "buck" if power < 100 else "interleaved_buck"
+
+ if power < 50:
+ return "flyback"
+ elif power < 150:
+ return "flyback" if not efficiency_priority else "active_clamp_flyback"
+ elif power < 300:
+ return "forward" if not efficiency_priority else "LLC"
+ else:
+ return "LLC" if efficiency_priority else "phase_shifted_full_bridge"
+
+
+def suggest_core_material(frequency: float, topology: str) -> list[str]:
+ """Suggest appropriate core materials."""
+ suggestions = []
+
+ if frequency < 100e3:
+ suggestions = ["3C90", "3C95", "N87"]
+ elif frequency < 300e3:
+ suggestions = ["3C95", "3C97", "N95", "N97"]
+ elif frequency < 1e6:
+ suggestions = ["3F4", "N49", "PC200"]
+ else:
+ suggestions = ["N49", "4F1"]
+
+ return suggestions
diff --git a/.gitignore b/.gitignore
index 445b628..ee2e725 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,12 @@
build/
dist/
venv/
+docs/_build/
+site/
+.claude/
*.pyc
-__pycache__/
\ No newline at end of file
+__pycache__/
+tmp/
+examples/_output/
+archive/
diff --git a/PRD.md b/PRD.md
new file mode 100644
index 0000000..80bddfa
--- /dev/null
+++ b/PRD.md
@@ -0,0 +1,705 @@
+# Product Requirements Document (PRD)
+# PyOpenMagnetics
+
+**Version**: 2.1
+**Date**: January 2025
+**Status**: Active Development
+
+---
+
+## Executive Summary
+
+PyOpenMagnetics transforms magnetic component design from a specialized art requiring decades of experience into an accessible, automated process. By wrapping the powerful MKF C++ engine with an intuitive Python API, we enable hardware engineers to design transformers and inductors in seconds rather than hours.
+
+### Vision Statement
+> Make magnetic design as easy as selecting a capacitor from a catalog.
+
+### Target Users
+1. **Hardware Engineers** - Power supply designers who need quick, reliable magnetics
+2. **Students** - Learning power electronics without magnetics expertise
+3. **AI Assistants** - Claude/GPT providing design recommendations via MCP
+4. **Researchers** - Exploring design spaces with optimization algorithms
+
+---
+
+## Problem Statement
+
+### Current Pain Points
+
+| Problem | Impact | Frequency |
+|---------|--------|-----------|
+| MAS JSON complexity | 50+ lines for simple design | Every design |
+| Manual waveform calculation | Error-prone, time-consuming | Every design |
+| Trial-and-error core selection | Days of iteration | 80% of projects |
+| No automated optimization | Sub-optimal designs ship | 60% of designs |
+| Disconnected tools | Copy-paste between tools | Daily |
+
+### The Gap
+
+```
+What engineers know: What MAS needs:
+───────────────────── ────────────────────────────
+Vin: 85-265VAC → {"operatingPoints": [{
+Vout: 12V @ 5A "excitationsPerWinding": [{
+fsw: 100kHz "current": {"waveform": {
+ "data": [0, 1.2, 0.8, ...],
+ "time": [0, 2.5e-6, ...]
+ }},
+ "voltage": {...}
+ }]
+ }]}
+```
+
+**PyOpenMagnetics bridges this gap.**
+
+---
+
+## Product Goals
+
+### Primary Goals (P0)
+1. **Reduce design time** from hours to seconds
+2. **Lower barrier to entry** for magnetic design
+3. **Improve design quality** through automated optimization
+4. **Enable AI integration** via MCP server
+
+### Secondary Goals (P1)
+1. Create industry-standard example library
+2. Bridge to FEM tools (FEMMT) for validation
+3. Support advanced topologies (LLC, DAB)
+4. Enable multi-objective optimization
+
+### Nice-to-have (P2)
+1. Natural language design input
+2. ML-accelerated design space exploration
+3. Integration with PCB tools
+4. Component sourcing integration
+
+---
+
+## Feature Specifications
+
+### Skill 1: Fluent Design API
+
+**Priority**: P0
+**Status**: ✅ Implemented (Sprint 1-3)
+
+#### Description
+Python API that accepts familiar engineering parameters and generates MAS internally.
+
+#### User Story
+> As a hardware engineer, I want to specify a power supply in terms I understand (Vin, Vout, Power, frequency) so that I can get magnetic designs without learning MAS JSON.
+
+#### API Specification
+```python
+from api.design import Design
+
+# Fluent builder pattern
+result = (Design.flyback()
+ .vin_ac(85, 265) # AC input range
+ .output(12, 5) # 12V @ 5A
+ .output(5, 0.5) # Auxiliary output
+ .fsw(100e3) # 100kHz
+ .efficiency(0.88) # Target efficiency
+ .max_height(15) # Max height in mm
+ .prefer("efficiency") # Optimization priority
+ .solve(max_results=5)) # Get top 5 designs
+
+# Result contains:
+# - Core shape and material
+# - Winding specification
+# - Loss breakdown
+# - Temperature rise
+# - Bill of materials
+```
+
+#### Supported Topologies
+| Topology | Builder | Status |
+|----------|---------|--------|
+| Flyback | `Design.flyback()` | ✅ Complete |
+| Buck | `Design.buck()` | ✅ Complete |
+| Boost | `Design.boost()` | ✅ Complete |
+| Forward | `Design.forward()` | ✅ Complete |
+| LLC | `Design.llc()` | ✅ Complete |
+| Inductor | `Design.inductor()` | ✅ Complete |
+| DAB/CLLC | `Design.dab()` | 🚧 Planned |
+| PFC | `Design.pfc()` | 🚧 Planned |
+
+#### Acceptance Criteria
+- [ ] All topologies produce valid MAS JSON
+- [ ] Design time < 5 seconds for simple designs
+- [ ] Results include actionable BOM
+- [ ] Error messages are helpful, not cryptic
+
+---
+
+### Skill 2: MCP Server for AI
+
+**Priority**: P0
+**Status**: ✅ Implemented (Sprint 2)
+
+#### Description
+Model Context Protocol server enabling AI assistants to design magnetics.
+
+#### User Story
+> As an AI assistant user, I want to ask Claude to design a transformer so that I can get a complete design recommendation in conversation.
+
+#### Tools
+| Tool | Description |
+|------|-------------|
+| `design_power_supply_magnetic` | Design transformer/inductor |
+| `analyze_existing_design` | Analyze given component |
+| `compare_materials` | Compare core materials |
+| `suggest_wire` | Recommend wire for current/freq |
+
+#### Resources
+| Resource | Description |
+|----------|-------------|
+| `database/cores/{family}` | List cores in family |
+| `database/materials/{type}` | List materials by type |
+
+#### Example Conversation
+```
+User: Design a transformer for a 65W USB-C charger.
+ Universal AC input, 20V/3.25A output, 100kHz.
+
+AI: [Calls design_power_supply_magnetic]
+
+ For your 65W USB-C charger, I recommend:
+
+ **EFD 25 + 3C95**
+ - Primary: 56T AWG 28
+ - Secondary: 7T AWG 22 x3 parallel
+ - Gap: 0.35mm
+ - Losses: 1.2W (core 0.7W + copper 0.5W)
+ - Temperature rise: ~35°C
+
+ Design notes:
+ - Consider interleaved winding for lower leakage
+ - Margin tape required for safety isolation
+```
+
+---
+
+### Skill 3: Streamlit GUI
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 4)
+
+#### Description
+Web-based GUI for engineers who prefer visual interfaces.
+
+#### Pages
+1. **Design** - Topology selection, parameter input, results
+2. **Database** - Browse cores, materials, wires
+3. **Analysis** - Analyze existing designs
+4. **Compare** - Side-by-side design comparison
+
+#### Launch
+```bash
+streamlit run api/gui/app.py
+```
+
+---
+
+### Skill 4: Example Library
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 3, 8)
+
+#### Categories
+| Category | Examples | Status |
+|----------|----------|--------|
+| Consumer | USB PD 20W/65W/140W, Laptop adapters | ✅ |
+| Automotive | 48V DC-DC, Gate drivers, EV boost | ✅ |
+| Industrial | DIN rail, Medical, VFD chokes | ✅ |
+| Telecom | PoE, Rectifiers | ✅ |
+| Advanced | NSGA2 optimization, Custom simulation | ✅ |
+
+#### Example Format
+Each example includes:
+- Real-world context and equivalents
+- Complete working code
+- Expected output documentation
+- Design notes and warnings
+
+---
+
+### Skill 5: FEMMT Bridge
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 5)
+
+#### Description
+Export PyOpenMagnetics designs to FEMMT for FEM validation.
+
+```python
+from api.bridges.femmt import export_to_femmt
+
+femmt_script = export_to_femmt(design_result)
+# Generates executable Python script for FEMMT
+```
+
+---
+
+### Skill 6: SW Architect Tools
+
+**Priority**: P2
+**Status**: ✅ Implemented (Sprint 6)
+
+#### Description
+Code analysis tools for maintaining the codebase.
+
+- Module analyzer
+- Pattern documentation
+- API docs generator
+
+---
+
+### Skill 7: Magnetic Expert Knowledge
+
+**Priority**: P1
+**Status**: Archived (moved to `.archive/`)
+
+#### Description
+Domain knowledge base - archived as functionality integrated into Design API and examples.
+
+---
+
+### Skill 8: Legacy Migration
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 8)
+
+#### Description
+Upgrade legacy MATLAB/notebook code to fluent API.
+
+- Created boost waveform calculator
+- Ported NSGA2 optimization example
+- Archived MATLAB legacy code
+
+---
+
+### Skill 9: Data Models
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 9)
+
+#### Description
+Strongly-typed dataclass models for topologies, electrical specifications, and operating points.
+
+#### User Story
+> As an engineer, I want structured data types for my design specs so that I get IDE autocomplete, type checking, and clear documentation of all parameters.
+
+#### Modules
+
+**`api/models/topology.py`** - PWM and Resonant Topologies
+```python
+from api.models import BuckTopology, FlybackTopology, LLCTopology, OperatingMode
+
+# PWM topology with all parameters exposed
+buck = BuckTopology(
+ fsw_hz=500e3,
+ mode=OperatingMode.CCM,
+ sync_rectification=True
+)
+
+# Isolated flyback with clamp configuration
+flyback = FlybackTopology(
+ fsw_hz=100e3,
+ turns_ratio=8.0,
+ clamp_type="rcd",
+ max_duty=0.45
+)
+
+# LLC resonant with Q factor and gain range
+llc = LLCTopology(
+ f_res_hz=100e3,
+ fsw_min_hz=80e3,
+ fsw_max_hz=150e3,
+ Q=0.3,
+ k=6.0,
+ gain_min=0.9,
+ gain_max=1.1
+)
+```
+
+**`api/models/specs.py`** - Electrical Specifications
+```python
+from api.models import VoltageSpec, CurrentSpec, PowerSupplySpec, PortSpec
+
+# DC voltage with tolerance
+v_out = VoltageSpec.dc(3.3, tolerance_pct=3) # 3.3V ± 3%
+
+# DC voltage range
+v_in = VoltageSpec.dc_range(10, 14) # 10V to 14V
+
+# AC voltage with RMS and peak calculated
+v_ac = VoltageSpec.ac(230, v_min_rms=207, v_max_rms=253)
+
+# Current with ripple (RMS auto-calculated)
+i_out = CurrentSpec.dc(10, ripple_pp=3) # 10A DC, 3A ripple
+
+# Complete power supply specification
+psu = PowerSupplySpec(
+ name="12V to 3.3V Buck",
+ inputs=[PortSpec("Input", v_in, CurrentSpec.dc(3.5))],
+ outputs=[PortSpec("Output", v_out, i_out)],
+ efficiency=0.95,
+ isolation_v=None # Non-isolated
+)
+
+print(f"Output power: {psu.total_output_power}W")
+print(f"Input power: {psu.total_input_power}W")
+```
+
+**`api/models/operating_point.py`** - Waveforms and Operating Points
+```python
+from api.models import Waveform, WindingExcitation, OperatingPointModel
+
+# Create waveforms
+triangular = Waveform.triangular(v_min=0, v_max=10, duty=0.5, period=1e-5)
+rectangular = Waveform.rectangular(v_high=12, v_low=0, duty=0.4, period=1e-5)
+sinusoidal = Waveform.sinusoidal(amplitude=1.5, frequency=100e3)
+
+# Define operating point
+op = OperatingPointModel(
+ name="Full Load",
+ excitations=[
+ WindingExcitation(
+ name="Primary",
+ current=triangular,
+ frequency_hz=100e3
+ )
+ ],
+ ambient_temperature_c=40
+)
+
+# Convert to MAS format
+mas_dict = op.to_mas()
+```
+
+#### Topology Classes
+| Class | Description | Key Parameters |
+|-------|-------------|----------------|
+| `BuckTopology` | Step-down converter | `fsw_hz`, `duty_cycle`, `sync_rectification` |
+| `BoostTopology` | Step-up converter | `fsw_hz`, `duty_cycle` |
+| `BuckBoostTopology` | Inverting converter | `fsw_hz`, `duty_cycle` |
+| `FlybackTopology` | Isolated flyback | `turns_ratio`, `clamp_type`, `max_duty` |
+| `ForwardTopology` | Isolated forward | `variant`, `reset_method`, `turns_ratio` |
+| `PushPullTopology` | Push-pull | `turns_ratio`, `max_duty` |
+| `FullBridgeTopology` | Full-bridge | `phase_shift`, `turns_ratio` |
+| `LLCTopology` | LLC resonant | `f_res_hz`, `Lm_h`, `Lr_h`, `Q`, `k` |
+| `LCCTopology` | LCC resonant | `Lr_h`, `Cs_f`, `Cp_f` |
+
+#### Specification Classes
+| Class | Description | Factory Methods |
+|-------|-------------|-----------------|
+| `VoltageSpec` | Complete voltage spec | `.dc()`, `.dc_range()`, `.ac()` |
+| `CurrentSpec` | Complete current spec | `.dc()` |
+| `PowerSpec` | Power specification | `.from_vi()` |
+| `PortSpec` | Input/output port | - |
+| `PowerSupplySpec` | Complete PSU spec | - |
+
+---
+
+### Skill 10: Design Reports
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 9)
+
+#### Description
+Comprehensive visual design reports with Pareto analysis, loss breakdowns, and multi-objective comparisons.
+
+#### User Story
+> As an engineer reviewing designs, I want visual reports showing trade-offs between volume, loss, and efficiency so that I can make informed decisions about which design to use.
+
+#### Report Contents
+When `output_dir` is specified in `solve()`, the following reports are generated:
+
+| File | Description |
+|------|-------------|
+| `design_report.png` | Main 2x3 dashboard with all visualizations |
+| `pareto_detailed.png` | Core loss vs copper loss with composition analysis |
+| `volume_loss_pareto.png` | **NEW** Volume vs total loss trade-off with Pareto frontier |
+| `parallel_coordinates.png` | Multi-dimensional design comparison |
+| `heatmap.png` | Design characteristics heatmap |
+| `report_summary.json` | Machine-readable summary with all data |
+| `results.json` | Raw design results |
+
+#### Volume vs Loss Pareto Plot
+The new Volume/Loss Pareto plot shows:
+- Scatter plot of Volume (cm³) vs Total Loss (W)
+- Pareto-optimal frontier highlighted
+- Loss density (W/cm³) bar chart
+- Color-coded designs (best, top 3, Pareto-optimal)
+
+```python
+# Generate reports with volume analysis
+results = Design.flyback() \
+ .vin_ac(85, 265) \
+ .output(12, 5) \
+ .fsw(100e3) \
+ .solve(verbose=True, output_dir="output/my_design")
+
+# Reports include volume_loss_pareto.png
+```
+
+#### Data Extracted for Reports
+| Field | Description | Unit |
+|-------|-------------|------|
+| `height_mm` | Component height | mm |
+| `width_mm` | Component width | mm |
+| `depth_mm` | Component depth | mm |
+| `volume_cm3` | Calculated volume (H×W×D/1000) | cm³ |
+| `weight_g` | Component weight | g |
+| `core_loss_w` | Core losses | W |
+| `copper_loss_w` | Winding losses | W |
+| `total_loss_w` | Total losses | W |
+| `temp_rise_c` | Temperature rise | K |
+
+---
+
+### Skill 11: Progress Feedback
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 9)
+
+#### Description
+Animated progress spinner during long optimization operations to provide user feedback.
+
+#### User Story
+> As an engineer running optimizations, I want visual feedback during long operations so that I know the program is working and hasn't frozen.
+
+#### Implementation
+When `verbose=True`, the `solve()` method displays:
+1. Timestamp for input processing completion
+2. Animated Unicode spinner during optimization
+3. Spinner message showing max results and core mode
+4. Final completion time
+
+```python
+# Example output with progress feedback
+results = Design.flyback() \
+ .vin_ac(85, 265) \
+ .output(12, 5) \
+ .solve(verbose=True)
+
+# Console output:
+# [14:23:45] Processing inputs...
+# [14:23:45] Input processing: 0.15s
+# [14:23:45] Starting design optimization...
+# ⠹ Evaluating designs (max 30, available cores)...
+# [14:24:12] Optimization complete: 27.34s
+# [14:24:12] Found 8 designs in 27.49s total
+```
+
+#### Spinner Animation
+Uses Unicode Braille pattern characters for smooth animation:
+`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`
+
+---
+
+## Multi-Objective Optimization Module
+
+**Priority**: P1
+**Status**: ✅ Implemented (Sprint 8)
+
+#### Description
+NSGA-II optimization for Pareto-optimal designs.
+
+```python
+from api.optimization import NSGAOptimizer
+
+optimizer = NSGAOptimizer(
+ objectives=["mass", "total_loss"],
+ constraints={"inductance": (100e-6, 140e-6)}
+)
+optimizer.add_variable("turns", range=(20, 60))
+optimizer.add_variable("core_size", range=(0.5, 2.0))
+
+pareto_front = optimizer.run(generations=50)
+```
+
+---
+
+## Version Tiers (FREE vs PRO)
+
+### Rationale
+Advanced features require significant R&D investment. Tiering enables:
+1. Wide adoption via free core features
+2. Sustainable development via PRO revenue
+3. Clear upgrade path for serious users
+
+### Feature Matrix
+
+| Feature | FREE | PRO |
+|---------|:----:|:---:|
+| **Design API** | | |
+| Buck converter | ✅ | ✅ |
+| Boost converter | ✅ | ✅ |
+| Flyback (basic) | ✅ | ✅ |
+| Forward converter | ❌ | ✅ |
+| LLC resonant | ❌ | ✅ |
+| DAB/CLLC | ❌ | ✅ |
+| **Database** | | |
+| Core shapes | ✅ | ✅ |
+| Ferrite materials | ✅ | ✅ |
+| Powder cores (basic) | ✅ | ✅ |
+| Powder cores (full 25+) | ❌ | ✅ |
+| Nanocrystalline | ❌ | ✅ |
+| **Optimization** | | |
+| Design adviser | ✅ | ✅ |
+| Multi-objective (NSGA-II) | ❌ | ✅ |
+| Pareto visualization | ❌ | ✅ |
+| **Interfaces** | | |
+| Python API | ✅ | ✅ |
+| MCP Server | ❌ | ✅ |
+| Streamlit GUI | ❌ | ✅ |
+| **Integrations** | | |
+| FEMMT export | ❌ | ✅ |
+| LTspice export | ✅ | ✅ |
+| **Knowledge** | | |
+| Basic examples | ✅ | ✅ |
+| Expert knowledge base | ❌ | ✅ |
+| Application guides | ❌ | ✅ |
+| **Support** | | |
+| GitHub Issues | ✅ | ✅ |
+| Priority support | ❌ | ✅ |
+| Custom development | ❌ | ✅ |
+
+### Implementation
+```python
+# api/__init__.py
+import os
+
+PYOM_LICENSE = os.getenv("PYOPENMAGNETICS_LICENSE", "FREE")
+
+def require_pro(feature_name):
+ if PYOM_LICENSE != "PRO":
+ raise LicenseError(
+ f"'{feature_name}' requires PyOpenMagnetics PRO. "
+ f"Visit https://openmagnetics.com/pro for licensing."
+ )
+
+# In optimization.py
+def NSGAOptimizer(...):
+ require_pro("Multi-objective optimization")
+ ...
+```
+
+---
+
+## Technical Requirements
+
+### Performance
+| Metric | Target | Current |
+|--------|--------|---------|
+| Simple design time | < 5s | ~2s |
+| Complex design time | < 30s | ~15s |
+| Memory usage | < 500MB | ~200MB |
+| Startup time | < 2s | ~1s |
+
+### Compatibility
+- Python 3.10, 3.11, 3.12
+- Ubuntu 22.04+, Windows 10+, macOS 12+
+- x86_64, arm64
+
+### Dependencies
+- Core: numpy, pybind11
+- Optional: pymoo (optimization), streamlit (GUI), mcp (AI)
+
+---
+
+## Quality Assurance
+
+### Testing Strategy
+1. **Unit tests** - All API functions
+2. **Integration tests** - End-to-end workflows
+3. **Example validation** - All examples run successfully
+4. **Regression tests** - Design results within tolerance
+
+### Test Coverage Target
+- Core API: 90%
+- Design builders: 85%
+- Expert knowledge: 80%
+
+### Pre-commit Checks
+```bash
+./scripts/pre_commit_check.sh
+# Runs: syntax check, pytest, example validation
+```
+
+---
+
+## Roadmap
+
+### Q1 2025 (Current)
+- [x] Fluent Design API
+- [x] MCP Server
+- [x] Streamlit GUI
+- [x] Example library
+- [x] FEMMT bridge
+- [x] Expert knowledge base
+- [x] NSGA-II optimization
+- [x] Legacy migration
+- [x] Data Models (Topology, Specs, Operating Points)
+- [x] Design Reports with Volume/Loss Pareto
+- [x] Progress Feedback (animated spinner)
+
+### Q2 2025
+- [ ] DAB/CLLC topology
+- [ ] PFC boost builder
+- [ ] Thermal modeling
+- [ ] Component sourcing API
+
+### Q3 2025
+- [ ] ML surrogate models
+- [ ] Natural language input
+- [ ] PCB integration
+- [ ] Mobile-friendly GUI
+
+### Q4 2025
+- [ ] Cloud deployment
+- [ ] Enterprise features
+- [ ] API metering
+- [ ] Custom material fitting
+
+---
+
+## Success Metrics
+
+| Metric | Target | Measurement |
+|--------|--------|-------------|
+| Design time reduction | 10x | User study |
+| Adoption (downloads) | 10K/month | PyPI stats |
+| Active users | 1K/month | Telemetry |
+| PRO conversion | 5% | Sales |
+| GitHub stars | 1K | GitHub |
+| Issue resolution | < 7 days | GitHub |
+
+---
+
+## Risks and Mitigations
+
+| Risk | Impact | Probability | Mitigation |
+|------|--------|-------------|------------|
+| MKF API changes | High | Low | Pin MKF version, adapter layer |
+| Competition | Medium | Medium | Focus on UX, examples |
+| Performance issues | Medium | Low | Profiling, caching |
+| License confusion | Low | Medium | Clear documentation |
+
+---
+
+## Appendix
+
+### Glossary
+- **MAS**: Magnetic Agnostic Structure - JSON schema for magnetic components
+- **MKF**: Magnetics Knowledge Foundation - C++ simulation engine
+- **MCP**: Model Context Protocol - AI tool integration standard
+- **FEMMT**: FEM Magnetics Toolbox - FEM simulation tool
+
+### References
+- [MAS Schema](https://github.com/OpenMagnetics/MAS)
+- [MKF Engine](https://github.com/OpenMagnetics/MKF)
+- [FEMMT](https://github.com/upb-lea/FEM_Magnetics_Toolbox)
+- [MCP Specification](https://modelcontextprotocol.io)
diff --git a/PyOpenMagnetics.cpython-313-x86_64-linux-gnu.so b/PyOpenMagnetics.cpython-313-x86_64-linux-gnu.so
new file mode 100644
index 0000000..a8e8c42
Binary files /dev/null and b/PyOpenMagnetics.cpython-313-x86_64-linux-gnu.so differ
diff --git a/README.md b/README.md
index 7504863..02cea48 100644
--- a/README.md
+++ b/README.md
@@ -1,356 +1,328 @@
-# PyOpenMagnetics - Python Wrapper for OpenMagnetics
+# PyOpenMagnetics
-[](https://www.python.org/downloads/)
+[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/MIT)
+[]()
-**PyOpenMagnetics** is a Python wrapper for [MKF (Magnetics Knowledge Foundation)](https://github.com/OpenMagnetics/MKF), the simulation engine of [OpenMagnetics](https://github.com/OpenMagnetics), providing a comprehensive toolkit for designing and analyzing magnetic components such as transformers and inductors.
+**PyOpenMagnetics** is a Python wrapper for [MKF (Magnetics Knowledge Foundation)](https://github.com/OpenMagnetics/MKF), providing a comprehensive toolkit for designing magnetic components (transformers, inductors, chokes) for power electronics.
+
+## Why PyOpenMagnetics?
+
+| Traditional Approach | PyOpenMagnetics |
+|---------------------|-----------------|
+| 50+ lines of JSON configuration | `Design.flyback().vin_ac(85,265).output(12,5).solve()` |
+| Manual waveform calculations | Automatic waveform synthesis |
+| Trial-and-error core selection | AI-powered recommendations |
+| Separate tools for each topology | Unified API for all converters |
## Features
-- 🧲 **Core Database**: Access to extensive database of core shapes, materials, and manufacturers
-- 🔌 **Winding Design**: Automatic winding calculations with support for various wire types (round, litz, rectangular, planar)
-- 📊 **Loss Calculations**: Core losses (Steinmetz), winding losses (DC, skin effect, proximity effect)
-- 🎯 **Design Adviser**: Automated recommendations for optimal magnetic designs
-- 📈 **Signal Processing**: Harmonic analysis, waveform processing
-- 🖼️ **Visualization**: SVG plotting of cores, windings, magnetic fields
-- 🔧 **SPICE Export**: Export magnetic components as SPICE subcircuits
+### Core Engine
+- **Database Access**: 500+ core shapes, 100+ materials, extensive wire library
+- **Loss Models**: Steinmetz, IGSE, MSE, BARG, ROSHEN, ALBACH for core losses
+- **Winding Analysis**: DC, skin effect, proximity effect losses
+- **Design Adviser**: Automated optimal magnetic design recommendations
+- **Visualization**: SVG plotting of cores, windings, magnetic fields
-## Installation
+### Fluent Design API (NEW)
+```python
+from api.design import Design
+
+# Design a 65W USB-C charger transformer
+results = (Design.flyback()
+ .vin_ac(85, 265) # Universal AC input
+ .output(20, 3.25) # 20V @ 3.25A
+ .fsw(100e3) # 100kHz switching
+ .prefer("efficiency")
+ .solve())
+
+print(f"Best: {results[0].core} + {results[0].material}")
+print(f"Turns: {results[0].primary_turns}T / {results[0].secondary_turns}T")
+print(f"Losses: {results[0].total_loss_w:.2f}W")
+```
+
+### Supported Topologies
+| Isolated | Non-Isolated | Magnetics |
+|----------|--------------|-----------|
+| Flyback | Buck | Inductor |
+| Forward | Boost | CM Choke |
+| LLC | Buck-Boost | Current Transformer |
+| DAB/CLLC | SEPIC | Gate Drive Transformer |
+
+### Multi-Objective Optimization (NEW)
+```python
+from api.optimization import NSGAOptimizer
+
+optimizer = NSGAOptimizer(
+ objectives=["mass", "total_loss"],
+ constraints={"inductance": (100e-6, 140e-6)}
+)
+optimizer.add_variable("turns", range=(20, 60))
+optimizer.add_variable("core_size", range=(0.5, 2.0))
+
+pareto_front = optimizer.run(generations=50)
+# Returns Pareto-optimal designs trading off mass vs efficiency
+```
-### From PyPI (recommended)
+## Installation
+### From PyPI
```bash
pip install PyOpenMagnetics
```
### From Source
-
```bash
-git clone https://github.com/OpenMagnetics/PyOpenMagnetics.git
-cd PyOpenMagnetics
+git clone https://github.com/OpenMagnetics/PyMKF.git
+cd PyMKF
pip install .
```
-## Quick Start
+### Development Install
+```bash
+pip install -e ".[dev]"
+```
-### Basic Example: Creating a Core
+**Build Requirements**: CMake 3.15+, C++23 compiler, Node.js 18+
-```python
-import PyOpenMagnetics
+## Quick Start
-# Find a core shape by name
-shape = PyOpenMagnetics.find_core_shape_by_name("E 42/21/15")
+### 1. Simple Inductor Design
+```python
+from api.design import Design
-# Find a core material by name
-material = PyOpenMagnetics.find_core_material_by_name("3C95")
+# 100µH inductor for buck converter
+design = Design.buck().vin(12, 24).vout(5).iout(3).fsw(500e3)
+results = design.solve()
-# Create a core with gapping
-core_data = {
- "functionalDescription": {
- "shape": shape,
- "material": material,
- "gapping": [{"type": "subtractive", "length": 0.001}], # 1mm gap
- "numberStacks": 1
- }
-}
+for r in results[:3]:
+ print(f"{r.core}: {r.primary_turns}T, {r.total_loss_w:.2f}W loss")
+```
-# Calculate complete core data
-core = PyOpenMagnetics.calculate_core_data(core_data, False)
-print(f"Effective area: {core['processedDescription']['effectiveParameters']['effectiveArea']} m²")
+### 2. Flyback Transformer
+```python
+# Multi-output flyback for USB charger
+design = (Design.flyback()
+ .vin_ac(85, 265)
+ .output(12, 2) # 12V @ 2A
+ .output(5, 0.5) # 5V @ 0.5A (auxiliary)
+ .fsw(100e3)
+ .isolation("reinforced")
+ .solve())
```
-### Design Adviser: Get Magnetic Recommendations
+### 3. Boost Inductor for EV Charger
+```python
+# 10kW boost stage
+design = (Design.boost()
+ .vin(200, 450)
+ .vout(800)
+ .pout(10000)
+ .fsw(100e3)
+ .ambient_temperature(70)
+ .solve())
+```
+### 4. Using the Low-Level API
```python
import PyOpenMagnetics
-# Define design requirements
+# Direct MAS JSON usage for advanced users
inputs = {
"designRequirements": {
- "magnetizingInductance": {
- "minimum": 100e-6, # 100 µH minimum
- "nominal": 110e-6 # 110 µH nominal
- },
- "turnsRatios": [{"nominal": 5.0}] # 5:1 turns ratio
+ "magnetizingInductance": {"nominal": 100e-6}
},
- "operatingPoints": [
- {
- "name": "Nominal",
- "conditions": {"ambientTemperature": 25},
- "excitationsPerWinding": [
- {
- "name": "Primary",
- "frequency": 100000, # 100 kHz
- "current": {
- "waveform": {
- "data": [0, 1.0, 0],
- "time": [0, 5e-6, 10e-6]
- }
- },
- "voltage": {
- "waveform": {
- "data": [50, 50, -50, -50],
- "time": [0, 5e-6, 5e-6, 10e-6]
- }
- }
- }
- ]
- }
- ]
+ "operatingPoints": [...]
}
-# Process inputs (adds harmonics and validation)
-processed_inputs = PyOpenMagnetics.process_inputs(inputs)
-
-# Get magnetic recommendations
-# core_mode: "available cores" (stock cores) or "standard cores" (all standard shapes)
-magnetics = PyOpenMagnetics.calculate_advised_magnetics(processed_inputs, 5, "standard cores")
-
-for i, mag in enumerate(magnetics):
- if "magnetic" in mag:
- core = mag["magnetic"]["core"]["functionalDescription"]
- print(f"{i+1}. {core['shape']['name']} - {core['material']['name']}")
+processed = PyOpenMagnetics.process_inputs(inputs)
+results = PyOpenMagnetics.calculate_advised_magnetics(processed, 5, "standard cores")
```
-### Calculate Core Losses
+## Project Structure
-```python
-import PyOpenMagnetics
-
-# Define core and operating point
-core_data = {...} # Your core definition
-operating_point = {
- "name": "Nominal",
- "conditions": {"ambientTemperature": 25},
- "excitationsPerWinding": [
- {
- "frequency": 100000,
- "magneticFluxDensity": {
- "processed": {
- "peakToPeak": 0.2, # 200 mT peak-to-peak
- "offset": 0
- }
- }
- }
- ]
-}
-
-losses = PyOpenMagnetics.calculate_core_losses(core_data, operating_point, "IGSE")
-print(f"Core losses: {losses['coreLosses']} W")
```
-
-### Winding a Coil
-
-```python
-import PyOpenMagnetics
-
-# Define coil requirements
-coil_functional_description = [
- {
- "name": "Primary",
- "numberTurns": 50,
- "numberParallels": 1,
- "wire": "Round 0.5 - Grade 1"
- },
- {
- "name": "Secondary",
- "numberTurns": 10,
- "numberParallels": 3,
- "wire": "Round 1.0 - Grade 1"
- }
-]
-
-# Wind the coil on the core
-result = PyOpenMagnetics.wind(core_data, coil_functional_description, bobbin_data, [1, 1], [])
-print(f"Winding successful: {result.get('windingResult', 'unknown')}")
+PyMKF/
+├── api/ # Python API layer
+│ ├── design.py # Fluent Design API
+│ ├── mas.py # MAS waveform generators
+│ ├── optimization.py # NSGA-II optimizer
+│ ├── results.py # Result formatting
+│ ├── expert/ # Domain knowledge
+│ │ ├── knowledge.py # Materials, applications, tradeoffs
+│ │ ├── examples.py # Example generator
+│ │ └── conversation.py # Interactive design guide
+│ ├── mcp/ # MCP server for AI assistants
+│ ├── gui/ # Streamlit GUI
+│ ├── bridges/ # External tool bridges
+│ │ └── femmt.py # FEMMT FEM export
+│ └── architect/ # Code analysis tools
+├── examples/ # Real-world design examples
+│ ├── consumer/ # USB chargers, laptops
+│ ├── automotive/ # EV, 48V systems
+│ ├── industrial/ # DIN rail, medical, VFD
+│ ├── telecom/ # PoE, rectifiers
+│ └── advanced/ # Optimization, custom simulation
+├── src/ # C++ pybind11 bindings
+├── tests/ # Pytest test suite
+└── archive/ # Legacy code reference
```
-## Flyback Converter Wizard
-
-PyOpenMagnetics includes a complete flyback converter design wizard. See `flyback.py` for a full example:
+## Examples
-```python
-from flyback import design_flyback, create_mas_inputs, get_advised_magnetics
-
-# Define flyback specifications
-specs = {
- "input_voltage_min": 90,
- "input_voltage_max": 375,
- "outputs": [{"voltage": 12, "current": 2, "diode_drop": 0.5}],
- "switching_frequency": 100000,
- "max_duty_cycle": 0.45,
- "efficiency": 0.85,
- "current_ripple_ratio": 0.4,
- "force_dcm": False,
- "safety_margin": 0.85,
- "ambient_temperature": 40,
- "max_drain_source_voltage": None,
-}
-
-# Calculate magnetic requirements
-design = design_flyback(specs)
-print(f"Required inductance: {design['min_inductance']*1e6:.1f} µH")
-print(f"Turns ratio: {design['turns_ratios'][0]:.2f}")
-
-# Create inputs for PyOpenMagnetics
-inputs = create_mas_inputs(specs, design)
+Run individual examples:
+```bash
+python examples/consumer/usb_pd_65w.py
+python examples/automotive/boost_half_bridge_multi_op.py
+python examples/industrial/boost_inductor_design.py
+python examples/advanced/nsga2_inductor_optimization.py
+```
-# Get recommended magnetics
-magnetics = get_advised_magnetics(inputs, max_results=5)
+Run all examples:
+```bash
+./scripts/run_examples.sh
```
-## API Reference
+## Testing
-### Database Access
+```bash
+# Run all tests
+pytest tests/ -v
-| Function | Description |
-|----------|-------------|
-| `get_core_materials()` | Get all available core materials |
-| `get_core_shapes()` | Get all available core shapes |
-| `get_wires()` | Get all available wires |
-| `get_bobbins()` | Get all available bobbins |
-| `find_core_material_by_name(name)` | Find core material by name |
-| `find_core_shape_by_name(name)` | Find core shape by name |
-| `find_wire_by_name(name)` | Find wire by name |
+# Run specific test module
+pytest tests/test_design_builder.py -v
-### Core Calculations
+# Run with coverage
+pytest tests/ --cov=api --cov-report=html
+```
-| Function | Description |
-|----------|-------------|
-| `calculate_core_data(core, process)` | Calculate complete core data |
-| `calculate_core_gapping(core, gapping)` | Calculate gapping configuration |
-| `calculate_inductance_from_number_turns_and_gapping(...)` | Calculate inductance |
-| `calculate_core_losses(core, operating_point, model)` | Calculate core losses |
+Pre-commit validation:
+```bash
+./scripts/pre_commit_check.sh
+```
-### Winding Functions
+## Documentation
-| Function | Description |
+| Resource | Description |
|----------|-------------|
-| `wind(core, coil, bobbin, pattern, layers)` | Wind coils on a core |
-| `calculate_winding_losses(...)` | Calculate total winding losses |
-| `calculate_ohmic_losses(...)` | Calculate DC losses |
-| `calculate_skin_effect_losses(...)` | Calculate skin effect losses |
-| `calculate_proximity_effect_losses(...)` | Calculate proximity effect losses |
+| [llms.txt](llms.txt) | Comprehensive API reference (AI-friendly) |
+| [CLAUDE.md](CLAUDE.md) | Development guide for Claude Code |
+| [PRD.md](PRD.md) | Product Requirements Document |
+| [docs/](docs/) | Detailed documentation |
+| [notebooks/](notebooks/) | Interactive tutorials |
-### Design Adviser
+## API Reference
-| Function | Description |
-|----------|-------------|
-| `calculate_advised_cores(inputs, max_results)` | Get recommended cores |
-| `calculate_advised_magnetics(inputs, max, mode)` | Get complete designs |
-| `process_inputs(inputs)` | Process and validate inputs |
+### Design Builder
-### Visualization
+| Method | Description |
+|--------|-------------|
+| `Design.flyback()` | Flyback transformer builder |
+| `Design.buck()` | Buck converter inductor builder |
+| `Design.boost()` | Boost converter inductor builder |
+| `Design.forward()` | Forward transformer builder |
+| `Design.llc()` | LLC resonant transformer builder |
+| `Design.inductor()` | Standalone inductor builder |
-| Function | Description |
-|----------|-------------|
-| `plot_core(core, ...)` | Generate SVG of core |
-| `plot_sections(magnetic, ...)` | Plot winding sections |
-| `plot_layers(magnetic, ...)` | Plot winding layers |
-| `plot_turns(magnetic, ...)` | Plot individual turns |
-| `plot_field(magnetic, ...)` | Plot magnetic field |
+### Builder Methods
-### Settings
+| Method | Description |
+|--------|-------------|
+| `.vin(min, max)` | Set DC input voltage range |
+| `.vin_ac(min, max)` | Set AC input voltage range |
+| `.vout(voltage)` | Set output voltage |
+| `.output(voltage, current)` | Add output (transformers) |
+| `.fsw(frequency)` | Set switching frequency |
+| `.prefer(priority)` | Set optimization priority |
+| `.solve(max_results)` | Run design optimization |
-| Function | Description |
-|----------|-------------|
-| `get_settings()` | Get current settings |
-| `set_settings(settings)` | Configure settings |
-| `reset_settings()` | Reset to defaults |
-
-### SPICE Export
+### Expert Knowledge
| Function | Description |
|----------|-------------|
-| `export_magnetic_as_subcircuit(magnetic, ...)` | Export as SPICE model |
-
-## Core Materials
-
-PyOpenMagnetics includes materials from major manufacturers:
-
-- **TDK/EPCOS**: N27, N49, N87, N95, N97, etc.
-- **Ferroxcube**: 3C90, 3C94, 3C95, 3F3, 3F4, etc.
-- **Fair-Rite**: Various ferrite materials
-- **Magnetics Inc.**: Powder cores (MPP, High Flux, Kool Mu)
-- **Micrometals**: Iron powder cores
-
-## Core Shapes
-
-Supported shape families include:
-
-- **E cores**: E, EI, EFD, EQ, ER
-- **ETD/EC cores**: ETD, EC
-- **PQ/PM cores**: PQ, PM
-- **RM cores**: RM, RM/ILP
-- **Toroidal**: Various sizes
-- **Pot cores**: P, PT
-- **U/UI cores**: U, UI, UR
-- **Planar**: E-LP, EQ-LP, etc.
-
-## Wire Types
-
-- **Round enamelled wire**: Various AWG and IEC sizes
-- **Litz wire**: Multiple strand configurations
-- **Rectangular wire**: For high-current applications
-- **Foil**: For planar magnetics
-- **Planar PCB**: For integrated designs
-
-## Configuration
-
-Use `set_settings()` to configure:
-
-```python
-settings = PyOpenMagnetics.get_settings()
-settings["coilAllowMarginTape"] = True
-settings["coilWindEvenIfNotFit"] = False
-settings["painterNumberPointsX"] = 50
-PyOpenMagnetics.set_settings(settings)
-```
+| `suggest_powder_core_material()` | Get material recommendations |
+| `calculate_core_loss()` | Steinmetz loss calculation |
+| `get_application_info()` | Application-specific guidance |
+| `suggest_topology()` | Topology recommendation |
+
+### Optimization
+
+| Class/Function | Description |
+|----------------|-------------|
+| `NSGAOptimizer` | Multi-objective NSGA-II optimizer |
+| `create_inductor_optimizer()` | Pre-configured inductor optimizer |
+| `ParetoFront` | Collection of optimal solutions |
+
+## Core Materials Database
+
+### Ferrite
+- TDK/EPCOS: N27, N49, N87, N95, N97
+- Ferroxcube: 3C90, 3C94, 3C95, 3C97, 3F3, 3F4
+
+### Powder Cores (NEW - 25+ materials)
+- **MPP**: 26µ, 60µ, 125µ, 147µ, 160µ, 173µ, 200µ
+- **High Flux**: 26µ, 60µ, 125µ, 147µ, 160µ
+- **Sendust/Kool Mu**: 26µ, 60µ, 75µ, 90µ, 125µ
+- **Mega Flux/XFlux**: 26µ, 50µ, 60µ, 75µ, 90µ
+
+### Nanocrystalline & Amorphous
+- Vitroperm, Finemet equivalents
+- Metglas amorphous alloys
+
+## Version Tiers
+
+| Feature | FREE | PRO |
+|---------|------|-----|
+| Basic topologies (buck, boost, flyback) | ✅ | ✅ |
+| Design adviser | ✅ | ✅ |
+| Core/material database | ✅ | ✅ |
+| Multi-objective optimization | ❌ | ✅ |
+| MCP server for AI | ❌ | ✅ |
+| Streamlit GUI | ❌ | ✅ |
+| FEMMT bridge | ❌ | ✅ |
+| Expert knowledge base | ❌ | ✅ |
+| Advanced topologies (LLC, DAB) | ❌ | ✅ |
+| Priority support | ❌ | ✅ |
## Contributing
-Contributions are welcome! Please see the [OpenMagnetics](https://github.com/OpenMagnetics) organization for contribution guidelines.
-
-## Documentation
-
-### Quick Start
-- **[llms.txt](llms.txt)** - Comprehensive API reference optimized for AI assistants and quick lookup
-- **[examples/](examples/)** - Practical example scripts for common design workflows
-- **[PyOpenMagnetics.pyi](PyOpenMagnetics.pyi)** - Type stubs for IDE autocompletion
-
-### Tutorials
-- **[notebooks/](notebooks/)** - Interactive Jupyter notebook tutorials with visualizations
- - [Getting Started](notebooks/01_getting_started.ipynb) - Introduction to PyOpenMagnetics
- - [Buck Inductor](notebooks/02_buck_inductor.ipynb) - Complete inductor design workflow
- - [Core Losses](notebooks/03_core_losses.ipynb) - In-depth core loss analysis
+Contributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
-### Reference
-- **[docs/errors.md](docs/errors.md)** - Common errors and solutions
-- **[docs/performance.md](docs/performance.md)** - Performance optimization guide
-- **[docs/compatibility.md](docs/compatibility.md)** - Python/platform version compatibility
-
-### Validation
-- **[api/validation.py](api/validation.py)** - Runtime JSON schema validation for inputs
+```bash
+# Development setup
+git clone https://github.com/OpenMagnetics/PyMKF.git
+cd PyMKF
+pip install -e ".[dev]"
+pre-commit install
+
+# Run tests before submitting PR
+./scripts/pre_commit_check.sh
+```
## License
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+MIT License - see [LICENSE](LICENSE) for details.
## Related Projects
-- [MKF](https://github.com/OpenMagnetics/MKF) - C++ magnetics library
+- [MKF](https://github.com/OpenMagnetics/MKF) - C++ magnetics engine
- [MAS](https://github.com/OpenMagnetics/MAS) - Magnetic Agnostic Structure schema
- [OpenMagnetics Web](https://openmagnetics.com) - Online design tool
+- [FEMMT](https://github.com/upb-lea/FEM_Magnetics_Toolbox) - FEM simulation
-## References
+## Citation
-- Maniktala, S. "Switching Power Supplies A-Z", 2nd Edition
-- Basso, C. "Switch-Mode Power Supplies", 2nd Edition
-- McLyman, C. "Transformer and Inductor Design Handbook"
+If you use PyOpenMagnetics in research, please cite:
+```bibtex
+@software{pyopenmagnetics,
+ title = {PyOpenMagnetics: Python Toolkit for Magnetic Component Design},
+ url = {https://github.com/OpenMagnetics/PyMKF},
+ year = {2024}
+}
+```
## Support
-For questions and support:
-- 🐛 [GitHub Issues](https://github.com/OpenMagnetics/PyOpenMagnetics/issues)
-- 💬 [Discussions](https://github.com/OpenMagnetics/PyOpenMagnetics/discussions)
-- 📧 Contact the maintainers
\ No newline at end of file
+- [GitHub Issues](https://github.com/OpenMagnetics/PyMKF/issues)
+- [Discussions](https://github.com/OpenMagnetics/PyMKF/discussions)
+- Email: support@openmagnetics.com
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..a633c69
--- /dev/null
+++ b/api/__init__.py
@@ -0,0 +1,27 @@
+"""
+PyOpenMagnetics Python API - Fluent design interface.
+
+Example:
+ from api.design import Design
+ results = Design.buck().vin(12,24).vout(5).iout(3).fsw(500e3).solve()
+
+Optimization:
+ from api.optimization import NSGAOptimizer
+ optimizer = NSGAOptimizer(objectives=["mass", "total_loss"])
+ optimizer.add_variable("turns", range=(20, 60))
+ pareto_front = optimizer.run()
+"""
+
+from .design import Design
+from .results import DesignResult, WindingInfo, BOMItem
+from .optimization import NSGAOptimizer, create_inductor_optimizer, ParetoFront
+
+__all__ = [
+ "Design",
+ "DesignResult",
+ "WindingInfo",
+ "BOMItem",
+ "NSGAOptimizer",
+ "create_inductor_optimizer",
+ "ParetoFront",
+]
diff --git a/api/architect/__init__.py b/api/architect/__init__.py
new file mode 100644
index 0000000..5cff93a
--- /dev/null
+++ b/api/architect/__init__.py
@@ -0,0 +1,20 @@
+"""
+Software Architect Tools for PyOpenMagnetics.
+
+Tools for code analysis, pattern documentation, and architecture generation.
+"""
+
+from .analyzer import analyze_module, analyze_package, get_module_metrics
+from .patterns import PATTERNS, get_pattern, list_patterns
+from .docs_generator import generate_api_docs, generate_architecture_diagram
+
+__all__ = [
+ "analyze_module",
+ "analyze_package",
+ "get_module_metrics",
+ "PATTERNS",
+ "get_pattern",
+ "list_patterns",
+ "generate_api_docs",
+ "generate_architecture_diagram",
+]
diff --git a/api/architect/analyzer.py b/api/architect/analyzer.py
new file mode 100644
index 0000000..dd68ab5
--- /dev/null
+++ b/api/architect/analyzer.py
@@ -0,0 +1,347 @@
+"""
+Code Analyzer for PyOpenMagnetics.
+
+Tools for analyzing Python modules and suggesting improvements.
+"""
+
+import ast
+import os
+from dataclasses import dataclass, field
+from typing import Optional
+from pathlib import Path
+
+
+@dataclass
+class FunctionMetrics:
+ """Metrics for a single function."""
+ name: str
+ line_number: int
+ line_count: int
+ parameter_count: int
+ has_docstring: bool
+ has_type_hints: bool
+ complexity: int = 0 # Cyclomatic complexity estimate
+
+
+@dataclass
+class ClassMetrics:
+ """Metrics for a single class."""
+ name: str
+ line_number: int
+ line_count: int
+ method_count: int
+ has_docstring: bool
+ base_classes: list[str] = field(default_factory=list)
+ methods: list[FunctionMetrics] = field(default_factory=list)
+
+
+@dataclass
+class ModuleMetrics:
+ """Metrics for a Python module."""
+ path: str
+ name: str
+ line_count: int
+ import_count: int
+ function_count: int
+ class_count: int
+ has_docstring: bool
+ functions: list[FunctionMetrics] = field(default_factory=list)
+ classes: list[ClassMetrics] = field(default_factory=list)
+ imports: list[str] = field(default_factory=list)
+
+
+class CodeAnalyzer(ast.NodeVisitor):
+ """AST-based code analyzer."""
+
+ def __init__(self, source: str):
+ self.source = source
+ self.lines = source.split('\n')
+ self.functions: list[FunctionMetrics] = []
+ self.classes: list[ClassMetrics] = []
+ self.imports: list[str] = []
+ self._current_class: Optional[ClassMetrics] = None
+
+ def analyze(self) -> dict:
+ """Analyze the source code."""
+ tree = ast.parse(self.source)
+ self.visit(tree)
+ return {
+ "functions": self.functions,
+ "classes": self.classes,
+ "imports": self.imports,
+ }
+
+ def visit_Import(self, node: ast.Import):
+ for alias in node.names:
+ self.imports.append(alias.name)
+ self.generic_visit(node)
+
+ def visit_ImportFrom(self, node: ast.ImportFrom):
+ module = node.module or ""
+ for alias in node.names:
+ self.imports.append(f"{module}.{alias.name}")
+ self.generic_visit(node)
+
+ def visit_FunctionDef(self, node: ast.FunctionDef):
+ # Count lines
+ end_line = node.end_lineno or node.lineno
+ line_count = end_line - node.lineno + 1
+
+ # Check docstring
+ has_docstring = (
+ node.body and
+ isinstance(node.body[0], ast.Expr) and
+ isinstance(node.body[0].value, ast.Constant)
+ )
+
+ # Check type hints
+ has_type_hints = node.returns is not None or any(
+ arg.annotation is not None for arg in node.args.args
+ )
+
+ # Estimate cyclomatic complexity (simplified)
+ complexity = 1
+ for child in ast.walk(node):
+ if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
+ complexity += 1
+ elif isinstance(child, ast.BoolOp):
+ complexity += len(child.values) - 1
+
+ func_metrics = FunctionMetrics(
+ name=node.name,
+ line_number=node.lineno,
+ line_count=line_count,
+ parameter_count=len(node.args.args),
+ has_docstring=has_docstring,
+ has_type_hints=has_type_hints,
+ complexity=complexity
+ )
+
+ if self._current_class:
+ self._current_class.methods.append(func_metrics)
+ else:
+ self.functions.append(func_metrics)
+
+ self.generic_visit(node)
+
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
+ # Treat async functions same as regular
+ self.visit_FunctionDef(node) # type: ignore
+
+ def visit_ClassDef(self, node: ast.ClassDef):
+ # Count lines
+ end_line = node.end_lineno or node.lineno
+ line_count = end_line - node.lineno + 1
+
+ # Check docstring
+ has_docstring = (
+ node.body and
+ isinstance(node.body[0], ast.Expr) and
+ isinstance(node.body[0].value, ast.Constant)
+ )
+
+ # Get base classes
+ bases = []
+ for base in node.bases:
+ if isinstance(base, ast.Name):
+ bases.append(base.id)
+ elif isinstance(base, ast.Attribute):
+ bases.append(f"{base.value.id}.{base.attr}" if isinstance(base.value, ast.Name) else base.attr)
+
+ class_metrics = ClassMetrics(
+ name=node.name,
+ line_number=node.lineno,
+ line_count=line_count,
+ method_count=0,
+ has_docstring=has_docstring,
+ base_classes=bases,
+ )
+
+ self._current_class = class_metrics
+ self.generic_visit(node)
+ class_metrics.method_count = len(class_metrics.methods)
+ self._current_class = None
+
+ self.classes.append(class_metrics)
+
+
+def analyze_module(module_path: str) -> ModuleMetrics:
+ """
+ Analyze a Python module file.
+
+ Args:
+ module_path: Path to the Python file
+
+ Returns:
+ ModuleMetrics with detailed analysis
+
+ Example:
+ >>> metrics = analyze_module("api/design.py")
+ >>> print(f"Classes: {metrics.class_count}, Functions: {metrics.function_count}")
+ """
+ path = Path(module_path)
+ if not path.exists():
+ raise FileNotFoundError(f"Module not found: {module_path}")
+
+ source = path.read_text()
+ lines = source.split('\n')
+
+ # Check module docstring
+ has_docstring = False
+ try:
+ tree = ast.parse(source)
+ if tree.body and isinstance(tree.body[0], ast.Expr):
+ if isinstance(tree.body[0].value, ast.Constant):
+ has_docstring = True
+ except SyntaxError:
+ pass
+
+ analyzer = CodeAnalyzer(source)
+ try:
+ analyzer.analyze()
+ except SyntaxError as e:
+ return ModuleMetrics(
+ path=str(path),
+ name=path.stem,
+ line_count=len(lines),
+ import_count=0,
+ function_count=0,
+ class_count=0,
+ has_docstring=False,
+ )
+
+ return ModuleMetrics(
+ path=str(path),
+ name=path.stem,
+ line_count=len(lines),
+ import_count=len(analyzer.imports),
+ function_count=len(analyzer.functions),
+ class_count=len(analyzer.classes),
+ has_docstring=has_docstring,
+ functions=analyzer.functions,
+ classes=analyzer.classes,
+ imports=analyzer.imports,
+ )
+
+
+def analyze_package(package_path: str) -> list[ModuleMetrics]:
+ """
+ Analyze all Python modules in a package.
+
+ Args:
+ package_path: Path to the package directory
+
+ Returns:
+ List of ModuleMetrics for each .py file
+ """
+ path = Path(package_path)
+ if not path.is_dir():
+ raise NotADirectoryError(f"Not a directory: {package_path}")
+
+ results = []
+ for py_file in path.rglob("*.py"):
+ try:
+ results.append(analyze_module(str(py_file)))
+ except Exception:
+ pass
+
+ return results
+
+
+def get_module_metrics(module_path: str) -> dict:
+ """
+ Get simplified metrics dict for a module.
+
+ Returns:
+ Dict with summary metrics
+ """
+ metrics = analyze_module(module_path)
+ return {
+ "path": metrics.path,
+ "name": metrics.name,
+ "lines": metrics.line_count,
+ "imports": metrics.import_count,
+ "functions": metrics.function_count,
+ "classes": metrics.class_count,
+ "has_docstring": metrics.has_docstring,
+ "total_methods": sum(c.method_count for c in metrics.classes),
+ "avg_function_complexity": (
+ sum(f.complexity for f in metrics.functions) / len(metrics.functions)
+ if metrics.functions else 0
+ ),
+ }
+
+
+def suggest_refactorings(module_path: str) -> list[dict]:
+ """
+ Suggest potential refactorings for a module.
+
+ Args:
+ module_path: Path to analyze
+
+ Returns:
+ List of suggestion dicts with type, location, and description
+ """
+ metrics = analyze_module(module_path)
+ suggestions = []
+
+ # Check for missing docstrings
+ if not metrics.has_docstring:
+ suggestions.append({
+ "type": "missing_docstring",
+ "location": f"{metrics.name}:1",
+ "description": "Module is missing a docstring",
+ "severity": "info"
+ })
+
+ for func in metrics.functions:
+ if not func.has_docstring:
+ suggestions.append({
+ "type": "missing_docstring",
+ "location": f"{metrics.name}:{func.line_number}",
+ "description": f"Function '{func.name}' is missing a docstring",
+ "severity": "info"
+ })
+
+ if func.complexity > 10:
+ suggestions.append({
+ "type": "high_complexity",
+ "location": f"{metrics.name}:{func.line_number}",
+ "description": f"Function '{func.name}' has high complexity ({func.complexity})",
+ "severity": "warning"
+ })
+
+ if func.line_count > 50:
+ suggestions.append({
+ "type": "long_function",
+ "location": f"{metrics.name}:{func.line_number}",
+ "description": f"Function '{func.name}' is long ({func.line_count} lines)",
+ "severity": "info"
+ })
+
+ if func.parameter_count > 5:
+ suggestions.append({
+ "type": "many_parameters",
+ "location": f"{metrics.name}:{func.line_number}",
+ "description": f"Function '{func.name}' has many parameters ({func.parameter_count})",
+ "severity": "info"
+ })
+
+ for cls in metrics.classes:
+ if not cls.has_docstring:
+ suggestions.append({
+ "type": "missing_docstring",
+ "location": f"{metrics.name}:{cls.line_number}",
+ "description": f"Class '{cls.name}' is missing a docstring",
+ "severity": "info"
+ })
+
+ if cls.method_count > 20:
+ suggestions.append({
+ "type": "large_class",
+ "location": f"{metrics.name}:{cls.line_number}",
+ "description": f"Class '{cls.name}' has many methods ({cls.method_count})",
+ "severity": "info"
+ })
+
+ return suggestions
diff --git a/api/architect/docs_generator.py b/api/architect/docs_generator.py
new file mode 100644
index 0000000..eef6517
--- /dev/null
+++ b/api/architect/docs_generator.py
@@ -0,0 +1,256 @@
+"""
+Documentation Generator for PyOpenMagnetics.
+
+Tools for generating API documentation and architecture diagrams.
+"""
+
+from pathlib import Path
+from typing import Optional
+from .analyzer import analyze_module, analyze_package, ModuleMetrics
+
+
+def generate_api_docs(package_path: str, output_path: Optional[str] = None) -> str:
+ """
+ Generate API documentation for a Python package.
+
+ Args:
+ package_path: Path to the package directory
+ output_path: Optional path to write the documentation
+
+ Returns:
+ Markdown documentation string
+ """
+ modules = analyze_package(package_path)
+ lines = []
+
+ # Header
+ package_name = Path(package_path).name
+ lines.append(f"# {package_name} API Reference")
+ lines.append("")
+ lines.append("Auto-generated API documentation.")
+ lines.append("")
+
+ # Summary table
+ lines.append("## Module Summary")
+ lines.append("")
+ lines.append("| Module | Classes | Functions | Lines |")
+ lines.append("|--------|---------|-----------|-------|")
+ for m in sorted(modules, key=lambda x: x.path):
+ rel_path = Path(m.path).relative_to(package_path) if package_path in m.path else m.path
+ lines.append(f"| {rel_path} | {m.class_count} | {m.function_count} | {m.line_count} |")
+ lines.append("")
+
+ # Detailed documentation
+ for m in sorted(modules, key=lambda x: x.path):
+ rel_path = Path(m.path).relative_to(package_path) if package_path in m.path else m.path
+ lines.append(f"## {rel_path}")
+ lines.append("")
+
+ if m.classes:
+ lines.append("### Classes")
+ lines.append("")
+ for cls in m.classes:
+ lines.append(f"#### `{cls.name}`")
+ if cls.base_classes:
+ lines.append(f"*Inherits from: {', '.join(cls.base_classes)}*")
+ lines.append("")
+ lines.append(f"- Line: {cls.line_number}")
+ lines.append(f"- Methods: {cls.method_count}")
+ lines.append("")
+
+ if cls.methods:
+ lines.append("**Methods:**")
+ lines.append("")
+ for method in cls.methods:
+ hint = " (typed)" if method.has_type_hints else ""
+ lines.append(f"- `{method.name}({method.parameter_count} params)`{hint}")
+ lines.append("")
+
+ if m.functions:
+ lines.append("### Functions")
+ lines.append("")
+ for func in m.functions:
+ hint = " (typed)" if func.has_type_hints else ""
+ lines.append(f"- `{func.name}({func.parameter_count} params)`{hint} - Line {func.line_number}")
+ lines.append("")
+
+ doc_text = "\n".join(lines)
+
+ if output_path:
+ Path(output_path).write_text(doc_text)
+
+ return doc_text
+
+
+def generate_architecture_diagram(package_path: str, format: str = "mermaid") -> str:
+ """
+ Generate an architecture diagram for a package.
+
+ Args:
+ package_path: Path to the package directory
+ format: Output format ("mermaid" or "ascii")
+
+ Returns:
+ Diagram string
+ """
+ modules = analyze_package(package_path)
+
+ if format == "mermaid":
+ return _generate_mermaid_diagram(modules, package_path)
+ else:
+ return _generate_ascii_diagram(modules, package_path)
+
+
+def _generate_mermaid_diagram(modules: list[ModuleMetrics], package_path: str) -> str:
+ """Generate Mermaid.js diagram."""
+ lines = ["```mermaid", "graph TD"]
+
+ # Group modules by directory
+ dirs = {}
+ for m in modules:
+ rel_path = Path(m.path).relative_to(package_path) if package_path in m.path else Path(m.path)
+ parent = str(rel_path.parent) if rel_path.parent != Path(".") else "root"
+ if parent not in dirs:
+ dirs[parent] = []
+ dirs[parent].append(m)
+
+ # Create subgraphs for directories
+ for dir_name, dir_modules in dirs.items():
+ if dir_name != "root":
+ safe_name = dir_name.replace("/", "_").replace(".", "_")
+ lines.append(f" subgraph {safe_name}[{dir_name}]")
+
+ for m in dir_modules:
+ node_id = m.name.replace(".", "_")
+ label = f"{m.name}
{m.class_count}C {m.function_count}F"
+ lines.append(f" {node_id}[{label}]")
+
+ if dir_name != "root":
+ lines.append(" end")
+
+ # Add dependency edges based on imports
+ for m in modules:
+ src = m.name.replace(".", "_")
+ for imp in m.imports:
+ # Only show internal dependencies
+ for other in modules:
+ if other.name in imp and other.name != m.name:
+ dst = other.name.replace(".", "_")
+ lines.append(f" {src} --> {dst}")
+ break
+
+ lines.append("```")
+ return "\n".join(lines)
+
+
+def _generate_ascii_diagram(modules: list[ModuleMetrics], package_path: str) -> str:
+ """Generate ASCII diagram."""
+ lines = []
+ lines.append("=" * 60)
+ lines.append(f" Architecture: {Path(package_path).name}")
+ lines.append("=" * 60)
+ lines.append("")
+
+ # Group by directory
+ dirs = {}
+ for m in modules:
+ rel_path = Path(m.path).relative_to(package_path) if package_path in m.path else Path(m.path)
+ parent = str(rel_path.parent) if rel_path.parent != Path(".") else "."
+ if parent not in dirs:
+ dirs[parent] = []
+ dirs[parent].append(m)
+
+ for dir_name in sorted(dirs.keys()):
+ dir_modules = dirs[dir_name]
+
+ if dir_name != ".":
+ lines.append(f" [{dir_name}/]")
+
+ for m in dir_modules:
+ prefix = " " if dir_name != "." else " "
+ stats = f"({m.class_count}C, {m.function_count}F, {m.line_count}L)"
+ lines.append(f"{prefix}+-- {m.name}.py {stats}")
+
+ lines.append("")
+
+ return "\n".join(lines)
+
+
+def generate_dependency_graph(package_path: str) -> dict:
+ """
+ Generate a dependency graph for the package.
+
+ Returns:
+ Dict with nodes and edges for graph visualization
+ """
+ modules = analyze_package(package_path)
+
+ nodes = []
+ edges = []
+
+ for m in modules:
+ nodes.append({
+ "id": m.name,
+ "label": m.name,
+ "classes": m.class_count,
+ "functions": m.function_count,
+ "lines": m.line_count
+ })
+
+ for imp in m.imports:
+ for other in modules:
+ if other.name in imp and other.name != m.name:
+ edges.append({
+ "source": m.name,
+ "target": other.name
+ })
+ break
+
+ return {"nodes": nodes, "edges": edges}
+
+
+def generate_module_summary(module_path: str) -> str:
+ """
+ Generate a summary for a single module.
+
+ Args:
+ module_path: Path to the Python file
+
+ Returns:
+ Markdown summary string
+ """
+ m = analyze_module(module_path)
+ lines = []
+
+ lines.append(f"# Module: {m.name}")
+ lines.append("")
+ lines.append(f"**Path:** `{m.path}`")
+ lines.append(f"**Lines:** {m.line_count}")
+ lines.append(f"**Has Docstring:** {'Yes' if m.has_docstring else 'No'}")
+ lines.append("")
+
+ if m.classes:
+ lines.append("## Classes")
+ lines.append("")
+ for cls in m.classes:
+ bases = f" ({', '.join(cls.base_classes)})" if cls.base_classes else ""
+ lines.append(f"### {cls.name}{bases}")
+ lines.append(f"- **Line:** {cls.line_number}")
+ lines.append(f"- **Methods:** {cls.method_count}")
+ if cls.methods:
+ lines.append("")
+ for method in cls.methods:
+ lines.append(f" - `{method.name}()`")
+ lines.append("")
+
+ if m.functions:
+ lines.append("## Functions")
+ lines.append("")
+ for func in m.functions:
+ lines.append(f"### `{func.name}()`")
+ lines.append(f"- **Line:** {func.line_number}")
+ lines.append(f"- **Parameters:** {func.parameter_count}")
+ lines.append(f"- **Complexity:** {func.complexity}")
+ lines.append("")
+
+ return "\n".join(lines)
diff --git a/api/architect/patterns.py b/api/architect/patterns.py
new file mode 100644
index 0000000..7b7d845
--- /dev/null
+++ b/api/architect/patterns.py
@@ -0,0 +1,272 @@
+"""
+Design Pattern Library for PyOpenMagnetics.
+
+Documents the design patterns used in the codebase with examples.
+"""
+
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class PatternInfo:
+ """Information about a design pattern."""
+ name: str
+ category: str # creational, structural, behavioral
+ description: str
+ use_case: str
+ example_location: str
+ example_code: str
+
+
+PATTERNS = {
+ "fluent_builder": PatternInfo(
+ name="Fluent Builder",
+ category="creational",
+ description="""
+The Fluent Builder pattern provides a readable way to construct complex objects
+step by step. Each method returns 'self' to enable method chaining.
+ """.strip(),
+ use_case="""
+Used for topology builders (FlybackBuilder, BuckBuilder, etc.) to allow
+engineers to specify design parameters in a natural, readable way.
+ """.strip(),
+ example_location="api/design.py",
+ example_code="""
+# Fluent builder pattern in action
+design = (
+ Design.flyback()
+ .vin_ac(85, 265)
+ .output(12, 5)
+ .fsw(100e3)
+ .efficiency(0.87)
+ .max_height(15)
+ .solve()
+)
+ """.strip(),
+ ),
+
+ "factory_method": PatternInfo(
+ name="Factory Method",
+ category="creational",
+ description="""
+The Factory Method pattern provides an interface for creating objects without
+specifying their concrete classes. Static methods on a class create instances
+of appropriate subclasses.
+ """.strip(),
+ use_case="""
+Used in the Design class to create topology-specific builders without
+requiring the user to know the specific builder class names.
+ """.strip(),
+ example_location="api/design.py",
+ example_code="""
+class Design:
+ @staticmethod
+ def flyback() -> 'FlybackBuilder':
+ return FlybackBuilder()
+
+ @staticmethod
+ def buck() -> 'BuckBuilder':
+ return BuckBuilder()
+
+# Usage - user doesn't need to know builder class names
+builder = Design.flyback()
+ """.strip(),
+ ),
+
+ "dataclass_result": PatternInfo(
+ name="Dataclass Result",
+ category="structural",
+ description="""
+Using dataclasses to represent structured result data provides automatic
+__init__, __repr__, and comparison methods. Combined with factory classmethods,
+this creates clean, immutable result objects.
+ """.strip(),
+ use_case="""
+Used for DesignResult, WindingInfo, and BOMItem to represent magnetic
+component design outputs in a structured, type-safe way.
+ """.strip(),
+ example_location="api/results.py",
+ example_code="""
+@dataclass
+class DesignResult:
+ core: str
+ material: str
+ windings: list[WindingInfo]
+ air_gap_mm: float
+ # ... more fields
+
+ @classmethod
+ def from_mas(cls, mas: dict) -> "DesignResult":
+ # Parse MAS format into structured result
+ return cls(...)
+ """.strip(),
+ ),
+
+ "template_method": PatternInfo(
+ name="Template Method",
+ category="behavioral",
+ description="""
+The Template Method pattern defines the skeleton of an algorithm in a base class,
+with concrete steps implemented by subclasses. Abstract methods ensure subclasses
+provide required implementations.
+ """.strip(),
+ use_case="""
+Used in TopologyBuilder base class. The solve() and to_mas() methods define
+the algorithm structure, while subclasses implement _generate_operating_points()
+and _generate_design_requirements().
+ """.strip(),
+ example_location="api/design.py",
+ example_code="""
+class TopologyBuilder(ABC):
+ def to_mas(self) -> dict:
+ # Template method - defines structure
+ return {
+ "designRequirements": self._generate_design_requirements(),
+ "operatingPoints": self._generate_operating_points()
+ }
+
+ @abstractmethod
+ def _generate_operating_points(self) -> list[dict]:
+ # Must be implemented by subclasses
+ ...
+ """.strip(),
+ ),
+
+ "strategy": PatternInfo(
+ name="Strategy",
+ category="behavioral",
+ description="""
+The Strategy pattern defines a family of algorithms and makes them interchangeable.
+The algorithm can be selected at runtime without changing the clients that use it.
+ """.strip(),
+ use_case="""
+Used for loss calculation models (IGSE, STEINMETZ, etc.) where different
+algorithms can be selected for core loss calculation.
+ """.strip(),
+ example_location="PyOpenMagnetics bindings",
+ example_code="""
+# Different loss calculation strategies
+models = {
+ "coreLosses": "IGSE", # or "STEINMETZ", "MSE", etc.
+ "reluctance": "ZHANG",
+ "coreTemperature": "MANIKTALA"
+}
+
+losses = PyOpenMagnetics.calculate_core_losses(
+ core, coil, inputs, models
+)
+ """.strip(),
+ ),
+
+ "facade": PatternInfo(
+ name="Facade",
+ category="structural",
+ description="""
+The Facade pattern provides a simplified interface to a complex subsystem.
+It wraps a complicated system with a simpler interface.
+ """.strip(),
+ use_case="""
+The MCP tools module acts as a facade over the Design API and PyOpenMagnetics,
+providing simple functions for AI assistants to use.
+ """.strip(),
+ example_location="api/mcp/tools.py",
+ example_code="""
+def design_magnetic(
+ topology: str,
+ vin_min: float,
+ vin_max: float,
+ outputs: list[dict],
+ frequency_hz: float,
+ **kwargs
+) -> dict:
+ # Facade hides complexity of Design API
+ builder = Design.flyback()
+ builder.vin_ac(vin_min, vin_max)
+ # ... setup
+ return {"designs": results}
+ """.strip(),
+ ),
+
+ "adapter": PatternInfo(
+ name="Adapter",
+ category="structural",
+ description="""
+The Adapter pattern converts the interface of a class into another interface
+that clients expect. It lets classes work together that couldn't otherwise.
+ """.strip(),
+ use_case="""
+The FEMMT bridge acts as an adapter, converting PyOpenMagnetics DesignResult
+format into FEMMT's expected input format.
+ """.strip(),
+ example_location="api/bridges/femmt.py",
+ example_code="""
+class FEMMTExporter:
+ # Adapts DesignResult to FEMMT format
+ CORE_TYPE_MAP = {"E": "E", "ETD": "E", "PQ": "PQ", ...}
+ MATERIAL_MAP = {"3C95": "N95", "N87": "N87", ...}
+
+ def export(self, design: DesignResult) -> str:
+ # Convert PyOpenMagnetics format to FEMMT script
+ ...
+ """.strip(),
+ ),
+}
+
+
+def get_pattern(name: str) -> Optional[PatternInfo]:
+ """
+ Get information about a specific design pattern.
+
+ Args:
+ name: Pattern name (e.g., "fluent_builder")
+
+ Returns:
+ PatternInfo or None if not found
+ """
+ return PATTERNS.get(name)
+
+
+def list_patterns() -> list[str]:
+ """List all documented patterns."""
+ return list(PATTERNS.keys())
+
+
+def patterns_by_category(category: str) -> list[PatternInfo]:
+ """
+ Get all patterns in a category.
+
+ Args:
+ category: "creational", "structural", or "behavioral"
+
+ Returns:
+ List of matching PatternInfo objects
+ """
+ return [p for p in PATTERNS.values() if p.category == category]
+
+
+def generate_pattern_docs() -> str:
+ """Generate markdown documentation for all patterns."""
+ lines = ["# Design Patterns in PyOpenMagnetics", ""]
+
+ for category in ["creational", "structural", "behavioral"]:
+ patterns = patterns_by_category(category)
+ if patterns:
+ lines.append(f"## {category.title()} Patterns")
+ lines.append("")
+
+ for p in patterns:
+ lines.append(f"### {p.name}")
+ lines.append("")
+ lines.append(p.description)
+ lines.append("")
+ lines.append(f"**Use Case:** {p.use_case}")
+ lines.append("")
+ lines.append(f"**Example Location:** `{p.example_location}`")
+ lines.append("")
+ lines.append("```python")
+ lines.append(p.example_code)
+ lines.append("```")
+ lines.append("")
+
+ return "\n".join(lines)
diff --git a/api/design.py b/api/design.py
new file mode 100644
index 0000000..5788c7f
--- /dev/null
+++ b/api/design.py
@@ -0,0 +1,1009 @@
+"""
+Fluent Design API for magnetic component design.
+
+Provides topology builders for flyback, buck, boost, forward, LLC, and inductor designs.
+
+Example:
+ from api.design import Design
+ results = Design.buck().vin(12,24).vout(5).iout(3).fsw(500e3).solve()
+"""
+
+import math
+import json
+import sys
+import threading
+from abc import ABC, abstractmethod
+from typing import Self, Optional, Any
+from dataclasses import dataclass
+
+from . import waveforms
+
+
+def _progress_spinner(stop_event: threading.Event, message: str):
+ """Show animated spinner during long operations.
+
+ Args:
+ stop_event: Event to signal when to stop the spinner
+ message: Message to display alongside the spinner
+ """
+ chars = "\u28cb\u2819\u28b9\u2838\u283c\u2834\u2826\u2827\u2807\u280f"
+ i = 0
+ while not stop_event.is_set():
+ sys.stdout.write(f"\r{chars[i % len(chars)]} {message}")
+ sys.stdout.flush()
+ stop_event.wait(0.1) # More efficient than time.sleep
+ i += 1
+ # Clear the spinner line
+ sys.stdout.write("\r" + " " * (len(message) + 3) + "\r")
+ sys.stdout.flush()
+
+
+@dataclass
+class DesignSpec:
+ """Intermediate representation of a design specification."""
+ topology: str
+ params: dict
+ constraints: dict
+ operating_points: list
+
+
+# =============================================================================
+# Base Builder
+# =============================================================================
+
+class TopologyBuilder(ABC):
+ """Abstract base class for power converter topology builders."""
+
+ def __init__(self):
+ self._params: dict[str, Any] = {}
+ self._constraints: dict[str, Any] = {}
+ self._core_families: Optional[list[str]] = None
+ self._max_height_mm: Optional[float] = None
+ self._max_width_mm: Optional[float] = None
+ self._max_depth_mm: Optional[float] = None
+ self._priority: str = "efficiency"
+ self._ambient_temp: float = 25.0
+ self._max_temp_rise: Optional[float] = None
+
+ def max_height(self, mm: float) -> Self:
+ self._max_height_mm = mm
+ return self
+
+ def max_width(self, mm: float) -> Self:
+ self._max_width_mm = mm
+ return self
+
+ def max_depth(self, mm: float) -> Self:
+ self._max_depth_mm = mm
+ return self
+
+ def max_dimensions(self, width_mm: float, height_mm: float, depth_mm: float) -> Self:
+ self._max_width_mm, self._max_height_mm, self._max_depth_mm = width_mm, height_mm, depth_mm
+ return self
+
+ def core_families(self, families: list[str]) -> Self:
+ self._core_families = families
+ return self
+
+ def prefer(self, priority: str) -> Self:
+ if priority not in ("efficiency", "cost", "size"):
+ raise ValueError(f"Invalid priority: {priority}")
+ self._priority = priority
+ return self
+
+ def ambient_temperature(self, celsius: float) -> Self:
+ self._ambient_temp = celsius
+ return self
+
+ def max_temperature_rise(self, kelvin: float) -> Self:
+ self._max_temp_rise = kelvin
+ return self
+
+ @abstractmethod
+ def _generate_operating_points(self) -> list[dict]: ...
+ @abstractmethod
+ def _generate_design_requirements(self) -> dict: ...
+ @abstractmethod
+ def _topology_name(self) -> str: ...
+
+ def build(self) -> DesignSpec:
+ return DesignSpec(self._topology_name(), self._params.copy(),
+ self._constraints.copy(), self._generate_operating_points())
+
+ def to_mas(self) -> dict:
+ return {"designRequirements": self._generate_design_requirements(),
+ "operatingPoints": self._generate_operating_points()}
+
+ def solve(self, max_results: int = 30, core_mode: str = "available cores",
+ verbose: bool = False, output_dir: Optional[str] = None,
+ auto_relax: bool = False, relax_step: float = 0.1) -> list:
+ """
+ Run the design optimization and return results.
+
+ Args:
+ max_results: Maximum number of designs to return (default: 30)
+ core_mode: "available cores" or "standard cores"
+ verbose: If True, print progress information
+ output_dir: If provided, save detailed results and plots here
+ auto_relax: If True and no results found, automatically relax constraints
+ relax_step: Relaxation step (0.1 = 10% increase per iteration)
+ """
+ import time
+ import PyOpenMagnetics
+ from .results import DesignResult
+
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] Processing inputs...")
+
+ start_time = time.time()
+ processed = PyOpenMagnetics.process_inputs(self.to_mas())
+
+ if verbose:
+ process_time = time.time() - start_time
+ print(f"[{time.strftime('%H:%M:%S')}] Input processing: {process_time:.2f}s")
+ print(f"[{time.strftime('%H:%M:%S')}] Starting design optimization...")
+
+ # Start progress spinner for long optimization
+ stop_event = None
+ spinner_thread = None
+ if verbose:
+ stop_event = threading.Event()
+ spinner_msg = f"Evaluating designs (max {max_results}, {core_mode})..."
+ spinner_thread = threading.Thread(
+ target=_progress_spinner,
+ args=(stop_event, spinner_msg),
+ daemon=True
+ )
+ spinner_thread.start()
+
+ opt_start = time.time()
+ try:
+ result = PyOpenMagnetics.calculate_advised_magnetics(processed, max_results, core_mode)
+ finally:
+ # Always stop the spinner
+ if stop_event:
+ stop_event.set()
+ if spinner_thread:
+ spinner_thread.join(timeout=1.0)
+
+ opt_time = time.time() - opt_start
+
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] Optimization complete: {opt_time:.2f}s")
+
+ if isinstance(result, str):
+ results = json.loads(result)
+ elif isinstance(result, dict):
+ data = result.get("data", result)
+ if isinstance(data, str):
+ if data.startswith("Exception:"):
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] ERROR: {data}")
+ # Parse common errors and provide guidance
+ if "turns ratio" in data.lower() and "greater than 0" in data.lower():
+ print("\n" + "="*60)
+ print("DIAGNOSIS: Negative turns ratio detected")
+ print("="*60)
+ print(" This usually means a negative output voltage was specified.")
+ print(" Transformers use absolute voltage values - the polarity is")
+ print(" determined by winding direction, not the turns ratio.")
+ print("\n FIX: Use positive voltage value, e.g.:")
+ print(" .output(8, 0.2) instead of .output(-8, 0.2)")
+ print("="*60 + "\n")
+ return []
+ results = json.loads(data)
+ else:
+ results = data if isinstance(data, list) else [data]
+ else:
+ results = result if isinstance(result, list) else [result] if result else []
+
+ design_results = [DesignResult.from_mas(r) for r in results if isinstance(r, dict) and "magnetic" in r]
+
+ if verbose:
+ total_time = time.time() - start_time
+ print(f"[{time.strftime('%H:%M:%S')}] Found {len(design_results)} designs in {total_time:.2f}s total")
+
+ # Auto-relax constraints if no results and auto_relax enabled
+ if not design_results and auto_relax:
+ design_results = self._try_relaxed_constraints(
+ max_results, core_mode, verbose, relax_step
+ )
+
+ # Save results and generate Pareto plot if output_dir specified
+ if output_dir and design_results:
+ self._save_results(design_results, output_dir, verbose)
+
+ return design_results
+
+ def _save_results(self, results: list, output_dir: str, verbose: bool = False):
+ """Save results to JSON and generate comprehensive design report."""
+ import os
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Save results as JSON
+ results_data = []
+ for i, r in enumerate(results):
+ results_data.append({
+ "rank": i + 1,
+ "core": r.core,
+ "material": r.material,
+ "primary_turns": r.primary_turns,
+ "primary_wire": r.primary_wire if hasattr(r, 'primary_wire') else "Unknown",
+ "air_gap_mm": r.air_gap_mm,
+ "core_loss_w": r.core_loss_w,
+ "copper_loss_w": r.copper_loss_w,
+ "total_loss_w": r.total_loss_w,
+ "temp_rise_c": r.temp_rise_c if hasattr(r, 'temp_rise_c') else 0,
+ })
+
+ json_path = os.path.join(output_dir, "results.json")
+ with open(json_path, "w") as f:
+ json.dump(results_data, f, indent=2)
+
+ if verbose:
+ print(f"[Results saved to {json_path}]")
+
+ # Generate comprehensive design report with matplotlib
+ try:
+ from .report import generate_design_report
+
+ # Build specs dict from builder state
+ specs = self._get_report_specs()
+
+ # Generate title from topology
+ topology_name = self.__class__.__name__.replace('Builder', '')
+ title = f"{topology_name} Transformer Design Report"
+
+ generate_design_report(results, output_dir, title, specs, verbose)
+
+ except ImportError as e:
+ if verbose:
+ print(f"[Design report skipped - matplotlib not installed: {e}]")
+ # Fall back to basic Pareto plot
+ if len(results) >= 2:
+ try:
+ self._generate_pareto_plot(results_data, output_dir, verbose)
+ except ImportError:
+ pass
+
+ def _get_report_specs(self) -> dict:
+ """Get specifications dict for report generation."""
+ specs = {}
+ if hasattr(self, '_frequency'):
+ specs['frequency_hz'] = self._frequency
+ if hasattr(self, '_outputs') and self._outputs:
+ # Handle both dict format {"voltage": v, "current": i} and tuple format (v, i)
+ total_power = 0
+ for output in self._outputs:
+ if isinstance(output, dict):
+ total_power += output.get('voltage', 0) * output.get('current', 0)
+ elif isinstance(output, (list, tuple)) and len(output) >= 2:
+ total_power += output[0] * output[1]
+ specs['power_w'] = total_power
+ if hasattr(self, '_efficiency'):
+ specs['efficiency'] = self._efficiency
+ return specs
+
+ def _generate_pareto_plot(self, results_data: list, output_dir: str, verbose: bool = False):
+ """Generate a Pareto front plot showing loss vs core size tradeoff."""
+ import matplotlib
+ matplotlib.use('Agg') # Non-interactive backend
+ import matplotlib.pyplot as plt
+ import os
+
+ # Extract data for plotting
+ cores = [r["core"] for r in results_data]
+ total_losses = [r["total_loss_w"] for r in results_data]
+ core_losses = [r["core_loss_w"] for r in results_data]
+ copper_losses = [r["copper_loss_w"] for r in results_data]
+
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
+
+ # Plot 1: Total loss ranking
+ ax1 = axes[0]
+ colors = ['green' if i == 0 else 'steelblue' for i in range(len(results_data))]
+ bars = ax1.barh(range(len(cores)), total_losses, color=colors)
+ ax1.set_yticks(range(len(cores)))
+ ax1.set_yticklabels([f"{c}" for c in cores])
+ ax1.set_xlabel('Total Loss (W)')
+ ax1.set_title('Design Comparison - Total Loss')
+ ax1.invert_yaxis()
+
+ # Add value labels
+ for bar, loss in zip(bars, total_losses):
+ ax1.text(bar.get_width() + 0.05, bar.get_y() + bar.get_height()/2,
+ f'{loss:.2f}W', va='center', fontsize=9)
+
+ # Plot 2: Loss breakdown (stacked bar)
+ ax2 = axes[1]
+ y_pos = range(len(cores))
+ ax2.barh(y_pos, core_losses, label='Core Loss', color='coral')
+ ax2.barh(y_pos, copper_losses, left=core_losses, label='Copper Loss', color='steelblue')
+ ax2.set_yticks(y_pos)
+ ax2.set_yticklabels(cores)
+ ax2.set_xlabel('Loss (W)')
+ ax2.set_title('Loss Breakdown')
+ ax2.legend(loc='lower right')
+ ax2.invert_yaxis()
+
+ plt.tight_layout()
+
+ plot_path = os.path.join(output_dir, "pareto_plot.png")
+ plt.savefig(plot_path, dpi=150, bbox_inches='tight')
+ plt.close()
+
+ if verbose:
+ print(f"[Pareto plot saved to {plot_path}]")
+
+ def _get_max_dimensions(self) -> Optional[dict]:
+ if not any([self._max_width_mm, self._max_height_mm, self._max_depth_mm]):
+ return None
+ dims = {}
+ if self._max_width_mm: dims["width"] = self._max_width_mm / 1000.0
+ if self._max_height_mm: dims["height"] = self._max_height_mm / 1000.0
+ if self._max_depth_mm: dims["depth"] = self._max_depth_mm / 1000.0
+ return dims
+
+ def _try_relaxed_constraints(self, max_results: int, core_mode: str,
+ verbose: bool, relax_step: float) -> list:
+ """Try relaxing constraints to find what's blocking the design."""
+ import time
+ import PyOpenMagnetics
+ from .results import DesignResult
+
+ if verbose:
+ print(f"\n[{time.strftime('%H:%M:%S')}] No designs found. Analyzing constraints...")
+
+ # Store original constraints
+ orig_height = self._max_height_mm
+ orig_width = self._max_width_mm
+ orig_depth = self._max_depth_mm
+
+
+ # Try relaxing dimensions iteratively
+ for iteration in range(1, 6): # Max 5 iterations (50% relaxation)
+ multiplier = 1 + (relax_step * iteration)
+
+ if orig_height:
+ self._max_height_mm = orig_height * multiplier
+ if orig_width:
+ self._max_width_mm = orig_width * multiplier
+ if orig_depth:
+ self._max_depth_mm = orig_depth * multiplier
+
+ if verbose:
+ relaxed = []
+ if orig_height:
+ relaxed.append(f"height: {orig_height:.1f}→{self._max_height_mm:.1f}mm")
+ if orig_width:
+ relaxed.append(f"width: {orig_width:.1f}→{self._max_width_mm:.1f}mm")
+ if orig_depth:
+ relaxed.append(f"depth: {orig_depth:.1f}→{self._max_depth_mm:.1f}mm")
+ print(f"[{time.strftime('%H:%M:%S')}] Iteration {iteration}: +{relax_step*iteration*100:.0f}% ({', '.join(relaxed)})")
+
+ try:
+ processed = PyOpenMagnetics.process_inputs(self.to_mas())
+ result = PyOpenMagnetics.calculate_advised_magnetics(processed, max_results, core_mode)
+
+ if isinstance(result, str):
+ parsed = json.loads(result)
+ elif isinstance(result, dict):
+ data = result.get("data", result)
+ if isinstance(data, str):
+ if data.startswith("Exception:"):
+ continue
+ parsed = json.loads(data)
+ else:
+ parsed = data if isinstance(data, list) else [data]
+ else:
+ parsed = result if isinstance(result, list) else [result] if result else []
+
+ design_results = [DesignResult.from_mas(r) for r in parsed
+ if isinstance(r, dict) and "magnetic" in r]
+
+ if design_results:
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] SUCCESS! Found {len(design_results)} designs")
+ print(f"\n{'='*60}")
+ print("BLOCKING CONSTRAINT ANALYSIS:")
+ print(f"{'='*60}")
+ if orig_height and self._max_height_mm > orig_height:
+ print(f" HEIGHT was too restrictive:")
+ print(f" Original: {orig_height:.1f} mm")
+ print(f" Required: >{orig_height:.1f} mm (relaxed to {self._max_height_mm:.1f} mm)")
+ if orig_width and self._max_width_mm > orig_width:
+ print(f" WIDTH was too restrictive:")
+ print(f" Original: {orig_width:.1f} mm")
+ print(f" Required: >{orig_width:.1f} mm (relaxed to {self._max_width_mm:.1f} mm)")
+ if orig_depth and self._max_depth_mm > orig_depth:
+ print(f" DEPTH was too restrictive:")
+ print(f" Original: {orig_depth:.1f} mm")
+ print(f" Required: >{orig_depth:.1f} mm (relaxed to {self._max_depth_mm:.1f} mm)")
+ print(f"\nSmallest core found: {design_results[0].core}")
+ print(f"{'='*60}\n")
+
+ # Restore original constraints
+ self._max_height_mm = orig_height
+ self._max_width_mm = orig_width
+ self._max_depth_mm = orig_depth
+ return design_results
+
+ except Exception as e:
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] Error: {e}")
+ continue
+
+ # Restore original constraints
+ self._max_height_mm = orig_height
+ self._max_width_mm = orig_width
+ self._max_depth_mm = orig_depth
+
+ if verbose:
+ print(f"[{time.strftime('%H:%M:%S')}] Could not find designs even with +50% relaxation")
+ print("\nPossible issues:")
+ print(" - Frequency may be too high for available core materials")
+ print(" - Power requirements may exceed smallest core capability")
+ print(" - Try different core_mode ('standard cores' vs 'available cores')")
+
+ return []
+
+
+# =============================================================================
+# Flyback Builder
+# =============================================================================
+
+class FlybackBuilder(TopologyBuilder):
+ """Flyback transformer design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._vin_min: Optional[float] = None
+ self._vin_max: Optional[float] = None
+ self._vin_is_ac: bool = False
+ self._outputs: list[dict] = []
+ self._frequency: float = 100e3
+ self._efficiency: float = 0.85
+ self._mode: str = "ccm"
+ self._isolation_type: Optional[str] = None
+ self._magnetizing_inductance: Optional[float] = None
+ self._turns_ratio: Optional[float] = None
+
+ def _topology_name(self) -> str: return "flyback"
+
+ def vin_ac(self, min_v: float, max_v: float) -> Self:
+ self._vin_min, self._vin_max, self._vin_is_ac = min_v, max_v, True
+ return self
+
+ def vin_dc(self, min_v: float, max_v: Optional[float] = None) -> Self:
+ self._vin_min, self._vin_max, self._vin_is_ac = min_v, max_v or min_v, False
+ return self
+
+ def output(self, voltage: float, current: float) -> Self:
+ self._outputs.append({"voltage": voltage, "current": current})
+ return self
+
+ def fsw(self, frequency: float) -> Self:
+ self._frequency = frequency
+ return self
+
+ def efficiency(self, target: float) -> Self:
+ self._efficiency = target
+ return self
+
+ def mode(self, mode: str) -> Self:
+ if mode not in ("ccm", "dcm", "bcm"):
+ raise ValueError(f"Invalid mode: {mode}")
+ self._mode = mode
+ return self
+
+ def isolation(self, insulation_type: str, standard: Optional[str] = None) -> Self:
+ self._isolation_type = insulation_type
+ return self
+
+ def _get_dc_bus_voltages(self) -> tuple[float, float]:
+ if self._vin_min is None:
+ raise ValueError("Input voltage not specified")
+ if self._vin_is_ac:
+ return self._vin_min * math.sqrt(2) * 0.9, self._vin_max * math.sqrt(2)
+ return self._vin_min, self._vin_max
+
+ def _calculate_total_output_power(self) -> float:
+ if not self._outputs:
+ raise ValueError("No outputs specified")
+ return sum(out["voltage"] * out["current"] for out in self._outputs)
+
+ def _calculate_turns_ratio(self, vin: float) -> float:
+ if self._turns_ratio: return self._turns_ratio
+ return (vin * 0.45) / (self._outputs[0]["voltage"] * 0.55)
+
+ def _calculate_duty_cycle(self, vin: float, n: float) -> float:
+ vout_reflected = n * self._outputs[0]["voltage"]
+ return vout_reflected / (vin + vout_reflected)
+
+ def _calculate_magnetizing_inductance(self, vin_min: float, n: float) -> float:
+ if self._magnetizing_inductance: return self._magnetizing_inductance
+ pout = self._calculate_total_output_power()
+ pin = pout / self._efficiency
+ d = self._calculate_duty_cycle(vin_min, n)
+ ton = d / self._frequency
+ if self._mode == "dcm":
+ ipk_target = 2.5 * (pin / vin_min)
+ return vin_min * ton / ipk_target
+ else:
+ i_avg = pin / vin_min
+ delta_i = 0.3 * 2 * i_avg
+ return vin_min * ton / delta_i
+
+ def _generate_design_requirements(self) -> dict:
+ vin_min, vin_max = self._get_dc_bus_voltages()
+ n = self._calculate_turns_ratio(vin_min)
+ lm = self._calculate_magnetizing_inductance(vin_min, n)
+ turns_ratios = [n]
+ if len(self._outputs) > 1:
+ for out in self._outputs[1:]:
+ turns_ratios.append(self._outputs[0]["voltage"] / out["voltage"])
+ insulation = waveforms.generate_insulation_requirements(self._isolation_type) if self._isolation_type else None
+ return waveforms.generate_design_requirements(lm, turns_ratios, insulation=insulation,
+ max_dimensions=self._get_max_dimensions(), name="Flyback Transformer")
+
+ def _generate_operating_points(self) -> list[dict]:
+ vin_min, vin_max = self._get_dc_bus_voltages()
+ n = self._calculate_turns_ratio(vin_min)
+ lm = self._calculate_magnetizing_inductance(vin_min, n)
+ pout = self._calculate_total_output_power()
+ vout = self._outputs[0]["voltage"]
+ ops = []
+ for vin, label in [(vin_min, "Low Line"), (vin_max, "High Line")]:
+ if vin == vin_max and vin_max <= vin_min * 1.1: continue
+ d = self._calculate_duty_cycle(vin, n)
+ primary_current = waveforms.flyback_primary_current(vin, vout, pout, n, lm, self._frequency, self._efficiency, self._mode)
+ primary_voltage = waveforms.rectangular_voltage(vin, 0, d, self._frequency)
+ excitations = [{"name": "Primary", "current": primary_current, "voltage": primary_voltage}]
+ for i, out in enumerate(self._outputs):
+ sec_current = waveforms.flyback_secondary_current(vin, out["voltage"], out["current"],
+ n if i == 0 else n * (vout / out["voltage"]), lm, self._frequency, self._mode)
+ excitations.append({"name": f"Secondary{i+1}" if len(self._outputs) > 1 else "Secondary", "current": sec_current})
+ ops.append(waveforms.generate_operating_point(self._frequency, excitations, label, self._ambient_temp))
+ return ops
+
+ def get_calculated_parameters(self) -> dict:
+ vin_min, vin_max = self._get_dc_bus_voltages()
+ n = self._calculate_turns_ratio(vin_min)
+ lm = self._calculate_magnetizing_inductance(vin_min, n)
+ return {"vin_dc_min": vin_min, "vin_dc_max": vin_max, "turns_ratio": n,
+ "magnetizing_inductance_uH": lm * 1e6, "duty_cycle_low_line": self._calculate_duty_cycle(vin_min, n),
+ "output_power_w": self._calculate_total_output_power(), "frequency_kHz": self._frequency / 1000}
+
+
+# =============================================================================
+# Buck Builder
+# =============================================================================
+
+class BuckBuilder(TopologyBuilder):
+ """Buck converter inductor design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._vin_min: Optional[float] = None
+ self._vin_max: Optional[float] = None
+ self._vout: Optional[float] = None
+ self._iout: Optional[float] = None
+ self._frequency: float = 100e3
+ self._ripple_ratio: float = 0.3
+ self._inductance: Optional[float] = None
+
+ def _topology_name(self) -> str: return "buck"
+
+ def vin(self, min_v: float, max_v: Optional[float] = None) -> Self:
+ self._vin_min, self._vin_max = min_v, max_v or min_v
+ return self
+
+ def vout(self, voltage: float) -> Self:
+ self._vout = voltage
+ return self
+
+ def iout(self, current: float) -> Self:
+ self._iout = current
+ return self
+
+ def fsw(self, frequency: float) -> Self:
+ self._frequency = frequency
+ return self
+
+ def ripple_ratio(self, ratio: float) -> Self:
+ self._ripple_ratio = ratio
+ return self
+
+ def inductance(self, value: float) -> Self:
+ self._inductance = value
+ return self
+
+ def _validate_params(self):
+ if self._vin_min is None: raise ValueError("Input voltage not specified")
+ if self._vout is None: raise ValueError("Output voltage not specified")
+ if self._iout is None: raise ValueError("Output current not specified")
+ if self._vout >= self._vin_min: raise ValueError("Buck: Vout must be less than Vin_min")
+
+ def _calculate_inductance(self) -> float:
+ if self._inductance: return self._inductance
+ d = self._vout / self._vin_max
+ ton = d / self._frequency
+ delta_i = self._ripple_ratio * self._iout
+ return (self._vin_max - self._vout) * ton / delta_i
+
+ def _generate_design_requirements(self) -> dict:
+ self._validate_params()
+ return waveforms.generate_design_requirements(self._calculate_inductance(), [],
+ max_dimensions=self._get_max_dimensions(), name="Buck Inductor")
+
+ def _generate_operating_points(self) -> list[dict]:
+ self._validate_params()
+ L = self._calculate_inductance()
+ ops = []
+ for vin, label in [(self._vin_max, "Max Vin"), (self._vin_min, "Min Vin")]:
+ if vin == self._vin_min and self._vin_max <= self._vin_min * 1.1: continue
+ current = waveforms.buck_inductor_current(vin, self._vout, self._iout, L, self._frequency)
+ voltage = waveforms.buck_inductor_voltage(vin, self._vout, self._frequency)
+ ops.append(waveforms.generate_operating_point(self._frequency,
+ [{"name": "Inductor", "current": current, "voltage": voltage}], label, self._ambient_temp))
+ return ops
+
+ def get_calculated_parameters(self) -> dict:
+ self._validate_params()
+ L = self._calculate_inductance()
+ d_max = self._vout / self._vin_min # Duty cycle at min Vin
+ delta_i = self._ripple_ratio * self._iout
+ i_peak = self._iout + delta_i / 2
+ return {"vin_min": self._vin_min, "vin_max": self._vin_max, "vout": self._vout, "iout": self._iout,
+ "inductance_uH": L * 1e6, "output_power_w": self._vout * self._iout, "frequency_kHz": self._frequency / 1000,
+ "duty_cycle": d_max, "i_ripple_pp": delta_i, "i_peak": i_peak}
+
+
+# =============================================================================
+# Boost Builder
+# =============================================================================
+
+class BoostBuilder(TopologyBuilder):
+ """Boost converter inductor design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._vin_min: Optional[float] = None
+ self._vin_max: Optional[float] = None
+ self._vout: Optional[float] = None
+ self._pout: Optional[float] = None
+ self._frequency: float = 100e3
+ self._ripple_ratio: float = 0.3
+ self._inductance: Optional[float] = None
+ self._efficiency: float = 0.9
+
+ def _topology_name(self) -> str: return "boost"
+
+ def vin(self, min_v: float, max_v: Optional[float] = None) -> Self:
+ self._vin_min, self._vin_max = min_v, max_v or min_v
+ return self
+
+ def vout(self, voltage: float) -> Self:
+ self._vout = voltage
+ return self
+
+ def pout(self, power: float) -> Self:
+ self._pout = power
+ return self
+
+ def fsw(self, frequency: float) -> Self:
+ self._frequency = frequency
+ return self
+
+ def efficiency(self, target: float) -> Self:
+ self._efficiency = target
+ return self
+
+ def _validate_params(self):
+ if self._vin_min is None: raise ValueError("Input voltage not specified")
+ if self._vout is None: raise ValueError("Output voltage not specified")
+ if self._pout is None: raise ValueError("Output power not specified")
+ if self._vout <= self._vin_max: raise ValueError("Boost: Vout must be greater than Vin_max")
+
+ def _calculate_inductance(self) -> float:
+ if self._inductance: return self._inductance
+ d = 1 - (self._vin_min / self._vout)
+ ton = d / self._frequency
+ pin = self._pout / self._efficiency
+ iin_avg = pin / self._vin_min
+ delta_i = self._ripple_ratio * iin_avg
+ return self._vin_min * ton / delta_i
+
+ def _generate_design_requirements(self) -> dict:
+ self._validate_params()
+ return waveforms.generate_design_requirements(self._calculate_inductance(), [],
+ max_dimensions=self._get_max_dimensions(), name="Boost Inductor")
+
+ def _generate_operating_points(self) -> list[dict]:
+ self._validate_params()
+ L = self._calculate_inductance()
+ ops = []
+ for vin, label in [(self._vin_min, "Min Vin"), (self._vin_max, "Max Vin")]:
+ if vin == self._vin_max and self._vin_max <= self._vin_min * 1.1: continue
+ current = waveforms.boost_inductor_current(vin, self._vout, self._pout, L, self._frequency, self._efficiency)
+ voltage = waveforms.boost_inductor_voltage(vin, self._vout, self._frequency)
+ ops.append(waveforms.generate_operating_point(self._frequency,
+ [{"name": "Inductor", "current": current, "voltage": voltage}], label, self._ambient_temp))
+ return ops
+
+ def get_calculated_parameters(self) -> dict:
+ self._validate_params()
+ return {"vin_min": self._vin_min, "vin_max": self._vin_max, "vout": self._vout, "pout": self._pout,
+ "inductance_uH": self._calculate_inductance() * 1e6, "frequency_kHz": self._frequency / 1000}
+
+
+# =============================================================================
+# Inductor Builder
+# =============================================================================
+
+class InductorBuilder(TopologyBuilder):
+ """Standalone inductor design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._inductance: Optional[float] = None
+ self._tolerance: float = 0.1
+ self._idc: float = 0.0
+ self._iac_pp: float = 0.0
+ self._iac_rms: float = 0.0
+ self._frequency: float = 100e3
+ self._duty_cycle: float = 0.5
+ self._waveform_type: str = "triangular"
+
+ def _topology_name(self) -> str: return "inductor"
+
+ def inductance(self, value: float, tolerance: float = 0.1) -> Self:
+ self._inductance, self._tolerance = value, tolerance
+ return self
+
+ def idc(self, current: float) -> Self:
+ self._idc = current
+ return self
+
+ def iac_pp(self, current: float) -> Self:
+ self._iac_pp = current
+ self._iac_rms = current / (2 * math.sqrt(3))
+ return self
+
+ def fsw(self, frequency: float) -> Self:
+ self._frequency = frequency
+ return self
+
+ def duty_cycle(self, duty: float) -> Self:
+ self._duty_cycle = duty
+ return self
+
+ def _validate_params(self):
+ if self._inductance is None: raise ValueError("Inductance not specified")
+
+ def _generate_design_requirements(self) -> dict:
+ self._validate_params()
+ return waveforms.generate_design_requirements(self._inductance, [],
+ max_dimensions=self._get_max_dimensions(), name="Inductor", tolerance=self._tolerance)
+
+ def _generate_operating_points(self) -> list[dict]:
+ self._validate_params()
+ if self._waveform_type == "sinusoidal":
+ current = waveforms.sinusoidal_current(self._iac_rms or 0.1, self._frequency, self._idc)
+ else:
+ current = waveforms.triangular_current(self._idc, self._iac_pp or 0.1, self._duty_cycle, self._frequency)
+ return [waveforms.generate_operating_point(self._frequency, [{"name": "Inductor", "current": current}],
+ "Operating Point", self._ambient_temp)]
+
+ def get_calculated_parameters(self) -> dict:
+ self._validate_params()
+ return {"inductance_uH": self._inductance * 1e6, "i_dc": self._idc, "i_ripple_pp": self._iac_pp,
+ "i_peak": self._idc + self._iac_pp / 2, "frequency_kHz": self._frequency / 1000}
+
+
+# =============================================================================
+# Forward Builder
+# =============================================================================
+
+class ForwardBuilder(TopologyBuilder):
+ """Forward converter transformer design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._variant: str = "two_switch"
+ self._vin_min: Optional[float] = None
+ self._vin_max: Optional[float] = None
+ self._outputs: list[dict] = []
+ self._frequency: float = 100e3
+ self._efficiency: float = 0.9
+ self._max_duty: float = 0.45
+ self._magnetizing_inductance: Optional[float] = None
+
+ def _topology_name(self) -> str: return f"forward_{self._variant}"
+
+ def variant(self, variant_type: str) -> Self:
+ if variant_type not in ("single_switch", "two_switch", "active_clamp"):
+ raise ValueError(f"Invalid variant: {variant_type}")
+ self._variant = variant_type
+ self._max_duty = 0.45 if variant_type == "single_switch" else 0.5
+ return self
+
+ def vin_dc(self, min_v: float, max_v: Optional[float] = None) -> Self:
+ self._vin_min, self._vin_max = min_v, max_v or min_v
+ return self
+
+ def output(self, voltage: float, current: float) -> Self:
+ self._outputs.append({"voltage": voltage, "current": current})
+ return self
+
+ def fsw(self, frequency: float) -> Self:
+ self._frequency = frequency
+ return self
+
+ def _validate_params(self):
+ if self._vin_min is None: raise ValueError("Input voltage not specified")
+ if not self._outputs: raise ValueError("No outputs specified")
+
+ def _calculate_turns_ratio(self) -> float:
+ return self._outputs[0]["voltage"] / (self._vin_max * self._max_duty)
+
+ def _calculate_magnetizing_inductance(self) -> float:
+ if self._magnetizing_inductance: return self._magnetizing_inductance
+ pout = sum(o["voltage"] * o["current"] for o in self._outputs)
+ pin = pout / self._efficiency
+ i_pri_avg = pin / self._vin_min
+ i_mag_target = 0.05 * i_pri_avg
+ ton = self._max_duty / self._frequency
+ return self._vin_min * ton / (2 * i_mag_target)
+
+ def _generate_design_requirements(self) -> dict:
+ self._validate_params()
+ n = self._calculate_turns_ratio()
+ lm = self._calculate_magnetizing_inductance()
+ turns_ratios = [n] + [n * (o["voltage"] / self._outputs[0]["voltage"]) for o in self._outputs[1:]]
+ return waveforms.generate_design_requirements(lm, turns_ratios, max_dimensions=self._get_max_dimensions(),
+ name=f"Forward Transformer ({self._variant})")
+
+ def _generate_operating_points(self) -> list[dict]:
+ self._validate_params()
+ n = self._calculate_turns_ratio()
+ lm = self._calculate_magnetizing_inductance()
+ ops = []
+ for vin, label in [(self._vin_min, "Min Vin"), (self._vin_max, "Max Vin")]:
+ if vin == self._vin_max and self._vin_max <= self._vin_min * 1.1: continue
+ d = min((self._outputs[0]["voltage"] / vin) / n, self._max_duty)
+ ton, period = d / self._frequency, 1 / self._frequency
+ i_sec = self._outputs[0]["current"]
+ i_pri = i_sec * n
+ delta_i_mag = vin * ton / lm
+ primary_current = {"waveform": {"data": [i_pri - delta_i_mag/2, i_pri + delta_i_mag/2, 0, 0],
+ "time": [0, ton, ton, period]}}
+ primary_voltage = waveforms.rectangular_voltage(vin, 0, d, self._frequency)
+ excitations = [{"name": "Primary", "current": primary_current, "voltage": primary_voltage}]
+ for i, out in enumerate(self._outputs):
+ sec_current = {"waveform": {"data": [out["current"], out["current"], 0, 0], "time": [0, ton, ton, period]}}
+ excitations.append({"name": f"Secondary{i+1}" if len(self._outputs) > 1 else "Secondary", "current": sec_current})
+ ops.append(waveforms.generate_operating_point(self._frequency, excitations, label, self._ambient_temp))
+ return ops
+
+ def get_calculated_parameters(self) -> dict:
+ self._validate_params()
+ return {"variant": self._variant, "turns_ratio": self._calculate_turns_ratio(),
+ "magnetizing_inductance_mH": self._calculate_magnetizing_inductance() * 1e3,
+ "frequency_kHz": self._frequency / 1000}
+
+
+# =============================================================================
+# LLC Builder
+# =============================================================================
+
+class LLCBuilder(TopologyBuilder):
+ """LLC resonant converter transformer design builder."""
+
+ def __init__(self):
+ super().__init__()
+ self._vin_min: Optional[float] = None
+ self._vin_max: Optional[float] = None
+ self._outputs: list[dict] = []
+ self._resonant_freq: Optional[float] = None
+ self._magnetizing_inductance: Optional[float] = None
+ self._leakage_inductance: Optional[float] = None
+ self._quality_factor: float = 0.3
+ self._efficiency: float = 0.95
+
+ def _topology_name(self) -> str: return "llc"
+
+ def vin_dc(self, min_v: float, max_v: Optional[float] = None) -> Self:
+ self._vin_min, self._vin_max = min_v, max_v or min_v
+ return self
+
+ def output(self, voltage: float, current: float) -> Self:
+ self._outputs.append({"voltage": voltage, "current": current})
+ return self
+
+ def resonant_frequency(self, freq: float) -> Self:
+ self._resonant_freq = freq
+ return self
+
+ def quality_factor(self, q: float) -> Self:
+ self._quality_factor = q
+ return self
+
+ def _validate_params(self):
+ if self._vin_min is None: raise ValueError("Input voltage not specified")
+ if not self._outputs: raise ValueError("No outputs specified")
+ if self._resonant_freq is None: raise ValueError("Resonant frequency not specified")
+
+ def _calculate_turns_ratio(self) -> float:
+ vin_nom = (self._vin_min + self._vin_max) / 2
+ return vin_nom / (2 * self._outputs[0]["voltage"])
+
+ def _calculate_leakage_inductance(self) -> float:
+ if self._leakage_inductance: return self._leakage_inductance
+ n = self._calculate_turns_ratio()
+ rac = (8 / (math.pi**2)) * (n**2) * self._outputs[0]["voltage"] / self._outputs[0]["current"]
+ return self._quality_factor * rac / (2 * math.pi * self._resonant_freq)
+
+ def _calculate_magnetizing_inductance(self) -> float:
+ if self._magnetizing_inductance: return self._magnetizing_inductance
+ return self._calculate_leakage_inductance() * 7
+
+ def _generate_design_requirements(self) -> dict:
+ self._validate_params()
+ n = self._calculate_turns_ratio()
+ turns_ratios = [n] + [n * (o["voltage"] / self._outputs[0]["voltage"]) for o in self._outputs[1:]]
+ return waveforms.generate_design_requirements(self._calculate_magnetizing_inductance(), turns_ratios,
+ leakage_inductance=self._calculate_leakage_inductance(), max_dimensions=self._get_max_dimensions(),
+ name="LLC Transformer")
+
+ def _generate_operating_points(self) -> list[dict]:
+ self._validate_params()
+ n = self._calculate_turns_ratio()
+ lm = self._calculate_magnetizing_inductance()
+ i_load_reflected = (self._outputs[0]["current"] * 2 * math.sqrt(2) / math.pi) / n
+ vin_nom = (self._vin_min + self._vin_max) / 2
+ i_mag_pk = vin_nom / (4 * lm * self._resonant_freq)
+ i_pri_rms = math.sqrt((i_load_reflected / math.sqrt(2))**2 + (i_mag_pk / math.sqrt(2))**2)
+ primary_current = waveforms.sinusoidal_current(i_pri_rms, self._resonant_freq)
+ # LLC primary voltage is approximately sinusoidal at resonance
+ v_pri_pk = vin_nom / 2 # Half-bridge LLC
+ primary_voltage = waveforms.sinusoidal_current(v_pri_pk / math.sqrt(2), self._resonant_freq) # RMS
+ excitations = [{"name": "Primary", "current": primary_current, "voltage": primary_voltage}]
+ for i, out in enumerate(self._outputs):
+ i_sec_rms = out["current"] * math.pi / (2 * math.sqrt(2))
+ excitations.append({"name": f"Secondary{i+1}" if len(self._outputs) > 1 else "Secondary",
+ "current": waveforms.sinusoidal_current(i_sec_rms, self._resonant_freq)})
+ return [waveforms.generate_operating_point(self._resonant_freq, excitations, "Resonant Operation", self._ambient_temp)]
+
+ def get_calculated_parameters(self) -> dict:
+ self._validate_params()
+ return {"turns_ratio": self._calculate_turns_ratio(),
+ "magnetizing_inductance_uH": self._calculate_magnetizing_inductance() * 1e6,
+ "leakage_inductance_uH": self._calculate_leakage_inductance() * 1e6,
+ "resonant_frequency_kHz": self._resonant_freq / 1000}
+
+
+# =============================================================================
+# Design Factory
+# =============================================================================
+
+class Design:
+ """Factory class for creating topology builders."""
+
+ @staticmethod
+ def flyback() -> FlybackBuilder: return FlybackBuilder()
+
+ @staticmethod
+ def buck() -> BuckBuilder: return BuckBuilder()
+
+ @staticmethod
+ def boost() -> BoostBuilder: return BoostBuilder()
+
+ @staticmethod
+ def inductor() -> InductorBuilder: return InductorBuilder()
+
+ @staticmethod
+ def forward() -> ForwardBuilder: return ForwardBuilder()
+
+ @staticmethod
+ def llc() -> LLCBuilder: return LLCBuilder()
diff --git a/api/models/__init__.py b/api/models/__init__.py
new file mode 100644
index 0000000..61cde1e
--- /dev/null
+++ b/api/models/__init__.py
@@ -0,0 +1,73 @@
+"""
+Data models for magnetic component design.
+
+This module provides dataclass models for:
+- PWM and resonant topologies (Buck, Boost, Flyback, LLC, etc.)
+- Voltage, current, and power specifications
+- Operating points and waveforms
+"""
+
+from .topology import (
+ ModulationType,
+ OperatingMode,
+ PWMTopology,
+ BuckTopology,
+ BoostTopology,
+ BuckBoostTopology,
+ FlybackTopology,
+ ForwardTopology,
+ PushPullTopology,
+ FullBridgeTopology,
+ ResonantTopology,
+ LLCTopology,
+ LCCTopology,
+)
+
+from .specs import (
+ ACDCType,
+ VoltageSpec,
+ CurrentSpec,
+ PowerSpec,
+ PortSpec,
+ PowerSupplySpec,
+)
+
+from .operating_point import (
+ WaveformType,
+ Waveform,
+ WindingExcitation,
+ OperatingPointModel,
+)
+
+__all__ = [
+ # Topology enums
+ "ModulationType",
+ "OperatingMode",
+ # PWM topologies
+ "PWMTopology",
+ "BuckTopology",
+ "BoostTopology",
+ "BuckBoostTopology",
+ "FlybackTopology",
+ "ForwardTopology",
+ "PushPullTopology",
+ "FullBridgeTopology",
+ # Resonant topologies
+ "ResonantTopology",
+ "LLCTopology",
+ "LCCTopology",
+ # Specification enums
+ "ACDCType",
+ # Specification dataclasses
+ "VoltageSpec",
+ "CurrentSpec",
+ "PowerSpec",
+ "PortSpec",
+ "PowerSupplySpec",
+ # Operating point enums
+ "WaveformType",
+ # Operating point dataclasses
+ "Waveform",
+ "WindingExcitation",
+ "OperatingPointModel",
+]
diff --git a/api/models/operating_point.py b/api/models/operating_point.py
new file mode 100644
index 0000000..7bd1604
--- /dev/null
+++ b/api/models/operating_point.py
@@ -0,0 +1,221 @@
+"""
+Waveform and Operating Point dataclass models.
+
+Provides structured representations for periodic waveforms and
+operating point specifications used in magnetic component simulation.
+"""
+
+from dataclasses import dataclass, field
+from typing import List, Optional
+from enum import Enum
+
+
+class WaveformType(Enum):
+ """Waveform shape type."""
+ TRIANGULAR = "triangular"
+ SINUSOIDAL = "sinusoidal"
+ RECTANGULAR = "rectangular"
+ TRAPEZOIDAL = "trapezoidal"
+ CUSTOM = "custom"
+
+
+@dataclass
+class Waveform:
+ """Generic periodic waveform.
+
+ Represents a waveform as sample points over one period.
+
+ Attributes:
+ data: Sample values (voltage or current)
+ time: Time points in seconds
+ waveform_type: Type of waveform
+ """
+ data: List[float] # Sample values
+ time: List[float] # Time points (s)
+ waveform_type: WaveformType = WaveformType.CUSTOM
+
+ @classmethod
+ def triangular(cls, v_min: float, v_max: float, duty: float,
+ period: float) -> "Waveform":
+ """Create triangular waveform.
+
+ Args:
+ v_min: Minimum value (at start)
+ v_max: Maximum value (at peak)
+ duty: Duty cycle (rise time / period)
+ period: Period in seconds
+
+ Returns:
+ Waveform with triangular shape
+ """
+ t_rise = duty * period
+ return cls(
+ data=[v_min, v_max, v_min],
+ time=[0.0, t_rise, period],
+ waveform_type=WaveformType.TRIANGULAR
+ )
+
+ @classmethod
+ def rectangular(cls, v_high: float, v_low: float, duty: float,
+ period: float) -> "Waveform":
+ """Create rectangular (square) waveform.
+
+ Args:
+ v_high: High level value
+ v_low: Low level value
+ duty: Duty cycle (high time / period)
+ period: Period in seconds
+
+ Returns:
+ Waveform with rectangular shape
+ """
+ t_on = duty * period
+ return cls(
+ data=[v_high, v_high, v_low, v_low],
+ time=[0.0, t_on, t_on, period],
+ waveform_type=WaveformType.RECTANGULAR
+ )
+
+ @classmethod
+ def sinusoidal(cls, amplitude: float, frequency: float, dc_offset: float = 0.0,
+ num_points: int = 64) -> "Waveform":
+ """Create sinusoidal waveform.
+
+ Args:
+ amplitude: Peak amplitude
+ frequency: Frequency in Hz
+ dc_offset: DC offset value
+ num_points: Number of sample points
+
+ Returns:
+ Waveform with sinusoidal shape
+ """
+ import math
+ period = 1.0 / frequency
+ data = []
+ time = []
+ for i in range(num_points + 1):
+ t = i * period / num_points
+ v = dc_offset + amplitude * math.sin(2 * math.pi * frequency * t)
+ time.append(t)
+ data.append(v)
+ return cls(
+ data=data,
+ time=time,
+ waveform_type=WaveformType.SINUSOIDAL
+ )
+
+ def to_dict(self) -> dict:
+ """Convert to MAS-compatible dict format."""
+ return {
+ "waveform": {
+ "data": self.data,
+ "time": self.time
+ }
+ }
+
+
+@dataclass
+class WindingExcitation:
+ """Excitation for a single winding.
+
+ Attributes:
+ name: Winding name (e.g., "Primary", "Secondary")
+ current: Current waveform (optional)
+ voltage: Voltage waveform (optional)
+ frequency_hz: Operating frequency in Hz
+ """
+ name: str = "Primary"
+ current: Optional[Waveform] = None
+ voltage: Optional[Waveform] = None
+ frequency_hz: float = 100000
+
+ def to_dict(self) -> dict:
+ """Convert to MAS-compatible dict format."""
+ result = {
+ "name": self.name,
+ "frequency": self.frequency_hz,
+ }
+ if self.current:
+ result["current"] = self.current.to_dict()
+ if self.voltage:
+ result["voltage"] = self.voltage.to_dict()
+ return result
+
+
+@dataclass
+class OperatingPointModel:
+ """Complete operating point specification.
+
+ Defines a single operating condition with all winding excitations.
+
+ Attributes:
+ name: Operating point name (e.g., "Low Line", "Full Load")
+ excitations: List of winding excitations
+ ambient_temperature_c: Ambient temperature in Celsius
+ weight: Weighting factor for multi-OP analysis
+ """
+ name: str = "Operating Point"
+ excitations: List[WindingExcitation] = field(default_factory=list)
+ ambient_temperature_c: float = 25.0
+ weight: float = 1.0 # For multi-OP weighted analysis
+
+ def to_mas(self) -> dict:
+ """Convert to MAS-compliant dict.
+
+ Returns:
+ Dict in MAS operatingPoint format
+ """
+ return {
+ "name": self.name,
+ "conditions": {
+ "ambientTemperature": self.ambient_temperature_c
+ },
+ "excitationsPerWinding": [exc.to_dict() for exc in self.excitations]
+ }
+
+ @classmethod
+ def from_mas(cls, mas_op: dict) -> "OperatingPointModel":
+ """Create from MAS operatingPoint dict.
+
+ Args:
+ mas_op: MAS operating point dict
+
+ Returns:
+ OperatingPointModel instance
+ """
+ conditions = mas_op.get("conditions", {})
+ excitations = []
+
+ for exc_data in mas_op.get("excitationsPerWinding", []):
+ current = None
+ voltage = None
+
+ if "current" in exc_data and exc_data["current"]:
+ curr_wf = exc_data["current"].get("waveform", {})
+ if curr_wf:
+ current = Waveform(
+ data=curr_wf.get("data", []),
+ time=curr_wf.get("time", [])
+ )
+
+ if "voltage" in exc_data and exc_data["voltage"]:
+ volt_wf = exc_data["voltage"].get("waveform", {})
+ if volt_wf:
+ voltage = Waveform(
+ data=volt_wf.get("data", []),
+ time=volt_wf.get("time", [])
+ )
+
+ excitations.append(WindingExcitation(
+ name=exc_data.get("name", "Winding"),
+ current=current,
+ voltage=voltage,
+ frequency_hz=exc_data.get("frequency", 100000)
+ ))
+
+ return cls(
+ name=mas_op.get("name", "Operating Point"),
+ excitations=excitations,
+ ambient_temperature_c=conditions.get("ambientTemperature", 25.0)
+ )
diff --git a/api/models/specs.py b/api/models/specs.py
new file mode 100644
index 0000000..177cd96
--- /dev/null
+++ b/api/models/specs.py
@@ -0,0 +1,238 @@
+"""
+Voltage, Current, and Power specification dataclass models.
+
+Provides complete electrical specifications with all measurement types
+(nominal, min, max, RMS, peak, peak-to-peak, etc.) for power supply design.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional, List
+from enum import Enum
+import math
+
+
+class ACDCType(Enum):
+ """AC or DC voltage/current type."""
+ AC = "ac"
+ DC = "dc"
+
+
+@dataclass
+class VoltageSpec:
+ """Complete voltage specification with all measurement types.
+
+ Attributes:
+ nominal: Nominal/typical value in Volts
+ min: Minimum value in Volts
+ max: Maximum value in Volts
+ rms: RMS value in Volts (for AC)
+ avg: Average value in Volts
+ peak: Peak value in Volts
+ peak_to_peak: Peak-to-peak value in Volts
+ ac_dc: AC or DC type
+ """
+ nominal: float # Nominal/typical value (V)
+ min: Optional[float] = None # Minimum value (V)
+ max: Optional[float] = None # Maximum value (V)
+ rms: Optional[float] = None # RMS value (V) - for AC
+ avg: Optional[float] = None # Average value (V)
+ peak: Optional[float] = None # Peak value (V)
+ peak_to_peak: Optional[float] = None # Peak-to-peak (V)
+ ac_dc: ACDCType = ACDCType.DC
+
+ @classmethod
+ def dc(cls, nominal: float, tolerance_pct: float = 5.0) -> "VoltageSpec":
+ """Create DC voltage spec with tolerance.
+
+ Args:
+ nominal: Nominal voltage in Volts
+ tolerance_pct: Tolerance percentage (default 5%)
+
+ Returns:
+ VoltageSpec configured for DC with min/max from tolerance
+ """
+ delta = nominal * tolerance_pct / 100
+ return cls(
+ nominal=nominal,
+ min=nominal - delta,
+ max=nominal + delta,
+ avg=nominal,
+ ac_dc=ACDCType.DC
+ )
+
+ @classmethod
+ def dc_range(cls, v_min: float, v_max: float) -> "VoltageSpec":
+ """Create DC voltage spec from min/max range.
+
+ Args:
+ v_min: Minimum voltage in Volts
+ v_max: Maximum voltage in Volts
+
+ Returns:
+ VoltageSpec with nominal at midpoint
+ """
+ return cls(
+ nominal=(v_min + v_max) / 2,
+ min=v_min,
+ max=v_max,
+ avg=(v_min + v_max) / 2,
+ ac_dc=ACDCType.DC
+ )
+
+ @classmethod
+ def ac(cls, v_rms: float, v_min_rms: Optional[float] = None,
+ v_max_rms: Optional[float] = None) -> "VoltageSpec":
+ """Create AC voltage spec.
+
+ Args:
+ v_rms: RMS voltage in Volts
+ v_min_rms: Minimum RMS voltage (optional)
+ v_max_rms: Maximum RMS voltage (optional)
+
+ Returns:
+ VoltageSpec configured for AC with peak values calculated
+ """
+ v_peak = v_rms * math.sqrt(2)
+ return cls(
+ nominal=v_rms,
+ min=v_min_rms,
+ max=v_max_rms,
+ rms=v_rms,
+ peak=v_peak,
+ peak_to_peak=2 * v_peak,
+ ac_dc=ACDCType.AC
+ )
+
+
+@dataclass
+class CurrentSpec:
+ """Complete current specification with all measurement types.
+
+ Attributes:
+ nominal: Nominal/typical value in Amps
+ min: Minimum value in Amps
+ max: Maximum value in Amps
+ rms: RMS value in Amps
+ avg: Average value in Amps
+ peak: Peak value in Amps
+ peak_to_peak: Peak-to-peak ripple in Amps
+ dc_bias: DC component in Amps
+ """
+ nominal: float # Nominal/typical value (A)
+ min: Optional[float] = None # Minimum value (A)
+ max: Optional[float] = None # Maximum value (A)
+ rms: Optional[float] = None # RMS value (A)
+ avg: Optional[float] = None # Average value (A)
+ peak: Optional[float] = None # Peak value (A)
+ peak_to_peak: Optional[float] = None # Peak-to-peak ripple (A)
+ dc_bias: Optional[float] = None # DC component (A)
+
+ @classmethod
+ def dc(cls, i_dc: float, ripple_pp: float = 0.0) -> "CurrentSpec":
+ """Create DC current spec with optional ripple.
+
+ Args:
+ i_dc: DC current in Amps
+ ripple_pp: Peak-to-peak ripple current (default 0)
+
+ Returns:
+ CurrentSpec with RMS calculated for triangle wave on DC
+ """
+ i_peak = i_dc + ripple_pp / 2
+ i_min = i_dc - ripple_pp / 2
+ # RMS of triangle wave on DC: sqrt(Idc^2 + (Ipp^2/12))
+ i_rms = math.sqrt(i_dc**2 + (ripple_pp**2 / 12))
+ return cls(
+ nominal=i_dc,
+ min=i_min,
+ max=i_peak,
+ rms=i_rms,
+ avg=i_dc,
+ peak=i_peak,
+ peak_to_peak=ripple_pp,
+ dc_bias=i_dc
+ )
+
+
+@dataclass
+class PowerSpec:
+ """Power specification.
+
+ Attributes:
+ nominal_w: Nominal power in Watts
+ min_w: Minimum power in Watts
+ max_w: Maximum power in Watts
+ """
+ nominal_w: float # Nominal power (W)
+ min_w: Optional[float] = None # Minimum power (W)
+ max_w: Optional[float] = None # Maximum power (W)
+
+ @classmethod
+ def from_vi(cls, voltage: VoltageSpec, current: CurrentSpec) -> "PowerSpec":
+ """Calculate power from voltage and current specs.
+
+ Args:
+ voltage: Voltage specification
+ current: Current specification
+
+ Returns:
+ PowerSpec with nominal/min/max calculated from V*I
+ """
+ p_nom = voltage.nominal * current.nominal
+ p_min = (voltage.min or voltage.nominal) * (current.min or 0)
+ p_max = (voltage.max or voltage.nominal) * (current.max or current.nominal)
+ return cls(nominal_w=p_nom, min_w=p_min, max_w=p_max)
+
+
+@dataclass
+class PortSpec:
+ """Input or output port specification.
+
+ Attributes:
+ name: Port name (e.g., "Input", "Output 1")
+ voltage: Voltage specification
+ current: Current specification
+ """
+ name: str = "port"
+ voltage: VoltageSpec = field(default_factory=lambda: VoltageSpec(nominal=0))
+ current: CurrentSpec = field(default_factory=lambda: CurrentSpec(nominal=0))
+
+ @property
+ def power(self) -> PowerSpec:
+ """Calculate power from voltage and current."""
+ return PowerSpec.from_vi(self.voltage, self.current)
+
+
+@dataclass
+class PowerSupplySpec:
+ """Complete power supply specification.
+
+ Defines a complete power supply with inputs, outputs, and targets.
+
+ Attributes:
+ name: Power supply name/description
+ inputs: List of input port specifications
+ outputs: List of output port specifications
+ efficiency: Target efficiency (0-1)
+ isolation_v: Isolation voltage in Volts, None for non-isolated
+ """
+ name: str = "Power Supply"
+ inputs: List[PortSpec] = field(default_factory=list)
+ outputs: List[PortSpec] = field(default_factory=list)
+ efficiency: float = 0.90 # Target efficiency
+ isolation_v: Optional[float] = None # Isolation voltage (V), None = non-isolated
+
+ @property
+ def total_output_power(self) -> float:
+ """Calculate total output power in Watts."""
+ return sum(p.power.nominal_w for p in self.outputs)
+
+ @property
+ def total_input_power(self) -> float:
+ """Calculate required input power based on output and efficiency."""
+ return self.total_output_power / self.efficiency
+
+ @property
+ def is_isolated(self) -> bool:
+ """Check if this is an isolated power supply."""
+ return self.isolation_v is not None
diff --git a/api/models/topology.py b/api/models/topology.py
new file mode 100644
index 0000000..cd100f8
--- /dev/null
+++ b/api/models/topology.py
@@ -0,0 +1,235 @@
+"""
+PWM and Resonant topology dataclass models.
+
+Provides structured representations of power converter topologies
+with their key parameters and operating characteristics.
+"""
+
+from dataclasses import dataclass
+from typing import Optional, Literal
+from enum import Enum
+
+
+class ModulationType(Enum):
+ """Modulation type for power converters."""
+ PWM = "pwm"
+ PFM = "pfm"
+ HYBRID = "hybrid"
+
+
+class OperatingMode(Enum):
+ """Inductor/transformer operating mode."""
+ CCM = "ccm" # Continuous conduction mode
+ DCM = "dcm" # Discontinuous conduction mode
+ BCM = "bcm" # Boundary/critical conduction mode
+
+
+# =============================================================================
+# PWM Topologies
+# =============================================================================
+
+@dataclass
+class PWMTopology:
+ """Base class for PWM-based topologies."""
+ name: str
+ fsw_hz: float # Switching frequency
+ modulation: ModulationType = ModulationType.PWM
+ mode: OperatingMode = OperatingMode.CCM
+
+
+@dataclass
+class BuckTopology(PWMTopology):
+ """Buck (step-down) converter topology.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ duty_cycle: Calculated from Vout/Vin
+ sync_rectification: Use synchronous rectification (SR MOSFET)
+ """
+ name: str = "buck"
+ fsw_hz: float = 100e3
+ duty_cycle: Optional[float] = None
+ sync_rectification: bool = False
+
+
+@dataclass
+class BoostTopology(PWMTopology):
+ """Boost (step-up) converter topology.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ duty_cycle: Calculated from 1 - Vin/Vout
+ """
+ name: str = "boost"
+ fsw_hz: float = 100e3
+ duty_cycle: Optional[float] = None
+
+
+@dataclass
+class BuckBoostTopology(PWMTopology):
+ """Buck-Boost (inverting) converter topology.
+
+ Can step voltage up or down with inverted polarity.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ duty_cycle: Calculated from Vout/(Vin + Vout)
+ """
+ name: str = "buck_boost"
+ fsw_hz: float = 100e3
+ duty_cycle: Optional[float] = None
+
+
+@dataclass
+class FlybackTopology(PWMTopology):
+ """Flyback isolated converter topology.
+
+ Most common isolated topology for <150W applications.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ turns_ratio: Primary to secondary turns ratio (Np/Ns)
+ clamp_type: Snubber/clamp circuit type
+ max_duty: Maximum duty cycle (typically 0.45-0.5)
+ """
+ name: str = "flyback"
+ fsw_hz: float = 100e3
+ turns_ratio: float = 1.0
+ clamp_type: Literal["rcd", "active", "none"] = "rcd"
+ max_duty: float = 0.5
+
+
+@dataclass
+class ForwardTopology(PWMTopology):
+ """Forward isolated converter topology.
+
+ Higher power density than flyback, requires output inductor.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ turns_ratio: Primary to secondary turns ratio
+ variant: Single-switch, two-switch, or active clamp
+ reset_method: Core reset mechanism
+ max_duty: Maximum duty cycle
+ """
+ name: str = "forward"
+ fsw_hz: float = 100e3
+ turns_ratio: float = 1.0
+ variant: Literal["single", "two_switch", "active_clamp"] = "two_switch"
+ reset_method: Literal["tertiary", "active_clamp", "rcd"] = "tertiary"
+ max_duty: float = 0.5
+
+
+@dataclass
+class PushPullTopology(PWMTopology):
+ """Push-Pull isolated converter topology.
+
+ Good for low input voltage applications (12V, 24V).
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ turns_ratio: Primary to secondary turns ratio
+ max_duty: Maximum duty cycle per switch (total effective = 2x)
+ """
+ name: str = "push_pull"
+ fsw_hz: float = 100e3
+ turns_ratio: float = 1.0
+ max_duty: float = 0.45 # Per switch
+
+
+@dataclass
+class FullBridgeTopology(PWMTopology):
+ """Full-Bridge isolated converter topology.
+
+ High power topology with 4 switches on primary side.
+
+ Attributes:
+ fsw_hz: Switching frequency in Hz
+ turns_ratio: Primary to secondary turns ratio
+ phase_shift: Use phase-shifted ZVS control
+ max_duty: Maximum effective duty cycle
+ """
+ name: str = "full_bridge"
+ fsw_hz: float = 100e3
+ turns_ratio: float = 1.0
+ phase_shift: bool = False
+ max_duty: float = 0.95
+
+
+# =============================================================================
+# Resonant Topologies
+# =============================================================================
+
+@dataclass
+class ResonantTopology:
+ """Base class for resonant topologies."""
+ name: str
+ f_res_hz: float # Resonant frequency
+ fsw_min_hz: float # Min switching frequency
+ fsw_max_hz: float # Max switching frequency
+
+
+@dataclass
+class LLCTopology(ResonantTopology):
+ """LLC resonant converter topology.
+
+ High efficiency topology using resonant tank circuit.
+ Achieves ZVS for primary switches and ZCS for secondary rectifiers.
+
+ Attributes:
+ f_res_hz: Resonant frequency (calculated from Lr and Cr)
+ fsw_min_hz: Minimum switching frequency
+ fsw_max_hz: Maximum switching frequency
+ Lm_h: Magnetizing inductance in Henries
+ Lr_h: Resonant (leakage) inductance in Henries
+ Cr_f: Resonant capacitance in Farads
+ turns_ratio: Transformer turns ratio
+ Q: Quality factor at full load
+ k: Inductance ratio Lm/Lr
+ gain_min: Minimum voltage gain
+ gain_max: Maximum voltage gain
+ """
+ name: str = "llc"
+ f_res_hz: float = 100e3
+ fsw_min_hz: float = 80e3
+ fsw_max_hz: float = 150e3
+ Lm_h: float = 0.0 # Magnetizing inductance
+ Lr_h: float = 0.0 # Resonant inductance
+ Cr_f: float = 0.0 # Resonant capacitance
+ turns_ratio: float = 1.0
+ Q: float = 0.3 # Quality factor at full load
+ k: float = 5.0 # Inductance ratio Lm/Lr
+ gain_min: float = 0.9 # Min voltage gain
+ gain_max: float = 1.1 # Max voltage gain
+
+ def calculate_resonant_frequency(self) -> float:
+ """Calculate resonant frequency from Lr and Cr."""
+ import math
+ if self.Lr_h > 0 and self.Cr_f > 0:
+ return 1 / (2 * math.pi * (self.Lr_h * self.Cr_f) ** 0.5)
+ return self.f_res_hz
+
+
+@dataclass
+class LCCTopology(ResonantTopology):
+ """LCC resonant converter topology.
+
+ Uses series inductor with series and parallel capacitors.
+
+ Attributes:
+ f_res_hz: Resonant frequency
+ fsw_min_hz: Minimum switching frequency
+ fsw_max_hz: Maximum switching frequency
+ Lr_h: Resonant inductance in Henries
+ Cs_f: Series capacitance in Farads
+ Cp_f: Parallel capacitance in Farads
+ turns_ratio: Transformer turns ratio
+ """
+ name: str = "lcc"
+ f_res_hz: float = 100e3
+ fsw_min_hz: float = 80e3
+ fsw_max_hz: float = 150e3
+ Lr_h: float = 0.0
+ Cs_f: float = 0.0 # Series capacitance
+ Cp_f: float = 0.0 # Parallel capacitance
+ turns_ratio: float = 1.0
diff --git a/api/optimization.py b/api/optimization.py
new file mode 100644
index 0000000..86a82e8
--- /dev/null
+++ b/api/optimization.py
@@ -0,0 +1,463 @@
+"""
+Multi-objective optimization module for magnetic component design.
+
+Provides wrappers around pymoo's NSGA-II algorithm for Pareto optimization
+of inductor and transformer designs.
+
+Example:
+ from api.optimization import NSGAOptimizer
+
+ optimizer = NSGAOptimizer(
+ objectives=["mass", "total_loss"],
+ constraints={"inductance": (100e-6, 140e-6), "max_temp_rise": 50}
+ )
+ optimizer.add_variable("turns", range=(20, 60))
+ pareto_front = optimizer.run(generations=50)
+"""
+
+import math
+from dataclasses import dataclass, field
+from typing import Optional, Callable, Any
+from abc import ABC, abstractmethod
+
+
+@dataclass
+class DesignVariable:
+ """Definition of a design variable for optimization."""
+ name: str
+ var_type: str # "continuous", "integer", "discrete"
+ bounds: Optional[tuple] = None # (min, max) for continuous/integer
+ choices: Optional[list] = None # For discrete variables
+
+ def __post_init__(self):
+ if self.var_type in ("continuous", "integer") and self.bounds is None:
+ raise ValueError(f"Variable {self.name}: bounds required for {self.var_type}")
+ if self.var_type == "discrete" and self.choices is None:
+ raise ValueError(f"Variable {self.name}: choices required for discrete")
+
+
+@dataclass
+class OptimizationResult:
+ """Result from a single Pareto-optimal design."""
+ variables: dict
+ objectives: dict
+ constraints: dict
+ feasible: bool = True
+
+ @property
+ def mass_kg(self) -> Optional[float]:
+ return self.objectives.get("mass")
+
+ @property
+ def total_loss_w(self) -> Optional[float]:
+ return self.objectives.get("total_loss")
+
+
+@dataclass
+class ParetoFront:
+ """Collection of Pareto-optimal solutions."""
+ solutions: list[OptimizationResult] = field(default_factory=list)
+ generations: int = 0
+ population_size: int = 0
+ converged: bool = False
+
+ def __len__(self) -> int:
+ return len(self.solutions)
+
+ def __iter__(self):
+ return iter(self.solutions)
+
+ def __getitem__(self, idx) -> OptimizationResult:
+ return self.solutions[idx]
+
+ def sort_by(self, objective: str, ascending: bool = True) -> list[OptimizationResult]:
+ """Sort solutions by a specific objective."""
+ return sorted(
+ self.solutions,
+ key=lambda x: x.objectives.get(objective, float('inf')),
+ reverse=not ascending
+ )
+
+ def filter_feasible(self) -> list[OptimizationResult]:
+ """Return only feasible solutions."""
+ return [s for s in self.solutions if s.feasible]
+
+
+class BaseOptimizer(ABC):
+ """Abstract base class for optimizers."""
+
+ def __init__(
+ self,
+ objectives: list[str],
+ constraints: Optional[dict] = None,
+ ):
+ self.objectives = objectives
+ self.constraints = constraints or {}
+ self.variables: list[DesignVariable] = []
+ self._evaluator: Optional[Callable] = None
+
+ def add_variable(
+ self,
+ name: str,
+ *,
+ range: Optional[tuple] = None,
+ choices: Optional[list] = None,
+ var_type: Optional[str] = None,
+ ) -> "BaseOptimizer":
+ """Add a design variable to the optimization."""
+ if choices is not None:
+ var = DesignVariable(name, "discrete", choices=choices)
+ elif range is not None:
+ inferred_type = var_type or ("integer" if all(isinstance(x, int) for x in range) else "continuous")
+ var = DesignVariable(name, inferred_type, bounds=range)
+ else:
+ raise ValueError(f"Variable {name}: provide either range or choices")
+
+ self.variables.append(var)
+ return self
+
+ def set_evaluator(self, evaluator: Callable[[dict], dict]) -> "BaseOptimizer":
+ """Set custom evaluation function.
+
+ Args:
+ evaluator: Function that takes variable dict and returns
+ dict with objective values.
+ """
+ self._evaluator = evaluator
+ return self
+
+ @abstractmethod
+ def run(
+ self,
+ generations: int = 50,
+ population: int = 100,
+ seed: Optional[int] = None,
+ ) -> ParetoFront:
+ """Run the optimization."""
+ ...
+
+
+class NSGAOptimizer(BaseOptimizer):
+ """NSGA-II multi-objective optimizer for magnetic design.
+
+ Uses pymoo for optimization if available, falls back to random sampling.
+ """
+
+ def __init__(
+ self,
+ objectives: list[str],
+ constraints: Optional[dict] = None,
+ ):
+ super().__init__(objectives, constraints)
+ self._pymoo_available = self._check_pymoo()
+
+ def _check_pymoo(self) -> bool:
+ """Check if pymoo is available."""
+ try:
+ import pymoo
+ return True
+ except ImportError:
+ return False
+
+ def run(
+ self,
+ generations: int = 50,
+ population: int = 100,
+ seed: Optional[int] = None,
+ ) -> ParetoFront:
+ """Run NSGA-II optimization.
+
+ Args:
+ generations: Number of generations to evolve
+ population: Population size per generation
+ seed: Random seed for reproducibility
+
+ Returns:
+ ParetoFront with optimal solutions
+ """
+ if not self.variables:
+ raise ValueError("No design variables defined. Use add_variable().")
+
+ if self._pymoo_available:
+ return self._run_pymoo(generations, population, seed)
+ else:
+ print("Note: pymoo not available. Using random sampling fallback.")
+ return self._run_random_sampling(generations * population, seed)
+
+ def _run_pymoo(
+ self,
+ generations: int,
+ population: int,
+ seed: Optional[int],
+ ) -> ParetoFront:
+ """Run optimization using pymoo's NSGA-II."""
+ import numpy as np
+ from pymoo.algorithms.moo.nsga2 import NSGA2
+ from pymoo.core.problem import Problem
+ from pymoo.optimize import minimize
+ from pymoo.operators.crossover.sbx import SBX
+ from pymoo.operators.mutation.pm import PM
+ from pymoo.operators.sampling.rnd import FloatRandomSampling
+
+ optimizer = self
+
+ class MagneticProblem(Problem):
+ def __init__(self):
+ n_var = len(optimizer.variables)
+ n_obj = len(optimizer.objectives)
+
+ # Set up bounds
+ xl = []
+ xu = []
+ for var in optimizer.variables:
+ if var.var_type == "discrete":
+ xl.append(0)
+ xu.append(len(var.choices) - 1)
+ else:
+ xl.append(var.bounds[0])
+ xu.append(var.bounds[1])
+
+ super().__init__(
+ n_var=n_var,
+ n_obj=n_obj,
+ n_ieq_constr=len(optimizer.constraints),
+ xl=np.array(xl),
+ xu=np.array(xu),
+ )
+
+ def _evaluate(self, x, out, *args, **kwargs):
+ F = []
+ G = []
+
+ for xi in x:
+ # Convert to variable dict
+ var_dict = {}
+ for i, var in enumerate(optimizer.variables):
+ if var.var_type == "discrete":
+ idx = int(round(xi[i]))
+ idx = max(0, min(idx, len(var.choices) - 1))
+ var_dict[var.name] = var.choices[idx]
+ elif var.var_type == "integer":
+ var_dict[var.name] = int(round(xi[i]))
+ else:
+ var_dict[var.name] = xi[i]
+
+ # Evaluate
+ if optimizer._evaluator:
+ result = optimizer._evaluator(var_dict)
+ else:
+ result = optimizer._default_evaluator(var_dict)
+
+ # Extract objectives
+ f = [result.get(obj, float('inf')) for obj in optimizer.objectives]
+ F.append(f)
+
+ # Extract constraints (g <= 0 is feasible)
+ g = []
+ for name, (lo, hi) in optimizer.constraints.items():
+ val = result.get(name, 0)
+ g.append(lo - val) # val >= lo
+ g.append(val - hi) # val <= hi
+ G.append(g)
+
+ out["F"] = np.array(F)
+ if G:
+ out["G"] = np.array(G)
+
+ problem = MagneticProblem()
+
+ algorithm = NSGA2(
+ pop_size=population,
+ sampling=FloatRandomSampling(),
+ crossover=SBX(prob=0.9, eta=15),
+ mutation=PM(eta=20),
+ eliminate_duplicates=True,
+ )
+
+ res = minimize(
+ problem,
+ algorithm,
+ ("n_gen", generations),
+ seed=seed,
+ verbose=False,
+ )
+
+ # Convert results to ParetoFront
+ solutions = []
+ for i, (x, f) in enumerate(zip(res.X, res.F)):
+ var_dict = {}
+ for j, var in enumerate(self.variables):
+ if var.var_type == "discrete":
+ idx = int(round(x[j]))
+ var_dict[var.name] = var.choices[idx]
+ elif var.var_type == "integer":
+ var_dict[var.name] = int(round(x[j]))
+ else:
+ var_dict[var.name] = x[j]
+
+ obj_dict = {obj: f[k] for k, obj in enumerate(self.objectives)}
+
+ # Check constraint violation
+ feasible = True
+ if res.G is not None:
+ feasible = all(g <= 0 for g in res.G[i])
+
+ solutions.append(OptimizationResult(
+ variables=var_dict,
+ objectives=obj_dict,
+ constraints={},
+ feasible=feasible,
+ ))
+
+ return ParetoFront(
+ solutions=solutions,
+ generations=generations,
+ population_size=population,
+ converged=True,
+ )
+
+ def _run_random_sampling(
+ self,
+ n_samples: int,
+ seed: Optional[int],
+ ) -> ParetoFront:
+ """Fallback: random sampling with Pareto filtering."""
+ import random
+ if seed is not None:
+ random.seed(seed)
+
+ samples = []
+ for _ in range(n_samples):
+ var_dict = {}
+ for var in self.variables:
+ if var.var_type == "discrete":
+ var_dict[var.name] = random.choice(var.choices)
+ elif var.var_type == "integer":
+ var_dict[var.name] = random.randint(var.bounds[0], var.bounds[1])
+ else:
+ var_dict[var.name] = random.uniform(var.bounds[0], var.bounds[1])
+
+ if self._evaluator:
+ result = self._evaluator(var_dict)
+ else:
+ result = self._default_evaluator(var_dict)
+
+ obj_dict = {obj: result.get(obj, float('inf')) for obj in self.objectives}
+
+ # Check constraints
+ feasible = True
+ for name, (lo, hi) in self.constraints.items():
+ val = result.get(name, 0)
+ if val < lo or val > hi:
+ feasible = False
+ break
+
+ samples.append(OptimizationResult(
+ variables=var_dict,
+ objectives=obj_dict,
+ constraints={},
+ feasible=feasible,
+ ))
+
+ # Filter to Pareto front
+ pareto = self._pareto_filter(samples)
+
+ return ParetoFront(
+ solutions=pareto,
+ generations=1,
+ population_size=n_samples,
+ converged=False,
+ )
+
+ def _pareto_filter(self, samples: list[OptimizationResult]) -> list[OptimizationResult]:
+ """Filter samples to Pareto-optimal set."""
+ feasible = [s for s in samples if s.feasible]
+ if not feasible:
+ return samples[:10] # Return some samples even if infeasible
+
+ pareto = []
+ for candidate in feasible:
+ dominated = False
+ for other in feasible:
+ if other is candidate:
+ continue
+ # Check if other dominates candidate
+ better_in_all = all(
+ other.objectives[obj] <= candidate.objectives[obj]
+ for obj in self.objectives
+ )
+ strictly_better = any(
+ other.objectives[obj] < candidate.objectives[obj]
+ for obj in self.objectives
+ )
+ if better_in_all and strictly_better:
+ dominated = True
+ break
+ if not dominated:
+ pareto.append(candidate)
+
+ return pareto
+
+ def _default_evaluator(self, variables: dict) -> dict:
+ """Default evaluator using simplified inductor model."""
+ # Extract variables
+ turns = variables.get("turns", 30)
+ core_size = variables.get("core_size", 1.0) # Relative size factor
+ wire_gauge = variables.get("wire_gauge", 18)
+
+ # Simplified physics model
+ # Mass ~ core_size^3 + turns * wire_mass
+ wire_area = 0.5 * (0.127 ** (wire_gauge / 10)) # Simplified AWG
+ core_mass = 0.1 * core_size ** 3
+ wire_mass = turns * 0.05 * wire_area * core_size
+ total_mass = core_mass + wire_mass
+
+ # Losses ~ core_loss/turns^2 + copper_loss*turns
+ core_loss = 10 * core_size ** 2 / (turns ** 1.5)
+ copper_loss = 0.1 * turns * (1 / wire_area) * core_size
+ total_loss = core_loss + copper_loss
+
+ # Inductance
+ inductance = turns ** 2 * core_size * 1e-6 # Simplified
+
+ return {
+ "mass": total_mass,
+ "total_loss": total_loss,
+ "core_loss": core_loss,
+ "copper_loss": copper_loss,
+ "inductance": inductance,
+ }
+
+
+def create_inductor_optimizer(
+ target_inductance: float,
+ tolerance: float = 0.2,
+ max_loss: Optional[float] = None,
+ max_mass: Optional[float] = None,
+) -> NSGAOptimizer:
+ """Factory function to create a pre-configured inductor optimizer.
+
+ Args:
+ target_inductance: Target inductance in Henries
+ tolerance: Inductance tolerance (default ±20%)
+ max_loss: Maximum acceptable loss in Watts (optional)
+ max_mass: Maximum acceptable mass in kg (optional)
+
+ Returns:
+ Configured NSGAOptimizer ready for variable addition
+ """
+ constraints = {
+ "inductance": (
+ target_inductance * (1 - tolerance),
+ target_inductance * (1 + tolerance),
+ )
+ }
+ if max_loss:
+ constraints["total_loss"] = (0, max_loss)
+ if max_mass:
+ constraints["mass"] = (0, max_mass)
+
+ return NSGAOptimizer(
+ objectives=["mass", "total_loss"],
+ constraints=constraints,
+ )
diff --git a/api/report.py b/api/report.py
new file mode 100644
index 0000000..b21a1c5
--- /dev/null
+++ b/api/report.py
@@ -0,0 +1,668 @@
+"""
+PyOpenMagnetics Design Report Generator
+
+Generates comprehensive visual reports for magnetic component designs
+using matplotlib. Includes:
+- Pareto front analysis
+- Parallel coordinate plots
+- Radar charts for multi-objective comparison
+- Loss breakdown visualizations
+- Design ranking tables
+"""
+
+import os
+import json
+from typing import List, Optional, Dict, Any
+from dataclasses import dataclass
+
+
+def _ensure_matplotlib():
+ """Lazily import matplotlib with Agg backend."""
+ import matplotlib
+ matplotlib.use('Agg')
+ import matplotlib.pyplot as plt
+ import numpy as np
+ return plt, np
+
+
+def generate_design_report(
+ results: List[Any],
+ output_dir: str,
+ title: str = "Magnetic Design Report",
+ specs: Optional[Dict[str, Any]] = None,
+ verbose: bool = False
+) -> str:
+ """
+ Generate a comprehensive design report with multiple visualizations.
+
+ Args:
+ results: List of DesignResult objects from Design.solve()
+ output_dir: Directory to save report files
+ title: Report title
+ specs: Optional design specifications dict
+ verbose: Print progress messages
+
+ Returns:
+ Path to the generated report image
+ """
+ plt, np = _ensure_matplotlib()
+ os.makedirs(output_dir, exist_ok=True)
+
+ if not results:
+ if verbose:
+ print("[No results to generate report]")
+ return ""
+
+ # Convert to report data
+ report_data = _extract_report_data(results)
+
+ # Generate main report (2x3 grid)
+ fig = plt.figure(figsize=(18, 12))
+ fig.suptitle(title, fontsize=16, fontweight='bold', y=0.98)
+
+ gs = fig.add_gridspec(2, 3, hspace=0.3, wspace=0.25,
+ left=0.05, right=0.95, top=0.92, bottom=0.08)
+
+ # Row 1
+ ax1 = fig.add_subplot(gs[0, 0])
+ _plot_pareto_front(ax1, report_data, plt, np)
+
+ ax2 = fig.add_subplot(gs[0, 1])
+ _plot_loss_breakdown_stacked(ax2, report_data, plt)
+
+ ax3 = fig.add_subplot(gs[0, 2])
+ _plot_loss_pie(ax3, report_data[0], plt)
+
+ # Row 2
+ ax4 = fig.add_subplot(gs[1, 0], projection='polar')
+ _plot_radar_chart(ax4, report_data[:min(5, len(report_data))], plt, np)
+
+ ax5 = fig.add_subplot(gs[1, 1])
+ _plot_ranking_bars(ax5, report_data, plt)
+
+ ax6 = fig.add_subplot(gs[1, 2])
+ _plot_summary_card(ax6, report_data[0], specs, plt)
+
+ report_path = os.path.join(output_dir, "design_report.png")
+ plt.savefig(report_path, dpi=150, bbox_inches='tight', facecolor='white')
+ plt.close()
+
+ if verbose:
+ print(f"[Design report saved to {report_path}]")
+
+ # Generate additional plots
+ _generate_pareto_detailed(report_data, output_dir, plt, np, verbose)
+ _generate_volume_loss_pareto(report_data, output_dir, plt, np, verbose)
+ _generate_parallel_coordinates(report_data, output_dir, plt, np, verbose)
+ _generate_heatmap(report_data, output_dir, plt, np, verbose)
+ _save_json_summary(report_data, output_dir, specs, verbose)
+
+ return report_path
+
+
+def _extract_report_data(results: List[Any]) -> List[dict]:
+ """Extract report data from DesignResult objects."""
+ report_data = []
+ for i, r in enumerate(results):
+ height = getattr(r, 'height_mm', 0) or 0
+ width = getattr(r, 'width_mm', 0) or 0
+ depth = getattr(r, 'depth_mm', 0) or 0
+ volume = (height * width * depth) / 1000.0 if height and width and depth else 0
+ report_data.append({
+ "rank": i + 1,
+ "core": getattr(r, 'core', 'Unknown'),
+ "material": getattr(r, 'material', 'Unknown'),
+ "primary_turns": getattr(r, 'primary_turns', 0),
+ "primary_wire": getattr(r, 'primary_wire', 'Unknown'),
+ "air_gap_mm": getattr(r, 'air_gap_mm', 0),
+ "core_loss_w": getattr(r, 'core_loss_w', 0),
+ "copper_loss_w": getattr(r, 'copper_loss_w', 0),
+ "total_loss_w": getattr(r, 'total_loss_w', 0),
+ "temp_rise_c": getattr(r, 'temp_rise_c', 0),
+ "height_mm": height,
+ "width_mm": width,
+ "depth_mm": depth,
+ "volume_cm3": volume,
+ "weight_g": getattr(r, 'weight_g', 0) or 0,
+ })
+ return report_data
+
+
+def _plot_pareto_front(ax, data: list, plt, np):
+ """Plot Pareto front: Core Loss vs Copper Loss."""
+ core_losses = np.array([d['core_loss_w'] for d in data])
+ copper_losses = np.array([d['copper_loss_w'] for d in data])
+ total_losses = np.array([d['total_loss_w'] for d in data])
+
+ # Size points by inverse of total loss (better = bigger)
+ if total_losses.max() > 0:
+ sizes = 200 * (1 - (total_losses - total_losses.min()) /
+ (total_losses.max() - total_losses.min() + 0.001)) + 50
+ else:
+ sizes = np.full_like(total_losses, 100)
+
+ # Color by rank
+ colors = plt.cm.RdYlGn_r(np.linspace(0, 1, len(data)))
+
+ scatter = ax.scatter(core_losses, copper_losses, s=sizes, c=colors,
+ edgecolors='black', linewidths=1, alpha=0.8)
+
+ # Mark Pareto-optimal points
+ pareto_mask = _find_pareto_front(core_losses, copper_losses)
+ if pareto_mask.sum() > 1:
+ pareto_core = core_losses[pareto_mask]
+ pareto_copper = copper_losses[pareto_mask]
+ # Sort for line drawing
+ sort_idx = np.argsort(pareto_core)
+ ax.plot(pareto_core[sort_idx], pareto_copper[sort_idx],
+ 'b--', alpha=0.5, linewidth=2, label='Pareto Front')
+
+ # Annotate top 3
+ for i in range(min(3, len(data))):
+ ax.annotate(f"#{i+1}", (core_losses[i], copper_losses[i]),
+ textcoords="offset points", xytext=(5, 5),
+ fontsize=9, fontweight='bold')
+
+ ax.set_xlabel('Core Loss (W)', fontsize=10)
+ ax.set_ylabel('Copper Loss (W)', fontsize=10)
+ ax.set_title('Pareto Front Analysis', fontsize=11, fontweight='bold')
+ ax.grid(True, alpha=0.3)
+ # Only show legend if there are labeled artists
+ handles, labels = ax.get_legend_handles_labels()
+ if labels:
+ ax.legend(loc='upper right', fontsize=8)
+
+
+def _find_pareto_front(x, y):
+ """Find Pareto-optimal points (minimize both objectives)."""
+ import numpy as np
+ n = len(x)
+ pareto = np.ones(n, dtype=bool)
+ for i in range(n):
+ for j in range(n):
+ if i != j:
+ if x[j] <= x[i] and y[j] <= y[i] and (x[j] < x[i] or y[j] < y[i]):
+ pareto[i] = False
+ break
+ return pareto
+
+
+def _plot_loss_breakdown_stacked(ax, data: list, plt):
+ """Plot stacked bar chart of loss components."""
+ cores = [f"#{d['rank']}" for d in data]
+ core_losses = [d['core_loss_w'] for d in data]
+ copper_losses = [d['copper_loss_w'] for d in data]
+
+ x = range(len(cores))
+ ax.bar(x, core_losses, label='Core Loss', color='#e74c3c', edgecolor='white')
+ ax.bar(x, copper_losses, bottom=core_losses, label='Copper Loss',
+ color='#3498db', edgecolor='white')
+
+ ax.set_xticks(x)
+ ax.set_xticklabels(cores, fontsize=9)
+ ax.set_ylabel('Loss (W)', fontsize=10)
+ ax.set_title('Loss Breakdown by Design', fontsize=11, fontweight='bold')
+ ax.legend(loc='upper right', fontsize=8)
+ ax.grid(axis='y', alpha=0.3)
+
+ # Add total labels
+ for i, (c, cu) in enumerate(zip(core_losses, copper_losses)):
+ ax.text(i, c + cu + 0.02 * max([c + cu for c, cu in zip(core_losses, copper_losses)]),
+ f'{c+cu:.2f}W', ha='center', fontsize=8, fontweight='bold')
+
+
+def _plot_loss_pie(ax, best: dict, plt):
+ """Plot pie chart for best design."""
+ core_loss = best['core_loss_w']
+ copper_loss = best['copper_loss_w']
+
+ if core_loss + copper_loss < 0.001:
+ ax.text(0.5, 0.5, 'No loss data', ha='center', va='center',
+ transform=ax.transAxes, fontsize=12)
+ ax.axis('off')
+ return
+
+ sizes = [core_loss, copper_loss]
+ labels = [f'Core\n{core_loss:.2f}W', f'Copper\n{copper_loss:.2f}W']
+ colors = ['#e74c3c', '#3498db']
+ explode = (0.05, 0)
+
+ wedges, texts, autotexts = ax.pie(
+ sizes, labels=labels, colors=colors, explode=explode,
+ autopct='%1.1f%%', startangle=90, textprops={'fontsize': 9}
+ )
+ for autotext in autotexts:
+ autotext.set_fontweight('bold')
+
+ ax.set_title(f'Best Design Loss Split\n({best["core"]})',
+ fontsize=11, fontweight='bold')
+
+
+def _plot_radar_chart(ax, data: list, plt, np):
+ """Plot radar chart comparing top designs on multiple metrics."""
+ if len(data) < 2:
+ ax.text(0.5, 0.5, 'Need 2+ designs\nfor radar chart',
+ ha='center', va='center', transform=ax.transAxes)
+ ax.axis('off')
+ return
+
+ # Metrics to compare (normalized 0-1, lower is better for losses)
+ categories = ['Core Loss', 'Copper Loss', 'Total Loss', 'Turns', 'Gap']
+ N = len(categories)
+
+ # Extract and normalize data
+ metrics = {
+ 'Core Loss': [d['core_loss_w'] for d in data],
+ 'Copper Loss': [d['copper_loss_w'] for d in data],
+ 'Total Loss': [d['total_loss_w'] for d in data],
+ 'Turns': [d['primary_turns'] for d in data],
+ 'Gap': [d['air_gap_mm'] for d in data],
+ }
+
+ # Normalize each metric (invert so lower=better becomes higher on chart)
+ normalized = {}
+ for key, values in metrics.items():
+ vmin, vmax = min(values), max(values)
+ if vmax > vmin:
+ # Invert: best (lowest) becomes 1, worst becomes 0
+ normalized[key] = [1 - (v - vmin) / (vmax - vmin) for v in values]
+ else:
+ normalized[key] = [0.5] * len(values)
+
+ # Setup radar chart
+ angles = np.linspace(0, 2 * np.pi, N, endpoint=False).tolist()
+ angles += angles[:1] # Complete the loop
+
+ ax.set_theta_offset(np.pi / 2)
+ ax.set_theta_direction(-1)
+
+ ax.set_xticks(angles[:-1])
+ ax.set_xticklabels(categories, fontsize=8)
+
+ # Plot each design
+ colors = plt.cm.Set2(np.linspace(0, 1, len(data)))
+ for i, d in enumerate(data):
+ values = [normalized[cat][i] for cat in categories]
+ values += values[:1]
+ ax.plot(angles, values, 'o-', linewidth=2, color=colors[i],
+ label=f"#{d['rank']}: {d['core'][:10]}")
+ ax.fill(angles, values, alpha=0.1, color=colors[i])
+
+ ax.set_ylim(0, 1)
+ ax.set_title('Multi-Objective Comparison', fontsize=11, fontweight='bold', y=1.08)
+ ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1), fontsize=7)
+
+
+def _plot_ranking_bars(ax, data: list, plt):
+ """Plot horizontal bar chart ranking all designs."""
+ labels = [f"#{d['rank']}: {d['core'][:12]}" for d in data]
+ losses = [d['total_loss_w'] for d in data]
+
+ colors = ['#2ecc71' if i == 0 else '#3498db' if i < 3 else '#95a5a6'
+ for i in range(len(data))]
+
+ bars = ax.barh(range(len(labels)), losses, color=colors, edgecolor='white')
+ ax.set_yticks(range(len(labels)))
+ ax.set_yticklabels(labels, fontsize=9)
+ ax.set_xlabel('Total Loss (W)', fontsize=10)
+ ax.set_title('Design Ranking', fontsize=11, fontweight='bold')
+ ax.invert_yaxis()
+ ax.grid(axis='x', alpha=0.3)
+
+ # Value labels
+ max_loss = max(losses) if losses else 1
+ for bar, loss in zip(bars, losses):
+ ax.text(bar.get_width() + max_loss * 0.02, bar.get_y() + bar.get_height()/2,
+ f'{loss:.2f}W', va='center', fontsize=9)
+
+
+def _plot_summary_card(ax, best: dict, specs: Optional[dict], plt):
+ """Plot summary card for best design."""
+ ax.axis('off')
+
+ lines = [
+ ("RECOMMENDED DESIGN", 'bold', 14, '#2c3e50'),
+ ("", 'normal', 6, 'black'),
+ (f"Core: {best['core']}", 'bold', 11, '#27ae60'),
+ (f"Material: {best['material']}", 'normal', 11, 'black'),
+ (f"Primary: {best['primary_turns']}T", 'normal', 11, 'black'),
+ (f"Air Gap: {best['air_gap_mm']:.2f} mm", 'normal', 11, 'black'),
+ ("", 'normal', 6, 'black'),
+ (f"Core Loss: {best['core_loss_w']:.3f} W", 'normal', 10, '#e74c3c'),
+ (f"Copper Loss: {best['copper_loss_w']:.3f} W", 'normal', 10, '#3498db'),
+ (f"Total Loss: {best['total_loss_w']:.3f} W", 'bold', 12, '#2c3e50'),
+ ("", 'normal', 6, 'black'),
+ (f"Temp Rise: ~{best['temp_rise_c']:.0f} C", 'normal', 10,
+ '#e74c3c' if best['temp_rise_c'] > 40 else '#27ae60'),
+ ]
+
+ y_pos = 0.95
+ for text, weight, size, color in lines:
+ ax.text(0.08, y_pos, text, transform=ax.transAxes,
+ fontsize=size, fontweight=weight, color=color,
+ verticalalignment='top', fontfamily='sans-serif')
+ y_pos -= 0.075 if size >= 10 else 0.05
+
+ # Border
+ rect = plt.Rectangle((0.02, 0.02), 0.96, 0.96, fill=False,
+ edgecolor='#27ae60', linewidth=3,
+ transform=ax.transAxes)
+ ax.add_patch(rect)
+
+
+def _generate_pareto_detailed(data: list, output_dir: str, plt, np, verbose: bool):
+ """Generate detailed Pareto analysis plot."""
+ if len(data) < 2:
+ return
+
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
+ fig.suptitle('Detailed Pareto Analysis', fontsize=14, fontweight='bold')
+
+ core_losses = np.array([d['core_loss_w'] for d in data])
+ copper_losses = np.array([d['copper_loss_w'] for d in data])
+ total_losses = np.array([d['total_loss_w'] for d in data])
+ turns = np.array([d['primary_turns'] for d in data])
+
+ # Plot 1: Core vs Copper Loss
+ ax1 = axes[0]
+ scatter = ax1.scatter(core_losses, copper_losses, c=total_losses,
+ cmap='RdYlGn_r', s=100, edgecolors='black')
+ plt.colorbar(scatter, ax=ax1, label='Total Loss (W)')
+ ax1.set_xlabel('Core Loss (W)')
+ ax1.set_ylabel('Copper Loss (W)')
+ ax1.set_title('Core vs Copper Loss')
+ ax1.grid(True, alpha=0.3)
+
+ # Plot 2: Total Loss vs Turns
+ ax2 = axes[1]
+ scatter = ax2.scatter(turns, total_losses, c=range(len(data)),
+ cmap='viridis', s=100, edgecolors='black')
+ plt.colorbar(scatter, ax=ax2, label='Rank')
+ ax2.set_xlabel('Primary Turns')
+ ax2.set_ylabel('Total Loss (W)')
+ ax2.set_title('Loss vs Complexity')
+ ax2.grid(True, alpha=0.3)
+
+ # Plot 3: Loss composition ratio
+ ax3 = axes[2]
+ core_ratio = core_losses / (total_losses + 0.001)
+ ax3.bar(range(len(data)), core_ratio, label='Core Loss %', color='#e74c3c')
+ ax3.bar(range(len(data)), 1 - core_ratio, bottom=core_ratio,
+ label='Copper Loss %', color='#3498db')
+ ax3.set_xticks(range(len(data)))
+ ax3.set_xticklabels([f"#{d['rank']}" for d in data])
+ ax3.set_ylabel('Loss Fraction')
+ ax3.set_title('Loss Composition')
+ ax3.legend(loc='upper right')
+ ax3.axhline(y=0.5, color='black', linestyle='--', alpha=0.5)
+
+ plt.tight_layout()
+ path = os.path.join(output_dir, "pareto_detailed.png")
+ plt.savefig(path, dpi=150, bbox_inches='tight')
+ plt.close()
+
+ if verbose:
+ print(f"[Pareto analysis saved to {path}]")
+
+
+def _generate_volume_loss_pareto(data: list, output_dir: str, plt, np, verbose: bool):
+ """Generate Volume vs Total Loss Pareto plot."""
+ if len(data) < 2:
+ return
+
+ # Check if volume data is available
+ volumes = np.array([d['volume_cm3'] for d in data])
+ if volumes.max() == 0:
+ if verbose:
+ print("[Volume/loss Pareto skipped - no dimension data]")
+ return
+
+ fig, axes = plt.subplots(1, 2, figsize=(14, 6))
+ fig.suptitle('Volume vs Loss Trade-off Analysis', fontsize=14, fontweight='bold')
+
+ total_losses = np.array([d['total_loss_w'] for d in data])
+
+ # Plot 1: Volume vs Total Loss scatter with Pareto frontier
+ ax1 = axes[0]
+
+ # Find Pareto-optimal points (minimize both volume and loss)
+ pareto_mask = _find_pareto_front(volumes, total_losses)
+
+ # Size points by efficiency (lower total loss = bigger)
+ if total_losses.max() > total_losses.min():
+ sizes = 200 * (1 - (total_losses - total_losses.min()) /
+ (total_losses.max() - total_losses.min() + 0.001)) + 50
+ else:
+ sizes = np.full_like(total_losses, 100)
+
+ # Color by rank
+ colors = plt.cm.RdYlGn_r(np.linspace(0, 1, len(data)))
+
+ ax1.scatter(volumes, total_losses, s=sizes, c=colors,
+ edgecolors='black', linewidths=1, alpha=0.8)
+
+ # Draw Pareto frontier line
+ if pareto_mask.sum() > 1:
+ pareto_vol = volumes[pareto_mask]
+ pareto_loss = total_losses[pareto_mask]
+ sort_idx = np.argsort(pareto_vol)
+ ax1.plot(pareto_vol[sort_idx], pareto_loss[sort_idx],
+ 'b--', alpha=0.6, linewidth=2, label='Pareto Front')
+ ax1.fill_between(pareto_vol[sort_idx], pareto_loss[sort_idx],
+ total_losses.max() * 1.1, alpha=0.1, color='blue')
+
+ # Annotate top designs and Pareto-optimal ones
+ for i, d in enumerate(data):
+ if i < 3 or pareto_mask[i]:
+ label = f"#{d['rank']}" if i < 3 else "*"
+ ax1.annotate(label, (volumes[i], total_losses[i]),
+ textcoords="offset points", xytext=(5, 5),
+ fontsize=8, fontweight='bold')
+
+ ax1.set_xlabel('Volume (cm³)', fontsize=11)
+ ax1.set_ylabel('Total Loss (W)', fontsize=11)
+ ax1.set_title('Volume vs Total Loss', fontsize=12, fontweight='bold')
+ ax1.grid(True, alpha=0.3)
+ handles, labels = ax1.get_legend_handles_labels()
+ if labels:
+ ax1.legend(loc='upper right', fontsize=9)
+
+ # Plot 2: Power density comparison (Loss/Volume)
+ ax2 = axes[1]
+
+ loss_density = total_losses / (volumes + 0.001) # W/cm³
+ bars_colors = ['#2ecc71' if i == 0 else '#3498db' if i < 3 else '#95a5a6'
+ for i in range(len(data))]
+
+ # Highlight Pareto-optimal designs
+ for i, is_pareto in enumerate(pareto_mask):
+ if is_pareto and i >= 3:
+ bars_colors[i] = '#9b59b6' # Purple for Pareto-optimal
+
+ core_labels = [f"#{d['rank']}: {d['core'][:12]}" for d in data]
+ bars = ax2.barh(range(len(data)), loss_density, color=bars_colors, edgecolor='white')
+ ax2.set_yticks(range(len(data)))
+ ax2.set_yticklabels(core_labels, fontsize=9)
+ ax2.set_xlabel('Loss Density (W/cm³)', fontsize=11)
+ ax2.set_title('Loss Density Comparison', fontsize=12, fontweight='bold')
+ ax2.invert_yaxis()
+ ax2.grid(axis='x', alpha=0.3)
+
+ # Add value labels
+ max_density = max(loss_density) if len(loss_density) > 0 else 1
+ for bar, density, vol in zip(bars, loss_density, volumes):
+ ax2.text(bar.get_width() + max_density * 0.02, bar.get_y() + bar.get_height()/2,
+ f'{density:.3f} W/cm³ ({vol:.1f}cm³)', va='center', fontsize=8)
+
+ # Add legend for colors
+ from matplotlib.patches import Patch
+ legend_elements = [
+ Patch(facecolor='#2ecc71', label='Best'),
+ Patch(facecolor='#3498db', label='Top 3'),
+ Patch(facecolor='#9b59b6', label='Pareto-optimal'),
+ Patch(facecolor='#95a5a6', label='Other'),
+ ]
+ ax2.legend(handles=legend_elements, loc='lower right', fontsize=8)
+
+ plt.tight_layout()
+ path = os.path.join(output_dir, "volume_loss_pareto.png")
+ plt.savefig(path, dpi=150, bbox_inches='tight')
+ plt.close()
+
+ if verbose:
+ print(f"[Volume/loss Pareto saved to {path}]")
+
+
+def _generate_parallel_coordinates(data: list, output_dir: str, plt, np, verbose: bool):
+ """Generate parallel coordinates plot for multi-dimensional comparison."""
+ if len(data) < 2:
+ return
+
+ fig, ax = plt.subplots(figsize=(12, 6))
+
+ # Define dimensions
+ dims = ['Core Loss', 'Cu Loss', 'Total Loss', 'Turns', 'Gap']
+ values = {
+ 'Core Loss': [d['core_loss_w'] for d in data],
+ 'Cu Loss': [d['copper_loss_w'] for d in data],
+ 'Total Loss': [d['total_loss_w'] for d in data],
+ 'Turns': [d['primary_turns'] for d in data],
+ 'Gap': [d['air_gap_mm'] for d in data],
+ }
+
+ # Normalize each dimension to 0-1
+ norm_values = {}
+ for dim in dims:
+ vmin, vmax = min(values[dim]), max(values[dim])
+ if vmax > vmin:
+ norm_values[dim] = [(v - vmin) / (vmax - vmin) for v in values[dim]]
+ else:
+ norm_values[dim] = [0.5] * len(values[dim])
+
+ # Plot each design as a polyline
+ x = range(len(dims))
+ colors = plt.cm.RdYlGn_r(np.linspace(0, 1, len(data)))
+
+ for i, d in enumerate(data):
+ y = [norm_values[dim][i] for dim in dims]
+ linewidth = 3 if i == 0 else 1.5
+ alpha = 1.0 if i < 3 else 0.5
+ ax.plot(x, y, 'o-', color=colors[i], linewidth=linewidth, alpha=alpha,
+ label=f"#{d['rank']}: {d['core'][:10]}", markersize=8)
+
+ ax.set_xticks(x)
+ ax.set_xticklabels(dims, fontsize=10)
+ ax.set_ylabel('Normalized Value (0-1)', fontsize=10)
+ ax.set_title('Parallel Coordinates: Multi-Objective Comparison',
+ fontsize=12, fontweight='bold')
+ ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=8)
+ ax.grid(True, alpha=0.3)
+ ax.set_ylim(-0.05, 1.05)
+
+ # Add axis labels with actual ranges
+ for i, dim in enumerate(dims):
+ vmin, vmax = min(values[dim]), max(values[dim])
+ ax.text(i, -0.12, f'{vmin:.2f}', ha='center', fontsize=8, color='gray')
+ ax.text(i, 1.08, f'{vmax:.2f}', ha='center', fontsize=8, color='gray')
+
+ plt.tight_layout()
+ path = os.path.join(output_dir, "parallel_coordinates.png")
+ plt.savefig(path, dpi=150, bbox_inches='tight')
+ plt.close()
+
+ if verbose:
+ print(f"[Parallel coordinates saved to {path}]")
+
+
+def _generate_heatmap(data: list, output_dir: str, plt, np, verbose: bool):
+ """Generate heatmap of design characteristics."""
+ if len(data) < 2:
+ return
+
+ fig, ax = plt.subplots(figsize=(10, 6))
+
+ # Build matrix
+ metrics = ['Core Loss', 'Copper Loss', 'Total Loss', 'Turns', 'Gap']
+ matrix = []
+ for d in data:
+ row = [
+ d['core_loss_w'],
+ d['copper_loss_w'],
+ d['total_loss_w'],
+ d['primary_turns'],
+ d['air_gap_mm'],
+ ]
+ matrix.append(row)
+
+ matrix = np.array(matrix)
+
+ # Normalize columns
+ matrix_norm = np.zeros_like(matrix)
+ for j in range(matrix.shape[1]):
+ col = matrix[:, j]
+ vmin, vmax = col.min(), col.max()
+ if vmax > vmin:
+ matrix_norm[:, j] = (col - vmin) / (vmax - vmin)
+ else:
+ matrix_norm[:, j] = 0.5
+
+ # Plot heatmap
+ im = ax.imshow(matrix_norm, cmap='RdYlGn_r', aspect='auto')
+
+ # Labels
+ ax.set_xticks(range(len(metrics)))
+ ax.set_xticklabels(metrics, fontsize=10)
+ ax.set_yticks(range(len(data)))
+ ax.set_yticklabels([f"#{d['rank']}: {d['core'][:10]}" for d in data], fontsize=9)
+
+ # Add value annotations
+ for i in range(len(data)):
+ for j in range(len(metrics)):
+ val = matrix[i, j]
+ text = f'{val:.2f}' if val < 100 else f'{val:.0f}'
+ color = 'white' if matrix_norm[i, j] > 0.5 else 'black'
+ ax.text(j, i, text, ha='center', va='center', fontsize=8, color=color)
+
+ ax.set_title('Design Characteristics Heatmap', fontsize=12, fontweight='bold')
+ plt.colorbar(im, label='Normalized (0=Best, 1=Worst)')
+
+ plt.tight_layout()
+ path = os.path.join(output_dir, "heatmap.png")
+ plt.savefig(path, dpi=150, bbox_inches='tight')
+ plt.close()
+
+ if verbose:
+ print(f"[Heatmap saved to {path}]")
+
+
+def _save_json_summary(data: list, output_dir: str, specs: Optional[dict], verbose: bool):
+ """Save comprehensive JSON summary."""
+ summary = {
+ "report_version": "2.0",
+ "num_designs": len(data),
+ "best_design": data[0] if data else None,
+ "specifications": specs,
+ "all_designs": data,
+ "statistics": {
+ "min_total_loss": min(d['total_loss_w'] for d in data) if data else 0,
+ "max_total_loss": max(d['total_loss_w'] for d in data) if data else 0,
+ "avg_total_loss": sum(d['total_loss_w'] for d in data) / len(data) if data else 0,
+ "loss_range": (max(d['total_loss_w'] for d in data) -
+ min(d['total_loss_w'] for d in data)) if data else 0,
+ },
+ "files_generated": [
+ "design_report.png",
+ "pareto_detailed.png",
+ "volume_loss_pareto.png",
+ "parallel_coordinates.png",
+ "heatmap.png",
+ "report_summary.json",
+ ]
+ }
+
+ json_path = os.path.join(output_dir, "report_summary.json")
+ with open(json_path, 'w') as f:
+ json.dump(summary, f, indent=2)
+
+ if verbose:
+ print(f"[JSON summary saved to {json_path}]")
diff --git a/api/results.py b/api/results.py
new file mode 100644
index 0000000..ea7e75e
--- /dev/null
+++ b/api/results.py
@@ -0,0 +1,196 @@
+"""
+Human-readable design result formatting.
+
+Converts raw MAS output into engineer-friendly result objects.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+@dataclass
+class WindingInfo:
+ """Information about a single winding."""
+ name: str
+ turns: int
+ wire: str
+ wire_diameter_mm: Optional[float] = None
+ parallels: int = 1
+ isolation_side: str = "primary"
+
+
+@dataclass
+class BOMItem:
+ """Bill of materials item."""
+ component: str
+ part_number: str
+ quantity: int
+ description: str
+ manufacturer: Optional[str] = None
+
+
+@dataclass
+class DesignResult:
+ """Human-readable design result with all relevant magnetic component info."""
+ core: str
+ material: str
+ core_family: str
+ windings: list[WindingInfo]
+ air_gap_mm: float
+ gap_type: str = "subtractive"
+ num_gaps: int = 1
+ core_loss_w: float = 0.0
+ copper_loss_w: float = 0.0
+ total_loss_w: float = 0.0
+ temp_rise_c: float = 0.0
+ max_temperature_c: float = 0.0
+ bpk_tesla: float = 0.0
+ saturation_margin: float = 0.0
+ inductance_h: float = 0.0
+ height_mm: float = 0.0
+ width_mm: float = 0.0
+ depth_mm: float = 0.0
+ weight_g: float = 0.0
+ bom: list[BOMItem] = field(default_factory=list)
+ warnings: list[str] = field(default_factory=list)
+ _raw_mas: Optional[dict] = field(default=None, repr=False)
+
+ @property
+ def primary_turns(self) -> Optional[int]:
+ for w in self.windings:
+ if w.isolation_side == "primary" or w.name.lower() == "primary":
+ return w.turns
+ return self.windings[0].turns if self.windings else None
+
+ @property
+ def primary_wire(self) -> Optional[str]:
+ for w in self.windings:
+ if w.isolation_side == "primary" or w.name.lower() == "primary":
+ return w.wire
+ return self.windings[0].wire if self.windings else None
+
+ @property
+ def secondary_turns(self) -> Optional[int]:
+ for w in self.windings:
+ if w.isolation_side == "secondary" or w.name.lower() == "secondary":
+ return w.turns
+ return None
+
+ @property
+ def secondary_wire(self) -> Optional[str]:
+ for w in self.windings:
+ if w.isolation_side == "secondary" or w.name.lower() == "secondary":
+ return w.wire
+ return None
+
+ @classmethod
+ def from_mas(cls, mas: dict) -> "DesignResult":
+ """Create DesignResult from MAS output dict."""
+ magnetic = mas.get("magnetic", {})
+ outputs = mas.get("outputs", {})
+ core = magnetic.get("core", {})
+ coil = magnetic.get("coil", {})
+
+ # Core info
+ func_desc = core.get("functionalDescription", {})
+ shape_info = func_desc.get("shape", {})
+ material_info = func_desc.get("material", {})
+
+ if isinstance(shape_info, str):
+ core_name, core_family = shape_info, shape_info.split()[0] if shape_info else ""
+ else:
+ core_name = shape_info.get("name", "Unknown")
+ core_family = shape_info.get("family", core_name.split()[0] if core_name else "")
+
+ material_name = material_info if isinstance(material_info, str) else material_info.get("name", "Unknown")
+
+ # Gap info
+ gapping = func_desc.get("gapping", [])
+ total_gap, gap_type, num_gaps = 0.0, "subtractive", 0
+ for gap in gapping:
+ if gap.get("type") in ("subtractive", "additive"):
+ total_gap += gap.get("length", 0)
+ gap_type = gap.get("type", "subtractive")
+ num_gaps += 1
+
+ # Windings
+ windings = []
+ for w in coil.get("functionalDescription", []):
+ wire_info = w.get("wire", {})
+ wire_name = wire_info if isinstance(wire_info, str) else wire_info.get("name", "Unknown")
+ wire_diameter = None
+ if isinstance(wire_info, dict):
+ conducting = wire_info.get("conductingDiameter", {})
+ if isinstance(conducting, dict) and conducting.get("nominal"):
+ wire_diameter = conducting["nominal"] * 1000
+ windings.append(WindingInfo(w.get("name", f"Winding {len(windings)+1}"),
+ w.get("numberTurns", 0), wire_name, wire_diameter,
+ w.get("numberParallels", 1), w.get("isolationSide", "primary")))
+
+ # Losses from outputs
+ core_loss, copper_loss, temp_rise, max_temp, bpk = 0.0, 0.0, 0.0, 0.0, 0.0
+ if outputs:
+ op_output = outputs[0] if isinstance(outputs, list) and outputs else outputs if isinstance(outputs, dict) else {}
+ cl = op_output.get("coreLosses", {})
+ if isinstance(cl, dict):
+ core_loss = cl.get("coreLosses", 0.0) or 0.0
+ bpk = cl.get("magneticFluxDensityPeak", 0.0) or 0.0
+ temp_rise = cl.get("maximumCoreTemperatureRise", 0.0) or 0.0
+ max_temp = cl.get("maximumCoreTemperature", 0.0) or 0.0
+ elif isinstance(cl, (int, float)):
+ core_loss = float(cl)
+ wl = op_output.get("windingLosses", {})
+ copper_loss = wl.get("windingLosses", 0.0) if isinstance(wl, dict) else float(wl) if isinstance(wl, (int, float)) else 0.0
+
+ # Dimensions - extract from shape dimensions (A=width, B=height, C=depth/2)
+ height, width, depth = 0.0, 0.0, 0.0
+ shape_dims = shape_info.get("dimensions", {}) if isinstance(shape_info, dict) else {}
+
+ def get_dim_mm(dim_data):
+ """Extract dimension in mm from MAS dimension object."""
+ if not isinstance(dim_data, dict):
+ return 0.0
+ nominal = dim_data.get("nominal")
+ if nominal is not None:
+ return nominal * 1000
+ min_val = dim_data.get("minimum")
+ max_val = dim_data.get("maximum")
+ if min_val is not None and max_val is not None:
+ return (min_val + max_val) / 2 * 1000
+ return (min_val or max_val or 0.0) * 1000
+
+ if shape_dims:
+ width = get_dim_mm(shape_dims.get("A", {}))
+ height = get_dim_mm(shape_dims.get("B", {}))
+ depth = get_dim_mm(shape_dims.get("C", {})) * 2 # C is typically half-depth
+
+ saturation_margin = max(0, (0.35 - bpk) / 0.35) if bpk > 0 else 0.0
+ warnings = []
+ if bpk > 0.3: warnings.append(f"High flux density ({bpk*1000:.0f} mT)")
+ if temp_rise > 40: warnings.append(f"High temperature rise ({temp_rise:.0f}K)")
+ if saturation_margin < 0.15: warnings.append("Low saturation margin")
+
+ bom = [BOMItem("Core", core_name, 1, f"{material_name} ferrite core",
+ material_info.get("manufacturer") if isinstance(material_info, dict) else None)]
+ for w in windings:
+ bom.append(BOMItem(f"{w.name} Wire", w.wire, 1, f"{w.turns} turns, {w.parallels} parallel(s)"))
+
+ return cls(core_name, material_name, core_family, windings, total_gap * 1000, gap_type, num_gaps,
+ core_loss, copper_loss, core_loss + copper_loss, temp_rise, max_temp, bpk,
+ saturation_margin, 0.0, height, width, depth, 0.0, bom, warnings, mas)
+
+ def summary(self) -> str:
+ lines = [f"Core: {self.core} ({self.material})", f"Air Gap: {self.air_gap_mm:.2f} mm", ""]
+ for w in self.windings:
+ line = f"{w.name}: {w.turns} turns of {w.wire}"
+ if w.parallels > 1: line += f" ({w.parallels} parallel)"
+ lines.append(line)
+ lines.extend(["", f"Core Loss: {self.core_loss_w:.3f} W", f"Copper Loss: {self.copper_loss_w:.3f} W",
+ f"Total Loss: {self.total_loss_w:.3f} W", "", f"B_peak: {self.bpk_tesla*1000:.1f} mT",
+ f"Temp Rise: {self.temp_rise_c:.1f} K"])
+ if self.warnings:
+ lines.extend(["", "Warnings:"] + [f" - {w}" for w in self.warnings])
+ return "\n".join(lines)
+
+ def __str__(self) -> str:
+ return self.summary()
diff --git a/api/waveforms.py b/api/waveforms.py
new file mode 100644
index 0000000..7ccd211
--- /dev/null
+++ b/api/waveforms.py
@@ -0,0 +1,313 @@
+"""
+MAS (Magnetic Agnostic Structure) generator and waveform utilities.
+
+Generates MAS-compliant input structures and waveforms for PyOpenMagnetics.
+"""
+
+import math
+from typing import Optional, Any
+
+
+# =============================================================================
+# Waveform Generators
+# =============================================================================
+
+def triangular_current(i_dc: float, i_ripple_pp: float, duty: float, freq: float) -> dict:
+ """Generate triangular current waveform (typical inductor current)."""
+ period = 1.0 / freq
+ t_on = duty * period
+ i_min = i_dc - i_ripple_pp / 2
+ i_max = i_dc + i_ripple_pp / 2
+ return {"waveform": {"data": [i_min, i_max, i_min], "time": [0, t_on, period]}}
+
+
+def rectangular_voltage(v_on: float, v_off: float, duty: float, freq: float) -> dict:
+ """Generate rectangular voltage waveform."""
+ period = 1.0 / freq
+ t_on = duty * period
+ return {"waveform": {"data": [v_on, v_on, v_off, v_off], "time": [0, t_on, t_on, period]}}
+
+
+def sinusoidal_current(i_rms: float, freq: float, dc_offset: float = 0.0, num_points: int = 64) -> dict:
+ """Generate sinusoidal current waveform."""
+ period = 1.0 / freq
+ i_peak = i_rms * math.sqrt(2)
+ times = [i * period / num_points for i in range(num_points + 1)]
+ data = [dc_offset + i_peak * math.sin(2 * math.pi * freq * t) for t in times]
+ return {"waveform": {"data": data, "time": times}}
+
+
+def flyback_primary_current(vin: float, vout: float, pout: float, n: float, lm: float,
+ freq: float, efficiency: float = 0.85, mode: str = "ccm") -> dict:
+ """Generate flyback primary winding current waveform."""
+ period = 1.0 / freq
+ pin = pout / efficiency
+ vout_reflected = n * vout
+ duty = vout_reflected / (vin + vout_reflected)
+ t_on = duty * period
+
+ if mode == "dcm":
+ i_pk = math.sqrt(2 * pin / (freq * lm * duty))
+ t_reset = 2 * lm * i_pk / vout_reflected
+ t_reset = min(t_reset, period - t_on)
+ return {"waveform": {"data": [0, i_pk, 0, 0], "time": [0, t_on, t_on + t_reset, period]}}
+ else:
+ i_avg = pin / vin
+ delta_i = (vin * t_on) / lm
+ i_min = max(0, i_avg - delta_i / 2)
+ i_max = i_avg + delta_i / 2
+ return {"waveform": {"data": [i_min, i_max, 0, 0], "time": [0, t_on, t_on, period]}}
+
+
+def flyback_secondary_current(vin: float, vout: float, iout: float, n: float, lm: float,
+ freq: float, mode: str = "ccm") -> dict:
+ """Generate flyback secondary winding current waveform."""
+ period = 1.0 / freq
+ vout_reflected = n * vout
+ duty = vout_reflected / (vin + vout_reflected)
+ t_on = duty * period
+ t_off = period - t_on
+ lm_sec = lm / (n * n)
+
+ if mode == "dcm":
+ pout = vout * iout
+ pin = pout / 0.85
+ i_pri_pk = math.sqrt(2 * pin / (freq * lm * duty))
+ i_sec_pk = i_pri_pk * n
+ t_reset = min(lm_sec * i_sec_pk / vout, t_off)
+ return {"waveform": {"data": [0, 0, i_sec_pk, 0, 0], "time": [0, t_on, t_on, t_on + t_reset, period]}}
+ else:
+ i_sec_avg = iout / (1 - duty)
+ delta_i_sec = (vout * t_off) / lm_sec
+ i_sec_max = i_sec_avg + delta_i_sec / 2
+ i_sec_min = max(0, i_sec_avg - delta_i_sec / 2)
+ return {"waveform": {"data": [0, 0, i_sec_max, i_sec_min], "time": [0, t_on, t_on, period]}}
+
+
+def buck_inductor_current(vin: float, vout: float, iout: float, inductance: float, freq: float) -> dict:
+ """Generate buck converter inductor current waveform."""
+ period = 1.0 / freq
+ duty = vout / vin
+ t_on = duty * period
+ delta_i = (vin - vout) * t_on / inductance
+ i_min = max(0, iout - delta_i / 2)
+ i_max = iout + delta_i / 2
+ return {"waveform": {"data": [i_min, i_max, i_min], "time": [0, t_on, period]}}
+
+
+def buck_inductor_voltage(vin: float, vout: float, freq: float) -> dict:
+ """Generate buck converter inductor voltage waveform."""
+ period = 1.0 / freq
+ duty = vout / vin
+ t_on = duty * period
+ return {"waveform": {"data": [vin - vout, vin - vout, -vout, -vout], "time": [0, t_on, t_on, period]}}
+
+
+def boost_inductor_current(vin: float, vout: float, pout: float, inductance: float,
+ freq: float, efficiency: float = 0.9) -> dict:
+ """Generate boost converter inductor current waveform."""
+ period = 1.0 / freq
+ duty = 1 - (vin / vout)
+ t_on = duty * period
+ pin = pout / efficiency
+ i_avg = pin / vin
+ delta_i = vin * t_on / inductance
+ i_min = max(0, i_avg - delta_i / 2)
+ i_max = i_avg + delta_i / 2
+ return {"waveform": {"data": [i_min, i_max, i_min], "time": [0, t_on, period]}}
+
+
+def boost_inductor_voltage(vin: float, vout: float, freq: float) -> dict:
+ """Generate boost converter inductor voltage waveform."""
+ period = 1.0 / freq
+ duty = 1 - (vin / vout)
+ t_on = duty * period
+ return {"waveform": {"data": [vin, vin, vin - vout, vin - vout], "time": [0, t_on, t_on, period]}}
+
+
+def boost_inductor_waveforms(
+ vin: float,
+ vout: float,
+ power: float,
+ inductance: float,
+ frequency: float,
+ efficiency: float = 0.9,
+) -> dict:
+ """
+ Calculate complete inductor waveforms for boost converter.
+
+ This function provides detailed waveform analysis including RMS values,
+ peak values, and other metrics needed for loss calculation.
+
+ Args:
+ vin: Input voltage (V)
+ vout: Output voltage (V)
+ power: Output power (W)
+ inductance: Inductor value (H)
+ frequency: Switching frequency (Hz)
+ efficiency: Converter efficiency (default 0.9)
+
+ Returns:
+ dict with waveform data and calculated metrics:
+ - current/voltage waveforms in MAS format
+ - i_dc, i_ripple_pp, i_rms, i_peak
+ - v_rms, duty_cycle
+ - l_critical (boundary conduction inductance)
+ """
+ period = 1.0 / frequency
+ duty = 1 - (vin / vout)
+ t_on = duty * period
+
+ # Input power and average current
+ pin = power / efficiency
+ i_dc = pin / vin
+
+ # Current ripple
+ delta_i = vin * t_on / inductance
+ i_min = max(0, i_dc - delta_i / 2)
+ i_max = i_dc + delta_i / 2
+
+ # RMS current (triangular waveform on DC)
+ # I_rms = sqrt(I_dc^2 + (delta_I)^2 / 12)
+ i_rms = math.sqrt(i_dc ** 2 + (delta_i ** 2) / 12)
+
+ # Voltage waveform
+ v_on = vin # During switch ON
+ v_off = vin - vout # During switch OFF (negative)
+
+ # RMS voltage
+ v_rms = math.sqrt(duty * v_on ** 2 + (1 - duty) * v_off ** 2)
+
+ # Critical inductance for boundary conduction mode (BCM)
+ # L_crit = Vin * D * (1-D) / (2 * f * I_out)
+ i_out = power / vout
+ l_critical = (vin * duty * (1 - duty)) / (2 * frequency * i_out) if i_out > 0 else 0
+
+ # Build MAS-format waveforms
+ current_waveform = {"waveform": {"data": [i_min, i_max, i_min], "time": [0, t_on, period]}}
+ voltage_waveform = {"waveform": {"data": [v_on, v_on, v_off, v_off], "time": [0, t_on, t_on, period]}}
+
+ return {
+ # MAS format waveforms
+ "current": current_waveform,
+ "voltage": voltage_waveform,
+
+ # Current metrics
+ "i_dc": i_dc,
+ "i_ripple_pp": delta_i,
+ "i_min": i_min,
+ "i_max": i_max,
+ "i_rms": i_rms,
+ "i_peak": i_max,
+
+ # Voltage metrics
+ "v_on": v_on,
+ "v_off": v_off,
+ "v_rms": v_rms,
+
+ # Operating parameters
+ "duty_cycle": duty,
+ "frequency": frequency,
+ "period": period,
+ "t_on": t_on,
+
+ # Design parameters
+ "l_critical": l_critical,
+ "power_in": pin,
+ "power_out": power,
+ "efficiency": efficiency,
+ }
+
+
+def calculate_critical_inductance_dcm(vin: float, vout: float, i_out: float, frequency: float) -> float:
+ """
+ Calculate critical inductance for DCM/CCM boundary.
+
+ Below this inductance, the converter operates in DCM.
+ Above this inductance, the converter operates in CCM.
+
+ Args:
+ vin: Input voltage (V)
+ vout: Output voltage (V)
+ i_out: Output current (A)
+ frequency: Switching frequency (Hz)
+
+ Returns:
+ Critical inductance in Henries
+ """
+ duty = 1 - (vin / vout)
+ return (vin * duty * (1 - duty)) / (2 * frequency * i_out)
+
+
+# =============================================================================
+# MAS Structure Generators
+# =============================================================================
+
+def generate_design_requirements(
+ magnetizing_inductance: float,
+ turns_ratios: Optional[list[float]] = None,
+ leakage_inductance: Optional[float] = None,
+ insulation: Optional[dict] = None,
+ max_dimensions: Optional[dict] = None,
+ max_temperature: Optional[float] = None,
+ name: Optional[str] = None,
+ tolerance: float = 0.1
+) -> dict:
+ """Generate MAS design requirements structure."""
+ req: dict[str, Any] = {
+ "magnetizingInductance": {
+ "nominal": magnetizing_inductance,
+ "minimum": magnetizing_inductance * (1 - tolerance),
+ "maximum": magnetizing_inductance * (1 + tolerance)
+ },
+ "turnsRatios": [{"nominal": r} for r in (turns_ratios or [])]
+ }
+ if leakage_inductance:
+ req["leakageInductance"] = {"nominal": leakage_inductance}
+ if insulation:
+ req["insulation"] = insulation
+ if max_dimensions:
+ req["maximumDimensions"] = max_dimensions
+ if max_temperature:
+ req["maximumTemperature"] = {"nominal": max_temperature}
+ if name:
+ req["name"] = name
+ return req
+
+
+def generate_operating_point(
+ frequency: float,
+ excitations: list[dict],
+ name: str = "Operating Point",
+ ambient_temperature: float = 25.0
+) -> dict:
+ """Generate MAS operating point structure."""
+ op: dict[str, Any] = {
+ "name": name,
+ "conditions": {"ambientTemperature": ambient_temperature},
+ "excitationsPerWinding": []
+ }
+ for exc in excitations:
+ exc_entry: dict[str, Any] = {"frequency": frequency}
+ if "current" in exc:
+ exc_entry["current"] = exc["current"]
+ if "voltage" in exc:
+ exc_entry["voltage"] = exc["voltage"]
+ if "name" in exc:
+ exc_entry["name"] = exc["name"]
+ op["excitationsPerWinding"].append(exc_entry)
+ return op
+
+
+def generate_insulation_requirements(
+ insulation_type: str = "Functional",
+ pollution_degree: str = "P2",
+ overvoltage_category: str = "OVC-II",
+ standards: Optional[list[str]] = None
+) -> dict:
+ """Generate MAS insulation requirements structure."""
+ ins = {"insulationType": insulation_type, "pollutionDegree": pollution_degree,
+ "overvoltageCategory": overvoltage_category}
+ if standards:
+ ins["standards"] = standards
+ return ins
diff --git a/docs/Doxyfile b/docs/Doxyfile
new file mode 100644
index 0000000..096e30d
--- /dev/null
+++ b/docs/Doxyfile
@@ -0,0 +1,151 @@
+# Doxyfile configuration for PyOpenMagnetics C++ bindings documentation
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+PROJECT_NAME = "PyOpenMagnetics C++ Bindings"
+PROJECT_NUMBER = "1.0"
+PROJECT_BRIEF = "Python bindings for magnetic component design"
+PROJECT_LOGO =
+OUTPUT_DIRECTORY = docs/doxygen
+CREATE_SUBDIRS = NO
+OUTPUT_LANGUAGE = English
+BRIEF_MEMBER_DESC = YES
+REPEAT_BRIEF = YES
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+EXTRACT_ALL = YES
+EXTRACT_PRIVATE = NO
+EXTRACT_STATIC = YES
+EXTRACT_LOCAL_CLASSES = YES
+HIDE_UNDOC_MEMBERS = NO
+HIDE_UNDOC_CLASSES = NO
+HIDE_FRIEND_COMPOUNDS = NO
+BRIEF_MEMBER_DESC = YES
+INLINE_INHERITED_MEMB = NO
+FULL_PATH_NAMES = NO
+STRIP_FROM_PATH = src/
+CASE_SENSE_NAMES = YES
+GENERATE_TODOLIST = YES
+GENERATE_TESTLIST = YES
+GENERATE_BUGLIST = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+QUIET = NO
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = YES
+WARN_IF_DOC_ERROR = YES
+WARN_NO_PARAMDOC = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+INPUT = src/
+INPUT_ENCODING = UTF-8
+FILE_PATTERNS = *.h \
+ *.cpp
+RECURSIVE = NO
+EXCLUDE =
+EXCLUDE_PATTERNS =
+EXCLUDE_SYMBOLS =
+EXAMPLE_PATH =
+EXAMPLE_PATTERNS =
+EXAMPLE_RECURSIVE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+SOURCE_BROWSER = YES
+INLINE_SOURCES = NO
+STRIP_CODE_COMMENTS = YES
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION = YES
+REFERENCES_LINK_SOURCE = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+ALPHABETICAL_INDEX = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+GENERATE_HTML = YES
+HTML_OUTPUT = html
+HTML_FILE_EXTENSION = .html
+HTML_COLORSTYLE_HUE = 220
+HTML_COLORSTYLE_SAT = 100
+HTML_COLORSTYLE_GAMMA = 80
+HTML_TIMESTAMP = YES
+HTML_DYNAMIC_SECTIONS = YES
+GENERATE_TREEVIEW = YES
+TREEVIEW_WIDTH = 250
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+GENERATE_LATEX = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+GENERATE_MAN = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+GENERATE_XML = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = NO
+SEARCH_INCLUDES = YES
+INCLUDE_PATH =
+INCLUDE_FILE_PATTERNS =
+PREDEFINED =
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+HAVE_DOT = NO
+CLASS_GRAPH = YES
+COLLABORATION_GRAPH = YES
+UML_LOOK = NO
+INCLUDE_GRAPH = YES
+INCLUDED_BY_GRAPH = YES
+CALL_GRAPH = NO
+CALLER_GRAPH = NO
+GRAPHICAL_HIERARCHY = YES
+DIRECTORY_GRAPH = YES
+DOT_IMAGE_FORMAT = png
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..ed5ec40
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,165 @@
+# Contributing Guide
+
+## Setting Up Development Environment
+
+### Prerequisites
+
+1. Install build dependencies:
+
+```bash
+# Linux
+sudo apt-get install python3-dev cmake build-essential
+
+# macOS
+brew install cmake python
+
+# Windows
+# Install Visual Studio Build Tools with C++ support
+```
+
+2. Clone the repository:
+
+```bash
+git clone https://github.com/OpenMagnetics/PyMKF.git
+cd PyMKF
+```
+
+3. Create virtual environment:
+
+```bash
+python -m venv venv
+source venv/bin/activate # Linux/macOS
+.\venv\Scripts\activate # Windows
+```
+
+4. Install development dependencies:
+
+```bash
+pip install -e ".[dev]"
+```
+
+## Project Structure
+
+```
+PyMKF/
+├── api/ # Python API layer
+│ ├── design.py # Fluent Design API
+│ ├── MAS.py # Auto-generated dataclasses
+│ ├── validation.py # JSON schema validation
+│ ├── mcp/ # MCP Server
+│ └── gui/ # Streamlit GUI
+├── src/ # C++ pybind11 bindings
+│ ├── module.cpp # Main binding definitions
+│ ├── database.cpp # Core/material/wire database
+│ ├── advisers.cpp # Design recommendation algorithms
+│ └── ...
+├── tests/ # pytest test suite
+├── examples/ # Working examples
+├── notebooks/ # Jupyter notebooks
+└── docs/ # Documentation
+```
+
+## Running Tests
+
+```bash
+# Run all tests
+pytest tests/ -v --tb=short
+
+# Run specific test file
+pytest tests/test_core.py -v
+
+# Run all examples
+./scripts/run_examples.sh
+
+# Quick validation
+./scripts/pre_commit_check.sh
+```
+
+## Code Style
+
+### Python Code
+
+- Follow PEP 8
+- Use type hints
+- Document using NumPy style docstrings
+
+```python
+def calculate_losses(
+ core_data: dict[str, Any],
+ temperature: float
+) -> float:
+ """Calculate core losses.
+
+ Parameters
+ ----------
+ core_data : dict[str, Any]
+ Core specifications and properties
+ temperature : float
+ Operating temperature in Celsius
+
+ Returns
+ -------
+ float
+ Calculated losses in Watts
+ """
+ pass
+```
+
+### C++ Code
+
+- Follow the Google C++ Style Guide
+- Use consistent naming conventions
+- Document using Doxygen style
+
+## Building Documentation
+
+```bash
+# Install documentation dependencies
+pip install mkdocs mkdocs-material
+
+# Serve documentation locally
+mkdocs serve
+
+# Build documentation
+mkdocs build
+```
+
+## Contributing Workflow
+
+1. Create a new branch:
+```bash
+git checkout -b feature/new-feature
+```
+
+2. Make changes and commit:
+```bash
+git add .
+git commit -m "feat: add new feature"
+```
+
+3. Push changes and create Pull Request
+
+### Commit Messages
+
+Follow conventional commits:
+
+- `feat:` New feature
+- `fix:` Bug fix
+- `docs:` Documentation
+- `style:` Formatting
+- `refactor:` Code restructuring
+- `test:` Adding tests
+- `chore:` Maintenance
+
+## Code Review Process
+
+1. Submit PR with:
+ - Clear description
+ - Test results
+ - Documentation updates
+
+2. Address review comments
+
+3. Ensure CI/CD pipeline passes:
+ - Tests pass
+ - Code style checks pass
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..5e21657
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,157 @@
+# Examples
+
+## Basic Usage
+
+### Design a Flyback Transformer
+
+```python
+from api.design import Design
+
+design = Design.flyback() \
+ .vin_ac(85, 265) \ # Universal AC input
+ .output(12, 5) \ # 12V @ 5A = 60W
+ .fsw(100e3) \ # 100 kHz switching
+ .efficiency(0.88) \ # Target efficiency
+ .max_height(25) \ # Size constraint
+ .prefer("efficiency") # Optimization priority
+
+results = design.solve(max_results=MAX_RESULTS, verbose=True)
+```
+
+### Design a Buck Inductor
+
+```python
+design = Design.buck() \
+ .vin(10, 14) \ # 12V +/- tolerance
+ .vout(3.3) \ # Output voltage
+ .iout(10) \ # Output current
+ .fsw(500e3) \ # Switching frequency
+ .ripple_ratio(0.3) # 30% current ripple
+
+results = design.solve()
+```
+
+### Design a Boost Inductor
+
+```python
+design = Design.boost() \
+ .vin(200, 400) \ # Input voltage range
+ .vout(800) \ # Output voltage
+ .pout(3000) \ # Output power
+ .fsw(65e3) \ # Switching frequency
+ .efficiency(0.97) # Target efficiency
+
+results = design.solve()
+```
+
+## Database Queries
+
+### Query Materials
+
+```python
+import PyOpenMagnetics
+
+# Get all available materials
+materials = PyOpenMagnetics.get_core_material_names()
+print(f"Available materials: {len(materials)}")
+
+# Get material details
+material = PyOpenMagnetics.find_core_material_by_name("3C95")
+
+# Get Steinmetz coefficients for loss calculation
+steinmetz = PyOpenMagnetics.get_core_material_steinmetz_coefficients("3C95", 100000)
+print(f"k={steinmetz['k']}, alpha={steinmetz['alpha']}, beta={steinmetz['beta']}")
+```
+
+### Query Core Shapes
+
+```python
+# Get all available shapes
+shapes = PyOpenMagnetics.get_core_shape_names()
+
+# Get shape details
+shape = PyOpenMagnetics.find_core_shape_by_name("E 42/21/15")
+```
+
+### Query Wires
+
+```python
+# Get available wires
+wires = PyOpenMagnetics.get_wire_names()
+
+# Get wire details
+wire = PyOpenMagnetics.find_wire_by_name("Round 0.5 - Grade 1")
+```
+
+## Loss Calculations
+
+### Core Losses
+
+```python
+models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+
+losses = PyOpenMagnetics.calculate_core_losses(core, coil, inputs, models)
+print(f"Core loss: {losses['coreLosses']:.3f} W")
+print(f"B_peak: {losses['magneticFluxDensityPeak']*1000:.1f} mT")
+```
+
+### Winding Losses
+
+```python
+winding_losses = PyOpenMagnetics.calculate_winding_losses(magnetic, operating_point, 85)
+print(f"Winding loss: {winding_losses['windingLosses']:.3f} W")
+```
+
+## Real-World Examples
+
+The `examples/` directory contains 17 working examples across different applications:
+
+### Consumer Electronics
+- USB PD 65W charger (`consumer/usb_pd_65w.py`)
+
+### Automotive
+- 48V DC-DC converters (`automotive/boost_half_bridge_multi_op.py`)
+- Gate drive transformers (`automotive/gate_drive_isolated.py`)
+
+### Industrial
+- Boost inductor design (`industrial/boost_inductor_design.py`)
+
+### Telecom
+- 48V 3kW rectifier (`telecom/rectifier_48v_3kw.py`)
+
+### Advanced
+- Custom magnetic simulation (`advanced/custom_magnetic_simulation.py`)
+- NSGA2 multi-objective optimization (`advanced/nsga2_inductor_optimization.py`)
+
+## Interactive Notebooks
+
+The `notebooks/` directory contains Jupyter notebooks for interactive learning:
+
+1. **01_getting_started.ipynb** - Introduction to PyOpenMagnetics
+2. **02_buck_inductor.ipynb** - Buck converter inductor design
+3. **03_core_losses.ipynb** - Core loss analysis and material comparison
+
+## Best Practices
+
+1. **Always process inputs first**
+ ```python
+ processed = PyOpenMagnetics.process_inputs(inputs)
+ magnetics = PyOpenMagnetics.calculate_advised_magnetics(processed, 5, "STANDARD_CORES")
+ ```
+
+2. **Handle return values correctly**
+ ```python
+ result = PyOpenMagnetics.calculate_advised_magnetics(processed, 5, "STANDARD_CORES")
+ if isinstance(result, str):
+ magnetics = json.loads(result)
+ elif isinstance(result, dict):
+ data = result.get("data", result)
+ magnetics = json.loads(data) if isinstance(data, str) else data
+ ```
+
+3. **Check for errors**
+ ```python
+ result = PyOpenMagnetics.calculate_core_data(data, False)
+ if isinstance(result, str) and result.startswith("Exception:"):
+ print(f"Error: {result}")
+ ```
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..0afe66e
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,39 @@
+# PyOpenMagnetics Documentation
+
+Welcome to the PyOpenMagnetics documentation!
+
+PyOpenMagnetics is a Python library for designing and analyzing magnetic components - transformers, inductors, and chokes for power electronics. It wraps the OpenMagnetics MKF C++ engine and provides multiple interfaces.
+
+## Features
+
+- **Fluent Design API** - `Design.flyback().vin_ac(85, 265).output(12, 5).solve()`
+- **Low-level C++ Bindings** - Direct access to MKF engine via `PyOpenMagnetics` module
+- **MCP Server** - AI assistant integration for natural language design
+- **Streamlit GUI** - Visual interface for hardware engineers
+
+## Quick Start
+
+```python
+from api.design import Design
+
+result = Design.flyback() \
+ .vin_ac(85, 265) \
+ .output(12, 5) \
+ .fsw(100e3) \
+ .solve(verbose=True)
+
+print(f"Best design: {result[0].core} with {result[0].material}")
+print(f"Total loss: {result[0].total_loss_w:.2f} W")
+```
+
+## Available Functions
+
+PyOpenMagnetics provides comprehensive functionality including:
+
+- Core data calculation and database access
+- Material property queries (Steinmetz coefficients, permeability)
+- Winding design and loss calculations
+- Design advisers for optimal component selection
+- Visualization tools (core plots, field plots)
+
+See the [Examples](examples.md) section for detailed usage patterns.
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..78bf3bb
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,65 @@
+# Installation Guide
+
+## Requirements
+
+### Core Dependencies
+- Python >= 3.10
+- numpy >= 1.19.0
+- pandas >= 1.3.0
+
+### Build Dependencies
+- CMake >= 3.15
+- C++ compiler with C++23 support
+- pybind11
+- Node.js 18+ (for schema generation)
+
+## Installation Methods
+
+### From PyPI (Recommended)
+
+PyOpenMagnetics can be installed using pip:
+
+```bash
+pip install pyopenmagnetics
+```
+
+### From Source
+
+```bash
+git clone https://github.com/OpenMagnetics/PyMKF.git
+cd PyMKF
+pip install .
+```
+
+### Build Wheel
+
+```bash
+python -m build
+```
+
+### Cross-Platform Wheel Building
+
+```bash
+python -m cibuildwheel --output-dir wheelhouse
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **CMake Not Found**
+ - Ensure CMake is installed and in your PATH
+ - Check CMake version requirements (>= 3.15)
+
+2. **Compiler Errors**
+ - Verify C++23 compiler support
+ - On Linux, use gcc-toolset-13 or newer
+
+3. **Python Version Mismatch**
+ - PyOpenMagnetics supports Python 3.10-3.12
+ - Check virtual environment settings
+
+## References
+
+- [PyPI Package](https://pypi.org/project/PyMKF/)
+- [GitHub Repository](https://github.com/OpenMagnetics/PyMKF)
diff --git a/docs/intro.md b/docs/intro.md
new file mode 100644
index 0000000..ffdb859
--- /dev/null
+++ b/docs/intro.md
@@ -0,0 +1,70 @@
+# Introduction to PyOpenMagnetics
+
+PyOpenMagnetics is a comprehensive Python library for magnetic component design. It provides tools for designing transformers, inductors, and chokes used in power electronics applications.
+
+## Architecture
+
+```
+User Interfaces
+├── api/design.py Fluent Design API (Design.flyback(), Design.buck(), etc.)
+├── api/mcp/ MCP Server for AI assistants
+└── api/gui/ Streamlit GUI
+ │
+ ▼
+PyOpenMagnetics Module
+├── Database Access get_core_materials(), find_core_shape_by_name(), get_wires()
+├── Core Calculations calculate_core_data(), calculate_inductance_from_number_turns_and_gapping()
+├── Loss Models calculate_core_losses() [STEINMETZ, IGSE, MSE, BARG, ROSHEN, ALBACH]
+├── Winding Engine wind(), wind_by_sections(), wind_by_layers()
+├── Design Adviser process_inputs(), calculate_advised_cores(), calculate_advised_magnetics()
+├── Simulation simulate(), magnetic_autocomplete()
+└── Visualization plot_core(), plot_coil_2d(), plot_field_2d()
+ │
+ ▼
+MKF C++ Engine (via pybind11)
+ │
+ ▼
+MAS JSON Schema (Magnetic Agnostic Structure)
+```
+
+## Key Concepts
+
+### MAS Schema (Magnetic Agnostic Structure)
+
+Standardized JSON structures for magnetic components:
+
+- **Inputs**: Design requirements + operating points
+- **Magnetic**: Core + Coil assembly
+- **Outputs**: Simulation results (losses, temperature, inductance)
+- **Mas**: Complete specification (Inputs + Magnetic + Outputs)
+
+### Data Flow
+
+```
+User specs → Design API → MAS JSON → C++ Engine → MAS JSON → DesignResult objects
+```
+
+## Supported Topologies
+
+| Topology | Method | Use Case |
+|----------|--------|----------|
+| Flyback | `Design.flyback()` | Isolated SMPS <150W |
+| Buck | `Design.buck()` | Non-isolated step-down |
+| Boost | `Design.boost()` | Non-isolated step-up, PFC |
+| Forward | `Design.forward()` | Isolated, higher power |
+| LLC | `Design.llc()` | High efficiency, resonant |
+| Inductor | `Design.inductor()` | Standalone inductors |
+
+## Core Materials
+
+| Manufacturer | Materials |
+|--------------|-----------|
+| TDK/EPCOS | N27, N49, N87, N95, N97, PC40, PC95 |
+| Ferroxcube | 3C90, 3C94, 3C95, 3C96, 3F3, 3F4, 3F35 |
+| Fair-Rite | 67, 77, 78 |
+| Magnetics Inc | MPP, High Flux, Kool Mu, XFlux |
+| Micrometals | Iron powder: -2, -8, -18, -26, -52 |
+
+## Core Shape Families
+
+E, EI, EFD, EQ, ER, ETD, EC, PQ, PM, RM, T (toroidal), P, PT, U, UI, LP (planar)
diff --git a/docs/src_reference.md b/docs/src_reference.md
new file mode 100644
index 0000000..1eb40a2
--- /dev/null
+++ b/docs/src_reference.md
@@ -0,0 +1,429 @@
+# PyOpenMagnetics C++ Bindings Reference
+
+## Overview
+
+The `src/` directory contains **24 files** (12 headers, 12 implementations) that create Python bindings for the OpenMagnetics C++ engine via pybind11. The module exposes **183 Python functions** organized into 11 categories.
+
+**Module Name:** `PyOpenMagnetics`
+
+## Documentation
+
+All source files include comprehensive Doxygen documentation:
+- File-level documentation explaining purpose and usage
+- Function-level documentation with parameters, return values, and examples
+- Cross-references between related modules
+
+### Generating HTML Documentation
+
+```bash
+# From project root
+cd docs
+doxygen Doxyfile
+# Open docs/doxygen/html/index.html in browser
+```
+
+## Architecture
+
+```
+User Python Code
+ |
+ v
+module.cpp (pybind11 entry point)
+ |
+ v
+[Python Bindings Registration]
+ |-- database.cpp (Load/cache designs)
+ |-- core.cpp (Material & geometry)
+ |-- wire.cpp (Wire database)
+ |-- bobbin.cpp (Coil support)
+ |-- winding.cpp (Wire placement)
+ |-- advisers.cpp (Design recommendation)
+ |-- losses.cpp (Loss calculations)
+ |-- simulation.cpp (Full EM simulation)
+ |-- plotting.cpp (SVG visualization)
+ |-- settings.cpp (Config & defaults)
+ +-- utils.cpp (Helper functions)
+ |
+ v
+OpenMagnetics C++ Library (MKF engine)
+ |
+ v
+MAS Objects (JSON-serializable design specs)
+```
+
+## File Reference
+
+### 1. module.cpp - Entry Point
+**Purpose:** Main pybind11 module definition
+**Key:** `PYBIND11_MODULE(PyOpenMagnetics, m)` creates the module and registers all 11 binding namespaces.
+
+### 2. common.h - Shared Infrastructure
+**Purpose:** Global headers, pybind11/JSON includes, MAS library integration
+**Key Declarations:**
+```cpp
+extern std::map masDatabase; // In-memory MAS cache
+```
+**Dependencies:** Foundation for all other modules
+
+### 3. database.h/cpp - Database Access & Caching
+**Purpose:** Load and manage magnetic component databases; cache MAS objects
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `load_databases` | `databases_json` | void | Load all component databases |
+| `read_databases` | `path`, `add_internal_data` | str | Read NDJSON files |
+| `load_mas` | `key`, `mas_json`, `expand` | str | Cache MAS object |
+| `load_magnetic` | `key`, `magnetic_json`, `expand` | str | Cache magnetic design |
+| `read_mas` | `key` | json | Retrieve cached MAS |
+| `load_core_materials` | `file` (optional) | size_t | Load materials database |
+| `load_core_shapes` | `file` (optional) | size_t | Load shapes database |
+| `load_wires` | `file` (optional) | size_t | Load wires database |
+| `clear_databases` | - | void | Reset all caches |
+| `is_core_material_database_empty` | - | bool | Check if empty |
+| `is_core_shape_database_empty` | - | bool | Check if empty |
+| `is_wire_database_empty` | - | bool | Check if empty |
+| `load_magnetics_from_file` | `path`, `expand` | str | Load from NDJSON |
+| `clear_magnetic_cache` | - | str | Clear cached magnetics |
+
+### 4. core.h/cpp - Core Materials & Geometry
+**Purpose:** Query core materials/shapes and perform core calculations
+
+#### Material Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_core_materials` | - | json array | All available materials |
+| `get_core_material_names` | - | json array | Material name list |
+| `get_core_material_names_by_manufacturer` | `manufacturer` | json array | Filtered by manufacturer |
+| `find_core_material_by_name` | `name` | json | Complete material data |
+| `get_material_permeability` | `name`, `temp`, `dcBias`, `freq` | double | Permeability at conditions |
+| `get_material_resistivity` | `name`, `temp` | double | Resistivity (Ohm*m) |
+| `get_core_material_steinmetz_coefficients` | `name`, `freq` | json | k, alpha, beta coefficients |
+| `get_core_temperature_dependant_parameters` | `core_data`, `temp` | json | Bsat, Hsat, permeability |
+
+#### Shape Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_core_shapes` | - | json array | All shapes |
+| `get_core_shape_families` | - | json array | Shape families (E, ETD, PQ...) |
+| `get_core_shape_names` | `include_toroidal` | json array | Shape names |
+| `find_core_shape_by_name` | `name` | json | Complete shape data |
+| `calculate_shape_data` | `shape_json` | json | Derived shape properties |
+
+#### Core Calculation Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_core_data` | `core_json`, `include_material` | json | Full core analysis |
+| `calculate_core_processed_description` | `core_json` | json | Effective parameters |
+| `calculate_core_geometrical_description` | `core_json` | json array | 3D geometry |
+| `calculate_core_gapping` | `core_json` | json array | Gap configuration |
+| `calculate_gap_reluctance` | `gap_data`, `model` | json | Gap reluctance |
+| `calculate_inductance_from_number_turns_and_gapping` | `core`, `coil`, `op`, `models` | double | Inductance (H) |
+| `calculate_number_turns_from_gapping_and_inductance` | `core`, `inputs`, `models` | double | Required turns |
+| `calculate_gapping_from_number_turns_and_inductance` | `core`, `coil`, `inputs`, `gap_type`, `decimals`, `models` | json | Core with gaps |
+| `calculate_core_maximum_magnetic_energy` | `core_json`, `op_json` | double | Max energy (J) |
+| `calculate_saturation_current` | `magnetic_json`, `temp` | double | Saturation current (A) |
+
+#### Availability Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_available_shape_families` | - | list[str] | All shape families |
+| `get_available_core_materials` | `manufacturer` (opt) | list[str] | Materials list |
+| `get_available_core_manufacturers` | - | list[str] | Manufacturers |
+| `get_available_core_shapes` | - | list[str] | Shape names |
+| `get_available_cores` | - | json array | Pre-configured cores |
+
+### 5. wire.h/cpp - Wire Database & Calculations
+**Purpose:** Query wire database and compute wire properties
+
+#### Database Access
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_wires` | - | json array | All wires |
+| `get_wire_names` | - | json array | Wire names |
+| `get_wire_materials` | - | json array | Conductor materials |
+| `find_wire_by_name` | `name` | json | Wire data |
+| `find_wire_by_dimension` | `dim`, `type`, `standard` | json | Closest match |
+| `get_wire_data_by_standard_name` | `standard_name` | json | e.g., "AWG 24" |
+
+#### Dimension Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_wire_outer_diameter_bare_litz` | `diam`, `n_cond`, `grade`, `std` | double | Bare litz diameter |
+| `get_wire_outer_diameter_served_litz` | `diam`, `n_cond`, `grade`, `n_layers`, `std` | double | Served diameter |
+| `get_wire_outer_diameter_insulated_litz` | `diam`, `n_cond`, `n_layers`, `thick`, `grade`, `std` | double | Insulated diameter |
+| `get_wire_outer_diameter_enamelled_round` | `diam`, `grade`, `std` | double | Enamelled diameter |
+| `get_wire_outer_width_rectangular` | `width`, `grade`, `std` | double | Rectangular width |
+| `get_wire_outer_height_rectangular` | `height`, `grade`, `std` | double | Rectangular height |
+| `get_outer_dimensions` | `wire_json` | list[double] | [width, height] |
+
+#### Coating Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_coating` | `wire_json` | json | Insulation data |
+| `get_coating_thickness` | `wire_json` | double | Thickness (m) |
+| `get_coating_relative_permittivity` | `wire_json` | double | Dielectric constant |
+| `get_available_wire_types` | - | list[str] | round, litz, rectangular, foil |
+| `get_available_wire_standards` | - | list[str] | IEC 60317, NEMA, etc. |
+
+### 6. bobbin.h/cpp - Bobbin Management
+**Purpose:** Query and create bobbins
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_bobbins` | - | json array | All bobbins |
+| `get_bobbin_names` | - | json array | Bobbin names |
+| `find_bobbin_by_name` | `name` | json | Bobbin data |
+| `create_basic_bobbin` | `core_data`, `null_dims` | json | Auto-create from core |
+| `create_basic_bobbin_by_thickness` | `core_data`, `thickness` | json | With specific thickness |
+| `calculate_bobbin_data` | `magnetic_json` | json | Compute properties |
+| `check_if_fits` | `bobbin`, `dimension`, `is_horiz` | bool | Validate fit |
+
+### 7. winding.h/cpp - Winding Placement Engine
+**Purpose:** Place wires in magnetic component windings
+
+#### Winding Methods
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `wind` | `coil`, `reps`, `proportions`, `pattern`, `margins` | json | Main winding algorithm |
+| `wind_planar` | `coil`, `stackup`, `borders`, `wire_dist`, `insul_thick`, `core_dist` | json | PCB winding |
+| `wind_by_sections` | `coil`, `reps`, `proportions`, `pattern`, `insul_thick` | json | Section-based |
+| `wind_by_layers` | `coil`, `insul_layers`, `insul_thick` | json | Layer-based |
+| `wind_by_turns` | `coil` | json | Turn placement |
+| `delimit_and_compact` | `coil` | json | Optimize space |
+
+#### Analysis Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_layers_by_winding_index` | `coil`, `index` | json array | Layers for winding |
+| `get_layers_by_section` | `coil`, `section` | json array | Layers in section |
+| `are_sections_and_layers_fitting` | `coil` | bool | Validate space |
+| `calculate_number_turns` | `n_primary`, `design_req` | list[int] | Turns per winding |
+
+#### Insulation Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_insulation_materials` | - | json array | All insulation specs |
+| `find_insulation_material_by_name` | `name` | json | Material data |
+| `calculate_insulation` | `inputs_json` | json | Safety requirements |
+| `get_available_winding_orientations` | - | list[str] | contiguous, overlapping |
+| `get_available_coil_alignments` | - | list[str] | inner, outer, spread, centered |
+
+### 8. advisers.h/cpp - Design Recommendation
+**Purpose:** Recommend optimal core/magnetic designs
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_advised_cores` | `inputs`, `weights`, `max_results`, `core_mode` | json array | Recommended cores |
+| `calculate_advised_magnetics` | `inputs`, `max_results`, `core_mode` | json array | Complete designs |
+| `calculate_advised_magnetics_from_catalog` | `inputs`, `catalog`, `max_results` | json | From custom catalog |
+| `calculate_advised_magnetics_from_cache` | `inputs`, `filter_flow`, `max_results` | json | From cached designs |
+
+**Core Mode:** `AVAILABLE_CORES` or `STANDARD_CORES`
+**Weights:** `COST`, `EFFICIENCY`, `DIMENSIONS`
+
+### 9. losses.h/cpp - Loss Calculations
+**Purpose:** Calculate core and winding losses
+
+#### Core Losses
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_core_losses` | `core`, `coil`, `inputs`, `models` | json | Core loss + B_peak |
+| `get_core_losses_model_information` | `material` | json | Model descriptions |
+| `calculate_steinmetz_coefficients` | `data`, `ranges` | json | Fit k, alpha, beta |
+
+**Loss Models:** STEINMETZ, IGSE, MSE, BARG, ROSHEN, ALBACH
+
+#### Winding Losses
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_winding_losses` | `magnetic`, `op`, `temp` | json | Total winding loss |
+| `calculate_ohmic_losses` | `coil`, `op`, `temp` | json | DC I²R losses |
+| `calculate_proximity_effect_losses` | `coil`, `temp`, `output`, `field` | json | Proximity losses |
+| `calculate_skin_effect_losses` | `coil`, `output`, `temp` | json | Skin losses |
+
+#### Per-Meter Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_dc_resistance_per_meter` | `wire`, `temp` | double | DC R (Ohm/m) |
+| `calculate_dc_losses_per_meter` | `wire`, `current`, `temp` | double | DC loss (W/m) |
+| `calculate_skin_ac_factor` | `wire`, `current`, `temp` | double | Rac/Rdc ratio |
+| `calculate_effective_skin_depth` | `material`, `current`, `temp` | double | Skin depth (m) |
+
+### 10. simulation.h/cpp - Full EM Simulation
+**Purpose:** Complete magnetic component simulation
+
+#### Main Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `simulate` | `inputs`, `magnetic`, `models` | json | Full MAS with outputs |
+| `export_magnetic_as_subcircuit` | `magnetic` | json | SPICE netlist |
+| `mas_autocomplete` | `mas`, `config` | json | Fill missing fields |
+| `magnetic_autocomplete` | `magnetic`, `config` | json | Complete magnetic |
+| `process_inputs` | `inputs` | json | **CRITICAL:** Add harmonics |
+
+#### Matrix Functions
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `calculate_inductance_matrix` | `magnetic`, `freq`, `models` | json | Self & mutual L |
+| `calculate_leakage_inductance` | `magnetic`, `freq`, `source_idx` | json | Leakage L |
+| `calculate_dc_resistance_per_winding` | `coil`, `temp` | json array | DC R per winding |
+| `calculate_resistance_matrix` | `magnetic`, `temp`, `freq` | json | AC R matrix |
+| `calculate_stray_capacitance` | `coil`, `op`, `models` | json | Parasitic caps |
+
+### 11. plotting.h/cpp - SVG Visualization
+**Purpose:** Generate SVG visualizations
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `plot_core` | `magnetic`, `path` (opt) | json | Core cross-section |
+| `plot_magnetic` | `magnetic`, `path` (opt) | json | Full assembly |
+| `plot_magnetic_field` | `magnetic`, `op`, `path` (opt) | json | H-field visualization |
+| `plot_electric_field` | `magnetic`, `op`, `path` (opt) | json | E-field visualization |
+| `plot_wire` | `wire`, `path` (opt) | json | Wire cross-section |
+| `plot_bobbin` | `magnetic`, `path` (opt) | json | Bobbin structure |
+
+**Return:** `{success: bool, svg: str, error: str}`
+
+### 12. settings.h/cpp - Configuration
+**Purpose:** Global configuration and defaults
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `get_constants` | - | dict | Physical constants |
+| `get_defaults` | - | dict | Default models & values |
+| `get_settings` | - | json | Current settings |
+| `set_settings` | `settings` | void | Update settings |
+| `reset_settings` | - | void | Reset to defaults |
+| `get_default_models` | - | json | Default model selections |
+
+**Key Constants:** vacuumPermeability, vacuumPermittivity, residualGap, minimumNonResidualGap
+
+### 13. utils.h/cpp - Utilities
+**Purpose:** Signal processing and helper functions
+
+| Function | Parameters | Returns | Purpose |
+|----------|-----------|---------|---------|
+| `resolve_dimension_with_tolerance` | `dim_json` | double | Extract nominal value |
+| `calculate_basic_processed_data` | `waveform` | json | RMS, peak, offset |
+| `calculate_harmonics` | `waveform`, `freq` | json | FFT analysis |
+| `calculate_sampled_waveform` | `waveform`, `freq` | json | Resample uniformly |
+| `calculate_instantaneous_power` | `excitation` | double | P(t) value |
+| `calculate_rms_power` | `excitation` | double | Average power |
+| `calculate_reflected_secondary` | `primary`, `turns_ratio` | json | Secondary reflection |
+| `calculate_reflected_primary` | `secondary`, `turns_ratio` | json | Primary reflection |
+
+---
+
+## Module Dependencies
+
+| Module | Depends On |
+|--------|-----------|
+| database | common |
+| core | common |
+| wire | common |
+| bobbin | common, core, wire |
+| winding | common, core, wire, bobbin |
+| advisers | common, database, core, wire, bobbin, winding, settings |
+| losses | common, core, winding, wire |
+| simulation | common, core, winding, losses, settings |
+| plotting | common, simulation |
+| settings | common |
+| utils | common, settings |
+
+**Critical Path:** `advisers.cpp` depends on all other modules
+
+---
+
+## Function Count by Category
+
+| Category | Count | Purpose |
+|----------|-------|---------|
+| Database | 15 | Data loading & caching |
+| Advisers | 4 | Design recommendation |
+| Core | 42 | Materials, shapes, calculations |
+| Wire | 32 | Wire database & selection |
+| Bobbin | 8 | Bobbin lookup & fitting |
+| Winding | 23 | Coil placement & insulation |
+| Losses | 22 | Core & winding loss models |
+| Simulation | 16 | Full EM simulation & matrices |
+| Plotting | 6 | SVG visualization |
+| Settings | 6 | Configuration & constants |
+| Utils | 9 | Signal processing |
+| **TOTAL** | **183** | Complete magnetic design API |
+
+---
+
+## Key Design Patterns
+
+1. **JSON-Based API:** All complex types use JSON for data exchange
+2. **Error Handling:** Functions return `"Exception: ..."` strings on error
+3. **Models Parameter:** Loss/reluctance models are pluggable via `models_json`
+4. **Database Caching:** MAS objects stored in `masDatabase` for reuse
+5. **process_inputs() Required:** Must call before adviser functions to add harmonics
+
+---
+
+## Usage Examples
+
+### Load Databases and Query Materials
+```python
+import PyOpenMagnetics
+
+# Load databases (typically done once at startup)
+PyOpenMagnetics.load_core_materials()
+PyOpenMagnetics.load_core_shapes()
+PyOpenMagnetics.load_wires()
+
+# Query materials
+materials = PyOpenMagnetics.get_core_material_names()
+n87 = PyOpenMagnetics.find_core_material_by_name("N87")
+steinmetz = PyOpenMagnetics.get_core_material_steinmetz_coefficients("N87", 100000)
+```
+
+### Calculate Core Properties
+```python
+# Get shape and create core
+shape = PyOpenMagnetics.find_core_shape_by_name("E 42/21/15")
+core_data = PyOpenMagnetics.calculate_core_data(core_json, True)
+
+# Calculate inductance
+L = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
+ core, coil, operating_point, models
+)
+```
+
+### Run Full Simulation
+```python
+# CRITICAL: Always process inputs first
+processed = PyOpenMagnetics.process_inputs(inputs)
+
+# Get design recommendations
+magnetics = PyOpenMagnetics.calculate_advised_magnetics(
+ processed, 5, "STANDARD_CORES"
+)
+
+# Full simulation with losses
+result = PyOpenMagnetics.simulate(processed, magnetic, models)
+```
+
+### Calculate Losses
+```python
+models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+
+# Core losses
+core_loss = PyOpenMagnetics.calculate_core_losses(core, coil, inputs, models)
+
+# Winding losses
+winding_loss = PyOpenMagnetics.calculate_winding_losses(magnetic, op, 85)
+```
+
+### Generate Visualizations
+```python
+# Plot core cross-section
+result = PyOpenMagnetics.plot_core(magnetic)
+if result["success"]:
+ svg_content = result["svg"]
+
+# Plot complete magnetic assembly
+result = PyOpenMagnetics.plot_magnetic(magnetic, "/path/to/output.svg")
+```
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000..9520676
--- /dev/null
+++ b/examples/__init__.py
@@ -0,0 +1,10 @@
+"""PyOpenMagnetics Basic Examples - flyback_design.py, buck_inductor.py"""
+
+from .common import DEFAULT_MAX_RESULTS, get_output_dir, generate_example_report, print_results_summary
+
+__all__ = [
+ "DEFAULT_MAX_RESULTS",
+ "get_output_dir",
+ "generate_example_report",
+ "print_results_summary",
+]
diff --git a/examples/buck_inductor.py b/examples/buck_inductor.py
index ad26d07..97ff5fe 100644
--- a/examples/buck_inductor.py
+++ b/examples/buck_inductor.py
@@ -2,228 +2,180 @@
PyOpenMagnetics Examples - Buck Inductor Design
This example demonstrates designing a buck converter output inductor:
-1. Define operating conditions
+1. Define operating conditions using the fluent API
2. Calculate inductance requirements
-3. Select core and wire
-4. Verify losses and saturation
+3. Get 50 Pareto-optimal designs
+4. Generate visual reports
"""
-import PyOpenMagnetics
+import sys
+import os
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from api.design import Design
+from api.models import (
+ VoltageSpec, CurrentSpec, BuckTopology, PowerSupplySpec, PortSpec
+)
+from examples.common import (
+ DEFAULT_MAX_RESULTS, generate_example_report, print_results_summary
+)
+
+# Define specifications using datamodels
+psu_spec = PowerSupplySpec(
+ name="Buck Inductor 33W",
+ inputs=[PortSpec(
+ name="12V Input",
+ voltage=VoltageSpec.dc_range(10, 14),
+ current=CurrentSpec.dc(3)
+ )],
+ outputs=[PortSpec(
+ name="3.3V Output",
+ voltage=VoltageSpec.dc(3.3, tolerance_pct=5),
+ current=CurrentSpec.dc(10)
+ )],
+ efficiency=0.95,
+ isolation_v=None # Non-isolated
+)
+
+topology = BuckTopology(fsw_hz=500e3)
def design_buck_inductor():
"""
Design a buck converter inductor for 12V to 3.3V @ 10A.
-
+
Specifications:
- Input: 12V (10-14V range)
- Output: 3.3V @ 10A
- Switching frequency: 500 kHz
- Current ripple: 30% of Iout (3A p-p)
- - Required inductance: ~4.7 µH
+ - Required inductance: ~4.7 uH
"""
-
print("=" * 60)
print("BUCK INDUCTOR DESIGN")
- print("12V → 3.3V @ 10A, 500 kHz")
+ print("12V -> 3.3V @ 10A, 500 kHz")
print("=" * 60)
-
- # Calculate duty cycle and inductance
- Vin = 12
- Vout = 3.3
- Iout = 10
- fsw = 500000
+
+ # Calculate expected values
+ Vin = psu_spec.inputs[0].voltage.nominal
+ Vout = psu_spec.outputs[0].voltage.nominal
+ Iout = psu_spec.outputs[0].current.nominal
+ fsw = topology.fsw_hz
ripple_ratio = 0.3
-
- D = Vout / Vin # Duty cycle ≈ 0.275
+
+ D = Vout / Vin # Duty cycle ~ 0.275
delta_I = Iout * ripple_ratio # 3A ripple
- L = (Vin - Vout) * D / (fsw * delta_I) # ~4.7 µH
-
+ L = (Vin - Vout) * D / (fsw * delta_I) # ~4.7 uH
+
print(f"\nCalculated parameters:")
print(f" Duty cycle: {D*100:.1f}%")
print(f" Current ripple: {delta_I:.1f} A p-p")
- print(f" Required inductance: {L*1e6:.2f} µH")
-
+ print(f" Required inductance: {L*1e6:.2f} uH")
+
# Peak and RMS currents
I_peak = Iout + delta_I/2 # 11.5A
I_valley = Iout - delta_I/2 # 8.5A
- I_rms = Iout # Approximately equal to DC for low ripple
-
+
print(f" Peak current: {I_peak:.1f} A")
print(f" Valley current: {I_valley:.1f} A")
-
- # Define inputs for PyOpenMagnetics
- inputs = {
- "designRequirements": {
- "magnetizingInductance": {
- "nominal": L,
- "minimum": L * 0.9,
- "maximum": L * 1.1
- }
- },
- "operatingPoints": [{
- "name": "Full Load",
- "conditions": {"ambientTemperature": 50},
- "excitationsPerWinding": [{
- "name": "Main",
- "frequency": fsw,
- # Triangular ripple current on DC bias
- "current": {
- "waveform": {
- "data": [I_valley, I_peak, I_valley],
- "time": [0, D/fsw, 1/fsw]
- }
- },
- # Rectangular voltage
- "voltage": {
- "waveform": {
- "data": [Vin - Vout, Vin - Vout, -Vout, -Vout],
- "time": [0, D/fsw, D/fsw, 1/fsw]
- }
- }
- }]
- }]
- }
-
- # Process inputs
- print("\n[1] Processing inputs...")
- processed = PyOpenMagnetics.process_inputs(inputs)
-
- # Get inductor designs
- print("\n[2] Finding suitable cores...")
-
- # For inductors, we want powder cores (low loss with DC bias)
- materials = PyOpenMagnetics.get_core_material_names()
- powder_materials = [m for m in materials if any(x in m for x in ["MPP", "High Flux", "Kool", "XFlux", "-26", "-52"])]
- print(f" Found {len(powder_materials)} powder core materials")
-
- # Get recommendations
- weights = {
- "EFFICIENCY": 1.0,
- "DIMENSIONS": 0.8,
- "COST": 0.3
- }
-
- magnetics = PyOpenMagnetics.calculate_advised_magnetics(
- processed,
- max_results=5,
- core_mode="STANDARD_CORES"
+
+ # Design using the fluent API
+ design = (
+ Design.buck()
+ .vin(psu_spec.inputs[0].voltage.min, psu_spec.inputs[0].voltage.max)
+ .vout(Vout)
+ .iout(Iout)
+ .fsw(fsw)
+ .ripple_ratio(ripple_ratio) # 30% current ripple
+ .prefer("efficiency") # Optimize for lowest losses
+ .ambient_temperature(50) # 50C ambient
)
-
- print(f" Found {len(magnetics)} suitable designs")
-
- # Analyze designs
- print("\n[3] Analyzing designs:")
- print("-" * 60)
-
- models = {
- "coreLosses": "IGSE",
- "reluctance": "ZHANG"
+
+ # Get design recommendations
+ print(f"\nFinding optimal designs (max {DEFAULT_MAX_RESULTS})...")
+ results = design.solve(max_results=DEFAULT_MAX_RESULTS, verbose=True)
+
+ if not results:
+ print("No suitable designs found.")
+ return None
+
+ # Display results summary
+ print_results_summary(results)
+
+ # Generate visual reports
+ specs = {
+ "power_w": psu_spec.total_output_power,
+ "frequency_hz": topology.fsw_hz,
+ "topology": topology.name,
+ "inductance_uH": L * 1e6,
}
-
- for i, mas in enumerate(magnetics[:3]):
- if "magnetic" not in mas:
- continue
-
- magnetic = mas["magnetic"]
- core = magnetic["core"]
- coil = magnetic["coil"]
-
- shape = core["functionalDescription"]["shape"]["name"]
- material = core["functionalDescription"]["material"]["name"]
-
- # Check for gapping
- gapping = core["functionalDescription"].get("gapping", [])
- total_gap = sum(g.get("length", 0) for g in gapping) * 1000 # mm
-
- # Calculate inductance to verify
- actual_L = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
- core, coil, processed["operatingPoints"][0], models
- )
-
- # Calculate losses
- losses = PyOpenMagnetics.calculate_core_losses(core, coil, processed, models)
- winding_losses = PyOpenMagnetics.calculate_winding_losses(
- magnetic, processed["operatingPoints"][0], 85
- )
-
- print(f"\nDesign #{i+1}: {shape} / {material}")
- print(f" Gap: {total_gap:.2f} mm total")
- print(f" Inductance: {actual_L*1e6:.2f} µH (target: {L*1e6:.2f} µH)")
- print(f" Core losses: {losses.get('coreLosses', 0):.3f} W")
- print(f" Winding losses: {winding_losses.get('windingLosses', 0):.3f} W")
- print(f" B_peak: {losses.get('magneticFluxDensityPeak', 0)*1000:.0f} mT")
-
- # Get turns
- if "functionalDescription" in coil:
- turns = coil["functionalDescription"][0]["numberTurns"]
- print(f" Turns: {turns}")
-
- # Saturation check
- print("\n[4] Saturation margin:")
- if magnetics:
- best = magnetics[0]["magnetic"]
- I_sat = PyOpenMagnetics.calculate_saturation_current(best, 85)
- margin = (I_sat - I_peak) / I_peak * 100
- print(f" Saturation current: {I_sat:.1f} A")
- print(f" Margin above I_peak: {margin:.0f}%")
-
- print("\n" + "=" * 60)
- return magnetics[0] if magnetics else None
+ generate_example_report(
+ results,
+ "buck_inductor",
+ "Buck Inductor 33W - Design Report",
+ specs=specs
+ )
+
+ return results
def compare_wire_options():
"""Compare different wire options for high-current applications."""
-
+ import PyOpenMagnetics
+
print("\n" + "=" * 60)
print("WIRE COMPARISON FOR 10A APPLICATION")
print("=" * 60)
-
+
# For 10A, we need substantial copper area
- # Target: ~4 A/mm² current density → ~2.5 mm² area → ~1.8mm diameter
-
+ # Target: ~4 A/mm2 current density -> ~2.5 mm2 area -> ~1.8mm diameter
+
options = [
("Round 1.6mm", 0.0016, "round"),
("Round 1.8mm", 0.0018, "round"),
("Rectangular", 0.002, "rectangular"),
]
-
- print(f"\nComparing wires for 10A RMS at 100°C:")
+
+ print(f"\nComparing wires for 10A RMS at 100C:")
print("-" * 50)
-
+
current = {
"processed": {
"rms": 10,
"peakToPeak": 3
}
}
-
+
for name, dim, wire_type in options:
try:
wire = PyOpenMagnetics.find_wire_by_dimension(dim, wire_type, "IEC 60317")
-
+
R_dc = PyOpenMagnetics.calculate_dc_resistance_per_meter(wire, 100)
P_dc = PyOpenMagnetics.calculate_dc_losses_per_meter(wire, current, 100)
-
+
print(f"\n{name}:")
- print(f" R_dc: {R_dc*1000:.2f} mΩ/m")
+ print(f" R_dc: {R_dc*1000:.2f} mOhm/m")
print(f" P_dc (10A): {P_dc:.2f} W/m")
-
+
# For rectangular, show dimensions
if wire_type == "rectangular" and "conductingWidth" in wire:
w = wire["conductingWidth"]["nominal"] * 1000
h = wire["conductingHeight"]["nominal"] * 1000
- print(f" Dimensions: {w:.2f} × {h:.2f} mm")
-
- except Exception as e:
+ print(f" Dimensions: {w:.2f} x {h:.2f} mm")
+
+ except Exception:
print(f"\n{name}: Not available")
if __name__ == "__main__":
# Design the buck inductor
- best_design = design_buck_inductor()
-
+ results = design_buck_inductor()
+
+ if results:
+ best = results[0]
+ print(f"\nRecommended: {best.core} with {best.material}")
+
# Compare wire options
compare_wire_options()
-
- print("\n✓ Buck inductor design complete!")
+
+ print("\n[OK] Buck inductor design complete!")
diff --git a/examples/common.py b/examples/common.py
new file mode 100644
index 0000000..e9f9c12
--- /dev/null
+++ b/examples/common.py
@@ -0,0 +1,132 @@
+"""
+Common utilities for PyOpenMagnetics examples.
+
+Provides standardized output generation for all examples.
+"""
+
+import os
+from pathlib import Path
+from typing import List, Any, Optional, Dict
+
+# Standard configuration for all examples
+DEFAULT_MAX_RESULTS = 50
+OUTPUT_BASE_DIR = Path(__file__).parent / "_output"
+
+
+def get_output_dir(example_name: str) -> str:
+ """Get standardized output directory for an example.
+
+ Args:
+ example_name: Short name like "usb_pd_65w" or "buck_inductor"
+
+ Returns:
+ Path string to output directory
+ """
+ output_dir = OUTPUT_BASE_DIR / example_name
+ output_dir.mkdir(parents=True, exist_ok=True)
+ return str(output_dir)
+
+
+def generate_example_report(
+ results: List[Any],
+ example_name: str,
+ title: str,
+ specs: Optional[Dict[str, Any]] = None,
+ verbose: bool = True
+) -> Optional[str]:
+ """
+ Generate standardized reports for an example.
+
+ Generates:
+ - design_report.png: Main dashboard with Pareto front
+ - pareto_detailed.png: Detailed Pareto analysis
+ - parallel_coordinates.png: Multi-objective comparison
+ - heatmap.png: Design characteristics
+ - report_summary.json: Statistics
+
+ Args:
+ results: List of DesignResult objects
+ example_name: Short name for output directory
+ title: Report title
+ specs: Optional specifications dict
+ verbose: Print progress
+
+ Returns:
+ Path to output directory, or None if report generation failed
+ """
+ if not results:
+ if verbose:
+ print("[No results to generate report]")
+ return None
+
+ output_dir = get_output_dir(example_name)
+
+ try:
+ from api.report import generate_design_report
+
+ if verbose:
+ print(f"\nGenerating reports for {len(results)} designs...")
+
+ generate_design_report(
+ results,
+ output_dir,
+ title=title,
+ specs=specs,
+ verbose=verbose
+ )
+
+ if verbose:
+ print(f"\nReports saved to: {output_dir}/")
+ print(" - design_report.png (main dashboard with Pareto front)")
+ print(" - pareto_detailed.png (loss vs volume analysis)")
+ print(" - parallel_coordinates.png (multi-objective comparison)")
+ print(" - heatmap.png (design characteristics)")
+ print(" - report_summary.json (statistics)")
+
+ return output_dir
+
+ except ImportError as e:
+ if verbose:
+ print(f"\n[Visual reports skipped - matplotlib not installed: {e}]")
+ return None
+ except Exception as e:
+ if verbose:
+ print(f"\n[Report generation failed: {e}]")
+ return None
+
+
+def print_results_summary(results: List[Any], max_display: int = 10):
+ """Print a summary of design results.
+
+ Args:
+ results: List of DesignResult objects
+ max_display: Maximum number of results to display in detail
+ """
+ if not results:
+ print("No designs found.")
+ return
+
+ print(f"\nFound {len(results)} designs (showing top {min(len(results), max_display)}):\n")
+
+ for i, r in enumerate(results[:max_display], 1):
+ print(f"Design #{i}: {r.core} / {r.material}")
+ print(f" Primary: {r.primary_turns}T, {r.primary_wire}")
+ if hasattr(r, 'secondary_turns') and r.secondary_turns:
+ print(f" Secondary: {r.secondary_turns}T")
+ print(f" Air gap: {r.air_gap_mm:.2f} mm")
+ print(f" Core loss: {r.core_loss_w:.3f} W")
+ print(f" Cu loss: {r.copper_loss_w:.3f} W")
+ print(f" Total loss: {r.total_loss_w:.3f} W")
+ if r.temp_rise_c:
+ print(f" Temp rise: {r.temp_rise_c:.1f} K")
+ print()
+
+ if len(results) > max_display:
+ print(f" ... and {len(results) - max_display} more designs")
+
+ # Statistics
+ total_losses = [r.total_loss_w for r in results]
+ print(f"\nStatistics ({len(results)} designs):")
+ print(f" Min loss: {min(total_losses):.3f} W")
+ print(f" Max loss: {max(total_losses):.3f} W")
+ print(f" Avg loss: {sum(total_losses)/len(total_losses):.3f} W")
diff --git a/examples/flyback_design.py b/examples/flyback_design.py
index 821625b..bb6d873 100644
--- a/examples/flyback_design.py
+++ b/examples/flyback_design.py
@@ -2,227 +2,161 @@
PyOpenMagnetics Examples - Flyback Transformer Design
This example demonstrates a complete flyback transformer design workflow:
-1. Define converter specifications
-2. Get design recommendations
+1. Define converter specifications using the fluent API
+2. Get design recommendations (50 Pareto-optimal solutions)
3. Analyze losses and performance
-4. Visualize the result
+4. Generate visual reports
For more examples, see llms.txt in the PyOpenMagnetics directory.
"""
-import PyOpenMagnetics
+import sys
+import os
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from api.design import Design
+from api.models import (
+ VoltageSpec, CurrentSpec, FlybackTopology, PowerSupplySpec, PortSpec
+)
+from examples.common import (
+ DEFAULT_MAX_RESULTS, generate_example_report, print_results_summary
+)
+
+# Define specifications using datamodels
+psu_spec = PowerSupplySpec(
+ name="Flyback Transformer 72W",
+ inputs=[PortSpec(
+ name="AC Input",
+ voltage=VoltageSpec.ac(230, v_min_rms=85, v_max_rms=265),
+ current=CurrentSpec.dc(0.6)
+ )],
+ outputs=[PortSpec(
+ name="DC Output",
+ voltage=VoltageSpec.dc(24, tolerance_pct=5),
+ current=CurrentSpec.dc(3)
+ )],
+ efficiency=0.85,
+ isolation_v=3000
+)
+
+topology = FlybackTopology(fsw_hz=100e3, max_duty=0.45)
def design_flyback_transformer():
"""
Design a flyback transformer for a 24V/3A output from universal AC input.
-
+
Specifications:
- Input: 85-265 VAC (rectified to ~120-375 VDC)
- Output: 24V @ 3A (72W)
- Switching frequency: 100 kHz
- Efficiency target: 85%
"""
-
- # Step 1: Define the converter operating conditions
print("=" * 60)
print("FLYBACK TRANSFORMER DESIGN")
- print("Output: 24V @ 3A (72W)")
+ print("Input: 85-265 VAC | Output: 24V @ 3A (72W)")
print("=" * 60)
-
- # Define design requirements
- inputs = {
- "designRequirements": {
- "magnetizingInductance": {
- "nominal": 200e-6, # 200 µH magnetizing inductance
- "minimum": 180e-6,
- "maximum": 220e-6
- },
- "turnsRatios": [
- {"nominal": 6.0} # Npri/Nsec = 6:1
- ],
- "insulation": {
- "insulationType": "Functional",
- "pollutionDegree": "P2",
- "overvoltageCategory": "OVC-II"
- }
- },
- "operatingPoints": [
- {
- "name": "Low Line (85 VAC)",
- "conditions": {"ambientTemperature": 40},
- "excitationsPerWinding": [{
- "name": "Primary",
- "frequency": 100000,
- # Typical flyback triangular current waveform
- "current": {
- "waveform": {
- "data": [0.2, 1.8, 1.8, 0.2],
- "time": [0, 4.5e-6, 4.5e-6, 10e-6]
- }
- },
- # Square wave voltage during on-time
- "voltage": {
- "waveform": {
- "data": [120, 120, 0, 0],
- "time": [0, 4.5e-6, 4.5e-6, 10e-6]
- }
- }
- }]
- },
- {
- "name": "High Line (265 VAC)",
- "conditions": {"ambientTemperature": 40},
- "excitationsPerWinding": [{
- "name": "Primary",
- "frequency": 100000,
- "current": {
- "waveform": {
- "data": [0.1, 0.6, 0.6, 0.1],
- "time": [0, 2e-6, 2e-6, 10e-6]
- }
- },
- "voltage": {
- "waveform": {
- "data": [375, 375, 0, 0],
- "time": [0, 2e-6, 2e-6, 10e-6]
- }
- }
- }]
- }
- ]
- }
-
- # Step 2: Process inputs (adds harmonics for accurate loss calculation)
- print("\n[1] Processing inputs...")
- processed_inputs = PyOpenMagnetics.process_inputs(inputs)
- print(" ✓ Harmonics calculated")
-
- # Step 3: Get design recommendations
- print("\n[2] Getting design recommendations...")
-
- weights = {
- "EFFICIENCY": 1.0, # Prioritize efficiency
- "DIMENSIONS": 0.5, # Secondary: small size
- "COST": 0.3 # Tertiary: low cost
- }
-
- magnetics = PyOpenMagnetics.calculate_advised_magnetics(
- processed_inputs,
- max_results=3,
- core_mode="STANDARD_CORES"
+
+ # Step 1: Define the converter using fluent API
+ design = (
+ Design.flyback()
+ .vin_ac(psu_spec.inputs[0].voltage.min, psu_spec.inputs[0].voltage.max)
+ .output(psu_spec.outputs[0].voltage.nominal, psu_spec.outputs[0].current.nominal)
+ .fsw(topology.fsw_hz)
+ .efficiency(psu_spec.efficiency)
+ .prefer("efficiency") # Optimize for efficiency
)
-
- print(f" ✓ Found {len(magnetics)} suitable designs")
-
- # Step 4: Analyze top recommendations
- print("\n[3] Analyzing top designs:")
- print("-" * 60)
-
- models = {
- "coreLosses": "IGSE",
- "reluctance": "ZHANG",
- "coreTemperature": "MANIKTALA"
+
+ # Step 2: Get calculated parameters
+ params = design.get_calculated_parameters()
+ print("\nCalculated Parameters:")
+ print(f" Turns ratio (n): {params['turns_ratio']:.2f}")
+ print(f" Mag inductance (Lm): {params['magnetizing_inductance_uH']:.1f} uH")
+ print(f" Duty cycle (D): {params['duty_cycle_low_line']:.2%}")
+
+ # Step 3: Get design recommendations (50 Pareto-optimal solutions)
+ print(f"\nFinding optimal designs (max {DEFAULT_MAX_RESULTS})...")
+ results = design.solve(max_results=DEFAULT_MAX_RESULTS, verbose=True)
+
+ if not results:
+ print("No suitable designs found.")
+ return None
+
+ # Step 4: Display results summary
+ print_results_summary(results)
+
+ # Step 5: Generate visual reports (Pareto front, etc.)
+ specs = {
+ "power_w": psu_spec.total_output_power,
+ "frequency_hz": topology.fsw_hz,
+ "efficiency": psu_spec.efficiency,
+ "topology": topology.name,
}
-
- for i, mas in enumerate(magnetics):
- if "magnetic" not in mas:
- continue
-
- magnetic = mas["magnetic"]
- core = magnetic["core"]
- coil = magnetic["coil"]
-
- # Get core info
- shape_name = core["functionalDescription"]["shape"]["name"]
- material_name = core["functionalDescription"]["material"]["name"]
-
- # Calculate losses for worst-case (low line)
- losses = PyOpenMagnetics.calculate_core_losses(
- core, coil, processed_inputs, models
- )
-
- # Calculate winding losses
- winding_losses = PyOpenMagnetics.calculate_winding_losses(
- magnetic,
- processed_inputs["operatingPoints"][0], # Low line
- temperature=80 # Estimated operating temperature
- )
-
- core_loss = losses.get("coreLosses", 0)
- winding_loss = winding_losses.get("windingLosses", 0)
- total_loss = core_loss + winding_loss
-
- print(f"\nDesign #{i+1}: {shape_name} / {material_name}")
- print(f" Core losses: {core_loss:.3f} W")
- print(f" Winding losses: {winding_loss:.3f} W")
- print(f" Total losses: {total_loss:.3f} W")
- print(f" B_peak: {losses.get('magneticFluxDensityPeak', 0)*1000:.1f} mT")
-
- # Get winding info
- if "functionalDescription" in coil:
- for winding in coil["functionalDescription"]:
- print(f" {winding['name']}: {winding['numberTurns']} turns")
-
- print("\n" + "=" * 60)
- print("Design complete! Best design is #1")
-
- return magnetics[0] if magnetics else None
+ generate_example_report(
+ results,
+ "flyback_design",
+ "Flyback Transformer 72W - Design Report",
+ specs=specs
+ )
+
+ return results
def explore_core_database():
"""Demonstrate database access functions."""
-
+ import PyOpenMagnetics
+
print("\n" + "=" * 60)
print("CORE DATABASE EXPLORATION")
print("=" * 60)
-
+
# Get available shape families
families = PyOpenMagnetics.get_core_shape_families()
print(f"\nShape families: {', '.join(families[:10])}...")
-
+
# Get materials by manufacturer
ferroxcube = PyOpenMagnetics.get_core_material_names_by_manufacturer("Ferroxcube")
print(f"Ferroxcube materials: {', '.join(ferroxcube[:5])}...")
-
+
tdk = PyOpenMagnetics.get_core_material_names_by_manufacturer("TDK")
print(f"TDK materials: {', '.join(tdk[:5])}...")
-
+
# Get material properties
- print("\n3C95 Properties at 25°C:")
+ print("\n3C95 Properties at 25C:")
mu = PyOpenMagnetics.get_material_permeability("3C95", 25, 0, 100000)
print(f" Permeability (100 kHz): {mu:.0f}")
-
+
rho = PyOpenMagnetics.get_material_resistivity("3C95", 25)
- print(f" Resistivity: {rho:.2f} Ω·m")
-
+ print(f" Resistivity: {rho:.2f} Ohm*m")
+
material = PyOpenMagnetics.find_core_material_by_name("3C95")
steinmetz = PyOpenMagnetics.get_core_material_steinmetz_coefficients(material, 100000)
- print(f" Steinmetz k={steinmetz['k']:.2e}, α={steinmetz['alpha']:.2f}, β={steinmetz['beta']:.2f}")
+ print(f" Steinmetz k={steinmetz['k']:.2e}, a={steinmetz['alpha']:.2f}, b={steinmetz['beta']:.2f}")
def wire_selection_example():
"""Demonstrate wire selection and loss calculation."""
-
+ import PyOpenMagnetics
+
print("\n" + "=" * 60)
print("WIRE SELECTION")
print("=" * 60)
-
+
# Find available wire types
wire_types = PyOpenMagnetics.get_available_wire_types()
print(f"\nAvailable wire types: {wire_types}")
-
+
# Find wire by dimension
round_wire = PyOpenMagnetics.find_wire_by_dimension(0.0005, "round", "IEC 60317")
print(f"\n0.5mm round wire: {round_wire.get('name', 'N/A')}")
-
+
# Calculate DC resistance
R_dc = PyOpenMagnetics.calculate_dc_resistance_per_meter(round_wire, 25)
- print(f" DC resistance: {R_dc*1000:.2f} mΩ/m at 25°C")
-
+ print(f" DC resistance: {R_dc*1000:.2f} mOhm/m at 25C")
+
R_dc_hot = PyOpenMagnetics.calculate_dc_resistance_per_meter(round_wire, 100)
- print(f" DC resistance: {R_dc_hot*1000:.2f} mΩ/m at 100°C")
-
+ print(f" DC resistance: {R_dc_hot*1000:.2f} mOhm/m at 100C")
+
# Litz wire for high frequency
print("\nFor high-frequency applications, consider litz wire:")
litz_wires = [w for w in PyOpenMagnetics.get_wire_names() if "litz" in w.lower()]
@@ -231,12 +165,16 @@ def wire_selection_example():
if __name__ == "__main__":
# Run the flyback design example
- best_design = design_flyback_transformer()
-
+ results = design_flyback_transformer()
+
+ if results:
+ best = results[0]
+ print(f"\nRecommended: {best.core} with {best.material}")
+
# Explore the database
explore_core_database()
-
+
# Wire selection
wire_selection_example()
-
- print("\n✓ All examples completed successfully!")
+
+ print("\n[OK] All examples completed successfully!")
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..5bc78f2
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,42 @@
+site_name: PyOpenMagnetics
+site_description: Python library for magnetic component design
+repo_url: https://github.com/OpenMagnetics/PyMKF
+
+theme:
+ name: material
+ features:
+ - navigation.tabs
+ - navigation.sections
+ - search.suggest
+ - search.highlight
+ - content.code.copy
+ palette:
+ - scheme: default
+ primary: indigo
+ accent: indigo
+
+nav:
+ - Home: index.md
+ - Getting Started:
+ - Installation: installation.md
+ - Introduction: intro.md
+ - Examples: examples.md
+ - Contributing: contributing.md
+
+plugins:
+ - search
+
+markdown_extensions:
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.superfences
+ - admonition
+ - pymdownx.details
+ - tables
+ - attr_list
+ - md_in_html
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/OpenMagnetics/PyMKF
diff --git a/notebooks/01_getting_started.ipynb b/notebooks/01_getting_started.ipynb
index cdf6d79..f80e20e 100644
--- a/notebooks/01_getting_started.ipynb
+++ b/notebooks/01_getting_started.ipynb
@@ -257,20 +257,7 @@
"cell_type": "markdown",
"id": "f53c6981",
"metadata": {},
- "source": [
- "## Next Steps\n",
- "\n",
- "- **02_buck_inductor.ipynb**: Design a buck converter inductor\n",
- "- **03_flyback_transformer.ipynb**: Design a flyback transformer\n",
- "- **04_core_adviser.ipynb**: Use the core adviser to find optimal cores\n",
- "- **05_winding_losses.ipynb**: Calculate winding losses with proximity effects\n",
- "\n",
- "## Resources\n",
- "\n",
- "- [PyOpenMagnetics Documentation](https://github.com/OpenMagnetics/PyMKF)\n",
- "- [MAS Schema](https://github.com/OpenMagnetics/MAS) - Magnetic Agnostic Structure\n",
- "- [MKF Engine](https://github.com/OpenMagnetics/MKF) - Magnetics Knowledge Foundation"
- ]
+ "source": "## Next Steps\n\n- **02_buck_inductor.ipynb**: Design a buck converter inductor\n- **03_core_losses.ipynb**: Explore core loss calculations and material comparison\n\n## Resources\n\n- [PyOpenMagnetics Documentation](https://github.com/OpenMagnetics/PyMKF)\n- [MAS Schema](https://github.com/OpenMagnetics/MAS) - Magnetic Agnostic Structure\n- [MKF Engine](https://github.com/OpenMagnetics/MKF) - Magnetics Knowledge Foundation"
}
],
"metadata": {
@@ -280,4 +267,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
+}
\ No newline at end of file
diff --git a/notebooks/03_core_losses.ipynb b/notebooks/03_core_losses.ipynb
index d0c0646..77755f2 100644
--- a/notebooks/03_core_losses.ipynb
+++ b/notebooks/03_core_losses.ipynb
@@ -51,7 +51,7 @@
"$$ P_v = k \\cdot f^\\alpha \\cdot B^\\beta $$\n",
"\n",
"Where:\n",
- "- $P_v$ is volumetric power loss (W/m³)\n",
+ "- $P_v$ is volumetric power loss (W/m\u00b3)\n",
"- $f$ is frequency (Hz)\n",
"- $B$ is peak flux density (T)\n",
"- $k$, $\\alpha$, $\\beta$ are material-specific constants"
@@ -59,22 +59,10 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": null,
"id": "b0700b69",
"metadata": {},
- "outputs": [
- {
- "ename": "NameError",
- "evalue": "name 'PyOpenMagnetics' is not defined",
- "output_type": "error",
- "traceback": [
- "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
- "\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
- "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 14\u001b[39m\n\u001b[32m 2\u001b[39m core_data = {\n\u001b[32m 3\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mfunctionalDescription\u001b[39m\u001b[33m\"\u001b[39m: {\n\u001b[32m 4\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mname\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mTest Core\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 10\u001b[39m }\n\u001b[32m 11\u001b[39m }\n\u001b[32m 13\u001b[39m \u001b[38;5;66;03m# API: calculate_core_data(core_data, include_material_data)\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m14\u001b[39m core = \u001b[43mPyOpenMagnetics\u001b[49m.calculate_core_data(core_data, \u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[32m 15\u001b[39m volume = core[\u001b[33m'\u001b[39m\u001b[33mprocessedDescription\u001b[39m\u001b[33m'\u001b[39m][\u001b[33m'\u001b[39m\u001b[33meffectiveParameters\u001b[39m\u001b[33m'\u001b[39m][\u001b[33m'\u001b[39m\u001b[33meffectiveVolume\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m 16\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCore shape: E 42/21/15\u001b[39m\u001b[33m\"\u001b[39m)\n",
- "\u001b[31mNameError\u001b[39m: name 'PyOpenMagnetics' is not defined"
- ]
- }
- ],
+ "outputs": [],
"source": [
"# Create a test core\n",
"core_data = {\n",
@@ -93,7 +81,7 @@
"volume = core['processedDescription']['effectiveParameters']['effectiveVolume']\n",
"print(f\"Core shape: E 42/21/15\")\n",
"print(f\"Material: 3C95\")\n",
- "print(f\"Effective volume: {volume * 1e6:.2f} cm³\")"
+ "print(f\"Effective volume: {volume * 1e6:.2f} cm\u00b3\")"
]
},
{
@@ -108,26 +96,10 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"id": "fcbde5e4",
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Steinmetz coefficients for 3C95:\n",
- "\n",
- " Frequency k alpha beta\n",
- "--------------------------------------------\n",
- " 25 kHz Error: name 'PyOpenMagnetics' is not defined\n",
- " 50 kHz Error: name 'PyOpenMagnetics' is not defined\n",
- " 100 kHz Error: name 'PyOpenMagnetics' is not defined\n",
- " 200 kHz Error: name 'PyOpenMagnetics' is not defined\n",
- " 500 kHz Error: name 'PyOpenMagnetics' is not defined\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"# Get Steinmetz coefficients at different frequencies\n",
"frequencies = [25000, 50000, 100000, 200000, 500000]\n",
@@ -168,7 +140,7 @@
"\n",
"if steinmetz_data:\n",
" print(f\"\\nEstimated core losses at B_peak = {B_peak*1000:.0f} mT:\\n\")\n",
- " print(f\"{'Frequency':>12} {'P_v (kW/m³)':>15} {'P_core (W)':>12}\")\n",
+ " print(f\"{'Frequency':>12} {'P_v (kW/m\u00b3)':>15} {'P_core (W)':>12}\")\n",
" print(\"-\" * 42)\n",
" \n",
" for freq, k, alpha, beta in steinmetz_data:\n",
@@ -215,7 +187,7 @@
" if k and alpha and beta:\n",
" P_v = k * (frequency ** alpha) * (B_peak ** beta)\n",
" comparison_data.append((mat, P_v, k, alpha, beta))\n",
- " print(f\"{mat}: P_v = {P_v/1000:.2f} kW/m³\")\n",
+ " print(f\"{mat}: P_v = {P_v/1000:.2f} kW/m\u00b3\")\n",
" else:\n",
" print(f\"{mat}: Coefficients not available\")\n",
" except Exception as e:\n",
@@ -232,12 +204,12 @@
"# Visualize material comparison\n",
"if HAS_MATPLOTLIB and comparison_data:\n",
" materials = [d[0] for d in comparison_data]\n",
- " p_values = [d[1]/1000 for d in comparison_data] # kW/m³\n",
+ " p_values = [d[1]/1000 for d in comparison_data] # kW/m\u00b3\n",
" \n",
" plt.figure(figsize=(10, 6))\n",
" bars = plt.bar(materials, p_values, color=['#3498db', '#2ecc71', '#9b59b6', '#e74c3c', '#f1c40f'][:len(materials)])\n",
" plt.xlabel('Material', fontsize=12)\n",
- " plt.ylabel('Volumetric Power Loss (kW/m³)', fontsize=12)\n",
+ " plt.ylabel('Volumetric Power Loss (kW/m\u00b3)', fontsize=12)\n",
" plt.title(f'Core Loss Comparison at {frequency/1000:.0f} kHz, {B_peak*1000:.0f} mT', fontsize=14)\n",
" plt.grid(True, axis='y', alpha=0.3)\n",
" \n",
@@ -285,7 +257,7 @@
" permeability_data[mat].append(None)\n",
"\n",
"# Print table\n",
- "print(f\"Relative Permeability vs Frequency (at {temperature}°C, no DC bias):\\n\")\n",
+ "print(f\"Relative Permeability vs Frequency (at {temperature}\u00b0C, no DC bias):\\n\")\n",
"print(f\"{'Frequency':>12} {'3C95':>10} {'N87':>10}\")\n",
"print(\"-\" * 34)\n",
"for i, freq in enumerate(frequencies):\n",
@@ -353,7 +325,7 @@
"print(f\" Name: {material.get('name', 'N/A')}\")\n",
"print(f\" Manufacturer: {material.get('manufacturer', 'N/A')}\")\n",
"if 'curiTemperature' in material:\n",
- " print(f\" Curie temperature: {material['curiTemperature']}°C\")"
+ " print(f\" Curie temperature: {material['curiTemperature']}\u00b0C\")"
]
},
{
@@ -370,9 +342,9 @@
"print(f\"\"\"\n",
"Key Takeaways:\n",
"\n",
- "1. Steinmetz Equation: P = k × f^α × B^β\n",
- " - k, α, β vary with frequency range\n",
- " - Typical α ≈ 1.2-1.5, β ≈ 2.0-2.5 for ferrites\n",
+ "1. Steinmetz Equation: P = k \u00d7 f^\u03b1 \u00d7 B^\u03b2\n",
+ " - k, \u03b1, \u03b2 vary with frequency range\n",
+ " - Typical \u03b1 \u2248 1.2-1.5, \u03b2 \u2248 2.0-2.5 for ferrites\n",
"\n",
"2. Material Selection:\n",
" - Lower loss at target frequency = better efficiency\n",
@@ -384,7 +356,7 @@
" - Higher frequency = smaller core, but more losses\n",
"\n",
"4. Temperature Effects:\n",
- " - Most ferrites have a loss minimum around 80-100°C\n",
+ " - Most ferrites have a loss minimum around 80-100\u00b0C\n",
" - Permeability drops at high temperatures\n",
"\n",
"For complete loss calculations including winding effects,\n",
@@ -425,21 +397,7 @@
"cell_type": "markdown",
"id": "71cdbbc8",
"metadata": {},
- "source": [
- "## Summary\n",
- "\n",
- "Key takeaways:\n",
- "\n",
- "1. **Core losses increase non-linearly** with both flux density and frequency\n",
- "2. **Temperature affects losses** - many materials have a loss minimum around 80-100°C\n",
- "3. **Material selection is critical** - different materials are optimized for different frequencies\n",
- "4. **Waveform matters** - non-sinusoidal waveforms generally have higher losses\n",
- "\n",
- "## Next Steps\n",
- "\n",
- "- Try the **04_core_adviser.ipynb** notebook to find optimal cores automatically\n",
- "- Explore **05_winding_losses.ipynb** for complete loss analysis"
- ]
+ "source": "## Summary\n\nKey takeaways:\n\n1. **Core losses increase non-linearly** with both flux density and frequency\n2. **Temperature affects losses** - many materials have a loss minimum around 80-100\u00b0C\n3. **Material selection is critical** - different materials are optimized for different frequencies\n4. **Waveform matters** - non-sinusoidal waveforms generally have higher losses\n\n## Resources\n\n- [PyOpenMagnetics Documentation](https://github.com/OpenMagnetics/PyMKF)\n- [MAS Schema](https://github.com/OpenMagnetics/MAS) - Magnetic Agnostic Structure"
}
],
"metadata": {
@@ -463,4 +421,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
+}
\ No newline at end of file
diff --git a/scripts/block_push.sh b/scripts/block_push.sh
new file mode 100644
index 0000000..614e897
--- /dev/null
+++ b/scripts/block_push.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+# PyOpenMagnetics - Push Blocker
+# This script prevents accidental pushes to origin until FREE/PRO split is complete
+#
+# Installation:
+# cp scripts/block_push.sh .git/hooks/pre-push
+# chmod +x .git/hooks/pre-push
+
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+echo ""
+echo -e "${RED}========================================${NC}"
+echo -e "${RED} PUSH BLOCKED${NC}"
+echo -e "${RED}========================================${NC}"
+echo ""
+echo -e "${YELLOW}PyOpenMagnetics push to origin is currently blocked.${NC}"
+echo ""
+echo "Reason: Features need to be split between FREE and PRO versions"
+echo " before public release."
+echo ""
+echo -e "${BLUE}FREE Features:${NC}"
+echo " - Basic topologies (buck, boost, flyback)"
+echo " - Design adviser"
+echo " - Core/material database"
+echo " - Python API"
+echo " - Basic examples"
+echo ""
+echo -e "${BLUE}PRO Features (to be separated):${NC}"
+echo " - Multi-objective optimization (NSGA-II)"
+echo " - MCP server for AI"
+echo " - Streamlit GUI"
+echo " - FEMMT bridge"
+echo " - Expert knowledge base"
+echo " - Advanced topologies (LLC, DAB)"
+echo " - Full powder core materials (25+)"
+echo ""
+echo "See PRD.md for the complete feature matrix."
+echo ""
+echo -e "${YELLOW}To force push (NOT RECOMMENDED):${NC}"
+echo " git push --no-verify"
+echo ""
+echo -e "${YELLOW}To remove this block after FREE/PRO split:${NC}"
+echo " rm .git/hooks/pre-push"
+echo ""
+
+exit 1
diff --git a/scripts/pre_commit_check.sh b/scripts/pre_commit_check.sh
new file mode 100644
index 0000000..7bd9599
--- /dev/null
+++ b/scripts/pre_commit_check.sh
@@ -0,0 +1,174 @@
+#!/bin/bash
+# PyOpenMagnetics - Pre-Commit Check Script
+# Run this before committing to ensure code quality
+#
+# Usage:
+# ./scripts/pre_commit_check.sh # Run all checks
+# ./scripts/pre_commit_check.sh --quick # Run only fast checks
+# ./scripts/pre_commit_check.sh --full # Run all checks including examples
+
+set -e # Exit on first error
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Parse arguments
+QUICK_MODE=false
+FULL_MODE=false
+for arg in "$@"; do
+ case $arg in
+ --quick)
+ QUICK_MODE=true
+ ;;
+ --full)
+ FULL_MODE=true
+ ;;
+ esac
+done
+
+cd "$PROJECT_ROOT"
+
+# Use virtual environment Python if it exists (required for PyOpenMagnetics C++ bindings)
+if [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
+ PYTHON="$PROJECT_ROOT/.venv/bin/python"
+else
+ PYTHON="python3"
+fi
+
+# Add project root to PYTHONPATH so imports work
+export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH"
+
+# Output directory for results
+OUTPUT_DIR="$PROJECT_ROOT/examples/_output/checks"
+mkdir -p "$OUTPUT_DIR"
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE} PyOpenMagnetics Pre-Commit Checks${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+# Track overall status
+CHECKS_PASSED=0
+CHECKS_FAILED=0
+
+# Helper function
+run_check() {
+ local name="$1"
+ local command="$2"
+ local output_file="$OUTPUT_DIR/$(echo "$name" | tr ' /' '_').log"
+
+ echo -n " $name... "
+
+ if eval "$command" > "$output_file" 2>&1; then
+ echo -e "${GREEN}PASS${NC}"
+ CHECKS_PASSED=$((CHECKS_PASSED + 1))
+ return 0
+ else
+ echo -e "${RED}FAIL${NC}"
+ echo " Output (see $output_file):"
+ head -30 "$output_file" | sed 's/^/ /'
+ CHECKS_FAILED=$((CHECKS_FAILED + 1))
+ return 1
+ fi
+}
+
+# =============================================================================
+# 1. Python Syntax Check
+# =============================================================================
+echo -e "${BLUE}1. Syntax Checks${NC}"
+
+# Check all Python files for syntax errors
+check_python_syntax() {
+ local errors=0
+ while IFS= read -r -d '' file; do
+ if ! $PYTHON -m py_compile "$file" 2>/dev/null; then
+ echo "Syntax error in: $file"
+ errors=$((errors + 1))
+ fi
+ done < <(find "$PROJECT_ROOT/api" "$PROJECT_ROOT/examples" -name "*.py" -print0 2>/dev/null)
+ return $errors
+}
+run_check "Python syntax (api/)" "check_python_syntax"
+echo ""
+
+# =============================================================================
+# 2. Import Checks
+# =============================================================================
+echo -e "${BLUE}2. Import Checks${NC}"
+
+run_check "Import api module" "$PYTHON -c 'import api'"
+run_check "Import api.design" "$PYTHON -c 'from api.design import Design'"
+run_check "Import api.waveforms" "$PYTHON -c 'from api.waveforms import boost_inductor_waveforms'"
+run_check "Import api.optimization" "$PYTHON -c 'from api.optimization import NSGAOptimizer'"
+echo ""
+
+# =============================================================================
+# 3. Unit Tests
+# =============================================================================
+echo -e "${BLUE}3. Unit Tests${NC}"
+
+if [ "$QUICK_MODE" = true ]; then
+ run_check "Quick pytest (fast tests only)" "$PYTHON -m pytest tests/ -v --tb=short -x -q --ignore=tests/test_examples_integration.py 2>&1 | tail -20"
+else
+ run_check "Pytest (all unit tests)" "$PYTHON -m pytest tests/ -v --tb=short -x 2>&1 | tail -30"
+fi
+echo ""
+
+# =============================================================================
+# 4. Example Validation (full mode only)
+# =============================================================================
+if [ "$FULL_MODE" = true ]; then
+ echo -e "${BLUE}4. Example Validation${NC}"
+ run_check "Run all examples" "$SCRIPT_DIR/run_examples.sh"
+ echo ""
+fi
+
+# =============================================================================
+# 5. Type Hints Check (optional - if mypy installed)
+# =============================================================================
+if command -v mypy &> /dev/null && [ "$QUICK_MODE" != true ]; then
+ echo -e "${BLUE}5. Type Checking (mypy)${NC}"
+ run_check "mypy api/" "mypy api/ --ignore-missing-imports --no-error-summary 2>&1 | tail -20"
+ echo ""
+fi
+
+# =============================================================================
+# 6. Documentation Check
+# =============================================================================
+echo -e "${BLUE}6. Documentation${NC}"
+
+run_check "README.md exists" "test -f README.md"
+run_check "PRD.md exists" "test -f PRD.md"
+run_check "CLAUDE.md exists" "test -f CLAUDE.md"
+echo ""
+
+# =============================================================================
+# Summary
+# =============================================================================
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE} Summary${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo -e " ${GREEN}Passed:${NC} $CHECKS_PASSED"
+echo -e " ${RED}Failed:${NC} $CHECKS_FAILED"
+echo ""
+
+if [ $CHECKS_FAILED -gt 0 ]; then
+ echo -e "${RED}Pre-commit checks failed!${NC}"
+ echo -e "${YELLOW}Please fix the issues above before committing.${NC}"
+ exit 1
+else
+ echo -e "${GREEN}All pre-commit checks passed!${NC}"
+ echo ""
+ echo -e "${YELLOW}IMPORTANT: Do NOT push to origin yet!${NC}"
+ echo -e "${YELLOW}Features need to be split between FREE and PRO versions.${NC}"
+ echo ""
+ echo "See PRD.md for the FREE vs PRO feature matrix."
+ exit 0
+fi
diff --git a/src/advisers.h b/src/advisers.h
index c64a773..0688910 100644
--- a/src/advisers.h
+++ b/src/advisers.h
@@ -1,17 +1,117 @@
+/**
+ * @file advisers.h
+ * @brief Design recommendation functions for PyOpenMagnetics
+ *
+ * Provides intelligent design recommendation functions that analyze requirements
+ * and return optimal core and magnetic component selections.
+ *
+ * ## Advisor Workflow
+ * 1. Prepare inputs with design requirements and operating points
+ * 2. Call process_inputs() to add harmonics data (CRITICAL)
+ * 3. Call adviser functions to get ranked recommendations
+ *
+ * ## Core Modes
+ * - AVAILABLE_CORES: Use only cores with stock availability
+ * - STANDARD_CORES: Use all standard cores regardless of stock
+ *
+ * ## Optimization Weights
+ * - COST: Minimize component cost
+ * - EFFICIENCY: Maximize power efficiency
+ * - DIMENSIONS: Minimize physical size
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Prepare inputs
+ * inputs = {"designRequirements": {...}, "operatingPoints": [...]}
+ * processed = pom.process_inputs(inputs) # CRITICAL: must call first!
+ *
+ * # Get core recommendations
+ * weights = {"COST": 1, "EFFICIENCY": 1, "DIMENSIONS": 0.5}
+ * cores = pom.calculate_advised_cores(processed, weights, 10, "AVAILABLE_CORES")
+ *
+ * # Get complete magnetic designs
+ * magnetics = pom.calculate_advised_magnetics(processed, 5, "STANDARD_CORES")
+ * @endcode
+ *
+ * @warning Always call process_inputs() before using adviser functions!
+ *
+ * @see simulation.h for process_inputs() function
+ * @see core.h for core query functions
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Core adviser
+/**
+ * @brief Get recommended cores for given design requirements
+ *
+ * Analyzes the input requirements and returns a ranked list of suitable cores
+ * based on the specified weights for cost, efficiency, and dimensions.
+ *
+ * @param inputsJson JSON object with design requirements and operating points
+ * (should be processed using process_inputs() first)
+ * @param weightsJson JSON object with filter weights:
+ * {"COST": 0-1, "EFFICIENCY": 0-1, "DIMENSIONS": 0-1}
+ * @param maximumNumberResults Maximum number of core recommendations to return
+ * @param coreModeJson Core selection mode: "AVAILABLE_CORES" or "STANDARD_CORES"
+ * @return JSON array of recommended cores sorted by score (best first)
+ */
json calculate_advised_cores(json inputsJson, json weightsJson, int maximumNumberResults, json coreModeJson);
-// Magnetic adviser
+/**
+ * @brief Get recommended complete magnetic designs
+ *
+ * Performs full magnetic design optimization including core selection,
+ * winding configuration, and all parameters. Returns complete Mas
+ * (Magnetic Assembly Specification) objects ready for manufacturing.
+ *
+ * @param inputsJson JSON object with design requirements and operating points
+ * (should be processed using process_inputs() first)
+ * @param maximumNumberResults Maximum number of magnetic recommendations
+ * @param coreModeJson Core selection mode: "AVAILABLE_CORES" or "STANDARD_CORES"
+ * @return JSON array of complete Mas objects sorted by score (best first)
+ */
json calculate_advised_magnetics(json inputsJson, int maximumNumberResults, json coreModeJson);
+
+/**
+ * @brief Get recommended magnetics from a custom component catalog
+ *
+ * Evaluates magnetic components from a user-provided catalog against
+ * the design requirements and returns ranked recommendations.
+ *
+ * @param inputsJson JSON object with design requirements and operating points
+ * @param catalogJson JSON array of Magnetic objects to evaluate
+ * @param maximumNumberResults Maximum number of recommendations to return
+ * @return JSON object with "data" array containing ranked results
+ * Each result has "mas" (Mas object) and "scoring" (float score)
+ */
json calculate_advised_magnetics_from_catalog(json inputsJson, json catalogJson, int maximumNumberResults);
+
+/**
+ * @brief Get recommended magnetics from previously cached designs
+ *
+ * Evaluates cached magnetic designs against the requirements using
+ * a custom filter flow for advanced filtering operations.
+ *
+ * @param inputsJson JSON object with design requirements and operating points
+ * @param filterFlowJson JSON array of MagneticFilterOperation objects
+ * defining the filtering pipeline
+ * @param maximumNumberResults Maximum number of recommendations to return
+ * @return JSON object with "data" array, or error string if cache is empty
+ *
+ * @note Cache must be populated using load_magnetics_from_file() first
+ */
json calculate_advised_magnetics_from_cache(json inputsJson, json filterFlowJson, int maximumNumberResults);
+/**
+ * @brief Register adviser-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_adviser_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/bobbin.h b/src/bobbin.h
index ce07866..1025cba 100644
--- a/src/bobbin.h
+++ b/src/bobbin.h
@@ -1,18 +1,119 @@
+/**
+ * @file bobbin.h
+ * @brief Bobbin/coil former management functions for PyOpenMagnetics
+ *
+ * Provides functions for querying bobbin databases, creating bobbins from
+ * core specifications, and validating winding fit.
+ *
+ * ## Bobbin Types
+ * - Standard bobbins: Pre-made formers matching common core shapes
+ * - Custom bobbins: Created from core dimensions with specified wall thickness
+ * - Quick bobbins: Auto-generated with default parameters
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Query bobbins
+ * bobbins = pom.get_bobbin_names()
+ * bobbin = pom.find_bobbin_by_name("ETD 49 bobbin")
+ *
+ * # Create bobbin from core
+ * core = pom.calculate_core_data(core_spec, False)
+ * bobbin = pom.create_basic_bobbin(core, False)
+ *
+ * # Or with specific wall thickness
+ * bobbin = pom.create_basic_bobbin_by_thickness(core, 0.002) # 2mm walls
+ *
+ * # Check if wire fits
+ * fits = pom.check_if_fits(bobbin, wire_diameter, True)
+ * @endcode
+ *
+ * @see core.h for core specifications
+ * @see winding.h for placing wires in bobbins
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
+/**
+ * @brief Get all available bobbins from the database
+ * @return JSON array of Bobbin objects
+ */
json get_bobbins();
+
+/**
+ * @brief Get list of all bobbin names
+ * @return JSON array of bobbin name strings
+ */
json get_bobbin_names();
+
+/**
+ * @brief Find complete bobbin data by name
+ * @param bobbinName Bobbin name (e.g., "ETD 49 bobbin")
+ * @return JSON Bobbin object with full specification
+ */
json find_bobbin_by_name(json bobbinName);
+
+/**
+ * @brief Create a basic bobbin from core data
+ *
+ * Generates a bobbin that fits the core's winding window with
+ * default wall thickness and margins.
+ *
+ * @param coreDataJson JSON object with core specification
+ * @param nullDimensions If true, set dimensions to null for later calculation
+ * @return JSON Bobbin object
+ */
json create_basic_bobbin(json coreDataJson, bool nullDimensions);
+
+/**
+ * @brief Create a bobbin with specified wall thickness
+ *
+ * @param coreDataJson JSON object with core specification
+ * @param thickness Wall thickness in meters
+ * @return JSON Bobbin object with specified wall thickness
+ */
json create_basic_bobbin_by_thickness(json coreDataJson, double thickness);
+
+/**
+ * @brief Calculate bobbin specifications from magnetic assembly
+ *
+ * Extracts and processes bobbin data from a complete magnetic object.
+ * Creates a quick bobbin if the magnetic has a "Dummy" bobbin reference.
+ *
+ * @param magneticJson JSON Magnetic object containing coil with bobbin
+ * @return JSON Bobbin object with computed properties
+ */
json calculate_bobbin_data(json magneticJson);
+
+/**
+ * @brief Process bobbin geometry and calculate derived parameters
+ * @param bobbinJson JSON Bobbin object
+ * @return JSON Bobbin object with processed geometry
+ */
json process_bobbin(json bobbinJson);
+
+/**
+ * @brief Check if a dimension fits in the bobbin's winding window
+ *
+ * Validates whether a wire or other element of given dimension
+ * will fit in the available winding space.
+ *
+ * @param bobbinJson JSON Bobbin object
+ * @param dimension Dimension to check in meters
+ * @param isHorizontalOrRadial True for horizontal/radial, false for vertical/axial
+ * @return true if the dimension fits, false otherwise
+ */
bool check_if_fits(json bobbinJson, double dimension, bool isHorizontalOrRadial);
+/**
+ * @brief Register bobbin-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_bobbin_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/common.h b/src/common.h
index 1e6ee23..4d30d13 100644
--- a/src/common.h
+++ b/src/common.h
@@ -1,3 +1,23 @@
+/**
+ * @file common.h
+ * @brief Common includes and shared declarations for PyOpenMagnetics bindings
+ *
+ * This header provides the foundation for all PyOpenMagnetics Python bindings.
+ * It includes necessary pybind11 headers, JSON libraries, and the OpenMagnetics
+ * MAS (Magnetic Agnostic Structure) library.
+ *
+ * @note This file must be included before any other PyMKF headers.
+ *
+ * ## Dependencies
+ * - pybind11: Python/C++ binding library
+ * - nlohmann/json: JSON serialization
+ * - magic_enum: Enum reflection
+ * - OpenMagnetics MAS: Core magnetic design library
+ *
+ * @author OpenMagnetics Contributors
+ * @copyright MIT License
+ */
+
#pragma once
#include
@@ -45,9 +65,25 @@ namespace py = pybind11;
#define STRINGIFY(x) #x
#define MACRO_STRINGIFY(x) STRINGIFY(x)
+/**
+ * @namespace PyMKF
+ * @brief PyOpenMagnetics Python bindings namespace
+ *
+ * Contains all functions and declarations for the PyOpenMagnetics Python module.
+ * Functions in this namespace are exposed to Python via pybind11.
+ */
namespace PyMKF {
-// Global database declaration - defined in module.cpp
+/**
+ * @brief Global in-memory cache for MAS (Magnetic Agnostic Structure) objects
+ *
+ * This map stores loaded MAS objects keyed by string identifiers.
+ * Used for caching magnetic designs between Python calls.
+ *
+ * @note Defined in module.cpp, declared extern here for shared access.
+ *
+ * @see load_mas(), read_mas()
+ */
extern std::map masDatabase;
} // namespace PyMKF
diff --git a/src/core.h b/src/core.h
index b7da22d..fa1074f 100644
--- a/src/core.h
+++ b/src/core.h
@@ -1,51 +1,332 @@
+/**
+ * @file core.h
+ * @brief Core material and shape query functions for PyOpenMagnetics
+ *
+ * Provides comprehensive functions for querying core materials and shapes,
+ * calculating core parameters, and performing inductance/gap calculations.
+ *
+ * ## Core Materials
+ * Supported manufacturers: TDK/EPCOS, Ferroxcube, Fair-Rite, Magnetics Inc, Micrometals
+ * Material types: Ferrite (MnZn, NiZn), Iron Powder, Amorphous, Nanocrystalline
+ *
+ * ## Core Shapes
+ * Families: E, EI, EFD, EQ, ER, ETD, EC, PQ, PM, RM, T (toroidal), P, PT, U, UI, LP
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Query materials
+ * materials = pom.get_core_material_names()
+ * n87 = pom.find_core_material_by_name("N87")
+ * steinmetz = pom.get_core_material_steinmetz_coefficients("N87", 100000)
+ *
+ * # Query shapes
+ * shapes = pom.get_core_shape_names(include_toroidal=True)
+ * e42 = pom.find_core_shape_by_name("E 42/21/15")
+ *
+ * # Calculate inductance
+ * L = pom.calculate_inductance_from_number_turns_and_gapping(core, coil, op, models)
+ * @endcode
+ *
+ * @see database.h for loading databases
+ * @see losses.h for core loss calculations
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Core materials
+// ============================================================================
+// Core Material Functions
+// ============================================================================
+
+/**
+ * @brief Get all available core materials from the database
+ * @return JSON array of CoreMaterial objects with full specifications
+ */
json get_core_materials();
+
+/**
+ * @brief Calculate initial permeability under specific conditions
+ * @param materialName Material name or JSON object
+ * @param temperature Temperature in Celsius
+ * @param magneticFieldDcBias DC magnetic field bias in A/m
+ * @param frequency Operating frequency in Hz
+ * @return Initial relative permeability (dimensionless)
+ */
double get_material_permeability(json materialName, double temperature, double magneticFieldDcBias, double frequency);
+
+/**
+ * @brief Calculate electrical resistivity for a core material
+ * @param materialName Material name or JSON object
+ * @param temperature Temperature in Celsius
+ * @return Resistivity in Ohm·m
+ */
double get_material_resistivity(json materialName, double temperature);
+
+/**
+ * @brief Get Steinmetz coefficients for core loss calculation
+ *
+ * Returns k, alpha, beta for: Pv = k * f^alpha * B^beta
+ *
+ * @param materialName Material name or JSON object
+ * @param frequency Operating frequency in Hz for coefficient selection
+ * @return JSON SteinmetzCoreLossesMethodRangeDatum with k, alpha, beta
+ */
json get_core_material_steinmetz_coefficients(json materialName, double frequency);
+
+/**
+ * @brief Get list of all core material names
+ * @return JSON array of material name strings
+ */
json get_core_material_names();
+
+/**
+ * @brief Get core material names filtered by manufacturer
+ * @param manufacturerName Manufacturer name (e.g., "TDK", "Ferroxcube")
+ * @return JSON array of material name strings
+ */
json get_core_material_names_by_manufacturer(std::string manufacturerName);
+
+/**
+ * @brief Find complete core material data by name
+ * @param materialName Material name (e.g., "3C95", "N87")
+ * @return JSON CoreMaterial object with full specification
+ */
json find_core_material_by_name(json materialName);
+
+/**
+ * @brief Get material data by name (alias for find_core_material_by_name)
+ * @param materialName Material name string
+ * @return JSON CoreMaterial object
+ */
json get_material_data(std::string materialName);
+
+/**
+ * @brief Get list of available core materials, optionally filtered by manufacturer
+ * @param manufacturer Manufacturer name filter (empty for all)
+ * @return Vector of material name strings
+ */
std::vector get_available_core_materials(std::string manufacturer);
+
+/**
+ * @brief Get list of all core material manufacturers
+ * @return Vector of manufacturer name strings
+ */
std::vector get_available_core_manufacturers();
-// Core shapes
+// ============================================================================
+// Core Shape Functions
+// ============================================================================
+
+/**
+ * @brief Get all available core shapes from the database
+ * @return JSON array of CoreShape objects
+ */
json get_core_shapes();
+
+/**
+ * @brief Get list of unique core shape families
+ * @return JSON array of CoreShapeFamily strings
+ */
json get_core_shape_families();
+
+/**
+ * @brief Get list of core shape names
+ * @param includeToroidal Whether to include toroidal shapes
+ * @return JSON array of shape name strings
+ */
json get_core_shape_names(bool includeToroidal);
+
+/**
+ * @brief Find complete core shape data by name
+ * @param shapeName Shape name (e.g., "E 42/21/15", "ETD 49/25/16")
+ * @return JSON CoreShape object with full dimensional data
+ */
json find_core_shape_by_name(json shapeName);
+
+/**
+ * @brief Get shape data by name
+ * @param shapeName Shape name string
+ * @return JSON CoreShape object
+ */
json get_shape_data(std::string shapeName);
+
+/**
+ * @brief Calculate complete core data from shape specification
+ * @param shapeJson JSON CoreShape object
+ * @return JSON Core object with processed dimensions
+ */
json calculate_shape_data(json shapeJson);
+
+/**
+ * @brief Get list of all shape family types
+ * @return Vector of family name strings
+ */
std::vector get_available_shape_families();
+
+/**
+ * @brief Get list of all core shape families (alias)
+ * @return Vector of family name strings
+ */
std::vector get_available_core_shape_families();
+
+/**
+ * @brief Get list of all available core shape names
+ * @return Vector of shape name strings
+ */
std::vector get_available_core_shapes();
+
+/**
+ * @brief Get all pre-configured cores from database
+ * @return JSON array of Core objects
+ */
json get_available_cores();
-// Core calculations
+// ============================================================================
+// Core Calculation Functions
+// ============================================================================
+
+/**
+ * @brief Process core functional description and compute derived parameters
+ *
+ * Calculates effective magnetic parameters (Ae, le, Ve), winding window
+ * dimensions, column geometry, and geometrical description.
+ *
+ * @param coreDataJson JSON object with functionalDescription
+ * @param includeMaterialData Whether to include full material curves
+ * @return JSON Core object with all descriptions populated
+ */
json calculate_core_data(json coreDataJson, bool includeMaterialData);
+
+/**
+ * @brief Calculate only the processed description for a core
+ * @param coreDataJson JSON object with functionalDescription
+ * @return JSON CoreProcessedDescription with effectiveParameters
+ */
json calculate_core_processed_description(json coreDataJson);
+
+/**
+ * @brief Calculate geometrical description for visualization
+ * @param coreDataJson JSON object with functionalDescription
+ * @return JSON array of CoreGeometricalDescriptionElement objects
+ */
json calculate_core_geometrical_description(json coreDataJson);
+
+/**
+ * @brief Process and calculate gapping configuration
+ * @param coreDataJson JSON object with core and gapping specification
+ * @return JSON array of processed CoreGap objects
+ */
json calculate_core_gapping(json coreDataJson);
+
+/**
+ * @brief Load and process multiple cores from JSON array
+ * @param coresJson JSON array of core specifications
+ * @return JSON array of processed Core objects
+ */
json load_core_data(json coresJson);
+
+/**
+ * @brief Get temperature-dependent magnetic parameters
+ * @param coreData JSON object with core specification
+ * @param temperature Temperature in Celsius
+ * @return JSON with Bsat, Hsat, permeability, reluctance, resistivity
+ */
json get_core_temperature_dependant_parameters(json coreData, double temperature);
+
+/**
+ * @brief Calculate maximum magnetic energy storage capacity
+ * @param coreDataJson JSON object with core specification
+ * @param operatingPointJson JSON operating point (optional, for DC bias)
+ * @return Maximum storable magnetic energy in Joules
+ */
double calculate_core_maximum_magnetic_energy(json coreDataJson, json operatingPointJson);
+
+/**
+ * @brief Calculate saturation current for a magnetic component
+ * @param magneticJson JSON Magnetic object (core + coil)
+ * @param temperature Operating temperature in Celsius
+ * @return Saturation current in Amperes
+ */
double calculate_saturation_current(json magneticJson, double temperature);
+
+/**
+ * @brief Calculate core temperature from thermal resistance
+ * @param coreJson JSON Core object with thermal resistance data
+ * @param totalLosses Total power dissipation in Watts
+ * @return Estimated core temperature in Celsius
+ */
double calculate_temperature_from_core_thermal_resistance(json coreJson, double totalLosses);
-// Gap and reluctance
+// ============================================================================
+// Gap and Reluctance Functions
+// ============================================================================
+
+/**
+ * @brief Calculate magnetic reluctance of an air gap
+ *
+ * Models: ZHANG, MUEHLETHALER, PARTRIDGE, EFFECTIVE_AREA, STENGLEIN, BALAKRISHNAN, CLASSIC
+ *
+ * @param coreGapData JSON CoreGap object with gap geometry
+ * @param modelNameString Reluctance model name
+ * @return JSON AirGapReluctanceOutput with reluctance and fringing factor
+ */
json calculate_gap_reluctance(json coreGapData, std::string modelNameString);
+
+/**
+ * @brief Get documentation for available gap reluctance models
+ * @return JSON object with model information, errors, and links
+ */
json get_gap_reluctance_model_information();
+
+/**
+ * @brief Calculate inductance from turns count and gap configuration
+ *
+ * Uses: L = N² / R_total
+ *
+ * @param coreData JSON object with core specification
+ * @param coilData JSON object with coil specification (for turns)
+ * @param operatingPointData JSON operating point for DC bias consideration
+ * @param modelsData JSON dict with "reluctance" model selection
+ * @return Magnetizing inductance in Henries
+ */
double calculate_inductance_from_number_turns_and_gapping(json coreData, json coilData, json operatingPointData, json modelsData);
+
+/**
+ * @brief Calculate required turns from gap and target inductance
+ *
+ * Computes: N = sqrt(L * R_total)
+ *
+ * @param coreData JSON object with core specification
+ * @param inputsData JSON Inputs with magnetizingInductance requirement
+ * @param modelsData JSON dict with "reluctance" model selection
+ * @return Required number of turns (may be non-integer)
+ */
double calculate_number_turns_from_gapping_and_inductance(json coreData, json inputsData, json modelsData);
+
+/**
+ * @brief Calculate required gap from turns count and target inductance
+ *
+ * Iteratively solves for gap length to achieve target inductance.
+ *
+ * @param coreData JSON object with core specification
+ * @param coilData JSON object with coil specification
+ * @param inputsData JSON Inputs with magnetizingInductance requirement
+ * @param gappingTypeJson Gap type ("SUBTRACTIVE", "ADDITIVE", "DISTRIBUTED")
+ * @param decimals Precision in decimal places for gap length
+ * @param modelsData JSON dict with "reluctance" model selection
+ * @return JSON Core object with updated gapping configuration
+ */
json calculate_gapping_from_number_turns_and_inductance(json coreData, json coilData, json inputsData, std::string gappingTypeJson, int decimals, json modelsData);
+/**
+ * @brief Register core-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_core_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/database.h b/src/database.h
index 6d1a5d0..e64a72b 100644
--- a/src/database.h
+++ b/src/database.h
@@ -1,27 +1,152 @@
+/**
+ * @file database.h
+ * @brief Database loading and caching functions for PyOpenMagnetics
+ *
+ * Provides functions to load and manage magnetic component databases including
+ * core materials, core shapes, wires, bobbins, and insulation materials.
+ * Also provides MAS object caching for efficient reuse of magnetic designs.
+ *
+ * ## Database Files (NDJSON format)
+ * - core_materials.ndjson: Ferrite, iron powder, and other core materials
+ * - core_shapes.ndjson: E, ETD, PQ, RM, toroidal, and other core shapes
+ * - wires.ndjson: Round, litz, rectangular, and foil wire specifications
+ * - bobbins.ndjson: Bobbin/coil former specifications
+ * - insulation_materials.ndjson: Insulation tape and coating materials
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Load all databases
+ * pom.load_core_materials()
+ * pom.load_core_shapes()
+ * pom.load_wires()
+ *
+ * # Check if loaded
+ * if not pom.is_core_material_database_empty():
+ * print("Materials loaded")
+ * @endcode
+ *
+ * @see core.h for material/shape query functions
+ * @see wire.h for wire query functions
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
+/**
+ * @brief Load all component databases from a JSON object
+ * @param databasesJson JSON object containing all database data
+ */
void load_databases(json databasesJson);
+
+/**
+ * @brief Read databases from NDJSON files in a directory
+ * @param path Path to directory containing database files
+ * @param addInternalData Whether to include internal computed data
+ * @return "0" on success, error message on failure
+ */
std::string read_databases(std::string path, bool addInternalData);
+
+/**
+ * @brief Load and cache a MAS (Magnetic Agnostic Structure) object
+ * @param key Unique identifier for caching
+ * @param masJson JSON representation of the MAS object
+ * @param expand Whether to autocomplete missing fields
+ * @return Number of cached objects as string, or error message
+ */
std::string load_mas(std::string key, json masJson, bool expand);
+
+/**
+ * @brief Load and cache a Magnetic component
+ * @param key Unique identifier for caching
+ * @param magneticJson JSON representation of the Magnetic object
+ * @param expand Whether to autocomplete missing fields
+ * @return Number of cached objects as string, or error message
+ */
std::string load_magnetic(std::string key, json magneticJson, bool expand);
+
+/**
+ * @brief Load and cache multiple Magnetic components
+ * @param keys JSON array of unique identifiers
+ * @param magneticJsons JSON array of Magnetic objects
+ * @param expand Whether to autocomplete missing fields
+ * @return Number of cached objects as string, or error message
+ */
std::string load_magnetics(std::string keys, json magneticJsons, bool expand);
+
+/**
+ * @brief Retrieve a cached MAS object by key
+ * @param key Unique identifier used when loading
+ * @return JSON representation of the MAS object
+ */
json read_mas(std::string key);
+/**
+ * @brief Load core materials database
+ * @param fileToLoad Optional path to custom database file (empty for default)
+ * @return Number of materials loaded
+ */
size_t load_core_materials(std::string fileToLoad);
+
+/**
+ * @brief Load core shapes database
+ * @param fileToLoad Optional path to custom database file (empty for default)
+ * @return Number of shapes loaded
+ */
size_t load_core_shapes(std::string fileToLoad);
+
+/**
+ * @brief Load wires database
+ * @param fileToLoad Optional path to custom database file (empty for default)
+ * @return Number of wires loaded
+ */
size_t load_wires(std::string fileToLoad);
+
+/**
+ * @brief Clear all loaded databases from memory
+ */
void clear_databases();
+
+/**
+ * @brief Check if core material database is empty
+ * @return true if no materials are loaded
+ */
bool is_core_material_database_empty();
+
+/**
+ * @brief Check if core shape database is empty
+ * @return true if no shapes are loaded
+ */
bool is_core_shape_database_empty();
+
+/**
+ * @brief Check if wire database is empty
+ * @return true if no wires are loaded
+ */
bool is_wire_database_empty();
+/**
+ * @brief Load magnetic components from an NDJSON file into cache
+ * @param path Path to NDJSON file with magnetic components
+ * @param expand Whether to autocomplete missing fields
+ * @return Number of cached magnetics as string, or error message
+ */
std::string load_magnetics_from_file(std::string path, bool expand);
+
+/**
+ * @brief Clear the magnetic component cache
+ * @return Number of remaining cached items (should be 0)
+ */
std::string clear_magnetic_cache();
+/**
+ * @brief Register database-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_database_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/losses.h b/src/losses.h
index 00a2132..6a90c85 100644
--- a/src/losses.h
+++ b/src/losses.h
@@ -1,33 +1,248 @@
+/**
+ * @file losses.h
+ * @brief Loss calculation functions for PyOpenMagnetics
+ *
+ * Provides comprehensive functions for calculating core losses and winding
+ * losses in magnetic components. Supports multiple loss models and provides
+ * detailed breakdown of loss components.
+ *
+ * ## Core Loss Models
+ * - STEINMETZ: Original Steinmetz equation (sinusoidal only)
+ * - IGSE: Improved Generalized Steinmetz Equation (arbitrary waveforms)
+ * - MSE: Modified Steinmetz Equation
+ * - BARG: Barg model for high-flux materials
+ * - ROSHEN: Roshen model with DC bias support
+ * - ALBACH: Albach frequency-domain model
+ * - PROPRIETARY: Manufacturer-specific models
+ *
+ * ## Winding Loss Components
+ * - Ohmic losses: DC I²R losses
+ * - Skin effect: Current crowding at high frequency
+ * - Proximity effect: Eddy currents from nearby conductors
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+ *
+ * # Calculate core losses
+ * core_loss = pom.calculate_core_losses(core, coil, inputs, models)
+ * print(f"Core loss: {core_loss['coreLosses']:.2f} W")
+ * print(f"B_peak: {core_loss['magneticFluxDensityPeak']*1000:.1f} mT")
+ *
+ * # Calculate winding losses
+ * winding_loss = pom.calculate_winding_losses(magnetic, op, 85)
+ * print(f"Winding loss: {winding_loss['windingLosses']:.2f} W")
+ * @endcode
+ *
+ * @see core.h for core specifications
+ * @see winding.h for coil specifications
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Core losses
+// ============================================================================
+// Core Loss Functions
+// ============================================================================
+
+/**
+ * @brief Calculate core losses for a magnetic component
+ *
+ * @param coreData JSON object with core specification
+ * @param coilData JSON object with coil specification
+ * @param inputsData JSON object with operating points
+ * @param modelsData JSON dict specifying models:
+ * {"coreLosses": "IGSE", "reluctance": "ZHANG", "coreTemperature": "..."}
+ * @return JSON object with coreLosses, magneticFluxDensityPeak, temperature, etc.
+ */
json calculate_core_losses(json coreData, json coilData, json inputsData, json modelsData);
+
+/**
+ * @brief Get documentation for available core loss models
+ * @param material JSON object with material data for model availability
+ * @return JSON object with model information, errors, and links
+ */
json get_core_losses_model_information(json material);
+
+/**
+ * @brief Get documentation for core temperature models
+ * @return JSON object with model information, errors, and links
+ */
json get_core_temperature_model_information();
+
+/**
+ * @brief Fit Steinmetz coefficients from measured loss data
+ *
+ * @param dataJson JSON array of VolumetricLossesPoint measurements
+ * @param rangesJson JSON array of [min_freq, max_freq] tuples
+ * @return JSON array of SteinmetzCoreLossesMethodRangeDatum with k, alpha, beta
+ */
json calculate_steinmetz_coefficients(json dataJson, json rangesJson);
+
+/**
+ * @brief Fit Steinmetz coefficients with error estimation
+ *
+ * @param dataJson JSON array of VolumetricLossesPoint measurements
+ * @param rangesJson JSON array of frequency range tuples
+ * @return JSON object with coefficientsPerRange and errorPerRange
+ */
json calculate_steinmetz_coefficients_with_error(json dataJson, json rangesJson);
-// Winding losses
+// ============================================================================
+// Winding Loss Functions
+// ============================================================================
+
+/**
+ * @brief Calculate total winding losses including all AC effects
+ *
+ * Computes DC ohmic, skin effect, and proximity effect losses.
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param operatingPointJson JSON object with excitation conditions
+ * @param temperature Winding temperature in Celsius
+ * @return JSON WindingLossesOutput with total and component losses
+ */
json calculate_winding_losses(json magneticJson, json operatingPointJson, double temperature);
+
+/**
+ * @brief Calculate DC ohmic losses only
+ * @param coilJson JSON object with coil specification
+ * @param operatingPointJson JSON object with current excitation
+ * @param temperature Wire temperature in Celsius
+ * @return JSON WindingLossesOutput with ohmicLosses field
+ */
json calculate_ohmic_losses(json coilJson, json operatingPointJson, double temperature);
+
+/**
+ * @brief Calculate magnetic field strength distribution in winding window
+ *
+ * Used for proximity effect calculations and field visualization.
+ *
+ * @param operatingPointJson JSON object with excitation conditions
+ * @param magneticJson JSON object with magnetic specification
+ * @return JSON WindingWindowMagneticStrengthFieldOutput with field data
+ */
json calculate_magnetic_field_strength_field(json operatingPointJson, json magneticJson);
+
+/**
+ * @brief Calculate proximity effect losses from pre-computed field
+ *
+ * @param coilJson JSON object with coil specification
+ * @param temperature Wire temperature in Celsius
+ * @param windingLossesOutputJson Previous WindingLossesOutput for accumulation
+ * @param windingWindowMagneticStrengthFieldOutputJson Field data from calculate_magnetic_field_strength_field()
+ * @return Updated JSON WindingLossesOutput with proximity losses added
+ */
json calculate_proximity_effect_losses(json coilJson, double temperature, json windingLossesOutputJson, json windingWindowMagneticStrengthFieldOutputJson);
+
+/**
+ * @brief Calculate skin effect losses in coil windings
+ *
+ * @param coilJson JSON object with coil specification
+ * @param windingLossesOutputJson Previous WindingLossesOutput
+ * @param temperature Wire temperature in Celsius
+ * @return Updated JSON WindingLossesOutput with skin losses added
+ */
json calculate_skin_effect_losses(json coilJson, json windingLossesOutputJson, double temperature);
+
+/**
+ * @brief Calculate skin effect losses per meter of wire
+ *
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @param currentDivider Current sharing factor (1.0 for single conductor)
+ * @return JSON object with skin effect loss power per meter in W/m
+ */
json calculate_skin_effect_losses_per_meter(json wireJson, json currentJson, double temperature, double currentDivider);
-// DC resistance and losses
+// ============================================================================
+// DC Resistance and Per-Meter Loss Functions
+// ============================================================================
+
+/**
+ * @brief Calculate DC resistance per meter of wire
+ * @param wireJson JSON object with wire specification
+ * @param temperature Wire temperature in Celsius
+ * @return DC resistance in Ohms per meter
+ */
double calculate_dc_resistance_per_meter(json wireJson, double temperature);
+
+/**
+ * @brief Calculate DC ohmic losses per meter of wire
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @return DC power loss in Watts per meter
+ */
double calculate_dc_losses_per_meter(json wireJson, json currentJson, double temperature);
+
+/**
+ * @brief Calculate skin effect AC resistance factor (Fr = Rac/Rdc)
+ *
+ * Fr = 1.0 means no skin effect; Fr > 1.0 indicates skin effect losses.
+ *
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @return AC factor (dimensionless ratio >= 1.0)
+ */
double calculate_skin_ac_factor(json wireJson, json currentJson, double temperature);
+
+/**
+ * @brief Calculate AC skin effect losses per meter (excluding DC)
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @return AC skin effect power loss in Watts per meter
+ */
double calculate_skin_ac_losses_per_meter(json wireJson, json currentJson, double temperature);
+
+/**
+ * @brief Calculate total AC resistance per meter including skin effect
+ *
+ * Rac = Rdc * Fr where Fr is the skin effect AC factor.
+ *
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @return Total AC resistance in Ohms per meter
+ */
double calculate_skin_ac_resistance_per_meter(json wireJson, json currentJson, double temperature);
+
+/**
+ * @brief Calculate effective current density in wire conductor
+ *
+ * Accounts for frequency-dependent current distribution.
+ *
+ * @param wireJson JSON object with wire specification
+ * @param currentJson JSON SignalDescriptor with current waveform
+ * @param temperature Wire temperature in Celsius
+ * @return Effective current density in A/m²
+ */
double calculate_effective_current_density(json wireJson, json currentJson, double temperature);
+
+/**
+ * @brief Calculate effective skin depth for a conductor material
+ *
+ * delta = sqrt(2*rho / (omega*mu))
+ *
+ * @param materialName Name of conductor material (e.g., "copper")
+ * @param currentJson JSON SignalDescriptor with effective frequency
+ * @param temperature Conductor temperature in Celsius
+ * @return Skin depth in meters, or -1 if frequency not available
+ */
double calculate_effective_skin_depth(std::string materialName, json currentJson, double temperature);
+/**
+ * @brief Register losses-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_losses_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/module.cpp b/src/module.cpp
index cd50512..11dec4e 100644
--- a/src/module.cpp
+++ b/src/module.cpp
@@ -1,3 +1,60 @@
+/**
+ * @file module.cpp
+ * @brief Main pybind11 module entry point for PyOpenMagnetics
+ *
+ * This file creates the Python module 'PyOpenMagnetics' and registers all
+ * binding namespaces. It serves as the main entry point when the module
+ * is imported in Python.
+ *
+ * ## Module Structure
+ *
+ * The PyOpenMagnetics module exposes 183 functions organized into 11 categories:
+ *
+ * | Category | Count | Description |
+ * |-------------|-------|---------------------------------------|
+ * | Database | 15 | Data loading & caching |
+ * | Core | 42 | Materials, shapes, calculations |
+ * | Wire | 32 | Wire database & selection |
+ * | Bobbin | 8 | Bobbin lookup & fitting |
+ * | Winding | 23 | Coil placement & insulation |
+ * | Advisers | 4 | Design recommendation |
+ * | Losses | 22 | Core & winding loss models |
+ * | Simulation | 16 | Full EM simulation & matrices |
+ * | Plotting | 6 | SVG visualization |
+ * | Settings | 6 | Configuration & constants |
+ * | Utils | 9 | Signal processing |
+ *
+ * ## Quick Start
+ *
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Load databases
+ * pom.load_core_materials()
+ * pom.load_core_shapes()
+ * pom.load_wires()
+ *
+ * # Design a magnetic component
+ * inputs = {"designRequirements": {...}, "operatingPoints": [...]}
+ * processed = pom.process_inputs(inputs) # CRITICAL: must call first!
+ * magnetics = pom.calculate_advised_magnetics(processed, 5, "STANDARD_CORES")
+ *
+ * # Simulate and analyze
+ * result = pom.simulate(processed, magnetics[0], {"coreLosses": "IGSE"})
+ * @endcode
+ *
+ * ## Build Information
+ *
+ * Built with pybind11 for Python/C++ interoperability.
+ * Requires C++23 compatible compiler.
+ *
+ * @see common.h for shared declarations
+ * @see https://github.com/OpenMagnetics for project repository
+ *
+ * @author OpenMagnetics Contributors
+ * @copyright MIT License
+ */
+
#include "common.h"
#include "database.h"
#include "core.h"
@@ -11,19 +68,49 @@
#include "settings.h"
#include "utils.h"
+/**
+ * @brief PyOpenMagnetics Python module definition
+ *
+ * PYBIND11_MODULE macro creates the module entry point.
+ * All binding registration functions are called here to expose
+ * C++ functions to Python.
+ *
+ * @param PyOpenMagnetics Module name as seen in Python
+ * @param m Module handle for registering functions
+ */
PYBIND11_MODULE(PyOpenMagnetics, m) {
- m.doc() = "OpenMagnetics Python bindings for magnetic component design";
+ m.doc() = R"pbdoc(
+ PyOpenMagnetics - Python bindings for magnetic component design
+
+ OpenMagnetics Python module provides comprehensive tools for designing
+ transformers, inductors, and chokes for power electronics applications.
+
+ Key Features:
+ - 183 functions for magnetic component design
+ - Support for 1000+ core shapes and materials
+ - Multiple loss models (Steinmetz, iGSE, MSE, etc.)
+ - Design recommendation engine
+ - SVG visualization output
+
+ Quick Start:
+ >>> import PyOpenMagnetics as pom
+ >>> pom.load_core_materials()
+ >>> pom.load_core_shapes()
+ >>> materials = pom.get_core_material_names()
+
+ IMPORTANT: Always call process_inputs() before using adviser functions!
+ )pbdoc";
- // Register all module bindings
- PyMKF::register_database_bindings(m);
- PyMKF::register_core_bindings(m);
- PyMKF::register_wire_bindings(m);
- PyMKF::register_bobbin_bindings(m);
- PyMKF::register_winding_bindings(m);
- PyMKF::register_adviser_bindings(m);
- PyMKF::register_losses_bindings(m);
- PyMKF::register_simulation_bindings(m);
- PyMKF::register_plotting_bindings(m);
- PyMKF::register_settings_bindings(m);
- PyMKF::register_utils_bindings(m);
-}
\ No newline at end of file
+ // Register all module bindings in dependency order
+ PyMKF::register_database_bindings(m); // Database loading (no deps)
+ PyMKF::register_core_bindings(m); // Core materials & shapes
+ PyMKF::register_wire_bindings(m); // Wire database
+ PyMKF::register_bobbin_bindings(m); // Bobbin management
+ PyMKF::register_winding_bindings(m); // Coil winding engine
+ PyMKF::register_adviser_bindings(m); // Design recommendation
+ PyMKF::register_losses_bindings(m); // Loss calculations
+ PyMKF::register_simulation_bindings(m); // Full EM simulation
+ PyMKF::register_plotting_bindings(m); // SVG visualization
+ PyMKF::register_settings_bindings(m); // Configuration
+ PyMKF::register_utils_bindings(m); // Utility functions
+}
diff --git a/src/plotting.h b/src/plotting.h
index 88d8f89..4e7a431 100644
--- a/src/plotting.h
+++ b/src/plotting.h
@@ -1,17 +1,123 @@
+/**
+ * @file plotting.h
+ * @brief SVG visualization functions for PyOpenMagnetics
+ *
+ * Provides functions for generating 2D cross-section visualizations of
+ * magnetic components as SVG graphics.
+ *
+ * ## Plot Types
+ * - plot_core(): Core cross-section only
+ * - plot_magnetic(): Complete assembly (core + bobbin + coil)
+ * - plot_magnetic_field(): H-field distribution with arrows
+ * - plot_electric_field(): E-field (voltage gradient) distribution
+ * - plot_wire(): Wire cross-section with strands (for litz)
+ * - plot_bobbin(): Core with bobbin/coil former
+ *
+ * ## Output Format
+ * All functions return JSON with:
+ * - success: Boolean indicating operation status
+ * - svg: SVG string content for rendering/saving
+ * - error: Error message if success is false
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Plot complete magnetic assembly
+ * result = pom.plot_magnetic(magnetic)
+ * if result["success"]:
+ * with open("magnetic.svg", "w") as f:
+ * f.write(result["svg"])
+ *
+ * # Plot magnetic field distribution
+ * result = pom.plot_magnetic_field(magnetic, operating_point)
+ * if result["success"]:
+ * # Display or save SVG
+ * pass
+ *
+ * # Plot with custom output path
+ * result = pom.plot_core(magnetic, "/path/to/output.svg")
+ * @endcode
+ *
+ * @see settings.h for visualization configuration
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Core plotting functions
+/**
+ * @brief Generate 2D cross-section of magnetic core as SVG
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_core(json magneticJson, std::string outputPath = "");
+
+/**
+ * @brief Generate complete magnetic assembly visualization as SVG
+ *
+ * Shows core, bobbin, and coil turns in cross-section view.
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_magnetic(json magneticJson, std::string outputPath = "");
+
+/**
+ * @brief Plot magnetic field (H-field) distribution as SVG
+ *
+ * Generates visualization showing magnetic field strength across
+ * the winding window with arrows indicating field direction.
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param operatingPointJson Operating conditions with excitation currents
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_magnetic_field(json magneticJson, json operatingPointJson, std::string outputPath = "");
+
+/**
+ * @brief Plot electric field (E-field) distribution as SVG
+ *
+ * Generates visualization showing voltage gradient across
+ * the winding window.
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param operatingPointJson Operating conditions with excitation voltages
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_electric_field(json magneticJson, json operatingPointJson, std::string outputPath = "");
+
+/**
+ * @brief Generate wire cross-section visualization as SVG
+ *
+ * Shows wire structure including conductor, insulation layers,
+ * and for litz wire, the individual strands arrangement.
+ *
+ * @param wireDataJson JSON object with wire specification
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_wire(json wireDataJson, std::string outputPath = "");
+
+/**
+ * @brief Generate bobbin visualization with core as SVG
+ *
+ * @param magneticJson JSON object with complete magnetic specification
+ * @param outputPath Optional file path to save SVG (empty = temp directory)
+ * @return JSON object with {success, svg, error}
+ */
json plot_bobbin(json magneticJson, std::string outputPath = "");
-// Register all plotting bindings
+/**
+ * @brief Register plotting-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_plotting_bindings(py::module& m);
-} // namespace PyMKF
\ No newline at end of file
+} // namespace PyMKF
diff --git a/src/settings.h b/src/settings.h
index 82b5f61..5121a67 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -1,17 +1,143 @@
+/**
+ * @file settings.h
+ * @brief Configuration and defaults functions for PyOpenMagnetics
+ *
+ * Provides functions to query and modify library settings, retrieve
+ * physical constants, and access default values for models and parameters.
+ *
+ * ## Settings Categories
+ * - Coil winding options (margin tape, insulated wire, etc.)
+ * - Visualization/painter settings (colors, resolution, scale)
+ * - Magnetic field calculation options
+ * - Core/material database options
+ *
+ * ## Key Constants
+ * - vacuumPermeability: μ₀ = 4π × 10⁻⁷ H/m
+ * - vacuumPermittivity: ε₀ = 8.854 × 10⁻¹² F/m
+ * - residualGap: Minimum gap for machining tolerance
+ * - minimumNonResidualGap: Minimum intentional gap
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Get physical constants
+ * constants = pom.get_constants()
+ * print(f"μ₀ = {constants['vacuumPermeability']}")
+ *
+ * # Get default model selections
+ * defaults = pom.get_default_models()
+ * print(f"Default core loss model: {defaults['coreLosses']}")
+ *
+ * # Modify settings
+ * settings = pom.get_settings()
+ * settings["useOnlyCoresInStock"] = True
+ * settings["painterNumberPointsX"] = 100
+ * pom.set_settings(settings)
+ *
+ * # Reset to defaults
+ * pom.reset_settings()
+ * @endcode
+ *
+ * @see plotting.h for visualization functions
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Settings and defaults
+/**
+ * @brief Get physical and system constants
+ *
+ * @return Python dict with constant names and values:
+ * - vacuumPermeability: μ₀ in H/m
+ * - vacuumPermittivity: ε₀ in F/m
+ * - residualGap: Residual gap in meters
+ * - minimumNonResidualGap: Minimum gap in meters
+ * - spacerProtudingPercentage: Spacer protrusion factor
+ * - coilPainterScale: Visualization scale factor
+ * - minimumDistributedFringingFactor: Min fringing
+ * - maximumDistributedFringingFactor: Max fringing
+ * - initialGapLengthForSearching: Initial gap search value
+ * - roshenMagneticFieldStrengthStep: Roshen model step
+ * - foilToSectionMargin: Foil winding margin
+ * - planarToSectionMargin: Planar winding margin
+ */
py::dict get_constants();
+
+/**
+ * @brief Get default values for parameters and models
+ *
+ * @return Python dict with defaults:
+ * - coreLossesModelDefault: Default core loss model
+ * - coreTemperatureModelDefault: Default temperature model
+ * - reluctanceModelDefault: Default reluctance model
+ * - magneticFieldStrengthModelDefault: Default H-field model
+ * - maximumProportionMagneticFluxDensitySaturation: B_max factor
+ * - coreAdviserFrequencyReference: Adviser reference frequency
+ * - coreAdviserMagneticFluxDensityReference: Reference B-field
+ * - maximumCurrentDensity: Max current density limit
+ * - ambientTemperature: Default ambient temperature
+ * - measurementFrequency: Default measurement frequency
+ * - defaultInsulationMaterial: Default insulation type
+ */
py::dict get_defaults();
+
+/**
+ * @brief Get current library settings
+ *
+ * @return JSON object with all configurable settings:
+ * - coilAllowMarginTape: Allow margin tape in windings
+ * - coilAllowInsulatedWire: Allow insulated wire
+ * - coilFillSectionsWithMarginTape: Fill with tape
+ * - coilWindEvenIfNotFit: Wind even if doesn't fit
+ * - coilDelimitAndCompact: Compact winding layout
+ * - coilTryRewind: Retry winding on failure
+ * - useOnlyCoresInStock: Limit to in-stock cores
+ * - painterNumberPointsX/Y: Field plot resolution
+ * - painterMode: Visualization mode
+ * - painterColorFerrite/Bobbin/Copper/etc.: Colors
+ * - magneticFieldNumberPointsX/Y: Field calc resolution
+ * - magneticFieldIncludeFringing: Include fringing effects
+ */
json get_settings();
+
+/**
+ * @brief Update library settings
+ *
+ * @param settingsJson JSON object with settings to update
+ * Only included keys will be modified
+ *
+ * @throws std::runtime_error if settings are invalid
+ */
void set_settings(json settingsJson);
+
+/**
+ * @brief Reset all settings to default values
+ *
+ * Restores all library settings to their initial defaults.
+ * Useful for ensuring consistent behavior between tests.
+ */
void reset_settings();
+
+/**
+ * @brief Get names of default calculation models
+ *
+ * @return JSON object mapping model types to default names:
+ * - coreLosses: Default core loss model name
+ * - coreTemperature: Default temperature model name
+ * - reluctance: Default reluctance model name
+ * - magneticFieldStrength: Default H-field model name
+ * - magneticFieldStrengthFringingEffect: Default fringing model
+ */
json get_default_models();
+/**
+ * @brief Register settings-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_settings_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/simulation.h b/src/simulation.h
index 1e3812f..fd3c184 100644
--- a/src/simulation.h
+++ b/src/simulation.h
@@ -1,37 +1,240 @@
+/**
+ * @file simulation.h
+ * @brief Simulation and matrix calculation functions for PyOpenMagnetics
+ *
+ * Provides functions for complete magnetic simulation, circuit export,
+ * input processing, and electrical matrix calculations.
+ *
+ * ## Key Functions
+ * - simulate(): Full electromagnetic simulation
+ * - process_inputs(): CRITICAL - must call before using advisers
+ * - calculate_inductance_matrix(): Self and mutual inductances
+ * - calculate_resistance_matrix(): AC resistance matrix
+ * - calculate_stray_capacitance(): Parasitic capacitances
+ *
+ * ## Circuit Export
+ * Supports export to SPICE-compatible subcircuit format for circuit simulation.
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # CRITICAL: Always process inputs first!
+ * inputs = {"designRequirements": {...}, "operatingPoints": [...]}
+ * processed = pom.process_inputs(inputs)
+ *
+ * # Run full simulation
+ * models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+ * result = pom.simulate(processed, magnetic, models)
+ *
+ * # Get inductance matrix
+ * L_matrix = pom.calculate_inductance_matrix(magnetic, 100000, models)
+ *
+ * # Export for circuit simulation
+ * subcircuit = pom.export_magnetic_as_subcircuit(magnetic)
+ * @endcode
+ *
+ * @warning Always call process_inputs() before adviser functions!
+ *
+ * @see advisers.h for design recommendation functions
+ * @see losses.h for loss calculations
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Simulation
+// ============================================================================
+// Simulation Functions
+// ============================================================================
+
+/**
+ * @brief Run complete magnetic simulation
+ *
+ * Performs full electromagnetic simulation including inductance calculation,
+ * field distribution, and performance metrics.
+ *
+ * @param inputsJson JSON object with operating points and conditions
+ * @param magneticJson JSON object with magnetic component specification
+ * @param modelsData JSON object specifying models to use
+ * @return JSON Mas object with simulation results in outputs
+ */
json simulate(json inputsJson, json magneticJson, json modelsData);
-// Export
+// ============================================================================
+// Export Functions
+// ============================================================================
+
+/**
+ * @brief Export magnetic as SPICE-compatible subcircuit
+ *
+ * Generates subcircuit netlist representation for circuit simulation
+ * tools like LTspice, ngspice, etc.
+ *
+ * @param magneticJson JSON object with magnetic component specification
+ * @return JSON string containing the subcircuit definition
+ */
ordered_json export_magnetic_as_subcircuit(json magneticJson);
-// Autocomplete
+// ============================================================================
+// Autocomplete Functions
+// ============================================================================
+
+/**
+ * @brief Autocomplete a partial Mas structure
+ *
+ * Fills in missing fields and calculates derived values.
+ *
+ * @param masJson Partial Mas JSON object to complete
+ * @param configuration Configuration options for autocomplete
+ * @return Complete Mas JSON object with all fields populated
+ */
json mas_autocomplete(json masJson, json configuration);
+
+/**
+ * @brief Autocomplete a partial Magnetic specification
+ *
+ * Fills in missing fields and calculates derived values for core and coil.
+ *
+ * @param magneticJson Partial Magnetic JSON object to complete
+ * @param configuration Configuration options for autocomplete
+ * @return Complete Magnetic JSON object with all fields populated
+ */
json magnetic_autocomplete(json magneticJson, json configuration);
-// Input processing
+// ============================================================================
+// Input Processing Functions
+// ============================================================================
+
+/**
+ * @brief Process raw input data and calculate derived quantities
+ *
+ * @warning CRITICAL: Must call this before using adviser functions!
+ *
+ * Calculates:
+ * - Harmonic content of waveforms (FFT analysis)
+ * - Processed waveform data (RMS, peak, offset)
+ * - Reconstructs waveforms from processed data if needed
+ *
+ * @param inputsJson Raw input JSON object with operating points
+ * @return Processed inputs JSON with harmonics and processed data
+ */
json process_inputs(json inputsJson);
+
+/**
+ * @brief Extract operating point from circuit simulation data
+ *
+ * @param fileJson JSON object with simulation file data
+ * @param numberWindings Number of windings in the magnetic
+ * @param frequency Operating frequency in Hz
+ * @param desiredMagnetizingInductance Target inductance value
+ * @param mapColumnNamesJson Mapping of column names to signals
+ * @return JSON OperatingPoint object
+ */
json extract_operating_point(json fileJson, size_t numberWindings, double frequency, double desiredMagnetizingInductance, json mapColumnNamesJson);
+
+/**
+ * @brief Extract column name mapping from circuit simulation file
+ *
+ * @param fileJson JSON object with simulation file data
+ * @param numberWindings Number of windings to map
+ * @param frequency Operating frequency for signal identification
+ * @return JSON array mapping signal types to column names
+ */
json extract_map_column_names(json fileJson, size_t numberWindings, double frequency);
+
+/**
+ * @brief Extract all column names from a circuit simulation file
+ * @param fileJson JSON object with simulation file data
+ * @return JSON array of column name strings
+ */
json extract_column_names(json fileJson);
-// Inductance calculations
+// ============================================================================
+// Inductance Functions
+// ============================================================================
+
+/**
+ * @brief Calculate complete inductance matrix
+ *
+ * Computes self inductances (diagonal) and mutual inductances (off-diagonal)
+ * at the specified frequency.
+ *
+ * @param magneticJson JSON object with magnetic component specification
+ * @param frequency Operating frequency in Hz
+ * @param modelsData JSON object specifying reluctance model
+ * @return JSON object with inductance matrix
+ */
json calculate_inductance_matrix(json magneticJson, double frequency, json modelsData);
+
+/**
+ * @brief Calculate leakage inductance between windings
+ *
+ * @param magneticJson JSON object with magnetic component specification
+ * @param frequency Operating frequency in Hz
+ * @param sourceIndex Index of the source winding (0-based)
+ * @return JSON object with leakage inductance values
+ */
json calculate_leakage_inductance(json magneticJson, double frequency, size_t sourceIndex);
-// Resistance calculations
+// ============================================================================
+// Resistance Functions
+// ============================================================================
+
+/**
+ * @brief Calculate DC resistance for each winding
+ *
+ * @param coilJson JSON object with coil specification
+ * @param temperature Temperature in Celsius
+ * @return JSON array with DC resistance per winding in Ohms
+ */
json calculate_dc_resistance_per_winding(json coilJson, double temperature);
+
+/**
+ * @brief Calculate frequency-dependent resistance matrix
+ *
+ * Uses the Spreen (1990) method with inductance ratio scaling.
+ *
+ * @param magneticJson JSON object with magnetic component specification
+ * @param temperature Temperature in Celsius
+ * @param frequency Operating frequency in Hz
+ * @return JSON object with resistance matrix
+ */
json calculate_resistance_matrix(json magneticJson, double temperature, double frequency);
-// Capacitance calculations
+// ============================================================================
+// Capacitance Functions
+// ============================================================================
+
+/**
+ * @brief Calculate stray capacitance for a coil
+ *
+ * Computes turn-to-turn and winding-to-winding capacitances.
+ *
+ * @param coilJson JSON object with coil specification
+ * @param operatingPointJson JSON object with operating conditions
+ * @param modelsData JSON object specifying capacitance model
+ * @return JSON object with capacitance values and Maxwell matrix
+ */
json calculate_stray_capacitance(json coilJson, json operatingPointJson, json modelsData);
+
+/**
+ * @brief Calculate Maxwell capacitance matrix
+ *
+ * Converts inter-winding capacitance values to Maxwell format.
+ *
+ * @param coilJson JSON object with coil specification
+ * @param capacitanceAmongWindingsJson JSON object with capacitance between windings
+ * @return JSON array containing the Maxwell capacitance matrix
+ */
json calculate_maxwell_capacitance_matrix(json coilJson, json capacitanceAmongWindingsJson);
+/**
+ * @brief Register simulation-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_simulation_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/utils.h b/src/utils.h
index 1d38d82..eecd387 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -1,28 +1,185 @@
+/**
+ * @file utils.h
+ * @brief Utility functions for PyOpenMagnetics
+ *
+ * Provides helper functions for signal processing, waveform analysis,
+ * power calculations, and data type conversions.
+ *
+ * ## Waveform Processing
+ * - calculate_basic_processed_data(): Extract RMS, peak, offset
+ * - calculate_harmonics(): FFT analysis for harmonic content
+ * - calculate_sampled_waveform(): Uniform resampling
+ *
+ * ## Power Calculations
+ * - calculate_instantaneous_power(): Point-by-point V×I
+ * - calculate_rms_power(): Vrms × Irms
+ *
+ * ## Transformer Reflections
+ * - calculate_reflected_secondary(): Primary to secondary side
+ * - calculate_reflected_primary(): Secondary to primary side
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Process a waveform
+ * waveform = {"data": [0, 1, 0, -1, 0], "time": [0, 0.25, 0.5, 0.75, 1.0]}
+ * processed = pom.calculate_basic_processed_data(waveform)
+ * print(f"RMS: {processed['rms']}, Peak: {processed['peak']}")
+ *
+ * # Calculate harmonics
+ * harmonics = pom.calculate_harmonics(waveform, 100000) # 100 kHz fundamental
+ *
+ * # Reflect excitation through transformer
+ * secondary = pom.calculate_reflected_secondary(primary_excitation, 10) # 10:1 ratio
+ * @endcode
+ *
+ * @see simulation.h for process_inputs() which uses these utilities
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Utility functions for processing and calculations
+// ============================================================================
+// Dimension Utilities
+// ============================================================================
+
+/**
+ * @brief Resolve a dimension specification with tolerances
+ *
+ * Extracts a single nominal value from dimension data that may contain
+ * nominal, minimum, and maximum values.
+ *
+ * @param dimensionWithToleranceJson JSON DimensionWithTolerance object
+ * @return Resolved dimension value as double
+ */
double resolve_dimension_with_tolerance(json dimensionWithToleranceJson);
+
+// ============================================================================
+// Waveform Processing Functions
+// ============================================================================
+
+/**
+ * @brief Calculate basic processed data from a waveform
+ *
+ * Extracts peak-to-peak, RMS, offset, peak, and other basic metrics.
+ *
+ * @param waveformJson JSON Waveform object with data and time arrays
+ * @return JSON Processed object with computed characteristics
+ */
json calculate_basic_processed_data(json waveformJson);
+
+/**
+ * @brief Calculate harmonic content of a waveform (FFT analysis)
+ *
+ * @param waveformJson JSON Waveform object with data and time arrays
+ * @param frequency Fundamental frequency in Hz
+ * @return JSON Harmonics object with amplitudes and frequencies
+ */
json calculate_harmonics(json waveformJson, double frequency);
+
+/**
+ * @brief Resample a waveform at uniform intervals
+ *
+ * Interpolates waveform data to create uniformly sampled points
+ * suitable for FFT analysis.
+ *
+ * @param waveformJson JSON Waveform object
+ * @param frequency Frequency for determining sample period
+ * @return JSON Waveform object with uniform sampling
+ */
json calculate_sampled_waveform(json waveformJson, double frequency);
+
+/**
+ * @brief Calculate complete processed data from a signal descriptor
+ *
+ * Computes RMS, peak, offset, effective frequency, and other metrics.
+ *
+ * @param signalDescriptorJson JSON SignalDescriptor object
+ * @param sampledWaveformJson JSON Waveform with uniform samples
+ * @param includeDcComponent Whether to include DC in calculations
+ * @return JSON Processed object with complete analysis
+ */
json calculate_processed_data(json signalDescriptorJson, json sampledWaveformJson, bool includeDcComponent);
-// Power calculation utilities
+// ============================================================================
+// Power Calculation Functions
+// ============================================================================
+
+/**
+ * @brief Calculate instantaneous power from excitation
+ *
+ * Computes point-by-point product of voltage and current waveforms.
+ *
+ * @param excitationJson JSON OperatingPointExcitation with voltage and current
+ * @return Instantaneous power value in Watts
+ */
double calculate_instantaneous_power(json excitationJson);
+
+/**
+ * @brief Calculate RMS (apparent) power from excitation
+ *
+ * Computes Vrms × Irms product.
+ *
+ * @param excitationJson JSON OperatingPointExcitation with voltage and current
+ * @return RMS power value in Watts (apparent power)
+ */
double calculate_rms_power(json excitationJson);
-// Reflection utilities
+// ============================================================================
+// Transformer Reflection Functions
+// ============================================================================
+
+/**
+ * @brief Calculate reflected secondary winding excitation
+ *
+ * Transforms primary winding excitation to secondary side using turns ratio.
+ * V_secondary = V_primary / n
+ * I_secondary = I_primary × n
+ *
+ * @param primaryExcitationJson JSON OperatingPointExcitation for primary
+ * @param turnRatio Primary to secondary turns ratio (Np/Ns)
+ * @return JSON OperatingPointExcitation for secondary side
+ */
json calculate_reflected_secondary(json primaryExcitationJson, double turnRatio);
+
+/**
+ * @brief Calculate reflected primary winding excitation
+ *
+ * Transforms secondary winding excitation to primary side using turns ratio.
+ * V_primary = V_secondary × n
+ * I_primary = I_secondary / n
+ *
+ * @param secondaryExcitationJson JSON OperatingPointExcitation for secondary
+ * @param turnRatio Primary to secondary turns ratio (Np/Ns)
+ * @return JSON OperatingPointExcitation for primary side
+ */
json calculate_reflected_primary(json secondaryExcitationJson, double turnRatio);
-// Array conversion utilities
+// ============================================================================
+// Array Conversion Functions
+// ============================================================================
+
+/**
+ * @brief Convert C++ vector of vectors to Python nested list
+ * @param arrayOfArrays C++ std::vector>
+ * @return Python list of lists
+ */
py::list list_of_list_to_python_list(std::vector> arrayOfArrays);
+
+/**
+ * @brief Convert Python list to C++ vector
+ * @param pythonList Python list of numeric values
+ * @return C++ std::vector
+ */
std::vector python_list_to_vector(py::list pythonList);
-// Register all utility bindings
+/**
+ * @brief Register utility-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_utils_bindings(py::module& m);
-} // namespace PyMKF
\ No newline at end of file
+} // namespace PyMKF
diff --git a/src/winding.h b/src/winding.h
index e0976a0..abfbe3b 100644
--- a/src/winding.h
+++ b/src/winding.h
@@ -1,39 +1,248 @@
+/**
+ * @file winding.h
+ * @brief Winding placement engine functions for PyOpenMagnetics
+ *
+ * Provides comprehensive functions for placing wires in magnetic component
+ * windings, organizing them into sections, layers, and individual turns.
+ * Supports various winding patterns including interleaved and sectored windings.
+ *
+ * ## Winding Process
+ * 1. wind_by_sections(): Divide winding window into sections
+ * 2. wind_by_layers(): Organize sections into layers
+ * 3. wind_by_turns(): Place individual turns within layers
+ * 4. delimit_and_compact(): Optimize turn positions
+ *
+ * Or use wind() for the complete process in one call.
+ *
+ * ## Winding Patterns
+ * - Contiguous: All turns of one winding together
+ * - Interleaved: Alternating primary/secondary sections
+ * - Sectored: Multiple sections with insulation between
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * coil = {
+ * "bobbin": bobbin_data,
+ * "functionalDescription": [
+ * {"name": "Primary", "numberTurns": 20, "wire": wire1},
+ * {"name": "Secondary", "numberTurns": 5, "wire": wire2}
+ * ]
+ * }
+ *
+ * # Wind with interleaved pattern (P-S-P-S)
+ * result = pom.wind(coil, 2, [0.5, 0.5], [0, 1], [[0.001, 0.001]])
+ * @endcode
+ *
+ * @see bobbin.h for bobbin specifications
+ * @see wire.h for wire specifications
+ * @see losses.h for winding loss calculations
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Winding functions
+// ============================================================================
+// Winding Functions
+// ============================================================================
+
+/**
+ * @brief Wind coils on a magnetic core (complete process)
+ *
+ * @param coilJson JSON object with bobbin and winding functional description
+ * @param repetitions Number of times to repeat the winding pattern
+ * @param proportionPerWindingJson Proportion of window for each winding
+ * @param patternJson Winding order pattern (e.g., [0, 1] for P-S-P-S)
+ * @param marginPairsJson Margin tape pairs [[left, right], ...]
+ * @return JSON Coil object with complete winding description
+ */
json wind(json coilJson, size_t repetitions, json proportionPerWindingJson, json patternJson, json marginPairsJson);
+
+/**
+ * @brief Wind planar (PCB) coils with layer stack-up
+ *
+ * @param coilJson JSON Coil specification
+ * @param stackUpJson Layer stack-up array [winding_index per layer]
+ * @param borderToWireDistance Clearance from board edge in meters
+ * @param wireToWireDistanceJson Spacing between traces per layer
+ * @param insulationThicknessJson Insulation between layer pairs
+ * @param coreToLayerDistance Gap between core and first layer
+ * @return JSON Coil object with planar winding arrangement
+ */
json wind_planar(json coilJson, json stackUpJson, double borderToWireDistance, json wireToWireDistanceJson, json insulationThicknessJson, double coreToLayerDistance);
+
+/**
+ * @brief Wind coil organized by sections only
+ *
+ * @param coilJson JSON Coil specification
+ * @param repetitions Pattern repetition count
+ * @param proportionPerWindingJson Window proportion per winding
+ * @param patternJson Winding order pattern
+ * @param insulationThickness Inter-section insulation in meters
+ * @return JSON Coil with sectionsDescription populated
+ */
json wind_by_sections(json coilJson, size_t repetitions, json proportionPerWindingJson, json patternJson, double insulationThickness);
+
+/**
+ * @brief Wind coil with layer-level detail from section description
+ *
+ * @param coilJson JSON Coil with sectionsDescription
+ * @param insulationLayersJson Insulation layer specs between windings
+ * @param insulationThickness Default insulation thickness in meters
+ * @return JSON Coil with layersDescription populated
+ */
json wind_by_layers(json coilJson, json insulationLayersJson, double insulationThickness);
+
+/**
+ * @brief Wind coil with turn-level detail from layer description
+ * @param coilJson JSON Coil with layersDescription
+ * @return JSON Coil with turnsDescription populated
+ */
json wind_by_turns(json coilJson);
+
+/**
+ * @brief Delimit and compact winding layout
+ *
+ * Optimizes turn positions within the winding window to minimize space usage.
+ *
+ * @param coilJson JSON Coil with complete winding description
+ * @return JSON Coil with optimized turn positions
+ */
json delimit_and_compact(json coilJson);
-// Layer and section functions
+// ============================================================================
+// Layer and Section Functions
+// ============================================================================
+
+/**
+ * @brief Get all layers belonging to a specific winding
+ * @param coilJson JSON Coil with layersDescription
+ * @param windingIndex Zero-based winding index
+ * @return JSON array of Layer objects for that winding
+ */
json get_layers_by_winding_index(json coilJson, int windingIndex);
+
+/**
+ * @brief Get all layers within a named section
+ * @param coilJson JSON Coil with layersDescription
+ * @param sectionName Name of the section
+ * @return JSON array of Layer objects in that section
+ */
json get_layers_by_section(json coilJson, json sectionName);
+
+/**
+ * @brief Get only conduction sections (excluding insulation sections)
+ * @param coilJson JSON Coil with sectionsDescription
+ * @return JSON array of Section objects with type "conduction"
+ */
json get_sections_description_conduction(json coilJson);
+
+/**
+ * @brief Check if all sections and layers fit within the winding window
+ * @param coilJson JSON Coil with winding description
+ * @return true if everything fits, false otherwise
+ */
bool are_sections_and_layers_fitting(json coilJson);
+
+/**
+ * @brief Add margin tape to a section
+ * @param coilJson JSON Coil specification
+ * @param sectionIndex Zero-based section index
+ * @param top_or_left_margin Top/left margin in meters
+ * @param bottom_or_right_margin Bottom/right margin in meters
+ * @return Updated JSON Coil with margin added
+ */
json add_margin_to_section_by_index(json coilJson, int sectionIndex, double top_or_left_margin, double bottom_or_right_margin);
-// Winding orientation and alignment
+// ============================================================================
+// Winding Orientation and Alignment
+// ============================================================================
+
+/**
+ * @brief Get list of available winding orientation options
+ * @return Vector of orientation strings: "contiguous", "overlapping"
+ */
std::vector get_available_winding_orientations();
+
+/**
+ * @brief Get list of available coil alignment options
+ * @return Vector of alignment strings: "inner or top", "outer or bottom", "spread", "centered"
+ */
std::vector get_available_coil_alignments();
-// Number of turns
+// ============================================================================
+// Number of Turns
+// ============================================================================
+
+/**
+ * @brief Calculate optimal number of turns for all windings
+ *
+ * Given primary turns and design requirements (turns ratios),
+ * calculates the turns for all windings.
+ *
+ * @param numberTurnsPrimary Number of primary turns
+ * @param designRequirementsJson JSON DesignRequirements with turnsRatios
+ * @return Vector of integer turns for each winding
+ */
std::vector calculate_number_turns(int numberTurnsPrimary, json designRequirementsJson);
-// Insulation
+// ============================================================================
+// Insulation Functions
+// ============================================================================
+
+/**
+ * @brief Get all available insulation materials
+ * @return JSON array of InsulationMaterial objects
+ */
json get_insulation_materials();
+
+/**
+ * @brief Get list of all insulation material names
+ * @return JSON array of material name strings
+ */
json get_insulation_material_names();
+
+/**
+ * @brief Find insulation material data by name
+ * @param insulationMaterialName Insulation material name
+ * @return JSON InsulationMaterial object
+ */
json find_insulation_material_by_name(json insulationMaterialName);
+
+/**
+ * @brief Calculate insulation requirements per safety standards
+ *
+ * Computes creepage, clearance, and dielectric requirements based on
+ * IEC 60664-1, IEC 61558-1, IEC 62368-1, or IEC 60335-1 standards.
+ *
+ * @param inputsJson JSON Inputs with insulation requirements
+ * @return JSON object with creepageDistance, clearance, withstandVoltage, etc.
+ */
json calculate_insulation(json inputsJson);
+
+/**
+ * @brief Get the insulation material used in a specific layer
+ * @param coilJson JSON Coil specification
+ * @param layerName Name of the insulation layer
+ * @return JSON InsulationMaterial object
+ */
json get_insulation_layer_insulation_material(json coilJson, std::string layerName);
+
+/**
+ * @brief Get isolation side designation from winding index
+ * @param index Winding index (0 = primary, 1+ = secondaries)
+ * @return JSON IsolationSide string
+ */
json get_isolation_side_from_index(size_t index);
+/**
+ * @brief Register winding-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_winding_bindings(py::module& m);
} // namespace PyMKF
diff --git a/src/wire.h b/src/wire.h
index c21e087..70ee813 100644
--- a/src/wire.h
+++ b/src/wire.h
@@ -1,51 +1,312 @@
+/**
+ * @file wire.h
+ * @brief Wire database and calculation functions for PyOpenMagnetics
+ *
+ * Provides comprehensive functions for querying wire databases, calculating
+ * wire dimensions with insulation, and selecting appropriate wires for designs.
+ *
+ * ## Wire Types
+ * - Round: Solid round magnet wire with enamel coating
+ * - Litz: Multi-strand twisted wire for reduced AC losses
+ * - Rectangular: Flat/strip wire for high current applications
+ * - Foil: Ultra-thin conductor for planar magnetics
+ *
+ * ## Wire Standards
+ * - IEC 60317: International standard for enamelled round wire
+ * - NEMA MW 1000: North American magnet wire standard
+ * - JIS C 3202: Japanese industrial standard
+ *
+ * ## Usage Example
+ * @code{.py}
+ * import PyOpenMagnetics as pom
+ *
+ * # Query wires
+ * wires = pom.get_wire_names()
+ * wire = pom.find_wire_by_name("Round 0.5 - Grade 1")
+ *
+ * # Get wire dimensions
+ * outer_diam = pom.get_wire_outer_diameter_enamelled_round(0.5e-3, 1, "IEC 60317")
+ *
+ * # Find equivalent wire for different application
+ * litz_equiv = pom.get_equivalent_wire(wire, "litz", 100000)
+ * @endcode
+ *
+ * @see winding.h for wire placement in coils
+ * @see losses.h for wire loss calculations
+ */
+
#pragma once
#include "common.h"
namespace PyMKF {
-// Wire retrieval
+// ============================================================================
+// Wire Retrieval Functions
+// ============================================================================
+
+/**
+ * @brief Get all available wires from the database
+ * @return JSON array of Wire objects with full specifications
+ */
json get_wires();
+
+/**
+ * @brief Get list of all wire names
+ * @return JSON array of wire name strings
+ */
json get_wire_names();
+
+/**
+ * @brief Get all available wire conductor materials
+ * @return JSON array of WireMaterial objects (copper, aluminum, etc.)
+ */
json get_wire_materials();
+
+/**
+ * @brief Get list of all wire material names
+ * @return JSON array of material name strings
+ */
json get_wire_material_names();
+
+/**
+ * @brief Find complete wire data by name
+ * @param wireName Wire name (e.g., "Round 0.5 - Grade 1")
+ * @return JSON Wire object with full specification
+ */
json find_wire_by_name(json wireName);
+
+/**
+ * @brief Find wire conductor material data by name
+ * @param wireMaterialName Material name (e.g., "copper", "aluminum")
+ * @return JSON WireMaterial object
+ */
json find_wire_material_by_name(json wireMaterialName);
+
+/**
+ * @brief Find wire by dimension, type, and standard
+ * @param dimension Target dimension in meters (diameter for round wire)
+ * @param wireTypeJson Wire type ("round", "litz", "rectangular", "foil")
+ * @param wireStandardJson Standard ("IEC 60317", "NEMA MW 1000", etc.)
+ * @return JSON Wire object matching criteria
+ */
json find_wire_by_dimension(double dimension, json wireTypeJson, json wireStandardJson);
-// Wire data functions
+// ============================================================================
+// Wire Data Functions
+// ============================================================================
+
+/**
+ * @brief Get complete wire data from winding specification
+ * @param windingDataJson JSON Winding object with wire field
+ * @return JSON Wire object with complete specification
+ */
json get_wire_data(json windingDataJson);
+
+/**
+ * @brief Get wire data by name
+ * @param name Wire name string
+ * @return JSON Wire object
+ */
json get_wire_data_by_name(std::string name);
+
+/**
+ * @brief Get wire data by standard designation
+ * @param standardName Standard wire size (e.g., "AWG 24", "0.50 mm")
+ * @return JSON Wire object
+ */
json get_wire_data_by_standard_name(std::string standardName);
+
+/**
+ * @brief Get strand wire data by standard name (for litz strands)
+ * @param standardName Strand size designation
+ * @return JSON Wire object for individual strand
+ */
json get_strand_by_standard_name(std::string standardName);
+
+/**
+ * @brief Get bare conductor diameter by standard name
+ * @param standardName Wire size designation
+ * @return Conducting diameter in meters
+ */
double get_wire_conducting_diameter_by_standard_name(std::string standardName);
-// Wire dimensions
+// ============================================================================
+// Wire Dimension Functions
+// ============================================================================
+
+/**
+ * @brief Get outer width of rectangular wire including insulation
+ * @param conductingWidth Conductor width in meters
+ * @param grade Insulation grade (1, 2, 3)
+ * @param wireStandardJson Wire standard
+ * @return Outer width in meters
+ */
double get_wire_outer_width_rectangular(double conductingWidth, int grade, json wireStandardJson);
+
+/**
+ * @brief Get outer height of rectangular wire including insulation
+ * @param conductingHeight Conductor height in meters
+ * @param grade Insulation grade (1, 2, 3)
+ * @param wireStandardJson Wire standard
+ * @return Outer height in meters
+ */
double get_wire_outer_height_rectangular(double conductingHeight, int grade, json wireStandardJson);
+
+/**
+ * @brief Get outer diameter of bare litz bundle (no serving)
+ * @param conductingDiameter Strand conductor diameter in meters
+ * @param numberConductors Number of strands in bundle
+ * @param grade Strand insulation grade
+ * @param wireStandardJson Wire standard
+ * @return Bundle diameter in meters
+ */
double get_wire_outer_diameter_bare_litz(double conductingDiameter, int numberConductors, int grade, json wireStandardJson);
+
+/**
+ * @brief Get outer diameter of litz wire with serving
+ * @param conductingDiameter Strand conductor diameter in meters
+ * @param numberConductors Number of strands in bundle
+ * @param grade Strand insulation grade
+ * @param numberLayers Number of serving layers
+ * @param wireStandardJson Wire standard
+ * @return Served diameter in meters
+ */
double get_wire_outer_diameter_served_litz(double conductingDiameter, int numberConductors, int grade, int numberLayers, json wireStandardJson);
+
+/**
+ * @brief Get outer diameter of fully insulated litz wire
+ * @param conductingDiameter Strand conductor diameter in meters
+ * @param numberConductors Number of strands in bundle
+ * @param numberLayers Number of insulation layers
+ * @param thicknessLayers Thickness per insulation layer in meters
+ * @param grade Strand insulation grade
+ * @param wireStandardJson Wire standard
+ * @return Insulated diameter in meters
+ */
double get_wire_outer_diameter_insulated_litz(double conductingDiameter, int numberConductors, int numberLayers, double thicknessLayers, int grade, json wireStandardJson);
+
+/**
+ * @brief Get outer diameter of enamelled round wire
+ * @param conductingDiameter Conductor diameter in meters
+ * @param grade Insulation grade (1, 2, 3)
+ * @param wireStandardJson Wire standard
+ * @return Outer diameter in meters
+ */
double get_wire_outer_diameter_enamelled_round(double conductingDiameter, int grade, json wireStandardJson);
+
+/**
+ * @brief Get outer diameter of insulated round wire
+ * @param conductingDiameter Conductor diameter in meters
+ * @param numberLayers Number of insulation layers
+ * @param thicknessLayers Thickness per layer in meters
+ * @param wireStandardJson Wire standard
+ * @return Outer diameter in meters
+ */
double get_wire_outer_diameter_insulated_round(double conductingDiameter, int numberLayers, double thicknessLayers, json wireStandardJson);
+
+/**
+ * @brief Get outer dimensions of any wire type
+ * @param wireJson JSON Wire object
+ * @return Vector [width, height] or [diameter] in meters
+ */
std::vector get_outer_dimensions(json wireJson);
-// Wire utilities
+// ============================================================================
+// Wire Utility Functions
+// ============================================================================
+
+/**
+ * @brief Get equivalent wire for comparison or substitution
+ * @param oldWireJson JSON Wire object to find equivalent for
+ * @param newWireTypeJson Target wire type
+ * @param effectivefrequency Operating frequency in Hz
+ * @return JSON Wire object representing equivalent
+ */
json get_equivalent_wire(json oldWireJson, json newWireTypeJson, double effectivefrequency);
+
+/**
+ * @brief Get coating/insulation data for a wire
+ * @param wireJson JSON Wire object
+ * @return JSON WireCoating object
+ */
json get_coating(json wireJson);
+
+/**
+ * @brief Get human-readable coating label
+ * @param wireJson JSON Wire object
+ * @return Coating label string
+ */
json get_coating_label(json wireJson);
+
+/**
+ * @brief Get wire coating specification by label
+ * @param label Coating label string
+ * @return JSON WireCoating object
+ */
json get_wire_coating_by_label(std::string label);
+
+/**
+ * @brief Get available coating labels for a wire type
+ * @param wireTypeJson Wire type
+ * @return Vector of available coating label strings
+ */
std::vector get_coating_labels_by_type(json wireTypeJson);
+
+/**
+ * @brief Get thickness of wire coating/insulation
+ * @param wireJson JSON Wire object
+ * @return Coating thickness in meters
+ */
double get_coating_thickness(json wireJson);
+
+/**
+ * @brief Get relative permittivity of coating
+ * @param wireJson JSON Wire object
+ * @return Relative permittivity (dimensionless)
+ */
double get_coating_relative_permittivity(json wireJson);
+
+/**
+ * @brief Get insulation material of wire coating
+ * @param wireJson JSON Wire object
+ * @return JSON InsulationMaterial object
+ */
json get_coating_insulation_material(json wireJson);
-// Wire availability
+// ============================================================================
+// Wire Availability Functions
+// ============================================================================
+
+/**
+ * @brief Get list of all available wire names
+ * @return Vector of wire name strings
+ */
std::vector get_available_wires();
+
+/**
+ * @brief Get list of unique wire diameters for a standard
+ * @param wireStandardJson Wire standard to query
+ * @return Vector of standard size designation strings
+ */
std::vector get_unique_wire_diameters(json wireStandardJson);
+
+/**
+ * @brief Get list of available wire types
+ * @return Vector of type strings: "round", "litz", "rectangular", "foil"
+ */
std::vector get_available_wire_types();
+
+/**
+ * @brief Get list of available wire standards
+ * @return Vector of standard strings: "IEC 60317", "NEMA MW 1000", etc.
+ */
std::vector get_available_wire_standards();
+/**
+ * @brief Register wire-related Python bindings
+ * @param m Reference to the pybind11 module
+ */
void register_wire_bindings(py::module& m);
} // namespace PyMKF
diff --git a/tas/__init__.py b/tas/__init__.py
new file mode 100644
index 0000000..0ccba95
--- /dev/null
+++ b/tas/__init__.py
@@ -0,0 +1,61 @@
+"""
+TAS (Topology Agnostic Structure) - Power electronics interchange format.
+
+A simplified format for basic DC-DC converter design exchange.
+Uses waveforms as the universal language for topology-agnostic design.
+"""
+
+from tas.models.waveforms import TASWaveform, WaveformShape
+from tas.models.components import (
+ TASInductor,
+ TASCapacitor,
+ TASSwitch,
+ TASDiode,
+ TASMagnetic,
+ TASComponentList,
+)
+from tas.models.inputs import (
+ TASInputs,
+ TASRequirements,
+ TASOperatingPoint,
+ TASModulation,
+ ModulationType,
+ ControlMode,
+ OperatingMode,
+)
+from tas.models.outputs import TASOutputs, TASLossBreakdown, TASKPIs
+from tas.models.tas_root import (
+ TASDocument,
+ TASMetadata,
+ create_buck_tas,
+ create_boost_tas,
+ create_flyback_tas,
+)
+
+__all__ = [
+ "TASDocument",
+ "TASMetadata",
+ "TASWaveform",
+ "WaveformShape",
+ "TASInductor",
+ "TASCapacitor",
+ "TASSwitch",
+ "TASDiode",
+ "TASMagnetic",
+ "TASComponentList",
+ "TASInputs",
+ "TASRequirements",
+ "TASOperatingPoint",
+ "TASModulation",
+ "ModulationType",
+ "ControlMode",
+ "OperatingMode",
+ "TASOutputs",
+ "TASLossBreakdown",
+ "TASKPIs",
+ "create_buck_tas",
+ "create_boost_tas",
+ "create_flyback_tas",
+]
+
+__version__ = "0.1.0"
diff --git a/tas/examples/boost_5v_to_12v.json b/tas/examples/boost_5v_to_12v.json
new file mode 100644
index 0000000..28b7505
--- /dev/null
+++ b/tas/examples/boost_5v_to_12v.json
@@ -0,0 +1,101 @@
+{
+ "metadata": {
+ "name": "Boost 5V to 12V @ 1A",
+ "version": "0.1.0",
+ "description": "Basic boost converter for 5V to 12V conversion",
+ "author": "TAS Generator"
+ },
+ "inputs": {
+ "requirements": {
+ "v_in_min": 4.5,
+ "v_in_max": 5.5,
+ "v_out": 12.0,
+ "i_out_max": 1.0,
+ "efficiency_target": 0.90
+ },
+ "operating_points": [
+ {
+ "name": "full_load",
+ "frequency": 300000,
+ "duty_cycle": 0.583,
+ "mode": "ccm",
+ "ambient_temperature": 25.0,
+ "modulation": {
+ "type": "pwm",
+ "control_mode": "current_mode",
+ "frequency_fixed": true,
+ "max_duty": 0.85
+ },
+ "waveforms": {
+ "inductor_current": {
+ "data": [2.2, 2.8, 2.2],
+ "time": [0.0, 1.944e-6, 3.33e-6],
+ "shape": "triangular",
+ "unit": "A"
+ },
+ "switch_voltage": {
+ "data": [0.0, 0.0, 12.0, 12.0],
+ "time": [0.0, 1.944e-6, 1.944e-6, 3.33e-6],
+ "shape": "rectangular",
+ "unit": "V"
+ }
+ }
+ }
+ ]
+ },
+ "components": {
+ "inductors": [
+ {
+ "name": "L1",
+ "type": "inductor",
+ "inductance": 47e-6,
+ "dcr": 0.03,
+ "saturation_current": 4.0,
+ "description": "Main boost inductor"
+ }
+ ],
+ "capacitors": [
+ {
+ "name": "C_out",
+ "type": "capacitor",
+ "capacitance": 47e-6,
+ "esr": 0.02,
+ "voltage_rating": 16.0,
+ "description": "Output capacitor"
+ }
+ ],
+ "switches": [
+ {
+ "name": "Q1",
+ "type": "switch",
+ "rds_on": 0.02,
+ "v_ds_max": 20.0,
+ "i_d_max": 5.0,
+ "description": "Main switch"
+ }
+ ],
+ "diodes": [
+ {
+ "name": "D1",
+ "type": "diode",
+ "vf": 0.4,
+ "v_rrm": 20.0,
+ "description": "Boost diode"
+ }
+ ]
+ },
+ "outputs": {
+ "losses": {
+ "core_loss": 0.08,
+ "winding_loss": 0.15,
+ "switch_conduction": 0.14,
+ "switch_switching": 0.20,
+ "diode_conduction": 0.40,
+ "capacitor_esr": 0.02,
+ "total": 0.99
+ },
+ "kpis": {
+ "efficiency": 0.924
+ }
+ }
+}
diff --git a/tas/examples/buck_12v_to_5v.json b/tas/examples/buck_12v_to_5v.json
new file mode 100644
index 0000000..e9697b7
--- /dev/null
+++ b/tas/examples/buck_12v_to_5v.json
@@ -0,0 +1,101 @@
+{
+ "metadata": {
+ "name": "Buck 12V to 5V @ 3A",
+ "version": "0.1.0",
+ "description": "Basic buck converter for 12V to 5V conversion",
+ "author": "TAS Generator"
+ },
+ "inputs": {
+ "requirements": {
+ "v_in_min": 10.0,
+ "v_in_max": 14.0,
+ "v_out": 5.0,
+ "i_out_max": 3.0,
+ "efficiency_target": 0.92
+ },
+ "operating_points": [
+ {
+ "name": "full_load",
+ "frequency": 500000,
+ "duty_cycle": 0.417,
+ "mode": "ccm",
+ "ambient_temperature": 25.0,
+ "modulation": {
+ "type": "pwm",
+ "control_mode": "voltage_mode",
+ "frequency_fixed": true,
+ "max_duty": 0.9
+ },
+ "waveforms": {
+ "inductor_current": {
+ "data": [2.7, 3.3, 2.7],
+ "time": [0.0, 8.33e-7, 2.0e-6],
+ "shape": "triangular",
+ "unit": "A"
+ },
+ "switch_voltage": {
+ "data": [0.0, 0.0, 12.0, 12.0],
+ "time": [0.0, 8.33e-7, 8.33e-7, 2.0e-6],
+ "shape": "rectangular",
+ "unit": "V"
+ }
+ }
+ }
+ ]
+ },
+ "components": {
+ "inductors": [
+ {
+ "name": "L1",
+ "type": "inductor",
+ "inductance": 22e-6,
+ "dcr": 0.015,
+ "saturation_current": 5.0,
+ "description": "Main buck inductor"
+ }
+ ],
+ "capacitors": [
+ {
+ "name": "C_out",
+ "type": "capacitor",
+ "capacitance": 100e-6,
+ "esr": 0.01,
+ "voltage_rating": 10.0,
+ "description": "Output capacitor"
+ }
+ ],
+ "switches": [
+ {
+ "name": "Q1",
+ "type": "switch",
+ "rds_on": 0.01,
+ "v_ds_max": 30.0,
+ "i_d_max": 10.0,
+ "description": "High-side switch"
+ }
+ ],
+ "diodes": [
+ {
+ "name": "D1",
+ "type": "diode",
+ "vf": 0.5,
+ "v_rrm": 30.0,
+ "description": "Freewheeling diode"
+ }
+ ]
+ },
+ "outputs": {
+ "losses": {
+ "core_loss": 0.05,
+ "winding_loss": 0.12,
+ "switch_conduction": 0.09,
+ "switch_switching": 0.15,
+ "diode_conduction": 0.25,
+ "capacitor_esr": 0.01,
+ "total": 0.67
+ },
+ "kpis": {
+ "efficiency": 0.957
+ }
+ }
+}
diff --git a/tas/examples/buck_boost_inverting.json b/tas/examples/buck_boost_inverting.json
new file mode 100644
index 0000000..9ca0e30
--- /dev/null
+++ b/tas/examples/buck_boost_inverting.json
@@ -0,0 +1,95 @@
+{
+ "metadata": {
+ "name": "Buck-Boost Inverting 12V to -5V @ 0.5A",
+ "version": "0.1.0",
+ "description": "Inverting buck-boost converter for negative rail generation",
+ "author": "TAS Generator"
+ },
+ "inputs": {
+ "requirements": {
+ "v_in_min": 10.0,
+ "v_in_max": 14.0,
+ "v_out": -5.0,
+ "i_out_max": 0.5,
+ "efficiency_target": 0.85
+ },
+ "operating_points": [
+ {
+ "name": "full_load",
+ "frequency": 250000,
+ "duty_cycle": 0.294,
+ "mode": "ccm",
+ "ambient_temperature": 25.0,
+ "modulation": {
+ "type": "pwm",
+ "control_mode": "voltage_mode",
+ "frequency_fixed": true,
+ "max_duty": 0.8
+ },
+ "waveforms": {
+ "inductor_current": {
+ "data": [0.35, 0.65, 0.35],
+ "time": [0.0, 1.176e-6, 4.0e-6],
+ "shape": "triangular",
+ "unit": "A"
+ }
+ }
+ }
+ ]
+ },
+ "components": {
+ "inductors": [
+ {
+ "name": "L1",
+ "type": "inductor",
+ "inductance": 100e-6,
+ "dcr": 0.05,
+ "saturation_current": 2.0,
+ "description": "Main inductor"
+ }
+ ],
+ "capacitors": [
+ {
+ "name": "C_out",
+ "type": "capacitor",
+ "capacitance": 22e-6,
+ "esr": 0.03,
+ "voltage_rating": 10.0,
+ "description": "Output capacitor"
+ }
+ ],
+ "switches": [
+ {
+ "name": "Q1",
+ "type": "switch",
+ "rds_on": 0.03,
+ "v_ds_max": 25.0,
+ "i_d_max": 3.0,
+ "description": "Main switch"
+ }
+ ],
+ "diodes": [
+ {
+ "name": "D1",
+ "type": "diode",
+ "vf": 0.5,
+ "v_rrm": 25.0,
+ "description": "Output diode"
+ }
+ ]
+ },
+ "outputs": {
+ "losses": {
+ "core_loss": 0.03,
+ "winding_loss": 0.05,
+ "switch_conduction": 0.04,
+ "switch_switching": 0.08,
+ "diode_conduction": 0.15,
+ "capacitor_esr": 0.01,
+ "total": 0.36
+ },
+ "kpis": {
+ "efficiency": 0.874
+ }
+ }
+}
diff --git a/tas/examples/flyback_48v_to_12v.json b/tas/examples/flyback_48v_to_12v.json
new file mode 100644
index 0000000..edd3f80
--- /dev/null
+++ b/tas/examples/flyback_48v_to_12v.json
@@ -0,0 +1,116 @@
+{
+ "metadata": {
+ "name": "Flyback 48V to 12V @ 2A Isolated",
+ "version": "0.1.0",
+ "description": "Isolated flyback converter for telecom applications",
+ "author": "TAS Generator"
+ },
+ "inputs": {
+ "requirements": {
+ "v_in_min": 36.0,
+ "v_in_max": 60.0,
+ "v_out": 12.0,
+ "i_out_max": 2.0,
+ "efficiency_target": 0.88,
+ "isolation_voltage": 1500.0
+ },
+ "operating_points": [
+ {
+ "name": "full_load",
+ "frequency": 100000,
+ "duty_cycle": 0.35,
+ "mode": "ccm",
+ "ambient_temperature": 25.0,
+ "modulation": {
+ "type": "pwm",
+ "control_mode": "current_mode",
+ "frequency_fixed": true,
+ "max_duty": 0.5
+ },
+ "waveforms": {
+ "primary_current": {
+ "data": [0.4, 1.2, 0.0, 0.0, 0.4],
+ "time": [0.0, 3.5e-6, 3.5e-6, 1.0e-5, 1.0e-5],
+ "shape": "custom",
+ "unit": "A"
+ },
+ "secondary_current": {
+ "data": [0.0, 0.0, 4.8, 1.6, 0.0],
+ "time": [0.0, 3.5e-6, 3.5e-6, 1.0e-5, 1.0e-5],
+ "shape": "custom",
+ "unit": "A"
+ }
+ }
+ },
+ {
+ "name": "light_load",
+ "frequency": 100000,
+ "duty_cycle": 0.15,
+ "mode": "dcm",
+ "ambient_temperature": 25.0,
+ "modulation": {
+ "type": "pfm",
+ "control_mode": "current_mode",
+ "frequency_fixed": false
+ }
+ }
+ ]
+ },
+ "components": {
+ "magnetics": [
+ {
+ "name": "T1",
+ "type": "magnetic",
+ "magnetizing_inductance": 200e-6,
+ "leakage_inductance": 2e-6,
+ "turns_ratio": 4.0,
+ "core_material": "N87",
+ "core_shape": "E 25/13/7",
+ "description": "Flyback transformer"
+ }
+ ],
+ "capacitors": [
+ {
+ "name": "C_out",
+ "type": "capacitor",
+ "capacitance": 220e-6,
+ "esr": 0.015,
+ "voltage_rating": 25.0,
+ "description": "Output capacitor"
+ }
+ ],
+ "switches": [
+ {
+ "name": "Q1",
+ "type": "switch",
+ "rds_on": 0.08,
+ "v_ds_max": 100.0,
+ "i_d_max": 3.0,
+ "description": "Primary switch"
+ }
+ ],
+ "diodes": [
+ {
+ "name": "D1",
+ "type": "diode",
+ "vf": 0.5,
+ "v_rrm": 40.0,
+ "description": "Secondary rectifier"
+ }
+ ]
+ },
+ "outputs": {
+ "losses": {
+ "core_loss": 0.35,
+ "winding_loss": 0.45,
+ "switch_conduction": 0.12,
+ "switch_switching": 0.40,
+ "diode_conduction": 0.60,
+ "capacitor_esr": 0.05,
+ "total": 1.97
+ },
+ "kpis": {
+ "efficiency": 0.924
+ }
+ }
+}
diff --git a/tas/models/__init__.py b/tas/models/__init__.py
new file mode 100644
index 0000000..6e8e69a
--- /dev/null
+++ b/tas/models/__init__.py
@@ -0,0 +1,54 @@
+"""TAS models for basic DC-DC converter interchange."""
+
+from tas.models.waveforms import TASWaveform, WaveformShape
+from tas.models.components import (
+ TASInductor,
+ TASCapacitor,
+ TASSwitch,
+ TASDiode,
+ TASMagnetic,
+ TASComponentList,
+)
+from tas.models.inputs import (
+ TASInputs,
+ TASRequirements,
+ TASOperatingPoint,
+ TASModulation,
+ ModulationType,
+ ControlMode,
+ OperatingMode,
+)
+from tas.models.outputs import TASOutputs, TASLossBreakdown, TASKPIs
+from tas.models.tas_root import (
+ TASDocument,
+ TASMetadata,
+ create_buck_tas,
+ create_boost_tas,
+ create_flyback_tas,
+)
+
+__all__ = [
+ "TASDocument",
+ "TASMetadata",
+ "TASWaveform",
+ "WaveformShape",
+ "TASInductor",
+ "TASCapacitor",
+ "TASSwitch",
+ "TASDiode",
+ "TASMagnetic",
+ "TASComponentList",
+ "TASInputs",
+ "TASRequirements",
+ "TASOperatingPoint",
+ "TASModulation",
+ "ModulationType",
+ "ControlMode",
+ "OperatingMode",
+ "TASOutputs",
+ "TASLossBreakdown",
+ "TASKPIs",
+ "create_buck_tas",
+ "create_boost_tas",
+ "create_flyback_tas",
+]
diff --git a/tas/models/components.py b/tas/models/components.py
new file mode 100644
index 0000000..7e7710b
--- /dev/null
+++ b/tas/models/components.py
@@ -0,0 +1,237 @@
+"""
+TAS Component models - Simple component definitions for basic DC-DC converters.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional, List, Dict, Any
+from enum import Enum
+
+
+class ComponentType(Enum):
+ """Component type enumeration."""
+ INDUCTOR = "inductor"
+ CAPACITOR = "capacitor"
+ SWITCH = "switch"
+ DIODE = "diode"
+ MAGNETIC = "magnetic"
+
+
+@dataclass
+class TASInductor:
+ """Inductor component."""
+ name: str
+ inductance: float = 0.0 # H
+ dcr: float = 0.0 # Ohms
+ saturation_current: Optional[float] = None # A
+ core_material: Optional[str] = None
+ core_shape: Optional[str] = None
+ description: str = ""
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "type": "inductor",
+ "inductance": self.inductance,
+ "dcr": self.dcr,
+ }
+ if self.saturation_current is not None:
+ result["saturation_current"] = self.saturation_current
+ if self.core_material:
+ result["core_material"] = self.core_material
+ if self.core_shape:
+ result["core_shape"] = self.core_shape
+ if self.description:
+ result["description"] = self.description
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASInductor":
+ return cls(
+ name=d.get("name", ""),
+ inductance=d.get("inductance", 0.0),
+ dcr=d.get("dcr", 0.0),
+ saturation_current=d.get("saturation_current"),
+ core_material=d.get("core_material"),
+ core_shape=d.get("core_shape"),
+ description=d.get("description", ""),
+ )
+
+
+@dataclass
+class TASCapacitor:
+ """Capacitor component."""
+ name: str
+ capacitance: float = 0.0 # F
+ esr: float = 0.0 # Ohms
+ voltage_rating: Optional[float] = None # V
+ description: str = ""
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "type": "capacitor",
+ "capacitance": self.capacitance,
+ "esr": self.esr,
+ }
+ if self.voltage_rating is not None:
+ result["voltage_rating"] = self.voltage_rating
+ if self.description:
+ result["description"] = self.description
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASCapacitor":
+ return cls(
+ name=d.get("name", ""),
+ capacitance=d.get("capacitance", 0.0),
+ esr=d.get("esr", 0.0),
+ voltage_rating=d.get("voltage_rating"),
+ description=d.get("description", ""),
+ )
+
+
+@dataclass
+class TASSwitch:
+ """Switch component (MOSFET)."""
+ name: str
+ rds_on: float = 0.0 # Ohms
+ v_ds_max: Optional[float] = None # V
+ i_d_max: Optional[float] = None # A
+ qg_total: Optional[float] = None # C
+ description: str = ""
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "type": "switch",
+ "rds_on": self.rds_on,
+ }
+ if self.v_ds_max is not None:
+ result["v_ds_max"] = self.v_ds_max
+ if self.i_d_max is not None:
+ result["i_d_max"] = self.i_d_max
+ if self.qg_total is not None:
+ result["qg_total"] = self.qg_total
+ if self.description:
+ result["description"] = self.description
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASSwitch":
+ return cls(
+ name=d.get("name", ""),
+ rds_on=d.get("rds_on", 0.0),
+ v_ds_max=d.get("v_ds_max"),
+ i_d_max=d.get("i_d_max"),
+ qg_total=d.get("qg_total"),
+ description=d.get("description", ""),
+ )
+
+
+@dataclass
+class TASDiode:
+ """Diode component."""
+ name: str
+ vf: float = 0.0 # V
+ v_rrm: Optional[float] = None # V
+ description: str = ""
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "type": "diode",
+ "vf": self.vf,
+ }
+ if self.v_rrm is not None:
+ result["v_rrm"] = self.v_rrm
+ if self.description:
+ result["description"] = self.description
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASDiode":
+ return cls(
+ name=d.get("name", ""),
+ vf=d.get("vf", 0.0),
+ v_rrm=d.get("v_rrm"),
+ description=d.get("description", ""),
+ )
+
+
+@dataclass
+class TASMagnetic:
+ """Magnetic component (transformer)."""
+ name: str
+ magnetizing_inductance: float = 0.0 # H
+ leakage_inductance: float = 0.0 # H
+ turns_ratio: float = 1.0
+ core_material: Optional[str] = None
+ core_shape: Optional[str] = None
+ description: str = ""
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "type": "magnetic",
+ "magnetizing_inductance": self.magnetizing_inductance,
+ "leakage_inductance": self.leakage_inductance,
+ "turns_ratio": self.turns_ratio,
+ }
+ if self.core_material:
+ result["core_material"] = self.core_material
+ if self.core_shape:
+ result["core_shape"] = self.core_shape
+ if self.description:
+ result["description"] = self.description
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASMagnetic":
+ return cls(
+ name=d.get("name", ""),
+ magnetizing_inductance=d.get("magnetizing_inductance", 0.0),
+ leakage_inductance=d.get("leakage_inductance", 0.0),
+ turns_ratio=d.get("turns_ratio", 1.0),
+ core_material=d.get("core_material"),
+ core_shape=d.get("core_shape"),
+ description=d.get("description", ""),
+ )
+
+
+@dataclass
+class TASComponentList:
+ """Container for all components."""
+ inductors: List[TASInductor] = field(default_factory=list)
+ capacitors: List[TASCapacitor] = field(default_factory=list)
+ switches: List[TASSwitch] = field(default_factory=list)
+ diodes: List[TASDiode] = field(default_factory=list)
+ magnetics: List[TASMagnetic] = field(default_factory=list)
+
+ def to_dict(self) -> dict:
+ result = {}
+ if self.inductors:
+ result["inductors"] = [c.to_dict() for c in self.inductors]
+ if self.capacitors:
+ result["capacitors"] = [c.to_dict() for c in self.capacitors]
+ if self.switches:
+ result["switches"] = [c.to_dict() for c in self.switches]
+ if self.diodes:
+ result["diodes"] = [c.to_dict() for c in self.diodes]
+ if self.magnetics:
+ result["magnetics"] = [c.to_dict() for c in self.magnetics]
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASComponentList":
+ return cls(
+ inductors=[TASInductor.from_dict(c) for c in d.get("inductors", [])],
+ capacitors=[TASCapacitor.from_dict(c) for c in d.get("capacitors", [])],
+ switches=[TASSwitch.from_dict(c) for c in d.get("switches", [])],
+ diodes=[TASDiode.from_dict(c) for c in d.get("diodes", [])],
+ magnetics=[TASMagnetic.from_dict(c) for c in d.get("magnetics", [])],
+ )
+
+ @property
+ def all_components(self) -> list:
+ """Get all components as a flat list."""
+ return self.inductors + self.capacitors + self.switches + self.diodes + self.magnetics
diff --git a/tas/models/inputs.py b/tas/models/inputs.py
new file mode 100644
index 0000000..ed76245
--- /dev/null
+++ b/tas/models/inputs.py
@@ -0,0 +1,178 @@
+"""
+TAS Inputs models - Requirements and operating points for basic DC-DC converters.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional, List, Dict
+from enum import Enum
+
+from tas.models.waveforms import TASWaveform
+
+
+class OperatingMode(Enum):
+ """Converter operating mode."""
+ CCM = "ccm" # Continuous conduction mode
+ DCM = "dcm" # Discontinuous conduction mode
+ BCM = "bcm" # Boundary conduction mode
+
+
+class ModulationType(Enum):
+ """Basic modulation schemes for DC-DC converters."""
+ PWM = "pwm" # Pulse Width Modulation (fixed frequency)
+ PFM = "pfm" # Pulse Frequency Modulation (variable frequency)
+ HYSTERETIC = "hysteretic" # Hysteretic control
+
+
+class ControlMode(Enum):
+ """Control loop architecture."""
+ VOLTAGE_MODE = "voltage_mode"
+ CURRENT_MODE = "current_mode"
+
+
+@dataclass
+class TASModulation:
+ """
+ Modulation and control specification for DC-DC converters.
+
+ The modulation type fundamentally affects waveform behavior.
+ """
+ type: ModulationType = ModulationType.PWM
+ control_mode: ControlMode = ControlMode.VOLTAGE_MODE
+ frequency_fixed: bool = True
+ max_duty: float = 0.9
+ min_duty: float = 0.0
+
+ def to_dict(self) -> dict:
+ return {
+ "type": self.type.value,
+ "control_mode": self.control_mode.value,
+ "frequency_fixed": self.frequency_fixed,
+ "max_duty": self.max_duty,
+ "min_duty": self.min_duty,
+ }
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASModulation":
+ return cls(
+ type=ModulationType(d["type"]) if d.get("type") else ModulationType.PWM,
+ control_mode=ControlMode(d["control_mode"]) if d.get("control_mode") else ControlMode.VOLTAGE_MODE,
+ frequency_fixed=d.get("frequency_fixed", True),
+ max_duty=d.get("max_duty", 0.9),
+ min_duty=d.get("min_duty", 0.0),
+ )
+
+
+@dataclass
+class TASRequirements:
+ """Electrical requirements for the converter."""
+ v_in_min: float = 0.0
+ v_in_max: float = 0.0
+ v_in_nominal: Optional[float] = None
+ v_out: float = 0.0
+ i_out_max: float = 0.0
+ p_out_max: Optional[float] = None
+ efficiency_target: float = 0.9
+ isolation_voltage: Optional[float] = None # V, None = non-isolated
+
+ def to_dict(self) -> dict:
+ result = {
+ "v_in_min": self.v_in_min,
+ "v_in_max": self.v_in_max,
+ "v_out": self.v_out,
+ "i_out_max": self.i_out_max,
+ "efficiency_target": self.efficiency_target,
+ }
+ if self.v_in_nominal is not None:
+ result["v_in_nominal"] = self.v_in_nominal
+ if self.p_out_max is not None:
+ result["p_out_max"] = self.p_out_max
+ if self.isolation_voltage is not None:
+ result["isolation_voltage"] = self.isolation_voltage
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASRequirements":
+ return cls(
+ v_in_min=d.get("v_in_min", 0.0),
+ v_in_max=d.get("v_in_max", 0.0),
+ v_in_nominal=d.get("v_in_nominal"),
+ v_out=d.get("v_out", 0.0),
+ i_out_max=d.get("i_out_max", 0.0),
+ p_out_max=d.get("p_out_max"),
+ efficiency_target=d.get("efficiency_target", 0.9),
+ isolation_voltage=d.get("isolation_voltage"),
+ )
+
+
+@dataclass
+class TASOperatingPoint:
+ """
+ Operating point with waveform-based excitations.
+
+ The modulation field captures how the converter is controlled,
+ which fundamentally affects the waveform shapes.
+ """
+ name: str = "nominal"
+ description: str = ""
+ frequency: float = 100e3 # Hz
+ duty_cycle: Optional[float] = None # 0-1
+ mode: OperatingMode = OperatingMode.CCM
+ modulation: Optional[TASModulation] = None
+ waveforms: Dict[str, TASWaveform] = field(default_factory=dict)
+ ambient_temperature: float = 25.0 # C
+
+ def to_dict(self) -> dict:
+ result = {
+ "name": self.name,
+ "frequency": self.frequency,
+ "mode": self.mode.value,
+ "ambient_temperature": self.ambient_temperature,
+ }
+ if self.description:
+ result["description"] = self.description
+ if self.duty_cycle is not None:
+ result["duty_cycle"] = self.duty_cycle
+ if self.modulation:
+ result["modulation"] = self.modulation.to_dict()
+ if self.waveforms:
+ result["waveforms"] = {k: v.to_dict() for k, v in self.waveforms.items()}
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASOperatingPoint":
+ mode = OperatingMode(d["mode"]) if d.get("mode") else OperatingMode.CCM
+ modulation = TASModulation.from_dict(d["modulation"]) if d.get("modulation") else None
+ waveforms = {k: TASWaveform.from_dict(v) for k, v in d.get("waveforms", {}).items()}
+ return cls(
+ name=d.get("name", "nominal"),
+ description=d.get("description", ""),
+ frequency=d.get("frequency", 100e3),
+ duty_cycle=d.get("duty_cycle"),
+ mode=mode,
+ modulation=modulation,
+ waveforms=waveforms,
+ ambient_temperature=d.get("ambient_temperature", 25.0),
+ )
+
+
+@dataclass
+class TASInputs:
+ """Complete inputs section of a TAS document."""
+ requirements: TASRequirements = field(default_factory=TASRequirements)
+ operating_points: List[TASOperatingPoint] = field(default_factory=list)
+
+ def to_dict(self) -> dict:
+ result = {"requirements": self.requirements.to_dict()}
+ if self.operating_points:
+ result["operating_points"] = [op.to_dict() for op in self.operating_points]
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASInputs":
+ return cls(
+ requirements=TASRequirements.from_dict(d.get("requirements", {})),
+ operating_points=[
+ TASOperatingPoint.from_dict(op)
+ for op in d.get("operating_points", [])
+ ],
+ )
diff --git a/tas/models/outputs.py b/tas/models/outputs.py
new file mode 100644
index 0000000..a500471
--- /dev/null
+++ b/tas/models/outputs.py
@@ -0,0 +1,96 @@
+"""
+TAS Output models - Simplified results for basic DC-DC converters.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional, Dict
+
+
+@dataclass
+class TASLossBreakdown:
+ """Loss breakdown by component."""
+ core_loss: float = 0.0 # W
+ winding_loss: float = 0.0 # W
+ switch_conduction: float = 0.0 # W
+ switch_switching: float = 0.0 # W
+ diode_conduction: float = 0.0 # W
+ capacitor_esr: float = 0.0 # W
+
+ @property
+ def total(self) -> float:
+ """Total losses."""
+ return (self.core_loss + self.winding_loss +
+ self.switch_conduction + self.switch_switching +
+ self.diode_conduction + self.capacitor_esr)
+
+ def to_dict(self) -> dict:
+ return {
+ "core_loss": self.core_loss,
+ "winding_loss": self.winding_loss,
+ "switch_conduction": self.switch_conduction,
+ "switch_switching": self.switch_switching,
+ "diode_conduction": self.diode_conduction,
+ "capacitor_esr": self.capacitor_esr,
+ "total": self.total,
+ }
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASLossBreakdown":
+ return cls(
+ core_loss=d.get("core_loss", 0.0),
+ winding_loss=d.get("winding_loss", 0.0),
+ switch_conduction=d.get("switch_conduction", 0.0),
+ switch_switching=d.get("switch_switching", 0.0),
+ diode_conduction=d.get("diode_conduction", 0.0),
+ capacitor_esr=d.get("capacitor_esr", 0.0),
+ )
+
+
+@dataclass
+class TASKPIs:
+ """Key performance indicators."""
+ efficiency: float = 0.0 # 0-1
+ power_density: Optional[float] = None # W/cm³
+ cost: Optional[float] = None # USD
+
+ def to_dict(self) -> dict:
+ result = {"efficiency": self.efficiency}
+ if self.power_density is not None:
+ result["power_density"] = self.power_density
+ if self.cost is not None:
+ result["cost"] = self.cost
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASKPIs":
+ return cls(
+ efficiency=d.get("efficiency", 0.0),
+ power_density=d.get("power_density"),
+ cost=d.get("cost"),
+ )
+
+
+@dataclass
+class TASOutputs:
+ """Simplified outputs section."""
+ losses: Optional[TASLossBreakdown] = None
+ kpis: Optional[TASKPIs] = None
+ notes: str = ""
+
+ def to_dict(self) -> dict:
+ result = {}
+ if self.losses:
+ result["losses"] = self.losses.to_dict()
+ if self.kpis:
+ result["kpis"] = self.kpis.to_dict()
+ if self.notes:
+ result["notes"] = self.notes
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASOutputs":
+ return cls(
+ losses=TASLossBreakdown.from_dict(d["losses"]) if d.get("losses") else None,
+ kpis=TASKPIs.from_dict(d["kpis"]) if d.get("kpis") else None,
+ notes=d.get("notes", ""),
+ )
diff --git a/tas/models/tas_root.py b/tas/models/tas_root.py
new file mode 100644
index 0000000..df32bac
--- /dev/null
+++ b/tas/models/tas_root.py
@@ -0,0 +1,223 @@
+"""
+TAS Root document - Simplified format for basic DC-DC converters.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional
+from datetime import datetime
+
+from tas.models.inputs import TASInputs
+from tas.models.outputs import TASOutputs
+from tas.models.components import TASComponentList
+
+
+@dataclass
+class TASMetadata:
+ """Document metadata."""
+ name: str = ""
+ version: str = "0.1.0"
+ description: str = ""
+ author: str = ""
+ created: str = ""
+ modified: str = ""
+
+ def to_dict(self) -> dict:
+ result = {"name": self.name, "version": self.version}
+ if self.description:
+ result["description"] = self.description
+ if self.author:
+ result["author"] = self.author
+ if self.created:
+ result["created"] = self.created
+ if self.modified:
+ result["modified"] = self.modified
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASMetadata":
+ return cls(
+ name=d.get("name", ""),
+ version=d.get("version", "0.1.0"),
+ description=d.get("description", ""),
+ author=d.get("author", ""),
+ created=d.get("created", ""),
+ modified=d.get("modified", ""),
+ )
+
+
+@dataclass
+class TASDocument:
+ """
+ Simplified TAS document for basic DC-DC converters.
+
+ Structure:
+ - metadata: Document info (name, version, author)
+ - inputs: Requirements and operating points
+ - components: Inductors, capacitors, switches, diodes
+ - outputs: Losses, KPIs, results
+ """
+ metadata: TASMetadata = field(default_factory=TASMetadata)
+ inputs: TASInputs = field(default_factory=TASInputs)
+ components: TASComponentList = field(default_factory=TASComponentList)
+ outputs: Optional[TASOutputs] = None
+
+ def to_dict(self) -> dict:
+ result = {
+ "metadata": self.metadata.to_dict(),
+ "inputs": self.inputs.to_dict(),
+ }
+ if self.components.all_components:
+ result["components"] = self.components.to_dict()
+ if self.outputs:
+ result["outputs"] = self.outputs.to_dict()
+ return result
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASDocument":
+ return cls(
+ metadata=TASMetadata.from_dict(d.get("metadata", {})),
+ inputs=TASInputs.from_dict(d.get("inputs", {})),
+ components=TASComponentList.from_dict(d.get("components", {})),
+ outputs=TASOutputs.from_dict(d["outputs"]) if d.get("outputs") else None,
+ )
+
+ def to_json(self, indent: int = 2) -> str:
+ """Serialize to JSON string."""
+ import json
+ return json.dumps(self.to_dict(), indent=indent)
+
+ @classmethod
+ def from_json(cls, json_str: str) -> "TASDocument":
+ """Deserialize from JSON string."""
+ import json
+ return cls.from_dict(json.loads(json_str))
+
+ def save(self, filepath: str) -> None:
+ """Save to JSON file."""
+ with open(filepath, "w") as f:
+ f.write(self.to_json())
+
+ @classmethod
+ def load(cls, filepath: str) -> "TASDocument":
+ """Load from JSON file."""
+ with open(filepath, "r") as f:
+ return cls.from_json(f.read())
+
+
+def create_buck_tas(
+ name: str,
+ v_in_min: float,
+ v_in_max: float,
+ v_out: float,
+ i_out: float,
+ frequency: float,
+) -> TASDocument:
+ """Factory function for buck converter TAS."""
+ from tas.models.inputs import TASRequirements, TASOperatingPoint, TASInputs
+
+ now = datetime.now().isoformat()
+ return TASDocument(
+ metadata=TASMetadata(
+ name=name,
+ description="Buck converter design",
+ created=now,
+ modified=now,
+ ),
+ inputs=TASInputs(
+ requirements=TASRequirements(
+ v_in_min=v_in_min,
+ v_in_max=v_in_max,
+ v_out=v_out,
+ i_out_max=i_out,
+ ),
+ operating_points=[
+ TASOperatingPoint(
+ name="nominal",
+ frequency=frequency,
+ duty_cycle=v_out / ((v_in_min + v_in_max) / 2),
+ )
+ ],
+ ),
+ )
+
+
+def create_boost_tas(
+ name: str,
+ v_in_min: float,
+ v_in_max: float,
+ v_out: float,
+ i_out: float,
+ frequency: float,
+) -> TASDocument:
+ """Factory function for boost converter TAS."""
+ from tas.models.inputs import TASRequirements, TASOperatingPoint, TASInputs
+
+ v_in_nom = (v_in_min + v_in_max) / 2
+ duty = 1 - (v_in_nom / v_out) if v_out > v_in_nom else 0.5
+
+ now = datetime.now().isoformat()
+ return TASDocument(
+ metadata=TASMetadata(
+ name=name,
+ description="Boost converter design",
+ created=now,
+ modified=now,
+ ),
+ inputs=TASInputs(
+ requirements=TASRequirements(
+ v_in_min=v_in_min,
+ v_in_max=v_in_max,
+ v_out=v_out,
+ i_out_max=i_out,
+ ),
+ operating_points=[
+ TASOperatingPoint(
+ name="nominal",
+ frequency=frequency,
+ duty_cycle=duty,
+ )
+ ],
+ ),
+ )
+
+
+def create_flyback_tas(
+ name: str,
+ v_in_min: float,
+ v_in_max: float,
+ v_out: float,
+ i_out: float,
+ frequency: float,
+ turns_ratio: float = 1.0,
+) -> TASDocument:
+ """Factory function for flyback converter TAS."""
+ from tas.models.inputs import TASRequirements, TASOperatingPoint, TASInputs
+
+ v_in_nom = (v_in_min + v_in_max) / 2
+ duty = (v_out * turns_ratio) / (v_in_nom + v_out * turns_ratio)
+
+ now = datetime.now().isoformat()
+ return TASDocument(
+ metadata=TASMetadata(
+ name=name,
+ description="Flyback converter design",
+ created=now,
+ modified=now,
+ ),
+ inputs=TASInputs(
+ requirements=TASRequirements(
+ v_in_min=v_in_min,
+ v_in_max=v_in_max,
+ v_out=v_out,
+ i_out_max=i_out,
+ isolation_voltage=1500.0, # Basic isolation
+ ),
+ operating_points=[
+ TASOperatingPoint(
+ name="nominal",
+ frequency=frequency,
+ duty_cycle=duty,
+ )
+ ],
+ ),
+ )
diff --git a/tas/models/waveforms.py b/tas/models/waveforms.py
new file mode 100644
index 0000000..6b2173f
--- /dev/null
+++ b/tas/models/waveforms.py
@@ -0,0 +1,114 @@
+"""
+TAS Waveform models - MAS-compatible waveform definitions.
+
+Waveforms are the universal language that makes TAS topology-agnostic.
+"""
+
+from dataclasses import dataclass
+from typing import List
+from enum import Enum
+import math
+
+
+class WaveformShape(Enum):
+ """Standard waveform shapes."""
+ TRIANGULAR = "triangular"
+ RECTANGULAR = "rectangular"
+ CUSTOM = "custom"
+
+
+@dataclass
+class TASWaveform:
+ """
+ MAS-compatible waveform representation.
+
+ Represents a periodic waveform as time/data sample pairs.
+ """
+ data: List[float]
+ time: List[float]
+ shape: WaveformShape = WaveformShape.CUSTOM
+ unit: str = ""
+
+ @classmethod
+ def triangular(cls, v_min: float, v_max: float, duty: float,
+ period: float, unit: str = "") -> "TASWaveform":
+ """Create triangular waveform (typical inductor current)."""
+ t_rise = duty * period
+ return cls(
+ data=[v_min, v_max, v_min],
+ time=[0.0, t_rise, period],
+ shape=WaveformShape.TRIANGULAR,
+ unit=unit,
+ )
+
+ @classmethod
+ def rectangular(cls, v_high: float, v_low: float, duty: float,
+ period: float, unit: str = "") -> "TASWaveform":
+ """Create rectangular waveform (typical switch voltage)."""
+ t_on = duty * period
+ return cls(
+ data=[v_high, v_high, v_low, v_low],
+ time=[0.0, t_on, t_on, period],
+ shape=WaveformShape.RECTANGULAR,
+ unit=unit,
+ )
+
+ def to_mas(self) -> dict:
+ """Convert to MAS-compatible dict format."""
+ return {"waveform": {"data": self.data, "time": self.time}}
+
+ @classmethod
+ def from_mas(cls, mas_dict: dict, unit: str = "") -> "TASWaveform":
+ """Create from MAS waveform dict."""
+ wf = mas_dict.get("waveform", mas_dict)
+ return cls(
+ data=wf.get("data", []),
+ time=wf.get("time", []),
+ unit=unit,
+ )
+
+ def to_dict(self) -> dict:
+ """Convert to JSON-serializable dict."""
+ return {
+ "data": self.data,
+ "time": self.time,
+ "shape": self.shape.value if self.shape else None,
+ "unit": self.unit,
+ }
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TASWaveform":
+ """Create from dict."""
+ shape = WaveformShape(d["shape"]) if d.get("shape") else WaveformShape.CUSTOM
+ return cls(
+ data=d.get("data", []),
+ time=d.get("time", []),
+ shape=shape,
+ unit=d.get("unit", ""),
+ )
+
+ @property
+ def period(self) -> float:
+ """Get waveform period in seconds."""
+ return max(self.time) if self.time else 0.0
+
+ @property
+ def frequency(self) -> float:
+ """Get waveform frequency in Hz."""
+ p = self.period
+ return 1.0 / p if p > 0 else 0.0
+
+ @property
+ def peak(self) -> float:
+ """Get peak (maximum) value."""
+ return max(self.data) if self.data else 0.0
+
+ @property
+ def min(self) -> float:
+ """Get minimum value."""
+ return min(self.data) if self.data else 0.0
+
+ @property
+ def peak_to_peak(self) -> float:
+ """Get peak-to-peak value."""
+ return self.peak - self.min
diff --git a/tests/test_bobbin_extended.py b/tests/test_bobbin_extended.py
new file mode 100644
index 0000000..5489604
--- /dev/null
+++ b/tests/test_bobbin_extended.py
@@ -0,0 +1,89 @@
+"""
+Tests for PyOpenMagnetics bobbin functions (extended coverage).
+
+Covers create_basic_bobbin(), bobbin data access, and bobbin fitting checks.
+"""
+import pytest
+import PyOpenMagnetics
+
+
+class TestCreateBobbin:
+ """Test create_basic_bobbin() function."""
+
+ def test_create_bobbin_from_etd_core(self, computed_core):
+ """Create a basic bobbin from ETD 49 core."""
+ bobbin = PyOpenMagnetics.create_basic_bobbin(computed_core, 0.001)
+ assert isinstance(bobbin, dict)
+
+ def test_create_bobbin_returns_dict(self, computed_core):
+ """create_basic_bobbin should return a dict."""
+ bobbin = PyOpenMagnetics.create_basic_bobbin(computed_core, 0.001)
+ assert isinstance(bobbin, dict)
+ assert len(bobbin) > 0
+
+ def test_create_bobbin_from_e_core(self):
+ """Create bobbin from E-shaped core."""
+ core_data = {
+ "functionalDescription": {
+ "type": "two-piece set",
+ "material": "3C95",
+ "shape": "E 42/21/15",
+ "gapping": [],
+ "numberStacks": 1
+ }
+ }
+ core = PyOpenMagnetics.calculate_core_data(core_data, False)
+ bobbin = PyOpenMagnetics.create_basic_bobbin(core, 0.001)
+ assert isinstance(bobbin, dict)
+
+ def test_create_bobbin_different_margins(self, computed_core):
+ """Create bobbins with different margin sizes."""
+ bobbin_small = PyOpenMagnetics.create_basic_bobbin(computed_core, 0.0005)
+ bobbin_large = PyOpenMagnetics.create_basic_bobbin(computed_core, 0.002)
+ assert isinstance(bobbin_small, dict)
+ assert isinstance(bobbin_large, dict)
+
+ def test_bobbin_has_processed_description(self, basic_bobbin):
+ """Created bobbin should have processed description."""
+ has_description = (
+ "processedDescription" in basic_bobbin
+ or "functionalDescription" in basic_bobbin
+ )
+ assert has_description
+
+
+class TestBobbinDatabase:
+ """Test bobbin database access."""
+
+ def test_get_bobbins(self):
+ """Get all bobbins from database."""
+ bobbins = PyOpenMagnetics.get_bobbins()
+ assert isinstance(bobbins, list)
+
+ def test_get_bobbin_names(self):
+ """Get bobbin names from database."""
+ names = PyOpenMagnetics.get_bobbin_names()
+ assert isinstance(names, list)
+
+ def test_find_bobbin_by_name(self):
+ """Find bobbin by name from database."""
+ names = PyOpenMagnetics.get_bobbin_names()
+ if len(names) > 0:
+ bobbin = PyOpenMagnetics.find_bobbin_by_name(names[0])
+ assert isinstance(bobbin, dict)
+
+
+class TestBobbinFitting:
+ """Test coil fitting in bobbin."""
+
+ def test_sections_and_layers_fitting_true(self, wound_inductor_coil):
+ """A properly wound coil should fit."""
+ result = PyOpenMagnetics.are_sections_and_layers_fitting(wound_inductor_coil)
+ assert isinstance(result, bool)
+ # A 20-turn coil on ETD 49 should fit
+ assert result is True
+
+ def test_sections_and_layers_fitting_check(self, wound_transformer_coil):
+ """Check fitting for transformer coil."""
+ result = PyOpenMagnetics.are_sections_and_layers_fitting(wound_transformer_coil)
+ assert isinstance(result, bool)
diff --git a/tests/test_converter_topologies.py b/tests/test_converter_topologies.py
new file mode 100644
index 0000000..a82ad14
--- /dev/null
+++ b/tests/test_converter_topologies.py
@@ -0,0 +1,384 @@
+"""
+Tests for PyOpenMagnetics converter topology processor functions.
+
+Many topology processors are defined in the .pyi type stubs but may not be
+implemented in the current build. Tests are marked xfail where needed.
+This file also tests the Design API's topology processing via process_inputs().
+"""
+import pytest
+import PyOpenMagnetics
+
+
+def _has_attr(name):
+ """Check if PyOpenMagnetics has a given attribute."""
+ return hasattr(PyOpenMagnetics, name)
+
+
+class TestProcessFlyback:
+ """Test process_flyback() converter processor."""
+
+ @pytest.mark.skipif(not _has_attr("process_flyback"), reason="process_flyback not available")
+ def test_basic_flyback(self):
+ """Process basic flyback converter specification."""
+ flyback = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 5,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.88,
+ "turnsRatio": 0.1
+ }
+ result = PyOpenMagnetics.process_flyback(flyback)
+ assert isinstance(result, dict)
+
+ @pytest.mark.skipif(not _has_attr("process_flyback"), reason="process_flyback not available")
+ def test_flyback_has_operating_points(self):
+ """Flyback result should have operating points."""
+ flyback = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 5,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.88,
+ "turnsRatio": 0.1
+ }
+ result = PyOpenMagnetics.process_flyback(flyback)
+ if isinstance(result, dict):
+ assert "operatingPoints" in result
+
+ @pytest.mark.skipif(not _has_attr("process_flyback"), reason="process_flyback not available")
+ def test_flyback_has_design_requirements(self):
+ """Flyback result should have design requirements."""
+ flyback = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 5,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.88,
+ "turnsRatio": 0.1
+ }
+ result = PyOpenMagnetics.process_flyback(flyback)
+ if isinstance(result, dict):
+ assert "designRequirements" in result
+
+
+class TestProcessBuck:
+ """Test process_buck() converter processor."""
+
+ @pytest.mark.skipif(not _has_attr("process_buck"), reason="process_buck not available")
+ def test_basic_buck(self):
+ """Process basic buck converter specification."""
+ buck = {
+ "inputVoltage": {"nominal": 48},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "currentRipple": 0.2
+ }
+ result = PyOpenMagnetics.process_buck(buck)
+ assert isinstance(result, dict)
+
+ @pytest.mark.skipif(not _has_attr("process_buck"), reason="process_buck not available")
+ def test_buck_has_operating_point(self):
+ """Buck result should have operating points."""
+ buck = {
+ "inputVoltage": {"nominal": 48},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "currentRipple": 0.2
+ }
+ result = PyOpenMagnetics.process_buck(buck)
+ if isinstance(result, dict):
+ assert "operatingPoints" in result
+
+ @pytest.mark.skipif(not _has_attr("process_buck"), reason="process_buck not available")
+ def test_buck_has_inductance(self):
+ """Buck result should have magnetizing inductance requirement."""
+ buck = {
+ "inputVoltage": {"nominal": 48},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "currentRipple": 0.2
+ }
+ result = PyOpenMagnetics.process_buck(buck)
+ if isinstance(result, dict) and "designRequirements" in result:
+ assert "magnetizingInductance" in result["designRequirements"]
+
+
+class TestProcessBoost:
+ """Test process_boost() converter processor."""
+
+ @pytest.mark.skipif(not _has_attr("process_boost"), reason="process_boost not available")
+ def test_basic_boost(self):
+ """Process basic boost converter specification."""
+ boost = {
+ "inputVoltage": {"nominal": 12},
+ "outputVoltage": 48,
+ "outputCurrent": 2,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "currentRipple": 0.2
+ }
+ result = PyOpenMagnetics.process_boost(boost)
+ assert isinstance(result, dict)
+
+
+class TestProcessForward:
+ """Test forward converter processor functions."""
+
+ @pytest.mark.skipif(not _has_attr("process_single_switch_forward"), reason="process_single_switch_forward not available")
+ def test_single_switch_forward(self):
+ """Process single-switch forward converter."""
+ forward = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 0.1,
+ "maxDutyCycle": 0.45
+ }
+ result = PyOpenMagnetics.process_single_switch_forward(forward)
+ assert isinstance(result, dict)
+
+ @pytest.mark.skipif(not _has_attr("process_two_switch_forward"), reason="process_two_switch_forward not available")
+ def test_two_switch_forward(self):
+ """Process two-switch forward converter."""
+ forward = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 0.1,
+ "maxDutyCycle": 0.45
+ }
+ result = PyOpenMagnetics.process_two_switch_forward(forward)
+ assert isinstance(result, dict)
+
+ @pytest.mark.skipif(not _has_attr("process_active_clamp_forward"), reason="process_active_clamp_forward not available")
+ def test_active_clamp_forward(self):
+ """Process active-clamp forward converter."""
+ forward = {
+ "inputVoltage": {"nominal": 325},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 0.1,
+ "maxDutyCycle": 0.45
+ }
+ result = PyOpenMagnetics.process_active_clamp_forward(forward)
+ assert isinstance(result, dict)
+
+
+class TestProcessPushPull:
+ """Test process_push_pull() converter processor."""
+
+ @pytest.mark.skipif(not _has_attr("process_push_pull"), reason="process_push_pull not available")
+ def test_basic_push_pull(self):
+ """Process basic push-pull converter."""
+ push_pull = {
+ "inputVoltage": {"nominal": 12},
+ "outputVoltage": 48,
+ "outputCurrent": 5,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 4
+ }
+ result = PyOpenMagnetics.process_push_pull(push_pull)
+ assert isinstance(result, dict)
+
+
+class TestProcessIsolatedConverters:
+ """Test isolated converter processor functions."""
+
+ @pytest.mark.skipif(not _has_attr("process_isolated_buck"), reason="process_isolated_buck not available")
+ def test_isolated_buck(self):
+ """Process isolated buck converter."""
+ isolated_buck = {
+ "inputVoltage": {"nominal": 48},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 0.25
+ }
+ result = PyOpenMagnetics.process_isolated_buck(isolated_buck)
+ assert isinstance(result, dict)
+
+ @pytest.mark.skipif(not _has_attr("process_isolated_buck_boost"), reason="process_isolated_buck_boost not available")
+ def test_isolated_buck_boost(self):
+ """Process isolated buck-boost converter."""
+ isolated_buck_boost = {
+ "inputVoltage": {"nominal": 48},
+ "outputVoltage": 12,
+ "outputCurrent": 10,
+ "switchingFrequency": 100000,
+ "ambientTemperature": 25,
+ "efficiency": 0.90,
+ "turnsRatio": 0.25
+ }
+ result = PyOpenMagnetics.process_isolated_buck_boost(isolated_buck_boost)
+ assert isinstance(result, dict)
+
+
+class TestTopologyViaProcessInputs:
+ """Test topology processing through process_inputs() which is always available."""
+
+ def test_inductor_inputs_processing(self, inductor_inputs):
+ """Process inductor inputs through process_inputs."""
+ result = PyOpenMagnetics.process_inputs(inductor_inputs)
+ assert isinstance(result, dict)
+ assert "operatingPoints" in result
+ assert "designRequirements" in result
+
+ def test_flyback_inputs_processing(self):
+ """Process flyback-like inputs through process_inputs."""
+ flyback = {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": [{"nominal": 1}]
+ },
+ "operatingPoints": [
+ {
+ "name": "Flyback Op Point",
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [
+ {
+ "frequency": 100000,
+ "current": {
+ "processed": {
+ "dutyCycle": 0.4,
+ "label": "Flyback primary",
+ "offset": 10,
+ "peakToPeak": 20
+ }
+ }
+ },
+ {
+ "frequency": 100000,
+ "current": {
+ "processed": {
+ "dutyCycle": 0.6,
+ "label": "Flyback secondary",
+ "offset": 10,
+ "peakToPeak": 20
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ result = PyOpenMagnetics.process_inputs(flyback)
+ assert isinstance(result, dict)
+ assert "operatingPoints" in result
+ assert "designRequirements" in result
+
+ def test_high_frequency_inputs_processing(self, high_frequency_inputs):
+ """Process high frequency inputs through process_inputs."""
+ result = PyOpenMagnetics.process_inputs(high_frequency_inputs)
+ assert isinstance(result, dict)
+ assert "operatingPoints" in result
+
+ def test_inputs_preserve_inductance(self, inductor_inputs):
+ """process_inputs should preserve magnetizing inductance."""
+ result = PyOpenMagnetics.process_inputs(inductor_inputs)
+ mag_ind = result["designRequirements"]["magnetizingInductance"]
+ assert abs(mag_ind["nominal"] - 100e-6) < 1e-9
+
+ def test_inputs_preserve_turns_ratios(self):
+ """process_inputs should preserve turns ratios."""
+ inputs = {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": [{"nominal": 0.307692}]
+ },
+ "operatingPoints": [
+ {
+ "name": "Nominal",
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [
+ {
+ "frequency": 100000,
+ "current": {
+ "processed": {"dutyCycle": 0.5, "label": "Triangular", "offset": 0, "peakToPeak": 10}
+ }
+ },
+ {
+ "frequency": 100000,
+ "current": {
+ "processed": {"dutyCycle": 0.5, "label": "Triangular", "offset": 0, "peakToPeak": 5}
+ }
+ }
+ ]
+ }
+ ]
+ }
+ result = PyOpenMagnetics.process_inputs(inputs)
+ ratios = result["designRequirements"]["turnsRatios"]
+ assert isinstance(ratios, list)
+ assert len(ratios) == 1
+
+ def test_inputs_add_harmonics(self, inductor_inputs):
+ """process_inputs should add harmonics to excitations."""
+ result = PyOpenMagnetics.process_inputs(inductor_inputs)
+ excitation = result["operatingPoints"][0]["excitationsPerWinding"][0]
+ assert "harmonics" in excitation.get("current", {})
+
+ def test_inputs_add_processed_data(self, inductor_inputs):
+ """process_inputs should add processed data to excitations."""
+ result = PyOpenMagnetics.process_inputs(inductor_inputs)
+ excitation = result["operatingPoints"][0]["excitationsPerWinding"][0]
+ assert "processed" in excitation.get("current", {})
+
+
+class TestAvailableCalculationFunctions:
+ """Test calculation utility functions that are always available."""
+
+ def test_calculate_basic_processed_data(self):
+ """calculate_basic_processed_data should process waveform data."""
+ if not _has_attr("calculate_basic_processed_data"):
+ pytest.skip("calculate_basic_processed_data not available")
+ waveform = {
+ "data": [-5, 5, -5],
+ "time": [0, 0.0000025, 0.00001]
+ }
+ result = PyOpenMagnetics.calculate_basic_processed_data(waveform)
+ assert isinstance(result, dict)
+
+ def test_calculate_harmonics(self):
+ """calculate_harmonics should decompose waveform."""
+ if not _has_attr("calculate_harmonics"):
+ pytest.skip("calculate_harmonics not available")
+ signal = {
+ "waveform": {
+ "data": [-5, 5, -5],
+ "time": [0, 0.0000025, 0.00001]
+ }
+ }
+ result = PyOpenMagnetics.calculate_harmonics(signal, 100000)
+ assert isinstance(result, dict)
+
+ def test_resolve_dimension_with_tolerance(self):
+ """resolve_dimension_with_tolerance should return a value."""
+ if not _has_attr("resolve_dimension_with_tolerance"):
+ pytest.skip("resolve_dimension_with_tolerance not available")
+ dim = {"nominal": 100e-6, "minimum": 90e-6, "maximum": 110e-6}
+ result = PyOpenMagnetics.resolve_dimension_with_tolerance(dim)
+ assert isinstance(result, (int, float))
+ assert result > 0
diff --git a/tests/test_core_calculations.py b/tests/test_core_calculations.py
new file mode 100644
index 0000000..76f6a9a
--- /dev/null
+++ b/tests/test_core_calculations.py
@@ -0,0 +1,270 @@
+"""
+Tests for PyOpenMagnetics core calculation functions.
+
+Covers gap reluctance, inductance from turns, turns from inductance,
+gapping from turns, magnetic energy, saturation current, and
+temperature-dependent parameters.
+"""
+import pytest
+import PyOpenMagnetics
+
+
+class TestGapReluctance:
+ """Test calculate_gap_reluctance() function."""
+
+ def test_gap_reluctance_zhang_model(self):
+ """Calculate gap reluctance using ZHANG model."""
+ gap = {
+ "type": "subtractive",
+ "length": 0.001,
+ "area": 0.000211,
+ "shape": "round",
+ "coordinates": [0, 0, 0],
+ "distanceClosestNormalSurface": 0.01,
+ "distanceClosestParallelSurface": 0.005,
+ "sectionDimensions": [0.0164, 0.0164]
+ }
+ result = PyOpenMagnetics.calculate_gap_reluctance(gap, "ZHANG")
+ assert isinstance(result, dict)
+
+ def test_gap_reluctance_positive(self):
+ """Gap reluctance should be a positive value."""
+ gap = {
+ "type": "subtractive",
+ "length": 0.001,
+ "area": 0.000211,
+ "shape": "round",
+ "coordinates": [0, 0, 0],
+ "distanceClosestNormalSurface": 0.01,
+ "distanceClosestParallelSurface": 0.005,
+ "sectionDimensions": [0.0164, 0.0164]
+ }
+ result = PyOpenMagnetics.calculate_gap_reluctance(gap, "ZHANG")
+ if isinstance(result, dict) and "reluctance" in result:
+ assert result["reluctance"] > 0
+
+ def test_gap_reluctance_different_models(self):
+ """Different reluctance models should produce results."""
+ gap = {
+ "type": "subtractive",
+ "length": 0.001,
+ "area": 0.000211,
+ "shape": "round",
+ "coordinates": [0, 0, 0],
+ "distanceClosestNormalSurface": 0.01,
+ "distanceClosestParallelSurface": 0.005,
+ "sectionDimensions": [0.0164, 0.0164]
+ }
+ for model in ["ZHANG", "CLASSIC"]:
+ result = PyOpenMagnetics.calculate_gap_reluctance(gap, model)
+ assert isinstance(result, dict)
+
+
+class TestInductanceFromTurns:
+ """Test calculate_inductance_from_number_turns_and_gapping()."""
+
+ def test_inductance_from_turns(self, computed_core, wound_inductor_coil, triangular_operating_point):
+ """Calculate inductance from known turns and gapping."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
+ computed_core, wound_inductor_coil, triangular_operating_point, models
+ )
+ assert isinstance(result, (int, float))
+
+ def test_inductance_is_positive(self, computed_core, wound_inductor_coil, triangular_operating_point):
+ """Inductance should be a positive value."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
+ computed_core, wound_inductor_coil, triangular_operating_point, models
+ )
+ if isinstance(result, (int, float)):
+ assert result > 0
+
+ def test_inductance_physical_range(self, computed_core, wound_inductor_coil, triangular_operating_point):
+ """Inductance should be in a physically reasonable range."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
+ computed_core, wound_inductor_coil, triangular_operating_point, models
+ )
+ if isinstance(result, (int, float)):
+ # For 20 turns on ETD 49 with small gap, expect uH to mH range
+ assert 1e-9 < result < 1 # 1 nH to 1 H
+
+ def test_inductance_different_models(self, computed_core, wound_inductor_coil, triangular_operating_point):
+ """Different reluctance models should produce results."""
+ for model_name in ["ZHANG", "CLASSIC"]:
+ models = {"reluctance": model_name}
+ result = PyOpenMagnetics.calculate_inductance_from_number_turns_and_gapping(
+ computed_core, wound_inductor_coil, triangular_operating_point, models
+ )
+ assert isinstance(result, (int, float))
+
+
+class TestTurnsFromInductance:
+ """Test calculate_number_turns_from_gapping_and_inductance()."""
+
+ def test_turns_from_inductance(self, computed_core, processed_inductor_inputs):
+ """Calculate number of turns for target inductance."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_number_turns_from_gapping_and_inductance(
+ computed_core, processed_inductor_inputs, models
+ )
+ assert isinstance(result, (int, float))
+
+ def test_turns_is_positive(self, computed_core, processed_inductor_inputs):
+ """Number of turns should be positive."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_number_turns_from_gapping_and_inductance(
+ computed_core, processed_inductor_inputs, models
+ )
+ if isinstance(result, (int, float)):
+ assert result > 0
+
+ def test_turns_is_reasonable(self, computed_core, processed_inductor_inputs):
+ """Number of turns should be in a reasonable range for this core."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_number_turns_from_gapping_and_inductance(
+ computed_core, processed_inductor_inputs, models
+ )
+ if isinstance(result, (int, float)):
+ # For 100uH on ETD 49, expect single digits to low hundreds
+ assert 1 < result < 500
+
+
+class TestGappingFromTurns:
+ """Test calculate_gapping_from_number_turns_and_inductance()."""
+
+ @pytest.mark.xfail(reason="May fail with 'bad optional access' in some builds")
+ def test_gapping_from_turns(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Calculate gap for given turns and inductance."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_gapping_from_number_turns_and_inductance(
+ computed_core, wound_inductor_coil, processed_inductor_inputs,
+ "SUBTRACTIVE", 4, models
+ )
+ assert isinstance(result, dict)
+
+ @pytest.mark.xfail(reason="May fail with 'bad optional access' in some builds")
+ def test_gapping_returns_core(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Result should be a Core dict with gapping."""
+ models = {"reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_gapping_from_number_turns_and_inductance(
+ computed_core, wound_inductor_coil, processed_inductor_inputs,
+ "SUBTRACTIVE", 4, models
+ )
+ if isinstance(result, dict):
+ assert "functionalDescription" in result
+ if "gapping" in result.get("functionalDescription", {}):
+ assert isinstance(result["functionalDescription"]["gapping"], list)
+
+
+class TestMagneticEnergy:
+ """Test calculate_core_maximum_magnetic_energy()."""
+
+ def test_magnetic_energy_basic(self, computed_core, triangular_operating_point):
+ """Calculate maximum magnetic energy storage."""
+ result = PyOpenMagnetics.calculate_core_maximum_magnetic_energy(
+ computed_core, triangular_operating_point
+ )
+ assert isinstance(result, (int, float))
+
+ def test_magnetic_energy_positive(self, computed_core, triangular_operating_point):
+ """Magnetic energy should be positive."""
+ result = PyOpenMagnetics.calculate_core_maximum_magnetic_energy(
+ computed_core, triangular_operating_point
+ )
+ if isinstance(result, (int, float)):
+ assert result > 0
+
+
+class TestSaturationCurrent:
+ """Test calculate_saturation_current()."""
+
+ def test_saturation_current_basic(self, simple_magnetic):
+ """Calculate saturation current for magnetic assembly."""
+ result = PyOpenMagnetics.calculate_saturation_current(simple_magnetic, 25.0)
+ assert isinstance(result, (int, float))
+
+ def test_saturation_current_positive(self, simple_magnetic):
+ """Saturation current should be positive."""
+ result = PyOpenMagnetics.calculate_saturation_current(simple_magnetic, 25.0)
+ if isinstance(result, (int, float)):
+ assert result > 0
+
+
+class TestTemperatureDependantParameters:
+ """Test get_core_temperature_dependant_parameters()."""
+
+ def test_temperature_params_at_25c(self, computed_core):
+ """Get temperature-dependent parameters at 25C."""
+ result = PyOpenMagnetics.get_core_temperature_dependant_parameters(
+ computed_core, 25.0
+ )
+ assert isinstance(result, dict)
+
+ def test_temperature_params_has_fields(self, computed_core):
+ """Temperature params should include expected fields."""
+ result = PyOpenMagnetics.get_core_temperature_dependant_parameters(
+ computed_core, 25.0
+ )
+ if isinstance(result, dict):
+ assert len(result) > 0
+ # May include: magneticFluxDensitySaturation, initialPermeability,
+ # effectivePermeability, reluctance, permeance, resistivity
+
+ def test_temperature_params_at_100c(self, computed_core):
+ """Get parameters at 100C (should differ from 25C)."""
+ result_25 = PyOpenMagnetics.get_core_temperature_dependant_parameters(
+ computed_core, 25.0
+ )
+ result_100 = PyOpenMagnetics.get_core_temperature_dependant_parameters(
+ computed_core, 100.0
+ )
+ assert isinstance(result_25, dict)
+ assert isinstance(result_100, dict)
+
+
+class TestCoreAvailability:
+ """Test core data availability and shape queries."""
+
+ def test_get_core_shape_families(self):
+ """Shape families should include common types."""
+ families = PyOpenMagnetics.get_core_shape_families()
+ assert isinstance(families, list)
+ assert len(families) > 0
+
+ def test_shape_families_include_common(self):
+ """Shape families should include E, ETD, PQ."""
+ families = PyOpenMagnetics.get_core_shape_families()
+ families_lower = [f.lower() for f in families]
+ # At least some common families should be present
+ common = ["e", "etd", "pq", "rm"]
+ found = [f for f in common if f in families_lower]
+ assert len(found) > 0
+
+ def test_get_manufacturers(self):
+ """Should get materials from specific manufacturers."""
+ ferroxcube = PyOpenMagnetics.get_core_material_names_by_manufacturer("Ferroxcube")
+ assert isinstance(ferroxcube, list)
+
+ def test_calculate_shape_data(self):
+ """find_core_shape_by_name should return shape geometry."""
+ shape = PyOpenMagnetics.find_core_shape_by_name("ETD 49/25/16")
+ assert isinstance(shape, dict)
+ assert "name" in shape or "family" in shape
+
+ def test_calculate_core_data_for_different_shapes(self):
+ """calculate_core_data should work for various core shapes."""
+ shapes_to_test = ["ETD 49/25/16", "E 42/21/15"]
+ for shape_name in shapes_to_test:
+ core_data = {
+ "functionalDescription": {
+ "type": "two-piece set",
+ "material": "3C95",
+ "shape": shape_name,
+ "gapping": [],
+ "numberStacks": 1
+ }
+ }
+ result = PyOpenMagnetics.calculate_core_data(core_data, False)
+ assert isinstance(result, dict)
diff --git a/tests/test_design_builder.py b/tests/test_design_builder.py
new file mode 100644
index 0000000..704eccc
--- /dev/null
+++ b/tests/test_design_builder.py
@@ -0,0 +1,198 @@
+"""Tests for PyOpenMagnetics Design Builder API."""
+
+import pytest
+import math
+
+
+class TestDesignImports:
+ """Test module imports."""
+
+ def test_import_design(self):
+ from api.design import Design
+ assert Design is not None
+
+ def test_import_result(self):
+ from api.results import DesignResult
+ assert DesignResult is not None
+
+ def test_factory_methods(self):
+ from api.design import Design
+ assert callable(Design.flyback)
+ assert callable(Design.buck)
+ assert callable(Design.boost)
+ assert callable(Design.inductor)
+ assert callable(Design.forward)
+ assert callable(Design.llc)
+
+
+class TestFlybackBuilder:
+ """Test FlybackBuilder."""
+
+ def test_basic_flyback(self):
+ from api.design import Design
+ builder = Design.flyback().vin_ac(85, 265).output(12, 5).fsw(100e3)
+ assert builder._vin_min == 85
+ assert builder._vin_max == 265
+ assert builder._vin_is_ac is True
+ assert len(builder._outputs) == 1
+ assert builder._frequency == 100e3
+
+ def test_dc_input(self):
+ from api.design import Design
+ builder = Design.flyback().vin_dc(380, 420).output(12, 5).fsw(100e3)
+ assert builder._vin_is_ac is False
+
+ def test_multiple_outputs(self):
+ from api.design import Design
+ builder = Design.flyback().vin_ac(85, 265).output(12, 2).output(5, 1).output(3.3, 0.5).fsw(100e3)
+ assert len(builder._outputs) == 3
+
+ def test_calculated_parameters(self):
+ from api.design import Design
+ builder = Design.flyback().vin_ac(85, 265).output(12, 5).fsw(100e3)
+ params = builder.get_calculated_parameters()
+ assert "turns_ratio" in params
+ assert "magnetizing_inductance_uH" in params
+
+ def test_to_mas(self):
+ from api.design import Design
+ builder = Design.flyback().vin_ac(85, 265).output(12, 5).fsw(100e3)
+ mas = builder.to_mas()
+ assert "designRequirements" in mas
+ assert "operatingPoints" in mas
+
+ def test_solve_flyback(self):
+ from api.design import Design
+ results = Design.flyback().vin_ac(85, 265).output(12, 5).fsw(100e3).solve(max_results=MAX_RESULTS)
+ assert isinstance(results, list)
+
+
+class TestBuckBuilder:
+ """Test BuckBuilder."""
+
+ def test_basic_buck(self):
+ from api.design import Design
+ builder = Design.buck().vin(12, 24).vout(5).iout(3).fsw(500e3)
+ assert builder._vin_min == 12
+ assert builder._vout == 5
+ assert builder._iout == 3
+
+ def test_calculated_parameters(self):
+ from api.design import Design
+ builder = Design.buck().vin(12, 24).vout(5).iout(3).fsw(500e3)
+ params = builder.get_calculated_parameters()
+ assert "inductance_uH" in params
+ assert params["inductance_uH"] > 0
+
+ def test_solve_buck(self):
+ from api.design import Design
+ results = Design.buck().vin(12, 24).vout(5).iout(3).fsw(500e3).solve(max_results=MAX_RESULTS)
+ assert isinstance(results, list)
+
+ def test_invalid_vout(self):
+ from api.design import Design
+ builder = Design.buck().vin(5, 12).vout(15).iout(1).fsw(100e3)
+ with pytest.raises(ValueError, match="Vout must be less than"):
+ builder.get_calculated_parameters()
+
+
+class TestBoostBuilder:
+ """Test BoostBuilder."""
+
+ def test_basic_boost(self):
+ from api.design import Design
+ builder = Design.boost().vin(3.0, 4.2).vout(5).pout(2).fsw(1e6)
+ assert builder._vin_min == 3.0
+ assert builder._vout == 5
+
+ def test_solve_boost(self):
+ from api.design import Design
+ results = Design.boost().vin(3.0, 4.2).vout(5).pout(2).fsw(1e6).solve(max_results=MAX_RESULTS)
+ assert isinstance(results, list)
+
+
+class TestInductorBuilder:
+ """Test InductorBuilder."""
+
+ def test_basic_inductor(self):
+ from api.design import Design
+ builder = Design.inductor().inductance(100e-6).idc(5).iac_pp(1).fsw(100e3)
+ assert builder._inductance == 100e-6
+ assert builder._idc == 5
+
+ def test_calculated_parameters(self):
+ from api.design import Design
+ builder = Design.inductor().inductance(100e-6).idc(5).iac_pp(1).fsw(100e3)
+ params = builder.get_calculated_parameters()
+ assert params["inductance_uH"] == 100
+
+ def test_solve_inductor(self):
+ from api.design import Design
+ results = Design.inductor().inductance(100e-6).idc(5).iac_pp(1).fsw(100e3).solve(max_results=MAX_RESULTS)
+ assert isinstance(results, list)
+
+
+class TestForwardBuilder:
+ """Test ForwardBuilder."""
+
+ def test_basic_forward(self):
+ from api.design import Design
+ builder = Design.forward().vin_dc(380, 420).output(12, 10).fsw(100e3)
+ assert builder._vin_min == 380
+ assert len(builder._outputs) == 1
+
+ def test_variant(self):
+ from api.design import Design
+ builder = Design.forward().variant("two_switch").vin_dc(380, 420).output(12, 10).fsw(100e3)
+ assert builder._variant == "two_switch"
+
+
+class TestLLCBuilder:
+ """Test LLCBuilder."""
+
+ def test_basic_llc(self):
+ from api.design import Design
+ builder = Design.llc().vin_dc(380, 420).output(12, 20).resonant_frequency(100e3)
+ assert builder._vin_min == 380
+ assert builder._resonant_freq == 100e3
+
+ def test_calculated_parameters(self):
+ from api.design import Design
+ builder = Design.llc().vin_dc(380, 420).output(12, 20).resonant_frequency(100e3)
+ params = builder.get_calculated_parameters()
+ assert "turns_ratio" in params
+ assert "magnetizing_inductance_uH" in params
+
+
+class TestDesignResult:
+ """Test DesignResult parsing."""
+
+ def test_from_mas_basic(self):
+ from api.results import DesignResult
+ mas_data = {
+ "magnetic": {
+ "core": {"functionalDescription": {"shape": {"name": "ETD 34"}, "material": {"name": "3C95"},
+ "gapping": [{"type": "subtractive", "length": 0.0005}]}},
+ "coil": {"functionalDescription": [{"name": "Primary", "numberTurns": 20, "wire": "AWG 24"}]}
+ }
+ }
+ result = DesignResult.from_mas(mas_data)
+ assert result.core == "ETD 34"
+ assert result.material == "3C95"
+ assert result.air_gap_mm == 0.5
+
+
+class TestConstraints:
+ """Test constraint methods."""
+
+ def test_max_dimensions(self):
+ from api.design import Design
+ builder = Design.buck().vin(12, 24).vout(5).iout(3).fsw(500e3)
+ builder.max_height(20).max_width(30).max_depth(25)
+ dims = builder._get_max_dimensions()
+ assert dims["height"] == 0.020
+
+ def test_prefer_invalid(self):
+ from api.design import Design
+ with pytest.raises(ValueError):
+ Design.buck().prefer("invalid")
diff --git a/tests/test_losses.py b/tests/test_losses.py
new file mode 100644
index 0000000..db09e42
--- /dev/null
+++ b/tests/test_losses.py
@@ -0,0 +1,237 @@
+"""
+Tests for PyOpenMagnetics loss calculation functions.
+
+Covers core losses (multiple models), winding losses (ohmic, skin, proximity),
+wire-level per-meter loss functions, and Steinmetz coefficients.
+"""
+import pytest
+import PyOpenMagnetics
+
+
+class TestCoreLosses:
+ """Test calculate_core_losses() function."""
+
+ def test_core_losses_igse_model(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Calculate core losses using IGSE model."""
+ models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_core_losses(
+ computed_core, wound_inductor_coil, processed_inductor_inputs, models
+ )
+ assert isinstance(result, dict)
+
+ def test_core_losses_returns_positive_value(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Core losses should be a positive number."""
+ models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_core_losses(
+ computed_core, wound_inductor_coil, processed_inductor_inputs, models
+ )
+ if isinstance(result, dict) and "coreLosses" in result:
+ assert result["coreLosses"] > 0
+
+ def test_core_losses_has_flux_density(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Core loss result should include magnetic flux density."""
+ models = {"coreLosses": "IGSE", "reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_core_losses(
+ computed_core, wound_inductor_coil, processed_inductor_inputs, models
+ )
+ if isinstance(result, dict):
+ # Should have B peak data
+ has_flux = (
+ "magneticFluxDensityPeak" in result
+ or "magneticFluxDensityAcPeak" in result
+ or "magneticFluxDensity" in result
+ )
+ assert has_flux or "coreLosses" in result
+
+ def test_core_losses_steinmetz_model(self, computed_core, wound_inductor_coil, processed_inductor_inputs):
+ """Calculate core losses using Steinmetz model."""
+ models = {"coreLosses": "STEINMETZ", "reluctance": "ZHANG"}
+ result = PyOpenMagnetics.calculate_core_losses(
+ computed_core, wound_inductor_coil, processed_inductor_inputs, models
+ )
+ assert isinstance(result, dict)
+
+
+class TestCoreLossModelInfo:
+ """Test core loss model information functions."""
+
+ def test_get_core_losses_model_information(self):
+ """Get available loss models for a material."""
+ material = PyOpenMagnetics.find_core_material_by_name("3C95")
+ result = PyOpenMagnetics.get_core_losses_model_information(material)
+ assert isinstance(result, dict)
+
+ def test_model_info_has_content(self):
+ """Model info should have some content."""
+ material = PyOpenMagnetics.find_core_material_by_name("3C95")
+ result = PyOpenMagnetics.get_core_losses_model_information(material)
+ assert len(result) > 0
+
+
+class TestSteinmetzCoefficients:
+ """Test Steinmetz coefficient retrieval."""
+
+ def test_steinmetz_coefficients_by_name(self):
+ """Get Steinmetz coefficients by material name string."""
+ result = PyOpenMagnetics.get_core_material_steinmetz_coefficients("3C95", 100000)
+ assert isinstance(result, dict)
+
+ def test_steinmetz_coefficients_by_material_dict(self):
+ """Get Steinmetz coefficients by material dict."""
+ material = PyOpenMagnetics.find_core_material_by_name("3C95")
+ result = PyOpenMagnetics.get_core_material_steinmetz_coefficients(material, 100000)
+ assert isinstance(result, dict)
+
+ def test_steinmetz_has_k_alpha_beta(self):
+ """Steinmetz result should have k, alpha, beta coefficients."""
+ result = PyOpenMagnetics.get_core_material_steinmetz_coefficients("3C95", 100000)
+ # Standard Steinmetz equation: P = k * f^alpha * B^beta
+ has_coefficients = "k" in result or "alpha" in result or "beta" in result
+ assert has_coefficients
+
+ def test_steinmetz_at_different_frequencies(self):
+ """Coefficients may differ at different frequencies."""
+ result_100k = PyOpenMagnetics.get_core_material_steinmetz_coefficients("3C95", 100000)
+ result_500k = PyOpenMagnetics.get_core_material_steinmetz_coefficients("3C95", 500000)
+ assert isinstance(result_100k, dict)
+ assert isinstance(result_500k, dict)
+
+
+class TestWindingLosses:
+ """Test calculate_winding_losses() function."""
+
+ def test_winding_losses_basic(self, simple_magnetic, triangular_operating_point):
+ """Calculate total winding losses."""
+ result = PyOpenMagnetics.calculate_winding_losses(
+ simple_magnetic, triangular_operating_point, 25.0
+ )
+ assert isinstance(result, dict)
+
+ def test_winding_losses_returns_total(self, simple_magnetic, triangular_operating_point):
+ """Winding loss result should include loss data."""
+ result = PyOpenMagnetics.calculate_winding_losses(
+ simple_magnetic, triangular_operating_point, 25.0
+ )
+ if isinstance(result, dict):
+ # Result may contain various keys depending on the build
+ assert len(result) > 0
+
+ def test_winding_losses_positive(self, simple_magnetic, triangular_operating_point):
+ """Winding losses should be positive."""
+ result = PyOpenMagnetics.calculate_winding_losses(
+ simple_magnetic, triangular_operating_point, 25.0
+ )
+ if isinstance(result, dict) and "windingLosses" in result:
+ losses = result["windingLosses"]
+ if isinstance(losses, (int, float)):
+ assert losses >= 0
+
+ def test_winding_losses_at_high_temperature(self, simple_magnetic, triangular_operating_point):
+ """Winding losses should increase at higher temperature."""
+ result_25 = PyOpenMagnetics.calculate_winding_losses(
+ simple_magnetic, triangular_operating_point, 25.0
+ )
+ result_100 = PyOpenMagnetics.calculate_winding_losses(
+ simple_magnetic, triangular_operating_point, 100.0
+ )
+ # Both should return valid results
+ assert isinstance(result_25, dict)
+ assert isinstance(result_100, dict)
+
+
+class TestOhmicLosses:
+ """Test calculate_ohmic_losses() function."""
+
+ def test_ohmic_losses_basic(self, wound_inductor_coil, triangular_operating_point):
+ """Calculate DC ohmic losses."""
+ result = PyOpenMagnetics.calculate_ohmic_losses(
+ wound_inductor_coil, triangular_operating_point, 25.0
+ )
+ assert isinstance(result, dict)
+
+ def test_ohmic_losses_positive(self, wound_inductor_coil, triangular_operating_point):
+ """Ohmic losses should be non-negative."""
+ result = PyOpenMagnetics.calculate_ohmic_losses(
+ wound_inductor_coil, triangular_operating_point, 25.0
+ )
+ if isinstance(result, dict) and "ohmicLosses" in result:
+ losses = result["ohmicLosses"]
+ if isinstance(losses, (int, float)):
+ assert losses >= 0
+
+
+class TestDcResistancePerMeter:
+ """Test wire-level per-meter loss calculations."""
+
+ def test_dc_resistance_per_meter(self, sample_round_wire):
+ """DC resistance per meter should be positive."""
+ result = PyOpenMagnetics.calculate_dc_resistance_per_meter(sample_round_wire, 25.0)
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_dc_resistance_increases_with_temperature(self, sample_round_wire):
+ """DC resistance should increase with temperature for copper."""
+ r_25 = PyOpenMagnetics.calculate_dc_resistance_per_meter(sample_round_wire, 25.0)
+ r_100 = PyOpenMagnetics.calculate_dc_resistance_per_meter(sample_round_wire, 100.0)
+ assert r_100 > r_25
+
+ def test_dc_losses_per_meter(self, sample_round_wire, current_excitation_with_harmonics):
+ """DC losses per meter should be positive."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_dc_losses_per_meter(sample_round_wire, current, 25.0)
+ assert isinstance(result, float)
+ assert result >= 0
+
+ def test_skin_ac_losses_per_meter(self, sample_round_wire, current_excitation_with_harmonics):
+ """Skin effect AC losses per meter should be non-negative."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_skin_ac_losses_per_meter(sample_round_wire, current, 25.0)
+ assert isinstance(result, float)
+ assert result >= 0
+
+ def test_skin_ac_resistance_per_meter(self, sample_round_wire, current_excitation_with_harmonics):
+ """Skin effect AC resistance per meter should be positive."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_skin_ac_resistance_per_meter(sample_round_wire, current, 25.0)
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_skin_ac_factor(self, sample_round_wire, current_excitation_with_harmonics):
+ """AC factor (Rac/Rdc) should be >= 1.0."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_skin_ac_factor(sample_round_wire, current, 25.0)
+ assert isinstance(result, float)
+ assert result >= 1.0
+
+ def test_effective_current_density(self, sample_round_wire, current_excitation_with_harmonics):
+ """Effective current density should be positive."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_effective_current_density(sample_round_wire, current, 25.0)
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_effective_skin_depth(self, current_excitation_with_harmonics):
+ """Skin depth should be positive for copper."""
+ current = current_excitation_with_harmonics.get("current", current_excitation_with_harmonics)
+ result = PyOpenMagnetics.calculate_effective_skin_depth("copper", current, 25.0)
+ assert isinstance(result, float)
+ assert result > 0
+
+
+class TestMagneticFieldStrength:
+ """Test magnetic field strength calculation."""
+
+ def test_calculate_field_strength(self, simple_magnetic, triangular_operating_point):
+ """Calculate magnetic field distribution in winding window."""
+ result = PyOpenMagnetics.calculate_magnetic_field_strength_field(
+ triangular_operating_point, simple_magnetic
+ )
+ assert isinstance(result, dict)
+
+ def test_field_strength_has_data(self, simple_magnetic, triangular_operating_point):
+ """Field strength result should contain field data."""
+ result = PyOpenMagnetics.calculate_magnetic_field_strength_field(
+ triangular_operating_point, simple_magnetic
+ )
+ if isinstance(result, dict):
+ assert len(result) > 0
diff --git a/tests/test_mas_dataclasses.py b/tests/test_mas_dataclasses.py
new file mode 100644
index 0000000..a6417dd
--- /dev/null
+++ b/tests/test_mas_dataclasses.py
@@ -0,0 +1,741 @@
+"""
+Tests for MAS.py auto-generated dataclasses.
+
+Verifies from_dict/to_dict round-trip fidelity, enum validation,
+Union type handling, and edge cases for the Magnetic Agnostic Structure
+data model.
+"""
+import pytest
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from api.MAS import (
+ DimensionWithTolerance,
+ CTI,
+ InsulationType,
+ OvervoltageCategory,
+ PollutionDegree,
+ InsulationStandards,
+ InsulationRequirements,
+ IsolationSide,
+ Market,
+ MaximumDimensions,
+ DesignRequirements,
+ Topology,
+ OperatingConditions,
+ Cooling,
+ Harmonics,
+ WaveformLabel,
+ Processed,
+ Waveform,
+ SignalDescriptor,
+ OperatingPointExcitation,
+ OperatingPoint,
+ Inputs,
+ ManufacturerInfo,
+ Status,
+ Outputs,
+ Mas,
+ Masfromdict,
+ Mastodict,
+ from_float,
+ from_str,
+ from_int,
+ from_bool,
+ from_list,
+ from_none,
+ from_union,
+)
+
+
+class TestDimensionWithTolerance:
+ """Test DimensionWithTolerance dataclass."""
+
+ def test_nominal_only(self):
+ """Create with only nominal value."""
+ obj = {"nominal": 100e-6}
+ dim = DimensionWithTolerance.from_dict(obj)
+ assert dim.nominal == 100e-6
+ assert dim.minimum is None
+ assert dim.maximum is None
+
+ def test_all_fields(self):
+ """Create with all fields populated."""
+ obj = {
+ "nominal": 100e-6,
+ "minimum": 90e-6,
+ "maximum": 110e-6,
+ "excludeMinimum": False,
+ "excludeMaximum": True
+ }
+ dim = DimensionWithTolerance.from_dict(obj)
+ assert dim.nominal == 100e-6
+ assert dim.minimum == 90e-6
+ assert dim.maximum == 110e-6
+ assert dim.excludeMinimum is False
+ assert dim.excludeMaximum is True
+
+ def test_round_trip(self):
+ """from_dict(obj).to_dict() should preserve data."""
+ obj = {"nominal": 50.0, "minimum": 45.0, "maximum": 55.0}
+ dim = DimensionWithTolerance.from_dict(obj)
+ result = dim.to_dict()
+ assert result["nominal"] == 50.0
+ assert result["minimum"] == 45.0
+ assert result["maximum"] == 55.0
+
+ def test_none_fields_omitted_from_to_dict(self):
+ """to_dict() should omit None fields."""
+ obj = {"nominal": 100e-6}
+ dim = DimensionWithTolerance.from_dict(obj)
+ result = dim.to_dict()
+ assert "nominal" in result
+ assert "minimum" not in result
+ assert "maximum" not in result
+ assert "excludeMinimum" not in result
+
+ def test_integer_nominal_converted_to_float(self):
+ """Integer input should be accepted and converted."""
+ obj = {"nominal": 100}
+ dim = DimensionWithTolerance.from_dict(obj)
+ assert isinstance(dim.nominal, float)
+ assert dim.nominal == 100.0
+
+
+class TestEnumTypes:
+ """Test enum dataclasses from MAS schema."""
+
+ def test_cti_enum(self):
+ """CTI enum should have correct values."""
+ assert CTI.GroupI.value == "Group I"
+ assert CTI.GroupII.value == "Group II"
+ assert CTI.GroupIIIA.value == "Group IIIA"
+ assert CTI.GroupIIIB.value == "Group IIIB"
+
+ def test_insulation_type_enum(self):
+ """InsulationType should have all standard types."""
+ assert InsulationType.Basic.value == "Basic"
+ assert InsulationType.Double.value == "Double"
+ assert InsulationType.Functional.value == "Functional"
+ assert InsulationType.Reinforced.value == "Reinforced"
+ assert InsulationType.Supplementary.value == "Supplementary"
+
+ def test_topology_enum(self):
+ """Topology enum should include common converter topologies."""
+ assert Topology.FlybackConverter.value == "Flyback Converter"
+ assert Topology.BuckConverter.value == "Buck Converter"
+ assert Topology.BoostConverter.value == "Boost Converter"
+ assert Topology.PushPullConverter.value == "Push-Pull Converter"
+
+ def test_waveform_label_enum(self):
+ """WaveformLabel enum should include standard waveform types."""
+ assert WaveformLabel.Sinusoidal.value == "Sinusoidal"
+ assert WaveformLabel.Triangular.value == "Triangular"
+ assert WaveformLabel.Rectangular.value == "Rectangular"
+ assert WaveformLabel.Custom.value == "Custom"
+
+ def test_isolation_side_enum(self):
+ """IsolationSide should include primary and secondary."""
+ assert IsolationSide.primary.value == "primary"
+ assert IsolationSide.secondary.value == "secondary"
+ assert IsolationSide.tertiary.value == "tertiary"
+
+ def test_market_enum(self):
+ """Market enum should have standard categories."""
+ assert Market.Commercial.value == "Commercial"
+ assert Market.Industrial.value == "Industrial"
+ assert Market.Medical.value == "Medical"
+
+
+class TestInsulationRequirements:
+ """Test InsulationRequirements dataclass."""
+
+ def test_basic_insulation(self):
+ """Create basic insulation requirements."""
+ obj = {
+ "insulationType": "Basic",
+ "cti": "Group I"
+ }
+ ins = InsulationRequirements.from_dict(obj)
+ assert ins.insulationType == InsulationType.Basic
+ assert ins.cti == CTI.GroupI
+
+ def test_with_optional_fields(self):
+ """Create with altitude and standards."""
+ obj = {
+ "insulationType": "Reinforced",
+ "altitude": {"nominal": 2000},
+ "standards": ["IEC 62368-1"]
+ }
+ ins = InsulationRequirements.from_dict(obj)
+ assert ins.insulationType == InsulationType.Reinforced
+ assert ins.altitude.nominal == 2000
+ assert ins.standards == [InsulationStandards.IEC623681]
+
+ def test_to_dict_omits_none(self):
+ """to_dict should omit None fields."""
+ obj = {"insulationType": "Double"}
+ ins = InsulationRequirements.from_dict(obj)
+ result = ins.to_dict()
+ assert "insulationType" in result
+ assert result["insulationType"] == "Double"
+ assert "cti" not in result
+ assert "altitude" not in result
+
+
+class TestDesignRequirements:
+ """Test DesignRequirements dataclass."""
+
+ def test_inductor_requirements(self):
+ """Inductor has inductance but no turns ratios."""
+ obj = {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": []
+ }
+ dr = DesignRequirements.from_dict(obj)
+ assert dr.magnetizingInductance.nominal == 100e-6
+ assert dr.turnsRatios == []
+
+ def test_transformer_requirements(self):
+ """Transformer has turns ratios."""
+ obj = {
+ "magnetizingInductance": {"nominal": 500e-6},
+ "turnsRatios": [{"nominal": 0.1}]
+ }
+ dr = DesignRequirements.from_dict(obj)
+ assert len(dr.turnsRatios) == 1
+ assert abs(dr.turnsRatios[0].nominal - 0.1) < 1e-9
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": [{"nominal": 0.5}],
+ "name": "Test DR",
+ "topology": "Flyback Converter"
+ }
+ dr = DesignRequirements.from_dict(obj)
+ result = dr.to_dict()
+ assert result["magnetizingInductance"]["nominal"] == 100e-6
+ assert len(result["turnsRatios"]) == 1
+ assert result["name"] == "Test DR"
+ assert result["topology"] == "Flyback Converter"
+
+
+class TestOperatingPoint:
+ """Test OperatingPoint and related dataclasses."""
+
+ def test_basic_operating_point(self):
+ """Create basic operating point."""
+ obj = {
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [
+ {"frequency": 100000}
+ ]
+ }
+ op = OperatingPoint.from_dict(obj)
+ assert op.conditions.ambientTemperature == 25
+ assert len(op.excitationsPerWinding) == 1
+ assert op.excitationsPerWinding[0].frequency == 100000
+
+ def test_with_waveform(self):
+ """Operating point with waveform data."""
+ obj = {
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [
+ {
+ "frequency": 100000,
+ "current": {
+ "waveform": {
+ "data": [-5, 5, -5],
+ "time": [0, 0.0000025, 0.00001]
+ }
+ }
+ }
+ ]
+ }
+ op = OperatingPoint.from_dict(obj)
+ waveform = op.excitationsPerWinding[0].current.waveform
+ assert len(waveform.data) == 3
+ assert len(waveform.time) == 3
+
+ def test_with_processed_data(self):
+ """Operating point with processed signal data."""
+ obj = {
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [
+ {
+ "frequency": 100000,
+ "current": {
+ "processed": {
+ "label": "Sinusoidal",
+ "offset": 0,
+ "peakToPeak": 10,
+ "dutyCycle": 0.5
+ }
+ }
+ }
+ ]
+ }
+ op = OperatingPoint.from_dict(obj)
+ processed = op.excitationsPerWinding[0].current.processed
+ assert processed.label == WaveformLabel.Sinusoidal
+ assert processed.peakToPeak == 10
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {
+ "name": "Nominal",
+ "conditions": {"ambientTemperature": 42},
+ "excitationsPerWinding": [
+ {
+ "frequency": 200000,
+ "current": {
+ "processed": {
+ "label": "Triangular",
+ "offset": 5,
+ "peakToPeak": 3
+ }
+ }
+ }
+ ]
+ }
+ op = OperatingPoint.from_dict(obj)
+ result = op.to_dict()
+ assert result["name"] == "Nominal"
+ assert result["conditions"]["ambientTemperature"] == 42
+ assert result["excitationsPerWinding"][0]["frequency"] == 200000
+
+
+class TestInputs:
+ """Test Inputs dataclass."""
+
+ def test_basic_inputs(self):
+ """Create basic inputs structure."""
+ obj = {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": []
+ },
+ "operatingPoints": [
+ {
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [{"frequency": 100000}]
+ }
+ ]
+ }
+ inp = Inputs.from_dict(obj)
+ assert inp.designRequirements.magnetizingInductance.nominal == 100e-6
+ assert len(inp.operatingPoints) == 1
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 50e-6},
+ "turnsRatios": [{"nominal": 0.25}]
+ },
+ "operatingPoints": [
+ {
+ "conditions": {"ambientTemperature": 85},
+ "excitationsPerWinding": [{"frequency": 500000}]
+ }
+ ]
+ }
+ inp = Inputs.from_dict(obj)
+ result = inp.to_dict()
+ assert result["designRequirements"]["magnetizingInductance"]["nominal"] == 50e-6
+ assert len(result["designRequirements"]["turnsRatios"]) == 1
+ assert result["operatingPoints"][0]["conditions"]["ambientTemperature"] == 85
+
+ def test_missing_required_field_raises(self):
+ """Missing required field should raise AssertionError."""
+ with pytest.raises((AssertionError, KeyError, TypeError)):
+ Inputs.from_dict({"designRequirements": {"magnetizingInductance": {"nominal": 100e-6}, "turnsRatios": []}})
+
+
+class TestSignalDescriptor:
+ """Test SignalDescriptor (harmonics + processed + waveform)."""
+
+ def test_with_waveform(self):
+ """SignalDescriptor with waveform data."""
+ obj = {
+ "waveform": {
+ "data": [0, 1, 0, -1, 0],
+ "time": [0, 0.25e-5, 0.5e-5, 0.75e-5, 1e-5]
+ }
+ }
+ sd = SignalDescriptor.from_dict(obj)
+ assert sd.waveform is not None
+ assert len(sd.waveform.data) == 5
+
+ def test_with_processed(self):
+ """SignalDescriptor with processed data."""
+ obj = {
+ "processed": {
+ "label": "Sinusoidal",
+ "offset": 0,
+ "peakToPeak": 10,
+ "rms": 3.54
+ }
+ }
+ sd = SignalDescriptor.from_dict(obj)
+ assert sd.processed is not None
+ assert sd.processed.rms == 3.54
+
+ def test_with_harmonics(self):
+ """SignalDescriptor with harmonics."""
+ obj = {
+ "harmonics": {
+ "amplitudes": [5.0, 0.1, 0.05],
+ "frequencies": [100000, 200000, 300000]
+ }
+ }
+ sd = SignalDescriptor.from_dict(obj)
+ assert sd.harmonics is not None
+ assert len(sd.harmonics.amplitudes) == 3
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {
+ "processed": {
+ "label": "Triangular",
+ "offset": 2.5,
+ "peakToPeak": 5
+ },
+ "harmonics": {
+ "amplitudes": [2.5, 0.3],
+ "frequencies": [100000, 300000]
+ }
+ }
+ sd = SignalDescriptor.from_dict(obj)
+ result = sd.to_dict()
+ assert result["processed"]["label"] == "Triangular"
+ assert len(result["harmonics"]["amplitudes"]) == 2
+
+
+class TestWaveform:
+ """Test Waveform dataclass."""
+
+ def test_basic_waveform(self):
+ """Create waveform with data and time."""
+ obj = {"data": [1, 2, 3], "time": [0, 0.5, 1.0]}
+ wf = Waveform.from_dict(obj)
+ assert len(wf.data) == 3
+ assert len(wf.time) == 3
+
+ def test_equidistant_waveform(self):
+ """Waveform without time (equidistant points)."""
+ obj = {"data": [0, 1, 0, -1]}
+ wf = Waveform.from_dict(obj)
+ assert len(wf.data) == 4
+ assert wf.time is None
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {"data": [1.0, 2.0, 3.0], "time": [0.0, 0.5, 1.0], "numberPeriods": 1}
+ wf = Waveform.from_dict(obj)
+ result = wf.to_dict()
+ assert result["data"] == [1.0, 2.0, 3.0]
+ assert result["numberPeriods"] == 1
+
+
+class TestProcessed:
+ """Test Processed dataclass."""
+
+ def test_minimal_processed(self):
+ """Create processed with only required fields."""
+ obj = {"label": "Sinusoidal", "offset": 0}
+ p = Processed.from_dict(obj)
+ assert p.label == WaveformLabel.Sinusoidal
+ assert p.offset == 0
+ assert p.rms is None
+
+ def test_full_processed(self):
+ """Create processed with all fields."""
+ obj = {
+ "label": "Triangular",
+ "offset": 5,
+ "rms": 2.88,
+ "peakToPeak": 10,
+ "dutyCycle": 0.5,
+ "peak": 10,
+ "thd": 0.12
+ }
+ p = Processed.from_dict(obj)
+ assert p.label == WaveformLabel.Triangular
+ assert p.rms == 2.88
+ assert p.dutyCycle == 0.5
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip preserving set fields."""
+ obj = {"label": "Rectangular", "offset": 2.5, "peakToPeak": 10}
+ p = Processed.from_dict(obj)
+ result = p.to_dict()
+ assert result["label"] == "Rectangular"
+ assert result["offset"] == 2.5
+ assert result["peakToPeak"] == 10
+ # None fields omitted
+ assert "rms" not in result
+
+
+class TestHarmonicsDataclass:
+ """Test Harmonics dataclass."""
+
+ def test_basic_harmonics(self):
+ """Create harmonics with amplitudes and frequencies."""
+ obj = {
+ "amplitudes": [5.0, 1.0, 0.5],
+ "frequencies": [100000, 200000, 300000]
+ }
+ h = Harmonics.from_dict(obj)
+ assert len(h.amplitudes) == 3
+ assert len(h.frequencies) == 3
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {
+ "amplitudes": [3.14, 0.5],
+ "frequencies": [50000, 150000]
+ }
+ h = Harmonics.from_dict(obj)
+ result = h.to_dict()
+ assert result["amplitudes"] == [3.14, 0.5]
+ assert result["frequencies"] == [50000, 150000]
+
+
+class TestOperatingConditions:
+ """Test OperatingConditions dataclass."""
+
+ def test_basic_conditions(self):
+ """Create with ambient temperature."""
+ obj = {"ambientTemperature": 25}
+ oc = OperatingConditions.from_dict(obj)
+ assert oc.ambientTemperature == 25
+
+ def test_with_cooling(self):
+ """Create with cooling configuration."""
+ obj = {
+ "ambientTemperature": 40,
+ "cooling": {
+ "fluid": "air",
+ "temperature": 25
+ }
+ }
+ oc = OperatingConditions.from_dict(obj)
+ assert oc.cooling is not None
+ assert oc.cooling.fluid == "air"
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {"ambientTemperature": 85, "name": "Hot"}
+ oc = OperatingConditions.from_dict(obj)
+ result = oc.to_dict()
+ assert result["ambientTemperature"] == 85
+ assert result["name"] == "Hot"
+
+
+class TestMaximumDimensions:
+ """Test MaximumDimensions dataclass."""
+
+ def test_all_dimensions(self):
+ """Create with all three dimensions."""
+ obj = {"width": 0.05, "height": 0.03, "depth": 0.04}
+ md = MaximumDimensions.from_dict(obj)
+ assert md.width == 0.05
+ assert md.height == 0.03
+ assert md.depth == 0.04
+
+ def test_partial_dimensions(self):
+ """Create with only height constraint."""
+ obj = {"height": 0.025}
+ md = MaximumDimensions.from_dict(obj)
+ assert md.height == 0.025
+ assert md.width is None
+ assert md.depth is None
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {"height": 0.02}
+ md = MaximumDimensions.from_dict(obj)
+ result = md.to_dict()
+ assert result["height"] == 0.02
+ assert "width" not in result
+
+
+class TestManufacturerInfo:
+ """Test ManufacturerInfo dataclass."""
+
+ def test_basic_info(self):
+ """Create with required name field."""
+ obj = {"name": "Ferroxcube"}
+ mi = ManufacturerInfo.from_dict(obj)
+ assert mi.name == "Ferroxcube"
+
+ def test_with_status(self):
+ """Create with production status."""
+ obj = {"name": "TDK", "status": "production", "reference": "B66311"}
+ mi = ManufacturerInfo.from_dict(obj)
+ assert mi.status == Status.production
+ assert mi.reference == "B66311"
+
+ def test_round_trip(self):
+ """from_dict -> to_dict round-trip."""
+ obj = {"name": "Magnetics Inc", "family": "Kool Mu"}
+ mi = ManufacturerInfo.from_dict(obj)
+ result = mi.to_dict()
+ assert result["name"] == "Magnetics Inc"
+ assert result["family"] == "Kool Mu"
+
+
+class TestHelperFunctions:
+ """Test module-level helper functions."""
+
+ def test_from_float_with_int(self):
+ """from_float should accept int and return float."""
+ result = from_float(42)
+ assert isinstance(result, float)
+ assert result == 42.0
+
+ def test_from_float_with_float(self):
+ """from_float should accept float."""
+ result = from_float(3.14)
+ assert result == 3.14
+
+ def test_from_float_rejects_bool(self):
+ """from_float should reject bool (subclass of int)."""
+ with pytest.raises(AssertionError):
+ from_float(True)
+
+ def test_from_str_rejects_int(self):
+ """from_str should reject non-string."""
+ with pytest.raises(AssertionError):
+ from_str(42)
+
+ def test_from_int_rejects_bool(self):
+ """from_int should reject bool."""
+ with pytest.raises(AssertionError):
+ from_int(True)
+
+ def test_from_bool_accepts_bool(self):
+ """from_bool should accept True/False."""
+ assert from_bool(True) is True
+ assert from_bool(False) is False
+
+ def test_from_list_applies_function(self):
+ """from_list should apply converter function to each element."""
+ result = from_list(from_float, [1, 2.5, 3])
+ assert result == [1.0, 2.5, 3.0]
+
+ def test_from_none_accepts_none(self):
+ """from_none should accept None."""
+ assert from_none(None) is None
+
+ def test_from_none_rejects_value(self):
+ """from_none should reject non-None."""
+ with pytest.raises(AssertionError):
+ from_none(42)
+
+ def test_from_union_tries_converters(self):
+ """from_union should try each converter in order."""
+ result = from_union([from_float, from_none], 42)
+ assert result == 42.0
+
+ result_none = from_union([from_float, from_none], None)
+ assert result_none is None
+
+
+class TestMasFunctions:
+ """Test module-level Mas helper functions."""
+
+ def test_masfromdict(self):
+ """Masfromdict should create Mas from dict."""
+ obj = {
+ "inputs": {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 100e-6},
+ "turnsRatios": []
+ },
+ "operatingPoints": [
+ {
+ "conditions": {"ambientTemperature": 25},
+ "excitationsPerWinding": [{"frequency": 100000}]
+ }
+ ]
+ },
+ "magnetic": {
+ "core": {
+ "functionalDescription": {
+ "type": "two-piece set",
+ "material": "3C95",
+ "shape": "ETD 49/25/16",
+ "gapping": [],
+ "numberStacks": 1
+ }
+ },
+ "coil": {
+ "bobbin": "ETD 49/25/16",
+ "functionalDescription": [
+ {
+ "name": "Primary",
+ "numberTurns": 20,
+ "numberParallels": 1,
+ "isolationSide": "primary",
+ "wire": "Round 0.5 - Grade 1"
+ }
+ ]
+ }
+ },
+ "outputs": []
+ }
+ mas = Masfromdict(obj)
+ assert mas.inputs.designRequirements.magnetizingInductance.nominal == 100e-6
+
+ def test_mastodict(self):
+ """Mastodict should convert Mas to dict."""
+ obj = {
+ "inputs": {
+ "designRequirements": {
+ "magnetizingInductance": {"nominal": 50e-6},
+ "turnsRatios": []
+ },
+ "operatingPoints": [
+ {
+ "conditions": {"ambientTemperature": 40},
+ "excitationsPerWinding": [{"frequency": 200000}]
+ }
+ ]
+ },
+ "magnetic": {
+ "core": {
+ "functionalDescription": {
+ "type": "two-piece set",
+ "material": "3C95",
+ "shape": "E 42/21/15",
+ "gapping": [],
+ "numberStacks": 1
+ }
+ },
+ "coil": {
+ "bobbin": "E 42/21/15",
+ "functionalDescription": [
+ {
+ "name": "Primary",
+ "numberTurns": 10,
+ "numberParallels": 1,
+ "isolationSide": "primary",
+ "wire": "Round 0.5 - Grade 1"
+ }
+ ]
+ }
+ },
+ "outputs": []
+ }
+ mas = Masfromdict(obj)
+ result = Mastodict(mas)
+ assert isinstance(result, dict)
+ assert "inputs" in result
+ assert "magnetic" in result
+ assert "outputs" in result
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
new file mode 100644
index 0000000..da97eef
--- /dev/null
+++ b/tests/test_plotting.py
@@ -0,0 +1,109 @@
+"""
+Tests for PyOpenMagnetics visualization/plotting functions.
+
+Covers plot_core, plot_magnetic, plot_wire, plot_bobbin SVG generation.
+Plot functions return dict with {success: bool, svg: str} or just str.
+"""
+import pytest
+import PyOpenMagnetics
+
+
+def _get_svg(result):
+ """Extract SVG string from plot result (may be str or dict)."""
+ if isinstance(result, str):
+ return result
+ if isinstance(result, dict):
+ return result.get("svg", "")
+ return ""
+
+
+def _is_svg(result):
+ """Check if result contains valid SVG content."""
+ svg = _get_svg(result)
+ return isinstance(svg, str) and "