Metadata-Version: 2.4
Name: zainar-halo
Version: 0.1.4
Summary: ZaiNar RF halo awareness — egocentric proximity for tracked nodes
Author: Zainar
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: device-connect-edge<0.3,>=0.2.5
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Provides-Extra: integration
Requires-Dist: pytest>=8.0; extra == "integration"
Requires-Dist: pytest-asyncio>=0.23; extra == "integration"

# zainar-halo

ZaiNar RF halo awareness — egocentric proximity for Device Connect nodes.

A robot's onboard sensors see line-of-sight only. ZaiNar's RF network sees
every tagged entity in a facility, through walls and around blind corners.
`zainar-halo` packages that awareness as Device Connect–native primitives:
a **halo** (per-node view of surrounding entities) computed locally by the
robot from a zone broadcast, with no GPU and no training run required.

## Install

```bash
pip install zainar-halo
```

Requires `device-connect-edge>=0.2.5`.

## What's in the box

| Class | What it does |
|---|---|
| `ZainarLocationService` | `DeviceDriver` publisher: `ingest_position()` queue → 1Hz `@periodic` tick → `@emit` per zone |
| `RobotDriver` | `DeviceDriver` subscriber: `@on` halo_update → local `compute_halo()` → `@rpc` |
| `HaloConfig` | Per-node filter spec — radius, entity types, time-decay, detail, k-cap |
| `Position` | One tracked entity's 3D position (cm-int, spatial_vocab wire types) |
| `HaloProvider` | Computes halos from a list of positions |
| `TMIPayload` / `TMIEntity` | TMI (Traffic Management Information) zone broadcast wire contract |

## Quick start — location service (publisher side)

```python
from zainar_halo import ZainarLocationService, HaloConfig, Position
import time

service = ZainarLocationService(
    operator_configs={"amr-7": HaloConfig(radius_cm=1500, entity_types=["forklift", "human"])},
)
# register with DeviceRuntime and connect

# In your kinesis-gw subscriber callback:
service.ingest_position(Position(
    device_id="amr-7", entity_type="amr",
    x_cm=2200, y_cm=500, z_cm=0,
    measurement_time=time.time(),
))
# The @periodic tick broadcasts automatically at 1Hz
```

## Quick start — robot subscriber

```python
from zainar_halo import HaloConfig, RobotDriver

class MyRobot(RobotDriver):
    async def _on_halo_computed(self, halo: dict) -> None:
        # feed to costmap, planner, alerting...
        nearest = halo["nearest_entity_type"]
        dist_m  = (halo["nearest_distance_cm"] or 0) / 100
        print(f"{nearest} at {dist_m:.1f}m")

robot = MyRobot(halo_config=HaloConfig(radius_cm=1500, entity_types=["forklift", "human"]))
# register with DeviceRuntime and connect — the @on subscription is automatic
```

## Design note — awareness not firehose

`get_halo(device_id)` returns a **filtered, egocentric view** — entities within
`radius_cm`, capped by `entity_types` and `max_age_s`. It is not a raw position
feed. This is intentional:

- An MCP agent asking "what's around amr-7?" gets a safety-relevant subset, not
  the full facility tracking log.
- Operators set the filter via `set_halo_config()`. The config is stamped into
  every broadcast tick so all consumers use the same safety perimeter.
- For raw zone position lists, use `get_zone_entities(zone)`.

## Dense zones and the k-cap

When a zone has many tracked entities, `HaloConfig.max_members` limits the
halo to the nearest k. This bounds per-robot compute to O(k) regardless of
zone density:

```python
# Nearest 10 only — ignores everything beyond the 10th closest
HaloConfig(radius_cm=1500, max_members=10)
```

Default: `None` (unbounded — all entities within radius).

## Performance

From the S5 scale test (200 robots, one zone, NATS on localhost):

| Metric | Value |
|---|---|
| Payload size (200 entities) | 55 KB |
| Per-entity overhead | 284 bytes |
| All 200 robots received halo | 0.50s wall clock after first tick |
| Per-robot halo compute | < 1ms (pure CPU, O(k) in zone member count) |

Payload is O(N) in entity count — one JSON list, one NATS publish per zone, N
local computes (no central aggregation, no fan-out amplification).

## Units and conventions

- **Lengths**: centimeters as `int` (`distance_cm`, `radius_cm`, `x_cm`/`y_cm`/`z_cm`)
- **Speeds**: cm/s as `int` (`closing_speed_cmps`, `vx_cmps`)
- **Entity types**: spatial_vocab wire values — `human`, `amr`, `forklift`, `drone`, `pallet`, …
- **3D throughout** — distance is 3D Euclidean; a drone 4m overhead is 400cm away

## See also

- `zainar-halo-agent-tools` — FastMCP server exposing halo tools to LLM agents
- `CLAUDE.md` at repo root — full architecture, scenario, and integration guide
