HerdTag

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:

  1. Creates (or reuses) a fence called "Demo pasture" and assigns it to the virtual tags.
  2. Each tag starts with no fence and reports fence_id = 0.
  3. Every uplink is a real 11-byte payload wrapped in a TTS-format webhook body, POSTed to /ingest/ttn.
  4. 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_ACK set.
  5. It uplinks immediately on breach, returned and cue_ignored.

Stop it with Ctrl-C. Data stays in the server's DB. Delete the DB file for a clean slate.

Caveats