Files
toroid/model.py

222 lines
8.0 KiB
Python

from dataclasses import dataclass, field
from typing import List, Tuple, Optional, Dict
from core import core_loss_Pv
@dataclass
class TransformerModel:
"""
Simple transformer model with taps on primary and secondary.
Assumptions:
- Single primary winding with taps.
- Single secondary winding with taps.
- Ideal magnetic coupling (for now).
- Load is purely resistive.
"""
# Core geometry
Ae_mm2: float # effective core area in mm^2
# Turn counts (total)
Np_total: int
Ns_total: int
# Core loss model parameters (optional)
Ve_mm3: float = 0.0 # effective core volume in mm^3 (for core loss calculation)
use_core_loss_model: bool = False # if True, calculate core loss from B and f
# Tap positions in turns counted from a common reference (e.g. one end of the bobbin)
# Must include 0 and total turns if you want to be able to use full winding.
primary_taps: List[int] = field(default_factory=lambda: [0])
secondary_taps: List[int] = field(default_factory=lambda: [0])
# Optional: copper resistance per turn (ohms/turn) for each tap segment
# If specified, should be a list with length = len(taps) - 1
# primary_Rp_per_turn[i] is the R/turn for segment from taps[i] to taps[i+1]
# If not specified, falls back to Rp_per_turn/Rs_per_turn
primary_Rp_per_turn: Optional[List[float]] = None
secondary_Rs_per_turn: Optional[List[float]] = None
# Legacy: single resistance per turn for all segments (deprecated)
Rp_per_turn: float = 0.0
Rs_per_turn: float = 0.0
def _effective_turns(self, taps: List[int], tap: int) -> int:
"""
Compute effective turns for a given tap number.
tap is 1-indexed: tap=1 uses segment 1, tap=2 uses segments 1+2, etc.
Taps list represents incremental turns added at each segment.
First element should be 0, subsequent elements are turns to add.
Example: taps=[0, 50, 150] means:
- tap=1: 50 turns (first segment adds 50)
- tap=2: 50 + 150 = 200 turns (first segment adds 50, second adds 150)
"""
if tap < 1 or tap >= len(taps):
raise ValueError(f"Tap must be between 1 and {len(taps)-1}")
# Sum all incremental turns: taps[1] + taps[2] + ... + taps[tap]
total_turns = sum(taps[1:tap+1])
return total_turns
def _resistance_for_tap(
self,
taps: List[int],
tap: int,
R_per_turn_list: Optional[List[float]],
R_per_turn_legacy: float
) -> float:
"""
Compute total resistance for a given tap.
Sums resistance across all segments using incremental turns representation.
If R_per_turn_list is provided, uses segment-specific resistances.
Otherwise uses legacy single R_per_turn value.
With taps=[0, 50, 150] and tap=2:
- Segment 1 has 50 turns
- Segment 2 has 150 turns
- Total R = R_per_turn[0]*50 + R_per_turn[1]*150
"""
if tap < 1 or tap >= len(taps):
raise ValueError(f"Tap must be between 1 and {len(taps)-1}")
total_resistance = 0.0
if R_per_turn_list is not None:
# Use segment-specific resistance
if len(R_per_turn_list) != len(taps) - 1:
raise ValueError(f"R_per_turn list must have {len(taps)-1} elements")
# Sum resistance for each segment (taps[i] represents incremental turns)
for i in range(tap):
segment_turns = taps[i+1] # Incremental turns for this segment
total_resistance += R_per_turn_list[i] * segment_turns
else:
# Use legacy single R_per_turn
# Total turns is sum of all incremental segments
total_turns = sum(taps[1:tap+1])
total_resistance = R_per_turn_legacy * total_turns
return total_resistance
def simulate(
self,
primary_tap: int,
secondary_tap: int,
Vp_rms: float,
freq_hz: float,
load_ohms: float,
core_loss_W: float = 0.0,
) -> Dict[str, float]:
"""
Simulate one operating point.
Arguments:
- primary_tap: tap number (1-indexed), where tap=1 means taps[0] to taps[1], tap=2 means taps[0] to taps[2]
- secondary_tap: tap number (1-indexed), where tap=1 means taps[0] to taps[1], tap=2 means taps[0] to taps[2]
- Vp_rms: primary RMS voltage (V)
- freq_hz: excitation frequency (Hz)
- load_ohms: resistive load across the chosen secondary segment (ohms)
- core_loss_W: optional core loss at this operating point (W)
Returns a dict with:
- Np_eff, Ns_eff
- turns_ratio (Ns_eff / Np_eff)
- B_peak_T (Tesla)
- Vs_rms, Ip_rms, Is_rms
- P_out_W, P_in_W, P_cu_W, P_core_W, efficiency
- primary_tap, secondary_tap (echoed back)
"""
if freq_hz <= 0:
raise ValueError("Frequency must be > 0")
if load_ohms <= 0:
raise ValueError("Load resistance must be > 0")
# Effective turns
Np_eff = self._effective_turns(self.primary_taps, primary_tap)
Ns_eff = self._effective_turns(self.secondary_taps, secondary_tap)
if Np_eff == 0 or Ns_eff == 0:
raise ValueError("Effective turns cannot be zero")
# Core area in m^2
Ae_m2 = self.Ae_mm2 * 1e-6
# Peak flux density (sine excitation assumed)
B_peak_T = Vp_rms / (4.44 * Np_eff * Ae_m2 * freq_hz)
# Ideal turn ratio
turns_ratio = Ns_eff / Np_eff
# Ideal secondary RMS voltage
Vs_rms_ideal = Vp_rms * turns_ratio
# Load current and ideal powers (purely resistive)
Is_rms_ideal = Vs_rms_ideal / load_ohms
Ip_rms_ideal = Is_rms_ideal * turns_ratio # current ratio inverse of turns
# Copper resistances for the active segments
Rp_seg = self._resistance_for_tap(
self.primary_taps,
primary_tap,
self.primary_Rp_per_turn,
self.Rp_per_turn
)
Rs_seg = self._resistance_for_tap(
self.secondary_taps,
secondary_tap,
self.secondary_Rs_per_turn,
self.Rs_per_turn
)
# Copper losses (approx, assuming currents ~ ideal currents)
P_cu_p = Ip_rms_ideal**2 * Rp_seg
P_cu_s = Is_rms_ideal**2 * Rs_seg
P_cu_total = P_cu_p + P_cu_s
# Output power (resistive load)
P_out = Vs_rms_ideal**2 / load_ohms
# Core loss calculation
if self.use_core_loss_model and self.Ve_mm3 > 0:
# Calculate core loss from B and f using the model
# Convert volume to m^3
Ve_m3 = self.Ve_mm3 * 1e-9 # mm^3 to m^3
# Get core loss density in kW/m^3
try:
Pv_kW_m3 = core_loss_Pv(B_peak_T, freq_hz)
# Convert to watts
core_loss_calculated = Pv_kW_m3 * Ve_m3 * 1000 # kW/m^3 * m^3 * 1000 = W
except (ValueError, Exception) as e:
# If core loss model fails (e.g., B or f out of range), fall back to provided value
core_loss_calculated = core_loss_W
else:
# Use provided core loss value
core_loss_calculated = core_loss_W
# Total input power (approx)
P_in = P_out + P_cu_total + core_loss_calculated
efficiency = P_out / P_in if P_in > 0 else 1.0
return {
"Np_eff": Np_eff,
"Ns_eff": Ns_eff,
"turns_ratio": turns_ratio,
"B_peak_T": B_peak_T,
"Vp_rms": Vp_rms,
"Vs_rms": Vs_rms_ideal,
"Ip_rms": Ip_rms_ideal,
"Is_rms": Is_rms_ideal,
"P_out_W": P_out,
"P_cu_W": P_cu_total,
"P_cu_primary_W": P_cu_p,
"P_cu_secondary_W": P_cu_s,
"P_core_W": core_loss_calculated,
"P_in_W": P_in,
"efficiency": efficiency,
"primary_tap": primary_tap,
"secondary_tap": secondary_tap,
}