# Project export: PLUR: Predictive Large-Scale User Routing

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: AI-powered, physics based crowd safety simulation for live events - optimize schedules, predict crowd crush risk, save lives, and prevent injury.
- Devpost: https://devpost.com/software/plur-predictive-large-scale-user-routing
- GitHub: https://github.com/evanesmiller/PLUR
- Video: https://www.youtube.com/embed/s7lyyQapSe0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Evan Miller (12 commits), Eli (6 commits)

## Devpost submission (written by the team)

### Inspiration

PLUR (Predictive Large-Scale User Routing) was inspired by a problem that hits close to home for our team. We are all based in Los Angeles, avid concert and festival attendees, and several of us work professionally in event services and event security. We've experienced crowd management from both sides of the barricade—as staff responsible for keeping people safe and as patrons navigating massive crowds. The tragedy at Astroworld was a major catalyst for this project. It highlighted how difficult it can be for organizers to predict dangerous crowd conditions before they occur and how devastating the consequences can be when crowd dynamics are misunderstood. We've personally witnessed overcrowding, bottlenecks, and crowd crush conditions at events, and we wanted to explore how technology could help prevent similar incidents in the future. That led us to ask a simple question: What if festival organizers could simulate, optimize, and stress-test an entire event before a single attendee arrived? Our goal became building a platform that empowers organizers to make data-driven decisions about venue layouts, artist scheduling, crowd flow, and safety planning long before gates open. 🚀 What It Does PLUR combines crowd simulation, venue planning, schedule optimization, and AI-assisted decision making into a single platform designed specifically for large-scale events. Using a detailed geospatial model of a festival venue, organizers can simulate how tens of thousands of attendees move throughout the grounds. The system models: 🎤 Stages and artist performances 🚪 Entrances and exits ⭐ VIP areas 🚧 Barriers and restricted zones 🍔 Vendors and food courts 🍺 Bars and beverage stations 🚻 Restrooms and water stations 🏟️ Physical obstacles and venue infrastructure Each attendee is represented as an autonomous agent that moves throughout the venue based on artist demand, venue constraints, and crowd behavior patterns. This allows organizers to identify dangerous congestion points, bottlenecks, and potential crowd crush scenarios before the event takes place. One-Click Optimization One of PLUR's most powerful features is the Optimize button. With a single click, the platform automatically generates improved artist schedules and stage assignments based on projected attendance demand and artist popularity. The goal is to distribute crowds more effectively throughout the venue and reduce overcrowding risk without sacrificing the attendee experience. Rather than manually experimenting with hundreds of possible schedules, organizers can instantly receive optimized recommendations backed by simulation and crowd-flow analysis. AI Planning Assistants PLUR includes AI-powered assistants that help transform simulation outputs into actionable planning decisions. These AI agents can: Explain schedule and stage changes made during optimization Summarize crowd flow improvements Identify high-risk congestion areas Recommend crowd-control strategies Generate venue security briefings Suggest optimal restroom, water station, and bar placements Provide executive-level planning summaries Instead of presenting planners with raw heatmaps and density graphs, PLUR translates complex crowd dynamics into understandable recommendations. Interactive Venue Editing Event planners can also directly modify the venue itself. Users can: Move barriers Create or widen pathways Adjust vendor locations Relocate bars and beverage stations Reposition restrooms and water stations Test alternative venue layouts Every change can immediately be re-simulated, allowing planners to evaluate the impact before making real-world decisions. 🏗️ How We Built It At the core of PLUR is an agent-based crowd simulation engine powered by detailed geospatial venue data represented in GeoJSON. We modeled real-world festival environments using: Walkable areas Stages and attractions Entrances and exits VIP sections Vendor locations Restrooms and water stations Physical obstacles Restricted areas and barriers Each attendee is simulated as an independent agent navigating the venue while responding to attractions, obstacles, and crowd conditions. Running thousands of agents simultaneously allows us to generate realistic crowd-density maps and identify dangerous bottlenecks. Distributed Optimization Infrastructure To power our optimization engine, we deployed a distributed computing environment on a remote server cluster consisting of multiple virtual machine hosts totaling 44 CPU cores. This infrastructure allowed us to rapidly evaluate large numbers of artist schedule and stage assignment combinations while modeling their effects on crowd movement throughout the venue. The optimization engine considers: Artist popularity Projected attendance demand Venue layout Stage capacities Crowd movement patterns Bottleneck risk By leveraging distributed computation, organizers can evaluate complex scheduling scenarios in seconds rather than hours. AI-Powered Decision Support After each optimization run, an AI agent reviews the proposed changes and generates a human-readable explanation detailing: What changed Why the changes were made Expected crowd-flow improvements Potential tradeoffs Additional recommendations We also built specialized AI briefing agents that analyze venue layouts and simulation results to identify security risks, operational concerns, and infrastructure improvements. By combining simulation, optimization, distributed computing, geospatial modeling, and AI analysis, we created a comprehensive planning tool for large-scale events. ⚠️ Challenges We Ran Into One of our biggest challenges was balancing realism with performance. Simulating tens of thousands of attendees while maintaining an interactive user experience required significant optimization and efficient data structures. Another challenge was accurately modeling human behavior. Real crowds don't move perfectly or predictably, and small changes in venue design can dramatically alter crowd flow. Capturing those dynamics in a meaningful way required extensive experimentation and testing. Designing the optimization engine was also difficult. Popular artists naturally attract large crowds, but placing too many high-demand acts near each other—or scheduling them at conflicting times—can create dangerous conditions. We also faced the challenge of making simulation results understandable. Event organizers don't necessarily want to spend their day interpreting density maps and technical metrics—they want actionable recommendations. This challenge ultimately led to the creation of our AI-powered planning assistants. Finally, integrating venue editing, optimization, simulation, distributed infrastructure, and AI recommendations into a cohesive workflow within the limited timeframe of a hackathon was a significant challenge. 🏆 Accomplishments That We're Proud Of We're incredibly proud that we were able to build a complete end-to-end event planning platform during the hackathon. Some of our biggest accomplishments include: Building a large-scale agent-based crowd simulation system Creating a one-click optimization engine for artist scheduling and stage assignments Deploying distributed computation across a remote cluster with 44 CPU cores Developing AI agents that explain optimization decisions and generate security briefings Implementing venue editing capabilities for testing infrastructure changes Creating a system capable of identifying crowd crush risks before an event occurs Combining simulation, optimization, AI, and venue planning into a single workflow Most importantly, we're proud that PLUR addresses a real-world problem that directly impacts public safety. 📚 What We Learned This project taught us a tremendous amount about: Crowd dynamics Agent-based simulation Optimization algorithms Distributed computing Geospatial modeling AI-assisted decision making We learned how interconnected event planning really is. Artist scheduling, venue design, infrastructure placement, security operations, and attendee behavior all influence one another. A seemingly minor change—such as moving a restroom, widening a pathway, or adjusting a set time—can dramatically alter crowd flow throughout an entire venue. Perhaps most importantly, we learned that technology has the potential to make large events significantly safer when used proactively rather than reactively. 🔮 What's Next for PLUR? Our vision for PLUR extends far beyond this hackathon. In the short term, we plan to continue improving the accuracy of our simulation models, optimization algorithms, and AI planning assistants. We also want to incorporate additional real-world datasets and support increasingly complex event environments. Long term, our goal is to offer PLUR as a planning and safety platform for major event organizers and festival operators. We believe the platform could provide significant value to organizations such as Insomniac, Live Nation, and other large-scale event producers by helping them: Proactively identify safety risks Optimize event operations Improve attendee experiences Reduce crowd-related incidents Ultimately, we want PLUR to become a standard planning tool for large events—helping organizers create safer, smarter, and more enjoyable experiences for everyone.

## README (from the GitHub repository)

# PLUR — Predictive Large-scale User Routing

Crowd-crush prediction and mitigation tool for multi-stage music festivals. PLUR simulates how an audience moves through a venue, identifies where and when dangerous density conditions form, and gives event ops teams interactive controls — barriers, amenity repositioning, and schedule changes — to reduce risk before the event begins.

Built for the Ddoski's Lab + Anthropic + Most Technical hackathon tracks. Test venue: **HARD Summer 2025, Hollywood Park, Inglewood CA** (5 stages, ~80,000 attendees/day).

> **Disclaimer:** PLUR is a planning and decision-support prototype. It is not a validated or certified life-safety system. All recommendations must be reviewed by qualified event-safety professionals.

---

## What it does

**Simulate** — Run a two-tier crowd simulation across the full event day. The macroscopic layer computes per-stage populations over time; the microscopic social-force engine simulates individual agent movement and identifies crush-risk zones.

**Visualize** — Watch the crowd move in real time on a satellite-backed 3D map. Toggle heatmap, agent, and hotspot layers. Scrub the timeline or play back at variable speed.

**Mitigate** — Place and reposition physical barriers on the map by drawing them in the UI. Move restrooms, water stations, and bars to better distribute crowd load. Lock headliners and let the optimizer rearrange everything else.

**Optimize** — Submit your setlist to a distributed schedule optimizer running across **44 CPU cores on 7 virtual machines** (6 workers + 1 coordinator). It performs parallel local search across thousands of candidate schedules, scoring each against the crowd-crush risk model, and returns the arrangement that minimizes peak density while respecting locked headliner slots.

**Brief** — Generate a Claude-powered plain-text safety briefing covering risk windows, stage-by-stage danger levels, actionable ops recommendations, and specific suggestions for repositioning amenities based on current hotspot locations.

---

## How the simulation works

PLUR uses a two-tier engine:

- **Macroscopic layer** — analytic model covering the full event day. Computes per-stage population over time using artist draw scores (from Last.fm) and crowd migration between stages. Identifies risk windows where density is likely to spike.
- **Microscopic layer** — Helbing–Molnár social-force simulation run on a chosen time window (~5,000 subsampled agents, each representing ~16 real people). Uses numba JIT compilation and spatial hashing for real-time performance.

Risk is defined as `density ρ (people/m²)` and `pressure P = ρ × var(local_velocity)`. Cells at `ρ ≥ 6` or with a pressure spike are flagged red.

---

## Schedule optimizer — distributed cluster

The schedule optimizer runs on a dedicated compute cluster:

- **7 virtual machines** — 1 coordinator + 6 workers
- **44 CPU cores** total across all nodes
- **joblib + parallel local search** — the coordinator distributes candidate schedule perturbations across workers, each of which scores the candidate against the macroscopic risk model
- Headliner slots can be locked in the Set Times interface; the optimizer only moves unlocked artists
- Returns the proposed schedule, risk score before/after, a list of changes, and a Claude-generated plain-text rationale explaining each move

---

## Architecture

```
Browser (localhost)
  React + deck.gl + MapLibre GL (Esri satellite tiles — no token required)
        ↕  HTTP
Backend (FastAPI, Python 3.12)
  ├── VenueLoader        GeoJSON → occupancy grid, UTM projection (EPSG:32611)
  ├── DemandService      Last.fm + Ticketmaster → artist draw + affinity matrix (cached)
  ├── MacroModel         Share-of-audience timeline, risk window detection
  ├── MicroSim           numba social-force engine with spatial hashing
  ├── RiskAnalyzer       Density/pressure → zones, hotspots
  ├── ScheduleOptimizer  Distributed local search — 44 cores / 7 VMs (6 workers + 1 coord)
  ├── MitigationPlanner  Barrier/staff heuristics + sim validation
  ├── PLURAgent          Claude-powered schedule rationale and safety briefing
  └── ProjectStore       Redis-backed project persistence
```

---

## Setup

### Prerequisites

- Python 3.12
- Node.js 18+
- Redis (running on `localhost:6379`)
- Last.fm API key (free at [last.fm/api](https://www.last.fm/api))
- Anthropic API key (for the Claude safety briefing endpoint)

### Backend

```bash
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

export LASTFM_API_KEY=your_key_here
export ANTHROPIC_API_KEY=your_key_here
export REDIS_URL=redis://localhost:6379   # default if omitted

cd backend
uvicorn main:app --reload --port 8000
```

Interactive API docs: `http://localhost:8000/docs`

### Frontend

```bash
cd frontend
npm install
npm run dev
```

Opens at `http://localhost:5173`. API calls proxy to `http://localhost:8000`.

---

## API Endpoints

### Health

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/` | Health check, returns version |

### Venues

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/venues/{venue_id}` | Full venue GeoJSON, grid metadata, stages, gates, facilities |

### Projects

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/projects` | List all saved projects |
| `POST` | `/projects` | Create a new project |
| `GET` | `/projects/{id}` | Get project by ID |
| `PUT` | `/projects/{id}` | Update setlist, artists, or metadata |
| `DELETE` | `/projects/{id}` | Delete a project |
| `GET` | `/projects/{id}/sim` | Retrieve last saved simulation result |

### Simulation

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/simulate_festival` | Run full macro + micro sim; returns agent frames, hotspots |

**`POST /simulate_festival` body:**
```json
{
  "venue_id": "hard_summer_2025",
  "project_id": "abc123",
  "setlist": [{ "artist": "string", "stage": "string", "start": "HH:MM", "end": "HH:MM" }],
  "sliders": { "max_capacity": 80000, "tickets_sold": 60000, "n_agents": 5000 },
  "barriers": [[[lon, lat], ...]],
  "density_red": 6.0
}
```

### Optimization

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/optimize_schedule` | Distributed local-search optimizer; returns proposed schedule, risk delta, and Claude rationale |
| `POST` | `/safety_briefing` | Claude-generated safety briefing with amenity placement advice |
| `POST` | `/demand/scores` | Artist draw scores from Last.fm cache |

**`POST /optimize_schedule` body:**
```json
{
  "venue_id": "hard_summer_2025",
  "setlist": [...],
  "headliners": ["Artist A", "Artist B"],
  "sliders": { "max_capacity": 80000, "tickets_sold": 60000 }
}
```

**`POST /optimize_schedule` response:**
```json
{
  "proposed_schedule": [...],
  "risk_before": 0.82,
  "risk_after": 0.54,
  "changes": [{ "artist": "...", "from_stage": "...", "to_stage": "...", ... }],
  "rationale": "Plain-text Claude rationale..."
}
```

**`POST /safety_briefing` body:**
```json
{
  "venue_id": "hard_summer_2025",
  "setlist": [...],
  "sliders": { "max_capacity": 80000, "tickets_sold": 60000 },
  "peak_density": 4.7,
  "hotspots": [...],
  "amenities": [{ "id": "...", "name": "...", "facility_type": "restroom|water|bar", "lat": 0.0, "lon": 0.0 }]
}
```

---

## Interactive controls

### Barriers
Draw crowd-control barriers directly on the map. Click **Place Barrier** then click anywhere on the venue. Select a barrier to drag, resize, or rotate it. Barriers are included as obstacles in the next simulation run.

### Amenities
Restrooms, water stations, and bars are displayed as interactive dots on the map. Click to select, drag to reposition. Their positions are forwarded to the Claude safety briefing, which will suggest specific moves to reduce wait times and distribute crowd load away from hotspots.

### Set Times
Drag-and-drop artists between stage slots. Double-click a 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 46 recognized source files, 315 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (55 of 55)

```
.claude/commands/data.md
.claude/commands/frontend.md
.claude/commands/infra.md
.claude/commands/sim.md
.claude/projects/c--Users-jack--Documents-PLUR/memory/MEMORY.md
.claude/projects/c--Users-jack--Documents-PLUR/memory/user_current_teammate.md
.claude/settings.json
.gitignore
backend/agent/__init__.py
backend/agent/claude.py
backend/cluster.py
backend/data/venues/hard_summer_2025/meta.json
backend/data/venues/hard_summer_2025/README.md
backend/data/venues/hard_summer_2025/venue.geojson
backend/demand/__init__.py
backend/demand/service.py
backend/main.py
backend/optimize/__init__.py
backend/optimize/mitigation.py
backend/optimize/schedule.py
backend/sim/__init__.py
backend/sim/festival.py
backend/sim/macro.py
backend/sim/micro.py
backend/sim/pathfinding.py
backend/sim/risk.py
backend/store/__init__.py
backend/store/projects.py
backend/venue/__init__.py
backend/venue/loader.py
CLAUDE.md
dump.rdb
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/api/client.js
frontend/src/App.jsx
frontend/src/components/GeoJSONPreview.jsx
frontend/src/components/PLURAnimation.jsx
frontend/src/components/ProjectCard.jsx
frontend/src/deck/DeckMap.jsx
frontend/src/deck/layers.js
frontend/src/index.css
frontend/src/main.jsx
frontend/src/pages/Landing.jsx
frontend/src/pages/NewProject.jsx
frontend/src/pages/ProjectDetail.jsx
frontend/vite.config.js
hsd1.txt
README.md
requirements.txt
SURGE_Design_Doc.md
SURGE_Team_Handoff.md
```

### Dependencies

- frontend/package.json: @eslint/js@^10.0.1, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, deck.gl@^9.1.0, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, maplibre-gl@^5.6.0, react@^19.2.6, react-dom@^19.2.6, react-map-gl@^8.0.4, react-router-dom@^7.6.2, vite@^8.0.12
- requirements.txt: anthropic, dask[distributed], fastapi, joblib, llvmlite, numba, numpy, pandas, pyproj, python-dotenv, redis@>=4.2.0, requests, scipy, shapely, uvicorn[standard], websockets

### Recent commits (newest first)

- Save changes to geomap to redis
- fixed autofill
- Removed ghost obstacles
- amenity focus for agents, moveable amenities, updated claude safety prompt
- Merge pull request #1 from evanesmiller/cluster3
- Added Claude safety recommendation
- Working on UI fixes, frontend work
- Merge branch 'main' of https://github.com/evanesmiller/PLUR into cluster3
- Fixed PLUR animation, added swapping artists during project creation, account for B2B sets
- Merge remote-tracking branch 'origin/main' into cluster3
- Finalized basic simnulation logic
- scheduling attempt at a fix
- Cluster work
- Cluster work
- Updated sim to be more realistic
- Simulation added
- fixed barriers not being adjustable after UI fixes
- Polished frontend UI: labels, sliders, navigation, etc
- Frontend fixes
- dumb frontend changes

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.md

```markdown
# SURGE — Simulated Understanding of Risk in Gathering Events

Crowd-crush prediction & mitigation tool for multi-stage music festivals.
Hackathon project targeting Ddoski's Lab + Anthropic + Most Technical tracks.

## Source of Truth

- `SURGE_Design_Doc.md` — full engineering spec (architecture, API, repo layout, milestones M0–M9, parameters)
- `SURGE_Team_Handoff.md` — decisions log, current status, data stack, infra plan

Read both before making any architectural decisions.

## Key Decisions (DO NOT re-litigate)

- **Two-tier sim engine**: fast macroscopic layer (whole-day timeline) + detailed microscopic social-force layer (chosen window only, ~5-10k subsampled agents)
- **Real sim first; surrogate is deferred backup only**
- **Frontend**: React + deck.gl + MapLibre GL with free Esri satellite tiles (NO Mapbox token)
- **Backend**: Python 3.12, FastAPI, numba, numpy, scipy, shapely, pyproj, joblib
- **Placements are heuristic, not globally optimized** — report as "recommended"
- **Crush metric = density + pressure** (ρ ≥ 6 or pressure spike = red)
- **Framing**: "decision-support prototype, not a certified life-safety system"
- **DO NOT use Spotify API** (deprecated for new apps), AnyLogic, or Oasys MassMotion

## Data Sources

- **Last.fm API** (primary, free, key only) — artist.getInfo, geo.getTopArtists, artist.getSimilar, artist.getTopTags
- **Ticketmaster Discovery API** (secondary, free, 5k/day) — venue capacities
- Cache ALL API responses to JSON. Never call live during demo.

## Test Venue

HARD Summer 2025, Hollywood Park (Inglewood, CA), 7 stages, ~80k/day.
UTM Zone 11N / EPSG:32611 for metric projection.

## Repo Structure

```
surge/
├── backend/
│   ├── main.py                 # FastAPI app + routes
│   ├── venue/loader.py         # GeoJSON → grid, pyproj transforms
│   ├── demand/service.py       # Last.fm/Ticketmaster + composite index
│   ├── sim/macro.py            # share-of-audience timeline
│   ├── sim/micro.py            # numba social-force engine + spatial hash
│   ├── sim/risk.py             # density/pressure → zones, hotspots
│   ├── optimize/schedule.py    # local search + parallel scoring
│   ├── optimize/mitigation.py  # barrier/staff/facility heuristics
│   ├── agent/claude.py         # Claude-powered rationale/briefing
│   └── data/
│       ├── venues/hard_summer_2025/{venue.geojson,meta.json}
│       └── cache/*.json
├── frontend/
│   ├── src/App.jsx
│   ├── src/components/
│   └── src/deck/
├── scripts/
├── requirements.txt
└── README.md
```

## Milestones (MVP = through M6)

- M0: Scaffold (FastAPI + React/deck.gl/MapLibre with Esri satellite)
- M1: Venue GeoJSON + loader + render
- M2: Micro-sim (social-force + spatial hash + numba)
- M3: Risk fields (density/pressure → zones/hotspots + timeline)
- M4: Demand service (Last.fm/Ticketmaster cached) + macro model
- M5: Schedule optimizer (parallel local search, headliners-last)
- M6: Mitigations (barriers + staff heuristics + sim validation)
- M7: Claude agent
[truncated — 675 more characters]
```

### SURGE_Team_Handoff.md

```markdown
# SURGE — Team Handoff & Onboarding
*Crowd-crush prediction & mitigation tool for multi-stage music festivals.*
*Hackathon project. This doc lets a teammate pick up the work cold, using their own Claude account.*

---

## 0. Read this first (5-minute orientation)

**What we're building:** A tool where a festival organizer loads a scale venue map, optionally enters the set-list (artist × stage × time), tweaks event sliders (capacity, tickets sold, arrival profile), and the app **simulates crowd movement, predicts where/when crowd-crush danger forms, highlights red/green zones, and recommends barrier placements, staff placements, and a safer artist schedule** (keeping headliners last). Test venue: **HARD Summer 2025, Hollywood Park (Inglewood, CA), 7 stages, ~80k/day.**

**Read order to get up to speed:**
1. This handoff (decisions + status + how to continue).
2. `SURGE_Design_Doc.md` — the full engineering spec (architecture, API, repo layout, milestones M0–M9, parameters). **This is the source of truth for implementation.**

**Tracks we're targeting:** Ddoski's **Lab** (main) + **Anthropic** (built with Claude Code; Claude agent writes the safety briefing) + **Most Technical** (live agent-based sim on a distributed backend). Likely byproducts: Best UI/UX, Hacker's Choice, SkyDeck.

---

## 1. Current status

| Area | State |
|------|-------|
| Concept & scope | ✅ Locked (this doc + design doc) |
| Design document | ✅ Done (`SURGE_Design_Doc.md`) |
| Popularity/data stack | ✅ Decided & validated (see §3) |
| Compute infra plan | ✅ Decided: distributed Dask cluster on ESXi (see §4) |
| Golden VM build commands | ✅ Written (in the source chat / to be saved as `build_golden.sh`) |
| Venue map (HARD Summer) | ⬜ Not started — needs manual GIS authoring (see §5) |
| Code (backend/frontend) | ⬜ Not started — M0 scaffold is the next build step |
| API keys (Last.fm, Ticketmaster) | ⬜ TODO — register early (free) |
| Headliner designation | ⬜ TODO — needed as optimizer constraint |

**Next build step:** M0 scaffold — FastAPI hello-world on the coordinator (reachable via tunnel) + React/deck.gl/MapLibre showing the Esri satellite basemap over Hollywood Park.

---

## 2. Decision log (the "why" — don't re-litigate these)

These were worked out deliberately. Changing them without understanding the reason will cost you.

- **Problem choice — festival crowd-crush + safety-aware scheduling.** Picked over wildfire/disease/flood because those problems are *saturated* (thousands of existing repos). Crowd-crush is original; judges have no reference point. Chosen over a generic crowd sim by adding the **artist-demand-driven scheduling** axis, which nobody builds.
- **Two-tier simulation engine (the most important decision).** You cannot simulate 80k discrete agents in real time, and a continuous 8-hour social-force sim isn't interactive (one sim run is sequential in time, so cores don't speed up a single run). Solution: a **fast macroscopic layer** (share-of-audience
[truncated — 11354 more characters]
```

### requirements.txt

```
numpy
scipy
pandas
numba
llvmlite
shapely
pyproj
fastapi
uvicorn[standard]
websockets
requests
joblib
dask[distributed]
python-dotenv
anthropic
redis>=4.2.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-router-dom": "^7.6.2",
    "deck.gl": "^9.1.0",
    "maplibre-gl": "^5.6.0",
    "react-map-gl": "^8.0.4"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "vite": "^8.0.12"
  }
}

```

### backend/main.py

```python
from __future__ import annotations

import json
import os
from pathlib import Path

from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent.parent / ".env")

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from .agent.claude import PLURAgent
from .cluster import init_client, is_distributed, worker_count, submit, shutdown
from .demand.service import DemandService
from .optimize.schedule import ScheduleOptimizer
from .sim.macro import MacroModel
from .sim.festival import run_festival
from .store.projects import ProjectStore
from .venue.loader import VenueGrid, load_venue, load_venue_from_geojson

_DATA_DIR = Path(__file__).parent / "data"


def _slot_position_draw(artist: str, setlist: list[dict]) -> float:
    """Infer draw score from schedule position when API data is unavailable.

    Slots that start later in the day are assumed to be higher-billed acts.
    Returns a value in [0.1, 0.75] — capped below headliner territory so
    inferred acts never outrank artists with real streaming data.
    """
    def _t(s: str) -> int:
        h, m = s.split(":")
        return int(h) * 60 + int(m)

    times = [_t(e["start"]) for e in setlist if e.get("start")]
    if not times:
        return 0.2
    t_min, t_max = min(times), max(times)
    artist_start = next((_t(e["start"]) for e in setlist if e["artist"] == artist and e.get("start")), None)
    if artist_start is None or t_max == t_min:
        return 0.2
    # Linear map: earliest slot → 0.10, latest slot → 0.75
    return round(0.10 + 0.65 * (artist_start - t_min) / (t_max - t_min), 3)
_venue_cache: dict[str, VenueGrid] = {}

app = FastAPI(
    title="PLUR",
    description="Predictive Large-scale User Routing — crowd-crush prediction & mitigation for music festivals",
    version="0.1.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

_demand_svc = DemandService(_DATA_DIR / "cache")
_macro_model = MacroModel()
_scheduler = ScheduleOptimizer()
_agent = PLURAgent()
_project_store = ProjectStore(os.getenv("REDIS_URL", "redis://localhost:6379"))


@app.on_event("startup")
async def _startup():
    init_client()


@app.on_event("shutdown")
async def _shutdown():
    shutdown()


# ---------- request models ----------

class SetlistEntry(BaseModel):
    artist: str
    stage: str
    start: str  # "HH:MM"
    end: str
    locked: bool = False   # user locked — optimizer must not move
    manual: bool = True    # False = auto-filled — optimizer should prefer these


class DemandScoresRequest(BaseModel):
    artists: list[str]


class SimSliders(BaseModel):
    max_capacity: int = 80000
    tickets_sold: int = 75000
    arrival_steepness: float = 1.0
    n_agents: int = 5000


class OptimizeRequest(BaseModel):
    venue_id: str = "hard_summer_2025"
    setlist: list[SetlistEntry]
    headliners: list[str] = []
    sliders: SimSliders = SimSliders()


class FestivalSimRequest(BaseModel):
    venue_id: str = "hard_summer_2025"
    project_id: str = ""
    setlist: list[SetlistEntry] = []
    sliders: SimSliders = SimSliders()
    barriers: list[list[list[float]]] = []
    density_red: float = 6.0


class SafetyBriefingRequest(BaseModel):
    venue_id: str = "hard_summer_2025"
    setlist: list[SetlistEntry] = []
    sliders: SimSliders = SimSliders()
    peak_density: float = 0.0
    hotspots: list[dict] = []
    amenities: list[dict] = []


class CreateProjectRequest(BaseModel):
    name: str
    geojson: dict
    meta: dict = {}
    artists: list[str] = []
    setlist: list[dict] = []


class UpdateProjectRequest(BaseModel):
    name: str | None = None
    artists: list[str] | None = None
    setlist: list[dict] | None = None
    meta: dict | None = None


# ---------- helpers ----------

def _get_venue(venue_id: str) -> VenueGrid:
    if venue_id not in _venue_cache:
        try:
            _venue_cache[venue_id] = load_venue(venue_id, _DATA_DIR)
        except Exception as exc:
            raise HTTPException(status_code=404, detail=f"Venue '{venue_id}' not found: {exc}")
    return _venue_cache[venue_id]


def _setlist_dicts(entries: list[SetlistEntry]) -> list[dict]:
    return [e.model_dump() for e in entries]


# ---------- routes ----------

@app.get("/")
async def root():
    return {"status": "ok", "project": "PLUR", "version": "0.1.0"}


@app.get("/health")
async def health():
    return {
        "status": "ok",
        "distributed": is_distributed(),
        "dask_workers": worker_count(),
    }


@app.get("/venues")
async def list_venues():
    venues_dir = _DATA_DIR / "venues"
    result = []
    if not venues_dir.exists():
        return result
    for folder in sorted(venues_dir.iterdir()):
        meta_path = folder / "meta.json"
        if not meta_path.exists():
            continue
        meta = json.loads(meta_path.read_text())
        try:
            venue = _get_venue(folder.name)
            bbox = list(venue.bbox_lonlat)
            stages_out = [{"id": s["id"], "name": s["name"], "lonlat": s["lonlat"]} for s in venue.stages]
        except Exception:
            bbox = []
            stages_out = []
        venue_id = meta.get("id", folder.name)
        location = meta.get("location") or meta.get("venue") or meta.get("address", "")
        result.append({
            "id": venue_id,
            "name": meta.get("name", folder.name),
            "location": location,
            "bbox_lonlat": bbox,
            "stages": stages_out,
        })
    return result


@app.get("/venues/{venue_id}")
async def get_venue(venue_id: str):
    venue = _get_venue(venue_id)
    return {
        "id": venue_id,
        "meta": venue.meta,
        "geojson": venue.geojson,
        "grid": {
            "rows": venue.grid_shape[0],
            "cols": venue.grid_shape[1],
            "cell_m": venue.cell_m,
            "origin_m": list(v
[truncated — 5435 more characters]
```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'maplibre-gl/dist/maplibre-gl.css'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.jsx

```javascript
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Landing from './pages/Landing.jsx'
import ProjectDetail from './pages/ProjectDetail.jsx'
import NewProject from './pages/NewProject.jsx'

const router = createBrowserRouter([
  { path: '/', element: <Landing /> },
  { path: '/projects/new', element: <NewProject /> },
  { path: '/projects/:id', element: <ProjectDetail /> },
])

export default function App() {
  return <RouterProvider router={router} />
}

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>frontend</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### frontend/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

const apiTarget = process.env.VITE_API_TARGET || 'http://localhost:8000'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: apiTarget,
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, ''),
      },
      '/ws': {
        target: apiTarget,
        ws: true,
        changeOrigin: true,
      },
    },
  },
})

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
  },
])

```

[28 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]