import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm import gradio as gr import io from PIL import Image # ============================================================================ # CORE CLASSES (Same as before, but with comments for learning) # ============================================================================ class LGCP: """Log-Gaussian Cox Process model for uncertain target arrivals.""" def __init__(self, mean_func, var_func, corr_len=1.0): self.mean_func = mean_func self.var_func = var_func self.corr_len = corr_len def mean_intensity(self, s): """Compute mean intensity E[λ(s)] = exp(μ + σ²/2) for log-normal.""" mu = self.mean_func(s) sigma2 = self.var_func(s) return np.exp(mu + sigma2 / 2) def quantile_intensity(self, s, alpha): """Compute α-quantile: λ_α = exp(μ + z_α * σ).""" mu = self.mean_func(s) sigma = np.sqrt(self.var_func(s)) return np.exp(mu + norm.ppf(alpha) * sigma) def sample(self, s, n_samples=1): """Sample spatially correlated intensity functions.""" n = len(s) mu = self.mean_func(s) sigma = np.sqrt(self.var_func(s)) dist = np.abs(s.reshape(-1, 1) - s.reshape(1, -1)) corr = np.exp(-dist**2 / (2 * self.corr_len**2)) cov = np.outer(sigma, sigma) * corr + 1e-6 * np.eye(n) L = np.linalg.cholesky(cov) samples = np.zeros((n_samples, n)) for i in range(n_samples): samples[i] = np.exp(mu + L @ np.random.randn(n)) return samples class Sensors: """Gaussian detection probability model.""" def __init__(self, rho=0.95, sigma_l=0.25): self.rho = rho self.sigma_l = sigma_l def detect_prob(self, s, loc): """Detection probability γ(s, a) = ρ * exp(-(s-a)²/σ_l).""" return self.rho * np.exp(-(s - loc)**2 / self.sigma_l) def miss_prob(self, s, locs): """Probability all sensors miss: π(s, a) = ∏(1 - γ(s, a_i)).""" if len(locs) == 0: return np.ones_like(s) miss = np.ones_like(s) for loc in locs: miss *= (1 - self.detect_prob(s, loc)) return miss def greedy_optimize(grid, candidates, intensity, sensors, n_sensors): """Greedy sensor placement algorithm.""" ds = grid[1] - grid[0] selected = [] for _ in range(n_sensors): best_gain = -np.inf best_loc = None current_miss = sensors.miss_prob(grid, np.array(selected)) current_val = np.sum(intensity * (1 - current_miss)) * ds for c in candidates: if c in selected: continue new_miss = sensors.miss_prob(grid, np.array(selected + [c])) new_val = np.sum(intensity * (1 - new_miss)) * ds gain = new_val - current_val if gain > best_gain: best_gain = gain best_loc = c if best_loc is not None: selected.append(best_loc) return np.array(selected) def evaluate(grid, sensor_locs, intensity_samples, sensors): """Monte Carlo evaluation of sensor placement.""" ds = grid[1] - grid[0] miss = sensors.miss_prob(grid, sensor_locs) void_probs = [] for sample in intensity_samples: expected_missed = np.sum(sample * miss) * ds void_probs.append(np.exp(-expected_missed)) void_probs = np.array(void_probs) return { 'mean': np.mean(void_probs), 'std': np.std(void_probs), 'p5': np.percentile(void_probs, 5), 'p10': np.percentile(void_probs, 10), 'p25': np.percentile(void_probs, 25), 'median': np.median(void_probs), 'all': void_probs } # ============================================================================ # ENVIRONMENT CREATION # ============================================================================ def create_environment(hotspot1_pos, hotspot1_strength, hotspot2_pos, hotspot2_strength, uncertainty_pos, uncertainty_strength): """Create customizable environment.""" def mean_func(s): region1 = hotspot1_strength * np.exp(-((s - hotspot1_pos)**2) / 0.8) region2 = hotspot2_strength * np.exp(-((s - hotspot2_pos)**2) / 0.5) return -1.5 + region1 + region2 def var_func(s): base = 0.1 uncertain_zone = uncertainty_strength * np.exp(-((s - uncertainty_pos)**2) / 0.8) return base + uncertain_zone return mean_func, var_func # ============================================================================ # VISUALIZATION FUNCTIONS # ============================================================================ def plot_detection_formula(rho, sigma_l): """Visualize the detection probability formula.""" fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # Plot 1: Detection probability curve sensor_loc = 5 s = np.linspace(0, 10, 200) sensors = Sensors(rho=rho, sigma_l=sigma_l) detect_probs = sensors.detect_prob(s, sensor_loc) ax1 = axes[0] ax1.plot(s, detect_probs, 'b-', linewidth=2) ax1.axvline(sensor_loc, color='red', linestyle='--', label=f'Sensor at {sensor_loc}') ax1.fill_between(s, 0, detect_probs, alpha=0.3) ax1.set_xlabel('Location (s)', fontsize=12) ax1.set_ylabel('Detection Probability', fontsize=12) ax1.set_title(f'Detection Probability Formula\nγ(s) = {rho} × exp(-(s-{sensor_loc})² / {sigma_l})', fontsize=11) ax1.legend() ax1.grid(True, alpha=0.3) ax1.set_ylim(0, 1) # Plot 2: Effect of parameters ax2 = axes[1] # Different sigma_l values for sl in [0.1, 0.25, 0.5, 1.0]: temp_sensors = Sensors(rho=rho, sigma_l=sl) probs = temp_sensors.detect_prob(s, sensor_loc) ax2.plot(s, probs, label=f'σₗ = {sl}', linewidth=2) ax2.axvline(sensor_loc, color='gray', linestyle='--', alpha=0.5) ax2.set_xlabel('Location (s)', fontsize=12) ax2.set_ylabel('Detection Probability', fontsize=12) ax2.set_title(f'Effect of σₗ (sensor range)\nρ = {rho} fixed', fontsize=11) ax2.legend() ax2.grid(True, alpha=0.3) ax2.set_ylim(0, 1) plt.tight_layout() return fig def plot_intensity_explanation(hotspot1_pos, hotspot1_strength, hotspot2_pos, hotspot2_strength, uncertainty_pos, uncertainty_strength): """Visualize mean intensity and variance.""" fig, axes = plt.subplots(1, 3, figsize=(14, 4)) grid = np.linspace(0, 10, 200) mean_func, var_func = create_environment( hotspot1_pos, hotspot1_strength, hotspot2_pos, hotspot2_strength, uncertainty_pos, uncertainty_strength ) lgcp = LGCP(mean_func, var_func) # Plot 1: Mean function (mu) ax1 = axes[0] mu_values = mean_func(grid) ax1.plot(grid, mu_values, 'g-', linewidth=2) ax1.fill_between(grid, mu_values.min(), mu_values, alpha=0.3, color='green') ax1.set_xlabel('Location (s)', fontsize=12) ax1.set_ylabel('μ(s)', fontsize=12) ax1.set_title('Step 1: Mean of Log-Intensity μ(s)', fontsize=11) ax1.grid(True, alpha=0.3) # Plot 2: Variance function ax2 = axes[1] var_values = var_func(grid) ax2.plot(grid, var_values, 'orange', linewidth=2) ax2.fill_between(grid, 0, var_values, alpha=0.3, color='orange') ax2.set_xlabel('Location (s)', fontsize=12) ax2.set_ylabel('σ²(s)', fontsize=12) ax2.set_title('Step 2: Variance (Uncertainty) σ²(s)', fontsize=11) ax2.grid(True, alpha=0.3) # Plot 3: Final mean intensity ax3 = axes[2] mean_int = lgcp.mean_intensity(grid) q90_int = lgcp.quantile_intensity(grid, 0.90) ax3.plot(grid, mean_int, 'b-', linewidth=2, label='Mean Intensity E[λ]') ax3.plot(grid, q90_int, 'r-', linewidth=2, label='Q90 Intensity') ax3.fill_between(grid, mean_int, q90_int, alpha=0.3, color='red') ax3.set_xlabel('Location (s)', fontsize=12) ax3.set_ylabel('Intensity', fontsize=12) ax3.set_title('Step 3: Final Intensity = exp(μ + σ²/2)', fontsize=11) ax3.legend() ax3.grid(True, alpha=0.3) plt.tight_layout() return fig def run_full_analysis(n_sensors, rho, sigma_l, hotspot1_pos, hotspot1_strength, hotspot2_pos, hotspot2_strength, uncertainty_pos, uncertainty_strength, quantile, n_samples): """Run full sensor placement analysis.""" np.random.seed(42) # Setup grid = np.linspace(0, 10, 200) candidates = np.linspace(0.5, 9.5, 45) # Create models mean_func, var_func = create_environment( hotspot1_pos, hotspot1_strength, hotspot2_pos, hotspot2_strength, uncertainty_pos, uncertainty_strength ) lgcp = LGCP(mean_func, var_func, corr_len=0.8) sensors = Sensors(rho=rho, sigma_l=sigma_l) # Compute intensity functions mean_int = lgcp.mean_intensity(grid) quantile_int = lgcp.quantile_intensity(grid, quantile) # Optimize placements s_mean = greedy_optimize(grid, candidates, mean_int, sensors, n_sensors) s_quantile = greedy_optimize(grid, candidates, quantile_int, sensors, n_sensors) # Evaluate n_samples = int(n_samples) samples = lgcp.sample(grid, n_samples) r_mean = evaluate(grid, s_mean, samples, sensors) r_quantile = evaluate(grid, s_quantile, samples, sensors) # Create visualization fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Plot 1: Intensity functions ax1 = axes[0, 0] ax1.plot(grid, mean_int, 'b-', label='Mean intensity E[λ(s)]', linewidth=2) ax1.plot(grid, quantile_int, 'r-', label=f'{int(quantile*100)}th percentile', linewidth=2) ax1.fill_between(grid, 0, var_func(grid), alpha=0.3, color='gray', label='Uncertainty σ²(s)') ax1.set_xlabel('Location s', fontsize=12) ax1.set_ylabel('Intensity', fontsize=12) ax1.set_title('Intensity Functions', fontsize=12) ax1.legend() ax1.grid(True, alpha=0.3) # Plot 2: Sensor placements ax2 = axes[0, 1] ax2.plot(grid, mean_int, 'b-', alpha=0.5, linewidth=1) ax2.plot(grid, quantile_int, 'r-', alpha=0.5, linewidth=1) ax2.scatter(s_mean, np.zeros_like(s_mean) - 0.02, c='blue', s=150, marker='^', label=f'Mean-based sensors', zorder=5) ax2.scatter(s_quantile, np.zeros_like(s_quantile) - 0.06, c='red', s=150, marker='v', label=f'Q{int(quantile*100)}-based sensors', zorder=5) ax2.axvspan(uncertainty_pos - 1, uncertainty_pos + 1, alpha=0.2, color='yellow', label='High uncertainty zone') ax2.set_xlabel('Location s', fontsize=12) ax2.set_ylabel('Intensity', fontsize=12) ax2.set_title('Sensor Placements Comparison', fontsize=12) ax2.legend() ax2.grid(True, alpha=0.3) # Plot 3: Void probability distributions ax3 = axes[1, 0] ax3.hist(r_mean['all'], bins=50, alpha=0.5, label='Mean-based', color='blue', density=True) ax3.hist(r_quantile['all'], bins=50, alpha=0.5, label=f'Q{int(quantile*100)}-based', color='red', density=True) ax3.axvline(r_mean['p5'], color='blue', linestyle='--', linewidth=2, label=f'Mean 5th %: {r_mean["p5"]:.3f}') ax3.axvline(r_quantile['p5'], color='red', linestyle='--', linewidth=2, label=f'Q{int(quantile*100)} 5th %: {r_quantile["p5"]:.3f}') ax3.set_xlabel('Void Probability (lower = better)', fontsize=12) ax3.set_ylabel('Density', fontsize=12) ax3.set_title('Distribution of Void Probabilities', fontsize=12) ax3.legend() ax3.grid(True, alpha=0.3) # Plot 4: Detection coverage ax4 = axes[1, 1] miss_mean = sensors.miss_prob(grid, s_mean) miss_quantile = sensors.miss_prob(grid, s_quantile) ax4.plot(grid, 1 - miss_mean, 'b-', label='Mean-based coverage', linewidth=2) ax4.plot(grid, 1 - miss_quantile, 'r-', label=f'Q{int(quantile*100)}-based coverage', linewidth=2) ax4.axvspan(uncertainty_pos - 1, uncertainty_pos + 1, alpha=0.2, color='yellow', label='High uncertainty zone') ax4.set_xlabel('Location s', fontsize=12) ax4.set_ylabel('Detection Probability', fontsize=12) ax4.set_title('Detection Coverage Along Border', fontsize=12) ax4.legend() ax4.grid(True, alpha=0.3) plt.tight_layout() # Generate results text improvement = (r_quantile['p5'] - r_mean['p5']) / r_mean['p5'] * 100 results_text = f""" ## 📊 RESULTS ### Sensor Locations: - **Mean-based:** {np.round(s_mean, 2).tolist()} - **Q{int(quantile*100)}-based:** {np.round(s_quantile, 2).tolist()} ### Performance Comparison: | Metric | Mean-Based | Q{int(quantile*100)}-Based | Winner | |--------|-----------|------------|--------| | VP Mean | {r_mean['mean']:.4f} | {r_quantile['mean']:.4f} | {'Q'+str(int(quantile*100)) if r_quantile['mean'] < r_mean['mean'] else 'Mean'} | | VP Median | {r_mean['median']:.4f} | {r_quantile['median']:.4f} | {'Q'+str(int(quantile*100)) if r_quantile['median'] < r_mean['median'] else 'Mean'} | | VP 10th % | {r_mean['p10']:.4f} | {r_quantile['p10']:.4f} | {'Q'+str(int(quantile*100)) if r_quantile['p10'] > r_mean['p10'] else 'Mean'} | | **VP 5th %** | **{r_mean['p5']:.4f}** | **{r_quantile['p5']:.4f}** | **{'Q'+str(int(quantile*100)) if r_quantile['p5'] > r_mean['p5'] else 'Mean'}** | ### Key Finding: **Q{int(quantile*100)} worst-case (5th percentile) improvement: {improvement:.1f}%** {'✅ Conservative approach is BETTER for worst-case!' if improvement > 0 else '⚠️ Mean-based performs better in this scenario'} """ return fig, results_text def plot_single_sensor_demo(sensor_position, rho, sigma_l): """Interactive demo of a single sensor.""" fig, ax = plt.subplots(figsize=(10, 5)) grid = np.linspace(0, 10, 200) sensors = Sensors(rho=rho, sigma_l=sigma_l) # Detection probability detect = sensors.detect_prob(grid, sensor_position) # Plot ax.fill_between(grid, 0, detect, alpha=0.3, color='blue', label='Detection zone') ax.plot(grid, detect, 'b-', linewidth=2) ax.axvline(sensor_position, color='red', linestyle='--', linewidth=2, label=f'Sensor at {sensor_position:.1f}') # Add annotations ax.annotate(f'Max detection: {rho*100:.0f}%', xy=(sensor_position, rho), xytext=(sensor_position + 1, rho + 0.1), arrowprops=dict(arrowstyle='->', color='black'), fontsize=11) ax.set_xlabel('Location along border', fontsize=12) ax.set_ylabel('Detection Probability', fontsize=12) ax.set_title(f'Single Sensor Detection Coverage\nFormula: γ(s) = {rho} × exp(-(s - {sensor_position:.1f})² / {sigma_l})', fontsize=12) ax.legend(loc='upper right') ax.grid(True, alpha=0.3) ax.set_xlim(0, 10) ax.set_ylim(0, 1.1) plt.tight_layout() return fig # ============================================================================ # GRADIO INTERFACE # ============================================================================ with gr.Blocks(title="🎯 Sensor Placement Explorer", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🎯 Risk-Aware Sensor Placement Explorer **Learn how to optimally place sensors to detect intruders along a border!** This interactive tool helps you understand: - How the **detection formula** works - What **mean intensity** and **variance** mean - Why **conservative (Q90) placement** can be better than **mean-based placement** --- """) with gr.Tabs(): # ===================================================================== # TAB 1: Detection Formula # ===================================================================== with gr.TabItem("1️⃣ Detection Formula"): gr.Markdown(""" ## Understanding the Detection Formula Each sensor detects intruders based on **distance**: $$\\gamma(s, a) = \\rho \\times e^{-\\frac{(s - a)^2}{\\sigma_l}}$$ - **ρ (rho)**: Maximum detection probability (e.g., 95%) - **σₗ (sigma_l)**: Sensor range (higher = wider coverage) - **s - a**: Distance between intruder and sensor **Try adjusting the sliders to see how parameters affect detection!** """) with gr.Row(): rho_slider = gr.Slider(0.5, 1.0, value=0.95, step=0.05, label="ρ (rho) - Maximum Detection Probability") sigma_l_slider = gr.Slider(0.1, 2.0, value=0.25, step=0.05, label="σₗ (sigma_l) - Sensor Range") detect_plot = gr.Plot(label="Detection Probability Visualization") detect_btn = gr.Button("🔍 Update Detection Plot", variant="primary") detect_btn.click(plot_detection_formula, [rho_slider, sigma_l_slider], detect_plot) # ===================================================================== # TAB 2: Single Sensor Demo # ===================================================================== with gr.TabItem("2️⃣ Single Sensor Demo"): gr.Markdown(""" ## Interactive Single Sensor Move the sensor along the border and see its detection coverage! The **blue shaded area** shows where the sensor can detect intruders. """) with gr.Row(): pos_slider = gr.Slider(0, 10, value=5, step=0.1, label="Sensor Position (0-10)") rho_slider2 = gr.Slider(0.5, 1.0, value=0.95, step=0.05, label="ρ (rho) - Max Detection") sigma_l_slider2 = gr.Slider(0.1, 2.0, value=0.25, step=0.05, label="σₗ (sigma_l) - Range") single_plot = gr.Plot(label="Single Sensor Coverage") single_btn = gr.Button("🎯 Update Sensor", variant="primary") single_btn.click(plot_single_sensor_demo, [pos_slider, rho_slider2, sigma_l_slider2], single_plot) # ===================================================================== # TAB 3: Intensity Explanation # ===================================================================== with gr.TabItem("3️⃣ Intensity & Uncertainty"): gr.Markdown(""" ## Understanding Intensity and Uncertainty **Intensity** = How many intruders expected at each location (heat map) **Variance/Uncertainty** = How unsure we are about the estimate The formula: **Mean Intensity = exp(μ + σ²/2)** - Higher variance → higher mean intensity (because extreme values pull average up) """) with gr.Row(): with gr.Column(): gr.Markdown("### Hotspot 1 (Left region)") h1_pos = gr.Slider(0, 10, value=2.5, step=0.5, label="Position") h1_str = gr.Slider(0, 1, value=0.3, step=0.1, label="Strength") with gr.Column(): gr.Markdown("### Hotspot 2 (Right region)") h2_pos = gr.Slider(0, 10, value=7.5, step=0.5, label="Position") h2_str = gr.Slider(0, 1, value=0.2, step=0.1, label="Strength") with gr.Column(): gr.Markdown("### Uncertainty Zone") u_pos = gr.Slider(0, 10, value=5, step=0.5, label="Position") u_str = gr.Slider(0, 4, value=2.0, step=0.2, label="Strength") intensity_plot = gr.Plot(label="Intensity Visualization") intensity_btn = gr.Button("📊 Update Intensity Plot", variant="primary") intensity_btn.click(plot_intensity_explanation, [h1_pos, h1_str, h2_pos, h2_str, u_pos, u_str], intensity_plot) # ===================================================================== # TAB 4: Full Analysis # ===================================================================== with gr.TabItem("4️⃣ Full Analysis"): gr.Markdown(""" ## Complete Sensor Placement Analysis Compare **Mean-based** vs **Conservative (Quantile-based)** sensor placement! - **Mean-based**: Optimizes for average conditions - **Quantile-based**: Prepares for worse-than-expected scenarios """) with gr.Row(): with gr.Column(): gr.Markdown("### Sensor Settings") n_sensors = gr.Slider(2, 10, value=6, step=1, label="Number of Sensors") rho_full = gr.Slider(0.5, 1.0, value=0.95, step=0.05, label="ρ (Max Detection)") sigma_l_full = gr.Slider(0.1, 2.0, value=0.25, step=0.05, label="σₗ (Sensor Range)") with gr.Column(): gr.Markdown("### Environment Settings") h1_pos_full = gr.Slider(0, 10, value=2.5, step=0.5, label="Hotspot 1 Position") h1_str_full = gr.Slider(0, 1, value=0.3, step=0.1, label="Hotspot 1 Strength") h2_pos_full = gr.Slider(0, 10, value=7.5, step=0.5, label="Hotspot 2 Position") h2_str_full = gr.Slider(0, 1, value=0.2, step=0.1, label="Hotspot 2 Strength") with gr.Column(): gr.Markdown("### Uncertainty & Analysis") u_pos_full = gr.Slider(0, 10, value=5, step=0.5, label="Uncertainty Zone Position") u_str_full = gr.Slider(0, 4, value=2.0, step=0.2, label="Uncertainty Strength") quantile = gr.Slider(0.7, 0.99, value=0.90, step=0.01, label="Quantile (e.g., 0.90 = Q90)") n_samples = gr.Slider(500, 5000, value=2000, step=500, label="Monte Carlo Samples") full_plot = gr.Plot(label="Analysis Results") results_md = gr.Markdown("*Click 'Run Full Analysis' to see results*") full_btn = gr.Button("🚀 Run Full Analysis", variant="primary", size="lg") full_btn.click(run_full_analysis, [n_sensors, rho_full, sigma_l_full, h1_pos_full, h1_str_full, h2_pos_full, h2_str_full, u_pos_full, u_str_full, quantile, n_samples], [full_plot, results_md]) # ===================================================================== # TAB 5: Learning Summary # ===================================================================== with gr.TabItem("📚 Learning Summary"): gr.Markdown(""" ## Key Concepts Summary ### 1️⃣ Detection Formula ``` γ(s, a) = ρ × exp(-(s - a)² / σₗ) ``` - **ρ**: Max detection probability (0.95 = 95%) - **σₗ**: How far the sensor can "see" - **The negative sign**: Makes probability DECREASE with distance --- ### 2️⃣ Mean Intensity Formula ``` E[λ(s)] = exp(μ + σ²/2) ``` - **μ**: Average of log-intensity - **σ²**: Variance (uncertainty) - **Why σ²/2?**: Corrects for the fact that exp() shifts the average up --- ### 3️⃣ Quantile Intensity (Conservative) ``` λ_α(s) = exp(μ + z_α × σ) ``` - **z_α**: The z-score for percentile α (e.g., z₀.₉₀ ≈ 1.28) - **Higher quantile = more conservative** --- ### 4️⃣ Greedy Algorithm 1. Start with no sensors 2. Try each possible location 3. Pick the one with BIGGEST improvement 4. Repeat until all sensors placed --- ### 5️⃣ When to Use Each Approach | Situation | Approach | Why | |-----------|----------|-----| | Wildlife monitoring | Mean-based | Low stakes, average is fine | | Border security | Q90-based | High stakes, need worst-case protection | | Nuclear facility | Q95+ based | Critical, can't afford to miss | --- ### 6️⃣ Key Insight **Conservative placement (Q90) may sacrifice ~1% average performance but gains 10-15% improvement in worst-case scenarios!** For high-stakes applications, this trade-off is almost always worth it. """) gr.Markdown(""" --- ### 🔗 How to Use This App 1. **Start with Tab 1**: Understand how the detection formula works 2. **Try Tab 2**: Move a single sensor around and see its coverage 3. **Explore Tab 3**: See how intensity and uncertainty are calculated 4. **Run Tab 4**: Do a full analysis comparing Mean vs Conservative placement 5. **Review Tab 5**: Summarize what you've learned! --- *Created for learning sensor placement optimization with Log-Gaussian Cox Process models* """) # Launch the app if __name__ == "__main__": demo.launch()