Naz786's picture
Create app.py
9180006 verified
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()