Inspiration

I grew up in Hamilton, Montana, in the Bitterroot Valley, with the mountains right outside my door. As a kid I was constantly exploring the woods, and more than once I found out what it feels like to actually be lost: every ridge and drainage starts to look the same, and the light is going. I got lucky. Growing up there, I also heard plenty of stories of children and adults who weren't, people lost for days while searchers covered huge areas of steep, forested terrain.

Those searches often come down to time and to where the resources go. Air-scent dogs are one of the best tools a search team has, but they don't search for people. They search for scent, and scent goes wherever the air takes it. In mountain country that changes hour by hour: sun-warmed slopes lift scent up and away in the afternoon, and at dusk cool air drains it downhill into the valleys. Search managers already map where a person probably is, but the map a dog actually works is a different one.

I wanted to build that missing step: turn "where they might be" into "where a dog could smell them", and use it to tell each dog team where to start, which way to work, and when. Even a few hours saved in a search like the ones I grew up hearing about could make the difference.

What it does

Scentline is a planning tool for deploying air-scent dog teams.

  • Pick a last known point anywhere in the US. In about 40 seconds the server downloads real elevation, land cover, trails and today's weather forecast, and runs the US Forest Service's WindNinja model for 8 hours of terrain-adjusted wind.
  • Probability map: where the person probably is, from lost-person statistics for the subject (child, hiker, person with dementia…), shaped by trails, streams, slope, water and cliffs.
  • Scent simulation: scent released from every likely location and carried by the wind at dog-nose height. Drag the time bar and watch it drain into the drainages after sunset.
  • Team deployment: start points downwind of the most likely ground, each with an upwind heading, its best hour, and how much probability it covers, plus a spoken radio-style briefing.
  • Alerts and searched areas: when a dog alerts, the scent is traced backwards through the wind to narrow the search; areas searched with no alert push probability elsewhere.
  • Works offline: a bundled Catskills scenario (a 9-year-old missing from a campsite at 4 PM) runs entirely in the browser.

How I built it

Front end: React, TypeScript and three.js (React Three Fiber) render real terrain with contour shading, wind streaks and up to 15,000 scent particles. Every heavy model runs in Web Workers, so the UI never freezes. The landing page tells the story as a scroll-driven low-poly diorama, built from Synty POLYGON art that I converted to glTF with Blender and three.js scripts.

Back end: FastAPI in Docker, with WindNinja compiled in. It fetches USGS 3DEP elevation, NLCD land cover, OpenStreetMap features and Open-Meteo weather, and runs WindNinja initialised from NOAA's HRRR forecast. Results are cached per area, so a repeat is instant.

Voice briefings with Grok: after teams are deployed, Scentline reads a radio-style briefing for every team: where to start relative to the last known point, which way to work into the wind, the wind speed, the best hour and how much probability that start covers. The text is built only from the model's results, so the AI never invents search details; it only gives them a voice. The server integrates xAI's Grok Voice text-to-speech API: the key stays on the server, each briefing is cached so a replay is free, and the endpoint is rate-limited. With no key configured, or offline, the browser's built-in voice reads the same briefing, which is what the public demo uses.

The models are deliberately simple, explainable rules, and every number is listed on the site:

  • Where they might be: a log-normal distance prior around the last known point, with median \( m \) and spread \( s \) per profile, spread over the ring at distance \( d \): $$ w(d) \propto \frac{1}{2\pi d}\cdot\frac{1}{d\,s\sqrt{2\pi}}\exp!\left(-\frac{(\ln d-\ln m)^2}{2s^2}\right) $$ then multiplied by trail and stream attraction \( 1 + A\,e^{-\text{dist}/L} \), a slope factor \( e^{-\theta/25^\circ} \), and a penalty for cells cut off by rivers or cliffs.
  • Wind: WindNinja reports speed and the direction the wind blows from, so \( u = -S\sin\phi,\; v = -S\cos\phi \). Dogs smell at about 0.6 m while the model wind is at 2 m, so I scale it with a log wind profile \( u(z) \propto \ln(z/z_0) \), with roughness \( z_0 \) from land cover, and damp it under forest canopy.
  • Scent: particles are advected by the wind plus a random walk with diffusivity \( K = 0.5 + 0.3\,U \) (step \( \sim \mathcal{N}(0, \sqrt{2K\Delta t}) \)). Strength decays as \( e^{-\Delta t/\tau} \), with \( \tau \) set by humidity, temperature and sun. Scent lifts off sunny slopes in light wind and pools in calm hollows. To cover forecast error, I run 6 ensemble members, each with the wind rotated \( \pm 20^\circ \) and scaled \( 0.7\text{–}1.3\times \).
  • Deployment: I track which likely locations feed scent to each spot and place teams greedily. Each team is assumed to detect 70% of the scent it covers, and the next team stays at least 300 m away.
  • Alerts: I release particles at the alert and run the wind backwards for an hour to get a zone \( B_i \), then update the map Bayes-style: $$ P_{\text{post}}(x) \propto P_{\text{prior}}(x)\prod_i\big(\varepsilon + B_i(x)\big),\qquad \varepsilon = 0.02 $$

Challenges I ran into

  • Making scent behave plausibly. The first versions let particles get stuck in tiny terrain pits instead of flowing down-valley at dusk. Getting the evening "drainage" to look and behave right took several iterations of the wind and pooling rules.
  • Speed. Full fluid dynamics was never an option, so I used WindNinja's fast mass-conserving solver (seconds per hour of wind), cached everything per area, and moved the particle ensembles into Web Workers to keep the browser at interactive frame rates.
  • 3D asset pipeline. The dog's animation clips shipped as ASCII FBX, which Blender can't import. I parsed them with three.js's FBXLoader instead and applied them directly to the skinned rescue dog.
  • Subtle interaction bugs. For example, clicks inside the focus square were randomly rejected because a hidden lower-resolution terrain layer underneath was catching them.

Accomplishments that I'm proud of

  • A real end-to-end pipeline: pick any US location and get terrain-adjusted wind, scent and team placements in about 40 seconds.
  • A planner that stays honest: no invented results, every parameter documented, and a clear "research prototype" label.
  • A scroll-driven 3D story that explains the whole idea, from last-seen to found, in under a minute.

What I learned

  • How strongly terrain and time of day control where scent goes, and why "downwind" alone isn't enough in the mountains.
  • How to run an operational wind model (WindNinja with NOAA HRRR) as a service, and how to keep heavy simulation off the browser's main thread.
  • How much clarity matters: a plausible, explainable model that a K9 handler can argue with beats a black box.

What's next for Scentline

  • Validation against real searches and GPS-tracked training searches.
  • Live updates from dogs' GPS tracks to mark searched ground automatically.
  • A field view for team leaders on a phone, plus voice briefings over the radio.

Built With

Share this project:

Updates

Submission history