initial commit
This commit is contained in:
199
model.py
Normal file
199
model.py
Normal file
@@ -0,0 +1,199 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user