from dataclasses import dataclass, field from typing import List, Tuple, Optional, Dict @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 # 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 # Total input power (approx) P_in = P_out + P_cu_total + core_loss_W 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_W, "P_in_W": P_in, "efficiency": efficiency, "primary_tap": primary_tap, "secondary_tap": secondary_tap, }