> ## Documentation Index
> Fetch the complete documentation index at: https://docs.olira.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Passive signal ingestion

> Upload continuous sensor streams as Parquet. Currently accelerometer, gyroscope, and GPS; more sensors will follow.

<Warning>
  This feature is in beta. Behavior and availability may change as we expand
  support.
</Warning>

Use **passive signal ingestion** when you have continuous or high-rate sensor streams as **Parquet**. Olira absorbs the data, runs feature processing, and emits **derived logs** into the normal event-log path (then available in views and patient state like any other log).

**Supported today:** `accelerometer`, `gyroscope`, `gps`. Additional sensor types will be added over time — use the same `send_signals` API as new types land.

## Pipeline

```
Parquet upload → absorb → feature processing → derived logs
```

1. **Upload** — send a Parquet batch with `OliraClient.send_signals` (the SDK chooses sync vs bulk upload for you).
2. **Absorb** — Olira validates and normalizes the series (UTC timestamps, canonical units).
3. **Features** — Olira computes features from the absorbed series and emits derived logs. Feature jobs always read the absorbed series, not the raw upload bytes.

## Deduplication

* Timestamps are stored at **millisecond** resolution.
* The same `(patient, device, timestamp)` is kept **once** — the first write wins on re-upload.
* An identical file (same content hash) may be accepted as a no-op (`deduplicated: true` on the job).

Job field `records_deduplicated` counts overlap skips during absorb; that is separate from a content-hash no-op at upload.

## What you need

1. **API key** with **`sdk:event-log`**.
2. **Patient** already in your org.
3. **Parquet** rows with a `ts` column plus fields for the sensor type. Today:
   * **accelerometer** — `x`, `y`, `z` (m/s²; `g` / `milli-g` accepted via metadata)
   * **gyroscope** — `x`, `y`, `z` (rad/s)
   * **gps** — `lat`, `lon` (optional altitude, accuracy, speed, bearing)
4. Optional: `pip install olira[signals]` so the SDK can serialize `records=` to Parquet (or pass `parquet=` bytes yourself).

## SDK

<CodeGroup>
  ```python Python theme={null}
  from datetime import datetime, timezone
  from olira import OliraClient

  client = OliraClient(api_key="YOUR_API_KEY")

  handle = client.send_signals(
      patient_id="8a4fde23-0f1b-4c2a-9d7e-b36c1a5f0e82",
      sensor_type="accelerometer",
      source_device="phone-imu-1",
      sample_rate_hz=60.0,
      records=[
          {"ts": datetime(2026, 7, 26, 12, 0, 0, tzinfo=timezone.utc), "x": 0.1, "y": 0.0, "z": 9.8},
          {"ts": datetime(2026, 7, 26, 12, 0, 0, 16667, tzinfo=timezone.utc), "x": 0.12, "y": -0.02, "z": 9.81},
      ],
  )
  job = handle.wait()  # or handle.poll()
  print(job.status, job.records_written, job.records_deduplicated)
  ```

  ```csharp C# theme={null}
  using Olira;

  using var client = new OliraClient(apiKey: "YOUR_API_KEY");

  var handle = client.SendSignals(
      patientId: "8a4fde23-0f1b-4c2a-9d7e-b36c1a5f0e82",
      sensorType: "accelerometer",
      sourceDevice: "phone-imu-1",
      sampleRateHz: 60.0,
      records:
      [
          new Dictionary<string, object?>
          {
              ["ts"] = new DateTimeOffset(2026, 7, 26, 12, 0, 0, TimeSpan.Zero),
              ["x"] = 0.1,
              ["y"] = 0.0,
              ["z"] = 9.8,
          },
          new Dictionary<string, object?>
          {
              ["ts"] = new DateTimeOffset(2026, 7, 26, 12, 0, 0, 16, TimeSpan.Zero),
              ["x"] = 0.12,
              ["y"] = -0.02,
              ["z"] = 9.81,
          },
      ]);
  var job = handle.Wait();  // or handle.Poll()
  Console.WriteLine($"{job.Status} {job.RecordsWritten} {job.RecordsDeduplicated}");
  ```
</CodeGroup>

Small batches go through a synchronous upload; larger batches use a presigned upload URL. On the bulk path, PUT the Parquet bytes **without** the SDK `Authorization` header.

Method reference: [send\_signals](/reference/sdk/signals/send-signals), [get\_signal\_job](/reference/sdk/signals/get-signal-job).

## Job statuses

| Status       | Meaning                                         |
| ------------ | ----------------------------------------------- |
| `received`   | Accepted / queued                               |
| `processing` | Absorb in progress (`progress_pct` may advance) |
| `done`       | All batches absorbed successfully               |
| `partial`    | Some rows quarantined or batches failed         |
| `failed`     | Job failed                                      |

## After absorb

Wait for absorption (`handle.wait()`), then use derived logs and views like any other event data. Feature processing runs on a schedule after new signal data lands — you do not need to call a separate feature API.
