Source: docs/simulation.md
Simulation and demo
There are two ways to exercise the system without hardware:
| Tool | What it is | Use it to |
|---|---|---|
herdtag sim |
Offline, faster-than-real-time herd simulation against the controller | Compare settings and response models; estimate breach rate and battery life |
herdtag demo |
Virtual tags that talk to a running backend in real wire formats | Demo or test the backend, map and dashboards end to end |
Both use the same cow behaviour and GPS models from src/herdtag/sim.py, and the real FenceController.
herdtag sim
uv run herdtag sim # all three response models, 20 cows, 1 day
uv run herdtag sim --cows 20 --days 2 --pull 0.3 # stronger urge to leave
uv run herdtag sim --cows 5 --days 0.5 --response trained --seed 7
| Flag | Default | Meaning |
|---|---|---|
--response |
all |
none, naive, trained or all |
--cows |
20 | Herd size |
--days |
1.0 | Simulated duration. Runtime is roughly 0.1–0.2 s per cow-day per model |
--pull |
0.15 | 0–1: how strongly cows head toward greener grass north of the fence |
--seed |
1 | Random seed. Runs are reproducible |
Example output:
5 cows x 0.5 days, response model = trained
time outside fence 0.00%
breach events 0 (0.00/cow/day)
cue bursts 45 (18.0/cow/day)
cues -> turned 5 cue episodes given up: 0
cues while not closing 11 (truth says the cow wasn't approaching)
GPS fixes 8360/cow/day uplinks 96/cow/day
est. battery (no solar) 5 days
From Python, herdtag.sim.run(cows, days, response, fence=..., config=Config(...), gps=GpsModel(...), pull=..., seed=...) returns a SimResult with .totals, .per_cow_day, .battery_days and .report(). Pass a custom Config to try controller settings, such as a wider warning zone or a longer idle interval.
Metrics
| Metric | Meaning | How to read it |
|---|---|---|
| time outside fence | Share of cow-time truly outside | Inflated: escaped cows never come back in this model. Use breach events instead |
| breach events | Times the controller declared a breach | The main containment metric |
| cue bursts | Cues played | Dose per animal. Compare with the welfare caps |
| cues → turned | turned events: cueing stopped because the cow stopped approaching |
The learning signal. Noise can cause some, even with none |
| cue episodes given up | cue_ignored events |
Animals that pushed through a whole episode |
| cues while not closing | Cues where the true distance hadn't shrunk since the previous fix | False cues caused by GPS noise. Should be low |
| GPS fixes / uplinks per cow-day | Duty cycle | Feeds the energy estimate |
| est. battery (no solar) | PowerModel.battery_days |
Uses placeholder power figures until measured on the bench |
Models
Cow behaviour
Each cow switches between three modes, chosen at random when the current one ends:
| Mode | Probability | Speed | Typical duration |
|---|---|---|---|
| graze | 60 % | 0.08 m/s | ~20 min |
| walk | 15 % | 0.7 m/s | ~3 min |
| rest | 25 % | 0 | ~30 min |
Heading drifts as a random walk, and is nudged toward an attractor point outside the fence (greener grass). --pull sets how hard. The default pasture is a 400 × 400 m square with a notch cut into the north side, so the polygon isn't convex.
When a cue is played, the cow responds with probability p (see below). It turns toward the interior (the best of 16 directions), and walks at 0.6 m/s for 30 s before going back to normal behaviour.
GPS error
Error on each axis follows an AR(1) process: σ = 2.12 m, correlation time 60 s. That gives about 2.5 m median and about 5.2 m 95th-percentile radial error, matching published virtual-fencing collar figures. Real GPS error drifts slowly rather than jumping independently on each fix, and the AR(1) process captures that.
2 % of fixes are degraded: an extra 12 m σ error, with h_acc = 15 reported, which the controller rejects. Good fixes report h_acc ≈ |N(3, 1)|, minimum 1.5 m. Note that the reported h_acc isn't tied to the actual error on good fixes. A real receiver's estimate is correlated with it.
Response models
The probability of responding to a cue decays from p_initial to p_asymptote over roughly tau_bursts cues heard:
p(n) = p_asymptote + (p_initial - p_asymptote) * exp(-n / tau_bursts)
| Model | p_initial | p_asymptote | tau | Represents |
|---|---|---|---|---|
none |
0 | 0 | — | Cows ignore the tag. Baseline cost of the controller |
naive |
0.35 | 0.08 | 40 | Sound-only cue that habituates (the pessimistic case from the 2025 acoustic study) |
trained |
0.85 | 0.85 | — | Responds like a well-trained collar herd (optimistic ceiling) |
These curves are made up. The point of Phase 2 trials is to replace them with curves fitted to real animals.
herdtag demo
Needs a running server:
uv run herdtag-server # terminal 1
uv run herdtag demo --speed 30 # terminal 2
uv run herdtag demo --server http://127.0.0.1:8765 --speed 200 --pull 0.5 --response naive
uv run herdtag demo --lat 42.44 --lon -76.50 # put the pasture somewhere else
| Flag | Default | Meaning |
|---|---|---|
--server |
http://127.0.0.1:8000 |
Backend URL |
--cows |
10 | Number of virtual tags (tag-01…) |
--speed |
30 | Simulated seconds per real second |
--response |
trained |
Response model |
--lat, --lon |
Kansas Flint Hills (38.45, −96.55) | Pasture centre |
--pull |
0.3 | Urge to leave |
--heartbeat |
300 | Simulated seconds between routine uplinks. Real tags use 900 |
--seed |
1 | Random seed |
What it does:
- Creates (or reuses) a fence called "Demo pasture" and assigns it to the virtual tags.
- Each tag starts with no fence and reports
fence_id = 0. - Every uplink is a real 11-byte payload wrapped in a TTS-format webhook body, POSTed to
/ingest/ttn. - Right after each uplink the tag fetches queued downlinks (Class A behaviour), reassembles the fence, checks the CRC, and starts enforcing it. It then uplinks again within 5 simulated seconds with
FLAG_FENCE_ACKset. - It uplinks immediately on
breach,returnedandcue_ignored.
Stop it with Ctrl-C. Data stays in the server's DB. Delete the DB file for a clean slate.
Caveats
- Escaped cows never return in either tool. There's no "go home" drive or herd cohesion.
- Cows move independently. There's no herd following, which in reality strongly affects both breaches and response.
- The terrain is flat and open, with no tree cover degrading GPS.
- The energy figures come from datasheet-level guesses in
power.py. Replace them with PPK2 measurements from makerspace session 1. - Treat the outputs as "does the control logic behave sensibly, and which way do the trade-offs point". They aren't predictions of real containment.