Skip to content

Events document

An analysis container produces one output: a JSON document listing the events it found, written to the path the manifest names in outputs.events.path. This page specifies that document in prose and by schema so that it can be produced from any language. save_event_blocks in this package is one encoder for it; the schema is the definition.

The contract is the Pydantic model EventBlocksDocument; EventBlocksDocument.model_json_schema() publishes the JSON Schema for a container written in another language.

Shape

{
  "format_version": 1,
  "blocks": [
    {
      "origin_ms": 0,
      "period_ms": 30000,
      "labels": ["Sleep stage W", "Sleep stage W", "Sleep stage N1", "Sleep stage N2"],
      "probability": null,
      "value": null,
      "channels": ["Fpz-Cz"]
    },
    {
      "origin_ms": 4210000,
      "period_ms": 0,
      "labels": ["EEG arousal"],
      "probability": [0.91],
      "value": null,
      "channels": []
    }
  ],
  "device": {"software": "my-analysis", "version": "1.2.0"}
}
Field Type Meaning
format_version 1 The version of this format. It changes only when a field changes meaning.
blocks list of blocks The events, grouped as described below. An analysis that found nothing writes an empty list, which is a result; a document that is missing is a failure.
device object, optional Provenance the analysis chooses to record: software and model identity, versions, a hash of its input, anything descriptive. Stored with the result, never interpreted.

A block is a run of consecutive events of equal duration that share a set of channels:

Field Type Meaning
origin_ms integer ≥ 0 Start of the first slot, in milliseconds from the start of the recording
period_ms integer ≥ 0 Duration of every slot in the block. 0 marks instantaneous events, and every slot then sits at origin_ms
labels list of strings, at least one One label per slot, from the vocabulary. Labels may differ from slot to slot
probability list or null One entry per slot, each a number in [0, 1] or null; null for the whole block when no slot carries one. Only for an output the configuration declares probabilistic
value list or null One entry per slot, each a number or null. Only for an output the configuration declares valued
channels list of strings The channels every event in the block applies to, by the manifest's name. Empty means the whole recording

Slot i spans [origin_ms + i × period_ms, origin_ms + (i + 1) × period_ms). Every slot is one event. A block is a compact encoding of a list of events and has no meaning of its own. A night of 30-second epochs is one block of about a thousand slots, and a single arousal is a block of one slot.

What a document must satisfy

  • Times are integers in milliseconds from the start of the recording, never negative.
  • Labels come from the vocabulary, and only from the labels the configuration declares under outputs.events. That declaration is what tells the viewer and the reports what this analysis produces.
  • Channels are named as the user knows them: the manifest's name, never its source_name, which is the header label.
  • probability and value are written only for outputs the configuration declares probabilistic or valued, and each array has exactly as many entries as labels.
  • Order carries no information on the way in. The platform decodes every block to its events and sorts them by their own fields, so blocks may be written in whatever order is natural.
  • Encoding is UTF-8 JSON. The platform names the output path with a .json.gz suffix and expects the file gzip-compressed there. It detects compression by content rather than by suffix, so an uncompressed file is also read. Write the file compressed regardless.
  • Exit status is 0 after the document is written and non-zero on any failure. An analysis that exits 0 without writing has failed without signalling it.

Encoding events as blocks

The simplest correct encoding is one block per event: origin_ms is its start, period_ms its duration and labels a list of one. That encoding is complete but larger than necessary. Events on a regular grid, such as a hypnogram or any per-epoch output, should be written as one block per contiguous run with the same duration and channel set, with one label per slot.

Full probabilistic output, where every prediction carries a probability for every class, is one block per class on the same grid: the block's labels repeat that class and its probability array carries the per-slot values. The configuration declares the ability with supports_full_probabilistic_output, and a user asks for it per run through a custom parameter the configuration declares: by convention a bool keyed store-all-probabilities, default false. The platform passes it like any other parameter: the container reads manifest.parameters["store-all-probabilities"] and, when it is true, writes a block per class.

The platform's own encoder, events_to_blocks, sorts events by their fields and merges the ones that continue a block. A container does not need to reproduce it, because decoding and re-encoding preserves the events either way.

Vocabulary

Labels are the EDF+ annotation vocabulary as enumerated by EventLabel. Sleep stages are Sleep stage W, Sleep stage N1, Sleep stage N2, Sleep stage N3, Sleep stage R, and Sleep stage ? for an epoch that could not be scored; the respiratory, arousal and movement families follow the same convention. A label outside the vocabulary fails validation at load: in the container when the helper is used, on the platform otherwise.

Reading one: a downstream analysis

An analysis whose configuration declares an events input receives an upstream document at inputs.events.path, in this format. load_events_output reads it, and the flat event lists below, into an EventBlocksDocument; blocks_to_events expands that to a list of Event.

Flat event lists

The platform also reads two flat shapes: a JSON object with a top-level events list, and a bare JSON list of events, each {"start_time_ms", "end_time_ms", "label", "probability", "value", "channels"}. Both are encoded to blocks on upload. New containers should write the block document.

Producing one in Python

from yousleep_common.models.events import Event
from yousleep_common.utils.event_blocks import save_event_blocks

events = [
    Event(start_time_ms=0, end_time_ms=30000, label="Sleep stage W", channels=["Fpz-Cz"]),
    Event(start_time_ms=30000, end_time_ms=60000, label="Sleep stage N1", channels=["Fpz-Cz"]),
]
save_event_blocks(
    manifest.outputs.events.path,
    events,
    device={"software": "my-analysis", "version": "1.2.0"},  # optional
)

Building Event objects validates labels and times in the container rather than at upload. save_event_blocks encodes them to blocks and compresses the file when the path ends in .gz. device, when given, is written as the document's device block.