Jump to content

Temporal Contracts and Input Selection

From RidgeRun Developer Wiki

🚧 Documentation is under development

The Holoscan SDK guide is currently under active development. Some sections may be incomplete or change without notice.

Questions? Contact RidgeRun or email to support@ridgerun.com.

Contents Pin





Temporal Contracts and Input Selection

Camera and sensor streams can arrive at different rates or with different delays. A temporal contract lets you choose when an operator runs, which samples it uses, and what happens when data is late or missing:

  • The trigger tells the runtime when compute() becomes eligible.
  • The selectors tell the runtime which values to make available to that call.
  • The missing-data policy tells the runtime what to do when a required value is unavailable.

The contract applies to the typed ports introduced in Ports, Samples, and Payload Types. After the contract is accepted, the runtime watches the connected inputs, selects the data described by the contract, and calls compute() when the activation is eligible. The operator can focus on processing the selected data; it does not need to poll queues or coordinate input arrival timing itself. The contract does not create a timer or replace a queue policy.

For application code that uses these choices, see Authoring Runtime Behavior.

The examples below are contract() methods that return a holoscan::Contract for operators whose inputs were already declared in setup().

Tell the runtime when to call compute

The trigger defines the event that makes one activation eligible. The runtime honors that rule before calling compute():

Trigger Runtime activation rule
OnEach Call compute() when a new value is available on the selected input and make that value the activation's driver.
OnAnyInput Call compute() when any connected input becomes available. The runtime identifies the input that started the activation. This is also the default for an operator with connected inputs when no contract is supplied.
OnClock Create an activation candidate at each clock period. Selectors then determine which input data is available for that candidate.
OnAligned Call compute() when the runtime can select comparable values from all listed inputs within the configured time window.
OnWindow Call compute() when the runtime can form the anchor and its bounded input window, subject to the missing-data policy.

For a simple one-input transform, start with OnEach:

holoscan::Contract contract() const override {
  holoscan::Contract result;
  result.trigger(holoscan::OnEach{input});
  return result;
}

The trigger already selects the driving input. Add selectors only for other inputs that the same activation also needs.

Select the input data

After the trigger makes an activation eligible, selectors tell the runtime which data to prepare from each input:

Selector Runtime selection Typical use
Exact The value at the activation's reference point. Pair inputs that must correspond to the same event.
LatestWithin The newest causal value inside a maximum age. Use a slower or occasionally silent input in a faster pipeline.
DrainAll All causally available values as a bounded batch. Process every queued value that arrived since the previous selection.
BracketedWindow A bounded set around an anchor, including the configured history before and after it. Fuse an anchor frame with nearby sensor samples.

A selector describes selection, not storage. Choose the queue depth and overflow policy on the graph edge in Building Graphs and Queue Policies.

Read the selected value with the accessors introduced in Ports, Samples, and Payload Types. Use receive() when metadata is needed, receive_data() for a payload-only read, and selected_batch() for a batch selection.

Choose the missing-data policy

A selector can fail because no value has arrived, the value is too old, or a complete window cannot be formed. Set an on_missing() policy for inputs that are not guaranteed by the trigger:

Policy Result
Skip Do not call compute() for that candidate.
SafeDefault Call compute() with an explicit safe-default state and no fabricated payload.
ReuseLast Reuse a previously selected scalar value for a bounded age when a fresh value is unavailable.

Inspect the input selection state before using an input that can be missing or reused. A safe-default path should make its degraded behavior visible in the operator logic. A reused value is still marked as reused, so the application can report or handle that condition.

Sample a slower input in a faster pipeline

The common slow and fast fusion pattern runs at the fast input's rate and accepts the most recent slow value only while it is fresh. When the slow source stalls, the contract can reuse its last value for a bounded period:

holoscan::Contract contract() const override {
  holoscan::Contract result;
  result.trigger(holoscan::OnEach{camera});
  result.input(
      imu,
      holoscan::LatestWithin{
          .max_age = std::chrono::milliseconds(5),
      });
  result.on_missing(
      imu,
      holoscan::ReuseLast{
          .max_age = std::chrono::milliseconds(20),
      });
  return result;
}

The camera input drives each activation. The IMU value is marked fresh when it is no more than five milliseconds old. The last admitted IMU value can be reused for another bounded interval; after that, the selection is missing and the operator must follow its missing-data behavior.

The slow and fast fusion example shows this pattern with periodic sources and metadata forwarding. The First Operator guide also demonstrates OnEach, LatestWithin, and ReuseLast together.

Align inputs when the event matters

Use OnAligned when the application needs one value from each input at comparable capture times:

holoscan::Contract contract() const override {
  holoscan::Contract result;
  result.trigger(holoscan::OnAligned{
      .ports = holoscan::ports(left, right),
      .window = std::chrono::milliseconds(2),
  });
  result.input(left, holoscan::Exact{});
  result.input(right, holoscan::Exact{});
  return result;
}

The producers must provide capture timestamps that the graph can compare. Alignment combines the input selection; the operator still decides how to combine the payloads. Use separate named inputs when several producers feed a fusion operator, as described in Fan-Out, Fan-In, Mux, and Demux.

Use bounded windows for nearby samples

Use OnWindow when one input anchors a bounded set from another stream:

holoscan::Contract contract() const override {
  holoscan::Contract result;
  result.trigger(holoscan::OnWindow{
      .anchor = camera,
      .max_wait = std::chrono::milliseconds(20),
      .max_pending_windows = 2U,
  });
  result.input(camera, holoscan::Exact{});
  result.input(imu, holoscan::BracketedWindow{
      .lookbehind = 1U,
      .lookahead = 1U,
      .max_samples = 8U,
  });
  result.on_missing(imu, holoscan::SafeDefault{});
  return result;
}

A window needs enough queue history to retain its required samples. Keep the queue policy compatible with that requirement; a discard policy cannot provide a complete window. The anchored-window example shows timestamps, bounded history, and selected-batch access.

Keep advanced triggers secondary

Use OnPredicate when a typed predicate should decide eligibility. Readiness triggers such as OnReadable, OnNotified, and OnDoorbell are useful for logical events that do not arrive as ordinary payloads. These options still need explicit input selection when compute() expects data.

Continue the flow


Related Examples



Cookies help us deliver our services. By using our services, you agree to our use of cookies.