Skip to main content

Events

Irregular • File: EventPoints.csv • Writer: LaboEventPointDataWriter.cs

At a glance

EventPoints is the authoritative log of experimental events — everything that increments the global EventNumber or marks an epoch transition. Every event is stamped at trigger time with the exact frame and MonotonicExecutionTime, so the row reflects the real moment the event fired, not when the writer got around to serialising it.

Think of EventPoints as the answer to "when and why did things happen?" Every EventNumber change you see in other CSVs has a corresponding row in this file explaining what that number means.

When it writes

A row is emitted whenever external code calls LaboDataCapture.Instance.EventEntry(observation). Internally that calls the writer's RecordEntry, and the observation is stamped at construction time (via LaboObservation.Stamp()) with the current frame / time / epoch context.

Triggers include:

  • Event-point firing — an EventPoint object in the experience fires.
  • Action execution — an action bound to an event runs.
  • Response generation — responses generated by the experience (prompts, stimuli).
  • Epoch lifecycle transitions — epoch start, epoch end.

Every event can have child observations via the entry.children chain. The writer flattens these — one row per child — so a parent event with three children produces four rows in the file.

File & location

  • Default: EventPoints.csv in the run directory.
  • Writer: LaboEventPointDataWriter.cs.
  • Observation: Runtime/DataCapture/Observations/EventPointObservation.cs.

Configuration

EventPointsDataCapture SO (subclass of DataCaptureConfig). Fields:

FieldWhat it does
enabledLoggingMaster enable/disable.
fileNameOverride the default name.
flushEveryNRowsBatched flush cadence. Default 30.
writeHeaderWrite the header row on first emission. Default true.

Auto-created on experience open. WindowUtility.OpenExperience() populates this slot if missing — no need to create it manually before first use. To find trigger sites in code, grep for LaboDataCapture.Instance.EventEntry.

Columns

Shared prefix

See Column conventions — The shared prefix. The shared columns are captured by Stamp() at trigger time, so FrameNumber / MonotonicExecutionTime reflect the real moment the event fired.

Domain columns

ColumnTypeDescription
Event_TypestringWhat kind of event — EpochStart, EpochEnd, Response, EventPoint, etc.
Event_DescriptionstringShort description set on the event (from the event-point or epoch config).
Event_NotestringAdditional free-text note on the event itself.
Observation_NotestringFree-text note specific to this observation (separate from Event_Note).
Event_SourcestringWhat object / entity produced the event (e.g. Epoch, EventPoint, Interactive, Response).
Event_Source_ParentstringThe parent of the source if relevant (e.g. the Interactive's parent, the Epoch the EventPoint belongs to).
Event_TriggerstringWhat caused it (Timer, UserInput, Collision, Manual, etc.).
Action_NumberintIndex of the action fired, if the event involves an action. 0 if not action-related.
Action_TypestringType of action (Visual, Audio, Haptic, Undefined, etc.).

Sample rows

...shared...,Event_Type,Event_Description,Event_Note,Observation_Note,Event_Source,Event_Source_Parent,Event_Trigger,Action_Number,Action_Type
...,EpochStart,Baseline epoch begins,,,Epoch,,,0,Undefined
...,Response,Stimulus flash,user_note_example,obs_note,EventPoint,Interactive,Timer,3,Visual

Join with other streams

Every row already carries the shared prefix, so it's natively joinable to anything else on FrameNumber. The most common pattern is attaching the per-frame state context to each event:

import pandas as pd
exp = pd.read_csv("ExperienceState.csv")
events = pd.read_csv("EventPoints.csv")

enriched = events.merge(exp, on="FrameNumber", how="left", suffixes=("_event", "_frame"))

# Flag events that fired during slow frames:
slow_events = enriched[enriched["deltaTime"] > 0.025]

Or bucket a continuous stream by event:

eye = pd.read_csv("EyeTracking.csv")
# For each eye-tracking sample, find the most recent event before it:
eye_sorted = eye.sort_values("MonotonicExecutionTime")
events_sorted = events.sort_values("MonotonicExecutionTime")
bucketed = pd.merge_asof(eye_sorted, events_sorted, on="MonotonicExecutionTime", direction="backward")

Gotchas

  • Children are flattened. A single source event with three child observations produces four rows (parent + three children). Don't treat row count as a unique-event count without deduplicating on (FrameNumber, Event_Source, Event_Description) first — or use the explicit EventNumber from the shared prefix.
  • Event_Source vs. Event_Source_Parent is not always intuitive. An interactive object that fires an event through a response has Event_Source = EventPoint and Event_Source_Parent = Interactive. Check the writer's GetColumnName-equivalent logic if you need exact source mapping.
  • EventNumber in the shared prefix is not monotonic-per-row. It's the latest fired event number at trigger time. Two rows with the same EventNumber can exist (parent + children), or EventNumber can stay flat across several rows if none of them incremented the global counter.
  • Stamped at trigger time, not write time. The MonotonicExecutionTime in an EventPoints row is the real event moment, accurate to microseconds. An ExperienceState row for the same FrameNumber will have a slightly later MonotonicExecutionTime (cached at top-of-frame, written at LateUpdate). This is real, not noise.
  • Commas in free-text notes are replaced with semicolons. See Column conventions.

Analysis recipes