222 lines
8.0 KiB
Python
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,
|
|
}
|