"""Data models for campaign simulation.""" from dataclasses import dataclass, field @dataclass class Campaign: """Represents an email campaign.""" id: str name: str total_volume: int # Total number of emails to send start_minute: int # Minute when campaign should start (0-indexed) remaining_volume: int = field(init=False) # Emails left to send sent_volume: int = field(default=0) # Emails already sent completion_minute: int | None = field(default=None) # When campaign finished sends_by_minute: dict[int, int] = field( default_factory=dict ) # Minute -> emails sent def __post_init__(self) -> None: """Initialize remaining volume after creation.""" self.remaining_volume = self.total_volume def send_emails(self, minute: int, count: int) -> int: """ Send emails in the given minute. Args: minute: Current minute in simulation count: Number of emails to send Returns: Actual number of emails sent (may be less than count if not enough remaining) """ actual_count = min(count, self.remaining_volume) self.remaining_volume -= actual_count self.sent_volume += actual_count if minute in self.sends_by_minute: self.sends_by_minute[minute] += actual_count else: self.sends_by_minute[minute] = actual_count if self.remaining_volume == 0 and self.completion_minute is None: self.completion_minute = minute return actual_count def is_active(self, minute: int) -> bool: """Check if campaign should be sending emails at given minute.""" return minute >= self.start_minute and self.remaining_volume > 0 def reset(self) -> None: """Reset campaign to initial state for re-simulation.""" self.remaining_volume = self.total_volume self.sent_volume = 0 self.completion_minute = None self.sends_by_minute = {} @dataclass class SimulationResult: """Results from a simulation run.""" strategy_name: str campaigns: list[Campaign] total_minutes: int quota_per_minute: int quota_utilization: float # Percentage of quota actually used avg_completion_time: float # Average minutes to complete max_completion_time: int # Longest campaign duration def get_completion_times(self) -> dict[str, int]: """Get completion times for all campaigns.""" return { c.name: (c.completion_minute - c.start_minute) if c.completion_minute is not None else self.total_minutes - c.start_minute for c in self.campaigns } def calculate_slowdown_factors( self, baseline_times: dict[str, int] ) -> dict[str, float]: """ Calculate slowdown factor compared to baseline. Args: baseline_times: Completion times from baseline (alone) simulation Returns: Dict mapping campaign name to slowdown factor """ completion_times = self.get_completion_times() return { name: completion_times[name] / baseline_times[name] if baseline_times.get(name, 0) > 0 else 1.0 for name in completion_times } @dataclass class QuotaAllocation: """Quota allocation for a single minute.""" minute: int campaign_allocations: dict[str, int] # Campaign ID -> allocated quota total_quota: int active_campaigns: int