"""Streamlit app for parallel campaign quota allocation simulation.""" import copy import sys from pathlib import Path import streamlit as st # # Add parent directory to path to allow imports # parent_dir = Path(__file__).parent.parent # if str(parent_dir) not in sys.path: # sys.path.insert(0, str(parent_dir)) from src.models import Campaign # noqa: E402 from src.scenarios import ( # noqa: E402 ScenarioGenerator, get_preset_scenarios, ) from src.simulator import CampaignSimulator, compare_strategies # noqa: E402 from src.strategies import get_all_strategies # noqa: E402 from src.visualizations import ( # noqa: E402 create_completion_time_comparison, create_gantt_chart, create_metrics_table, create_quota_split_chart, create_slowdown_comparison, create_timeline_view, ) # Page configuration st.set_page_config( page_title="Campaign Quota Simulator", page_icon="📧", layout="wide", initial_sidebar_state="expanded", ) st.title("📧 Parallel Campaign Quota Allocation Simulator") st.markdown( """ This app simulates different quota allocation strategies for parallel email campaigns. Adjust parameters in the sidebar and run simulations to compare strategies. """ ) # Initialize session state if "campaigns" not in st.session_state: st.session_state.campaigns = [] if "simulation_results" not in st.session_state: st.session_state.simulation_results = None if "baseline_times" not in st.session_state: st.session_state.baseline_times = None # Sidebar configuration st.sidebar.header("⚙️ Configuration") # Quota settings st.sidebar.subheader("Quota Settings") quota_per_hour = st.sidebar.number_input( "Emails per Hour", min_value=100, max_value=100000, value=6000, step=100, help="Total number of emails that can be sent per hour", ) quota_per_minute = quota_per_hour // 60 st.sidebar.info(f"Quota per minute: **{quota_per_minute}** emails") # Priority spillover percentage st.sidebar.subheader("Priority+Spillover Settings") priority_percentage = st.sidebar.slider( "Priority Percentage", min_value=0.0, max_value=100.0, value=90.0, step=5.0, help="Percentage of quota allocated to priority (FCFS) campaigns", ) spillover_percentage = 100.0 - priority_percentage st.sidebar.info( f"Split: **{priority_percentage:.0f}%** priority / **{spillover_percentage:.0f}%** spillover" ) # Strategy selection st.sidebar.subheader("Strategy Selection") all_strategies = get_all_strategies(priority_percentage) strategy_names = [s.get_name() for s in all_strategies] selected_strategies = st.sidebar.multiselect( "Select Strategies to Compare", options=strategy_names, default=strategy_names, help="Choose which allocation strategies to simulate", ) # Scenario selection st.sidebar.header("📋 Scenario Setup") scenario_mode = st.sidebar.radio( "Scenario Mode", options=["Preset Scenarios", "Manual Campaigns", "Random Generation"], help="Choose how to create campaigns", ) generator = ScenarioGenerator() if scenario_mode == "Preset Scenarios": preset_scenarios = get_preset_scenarios() selected_preset = st.sidebar.selectbox( "Select Preset Scenario", options=list(preset_scenarios.keys()), help="Choose a predefined scenario", ) if st.sidebar.button("Load Preset Scenario", type="primary"): st.session_state.campaigns = copy.deepcopy(preset_scenarios[selected_preset]) st.success(f"Loaded: {selected_preset}") elif scenario_mode == "Manual Campaigns": st.sidebar.subheader("Add Campaign Manually") with st.sidebar.form("add_campaign_form"): campaign_name = st.text_input("Campaign Name", value="My Campaign") campaign_volume = st.number_input( "Total Volume", min_value=1, max_value=100000, value=5000, step=100 ) campaign_start = st.number_input( "Start Minute", min_value=0, max_value=500, value=0, step=1 ) if st.form_submit_button("Add Campaign"): new_campaign = Campaign( id=f"campaign_{len(st.session_state.campaigns) + 1}", name=campaign_name, total_volume=campaign_volume, start_minute=campaign_start, ) st.session_state.campaigns.append(new_campaign) st.success(f"Added: {campaign_name}") if st.sidebar.button("Clear All Campaigns"): st.session_state.campaigns = [] st.success("All campaigns cleared") else: # Random Generation st.sidebar.subheader("Random Generation Settings") num_campaigns = st.sidebar.slider( "Number of Campaigns", min_value=1, max_value=20, value=5, step=1 ) min_volume = st.sidebar.number_input( "Min Volume", min_value=100, max_value=50000, value=1000, step=100 ) max_volume = st.sidebar.number_input( "Max Volume", min_value=min_volume, max_value=100000, value=10000, step=100 ) time_window = st.sidebar.slider( "Time Window (minutes)", min_value=0, max_value=120, value=60, step=5, help="Campaigns will start randomly within this window", ) random_seed = st.sidebar.number_input( "Random Seed", min_value=0, max_value=10000, value=42, step=1, help="For reproducible results", ) if st.sidebar.button("Generate Random Campaigns", type="primary"): st.session_state.campaigns = generator.generate_random_campaigns( count=num_campaigns, min_volume=min_volume, max_volume=max_volume, time_window_minutes=time_window, seed=random_seed, ) st.success(f"Generated {num_campaigns} random campaigns") # Run simulation button st.sidebar.header("▶️ Run Simulation") if st.sidebar.button( "Run Simulation", type="primary", disabled=len(st.session_state.campaigns) == 0 ): if not selected_strategies: st.error("Please select at least one strategy") else: with st.spinner("Running simulations..."): # Get selected strategy objects strategies = [ s for s in all_strategies if s.get_name() in selected_strategies ] # Run simulations results = compare_strategies( st.session_state.campaigns, strategies, quota_per_hour ) # Calculate baseline times simulator = CampaignSimulator(quota_per_hour) baseline_times = simulator.get_baseline_times(st.session_state.campaigns) st.session_state.simulation_results = results st.session_state.baseline_times = baseline_times st.success(f"Simulation complete! Compared {len(results)} strategies.") # Main content area st.header("Current Campaigns") if st.session_state.campaigns: # Display campaigns table campaign_data = [ { "Name": c.name, "Volume": c.total_volume, "Start Minute": c.start_minute, } for c in st.session_state.campaigns ] st.dataframe(campaign_data, use_container_width=True) # Campaign management col1, col2 = st.columns([1, 4]) with col1: if st.button("Clear Campaigns"): st.session_state.campaigns = [] st.rerun() else: st.info( "No campaigns loaded. Use the sidebar to load a preset scenario, " "add campaigns manually, or generate random campaigns." ) # Display simulation results if st.session_state.simulation_results: st.header("📊 Simulation Results") results = st.session_state.simulation_results baseline_times = st.session_state.baseline_times # Metrics summary st.subheader("Summary Metrics") metrics_df = create_metrics_table(results) st.dataframe(metrics_df, use_container_width=True) # Strategy comparison charts st.subheader("Strategy Comparison") tab1, tab2 = st.tabs(["Completion Times", "Slowdown Factors"]) with tab1: fig_completion = create_completion_time_comparison(results) st.plotly_chart(fig_completion, use_container_width=True) with tab2: fig_slowdown = create_slowdown_comparison(results, baseline_times) st.plotly_chart(fig_slowdown, use_container_width=True) st.info( """ **Slowdown Factor** measures how much slower a campaign runs compared to running alone. - Factor of 1.0 = No slowdown (same speed as running alone) - Factor of 2.0 = Campaign takes twice as long - Lower is better """ ) # Per-strategy detailed views st.subheader("Detailed Views by Strategy") selected_strategy_for_detail = st.selectbox( "Select Strategy for Detailed View", options=[r.strategy_name for r in results], ) # Find the selected result selected_result = next( r for r in results if r.strategy_name == selected_strategy_for_detail ) # Create tabs for different visualizations detail_tab1, detail_tab2, detail_tab3 = st.tabs( ["Gantt Chart", "Quota Split by Minute", "Timeline View"] ) with detail_tab1: st.markdown( """ **Gantt Chart** shows when each campaign is actively sending emails. Each bar represents emails sent in a specific minute. """ ) fig_gantt = create_gantt_chart(selected_result) st.plotly_chart(fig_gantt, use_container_width=True) with detail_tab2: st.markdown( """ **Quota Split by Minute** shows how the available quota is distributed among campaigns at each minute. The red dashed line indicates the quota limit. """ ) fig_quota_split = create_quota_split_chart(selected_result) st.plotly_chart(fig_quota_split, use_container_width=True) with detail_tab3: st.markdown( """ **Timeline View** shows the sending rate for each campaign over time. """ ) fig_timeline = create_timeline_view(selected_result) st.plotly_chart(fig_timeline, use_container_width=True) # Campaign-level details st.subheader("Campaign Details") for campaign in selected_result.campaigns: with st.expander(f"📧 {campaign.name}"): col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Total Volume", campaign.total_volume) with col2: st.metric("Start Minute", campaign.start_minute) with col3: completion_time = ( campaign.completion_minute - campaign.start_minute if campaign.completion_minute is not None else "Not completed" ) st.metric("Completion Time", completion_time) with col4: baseline_time = baseline_times.get(campaign.name, 0) slowdown = ( (campaign.completion_minute - campaign.start_minute) / baseline_time if campaign.completion_minute is not None and baseline_time > 0 else 1.0 ) st.metric( "Slowdown Factor", f"{slowdown:.2f}x", delta=f"{(slowdown - 1.0) * 100:.1f}%", ) else: st.info("Run a simulation to see results") # Footer st.sidebar.markdown("---") st.sidebar.markdown( """ ### About This simulator helps evaluate different quota allocation strategies for parallel email campaigns sharing a common sending quota. **Strategies:** - **Equal Split**: Divide quota equally - **Proportional to Total**: Based on campaign size - **Proportional to Remaining**: Based on backlog (⚠️ penalizes early campaigns) - **FCFS**: Priority to earlier campaigns - **Weighted Fair**: Age-based weighting - **Priority+Spillover**: Configurable FCFS priority with spillover """ )